Skip to main content

frus_core/
geometry.rs

1//! Geometric primitives, expressed in logical pixels.
2//!
3//! frus's coordinate convention: the origin sits at the **top left**, X runs
4//! right, and Y runs **down** — the same convention as CSS.
5
6/// A **2D affine transform** (a 2×3 matrix). A point `(x, y)` becomes
7/// `(a·x + c·y + e, b·x + d·y + f)`: a 2×2 linear part `[[a, c], [b, d]]` (scale,
8/// rotation, shear) plus a translation `(e, f)`.
9///
10/// Compositions read "right to left": `a.then(b)` applies `a` **then** `b`. This is
11/// the unified representation of a [`crate::LayerTransform`]'s paint transforms:
12/// translation, scale (uniform or per axis) and rotation all fold into a **single**
13/// matrix, with no approximation introduced by composing them.
14#[derive(Clone, Copy, Debug, PartialEq)]
15pub struct Affine {
16    /// `[a, b, c, d, e, f]`: `x' = a·x + c·y + e`, `y' = b·x + d·y + f`.
17    pub m: [f32; 6],
18}
19
20impl Default for Affine {
21    fn default() -> Self {
22        Self::IDENTITY
23    }
24}
25
26impl Affine {
27    /// The identity — no transform at all.
28    pub const IDENTITY: Affine = Affine {
29        m: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
30    };
31
32    /// A pure translation by `(tx, ty)`.
33    pub const fn translation(tx: f32, ty: f32) -> Affine {
34        Affine {
35            m: [1.0, 0.0, 0.0, 1.0, tx, ty],
36        }
37    }
38
39    /// A per-axis scale, about the origin.
40    pub const fn scale(sx: f32, sy: f32) -> Affine {
41        Affine {
42            m: [sx, 0.0, 0.0, sy, 0.0, 0.0],
43        }
44    }
45
46    /// A rotation of `angle` radians (clockwise, with y pointing down), about the
47    /// origin.
48    pub fn rotation(angle: f32) -> Affine {
49        let (s, c) = angle.sin_cos();
50        Affine {
51            m: [c, s, -s, c, 0.0, 0.0],
52        }
53    }
54
55    /// Composition: `self.then(next)` applies `self` **then** `next`.
56    pub fn then(self, next: Affine) -> Affine {
57        // next ∘ self: expand next(self(p)).
58        let [a, b, c, d, e, f] = self.m;
59        let [a2, b2, c2, d2, e2, f2] = next.m;
60        Affine {
61            m: [
62                a2 * a + c2 * b,
63                b2 * a + d2 * b,
64                a2 * c + c2 * d,
65                b2 * c + d2 * d,
66                a2 * e + c2 * f + e2,
67                b2 * e + d2 * f + f2,
68            ],
69        }
70    }
71
72    /// The same transform, but **about `pivot`**, which stays fixed:
73    /// `T(pivot) ∘ self ∘ T(-pivot)`.
74    pub fn about(self, pivot: Point) -> Affine {
75        Affine::translation(-pivot.x, -pivot.y)
76            .then(self)
77            .then(Affine::translation(pivot.x, pivot.y))
78    }
79
80    /// Applies the transform to a point.
81    pub fn apply(self, p: Point) -> Point {
82        let [a, b, c, d, e, f] = self.m;
83        Point::new(a * p.x + c * p.y + e, b * p.x + d * p.y + f)
84    }
85
86    /// `true` when the transform is **axis-aligned**: its linear part is diagonal
87    /// (scale and/or translation, with no rotation and no shear), so the image of a
88    /// rectangle is still a rectangle.
89    pub fn is_axis_aligned(self) -> bool {
90        self.m[1].abs() < 1e-4 && self.m[2].abs() < 1e-4
91    }
92
93    /// The image of a rectangle under the transform. **Exact** when the matrix is
94    /// axis-aligned ([`Affine::is_axis_aligned`]); otherwise it returns the image's
95    /// **bounding box**, since a rectangle cannot represent a rotated shape.
96    pub fn apply_rect(self, r: Rect) -> Rect {
97        let a = self.apply(Point::new(r.x, r.y));
98        let b = self.apply(Point::new(r.x + r.width, r.y + r.height));
99        Rect::new(
100            a.x.min(b.x),
101            a.y.min(b.y),
102            (a.x - b.x).abs(),
103            (a.y - b.y).abs(),
104        )
105    }
106
107    /// The inverse transform — the identity if the matrix is degenerate.
108    pub fn inverse(self) -> Affine {
109        let [a, b, c, d, e, f] = self.m;
110        let det = a * d - b * c;
111        if det.abs() < f32::EPSILON {
112            return Affine::IDENTITY;
113        }
114        let inv = 1.0 / det;
115        Affine {
116            m: [
117                d * inv,
118                -b * inv,
119                -c * inv,
120                a * inv,
121                (c * f - d * e) * inv,
122                (b * e - a * f) * inv,
123            ],
124        }
125    }
126}
127
128/// A 2D point.
129#[derive(Clone, Copy, Debug, PartialEq, Default)]
130pub struct Point {
131    /// The horizontal coordinate, in logical pixels.
132    pub x: f32,
133    /// The vertical coordinate, in logical pixels.
134    pub y: f32,
135}
136
137impl Point {
138    /// The origin.
139    pub const ZERO: Self = Self::new(0.0, 0.0);
140
141    /// A point at `(x, y)`, in logical pixels.
142    pub const fn new(x: f32, y: f32) -> Self {
143        Self { x, y }
144    }
145
146    /// Multiplies both coordinates by `factor` (logical → physical conversion).
147    pub fn scale(self, factor: f32) -> Self {
148        Self::new(self.x * factor, self.y * factor)
149    }
150
151    /// Multiplies the coordinates **per axis** (`sx`, `sy`).
152    pub fn scale_xy(self, sx: f32, sy: f32) -> Self {
153        Self::new(self.x * sx, self.y * sy)
154    }
155}
156
157/// The **reading direction** of text and layout. An ambient context: in RTL, rows,
158/// alignment and directional insets all flip horizontally.
159#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
160pub enum TextDirection {
161    /// Left to right (Latin scripts; the default).
162    #[default]
163    Ltr,
164    /// Right to left (Arabic, Hebrew, and others).
165    Rtl,
166}
167
168impl TextDirection {
169    /// `true` when right-to-left.
170    pub fn is_rtl(self) -> bool {
171        matches!(self, TextDirection::Rtl)
172    }
173}
174
175/// Insets — padding or margin — per side, in logical pixels.
176#[derive(Clone, Copy, Debug, PartialEq, Default)]
177pub struct Insets {
178    /// The top inset, in logical pixels.
179    pub top: f32,
180    /// The right inset, in logical pixels.
181    pub right: f32,
182    /// The bottom inset, in logical pixels.
183    pub bottom: f32,
184    /// The left inset, in logical pixels.
185    pub left: f32,
186}
187
188impl Insets {
189    /// No inset on any side.
190    pub const ZERO: Self = Self::new(0.0, 0.0, 0.0, 0.0);
191
192    /// An inset per side, in logical pixels.
193    pub const fn new(top: f32, right: f32, bottom: f32, left: f32) -> Self {
194        Self {
195            top,
196            right,
197            bottom,
198            left,
199        }
200    }
201
202    /// The same inset on all four sides.
203    pub const fn uniform(value: f32) -> Self {
204        Self::new(value, value, value, value)
205    }
206}
207
208/// **Directional** insets: `start`/`end` instead of `left`/`right`. In LTR, `start`
209/// is the left; in RTL it is the right. Resolved into concrete [`Insets`] at layout
210/// time.
211#[derive(Clone, Copy, Debug, PartialEq, Default)]
212pub struct InsetsDirectional {
213    /// The top inset, in logical pixels.
214    pub top: f32,
215    /// The end-edge inset (right in LTR, left in RTL), in logical pixels.
216    pub end: f32,
217    /// The bottom inset, in logical pixels.
218    pub bottom: f32,
219    /// The start-edge inset (left in LTR, right in RTL), in logical pixels.
220    pub start: f32,
221}
222
223impl InsetsDirectional {
224    /// No inset on any side.
225    pub const ZERO: Self = Self::new(0.0, 0.0, 0.0, 0.0);
226
227    /// A directional inset per side, in logical pixels.
228    pub const fn new(top: f32, end: f32, bottom: f32, start: f32) -> Self {
229        Self {
230            top,
231            end,
232            bottom,
233            start,
234        }
235    }
236
237    /// A symmetric horizontal `start`/`end` inset.
238    pub const fn horizontal(value: f32) -> Self {
239        Self::new(0.0, value, 0.0, value)
240    }
241
242    /// Resolves to concrete insets for a direction: in RTL, `start` maps to right.
243    pub fn resolve(self, direction: TextDirection) -> Insets {
244        let (left, right) = if direction.is_rtl() {
245            (self.end, self.start)
246        } else {
247            (self.start, self.end)
248        };
249        Insets::new(self.top, right, self.bottom, left)
250    }
251}
252
253/// A 2D size (width × height).
254#[derive(Clone, Copy, Debug, PartialEq, Default)]
255pub struct Size {
256    /// The width, in logical pixels.
257    pub width: f32,
258    /// The height, in logical pixels.
259    pub height: f32,
260}
261
262impl Size {
263    /// No area.
264    pub const ZERO: Self = Self::new(0.0, 0.0);
265
266    /// A size of `width` × `height`, in logical pixels.
267    pub const fn new(width: f32, height: f32) -> Self {
268        Self { width, height }
269    }
270}
271
272/// Where to place a child inside its containing box: two **continuous** fractions
273/// in `[-1, 1]`. `x = -1` hugs the left, `0` centres, `+1` hugs the right; `y = -1`
274/// is the top, `+1` the bottom. Being continuous, it **interpolates**
275/// ([`crate::Lerp`]): a `Tween<Alignment>` slides a child from one anchor to
276/// another. The nine usual anchors are provided as constants
277/// (`Alignment::CENTER`, `Alignment::TOP_LEFT`, and so on).
278#[derive(Clone, Copy, Debug, PartialEq, Default)]
279pub struct Alignment {
280    /// Horizontal fraction: `-1` (left) … `0` (centre) … `+1` (right).
281    pub x: f32,
282    /// Vertical fraction: `-1` (top) … `0` (centre) … `+1` (bottom).
283    pub y: f32,
284}
285
286impl Alignment {
287    /// Hugs the top-left corner.
288    pub const TOP_LEFT: Self = Self::new(-1.0, -1.0);
289    /// Hugs the top edge, centred horizontally.
290    pub const TOP_CENTER: Self = Self::new(0.0, -1.0);
291    /// Hugs the top-right corner.
292    pub const TOP_RIGHT: Self = Self::new(1.0, -1.0);
293    /// Hugs the left edge, centred vertically.
294    pub const CENTER_LEFT: Self = Self::new(-1.0, 0.0);
295    /// The exact centre.
296    pub const CENTER: Self = Self::new(0.0, 0.0);
297    /// Hugs the right edge, centred vertically.
298    pub const CENTER_RIGHT: Self = Self::new(1.0, 0.0);
299    /// Hugs the bottom-left corner.
300    pub const BOTTOM_LEFT: Self = Self::new(-1.0, 1.0);
301    /// Hugs the bottom edge, centred horizontally.
302    pub const BOTTOM_CENTER: Self = Self::new(0.0, 1.0);
303    /// Hugs the bottom-right corner.
304    pub const BOTTOM_RIGHT: Self = Self::new(1.0, 1.0);
305
306    /// An `(x, y)` anchor, with fractions in `[-1, 1]`.
307    pub const fn new(x: f32, y: f32) -> Self {
308        Self { x, y }
309    }
310
311    /// The horizontal fraction mapped into `[0, 1]` (`0` = left, `1` = right): the
312    /// share of the free space to leave **before** the child on the x axis.
313    pub fn fraction_x(self) -> f32 {
314        ((self.x + 1.0) * 0.5).clamp(0.0, 1.0)
315    }
316
317    /// The vertical fraction mapped into `[0, 1]` (`0` = top, `1` = bottom).
318    pub fn fraction_y(self) -> f32 {
319        ((self.y + 1.0) * 0.5).clamp(0.0, 1.0)
320    }
321}
322
323/// A **directional** anchor: the horizontal axis is expressed **start → end**
324/// rather than left → right. `x_start = -1` hugs the **start** edge (left in LTR,
325/// right in RTL), `+1` the **end** edge. Resolved into a physical [`Alignment`] at
326/// render time from the reading direction — the anchor follows the text without the
327/// caller ever testing which way it runs.
328#[derive(Clone, Copy, Debug, PartialEq, Default)]
329pub struct AlignmentDirectional {
330    /// Start-to-end fraction: `-1` (start) … `0` (centre) … `+1` (end).
331    pub x_start: f32,
332    /// Vertical fraction: `-1` (top) … `+1` (bottom).
333    pub y: f32,
334}
335
336impl AlignmentDirectional {
337    /// Hugs the top-start corner (top-left in LTR, top-right in RTL).
338    pub const TOP_START: Self = Self::new(-1.0, -1.0);
339    /// Hugs the top edge, centred horizontally.
340    pub const TOP_CENTER: Self = Self::new(0.0, -1.0);
341    /// Hugs the top-end corner (top-right in LTR, top-left in RTL).
342    pub const TOP_END: Self = Self::new(1.0, -1.0);
343    /// Hugs the start edge, centred vertically.
344    pub const CENTER_START: Self = Self::new(-1.0, 0.0);
345    /// The exact centre.
346    pub const CENTER: Self = Self::new(0.0, 0.0);
347    /// Hugs the end edge, centred vertically.
348    pub const CENTER_END: Self = Self::new(1.0, 0.0);
349    /// Hugs the bottom-start corner (bottom-left in LTR, bottom-right in RTL).
350    pub const BOTTOM_START: Self = Self::new(-1.0, 1.0);
351    /// Hugs the bottom edge, centred horizontally.
352    pub const BOTTOM_CENTER: Self = Self::new(0.0, 1.0);
353    /// Hugs the bottom-end corner (bottom-right in LTR, bottom-left in RTL).
354    pub const BOTTOM_END: Self = Self::new(1.0, 1.0);
355
356    /// A directional `(x_start, y)` anchor, with fractions in `[-1, 1]`.
357    pub const fn new(x_start: f32, y: f32) -> Self {
358        Self { x_start, y }
359    }
360
361    /// Resolves to a **physical** anchor: in RTL, start maps to right (x is
362    /// mirrored); in LTR, start is left (x unchanged). `y` never depends on it.
363    pub fn resolve(self, direction: TextDirection) -> Alignment {
364        let x = if direction.is_rtl() {
365            -self.x_start
366        } else {
367            self.x_start
368        };
369        Alignment::new(x, self.y)
370    }
371}
372
373/// A **resolvable** anchor — either physical ([`Alignment`]) or directional
374/// ([`AlignmentDirectional`]). A widget accepts either one, through `Into`, and
375/// resolves it against the reading direction at render time.
376#[derive(Clone, Copy, Debug, PartialEq)]
377pub enum AlignmentGeometry {
378    /// A physical anchor, with absolute left and right.
379    Physical(Alignment),
380    /// A directional anchor (start/end, mirrored in RTL).
381    Directional(AlignmentDirectional),
382}
383
384impl AlignmentGeometry {
385    /// Resolves to a physical anchor for a reading direction; an already physical
386    /// anchor is returned unchanged.
387    pub fn resolve(self, direction: TextDirection) -> Alignment {
388        match self {
389            AlignmentGeometry::Physical(a) => a,
390            AlignmentGeometry::Directional(d) => d.resolve(direction),
391        }
392    }
393}
394
395impl AlignmentGeometry {
396    /// This anchor's two fractions, **in its own coordinates**: `x` for a physical
397    /// anchor, start-to-end for a directional one.
398    pub fn fractions(self) -> (f32, f32) {
399        match self {
400            AlignmentGeometry::Physical(a) => (a.x, a.y),
401            AlignmentGeometry::Directional(d) => (d.x_start, d.y),
402        }
403    }
404
405    /// The same **kind** of anchor with different fractions.
406    ///
407    /// This is what an animation between two anchors interpolates. It stays in the
408    /// anchor's own coordinates, so the reading direction is applied to the result rather
409    /// than to the endpoints — a directional anchor sliding from start to end mirrors as a
410    /// whole in a right-to-left script, instead of interpolating between two physical
411    /// points that were already mirrored and crossing in the middle.
412    pub fn with_fractions(self, x: f32, y: f32) -> Self {
413        match self {
414            AlignmentGeometry::Physical(_) => AlignmentGeometry::Physical(Alignment::new(x, y)),
415            AlignmentGeometry::Directional(_) => {
416                AlignmentGeometry::Directional(AlignmentDirectional::new(x, y))
417            }
418        }
419    }
420}
421
422impl From<Alignment> for AlignmentGeometry {
423    fn from(a: Alignment) -> Self {
424        AlignmentGeometry::Physical(a)
425    }
426}
427
428impl From<AlignmentDirectional> for AlignmentGeometry {
429    fn from(d: AlignmentDirectional) -> Self {
430        AlignmentGeometry::Directional(d)
431    }
432}
433
434/// The **window** insets, split by nature: `padding` is the area **permanently**
435/// taken by the system (status and navigation bars, the notch — all static), while
436/// `view_insets` is the area covered by **transient** UI, chiefly the soft
437/// keyboard. Avoiding the keyboard means keeping content clear of
438/// [`WindowInsets::safe`].
439#[derive(Clone, Copy, Debug, PartialEq)]
440pub struct WindowInsets {
441    /// Permanent system areas (bars, notch) **that are still worth avoiding**: the
442    /// intrusions, less whatever [`view_insets`](Self::view_insets) already covers.
443    ///
444    /// Zero at the bottom while the keyboard is up, because the navigation bar it
445    /// hides is not an edge anything needs to stay clear of any more. Padding a screen
446    /// by it as well would leave a strip of nothing between the content and the
447    /// keyboard.
448    pub padding: Insets,
449    /// Transient areas (the soft keyboard); in practice only the bottom moves.
450    ///
451    /// Measured from the window edge, so it already includes whatever bar it covers.
452    pub view_insets: Insets,
453    /// The intrusions **ignoring** anything transient: what the notch and the bars take
454    /// whether or not the keyboard is over them.
455    ///
456    /// The one that does not move when the keyboard opens, which is why a layout that
457    /// must not shift reads this rather than [`padding`](Self::padding).
458    pub view_padding: Insets,
459}
460
461impl WindowInsets {
462    /// No insets at all.
463    pub const ZERO: Self = Self {
464        padding: Insets::ZERO,
465        view_insets: Insets::ZERO,
466        view_padding: Insets::ZERO,
467    };
468
469    /// The intrusions of a surface with nothing transient over them: the bars and the
470    /// notch, and no keyboard.
471    ///
472    /// Here so that nobody has to assemble the three by hand and get them disagreeing.
473    /// `padding` and `view_padding` are the same thing while there is no keyboard, and
474    /// a hand-written literal that says otherwise describes a surface no platform can
475    /// report.
476    pub const fn bars(intrusions: Insets) -> Self {
477        Self {
478            padding: intrusions,
479            view_insets: Insets::ZERO,
480            view_padding: intrusions,
481        }
482    }
483
484    /// The total area to avoid: the per-side **maximum** of the two kinds. The
485    /// keyboard covers the navigation bar, so they are not added together.
486    pub fn safe(&self) -> Insets {
487        Insets::new(
488            self.padding.top.max(self.view_insets.top),
489            self.padding.right.max(self.view_insets.right),
490            self.padding.bottom.max(self.view_insets.bottom),
491            self.padding.left.max(self.view_insets.left),
492        )
493    }
494
495    /// Splits raw insets into `(padding, view_insets)` given a **keyboard-free**
496    /// reference. A **bottom** excess beyond that reference signals the keyboard;
497    /// `view_insets.bottom` then measures the **total** occlusion **from the window
498    /// edge**, bar included. That is what makes combining the two with `max`
499    /// correct.
500    pub fn from_baseline(baseline: Insets, current: Insets) -> WindowInsets {
501        let keyboard = (current.bottom - baseline.bottom).max(0.0);
502        // The intrusions with the keyboard taken back out: `current.bottom - keyboard`
503        // is the baseline's bottom by construction, and the other three sides do not
504        // move with a keyboard.
505        let view_padding = Insets::new(
506            current.top,
507            current.right,
508            current.bottom - keyboard,
509            current.left,
510        );
511        let view_insets = Insets::new(
512            0.0,
513            0.0,
514            if keyboard > 0.0 { current.bottom } else { 0.0 },
515            0.0,
516        );
517        WindowInsets {
518            padding: subtract(view_padding, view_insets),
519            view_insets,
520            view_padding,
521        }
522    }
523}
524
525/// `a` less `b`, side by side, never below zero.
526///
527/// What makes the padding the *remaining* intrusion rather than the whole one: a
528/// navigation bar under an open keyboard is covered, so nothing has to avoid it, and
529/// counting it twice would leave a strip of nothing above the keys.
530fn subtract(a: Insets, b: Insets) -> Insets {
531    Insets::new(
532        (a.top - b.top).max(0.0),
533        (a.right - b.right).max(0.0),
534        (a.bottom - b.bottom).max(0.0),
535        (a.left - b.left).max(0.0),
536    )
537}
538
539impl Default for WindowInsets {
540    fn default() -> Self {
541        Self::ZERO
542    }
543}
544
545/// An axis-aligned rectangle, in logical pixels.
546#[derive(Clone, Copy, Debug, PartialEq)]
547pub struct Rect {
548    /// Left edge.
549    pub x: f32,
550    /// Top edge.
551    pub y: f32,
552    /// Width, extending to the right.
553    pub width: f32,
554    /// Height, extending downwards.
555    pub height: f32,
556}
557
558impl Rect {
559    /// An "infinite" rectangle, used as a neutral clip that clips nothing.
560    pub const UNBOUNDED: Rect = Rect::new(-1.0e7, -1.0e7, 2.0e7, 2.0e7);
561
562    /// Builds a rectangle from its position (top-left corner) and its size.
563    pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
564        Self {
565            x,
566            y,
567            width,
568            height,
569        }
570    }
571
572    /// Offsets the rectangle by `(dx, dy)`.
573    pub fn translate(self, dx: f32, dy: f32) -> Self {
574        Self::new(self.x + dx, self.y + dy, self.width, self.height)
575    }
576
577    /// Multiplies position and size by `factor` (logical → physical conversion).
578    pub fn scale(self, factor: f32) -> Self {
579        Self::new(
580            self.x * factor,
581            self.y * factor,
582            self.width * factor,
583            self.height * factor,
584        )
585    }
586
587    /// Scales the rectangle by `factor` **about `pivot`**, which stays fixed:
588    /// `pos' = pivot + (pos - pivot) * factor`, with size × `factor`.
589    pub fn scale_about(self, pivot: Point, factor: f32) -> Self {
590        self.scale_about_xy(pivot, factor, factor)
591    }
592
593    /// Scales the rectangle **per axis** about `pivot`.
594    pub fn scale_about_xy(self, pivot: Point, sx: f32, sy: f32) -> Self {
595        Self::new(
596            pivot.x + (self.x - pivot.x) * sx,
597            pivot.y + (self.y - pivot.y) * sy,
598            self.width * sx,
599            self.height * sy,
600        )
601    }
602
603    /// Multiplies position and size **per axis** (`sx`, `sy`).
604    pub fn scale_xy(self, sx: f32, sy: f32) -> Self {
605        Self::new(self.x * sx, self.y * sy, self.width * sx, self.height * sy)
606    }
607
608    /// The intersection of two rectangles; zero-sized when they are disjoint.
609    pub fn intersect(self, other: Rect) -> Self {
610        let x0 = self.x.max(other.x);
611        let y0 = self.y.max(other.y);
612        let x1 = (self.x + self.width).min(other.x + other.width);
613        let y1 = (self.y + self.height).min(other.y + other.height);
614        Self::new(x0, y0, (x1 - x0).max(0.0), (y1 - y0).max(0.0))
615    }
616
617    /// The smallest rectangle enclosing both `self` **and** `other`.
618    pub fn union(self, other: Rect) -> Self {
619        let x0 = self.x.min(other.x);
620        let y0 = self.y.min(other.y);
621        let x1 = (self.x + self.width).max(other.x + other.width);
622        let y1 = (self.y + self.height).max(other.y + other.height);
623        Self::new(x0, y0, x1 - x0, y1 - y0)
624    }
625
626    /// `true` when `point` lies inside; the left and top edges count as inside.
627    pub fn contains(self, point: Point) -> bool {
628        point.x >= self.x
629            && point.x < self.x + self.width
630            && point.y >= self.y
631            && point.y < self.y + self.height
632    }
633
634    /// Builds a rectangle from an origin point and a size.
635    pub const fn from_point_size(origin: Point, size: Size) -> Self {
636        Self::new(origin.x, origin.y, size.width, size.height)
637    }
638
639    /// The top-left corner.
640    pub const fn origin(self) -> Point {
641        Point::new(self.x, self.y)
642    }
643
644    /// The rectangle's size.
645    pub const fn size(self) -> Size {
646        Size::new(self.width, self.height)
647    }
648
649    /// The `[x, y, width, height]` form, ready for the GPU.
650    pub const fn to_array(self) -> [f32; 4] {
651        [self.x, self.y, self.width, self.height]
652    }
653}
654
655/// **The rule for anything that measures itself**: report a *whole* number of pixels,
656/// rounded **up**.
657///
658/// The layout engine rounds every box to whole pixels, and it is right to: two boxes that
659/// abut on a fractional edge each anti-alias into the pixel they share, and what shows
660/// through between them is the background — a bright hairline seam, not a soft join.
661/// Milestone 496's measurement of it is `(136, 187, 136)` where red met blue over green.
662///
663/// The price of that rounding is this rule. A widget that measures itself at `146.4` and
664/// says so is given a box of `146`, and `146 < 146.4` — so at paint time, where it is
665/// measured again, it no longer fits and wraps, clips or overflows. That is milestone
666/// 289, and it presented as a mysteriously missing line of text rather than as
667/// arithmetic.
668///
669/// So a measurement goes through here, and the box it is rounded to is one it still fits
670/// in. Where the measurement is then clamped to a constraint, clamp **after**: a box a
671/// fraction wider than it was allowed is a different bug.
672///
673/// ```
674/// use frus_core::fits;
675/// assert_eq!(fits(146.4), 147.0);
676/// assert_eq!(fits(146.0), 146.0);
677/// ```
678pub fn fits(extent: f32) -> f32 {
679    // `ceil` and not `round`: rounding to nearest is the bug this exists to prevent, and
680    // it is the one a new measurer will write by reflex.
681    extent.ceil()
682}
683
684#[cfg(test)]
685mod tests {
686    use super::*;
687
688    #[test]
689    fn affine_composes_scale_then_rotate_about_a_pivot() {
690        use std::f32::consts::FRAC_PI_2;
691        let pivot = Point::new(10.0, 10.0);
692        // Scale ×2 then rotate +90°, both about the pivot.
693        let m = Affine::scale(2.0, 2.0)
694            .about(pivot)
695            .then(Affine::rotation(FRAC_PI_2).about(pivot));
696        // The pivot stays put.
697        let c = m.apply(pivot);
698        assert!(
699            (c.x - 10.0).abs() < 1e-3 && (c.y - 10.0).abs() < 1e-3,
700            "pivot fixe : {c:?}"
701        );
702        // Linear part = rotation(90°) ∘ scale(2) = [0, 2, -2, 0].
703        assert!(m.m[0].abs() < 1e-3 && (m.m[1] - 2.0).abs() < 1e-3);
704        assert!((m.m[2] + 2.0).abs() < 1e-3 && m.m[3].abs() < 1e-3);
705    }
706
707    #[test]
708    fn affine_inverse_round_trips() {
709        use std::f32::consts::FRAC_PI_3;
710        let m = Affine::scale(1.5, 0.5)
711            .about(Point::new(4.0, 7.0))
712            .then(Affine::rotation(FRAC_PI_3).about(Point::new(2.0, 3.0)))
713            .then(Affine::translation(5.0, -2.0));
714        let inv = m.inverse();
715        for p in [
716            Point::new(0.0, 0.0),
717            Point::new(12.0, -3.0),
718            Point::new(-5.0, 8.0),
719        ] {
720            let back = inv.apply(m.apply(p));
721            assert!(
722                (back.x - p.x).abs() < 1e-2 && (back.y - p.y).abs() < 1e-2,
723                "{p:?} -> {back:?}"
724            );
725        }
726    }
727
728    #[test]
729    fn directional_alignment_resolves_by_direction() {
730        // start = the reading edge: left in LTR, right in RTL.
731        let start = AlignmentDirectional::CENTER_START;
732        assert_eq!(start.resolve(TextDirection::Ltr), Alignment::CENTER_LEFT);
733        assert_eq!(start.resolve(TextDirection::Rtl), Alignment::CENTER_RIGHT);
734        // The centre and the vertical axis do not depend on direction.
735        let top = AlignmentDirectional::TOP_CENTER;
736        assert_eq!(top.resolve(TextDirection::Rtl), Alignment::TOP_CENTER);
737    }
738
739    #[test]
740    fn alignment_geometry_unifies_physical_and_directional() {
741        // Physical: resolved as-is, whatever the direction.
742        let phys: AlignmentGeometry = Alignment::CENTER_LEFT.into();
743        assert_eq!(phys.resolve(TextDirection::Rtl), Alignment::CENTER_LEFT);
744        // Directional: follows the reading direction.
745        let dir: AlignmentGeometry = AlignmentDirectional::CENTER_START.into();
746        assert_eq!(dir.resolve(TextDirection::Ltr), Alignment::CENTER_LEFT);
747        assert_eq!(dir.resolve(TextDirection::Rtl), Alignment::CENTER_RIGHT);
748    }
749
750    #[test]
751    fn directional_insets_flip_start_end() {
752        // start=10, end=4: in LTR start is left; in RTL start is right.
753        let d = InsetsDirectional::new(1.0, 4.0, 2.0, 10.0);
754        let ltr = d.resolve(TextDirection::Ltr);
755        assert_eq!(
756            (ltr.left, ltr.right, ltr.top, ltr.bottom),
757            (10.0, 4.0, 1.0, 2.0)
758        );
759        let rtl = d.resolve(TextDirection::Rtl);
760        assert_eq!(
761            (rtl.left, rtl.right),
762            (4.0, 10.0),
763            "start moves to the right under RTL"
764        );
765        // The vertical axis never moves.
766        assert_eq!((rtl.top, rtl.bottom), (1.0, 2.0));
767    }
768
769    #[test]
770    fn window_insets_split_and_safe_area() {
771        // Keyboard-free reference: the top and bottom system bars.
772        let baseline = Insets::new(84.0, 0.0, 45.0, 0.0);
773
774        // Keyboard closed: everything is static padding, nothing transient, and the
775        // intrusion that does not move is the same as the one that does.
776        let closed = WindowInsets::from_baseline(baseline, baseline);
777        assert_eq!(closed.padding, baseline);
778        assert_eq!(closed.view_padding, baseline);
779        assert_eq!(closed.view_insets, Insets::ZERO);
780        assert_eq!(closed.safe(), baseline);
781
782        // Keyboard open (bottom excess of 300): `view_insets.bottom` measures the
783        // total occlusion from the edge (345, bar included).
784        let open = WindowInsets::from_baseline(baseline, Insets::new(84.0, 0.0, 345.0, 0.0));
785        assert_eq!(open.view_insets, Insets::new(0.0, 0.0, 345.0, 0.0));
786        // The bar has not moved, so `view_padding` still reports it — while `padding`
787        // reports what is **left** to avoid, which at the bottom is nothing at all. The
788        // keyboard is over the bar; padding by both would leave a strip of nothing
789        // between the content and the keys.
790        assert_eq!(open.view_padding, baseline);
791        assert_eq!(open.padding, Insets::new(84.0, 0.0, 0.0, 0.0));
792        // The safe area is the per-side max — the keyboard covers the bar.
793        assert_eq!(open.safe(), Insets::new(84.0, 0.0, 345.0, 0.0));
794
795        // Current bottom BELOW the reference (bars hidden): no negative keyboard.
796        let hidden = WindowInsets::from_baseline(baseline, Insets::new(84.0, 0.0, 10.0, 0.0));
797        assert_eq!(hidden.view_insets, Insets::ZERO);
798        assert_eq!(hidden.padding.bottom, 10.0);
799        assert_eq!(hidden.view_padding.bottom, 10.0);
800    }
801
802    #[test]
803    fn rect_from_point_size_roundtrips() {
804        let r = Rect::from_point_size(Point::new(3.0, 4.0), Size::new(10.0, 20.0));
805        assert_eq!(r, Rect::new(3.0, 4.0, 10.0, 20.0));
806        assert_eq!(r.origin(), Point::new(3.0, 4.0));
807        assert_eq!(r.size(), Size::new(10.0, 20.0));
808        assert_eq!(r.to_array(), [3.0, 4.0, 10.0, 20.0]);
809    }
810}