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}