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 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 if let Some(line_clamp) = style.line_clamp.get(entity) {
173 paragraph_style.set_max_lines(line_clamp.0 as usize);
174 }
175
176 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(¶graph_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 if let Some(text) = style.text.get(entity) {
218 if !text.is_empty() {
219 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 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 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 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 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 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}