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}