#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct Rect {
pub x: i32,
pub y: i32,
pub width: i32,
pub height: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct Size {
pub w: i32,
pub h: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Point {
pub x: i32,
pub y: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum Direction {
Left,
Right,
Up,
Down,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct WindowId(pub isize);
impl Rect {
#[must_use]
pub fn is_empty(self) -> bool {
self.width <= 0 || self.height <= 0
}
#[must_use]
pub fn right(self) -> i32 {
self.x + self.width
}
#[must_use]
pub fn bottom(self) -> i32 {
self.y + self.height
}
#[must_use]
pub fn overlaps(self, other: Rect) -> bool {
self.x < other.right()
&& self.right() > other.x
&& self.y < other.bottom()
&& self.bottom() > other.y
}
#[must_use]
pub fn inset(self, amount: i32) -> Rect {
let shrink = amount.saturating_mul(2);
let new_width = (self.width - shrink).max(1);
let new_height = (self.height - shrink).max(1);
Rect {
x: self.x + amount,
y: self.y + amount,
width: new_width,
height: new_height,
}
}
#[must_use]
pub fn clamped_into(self, bounds: Rect) -> Rect {
let width = self.width.min(bounds.width).max(0);
let height = self.height.min(bounds.height).max(0);
let x = self
.x
.clamp(bounds.x, (bounds.right() - width).max(bounds.x));
let y = self
.y
.clamp(bounds.y, (bounds.bottom() - height).max(bounds.y));
Rect {
x,
y,
width,
height,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct InvisibleBounds {
pub left: i32,
pub top: i32,
pub right: i32,
pub bottom: i32,
}
impl InvisibleBounds {
#[must_use]
pub fn zero() -> Self {
Self {
left: 0,
top: 0,
right: 0,
bottom: 0,
}
}
#[must_use]
pub fn visible_to_window(self, visible: Rect) -> Rect {
Rect {
x: visible.x - self.left,
y: visible.y - self.top,
width: visible.width + self.left + self.right,
height: visible.height + self.top + self.bottom,
}
}
#[must_use]
pub fn window_to_visible(self, window: Rect) -> Rect {
Rect {
x: window.x + self.left,
y: window.y + self.top,
width: window.width - self.left - self.right,
height: window.height - self.top - self.bottom,
}
}
}
impl Default for InvisibleBounds {
fn default() -> Self {
Self::zero()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rect_overlaps_self() {
let r = Rect {
x: 0,
y: 0,
width: 100,
height: 100,
};
assert!(r.overlaps(r));
}
#[test]
fn rect_no_overlap_disjoint() {
let a = Rect {
x: 0,
y: 0,
width: 50,
height: 50,
};
let b = Rect {
x: 100,
y: 100,
width: 50,
height: 50,
};
assert!(!a.overlaps(b));
}
#[test]
fn rect_is_empty() {
assert!(
Rect {
x: 0,
y: 0,
width: 0,
height: 10
}
.is_empty()
);
assert!(
!Rect {
x: 0,
y: 0,
width: 1,
height: 1
}
.is_empty()
);
}
#[test]
fn rect_is_empty_negative_width() {
assert!(
Rect {
x: 0,
y: 0,
width: -5,
height: 100
}
.is_empty()
);
}
#[test]
fn rect_is_empty_negative_height() {
assert!(
Rect {
x: 0,
y: 0,
width: 100,
height: -1
}
.is_empty()
);
}
#[test]
fn rect_right_and_bottom() {
assert_eq!(
Rect {
x: 10,
y: 20,
width: 100,
height: 200
}
.right(),
110
);
assert_eq!(
Rect {
x: 10,
y: 20,
width: 100,
height: 200
}
.bottom(),
220
);
}
#[test]
fn rect_inset_basic() {
let r = Rect {
x: 100,
y: 200,
width: 300,
height: 400,
};
assert_eq!(
r.inset(5),
Rect {
x: 105,
y: 205,
width: 290,
height: 390,
}
);
}
#[test]
fn rect_inset_zero_is_identity() {
let r = Rect {
x: 7,
y: 11,
width: 42,
height: 53,
};
assert_eq!(r.inset(0), r);
}
#[test]
fn rect_inset_clamps_to_min_1x1() {
let r = Rect {
x: 0,
y: 0,
width: 20,
height: 15,
};
assert_eq!(
r.inset(15),
Rect {
x: 15,
y: 15,
width: 1,
height: 1,
}
);
}
#[test]
fn rect_inset_exact_half_produces_1x1() {
let r = Rect {
x: 10,
y: 10,
width: 10,
height: 10,
};
assert_eq!(
r.inset(5),
Rect {
x: 15,
y: 15,
width: 1,
height: 1,
}
);
}
#[test]
fn rect_overlaps_adjacent_no_overlap() {
let a = Rect {
x: 0,
y: 0,
width: 100,
height: 100,
};
let b = Rect {
x: 100,
y: 0,
width: 100,
height: 100,
};
assert!(!a.overlaps(b));
}
#[test]
fn rect_overlaps_partial() {
let a = Rect {
x: 0,
y: 0,
width: 100,
height: 100,
};
let b = Rect {
x: 50,
y: 50,
width: 100,
height: 100,
};
assert!(a.overlaps(b));
assert!(b.overlaps(a));
}
#[test]
fn rect_overlaps_contained() {
let outer = Rect {
x: 0,
y: 0,
width: 100,
height: 100,
};
let inner = Rect {
x: 25,
y: 25,
width: 50,
height: 50,
};
assert!(outer.overlaps(inner));
}
#[test]
fn direction_equality() {
assert_eq!(Direction::Left, Direction::Left);
assert_ne!(Direction::Left, Direction::Right);
}
#[test]
fn window_id_hashable() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(WindowId(1));
set.insert(WindowId(2));
assert!(set.contains(&WindowId(1)));
assert!(!set.contains(&WindowId(3)));
}
#[test]
fn window_id_clone_and_copy() {
let id = WindowId(42);
let copy = id;
assert_eq!(id, copy);
}
#[test]
fn direction_serialize_roundtrip_all_variants() {
for dir in [
Direction::Left,
Direction::Right,
Direction::Up,
Direction::Down,
] {
let json = serde_json::to_string(&dir).unwrap();
let parsed: Direction = serde_json::from_str(&json).unwrap();
assert_eq!(dir, parsed, "Direction roundtrip failed for: {dir:?}");
}
}
#[test]
fn direction_serialized_values() {
assert_eq!(
serde_json::to_string(&Direction::Left).unwrap(),
r#""Left""#
);
assert_eq!(
serde_json::to_string(&Direction::Right).unwrap(),
r#""Right""#
);
assert_eq!(serde_json::to_string(&Direction::Up).unwrap(), r#""Up""#);
assert_eq!(
serde_json::to_string(&Direction::Down).unwrap(),
r#""Down""#
);
}
#[test]
fn direction_deserialize_invalid_returns_none() {
let result: Result<Direction, _> = serde_json::from_str(r#""diagonal""#);
assert!(
result.is_err(),
"invalid direction should fail to deserialize"
);
}
#[test]
fn direction_deserialize_wrong_type_fails() {
let result: Result<Direction, _> = serde_json::from_str("42");
assert!(
result.is_err(),
"number should not deserialize as Direction"
);
}
#[test]
fn invisible_bounds_zero_all_zeros() {
let b = InvisibleBounds::zero();
assert_eq!(b.left, 0);
assert_eq!(b.top, 0);
assert_eq!(b.right, 0);
assert_eq!(b.bottom, 0);
}
#[test]
fn invisible_bounds_default_is_zero() {
assert_eq!(InvisibleBounds::default(), InvisibleBounds::zero());
}
#[test]
fn invisible_bounds_visible_to_window_expands() {
let bounds = InvisibleBounds {
left: 7,
top: 0,
right: 7,
bottom: 7,
};
let visible = Rect {
x: 100,
y: 0,
width: 800,
height: 600,
};
let window = bounds.visible_to_window(visible);
assert_eq!(window.x, 93);
assert_eq!(window.y, 0);
assert_eq!(window.width, 814);
assert_eq!(window.height, 607);
}
#[test]
fn invisible_bounds_window_to_visible_shrinks() {
let bounds = InvisibleBounds {
left: 7,
top: 0,
right: 7,
bottom: 7,
};
let window = Rect {
x: 93,
y: 0,
width: 814,
height: 607,
};
let visible = bounds.window_to_visible(window);
assert_eq!(visible.x, 100);
assert_eq!(visible.y, 0);
assert_eq!(visible.width, 800);
assert_eq!(visible.height, 600);
}
#[test]
fn invisible_bounds_roundtrip_visible_to_window_to_visible() {
let bounds = InvisibleBounds {
left: 7,
top: 3,
right: 7,
bottom: 7,
};
let original = Rect {
x: 500,
y: 200,
width: 1000,
height: 800,
};
let roundtrip = bounds.window_to_visible(bounds.visible_to_window(original));
assert_eq!(original, roundtrip);
}
#[test]
fn invisible_bounds_zero_bounds_is_identity() {
let bounds = InvisibleBounds::zero();
let rect = Rect {
x: 100,
y: 200,
width: 300,
height: 400,
};
assert_eq!(bounds.visible_to_window(rect), rect);
assert_eq!(bounds.window_to_visible(rect), rect);
}
#[test]
fn invisible_bounds_asymmetric() {
let bounds = InvisibleBounds {
left: 7,
top: 0,
right: 7,
bottom: 7,
};
let visible = Rect {
x: 0,
y: 0,
width: 100,
height: 100,
};
let window = bounds.visible_to_window(visible);
assert_eq!(window.y, 0);
assert_eq!(window.x, -7);
assert_eq!(window.width, 114); assert_eq!(window.height, 107); }
#[test]
fn invisible_bounds_serialize_roundtrip() {
let bounds = InvisibleBounds {
left: 7,
top: 0,
right: 7,
bottom: 7,
};
let json = serde_json::to_string(&bounds).unwrap();
let parsed: InvisibleBounds = serde_json::from_str(&json).unwrap();
assert_eq!(bounds, parsed);
}
#[test]
fn clamped_into_unchanged_when_already_inside() {
let bounds = Rect {
x: 0,
y: 0,
width: 1920,
height: 1080,
};
let r = Rect {
x: 100,
y: 100,
width: 800,
height: 600,
};
assert_eq!(r.clamped_into(bounds), r);
}
#[test]
fn clamped_into_shifts_left_edge_inward() {
let bounds = Rect {
x: 100,
y: 0,
width: 1000,
height: 1000,
};
let r = Rect {
x: -200,
y: 50,
width: 400,
height: 400,
};
let clamped = r.clamped_into(bounds);
assert_eq!(clamped.x, 100);
assert_eq!(clamped.y, 50);
assert_eq!(clamped.width, 400);
assert_eq!(clamped.height, 400);
}
#[test]
fn clamped_into_shifts_right_edge_inward() {
let bounds = Rect {
x: 0,
y: 0,
width: 1000,
height: 1000,
};
let r = Rect {
x: 900,
y: 0,
width: 400,
height: 400,
};
let clamped = r.clamped_into(bounds);
assert_eq!(clamped.x, 600); assert_eq!(clamped.width, 400);
}
#[test]
fn clamped_into_shifts_top_edge_inward() {
let bounds = Rect {
x: 0,
y: 100,
width: 1000,
height: 1000,
};
let r = Rect {
x: 50,
y: -300,
width: 200,
height: 200,
};
let clamped = r.clamped_into(bounds);
assert_eq!(clamped.y, 100);
}
#[test]
fn clamped_into_shifts_bottom_edge_inward() {
let bounds = Rect {
x: 0,
y: 0,
width: 1000,
height: 1000,
};
let r = Rect {
x: 0,
y: 900,
width: 200,
height: 200,
};
let clamped = r.clamped_into(bounds);
assert_eq!(clamped.y, 800); }
#[test]
fn clamped_into_shrinks_when_larger_than_bounds() {
let bounds = Rect {
x: 0,
y: 0,
width: 500,
height: 400,
};
let r = Rect {
x: -100,
y: -100,
width: 2000,
height: 2000,
};
assert_eq!(r.clamped_into(bounds), bounds);
}
#[test]
fn clamped_into_exactly_filling_bounds_is_unchanged() {
let bounds = Rect {
x: 0,
y: 0,
width: 1000,
height: 1000,
};
let r = Rect {
x: 0,
y: 0,
width: 1000,
height: 1000,
};
assert_eq!(r.clamped_into(bounds), r);
}
#[test]
fn clamped_into_offset_bounds() {
let bounds = Rect {
x: 1920,
y: 0,
width: 1280,
height: 1024,
};
let r = Rect {
x: 0,
y: 0,
width: 600,
height: 400,
};
let clamped = r.clamped_into(bounds);
assert_eq!(clamped.x, 1920);
assert_eq!(clamped.y, 0);
assert_eq!(clamped.width, 600);
assert_eq!(clamped.height, 400);
}
}