Skip to main content

aetna_core/tree/
geometry.rs

1//! Geometry primitives used by layout, hit-testing, and painting.
2
3/// A rectangle in **logical pixels**. The host's `scale_factor` is
4/// applied at paint time, so layout, hit-testing, and `Rect`-shaped
5/// API arguments all speak the same un-scaled coordinate space.
6///
7/// Origin top-left, +y down.
8#[derive(Clone, Copy, Debug, Default, PartialEq)]
9pub struct Rect {
10    pub x: f32,
11    pub y: f32,
12    pub w: f32,
13    pub h: f32,
14}
15
16impl Rect {
17    pub const fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
18        Self { x, y, w, h }
19    }
20
21    pub fn right(self) -> f32 {
22        self.x + self.w
23    }
24
25    pub fn bottom(self) -> f32 {
26        self.y + self.h
27    }
28
29    pub fn center_x(self) -> f32 {
30        self.x + self.w * 0.5
31    }
32
33    pub fn center_y(self) -> f32 {
34        self.y + self.h * 0.5
35    }
36
37    pub fn contains(self, x: f32, y: f32) -> bool {
38        x >= self.x && x < self.right() && y >= self.y && y < self.bottom()
39    }
40
41    pub fn intersect(self, other: Rect) -> Option<Rect> {
42        let x1 = self.x.max(other.x);
43        let y1 = self.y.max(other.y);
44        let x2 = self.right().min(other.right());
45        let y2 = self.bottom().min(other.bottom());
46        if x2 <= x1 {
47            return None;
48        }
49        if y2 <= y1 {
50            return None;
51        }
52        Some(Rect::new(x1, y1, x2 - x1, y2 - y1))
53    }
54
55    pub fn inset(self, p: Sides) -> Self {
56        Self::new(
57            self.x + p.left,
58            self.y + p.top,
59            (self.w - p.left - p.right).max(0.0),
60            (self.h - p.top - p.bottom).max(0.0),
61        )
62    }
63
64    /// Inverse of [`Self::inset`]: extend the rect outward by `p` on each side.
65    pub fn outset(self, p: Sides) -> Self {
66        Self::new(
67            self.x - p.left,
68            self.y - p.top,
69            self.w + p.left + p.right,
70            self.h + p.top + p.bottom,
71        )
72    }
73}
74
75/// Per-side padding/inset values.
76#[derive(Clone, Copy, Debug, Default, PartialEq)]
77pub struct Sides {
78    pub left: f32,
79    pub right: f32,
80    pub top: f32,
81    pub bottom: f32,
82}
83
84impl Sides {
85    pub const fn all(v: f32) -> Self {
86        Self {
87            left: v,
88            right: v,
89            top: v,
90            bottom: v,
91        }
92    }
93
94    pub const fn xy(x: f32, y: f32) -> Self {
95        Self {
96            left: x,
97            right: x,
98            top: y,
99            bottom: y,
100        }
101    }
102
103    /// Horizontal-only padding — sets `left` and `right` to `v`,
104    /// leaves `top` and `bottom` at `0`. Mirrors Tailwind's `px-N`.
105    pub const fn x(v: f32) -> Self {
106        Self {
107            left: v,
108            right: v,
109            top: 0.0,
110            bottom: 0.0,
111        }
112    }
113
114    /// Vertical-only padding — sets `top` and `bottom` to `v`,
115    /// leaves `left` and `right` at `0`. Mirrors Tailwind's `py-N`.
116    pub const fn y(v: f32) -> Self {
117        Self {
118            left: 0.0,
119            right: 0.0,
120            top: v,
121            bottom: v,
122        }
123    }
124
125    /// Left-only padding — sets `left` to `v`, leaves the other three
126    /// at `0`. Mirrors Tailwind's `pl-N` and [`Corners::left`].
127    pub const fn left(v: f32) -> Self {
128        Self {
129            left: v,
130            right: 0.0,
131            top: 0.0,
132            bottom: 0.0,
133        }
134    }
135
136    /// Right-only padding — sets `right` to `v`, leaves the other three
137    /// at `0`. Mirrors Tailwind's `pr-N` and [`Corners::right`].
138    pub const fn right(v: f32) -> Self {
139        Self {
140            left: 0.0,
141            right: v,
142            top: 0.0,
143            bottom: 0.0,
144        }
145    }
146
147    /// Top-only padding — sets `top` to `v`, leaves the other three
148    /// at `0`. Mirrors Tailwind's `pt-N` and [`Corners::top`].
149    pub const fn top(v: f32) -> Self {
150        Self {
151            left: 0.0,
152            right: 0.0,
153            top: v,
154            bottom: 0.0,
155        }
156    }
157
158    /// Bottom-only padding — sets `bottom` to `v`, leaves the other
159    /// three at `0`. Mirrors Tailwind's `pb-N` and [`Corners::bottom`].
160    pub const fn bottom(v: f32) -> Self {
161        Self {
162            left: 0.0,
163            right: 0.0,
164            top: 0.0,
165            bottom: v,
166        }
167    }
168
169    pub const fn zero() -> Self {
170        Self::all(0.0)
171    }
172}
173
174impl From<f32> for Sides {
175    fn from(v: f32) -> Self {
176        Sides::all(v)
177    }
178}
179
180/// Per-corner radius values, in logical pixels.
181///
182/// `radius` is authored as a single scalar in the common case
183/// (`.radius(tokens::RADIUS_MD)` works via [`From<f32>`]). Per-corner
184/// shapes are built with [`Corners::top`], [`Corners::bottom`],
185/// [`Corners::left`], [`Corners::right`], or by constructing the
186/// struct directly. The painter clamps each corner to half the shorter
187/// side, so over-large values render as a pill on that corner.
188#[derive(Clone, Copy, Debug, Default, PartialEq)]
189pub struct Corners {
190    pub tl: f32,
191    pub tr: f32,
192    pub br: f32,
193    pub bl: f32,
194}
195
196impl Corners {
197    pub const ZERO: Self = Self::all(0.0);
198
199    pub const fn all(r: f32) -> Self {
200        Self {
201            tl: r,
202            tr: r,
203            br: r,
204            bl: r,
205        }
206    }
207
208    /// Round the top two corners (`tl`, `tr`); leave `bl` / `br` at 0.
209    /// Use this on a header strip nested in a rounded card so the
210    /// strip's top corners follow the card's curve.
211    pub const fn top(r: f32) -> Self {
212        Self {
213            tl: r,
214            tr: r,
215            br: 0.0,
216            bl: 0.0,
217        }
218    }
219
220    /// Round the bottom two corners (`bl`, `br`); leave `tl` / `tr` at 0.
221    pub const fn bottom(r: f32) -> Self {
222        Self {
223            tl: 0.0,
224            tr: 0.0,
225            br: r,
226            bl: r,
227        }
228    }
229
230    /// Round the left two corners (`tl`, `bl`); leave `tr` / `br` at 0.
231    pub const fn left(r: f32) -> Self {
232        Self {
233            tl: r,
234            tr: 0.0,
235            br: 0.0,
236            bl: r,
237        }
238    }
239
240    /// Round the right two corners (`tr`, `br`); leave `tl` / `bl` at 0.
241    pub const fn right(r: f32) -> Self {
242        Self {
243            tl: 0.0,
244            tr: r,
245            br: r,
246            bl: 0.0,
247        }
248    }
249
250    /// True when every corner has the same radius. The painter takes
251    /// a fast path on uniform corners, and SVG bundle output emits
252    /// `<rect rx>` rather than a `<path>`.
253    pub fn is_uniform(self) -> bool {
254        self.tl == self.tr && self.tr == self.br && self.br == self.bl
255    }
256
257    /// True when at least one corner has a non-zero radius.
258    pub fn any_nonzero(self) -> bool {
259        self.tl > 0.0 || self.tr > 0.0 || self.br > 0.0 || self.bl > 0.0
260    }
261
262    /// Largest of the four corner radii. The painter uses this for
263    /// shadow / focus-ring SDF approximation, where "loosely the
264    /// silhouette of the rounded shape" is enough.
265    pub fn max(self) -> f32 {
266        self.tl.max(self.tr).max(self.br).max(self.bl)
267    }
268
269    /// Pack as a `[f32; 4]` in `(tl, tr, br, bl)` order — the layout the
270    /// shader's `slot_e` instance attribute expects.
271    pub fn to_array(self) -> [f32; 4] {
272        [self.tl, self.tr, self.br, self.bl]
273    }
274}
275
276impl From<f32> for Corners {
277    fn from(r: f32) -> Self {
278        Corners::all(r)
279    }
280}
281
282#[cfg(test)]
283mod corners_tests {
284    use super::*;
285
286    #[test]
287    fn shorthand_constructors_only_round_their_named_corners() {
288        let top = Corners::top(8.0);
289        assert_eq!(
290            top,
291            Corners {
292                tl: 8.0,
293                tr: 8.0,
294                br: 0.0,
295                bl: 0.0
296            }
297        );
298
299        let bottom = Corners::bottom(8.0);
300        assert_eq!(
301            bottom,
302            Corners {
303                tl: 0.0,
304                tr: 0.0,
305                br: 8.0,
306                bl: 8.0
307            }
308        );
309
310        let left = Corners::left(8.0);
311        assert_eq!(
312            left,
313            Corners {
314                tl: 8.0,
315                tr: 0.0,
316                br: 0.0,
317                bl: 8.0
318            }
319        );
320
321        let right = Corners::right(8.0);
322        assert_eq!(
323            right,
324            Corners {
325                tl: 0.0,
326                tr: 8.0,
327                br: 8.0,
328                bl: 0.0
329            }
330        );
331    }
332
333    #[test]
334    fn is_uniform_is_true_only_when_all_four_corners_match() {
335        assert!(Corners::all(8.0).is_uniform());
336        assert!(Corners::ZERO.is_uniform());
337        assert!(!Corners::top(8.0).is_uniform());
338    }
339
340    #[test]
341    fn from_f32_produces_uniform_corners_for_back_compat() {
342        // Existing call sites do `.radius(tokens::RADIUS_MD)` against
343        // an f32; the chainable accepts `impl Into<Corners>` and the
344        // float promotes to uniform corners.
345        let c: Corners = 12.0_f32.into();
346        assert_eq!(c, Corners::all(12.0));
347    }
348}