ailloli_ui_core/geometry/
clip_shape.rs

1//! Logical clip regions (GPU strategy is chosen in `ailloli_ui_render_wgpu`).
2
3use super::Rect;
4
5/// Axis-aligned or rounded clip region in local coordinates.
6///
7/// Possible values are [`ClipShape::Rect`] and [`ClipShape::RoundRect`].
8///
9/// # Examples
10///
11/// ```
12/// use ailloli_ui_core::{ClipShape, Rect};
13/// assert!(ClipShape::rect(Rect::new(0.0, 0.0, 10.0, 10.0)).contains_point(5.0, 5.0));
14/// ```
15#[derive(Debug, Clone, Copy, PartialEq)]
16pub enum ClipShape {
17    /// Axis-aligned rectangle.
18    Rect(Rect),
19    /// Rounded rectangle with uniform corner radius.
20    RoundRect {
21        /// Axis-aligned outer bounds in local logical pixels.
22        rect: Rect,
23        /// Requested uniform radius in logical pixels.
24        ///
25        /// Hit-testing clamps this to a non-negative half-extent, but the stored
26        /// value itself is not normalized.
27        radius: f32,
28    },
29}
30
31impl ClipShape {
32    /// Creates a rectangular clip.
33    ///
34    /// # Examples
35    ///
36    /// ```
37    /// use ailloli_ui_core::{ClipShape, Rect};
38    /// let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
39    /// assert_eq!(ClipShape::rect(rect).bounding_rect(), rect);
40    /// ```
41    pub fn rect(r: Rect) -> Self {
42        Self::Rect(r)
43    }
44
45    /// Creates a rounded-rect clip without normalizing its radius or bounds.
46    ///
47    /// # Examples
48    ///
49    /// ```
50    /// use ailloli_ui_core::{ClipShape, Rect};
51    /// assert!(matches!(ClipShape::round_rect(Rect::new(0.0, 0.0, 10.0, 10.0), 2.0), ClipShape::RoundRect { .. }));
52    /// ```
53    pub fn round_rect(rect: Rect, radius: f32) -> Self {
54        Self::RoundRect { rect, radius }
55    }
56
57    /// Tight axis-aligned bounds of this clip (ignores corner cut-outs for `RoundRect`).
58    ///
59    /// # Examples
60    ///
61    /// ```
62    /// use ailloli_ui_core::{ClipShape, Rect};
63    /// let rect = Rect::new(1.0, 2.0, 3.0, 4.0);
64    /// assert_eq!(ClipShape::round_rect(rect, 2.0).bounding_rect(), rect);
65    /// ```
66    pub fn bounding_rect(&self) -> Rect {
67        match self {
68            Self::Rect(r) | Self::RoundRect { rect: r, .. } => *r,
69        }
70    }
71
72    /// Hit-test in **local** clip space (same coordinate system as `rect`).
73    ///
74    /// # Examples
75    ///
76    /// ```
77    /// use ailloli_ui_core::{ClipShape, Rect};
78    /// assert!(!ClipShape::round_rect(Rect::new(0.0, 0.0, 10.0, 10.0), 5.0).contains_point(0.0, 0.0));
79    /// ```
80    pub fn contains_point(&self, px: f32, py: f32) -> bool {
81        match self {
82            Self::Rect(r) => r.contains(px, py),
83            Self::RoundRect { rect, radius } => round_rect_contains(*rect, *radius, px, py),
84        }
85    }
86
87    /// Returns a conservative rectangular intersection of two clips.
88    ///
89    /// A rounded result keeps the rounded operand's radius, or the minimum
90    /// radius when both operands are rounded. This is not an exact geometric
91    /// intersection of corner arcs; it is a compact clip approximation. Zero-
92    /// area edge contact returns `None` through [`Rect::intersection`].
93    ///
94    /// # Examples
95    ///
96    /// ```
97    /// use ailloli_ui_core::{ClipShape, Rect};
98    /// let a = ClipShape::rect(Rect::new(0.0, 0.0, 10.0, 10.0));
99    /// let b = ClipShape::rect(Rect::new(5.0, 5.0, 10.0, 10.0));
100    /// assert_eq!(a.intersect(&b).unwrap().bounding_rect(), Rect::new(5.0, 5.0, 5.0, 5.0));
101    /// ```
102    pub fn intersect(&self, other: &Self) -> Option<Self> {
103        match (self, other) {
104            (Self::Rect(a), Self::Rect(b)) => a.intersection(*b).map(Self::Rect),
105            (Self::Rect(a), Self::RoundRect { rect: b, radius }) => {
106                a.intersection(*b).map(|r| Self::RoundRect {
107                    rect: r,
108                    radius: *radius,
109                })
110            }
111            (Self::RoundRect { rect: a, radius }, Self::Rect(b)) => {
112                a.intersection(*b).map(|r| Self::RoundRect {
113                    rect: r,
114                    radius: *radius,
115                })
116            }
117            (
118                Self::RoundRect {
119                    rect: a,
120                    radius: ra,
121                },
122                Self::RoundRect {
123                    rect: b,
124                    radius: rb,
125                },
126            ) => {
127                let r = a.intersection(*b)?;
128                Some(Self::RoundRect {
129                    rect: r,
130                    radius: ra.min(*rb),
131                })
132            }
133        }
134    }
135}
136
137/// Tests inclusive rounded bounds after clamping radius to the rectangle extents.
138fn round_rect_contains(rect: Rect, radius: f32, px: f32, py: f32) -> bool {
139    if px < rect.x || py < rect.y || px > rect.right() || py > rect.bottom() {
140        return false;
141    }
142    let r = radius.max(0.0).min(rect.w * 0.5).min(rect.h * 0.5);
143    if r <= 0.0 {
144        return true;
145    }
146    let right = rect.right();
147    let bottom = rect.bottom();
148    if px < rect.x + r && py < rect.y + r {
149        let cx = rect.x + r;
150        let cy = rect.y + r;
151        let dx = px - cx;
152        let dy = py - cy;
153        return dx * dx + dy * dy <= r * r;
154    }
155    if px > right - r && py < rect.y + r {
156        let cx = right - r;
157        let cy = rect.y + r;
158        let dx = px - cx;
159        let dy = py - cy;
160        return dx * dx + dy * dy <= r * r;
161    }
162    if px < rect.x + r && py > bottom - r {
163        let cx = rect.x + r;
164        let cy = bottom - r;
165        let dx = px - cx;
166        let dy = py - cy;
167        return dx * dx + dy * dy <= r * r;
168    }
169    if px > right - r && py > bottom - r {
170        let cx = right - r;
171        let cy = bottom - r;
172        let dx = px - cx;
173        let dy = py - cy;
174        return dx * dx + dy * dy <= r * r;
175    }
176    true
177}
178
179#[cfg(test)]
180mod tests {
181    //! Covers rectangular bounds and rounded-corner exclusion.
182
183    use super::*;
184
185    #[test]
186    fn rect_contains() {
187        let r = Rect::new(0.0, 0.0, 10.0, 10.0);
188        let c = ClipShape::Rect(r);
189        assert!(c.contains_point(5.0, 5.0));
190        assert!(!c.contains_point(11.0, 5.0));
191    }
192
193    #[test]
194    fn round_rect_excludes_corner() {
195        let r = Rect::new(0.0, 0.0, 100.0, 100.0);
196        let c = ClipShape::RoundRect {
197            rect: r,
198            radius: 20.0,
199        };
200        assert!(c.contains_point(50.0, 50.0));
201        assert!(!c.contains_point(2.0, 2.0));
202        assert!(c.contains_point(15.0, 15.0));
203    }
204}