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