Skip to main content

telar_ui_core/
text.rs

1use std::cell::RefCell;
2use std::rc::Rc;
3use std::sync::Arc;
4
5use geometry_core::Rect;
6use layout_core::{LayoutError, LayoutStyle};
7use platform_core::Event;
8use renderer_core::TextStyle;
9use ui_tree::{Component, EventResult, RenderNode};
10
11use crate::impl_leaf_widget;
12use crate::layout_leaf::LayoutLeaf;
13
14pub struct Text {
15    content: Rc<dyn Fn() -> String>,
16    cached_content: RefCell<(String, Arc<str>)>,
17    // Ink bounds memo for optical vertical centering: (text, width_bits) -> (ink_top, ink_height).
18    // Recomputed only when the text or its resolved width changes, so a static label costs no per-frame shaping.
19    cached_ink: RefCell<Option<(String, u32, f32, f32)>>,
20    style: Rc<dyn Fn() -> TextStyle>,
21    leaf: LayoutLeaf,
22}
23
24impl Text {
25    pub fn new(
26        content_fn: impl Fn() -> String + 'static,
27        layout_style: LayoutStyle,
28        style_fn: impl Fn() -> TextStyle + 'static,
29    ) -> Result<Self, LayoutError> {
30        // Stretch overrides any parent align-items (e.g. center) so text always fills the parent's cross-axis width instead of collapsing to 0.
31        let leaf = LayoutLeaf::register(layout_style.align_self_stretch())?;
32        Ok(Self {
33            content: Rc::new(content_fn),
34            cached_content: RefCell::new((String::new(), Arc::from(""))),
35            cached_ink: RefCell::new(None),
36            style: Rc::new(style_fn),
37            leaf,
38        })
39    }
40
41    /// Like [`Text::new`], but the leaf's height is measured from the content at its
42    /// resolved width, so the box grows to fit however many lines the text wraps
43    /// into and pushes following siblings down instead of overflowing onto them.
44    pub fn auto(
45        content_fn: impl Fn() -> String + 'static,
46        layout_style: LayoutStyle,
47        style_fn: impl Fn() -> TextStyle + 'static,
48    ) -> Result<Self, LayoutError> {
49        let content_fn: Rc<dyn Fn() -> String> = Rc::new(content_fn);
50        let style: Rc<dyn Fn() -> TextStyle> = Rc::new(style_fn);
51
52        let measure_content = Rc::clone(&content_fn);
53        let measure_style = Rc::clone(&style);
54        let measure = Box::new(move |max_width: f32| {
55            let s = (measure_style)();
56            renderer_text::measure_text(&(measure_content)(), max_width, &s)
57        });
58
59        let (node, rect) =
60            crate::context::new_measured_leaf(layout_style.align_self_stretch(), measure)?;
61        Ok(Self {
62            content: content_fn,
63            cached_content: RefCell::new((String::new(), Arc::from(""))),
64            cached_ink: RefCell::new(None),
65            style,
66            leaf: LayoutLeaf { node, rect },
67        })
68    }
69
70    pub fn single_line(
71        content_fn: impl Fn() -> String + 'static,
72        style_fn: impl Fn() -> TextStyle + 'static,
73    ) -> Result<Self, LayoutError> {
74        let height = style_fn().font_size * 1.4;
75        Text::new(content_fn, LayoutStyle::new().height(height), style_fn)
76    }
77}
78
79impl Component for Text {
80    fn view(&self) -> RenderNode {
81        let r = self.leaf.rect.get();
82        let text: Arc<str> = {
83            let new_str = (self.content)();
84            let mut cache = self.cached_content.borrow_mut();
85            if cache.0 != new_str {
86                let rc = Arc::from(new_str.as_str());
87                *cache = (new_str, Arc::clone(&rc));
88                rc
89            } else {
90                Arc::clone(&cache.1)
91            }
92        };
93        let style = (self.style)();
94        // Optically center the text's INK within the leaf. A text leaf stretches to fill its parent's cross
95        // axis (`align_self_stretch`), and the font's line box reserves ascent room for accents/descenders
96        // that a short run ("72%") leaves empty — so line-box-centered text sits visibly high next to an
97        // icon. Centering the actual drawn glyph extent lines the two up. Memoized per (text, width).
98        let (ink_top, ink_height) = {
99            let width_bits = r.width.to_bits();
100            let mut cache = self.cached_ink.borrow_mut();
101            match cache.as_ref() {
102                Some((t, w, top, h)) if *t == *text && *w == width_bits => (*top, *h),
103                _ => {
104                    let (top, h) = renderer_text::measure_ink_bounds(&text, r.width, &style);
105                    *cache = Some((text.to_string(), width_bits, top, h));
106                    (top, h)
107                }
108            }
109        };
110        // Render the full line box (so nothing clips), offset so the ink's own center lands on the leaf's
111        // center. When there is no ink (empty run) fall back to a top-aligned box.
112        let (_, line_height) = renderer_text::measure_text(&text, r.width, &style);
113        let y = if ink_height > 0.0 {
114            r.height / 2.0 - ink_top - ink_height / 2.0
115        } else {
116            0.0
117        };
118        self.leaf.at_layout_position(RenderNode::text(
119            text,
120            Rect {
121                x: 0.0,
122                y,
123                width: r.width,
124                height: line_height,
125            },
126            style,
127        ))
128    }
129
130    fn on_event(&mut self, _event: &Event) -> EventResult {
131        EventResult::Ignored
132    }
133
134    fn debug_name(&self) -> &'static str {
135        "Text"
136    }
137}
138
139impl_leaf_widget!(Text);