1use crate::text::{AnnotatedString, TextLayoutOptions, TextStyle};
23use cranpose_foundation::{
24 Constraints, DelegatableNode, DrawModifierNode, DrawScope, InvalidationKind,
25 LayoutModifierNode, Measurable, MeasurementProxy, ModifierNode, ModifierNodeContext,
26 ModifierNodeElement, 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 layout: crate::text::PreparedTextLayout,
53}
54
55#[derive(Debug)]
56struct TextPreparedLayoutOwner {
57 text: Rc<AnnotatedString>,
58 style: TextStyle,
59 options: TextLayoutOptions,
60 node_id: Cell<Option<cranpose_core::NodeId>>,
61 cache: RefCell<Vec<TextPreparedLayoutCacheEntry>>,
62}
63
64#[derive(Clone, Debug)]
65pub(crate) struct TextPreparedLayoutHandle {
66 owner: Rc<TextPreparedLayoutOwner>,
67}
68
69impl TextPreparedLayoutOwner {
70 fn new(
71 text: Rc<AnnotatedString>,
72 style: TextStyle,
73 options: TextLayoutOptions,
74 node_id: Option<cranpose_core::NodeId>,
75 ) -> Self {
76 Self {
77 text,
78 style,
79 options: options.normalized(),
80 node_id: Cell::new(node_id),
81 cache: RefCell::new(Vec::new()),
82 }
83 }
84
85 fn text(&self) -> &str {
86 self.text.text.as_str()
87 }
88
89 fn annotated_text(&self) -> Rc<AnnotatedString> {
90 self.text.clone()
91 }
92
93 fn annotated_string(&self) -> AnnotatedString {
94 (*self.text).clone()
95 }
96
97 fn style(&self) -> &TextStyle {
98 &self.style
99 }
100
101 fn options(&self) -> TextLayoutOptions {
102 self.options
103 }
104
105 fn node_id(&self) -> Option<cranpose_core::NodeId> {
106 self.node_id.get()
107 }
108
109 fn set_node_id(&self, node_id: Option<cranpose_core::NodeId>) {
110 if self.node_id.replace(node_id) != node_id {
111 self.cache.borrow_mut().clear();
112 }
113 }
114
115 fn prepare(&self, max_width: Option<f32>) -> crate::text::PreparedTextLayout {
116 let normalized_max_width = max_width.filter(|width| width.is_finite() && *width > 0.0);
117 let max_width_bits = normalized_max_width.map(f32::to_bits);
118
119 {
120 let mut cache = self.cache.borrow_mut();
121 if let Some(index) = cache
122 .iter()
123 .position(|entry| entry.max_width_bits == max_width_bits)
124 {
125 let entry = cache.remove(index);
126 let prepared = entry.layout.clone();
127 cache.insert(0, entry);
128 return prepared;
129 }
130 }
131
132 let prepared = crate::text::prepare_text_layout_for_node(
133 self.node_id(),
134 self.text.as_ref(),
135 &self.style,
136 self.options,
137 normalized_max_width,
138 );
139
140 let mut cache = self.cache.borrow_mut();
141 cache.insert(
142 0,
143 TextPreparedLayoutCacheEntry {
144 max_width_bits,
145 layout: prepared.clone(),
146 },
147 );
148 cache.truncate(PREPARED_LAYOUT_CACHE_CAPACITY);
149 prepared
150 }
151
152 fn measure_text_content(&self, max_width: Option<f32>) -> Size {
153 let prepared = self.prepare(max_width);
154 Size {
155 width: prepared.metrics.width,
156 height: prepared.metrics.height,
157 }
158 }
159}
160
161impl TextPreparedLayoutHandle {
162 fn new(owner: Rc<TextPreparedLayoutOwner>) -> Self {
163 Self { owner }
164 }
165
166 pub(crate) fn prepare(&self, max_width: Option<f32>) -> crate::text::PreparedTextLayout {
167 self.owner.prepare(max_width)
168 }
169
170 fn measure_text_content(&self, max_width: Option<f32>) -> Size {
171 self.owner.measure_text_content(max_width)
172 }
173}
174
175impl TextModifierNode {
176 pub fn new(text: Rc<AnnotatedString>, style: TextStyle, options: TextLayoutOptions) -> Self {
177 Self {
178 layout: Rc::new(TextPreparedLayoutOwner::new(text, style, options, None)),
179 state: NodeState::new(),
180 }
181 }
182
183 pub fn text(&self) -> &str {
184 self.layout.text()
185 }
186
187 pub fn annotated_text(&self) -> Rc<AnnotatedString> {
188 self.layout.annotated_text()
189 }
190
191 pub fn annotated_string(&self) -> AnnotatedString {
192 self.layout.annotated_string()
193 }
194
195 pub fn style(&self) -> &TextStyle {
196 self.layout.style()
197 }
198
199 pub fn options(&self) -> TextLayoutOptions {
200 self.layout.options()
201 }
202
203 fn measure_text_content(&self, max_width: Option<f32>) -> Size {
204 self.layout.measure_text_content(max_width)
205 }
206
207 pub(crate) fn prepared_layout_handle(&self) -> TextPreparedLayoutHandle {
208 TextPreparedLayoutHandle::new(self.layout.clone())
209 }
210}
211
212impl DelegatableNode for TextModifierNode {
213 fn node_state(&self) -> &NodeState {
214 &self.state
215 }
216}
217
218impl ModifierNode for TextModifierNode {
219 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
220 self.layout.set_node_id(context.node_id());
221 context.invalidate(InvalidationKind::Layout);
223 context.invalidate(InvalidationKind::Draw);
224 context.invalidate(InvalidationKind::Semantics);
225 }
226
227 fn on_detach(&mut self) {
228 self.layout.set_node_id(None);
229 }
230
231 fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
232 Some(self)
233 }
234
235 fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
236 Some(self)
237 }
238
239 fn as_semantics_node(&self) -> Option<&dyn SemanticsNode> {
240 Some(self)
241 }
242
243 fn as_semantics_node_mut(&mut self) -> Option<&mut dyn SemanticsNode> {
244 Some(self)
245 }
246
247 fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
248 Some(self)
249 }
250
251 fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
252 Some(self)
253 }
254}
255
256impl LayoutModifierNode for TextModifierNode {
257 fn measure(
258 &self,
259 _context: &mut dyn ModifierNodeContext,
260 _measurable: &dyn Measurable,
261 constraints: Constraints,
262 ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
263 let max_width = constraints
265 .max_width
266 .is_finite()
267 .then_some(constraints.max_width);
268 let text_size = self.measure_text_content(max_width);
269
270 let width = text_size
272 .width
273 .clamp(constraints.min_width, constraints.max_width);
274 let height = text_size
275 .height
276 .clamp(constraints.min_height, constraints.max_height);
277
278 cranpose_ui_layout::LayoutModifierMeasureResult::with_size(Size { width, height })
282 }
283
284 fn min_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
285 self.measure_text_content(None).width
286 }
287
288 fn max_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
289 self.measure_text_content(None).width
290 }
291
292 fn min_intrinsic_height(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
293 self.measure_text_content(Some(_width).filter(|w| w.is_finite() && *w > 0.0))
294 .height
295 }
296
297 fn max_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 create_measurement_proxy(&self) -> Option<Box<dyn MeasurementProxy>> {
303 Some(Box::new(TextMeasurementProxy {
304 layout: self.prepared_layout_handle(),
305 }))
306 }
307}
308
309struct TextMeasurementProxy {
314 layout: TextPreparedLayoutHandle,
315}
316
317impl TextMeasurementProxy {
318 fn measure_text_content(&self, max_width: Option<f32>) -> Size {
321 self.layout.measure_text_content(max_width)
322 }
323}
324
325impl MeasurementProxy for TextMeasurementProxy {
326 fn measure_proxy(
327 &self,
328 _context: &mut dyn ModifierNodeContext,
329 _measurable: &dyn Measurable,
330 constraints: Constraints,
331 ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
332 let max_width = constraints
334 .max_width
335 .is_finite()
336 .then_some(constraints.max_width);
337 let text_size = self.measure_text_content(max_width);
338
339 let width = text_size
341 .width
342 .clamp(constraints.min_width, constraints.max_width);
343 let height = text_size
344 .height
345 .clamp(constraints.min_height, constraints.max_height);
346
347 cranpose_ui_layout::LayoutModifierMeasureResult::with_size(Size { width, height })
349 }
350
351 fn min_intrinsic_width_proxy(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
352 self.measure_text_content(None).width
353 }
354
355 fn max_intrinsic_width_proxy(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
356 self.measure_text_content(None).width
357 }
358
359 fn min_intrinsic_height_proxy(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
360 self.measure_text_content(Some(_width).filter(|w| w.is_finite() && *w > 0.0))
361 .height
362 }
363
364 fn max_intrinsic_height_proxy(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
365 self.measure_text_content(Some(_width).filter(|w| w.is_finite() && *w > 0.0))
366 .height
367 }
368}
369
370impl DrawModifierNode for TextModifierNode {
371 fn draw(&self, _draw_scope: &mut dyn DrawScope) {
372 }
381}
382
383impl SemanticsNode for TextModifierNode {
384 fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
385 config.content_description = Some(self.text().to_string());
387 }
388}
389
390#[derive(Debug, Clone, PartialEq)]
399pub struct TextModifierElement {
400 text: Rc<AnnotatedString>,
401 style: TextStyle,
402 options: TextLayoutOptions,
403}
404
405impl TextModifierElement {
406 pub fn new(text: Rc<AnnotatedString>, style: TextStyle, options: TextLayoutOptions) -> Self {
407 Self {
408 text,
409 style,
410 options: options.normalized(),
411 }
412 }
413}
414
415fn hash_f32_bits<H: Hasher>(value: f32, state: &mut H) {
416 value.to_bits().hash(state);
417}
418
419fn hash_text_unit<H: Hasher>(unit: crate::text::TextUnit, state: &mut H) {
420 match unit {
421 crate::text::TextUnit::Unspecified => 0u8.hash(state),
422 crate::text::TextUnit::Sp(value) => {
423 1u8.hash(state);
424 hash_f32_bits(value, state);
425 }
426 crate::text::TextUnit::Em(value) => {
427 2u8.hash(state);
428 hash_f32_bits(value, state);
429 }
430 }
431}
432
433fn hash_color<H: Hasher>(color: crate::modifier::Color, state: &mut H) {
434 hash_f32_bits(color.0, state);
435 hash_f32_bits(color.1, state);
436 hash_f32_bits(color.2, state);
437 hash_f32_bits(color.3, state);
438}
439
440fn hash_option_color<H: Hasher>(color: &Option<crate::modifier::Color>, state: &mut H) {
441 match color {
442 Some(color) => {
443 1u8.hash(state);
444 hash_color(*color, state);
445 }
446 None => 0u8.hash(state),
447 }
448}
449
450fn hash_brush<H: Hasher>(brush: &crate::modifier::Brush, state: &mut H) {
451 match brush {
452 crate::modifier::Brush::Solid(color) => {
453 0u8.hash(state);
454 hash_color(*color, state);
455 }
456 crate::modifier::Brush::LinearGradient {
457 colors,
458 stops,
459 start,
460 end,
461 tile_mode,
462 } => {
463 1u8.hash(state);
464 colors.len().hash(state);
465 for color in colors {
466 hash_color(*color, state);
467 }
468 match stops {
469 Some(stops) => {
470 1u8.hash(state);
471 stops.len().hash(state);
472 for stop in stops {
473 hash_f32_bits(*stop, state);
474 }
475 }
476 None => 0u8.hash(state),
477 }
478 hash_f32_bits(start.x, state);
479 hash_f32_bits(start.y, state);
480 hash_f32_bits(end.x, state);
481 hash_f32_bits(end.y, state);
482 tile_mode.hash(state);
483 }
484 crate::modifier::Brush::RadialGradient {
485 colors,
486 stops,
487 center,
488 radius,
489 tile_mode,
490 } => {
491 2u8.hash(state);
492 colors.len().hash(state);
493 for color in colors {
494 hash_color(*color, state);
495 }
496 match stops {
497 Some(stops) => {
498 1u8.hash(state);
499 stops.len().hash(state);
500 for stop in stops {
501 hash_f32_bits(*stop, state);
502 }
503 }
504 None => 0u8.hash(state),
505 }
506 hash_f32_bits(center.x, state);
507 hash_f32_bits(center.y, state);
508 hash_f32_bits(*radius, state);
509 tile_mode.hash(state);
510 }
511 crate::modifier::Brush::SweepGradient {
512 colors,
513 stops,
514 center,
515 } => {
516 3u8.hash(state);
517 colors.len().hash(state);
518 for color in colors {
519 hash_color(*color, state);
520 }
521 match stops {
522 Some(stops) => {
523 1u8.hash(state);
524 stops.len().hash(state);
525 for stop in stops {
526 hash_f32_bits(*stop, state);
527 }
528 }
529 None => 0u8.hash(state),
530 }
531 hash_f32_bits(center.x, state);
532 hash_f32_bits(center.y, state);
533 }
534 }
535}
536
537fn hash_option_brush<H: Hasher>(brush: &Option<crate::modifier::Brush>, state: &mut H) {
538 match brush {
539 Some(brush) => {
540 1u8.hash(state);
541 hash_brush(brush, state);
542 }
543 None => 0u8.hash(state),
544 }
545}
546
547fn hash_option_alpha<H: Hasher>(alpha: &Option<f32>, state: &mut H) {
548 match alpha {
549 Some(alpha) => {
550 1u8.hash(state);
551 hash_f32_bits(*alpha, state);
552 }
553 None => 0u8.hash(state),
554 }
555}
556
557fn hash_option_baseline_shift<H: Hasher>(
558 baseline_shift: &Option<crate::text::BaselineShift>,
559 state: &mut H,
560) {
561 match baseline_shift {
562 Some(shift) => {
563 1u8.hash(state);
564 hash_f32_bits(shift.0, state);
565 }
566 None => 0u8.hash(state),
567 }
568}
569
570fn hash_option_text_geometric_transform<H: Hasher>(
571 transform: &Option<crate::text::TextGeometricTransform>,
572 state: &mut H,
573) {
574 match transform {
575 Some(transform) => {
576 1u8.hash(state);
577 hash_f32_bits(transform.scale_x, state);
578 hash_f32_bits(transform.skew_x, state);
579 }
580 None => 0u8.hash(state),
581 }
582}
583
584fn hash_option_shadow<H: Hasher>(shadow: &Option<crate::text::Shadow>, state: &mut H) {
585 match shadow {
586 Some(shadow) => {
587 1u8.hash(state);
588 hash_color(shadow.color, state);
589 hash_f32_bits(shadow.offset.x, state);
590 hash_f32_bits(shadow.offset.y, state);
591 hash_f32_bits(shadow.blur_radius, state);
592 }
593 None => 0u8.hash(state),
594 }
595}
596
597fn hash_option_text_indent<H: Hasher>(indent: &Option<crate::text::TextIndent>, state: &mut H) {
598 match indent {
599 Some(indent) => {
600 1u8.hash(state);
601 hash_text_unit(indent.first_line, state);
602 hash_text_unit(indent.rest_line, state);
603 }
604 None => 0u8.hash(state),
605 }
606}
607
608fn hash_option_text_draw_style<H: Hasher>(
609 draw_style: &Option<crate::text::TextDrawStyle>,
610 state: &mut H,
611) {
612 match draw_style {
613 Some(crate::text::TextDrawStyle::Fill) => {
614 1u8.hash(state);
615 0u8.hash(state);
616 }
617 Some(crate::text::TextDrawStyle::Stroke { width }) => {
618 1u8.hash(state);
619 1u8.hash(state);
620 hash_f32_bits(*width, state);
621 }
622 None => 0u8.hash(state),
623 }
624}
625
626fn hash_text_style<H: Hasher>(style: &TextStyle, state: &mut H) {
627 let span = &style.span_style;
628 let paragraph = &style.paragraph_style;
629
630 hash_option_color(&span.color, state);
631 hash_option_brush(&span.brush, state);
632 hash_option_alpha(&span.alpha, state);
633 hash_text_unit(span.font_size, state);
634 span.font_weight.hash(state);
635 span.font_style.hash(state);
636 span.font_synthesis.hash(state);
637 span.font_family.hash(state);
638 span.font_feature_settings.hash(state);
639 hash_text_unit(span.letter_spacing, state);
640 hash_option_baseline_shift(&span.baseline_shift, state);
641 hash_option_text_geometric_transform(&span.text_geometric_transform, state);
642 span.locale_list.hash(state);
643 hash_option_color(&span.background, state);
644 span.text_decoration.hash(state);
645 hash_option_shadow(&span.shadow, state);
646 span.platform_style.hash(state);
647 hash_option_text_draw_style(&span.draw_style, state);
648
649 paragraph.text_align.hash(state);
650 paragraph.text_direction.hash(state);
651 hash_text_unit(paragraph.line_height, state);
652 hash_option_text_indent(¶graph.text_indent, state);
653 paragraph.platform_style.hash(state);
654 paragraph.line_height_style.hash(state);
655 paragraph.line_break.hash(state);
656 paragraph.hyphens.hash(state);
657 paragraph.text_motion.hash(state);
658}
659
660impl Hash for TextModifierElement {
661 fn hash<H: Hasher>(&self, state: &mut H) {
662 self.text.text.hash(state);
663 hash_text_style(&self.style, state);
664 self.options.hash(state);
665 }
666}
667
668impl ModifierNodeElement for TextModifierElement {
669 type Node = TextModifierNode;
670
671 fn create(&self) -> Self::Node {
672 TextModifierNode::new(self.text.clone(), self.style.clone(), self.options)
673 }
674
675 fn update(&self, node: &mut Self::Node) {
676 let current = node.layout.as_ref();
677 if current.text != self.text
678 || current.style != self.style
679 || current.options != self.options
680 {
681 node.layout = Rc::new(TextPreparedLayoutOwner::new(
682 self.text.clone(),
683 self.style.clone(),
684 self.options,
685 current.node_id(),
686 ));
687 }
693 }
694
695 fn capabilities(&self) -> NodeCapabilities {
696 NodeCapabilities::LAYOUT | NodeCapabilities::DRAW | NodeCapabilities::SEMANTICS
698 }
699}
700
701#[cfg(test)]
702mod tests {
703 use super::*;
704 use crate::text::TextUnit;
705 use crate::text_layout_result::TextLayoutResult;
706 use cranpose_core::NodeId;
707 use cranpose_foundation::BasicModifierNodeContext;
708 use std::collections::hash_map::DefaultHasher;
709 use std::sync::mpsc;
710
711 fn hash_of(element: &TextModifierElement) -> u64 {
712 let mut hasher = DefaultHasher::new();
713 element.hash(&mut hasher);
714 hasher.finish()
715 }
716
717 struct RecordingPreparedLayoutMeasurer {
718 recorded: std::rc::Rc<std::cell::RefCell<Vec<Option<NodeId>>>>,
719 }
720
721 impl crate::text::TextMeasurer for RecordingPreparedLayoutMeasurer {
722 fn measure(
723 &self,
724 _text: &crate::text::AnnotatedString,
725 _style: &TextStyle,
726 ) -> crate::text::TextMetrics {
727 crate::text::TextMetrics {
728 width: 12.0,
729 height: 18.0,
730 line_height: 18.0,
731 line_count: 1,
732 }
733 }
734
735 fn prepare_with_options_for_node(
736 &self,
737 node_id: Option<NodeId>,
738 text: &crate::text::AnnotatedString,
739 _style: &TextStyle,
740 _options: TextLayoutOptions,
741 _max_width: Option<f32>,
742 ) -> crate::text::PreparedTextLayout {
743 self.recorded.borrow_mut().push(node_id);
744 crate::text::PreparedTextLayout {
745 text: text.clone(),
746 metrics: crate::text::TextMetrics {
747 width: 12.0,
748 height: 18.0,
749 line_height: 18.0,
750 line_count: 1,
751 },
752 did_overflow: false,
753 }
754 }
755
756 fn get_offset_for_position(
757 &self,
758 _text: &crate::text::AnnotatedString,
759 _style: &TextStyle,
760 _x: f32,
761 _y: f32,
762 ) -> usize {
763 0
764 }
765
766 fn get_cursor_x_for_offset(
767 &self,
768 _text: &crate::text::AnnotatedString,
769 _style: &TextStyle,
770 _offset: usize,
771 ) -> f32 {
772 0.0
773 }
774
775 fn layout(
776 &self,
777 _text: &crate::text::AnnotatedString,
778 _style: &TextStyle,
779 ) -> TextLayoutResult {
780 panic!("layout is not used in this test");
781 }
782 }
783
784 #[test]
785 fn hash_changes_when_style_changes() {
786 let text = Rc::new(AnnotatedString::from("Hello"));
787 let element_a = TextModifierElement::new(
788 text.clone(),
789 TextStyle::default(),
790 TextLayoutOptions::default(),
791 );
792 let style_b = TextStyle {
793 span_style: crate::text::SpanStyle {
794 font_size: TextUnit::Sp(18.0),
795 ..Default::default()
796 },
797 ..Default::default()
798 };
799 let element_b = TextModifierElement::new(text, style_b, TextLayoutOptions::default());
800
801 assert_ne!(element_a, element_b);
802 assert_ne!(hash_of(&element_a), hash_of(&element_b));
803 }
804
805 #[test]
806 fn hash_matches_for_equal_elements() {
807 let style = TextStyle {
808 span_style: crate::text::SpanStyle {
809 font_size: TextUnit::Sp(14.0),
810 letter_spacing: TextUnit::Em(0.1),
811 ..Default::default()
812 },
813 ..Default::default()
814 };
815 let options = TextLayoutOptions::default();
816 let text = Rc::new(AnnotatedString::from("Hash me"));
817 let element_a = TextModifierElement::new(text.clone(), style.clone(), options);
818 let element_b = TextModifierElement::new(text, style, options);
819
820 assert_eq!(element_a, element_b);
821 assert_eq!(hash_of(&element_a), hash_of(&element_b));
822 }
823
824 #[test]
825 fn measure_uses_attached_node_identity() {
826 let (tx, rx) = mpsc::channel();
827
828 std::thread::spawn(move || {
829 let recorded = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
830 crate::text::set_text_measurer(RecordingPreparedLayoutMeasurer {
831 recorded: recorded.clone(),
832 });
833
834 let mut node = TextModifierNode::new(
835 Rc::new(AnnotatedString::from("identity")),
836 TextStyle::default(),
837 TextLayoutOptions::default(),
838 );
839 let mut context = BasicModifierNodeContext::new();
840 context.set_node_id(Some(77));
841 node.on_attach(&mut context);
842
843 let size = node.measure_text_content(Some(96.0));
844 tx.send((recorded.borrow().clone(), size.width, size.height))
845 .expect("send measurement result");
846 });
847
848 let (recorded, width, height) = rx.recv().expect("receive measurement result");
849 assert_eq!(recorded, vec![Some(77)]);
850 assert_eq!(width, 12.0);
851 assert_eq!(height, 18.0);
852 }
853
854 #[test]
855 fn prepared_layout_cache_reuses_node_snapshot() {
856 let (tx, rx) = mpsc::channel();
857
858 std::thread::spawn(move || {
859 let recorded = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
860 crate::text::set_text_measurer(RecordingPreparedLayoutMeasurer {
861 recorded: recorded.clone(),
862 });
863
864 let mut node = TextModifierNode::new(
865 Rc::new(AnnotatedString::from("reuse")),
866 TextStyle::default(),
867 TextLayoutOptions::default(),
868 );
869 let mut context = BasicModifierNodeContext::new();
870 context.set_node_id(Some(88));
871 node.on_attach(&mut context);
872
873 let measured = node.measure_text_content(Some(120.0));
874 let prepared = node.prepared_layout_handle().prepare(Some(120.0));
875 tx.send((
876 recorded.borrow().clone(),
877 measured.width,
878 measured.height,
879 prepared.metrics.width,
880 prepared.metrics.height,
881 ))
882 .expect("send cached layout result");
883 });
884
885 let (recorded, measured_width, measured_height, prepared_width, prepared_height) =
886 rx.recv().expect("receive cached layout result");
887 assert_eq!(recorded, vec![Some(88)]);
888 assert_eq!(measured_width, prepared_width);
889 assert_eq!(measured_height, prepared_height);
890 }
891}