Skip to main content

repose_tree/
hash.rs

1//! Content hashing for change detection.
2
3use rapidhash::fast::RapidHasher;
4use repose_core::{
5    Brush, Color, Dp, Modifier, Px, Sp, TextOverflow, View, ViewKind,
6    animation::{AnimationSpec, Easing},
7    scroll::ScrollBinding,
8    text::{Shadow, SpanStyle, TextStyle},
9};
10use std::hash::{Hash, Hasher};
11
12#[allow(dead_code)]
13fn hash_view_content_inner(view: &View, hasher: &mut impl Hasher) {
14    view.id.hash(hasher);
15    hash_view_kind(&view.kind, hasher);
16    hash_modifier(&view.modifier, hasher);
17    view.scope_key.hash(hasher);
18    if let Some(semantics) = &view.semantics {
19        1u8.hash(hasher);
20        std::mem::discriminant(&semantics.role).hash(hasher);
21        semantics.label.hash(hasher);
22        semantics.focused.hash(hasher);
23        semantics.enabled.hash(hasher);
24        semantics.selectable_group.hash(hasher);
25        semantics.checked.hash(hasher);
26        semantics.selected.hash(hasher);
27        semantics.value.hash(hasher);
28    } else {
29        0u8.hash(hasher);
30    }
31    match view.modifier.key {
32        Some(key) => {
33            1u8.hash(hasher);
34            key.hash(hasher);
35        }
36        None => 0u8.hash(hasher),
37    }
38}
39
40pub fn hash_view_content(view: &View) -> u64 {
41    hash_view_facets(view).combined()
42}
43
44#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
45pub struct ViewHashes {
46    pub layout: u64,
47    pub measurement: u64,
48    pub paint: u64,
49    pub semantics: u64,
50}
51
52impl ViewHashes {
53    pub fn combined(self) -> u64 {
54        let mut hasher = RapidHasher::default();
55        0x5245_504f_5345_2d48u64.hash(&mut hasher);
56        self.layout.hash(&mut hasher);
57        self.measurement.hash(&mut hasher);
58        self.paint.hash(&mut hasher);
59        self.semantics.hash(&mut hasher);
60        hasher.finish()
61    }
62}
63
64#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
65pub struct SubtreeHashes {
66    pub layout: u64,
67    pub measurement: u64,
68    pub paint: u64,
69    pub semantics: u64,
70}
71
72impl SubtreeHashes {
73    pub fn combined(self) -> u64 {
74        let mut hasher = RapidHasher::default();
75        0x5245_504f_5345_2d53u64.hash(&mut hasher);
76        self.layout.hash(&mut hasher);
77        self.measurement.hash(&mut hasher);
78        self.paint.hash(&mut hasher);
79        self.semantics.hash(&mut hasher);
80        hasher.finish()
81    }
82}
83
84pub(crate) struct SubtreeHashBuilder {
85    layout: RapidHasher<'static>,
86    measurement: RapidHasher<'static>,
87    paint: RapidHasher<'static>,
88    semantics: RapidHasher<'static>,
89}
90
91impl SubtreeHashBuilder {
92    pub fn new(child_count: usize) -> Self {
93        let mut builder = Self {
94            layout: RapidHasher::default(),
95            measurement: RapidHasher::default(),
96            paint: RapidHasher::default(),
97            semantics: RapidHasher::default(),
98        };
99        builder.layout.write_u64(0x5355_4254_5245_45);
100        builder.measurement.write_u64(0x534d_4541_5355_5245);
101        builder.paint.write_u64(0x5041_494e_545f_5355);
102        builder.semantics.write_u64(0x5345_4d41_4e54_4943);
103        builder.layout.write_usize(child_count);
104        builder.measurement.write_usize(child_count);
105        builder.paint.write_usize(child_count);
106        builder.semantics.write_usize(child_count);
107        builder
108    }
109
110    pub fn push(&mut self, child: SubtreeHashes) {
111        self.layout.write_u64(child.layout);
112        self.measurement.write_u64(child.measurement);
113        self.paint.write_u64(child.paint);
114        self.semantics.write_u64(child.semantics);
115    }
116
117    pub fn finish(mut self, content: ViewHashes) -> SubtreeHashes {
118        self.layout.write_u64(content.layout);
119        self.measurement.write_u64(content.measurement);
120        self.paint.write_u64(content.paint);
121        self.semantics.write_u64(content.semantics);
122        SubtreeHashes {
123            layout: self.layout.finish(),
124            measurement: self.measurement.finish(),
125            paint: self.paint.finish(),
126            semantics: self.semantics.finish(),
127        }
128    }
129}
130
131const FACET_LAYOUT: u8 = 1;
132const FACET_MEASUREMENT: u8 = 2;
133const FACET_PAINT: u8 = 4;
134const FACET_SEMANTICS: u8 = 8;
135const FACET_LAYOUT_MEASUREMENT: u8 = FACET_LAYOUT | FACET_MEASUREMENT;
136const FACET_LAYOUT_PAINT: u8 = FACET_LAYOUT | FACET_PAINT;
137const FACET_LAYOUT_MEASUREMENT_PAINT: u8 = FACET_LAYOUT | FACET_MEASUREMENT | FACET_PAINT;
138const FACET_ALL: u8 = FACET_LAYOUT | FACET_MEASUREMENT | FACET_PAINT | FACET_SEMANTICS;
139
140type FacetHasher = RapidHasher<'static>;
141
142#[derive(Default)]
143struct FacetHashers {
144    hashers: [FacetHasher; 4],
145}
146
147impl FacetHashers {
148    fn hash(&mut self, mask: u8, write: impl Fn(&mut FacetHasher)) {
149        for (index, hasher) in self.hashers.iter_mut().enumerate() {
150            if mask & (1 << index) != 0 {
151                write(hasher);
152            }
153        }
154    }
155
156    fn finish(self) -> ViewHashes {
157        ViewHashes {
158            layout: self.hashers[0].finish(),
159            measurement: self.hashers[1].finish(),
160            paint: self.hashers[2].finish(),
161            semantics: self.hashers[3].finish(),
162        }
163    }
164}
165
166pub fn hash_view_facets(view: &View) -> ViewHashes {
167    let mut hashers = FacetHashers::default();
168    hashers.hash(FACET_ALL, |hasher| {
169        0x5245_504f_5345_2d56u64.hash(hasher);
170    });
171    facet_hash_view_kind(&view.kind, &mut hashers);
172    facet_hash_modifier(&view.modifier, &mut hashers);
173    hashers.hash(FACET_LAYOUT_PAINT, |hasher| view.id.hash(hasher));
174    hashers.hash(FACET_ALL ^ FACET_SEMANTICS, |hasher| {
175        view.scope_key.hash(hasher);
176    });
177    facet_hash_semantics(view.semantics.as_ref(), &mut hashers, FACET_SEMANTICS);
178    hashers.finish()
179}
180
181pub fn hash_view_layout(view: &View) -> u64 {
182    hash_view_facets(view).layout
183}
184
185pub fn hash_view_measurement(view: &View) -> u64 {
186    hash_view_facets(view).measurement
187}
188
189pub fn hash_view_paint(view: &View) -> u64 {
190    hash_view_facets(view).paint
191}
192
193pub fn hash_view_semantics(view: &View) -> u64 {
194    hash_view_facets(view).semantics
195}
196
197pub fn hash_subtree_facets(content: ViewHashes, children: &[SubtreeHashes]) -> SubtreeHashes {
198    let mut builder = SubtreeHashBuilder::new(children.len());
199    for child in children {
200        builder.push(*child);
201    }
202    builder.finish(content)
203}
204
205fn facet_hash_view_kind(kind: &ViewKind, hashers: &mut FacetHashers) {
206    hashers.hash(FACET_ALL, |hasher| {
207        std::mem::discriminant(kind).hash(hasher);
208    });
209    match kind {
210        ViewKind::Text {
211            text,
212            color,
213            font_size,
214            soft_wrap,
215            max_lines,
216            overflow,
217            font_family,
218            annotations,
219            text_align,
220            font_weight,
221            font_style,
222            text_decoration,
223            letter_spacing,
224            line_height,
225            url,
226            font_variation_settings,
227            draw_style,
228        } => {
229            hashers.hash(FACET_LAYOUT_MEASUREMENT, |h| text.hash(h));
230            hashers.hash(FACET_PAINT, |h| hash_color(color, h));
231            hashers.hash(FACET_LAYOUT_MEASUREMENT, |h| hash_sp(*font_size, h));
232            hashers.hash(FACET_LAYOUT_MEASUREMENT, |h| soft_wrap.hash(h));
233            hashers.hash(FACET_LAYOUT_MEASUREMENT, |h| max_lines.hash(h));
234            hashers.hash(FACET_LAYOUT_MEASUREMENT, |h| {
235                hash_text_overflow(overflow, h)
236            });
237            hashers.hash(FACET_LAYOUT_MEASUREMENT, |h| font_family.hash(h));
238            hashers.hash(FACET_LAYOUT_MEASUREMENT, |h| text_align.hash(h));
239            hashers.hash(FACET_LAYOUT_MEASUREMENT, |h| font_weight.0.hash(h));
240            hashers.hash(FACET_LAYOUT_MEASUREMENT, |h| font_style.hash(h));
241            hashers.hash(FACET_PAINT, |h| hash_text_decoration(text_decoration, h));
242            hashers.hash(FACET_LAYOUT_MEASUREMENT, |h| hash_sp(*letter_spacing, h));
243            hashers.hash(FACET_LAYOUT_MEASUREMENT, |h| hash_sp(*line_height, h));
244            hashers.hash(FACET_PAINT, |h| url.hash(h));
245            hashers.hash(FACET_LAYOUT_MEASUREMENT, |h| {
246                font_variation_settings.hash(h)
247            });
248            hashers.hash(FACET_LAYOUT_MEASUREMENT_PAINT, |h| {
249                hash_draw_style(draw_style, h)
250            });
251            if let Some(annotations) = annotations {
252                hashers.hash(FACET_LAYOUT_MEASUREMENT_PAINT, |h| {
253                    1u8.hash(h);
254                    annotations.len().hash(h);
255                    for span in annotations.iter() {
256                        span.start.hash(h);
257                        span.end.hash(h);
258                    }
259                });
260                for span in annotations.iter() {
261                    facet_hash_span_style(&span.style, hashers, FACET_ALL);
262                    if let Some(url) = &span.url {
263                        hashers.hash(FACET_PAINT, |h| url.hash(h));
264                    } else {
265                        hashers.hash(FACET_PAINT, |h| 0u8.hash(h));
266                    }
267                }
268            } else {
269                hashers.hash(FACET_LAYOUT_MEASUREMENT_PAINT, |h| 0u8.hash(h));
270            }
271        }
272        ViewKind::Image {
273            handle,
274            tint,
275            fit,
276            filter,
277            source_rect,
278            alignment,
279        } => {
280            hashers.hash(FACET_LAYOUT_MEASUREMENT_PAINT, |h| handle.hash(h));
281            // Natural size drives the view's intrinsic measurement, and it can
282            // arrive after the view was first composed (encoded uploads are
283            // decoded by the renderer), so it must invalidate measurement.
284            hashers.hash(FACET_MEASUREMENT, |h| {
285                repose_core::image_intrinsic_size(*handle).hash(h)
286            });
287            hashers.hash(FACET_PAINT, |h| hash_color(tint, h));
288            hashers.hash(FACET_PAINT, |h| std::mem::discriminant(fit).hash(h));
289            hashers.hash(FACET_PAINT, |h| std::mem::discriminant(filter).hash(h));
290            hashers.hash(FACET_PAINT, |h| source_rect.hash(h));
291            hashers.hash(FACET_PAINT, |h| std::mem::discriminant(alignment).hash(h));
292        }
293        ViewKind::Box
294        | ViewKind::Row
295        | ViewKind::Column
296        | ViewKind::ZStack
297        | ViewKind::OverlayHost
298        | ViewKind::SubcomposeLayout { .. } => {}
299        _ => {}
300    }
301}
302
303fn facet_hash_modifier(modifier: &Modifier, hashers: &mut FacetHashers) {
304    macro_rules! hash_value {
305        ($mask:expr, $value:expr) => {
306            hashers.hash($mask, |hasher| $value.hash(hasher));
307        };
308    }
309    macro_rules! hash_f32_value {
310        ($mask:expr, $value:expr) => {
311            hashers.hash($mask, |hasher| hash_f32($value, hasher));
312        };
313    }
314    macro_rules! hash_opt_f32_value {
315        ($mask:expr, $value:expr) => {
316            hashers.hash($mask, |hasher| hash_opt_f32($value, hasher));
317        };
318    }
319    macro_rules! hash_opt_dp_value {
320        ($mask:expr, $value:expr) => {
321            hashers.hash($mask, |hasher| hash_opt_dp($value, hasher));
322        };
323    }
324
325    hash_value!(FACET_LAYOUT_PAINT, modifier.key);
326    match modifier.size {
327        Some(size) => {
328            hashers.hash(FACET_LAYOUT, |h| {
329                hash_dp(size.width, h);
330                hash_dp(size.height, h);
331            });
332        }
333        None => hashers.hash(FACET_LAYOUT, |h| 0u8.hash(h)),
334    }
335    hash_opt_dp_value!(FACET_LAYOUT, modifier.width);
336    hash_opt_dp_value!(FACET_LAYOUT, modifier.height);
337    match modifier.required_size {
338        Some(size) => hashers.hash(FACET_LAYOUT, |h| {
339            hash_dp(size.width, h);
340            hash_dp(size.height, h);
341        }),
342        None => hashers.hash(FACET_LAYOUT, |h| 0u8.hash(h)),
343    }
344    hash_opt_dp_value!(FACET_LAYOUT, modifier.required_min_width);
345    hash_opt_dp_value!(FACET_LAYOUT, modifier.required_max_width);
346    hash_opt_dp_value!(FACET_LAYOUT, modifier.required_min_height);
347    hash_opt_dp_value!(FACET_LAYOUT, modifier.required_max_height);
348    hash_opt_dp_value!(FACET_LAYOUT, modifier.default_min_width);
349    hash_opt_dp_value!(FACET_LAYOUT, modifier.default_min_height);
350    hash_opt_f32_value!(FACET_LAYOUT, modifier.fill_max);
351    hash_opt_f32_value!(FACET_LAYOUT, modifier.fill_max_w);
352    hash_opt_f32_value!(FACET_LAYOUT, modifier.fill_max_h);
353    hashers.hash(FACET_PAINT, |h| modifier.repaint_boundary.hash(h));
354    hash_opt_dp_value!(FACET_LAYOUT, modifier.padding);
355    match modifier.padding_values {
356        Some(padding) => hashers.hash(FACET_LAYOUT, |h| {
357            hash_dp(padding.left, h);
358            hash_dp(padding.right, h);
359            hash_dp(padding.top, h);
360            hash_dp(padding.bottom, h);
361        }),
362        None => hashers.hash(FACET_LAYOUT, |h| 0u8.hash(h)),
363    }
364    hash_opt_dp_value!(FACET_LAYOUT, modifier.min_width);
365    hash_opt_dp_value!(FACET_LAYOUT, modifier.min_height);
366    hash_opt_dp_value!(FACET_LAYOUT, modifier.max_width);
367    hash_opt_dp_value!(FACET_LAYOUT, modifier.max_height);
368    if let Some(background) = &modifier.background {
369        hashers.hash(FACET_PAINT, |h| hash_brush(background, h));
370    } else {
371        hashers.hash(FACET_PAINT, |h| 0u8.hash(h));
372    }
373    if let Some(border) = &modifier.border {
374        hashers.hash(FACET_PAINT, |h| {
375            hash_dp(border.width, h);
376            hash_brush(&border.brush, h);
377            for radius in border.radius {
378                hash_dp(radius, h);
379            }
380        });
381    } else {
382        hashers.hash(FACET_PAINT, |h| 0u8.hash(h));
383    }
384    hash_opt_f32_value!(FACET_LAYOUT, modifier.flex_grow);
385    hash_opt_f32_value!(FACET_LAYOUT, modifier.flex_shrink);
386    hash_opt_dp_value!(FACET_LAYOUT, modifier.flex_basis);
387    hashers.hash(FACET_LAYOUT, |h| {
388        modifier
389            .flex_wrap
390            .map(|value| std::mem::discriminant(&value))
391            .hash(h);
392        modifier.flex_basis_content.hash(h);
393        modifier.flex_line_count.hash(h);
394        modifier
395            .flex_dir
396            .map(|value| std::mem::discriminant(&value))
397            .hash(h);
398    });
399    hashers.hash(FACET_LAYOUT, |h| {
400        modifier
401            .align_self
402            .map(|value| (value.keyword as u8, value.safety as u8))
403            .hash(h);
404        modifier
405            .justify_content
406            .map(|value| (value.keyword as u8, value.safety as u8))
407            .hash(h);
408        modifier
409            .align_items_container
410            .map(|value| (value.keyword as u8, value.safety as u8))
411            .hash(h);
412        modifier
413            .align_content
414            .map(|value| (value.keyword as u8, value.safety as u8))
415            .hash(h);
416        modifier.baseline_align.hash(h);
417    });
418    if let Some(radius) = modifier.clip_rounded {
419        hashers.hash(FACET_PAINT, |h| {
420            for value in radius {
421                hash_dp(value, h);
422            }
423        });
424    } else {
425        hashers.hash(FACET_PAINT, |h| 0u8.hash(h));
426    }
427    if let Some(transform) = &modifier.transform {
428        hashers.hash(FACET_PAINT, |h| {
429            hash_f32(transform.translate_x, h);
430            hash_f32(transform.translate_y, h);
431            hash_f32(transform.scale_x, h);
432            hash_f32(transform.scale_y, h);
433            hash_f32(transform.rotate, h);
434            hash_f32(transform.shear_x, h);
435            hash_f32(transform.shear_y, h);
436            hash_f32(transform.origin_x, h);
437            hash_f32(transform.origin_y, h);
438            for value in transform.perspective {
439                hash_f32(value, h);
440            }
441        });
442    } else {
443        hashers.hash(FACET_PAINT, |h| 0u8.hash(h));
444    }
445    hash_opt_f32_value!(FACET_PAINT, modifier.alpha);
446    hashers.hash(FACET_LAYOUT, |h| {
447        modifier
448            .position_type
449            .map(|value| std::mem::discriminant(&value))
450            .hash(h);
451    });
452    hash_opt_dp_value!(FACET_LAYOUT, modifier.offset_left);
453    hash_opt_dp_value!(FACET_LAYOUT, modifier.offset_right);
454    hash_opt_dp_value!(FACET_LAYOUT, modifier.offset_top);
455    hash_opt_dp_value!(FACET_LAYOUT, modifier.offset_bottom);
456    if let Some(grid) = &modifier.grid {
457        hashers.hash(FACET_LAYOUT, |h| {
458            grid.columns.hash(h);
459            hash_dp(grid.row_gap, h);
460            hash_dp(grid.column_gap, h);
461        });
462    } else {
463        hashers.hash(FACET_LAYOUT, |h| 0u8.hash(h));
464    }
465    hashers.hash(FACET_LAYOUT, |h| {
466        modifier.grid_col_span.hash(h);
467        modifier.grid_row_span.hash(h);
468    });
469    hash_opt_f32_value!(FACET_LAYOUT, modifier.aspect_ratio);
470    hashers.hash(FACET_LAYOUT, |h| modifier.intrinsic_width.hash(h));
471    hashers.hash(FACET_LAYOUT, |h| modifier.intrinsic_height.hash(h));
472    hash_opt_dp_value!(FACET_LAYOUT, modifier.fit_content_width);
473    hash_opt_dp_value!(FACET_LAYOUT, modifier.fit_content_height);
474    hashers.hash(FACET_LAYOUT, |h| match modifier.contain {
475        None => 0u8.hash(h),
476        Some(value) if value == taffy::Contain::CONTENT => 3u8.hash(h),
477        Some(value) if value == taffy::Contain::LAYOUT => 1u8.hash(h),
478        Some(value) if value == taffy::Contain::PAINT => 2u8.hash(h),
479        Some(_) => 4u8.hash(h),
480    });
481    hash_f32_value!(FACET_PAINT, modifier.z_index);
482    hash_opt_f32_value!(FACET_PAINT, modifier.render_z_index);
483    hashers.hash(FACET_PAINT, |h| {
484        modifier.input_blocker.hash(h);
485        modifier.click.hash(h);
486        modifier.disabled.hash(h);
487        modifier.focusable.hash(h);
488        modifier.hit_passthrough.hash(h);
489    });
490    hashers.hash(FACET_LAYOUT, |h| modifier.propagate_min.hash(h));
491    hashers.hash(FACET_PAINT, |h| modifier.focus_group.hash(h));
492    if let Some(blur) = &modifier.blur {
493        hashers.hash(FACET_PAINT, |h| {
494            hash_dp(blur.radius_x, h);
495            hash_dp(blur.radius_y, h);
496            std::mem::discriminant(&blur.edge_treatment).hash(h);
497        });
498    } else {
499        hashers.hash(FACET_PAINT, |h| 0u8.hash(h));
500    }
501    facet_hash_scroll_binding(modifier.scroll.as_ref(), hashers);
502    facet_hash_nested_scroll_connection(modifier.nested_scroll_connection.as_ref(), hashers);
503    facet_hash_scrollbar_style(modifier.scrollbar_style.as_ref(), hashers);
504    hashers.hash(FACET_PAINT, |h| {
505        modifier
506            .overflow
507            .map(|value| std::mem::discriminant(&value))
508            .hash(h);
509    });
510    if let Some(rect) = modifier.clip_rect {
511        hashers.hash(FACET_PAINT, |h| {
512            hash_dp(rect.left, h);
513            hash_dp(rect.top, h);
514            hash_dp(rect.right, h);
515            hash_dp(rect.bottom, h);
516            std::mem::discriminant(&rect.op).hash(h);
517        });
518    } else {
519        hashers.hash(FACET_PAINT, |h| 0u8.hash(h));
520    }
521    hash_opt_dp_value!(FACET_LAYOUT, modifier.gap);
522    hash_opt_dp_value!(FACET_LAYOUT, modifier.row_gap);
523    hash_opt_dp_value!(FACET_LAYOUT, modifier.column_gap);
524    hash_opt_dp_value!(FACET_LAYOUT, modifier.margin_top);
525    hash_opt_dp_value!(FACET_LAYOUT, modifier.margin_left);
526    hash_opt_dp_value!(FACET_LAYOUT, modifier.margin_right);
527    hash_opt_dp_value!(FACET_LAYOUT, modifier.margin_bottom);
528    hash_opt_f32_value!(FACET_PAINT, modifier.graphics_layer);
529    if let Some(shadow) = &modifier.shadow {
530        hashers.hash(FACET_PAINT, |h| {
531            hash_dp(shadow.blur_radius, h);
532            hash_dp(shadow.offset_y, h);
533            hash_color(&shadow.color, h);
534        });
535    } else {
536        hashers.hash(FACET_PAINT, |h| 0u8.hash(h));
537    }
538    if let Some(radius) = modifier.shadow_shape {
539        hashers.hash(FACET_PAINT, |h| {
540            for value in radius {
541                hash_dp(value, h);
542            }
543        });
544    } else {
545        hashers.hash(FACET_PAINT, |h| 0u8.hash(h));
546    }
547    facet_hash_semantics(modifier.semantics.as_ref(), hashers, FACET_SEMANTICS);
548    hashers.hash(FACET_PAINT, |h| {
549        hash_cursor(&modifier.cursor, h);
550        modifier.painter.is_some().hash(h);
551        modifier.text_selection.is_some().hash(h);
552        modifier.paint_callback.is_some().hash(h);
553        modifier.indication.is_some().hash(h);
554        modifier.drag_preview.is_some().hash(h);
555        modifier.on_scroll.is_some().hash(h);
556        modifier.on_pointer_down.is_some().hash(h);
557        modifier.on_pointer_move.is_some().hash(h);
558        modifier.on_pointer_up.is_some().hash(h);
559        modifier.on_pointer_cancel.is_some().hash(h);
560        modifier.on_pointer_enter.is_some().hash(h);
561        modifier.on_pointer_leave.is_some().hash(h);
562        modifier.on_click.is_some().hash(h);
563        modifier.on_double_click.is_some().hash(h);
564        modifier.on_long_click.is_some().hash(h);
565        modifier.on_globally_positioned.is_some().hash(h);
566        modifier.on_size_changed.is_some().hash(h);
567        modifier.on_key_event.is_some().hash(h);
568        modifier.on_preview_key_event.is_some().hash(h);
569        modifier.on_ime.is_some().hash(h);
570        modifier.on_drag_start.is_some().hash(h);
571        modifier.on_drag_end.is_some().hash(h);
572        modifier.on_drag_enter.is_some().hash(h);
573        modifier.on_drag_over.is_some().hash(h);
574        modifier.on_drag_leave.is_some().hash(h);
575        modifier.on_drop.is_some().hash(h);
576        modifier.on_action.is_some().hash(h);
577        modifier.focus_requester.is_some().hash(h);
578        modifier.on_focus_changed.is_some().hash(h);
579        modifier.interaction_source.is_some().hash(h);
580    });
581    hashers.hash(FACET_LAYOUT, |h| {
582        if let Some(layout) = &modifier.layout {
583            1u8.hash(h);
584            hash_rc_ptr_identity(layout, h);
585        } else {
586            0u8.hash(h);
587        }
588    });
589    if let Some(input) = &modifier.text_input {
590        facet_hash_text_input(input, hashers);
591    } else {
592        hashers.hash(FACET_ALL, |h| 0u8.hash(h));
593    }
594    if let Some(colors) = &modifier.state_colors {
595        hashers.hash(FACET_PAINT, |h| {
596            hash_color(&colors.default, h);
597            hash_color(&colors.hovered, h);
598            hash_color(&colors.focused, h);
599            hash_color(&colors.pressed, h);
600            hash_color(&colors.disabled, h);
601            hash_color(&colors.dragged, h);
602        });
603    } else {
604        hashers.hash(FACET_PAINT, |h| 0u8.hash(h));
605    }
606    if let Some(elevation) = &modifier.state_elevation {
607        hashers.hash(FACET_PAINT, |h| {
608            hash_dp(elevation.default, h);
609            hash_dp(elevation.hovered, h);
610            hash_dp(elevation.focused, h);
611            hash_dp(elevation.pressed, h);
612            hash_dp(elevation.disabled, h);
613            hash_dp(elevation.dragged, h);
614        });
615    } else {
616        hashers.hash(FACET_PAINT, |h| 0u8.hash(h));
617    }
618    if let Some(spec) = &modifier.animate_content_size {
619        facet_hash_animation_spec(spec, hashers, FACET_LAYOUT);
620    } else {
621        hashers.hash(FACET_LAYOUT, |h| 0u8.hash(h));
622    }
623}
624
625fn facet_hash_semantics(
626    semantics: Option<&repose_core::Semantics>,
627    hashers: &mut FacetHashers,
628    mask: u8,
629) {
630    hashers.hash(mask, |hasher| {
631        if let Some(semantics) = semantics {
632            1u8.hash(hasher);
633            std::mem::discriminant(&semantics.role).hash(hasher);
634            semantics.label.hash(hasher);
635            semantics.focused.hash(hasher);
636            semantics.enabled.hash(hasher);
637            semantics.selectable_group.hash(hasher);
638            semantics.checked.hash(hasher);
639            semantics.selected.hash(hasher);
640            semantics.value.hash(hasher);
641        } else {
642            0u8.hash(hasher);
643        }
644    });
645}
646
647fn facet_hash_span_style(style: &SpanStyle, hashers: &mut FacetHashers, mask: u8) {
648    hashers.hash(mask & FACET_PAINT, |h| {
649        hash_opt_color(style.color.as_ref(), h)
650    });
651    hashers.hash(mask & FACET_LAYOUT_MEASUREMENT, |h| {
652        if let Some(size) = style.font_size {
653            1u8.hash(h);
654            hash_sp(size, h);
655        } else {
656            0u8.hash(h);
657        }
658    });
659    hashers.hash(mask & FACET_LAYOUT_MEASUREMENT, |h| {
660        style.font_weight.hash(h);
661        style.font_family.hash(h);
662        style.font_style.hash(h);
663        style.text_align.hash(h);
664        if let Some(value) = style.letter_spacing {
665            1u8.hash(h);
666            hash_sp(value, h);
667        } else {
668            0u8.hash(h);
669        }
670        if let Some(value) = style.line_height {
671            1u8.hash(h);
672            hash_sp(value, h);
673        } else {
674            0u8.hash(h);
675        }
676    });
677    hashers.hash(mask & FACET_PAINT, |h| {
678        hash_opt_color(style.background.as_ref(), h);
679        if let Some(decoration) = &style.text_decoration {
680            1u8.hash(h);
681            hash_text_decoration(decoration, h);
682        } else {
683            0u8.hash(h);
684        }
685        hash_f32(style.alpha, h);
686    });
687    hashers.hash(mask & FACET_LAYOUT_MEASUREMENT, |h| {
688        hash_discriminant_option(&style.text_direction, h);
689        hash_discriminant_option(&style.font_synthesis, h);
690        if let Some(value) = style.baseline_shift {
691            1u8.hash(h);
692            hash_f32(value.0, h);
693        } else {
694            0u8.hash(h);
695        }
696        hash_discriminant_option(&style.hyphens, h);
697        hash_discriminant_option(&style.line_break, h);
698        if let Some(indent) = &style.text_indent {
699            1u8.hash(h);
700            hash_dp(indent.first_line, h);
701            hash_dp(indent.rest_lines, h);
702        } else {
703            0u8.hash(h);
704        }
705    });
706    hashers.hash(mask & FACET_LAYOUT_MEASUREMENT_PAINT, |h| {
707        if let Some(style) = &style.draw_style {
708            1u8.hash(h);
709            hash_draw_style(style, h);
710        } else {
711            0u8.hash(h);
712        }
713    });
714    hashers.hash(mask & FACET_LAYOUT_MEASUREMENT, |h| {
715        style.font_variation_settings.hash(h);
716    });
717}
718
719fn facet_hash_text_style(style: &TextStyle, hashers: &mut FacetHashers) {
720    hashers.hash(FACET_LAYOUT_MEASUREMENT, |h| {
721        hash_sp(style.font_size, h);
722        style.font_weight.hash(h);
723        style.font_family.hash(h);
724        style.font_style.hash(h);
725        style.text_align.hash(h);
726        hash_sp(style.letter_spacing, h);
727        hash_sp(style.line_height, h);
728        hash_discriminant_option(&style.text_direction, h);
729        std::mem::discriminant(&style.font_synthesis).hash(h);
730        hash_f32(style.baseline_shift.0, h);
731        std::mem::discriminant(&style.hyphens).hash(h);
732        std::mem::discriminant(&style.line_break).hash(h);
733        if let Some(indent) = &style.text_indent {
734            1u8.hash(h);
735            hash_dp(indent.first_line, h);
736            hash_dp(indent.rest_lines, h);
737        } else {
738            0u8.hash(h);
739        }
740        hash_draw_style(&style.draw_style, h);
741        style.locale_list.hash(h);
742        style.font_feature_settings.hash(h);
743        style.font_variation_settings.hash(h);
744    });
745    hashers.hash(FACET_PAINT, |h| {
746        hash_opt_color(style.color.as_ref(), h);
747        hash_opt_color(style.background.as_ref(), h);
748        if let Some(decoration) = &style.text_decoration {
749            1u8.hash(h);
750            hash_text_decoration(decoration, h);
751        } else {
752            0u8.hash(h);
753        }
754        if let Some(shadow) = &style.shadow {
755            1u8.hash(h);
756            hash_shadow(shadow, h);
757        } else {
758            0u8.hash(h);
759        }
760        hash_f32(style.alpha, h);
761        hash_draw_style(&style.draw_style, h);
762    });
763}
764
765fn facet_hash_text_input(input: &repose_core::TextInputConfig, hashers: &mut FacetHashers) {
766    hashers.hash(FACET_LAYOUT_MEASUREMENT, |h| {
767        input.hint.hash(h);
768        input.multiline.hash(h);
769        input.value.hash(h);
770        input.max_lines.hash(h);
771        input.min_lines.hash(h);
772        match &input.line_limits {
773            Some(repose_core::text::TextFieldLineLimits::SingleLine) => 1u8.hash(h),
774            Some(repose_core::text::TextFieldLineLimits::MultiLine {
775                min_height_in_lines,
776                max_height_in_lines,
777            }) => {
778                2u8.hash(h);
779                min_height_in_lines.hash(h);
780                max_height_in_lines.hash(h);
781            }
782            None => 0u8.hash(h),
783        }
784    });
785    hashers.hash(FACET_PAINT, |h| {
786        input.on_change.is_some().hash(h);
787        input.on_submit.is_some().hash(h);
788        input.focus_tracker.is_some().hash(h);
789        input.enabled.hash(h);
790        input.read_only.hash(h);
791        input.sensitive.hash(h);
792        std::mem::discriminant(&input.keyboard_type).hash(h);
793        std::mem::discriminant(&input.capitalization).hash(h);
794        std::mem::discriminant(&input.ime_action).hash(h);
795        input.auto_correct_enabled.hash(h);
796        if let Some(color) = input.cursor_color {
797            1u8.hash(h);
798            hash_color(&color, h);
799        } else {
800            0u8.hash(h);
801        }
802        input.on_text_layout.is_some().hash(h);
803        input.interaction_source.is_some().hash(h);
804        input.visual_transformation.is_some().hash(h);
805        if let Some(actions) = &input.keyboard_actions {
806            1u8.hash(h);
807            actions.on_done.is_some().hash(h);
808            actions.on_go.is_some().hash(h);
809            actions.on_next.is_some().hash(h);
810            actions.on_previous.is_some().hash(h);
811            actions.on_search.is_some().hash(h);
812            actions.on_send.is_some().hash(h);
813        } else {
814            0u8.hash(h);
815        }
816    });
817    if let Some(style) = &input.text_style {
818        hashers.hash(FACET_LAYOUT_MEASUREMENT_PAINT, |h| 1u8.hash(h));
819        facet_hash_text_style(style, hashers);
820    } else {
821        hashers.hash(FACET_LAYOUT_MEASUREMENT_PAINT, |h| 0u8.hash(h));
822    }
823}
824
825fn facet_hash_nested_scroll_connection(
826    connection: Option<&repose_core::nested_scroll::NestedScrollConnection>,
827    hashers: &mut FacetHashers,
828) {
829    hashers.hash(FACET_PAINT, |h| match connection {
830        Some(connection) => {
831            1u8.hash(h);
832            if let Some(callback) = &connection.on_pre_scroll {
833                1u8.hash(h);
834                hash_rc_ptr_identity(callback, h);
835            } else {
836                0u8.hash(h);
837            }
838            if let Some(callback) = &connection.on_post_scroll {
839                1u8.hash(h);
840                hash_rc_ptr_identity(callback, h);
841            } else {
842                0u8.hash(h);
843            }
844            if let Some(callback) = &connection.on_pre_fling {
845                1u8.hash(h);
846                hash_rc_ptr_identity(callback, h);
847            } else {
848                0u8.hash(h);
849            }
850            if let Some(callback) = &connection.on_post_fling {
851                1u8.hash(h);
852                hash_rc_ptr_identity(callback, h);
853            } else {
854                0u8.hash(h);
855            }
856        }
857        None => 0u8.hash(h),
858    });
859}
860
861fn facet_hash_scrollbar_style(
862    style: Option<&repose_core::ScrollbarStyle>,
863    hashers: &mut FacetHashers,
864) {
865    let Some(style) = style else {
866        hashers.hash(FACET_PAINT, |h| 0u8.hash(h));
867        return;
868    };
869    hashers.hash(FACET_PAINT, |h| {
870        1u8.hash(h);
871        style.thickness.0.to_bits().hash(h);
872        style.track_inset.0.to_bits().hash(h);
873        style.min_thumb_length.0.to_bits().hash(h);
874        style
875            .fixed_thumb_length
876            .map(|value| value.0.to_bits())
877            .hash(h);
878        style.radius.map(|value| value.0.to_bits()).hash(h);
879    });
880    facet_hash_control_visual_set(&style.track_visuals, hashers);
881    facet_hash_control_visual_set(&style.thumb_visuals, hashers);
882}
883
884fn facet_hash_control_visual_set(set: &repose_core::ControlVisualSet, hashers: &mut FacetHashers) {
885    for visual in [
886        &set.normal,
887        &set.hovered,
888        &set.pressed,
889        &set.dragged,
890        &set.focused,
891        &set.disabled,
892    ] {
893        match visual {
894            Some(visual) => {
895                hashers.hash(FACET_PAINT, |h| 1u8.hash(h));
896                facet_hash_control_visual(visual, hashers);
897            }
898            None => hashers.hash(FACET_PAINT, |h| 0u8.hash(h)),
899        }
900    }
901}
902
903fn facet_hash_control_visual(visual: &repose_core::ControlVisual, hashers: &mut FacetHashers) {
904    hashers.hash(FACET_PAINT, |h| std::mem::discriminant(visual).hash(h));
905    match visual {
906        repose_core::ControlVisual::Rect { brush, radius } => {
907            hashers.hash(FACET_PAINT, |h| {
908                hash_brush(brush, h);
909                for value in radius {
910                    value.0.to_bits().hash(h);
911                }
912            });
913        }
914        repose_core::ControlVisual::Image {
915            handle,
916            source_rect,
917            tint,
918            fit,
919            filter,
920            alignment,
921        } => {
922            hashers.hash(FACET_PAINT, |h| {
923                handle.hash(h);
924                source_rect.hash(h);
925                hash_color(tint, h);
926                std::mem::discriminant(fit).hash(h);
927                std::mem::discriminant(filter).hash(h);
928                std::mem::discriminant(alignment).hash(h);
929            });
930        }
931        repose_core::ControlVisual::Custom(_) => {
932            hashers.hash(FACET_PAINT, |h| {
933                visual.custom_identity().unwrap_or_default().hash(h)
934            });
935        }
936        _ => {}
937    }
938}
939
940fn facet_hash_scroll_binding(binding: Option<&ScrollBinding>, hashers: &mut FacetHashers) {
941    match binding {
942        Some(ScrollBinding::Vertical(binding)) => {
943            hashers.hash(FACET_LAYOUT_PAINT, |h| 1u8.hash(h));
944            facet_hash_scroll_axis(binding, hashers);
945        }
946        Some(ScrollBinding::Horizontal(binding)) => {
947            hashers.hash(FACET_LAYOUT_PAINT, |h| 2u8.hash(h));
948            facet_hash_scroll_axis(binding, hashers);
949        }
950        Some(ScrollBinding::Both(binding)) => {
951            hashers.hash(FACET_LAYOUT_PAINT, |h| 3u8.hash(h));
952            hashers.hash(FACET_LAYOUT, |h| binding.show_scrollbar.hash(h));
953            hashers.hash(FACET_PAINT, |h| {
954                binding.show_scrollbar.hash(h);
955                hash_rc_identity(&binding.on_scroll, h);
956                hash_rc_identity(&binding.set_viewport_width, h);
957                hash_rc_identity(&binding.set_viewport_height, h);
958                hash_rc_identity(&binding.set_content_width, h);
959                hash_rc_identity(&binding.set_content_height, h);
960                hash_rc_identity(&binding.get_offset_xy, h);
961                hash_rc_identity(&binding.set_offset_xy, h);
962                hash_rc_identity(&binding.tick, h);
963                hash_rc_identity(&binding.set_nested_scroll_parent, h);
964            });
965        }
966        None => hashers.hash(FACET_LAYOUT_PAINT, |h| 0u8.hash(h)),
967    }
968}
969
970fn facet_hash_scroll_axis(
971    binding: &repose_core::scroll::ScrollAxisBinding,
972    hashers: &mut FacetHashers,
973) {
974    hashers.hash(FACET_LAYOUT, |h| binding.show_scrollbar.hash(h));
975    hashers.hash(FACET_PAINT, |h| {
976        binding.show_scrollbar.hash(h);
977        hash_rc_identity(&binding.on_scroll, h);
978        hash_rc_identity(&binding.set_viewport_main, h);
979        hash_rc_identity(&binding.set_content_main, h);
980        hash_rc_identity(&binding.get_offset_main, h);
981        hash_rc_identity(&binding.set_offset_main, h);
982        hash_rc_identity(&binding.tick, h);
983        hash_rc_identity(&binding.set_nested_scroll_parent, h);
984    });
985}
986
987fn facet_hash_animation_spec(spec: &AnimationSpec, hashers: &mut FacetHashers, mask: u8) {
988    hashers.hash(mask, |h| {
989        spec.duration.as_nanos().hash(h);
990        hash_easing(&spec.easing, h);
991        spec.delay.as_nanos().hash(h);
992        if let Some(spring) = &spec.spring {
993            1u8.hash(h);
994            hash_f32(spring.damping_ratio, h);
995            hash_f32(spring.stiffness, h);
996            hash_f32(spring.settle_progress, h);
997            hash_f32(spring.settle_velocity, h);
998        } else {
999            0u8.hash(h);
1000        }
1001        if let Some(repeat) = &spec.repeat {
1002            1u8.hash(h);
1003            repeat.iterations.hash(h);
1004            repeat.reverse.hash(h);
1005            repeat.delay_between.as_nanos().hash(h);
1006        } else {
1007            0u8.hash(h);
1008        }
1009    });
1010}
1011
1012pub fn hash_subtree(content_hash: u64, children_hashes: &[u64]) -> u64 {
1013    let mut hasher = RapidHasher::default();
1014    content_hash.hash(&mut hasher);
1015    children_hashes.len().hash(&mut hasher);
1016    for hash in children_hashes {
1017        hash.hash(&mut hasher);
1018    }
1019    hasher.finish()
1020}
1021
1022fn hash_opt_color(color: Option<&Color>, hasher: &mut impl Hasher) {
1023    match color {
1024        Some(color) => {
1025            1u8.hash(hasher);
1026            hash_color(color, hasher);
1027        }
1028        None => 0u8.hash(hasher),
1029    }
1030}
1031
1032fn hash_path_effect(path_effect: &Option<repose_core::PathEffect>, hasher: &mut impl Hasher) {
1033    match path_effect {
1034        Some(repose_core::PathEffect::Corner { radius }) => {
1035            0u8.hash(hasher);
1036            hash_f32(*radius, hasher);
1037        }
1038        Some(repose_core::PathEffect::Dash { intervals, phase }) => {
1039            1u8.hash(hasher);
1040            intervals.len().hash(hasher);
1041            for interval in intervals {
1042                hash_f32(*interval, hasher);
1043            }
1044            hash_f32(*phase, hasher);
1045        }
1046        None => 2u8.hash(hasher),
1047    }
1048}
1049
1050fn hash_draw_style(style: &repose_core::DrawStyle, hasher: &mut impl Hasher) {
1051    match style {
1052        repose_core::DrawStyle::Fill => 0u8.hash(hasher),
1053        repose_core::DrawStyle::Stroke {
1054            width,
1055            cap,
1056            join,
1057            miter,
1058            path_effect,
1059        }
1060        | repose_core::DrawStyle::FillAndStroke {
1061            width,
1062            cap,
1063            join,
1064            miter,
1065            path_effect,
1066            ..
1067        } => {
1068            std::mem::discriminant(style).hash(hasher);
1069            if let repose_core::DrawStyle::FillAndStroke { outline_color, .. } = style {
1070                hash_opt_color(outline_color.as_ref(), hasher);
1071            }
1072            hash_f32(*width, hasher);
1073            (*cap as u8).hash(hasher);
1074            (*join as u8).hash(hasher);
1075            hash_f32(*miter, hasher);
1076            hash_path_effect(path_effect, hasher);
1077        }
1078    }
1079}
1080
1081fn hash_text_decoration(decoration: &repose_core::TextDecoration, hasher: &mut impl Hasher) {
1082    decoration.underline.hash(hasher);
1083    decoration.strikethrough.hash(hasher);
1084    hash_opt_color(decoration.color.as_ref(), hasher);
1085}
1086
1087fn hash_shadow(shadow: &Shadow, hasher: &mut impl Hasher) {
1088    hash_color(&shadow.color, hasher);
1089    hash_dp(shadow.offset_x, hasher);
1090    hash_dp(shadow.offset_y, hasher);
1091    hash_dp(shadow.blur_radius, hasher);
1092}
1093
1094#[allow(dead_code)]
1095fn hash_text_style(style: &TextStyle, hasher: &mut impl Hasher) {
1096    hash_sp(style.font_size, hasher);
1097    hash_opt_color(style.color.as_ref(), hasher);
1098    style.font_weight.hash(hasher);
1099    style.font_family.hash(hasher);
1100    style.font_style.hash(hasher);
1101    style.text_align.hash(hasher);
1102    hash_sp(style.letter_spacing, hasher);
1103    hash_sp(style.line_height, hasher);
1104    hash_opt_color(style.background.as_ref(), hasher);
1105    match &style.text_decoration {
1106        Some(decoration) => {
1107            1u8.hash(hasher);
1108            hash_text_decoration(decoration, hasher);
1109        }
1110        None => 0u8.hash(hasher),
1111    }
1112    match &style.shadow {
1113        Some(shadow) => {
1114            1u8.hash(hasher);
1115            hash_shadow(shadow, hasher);
1116        }
1117        None => 0u8.hash(hasher),
1118    }
1119    hash_discriminant_option(&style.text_direction, hasher);
1120    std::mem::discriminant(&style.font_synthesis).hash(hasher);
1121    hash_f32(style.baseline_shift.0, hasher);
1122    std::mem::discriminant(&style.hyphens).hash(hasher);
1123    std::mem::discriminant(&style.line_break).hash(hasher);
1124    match &style.text_indent {
1125        Some(indent) => {
1126            1u8.hash(hasher);
1127            hash_dp(indent.first_line, hasher);
1128            hash_dp(indent.rest_lines, hasher);
1129        }
1130        None => 0u8.hash(hasher),
1131    }
1132    hash_draw_style(&style.draw_style, hasher);
1133    hash_f32(style.alpha, hasher);
1134    style.locale_list.hash(hasher);
1135    style.font_feature_settings.hash(hasher);
1136    style.font_variation_settings.hash(hasher);
1137}
1138
1139#[allow(dead_code)]
1140fn hash_span_style(style: &SpanStyle, hasher: &mut impl Hasher) {
1141    hash_opt_color(style.color.as_ref(), hasher);
1142    match style.font_size {
1143        Some(size) => {
1144            1u8.hash(hasher);
1145            hash_sp(size, hasher);
1146        }
1147        None => 0u8.hash(hasher),
1148    }
1149    style.font_weight.hash(hasher);
1150    style.font_family.hash(hasher);
1151    style.font_style.hash(hasher);
1152    style.text_align.hash(hasher);
1153    match style.letter_spacing {
1154        Some(value) => {
1155            1u8.hash(hasher);
1156            hash_sp(value, hasher);
1157        }
1158        None => 0u8.hash(hasher),
1159    }
1160    match style.line_height {
1161        Some(value) => {
1162            1u8.hash(hasher);
1163            hash_sp(value, hasher);
1164        }
1165        None => 0u8.hash(hasher),
1166    }
1167    hash_opt_color(style.background.as_ref(), hasher);
1168    match &style.text_decoration {
1169        Some(decoration) => {
1170            1u8.hash(hasher);
1171            hash_text_decoration(decoration, hasher);
1172        }
1173        None => 0u8.hash(hasher),
1174    }
1175    hash_discriminant_option(&style.text_direction, hasher);
1176    hash_discriminant_option(&style.font_synthesis, hasher);
1177    match style.baseline_shift {
1178        Some(value) => {
1179            1u8.hash(hasher);
1180            hash_f32(value.0, hasher);
1181        }
1182        None => 0u8.hash(hasher),
1183    }
1184    hash_discriminant_option(&style.hyphens, hasher);
1185    hash_discriminant_option(&style.line_break, hasher);
1186    match &style.text_indent {
1187        Some(indent) => {
1188            1u8.hash(hasher);
1189            hash_dp(indent.first_line, hasher);
1190            hash_dp(indent.rest_lines, hasher);
1191        }
1192        None => 0u8.hash(hasher),
1193    }
1194    match &style.draw_style {
1195        Some(style) => {
1196            1u8.hash(hasher);
1197            hash_draw_style(style, hasher);
1198        }
1199        None => 0u8.hash(hasher),
1200    }
1201    hash_f32(style.alpha, hasher);
1202    style.font_variation_settings.hash(hasher);
1203}
1204
1205#[allow(dead_code)]
1206fn hash_view_kind(kind: &ViewKind, hasher: &mut impl Hasher) {
1207    std::mem::discriminant(kind).hash(hasher);
1208    match kind {
1209        ViewKind::Text {
1210            text,
1211            color,
1212            font_size,
1213            soft_wrap,
1214            max_lines,
1215            overflow,
1216            font_family,
1217            annotations,
1218            text_align,
1219            font_weight,
1220            font_style,
1221            text_decoration,
1222            letter_spacing,
1223            line_height,
1224            url,
1225            font_variation_settings,
1226            draw_style,
1227        } => {
1228            text.hash(hasher);
1229            hash_color(color, hasher);
1230            hash_sp(*font_size, hasher);
1231            soft_wrap.hash(hasher);
1232            max_lines.hash(hasher);
1233            hash_text_overflow(overflow, hasher);
1234            font_family.hash(hasher);
1235            text_align.hash(hasher);
1236            font_weight.0.hash(hasher);
1237            font_style.hash(hasher);
1238            hash_text_decoration(text_decoration, hasher);
1239            hash_sp(*letter_spacing, hasher);
1240            hash_sp(*line_height, hasher);
1241            url.hash(hasher);
1242            font_variation_settings.hash(hasher);
1243            hash_draw_style(draw_style, hasher);
1244            match annotations {
1245                Some(annotations) => {
1246                    1u8.hash(hasher);
1247                    annotations.len().hash(hasher);
1248                    for span in annotations.iter() {
1249                        span.start.hash(hasher);
1250                        span.end.hash(hasher);
1251                        hash_span_style(&span.style, hasher);
1252                        match &span.url {
1253                            Some(url) => {
1254                                1u8.hash(hasher);
1255                                url.hash(hasher);
1256                            }
1257                            None => 0u8.hash(hasher),
1258                        }
1259                    }
1260                }
1261                None => 0u8.hash(hasher),
1262            }
1263        }
1264        ViewKind::Image {
1265            handle,
1266            tint,
1267            fit,
1268            filter,
1269            source_rect,
1270            alignment,
1271        } => {
1272            handle.hash(hasher);
1273            repose_core::image_intrinsic_size(*handle).hash(hasher);
1274            hash_color(tint, hasher);
1275            std::mem::discriminant(fit).hash(hasher);
1276            std::mem::discriminant(filter).hash(hasher);
1277            source_rect.hash(hasher);
1278            std::mem::discriminant(alignment).hash(hasher);
1279        }
1280        ViewKind::OverlayHost
1281        | ViewKind::Box
1282        | ViewKind::Row
1283        | ViewKind::Column
1284        | ViewKind::ZStack
1285        | ViewKind::SubcomposeLayout { .. } => {}
1286        _ => {}
1287    }
1288}
1289
1290fn hash_discriminant_option<T>(value: &Option<T>, hasher: &mut impl Hasher) {
1291    match value {
1292        Some(value) => {
1293            1u8.hash(hasher);
1294            std::mem::discriminant(value).hash(hasher);
1295        }
1296        None => 0u8.hash(hasher),
1297    }
1298}
1299
1300fn hash_f32(v: f32, hasher: &mut impl Hasher) {
1301    v.to_bits().hash(hasher);
1302}
1303
1304fn hash_rc_identity<T: ?Sized>(value: &Option<std::rc::Rc<T>>, hasher: &mut impl Hasher) {
1305    value.is_some().hash(hasher);
1306}
1307
1308fn hash_rc_ptr_identity<T: ?Sized>(value: &std::rc::Rc<T>, hasher: &mut impl Hasher) {
1309    (std::rc::Rc::as_ptr(value) as *const () as usize).hash(hasher);
1310}
1311
1312#[allow(dead_code)]
1313fn hash_control_visual(visual: &repose_core::ControlVisual, hasher: &mut impl Hasher) {
1314    std::mem::discriminant(visual).hash(hasher);
1315    match visual {
1316        repose_core::ControlVisual::Rect { brush, radius } => {
1317            hash_brush(brush, hasher);
1318            for value in radius {
1319                value.0.to_bits().hash(hasher);
1320            }
1321        }
1322        repose_core::ControlVisual::Image {
1323            handle,
1324            source_rect,
1325            tint,
1326            fit,
1327            filter,
1328            alignment,
1329        } => {
1330            handle.hash(hasher);
1331            source_rect.hash(hasher);
1332            hash_color(tint, hasher);
1333            std::mem::discriminant(fit).hash(hasher);
1334            std::mem::discriminant(filter).hash(hasher);
1335            std::mem::discriminant(alignment).hash(hasher);
1336        }
1337        repose_core::ControlVisual::Custom(_) => {
1338            visual.custom_identity().unwrap_or_default().hash(hasher)
1339        }
1340        _ => {}
1341    }
1342}
1343
1344#[allow(dead_code)]
1345fn hash_control_visual_set(set: &repose_core::ControlVisualSet, hasher: &mut impl Hasher) {
1346    for visual in [
1347        &set.normal,
1348        &set.hovered,
1349        &set.pressed,
1350        &set.dragged,
1351        &set.focused,
1352        &set.disabled,
1353    ] {
1354        match visual {
1355            Some(visual) => {
1356                true.hash(hasher);
1357                hash_control_visual(visual, hasher);
1358            }
1359            None => false.hash(hasher),
1360        }
1361    }
1362}
1363
1364#[allow(dead_code)]
1365fn hash_scrollbar_style(style: Option<&repose_core::ScrollbarStyle>, hasher: &mut impl Hasher) {
1366    let Some(style) = style else {
1367        0u8.hash(hasher);
1368        return;
1369    };
1370    1u8.hash(hasher);
1371    style.thickness.0.to_bits().hash(hasher);
1372    style.track_inset.0.to_bits().hash(hasher);
1373    style.min_thumb_length.0.to_bits().hash(hasher);
1374    style
1375        .fixed_thumb_length
1376        .map(|value| value.0.to_bits())
1377        .hash(hasher);
1378    style.radius.map(|value| value.0.to_bits()).hash(hasher);
1379    hash_control_visual_set(&style.track_visuals, hasher);
1380    hash_control_visual_set(&style.thumb_visuals, hasher);
1381}
1382
1383#[allow(dead_code)]
1384fn hash_scroll_axis_binding(
1385    binding: &repose_core::scroll::ScrollAxisBinding,
1386    hasher: &mut impl Hasher,
1387) {
1388    binding.show_scrollbar.hash(hasher);
1389    hash_rc_identity(&binding.on_scroll, hasher);
1390    hash_rc_identity(&binding.set_viewport_main, hasher);
1391    hash_rc_identity(&binding.set_content_main, hasher);
1392    hash_rc_identity(&binding.get_offset_main, hasher);
1393    hash_rc_identity(&binding.set_offset_main, hasher);
1394    hash_rc_identity(&binding.tick, hasher);
1395    hash_rc_identity(&binding.set_nested_scroll_parent, hasher);
1396}
1397
1398#[allow(dead_code)]
1399fn hash_scroll_binding(binding: &ScrollBinding, hasher: &mut impl Hasher) {
1400    match binding {
1401        ScrollBinding::Vertical(binding) => {
1402            1u8.hash(hasher);
1403            hash_scroll_axis_binding(binding, hasher);
1404        }
1405        ScrollBinding::Horizontal(binding) => {
1406            2u8.hash(hasher);
1407            hash_scroll_axis_binding(binding, hasher);
1408        }
1409        ScrollBinding::Both(binding) => {
1410            3u8.hash(hasher);
1411            binding.show_scrollbar.hash(hasher);
1412            hash_rc_identity(&binding.on_scroll, hasher);
1413            hash_rc_identity(&binding.set_viewport_width, hasher);
1414            hash_rc_identity(&binding.set_viewport_height, hasher);
1415            hash_rc_identity(&binding.set_content_width, hasher);
1416            hash_rc_identity(&binding.set_content_height, hasher);
1417            hash_rc_identity(&binding.get_offset_xy, hasher);
1418            hash_rc_identity(&binding.set_offset_xy, hasher);
1419            hash_rc_identity(&binding.tick, hasher);
1420            hash_rc_identity(&binding.set_nested_scroll_parent, hasher);
1421        }
1422    }
1423}
1424
1425#[allow(dead_code)]
1426fn hash_cursor(cursor: &Option<repose_core::CursorIcon>, hasher: &mut impl Hasher) {
1427    match cursor {
1428        Some(repose_core::CursorIcon::Custom(image)) => {
1429            10u8.hash(hasher);
1430            image.size.hash(hasher);
1431            image.hotspot.hash(hasher);
1432            image.rgba.hash(hasher);
1433        }
1434        Some(cursor) => {
1435            1u8.hash(hasher);
1436            std::mem::discriminant(cursor).hash(hasher);
1437        }
1438        None => 0u8.hash(hasher),
1439    }
1440}
1441
1442fn hash_opt_f32(v: Option<f32>, hasher: &mut impl Hasher) {
1443    match v {
1444        Some(value) => {
1445            1u8.hash(hasher);
1446            hash_f32(value, hasher);
1447        }
1448        None => 0u8.hash(hasher),
1449    }
1450}
1451
1452fn hash_dp(v: Dp, hasher: &mut impl Hasher) {
1453    hash_f32(v.0, hasher);
1454}
1455
1456fn hash_opt_dp(v: Option<Dp>, hasher: &mut impl Hasher) {
1457    match v {
1458        Some(value) => {
1459            1u8.hash(hasher);
1460            hash_dp(value, hasher);
1461        }
1462        None => 0u8.hash(hasher),
1463    }
1464}
1465
1466fn hash_sp(v: Sp, hasher: &mut impl Hasher) {
1467    hash_f32(v.0, hasher);
1468}
1469
1470#[allow(dead_code)]
1471fn hash_px(v: Px, hasher: &mut impl Hasher) {
1472    hash_f32(v.0, hasher);
1473}
1474
1475#[allow(dead_code)]
1476fn hash_modifier(m: &Modifier, hasher: &mut impl Hasher) {
1477    match m.size {
1478        Some(size) => {
1479            1u8.hash(hasher);
1480            hash_dp(size.width, hasher);
1481            hash_dp(size.height, hasher);
1482        }
1483        None => 0u8.hash(hasher),
1484    }
1485    hash_opt_dp(m.width, hasher);
1486    hash_opt_dp(m.height, hasher);
1487    match m.required_size {
1488        Some(size) => {
1489            1u8.hash(hasher);
1490            hash_dp(size.width, hasher);
1491            hash_dp(size.height, hasher);
1492        }
1493        None => 0u8.hash(hasher),
1494    }
1495    hash_opt_dp(m.required_min_width, hasher);
1496    hash_opt_dp(m.required_max_width, hasher);
1497    hash_opt_dp(m.required_min_height, hasher);
1498    hash_opt_dp(m.required_max_height, hasher);
1499    hash_opt_dp(m.default_min_width, hasher);
1500    hash_opt_dp(m.default_min_height, hasher);
1501    hash_opt_f32(m.fill_max, hasher);
1502    hash_opt_f32(m.fill_max_w, hasher);
1503    hash_opt_f32(m.fill_max_h, hasher);
1504    m.repaint_boundary.hash(hasher);
1505    hash_opt_dp(m.padding, hasher);
1506    match m.padding_values {
1507        Some(padding) => {
1508            1u8.hash(hasher);
1509            hash_dp(padding.left, hasher);
1510            hash_dp(padding.right, hasher);
1511            hash_dp(padding.top, hasher);
1512            hash_dp(padding.bottom, hasher);
1513        }
1514        None => 0u8.hash(hasher),
1515    }
1516    hash_opt_dp(m.min_width, hasher);
1517    hash_opt_dp(m.min_height, hasher);
1518    hash_opt_dp(m.max_width, hasher);
1519    hash_opt_dp(m.max_height, hasher);
1520    match &m.background {
1521        Some(background) => {
1522            1u8.hash(hasher);
1523            hash_brush(background, hasher);
1524        }
1525        None => 0u8.hash(hasher),
1526    }
1527    match &m.border {
1528        Some(border) => {
1529            1u8.hash(hasher);
1530            hash_dp(border.width, hasher);
1531            hash_brush(&border.brush, hasher);
1532            for &radius in &border.radius {
1533                hash_dp(radius, hasher);
1534            }
1535        }
1536        None => 0u8.hash(hasher),
1537    }
1538    hash_opt_f32(m.flex_grow, hasher);
1539    hash_opt_f32(m.flex_shrink, hasher);
1540    hash_opt_dp(m.flex_basis, hasher);
1541    m.flex_wrap
1542        .map(|value| std::mem::discriminant(&value))
1543        .hash(hasher);
1544    m.flex_basis_content.hash(hasher);
1545    m.flex_line_count.hash(hasher);
1546    m.flex_dir
1547        .map(|value| std::mem::discriminant(&value))
1548        .hash(hasher);
1549    m.align_self
1550        .map(|value| (value.keyword as u8, value.safety as u8))
1551        .hash(hasher);
1552    m.justify_content
1553        .map(|value| (value.keyword as u8, value.safety as u8))
1554        .hash(hasher);
1555    m.align_items_container
1556        .map(|value| (value.keyword as u8, value.safety as u8))
1557        .hash(hasher);
1558    m.align_content
1559        .map(|value| (value.keyword as u8, value.safety as u8))
1560        .hash(hasher);
1561    m.baseline_align.hash(hasher);
1562    match m.clip_rounded {
1563        Some(radius) => {
1564            1u8.hash(hasher);
1565            for value in radius {
1566                hash_dp(value, hasher);
1567            }
1568        }
1569        None => 0u8.hash(hasher),
1570    }
1571    match m.transform {
1572        Some(transform) => {
1573            1u8.hash(hasher);
1574            hash_f32(transform.translate_x, hasher);
1575            hash_f32(transform.translate_y, hasher);
1576            hash_f32(transform.scale_x, hasher);
1577            hash_f32(transform.scale_y, hasher);
1578            hash_f32(transform.rotate, hasher);
1579            hash_f32(transform.shear_x, hasher);
1580            hash_f32(transform.shear_y, hasher);
1581            hash_f32(transform.origin_x, hasher);
1582            hash_f32(transform.origin_y, hasher);
1583            for value in transform.perspective {
1584                hash_f32(value, hasher);
1585            }
1586        }
1587        None => 0u8.hash(hasher),
1588    }
1589    hash_opt_f32(m.alpha, hasher);
1590    m.position_type
1591        .map(|value| std::mem::discriminant(&value))
1592        .hash(hasher);
1593    hash_opt_dp(m.offset_left, hasher);
1594    hash_opt_dp(m.offset_right, hasher);
1595    hash_opt_dp(m.offset_top, hasher);
1596    hash_opt_dp(m.offset_bottom, hasher);
1597    match &m.grid {
1598        Some(grid) => {
1599            1u8.hash(hasher);
1600            grid.columns.hash(hasher);
1601            hash_dp(grid.row_gap, hasher);
1602            hash_dp(grid.column_gap, hasher);
1603        }
1604        None => 0u8.hash(hasher),
1605    }
1606    m.grid_col_span.hash(hasher);
1607    m.grid_row_span.hash(hasher);
1608    hash_opt_f32(m.aspect_ratio, hasher);
1609    m.intrinsic_width.hash(hasher);
1610    m.intrinsic_height.hash(hasher);
1611    hash_opt_dp(m.fit_content_width, hasher);
1612    hash_opt_dp(m.fit_content_height, hasher);
1613    match m.contain {
1614        None => 0u8.hash(hasher),
1615        Some(value) if value == taffy::Contain::CONTENT => 3u8.hash(hasher),
1616        Some(value) if value == taffy::Contain::LAYOUT => 1u8.hash(hasher),
1617        Some(value) if value == taffy::Contain::PAINT => 2u8.hash(hasher),
1618        Some(_) => 4u8.hash(hasher),
1619    }
1620    hash_f32(m.z_index, hasher);
1621    hash_opt_f32(m.render_z_index, hasher);
1622    m.input_blocker.hash(hasher);
1623    m.click.hash(hasher);
1624    m.disabled.hash(hasher);
1625    m.focusable.hash(hasher);
1626    m.hit_passthrough.hash(hasher);
1627    m.propagate_min.hash(hasher);
1628    m.focus_group.hash(hasher);
1629    match &m.blur {
1630        Some(blur) => {
1631            1u8.hash(hasher);
1632            hash_dp(blur.radius_x, hasher);
1633            hash_dp(blur.radius_y, hasher);
1634            std::mem::discriminant(&blur.edge_treatment).hash(hasher);
1635        }
1636        None => 0u8.hash(hasher),
1637    }
1638    match &m.scroll {
1639        Some(binding) => hash_scroll_binding(binding, hasher),
1640        None => 0u8.hash(hasher),
1641    }
1642    hash_scrollbar_style(m.scrollbar_style.as_ref(), hasher);
1643    m.overflow
1644        .map(|value| std::mem::discriminant(&value))
1645        .hash(hasher);
1646    match m.clip_rect {
1647        Some(rect) => {
1648            1u8.hash(hasher);
1649            hash_dp(rect.left, hasher);
1650            hash_dp(rect.top, hasher);
1651            hash_dp(rect.right, hasher);
1652            hash_dp(rect.bottom, hasher);
1653            std::mem::discriminant(&rect.op).hash(hasher);
1654        }
1655        None => 0u8.hash(hasher),
1656    }
1657    hash_opt_dp(m.gap, hasher);
1658    hash_opt_dp(m.row_gap, hasher);
1659    hash_opt_dp(m.column_gap, hasher);
1660    hash_opt_dp(m.margin_top, hasher);
1661    hash_opt_dp(m.margin_left, hasher);
1662    hash_opt_dp(m.margin_right, hasher);
1663    hash_opt_dp(m.margin_bottom, hasher);
1664    hash_opt_f32(m.graphics_layer, hasher);
1665    match &m.shadow {
1666        Some(shadow) => {
1667            1u8.hash(hasher);
1668            hash_dp(shadow.blur_radius, hasher);
1669            hash_dp(shadow.offset_y, hasher);
1670            hash_color(&shadow.color, hasher);
1671        }
1672        None => 0u8.hash(hasher),
1673    }
1674    match m.shadow_shape {
1675        Some(radius) => {
1676            1u8.hash(hasher);
1677            for value in radius {
1678                hash_dp(value, hasher);
1679            }
1680        }
1681        None => 0u8.hash(hasher),
1682    }
1683    match &m.semantics {
1684        Some(semantics) => {
1685            1u8.hash(hasher);
1686            std::mem::discriminant(&semantics.role).hash(hasher);
1687            semantics.label.hash(hasher);
1688            semantics.focused.hash(hasher);
1689            semantics.enabled.hash(hasher);
1690            semantics.selectable_group.hash(hasher);
1691            semantics.checked.hash(hasher);
1692            semantics.selected.hash(hasher);
1693            semantics.value.hash(hasher);
1694        }
1695        None => 0u8.hash(hasher),
1696    }
1697    hash_cursor(&m.cursor, hasher);
1698    match &m.text_input {
1699        Some(input) => {
1700            1u8.hash(hasher);
1701            input.hint.hash(hasher);
1702            input.multiline.hash(hasher);
1703            input.value.hash(hasher);
1704            input.enabled.hash(hasher);
1705            input.read_only.hash(hasher);
1706            input.sensitive.hash(hasher);
1707            input.max_lines.hash(hasher);
1708            input.min_lines.hash(hasher);
1709            std::mem::discriminant(&input.keyboard_type).hash(hasher);
1710            std::mem::discriminant(&input.capitalization).hash(hasher);
1711            std::mem::discriminant(&input.ime_action).hash(hasher);
1712            input.auto_correct_enabled.hash(hasher);
1713            match input.cursor_color {
1714                Some(color) => {
1715                    1u8.hash(hasher);
1716                    hash_color(&color, hasher);
1717                }
1718                None => 0u8.hash(hasher),
1719            }
1720            match &input.text_style {
1721                Some(style) => {
1722                    1u8.hash(hasher);
1723                    hash_text_style(style, hasher);
1724                }
1725                None => 0u8.hash(hasher),
1726            }
1727            match &input.line_limits {
1728                Some(repose_core::text::TextFieldLineLimits::SingleLine) => 1u8.hash(hasher),
1729                Some(repose_core::text::TextFieldLineLimits::MultiLine {
1730                    min_height_in_lines,
1731                    max_height_in_lines,
1732                }) => {
1733                    2u8.hash(hasher);
1734                    min_height_in_lines.hash(hasher);
1735                    max_height_in_lines.hash(hasher);
1736                }
1737                None => 0u8.hash(hasher),
1738            }
1739        }
1740        None => 0u8.hash(hasher),
1741    }
1742    m.text_selection.is_some().hash(hasher);
1743    match &m.state_colors {
1744        Some(colors) => {
1745            1u8.hash(hasher);
1746            hash_color(&colors.default, hasher);
1747            hash_color(&colors.hovered, hasher);
1748            hash_color(&colors.focused, hasher);
1749            hash_color(&colors.pressed, hasher);
1750            hash_color(&colors.disabled, hasher);
1751            hash_color(&colors.dragged, hasher);
1752        }
1753        None => 0u8.hash(hasher),
1754    }
1755    match &m.state_elevation {
1756        Some(elevation) => {
1757            1u8.hash(hasher);
1758            hash_dp(elevation.default, hasher);
1759            hash_dp(elevation.hovered, hasher);
1760            hash_dp(elevation.focused, hasher);
1761            hash_dp(elevation.pressed, hasher);
1762            hash_dp(elevation.disabled, hasher);
1763            hash_dp(elevation.dragged, hasher);
1764        }
1765        None => 0u8.hash(hasher),
1766    }
1767    match &m.animate_content_size {
1768        Some(spec) => {
1769            1u8.hash(hasher);
1770            hash_animation_spec(spec, hasher);
1771        }
1772        None => 0u8.hash(hasher),
1773    }
1774}
1775
1776#[allow(dead_code)]
1777fn hash_animation_spec(spec: &AnimationSpec, hasher: &mut impl Hasher) {
1778    spec.duration.as_nanos().hash(hasher);
1779    hash_easing(&spec.easing, hasher);
1780    spec.delay.as_nanos().hash(hasher);
1781    match &spec.spring {
1782        Some(spring) => {
1783            1u8.hash(hasher);
1784            hash_f32(spring.damping_ratio, hasher);
1785            hash_f32(spring.stiffness, hasher);
1786            hash_f32(spring.settle_progress, hasher);
1787            hash_f32(spring.settle_velocity, hasher);
1788        }
1789        None => 0u8.hash(hasher),
1790    }
1791    match &spec.repeat {
1792        Some(repeat) => {
1793            1u8.hash(hasher);
1794            repeat.iterations.hash(hasher);
1795            repeat.reverse.hash(hasher);
1796            repeat.delay_between.as_nanos().hash(hasher);
1797        }
1798        None => 0u8.hash(hasher),
1799    }
1800}
1801
1802fn hash_easing(easing: &Easing, hasher: &mut impl Hasher) {
1803    std::mem::discriminant(easing).hash(hasher);
1804    match easing {
1805        Easing::SpringCrit { omega } => hash_f32(*omega, hasher),
1806        Easing::Custom(cb) => {
1807            hash_f32(cb.p1x, hasher);
1808            hash_f32(cb.p1y, hasher);
1809            hash_f32(cb.p2x, hasher);
1810            hash_f32(cb.p2y, hasher);
1811        }
1812        _ => {}
1813    }
1814}
1815
1816fn hash_color(c: &Color, hasher: &mut impl Hasher) {
1817    c.0.hash(hasher);
1818    c.1.hash(hasher);
1819    c.2.hash(hasher);
1820    c.3.hash(hasher);
1821}
1822
1823fn hash_brush(b: &Brush, hasher: &mut impl Hasher) {
1824    std::mem::discriminant(b).hash(hasher);
1825    match b {
1826        Brush::Solid(c) => hash_color(c, hasher),
1827        Brush::Linear {
1828            start,
1829            end,
1830            start_color,
1831            end_color,
1832        } => {
1833            hash_f32(start.x, hasher);
1834            hash_f32(start.y, hasher);
1835            hash_f32(end.x, hasher);
1836            hash_f32(end.y, hasher);
1837            hash_color(start_color, hasher);
1838            hash_color(end_color, hasher);
1839        }
1840        Brush::LinearNormalized {
1841            start,
1842            end,
1843            start_color,
1844            end_color,
1845        } => {
1846            hash_f32(start.x, hasher);
1847            hash_f32(start.y, hasher);
1848            hash_f32(end.x, hasher);
1849            hash_f32(end.y, hasher);
1850            hash_color(start_color, hasher);
1851            hash_color(end_color, hasher);
1852        }
1853        Brush::Radial {
1854            center,
1855            radius,
1856            start_color,
1857            end_color,
1858        } => {
1859            hash_f32(center.x, hasher);
1860            hash_f32(center.y, hasher);
1861            hash_f32(*radius, hasher);
1862            hash_color(start_color, hasher);
1863            hash_color(end_color, hasher);
1864        }
1865        Brush::Sweep {
1866            center,
1867            start_color,
1868            end_color,
1869        } => {
1870            hash_f32(center.x, hasher);
1871            hash_f32(center.y, hasher);
1872            hash_color(start_color, hasher);
1873            hash_color(end_color, hasher);
1874        }
1875        _ => {}
1876    }
1877}
1878
1879fn hash_text_overflow(o: &TextOverflow, hasher: &mut impl Hasher) {
1880    std::mem::discriminant(o).hash(hasher);
1881}
1882
1883#[cfg(test)]
1884mod tests {
1885    use super::*;
1886    use repose_core::{
1887        DrawStyle, FontStyle, FontWeight, ImageAlignment, ImageFilter, ImageFit, ImageSourceRect,
1888        Modifier, TextAlign, TextDecoration, UnitExt, View, ViewKind,
1889    };
1890
1891    #[test]
1892    fn test_same_view_same_hash() {
1893        let v1 = View::new(0, ViewKind::Box).modifier(Modifier::new().width(100.0.dp()));
1894        let v2 = View::new(0, ViewKind::Box).modifier(Modifier::new().width(100.0.dp()));
1895
1896        assert_eq!(hash_view_content(&v1), hash_view_content(&v2));
1897    }
1898
1899    #[test]
1900    fn image_source_rect_changes_hash() {
1901        let image = |source_rect| {
1902            View::new(
1903                0,
1904                ViewKind::Image {
1905                    handle: 1,
1906                    tint: Color::WHITE,
1907                    fit: ImageFit::FillBounds,
1908                    filter: ImageFilter::Nearest,
1909                    source_rect,
1910                    alignment: ImageAlignment::Center,
1911                },
1912            )
1913        };
1914        let full = image(None);
1915        let first = image(Some(ImageSourceRect::new(0, 0, 8, 8)));
1916        let second = image(Some(ImageSourceRect::new(8, 0, 8, 8)));
1917        assert_ne!(hash_view_content(&full), hash_view_content(&first));
1918        assert_ne!(hash_view_content(&first), hash_view_content(&second));
1919    }
1920
1921    #[test]
1922    fn test_scroll_presence_changes_hash() {
1923        use repose_core::scroll::ScrollAxisBinding;
1924
1925        let v1 = View::new(0, ViewKind::Box).modifier(Modifier::new());
1926        let v2 = View::new(0, ViewKind::Box).modifier(Modifier::new().vertical_scroll(
1927            ScrollAxisBinding {
1928                show_scrollbar: true,
1929                ..Default::default()
1930            },
1931        ));
1932
1933        assert_ne!(hash_view_content(&v1), hash_view_content(&v2));
1934    }
1935
1936    #[test]
1937    fn test_different_view_different_hash() {
1938        let v1 = View::new(0, ViewKind::Box).modifier(Modifier::new().width(100.0.dp()));
1939        let v2 = View::new(0, ViewKind::Box).modifier(Modifier::new().width(200.0.dp()));
1940
1941        assert_ne!(hash_view_content(&v1), hash_view_content(&v2));
1942    }
1943
1944    #[test]
1945    fn test_text_content_hash() {
1946        let v1 = View::new(
1947            0,
1948            ViewKind::Text {
1949                text: "Hello".to_string(),
1950                color: Color::WHITE,
1951                font_size: 16.0.sp(),
1952                soft_wrap: true,
1953                max_lines: None,
1954                overflow: TextOverflow::Visible,
1955                font_family: None,
1956                annotations: None,
1957                text_align: TextAlign::Unspecified,
1958                font_weight: FontWeight::NORMAL,
1959                font_style: FontStyle::Normal,
1960                text_decoration: TextDecoration::default(),
1961                letter_spacing: Sp::ZERO,
1962                line_height: Sp::ZERO,
1963                url: None,
1964                font_variation_settings: None,
1965                draw_style: DrawStyle::Fill,
1966            },
1967        );
1968        let v2 = View::new(
1969            0,
1970            ViewKind::Text {
1971                text: "Hello".to_string(),
1972                color: Color::WHITE,
1973                font_size: 16.0.sp(),
1974                soft_wrap: true,
1975                max_lines: None,
1976                overflow: TextOverflow::Visible,
1977                font_family: None,
1978                annotations: None,
1979                text_align: TextAlign::Unspecified,
1980                font_weight: FontWeight::NORMAL,
1981                font_style: FontStyle::Normal,
1982                text_decoration: TextDecoration::default(),
1983                letter_spacing: Sp::ZERO,
1984                line_height: Sp::ZERO,
1985                url: None,
1986                font_variation_settings: None,
1987                draw_style: DrawStyle::Fill,
1988            },
1989        );
1990        let v3 = View::new(
1991            0,
1992            ViewKind::Text {
1993                text: "World".to_string(),
1994                color: Color::WHITE,
1995                font_size: 16.0.sp(),
1996                soft_wrap: true,
1997                max_lines: None,
1998                overflow: TextOverflow::Visible,
1999                font_family: None,
2000                annotations: None,
2001                text_align: TextAlign::Unspecified,
2002                font_weight: FontWeight::NORMAL,
2003                font_style: FontStyle::Normal,
2004                text_decoration: TextDecoration::default(),
2005                letter_spacing: Sp::ZERO,
2006                line_height: Sp::ZERO,
2007                url: None,
2008                font_variation_settings: None,
2009                draw_style: DrawStyle::Fill,
2010            },
2011        );
2012
2013        assert_eq!(hash_view_content(&v1), hash_view_content(&v2));
2014        assert_ne!(hash_view_content(&v1), hash_view_content(&v3));
2015    }
2016
2017    fn text_view_with_url(url: Option<&str>) -> View {
2018        View::new(
2019            0,
2020            ViewKind::Text {
2021                text: "link".to_string(),
2022                color: Color::WHITE,
2023                font_size: 16.0.sp(),
2024                soft_wrap: true,
2025                max_lines: None,
2026                overflow: TextOverflow::Visible,
2027                font_family: None,
2028                annotations: None,
2029                text_align: TextAlign::Unspecified,
2030                font_weight: FontWeight::NORMAL,
2031                font_style: FontStyle::Normal,
2032                text_decoration: TextDecoration::default(),
2033                letter_spacing: Sp::ZERO,
2034                line_height: Sp::ZERO,
2035                url: url.map(std::sync::Arc::from),
2036                font_variation_settings: None,
2037                draw_style: DrawStyle::Fill,
2038            },
2039        )
2040    }
2041
2042    #[test]
2043    fn view_id_and_span_baseline_change_hash() {
2044        let first = View::new(1, ViewKind::Box);
2045        let second = View::new(2, ViewKind::Box);
2046        assert_ne!(hash_view_content(&first), hash_view_content(&second));
2047
2048        let mut a = text_view_with_url(None);
2049        let mut b = a.clone();
2050        let span = repose_core::TextSpan {
2051            start: 0,
2052            end: 4,
2053            style: repose_core::SpanStyle::default()
2054                .baseline_shift(repose_core::BaselineShift::Superscript),
2055            url: None,
2056        };
2057        if let ViewKind::Text { annotations, .. } = &mut a.kind {
2058            *annotations = Some(std::sync::Arc::from([span.clone()]));
2059        }
2060        if let ViewKind::Text { annotations, .. } = &mut b.kind {
2061            *annotations = Some(std::sync::Arc::from([repose_core::TextSpan {
2062                style: repose_core::SpanStyle::default()
2063                    .baseline_shift(repose_core::BaselineShift::Subscript),
2064                ..span
2065            }]));
2066        }
2067        assert_ne!(hash_view_content(&a), hash_view_content(&b));
2068    }
2069
2070    #[test]
2071    fn test_text_url_change_invalidates() {
2072        let a = text_view_with_url(None);
2073        let b = text_view_with_url(Some("https://a.example"));
2074        let c = text_view_with_url(Some("https://b.example"));
2075        assert_ne!(hash_view_content(&a), hash_view_content(&b));
2076        assert_ne!(hash_view_content(&b), hash_view_content(&c));
2077    }
2078
2079    #[test]
2080    fn test_semantics_state_invalidates() {
2081        let mk = |checked: Option<bool>| {
2082            View::new(0, ViewKind::Box).semantics(
2083                repose_core::Semantics::new(repose_core::Role::Checkbox)
2084                    .with_checked(checked.unwrap_or(false)),
2085            )
2086        };
2087        assert_ne!(
2088            hash_view_content(&mk(Some(true))),
2089            hash_view_content(&mk(Some(false)))
2090        );
2091        let m1 = View::new(0, ViewKind::Box).modifier(
2092            Modifier::new().semantics(repose_core::Semantics::new(repose_core::Role::Button)),
2093        );
2094        let m2 = View::new(0, ViewKind::Box).modifier(
2095            Modifier::new()
2096                .semantics(repose_core::Semantics::new(repose_core::Role::Button).with_value("v")),
2097        );
2098        assert_ne!(hash_view_content(&m1), hash_view_content(&m2));
2099    }
2100
2101    #[test]
2102    fn test_then_preserves_layer_fields() {
2103        let base = Modifier::new().width(10.0.dp());
2104        let overlay = Modifier::new()
2105            .blur(4.0.dp())
2106            .shadow_with_color(8.0.dp(), 2.0.dp(), Color::BLACK)
2107            .graphics_layer(0.5);
2108        let merged = base.then(overlay);
2109        assert!(merged.blur.is_some());
2110        assert!(merged.shadow.is_some());
2111        assert!(merged.graphics_layer.is_some());
2112        assert_eq!(
2113            Modifier::new()
2114                .z_index(5.0)
2115                .then(Modifier::new().z_index(0.0))
2116                .z_index,
2117            0.0
2118        );
2119    }
2120}