1use frus_core::{BorderRadius, ClipShape, Path, Rect, Scene};
5use frus_layout::Style;
6
7use crate::interaction::Status;
8use crate::theme::Theme;
9use crate::widget::Widget;
10
11pub struct ClipRRect<Msg = crate::callback::Callback> {
26 radius: BorderRadius,
27 children: Vec<Box<dyn Widget<Msg>>>,
28}
29
30impl<Msg> ClipRRect<Msg> {
31 pub fn new(radius: f32) -> Self {
34 Self::rounded(BorderRadius::uniform(radius))
35 }
36
37 pub fn rounded(radius: BorderRadius) -> Self {
39 Self {
40 radius,
41 children: Vec::new(),
42 }
43 }
44
45 pub fn child(mut self, child: impl Widget<Msg> + 'static) -> Self {
47 self.children.clear();
48 self.children.push(Box::new(child));
49 self
50 }
51}
52
53impl<Msg: Clone> Widget<Msg> for ClipRRect<Msg> {
54 fn style(&self) -> Style {
55 Style::default()
58 }
59
60 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
61 &self.children
62 }
63
64 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {
65 }
67
68 fn on_click(&self) -> Option<Msg> {
69 None
70 }
71
72 fn clip_shape(&self) -> Option<ClipShape> {
73 Some(ClipShape::RRect(self.radius.clamped()))
74 }
75}
76
77pub struct ClipOval<Msg = crate::callback::Callback> {
86 children: Vec<Box<dyn Widget<Msg>>>,
87}
88
89impl<Msg> ClipOval<Msg> {
90 pub fn new() -> Self {
92 Self {
93 children: Vec::new(),
94 }
95 }
96
97 pub fn child(mut self, child: impl Widget<Msg> + 'static) -> Self {
99 self.children.clear();
100 self.children.push(Box::new(child));
101 self
102 }
103}
104
105impl<Msg> Default for ClipOval<Msg> {
106 fn default() -> Self {
107 Self::new()
108 }
109}
110
111impl<Msg: Clone> Widget<Msg> for ClipOval<Msg> {
112 fn style(&self) -> Style {
113 Style::default()
114 }
115
116 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
117 &self.children
118 }
119
120 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
121
122 fn on_click(&self) -> Option<Msg> {
123 None
124 }
125
126 fn clip_shape(&self) -> Option<ClipShape> {
127 Some(ClipShape::Oval)
128 }
129}
130
131pub struct ClipPath<Msg = crate::callback::Callback> {
150 path: Path,
151 children: Vec<Box<dyn Widget<Msg>>>,
152}
153
154impl<Msg> ClipPath<Msg> {
155 pub fn new(path: Path) -> Self {
157 Self {
158 path,
159 children: Vec::new(),
160 }
161 }
162
163 pub fn child(mut self, child: impl Widget<Msg> + 'static) -> Self {
165 self.children.clear();
166 self.children.push(Box::new(child));
167 self
168 }
169}
170
171impl<Msg: Clone> Widget<Msg> for ClipPath<Msg> {
172 fn style(&self) -> Style {
173 Style::default()
174 }
175
176 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
177 &self.children
178 }
179
180 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
181
182 fn on_click(&self) -> Option<Msg> {
183 None
184 }
185
186 fn clip_path(&self) -> Option<&Path> {
187 Some(&self.path)
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use crate::Container;
195 use frus_core::{Color, Point, Primitive, Size};
196
197 #[test]
200 fn clip_rrect_wraps_child_in_a_rounded_layer() {
201 let red = Color::rgb(1.0, 0.0, 0.0);
202 let root = crate::Flex::<()>::column()
203 .width(100.0)
204 .child(ClipRRect::new(8.0).child(Container::new().width(40.0).height(40.0).color(red)));
205 let rt = crate::runtime::Runtime::default();
206 let theme = crate::Theme::dark();
207 let ui = crate::ui::build_ui(&root, Size::new(100.0, 200.0), &rt, &theme);
208 let layer = ui
209 .scene()
210 .primitives()
211 .iter()
212 .find_map(|p| match p {
213 Primitive::Layer {
214 clip_shape,
215 primitives,
216 ..
217 } => Some((clip_shape.clone(), primitives.clone())),
218 _ => None,
219 })
220 .expect("a clipping layer");
221 assert_eq!(
222 layer.0,
223 ClipShape::RRect(BorderRadius::uniform(8.0)),
224 "a rounded shape of radius 8"
225 );
226 assert!(
227 layer
228 .1
229 .iter()
230 .any(|p| matches!(p, Primitive::Rect { color, .. } if color.r > 0.5)),
231 "the child's red background is painted in the layer"
232 );
233 }
234
235 #[test]
237 fn clip_oval_emits_an_oval_layer() {
238 let blue = Color::rgb(0.0, 0.0, 1.0);
239 let root = crate::Flex::<()>::column()
240 .width(100.0)
241 .child(ClipOval::new().child(Container::new().width(40.0).height(40.0).color(blue)));
242 let rt = crate::runtime::Runtime::default();
243 let theme = crate::Theme::dark();
244 let ui = crate::ui::build_ui(&root, Size::new(100.0, 200.0), &rt, &theme);
245 let shape = ui.scene().primitives().iter().find_map(|p| match p {
246 Primitive::Layer { clip_shape, .. } => Some(clip_shape.clone()),
247 _ => None,
248 });
249 assert_eq!(shape, Some(ClipShape::Oval), "an elliptical shape");
250 }
251
252 #[test]
255 fn clip_path_emits_a_translated_path_layer() {
256 let diamond = Path::new()
258 .move_to(Point::new(20.0, 0.0))
259 .line_to(Point::new(40.0, 20.0))
260 .line_to(Point::new(20.0, 40.0))
261 .line_to(Point::new(0.0, 20.0))
262 .close();
263 let root = crate::Flex::<()>::column()
264 .width(100.0)
265 .padding(10.0)
266 .child(
267 ClipPath::new(diamond).child(
268 Container::new()
269 .width(40.0)
270 .height(40.0)
271 .color(Color::rgb(1.0, 0.0, 0.0)),
272 ),
273 );
274 let rt = crate::runtime::Runtime::default();
275 let theme = crate::Theme::dark();
276 let ui = crate::ui::build_ui(&root, Size::new(100.0, 200.0), &rt, &theme);
277 let shape = ui.scene().primitives().iter().find_map(|p| match p {
278 Primitive::Layer {
279 clip_shape: ClipShape::Path(path),
280 ..
281 } => Some(path.clone()),
282 _ => None,
283 });
284 let path = shape.expect("a path clipping layer");
285 let first = path.verbs().first().copied().expect("at least one verb");
288 match first {
289 frus_core::PathVerb::MoveTo(p) => assert!(
290 (p.x - 30.0).abs() < 0.6 && (p.y - 10.0).abs() < 0.6,
291 "vertex offset on screen to (30, 10): {p:?}"
292 ),
293 other => panic!("expected MoveTo as the first verb, got {other:?}"),
294 }
295 }
296
297 #[test]
300 fn clip_is_layout_passthrough() {
301 let red = Color::rgb(1.0, 0.0, 0.0);
302 let green = Color::rgb(0.0, 1.0, 0.0);
303 let root = crate::Flex::<()>::column()
304 .width(100.0)
305 .child(ClipRRect::new(6.0).child(Container::new().height(20.0).color(red)))
306 .child(Container::new().height(20.0).color(green));
307 let rt = crate::runtime::Runtime::default();
308 let theme = crate::Theme::dark();
309 let ui = crate::ui::build_ui(&root, Size::new(100.0, 200.0), &rt, &theme);
310 let green_y = ui
312 .scene()
313 .primitives()
314 .iter()
315 .flat_map(|p| match p {
316 Primitive::Layer { primitives, .. } => primitives.clone(),
317 other => vec![other.clone()],
318 })
319 .find_map(|p| match p {
320 Primitive::Rect { rect, color, .. } if color.g > 0.5 && color.r < 0.5 => {
321 Some(rect.y)
322 }
323 _ => None,
324 })
325 .expect("the 2nd child's green background");
326 assert!(
327 (green_y - 20.0).abs() < 0.5,
328 "sibling in its layout place: y = {green_y}"
329 );
330 }
331}