1use std::{
2 cell::{Cell, RefCell},
3 hash::{Hash, Hasher},
4 rc::Rc,
5};
6
7use cranpose_foundation::{
8 Constraints, DelegatableNode, DrawModifierNode, DrawScope, InvalidationKind,
9 LayoutModifierNode, Measurable, ModifierNode, ModifierNodeContext, ModifierNodeElement,
10 NodeCapabilities, NodeState, SemanticsConfiguration, SemanticsNode, Size,
11};
12
13use crate::text::{AnnotatedString, TextLayoutOptions, TextStyle};
14
15#[derive(Debug)]
25pub struct TextModifierNode {
26 layout: Rc<TextPreparedLayoutOwner>,
27 state: NodeState,
28}
29
30const PREPARED_LAYOUT_CACHE_CAPACITY: usize = 4;
31
32#[derive(Clone, Debug)]
33struct TextPreparedLayoutCacheEntry {
34 max_width_bits: Option<u32>,
35 text_generation: u64,
36 font_scale_fingerprint: u32,
37 layout: crate::text::PreparedTextLayout,
38}
39
40#[derive(Debug)]
41struct TextPreparedLayoutOwner {
42 text: Rc<AnnotatedString>,
43 style: TextStyle,
44 options: TextLayoutOptions,
45 node_id: Cell<Option<cranpose_core::NodeId>>,
46 measured_max_width: Cell<Option<Option<f32>>>,
47 cache: RefCell<Vec<TextPreparedLayoutCacheEntry>>,
48}
49
50#[derive(Clone, Debug)]
51pub(crate) struct TextPreparedLayoutHandle {
52 owner: Rc<TextPreparedLayoutOwner>,
53}
54
55impl TextPreparedLayoutOwner {
56 fn new(
57 text: Rc<AnnotatedString>,
58 style: TextStyle,
59 options: TextLayoutOptions,
60 node_id: Option<cranpose_core::NodeId>,
61 measured_max_width: Option<Option<f32>>,
62 ) -> Self {
63 Self {
64 text,
65 style,
66 options: options.normalized(),
67 node_id: Cell::new(node_id),
68 measured_max_width: Cell::new(measured_max_width),
69 cache: RefCell::new(Vec::new()),
70 }
71 }
72
73 fn text(&self) -> &str {
74 self.text.text.as_str()
75 }
76
77 fn annotated_text(&self) -> Rc<AnnotatedString> {
78 self.text.clone()
79 }
80
81 fn annotated_string(&self) -> AnnotatedString {
82 (*self.text).clone()
83 }
84
85 fn style(&self) -> &TextStyle {
86 &self.style
87 }
88
89 fn options(&self) -> TextLayoutOptions {
90 self.options
91 }
92
93 fn node_id(&self) -> Option<cranpose_core::NodeId> {
94 self.node_id.get()
95 }
96
97 fn set_node_id(&self, node_id: Option<cranpose_core::NodeId>) {
98 if self.node_id.replace(node_id) != node_id {
99 self.cache.borrow_mut().clear();
100 }
101 }
102
103 fn prepare(&self, max_width: Option<f32>) -> crate::text::PreparedTextLayout {
104 let normalized_max_width = max_width.filter(|width| width.is_finite() && *width > 0.0);
105 let max_width_bits = normalized_max_width.map(f32::to_bits);
106 let text_generation = crate::text::measure::current_text_generation();
107 let font_scale_fingerprint = crate::current_font_scale_curve().fingerprint();
108
109 {
110 let mut cache = self.cache.borrow_mut();
111 if let Some(index) = cache.iter().position(|entry| {
112 entry.max_width_bits == max_width_bits
113 && entry.text_generation == text_generation
114 && entry.font_scale_fingerprint == font_scale_fingerprint
115 }) {
116 let entry = cache.remove(index);
117 let prepared = entry.layout.clone();
118 cache.insert(0, entry);
119 return prepared;
120 }
121 }
122
123 let prepared = crate::text::prepare_text_layout_for_node(
124 self.node_id(),
125 self.text.as_ref(),
126 &self.style,
127 self.options,
128 normalized_max_width,
129 );
130
131 let mut cache = self.cache.borrow_mut();
132 cache.insert(
133 0,
134 TextPreparedLayoutCacheEntry {
135 max_width_bits,
136 text_generation,
137 font_scale_fingerprint,
138 layout: prepared.clone(),
139 },
140 );
141 cache.truncate(PREPARED_LAYOUT_CACHE_CAPACITY);
142 prepared
143 }
144
145 fn measure_text_content(&self, max_width: Option<f32>) -> Size {
146 let prepared = self.prepare(max_width);
147 Size {
148 width: prepared.metrics.width,
149 height: prepared.metrics.height,
150 }
151 }
152
153 fn measure_layout(&self, max_width: Option<f32>) -> Size {
154 self.measured_max_width.set(Some(max_width));
155 self.measure_text_content(max_width)
156 }
157
158 fn measured_layout(&self) -> Option<crate::text::PreparedTextLayout> {
159 self.measured_max_width
160 .get()
161 .map(|max_width| self.prepare(max_width))
162 }
163}
164
165impl TextPreparedLayoutHandle {
166 fn new(owner: Rc<TextPreparedLayoutOwner>) -> Self {
167 Self { owner }
168 }
169
170 pub(crate) fn measured_layout(&self) -> Option<crate::text::PreparedTextLayout> {
171 self.owner.measured_layout()
172 }
173}
174
175impl TextModifierNode {
176 pub fn new(text: Rc<AnnotatedString>, style: TextStyle, options: TextLayoutOptions) -> Self {
177 Self {
178 layout: Rc::new(TextPreparedLayoutOwner::new(
179 text, style, options, None, None,
180 )),
181 state: NodeState::new(),
182 }
183 }
184
185 pub fn text(&self) -> &str {
186 self.layout.text()
187 }
188
189 pub fn annotated_text(&self) -> Rc<AnnotatedString> {
190 self.layout.annotated_text()
191 }
192
193 pub fn annotated_string(&self) -> AnnotatedString {
194 self.layout.annotated_string()
195 }
196
197 pub fn style(&self) -> &TextStyle {
198 self.layout.style()
199 }
200
201 pub fn options(&self) -> TextLayoutOptions {
202 self.layout.options()
203 }
204
205 fn measure_text_content(&self, max_width: Option<f32>) -> Size {
206 self.layout.measure_text_content(max_width)
207 }
208
209 pub(crate) fn prepared_layout_handle(&self) -> TextPreparedLayoutHandle {
210 TextPreparedLayoutHandle::new(self.layout.clone())
211 }
212}
213
214impl DelegatableNode for TextModifierNode {
215 fn node_state(&self) -> &NodeState {
216 &self.state
217 }
218}
219
220impl ModifierNode for TextModifierNode {
221 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
222 self.layout.set_node_id(context.node_id());
223 context.invalidate(InvalidationKind::Layout);
224 context.invalidate(InvalidationKind::Draw);
225 context.invalidate(InvalidationKind::Semantics);
226 }
227
228 fn on_detach(&mut self) {
229 self.layout.set_node_id(None);
230 }
231
232 fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
233 Some(self)
234 }
235
236 fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
237 Some(self)
238 }
239
240 fn as_semantics_node(&self) -> Option<&dyn SemanticsNode> {
241 Some(self)
242 }
243
244 fn as_semantics_node_mut(&mut self) -> Option<&mut dyn SemanticsNode> {
245 Some(self)
246 }
247
248 fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
249 Some(self)
250 }
251
252 fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
253 Some(self)
254 }
255}
256
257impl LayoutModifierNode for TextModifierNode {
258 fn measure(
259 &self,
260 _context: &mut dyn ModifierNodeContext,
261 _measurable: &dyn Measurable,
262 constraints: Constraints,
263 ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
264 let max_width = constraints
265 .max_width
266 .is_finite()
267 .then_some(constraints.max_width);
268 let text_size = self.layout.measure_layout(max_width);
269
270 let width = text_size
271 .width
272 .clamp(constraints.min_width, constraints.max_width);
273 let height = text_size
274 .height
275 .clamp(constraints.min_height, constraints.max_height);
276
277 cranpose_ui_layout::LayoutModifierMeasureResult::with_size(Size { width, height })
278 }
279
280 fn min_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
281 self.measure_text_content(None).width
282 }
283
284 fn max_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
285 self.measure_text_content(None).width
286 }
287
288 fn min_intrinsic_height(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
289 self.measure_text_content(Some(_width).filter(|w| w.is_finite() && *w > 0.0))
290 .height
291 }
292
293 fn max_intrinsic_height(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
294 self.measure_text_content(Some(_width).filter(|w| w.is_finite() && *w > 0.0))
295 .height
296 }
297}
298
299impl DrawModifierNode for TextModifierNode {
300 fn draw(&self, _draw_scope: &mut dyn DrawScope) {}
301}
302
303impl SemanticsNode for TextModifierNode {
304 fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
305 config.content_description = Some(self.text().to_string());
306 }
307}
308
309#[derive(Debug, Clone, PartialEq)]
318pub struct TextModifierElement {
319 text: Rc<AnnotatedString>,
320 style: TextStyle,
321 options: TextLayoutOptions,
322}
323
324impl TextModifierElement {
325 pub fn new(text: Rc<AnnotatedString>, style: TextStyle, options: TextLayoutOptions) -> Self {
326 Self {
327 text,
328 style,
329 options: options.normalized(),
330 }
331 }
332}
333
334impl Hash for TextModifierElement {
335 fn hash<H: Hasher>(&self, state: &mut H) {
336 self.text.render_hash().hash(state);
337 self.style.render_hash().hash(state);
338 self.options.hash(state);
339 }
340}
341
342impl ModifierNodeElement for TextModifierElement {
343 type Node = TextModifierNode;
344
345 fn create(&self) -> Self::Node {
346 TextModifierNode::new(self.text.clone(), self.style.clone(), self.options)
347 }
348
349 fn update(&self, node: &mut Self::Node) {
350 let current = node.layout.as_ref();
351 if current.text != self.text
352 || current.style != self.style
353 || current.options != self.options
354 {
355 node.layout = Rc::new(TextPreparedLayoutOwner::new(
356 self.text.clone(),
357 self.style.clone(),
358 self.options,
359 current.node_id(),
360 current.measured_max_width.get(),
361 ));
362 }
363 }
364
365 fn capabilities(&self) -> NodeCapabilities {
366 NodeCapabilities::LAYOUT | NodeCapabilities::DRAW | NodeCapabilities::SEMANTICS
367 }
368}
369
370#[cfg(test)]
371#[path = "tests/text_modifier_node_tests.rs"]
372mod tests;