vizia_core/systems/
text.rs

1use skia_safe::{
2    font_arguments::VariationPosition,
3    textlayout::{
4        FontCollection, Paragraph, ParagraphBuilder, ParagraphStyle, RectHeightStyle,
5        RectWidthStyle, TextStyle,
6    },
7    BlendMode, FontArguments, FontStyle, Paint,
8};
9use vizia_storage::{LayoutChildIterator, LayoutTreeIterator};
10
11use crate::{cache::CachedData, prelude::*};
12
13pub(crate) fn text_system(cx: &mut Context) {
14    let iterator = LayoutTreeIterator::full(&cx.tree);
15    for entity in iterator {
16        if !cx.style.text_construction.contains(entity) {
17            continue;
18        }
19
20        if cx.style.text.contains(entity)
21            && cx.style.display.get(entity).copied().unwrap_or_default() != Display::None
22        {
23            if let Some(paragraph) =
24                build_paragraph(entity, &mut cx.style, &cx.tree, cx.text_context.font_collection())
25            {
26                cx.text_context.text_paragraphs.insert(entity, paragraph);
27                cx.style.needs_relayout();
28                cx.style.needs_text_layout(entity);
29            }
30        }
31    }
32
33    cx.style.text_construction.clear();
34}
35
36pub(crate) fn text_layout_system(cx: &mut Context) {
37    let iterator = LayoutTreeIterator::full(&cx.tree);
38    let mut redraw_entities = Vec::new();
39    for entity in iterator {
40        if !cx.style.text_layout.contains(entity) {
41            continue;
42        }
43
44        if let Some(paragraph) = cx.text_context.text_paragraphs.get_mut(entity) {
45            let bounds = cx.cache.get_bounds(entity);
46            let padding_left = cx
47                .style
48                .padding_left
49                .get(entity)
50                .copied()
51                .unwrap_or_default()
52                .to_px(bounds.width(), 0.0)
53                * cx.style.scale_factor();
54            let padding_right = cx
55                .style
56                .padding_right
57                .get(entity)
58                .copied()
59                .unwrap_or_default()
60                .to_px(bounds.width(), 0.0)
61                * cx.style.scale_factor();
62
63            let text_bounds = bounds.shrink_sides(padding_left, 0.0, padding_right, 0.0);
64
65            if !cx.style.width.get(entity).copied().unwrap_or_default().is_auto()
66                && !cx.style.height.get(entity).copied().unwrap_or_default().is_auto()
67            {
68                if cx.style.text_overflow.get(entity).copied().unwrap_or_default()
69                    == TextOverflow::Clip
70                {
71                    paragraph.layout(f32::MAX);
72                    paragraph
73                        .layout(text_bounds.width().max(paragraph.min_intrinsic_width() + 1.0));
74                    let mut text_bounds = text_bounds;
75                    text_bounds.w = paragraph.max_intrinsic_width();
76                    cx.text_context.text_bounds.insert(entity, text_bounds);
77                } else {
78                    paragraph.layout(text_bounds.width());
79                    let mut text_bounds = bounds;
80                    text_bounds.w = paragraph.max_intrinsic_width();
81                    cx.text_context.text_bounds.insert(entity, text_bounds);
82                }
83            }
84
85            layout_span(&cx.style, &mut cx.cache, &cx.tree, entity, paragraph, bounds);
86
87            redraw_entities.push(entity);
88        }
89    }
90    for entity in redraw_entities {
91        cx.needs_redraw(entity);
92    }
93    cx.style.text_layout.clear();
94}
95
96pub fn layout_span(
97    style: &Style,
98    cache: &mut CachedData,
99    tree: &Tree<Entity>,
100    entity: Entity,
101    paragraph: &Paragraph,
102    paragraph_bounds: BoundingBox,
103) -> BoundingBox {
104    let mut bounds = BoundingBox::default();
105    if style.text_span.get(entity).copied().unwrap_or_default() {
106        if let Some(range) = style.text_range.get(entity) {
107            let rects = paragraph.get_rects_for_range(
108                range.clone(),
109                RectHeightStyle::Tight,
110                RectWidthStyle::Tight,
111            );
112
113            let min_x = rects.iter().fold(1000000.0f32, |min, item| min.min(item.rect.x()));
114            let min_y = rects.iter().fold(1000000.0f32, |min, item| min.min(item.rect.y()));
115            let max_x = rects.iter().fold(0.0f32, |max, item| max.max(item.rect.right()));
116            let max_y = rects.iter().fold(0.0f32, |max, item| max.max(item.rect.bottom()));
117
118            bounds = BoundingBox::from_min_max(min_x, min_y, max_x, max_y);
119        }
120    }
121
122    let iter = LayoutChildIterator::new(tree, entity);
123    for child in iter {
124        if bounds.width() == 0.0 && bounds.height() == 0.0 {
125            bounds = layout_span(style, cache, tree, child, paragraph, paragraph_bounds);
126        } else {
127            bounds =
128                bounds.union(&layout_span(style, cache, tree, child, paragraph, paragraph_bounds));
129        }
130    }
131
132    if style.text_span.get(entity).copied().unwrap_or_default() {
133        cache.bounds.insert(
134            entity,
135            BoundingBox::from_min_max(
136                paragraph_bounds.x + bounds.x,
137                paragraph_bounds.y + bounds.y,
138                paragraph_bounds.x + bounds.right(),
139                paragraph_bounds.y + bounds.bottom(),
140            ),
141        );
142    }
143
144    bounds
145}
146
147pub fn build_paragraph(
148    entity: Entity,
149    style: &mut Style,
150    tree: &Tree<Entity>,
151    font_collection: &FontCollection,
152) -> Option<Paragraph> {
153    let mut paragraph_style = ParagraphStyle::default();
154    // paragraph_style.turn_hinting_off();
155
156    // Overflow
157    match style.text_overflow.get(entity) {
158        Some(&TextOverflow::Ellipsis) => {
159            paragraph_style.set_ellipsis("…");
160        }
161
162        Some(&TextOverflow::Clip) => {
163            paragraph_style.set_ellipsis("");
164        }
165
166        _ => {
167            paragraph_style.set_ellipsis("");
168        }
169    }
170
171    // Line Clamp
172    if let Some(line_clamp) = style.line_clamp.get(entity) {
173        paragraph_style.set_max_lines(line_clamp.0 as usize);
174    }
175
176    // if let Some(text_wrap) = style.text_wrap.get(entity).copied() {
177    //     if !text_wrap {
178    //         paragraph_style.set_max_lines(1);
179    //     }
180    // }
181
182    // Text Align
183    paragraph_style.set_text_align(
184        if let Some(text_align) = style.text_align.get(entity) {
185            *text_align
186        } else if let Some(alignment) = style.alignment.get(entity) {
187            match alignment {
188                Alignment::TopLeft | Alignment::Left | Alignment::BottomLeft => TextAlign::Left,
189                Alignment::TopCenter | Alignment::Center | Alignment::BottomCenter => {
190                    TextAlign::Center
191                }
192                Alignment::TopRight | Alignment::Right | Alignment::BottomRight => TextAlign::Right,
193            }
194        } else {
195            TextAlign::Left
196        }
197        .into(),
198    );
199
200    let mut paragraph_builder = ParagraphBuilder::new(&paragraph_style, font_collection);
201
202    add_block(style, tree, entity, &mut paragraph_builder, &mut 0);
203
204    paragraph_builder.add_text("\u{200B}");
205    paragraph_builder.build().into()
206}
207
208fn add_block(
209    style: &mut Style,
210    tree: &Tree<Entity>,
211    entity: Entity,
212    paragraph_builder: &mut ParagraphBuilder,
213    current: &mut usize,
214) {
215    // let mut new_current = current;
216
217    if let Some(text) = style.text.get(entity) {
218        if !text.is_empty() {
219            // Text Style
220
221            let mut text_style = TextStyle::new();
222
223            let font_color = style.font_color.get(entity).cloned().unwrap_or_default();
224
225            if let Some(text_decoration_line) = style.text_decoration_line.get(entity).copied() {
226                text_style.set_decoration_type(text_decoration_line.into());
227                text_style.set_decoration_color(font_color);
228            }
229
230            // Font Families
231            text_style.set_font_families(
232                style
233                    .font_family
234                    .get(entity)
235                    .map(Vec::as_slice)
236                    .unwrap_or(&[FamilyOwned::Generic(GenericFontFamily::SansSerif)]),
237            );
238
239            let mut paint = Paint::default();
240            // Font Color
241            if let Some(font_color) = style.font_color.get(entity) {
242                paint.set_color(*font_color);
243                paint.set_anti_alias(false);
244                paint.set_blend_mode(BlendMode::SrcOver);
245            }
246
247            if let Some(text_stroke) = style.text_stroke_width.get(entity) {
248                paint.set_stroke_width(text_stroke.to_px().unwrap_or(0.0));
249                paint.set_style(
250                    (*style.text_stroke_style.get(entity).unwrap_or(&TextStrokeStyle::default()))
251                        .into(),
252                );
253            }
254
255            text_style.set_foreground_paint(&paint);
256
257            if let Some(background_color) = style.background_color.get(entity) {
258                if style.text_span.get(entity).is_some() {
259                    let mut paint = Paint::default();
260                    paint.set_color(*background_color);
261                    paint.set_anti_alias(false);
262                    paint.set_blend_mode(BlendMode::SrcOver);
263                    text_style.set_background_paint(&paint);
264                }
265            }
266
267            // Font Size
268            let font_size = style.font_size.get(entity).map_or(16.0, |f| f.0);
269            text_style.set_font_size(font_size * style.scale_factor());
270
271            // Font Style
272            match (
273                style.font_weight.get(entity),
274                style.font_width.get(entity),
275                style.font_slant.get(entity),
276            ) {
277                (None, None, None) => {}
278                (weight, width, slant) => {
279                    text_style.set_font_style(FontStyle::new(
280                        weight.copied().unwrap_or_default().into(),
281                        width.copied().unwrap_or_default().into(),
282                        slant.copied().unwrap_or_default().into(),
283                    ));
284                }
285            }
286
287            // Font Variations
288            if let Some(coordinates) = style.font_variation_settings.get(entity) {
289                let coordinates = coordinates.iter().map(|c| c.0).collect::<Vec<_>>();
290                text_style.set_font_arguments(&FontArguments::new().set_variation_design_position(
291                    VariationPosition { coordinates: &coordinates },
292                ));
293            }
294
295            paragraph_builder.push_style(&text_style);
296            style.text_range.insert(entity, *current..*current + text.len());
297            paragraph_builder.add_text(text.as_str());
298            *current += text.len();
299        }
300    }
301
302    let iter = LayoutChildIterator::new(tree, entity);
303    for child in iter {
304        if style.text_span.get(child).copied().unwrap_or_default() {
305            add_block(style, tree, child, paragraph_builder, current);
306        }
307    }
308}