use crate::runtime::LayoutNode;
use stipple_geometry::{Rect, ScaleFactor};
#[derive(Clone, Debug, PartialEq)]
pub enum Damage {
None,
Regions(Vec<Rect>),
Full,
}
impl Damage {
pub fn is_empty(&self) -> bool {
matches!(self, Damage::None)
}
pub fn bounding(&self) -> Option<Rect> {
match self {
Damage::Regions(rs) => rs.iter().copied().reduce(|a, b| a.union(b)),
_ => None,
}
}
pub fn to_physical(&self, scale: ScaleFactor, bounds: Rect) -> Vec<Rect> {
let Damage::Regions(regions) = self else {
return Vec::new();
};
let s = scale.get();
regions
.iter()
.filter_map(|r| {
let phys = Rect::from_xywh(
(r.min_x() * s).floor(),
(r.min_y() * s).floor(),
(r.width() * s).ceil(),
(r.height() * s).ceil(),
);
let snapped = Rect::from_points(
stipple_geometry::Point::new(phys.min_x(), phys.min_y()),
stipple_geometry::Point::new((r.max_x() * s).ceil(), (r.max_y() * s).ceil()),
);
snapped.intersection(bounds).filter(|c| !c.is_empty())
})
.collect()
}
}
pub fn diff_trees(old: &LayoutNode, new: &LayoutNode) -> Damage {
let mut regions = Vec::new();
diff_node(old, new, &mut regions);
if regions.is_empty() {
Damage::None
} else {
Damage::Regions(coalesce(regions))
}
}
fn visuals_equal(a: &LayoutNode, b: &LayoutNode) -> bool {
a.bounds == b.bounds
&& a.decoration == b.decoration
&& a.content == b.content
&& a.caret == b.caret
&& a.selection == b.selection
}
fn diff_node(old: &LayoutNode, new: &LayoutNode, out: &mut Vec<Rect>) {
if !visuals_equal(old, new) {
out.push(old.bounds.union(new.bounds));
}
if old.children.len() != new.children.len() {
out.push(old.bounds.union(new.bounds));
return;
}
for (o, n) in old.children.iter().zip(&new.children) {
diff_node(o, n, out);
}
}
fn coalesce(mut rects: Vec<Rect>) -> Vec<Rect> {
let mut merged = true;
while merged {
merged = false;
let mut i = 0;
while i < rects.len() {
let mut j = i + 1;
while j < rects.len() {
if overlaps_or_touches(rects[i], rects[j]) {
rects[i] = rects[i].union(rects[j]);
rects.swap_remove(j);
merged = true;
} else {
j += 1;
}
}
i += 1;
}
}
rects
}
fn overlaps_or_touches(a: Rect, b: Rect) -> bool {
a.min_x() <= b.max_x()
&& b.min_x() <= a.max_x()
&& a.min_y() <= b.max_y()
&& b.min_y() <= a.max_y()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::element::BoxStyle;
use crate::runtime::NodeContent;
use stipple_render::Color;
fn node(bounds: Rect, fill: Option<Color>, children: Vec<LayoutNode>) -> LayoutNode {
LayoutNode {
bounds,
decoration: BoxStyle {
fill,
radius: 0.0,
border: None,
},
content: NodeContent::None,
action: None,
focus: None,
drag: None,
context: None,
caret: None,
selection: None,
text_pos: None,
wrap: false,
scroll: None,
clip: false,
children,
}
}
fn text_node(bounds: Rect, text: &str) -> LayoutNode {
LayoutNode {
bounds,
decoration: BoxStyle::default(),
content: NodeContent::Text {
text: text.into(),
size: 14.0,
color: Color::BLACK,
},
action: None,
focus: None,
drag: None,
context: None,
caret: None,
selection: None,
text_pos: None,
wrap: false,
scroll: None,
clip: false,
children: Vec::new(),
}
}
fn root(children: Vec<LayoutNode>) -> LayoutNode {
node(Rect::from_xywh(0.0, 0.0, 200.0, 100.0), None, children)
}
#[test]
fn identical_trees_have_no_damage() {
let a = root(vec![text_node(
Rect::from_xywh(10.0, 10.0, 40.0, 20.0),
"hi",
)]);
let b = a.clone();
assert_eq!(diff_trees(&a, &b), Damage::None);
assert!(diff_trees(&a, &b).is_empty());
}
#[test]
fn changed_text_damages_only_that_node() {
let a = root(vec![
text_node(Rect::from_xywh(10.0, 10.0, 40.0, 20.0), "0"),
text_node(Rect::from_xywh(10.0, 40.0, 40.0, 20.0), "stable"),
]);
let mut b = a.clone();
b.children[0] = text_node(Rect::from_xywh(10.0, 10.0, 40.0, 20.0), "1");
let dmg = diff_trees(&a, &b);
assert_eq!(
dmg,
Damage::Regions(vec![Rect::from_xywh(10.0, 10.0, 40.0, 20.0)])
);
let bound = dmg.bounding().unwrap();
assert!(bound.width() < 200.0 && bound.height() < 100.0);
}
#[test]
fn caret_move_alone_is_damage() {
let mut a = root(vec![text_node(Rect::from_xywh(0.0, 0.0, 40.0, 20.0), "ab")]);
a.children[0].caret = Some(2);
let mut b = a.clone();
b.children[0].caret = Some(1);
assert_eq!(
diff_trees(&a, &b),
Damage::Regions(vec![Rect::from_xywh(0.0, 0.0, 40.0, 20.0)])
);
}
#[test]
fn selection_change_alone_is_damage() {
let mut a = root(vec![text_node(
Rect::from_xywh(0.0, 0.0, 40.0, 20.0),
"abc",
)]);
a.children[0].selection = Some((0, 1));
let mut b = a.clone();
b.children[0].selection = Some((0, 3));
assert_eq!(
diff_trees(&a, &b),
Damage::Regions(vec![Rect::from_xywh(0.0, 0.0, 40.0, 20.0)])
);
}
#[test]
fn fill_change_is_detected() {
let a = root(vec![node(
Rect::from_xywh(0.0, 0.0, 50.0, 50.0),
Some(Color::rgb(10, 10, 10)),
vec![],
)]);
let mut b = a.clone();
b.children[0].decoration.fill = Some(Color::rgb(200, 0, 0));
assert_eq!(
diff_trees(&a, &b),
Damage::Regions(vec![Rect::from_xywh(0.0, 0.0, 50.0, 50.0)])
);
}
#[test]
fn moved_node_damages_old_and_new_position() {
let a = root(vec![node(
Rect::from_xywh(0.0, 0.0, 20.0, 20.0),
Some(Color::BLACK),
vec![],
)]);
let mut b = a.clone();
b.children[0].bounds = Rect::from_xywh(80.0, 0.0, 20.0, 20.0);
let dmg = diff_trees(&a, &b);
assert_eq!(
dmg,
Damage::Regions(vec![Rect::from_xywh(0.0, 0.0, 100.0, 20.0)])
);
}
#[test]
fn added_child_repaints_subtree() {
let a = root(vec![text_node(Rect::from_xywh(0.0, 0.0, 20.0, 20.0), "a")]);
let b = root(vec![
text_node(Rect::from_xywh(0.0, 0.0, 20.0, 20.0), "a"),
text_node(Rect::from_xywh(0.0, 30.0, 20.0, 20.0), "b"),
]);
assert_eq!(
diff_trees(&a, &b),
Damage::Regions(vec![Rect::from_xywh(0.0, 0.0, 200.0, 100.0)])
);
}
#[test]
fn coalesce_merges_touching_regions() {
let a = root(vec![
node(
Rect::from_xywh(0.0, 0.0, 50.0, 20.0),
Some(Color::BLACK),
vec![],
),
node(
Rect::from_xywh(50.0, 0.0, 50.0, 20.0),
Some(Color::BLACK),
vec![],
),
]);
let mut b = a.clone();
b.children[0].decoration.fill = Some(Color::rgb(1, 2, 3));
b.children[1].decoration.fill = Some(Color::rgb(1, 2, 3));
assert_eq!(
diff_trees(&a, &b),
Damage::Regions(vec![Rect::from_xywh(0.0, 0.0, 100.0, 20.0)])
);
}
#[test]
fn to_physical_scales_and_clips() {
let dmg = Damage::Regions(vec![Rect::from_xywh(10.0, 10.0, 40.0, 20.0)]);
let phys = dmg.to_physical(
ScaleFactor::new(2.0),
Rect::from_xywh(0.0, 0.0, 400.0, 400.0),
);
assert_eq!(phys, vec![Rect::from_xywh(20.0, 20.0, 80.0, 40.0)]);
assert!(
Damage::Full
.to_physical(ScaleFactor::IDENTITY, Rect::from_xywh(0.0, 0.0, 10.0, 10.0))
.is_empty()
);
assert!(
Damage::None
.to_physical(ScaleFactor::IDENTITY, Rect::from_xywh(0.0, 0.0, 10.0, 10.0))
.is_empty()
);
}
}