Skip to main content

azul_css/props/basic/
geometry.rs

1//! Basic geometry primitives (`LayoutPoint`, `LayoutSize`, `LayoutRect`) for
2//! layout calculations, using `isize` coordinates (as opposed to the `f32`-based
3//! logical coordinates in `core::geom`).
4
5use core::fmt;
6
7use crate::{
8    impl_option, impl_vec, impl_vec_clone, impl_vec_debug, impl_vec_mut, impl_vec_partialeq,
9    impl_vec_partialord,
10};
11
12/// Only used for calculations: Point coordinate (x, y) in layout space.
13#[derive(Copy, Default, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
14#[repr(C)]
15pub struct LayoutPoint {
16    pub x: isize,
17    pub y: isize,
18}
19
20impl fmt::Debug for LayoutPoint {
21    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
22        write!(f, "{}", self)
23    }
24}
25impl fmt::Display for LayoutPoint {
26    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
27        write!(f, "({}, {})", self.x, self.y)
28    }
29}
30
31impl LayoutPoint {
32    #[inline(always)]
33    pub const fn new(x: isize, y: isize) -> Self {
34        Self { x, y }
35    }
36    #[inline(always)]
37    pub const fn zero() -> Self {
38        Self::new(0, 0)
39    }
40}
41
42impl_option!(
43    LayoutPoint,
44    OptionLayoutPoint,
45    [Debug, Copy, Clone, PartialEq, PartialOrd]
46);
47
48/// Only used for calculations: Size (width, height) in layout space.
49#[derive(Copy, Default, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
50#[repr(C)]
51pub struct LayoutSize {
52    pub width: isize,
53    pub height: isize,
54}
55
56impl fmt::Debug for LayoutSize {
57    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
58        write!(f, "{}", self)
59    }
60}
61impl fmt::Display for LayoutSize {
62    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
63        write!(f, "{}x{}", self.width, self.height)
64    }
65}
66
67impl LayoutSize {
68    #[inline(always)]
69    pub const fn new(width: isize, height: isize) -> Self {
70        Self { width, height }
71    }
72    #[inline(always)]
73    pub const fn zero() -> Self {
74        Self::new(0, 0)
75    }
76    #[inline]
77    pub fn round(width: f32, height: f32) -> Self {
78        Self {
79            width: libm::roundf(width) as isize,
80            height: libm::roundf(height) as isize,
81        }
82    }
83}
84
85impl_option!(
86    LayoutSize,
87    OptionLayoutSize,
88    [Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Hash]
89);
90
91/// Only used for calculations: Rectangle (x, y, width, height) in layout space.
92#[derive(Copy, Clone, PartialEq, PartialOrd)]
93#[repr(C)]
94pub struct LayoutRect {
95    pub origin: LayoutPoint,
96    pub size: LayoutSize,
97}
98
99impl_option!(
100    LayoutRect,
101    OptionLayoutRect,
102    [Debug, Copy, Clone, PartialEq, PartialOrd]
103);
104impl_vec!(LayoutRect, LayoutRectVec, LayoutRectVecDestructor, LayoutRectVecDestructorType, LayoutRectVecSlice, OptionLayoutRect);
105impl_vec_clone!(LayoutRect, LayoutRectVec, LayoutRectVecDestructor);
106impl_vec_debug!(LayoutRect, LayoutRectVec);
107impl_vec_mut!(LayoutRect, LayoutRectVec);
108impl_vec_partialeq!(LayoutRect, LayoutRectVec);
109impl_vec_partialord!(LayoutRect, LayoutRectVec);
110
111impl fmt::Debug for LayoutRect {
112    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
113        write!(f, "{}", self)
114    }
115}
116impl fmt::Display for LayoutRect {
117    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
118        write!(f, "{} @ {}", self.size, self.origin)
119    }
120}
121
122impl LayoutRect {
123    #[inline(always)]
124    pub const fn new(origin: LayoutPoint, size: LayoutSize) -> Self {
125        Self { origin, size }
126    }
127    #[inline(always)]
128    pub const fn zero() -> Self {
129        Self::new(LayoutPoint::zero(), LayoutSize::zero())
130    }
131    #[inline(always)]
132    pub const fn max_x(&self) -> isize {
133        self.origin.x + self.size.width
134    }
135    #[inline(always)]
136    pub const fn min_x(&self) -> isize {
137        self.origin.x
138    }
139    #[inline(always)]
140    pub const fn max_y(&self) -> isize {
141        self.origin.y + self.size.height
142    }
143    #[inline(always)]
144    pub const fn min_y(&self) -> isize {
145        self.origin.y
146    }
147    #[inline(always)]
148    pub const fn width(&self) -> isize {
149        self.size.width
150    }
151    #[inline(always)]
152    pub const fn height(&self) -> isize {
153        self.size.height
154    }
155
156    pub const fn contains(&self, other: &LayoutPoint) -> bool {
157        self.min_x() <= other.x
158            && other.x < self.max_x()
159            && self.min_y() <= other.y
160            && other.y < self.max_y()
161    }
162
163    pub fn contains_f32(&self, other_x: f32, other_y: f32) -> bool {
164        self.min_x() as f32 <= other_x
165            && other_x < self.max_x() as f32
166            && self.min_y() as f32 <= other_y
167            && other_y < self.max_y() as f32
168    }
169
170    /// Like `contains()`, but returns the (x, y) offset of the hit point
171    /// relative to the rectangle origin. Unlike `contains()`, points exactly
172    /// on the boundary are excluded (returns `None`).
173    #[inline]
174    pub const fn hit_test(&self, other: &LayoutPoint) -> Option<LayoutPoint> {
175        let dx_left_edge = other.x - self.min_x();
176        let dx_right_edge = self.max_x() - other.x;
177        let dy_top_edge = other.y - self.min_y();
178        let dy_bottom_edge = self.max_y() - other.y;
179        if dx_left_edge > 0 && dx_right_edge > 0 && dy_top_edge > 0 && dy_bottom_edge > 0 {
180            Some(LayoutPoint::new(dx_left_edge, dy_top_edge))
181        } else {
182            None
183        }
184    }
185
186    /// Returns the bounding rectangle that covers every rectangle in the slice,
187    /// or `OptionLayoutRect::None` if the slice is empty.
188    #[inline]
189    pub fn union(rects: LayoutRectVecSlice) -> OptionLayoutRect {
190        let mut iter = rects.as_slice().iter().copied();
191        let Some(first) = iter.next() else {
192            return OptionLayoutRect::None;
193        };
194
195        let mut min_x = first.origin.x;
196        let mut min_y = first.origin.y;
197        let mut max_x = first.origin.x + first.size.width;
198        let mut max_y = first.origin.y + first.size.height;
199
200        for Self {
201            origin: LayoutPoint { x, y },
202            size: LayoutSize { width, height },
203        } in iter
204        {
205            max_x = max_x.max(x + width);
206            max_y = max_y.max(y + height);
207            min_x = min_x.min(x);
208            min_y = min_y.min(y);
209        }
210
211        OptionLayoutRect::Some(Self {
212            origin: LayoutPoint { x: min_x, y: min_y },
213            size: LayoutSize {
214                width: max_x - min_x,
215                height: max_y - min_y,
216            },
217        })
218    }
219
220    /// Returns true if `b` is fully contained inside `self`.
221    #[inline(always)]
222    pub const fn contains_rect(&self, b: &LayoutRect) -> bool {
223        let a = self;
224
225        let a_x = a.origin.x;
226        let a_y = a.origin.y;
227        let a_width = a.size.width;
228        let a_height = a.size.height;
229
230        let b_x = b.origin.x;
231        let b_y = b.origin.y;
232        let b_width = b.size.width;
233        let b_height = b.size.height;
234
235        b_x >= a_x
236            && b_y >= a_y
237            && b_x + b_width <= a_x + a_width
238            && b_y + b_height <= a_y + a_height
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    fn rect(x: isize, y: isize, w: isize, h: isize) -> LayoutRect {
247        LayoutRect::new(LayoutPoint::new(x, y), LayoutSize::new(w, h))
248    }
249
250    #[test]
251    fn union_slice_returns_bounding_rect() {
252        let vec: LayoutRectVec =
253            alloc::vec![rect(0, 0, 10, 10), rect(20, -5, 5, 30), rect(-3, 15, 4, 4)].into();
254        let slice = vec.as_c_slice();
255
256        match LayoutRect::union(slice) {
257            OptionLayoutRect::Some(r) => {
258                assert_eq!(r, rect(-3, -5, 28, 30));
259            }
260            OptionLayoutRect::None => panic!("expected Some bounding rect"),
261        }
262    }
263
264    #[test]
265    fn union_empty_slice_returns_none() {
266        let vec: LayoutRectVec = LayoutRectVec::new();
267        let slice = vec.as_c_slice();
268        assert!(matches!(LayoutRect::union(slice), OptionLayoutRect::None));
269    }
270}