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