1use crate::text::{AnnotatedString, TextLayoutOptions, TextStyle};
23use cranpose_foundation::{
24 Constraints, DelegatableNode, DrawModifierNode, DrawScope, InvalidationKind,
25 LayoutModifierNode, Measurable, ModifierNode, ModifierNodeContext, ModifierNodeElement,
26 NodeCapabilities, NodeState, SemanticsConfiguration, SemanticsNode, Size,
27};
28use std::cell::{Cell, RefCell};
29use std::hash::{Hash, Hasher};
30use std::rc::Rc;
31
32#[derive(Debug)]
42pub struct TextModifierNode {
43 layout: Rc<TextPreparedLayoutOwner>,
44 state: NodeState,
45}
46
47const PREPARED_LAYOUT_CACHE_CAPACITY: usize = 4;
48
49#[derive(Clone, Debug)]
50struct TextPreparedLayoutCacheEntry {
51 max_width_bits: Option<u32>,
52 layout: crate::text::PreparedTextLayout,
53}
54
55#[derive(Debug)]
56struct TextPreparedLayoutOwner {
57 text: Rc<AnnotatedString>,
58 style: TextStyle,
59 options: TextLayoutOptions,
60 node_id: Cell<Option<cranpose_core::NodeId>>,
61 cache: RefCell<Vec<TextPreparedLayoutCacheEntry>>,
62}
63
64#[derive(Clone, Debug)]
65pub(crate) struct TextPreparedLayoutHandle {
66 owner: Rc<TextPreparedLayoutOwner>,
67}
68
69impl TextPreparedLayoutOwner {
70 fn new(
71 text: Rc<AnnotatedString>,
72 style: TextStyle,
73 options: TextLayoutOptions,
74 node_id: Option<cranpose_core::NodeId>,
75 ) -> Self {
76 Self {
77 text,
78 style,
79 options: options.normalized(),
80 node_id: Cell::new(node_id),
81 cache: RefCell::new(Vec::new()),
82 }
83 }
84
85 fn text(&self) -> &str {
86 self.text.text.as_str()
87 }
88
89 fn annotated_text(&self) -> Rc<AnnotatedString> {
90 self.text.clone()
91 }
92
93 fn annotated_string(&self) -> AnnotatedString {
94 (*self.text).clone()
95 }
96
97 fn style(&self) -> &TextStyle {
98 &self.style
99 }
100
101 fn options(&self) -> TextLayoutOptions {
102 self.options
103 }
104
105 fn node_id(&self) -> Option<cranpose_core::NodeId> {
106 self.node_id.get()
107 }
108
109 fn set_node_id(&self, node_id: Option<cranpose_core::NodeId>) {
110 if self.node_id.replace(node_id) != node_id {
111 self.cache.borrow_mut().clear();
112 }
113 }
114
115 fn prepare(&self, max_width: Option<f32>) -> crate::text::PreparedTextLayout {
116 let normalized_max_width = max_width.filter(|width| width.is_finite() && *width > 0.0);
117 let max_width_bits = normalized_max_width.map(f32::to_bits);
118
119 {
120 let mut cache = self.cache.borrow_mut();
121 if let Some(index) = cache
122 .iter()
123 .position(|entry| entry.max_width_bits == max_width_bits)
124 {
125 let entry = cache.remove(index);
126 let prepared = entry.layout.clone();
127 cache.insert(0, entry);
128 return prepared;
129 }
130 }
131
132 let prepared = crate::text::prepare_text_layout_for_node(
133 self.node_id(),
134 self.text.as_ref(),
135 &self.style,
136 self.options,
137 normalized_max_width,
138 );
139
140 let mut cache = self.cache.borrow_mut();
141 cache.insert(
142 0,
143 TextPreparedLayoutCacheEntry {
144 max_width_bits,
145 layout: prepared.clone(),
146 },
147 );
148 cache.truncate(PREPARED_LAYOUT_CACHE_CAPACITY);
149 prepared
150 }
151
152 fn measure_text_content(&self, max_width: Option<f32>) -> Size {
153 let prepared = self.prepare(max_width);
154 Size {
155 width: prepared.metrics.width,
156 height: prepared.metrics.height,
157 }
158 }
159}
160
161impl TextPreparedLayoutHandle {
162 fn new(owner: Rc<TextPreparedLayoutOwner>) -> Self {
163 Self { owner }
164 }
165
166 pub(crate) fn prepare(&self, max_width: Option<f32>) -> crate::text::PreparedTextLayout {
167 self.owner.prepare(max_width)
168 }
169}
170
171impl TextModifierNode {
172 pub fn new(text: Rc<AnnotatedString>, style: TextStyle, options: TextLayoutOptions) -> Self {
173 Self {
174 layout: Rc::new(TextPreparedLayoutOwner::new(text, style, options, None)),
175 state: NodeState::new(),
176 }
177 }
178
179 pub fn text(&self) -> &str {
180 self.layout.text()
181 }
182
183 pub fn annotated_text(&self) -> Rc<AnnotatedString> {
184 self.layout.annotated_text()
185 }
186
187 pub fn annotated_string(&self) -> AnnotatedString {
188 self.layout.annotated_string()
189 }
190
191 pub fn style(&self) -> &TextStyle {
192 self.layout.style()
193 }
194
195 pub fn options(&self) -> TextLayoutOptions {
196 self.layout.options()
197 }
198
199 fn measure_text_content(&self, max_width: Option<f32>) -> Size {
200 self.layout.measure_text_content(max_width)
201 }
202
203 pub(crate) fn prepared_layout_handle(&self) -> TextPreparedLayoutHandle {
204 TextPreparedLayoutHandle::new(self.layout.clone())
205 }
206}
207
208impl DelegatableNode for TextModifierNode {
209 fn node_state(&self) -> &NodeState {
210 &self.state
211 }
212}
213
214impl ModifierNode for TextModifierNode {
215 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
216 self.layout.set_node_id(context.node_id());
217 context.invalidate(InvalidationKind::Layout);
219 context.invalidate(InvalidationKind::Draw);
220 context.invalidate(InvalidationKind::Semantics);
221 }
222
223 fn on_detach(&mut self) {
224 self.layout.set_node_id(None);
225 }
226
227 fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
228 Some(self)
229 }
230
231 fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
232 Some(self)
233 }
234
235 fn as_semantics_node(&self) -> Option<&dyn SemanticsNode> {
236 Some(self)
237 }
238
239 fn as_semantics_node_mut(&mut self) -> Option<&mut dyn SemanticsNode> {
240 Some(self)
241 }
242
243 fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
244 Some(self)
245 }
246
247 fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
248 Some(self)
249 }
250}
251
252impl LayoutModifierNode for TextModifierNode {
253 fn measure(
254 &self,
255 _context: &mut dyn ModifierNodeContext,
256 _measurable: &dyn Measurable,
257 constraints: Constraints,
258 ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
259 let max_width = constraints
261 .max_width
262 .is_finite()
263 .then_some(constraints.max_width);
264 let text_size = self.measure_text_content(max_width);
265
266 let width = text_size
268 .width
269 .clamp(constraints.min_width, constraints.max_width);
270 let height = text_size
271 .height
272 .clamp(constraints.min_height, constraints.max_height);
273
274 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 }
304}
305
306impl SemanticsNode for TextModifierNode {
307 fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
308 config.content_description = Some(self.text().to_string());
310 }
311}
312
313#[derive(Debug, Clone, PartialEq)]
322pub struct TextModifierElement {
323 text: Rc<AnnotatedString>,
324 style: TextStyle,
325 options: TextLayoutOptions,
326}
327
328impl TextModifierElement {
329 pub fn new(text: Rc<AnnotatedString>, style: TextStyle, options: TextLayoutOptions) -> Self {
330 Self {
331 text,
332 style,
333 options: options.normalized(),
334 }
335 }
336}
337
338impl Hash for TextModifierElement {
339 fn hash<H: Hasher>(&self, state: &mut H) {
340 self.text.render_hash().hash(state);
341 self.style.render_hash().hash(state);
342 self.options.hash(state);
343 }
344}
345
346impl ModifierNodeElement for TextModifierElement {
347 type Node = TextModifierNode;
348
349 fn create(&self) -> Self::Node {
350 TextModifierNode::new(self.text.clone(), self.style.clone(), self.options)
351 }
352
353 fn update(&self, node: &mut Self::Node) {
354 let current = node.layout.as_ref();
355 if current.text != self.text
356 || current.style != self.style
357 || current.options != self.options
358 {
359 node.layout = Rc::new(TextPreparedLayoutOwner::new(
360 self.text.clone(),
361 self.style.clone(),
362 self.options,
363 current.node_id(),
364 ));
365 }
366 }
367
368 fn capabilities(&self) -> NodeCapabilities {
369 NodeCapabilities::LAYOUT | NodeCapabilities::DRAW | NodeCapabilities::SEMANTICS
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377 use crate::text::TextUnit;
378 use crate::text_layout_result::TextLayoutResult;
379 use cranpose_core::NodeId;
380 use cranpose_foundation::BasicModifierNodeContext;
381 use std::collections::hash_map::DefaultHasher;
382 use std::sync::mpsc;
383
384 fn hash_of(element: &TextModifierElement) -> u64 {
385 let mut hasher = DefaultHasher::new();
386 element.hash(&mut hasher);
387 hasher.finish()
388 }
389
390 struct RecordingPreparedLayoutMeasurer {
391 recorded: std::rc::Rc<std::cell::RefCell<Vec<Option<NodeId>>>>,
392 }
393
394 impl crate::text::TextMeasurer for RecordingPreparedLayoutMeasurer {
395 fn measure(
396 &self,
397 _text: &crate::text::AnnotatedString,
398 _style: &TextStyle,
399 ) -> crate::text::TextMetrics {
400 crate::text::TextMetrics {
401 width: 12.0,
402 height: 18.0,
403 line_height: 18.0,
404 line_count: 1,
405 }
406 }
407
408 fn prepare_with_options_for_node(
409 &self,
410 node_id: Option<NodeId>,
411 text: &crate::text::AnnotatedString,
412 _style: &TextStyle,
413 _options: TextLayoutOptions,
414 _max_width: Option<f32>,
415 ) -> crate::text::PreparedTextLayout {
416 self.recorded.borrow_mut().push(node_id);
417 crate::text::PreparedTextLayout {
418 text: text.clone(),
419 visual_style: TextStyle::default(),
420 metrics: crate::text::TextMetrics {
421 width: 12.0,
422 height: 18.0,
423 line_height: 18.0,
424 line_count: 1,
425 },
426 did_overflow: false,
427 }
428 }
429
430 fn get_offset_for_position(
431 &self,
432 _text: &crate::text::AnnotatedString,
433 _style: &TextStyle,
434 _x: f32,
435 _y: f32,
436 ) -> usize {
437 0
438 }
439
440 fn get_cursor_x_for_offset(
441 &self,
442 _text: &crate::text::AnnotatedString,
443 _style: &TextStyle,
444 _offset: usize,
445 ) -> f32 {
446 0.0
447 }
448
449 fn layout(
450 &self,
451 _text: &crate::text::AnnotatedString,
452 _style: &TextStyle,
453 ) -> TextLayoutResult {
454 panic!("layout is not used in this test");
455 }
456 }
457
458 #[test]
459 fn hash_changes_when_style_changes() {
460 let text = Rc::new(AnnotatedString::from("Hello"));
461 let element_a = TextModifierElement::new(
462 text.clone(),
463 TextStyle::default(),
464 TextLayoutOptions::default(),
465 );
466 let style_b = TextStyle {
467 span_style: crate::text::SpanStyle {
468 font_size: TextUnit::Sp(18.0),
469 ..Default::default()
470 },
471 ..Default::default()
472 };
473 let element_b = TextModifierElement::new(text, style_b, TextLayoutOptions::default());
474
475 assert_ne!(element_a, element_b);
476 assert_ne!(hash_of(&element_a), hash_of(&element_b));
477 }
478
479 #[test]
480 fn hash_matches_for_equal_elements() {
481 let style = TextStyle {
482 span_style: crate::text::SpanStyle {
483 font_size: TextUnit::Sp(14.0),
484 letter_spacing: TextUnit::Em(0.1),
485 ..Default::default()
486 },
487 ..Default::default()
488 };
489 let options = TextLayoutOptions::default();
490 let text = Rc::new(AnnotatedString::from("Hash me"));
491 let element_a = TextModifierElement::new(text.clone(), style.clone(), options);
492 let element_b = TextModifierElement::new(text, style, options);
493
494 assert_eq!(element_a, element_b);
495 assert_eq!(hash_of(&element_a), hash_of(&element_b));
496 }
497
498 #[test]
499 fn measure_uses_attached_node_identity() {
500 let (tx, rx) = mpsc::channel();
501
502 std::thread::spawn(move || {
503 let recorded = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
504 let app_context = crate::AppContext::new();
505 app_context.enter(|| {
506 crate::text::set_text_measurer(RecordingPreparedLayoutMeasurer {
507 recorded: recorded.clone(),
508 });
509
510 let mut node = TextModifierNode::new(
511 Rc::new(AnnotatedString::from("identity")),
512 TextStyle::default(),
513 TextLayoutOptions::default(),
514 );
515 let mut context = BasicModifierNodeContext::new();
516 context.set_node_id(Some(77));
517 node.on_attach(&mut context);
518
519 let size = node.measure_text_content(Some(96.0));
520 tx.send((recorded.borrow().clone(), size.width, size.height))
521 .expect("send measurement result");
522 });
523 });
524
525 let (recorded, width, height) = rx.recv().expect("receive measurement result");
526 assert_eq!(recorded, vec![Some(77)]);
527 assert_eq!(width, 12.0);
528 assert_eq!(height, 18.0);
529 }
530
531 #[test]
532 fn prepared_layout_cache_reuses_node_snapshot() {
533 let (tx, rx) = mpsc::channel();
534
535 std::thread::spawn(move || {
536 let recorded = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
537 let app_context = crate::AppContext::new();
538 app_context.enter(|| {
539 crate::text::set_text_measurer(RecordingPreparedLayoutMeasurer {
540 recorded: recorded.clone(),
541 });
542
543 let mut node = TextModifierNode::new(
544 Rc::new(AnnotatedString::from("reuse")),
545 TextStyle::default(),
546 TextLayoutOptions::default(),
547 );
548 let mut context = BasicModifierNodeContext::new();
549 context.set_node_id(Some(88));
550 node.on_attach(&mut context);
551
552 let measured = node.measure_text_content(Some(120.0));
553 let prepared = node.prepared_layout_handle().prepare(Some(120.0));
554 tx.send((
555 recorded.borrow().clone(),
556 measured.width,
557 measured.height,
558 prepared.metrics.width,
559 prepared.metrics.height,
560 ))
561 .expect("send cached layout result");
562 });
563 });
564
565 let (recorded, measured_width, measured_height, prepared_width, prepared_height) =
566 rx.recv().expect("receive cached layout result");
567 assert_eq!(recorded, vec![Some(88)]);
568 assert_eq!(measured_width, prepared_width);
569 assert_eq!(measured_height, prepared_height);
570 }
571
572 #[test]
573 fn semantics_uses_source_text_for_scaled_overflow() {
574 let node = TextModifierNode::new(
575 Rc::new(AnnotatedString::from("Save Cranpose WebP")),
576 TextStyle::default(),
577 TextLayoutOptions {
578 overflow: crate::text::TextOverflow::ScaleDown {
579 min_font_size_sp: 9.0,
580 },
581 soft_wrap: false,
582 max_lines: 1,
583 min_lines: 1,
584 },
585 );
586 let mut config = SemanticsConfiguration::default();
587
588 node.merge_semantics(&mut config);
589
590 assert_eq!(
591 config.content_description.as_deref(),
592 Some("Save Cranpose WebP")
593 );
594 }
595}