1use crate::text::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};
29use std::rc::Rc;
30
31#[derive(Debug)]
41pub struct TextModifierNode {
42 text: Rc<str>,
43 style: TextStyle,
44 state: NodeState,
45}
46
47impl TextModifierNode {
48 pub fn new(text: Rc<str>, style: TextStyle) -> Self {
49 Self {
50 text,
51 style,
52 state: NodeState::new(),
53 }
54 }
55
56 pub fn text(&self) -> &str {
57 &self.text
58 }
59
60 pub fn text_arc(&self) -> Rc<str> {
61 self.text.clone()
62 }
63
64 pub fn style(&self) -> &TextStyle {
65 &self.style
66 }
67
68 fn measure_text_content(&self) -> Size {
69 let metrics = crate::text::measure_text(&self.text, &self.style);
70 Size {
71 width: metrics.width,
72 height: metrics.height,
73 }
74 }
75}
76
77impl DelegatableNode for TextModifierNode {
78 fn node_state(&self) -> &NodeState {
79 &self.state
80 }
81}
82
83impl ModifierNode for TextModifierNode {
84 fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
85 context.invalidate(InvalidationKind::Layout);
87 context.invalidate(InvalidationKind::Draw);
88 context.invalidate(InvalidationKind::Semantics);
89 }
90
91 fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
92 Some(self)
93 }
94
95 fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
96 Some(self)
97 }
98
99 fn as_semantics_node(&self) -> Option<&dyn SemanticsNode> {
100 Some(self)
101 }
102
103 fn as_semantics_node_mut(&mut self) -> Option<&mut dyn SemanticsNode> {
104 Some(self)
105 }
106
107 fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
108 Some(self)
109 }
110
111 fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
112 Some(self)
113 }
114}
115
116impl LayoutModifierNode for TextModifierNode {
117 fn measure(
118 &self,
119 _context: &mut dyn ModifierNodeContext,
120 _measurable: &dyn Measurable,
121 constraints: Constraints,
122 ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
123 let text_size = self.measure_text_content();
125
126 let width = text_size
128 .width
129 .clamp(constraints.min_width, constraints.max_width);
130 let height = text_size
131 .height
132 .clamp(constraints.min_height, constraints.max_height);
133
134 cranpose_ui_layout::LayoutModifierMeasureResult::with_size(Size { width, height })
138 }
139
140 fn min_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
141 self.measure_text_content().width
142 }
143
144 fn max_intrinsic_width(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
145 self.measure_text_content().width
146 }
147
148 fn min_intrinsic_height(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
149 self.measure_text_content().height
150 }
151
152 fn max_intrinsic_height(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
153 self.measure_text_content().height
154 }
155
156 fn create_measurement_proxy(&self) -> Option<Box<dyn MeasurementProxy>> {
157 Some(Box::new(TextMeasurementProxy {
158 text: self.text.clone(),
159 style: self.style.clone(), }))
161 }
162}
163
164struct TextMeasurementProxy {
169 text: Rc<str>,
170 style: TextStyle, }
172
173impl TextMeasurementProxy {
174 fn measure_text_content(&self) -> Size {
177 let metrics = crate::text::measure_text(&self.text, &self.style);
178 Size {
179 width: metrics.width,
180 height: metrics.height,
181 }
182 }
183}
184
185impl MeasurementProxy for TextMeasurementProxy {
186 fn measure_proxy(
187 &self,
188 _context: &mut dyn ModifierNodeContext,
189 _measurable: &dyn Measurable,
190 constraints: Constraints,
191 ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
192 let text_size = self.measure_text_content();
194
195 let width = text_size
197 .width
198 .clamp(constraints.min_width, constraints.max_width);
199 let height = text_size
200 .height
201 .clamp(constraints.min_height, constraints.max_height);
202
203 cranpose_ui_layout::LayoutModifierMeasureResult::with_size(Size { width, height })
205 }
206
207 fn min_intrinsic_width_proxy(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
208 self.measure_text_content().width
209 }
210
211 fn max_intrinsic_width_proxy(&self, _measurable: &dyn Measurable, _height: f32) -> f32 {
212 self.measure_text_content().width
213 }
214
215 fn min_intrinsic_height_proxy(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
216 self.measure_text_content().height
217 }
218
219 fn max_intrinsic_height_proxy(&self, _measurable: &dyn Measurable, _width: f32) -> f32 {
220 self.measure_text_content().height
221 }
222}
223
224impl DrawModifierNode for TextModifierNode {
225 fn draw(&self, _draw_scope: &mut dyn DrawScope) {
226 }
235}
236
237impl SemanticsNode for TextModifierNode {
238 fn merge_semantics(&self, config: &mut SemanticsConfiguration) {
239 config.content_description = Some(self.text.to_string());
241 }
242}
243
244#[derive(Debug, Clone, PartialEq)]
253pub struct TextModifierElement {
254 text: Rc<str>,
255 style: TextStyle,
256}
257
258impl TextModifierElement {
259 pub fn new(text: Rc<str>, style: TextStyle) -> Self {
260 Self { text, style }
261 }
262}
263
264fn hash_f32_bits<H: Hasher>(value: f32, state: &mut H) {
265 value.to_bits().hash(state);
266}
267
268fn hash_text_unit<H: Hasher>(unit: crate::text::TextUnit, state: &mut H) {
269 match unit {
270 crate::text::TextUnit::Unspecified => 0u8.hash(state),
271 crate::text::TextUnit::Sp(value) => {
272 1u8.hash(state);
273 hash_f32_bits(value, state);
274 }
275 crate::text::TextUnit::Em(value) => {
276 2u8.hash(state);
277 hash_f32_bits(value, state);
278 }
279 }
280}
281
282fn hash_color<H: Hasher>(color: crate::modifier::Color, state: &mut H) {
283 hash_f32_bits(color.0, state);
284 hash_f32_bits(color.1, state);
285 hash_f32_bits(color.2, state);
286 hash_f32_bits(color.3, state);
287}
288
289fn hash_option_color<H: Hasher>(color: &Option<crate::modifier::Color>, state: &mut H) {
290 match color {
291 Some(color) => {
292 1u8.hash(state);
293 hash_color(*color, state);
294 }
295 None => 0u8.hash(state),
296 }
297}
298
299fn hash_option_f32<H: Hasher>(value: Option<f32>, state: &mut H) {
300 match value {
301 Some(value) => {
302 1u8.hash(state);
303 hash_f32_bits(value, state);
304 }
305 None => 0u8.hash(state),
306 }
307}
308
309fn hash_option_shadow<H: Hasher>(shadow: &Option<crate::text::Shadow>, state: &mut H) {
310 match shadow {
311 Some(shadow) => {
312 1u8.hash(state);
313 hash_color(shadow.color, state);
314 hash_f32_bits(shadow.offset.x, state);
315 hash_f32_bits(shadow.offset.y, state);
316 hash_f32_bits(shadow.blur_radius, state);
317 }
318 None => 0u8.hash(state),
319 }
320}
321
322fn hash_option_text_indent<H: Hasher>(indent: &Option<crate::text::TextIndent>, state: &mut H) {
323 match indent {
324 Some(indent) => {
325 1u8.hash(state);
326 hash_text_unit(indent.first_line, state);
327 hash_text_unit(indent.rest_line, state);
328 }
329 None => 0u8.hash(state),
330 }
331}
332
333fn hash_text_style<H: Hasher>(style: &TextStyle, state: &mut H) {
334 hash_option_color(&style.color, state);
335 hash_text_unit(style.font_size, state);
336 style.font_weight.hash(state);
337 style.font_style.hash(state);
338 style.font_synthesis.hash(state);
339 style.font_family.hash(state);
340 style.font_feature_settings.hash(state);
341 hash_text_unit(style.letter_spacing, state);
342 hash_option_f32(style.baseline_shift, state);
343 style.text_geometric_transform.is_some().hash(state);
344 style.locale_list.is_some().hash(state);
345 hash_option_color(&style.background, state);
346 style.text_decoration.hash(state);
347 hash_option_shadow(&style.shadow, state);
348 style.text_align.hash(state);
349 style.text_direction.hash(state);
350 hash_text_unit(style.line_height, state);
351 hash_option_text_indent(&style.text_indent, state);
352}
353
354impl Hash for TextModifierElement {
355 fn hash<H: Hasher>(&self, state: &mut H) {
356 self.text.hash(state);
357 hash_text_style(&self.style, state);
358 }
359}
360
361impl ModifierNodeElement for TextModifierElement {
362 type Node = TextModifierNode;
363
364 fn create(&self) -> Self::Node {
365 TextModifierNode::new(self.text.clone(), self.style.clone())
366 }
367
368 fn update(&self, node: &mut Self::Node) {
369 let mut changed = false;
370 if node.text != self.text {
371 node.text = self.text.clone();
372 changed = true;
373 }
374 if node.style != self.style {
375 node.style = self.style.clone();
376 changed = true;
377 }
378
379 if changed {
380 }
386 }
387
388 fn capabilities(&self) -> NodeCapabilities {
389 NodeCapabilities::LAYOUT | NodeCapabilities::DRAW | NodeCapabilities::SEMANTICS
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397 use crate::text::TextUnit;
398 use std::collections::hash_map::DefaultHasher;
399
400 fn hash_of(element: &TextModifierElement) -> u64 {
401 let mut hasher = DefaultHasher::new();
402 element.hash(&mut hasher);
403 hasher.finish()
404 }
405
406 #[test]
407 fn hash_changes_when_style_changes() {
408 let text = Rc::<str>::from("Hello");
409 let element_a = TextModifierElement::new(text.clone(), TextStyle::default());
410 let style_b = TextStyle {
411 font_size: TextUnit::Sp(18.0),
412 ..Default::default()
413 };
414 let element_b = TextModifierElement::new(text, style_b);
415
416 assert_ne!(element_a, element_b);
417 assert_ne!(hash_of(&element_a), hash_of(&element_b));
418 }
419
420 #[test]
421 fn hash_matches_for_equal_elements() {
422 let style = TextStyle {
423 font_size: TextUnit::Sp(14.0),
424 letter_spacing: TextUnit::Em(0.1),
425 ..Default::default()
426 };
427 let element_a = TextModifierElement::new(Rc::<str>::from("Hash me"), style.clone());
428 let element_b = TextModifierElement::new(Rc::<str>::from("Hash me"), style);
429
430 assert_eq!(element_a, element_b);
431 assert_eq!(hash_of(&element_a), hash_of(&element_b));
432 }
433}