Skip to main content

tear_types/
geometry.rs

1//! Axis-aligned rectangles — the geometry the layout algebra renders to.
2//!
3//! A [`Rect`] is the bounding box a [`crate::LayoutNode`] subtree is
4//! allotted. The unit is the *caller's*: tear-core measures in terminal
5//! cells (cols × rows), mado scales the same rects to device pixels for
6//! the GPU. One [`crate::LayoutNode::compute_rects`] algorithm serves
7//! both — so a split looks identical whether the daemon is sizing a PTY
8//! winsize or mado is drawing pane borders. That single-renderer
9//! property is the whole reason this type lives in `tear-types` and not
10//! in either app (the compounding directive's "solve once" rule).
11//!
12//! Cells are `u16` — a terminal never exceeds 65535 cols/rows, and the
13//! width/height of a child is always ≤ its parent, so no arithmetic here
14//! overflows. Division is *gap-free and overlap-free by construction*:
15//! a split hands side `a` a clamped extent and side `b` the exact
16//! remainder, so `a.right() == b.x` (or `a.bottom() == b.y`) always —
17//! there is no representable layout with a one-cell seam or a one-cell
18//! double-draw.
19
20use serde::{Deserialize, Serialize};
21
22/// An axis-aligned rectangle in a discrete cell grid. `x`/`y` are the
23/// top-left origin; `w`/`h` are width/height. A zero-area rect is legal
24/// (it models a pane squeezed out of a too-small window) but
25/// [`Rect::is_empty`] flags it so renderers can skip it.
26#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub struct Rect {
28    pub x: u16,
29    pub y: u16,
30    pub w: u16,
31    pub h: u16,
32}
33
34impl Rect {
35    /// A rect at the origin with the given size — the usual window-root
36    /// bounding box (`Rect::new(0, 0, cols, rows)`).
37    #[must_use]
38    pub const fn new(x: u16, y: u16, w: u16, h: u16) -> Self {
39        Self { x, y, w, h }
40    }
41
42    /// A rect rooted at the origin (`x = y = 0`).
43    #[must_use]
44    pub const fn sized(w: u16, h: u16) -> Self {
45        Self { x: 0, y: 0, w, h }
46    }
47
48    /// The x coordinate one past the right edge (`x + w`). Widened to
49    /// `u32` so a full-width rect at a high `x` cannot wrap.
50    #[must_use]
51    pub const fn right(self) -> u32 {
52        self.x as u32 + self.w as u32
53    }
54
55    /// The y coordinate one past the bottom edge (`y + h`).
56    #[must_use]
57    pub const fn bottom(self) -> u32 {
58        self.y as u32 + self.h as u32
59    }
60
61    /// Area in cells.
62    #[must_use]
63    pub const fn area(self) -> u32 {
64        self.w as u32 * self.h as u32
65    }
66
67    /// True when either dimension is zero — the pane has been squeezed
68    /// to nothing and a renderer should skip it.
69    #[must_use]
70    pub const fn is_empty(self) -> bool {
71        self.w == 0 || self.h == 0
72    }
73
74    /// True when `(px, py)` falls inside the rect (right/bottom edges
75    /// exclusive — adjacent rects never both claim a boundary cell).
76    #[must_use]
77    pub fn contains(self, px: u16, py: u16) -> bool {
78        let (px, py) = (u32::from(px), u32::from(py));
79        u32::from(self.x) <= px && px < self.right() && u32::from(self.y) <= py && py < self.bottom()
80    }
81
82    /// Length of the overlap between two 1-D intervals `[a0, a1)` and
83    /// `[b0, b1)`. Used by directional navigation to score how much two
84    /// panes share an edge. Returns 0 when they don't overlap.
85    #[must_use]
86    pub(crate) fn span_overlap(a0: u32, a1: u32, b0: u32, b1: u32) -> u32 {
87        let lo = a0.max(b0);
88        let hi = a1.min(b1);
89        hi.saturating_sub(lo)
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn right_and_bottom_are_exclusive_edges() {
99        let r = Rect::new(2, 3, 10, 5);
100        assert_eq!(r.right(), 12);
101        assert_eq!(r.bottom(), 8);
102        assert!(r.contains(2, 3)); // top-left inclusive
103        assert!(!r.contains(12, 3)); // right exclusive
104        assert!(!r.contains(2, 8)); // bottom exclusive
105        assert!(r.contains(11, 7)); // last interior cell
106    }
107
108    #[test]
109    fn empty_when_either_dim_zero() {
110        assert!(Rect::sized(0, 9).is_empty());
111        assert!(Rect::sized(9, 0).is_empty());
112        assert!(!Rect::sized(1, 1).is_empty());
113        assert_eq!(Rect::new(0, 0, 4, 5).area(), 20);
114    }
115
116    #[test]
117    fn span_overlap_is_intersection_length() {
118        assert_eq!(Rect::span_overlap(0, 10, 5, 15), 5);
119        assert_eq!(Rect::span_overlap(0, 5, 5, 10), 0); // touching, not overlapping
120        assert_eq!(Rect::span_overlap(0, 5, 10, 15), 0); // disjoint
121        assert_eq!(Rect::span_overlap(2, 8, 0, 20), 6); // fully contained
122    }
123
124    #[test]
125    fn high_coordinate_right_edge_does_not_wrap() {
126        let r = Rect::new(u16::MAX - 3, 0, 3, 1);
127        assert_eq!(r.right(), u32::from(u16::MAX));
128    }
129}