1use crate::runtime::LayoutNode;
17use stipple_geometry::{Rect, ScaleFactor};
18
19#[derive(Clone, Debug, PartialEq)]
21pub enum Damage {
22 None,
24 Regions(Vec<Rect>),
26 Full,
29}
30
31impl Damage {
32 pub fn is_empty(&self) -> bool {
34 matches!(self, Damage::None)
35 }
36
37 pub fn bounding(&self) -> Option<Rect> {
40 match self {
41 Damage::Regions(rs) => rs.iter().copied().reduce(|a, b| a.union(b)),
42 _ => None,
43 }
44 }
45
46 pub fn to_physical(&self, scale: ScaleFactor, bounds: Rect) -> Vec<Rect> {
56 let Damage::Regions(regions) = self else {
57 return Vec::new();
58 };
59 let s = scale.get();
60 regions
61 .iter()
62 .filter_map(|r| {
63 let phys = Rect::from_xywh(
64 (r.min_x() * s).floor(),
65 (r.min_y() * s).floor(),
66 (r.width() * s).ceil(),
67 (r.height() * s).ceil(),
68 );
69 let snapped = Rect::from_points(
71 stipple_geometry::Point::new(phys.min_x(), phys.min_y()),
72 stipple_geometry::Point::new((r.max_x() * s).ceil(), (r.max_y() * s).ceil()),
73 );
74 snapped.intersection(bounds).filter(|c| !c.is_empty())
75 })
76 .collect()
77 }
78}
79
80pub fn diff_trees(old: &LayoutNode, new: &LayoutNode) -> Damage {
84 let mut regions = Vec::new();
85 diff_node(old, new, &mut regions);
86 if regions.is_empty() {
87 Damage::None
88 } else {
89 Damage::Regions(coalesce(regions))
90 }
91}
92
93fn visuals_equal(a: &LayoutNode, b: &LayoutNode) -> bool {
99 a.bounds == b.bounds
100 && a.decoration == b.decoration
101 && a.content == b.content
102 && a.caret == b.caret
103 && a.selection == b.selection
104}
105
106fn diff_node(old: &LayoutNode, new: &LayoutNode, out: &mut Vec<Rect>) {
107 if !visuals_equal(old, new) {
108 out.push(old.bounds.union(new.bounds));
109 }
110 if old.children.len() != new.children.len() {
111 out.push(old.bounds.union(new.bounds));
114 return;
115 }
116 for (o, n) in old.children.iter().zip(&new.children) {
117 diff_node(o, n, out);
118 }
119}
120
121fn coalesce(mut rects: Vec<Rect>) -> Vec<Rect> {
125 let mut merged = true;
126 while merged {
127 merged = false;
128 let mut i = 0;
129 while i < rects.len() {
130 let mut j = i + 1;
131 while j < rects.len() {
132 if overlaps_or_touches(rects[i], rects[j]) {
133 rects[i] = rects[i].union(rects[j]);
134 rects.swap_remove(j);
135 merged = true;
136 } else {
137 j += 1;
138 }
139 }
140 i += 1;
141 }
142 }
143 rects
144}
145
146fn overlaps_or_touches(a: Rect, b: Rect) -> bool {
149 a.min_x() <= b.max_x()
150 && b.min_x() <= a.max_x()
151 && a.min_y() <= b.max_y()
152 && b.min_y() <= a.max_y()
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use crate::element::BoxStyle;
159 use crate::runtime::NodeContent;
160 use stipple_render::Color;
161
162 fn node(bounds: Rect, fill: Option<Color>, children: Vec<LayoutNode>) -> LayoutNode {
163 LayoutNode {
164 bounds,
165 decoration: BoxStyle {
166 fill,
167 radius: 0.0,
168 border: None,
169 },
170 content: NodeContent::None,
171 action: None,
172 focus: None,
173 drag: None,
174 context: None,
175 caret: None,
176 selection: None,
177 text_pos: None,
178 wrap: false,
179 scroll: None,
180 clip: false,
181 children,
182 }
183 }
184
185 fn text_node(bounds: Rect, text: &str) -> LayoutNode {
186 LayoutNode {
187 bounds,
188 decoration: BoxStyle::default(),
189 content: NodeContent::Text {
190 text: text.into(),
191 size: 14.0,
192 color: Color::BLACK,
193 },
194 action: None,
195 focus: None,
196 drag: None,
197 context: None,
198 caret: None,
199 selection: None,
200 text_pos: None,
201 wrap: false,
202 scroll: None,
203 clip: false,
204 children: Vec::new(),
205 }
206 }
207
208 fn root(children: Vec<LayoutNode>) -> LayoutNode {
209 node(Rect::from_xywh(0.0, 0.0, 200.0, 100.0), None, children)
210 }
211
212 #[test]
213 fn identical_trees_have_no_damage() {
214 let a = root(vec![text_node(
215 Rect::from_xywh(10.0, 10.0, 40.0, 20.0),
216 "hi",
217 )]);
218 let b = a.clone();
219 assert_eq!(diff_trees(&a, &b), Damage::None);
220 assert!(diff_trees(&a, &b).is_empty());
221 }
222
223 #[test]
224 fn changed_text_damages_only_that_node() {
225 let a = root(vec![
226 text_node(Rect::from_xywh(10.0, 10.0, 40.0, 20.0), "0"),
227 text_node(Rect::from_xywh(10.0, 40.0, 40.0, 20.0), "stable"),
228 ]);
229 let mut b = a.clone();
230 b.children[0] = text_node(Rect::from_xywh(10.0, 10.0, 40.0, 20.0), "1");
231
232 let dmg = diff_trees(&a, &b);
233 assert_eq!(
235 dmg,
236 Damage::Regions(vec![Rect::from_xywh(10.0, 10.0, 40.0, 20.0)])
237 );
238 let bound = dmg.bounding().unwrap();
239 assert!(bound.width() < 200.0 && bound.height() < 100.0);
240 }
241
242 #[test]
243 fn caret_move_alone_is_damage() {
244 let mut a = root(vec![text_node(Rect::from_xywh(0.0, 0.0, 40.0, 20.0), "ab")]);
247 a.children[0].caret = Some(2);
248 let mut b = a.clone();
249 b.children[0].caret = Some(1);
250 assert_eq!(
251 diff_trees(&a, &b),
252 Damage::Regions(vec![Rect::from_xywh(0.0, 0.0, 40.0, 20.0)])
253 );
254 }
255
256 #[test]
257 fn selection_change_alone_is_damage() {
258 let mut a = root(vec![text_node(
261 Rect::from_xywh(0.0, 0.0, 40.0, 20.0),
262 "abc",
263 )]);
264 a.children[0].selection = Some((0, 1));
265 let mut b = a.clone();
266 b.children[0].selection = Some((0, 3));
267 assert_eq!(
268 diff_trees(&a, &b),
269 Damage::Regions(vec![Rect::from_xywh(0.0, 0.0, 40.0, 20.0)])
270 );
271 }
272
273 #[test]
274 fn fill_change_is_detected() {
275 let a = root(vec![node(
276 Rect::from_xywh(0.0, 0.0, 50.0, 50.0),
277 Some(Color::rgb(10, 10, 10)),
278 vec![],
279 )]);
280 let mut b = a.clone();
281 b.children[0].decoration.fill = Some(Color::rgb(200, 0, 0));
282 assert_eq!(
283 diff_trees(&a, &b),
284 Damage::Regions(vec![Rect::from_xywh(0.0, 0.0, 50.0, 50.0)])
285 );
286 }
287
288 #[test]
289 fn moved_node_damages_old_and_new_position() {
290 let a = root(vec![node(
291 Rect::from_xywh(0.0, 0.0, 20.0, 20.0),
292 Some(Color::BLACK),
293 vec![],
294 )]);
295 let mut b = a.clone();
296 b.children[0].bounds = Rect::from_xywh(80.0, 0.0, 20.0, 20.0);
297 let dmg = diff_trees(&a, &b);
299 assert_eq!(
300 dmg,
301 Damage::Regions(vec![Rect::from_xywh(0.0, 0.0, 100.0, 20.0)])
302 );
303 }
304
305 #[test]
306 fn added_child_repaints_subtree() {
307 let a = root(vec![text_node(Rect::from_xywh(0.0, 0.0, 20.0, 20.0), "a")]);
308 let b = root(vec![
309 text_node(Rect::from_xywh(0.0, 0.0, 20.0, 20.0), "a"),
310 text_node(Rect::from_xywh(0.0, 30.0, 20.0, 20.0), "b"),
311 ]);
312 assert_eq!(
314 diff_trees(&a, &b),
315 Damage::Regions(vec![Rect::from_xywh(0.0, 0.0, 200.0, 100.0)])
316 );
317 }
318
319 #[test]
320 fn coalesce_merges_touching_regions() {
321 let a = root(vec![
322 node(
323 Rect::from_xywh(0.0, 0.0, 50.0, 20.0),
324 Some(Color::BLACK),
325 vec![],
326 ),
327 node(
328 Rect::from_xywh(50.0, 0.0, 50.0, 20.0),
329 Some(Color::BLACK),
330 vec![],
331 ),
332 ]);
333 let mut b = a.clone();
334 b.children[0].decoration.fill = Some(Color::rgb(1, 2, 3));
335 b.children[1].decoration.fill = Some(Color::rgb(1, 2, 3));
336 assert_eq!(
338 diff_trees(&a, &b),
339 Damage::Regions(vec![Rect::from_xywh(0.0, 0.0, 100.0, 20.0)])
340 );
341 }
342
343 #[test]
344 fn to_physical_scales_and_clips() {
345 let dmg = Damage::Regions(vec![Rect::from_xywh(10.0, 10.0, 40.0, 20.0)]);
346 let phys = dmg.to_physical(
347 ScaleFactor::new(2.0),
348 Rect::from_xywh(0.0, 0.0, 400.0, 400.0),
349 );
350 assert_eq!(phys, vec![Rect::from_xywh(20.0, 20.0, 80.0, 40.0)]);
351
352 assert!(
354 Damage::Full
355 .to_physical(ScaleFactor::IDENTITY, Rect::from_xywh(0.0, 0.0, 10.0, 10.0))
356 .is_empty()
357 );
358 assert!(
359 Damage::None
360 .to_physical(ScaleFactor::IDENTITY, Rect::from_xywh(0.0, 0.0, 10.0, 10.0))
361 .is_empty()
362 );
363 }
364}