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 UiNode) -> impl 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 UiNode) -> impl 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 UiNode) -> impl 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 UiNode) -> impl 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 UiNode) -> impl 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::Event { update } => {
179            if let Some(args) = FOCUS_CHANGED_EVENT.on(update) {
180                let new_is_focused = args.is_focus_within(WIDGET.id());
181                if is_focused != new_is_focused {
182                    WIDGET.render();
183                    is_focused = new_is_focused;
184                }
185            }
186        }
187        UiNodeOp::Render { frame } => {
188            let r_txt = TEXT.resolved();
189
190            if let Some(range) = r_txt.caret.selection_range() {
191                let l_txt = TEXT.laidout();
192                let txt = r_txt.segmented_text.text();
193
194                let mut selection_color = SELECTION_COLOR_VAR.get();
195                if !is_focused && !r_txt.selection_toolbar_is_open {
196                    selection_color = selection_color.desaturate(100.pct());
197                }
198
199                for line_rect in l_txt.shaped_text.highlight_rects(range, txt) {
200                    if !line_rect.size.is_empty() {
201                        frame.push_color(line_rect, FrameValue::Value(selection_color));
202                    }
203                }
204            };
205        }
206        _ => {}
207    })
208}
209
210/// An UI node that renders the parent [`LaidoutText`].
211///
212/// This node renders the text only, decorators are rendered by other nodes.
213///
214/// This is the `Text!` widget inner most child node.
215///
216/// [`LaidoutText`]: super::LaidoutText
217pub fn render_text() -> impl UiNode {
218    #[derive(Clone, Copy, PartialEq)]
219    struct RenderedText {
220        version: u32,
221        synthesis: FontSynthesis,
222        color: Rgba,
223        aa: FontAntiAliasing,
224    }
225
226    let mut reuse = None;
227    let mut rendered = None;
228    let mut color_key = None;
229    let image_spatial_id = SpatialFrameId::new_unique();
230    let mut has_loading_images = false;
231
232    match_node_leaf(move |op| match op {
233        UiNodeOp::Init => {
234            WIDGET
235                .sub_var_render_update(&FONT_COLOR_VAR)
236                .sub_var_render(&FONT_AA_VAR)
237                .sub_var(&FONT_PALETTE_VAR)
238                .sub_var(&FONT_PALETTE_COLORS_VAR);
239
240            if FONT_COLOR_VAR.capabilities().contains(VarCapability::NEW) {
241                color_key = Some(FrameValueKey::new_unique());
242            }
243        }
244        UiNodeOp::Deinit => {
245            color_key = None;
246            reuse = None;
247            rendered = None;
248            has_loading_images = false;
249        }
250        UiNodeOp::Update { .. } => {
251            if FONT_PALETTE_VAR.is_new() || FONT_PALETTE_COLORS_VAR.is_new() {
252                if let Some(t) = TEXT.try_laidout() {
253                    if t.shaped_text.has_colored_glyphs() {
254                        WIDGET.render();
255                    }
256                }
257            }
258        }
259        UiNodeOp::Measure { desired_size, .. } => {
260            let txt = TEXT.laidout();
261            *desired_size = LAYOUT.constraints().fill_size_or(txt.shaped_text.size())
262        }
263        UiNodeOp::Layout { final_size, .. } => {
264            // layout implemented in `layout_text`, it sets the size as an exact size constraint, we return
265            // the size here for foreign nodes in the CHILD_LAYOUT+100 ..= CHILD range.
266            let txt = TEXT.laidout();
267            *final_size = LAYOUT.constraints().fill_size_or(txt.shaped_text.size())
268        }
269        UiNodeOp::Render { frame } => {
270            let r = TEXT.resolved();
271            let mut t = TEXT.layout();
272
273            let lh = t.shaped_text.line_height();
274            let clip = PxRect::from_size(t.shaped_text.align_size()).inflate(lh, lh); // clip inflated to allow some weird glyphs
275            let color = FONT_COLOR_VAR.get();
276            let color_value = if let Some(key) = color_key {
277                key.bind(color, FONT_COLOR_VAR.is_animating())
278            } else {
279                FrameValue::Value(color)
280            };
281
282            let aa = FONT_AA_VAR.get();
283
284            let rt = Some(RenderedText {
285                version: t.shaped_text_version,
286                synthesis: r.synthesis,
287                color,
288                aa,
289            });
290            if rendered != rt {
291                rendered = rt;
292                reuse = None;
293            }
294
295            t.render_info.transform = *frame.transform();
296            t.render_info.scale_factor = frame.scale_factor();
297
298            if std::mem::take(&mut has_loading_images)
299                && reuse.is_some()
300                && frame.render_widgets().delivery_list().enter_widget(WIDGET.id())
301            {
302                // loading emoji images request render on load
303                reuse = None;
304            }
305
306            frame.push_reuse(&mut reuse, |frame| {
307                if t.shaped_text.has_images() {
308                    let mut img_count = 0;
309                    let mut push_img_glyphs = |font: &Font, glyphs, offset: Option<euclid::Vector2D<f32, Px>>| match glyphs {
310                        ShapedImageGlyphs::Normal(glyphs) => {
311                            if let Some(offset) = offset {
312                                let mut glyphs = glyphs.to_vec();
313                                for g in &mut glyphs {
314                                    g.point.x += offset.x;
315                                    g.point.y += offset.y;
316                                }
317                                frame.push_text(clip, &glyphs, font, color_value, r.synthesis, aa);
318                            } else {
319                                frame.push_text(clip, glyphs, font, color_value, r.synthesis, aa);
320                            }
321                        }
322                        ShapedImageGlyphs::Image { rect, img, .. } => {
323                            let is_loading = img.with(|i| {
324                                if i.is_loaded() {
325                                    frame.push_reference_frame(
326                                        ReferenceFrameId::from_unique_child(image_spatial_id, img_count),
327                                        FrameValue::Value(PxTransform::translation(rect.origin.x, rect.origin.y)),
328                                        true,
329                                        true,
330                                        |frame| {
331                                            let size = rect.size.cast::<Px>();
332                                            frame.push_image(
333                                                PxRect::from_size(size),
334                                                size,
335                                                size,
336                                                PxSize::zero(),
337                                                i,
338                                                zng_view_api::ImageRendering::Pixelated,
339                                            );
340                                        },
341                                    );
342                                    img_count = img_count.wrapping_add(1);
343                                }
344                                i.is_loading()
345                            });
346                            if is_loading {
347                                has_loading_images = true;
348                                let id = WIDGET.id();
349                                img.hook(move |args| {
350                                    if args.value().is_loaded() {
351                                        UPDATES.render(id);
352                                    }
353                                    args.value().is_loading()
354                                })
355                                .perm();
356                            }
357                        }
358                    };
359
360                    match (&t.overflow, TEXT_OVERFLOW_VAR.get(), TEXT_EDITABLE_VAR.get()) {
361                        (Some(o), TextOverflow::Truncate(_), false) => {
362                            for glyphs in &o.included_glyphs {
363                                for (font, glyphs) in t.shaped_text.image_glyphs_slice(glyphs.clone()) {
364                                    push_img_glyphs(font, glyphs, None)
365                                }
366                            }
367
368                            if let Some(suf) = &t.overflow_suffix {
369                                let suf_offset = o.suffix_origin.to_vector().cast_unit();
370                                for (font, glyphs) in suf.image_glyphs() {
371                                    push_img_glyphs(font, glyphs, Some(suf_offset))
372                                }
373                            }
374                        }
375                        _ => {
376                            // no overflow truncating
377                            for (font, glyphs) in t.shaped_text.image_glyphs() {
378                                push_img_glyphs(font, glyphs, None)
379                            }
380                        }
381                    }
382                } else if t.shaped_text.has_colored_glyphs() || t.overflow_suffix.as_ref().map(|o| o.has_colored_glyphs()).unwrap_or(false)
383                {
384                    let palette_query = FONT_PALETTE_VAR.get();
385                    FONT_PALETTE_COLORS_VAR.with(|palette_colors| {
386                        let mut push_font_glyphs = |font: &Font, glyphs, offset: Option<euclid::Vector2D<f32, Px>>| {
387                            let mut palette = None;
388
389                            match glyphs {
390                                ShapedColoredGlyphs::Normal(glyphs) => {
391                                    if let Some(offset) = offset {
392                                        let mut glyphs = glyphs.to_vec();
393                                        for g in &mut glyphs {
394                                            g.point.x += offset.x;
395                                            g.point.y += offset.y;
396                                        }
397                                        frame.push_text(clip, &glyphs, font, color_value, r.synthesis, aa);
398                                    } else {
399                                        frame.push_text(clip, glyphs, font, color_value, r.synthesis, aa);
400                                    }
401                                }
402                                ShapedColoredGlyphs::Colored { point, glyphs, .. } => {
403                                    for (index, color_i) in glyphs.iter() {
404                                        let color = if let Some(color_i) = color_i {
405                                            if let Some(i) = palette_colors.iter().position(|(ci, _)| *ci == color_i as u16) {
406                                                palette_colors[i].1
407                                            } else {
408                                                // FontFace only parses colored glyphs if the font has at least one
409                                                // palette, so it is safe to unwrap here
410                                                let palette = palette
411                                                    .get_or_insert_with(|| font.face().color_palettes().palette(palette_query).unwrap());
412
413                                                // the font could have a bug and return an invalid palette index
414                                                palette.colors.get(color_i).copied().unwrap_or(color)
415                                            }
416                                        } else {
417                                            // color_i is None, meaning the base color.
418                                            color
419                                        };
420
421                                        let mut g = GlyphInstance { point, index };
422                                        if let Some(offset) = offset {
423                                            g.point.x += offset.x;
424                                            g.point.y += offset.y;
425                                        }
426                                        frame.push_text(clip, &[g], font, FrameValue::Value(color), r.synthesis, aa);
427                                    }
428                                }
429                            }
430                        };
431
432                        match (&t.overflow, TEXT_OVERFLOW_VAR.get(), TEXT_EDITABLE_VAR.get()) {
433                            (Some(o), TextOverflow::Truncate(_), false) => {
434                                for glyphs in &o.included_glyphs {
435                                    for (font, glyphs) in t.shaped_text.colored_glyphs_slice(glyphs.clone()) {
436                                        push_font_glyphs(font, glyphs, None)
437                                    }
438                                }
439
440                                if let Some(suf) = &t.overflow_suffix {
441                                    let suf_offset = o.suffix_origin.to_vector().cast_unit();
442                                    for (font, glyphs) in suf.colored_glyphs() {
443                                        push_font_glyphs(font, glyphs, Some(suf_offset))
444                                    }
445                                }
446                            }
447                            _ => {
448                                // no overflow truncating
449                                for (font, glyphs) in t.shaped_text.colored_glyphs() {
450                                    push_font_glyphs(font, glyphs, None)
451                                }
452                            }
453                        }
454                    });
455                } else {
456                    // no colored glyphs
457
458                    let mut push_font_glyphs = |font: &Font, glyphs: Cow<[GlyphInstance]>| {
459                        frame.push_text(clip, glyphs.as_ref(), font, color_value, r.synthesis, aa);
460                    };
461
462                    match (&t.overflow, TEXT_OVERFLOW_VAR.get(), TEXT_EDITABLE_VAR.get()) {
463                        (Some(o), TextOverflow::Truncate(_), false) => {
464                            for glyphs in &o.included_glyphs {
465                                for (font, glyphs) in t.shaped_text.glyphs_slice(glyphs.clone()) {
466                                    push_font_glyphs(font, Cow::Borrowed(glyphs))
467                                }
468                            }
469
470                            if let Some(suf) = &t.overflow_suffix {
471                                let suf_offset = o.suffix_origin.to_vector().cast_unit();
472                                for (font, glyphs) in suf.glyphs() {
473                                    let mut glyphs = glyphs.to_vec();
474                                    for g in &mut glyphs {
475                                        g.point += suf_offset;
476                                    }
477                                    push_font_glyphs(font, Cow::Owned(glyphs))
478                                }
479                            }
480                        }
481                        _ => {
482                            // no overflow truncating
483                            for (font, glyphs) in t.shaped_text.glyphs() {
484                                push_font_glyphs(font, Cow::Borrowed(glyphs))
485                            }
486                        }
487                    }
488                }
489            });
490        }
491        UiNodeOp::RenderUpdate { update } => {
492            TEXT.layout().render_info.transform = *update.transform();
493
494            if let Some(key) = color_key {
495                let color = FONT_COLOR_VAR.get();
496
497                update.update_color(key.update(color, FONT_COLOR_VAR.is_animating()));
498
499                let mut r = rendered.unwrap();
500                r.color = color;
501                rendered = Some(r);
502            }
503        }
504        _ => {}
505    })
506}