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