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 text_generation: u64,
53 layout: crate::text::PreparedTextLayout,
54}
55
56#[derive(Debug)]
57struct TextPreparedLayoutOwner {
58 text: Rc<AnnotatedString>,
59 style: TextStyle,
60 options: TextLayoutOptions,
61 node_id: Cell<Option<cranpose_core::NodeId>>,
62 cache: RefCell<Vec<TextPreparedLayoutCacheEntry>>,
63}
64
65#[derive(Clone, Debug)]
66pub(crate) struct TextPreparedLayoutHandle {
67 owner: Rc<TextPreparedLayoutOwner>,
68}
69
70impl TextPreparedLayoutOwner {
71 fn new(
72 text: Rc<AnnotatedString>,
73 style: TextStyle,
74 options: TextLayoutOptions,
75 node_id: Option<cranpose_core::NodeId>,
76 ) -> Self {
77 Self {
78 text,
79 style,
80 options: options.normalized(),
81 node_id: Cell::new(node_id),
82 cache: RefCell::new(Vec::new()),
83 }
84 }
85
86 fn text(&self) -> &str {
87 self.text.text.as_str()
88 }
89
90 fn annotated_text(&self) -> Rc<AnnotatedString> {
91 self.text.clone()
92 }
93
94 fn annotated_string(&self) -> AnnotatedString {
95 (*self.text).clone()
96 }
97
98 fn style(&self) -> &TextStyle {
99 &self.style
100 }
101
102 fn options(&self) -> TextLayoutOptions {
103 self.options
104 }
105
106 fn node_id(&self) -> Option<cranpose_core::NodeId> {
107 self.node_id.get()
108 }
109
110 fn set_node_id(&self, node_id: Option<cranpose_core::NodeId>) {
111 if self.node_id.replace(node_id) != node_id {
112 self.cache.borrow_mut().clear();
113 }
114 }
115
116 fn prepare(&self, max_width: Option<f32>) -> crate::text::PreparedTextLayout {
117 let normalized_max_width = max_width.filter(|width| width.is_finite() && *width > 0.0);
118 let max_width_bits = normalized_max_width.map(f32::to_bits);
119 let text_generation = crate::text::measure::current_text_generation();
120
121 {
122 let mut cache = self.cache.borrow_mut();
123 if let Some(index) = cache.iter().position(|entry| {
124 entry.max_width_bits == max_width_bits && entry.text_generation == text_generation
125 }) {
126 let entry = cache.remove(index);
127 let prepared = entry.layout.clone();
128 cache.insert(0, entry);
129 return prepared;
130 }
131 }
132
133 let prepared = crate::text::prepare_text_layout_for_node(
134 self.node_id(),
135 self.text.as_ref(),
136 &self.style,
137 self.options,
138 normalized_max_width,
139 );
140
141 let mut cache = self.cache.borrow_mut();
142 cache.insert(
143 0,
144 TextPreparedLayoutCacheEntry {
145 max_width_bits,
146 text_generation,
147 layout: prepared.clone(),
148 },
149 );
150 cache.truncate(PREPARED_LAYOUT_CACHE_CAPACITY);
151 prepared
152 }
153
154 fn measure_text_content(&self, max_width: Option<f32>) -> Size {
155 let prepared = self.prepare(max_width);
156 Size {
157 width: prepared.metrics.width,
158 height: prepared.metrics.height,
159 }
160 }
161}
162
163impl TextPreparedLayoutHandle {
164 fn new(owner: Rc<TextPreparedLayoutOwner>) -> Self {
165 Self { owner }
166 }
167
168 pub(crate) fn prepare(&self, max_width: Option<f32>) -> crate::text::PreparedTextLayout {
169 self.owner.prepare(max_width)
170 }
171}
172
173impl TextModifierNode {
174 pub fn new(text: Rc<AnnotatedString>, style: TextStyle, options: TextLayoutOptions) -> Self {
175 Self {
176 layout: Rc::new(TextPreparedLayoutOwner::new(text, style, options, None)),
177 state: NodeState::new(),
178 }
179 }
180
181 pub fn text(&self) -> &str {
182 self.layout.text()
183 }
184
185 pub fn annotated_text(&self) -> Rc<AnnotatedString> {
186 self.layout.annotated_text()
187 }
188
189 pub fn annotated_string(&self) -> AnnotatedString {
190 self.layout.annotated_string()
191 }
192
193 pub fn style(&self) -> &TextStyle {
194 self.layout.style()
195 }
196
197 pub fn options(&self) -> TextLayoutOptions {
198 self.layout.options()
199 }
200
201 fn measure_text_content(&self, max_width: Option<f32>) -> Size {
202 self.layout.measure_text_content(max_width)
203 }
204
205 pub(crate) fn prepared_layout_handle(&self) -> TextPreparedLayoutHandle {
206 TextPreparedLayoutHandle::new(self.layout.clone())
207 }
208}
209
210impl DelegatableNode for TextModifierNode {
211 fn node_state(&self) -> &NodeState {
212 &self.state
213 }
214}
215
216impl ModifierNode for TextModifierNode {
217 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
218 self.layout.set_node_id(context.node_id());
219 context.invalidate(InvalidationKind::Layout);
221 context.invalidate(InvalidationKind::Draw);
222 context.invalidate(InvalidationKind::Semantics);
223 }
224
225 fn on_detach(&mut self) {
226 self.layout.set_node_id(None);
227 }
228
229 fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
230 Some(self)
231 }
232
233 fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
234 Some(self)
235 }
236
237 fn as_semantics_node(&self) -> Option<&dyn SemanticsNode> {
238 Some(self)
239 }
240
241 fn as_semantics_node_mut(&mut self) -> Option<&mut dyn SemanticsNode> {
242 Some(self)
243 }
244
245 fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
246 Some(self)
247 }
248
249 fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
250 Some(self)
251 }
252}
253
254impl LayoutModifierNode for TextModifierNode {
255 fn measure(
256 &self,
257 _context: &mut dyn ModifierNodeContext,
258 _measurable: &dyn Measurable,
259 constraints: Constraints,
260 ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
261 let max_width = constraints
263 .max_width
264 .is_finite()
265 .then_some(constraints.max_width);
266 let text_size = self.measure_text_content(max_width);
267
268 let width = text_size
270 .width
271 .clamp(constraints.min_width, constraints.max_width);
272 let height = text_size
273 .height
274 .clamp(constraints.min_height, constraints.max_height);
275
276 cranpose_ui_layout::LayoutModifierMeasureResult::with_size(Size { width, height })
280 }
281
282 fn min_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
283 self.measure_text_content(None).width
284 }
285
286 fn max_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
287 self.measure_text_content(None).width
288 }
289
290 fn min_intrinsic_height(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
291 self.measure_text_content(Some(_width).filter(|w| w.is_finite() && *w > 0.0))
292 .height
293 }
294
295 fn max_intrinsic_height(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
296 self.measure_text_content(Some(_width).filter(|w| w.is_finite() && *w > 0.0))
297 .height
298 }
299}
300
301impl DrawModifierNode for TextModifierNode {
302 fn draw(&self, _draw_scope: &mut dyn DrawScope) {
303 }
306}
307
308impl SemanticsNode for TextModifierNode {
309 fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
310 config.content_description = Some(self.text().to_string());
312 }
313}
314
315#[derive(Debug, Clone, PartialEq)]
324pub struct TextModifierElement {
325 text: Rc<AnnotatedString>,
326 style: TextStyle,
327 options: TextLayoutOptions,
328}
329
330impl TextModifierElement {
331 pub fn new(text: Rc<AnnotatedString>, style: TextStyle, options: TextLayoutOptions) -> Self {
332 Self {
333 text,
334 style,
335 options: options.normalized(),
336 }
337 }
338}
339
340impl Hash for TextModifierElement {
341 fn hash<H: Hasher>(&self, state: &mut H) {
342 self.text.render_hash().hash(state);
343 self.style.render_hash().hash(state);
344 self.options.hash(state);
345 }
346}
347
348impl ModifierNodeElement for TextModifierElement {
349 type Node = TextModifierNode;
350
351 fn create(&self) -> Self::Node {
352 TextModifierNode::new(self.text.clone(), self.style.clone(), self.options)
353 }
354
355 fn update(&self, node: &mut Self::Node) {
356 let current = node.layout.as_ref();
357 if current.text != self.text
358 || current.style != self.style
359 || current.options != self.options
360 {
361 node.layout = Rc::new(TextPreparedLayoutOwner::new(
362 self.text.clone(),
363 self.style.clone(),
364 self.options,
365 current.node_id(),
366 ));
367 }
368 }
369
370 fn capabilities(&self) -> NodeCapabilities {
371 NodeCapabilities::LAYOUT | NodeCapabilities::DRAW | NodeCapabilities::SEMANTICS
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379 use crate::text::TextUnit;
380 use crate::text_layout_result::TextLayoutResult;
381 use cranpose_core::NodeId;
382 use cranpose_foundation::BasicModifierNodeContext;
383 use std::collections::hash_map::DefaultHasher;
384 use std::sync::mpsc;
385
386 fn hash_of(element: &TextModifierElement) -> u64 {
387 let mut hasher = DefaultHasher::new();
388 element.hash(&mut hasher);
389 hasher.finish()
390 }
391
392 struct RecordingPreparedLayoutMeasurer {
393 recorded: std::rc::Rc<std::cell::RefCell<Vec<Option<NodeId>>>>,
394 }
395
396 impl crate::text::TextMeasurer for RecordingPreparedLayoutMeasurer {
397 fn measure(
398 &self,
399 _text: &crate::text::AnnotatedString,
400 _style: &TextStyle,
401 ) -> crate::text::TextMetrics {
402 crate::text::TextMetrics {
403 width: 12.0,
404 height: 18.0,
405 line_height: 18.0,
406 line_count: 1,
407 }
408 }
409
410 fn prepare_with_options_for_node(
411 &self,
412 node_id: Option<NodeId>,
413 text: &crate::text::AnnotatedString,
414 _style: &TextStyle,
415 _options: TextLayoutOptions,
416 _max_width: Option<f32>,
417 ) -> crate::text::PreparedTextLayout {
418 self.recorded.borrow_mut().push(node_id);
419 crate::text::PreparedTextLayout {
420 text: text.clone(),
421 visual_style: TextStyle::default(),
422 metrics: crate::text::TextMetrics {
423 width: 12.0,
424 height: 18.0,
425 line_height: 18.0,
426 line_count: 1,
427 },
428 did_overflow: false,
429 }
430 }
431
432 fn get_offset_for_position(
433 &self,
434 _text: &crate::text::AnnotatedString,
435 _style: &TextStyle,
436 _x: f32,
437 _y: f32,
438 ) -> usize {
439 0
440 }
441
442 fn get_cursor_x_for_offset(
443 &self,
444 _text: &crate::text::AnnotatedString,
445 _style: &TextStyle,
446 _offset: usize,
447 ) -> f32 {
448 0.0
449 }
450
451 fn layout(
452 &self,
453 _text: &crate::text::AnnotatedString,
454 _style: &TextStyle,
455 ) -> TextLayoutResult {
456 panic!("layout is not used in this test");
457 }
458 }
459
460 struct FixedPreparedLayoutMeasurer {
461 height: f32,
462 line_height: f32,
463 }
464
465 impl crate::text::TextMeasurer for FixedPreparedLayoutMeasurer {
466 fn measure(
467 &self,
468 _text: &crate::text::AnnotatedString,
469 _style: &TextStyle,
470 ) -> crate::text::TextMetrics {
471 crate::text::TextMetrics {
472 width: 24.0,
473 height: self.height,
474 line_height: self.line_height,
475 line_count: (self.height / self.line_height).round().max(1.0) as usize,
476 }
477 }
478
479 fn prepare_with_options_for_node(
480 &self,
481 _node_id: Option<NodeId>,
482 text: &crate::text::AnnotatedString,
483 _style: &TextStyle,
484 _options: TextLayoutOptions,
485 _max_width: Option<f32>,
486 ) -> crate::text::PreparedTextLayout {
487 crate::text::PreparedTextLayout {
488 text: text.clone(),
489 visual_style: TextStyle::default(),
490 metrics: crate::text::TextMetrics {
491 width: 24.0,
492 height: self.height,
493 line_height: self.line_height,
494 line_count: (self.height / self.line_height).round().max(1.0) as usize,
495 },
496 did_overflow: false,
497 }
498 }
499
500 fn get_offset_for_position(
501 &self,
502 _text: &crate::text::AnnotatedString,
503 _style: &TextStyle,
504 _x: f32,
505 _y: f32,
506 ) -> usize {
507 0
508 }
509
510 fn get_cursor_x_for_offset(
511 &self,
512 _text: &crate::text::AnnotatedString,
513 _style: &TextStyle,
514 _offset: usize,
515 ) -> f32 {
516 0.0
517 }
518
519 fn layout(
520 &self,
521 _text: &crate::text::AnnotatedString,
522 _style: &TextStyle,
523 ) -> TextLayoutResult {
524 panic!("layout is not used in this test");
525 }
526 }
527
528 #[test]
529 fn hash_changes_when_style_changes() {
530 let text = Rc::new(AnnotatedString::from("Hello"));
531 let element_a = TextModifierElement::new(
532 text.clone(),
533 TextStyle::default(),
534 TextLayoutOptions::default(),
535 );
536 let style_b = TextStyle {
537 span_style: crate::text::SpanStyle {
538 font_size: TextUnit::Sp(18.0),
539 ..Default::default()
540 },
541 ..Default::default()
542 };
543 let element_b = TextModifierElement::new(text, style_b, TextLayoutOptions::default());
544
545 assert_ne!(element_a, element_b);
546 assert_ne!(hash_of(&element_a), hash_of(&element_b));
547 }
548
549 #[test]
550 fn hash_matches_for_equal_elements() {
551 let style = TextStyle {
552 span_style: crate::text::SpanStyle {
553 font_size: TextUnit::Sp(14.0),
554 letter_spacing: TextUnit::Em(0.1),
555 ..Default::default()
556 },
557 ..Default::default()
558 };
559 let options = TextLayoutOptions::default();
560 let text = Rc::new(AnnotatedString::from("Hash me"));
561 let element_a = TextModifierElement::new(text.clone(), style.clone(), options);
562 let element_b = TextModifierElement::new(text, style, options);
563
564 assert_eq!(element_a, element_b);
565 assert_eq!(hash_of(&element_a), hash_of(&element_b));
566 }
567
568 #[test]
569 fn measure_uses_attached_node_identity() {
570 let (tx, rx) = mpsc::channel();
571
572 std::thread::spawn(move || {
573 let recorded = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
574 let app_context = crate::AppContext::new();
575 app_context.enter(|| {
576 crate::text::set_text_measurer(RecordingPreparedLayoutMeasurer {
577 recorded: recorded.clone(),
578 });
579
580 let mut node = TextModifierNode::new(
581 Rc::new(AnnotatedString::from("identity")),
582 TextStyle::default(),
583 TextLayoutOptions::default(),
584 );
585 let mut context = BasicModifierNodeContext::new();
586 context.set_node_id(Some(77));
587 node.on_attach(&mut context);
588
589 let size = node.measure_text_content(Some(96.0));
590 tx.send((recorded.borrow().clone(), size.width, size.height))
591 .expect("send measurement result");
592 });
593 });
594
595 let (recorded, width, height) = rx.recv().expect("receive measurement result");
596 assert_eq!(recorded, vec![Some(77)]);
597 assert_eq!(width, 12.0);
598 assert_eq!(height, 18.0);
599 }
600
601 #[test]
602 fn prepared_layout_cache_reuses_node_snapshot() {
603 let (tx, rx) = mpsc::channel();
604
605 std::thread::spawn(move || {
606 let recorded = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
607 let app_context = crate::AppContext::new();
608 app_context.enter(|| {
609 crate::text::set_text_measurer(RecordingPreparedLayoutMeasurer {
610 recorded: recorded.clone(),
611 });
612
613 let mut node = TextModifierNode::new(
614 Rc::new(AnnotatedString::from("reuse")),
615 TextStyle::default(),
616 TextLayoutOptions::default(),
617 );
618 let mut context = BasicModifierNodeContext::new();
619 context.set_node_id(Some(88));
620 node.on_attach(&mut context);
621
622 let measured = node.measure_text_content(Some(120.0));
623 let prepared = node.prepared_layout_handle().prepare(Some(120.0));
624 tx.send((
625 recorded.borrow().clone(),
626 measured.width,
627 measured.height,
628 prepared.metrics.width,
629 prepared.metrics.height,
630 ))
631 .expect("send cached layout result");
632 });
633 });
634
635 let (recorded, measured_width, measured_height, prepared_width, prepared_height) =
636 rx.recv().expect("receive cached layout result");
637 assert_eq!(recorded, vec![Some(88)]);
638 assert_eq!(measured_width, prepared_width);
639 assert_eq!(measured_height, prepared_height);
640 }
641
642 #[test]
643 fn prepared_layout_cache_refreshes_when_text_service_changes() {
644 let (tx, rx) = mpsc::channel();
645
646 std::thread::spawn(move || {
647 let app_context = crate::AppContext::new();
648 app_context.enter(|| {
649 crate::text::set_text_measurer(FixedPreparedLayoutMeasurer {
650 height: 30.0,
651 line_height: 10.0,
652 });
653
654 let node = TextModifierNode::new(
655 Rc::new(AnnotatedString::from("a\nb\nc")),
656 TextStyle::default(),
657 TextLayoutOptions::default(),
658 );
659
660 let first = node.measure_text_content(Some(160.0));
661 crate::text::set_text_measurer(FixedPreparedLayoutMeasurer {
662 height: 60.0,
663 line_height: 20.0,
664 });
665 let second = node.measure_text_content(Some(160.0));
666 tx.send((first.height, second.height))
667 .expect("send measurement result");
668 });
669 });
670
671 let (first_height, second_height) = rx.recv().expect("receive measurement result");
672 assert_eq!(first_height, 30.0);
673 assert_eq!(second_height, 60.0);
674 }
675
676 #[test]
677 fn semantics_uses_source_text_for_scaled_overflow() {
678 let node = TextModifierNode::new(
679 Rc::new(AnnotatedString::from("Save Cranpose WebP")),
680 TextStyle::default(),
681 TextLayoutOptions {
682 overflow: crate::text::TextOverflow::ScaleDown {
683 min_font_size_sp: 9.0,
684 },
685 soft_wrap: false,
686 max_lines: 1,
687 min_lines: 1,
688 },
689 );
690 let mut config = SemanticsConfiguration::default();
691
692 node.merge_semantics(&mut config);
693
694 assert_eq!(
695 config.content_description.as_deref(),
696 Some("Save Cranpose WebP")
697 );
698 }
699}