Skip to main content

oxiui_core/
geometry.rs

1//! 2-D geometry primitives used across the OxiUI stack.
2//!
3//! All coordinates are `f32` logical pixels with the origin at the top-left,
4//! `x` increasing rightward and `y` increasing downward.
5
6use core::ops::{Add, Mul, Sub};
7
8/// A 2-D point in logical pixels.
9#[derive(Clone, Copy, Debug, Default, PartialEq)]
10pub struct Point {
11    /// Horizontal coordinate.
12    pub x: f32,
13    /// Vertical coordinate.
14    pub y: f32,
15}
16
17impl Point {
18    /// The origin point `(0, 0)`.
19    pub const ZERO: Point = Point { x: 0.0, y: 0.0 };
20
21    /// Construct a [`Point`] from explicit coordinates.
22    pub const fn new(x: f32, y: f32) -> Self {
23        Self { x, y }
24    }
25
26    /// Euclidean distance to `other`.
27    pub fn distance(self, other: Point) -> f32 {
28        let dx = self.x - other.x;
29        let dy = self.y - other.y;
30        (dx * dx + dy * dy).sqrt()
31    }
32}
33
34impl Add for Point {
35    type Output = Point;
36    fn add(self, rhs: Point) -> Point {
37        Point::new(self.x + rhs.x, self.y + rhs.y)
38    }
39}
40
41impl Sub for Point {
42    type Output = Point;
43    fn sub(self, rhs: Point) -> Point {
44        Point::new(self.x - rhs.x, self.y - rhs.y)
45    }
46}
47
48/// A 2-D size (width × height) in logical pixels.
49#[derive(Clone, Copy, Debug, Default, PartialEq)]
50pub struct Size {
51    /// Width in logical pixels (non-negative by convention).
52    pub width: f32,
53    /// Height in logical pixels (non-negative by convention).
54    pub height: f32,
55}
56
57impl Size {
58    /// A zero-area size.
59    pub const ZERO: Size = Size {
60        width: 0.0,
61        height: 0.0,
62    };
63
64    /// Construct a [`Size`] from explicit dimensions.
65    pub const fn new(width: f32, height: f32) -> Self {
66        Self { width, height }
67    }
68
69    /// Returns the area (`width * height`).
70    pub fn area(self) -> f32 {
71        self.width * self.height
72    }
73
74    /// Returns `true` if either dimension is zero or negative.
75    pub fn is_empty(self) -> bool {
76        self.width <= 0.0 || self.height <= 0.0
77    }
78
79    /// Clamp both dimensions into `[min, max]` component-wise.
80    pub fn clamp(self, min: Size, max: Size) -> Size {
81        Size::new(
82            self.width.clamp(min.width, max.width),
83            self.height.clamp(min.height, max.height),
84        )
85    }
86}
87
88impl Mul<f32> for Size {
89    type Output = Size;
90    fn mul(self, rhs: f32) -> Size {
91        Size::new(self.width * rhs, self.height * rhs)
92    }
93}
94
95/// Per-side insets (padding or margin) in logical pixels.
96#[derive(Clone, Copy, Debug, Default, PartialEq)]
97pub struct Insets {
98    /// Top inset.
99    pub top: f32,
100    /// Right inset.
101    pub right: f32,
102    /// Bottom inset.
103    pub bottom: f32,
104    /// Left inset.
105    pub left: f32,
106}
107
108impl Insets {
109    /// Zero on all sides.
110    pub const ZERO: Insets = Insets {
111        top: 0.0,
112        right: 0.0,
113        bottom: 0.0,
114        left: 0.0,
115    };
116
117    /// Construct per-side insets.
118    pub const fn new(top: f32, right: f32, bottom: f32, left: f32) -> Self {
119        Self {
120            top,
121            right,
122            bottom,
123            left,
124        }
125    }
126
127    /// The same inset on all four sides.
128    pub const fn all(v: f32) -> Self {
129        Self {
130            top: v,
131            right: v,
132            bottom: v,
133            left: v,
134        }
135    }
136
137    /// Symmetric insets: `vertical` on top/bottom, `horizontal` on left/right.
138    pub const fn symmetric(vertical: f32, horizontal: f32) -> Self {
139        Self {
140            top: vertical,
141            right: horizontal,
142            bottom: vertical,
143            left: horizontal,
144        }
145    }
146
147    /// Total horizontal inset (`left + right`).
148    pub fn horizontal(self) -> f32 {
149        self.left + self.right
150    }
151
152    /// Total vertical inset (`top + bottom`).
153    pub fn vertical(self) -> f32 {
154        self.top + self.bottom
155    }
156}
157
158/// An axis-aligned rectangle defined by its top-left [`Point`] and [`Size`].
159#[derive(Clone, Copy, Debug, Default, PartialEq)]
160pub struct Rect {
161    /// Top-left corner.
162    pub origin: Point,
163    /// Width and height.
164    pub size: Size,
165}
166
167impl Rect {
168    /// A zero rectangle at the origin.
169    pub const ZERO: Rect = Rect {
170        origin: Point::ZERO,
171        size: Size::ZERO,
172    };
173
174    /// Construct a rectangle from explicit `x`, `y`, `width`, `height`.
175    pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
176        Self {
177            origin: Point::new(x, y),
178            size: Size::new(width, height),
179        }
180    }
181
182    /// Construct a rectangle from an [`origin`](Rect::origin) and [`size`](Rect::size).
183    pub const fn from_origin_size(origin: Point, size: Size) -> Self {
184        Self { origin, size }
185    }
186
187    /// Left edge (`origin.x`).
188    pub fn left(&self) -> f32 {
189        self.origin.x
190    }
191
192    /// Top edge (`origin.y`).
193    pub fn top(&self) -> f32 {
194        self.origin.y
195    }
196
197    /// Right edge (`origin.x + width`).
198    pub fn right(&self) -> f32 {
199        self.origin.x + self.size.width
200    }
201
202    /// Bottom edge (`origin.y + height`).
203    pub fn bottom(&self) -> f32 {
204        self.origin.y + self.size.height
205    }
206
207    /// Width in logical pixels.
208    pub fn width(&self) -> f32 {
209        self.size.width
210    }
211
212    /// Height in logical pixels.
213    pub fn height(&self) -> f32 {
214        self.size.height
215    }
216
217    /// Geometric centre of the rectangle.
218    pub fn center(&self) -> Point {
219        Point::new(
220            self.origin.x + self.size.width * 0.5,
221            self.origin.y + self.size.height * 0.5,
222        )
223    }
224
225    /// Returns `true` if `p` lies within the rectangle (inclusive of the
226    /// top/left edges, exclusive of the bottom/right edges).
227    pub fn contains(&self, p: Point) -> bool {
228        p.x >= self.left() && p.x < self.right() && p.y >= self.top() && p.y < self.bottom()
229    }
230
231    /// Returns `true` if this rectangle shares any interior area with `other`.
232    pub fn intersects(&self, other: &Rect) -> bool {
233        self.left() < other.right()
234            && other.left() < self.right()
235            && self.top() < other.bottom()
236            && other.top() < self.bottom()
237    }
238
239    /// Returns the overlapping rectangle, or `None` if the two do not overlap.
240    pub fn intersection(&self, other: &Rect) -> Option<Rect> {
241        let left = self.left().max(other.left());
242        let top = self.top().max(other.top());
243        let right = self.right().min(other.right());
244        let bottom = self.bottom().min(other.bottom());
245        if right > left && bottom > top {
246            Some(Rect::new(left, top, right - left, bottom - top))
247        } else {
248            None
249        }
250    }
251
252    /// Returns the smallest rectangle enclosing both `self` and `other`.
253    pub fn union(&self, other: &Rect) -> Rect {
254        let left = self.left().min(other.left());
255        let top = self.top().min(other.top());
256        let right = self.right().max(other.right());
257        let bottom = self.bottom().max(other.bottom());
258        Rect::new(left, top, right - left, bottom - top)
259    }
260
261    /// Shrink the rectangle inward by `insets` (clamped to non-negative size).
262    pub fn deflate(&self, insets: Insets) -> Rect {
263        let w = (self.size.width - insets.horizontal()).max(0.0);
264        let h = (self.size.height - insets.vertical()).max(0.0);
265        Rect::new(self.left() + insets.left, self.top() + insets.top, w, h)
266    }
267
268    /// Grow the rectangle outward by `insets`.
269    pub fn inflate(&self, insets: Insets) -> Rect {
270        Rect::new(
271            self.left() - insets.left,
272            self.top() - insets.top,
273            self.size.width + insets.horizontal(),
274            self.size.height + insets.vertical(),
275        )
276    }
277
278    /// Returns `true` if width or height is zero or negative.
279    pub fn is_empty(&self) -> bool {
280        self.size.is_empty()
281    }
282}
283
284/// Box layout constraints: a `[min, max]` range for width and height.
285#[derive(Clone, Copy, Debug, PartialEq)]
286pub struct Constraints {
287    /// Minimum allowed size.
288    pub min: Size,
289    /// Maximum allowed size (use [`f32::INFINITY`] for unbounded).
290    pub max: Size,
291}
292
293impl Constraints {
294    /// Construct constraints from a min and max size.
295    pub const fn new(min: Size, max: Size) -> Self {
296        Self { min, max }
297    }
298
299    /// Tight constraints that force exactly `size`.
300    pub fn tight(size: Size) -> Self {
301        Self {
302            min: size,
303            max: size,
304        }
305    }
306
307    /// Loose constraints: `min` is zero, `max` is the given size.
308    pub fn loose(max: Size) -> Self {
309        Self {
310            min: Size::ZERO,
311            max,
312        }
313    }
314
315    /// Unbounded constraints (zero min, infinite max).
316    pub fn unbounded() -> Self {
317        Self {
318            min: Size::ZERO,
319            max: Size::new(f32::INFINITY, f32::INFINITY),
320        }
321    }
322
323    /// Clamp `size` into this constraint range.
324    pub fn constrain(&self, size: Size) -> Size {
325        size.clamp(self.min, self.max)
326    }
327
328    /// Returns `true` if only one size satisfies both width and height bounds.
329    pub fn is_tight(&self) -> bool {
330        self.min.width == self.max.width && self.min.height == self.max.height
331    }
332}
333
334impl Default for Constraints {
335    fn default() -> Self {
336        Self::unbounded()
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    #[test]
345    fn rect_contains_and_edges() {
346        let r = Rect::new(10.0, 20.0, 100.0, 50.0);
347        assert_eq!(r.right(), 110.0);
348        assert_eq!(r.bottom(), 70.0);
349        assert!(r.contains(Point::new(10.0, 20.0)));
350        assert!(r.contains(Point::new(109.9, 69.9)));
351        assert!(!r.contains(Point::new(110.0, 70.0))); // exclusive on far edges
352        assert!(!r.contains(Point::new(9.9, 20.0)));
353        assert_eq!(r.center(), Point::new(60.0, 45.0));
354    }
355
356    #[test]
357    fn rect_intersection_and_union() {
358        let a = Rect::new(0.0, 0.0, 10.0, 10.0);
359        let b = Rect::new(5.0, 5.0, 10.0, 10.0);
360        assert!(a.intersects(&b));
361        let i = a.intersection(&b).expect("rects overlap");
362        assert_eq!(i, Rect::new(5.0, 5.0, 5.0, 5.0));
363        let u = a.union(&b);
364        assert_eq!(u, Rect::new(0.0, 0.0, 15.0, 15.0));
365
366        let c = Rect::new(100.0, 100.0, 5.0, 5.0);
367        assert!(!a.intersects(&c));
368        assert!(a.intersection(&c).is_none());
369    }
370
371    #[test]
372    fn rect_deflate_inflate() {
373        let r = Rect::new(0.0, 0.0, 100.0, 100.0);
374        let d = r.deflate(Insets::all(10.0));
375        assert_eq!(d, Rect::new(10.0, 10.0, 80.0, 80.0));
376        let i = d.inflate(Insets::all(10.0));
377        assert_eq!(i, r);
378
379        // Deflate beyond size clamps to zero, never negative.
380        let tiny = Rect::new(0.0, 0.0, 5.0, 5.0);
381        let clamped = tiny.deflate(Insets::all(10.0));
382        assert_eq!(clamped.width(), 0.0);
383        assert_eq!(clamped.height(), 0.0);
384    }
385
386    #[test]
387    fn constraints_constrain() {
388        let c = Constraints::new(Size::new(10.0, 10.0), Size::new(100.0, 100.0));
389        assert_eq!(c.constrain(Size::new(5.0, 200.0)), Size::new(10.0, 100.0));
390        assert!(Constraints::tight(Size::new(50.0, 50.0)).is_tight());
391        assert!(!c.is_tight());
392    }
393
394    #[test]
395    fn point_arithmetic() {
396        let p = Point::new(3.0, 4.0) + Point::new(1.0, 1.0);
397        assert_eq!(p, Point::new(4.0, 5.0));
398        assert_eq!((p - Point::new(4.0, 5.0)), Point::ZERO);
399        assert_eq!(Point::ZERO.distance(Point::new(3.0, 4.0)), 5.0);
400    }
401
402    #[test]
403    fn insets_totals() {
404        let i = Insets::symmetric(8.0, 16.0);
405        assert_eq!(i.vertical(), 16.0);
406        assert_eq!(i.horizontal(), 32.0);
407    }
408}