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