257 lines
7.9 KiB
Rust
257 lines
7.9 KiB
Rust
//! Track and compute the damage of graphical primitives.
|
|
use crate::core::alignment;
|
|
use crate::core::{Rectangle, Size};
|
|
use crate::Primitive;
|
|
|
|
use std::sync::Arc;
|
|
|
|
/// A type that has some damage bounds.
|
|
pub trait Damage: PartialEq {
|
|
/// Returns the bounds of the [`Damage`].
|
|
fn bounds(&self) -> Rectangle;
|
|
}
|
|
|
|
impl<T: Damage> Damage for Primitive<T> {
|
|
fn bounds(&self) -> Rectangle {
|
|
match self {
|
|
Self::Text {
|
|
bounds,
|
|
horizontal_alignment,
|
|
vertical_alignment,
|
|
..
|
|
} => {
|
|
let mut bounds = *bounds;
|
|
|
|
bounds.x = match horizontal_alignment {
|
|
alignment::Horizontal::Left => bounds.x,
|
|
alignment::Horizontal::Center => {
|
|
bounds.x - bounds.width / 2.0
|
|
}
|
|
alignment::Horizontal::Right => bounds.x - bounds.width,
|
|
};
|
|
|
|
bounds.y = match vertical_alignment {
|
|
alignment::Vertical::Top => bounds.y,
|
|
alignment::Vertical::Center => {
|
|
bounds.y - bounds.height / 2.0
|
|
}
|
|
alignment::Vertical::Bottom => bounds.y - bounds.height,
|
|
};
|
|
|
|
bounds.expand(1.5)
|
|
}
|
|
Self::Paragraph {
|
|
paragraph,
|
|
position,
|
|
..
|
|
} => {
|
|
let mut bounds =
|
|
Rectangle::new(*position, paragraph.min_bounds);
|
|
|
|
bounds.x = match paragraph.horizontal_alignment {
|
|
alignment::Horizontal::Left => bounds.x,
|
|
alignment::Horizontal::Center => {
|
|
bounds.x - bounds.width / 2.0
|
|
}
|
|
alignment::Horizontal::Right => bounds.x - bounds.width,
|
|
};
|
|
|
|
bounds.y = match paragraph.vertical_alignment {
|
|
alignment::Vertical::Top => bounds.y,
|
|
alignment::Vertical::Center => {
|
|
bounds.y - bounds.height / 2.0
|
|
}
|
|
alignment::Vertical::Bottom => bounds.y - bounds.height,
|
|
};
|
|
|
|
bounds.expand(1.5)
|
|
}
|
|
Self::Editor {
|
|
editor, position, ..
|
|
} => {
|
|
let bounds = Rectangle::new(*position, editor.bounds);
|
|
|
|
bounds.expand(1.5)
|
|
}
|
|
Self::RawText(raw) => {
|
|
// TODO: Add `size` field to `raw` to compute more accurate
|
|
// damage bounds (?)
|
|
raw.clip_bounds.expand(1.5)
|
|
}
|
|
Self::Quad { bounds, shadow, .. } if shadow.color.a > 0.0 => {
|
|
let bounds_with_shadow = Rectangle {
|
|
x: bounds.x + shadow.offset.x.min(0.0) - shadow.blur_radius,
|
|
y: bounds.y + shadow.offset.y.min(0.0) - shadow.blur_radius,
|
|
width: bounds.width
|
|
+ shadow.offset.x.abs()
|
|
+ shadow.blur_radius * 2.0,
|
|
height: bounds.height
|
|
+ shadow.offset.y.abs()
|
|
+ shadow.blur_radius * 2.0,
|
|
};
|
|
|
|
bounds_with_shadow.expand(1.0)
|
|
}
|
|
Self::Quad { bounds, .. }
|
|
| Self::Image { bounds, .. }
|
|
| Self::Svg { bounds, .. } => bounds.expand(1.0),
|
|
Self::Clip { bounds, .. } => bounds.expand(1.0),
|
|
Self::Group { primitives } => primitives
|
|
.iter()
|
|
.map(Self::bounds)
|
|
.fold(Rectangle::with_size(Size::ZERO), |a, b| {
|
|
Rectangle::union(&a, &b)
|
|
}),
|
|
Self::Transform {
|
|
transformation,
|
|
content,
|
|
} => content.bounds() * *transformation,
|
|
Self::Cache { content } => content.bounds(),
|
|
Self::Custom(custom) => custom.bounds(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn regions<T: Damage>(a: &Primitive<T>, b: &Primitive<T>) -> Vec<Rectangle> {
|
|
match (a, b) {
|
|
(
|
|
Primitive::Group {
|
|
primitives: primitives_a,
|
|
},
|
|
Primitive::Group {
|
|
primitives: primitives_b,
|
|
},
|
|
) => return list(primitives_a, primitives_b),
|
|
(
|
|
Primitive::Clip {
|
|
bounds: bounds_a,
|
|
content: content_a,
|
|
..
|
|
},
|
|
Primitive::Clip {
|
|
bounds: bounds_b,
|
|
content: content_b,
|
|
..
|
|
},
|
|
) => {
|
|
if bounds_a == bounds_b {
|
|
return regions(content_a, content_b)
|
|
.into_iter()
|
|
.filter_map(|r| r.intersection(&bounds_a.expand(1.0)))
|
|
.collect();
|
|
} else {
|
|
return vec![bounds_a.expand(1.0), bounds_b.expand(1.0)];
|
|
}
|
|
}
|
|
(
|
|
Primitive::Transform {
|
|
transformation: transformation_a,
|
|
content: content_a,
|
|
},
|
|
Primitive::Transform {
|
|
transformation: transformation_b,
|
|
content: content_b,
|
|
},
|
|
) => {
|
|
if transformation_a == transformation_b {
|
|
return regions(content_a, content_b)
|
|
.into_iter()
|
|
.map(|r| r * *transformation_a)
|
|
.collect();
|
|
}
|
|
}
|
|
(
|
|
Primitive::Cache { content: content_a },
|
|
Primitive::Cache { content: content_b },
|
|
) => {
|
|
if Arc::ptr_eq(content_a, content_b) {
|
|
return vec![];
|
|
}
|
|
}
|
|
_ if a == b => return vec![],
|
|
_ => {}
|
|
}
|
|
|
|
let bounds_a = a.bounds();
|
|
let bounds_b = b.bounds();
|
|
|
|
if bounds_a == bounds_b {
|
|
vec![bounds_a]
|
|
} else {
|
|
vec![bounds_a, bounds_b]
|
|
}
|
|
}
|
|
|
|
/// Computes the damage regions between the two given lists of primitives.
|
|
pub fn list<T: Damage>(
|
|
previous: &[Primitive<T>],
|
|
current: &[Primitive<T>],
|
|
) -> Vec<Rectangle> {
|
|
let damage = previous
|
|
.iter()
|
|
.zip(current)
|
|
.flat_map(|(a, b)| regions(a, b));
|
|
|
|
if previous.len() == current.len() {
|
|
damage.collect()
|
|
} else {
|
|
let (smaller, bigger) = if previous.len() < current.len() {
|
|
(previous, current)
|
|
} else {
|
|
(current, previous)
|
|
};
|
|
|
|
// Extend damage by the added/removed primitives
|
|
damage
|
|
.chain(bigger[smaller.len()..].iter().map(Damage::bounds))
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
/// Groups the given damage regions that are close together inside the given
|
|
/// bounds.
|
|
pub fn group(
|
|
mut damage: Vec<Rectangle>,
|
|
scale_factor: f32,
|
|
bounds: Size<u32>,
|
|
) -> Vec<Rectangle> {
|
|
use std::cmp::Ordering;
|
|
|
|
const AREA_THRESHOLD: f32 = 20_000.0;
|
|
|
|
let bounds = Rectangle {
|
|
x: 0.0,
|
|
y: 0.0,
|
|
width: bounds.width as f32,
|
|
height: bounds.height as f32,
|
|
};
|
|
|
|
damage.sort_by(|a, b| {
|
|
a.x.partial_cmp(&b.x)
|
|
.unwrap_or(Ordering::Equal)
|
|
.then_with(|| a.y.partial_cmp(&b.y).unwrap_or(Ordering::Equal))
|
|
});
|
|
|
|
let mut output = Vec::new();
|
|
let mut scaled = damage
|
|
.into_iter()
|
|
.filter_map(|region| (region * scale_factor).intersection(&bounds))
|
|
.filter(|region| region.width >= 1.0 && region.height >= 1.0);
|
|
|
|
if let Some(mut current) = scaled.next() {
|
|
for region in scaled {
|
|
let union = current.union(®ion);
|
|
|
|
if union.area() - current.area() - region.area() <= AREA_THRESHOLD {
|
|
current = union;
|
|
} else {
|
|
output.push(current);
|
|
current = region;
|
|
}
|
|
}
|
|
|
|
output.push(current);
|
|
}
|
|
|
|
output
|
|
}
|