Skip to main content

incular_core/
geometry.rs

1//! Small renderer-neutral geometry and paint values.
2
3use std::ops::{Add, Sub};
4
5use palette::{FromColor, Hsl, Hsv, LinSrgba, Srgba, WithAlpha};
6
7pub use kurbo::Affine;
8use kurbo::{Point, Vec2};
9
10#[derive(Clone, Copy, Debug, Default, PartialEq)]
11pub struct Size {
12    pub width: f32,
13    pub height: f32,
14}
15
16impl Size {
17    pub const ZERO: Self = Self {
18        width: 0.0,
19        height: 0.0,
20    };
21
22    #[must_use]
23    pub fn new(width: f32, height: f32) -> Self {
24        assert!(
25            width.is_finite() && height.is_finite() && width >= 0.0 && height >= 0.0,
26            "sizes must be finite and non-negative (an unbounded maximum is a constraint, not a size: use `Constraints::at_most` or `Constraints::new` with `f32::INFINITY`)"
27        );
28        Self { width, height }
29    }
30}
31
32#[derive(Clone, Copy, Debug, Default, PartialEq)]
33pub struct Offset {
34    pub x: f32,
35    pub y: f32,
36}
37
38impl Offset {
39    pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
40
41    #[must_use]
42    pub const fn new(x: f32, y: f32) -> Self {
43        Self { x, y }
44    }
45
46    /// Replaces non-finite components with zero.
47    #[must_use]
48    pub fn finite_or_zero(self) -> Self {
49        Self::new(finite_or_zero(self.x), finite_or_zero(self.y))
50    }
51}
52
53/// Replaces NaN and infinities with zero so untrusted geometry cannot poison
54/// layout or rendering.
55#[inline]
56#[must_use]
57pub fn finite_or_zero(value: f32) -> f32 {
58    if value.is_finite() { value } else { 0.0 }
59}
60
61/// [`finite_or_zero`] clamped to be non-negative, for extents, insets and radii.
62#[inline]
63#[must_use]
64pub fn finite_non_negative(value: f32) -> f32 {
65    finite_or_zero(value).max(0.0)
66}
67
68impl Add for Offset {
69    type Output = Self;
70
71    fn add(self, rhs: Self) -> Self {
72        Self::new(self.x + rhs.x, self.y + rhs.y)
73    }
74}
75
76impl Sub for Offset {
77    type Output = Self;
78
79    fn sub(self, rhs: Self) -> Self {
80        Self::new(self.x - rhs.x, self.y - rhs.y)
81    }
82}
83
84#[derive(Clone, Copy, Debug, Default, PartialEq)]
85pub struct Rect {
86    pub origin: Offset,
87    pub size: Size,
88}
89
90impl Rect {
91    #[must_use]
92    pub fn from_origin_size(origin: Offset, size: Size) -> Self {
93        Self { origin, size }
94    }
95
96    #[must_use]
97    pub fn contains(self, point: Offset) -> bool {
98        point.x >= self.origin.x
99            && point.x <= self.origin.x + self.size.width
100            && point.y >= self.origin.y
101            && point.y <= self.origin.y + self.size.height
102    }
103
104    #[must_use]
105    pub fn intersects(self, other: Self) -> bool {
106        self.origin.x < other.origin.x + other.size.width
107            && other.origin.x < self.origin.x + self.size.width
108            && self.origin.y < other.origin.y + other.size.height
109            && other.origin.y < self.origin.y + self.size.height
110    }
111
112    #[must_use]
113    pub fn intersection(self, other: Self) -> Option<Self> {
114        let left = self.origin.x.max(other.origin.x);
115        let top = self.origin.y.max(other.origin.y);
116        let right = (self.origin.x + self.size.width).min(other.origin.x + other.size.width);
117        let bottom = (self.origin.y + self.size.height).min(other.origin.y + other.size.height);
118        (right >= left && bottom >= top).then(|| {
119            Self::from_origin_size(
120                Offset::new(left, top),
121                Size::new(right - left, bottom - top),
122            )
123        })
124    }
125}
126
127#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
128pub struct Color {
129    pub red: u8,
130    pub green: u8,
131    pub blue: u8,
132    pub alpha: u8,
133}
134
135impl Color {
136    pub const TRANSPARENT: Self = Self::rgba(0, 0, 0, 0);
137    pub const WHITE: Self = Self::rgba(255, 255, 255, 255);
138    pub const BLACK: Self = Self::rgba(0, 0, 0, 255);
139
140    #[must_use]
141    pub const fn rgba(red: u8, green: u8, blue: u8, alpha: u8) -> Self {
142        Self {
143            red,
144            green,
145            blue,
146            alpha,
147        }
148    }
149
150    /// Converts the compact encoded sRGBA8 value to straight, linear-sRGB
151    /// components. Palette owns the CPU transfer function; WGSL retains its
152    /// equivalent explicit shader-side math at the GPU boundary.
153    #[must_use]
154    pub fn to_linear_rgba(self) -> [f32; 4] {
155        let linear = Srgba::new(
156            self.red as f32 / 255.0,
157            self.green as f32 / 255.0,
158            self.blue as f32 / 255.0,
159            self.alpha as f32 / 255.0,
160        )
161        .into_linear();
162        [
163            linear.color.red,
164            linear.color.green,
165            linear.color.blue,
166            linear.alpha,
167        ]
168    }
169
170    /// Converts straight linear-sRGB components into compact encoded sRGBA8.
171    /// Non-finite and out-of-range components are sanitized to `0.0..=1.0`.
172    /// Palette owns the transfer function in both directions so CPU color-space
173    /// boundaries cannot drift from [`Self::to_linear_rgba`].
174    #[must_use]
175    pub fn from_linear_rgba([red, green, blue, alpha]: [f32; 4]) -> Self {
176        let encoded = Srgba::<u8>::from_linear(LinSrgba::new(
177            unit(red),
178            unit(green),
179            unit(blue),
180            unit(alpha),
181        ));
182        Self::rgba(
183            encoded.color.red,
184            encoded.color.green,
185            encoded.color.blue,
186            encoded.alpha,
187        )
188    }
189
190    /// Converts this sRGB color to the HSL representation used by style and
191    /// theme code. Hue is expressed in degrees and alpha in `0.0..=1.0`.
192    #[must_use]
193    pub fn to_hsl(self) -> HslColor {
194        HslColor::from_color(self)
195    }
196
197    /// Converts this sRGB color to HSV. Hue is expressed in degrees and alpha
198    /// in `0.0..=1.0`.
199    #[must_use]
200    pub fn to_hsv(self) -> HsvColor {
201        HsvColor::from_color(self)
202    }
203}
204
205/// Hue, saturation, lightness, and opacity in an sRGB color space.
206///
207/// Hue is normalized to `0.0..360.0`; saturation, lightness, and alpha are
208/// normalized to `0.0..=1.0`. Constructors accept non-finite values but
209/// sanitize them, keeping configuration values safe to use in rendering.
210#[derive(Clone, Copy, Debug, PartialEq)]
211pub struct HslColor {
212    pub hue: f32,
213    pub saturation: f32,
214    pub lightness: f32,
215    pub alpha: f32,
216}
217
218impl HslColor {
219    #[must_use]
220    pub fn new(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Self {
221        Self {
222            hue: normalize_hue(hue),
223            saturation: unit(saturation),
224            lightness: unit(lightness),
225            alpha: unit(alpha),
226        }
227    }
228
229    #[must_use]
230    pub fn from_color(color: Color) -> Self {
231        let source = palette_color(color);
232        let hsl = Hsl::from_color(source.color);
233        Self::new(
234            hsl.hue.into_degrees(),
235            hsl.saturation,
236            hsl.lightness,
237            source.alpha,
238        )
239    }
240
241    #[must_use]
242    pub fn to_color(self) -> Color {
243        let hsl = Hsl::new_srgb(
244            normalize_hue(self.hue),
245            unit(self.saturation),
246            unit(self.lightness),
247        );
248        color_from_palette(Srgba::from_color(hsl).with_alpha(unit(self.alpha)))
249    }
250}
251
252/// Hue, saturation, value, and opacity in an sRGB color space.
253#[derive(Clone, Copy, Debug, PartialEq)]
254pub struct HsvColor {
255    pub hue: f32,
256    pub saturation: f32,
257    pub value: f32,
258    pub alpha: f32,
259}
260
261impl HsvColor {
262    #[must_use]
263    pub fn new(hue: f32, saturation: f32, value: f32, alpha: f32) -> Self {
264        Self {
265            hue: normalize_hue(hue),
266            saturation: unit(saturation),
267            value: unit(value),
268            alpha: unit(alpha),
269        }
270    }
271
272    #[must_use]
273    pub fn from_color(color: Color) -> Self {
274        let source = palette_color(color);
275        let hsv = Hsv::from_color(source.color);
276        Self::new(
277            hsv.hue.into_degrees(),
278            hsv.saturation,
279            hsv.value,
280            source.alpha,
281        )
282    }
283
284    #[must_use]
285    pub fn to_color(self) -> Color {
286        let hsv = Hsv::new_srgb(
287            normalize_hue(self.hue),
288            unit(self.saturation),
289            unit(self.value),
290        );
291        color_from_palette(Srgba::from_color(hsv).with_alpha(unit(self.alpha)))
292    }
293}
294
295fn unit(value: f32) -> f32 {
296    if value.is_finite() {
297        value.clamp(0.0, 1.0)
298    } else {
299        0.0
300    }
301}
302
303fn normalize_hue(hue: f32) -> f32 {
304    if hue.is_finite() {
305        hue.rem_euclid(360.0)
306    } else {
307        0.0
308    }
309}
310
311fn palette_color(color: Color) -> Srgba {
312    Srgba::new(
313        color.red as f32 / 255.0,
314        color.green as f32 / 255.0,
315        color.blue as f32 / 255.0,
316        color.alpha as f32 / 255.0,
317    )
318}
319
320fn color_from_palette(color: Srgba) -> Color {
321    let byte = |value: f32| (unit(value) * 255.0).round() as u8;
322    Color::rgba(
323        byte(color.color.red),
324        byte(color.color.green),
325        byte(color.color.blue),
326        byte(color.alpha),
327    )
328}
329
330/// A retained two-dimensional affine transform.
331///
332/// Incular keeps its ordinary layout geometry in `f32`; Kurbo is the single
333/// authority for affine composition, inversion, and geometric bounds. The
334/// conversion happens only at this boundary, so applications do not need to
335/// adopt a second basic point/rectangle vocabulary for normal widget code.
336#[derive(Clone, Copy, Debug, PartialEq)]
337pub struct Transform(Affine);
338
339impl Default for Transform {
340    fn default() -> Self {
341        Self::IDENTITY
342    }
343}
344
345impl Transform {
346    pub const IDENTITY: Self = Self(Affine::IDENTITY);
347
348    #[must_use]
349    pub fn translation(offset: Offset) -> Self {
350        Self(Affine::translate(Vec2::new(
351            f64::from(offset.x),
352            f64::from(offset.y),
353        )))
354    }
355
356    #[must_use]
357    pub const fn scale(scale: f32) -> Self {
358        Self(Affine::scale(scale as f64))
359    }
360
361    #[must_use]
362    pub const fn scale_non_uniform(x: f32, y: f32) -> Self {
363        Self(Affine::scale_non_uniform(x as f64, y as f64))
364    }
365
366    /// Rotates clockwise in Incular's y-down coordinate system.
367    #[must_use]
368    pub fn rotation(radians: f32) -> Self {
369        Self(Affine::rotate(f64::from(radians)))
370    }
371
372    #[must_use]
373    pub const fn skew(x: f32, y: f32) -> Self {
374        Self(Affine::skew(x as f64, y as f64))
375    }
376
377    /// The affine `[a, b, c, d, e, f]` mapping `(x, y)` to
378    /// `(a·x + c·y + e, b·x + d·y + f)`: a 2D `Matrix4` given by the first
379    /// two entries of its first, second and fourth columns.
380    #[must_use]
381    pub const fn from_coefficients(coefficients: [f64; 6]) -> Self {
382        Self(Affine::new(coefficients))
383    }
384
385    /// The `[a, b, c, d, e, f]` coefficients [`Self::from_coefficients`]
386    /// takes.
387    #[must_use]
388    pub fn coefficients(self) -> [f64; 6] {
389        self.0.as_coeffs()
390    }
391
392    #[must_use]
393    pub const fn from_kurbo(affine: Affine) -> Self {
394        Self(affine)
395    }
396
397    #[must_use]
398    pub const fn to_kurbo(self) -> Affine {
399        self.0
400    }
401
402    /// Applies `other` first, then this transform.
403    #[must_use]
404    pub fn then(self, other: Self) -> Self {
405        Self(self.0 * other.0)
406    }
407
408    #[must_use]
409    pub fn inverse(self) -> Option<Self> {
410        let inverse = self.0.inverse();
411        inverse.is_finite().then_some(Self(inverse))
412    }
413
414    #[must_use]
415    pub fn inverse_translation(self) -> Self {
416        self.inverse().unwrap_or(Self::IDENTITY)
417    }
418
419    #[must_use]
420    pub fn translation_offset(self) -> Offset {
421        let coefficients = self.0.as_coeffs();
422        Offset::new(coefficients[4] as f32, coefficients[5] as f32)
423    }
424
425    #[must_use]
426    pub fn is_translation(self) -> bool {
427        let [a, b, c, d, _, _] = self.0.as_coeffs();
428        a == 1. && b == 0. && c == 0. && d == 1.
429    }
430
431    /// Conservative bound on the linear part's operator norm: no unit
432    /// vector stretches more than this. It is the Frobenius norm, which
433    /// dominates the operator norm, rounded up to a representable value
434    /// so the bound never underestimates through float conversion.
435    /// Unlike largest-column norms it also covers shear, where diagonal
436    /// directions stretch more than either basis vector.
437    ///
438    /// Degenerate zero maps report `0.0`. Non-finite inputs report
439    /// infinity, consistent with [`Self::inverse`] failing on them:
440    /// downstream geometry is culled rather than measured.
441    #[must_use]
442    pub fn magnification_bound(self) -> f32 {
443        let [a, b, c, d, _, _] = self.0.as_coeffs();
444        let bound = a.hypot(b).hypot(c.hypot(d));
445        if !bound.is_finite() {
446            f32::INFINITY
447        } else if bound == 0.0 {
448            // All-zero linear part stretches nothing exactly; rounding up
449            // a true zero would only whitelist nothing.
450            0.0
451        } else {
452            (bound as f32).next_up()
453        }
454    }
455
456    #[must_use]
457    pub fn transform_point(self, point: Offset) -> Offset {
458        let point = self.0 * Point::new(f64::from(point.x), f64::from(point.y));
459        Offset::new(point.x as f32, point.y as f32)
460    }
461
462    #[must_use]
463    pub fn inverse_transform_point(self, point: Offset) -> Option<Offset> {
464        self.inverse().map(|inverse| inverse.transform_point(point))
465    }
466
467    #[must_use]
468    pub fn transform_rect_bbox(self, rect: Rect) -> Rect {
469        let rect = kurbo::Rect::new(
470            f64::from(rect.origin.x),
471            f64::from(rect.origin.y),
472            f64::from(rect.origin.x + rect.size.width),
473            f64::from(rect.origin.y + rect.size.height),
474        );
475        let rect = self.0.transform_rect_bbox(rect);
476        Rect::from_origin_size(
477            Offset::new(rect.x0 as f32, rect.y0 as f32),
478            Size::new((rect.x1 - rect.x0) as f32, (rect.y1 - rect.y0) as f32),
479        )
480    }
481}
482
483bitflags::bitflags! {
484    /// Multi-dimensional damage invalidation representation across independent retained runtime phases.
485    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
486    pub struct Invalidation: u16 {
487        /// No invalidation required.
488        const NONE = 0;
489        /// Subtree widget build / structural reconciliation required.
490        const BUILD = 1 << 0;
491        /// Layout measurement and positioning recalculation required.
492        const LAYOUT = 1 << 1;
493        /// Picture display list re-recording required.
494        const PAINT = 1 << 2;
495        /// Retained compositor layer properties or transforms modified.
496        const COMPOSITE = 1 << 3;
497        /// Accessibility semantics tree update required.
498        const SEMANTICS = 1 << 4;
499        /// Hit-test routing / pointer target cache update required.
500        const HIT_TEST = 1 << 5;
501    }
502}
503
504/// Alias for [`Invalidation`] for backwards compatibility.
505pub type DirtyFlags = Invalidation;
506
507/// A type that can be linearly interpolated between two values.
508pub trait Lerp {
509    /// Linearly interpolates between `self` and `other` with parameter `t` in `[0.0, 1.0]`.
510    #[must_use]
511    fn lerp(&self, other: &Self, t: f32) -> Self;
512}
513
514impl Lerp for f32 {
515    fn lerp(&self, other: &Self, t: f32) -> Self {
516        self + (other - self) * t
517    }
518}
519
520impl Lerp for f64 {
521    fn lerp(&self, other: &Self, t: f32) -> Self {
522        self + (other - self) * f64::from(t)
523    }
524}
525
526/// Pairs interpolate component-wise, so one implicit animation can carry
527/// values that change together.
528impl<A: Lerp, B: Lerp> Lerp for (A, B) {
529    fn lerp(&self, other: &Self, t: f32) -> Self {
530        (self.0.lerp(&other.0, t), self.1.lerp(&other.1, t))
531    }
532}
533
534impl Lerp for Offset {
535    fn lerp(&self, other: &Self, t: f32) -> Self {
536        Self {
537            x: self.x.lerp(&other.x, t),
538            y: self.y.lerp(&other.y, t),
539        }
540    }
541}
542
543impl Lerp for Size {
544    fn lerp(&self, other: &Self, t: f32) -> Self {
545        Self {
546            width: self.width.lerp(&other.width, t).max(0.0),
547            height: self.height.lerp(&other.height, t).max(0.0),
548        }
549    }
550}
551
552/// Flutter's `Matrix4Tween.lerp`: both matrices are decomposed into
553/// translation, rotation quaternion and scale (vector_math's
554/// `Matrix4.decompose`); translation and scale interpolate linearly and the
555/// rotation by a normalized quaternion lerp, then the parts recompose. Skew is
556/// lost, as in Flutter.
557impl Lerp for Transform {
558    fn lerp(&self, other: &Self, t: f32) -> Self {
559        let t = f64::from(t);
560        let (begin_translation, begin_rotation, begin_scale) = decompose(self.0);
561        let (end_translation, end_rotation, end_scale) = decompose(other.0);
562        let mix = |a: f64, b: f64| a * (1.0 - t) + b * t;
563        let translation = [
564            mix(begin_translation[0], end_translation[0]),
565            mix(begin_translation[1], end_translation[1]),
566        ];
567        let mut rotation = [0.0; 4];
568        for (index, value) in rotation.iter_mut().enumerate() {
569            *value = mix(begin_rotation[index], end_rotation[index]);
570        }
571        let length = rotation
572            .iter()
573            .map(|value| value * value)
574            .sum::<f64>()
575            .sqrt();
576        if length != 0.0 {
577            for value in &mut rotation {
578                *value /= length;
579            }
580        }
581        let scale = [
582            mix(begin_scale[0], end_scale[0]),
583            mix(begin_scale[1], end_scale[1]),
584        ];
585        Self(compose(translation, rotation, scale))
586    }
587}
588
589/// vector_math's `Matrix4.decompose` for a 2D affine embedded in a 4x4
590/// matrix: translation, an `[x, y, z, w]` quaternion, and the x/y scale.
591fn decompose(affine: Affine) -> ([f64; 2], [f64; 4], [f64; 2]) {
592    let [a, b, c, d, e, f] = affine.as_coeffs();
593    let mut sx = a.hypot(b);
594    let sy = c.hypot(d);
595    if a * d - b * c < 0.0 {
596        sx = -sx;
597    }
598    // The normalized 3x3 rotation block, `m[row][column]`; the z column and
599    // row are the identity's.
600    let m = [
601        [a / sx, c / sy, 0.0],
602        [b / sx, d / sy, 0.0],
603        [0.0, 0.0, 1.0],
604    ];
605    ([e, f], quaternion_from_rotation(m), [sx, sy])
606}
607
608/// vector_math's `Quaternion.setFromRotation`.
609fn quaternion_from_rotation(m: [[f64; 3]; 3]) -> [f64; 4] {
610    let mut q = [0.0; 4];
611    let trace = m[0][0] + m[1][1] + m[2][2];
612    if trace > 0.0 {
613        let mut s = (trace + 1.0).sqrt();
614        q[3] = s * 0.5;
615        s = 0.5 / s;
616        q[0] = (m[2][1] - m[1][2]) * s;
617        q[1] = (m[0][2] - m[2][0]) * s;
618        q[2] = (m[1][0] - m[0][1]) * s;
619    } else {
620        let i = if m[0][0] < m[1][1] {
621            if m[1][1] < m[2][2] { 2 } else { 1 }
622        } else if m[0][0] < m[2][2] {
623            2
624        } else {
625            0
626        };
627        let j = (i + 1) % 3;
628        let k = (i + 2) % 3;
629        let mut s = (m[i][i] - m[j][j] - m[k][k] + 1.0).sqrt();
630        q[i] = s * 0.5;
631        s = 0.5 / s;
632        q[3] = (m[k][j] - m[j][k]) * s;
633        q[j] = (m[j][i] + m[i][j]) * s;
634        q[k] = (m[k][i] + m[i][k]) * s;
635    }
636    q
637}
638
639/// vector_math's `Matrix4.compose`, projected back onto the 2D affine.
640fn compose(translation: [f64; 2], rotation: [f64; 4], scale: [f64; 2]) -> Affine {
641    let [x, y, z, w] = rotation;
642    let (x2, y2, z2) = (x + x, y + y, z + z);
643    let (xx, xy) = (x * x2, x * y2);
644    let (yy, zz) = (y * y2, z * z2);
645    let wz = w * z2;
646    Affine::new([
647        (1.0 - (yy + zz)) * scale[0],
648        (xy + wz) * scale[0],
649        (xy - wz) * scale[1],
650        (1.0 - (xx + zz)) * scale[1],
651        translation[0],
652        translation[1],
653    ])
654}
655
656impl Lerp for Rect {
657    fn lerp(&self, other: &Self, t: f32) -> Self {
658        Self {
659            origin: self.origin.lerp(&other.origin, t),
660            size: self.size.lerp(&other.size, t),
661        }
662    }
663}
664
665impl Lerp for Color {
666    fn lerp(&self, other: &Self, t: f32) -> Self {
667        let t = t.clamp(0.0, 1.0);
668        let mix = |a: u8, b: u8| -> u8 {
669            (f32::from(a) + (f32::from(b) - f32::from(a)) * t)
670                .round()
671                .clamp(0.0, 255.0) as u8
672        };
673        Self::rgba(
674            mix(self.red, other.red),
675            mix(self.green, other.green),
676            mix(self.blue, other.blue),
677            mix(self.alpha, other.alpha),
678        )
679    }
680}
681
682impl Lerp for HslColor {
683    fn lerp(&self, other: &Self, t: f32) -> Self {
684        let t = t.clamp(0.0, 1.0);
685        let mix = |a: f32, b: f32| a + (b - a) * t;
686        Self::new(
687            mix(self.hue, other.hue),
688            mix(self.saturation, other.saturation),
689            mix(self.lightness, other.lightness),
690            mix(self.alpha, other.alpha),
691        )
692    }
693}
694
695impl Lerp for HsvColor {
696    fn lerp(&self, other: &Self, t: f32) -> Self {
697        let t = t.clamp(0.0, 1.0);
698        let mix = |a: f32, b: f32| a + (b - a) * t;
699        Self::new(
700            mix(self.hue, other.hue),
701            mix(self.saturation, other.saturation),
702            mix(self.value, other.value),
703            mix(self.alpha, other.alpha),
704        )
705    }
706}
707
708/// Identifies the invalidation scope required when a property changes.
709#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
710pub enum ChangeImpact {
711    /// No visual or layout change.
712    #[default]
713    None,
714    /// Requires only compositor layer property / transform update.
715    Composite,
716    /// Requires painting display list regeneration without layout changes.
717    Paint,
718    /// Requires layout measurement and positioning recalculation.
719    Layout,
720    /// Requires rebuilding widget subtree.
721    Build,
722}