1use euclid::{Box2D, Point2D};
6use webrender_api::units::LayoutPixel;
7
8#[derive(Clone, Debug, Default)]
15pub(crate) struct PaintedRegion {
16 rects: Vec<Box2D<f32, LayoutPixel>>,
18}
19
20impl PaintedRegion {
21 pub(crate) fn area(&self) -> f32 {
23 self.rects.iter().map(Box2D::area).sum()
24 }
25
26 pub(crate) fn bounds(&self) -> Box2D<f32, LayoutPixel> {
28 self.rects
32 .iter()
33 .copied()
34 .reduce(|bounds, rect| bounds.union(&rect))
35 .unwrap_or_else(Box2D::zero)
36 }
37
38 pub(crate) fn contains(&self, rect: Box2D<f32, LayoutPixel>) -> bool {
41 if rect.is_empty() {
42 return true;
43 }
44 if self
48 .rects
49 .iter()
50 .any(|existing| existing.contains_box(&rect))
51 {
52 return true;
53 }
54 subtract_all(rect, &self.rects).is_empty()
55 }
56
57 pub(crate) fn union(&mut self, rect: Box2D<f32, LayoutPixel>) {
61 let rect = rect.round_out();
62 if rect.is_empty() || self.contains(rect) {
63 return;
64 }
65 self.rects.extend(subtract_all(rect, &self.rects));
66 }
67
68 pub(crate) fn merge(&mut self, other: &PaintedRegion) {
69 for &rect in &other.rects {
70 self.union(rect);
71 }
72 }
73}
74
75fn subtract_all(
78 rect: Box2D<f32, LayoutPixel>,
79 others: &[Box2D<f32, LayoutPixel>],
80) -> Vec<Box2D<f32, LayoutPixel>> {
81 let mut pieces = vec![rect];
82 for other in others {
83 if pieces.is_empty() {
84 break;
85 }
86 pieces = pieces
87 .into_iter()
88 .flat_map(|piece| subtract_one(piece, *other))
89 .collect();
90 }
91 pieces
92}
93
94fn subtract_one(
97 a: Box2D<f32, LayoutPixel>,
98 b: Box2D<f32, LayoutPixel>,
99) -> Vec<Box2D<f32, LayoutPixel>> {
100 let Some(i) = a.intersection(&b) else {
101 return vec![a];
102 };
103 if i.is_empty() {
104 return vec![a];
105 }
106
107 let mut out = Vec::with_capacity(4);
108 if i.min.y > a.min.y {
109 out.push(Box2D::new(
110 Point2D::new(a.min.x, a.min.y),
111 Point2D::new(a.max.x, i.min.y),
112 ));
113 }
114 if i.max.y < a.max.y {
115 out.push(Box2D::new(
116 Point2D::new(a.min.x, i.max.y),
117 Point2D::new(a.max.x, a.max.y),
118 ));
119 }
120 if i.min.x > a.min.x {
121 out.push(Box2D::new(
122 Point2D::new(a.min.x, i.min.y),
123 Point2D::new(i.min.x, i.max.y),
124 ));
125 }
126 if i.max.x < a.max.x {
127 out.push(Box2D::new(
128 Point2D::new(i.max.x, i.min.y),
129 Point2D::new(a.max.x, i.max.y),
130 ));
131 }
132 out
133}
134
135#[cfg(test)]
136mod tests {
137 use euclid::Box2D;
138
139 use super::PaintedRegion;
140
141 fn rect(x0: f32, y0: f32, x1: f32, y1: f32) -> Box2D<f32, webrender_api::units::LayoutPixel> {
142 Box2D::new((x0, y0).into(), (x1, y1).into())
143 }
144
145 #[test]
146 fn repeated_identical_rect_does_not_grow() {
147 let mut region = PaintedRegion::default();
148 region.union(rect(0.0, 0.0, 10.0, 10.0));
149 assert_eq!(region.area(), 100.0);
150 for _ in 0..5 {
151 assert!(region.contains(rect(0.0, 0.0, 10.0, 10.0)));
152 region.union(rect(0.0, 0.0, 10.0, 10.0));
153 }
154 assert_eq!(region.area(), 100.0);
155 }
156
157 #[test]
158 fn overlapping_rects_are_not_double_counted() {
159 let mut region = PaintedRegion::default();
160 region.union(rect(0.0, 0.0, 10.0, 10.0));
161 region.union(rect(5.0, 5.0, 15.0, 15.0));
162 assert_eq!(region.area(), 175.0);
164 assert_eq!(region.bounds(), rect(0.0, 0.0, 15.0, 15.0));
165 }
166
167 #[test]
168 fn bounds_do_not_reach_back_to_the_origin() {
169 let mut region = PaintedRegion::default();
170 region.union(rect(100.0, 100.0, 200.0, 200.0));
171 assert_eq!(region.bounds(), rect(100.0, 100.0, 200.0, 200.0));
172 }
173
174 #[test]
175 fn disjoint_rects_sum() {
176 let mut region = PaintedRegion::default();
177 region.union(rect(0.0, 0.0, 10.0, 10.0));
178 region.union(rect(20.0, 20.0, 30.0, 30.0));
179 assert_eq!(region.area(), 200.0);
180 }
181
182 #[test]
183 fn fully_contained_rect_is_a_noop() {
184 let mut region = PaintedRegion::default();
185 region.union(rect(0.0, 0.0, 100.0, 100.0));
186 assert!(region.contains(rect(10.0, 10.0, 20.0, 20.0)));
187 region.union(rect(10.0, 10.0, 20.0, 20.0));
188 assert_eq!(region.area(), 10_000.0);
189 }
190
191 #[test]
192 fn empty_rect_is_a_noop() {
193 let mut region = PaintedRegion::default();
194 region.union(rect(5.0, 5.0, 5.0, 5.0));
195 assert_eq!(region.area(), 0.0);
196 assert!(region.contains(rect(5.0, 5.0, 5.0, 5.0)));
197 }
198}