Skip to main content

kozan_primitives/
geometry.rs

1/// A 2D point in logical coordinates (before DPI scaling).
2///
3/// A single `f32`-based type used throughout the platform. A
4/// physical vs logical distinction (for DPI awareness) can be
5/// added later with a unit marker generic if needed.
6#[derive(Clone, Copy, Debug, Default, PartialEq)]
7pub struct Point {
8    pub x: f32,
9    pub y: f32,
10}
11
12impl Point {
13    pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
14
15    #[must_use]
16    pub const fn new(x: f32, y: f32) -> Self {
17        Self { x, y }
18    }
19
20    #[must_use]
21    pub fn distance_to(self, other: Self) -> f32 {
22        let dx = self.x - other.x;
23        let dy = self.y - other.y;
24        (dx * dx + dy * dy).sqrt()
25    }
26
27    #[must_use]
28    pub fn offset(self, dx: f32, dy: f32) -> Self {
29        Self {
30            x: self.x + dx,
31            y: self.y + dy,
32        }
33    }
34}
35
36impl std::ops::Add<Offset> for Point {
37    type Output = Self;
38    fn add(self, rhs: Offset) -> Self {
39        Self {
40            x: self.x + rhs.dx,
41            y: self.y + rhs.dy,
42        }
43    }
44}
45
46impl std::ops::Sub for Point {
47    type Output = Offset;
48    fn sub(self, rhs: Self) -> Offset {
49        Offset {
50            dx: self.x - rhs.x,
51            dy: self.y - rhs.y,
52        }
53    }
54}
55
56impl std::ops::Sub<Offset> for Point {
57    type Output = Self;
58    fn sub(self, rhs: Offset) -> Self {
59        Self {
60            x: self.x - rhs.dx,
61            y: self.y - rhs.dy,
62        }
63    }
64}
65
66/// A 2D displacement vector.
67///
68/// Semantically different from [`Point`]: a point is a position,
69/// an offset is a delta between two positions. `Point - Point = Offset`,
70/// `Point + Offset = Point`.
71#[derive(Clone, Copy, Debug, Default, PartialEq)]
72pub struct Offset {
73    pub dx: f32,
74    pub dy: f32,
75}
76
77impl Offset {
78    pub const ZERO: Self = Self { dx: 0.0, dy: 0.0 };
79
80    #[must_use]
81    pub const fn new(dx: f32, dy: f32) -> Self {
82        Self { dx, dy }
83    }
84
85    #[must_use]
86    pub fn length(self) -> f32 {
87        (self.dx * self.dx + self.dy * self.dy).sqrt()
88    }
89}
90
91impl std::ops::Add for Offset {
92    type Output = Self;
93    fn add(self, rhs: Self) -> Self {
94        Self {
95            dx: self.dx + rhs.dx,
96            dy: self.dy + rhs.dy,
97        }
98    }
99}
100
101impl std::ops::Neg for Offset {
102    type Output = Self;
103    fn neg(self) -> Self {
104        Self {
105            dx: -self.dx,
106            dy: -self.dy,
107        }
108    }
109}
110
111/// A 2D size (width × height). Never negative.
112#[derive(Clone, Copy, Debug, Default, PartialEq)]
113pub struct Size {
114    pub width: f32,
115    pub height: f32,
116}
117
118impl Size {
119    pub const ZERO: Self = Self {
120        width: 0.0,
121        height: 0.0,
122    };
123
124    #[must_use]
125    pub const fn new(width: f32, height: f32) -> Self {
126        Self { width, height }
127    }
128
129    #[must_use]
130    pub fn area(self) -> f32 {
131        self.width * self.height
132    }
133
134    #[must_use]
135    pub fn is_empty(self) -> bool {
136        self.width <= 0.0 || self.height <= 0.0
137    }
138
139    #[must_use]
140    pub fn contains(self, point: Point) -> bool {
141        point.x >= 0.0 && point.x < self.width && point.y >= 0.0 && point.y < self.height
142    }
143}
144
145/// An axis-aligned rectangle defined by origin + size.
146///
147/// Origin + size is the canonical representation. Edge accessors
148/// (`left`, `top`, `right`, `bottom`) are provided for convenience.
149#[derive(Clone, Copy, Debug, Default, PartialEq)]
150pub struct Rect {
151    pub origin: Point,
152    pub size: Size,
153}
154
155impl Rect {
156    pub const ZERO: Self = Self {
157        origin: Point::ZERO,
158        size: Size::ZERO,
159    };
160
161    #[must_use]
162    pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
163        Self {
164            origin: Point::new(x, y),
165            size: Size::new(width, height),
166        }
167    }
168
169    #[must_use]
170    pub fn from_origin_size(origin: Point, size: Size) -> Self {
171        Self { origin, size }
172    }
173
174    /// Construct from left, top, right, bottom edges.
175    #[must_use]
176    pub fn from_ltrb(left: f32, top: f32, right: f32, bottom: f32) -> Self {
177        Self {
178            origin: Point::new(left, top),
179            size: Size::new(right - left, bottom - top),
180        }
181    }
182
183    #[must_use]
184    pub fn x(&self) -> f32 {
185        self.origin.x
186    }
187    #[must_use]
188    pub fn y(&self) -> f32 {
189        self.origin.y
190    }
191    #[must_use]
192    pub fn width(&self) -> f32 {
193        self.size.width
194    }
195    #[must_use]
196    pub fn height(&self) -> f32 {
197        self.size.height
198    }
199
200    #[must_use]
201    pub fn left(&self) -> f32 {
202        self.origin.x
203    }
204    #[must_use]
205    pub fn top(&self) -> f32 {
206        self.origin.y
207    }
208    #[must_use]
209    pub fn right(&self) -> f32 {
210        self.origin.x + self.size.width
211    }
212    #[must_use]
213    pub fn bottom(&self) -> f32 {
214        self.origin.y + self.size.height
215    }
216
217    #[must_use]
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    #[must_use]
226    pub fn is_empty(&self) -> bool {
227        self.size.is_empty()
228    }
229
230    #[must_use]
231    pub fn contains_point(&self, p: Point) -> bool {
232        p.x >= self.left() && p.x < self.right() && p.y >= self.top() && p.y < self.bottom()
233    }
234
235    #[must_use]
236    pub fn contains_rect(&self, other: &Rect) -> bool {
237        other.left() >= self.left()
238            && other.top() >= self.top()
239            && other.right() <= self.right()
240            && other.bottom() <= self.bottom()
241    }
242
243    #[must_use]
244    pub fn intersects(&self, other: &Rect) -> bool {
245        self.left() < other.right()
246            && self.right() > other.left()
247            && self.top() < other.bottom()
248            && self.bottom() > other.top()
249    }
250
251    /// Returns the intersection of two rects, or `None` if they don't overlap.
252    #[must_use]
253    pub fn intersection(&self, other: &Rect) -> Option<Self> {
254        if !self.intersects(other) {
255            return None;
256        }
257        let left = self.left().max(other.left());
258        let top = self.top().max(other.top());
259        let right = self.right().min(other.right());
260        let bottom = self.bottom().min(other.bottom());
261        Some(Self::from_ltrb(left, top, right, bottom))
262    }
263
264    /// Smallest rect that contains both.
265    #[must_use]
266    pub fn union(&self, other: &Rect) -> Self {
267        if self.is_empty() {
268            return *other;
269        }
270        if other.is_empty() {
271            return *self;
272        }
273        let left = self.left().min(other.left());
274        let top = self.top().min(other.top());
275        let right = self.right().max(other.right());
276        let bottom = self.bottom().max(other.bottom());
277        Self::from_ltrb(left, top, right, bottom)
278    }
279
280    /// Expand each edge outward.
281    #[must_use]
282    pub fn inflate(&self, dx: f32, dy: f32) -> Self {
283        Self::new(
284            self.origin.x - dx,
285            self.origin.y - dy,
286            self.size.width + dx * 2.0,
287            self.size.height + dy * 2.0,
288        )
289    }
290
291    /// Shrink each edge inward by per-edge amounts. Clamps size to zero.
292    #[must_use]
293    pub fn inset(&self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
294        Self::new(
295            self.origin.x + left,
296            self.origin.y + top,
297            (self.size.width - left - right).max(0.0),
298            (self.size.height - top - bottom).max(0.0),
299        )
300    }
301
302    /// Expand each edge outward by per-edge amounts.
303    #[must_use]
304    pub fn outset(&self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
305        Self::new(
306            self.origin.x - left,
307            self.origin.y - top,
308            self.size.width + left + right,
309            self.size.height + top + bottom,
310        )
311    }
312
313    #[must_use]
314    pub fn translate(&self, offset: Offset) -> Self {
315        Self {
316            origin: self.origin + offset,
317            size: self.size,
318        }
319    }
320}
321
322/// Per-edge values (top, right, bottom, left). Used for margin, padding,
323/// border widths — anything that has four directional values.
324#[derive(Clone, Copy, Debug, Default, PartialEq)]
325pub struct Edges<T: Copy> {
326    pub top: T,
327    pub right: T,
328    pub bottom: T,
329    pub left: T,
330}
331
332impl<T: Copy> Edges<T> {
333    pub const fn new(top: T, right: T, bottom: T, left: T) -> Self {
334        Self {
335            top,
336            right,
337            bottom,
338            left,
339        }
340    }
341
342    pub fn all(value: T) -> Self {
343        Self {
344            top: value,
345            right: value,
346            bottom: value,
347            left: value,
348        }
349    }
350
351    pub fn symmetric(vertical: T, horizontal: T) -> Self {
352        Self {
353            top: vertical,
354            right: horizontal,
355            bottom: vertical,
356            left: horizontal,
357        }
358    }
359}
360
361/// Per-corner values (top-left, top-right, bottom-right, bottom-left).
362/// Used for border-radius.
363#[derive(Clone, Copy, Debug, Default, PartialEq)]
364pub struct Corners<T: Copy> {
365    pub top_left: T,
366    pub top_right: T,
367    pub bottom_right: T,
368    pub bottom_left: T,
369}
370
371impl<T: Copy> Corners<T> {
372    pub const fn new(top_left: T, top_right: T, bottom_right: T, bottom_left: T) -> Self {
373        Self {
374            top_left,
375            top_right,
376            bottom_right,
377            bottom_left,
378        }
379    }
380
381    pub fn all(value: T) -> Self {
382        Self {
383            top_left: value,
384            top_right: value,
385            bottom_right: value,
386            bottom_left: value,
387        }
388    }
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394
395    #[test]
396    fn point_offset_arithmetic() {
397        let a = Point::new(10.0, 20.0);
398        let b = Point::new(30.0, 50.0);
399
400        let delta = b - a;
401        assert_eq!(delta, Offset::new(20.0, 30.0));
402
403        let c = a + delta;
404        assert_eq!(c, b);
405    }
406
407    #[test]
408    fn point_distance() {
409        let a = Point::new(0.0, 0.0);
410        let b = Point::new(3.0, 4.0);
411        assert!((a.distance_to(b) - 5.0).abs() < f32::EPSILON);
412    }
413
414    #[test]
415    fn size_empty_and_area() {
416        assert!(Size::ZERO.is_empty());
417        assert!(!Size::new(10.0, 5.0).is_empty());
418        assert_eq!(Size::new(3.0, 4.0).area(), 12.0);
419    }
420
421    #[test]
422    fn rect_edges() {
423        let r = Rect::new(10.0, 20.0, 100.0, 50.0);
424        assert_eq!(r.left(), 10.0);
425        assert_eq!(r.top(), 20.0);
426        assert_eq!(r.right(), 110.0);
427        assert_eq!(r.bottom(), 70.0);
428        assert_eq!(r.center(), Point::new(60.0, 45.0));
429    }
430
431    #[test]
432    fn rect_from_ltrb() {
433        let r = Rect::from_ltrb(10.0, 20.0, 110.0, 70.0);
434        assert_eq!(r.origin, Point::new(10.0, 20.0));
435        assert_eq!(r.size, Size::new(100.0, 50.0));
436    }
437
438    #[test]
439    fn rect_contains_point() {
440        let r = Rect::new(0.0, 0.0, 100.0, 100.0);
441        assert!(r.contains_point(Point::new(50.0, 50.0)));
442        assert!(r.contains_point(Point::new(0.0, 0.0)));
443        assert!(!r.contains_point(Point::new(100.0, 100.0))); // exclusive right/bottom
444        assert!(!r.contains_point(Point::new(-1.0, 50.0)));
445    }
446
447    #[test]
448    fn rect_intersection() {
449        let a = Rect::new(0.0, 0.0, 100.0, 100.0);
450        let b = Rect::new(50.0, 50.0, 100.0, 100.0);
451        let c = Rect::new(200.0, 200.0, 10.0, 10.0);
452
453        let ab = a.intersection(&b).unwrap();
454        assert_eq!(ab, Rect::new(50.0, 50.0, 50.0, 50.0));
455
456        assert!(a.intersection(&c).is_none());
457    }
458
459    #[test]
460    fn rect_union() {
461        let a = Rect::new(0.0, 0.0, 50.0, 50.0);
462        let b = Rect::new(25.0, 25.0, 50.0, 50.0);
463
464        let u = a.union(&b);
465        assert_eq!(u, Rect::new(0.0, 0.0, 75.0, 75.0));
466    }
467
468    #[test]
469    fn rect_inflate() {
470        let r = Rect::new(10.0, 10.0, 20.0, 20.0);
471        let expanded = r.inflate(5.0, 5.0);
472        assert_eq!(expanded, Rect::new(5.0, 5.0, 30.0, 30.0));
473    }
474
475    #[test]
476    fn rect_translate() {
477        let r = Rect::new(10.0, 20.0, 30.0, 40.0);
478        let moved = r.translate(Offset::new(5.0, -5.0));
479        assert_eq!(moved, Rect::new(15.0, 15.0, 30.0, 40.0));
480    }
481
482    #[test]
483    fn edges_constructors() {
484        let uniform = Edges::all(8.0f32);
485        assert_eq!(uniform.top, 8.0);
486        assert_eq!(uniform.right, 8.0);
487
488        let sym = Edges::symmetric(10.0f32, 20.0);
489        assert_eq!(sym.top, 10.0);
490        assert_eq!(sym.right, 20.0);
491        assert_eq!(sym.bottom, 10.0);
492        assert_eq!(sym.left, 20.0);
493    }
494
495    #[test]
496    fn corners_constructor() {
497        let c = Corners::all(4.0f32);
498        assert_eq!(c.top_left, 4.0);
499        assert_eq!(c.bottom_right, 4.0);
500    }
501
502    #[test]
503    fn offset_neg() {
504        let o = Offset::new(10.0, -5.0);
505        assert_eq!(-o, Offset::new(-10.0, 5.0));
506    }
507}