srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/core/src/geometry.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/core/src/geometry.rs')
-rw-r--r--crates/core/src/geometry.rs146
1 files changed, 146 insertions, 0 deletions
diff --git a/crates/core/src/geometry.rs b/crates/core/src/geometry.rs
index 3dd205a..62d4369 100644
--- a/crates/core/src/geometry.rs
+++ b/crates/core/src/geometry.rs
@@ -31,6 +31,66 @@ impl Rect {
|| other.bottom() <= self.y)
}
+ /// The overlapping region of two rects, or `None` if they don't
+ /// overlap at all (matches `overlaps`' own half-open semantics: a rect
+ /// that only touches another along an edge or corner does not count).
+ pub fn intersection(&self, other: &Rect) -> Option<Rect> {
+ if !self.overlaps(other) {
+ return None;
+ }
+ let x = self.x.max(other.x);
+ let y = self.y.max(other.y);
+ let right = self.right().min(other.right());
+ let bottom = self.bottom().min(other.bottom());
+ Some(Rect::new(x, y, (right - x) as u32, (bottom - y) as u32))
+ }
+
+ /// `self` minus `other`, as the (up to 4) axis-aligned pieces left
+ /// over - the standard top/bottom/left/right sliver decomposition
+ /// around the intersection. An empty `Vec` means `other` fully covers
+ /// `self`; a one-element `Vec` equal to `self` means they don't
+ /// overlap at all.
+ fn subtract_one(&self, other: &Rect) -> Vec<Rect> {
+ let Some(ix) = self.intersection(other) else { return vec![*self] };
+ let mut out = Vec::with_capacity(4);
+ // Top sliver: full width, above the intersection.
+ if ix.y > self.y {
+ out.push(Rect::new(self.x, self.y, self.width, (ix.y - self.y) as u32));
+ }
+ // Bottom sliver: full width, below the intersection.
+ if ix.bottom() < self.bottom() {
+ out.push(Rect::new(self.x, ix.bottom(), self.width, (self.bottom() - ix.bottom()) as u32));
+ }
+ // Left/right slivers are constrained to the intersection's own
+ // y-range (not self's full height), so the top/bottom slivers
+ // above don't get double-counted at the corners.
+ if ix.x > self.x {
+ out.push(Rect::new(self.x, ix.y, (ix.x - self.x) as u32, ix.height));
+ }
+ if ix.right() < self.right() {
+ out.push(Rect::new(ix.right(), ix.y, (self.right() - ix.right()) as u32, ix.height));
+ }
+ out
+ }
+
+ /// `self` minus every rect in `occluders` that overlaps it, as the
+ /// disjoint pieces still left over. Used to keep a window's border
+ /// from rendering on top of another window's content that's actually
+ /// stacked in front of it - see `crates/wayland/src/elements.rs`'s
+ /// `visible_border_fragments` doc comment for the fuller story on why
+ /// that's needed at all. An empty result means `occluders` between
+ /// them fully cover `self`.
+ pub fn subtract_all(&self, occluders: &[Rect]) -> Vec<Rect> {
+ let mut remaining = vec![*self];
+ for occluder in occluders {
+ if remaining.is_empty() {
+ break;
+ }
+ remaining = remaining.iter().flat_map(|r| r.subtract_one(occluder)).collect();
+ }
+ remaining
+ }
+
/// Shrinks the rect on all sides by `margin`, saturating at zero size.
pub fn inset(&self, margin: u32) -> Rect {
let m = margin as i32;
@@ -91,4 +151,90 @@ mod tests {
assert!(!r.contains_point(10, 10));
assert!(r.contains_point(9, 9));
}
+
+ #[test]
+ fn intersection_of_non_overlapping_rects_is_none() {
+ let a = Rect::new(0, 0, 10, 10);
+ let b = Rect::new(20, 20, 10, 10);
+ assert_eq!(a.intersection(&b), None);
+ }
+
+ #[test]
+ fn intersection_is_the_overlapping_region() {
+ let a = Rect::new(0, 0, 100, 100);
+ let b = Rect::new(50, 50, 100, 100);
+ assert_eq!(a.intersection(&b), Some(Rect::new(50, 50, 50, 50)));
+ }
+
+ #[test]
+ fn subtract_all_with_no_occluders_returns_the_rect_unchanged() {
+ let r = Rect::new(0, 0, 100, 100);
+ assert_eq!(r.subtract_all(&[]), vec![r]);
+ }
+
+ #[test]
+ fn subtract_all_with_a_non_overlapping_occluder_returns_the_rect_unchanged() {
+ let r = Rect::new(0, 0, 100, 100);
+ let occluder = Rect::new(200, 200, 10, 10);
+ assert_eq!(r.subtract_all(&[occluder]), vec![r]);
+ }
+
+ #[test]
+ fn subtract_all_with_a_fully_covering_occluder_returns_nothing() {
+ let r = Rect::new(10, 10, 20, 20);
+ let occluder = Rect::new(0, 0, 100, 100);
+ assert!(r.subtract_all(&[occluder]).is_empty());
+ }
+
+ /// This is the exact bug this whole mechanism exists to fix, found live:
+ /// a tall vertical border strip on a background window (e.g. its right
+ /// edge) with a foreground window's content covering its middle,
+ /// leaving only a sliver above and below visible - rather than the
+ /// border rendering straight through the foreground window's content.
+ #[test]
+ fn subtract_all_splits_a_tall_strip_around_a_covering_window_into_two_slivers() {
+ // A 3px-wide, 630px-tall right border strip...
+ let border = Rect::new(890, 126, 3, 630);
+ // ...with a foreground window covering its middle vertically.
+ let foreground = Rect::new(240, 277, 800, 630);
+ let pieces = border.subtract_all(&[foreground]);
+ // Only the sliver above the foreground window's top edge and the
+ // sliver below its bottom edge should remain - the foreground
+ // window's own height (630) exceeds the border's, so in this case
+ // the whole thing is covered from y=277 down; only the top sliver
+ // (126..277) survives.
+ assert_eq!(pieces, vec![Rect::new(890, 126, 3, 277 - 126)]);
+ }
+
+ #[test]
+ fn subtract_all_leaves_a_gap_when_the_occluder_only_covers_the_middle() {
+ let strip = Rect::new(0, 0, 5, 100);
+ let occluder = Rect::new(0, 30, 5, 20); // covers y in [30, 50)
+ let pieces = strip.subtract_all(&[occluder]);
+ assert_eq!(pieces.len(), 2);
+ assert!(pieces.contains(&Rect::new(0, 0, 5, 30)));
+ assert!(pieces.contains(&Rect::new(0, 50, 5, 50)));
+ }
+
+ #[test]
+ fn subtract_all_handles_multiple_occluders_in_sequence() {
+ let strip = Rect::new(0, 0, 5, 100);
+ let a = Rect::new(0, 10, 5, 10); // [10,20)
+ let b = Rect::new(0, 40, 5, 10); // [40,50)
+ let pieces = strip.subtract_all(&[a, b]);
+ assert_eq!(pieces.len(), 3);
+ assert!(pieces.contains(&Rect::new(0, 0, 5, 10)));
+ assert!(pieces.contains(&Rect::new(0, 20, 5, 20)));
+ assert!(pieces.contains(&Rect::new(0, 50, 5, 50)));
+ }
+
+ #[test]
+ fn subtract_all_handles_a_partial_side_overlap_without_losing_area() {
+ // Occluder only covers the left half of the rect - the right half
+ // (a "right sliver") must survive intact.
+ let r = Rect::new(0, 0, 100, 50);
+ let occluder = Rect::new(-10, -10, 60, 70); // covers x in [0,50)
+ let pieces = r.subtract_all(&[occluder]);
+ assert_eq!(pieces, vec![Rect::new(50, 0, 50, 50)]);
+ }
}