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 reactive_core::{Effect, effect};
9use renderer_core::TextStyle;
10use ui_tree::{Component, EventResult, RenderNode};
11
12use crate::context::mark_dirty;
13use crate::impl_leaf_widget;
14use crate::layout_leaf::LayoutLeaf;
15
16pub struct Text {
17 content: Rc<dyn Fn() -> String>,
18 cached_content: RefCell<(String, Arc<str>)>,
19 cached_ink: RefCell<Option<(String, u32, f32, f32)>>,
22 style: Rc<dyn Fn() -> TextStyle>,
23 leaf: LayoutLeaf,
24 _remeasure: Option<Effect>,
26}
27
28impl Text {
29 pub fn new(
30 content_fn: impl Fn() -> String + 'static,
31 layout_style: LayoutStyle,
32 style_fn: impl Fn() -> TextStyle + 'static,
33 ) -> Result<Self, LayoutError> {
34 let leaf = LayoutLeaf::register(layout_style.align_self_stretch())?;
36 Ok(Self {
37 content: Rc::new(content_fn),
38 cached_content: RefCell::new((String::new(), Arc::from(""))),
39 cached_ink: RefCell::new(None),
40 style: Rc::new(style_fn),
41 leaf,
42 _remeasure: None,
43 })
44 }
45
46 pub fn auto(
50 content_fn: impl Fn() -> String + 'static,
51 layout_style: LayoutStyle,
52 style_fn: impl Fn() -> TextStyle + 'static,
53 ) -> Result<Self, LayoutError> {
54 let content_fn: Rc<dyn Fn() -> String> = Rc::new(content_fn);
55 let style: Rc<dyn Fn() -> TextStyle> = Rc::new(style_fn);
56
57 let measure_content = Rc::clone(&content_fn);
58 let measure_style = Rc::clone(&style);
59 let measure = Box::new(move |max_width: f32| {
60 let s = (measure_style)();
61 renderer_text::measure_text(&(measure_content)(), max_width, &s)
62 });
63
64 let (node, rect) =
65 crate::context::new_measured_leaf(layout_style.align_self_stretch(), measure)?;
66 let dirty_content = Rc::clone(&content_fn);
68 let measured = RefCell::new(Option::<String>::None);
69 let remeasure = effect(move || {
70 let next = (dirty_content)();
71 if measured.borrow().as_deref() == Some(next.as_str()) {
72 return;
73 }
74 *measured.borrow_mut() = Some(next);
75 mark_dirty(node).ok();
76 });
77 Ok(Self {
78 content: content_fn,
79 cached_content: RefCell::new((String::new(), Arc::from(""))),
80 cached_ink: RefCell::new(None),
81 style,
82 leaf: LayoutLeaf { node, rect },
83 _remeasure: Some(remeasure),
84 })
85 }
86
87 pub fn single_line(
88 content_fn: impl Fn() -> String + 'static,
89 style_fn: impl Fn() -> TextStyle + 'static,
90 ) -> Result<Self, LayoutError> {
91 let height = style_fn().font_size * 1.4;
92 Text::new(content_fn, LayoutStyle::new().height(height), style_fn)
93 }
94}
95
96impl Component for Text {
97 fn view(&self) -> RenderNode {
98 let r = self.leaf.rect.get();
99 let text: Arc<str> = {
100 let new_str = (self.content)();
101 let mut cache = self.cached_content.borrow_mut();
102 if cache.0 != new_str {
103 let rc = Arc::from(new_str.as_str());
104 *cache = (new_str, Arc::clone(&rc));
105 rc
106 } else {
107 Arc::clone(&cache.1)
108 }
109 };
110 let style = (self.style)();
111 let (ink_top, ink_height) = {
116 let width_bits = r.width.to_bits();
117 let mut cache = self.cached_ink.borrow_mut();
118 match cache.as_ref() {
119 Some((t, w, top, h)) if *t == *text && *w == width_bits => (*top, *h),
120 _ => {
121 let (top, h) = renderer_text::measure_ink_bounds(&text, r.width, &style);
122 *cache = Some((text.to_string(), width_bits, top, h));
123 (top, h)
124 }
125 }
126 };
127 let (_, line_height) = renderer_text::measure_text(&text, r.width, &style);
130 let y = if ink_height > 0.0 {
131 r.height / 2.0 - ink_top - ink_height / 2.0
132 } else {
133 0.0
134 };
135 self.leaf.at_layout_position(RenderNode::text(
136 text,
137 Rect {
138 x: 0.0,
139 y,
140 width: r.width,
141 height: line_height,
142 },
143 style,
144 ))
145 }
146
147 fn on_event(&mut self, _event: &Event) -> EventResult {
148 EventResult::Ignored
149 }
150
151 fn debug_name(&self) -> &'static str {
152 "Text"
153 }
154}
155
156impl_leaf_widget!(Text);
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use crate::context::{compute_layout, new_container, relayout_if_dirty, reset_layout_runtime};
162 use crate::layout_item::LayoutItem;
163 use layout_core::AvailableSpace;
164 use reactive_core::signal;
165 use renderer_core::Color;
166
167 #[test]
174 fn a_measured_label_re_measures_when_its_content_changes() {
175 reset_layout_runtime();
176 let title = signal(String::from("Desktop"));
177 let read = title.read_only();
178 let label = Text::auto(
179 move || read.get(),
180 LayoutStyle::new(),
181 || TextStyle::new(13.0, Color::BLACK),
182 )
183 .unwrap();
184 let node = label.layout_node();
185 let root = new_container(
186 LayoutStyle::new().flex_row().width(1920.0).height(32.0),
187 &[node],
188 )
189 .unwrap();
190 let space = || {
191 compute_layout(
192 root,
193 AvailableSpace::Definite(1920.0),
194 AvailableSpace::Definite(32.0),
195 )
196 .unwrap()
197 };
198
199 space();
200 let short = label.leaf.rect.get().width;
201
202 title.set("hyprshell - Rust - Visual Studio Code".to_string());
203 relayout_if_dirty();
204 let long = label.leaf.rect.get().width;
205
206 assert!(
207 long > short,
208 "a title five times longer still measured {long}px, the width \"Desktop\" wanted ({short}px) — \
209 it will be wrapped into a box built for the old text"
210 );
211 }
212}