Skip to main content

fission_core/
diff.rs

1use fission_ir::{CoreIR, CoreNode, NodeId, Op, PaintOp};
2use std::collections::HashSet;
3
4#[derive(Debug, Default)]
5pub struct FrameDiff {
6    pub dirty_layout: HashSet<NodeId>,
7    pub dirty_paint: HashSet<NodeId>,
8    pub dirty_composite: HashSet<NodeId>,
9}
10
11pub fn diff_ir(prev: &CoreIR, next: &CoreIR) -> FrameDiff {
12    let mut diff = FrameDiff::default();
13
14    if prev.root != next.root {
15        let all_nodes: HashSet<NodeId> = next.nodes.keys().copied().collect();
16        diff.dirty_layout = all_nodes.clone();
17        diff.dirty_paint = all_nodes.clone();
18        diff.dirty_composite = all_nodes;
19        return diff;
20    }
21
22    for (id, next_node) in &next.nodes {
23        match prev.nodes.get(id) {
24            None => {
25                diff.dirty_layout.insert(*id);
26                diff.dirty_paint.insert(*id);
27                diff.dirty_composite.insert(*id);
28            }
29            Some(prev_node) => {
30                if node_requires_layout(prev_node, next_node) {
31                    diff.dirty_layout.insert(*id);
32                    continue;
33                }
34
35                if prev_node.composite != next_node.composite {
36                    diff.dirty_composite.insert(*id);
37                }
38
39                if node_requires_paint(prev_node, next_node) {
40                    diff.dirty_paint.insert(*id);
41                }
42            }
43        }
44    }
45
46    diff
47}
48
49fn node_requires_layout(prev: &CoreNode, next: &CoreNode) -> bool {
50    if prev.children != next.children || prev.parent != next.parent {
51        return true;
52    }
53
54    match (&prev.op, &next.op) {
55        (Op::Layout(prev_op), Op::Layout(next_op)) => prev_op != next_op,
56        (Op::Structural(prev_op), Op::Structural(next_op)) => prev_op != next_op,
57        (Op::Paint(prev_op), Op::Paint(next_op)) => paint_change_requires_layout(prev_op, next_op),
58        (Op::Semantics(_), Op::Semantics(_)) => false,
59        _ => true,
60    }
61}
62
63fn node_requires_paint(prev: &CoreNode, next: &CoreNode) -> bool {
64    match (&prev.op, &next.op) {
65        (Op::Paint(prev_op), Op::Paint(next_op)) => prev_op != next_op,
66        (Op::Semantics(_), Op::Semantics(_)) => false,
67        _ => false,
68    }
69}
70
71fn paint_change_requires_layout(prev: &PaintOp, next: &PaintOp) -> bool {
72    match (prev, next) {
73        (PaintOp::DrawText { .. }, PaintOp::DrawText { .. }) => prev != next,
74        (PaintOp::DrawRichText { .. }, PaintOp::DrawRichText { .. }) => prev != next,
75        (PaintOp::DrawText { .. }, _) | (_, PaintOp::DrawText { .. }) => true,
76        (PaintOp::DrawRichText { .. }, _) | (_, PaintOp::DrawRichText { .. }) => true,
77        _ => false,
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::diff_ir;
84    use fission_ir::op::Fill;
85    use fission_ir::{CompositeScalar, CompositeStyle, CoreIR, LayoutOp, NodeId, Op, PaintOp};
86
87    fn rect_ir(id_seed: u128, color: (u8, u8, u8, u8)) -> CoreIR {
88        let root = NodeId::derived(id_seed, &[0]);
89        let paint = NodeId::derived(id_seed, &[1]);
90        let mut ir = CoreIR::new();
91        ir.add_node(
92            paint,
93            Op::Paint(PaintOp::DrawRect {
94                fill: Some(Fill::Solid(fission_ir::op::Color {
95                    r: color.0,
96                    g: color.1,
97                    b: color.2,
98                    a: color.3,
99                })),
100                stroke: None,
101                corner_radius: 0.0,
102                shadow: None,
103            }),
104            vec![],
105        );
106        ir.add_node(
107            root,
108            Op::Layout(LayoutOp::Box {
109                width: Some(10.0),
110                height: Some(10.0),
111                min_width: None,
112                max_width: None,
113                min_height: None,
114                max_height: None,
115                padding: [0.0; 4],
116                flex_grow: 0.0,
117                flex_shrink: 0.0,
118                aspect_ratio: None,
119            }),
120            vec![paint],
121        );
122        ir.set_root(root);
123        for node in ir.nodes.values_mut() {
124            use std::hash::{Hash, Hasher};
125            let mut hasher = std::collections::hash_map::DefaultHasher::new();
126            node.op.hash(&mut hasher);
127            node.composite.hash(&mut hasher);
128            node.children.hash(&mut hasher);
129            node.parent.hash(&mut hasher);
130            node.hash = hasher.finish();
131        }
132        ir
133    }
134
135    #[test]
136    fn paint_only_changes_do_not_force_layout() {
137        let prev = rect_ir(1, (255, 0, 0, 255));
138        let next = rect_ir(1, (255, 0, 0, 128));
139        let diff = diff_ir(&prev, &next);
140        assert!(
141            diff.dirty_layout.is_empty(),
142            "paint-only changes should not invalidate layout"
143        );
144        assert_eq!(diff.dirty_paint.len(), 1);
145    }
146
147    #[test]
148    fn layout_changes_still_force_layout() {
149        let prev = rect_ir(2, (255, 0, 0, 255));
150        let mut next = rect_ir(2, (255, 0, 0, 255));
151        let root = next.root.expect("root");
152        if let Some(node) = next.nodes.get_mut(&root) {
153            node.op = Op::Layout(LayoutOp::Box {
154                width: Some(20.0),
155                height: Some(10.0),
156                min_width: None,
157                max_width: None,
158                min_height: None,
159                max_height: None,
160                padding: [0.0; 4],
161                flex_grow: 0.0,
162                flex_shrink: 0.0,
163                aspect_ratio: None,
164            });
165            use std::hash::{Hash, Hasher};
166            let mut hasher = std::collections::hash_map::DefaultHasher::new();
167            node.op.hash(&mut hasher);
168            node.composite.hash(&mut hasher);
169            node.children.hash(&mut hasher);
170            node.parent.hash(&mut hasher);
171            node.hash = hasher.finish();
172        }
173        let diff = diff_ir(&prev, &next);
174        assert!(diff.dirty_layout.contains(&root));
175    }
176
177    #[test]
178    fn text_paint_changes_force_layout() {
179        let root = NodeId::derived(3, &[0]);
180        let text = NodeId::derived(3, &[1]);
181        let mut prev = CoreIR::new();
182        prev.add_node(
183            text,
184            Op::Paint(PaintOp::DrawText {
185                text: "a".into(),
186                size: 12.0,
187                color: fission_ir::op::Color::BLACK,
188                underline: false,
189                wrap: true,
190                caret_index: None,
191                caret_color: None,
192                caret_width: None,
193                caret_height: None,
194                caret_radius: None,
195                paragraph_style: None,
196            }),
197            vec![],
198        );
199        prev.add_node(
200            root,
201            Op::Layout(LayoutOp::Box {
202                width: None,
203                height: None,
204                min_width: None,
205                max_width: None,
206                min_height: None,
207                max_height: None,
208                padding: [0.0; 4],
209                flex_grow: 0.0,
210                flex_shrink: 0.0,
211                aspect_ratio: None,
212            }),
213            vec![text],
214        );
215        prev.set_root(root);
216        let mut next = prev.clone();
217        if let Some(node) = next.nodes.get_mut(&text) {
218            node.op = Op::Paint(PaintOp::DrawText {
219                text: "much wider".into(),
220                size: 12.0,
221                color: fission_ir::op::Color::BLACK,
222                underline: false,
223                wrap: true,
224                caret_index: None,
225                caret_color: None,
226                caret_width: None,
227                caret_height: None,
228                caret_radius: None,
229                paragraph_style: None,
230            });
231        }
232        let diff = diff_ir(&prev, &next);
233        assert!(diff.dirty_layout.contains(&text));
234    }
235
236    #[test]
237    fn composite_changes_do_not_force_layout() {
238        let prev = rect_ir(4, (255, 0, 0, 255));
239        let mut next = rect_ir(4, (255, 0, 0, 255));
240        let root = next.root.expect("root");
241        if let Some(node) = next.nodes.get_mut(&root) {
242            node.composite = CompositeStyle {
243                opacity: Some(CompositeScalar::new(0.5)),
244                ..Default::default()
245            };
246            use std::hash::{Hash, Hasher};
247            let mut hasher = std::collections::hash_map::DefaultHasher::new();
248            node.op.hash(&mut hasher);
249            node.composite.hash(&mut hasher);
250            node.children.hash(&mut hasher);
251            node.parent.hash(&mut hasher);
252            node.hash = hasher.finish();
253        }
254        let diff = diff_ir(&prev, &next);
255        assert!(!diff.dirty_layout.contains(&root));
256        assert!(diff.dirty_composite.contains(&root));
257    }
258}