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.var().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            let color = FONT_COLOR_VAR.get();
278            let color_value = if let Some(key) = color_key {
279                key.bind(color, FONT_COLOR_VAR.is_animating())
280            } else {
281                FrameValue::Value(color)
282            };
283
284            let aa = FONT_AA_VAR.get();
285
286            let rt = Some(RenderedText {
287                version: t.shaped_text_version,
288                synthesis: r.synthesis,
289                color,
290                aa,
291            });
292            if rendered != rt {
293                rendered = rt;
294                reuse = None;
295            }
296
297            t.render_info.transform = *frame.transform();
298            t.render_info.scale_factor = frame.scale_factor();
299
300            if std::mem::take(&mut has_loading_images)
301                && reuse.is_some()
302                && frame.render_widgets().delivery_list().enter_widget(WIDGET.id())
303            {
304                // loading emoji images request render on load
305                reuse = None;
306            }
307
308            frame.push_reuse(&mut reuse, |frame| {
309                if t.shaped_text.has_images() {
310                    let mut img_count = 0;
311                    let mut push_img_glyphs = |font: &Font, glyphs, offset: Option<euclid::Vector2D<f32, Px>>| match glyphs {
312                        ShapedImageGlyphs::Normal(glyphs) => {
313                            if let Some(offset) = offset {
314                                let mut glyphs = glyphs.to_vec();
315                                for g in &mut glyphs {
316                                    g.point.x += offset.x;
317                                    g.point.y += offset.y;
318                                }
319                                frame.push_text(clip, &glyphs, font, color_value, r.synthesis, aa);
320                            } else {
321                                frame.push_text(clip, glyphs, font, color_value, r.synthesis, aa);
322                            }
323                        }
324                        ShapedImageGlyphs::Image { rect, img, .. } => {
325                            let is_loading = img.with(|i| {
326                                if i.is_loaded() {
327                                    frame.push_reference_frame(
328                                        ReferenceFrameId::from_unique_child(image_spatial_id, img_count),
329                                        FrameValue::Value(PxTransform::translation(rect.origin.x, rect.origin.y)),
330                                        true,
331                                        true,
332                                        |frame| {
333                                            let size = rect.size.cast::<Px>();
334                                            frame.push_image(
335                                                PxRect::from_size(size),
336                                                size,
337                                                size,
338                                                PxSize::zero(),
339                                                i,
340                                                zng_view_api::ImageRendering::Pixelated,
341                                            );
342                                        },
343                                    );
344                                    img_count = img_count.wrapping_add(1);
345                                }
346                                i.is_loading()
347                            });
348                            if is_loading {
349                                has_loading_images = true;
350                                let id = WIDGET.id();
351                                img.hook(move |args| {
352                                    if args.value().is_loaded() {
353                                        UPDATES.render(id);
354                                    }
355                                    args.value().is_loading()
356                                })
357                                .perm();
358                            }
359                        }
360                    };
361
362                    match (&t.overflow, TEXT_OVERFLOW_VAR.get(), TEXT_EDITABLE_VAR.get()) {
363                        (Some(o), TextOverflow::Truncate(_), false) => {
364                            for glyphs in &o.included_glyphs {
365                                for (font, glyphs) in t.shaped_text.image_glyphs_slice(glyphs.clone()) {
366                                    push_img_glyphs(font, glyphs, None)
367                                }
368                            }
369
370                            if let Some(suf) = &t.overflow_suffix {
371                                let suf_offset = o.suffix_origin.to_vector().cast_unit();
372                                for (font, glyphs) in suf.image_glyphs() {
373                                    push_img_glyphs(font, glyphs, Some(suf_offset))
374                                }
375                            }
376                        }
377                        _ => {
378                            // no overflow truncating
379                            for (font, glyphs) in t.shaped_text.image_glyphs() {
380                                push_img_glyphs(font, glyphs, None)
381                            }
382                        }
383                    }
384                } else if t.shaped_text.has_colored_glyphs() || t.overflow_suffix.as_ref().map(|o| o.has_colored_glyphs()).unwrap_or(false)
385                {
386                    let palette_query = FONT_PALETTE_VAR.get();
387                    FONT_PALETTE_COLORS_VAR.with(|palette_colors| {
388                        let mut push_font_glyphs = |font: &Font, glyphs, offset: Option<euclid::Vector2D<f32, Px>>| {
389                            let mut palette = None;
390
391                            match glyphs {
392                                ShapedColoredGlyphs::Normal(glyphs) => {
393                                    if let Some(offset) = offset {
394                                        let mut glyphs = glyphs.to_vec();
395                                        for g in &mut glyphs {
396                                            g.point.x += offset.x;
397                                            g.point.y += offset.y;
398                                        }
399                                        frame.push_text(clip, &glyphs, font, color_value, r.synthesis, aa);
400                                    } else {
401                                        frame.push_text(clip, glyphs, font, color_value, r.synthesis, aa);
402                                    }
403                                }
404                                ShapedColoredGlyphs::Colored { point, glyphs, .. } => {
405                                    for (index, color_i) in glyphs.iter() {
406                                        let color = if let Some(color_i) = color_i {
407                                            if let Some(i) = palette_colors.iter().position(|(ci, _)| *ci == color_i) {
408                                                palette_colors[i].1
409                                            } else {
410                                                // FontFace only parses colored glyphs if the font has at least one
411                                                // palette, so it is safe to unwrap here
412                                                let palette = palette
413                                                    .get_or_insert_with(|| font.face().color_palettes().palette(palette_query).unwrap());
414
415                                                // the font could have a bug and return an invalid palette index
416                                                palette.get(color_i).unwrap_or(color)
417                                            }
418                                        } else {
419                                            // color_i is None, meaning the base color.
420                                            color
421                                        };
422
423                                        let mut g = GlyphInstance::new(index, point);
424                                        if let Some(offset) = offset {
425                                            g.point.x += offset.x;
426                                            g.point.y += offset.y;
427                                        }
428                                        frame.push_text(clip, &[g], font, FrameValue::Value(color), r.synthesis, aa);
429                                    }
430                                }
431                            }
432                        };
433
434                        match (&t.overflow, TEXT_OVERFLOW_VAR.get(), TEXT_EDITABLE_VAR.get()) {
435                            (Some(o), TextOverflow::Truncate(_), false) => {
436                                for glyphs in &o.included_glyphs {
437                                    for (font, glyphs) in t.shaped_text.colored_glyphs_slice(glyphs.clone()) {
438                                        push_font_glyphs(font, glyphs, None)
439                                    }
440                                }
441
442                                if let Some(suf) = &t.overflow_suffix {
443                                    let suf_offset = o.suffix_origin.to_vector().cast_unit();
444                                    for (font, glyphs) in suf.colored_glyphs() {
445                                        push_font_glyphs(font, glyphs, Some(suf_offset))
446                                    }
447                                }
448                            }
449                            _ => {
450                                // no overflow truncating
451                                for (font, glyphs) in t.shaped_text.colored_glyphs() {
452                                    push_font_glyphs(font, glyphs, None)
453                                }
454                            }
455                        }
456                    });
457                } else {
458                    // no colored glyphs
459
460                    let mut push_font_glyphs = |font: &Font, glyphs: Cow<[GlyphInstance]>| {
461                        frame.push_text(clip, glyphs.as_ref(), font, color_value, r.synthesis, aa);
462                    };
463
464                    match (&t.overflow, TEXT_OVERFLOW_VAR.get(), TEXT_EDITABLE_VAR.get()) {
465                        (Some(o), TextOverflow::Truncate(_), false) => {
466                            for glyphs in &o.included_glyphs {
467                                for (font, glyphs) in t.shaped_text.glyphs_slice(glyphs.clone()) {
468                                    push_font_glyphs(font, Cow::Borrowed(glyphs))
469                                }
470                            }
471
472                            if let Some(suf) = &t.overflow_suffix {
473                                let suf_offset = o.suffix_origin.to_vector().cast_unit();
474                                for (font, glyphs) in suf.glyphs() {
475                                    let mut glyphs = glyphs.to_vec();
476                                    for g in &mut glyphs {
477                                        g.point += suf_offset;
478                                    }
479                                    push_font_glyphs(font, Cow::Owned(glyphs))
480                                }
481                            }
482                        }
483                        _ => {
484                            // no overflow truncating
485                            for (font, glyphs) in t.shaped_text.glyphs() {
486                                push_font_glyphs(font, Cow::Borrowed(glyphs))
487                            }
488                        }
489                    }
490                }
491            });
492        }
493        UiNodeOp::RenderUpdate { update } => {
494            TEXT.layout().render_info.transform = *update.transform();
495
496            if let Some(key) = color_key {
497                let color = FONT_COLOR_VAR.get();
498
499                update.update_color(key.update(color, FONT_COLOR_VAR.is_animating()));
500
501                let mut r = rendered.unwrap();
502                r.color = color;
503                rendered = Some(r);
504            }
505        }
506        _ => {}
507    })
508}