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}