Skip to main content

damascene_core/tree/
geometry.rs

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