Skip to main content

repose_tree/
hash.rs

1//! Content hashing for change detection.
2
3use ahash::AHasher;
4use repose_core::{Brush, Color, Modifier, TextOverflow, View, ViewKind};
5use std::hash::{Hash, Hasher};
6
7/// Compute a content hash for a View's immediate properties.
8/// This does NOT include children - that's handled separately.
9pub fn hash_view_content(view: &View) -> u64 {
10    let mut hasher = AHasher::default();
11
12    // Hash the kind
13    hash_view_kind(&view.kind, &mut hasher);
14
15    // Hash relevant modifier properties
16    hash_modifier(&view.modifier, &mut hasher);
17
18    // Hash user key if present
19    if let Some(key) = view.modifier.key {
20        key.hash(&mut hasher);
21    }
22
23    hasher.finish()
24}
25
26/// Compute a hash that includes the subtree structure.
27/// This combines the node's content hash with its children's subtree hashes.
28pub fn hash_subtree(content_hash: u64, children_hashes: &[u64]) -> u64 {
29    let mut hasher = AHasher::default();
30    content_hash.hash(&mut hasher);
31    children_hashes.len().hash(&mut hasher);
32    for &h in children_hashes {
33        h.hash(&mut hasher);
34    }
35    hasher.finish()
36}
37
38fn hash_view_kind(kind: &ViewKind, hasher: &mut impl Hasher) {
39    // Discriminant
40    std::mem::discriminant(kind).hash(hasher);
41
42    match kind {
43        ViewKind::Text {
44            text,
45            color,
46            font_size,
47            soft_wrap,
48            max_lines,
49            overflow,
50        } => {
51            text.hash(hasher);
52            hash_color(color, hasher);
53            ((font_size * 100.0) as u32).hash(hasher);
54            soft_wrap.hash(hasher);
55            max_lines.hash(hasher);
56            hash_text_overflow(overflow, hasher);
57        }
58        ViewKind::Button { .. } => {
59            // on_click is a closure, can't hash it
60            // We rely on the closure being recreated each frame anyway
61        }
62        ViewKind::TextField {
63            state_key, hint, ..
64        } => {
65            state_key.hash(hasher);
66            hint.hash(hasher);
67        }
68        ViewKind::Checkbox { checked, .. } => {
69            checked.hash(hasher);
70        }
71        ViewKind::RadioButton { selected, .. } => {
72            selected.hash(hasher);
73        }
74        ViewKind::Switch { checked, .. } => {
75            checked.hash(hasher);
76        }
77        ViewKind::Slider {
78            value,
79            min,
80            max,
81            step,
82            ..
83        } => {
84            ((value * 1000.0) as i32).hash(hasher);
85            ((min * 1000.0) as i32).hash(hasher);
86            ((max * 1000.0) as i32).hash(hasher);
87            step.map(|s| (s * 1000.0) as i32).hash(hasher);
88        }
89        ViewKind::RangeSlider {
90            start,
91            end,
92            min,
93            max,
94            step,
95            ..
96        } => {
97            ((start * 1000.0) as i32).hash(hasher);
98            ((end * 1000.0) as i32).hash(hasher);
99            ((min * 1000.0) as i32).hash(hasher);
100            ((max * 1000.0) as i32).hash(hasher);
101            step.map(|s| (s * 1000.0) as i32).hash(hasher);
102        }
103        ViewKind::ProgressBar {
104            value,
105            min,
106            max,
107            circular,
108        } => {
109            ((value * 1000.0) as i32).hash(hasher);
110            ((min * 1000.0) as i32).hash(hasher);
111            ((max * 1000.0) as i32).hash(hasher);
112            circular.hash(hasher);
113        }
114        ViewKind::Image { handle, tint, fit } => {
115            handle.hash(hasher);
116            hash_color(tint, hasher);
117            std::mem::discriminant(fit).hash(hasher);
118        }
119        ViewKind::Ellipse { rect, color } => {
120            hash_rect(rect, hasher);
121            hash_color(color, hasher);
122        }
123        ViewKind::EllipseBorder { rect, color, width } => {
124            hash_rect(rect, hasher);
125            hash_color(color, hasher);
126            ((width * 100.0) as u32).hash(hasher);
127        }
128        ViewKind::ScrollV { .. } | ViewKind::ScrollXY { .. } => {
129            // Scroll state is external, not part of content hash
130        }
131        ViewKind::OverlayHost
132        | ViewKind::Surface
133        | ViewKind::Box
134        | ViewKind::Row
135        | ViewKind::Column
136        | ViewKind::Stack => {
137            // These are just containers, discriminant is enough
138        }
139    }
140}
141
142fn hash_modifier(m: &Modifier, hasher: &mut impl Hasher) {
143    // Size
144    if let Some(s) = &m.size {
145        ((s.width * 100.0) as i32).hash(hasher);
146        ((s.height * 100.0) as i32).hash(hasher);
147    }
148    m.width.map(|w| (w * 100.0) as i32).hash(hasher);
149    m.height.map(|h| (h * 100.0) as i32).hash(hasher);
150    m.fill_max.hash(hasher);
151    m.fill_max_w.hash(hasher);
152    m.fill_max_h.hash(hasher);
153    m.repaint_boundary.hash(hasher);
154
155    // Padding
156    m.padding.map(|p| (p * 100.0) as i32).hash(hasher);
157    if let Some(pv) = &m.padding_values {
158        ((pv.left * 100.0) as i32).hash(hasher);
159        ((pv.right * 100.0) as i32).hash(hasher);
160        ((pv.top * 100.0) as i32).hash(hasher);
161        ((pv.bottom * 100.0) as i32).hash(hasher);
162    }
163
164    // Min/max size
165    m.min_width.map(|v| (v * 100.0) as i32).hash(hasher);
166    m.min_height.map(|v| (v * 100.0) as i32).hash(hasher);
167    m.max_width.map(|v| (v * 100.0) as i32).hash(hasher);
168    m.max_height.map(|v| (v * 100.0) as i32).hash(hasher);
169
170    // Background
171    if let Some(bg) = &m.background {
172        hash_brush(bg, hasher);
173    }
174
175    // Border
176    if let Some(b) = &m.border {
177        ((b.width * 100.0) as i32).hash(hasher);
178        hash_color(&b.color, hasher);
179        ((b.radius * 100.0) as i32).hash(hasher);
180    }
181
182    // Flex
183    m.flex_grow.map(|v| (v * 100.0) as i32).hash(hasher);
184    m.flex_shrink.map(|v| (v * 100.0) as i32).hash(hasher);
185    m.flex_basis.map(|v| (v * 100.0) as i32).hash(hasher);
186    m.flex_wrap.map(|v| std::mem::discriminant(&v)).hash(hasher);
187    m.flex_dir.map(|v| std::mem::discriminant(&v)).hash(hasher);
188    m.align_self
189        .map(|v| std::mem::discriminant(&v))
190        .hash(hasher);
191    m.justify_content
192        .map(|v| std::mem::discriminant(&v))
193        .hash(hasher);
194    m.align_items_container
195        .map(|v| std::mem::discriminant(&v))
196        .hash(hasher);
197    m.align_content
198        .map(|v| std::mem::discriminant(&v))
199        .hash(hasher);
200
201    // Clip
202    m.clip_rounded.map(|v| (v * 100.0) as i32).hash(hasher);
203
204    // Transform
205    if let Some(t) = &m.transform {
206        ((t.translate_x * 100.0) as i32).hash(hasher);
207        ((t.translate_y * 100.0) as i32).hash(hasher);
208        ((t.scale_x * 100.0) as i32).hash(hasher);
209        ((t.scale_y * 100.0) as i32).hash(hasher);
210        ((t.rotate * 1000.0) as i32).hash(hasher);
211    }
212
213    // Alpha
214    m.alpha.map(|a| (a * 255.0) as u8).hash(hasher);
215
216    // Position
217    m.position_type
218        .map(|v| std::mem::discriminant(&v))
219        .hash(hasher);
220    m.offset_left.map(|v| (v * 100.0) as i32).hash(hasher);
221    m.offset_right.map(|v| (v * 100.0) as i32).hash(hasher);
222    m.offset_top.map(|v| (v * 100.0) as i32).hash(hasher);
223    m.offset_bottom.map(|v| (v * 100.0) as i32).hash(hasher);
224
225    // Grid
226    if let Some(g) = &m.grid {
227        g.columns.hash(hasher);
228        ((g.row_gap * 100.0) as i32).hash(hasher);
229        ((g.column_gap * 100.0) as i32).hash(hasher);
230    }
231    m.grid_col_span.hash(hasher);
232    m.grid_row_span.hash(hasher);
233
234    // Aspect ratio
235    m.aspect_ratio.map(|v| (v * 100.0) as i32).hash(hasher);
236
237    // Z-index
238    ((m.z_index * 100.0) as i32).hash(hasher);
239    m.render_z_index.map(|v| (v * 100.0) as i32).hash(hasher);
240    m.input_blocker.hash(hasher);
241
242    // Clickable
243    m.click.hash(hasher);
244    (m.on_action.is_some()).hash(hasher);
245
246    (m.on_drag_start.is_some()).hash(hasher);
247    (m.on_drag_end.is_some()).hash(hasher);
248    (m.on_drag_enter.is_some()).hash(hasher);
249    (m.on_drag_over.is_some()).hash(hasher);
250    (m.on_drag_leave.is_some()).hash(hasher);
251    (m.on_drop.is_some()).hash(hasher);
252}
253
254fn hash_color(c: &Color, hasher: &mut impl Hasher) {
255    c.0.hash(hasher);
256    c.1.hash(hasher);
257    c.2.hash(hasher);
258    c.3.hash(hasher);
259}
260
261fn hash_brush(b: &Brush, hasher: &mut impl Hasher) {
262    std::mem::discriminant(b).hash(hasher);
263    match b {
264        Brush::Solid(c) => hash_color(c, hasher),
265        Brush::Linear {
266            start,
267            end,
268            start_color,
269            end_color,
270        } => {
271            ((start.x * 100.0) as i32).hash(hasher);
272            ((start.y * 100.0) as i32).hash(hasher);
273            ((end.x * 100.0) as i32).hash(hasher);
274            ((end.y * 100.0) as i32).hash(hasher);
275            hash_color(start_color, hasher);
276            hash_color(end_color, hasher);
277        }
278    }
279}
280
281fn hash_rect(r: &repose_core::Rect, hasher: &mut impl Hasher) {
282    ((r.x * 100.0) as i32).hash(hasher);
283    ((r.y * 100.0) as i32).hash(hasher);
284    ((r.w * 100.0) as i32).hash(hasher);
285    ((r.h * 100.0) as i32).hash(hasher);
286}
287
288fn hash_text_overflow(o: &TextOverflow, hasher: &mut impl Hasher) {
289    std::mem::discriminant(o).hash(hasher);
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295    use repose_core::{Modifier, View, ViewKind};
296
297    #[test]
298    fn test_same_view_same_hash() {
299        let v1 = View::new(0, ViewKind::Box).modifier(Modifier::new().width(100.0));
300        let v2 = View::new(0, ViewKind::Box).modifier(Modifier::new().width(100.0));
301
302        assert_eq!(hash_view_content(&v1), hash_view_content(&v2));
303    }
304
305    #[test]
306    fn test_different_view_different_hash() {
307        let v1 = View::new(0, ViewKind::Box).modifier(Modifier::new().width(100.0));
308        let v2 = View::new(0, ViewKind::Box).modifier(Modifier::new().width(200.0));
309
310        assert_ne!(hash_view_content(&v1), hash_view_content(&v2));
311    }
312
313    #[test]
314    fn test_text_content_hash() {
315        let v1 = View::new(
316            0,
317            ViewKind::Text {
318                text: "Hello".to_string(),
319                color: Color::WHITE,
320                font_size: 16.0,
321                soft_wrap: true,
322                max_lines: None,
323                overflow: TextOverflow::Visible,
324            },
325        );
326        let v2 = View::new(
327            0,
328            ViewKind::Text {
329                text: "Hello".to_string(),
330                color: Color::WHITE,
331                font_size: 16.0,
332                soft_wrap: true,
333                max_lines: None,
334                overflow: TextOverflow::Visible,
335            },
336        );
337        let v3 = View::new(
338            0,
339            ViewKind::Text {
340                text: "World".to_string(),
341                color: Color::WHITE,
342                font_size: 16.0,
343                soft_wrap: true,
344                max_lines: None,
345                overflow: TextOverflow::Visible,
346            },
347        );
348
349        assert_eq!(hash_view_content(&v1), hash_view_content(&v2));
350        assert_ne!(hash_view_content(&v1), hash_view_content(&v3));
351    }
352}