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