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