Skip to main content

zng_wgt_text/node/
render.rs

1use std::borrow::Cow;
2
3use zng_app::{
4    render::{FontSynthesis, FrameValueKey, ReferenceFrameId},
5    widget::{
6        WIDGET,
7        border::{LineOrientation, LineStyle},
8        node::{UiNode, UiNodeOp, match_node, match_node_leaf},
9    },
10};
11use zng_color::Rgba;
12use zng_ext_font::{Font, ShapedColoredGlyphs, ShapedImageGlyphs};
13use zng_ext_input::focus::FOCUS_CHANGED_EVENT;
14use zng_layout::{
15    context::LAYOUT,
16    unit::{Px, PxRect, PxSize},
17};
18use zng_view_api::{config::FontAntiAliasing, display_list::FrameValue, font::GlyphInstance};
19use zng_wgt::prelude::*;
20
21use crate::{
22    FONT_AA_VAR, FONT_COLOR_VAR, FONT_PALETTE_COLORS_VAR, FONT_PALETTE_VAR, IME_UNDERLINE_STYLE_VAR, OVERLINE_COLOR_VAR,
23    OVERLINE_STYLE_VAR, SELECTION_COLOR_VAR, STRIKETHROUGH_COLOR_VAR, STRIKETHROUGH_STYLE_VAR, TEXT_EDITABLE_VAR, TEXT_OVERFLOW_VAR,
24    TextOverflow, UNDERLINE_COLOR_VAR, UNDERLINE_STYLE_VAR,
25};
26
27use super::TEXT;
28
29/// An Ui node that renders the default underline visual using the parent [`LaidoutText`].
30///
31/// The lines are rendered before `child`, under it.
32///
33/// The `Text!` widgets introduces this node in `new_child`, around the [`render_strikethroughs`] node.
34///
35/// [`LaidoutText`]: super::LaidoutText
36pub fn render_underlines(child: impl IntoUiNode) -> UiNode {
37    match_node(child, move |_, op| match op {
38        UiNodeOp::Init => {
39            WIDGET.sub_var_render(&UNDERLINE_STYLE_VAR).sub_var_render(&UNDERLINE_COLOR_VAR);
40        }
41        UiNodeOp::Render { frame } => {
42            let t = TEXT.laidout();
43
44            if !t.underlines.is_empty() {
45                let style = UNDERLINE_STYLE_VAR.get();
46                if style != LineStyle::Hidden {
47                    let color = UNDERLINE_COLOR_VAR.get();
48                    for &(origin, width) in &t.underlines {
49                        frame.push_line(
50                            PxRect::new(origin, PxSize::new(width, t.underline_thickness)),
51                            LineOrientation::Horizontal,
52                            color,
53                            style,
54                        );
55                    }
56                }
57            }
58        }
59        _ => {}
60    })
61}
62
63/// An Ui node that renders the default IME preview underline visual using the parent [`LaidoutText`].
64///
65///
66/// The lines are rendered before `child`, under it.
67///
68/// The `Text!` widgets introduces this node in `new_child`, around the [`render_underlines`] node.
69///
70/// [`LaidoutText`]: super::LaidoutText
71pub fn render_ime_preview_underlines(child: impl IntoUiNode) -> UiNode {
72    match_node(child, move |_, op| match op {
73        UiNodeOp::Init => {
74            WIDGET.sub_var_render(&IME_UNDERLINE_STYLE_VAR).sub_var_render(&FONT_COLOR_VAR);
75        }
76        UiNodeOp::Render { frame } => {
77            let t = TEXT.laidout();
78
79            if !t.ime_underlines.is_empty() {
80                let style = IME_UNDERLINE_STYLE_VAR.get();
81                if style != LineStyle::Hidden {
82                    let color = FONT_COLOR_VAR.get();
83                    for &(origin, width) in &t.ime_underlines {
84                        frame.push_line(
85                            PxRect::new(origin, PxSize::new(width, t.ime_underline_thickness)),
86                            LineOrientation::Horizontal,
87                            color,
88                            style,
89                        );
90                    }
91                }
92            }
93        }
94        _ => {}
95    })
96}
97
98/// An Ui node that renders the default strikethrough visual using the parent [`LaidoutText`].
99///
100/// The lines are rendered after `child`, over it.
101///
102/// The `Text!` widgets introduces this node in `new_child`, around the [`render_overlines`] node.
103///
104/// [`LaidoutText`]: super::LaidoutText
105pub fn render_strikethroughs(child: impl IntoUiNode) -> UiNode {
106    match_node(child, move |_, op| match op {
107        UiNodeOp::Init => {
108            WIDGET
109                .sub_var_render(&STRIKETHROUGH_STYLE_VAR)
110                .sub_var_render(&STRIKETHROUGH_COLOR_VAR);
111        }
112        UiNodeOp::Render { frame } => {
113            let t = TEXT.laidout();
114            if !t.strikethroughs.is_empty() {
115                let style = STRIKETHROUGH_STYLE_VAR.get();
116                if style != LineStyle::Hidden {
117                    let color = STRIKETHROUGH_COLOR_VAR.get();
118                    for &(origin, width) in &t.strikethroughs {
119                        frame.push_line(
120                            PxRect::new(origin, PxSize::new(width, t.strikethrough_thickness)),
121                            LineOrientation::Horizontal,
122                            color,
123                            style,
124                        );
125                    }
126                }
127            }
128        }
129        _ => {}
130    })
131}
132
133/// An Ui node that renders the default overline visual using the parent [`LaidoutText`].
134///
135/// The lines are rendered before `child`, under it.
136///
137/// The `Text!` widgets introduces this node in `new_child`, around the [`render_text`] node.
138///
139/// [`LaidoutText`]: super::LaidoutText
140pub fn render_overlines(child: impl IntoUiNode) -> UiNode {
141    match_node(child, move |_, op| match op {
142        UiNodeOp::Init => {
143            WIDGET.sub_var_render(&OVERLINE_STYLE_VAR).sub_var_render(&OVERLINE_COLOR_VAR);
144        }
145        UiNodeOp::Render { frame } => {
146            let t = TEXT.laidout();
147            if !t.overlines.is_empty() {
148                let style = OVERLINE_STYLE_VAR.get();
149                if style != LineStyle::Hidden {
150                    let color = OVERLINE_COLOR_VAR.get();
151                    for &(origin, width) in &t.overlines {
152                        frame.push_line(
153                            PxRect::new(origin, PxSize::new(width, t.overline_thickness)),
154                            LineOrientation::Horizontal,
155                            color,
156                            style,
157                        );
158                    }
159                }
160            }
161        }
162        _ => {}
163    })
164}
165
166/// An Ui node that renders the text selection background.
167///
168/// The `Text!` widgets introduces this node in `new_child`, around the [`render_text`] node.
169///
170/// [`LaidoutText`]: super::LaidoutText
171pub fn render_selection(child: impl IntoUiNode) -> UiNode {
172    let mut is_focused = false;
173    match_node(child, move |_, op| match op {
174        UiNodeOp::Init => {
175            WIDGET.sub_var_render(&SELECTION_COLOR_VAR);
176            is_focused = false;
177        }
178        UiNodeOp::Update { .. } => {
179            // rich context extends update for focus on rich root to all selected leaves
180            FOCUS_CHANGED_EVENT.with_new(|updates| {
181                for args in updates.iter() {
182                    let new_is_focused = args.is_focus_within(TEXT.try_rich().map(|r| r.root_id).unwrap_or_else(|| WIDGET.id()));
183                    if is_focused != new_is_focused {
184                        WIDGET.render();
185                        is_focused = new_is_focused;
186                    }
187                }
188            });
189        }
190        UiNodeOp::Render { frame } => {
191            let r_txt = TEXT.resolved();
192
193            if let Some(range) = r_txt.caret.selection_range() {
194                let l_txt = TEXT.laidout();
195                let txt = r_txt.segmented_text.text();
196
197                let mut selection_color = SELECTION_COLOR_VAR.get();
198                if !is_focused && !r_txt.selection_toolbar_is_open {
199                    selection_color = selection_color.desaturate(100.pct());
200                }
201
202                for line_rect in l_txt.shaped_text.highlight_rects(range, txt) {
203                    if !line_rect.size.is_empty() {
204                        frame.push_color(line_rect, FrameValue::Value(selection_color));
205                    }
206                }
207            };
208        }
209        _ => {}
210    })
211}
212
213/// An UI node that renders the parent [`LaidoutText`].
214///
215/// This node renders the text only, decorators are rendered by other nodes.
216///
217/// This is the `Text!` widget inner most child node.
218///
219/// [`LaidoutText`]: super::LaidoutText
220pub fn render_text() -> UiNode {
221    #[derive(Clone, Copy, PartialEq)]
222    struct RenderedText {
223        version: u32,
224        synthesis: FontSynthesis,
225        color: Rgba,
226        aa: FontAntiAliasing,
227    }
228
229    let mut reuse = None;
230    let mut rendered = None;
231    let mut color_key = None;
232    let image_spatial_id = SpatialFrameId::new_unique();
233    let mut has_loading_images = false;
234
235    match_node_leaf(move |op| match op {
236        UiNodeOp::Init => {
237            WIDGET
238                .sub_var_render_update(&FONT_COLOR_VAR)
239                .sub_var_render(&FONT_AA_VAR)
240                .sub_var(&FONT_PALETTE_VAR)
241                .sub_var(&FONT_PALETTE_COLORS_VAR);
242
243            if FONT_COLOR_VAR.capabilities().contains(VarCapability::NEW) {
244                color_key = Some(FrameValueKey::new_unique());
245            }
246        }
247        UiNodeOp::Deinit => {
248            color_key = None;
249            reuse = None;
250            rendered = None;
251            has_loading_images = false;
252        }
253        UiNodeOp::Update { .. } => {
254            if (FONT_PALETTE_VAR.is_new() || FONT_PALETTE_COLORS_VAR.is_new())
255                && let Some(t) = TEXT.try_laidout()
256                && t.shaped_text.has_colored_glyphs()
257            {
258                WIDGET.render();
259            }
260        }
261        UiNodeOp::Measure { desired_size, .. } => {
262            let txt = TEXT.laidout();
263            *desired_size = LAYOUT.constraints().inner().fill_size_or(txt.shaped_text.size())
264        }
265        UiNodeOp::Layout { final_size, .. } => {
266            // layout implemented in `layout_text`, it sets the size as an exact size constraint, we return
267            // the size here for foreign nodes in the CHILD_LAYOUT+100 ..= CHILD range.
268            let txt = TEXT.laidout();
269            *final_size = LAYOUT.constraints().inner().fill_size_or(txt.shaped_text.size())
270        }
271        UiNodeOp::Render { frame } => {
272            let r = TEXT.resolved();
273            let mut t = TEXT.layout();
274
275            let lh = t.shaped_text.line_height();
276            let clip = PxRect::from_size(t.shaped_text.align_size()).inflate(lh, lh); // clip inflated to allow some weird glyphs
277            if clip.size.is_empty() {
278                return;
279            }
280
281            let color = FONT_COLOR_VAR.get();
282            let color_value = if let Some(key) = color_key {
283                key.bind(color, FONT_COLOR_VAR.is_animating())
284            } else {
285                FrameValue::Value(color)
286            };
287
288            let aa = FONT_AA_VAR.get();
289
290            let rt = Some(RenderedText {
291                version: t.shaped_text_version,
292                synthesis: r.synthesis,
293                color,
294                aa,
295            });
296            if rendered != rt {
297                rendered = rt;
298                reuse = None;
299            }
300
301            t.render_info.transform = *frame.transform();
302            t.render_info.scale_factor = frame.scale_factor();
303
304            if std::mem::take(&mut has_loading_images)
305                && reuse.is_some()
306                && frame.render_widgets().delivery_list().enter_widget(WIDGET.id())
307            {
308                // loading emoji images request render on load
309                reuse = None;
310            }
311
312            frame.push_reuse(&mut reuse, |frame| {
313                if t.shaped_text.has_images() {
314                    let mut img_count = 0;
315                    let mut push_img_glyphs = |font: &Font, glyphs, offset: Option<euclid::Vector2D<f32, Px>>| match glyphs {
316                        ShapedImageGlyphs::Normal(glyphs) => {
317                            if let Some(offset) = offset {
318                                let mut glyphs = glyphs.to_vec();
319                                for g in &mut glyphs {
320                                    g.point.x += offset.x;
321                                    g.point.y += offset.y;
322                                }
323                                frame.push_text(clip, &glyphs, font, color_value, r.synthesis, aa);
324                            } else {
325                                frame.push_text(clip, glyphs, font, color_value, r.synthesis, aa);
326                            }
327                        }
328                        ShapedImageGlyphs::Image { rect, img, .. } => {
329                            let is_loading = img.with(|i| {
330                                if i.is_loaded() {
331                                    frame.push_reference_frame(
332                                        ReferenceFrameId::from_unique_child(image_spatial_id, img_count),
333                                        FrameValue::Value(PxTransform::translation(rect.origin.x, rect.origin.y)),
334                                        true,
335                                        true,
336                                        |frame| {
337                                            let size = rect.size.cast::<Px>();
338                                            frame.push_image(
339                                                PxRect::from_size(size),
340                                                size,
341                                                size,
342                                                PxSize::zero(),
343                                                i,
344                                                zng_view_api::ImageRendering::Pixelated,
345                                            );
346                                        },
347                                    );
348                                    img_count = img_count.wrapping_add(1);
349                                }
350                                i.is_loading()
351                            });
352                            if is_loading {
353                                has_loading_images = true;
354                                let id = WIDGET.id();
355                                img.hook(move |args| {
356                                    if args.value().is_loaded() {
357                                        UPDATES.render(id);
358                                    }
359                                    args.value().is_loading()
360                                })
361                                .perm();
362                            }
363                        }
364                    };
365
366                    match (&t.overflow, TEXT_OVERFLOW_VAR.get(), TEXT_EDITABLE_VAR.get()) {
367                        (Some(o), TextOverflow::Truncate(_), false) => {
368                            for glyphs in &o.included_glyphs {
369                                for (font, glyphs) in t.shaped_text.image_glyphs_slice(glyphs.clone()) {
370                                    push_img_glyphs(font, glyphs, None)
371                                }
372                            }
373
374                            if let Some(suf) = &t.overflow_suffix {
375                                let suf_offset = o.suffix_origin.to_vector().cast_unit();
376                                for (font, glyphs) in suf.image_glyphs() {
377                                    push_img_glyphs(font, glyphs, Some(suf_offset))
378                                }
379                            }
380                        }
381                        _ => {
382                            // no overflow truncating
383                            for (font, glyphs) in t.shaped_text.image_glyphs() {
384                                push_img_glyphs(font, glyphs, None)
385                            }
386                        }
387                    }
388                } else if t.shaped_text.has_colored_glyphs() || t.overflow_suffix.as_ref().map(|o| o.has_colored_glyphs()).unwrap_or(false)
389                {
390                    let palette_query = FONT_PALETTE_VAR.get();
391                    FONT_PALETTE_COLORS_VAR.with(|palette_colors| {
392                        let mut push_font_glyphs = |font: &Font, glyphs, offset: Option<euclid::Vector2D<f32, Px>>| {
393                            let mut palette = None;
394
395                            match glyphs {
396                                ShapedColoredGlyphs::Normal(glyphs) => {
397                                    if let Some(offset) = offset {
398                                        let mut glyphs = glyphs.to_vec();
399                                        for g in &mut glyphs {
400                                            g.point.x += offset.x;
401                                            g.point.y += offset.y;
402                                        }
403                                        frame.push_text(clip, &glyphs, font, color_value, r.synthesis, aa);
404                                    } else {
405                                        frame.push_text(clip, glyphs, font, color_value, r.synthesis, aa);
406                                    }
407                                }
408                                ShapedColoredGlyphs::Colored { point, glyphs, .. } => {
409                                    for (index, color_i) in glyphs.iter() {
410                                        let color = if let Some(color_i) = color_i {
411                                            if let Some(i) = palette_colors.iter().position(|(ci, _)| *ci == color_i) {
412                                                palette_colors[i].1
413                                            } else {
414                                                // FontFace only parses colored glyphs if the font has at least one
415                                                // palette, so it is safe to unwrap here
416                                                let palette = palette
417                                                    .get_or_insert_with(|| font.face().color_palettes().palette(palette_query).unwrap());
418
419                                                // the font could have a bug and return an invalid palette index
420                                                palette.get(color_i).unwrap_or(color)
421                                            }
422                                        } else {
423                                            // color_i is None, meaning the base color.
424                                            color
425                                        };
426
427                                        let mut g = GlyphInstance::new(index, point);
428                                        if let Some(offset) = offset {
429                                            g.point.x += offset.x;
430                                            g.point.y += offset.y;
431                                        }
432                                        frame.push_text(clip, &[g], font, FrameValue::Value(color), r.synthesis, aa);
433                                    }
434                                }
435                            }
436                        };
437
438                        match (&t.overflow, TEXT_OVERFLOW_VAR.get(), TEXT_EDITABLE_VAR.get()) {
439                            (Some(o), TextOverflow::Truncate(_), false) => {
440                                for glyphs in &o.included_glyphs {
441                                    for (font, glyphs) in t.shaped_text.colored_glyphs_slice(glyphs.clone()) {
442                                        push_font_glyphs(font, glyphs, None)
443                                    }
444                                }
445
446                                if let Some(suf) = &t.overflow_suffix {
447                                    let suf_offset = o.suffix_origin.to_vector().cast_unit();
448                                    for (font, glyphs) in suf.colored_glyphs() {
449                                        push_font_glyphs(font, glyphs, Some(suf_offset))
450                                    }
451                                }
452                            }
453                            _ => {
454                                // no overflow truncating
455                                for (font, glyphs) in t.shaped_text.colored_glyphs() {
456                                    push_font_glyphs(font, glyphs, None)
457                                }
458                            }
459                        }
460                    });
461                } else {
462                    // no colored glyphs
463
464                    let mut push_font_glyphs = |font: &Font, glyphs: Cow<[GlyphInstance]>| {
465                        frame.push_text(clip, glyphs.as_ref(), font, color_value, r.synthesis, aa);
466                    };
467
468                    match (&t.overflow, TEXT_OVERFLOW_VAR.get(), TEXT_EDITABLE_VAR.get()) {
469                        (Some(o), TextOverflow::Truncate(_), false) => {
470                            for glyphs in &o.included_glyphs {
471                                for (font, glyphs) in t.shaped_text.glyphs_slice(glyphs.clone()) {
472                                    push_font_glyphs(font, Cow::Borrowed(glyphs))
473                                }
474                            }
475
476                            if let Some(suf) = &t.overflow_suffix {
477                                let suf_offset = o.suffix_origin.to_vector().cast_unit();
478                                for (font, glyphs) in suf.glyphs() {
479                                    let mut glyphs = glyphs.to_vec();
480                                    for g in &mut glyphs {
481                                        g.point += suf_offset;
482                                    }
483                                    push_font_glyphs(font, Cow::Owned(glyphs))
484                                }
485                            }
486                        }
487                        _ => {
488                            // no overflow truncating
489                            for (font, glyphs) in t.shaped_text.glyphs() {
490                                push_font_glyphs(font, Cow::Borrowed(glyphs))
491                            }
492                        }
493                    }
494                }
495            });
496        }
497        UiNodeOp::RenderUpdate { update } => {
498            TEXT.layout().render_info.transform = *update.transform();
499
500            if let Some(key) = color_key
501                && let Some(mut r) = rendered
502            {
503                let color = FONT_COLOR_VAR.get();
504
505                update.update_color(key.update(color, FONT_COLOR_VAR.is_animating()));
506
507                r.color = color;
508                rendered = Some(r);
509            }
510        }
511        _ => {}
512    })
513}