Skip to main content

term_wm_layout_engine/
rect.rs

1use core::fmt;
2
3/// A rectangle with signed origin and unsigned dimensions.
4///
5/// Used throughout the engine to represent both screen-space regions and
6/// floating-window geometry where off-screen coordinates are valid.
7#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
8pub struct LayoutRect {
9    pub x: i32,
10    pub y: i32,
11    pub width: u16,
12    pub height: u16,
13}
14
15impl LayoutRect {
16    /// Centre point of the rectangle, rounding down on odd dimensions.
17    pub fn center(&self) -> (i32, i32) {
18        (
19            self.x + i32::from(self.width) / 2,
20            self.y + i32::from(self.height) / 2,
21        )
22    }
23
24    /// Check if the rectangle has zero area.
25    pub fn is_empty(&self) -> bool {
26        self.width == 0 || self.height == 0
27    }
28
29    /// Compute the intersection of two rectangles.
30    pub fn intersection(&self, other: LayoutRect) -> LayoutRect {
31        let x1 = self.x.max(other.x);
32        let y1 = self.y.max(other.y);
33        let self_right = self.x.saturating_add(i32::from(self.width));
34        let other_right = other.x.saturating_add(i32::from(other.width));
35        let self_bottom = self.y.saturating_add(i32::from(self.height));
36        let other_bottom = other.y.saturating_add(i32::from(other.height));
37        let x2 = self_right.min(other_right);
38        let y2 = self_bottom.min(other_bottom);
39        if x2 <= x1 || y2 <= y1 {
40            LayoutRect {
41                x: 0,
42                y: 0,
43                width: 0,
44                height: 0,
45            }
46        } else {
47            LayoutRect {
48                x: x1,
49                y: y1,
50                width: (x2 - x1) as u16,
51                height: (y2 - y1) as u16,
52            }
53        }
54    }
55
56    /// Check if a point is inside the rectangle.
57    /// Generic over coordinate type - accepts both i32 and u16.
58    pub fn contains<T: Into<i32>>(&self, col: T, row: T) -> bool {
59        if self.width == 0 || self.height == 0 {
60            return false;
61        }
62        let col = col.into();
63        let row = row.into();
64        let max_x = self.x.saturating_add(i32::from(self.width));
65        let max_y = self.y.saturating_add(i32::from(self.height));
66        col >= self.x && col < max_x && row >= self.y && row < max_y
67    }
68
69    pub fn clamp(self, bounds: LayoutRect) -> LayoutRect {
70        let x1 = self.x.max(bounds.x);
71        let y1 = self.y.max(bounds.y);
72        let self_right = self.x.saturating_add(i32::from(self.width));
73        let bounds_right = bounds.x.saturating_add(i32::from(bounds.width));
74        let self_bottom = self.y.saturating_add(i32::from(self.height));
75        let bounds_bottom = bounds.y.saturating_add(i32::from(bounds.height));
76        let x2 = self_right.min(bounds_right);
77        let y2 = self_bottom.min(bounds_bottom);
78        if x2 <= x1 || y2 <= y1 {
79            return LayoutRect {
80                x: 0,
81                y: 0,
82                width: 0,
83                height: 0,
84            };
85        }
86        LayoutRect {
87            x: x1,
88            y: y1,
89            width: (x2.saturating_sub(x1)) as u16,
90            height: (y2.saturating_sub(y1)) as u16,
91        }
92    }
93
94    pub fn visible_portion(self, bounds: LayoutRect) -> LayoutRect {
95        self.clamp(bounds)
96    }
97
98    pub fn intersects(self, other: LayoutRect) -> bool {
99        let a_right = self.x.saturating_add(i32::from(self.width));
100        let a_bottom = self.y.saturating_add(i32::from(self.height));
101        let b_right = other.x.saturating_add(i32::from(other.width));
102        let b_bottom = other.y.saturating_add(i32::from(other.height));
103        self.x < b_right && a_right > other.x && self.y < b_bottom && a_bottom > other.y
104    }
105
106    /// Subtract the rect origin from screen coordinates to get local deltas.
107    ///
108    /// CATEGORY 3 — Scalar Geometry.
109    /// Pure arithmetic helper for text_renderer.rs and mouse_coord.rs.
110    /// Returns signed deltas that may be negative (positions outside the rect).
111    pub fn screen_to_local_point(&self, col: u16, row: u16) -> (i32, i32) {
112        (i32::from(col) - self.x, i32::from(row) - self.y)
113    }
114}
115
116/// Convenience wrapper around [`LayoutRect::contains`].
117/// Generic over coordinate type.
118pub fn rect_contains<T: Into<i32>>(rect: &LayoutRect, col: T, row: T) -> bool {
119    rect.contains(col, row)
120}
121
122/// Shrink a rectangle by the given margins on each side.
123/// The resulting width/height saturate at zero.
124/// Generic over margin types.
125pub fn inset<T: Into<u16> + Copy>(
126    rect: LayoutRect,
127    left: T,
128    right: T,
129    top: T,
130    bottom: T,
131) -> LayoutRect {
132    let left: u16 = left.into();
133    let right: u16 = right.into();
134    let top: u16 = top.into();
135    let bottom: u16 = bottom.into();
136    LayoutRect {
137        x: rect.x.saturating_add(i32::from(left)),
138        y: rect.y.saturating_add(i32::from(top)),
139        width: rect.width.saturating_sub(left.saturating_add(right)),
140        height: rect.height.saturating_sub(top.saturating_add(bottom)),
141    }
142}
143
144/// Offset a rectangle by `gap * index` along the given orientation.
145/// Used when placing children in a split with inter-child gaps.
146/// Generic over gap type.
147pub fn gap_insert<T: Into<u16>>(
148    rect: LayoutRect,
149    gap: T,
150    index: usize,
151    orientation: Orientation,
152) -> LayoutRect {
153    let offset = gap.into().saturating_mul(index as u16);
154    match orientation {
155        Orientation::Horizontal => LayoutRect {
156            x: rect.x.saturating_add(i32::from(offset)),
157            ..rect
158        },
159        Orientation::Vertical => LayoutRect {
160            y: rect.y.saturating_add(i32::from(offset)),
161            ..rect
162        },
163    }
164}
165
166/// Which diagonal quadrant of a rectangle a point falls in.
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub enum Quadrant {
169    North,
170    South,
171    East,
172    West,
173}
174
175impl Quadrant {
176    /// Map this quadrant to the corresponding tiling insertion position.
177    pub fn to_insert_position(self) -> crate::snap::InsertPosition {
178        use crate::snap::InsertPosition;
179        match self {
180            Quadrant::North => InsertPosition::Top,
181            Quadrant::South => InsertPosition::Bottom,
182            Quadrant::West => InsertPosition::Left,
183            Quadrant::East => InsertPosition::Right,
184        }
185    }
186}
187
188/// The direction children are stacked in a split container.
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub enum Orientation {
191    /// Children are placed left-to-right, sharing the available height.
192    Horizontal,
193    /// Children are placed top-to-bottom, sharing the available width.
194    Vertical,
195}
196
197/// An integer ratio `(p, q)` meaning `p/(p+q)` of the parent's size.
198///
199/// Remainder isolation guarantees `sum(child sizes) == parent size` —
200/// the last child receives any leftover pixels.
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub struct Ratio(pub u16, pub u16);
203
204impl Ratio {
205    /// Equal split: `(1, 1)` = 50/50.
206    pub fn half() -> Self {
207        Ratio(1, 1)
208    }
209
210    /// Numerator of the ratio.
211    pub fn left_part(&self) -> u16 {
212        self.0
213    }
214
215    /// Denominator contribution of the ratio.
216    pub fn right_part(&self) -> u16 {
217        self.1
218    }
219
220    /// Sum of both parts.
221    pub fn total(&self) -> u16 {
222        self.0 + self.1
223    }
224}
225
226/// Minimum dimensions enforced by tree mutation functions.
227#[derive(Debug, Clone, Copy, PartialEq, Eq)]
228pub struct SizeConstraints {
229    pub min_width: u16,
230    pub min_height: u16,
231}
232
233impl SizeConstraints {
234    pub fn fits_split(&self, area: &LayoutRect, orientation: Orientation) -> bool {
235        match orientation {
236            Orientation::Horizontal => {
237                area.width / 2 >= self.min_width && area.height >= self.min_height
238            }
239            Orientation::Vertical => {
240                area.height / 2 >= self.min_height && area.width >= self.min_width
241            }
242        }
243    }
244}
245
246/// Errors returned by tree mutation operations.
247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248pub enum LayoutError {
249    /// The operation would produce a child smaller than the allowed minimum.
250    ConstraintViolated(SizeConstraints),
251    /// The target node was not found in the tree.
252    NotFound,
253}
254
255impl fmt::Display for LayoutError {
256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257        match self {
258            LayoutError::ConstraintViolated(c) => {
259                write!(
260                    f,
261                    "minimum dimension violated (min {}x{})",
262                    c.min_width, c.min_height
263                )
264            }
265            LayoutError::NotFound => write!(f, "target node not found"),
266        }
267    }
268}
269
270/// A rectangle specification that can be either absolute or percentage-based.
271///
272/// Percentage values are relative to `bounds` at resolution time.
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub enum RectSpec {
275    /// Fixed pixel/cell position and size.
276    Absolute(LayoutRect),
277    /// Percentage of the bounding rectangle.
278    Percent {
279        x: u16,
280        y: u16,
281        width: u16,
282        height: u16,
283    },
284}
285
286impl RectSpec {
287    /// Resolve this spec against `bounds` to produce a concrete [`LayoutRect`].
288    pub fn resolve(&self, bounds: LayoutRect) -> LayoutRect {
289        match *self {
290            RectSpec::Absolute(r) => r,
291            RectSpec::Percent {
292                x,
293                y,
294                width,
295                height,
296            } => {
297                let bw = i32::from(bounds.width);
298                let bh = i32::from(bounds.height);
299                LayoutRect {
300                    x: bounds.x.saturating_add(bw * i32::from(x) / 100),
301                    y: bounds.y.saturating_add(bh * i32::from(y) / 100),
302                    width: ((bw * i32::from(width) / 100) as u16).min(bounds.width),
303                    height: ((bh * i32::from(height) / 100) as u16).min(bounds.height),
304                }
305            }
306        }
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    fn r(x: i32, y: i32, w: u16, h: u16) -> LayoutRect {
315        LayoutRect {
316            x,
317            y,
318            width: w,
319            height: h,
320        }
321    }
322
323    #[test]
324    fn center_of_rect() {
325        assert_eq!(r(10, 20, 100, 60).center(), (60, 50));
326    }
327
328    #[test]
329    fn contains_inside() {
330        assert!(r(0, 0, 10, 10).contains(5, 5));
331    }
332
333    #[test]
334    fn contains_outside() {
335        assert!(!r(0, 0, 10, 10).contains(10, 10));
336    }
337
338    #[test]
339    fn contains_zero_dim() {
340        assert!(!r(0, 0, 0, 10).contains(0, 0));
341    }
342
343    #[test]
344    fn ratio_half() {
345        assert_eq!(Ratio::half(), Ratio(1, 1));
346    }
347
348    #[test]
349    fn clamp_within_bounds() {
350        let result = r(5, 5, 10, 10).clamp(r(0, 0, 20, 20));
351        assert_eq!(result, r(5, 5, 10, 10));
352    }
353
354    #[test]
355    fn clamp_partially_outside() {
356        let result = r(-5, -5, 20, 20).clamp(r(0, 0, 10, 10));
357        assert_eq!(result, r(0, 0, 10, 10));
358    }
359
360    #[test]
361    fn clamp_fully_outside() {
362        let result = r(100, 100, 10, 10).clamp(r(0, 0, 10, 10));
363        assert_eq!(result.width, 0);
364        assert_eq!(result.height, 0);
365    }
366
367    #[test]
368    fn intersects_overlapping() {
369        assert!(r(0, 0, 10, 10).intersects(r(5, 5, 10, 10)));
370    }
371
372    #[test]
373    fn intersects_non_overlapping() {
374        assert!(!r(0, 0, 10, 10).intersects(r(20, 20, 10, 10)));
375    }
376
377    #[test]
378    fn visible_portion_same_as_clamp() {
379        let r1 = r(-5, -5, 20, 20);
380        let bounds = r(0, 0, 10, 10);
381        assert_eq!(r1.visible_portion(bounds), r1.clamp(bounds));
382    }
383
384    #[test]
385    fn inset_shrinks_rect() {
386        let result = inset(r(10, 10, 100, 50), 5u16, 5u16, 2u16, 2u16);
387        assert_eq!(result.x, 15);
388        assert_eq!(result.y, 12);
389        assert_eq!(result.width, 90);
390        assert_eq!(result.height, 46);
391    }
392
393    #[test]
394    fn gap_insert_horizontal() {
395        let result = gap_insert(r(0, 0, 80, 24), 2u16, 1, Orientation::Horizontal);
396        assert_eq!(result.x, 2);
397        assert_eq!(result.y, 0);
398    }
399
400    #[test]
401    fn gap_insert_vertical() {
402        let result = gap_insert(r(0, 0, 80, 24), 2u16, 1, Orientation::Vertical);
403        assert_eq!(result.x, 0);
404        assert_eq!(result.y, 2);
405    }
406
407    #[test]
408    fn rect_spec_absolute() {
409        let spec = RectSpec::Absolute(r(10, 20, 30, 40));
410        let resolved = spec.resolve(r(0, 0, 80, 24));
411        assert_eq!(resolved, r(10, 20, 30, 40));
412    }
413
414    #[test]
415    fn rect_spec_percent() {
416        let spec = RectSpec::Percent {
417            x: 50,
418            y: 50,
419            width: 50,
420            height: 50,
421        };
422        let resolved = spec.resolve(r(0, 0, 100, 100));
423        assert_eq!(resolved, r(50, 50, 50, 50));
424    }
425
426    #[test]
427    fn screen_to_local_point_inside() {
428        let rect = LayoutRect {
429            x: 10,
430            y: 10,
431            width: 80,
432            height: 24,
433        };
434        let (dx, dy) = rect.screen_to_local_point(15, 25);
435        assert_eq!(dx, 5);
436        assert_eq!(dy, 15);
437    }
438
439    #[test]
440    fn screen_to_local_point_negative_delta() {
441        let rect = LayoutRect {
442            x: 10,
443            y: 10,
444            width: 80,
445            height: 24,
446        };
447        let (dx, dy) = rect.screen_to_local_point(2, 2);
448        assert_eq!(dx, -8);
449        assert_eq!(dy, -8);
450    }
451}