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::RefCell;
29use std::hash::{Hash, Hasher};
30
31#[derive(Debug)]
41pub struct TextModifierNode {
42 text: AnnotatedString,
43 style: TextStyle,
44 options: TextLayoutOptions,
45 measure_cache: RefCell<Option<TextMeasureCacheEntry>>,
46 state: NodeState,
47}
48
49#[derive(Clone, Copy, Debug)]
50struct TextMeasureCacheEntry {
51 max_width_bits: Option<u32>,
52 size: Size,
53}
54
55impl TextModifierNode {
56 pub fn new(text: AnnotatedString, style: TextStyle, options: TextLayoutOptions) -> Self {
57 Self {
58 text,
59 style,
60 options: options.normalized(),
61 measure_cache: RefCell::new(None),
62 state: NodeState::new(),
63 }
64 }
65
66 pub fn text(&self) -> &str {
67 &self.text.text
68 }
69
70 pub fn annotated_string(&self) -> AnnotatedString {
71 self.text.clone()
72 }
73
74 pub fn style(&self) -> &TextStyle {
75 &self.style
76 }
77
78 pub fn options(&self) -> TextLayoutOptions {
79 self.options
80 }
81
82 fn measure_text_content(&self, max_width: Option<f32>) -> Size {
83 let cache_key = max_width.map(f32::to_bits);
84 if let Some(cache) = self.measure_cache.borrow().as_ref() {
85 if cache.max_width_bits == cache_key {
86 return cache.size;
87 }
88 }
89
90 let metrics = crate::text::measure_text_with_options(
91 &self.text,
92 &self.style,
93 self.options,
94 max_width,
95 );
96 let size = Size {
97 width: metrics.width,
98 height: metrics.height,
99 };
100 self.measure_cache
101 .borrow_mut()
102 .replace(TextMeasureCacheEntry {
103 max_width_bits: cache_key,
104 size,
105 });
106 size
107 }
108}
109
110impl DelegatableNode for TextModifierNode {
111 fn node_state(&self) -> &NodeState {
112 &self.state
113 }
114}
115
116impl ModifierNode for TextModifierNode {
117 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
118 context.invalidate(InvalidationKind::Layout);
120 context.invalidate(InvalidationKind::Draw);
121 context.invalidate(InvalidationKind::Semantics);
122 }
123
124 fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
125 Some(self)
126 }
127
128 fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
129 Some(self)
130 }
131
132 fn as_semantics_node(&self) -> Option<&dyn SemanticsNode> {
133 Some(self)
134 }
135
136 fn as_semantics_node_mut(&mut self) -> Option<&mut dyn SemanticsNode> {
137 Some(self)
138 }
139
140 fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
141 Some(self)
142 }
143
144 fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
145 Some(self)
146 }
147}
148
149impl LayoutModifierNode for TextModifierNode {
150 fn measure(
151 &self,
152 _context: &mut dyn ModifierNodeContext,
153 _measurable: &dyn Measurable,
154 constraints: Constraints,
155 ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
156 let max_width = constraints
158 .max_width
159 .is_finite()
160 .then_some(constraints.max_width);
161 let text_size = self.measure_text_content(max_width);
162
163 let width = text_size
165 .width
166 .clamp(constraints.min_width, constraints.max_width);
167 let height = text_size
168 .height
169 .clamp(constraints.min_height, constraints.max_height);
170
171 cranpose_ui_layout::LayoutModifierMeasureResult::with_size(Size { width, height })
175 }
176
177 fn min_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
178 self.measure_text_content(None).width
179 }
180
181 fn max_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
182 self.measure_text_content(None).width
183 }
184
185 fn min_intrinsic_height(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
186 self.measure_text_content(Some(_width).filter(|w| w.is_finite() && *w > 0.0))
187 .height
188 }
189
190 fn max_intrinsic_height(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
191 self.measure_text_content(Some(_width).filter(|w| w.is_finite() && *w > 0.0))
192 .height
193 }
194
195 fn create_measurement_proxy(&self) -> Option<Box<dyn MeasurementProxy>> {
196 Some(Box::new(TextMeasurementProxy {
197 text: self.text.clone(),
198 style: self.style.clone(),
199 options: self.options,
200 }))
201 }
202}
203
204struct TextMeasurementProxy {
209 text: AnnotatedString,
210 style: TextStyle,
211 options: TextLayoutOptions,
212}
213
214impl TextMeasurementProxy {
215 fn measure_text_content(&self, max_width: Option<f32>) -> Size {
218 let metrics = crate::text::measure_text_with_options(
219 &self.text,
220 &self.style,
221 self.options,
222 max_width,
223 );
224 Size {
225 width: metrics.width,
226 height: metrics.height,
227 }
228 }
229}
230
231impl MeasurementProxy for TextMeasurementProxy {
232 fn measure_proxy(
233 &self,
234 _context: &mut dyn ModifierNodeContext,
235 _measurable: &dyn Measurable,
236 constraints: Constraints,
237 ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
238 let max_width = constraints
240 .max_width
241 .is_finite()
242 .then_some(constraints.max_width);
243 let text_size = self.measure_text_content(max_width);
244
245 let width = text_size
247 .width
248 .clamp(constraints.min_width, constraints.max_width);
249 let height = text_size
250 .height
251 .clamp(constraints.min_height, constraints.max_height);
252
253 cranpose_ui_layout::LayoutModifierMeasureResult::with_size(Size { width, height })
255 }
256
257 fn min_intrinsic_width_proxy(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
258 self.measure_text_content(None).width
259 }
260
261 fn max_intrinsic_width_proxy(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
262 self.measure_text_content(None).width
263 }
264
265 fn min_intrinsic_height_proxy(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
266 self.measure_text_content(Some(_width).filter(|w| w.is_finite() && *w > 0.0))
267 .height
268 }
269
270 fn max_intrinsic_height_proxy(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
271 self.measure_text_content(Some(_width).filter(|w| w.is_finite() && *w > 0.0))
272 .height
273 }
274}
275
276impl DrawModifierNode for TextModifierNode {
277 fn draw(&self, _draw_scope: &mut dyn DrawScope) {
278 }
287}
288
289impl SemanticsNode for TextModifierNode {
290 fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
291 config.content_description = Some(self.text.text.clone());
293 }
294}
295
296#[derive(Debug, Clone, PartialEq)]
305pub struct TextModifierElement {
306 text: AnnotatedString,
307 style: TextStyle,
308 options: TextLayoutOptions,
309}
310
311impl TextModifierElement {
312 pub fn new(text: AnnotatedString, style: TextStyle, options: TextLayoutOptions) -> Self {
313 Self {
314 text,
315 style,
316 options: options.normalized(),
317 }
318 }
319}
320
321fn hash_f32_bits<H: Hasher>(value: f32, state: &mut H) {
322 value.to_bits().hash(state);
323}
324
325fn hash_text_unit<H: Hasher>(unit: crate::text::TextUnit, state: &mut H) {
326 match unit {
327 crate::text::TextUnit::Unspecified => 0u8.hash(state),
328 crate::text::TextUnit::Sp(value) => {
329 1u8.hash(state);
330 hash_f32_bits(value, state);
331 }
332 crate::text::TextUnit::Em(value) => {
333 2u8.hash(state);
334 hash_f32_bits(value, state);
335 }
336 }
337}
338
339fn hash_color<H: Hasher>(color: crate::modifier::Color, state: &mut H) {
340 hash_f32_bits(color.0, state);
341 hash_f32_bits(color.1, state);
342 hash_f32_bits(color.2, state);
343 hash_f32_bits(color.3, state);
344}
345
346fn hash_option_color<H: Hasher>(color: &Option<crate::modifier::Color>, state: &mut H) {
347 match color {
348 Some(color) => {
349 1u8.hash(state);
350 hash_color(*color, state);
351 }
352 None => 0u8.hash(state),
353 }
354}
355
356fn hash_brush<H: Hasher>(brush: &crate::modifier::Brush, state: &mut H) {
357 match brush {
358 crate::modifier::Brush::Solid(color) => {
359 0u8.hash(state);
360 hash_color(*color, state);
361 }
362 crate::modifier::Brush::LinearGradient {
363 colors,
364 stops,
365 start,
366 end,
367 tile_mode,
368 } => {
369 1u8.hash(state);
370 colors.len().hash(state);
371 for color in colors {
372 hash_color(*color, state);
373 }
374 match stops {
375 Some(stops) => {
376 1u8.hash(state);
377 stops.len().hash(state);
378 for stop in stops {
379 hash_f32_bits(*stop, state);
380 }
381 }
382 None => 0u8.hash(state),
383 }
384 hash_f32_bits(start.x, state);
385 hash_f32_bits(start.y, state);
386 hash_f32_bits(end.x, state);
387 hash_f32_bits(end.y, state);
388 tile_mode.hash(state);
389 }
390 crate::modifier::Brush::RadialGradient {
391 colors,
392 stops,
393 center,
394 radius,
395 tile_mode,
396 } => {
397 2u8.hash(state);
398 colors.len().hash(state);
399 for color in colors {
400 hash_color(*color, state);
401 }
402 match stops {
403 Some(stops) => {
404 1u8.hash(state);
405 stops.len().hash(state);
406 for stop in stops {
407 hash_f32_bits(*stop, state);
408 }
409 }
410 None => 0u8.hash(state),
411 }
412 hash_f32_bits(center.x, state);
413 hash_f32_bits(center.y, state);
414 hash_f32_bits(*radius, state);
415 tile_mode.hash(state);
416 }
417 crate::modifier::Brush::SweepGradient {
418 colors,
419 stops,
420 center,
421 } => {
422 3u8.hash(state);
423 colors.len().hash(state);
424 for color in colors {
425 hash_color(*color, state);
426 }
427 match stops {
428 Some(stops) => {
429 1u8.hash(state);
430 stops.len().hash(state);
431 for stop in stops {
432 hash_f32_bits(*stop, state);
433 }
434 }
435 None => 0u8.hash(state),
436 }
437 hash_f32_bits(center.x, state);
438 hash_f32_bits(center.y, state);
439 }
440 }
441}
442
443fn hash_option_brush<H: Hasher>(brush: &Option<crate::modifier::Brush>, state: &mut H) {
444 match brush {
445 Some(brush) => {
446 1u8.hash(state);
447 hash_brush(brush, state);
448 }
449 None => 0u8.hash(state),
450 }
451}
452
453fn hash_option_alpha<H: Hasher>(alpha: &Option<f32>, state: &mut H) {
454 match alpha {
455 Some(alpha) => {
456 1u8.hash(state);
457 hash_f32_bits(*alpha, state);
458 }
459 None => 0u8.hash(state),
460 }
461}
462
463fn hash_option_baseline_shift<H: Hasher>(
464 baseline_shift: &Option<crate::text::BaselineShift>,
465 state: &mut H,
466) {
467 match baseline_shift {
468 Some(shift) => {
469 1u8.hash(state);
470 hash_f32_bits(shift.0, state);
471 }
472 None => 0u8.hash(state),
473 }
474}
475
476fn hash_option_text_geometric_transform<H: Hasher>(
477 transform: &Option<crate::text::TextGeometricTransform>,
478 state: &mut H,
479) {
480 match transform {
481 Some(transform) => {
482 1u8.hash(state);
483 hash_f32_bits(transform.scale_x, state);
484 hash_f32_bits(transform.skew_x, state);
485 }
486 None => 0u8.hash(state),
487 }
488}
489
490fn hash_option_shadow<H: Hasher>(shadow: &Option<crate::text::Shadow>, state: &mut H) {
491 match shadow {
492 Some(shadow) => {
493 1u8.hash(state);
494 hash_color(shadow.color, state);
495 hash_f32_bits(shadow.offset.x, state);
496 hash_f32_bits(shadow.offset.y, state);
497 hash_f32_bits(shadow.blur_radius, state);
498 }
499 None => 0u8.hash(state),
500 }
501}
502
503fn hash_option_text_indent<H: Hasher>(indent: &Option<crate::text::TextIndent>, state: &mut H) {
504 match indent {
505 Some(indent) => {
506 1u8.hash(state);
507 hash_text_unit(indent.first_line, state);
508 hash_text_unit(indent.rest_line, state);
509 }
510 None => 0u8.hash(state),
511 }
512}
513
514fn hash_option_text_draw_style<H: Hasher>(
515 draw_style: &Option<crate::text::TextDrawStyle>,
516 state: &mut H,
517) {
518 match draw_style {
519 Some(crate::text::TextDrawStyle::Fill) => {
520 1u8.hash(state);
521 0u8.hash(state);
522 }
523 Some(crate::text::TextDrawStyle::Stroke { width }) => {
524 1u8.hash(state);
525 1u8.hash(state);
526 hash_f32_bits(*width, state);
527 }
528 None => 0u8.hash(state),
529 }
530}
531
532fn hash_text_style<H: Hasher>(style: &TextStyle, state: &mut H) {
533 let span = &style.span_style;
534 let paragraph = &style.paragraph_style;
535
536 hash_option_color(&span.color, state);
537 hash_option_brush(&span.brush, state);
538 hash_option_alpha(&span.alpha, state);
539 hash_text_unit(span.font_size, state);
540 span.font_weight.hash(state);
541 span.font_style.hash(state);
542 span.font_synthesis.hash(state);
543 span.font_family.hash(state);
544 span.font_feature_settings.hash(state);
545 hash_text_unit(span.letter_spacing, state);
546 hash_option_baseline_shift(&span.baseline_shift, state);
547 hash_option_text_geometric_transform(&span.text_geometric_transform, state);
548 span.locale_list.hash(state);
549 hash_option_color(&span.background, state);
550 span.text_decoration.hash(state);
551 hash_option_shadow(&span.shadow, state);
552 span.platform_style.hash(state);
553 hash_option_text_draw_style(&span.draw_style, state);
554
555 paragraph.text_align.hash(state);
556 paragraph.text_direction.hash(state);
557 hash_text_unit(paragraph.line_height, state);
558 hash_option_text_indent(¶graph.text_indent, state);
559 paragraph.platform_style.hash(state);
560 paragraph.line_height_style.hash(state);
561 paragraph.line_break.hash(state);
562 paragraph.hyphens.hash(state);
563 paragraph.text_motion.hash(state);
564}
565
566impl Hash for TextModifierElement {
567 fn hash<H: Hasher>(&self, state: &mut H) {
568 self.text.text.hash(state);
569 hash_text_style(&self.style, state);
570 self.options.hash(state);
571 }
572}
573
574impl ModifierNodeElement for TextModifierElement {
575 type Node = TextModifierNode;
576
577 fn create(&self) -> Self::Node {
578 TextModifierNode::new(self.text.clone(), self.style.clone(), self.options)
579 }
580
581 fn update(&self, node: &mut Self::Node) {
582 let mut changed = false;
583 if node.text != self.text {
584 node.text = self.text.clone();
585 changed = true;
586 }
587 if node.style != self.style {
588 node.style = self.style.clone();
589 changed = true;
590 }
591 if node.options != self.options {
592 node.options = self.options;
593 changed = true;
594 }
595
596 if changed {
597 node.measure_cache.borrow_mut().take();
598 }
604 }
605
606 fn capabilities(&self) -> NodeCapabilities {
607 NodeCapabilities::LAYOUT | NodeCapabilities::DRAW | NodeCapabilities::SEMANTICS
609 }
610}
611
612#[cfg(test)]
613mod tests {
614 use super::*;
615 use crate::text::TextUnit;
616 use std::collections::hash_map::DefaultHasher;
617
618 fn hash_of(element: &TextModifierElement) -> u64 {
619 let mut hasher = DefaultHasher::new();
620 element.hash(&mut hasher);
621 hasher.finish()
622 }
623
624 #[test]
625 fn hash_changes_when_style_changes() {
626 let text = AnnotatedString::from("Hello");
627 let element_a = TextModifierElement::new(
628 text.clone(),
629 TextStyle::default(),
630 TextLayoutOptions::default(),
631 );
632 let style_b = TextStyle {
633 span_style: crate::text::SpanStyle {
634 font_size: TextUnit::Sp(18.0),
635 ..Default::default()
636 },
637 ..Default::default()
638 };
639 let element_b = TextModifierElement::new(text, style_b, TextLayoutOptions::default());
640
641 assert_ne!(element_a, element_b);
642 assert_ne!(hash_of(&element_a), hash_of(&element_b));
643 }
644
645 #[test]
646 fn hash_matches_for_equal_elements() {
647 let style = TextStyle {
648 span_style: crate::text::SpanStyle {
649 font_size: TextUnit::Sp(14.0),
650 letter_spacing: TextUnit::Em(0.1),
651 ..Default::default()
652 },
653 ..Default::default()
654 };
655 let options = TextLayoutOptions::default();
656 let text = AnnotatedString::from("Hash me");
657 let element_a = TextModifierElement::new(text.clone(), style.clone(), options);
658 let element_b = TextModifierElement::new(text, style, options);
659
660 assert_eq!(element_a, element_b);
661 assert_eq!(hash_of(&element_a), hash_of(&element_b));
662 }
663}