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}