Skip to main content

ftui_core/
geometry.rs

1#![forbid(unsafe_code)]
2
3//! Geometric primitives.
4
5/// A 2D size in terminal cells.
6///
7/// Represents dimensions (width and height) using `u16` for terminal coordinates.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
9pub struct Size {
10    /// Width in cells.
11    pub width: u16,
12    /// Height in cells.
13    pub height: u16,
14}
15
16impl Size {
17    /// Zero size (0x0).
18    pub const ZERO: Self = Self {
19        width: 0,
20        height: 0,
21    };
22
23    /// Maximum size (u16::MAX x u16::MAX).
24    ///
25    /// Useful as a sentinel for "unbounded" or "as large as needed".
26    pub const MAX: Self = Self {
27        width: u16::MAX,
28        height: u16::MAX,
29    };
30
31    /// Create a new size.
32    #[inline]
33    pub const fn new(width: u16, height: u16) -> Self {
34        Self { width, height }
35    }
36
37    /// Check if this size has zero area.
38    #[inline]
39    pub const fn is_empty(&self) -> bool {
40        self.width == 0 || self.height == 0
41    }
42
43    /// Area in cells.
44    #[inline]
45    pub const fn area(&self) -> u32 {
46        self.width as u32 * self.height as u32
47    }
48
49    /// Clamp width and height to the given maximums.
50    #[inline]
51    pub const fn clamp_max(&self, max: Size) -> Size {
52        Size {
53            width: if self.width > max.width {
54                max.width
55            } else {
56                self.width
57            },
58            height: if self.height > max.height {
59                max.height
60            } else {
61                self.height
62            },
63        }
64    }
65
66    /// Clamp width and height to the given minimums.
67    #[inline]
68    pub const fn clamp_min(&self, min: Size) -> Size {
69        Size {
70            width: if self.width < min.width {
71                min.width
72            } else {
73                self.width
74            },
75            height: if self.height < min.height {
76                min.height
77            } else {
78                self.height
79            },
80        }
81    }
82}
83
84impl From<(u16, u16)> for Size {
85    fn from((width, height): (u16, u16)) -> Self {
86        Self { width, height }
87    }
88}
89
90impl From<Rect> for Size {
91    fn from(rect: Rect) -> Self {
92        Self {
93            width: rect.width,
94            height: rect.height,
95        }
96    }
97}
98
99/// A rectangle for scissor regions, layout bounds, and hit testing.
100///
101/// Uses terminal coordinates (0-indexed, origin at top-left).
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
103pub struct Rect {
104    /// Left edge (inclusive).
105    pub x: u16,
106    /// Top edge (inclusive).
107    pub y: u16,
108    /// Width in cells.
109    pub width: u16,
110    /// Height in cells.
111    pub height: u16,
112}
113
114impl Rect {
115    /// Create a new rectangle.
116    #[inline]
117    pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
118        Self {
119            x,
120            y,
121            width,
122            height,
123        }
124    }
125
126    /// Create a rectangle from origin with given size.
127    #[inline]
128    pub const fn from_size(width: u16, height: u16) -> Self {
129        Self::new(0, 0, width, height)
130    }
131
132    /// Left edge (inclusive). Alias for `self.x`.
133    #[inline]
134    pub const fn left(&self) -> u16 {
135        self.x
136    }
137
138    /// Top edge (inclusive). Alias for `self.y`.
139    #[inline]
140    pub const fn top(&self) -> u16 {
141        self.y
142    }
143
144    /// Right edge (exclusive).
145    #[inline]
146    pub const fn right(&self) -> u16 {
147        self.x.saturating_add(self.width)
148    }
149
150    /// Bottom edge (exclusive).
151    #[inline]
152    pub const fn bottom(&self) -> u16 {
153        self.y.saturating_add(self.height)
154    }
155
156    /// Area in cells.
157    #[inline]
158    pub const fn area(&self) -> u32 {
159        self.width as u32 * self.height as u32
160    }
161
162    /// Check if the rectangle has zero area.
163    #[inline]
164    pub const fn is_empty(&self) -> bool {
165        self.width == 0 || self.height == 0
166    }
167
168    /// Check if a point is inside the rectangle.
169    #[inline]
170    pub const fn contains(&self, x: u16, y: u16) -> bool {
171        x >= self.x && x < self.right() && y >= self.y && y < self.bottom()
172    }
173
174    /// Compute the intersection with another rectangle.
175    ///
176    /// Returns an empty rectangle if the rectangles don't overlap.
177    #[inline]
178    pub fn intersection(&self, other: &Rect) -> Rect {
179        self.intersection_opt(other).unwrap_or_default()
180    }
181
182    /// Create a new rectangle inside the current one with the given margin.
183    #[inline]
184    pub fn inner(&self, margin: Sides) -> Rect {
185        let x = self.x.saturating_add(margin.left);
186        let y = self.y.saturating_add(margin.top);
187        let width = self
188            .width
189            .saturating_sub(margin.left)
190            .saturating_sub(margin.right);
191        let height = self
192            .height
193            .saturating_sub(margin.top)
194            .saturating_sub(margin.bottom);
195
196        Rect {
197            x,
198            y,
199            width,
200            height,
201        }
202    }
203
204    /// Create a new rectangle that is the union of this rectangle and another.
205    ///
206    /// The result is the smallest rectangle that contains both.
207    ///
208    /// Note: a rectangle's *position* participates even when it is empty —
209    /// `Rect::new(100, 100, 0, 0).union(&r)` extends to x/y 100. When
210    /// accumulating a union over a collection, seed the fold with the first
211    /// element rather than `Rect::default()` (which would pin the result to
212    /// the origin).
213    #[inline]
214    pub fn union(&self, other: &Rect) -> Rect {
215        let x = self.x.min(other.x);
216        let y = self.y.min(other.y);
217        let right = self.right().max(other.right());
218        let bottom = self.bottom().max(other.bottom());
219
220        Rect {
221            x,
222            y,
223            width: right.saturating_sub(x),
224            height: bottom.saturating_sub(y),
225        }
226    }
227
228    /// Compute the intersection with another rectangle, returning `None` if no overlap.
229    #[inline]
230    pub fn intersection_opt(&self, other: &Rect) -> Option<Rect> {
231        let x = self.x.max(other.x);
232        let y = self.y.max(other.y);
233        let right = self.right().min(other.right());
234        let bottom = self.bottom().min(other.bottom());
235
236        if x < right && y < bottom {
237            Some(Rect::new(
238                x,
239                y,
240                right.saturating_sub(x),
241                bottom.saturating_sub(y),
242            ))
243        } else {
244            None
245        }
246    }
247}
248
249/// Sides for padding/margin.
250#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
251pub struct Sides {
252    /// Top padding/margin in cells.
253    pub top: u16,
254    /// Right padding/margin in cells.
255    pub right: u16,
256    /// Bottom padding/margin in cells.
257    pub bottom: u16,
258    /// Left padding/margin in cells.
259    pub left: u16,
260}
261
262impl Sides {
263    /// Create new sides with equal values.
264    pub const fn all(val: u16) -> Self {
265        Self {
266            top: val,
267            right: val,
268            bottom: val,
269            left: val,
270        }
271    }
272
273    /// Create new sides with horizontal values only.
274    pub const fn horizontal(val: u16) -> Self {
275        Self {
276            top: 0,
277            right: val,
278            bottom: 0,
279            left: val,
280        }
281    }
282
283    /// Create new sides with vertical values only.
284    pub const fn vertical(val: u16) -> Self {
285        Self {
286            top: val,
287            right: 0,
288            bottom: val,
289            left: 0,
290        }
291    }
292
293    /// Create new sides with specific values.
294    pub const fn new(top: u16, right: u16, bottom: u16, left: u16) -> Self {
295        Self {
296            top,
297            right,
298            bottom,
299            left,
300        }
301    }
302
303    /// Sum of left and right.
304    #[inline]
305    pub const fn horizontal_sum(&self) -> u16 {
306        self.left.saturating_add(self.right)
307    }
308
309    /// Sum of top and bottom.
310    #[inline]
311    pub const fn vertical_sum(&self) -> u16 {
312        self.top.saturating_add(self.bottom)
313    }
314}
315
316impl From<u16> for Sides {
317    fn from(val: u16) -> Self {
318        Self::all(val)
319    }
320}
321
322impl From<(u16, u16)> for Sides {
323    fn from((vertical, horizontal): (u16, u16)) -> Self {
324        Self {
325            top: vertical,
326            right: horizontal,
327            bottom: vertical,
328            left: horizontal,
329        }
330    }
331}
332
333impl From<(u16, u16, u16, u16)> for Sides {
334    fn from((top, right, bottom, left): (u16, u16, u16, u16)) -> Self {
335        Self {
336            top,
337            right,
338            bottom,
339            left,
340        }
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::{Rect, Sides, Size};
347
348    #[test]
349    fn rect_contains_edges() {
350        let rect = Rect::new(2, 3, 4, 5);
351        assert!(rect.contains(2, 3));
352        assert!(rect.contains(5, 7));
353        assert!(!rect.contains(6, 3));
354        assert!(!rect.contains(2, 8));
355    }
356
357    #[test]
358    fn rect_intersection_overlaps() {
359        let a = Rect::new(0, 0, 4, 4);
360        let b = Rect::new(2, 2, 4, 4);
361        assert_eq!(a.intersection(&b), Rect::new(2, 2, 2, 2));
362    }
363
364    #[test]
365    fn rect_intersection_no_overlap_is_empty() {
366        let a = Rect::new(0, 0, 2, 2);
367        let b = Rect::new(3, 3, 2, 2);
368        assert_eq!(a.intersection(&b), Rect::default());
369    }
370
371    #[test]
372    fn rect_inner_reduces() {
373        let rect = Rect::new(0, 0, 10, 10);
374        let inner = rect.inner(Sides {
375            top: 1,
376            right: 2,
377            bottom: 3,
378            left: 4,
379        });
380        assert_eq!(inner, Rect::new(4, 1, 4, 6));
381    }
382
383    #[test]
384    fn sides_constructors_and_conversions() {
385        assert_eq!(Sides::all(3), Sides::from(3));
386        assert_eq!(
387            Sides::horizontal(2),
388            Sides {
389                top: 0,
390                right: 2,
391                bottom: 0,
392                left: 2,
393            }
394        );
395        assert_eq!(
396            Sides::vertical(4),
397            Sides {
398                top: 4,
399                right: 0,
400                bottom: 4,
401                left: 0,
402            }
403        );
404        assert_eq!(
405            Sides::from((1, 2)),
406            Sides {
407                top: 1,
408                right: 2,
409                bottom: 1,
410                left: 2,
411            }
412        );
413        assert_eq!(
414            Sides::from((1, 2, 3, 4)),
415            Sides {
416                top: 1,
417                right: 2,
418                bottom: 3,
419                left: 4,
420            }
421        );
422    }
423
424    #[test]
425    fn sides_sums() {
426        let sides = Sides {
427            top: 1,
428            right: 2,
429            bottom: 3,
430            left: 4,
431        };
432        assert_eq!(sides.horizontal_sum(), 6);
433        assert_eq!(sides.vertical_sum(), 4);
434    }
435
436    // --- Rect constructors ---
437
438    #[test]
439    fn rect_new_and_default() {
440        let r = Rect::new(5, 10, 20, 15);
441        assert_eq!(r.x, 5);
442        assert_eq!(r.y, 10);
443        assert_eq!(r.width, 20);
444        assert_eq!(r.height, 15);
445
446        let d = Rect::default();
447        assert_eq!(d, Rect::new(0, 0, 0, 0));
448    }
449
450    #[test]
451    fn rect_from_size() {
452        let r = Rect::from_size(80, 24);
453        assert_eq!(r.x, 0);
454        assert_eq!(r.y, 0);
455        assert_eq!(r.width, 80);
456        assert_eq!(r.height, 24);
457    }
458
459    // --- Edge accessors ---
460
461    #[test]
462    fn rect_left_top_right_bottom() {
463        let r = Rect::new(10, 20, 30, 40);
464        assert_eq!(r.left(), 10);
465        assert_eq!(r.top(), 20);
466        assert_eq!(r.right(), 40);
467        assert_eq!(r.bottom(), 60);
468    }
469
470    #[test]
471    fn rect_right_bottom_saturating() {
472        // Near u16::MAX — should not overflow
473        let r = Rect::new(u16::MAX - 5, u16::MAX - 3, 100, 100);
474        assert_eq!(r.right(), u16::MAX);
475        assert_eq!(r.bottom(), u16::MAX);
476    }
477
478    // --- Area and is_empty ---
479
480    #[test]
481    fn rect_area() {
482        assert_eq!(Rect::new(0, 0, 10, 20).area(), 200);
483        assert_eq!(Rect::new(5, 5, 0, 10).area(), 0);
484        assert_eq!(Rect::new(0, 0, 1, 1).area(), 1);
485    }
486
487    #[test]
488    fn rect_is_empty() {
489        assert!(Rect::new(0, 0, 0, 0).is_empty());
490        assert!(Rect::new(5, 5, 0, 10).is_empty());
491        assert!(Rect::new(5, 5, 10, 0).is_empty());
492        assert!(!Rect::new(0, 0, 1, 1).is_empty());
493    }
494
495    // --- Contains ---
496
497    #[test]
498    fn rect_contains_boundary_conditions() {
499        let r = Rect::new(0, 0, 5, 5);
500        // Top-left corner (inclusive)
501        assert!(r.contains(0, 0));
502        // Just inside right/bottom edge
503        assert!(r.contains(4, 4));
504        // Right edge is exclusive
505        assert!(!r.contains(5, 0));
506        // Bottom edge is exclusive
507        assert!(!r.contains(0, 5));
508    }
509
510    #[test]
511    fn rect_contains_empty_rect() {
512        let r = Rect::new(5, 5, 0, 0);
513        // Empty rect contains nothing, not even its own origin
514        assert!(!r.contains(5, 5));
515    }
516
517    // --- Union ---
518
519    #[test]
520    fn rect_union_basic() {
521        let a = Rect::new(0, 0, 5, 5);
522        let b = Rect::new(3, 3, 5, 5);
523        let u = a.union(&b);
524        assert_eq!(u, Rect::new(0, 0, 8, 8));
525    }
526
527    #[test]
528    fn rect_union_disjoint() {
529        let a = Rect::new(0, 0, 2, 2);
530        let b = Rect::new(10, 10, 3, 3);
531        let u = a.union(&b);
532        assert_eq!(u, Rect::new(0, 0, 13, 13));
533    }
534
535    #[test]
536    fn rect_union_contained() {
537        let outer = Rect::new(0, 0, 10, 10);
538        let inner = Rect::new(2, 2, 3, 3);
539        assert_eq!(outer.union(&inner), outer);
540        assert_eq!(inner.union(&outer), outer);
541    }
542
543    #[test]
544    fn rect_union_self() {
545        let r = Rect::new(5, 10, 20, 15);
546        assert_eq!(r.union(&r), r);
547    }
548
549    // --- Intersection ---
550
551    #[test]
552    fn rect_intersection_self() {
553        let r = Rect::new(5, 5, 10, 10);
554        assert_eq!(r.intersection(&r), r);
555    }
556
557    #[test]
558    fn rect_intersection_contained() {
559        let outer = Rect::new(0, 0, 20, 20);
560        let inner = Rect::new(5, 5, 5, 5);
561        assert_eq!(outer.intersection(&inner), inner);
562        assert_eq!(inner.intersection(&outer), inner);
563    }
564
565    #[test]
566    fn rect_intersection_adjacent_no_overlap() {
567        // Rects share an edge but don't overlap (right edge is exclusive)
568        let a = Rect::new(0, 0, 5, 5);
569        let b = Rect::new(5, 0, 5, 5);
570        assert!(a.intersection(&b).is_empty());
571    }
572
573    #[test]
574    fn rect_intersection_opt_returns_none_for_no_overlap() {
575        let a = Rect::new(0, 0, 2, 2);
576        let b = Rect::new(5, 5, 2, 2);
577        assert_eq!(a.intersection_opt(&b), None);
578    }
579
580    #[test]
581    fn rect_intersection_opt_returns_some_for_overlap() {
582        let a = Rect::new(0, 0, 5, 5);
583        let b = Rect::new(3, 3, 5, 5);
584        assert_eq!(a.intersection_opt(&b), Some(Rect::new(3, 3, 2, 2)));
585    }
586
587    // --- Inner margin edge cases ---
588
589    #[test]
590    fn rect_inner_large_margin_clamps_to_zero() {
591        let r = Rect::new(0, 0, 10, 10);
592        let inner = r.inner(Sides::all(20));
593        // Width/height should clamp to 0 (not underflow)
594        assert_eq!(inner.width, 0);
595        assert_eq!(inner.height, 0);
596    }
597
598    #[test]
599    fn rect_inner_zero_margin() {
600        let r = Rect::new(5, 10, 20, 30);
601        let inner = r.inner(Sides::all(0));
602        assert_eq!(inner, r);
603    }
604
605    #[test]
606    fn rect_inner_asymmetric_margin() {
607        let r = Rect::new(0, 0, 20, 20);
608        let inner = r.inner(Sides::new(2, 3, 4, 5));
609        assert_eq!(inner.x, 5);
610        assert_eq!(inner.y, 2);
611        assert_eq!(inner.width, 12); // 20 - 5 - 3
612        assert_eq!(inner.height, 14); // 20 - 2 - 4
613    }
614
615    // --- Sides ---
616
617    #[test]
618    fn sides_new_explicit() {
619        let s = Sides::new(1, 2, 3, 4);
620        assert_eq!(s.top, 1);
621        assert_eq!(s.right, 2);
622        assert_eq!(s.bottom, 3);
623        assert_eq!(s.left, 4);
624    }
625
626    #[test]
627    fn sides_default_is_zero() {
628        let s = Sides::default();
629        assert_eq!(s, Sides::new(0, 0, 0, 0));
630    }
631
632    #[test]
633    fn sides_sums_saturating() {
634        let s = Sides::new(u16::MAX, 0, u16::MAX, 0);
635        assert_eq!(s.vertical_sum(), u16::MAX);
636    }
637
638    // --- Size tests ---
639
640    #[test]
641    fn size_new_and_constants() {
642        let s = Size::new(80, 24);
643        assert_eq!(s.width, 80);
644        assert_eq!(s.height, 24);
645
646        assert_eq!(Size::ZERO, Size::new(0, 0));
647        assert_eq!(Size::MAX, Size::new(u16::MAX, u16::MAX));
648    }
649
650    #[test]
651    fn size_default_is_zero() {
652        assert_eq!(Size::default(), Size::ZERO);
653    }
654
655    #[test]
656    fn size_is_empty() {
657        assert!(Size::ZERO.is_empty());
658        assert!(Size::new(0, 10).is_empty());
659        assert!(Size::new(10, 0).is_empty());
660        assert!(!Size::new(1, 1).is_empty());
661    }
662
663    #[test]
664    fn size_area() {
665        assert_eq!(Size::new(10, 20).area(), 200);
666        assert_eq!(Size::ZERO.area(), 0);
667        assert_eq!(Size::new(1, 1).area(), 1);
668    }
669
670    #[test]
671    fn size_clamp_max() {
672        let s = Size::new(100, 50);
673        assert_eq!(s.clamp_max(Size::new(80, 40)), Size::new(80, 40));
674        assert_eq!(s.clamp_max(Size::new(200, 200)), s);
675        assert_eq!(s.clamp_max(Size::new(80, 100)), Size::new(80, 50));
676    }
677
678    #[test]
679    fn size_clamp_min() {
680        let s = Size::new(10, 5);
681        assert_eq!(s.clamp_min(Size::new(20, 10)), Size::new(20, 10));
682        assert_eq!(s.clamp_min(Size::new(5, 3)), s);
683        assert_eq!(s.clamp_min(Size::new(15, 3)), Size::new(15, 5));
684    }
685
686    #[test]
687    fn size_from_tuple() {
688        let s: Size = (80, 24).into();
689        assert_eq!(s, Size::new(80, 24));
690    }
691
692    #[test]
693    fn size_from_rect() {
694        let r = Rect::new(5, 10, 80, 24);
695        let s: Size = r.into();
696        assert_eq!(s, Size::new(80, 24));
697    }
698}