1use frus_core::{
9 BorderRadius, BoxDecoration, ClipShape, Color, Curve, Insets, InsetsGeometry, Rect, Scene, Size,
10};
11use frus_layout::Style;
12
13use crate::container::Container;
14use crate::interaction::Status;
15use crate::theme::Theme;
16use crate::widget::Widget;
17
18pub struct Padding<Msg = crate::callback::Callback> {
35 padding: InsetsGeometry,
36 children: Vec<Box<dyn Widget<Msg>>>,
37}
38
39impl<Msg> Padding<Msg> {
40 pub fn new(padding: impl Into<InsetsGeometry>, child: impl Widget<Msg> + 'static) -> Self {
42 Self {
43 padding: padding.into(),
44 children: vec![Box::new(child)],
45 }
46 }
47}
48
49impl<Msg> Padding<Msg> {
50 fn laid_out(&self, direction: frus_core::TextDirection) -> Insets {
55 self.padding.laid_out(direction)
56 }
57}
58
59impl<Msg: Clone> Widget<Msg> for Padding<Msg> {
60 fn style(&self) -> Style {
61 Style {
62 padding: self.laid_out(frus_core::TextDirection::Ltr),
63 ..Style::default()
64 }
65 }
66
67 fn style_themed(&self, theme: &Theme) -> Style {
68 Style {
69 padding: self.laid_out(theme.direction),
70 ..Style::default()
71 }
72 }
73
74 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
75 &self.children
76 }
77
78 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {
79 }
81
82 fn on_click(&self) -> Option<Msg> {
83 None
84 }
85
86 fn debug_name(&self) -> &'static str {
87 "Padding"
88 }
89}
90
91pub struct ColoredBox<Msg = crate::callback::Callback> {
101 inner: Container<Msg>,
102}
103
104impl<Msg: Clone + 'static> ColoredBox<Msg> {
105 pub fn new(color: Color, child: impl Widget<Msg> + 'static) -> Self {
107 Self {
108 inner: Container::new().color(color).child(child),
109 }
110 }
111}
112
113crate::animated::forward_to_container!(ColoredBox);
114
115pub struct DecoratedBox<Msg = crate::callback::Callback> {
128 inner: Container<Msg>,
129}
130
131impl<Msg: Clone + 'static> DecoratedBox<Msg> {
132 pub fn new(decoration: BoxDecoration, child: impl Widget<Msg> + 'static) -> Self {
134 Self {
135 inner: Container::new().decoration(decoration).child(child),
136 }
137 }
138
139 pub fn foreground(decoration: BoxDecoration, child: impl Widget<Msg> + 'static) -> Self {
142 Self {
143 inner: Container::new().foreground(decoration).child(child),
144 }
145 }
146}
147
148crate::animated::forward_to_container!(DecoratedBox);
149
150pub struct ClipRect<Msg = crate::callback::Callback> {
161 children: Vec<Box<dyn Widget<Msg>>>,
162}
163
164impl<Msg> ClipRect<Msg> {
165 pub fn new() -> Self {
167 Self {
168 children: Vec::new(),
169 }
170 }
171
172 pub fn child(mut self, child: impl Widget<Msg> + 'static) -> Self {
174 self.children.clear();
175 self.children.push(Box::new(child));
176 self
177 }
178}
179
180impl<Msg> Default for ClipRect<Msg> {
181 fn default() -> Self {
182 Self::new()
183 }
184}
185
186impl<Msg: Clone> Widget<Msg> for ClipRect<Msg> {
187 fn style(&self) -> Style {
188 Style::default()
189 }
190
191 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
192 &self.children
193 }
194
195 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
196
197 fn on_click(&self) -> Option<Msg> {
198 None
199 }
200
201 fn clip_shape(&self) -> Option<ClipShape> {
202 Some(ClipShape::Rect)
203 }
204
205 fn debug_name(&self) -> &'static str {
206 "ClipRect"
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use crate::{build_ui, Runtime};
214 use frus_core::{Border, InsetsDirectional, Primitive};
215
216 const RED: Color = Color::rgb(1.0, 0.0, 0.0);
217 const BLUE: Color = Color::rgb(0.0, 0.0, 1.0);
218
219 fn square() -> Container<()> {
221 Container::new().width(20.0).height(20.0).color(RED)
222 }
223
224 fn frame(root: &dyn Widget<()>, theme: &Theme) -> Vec<Primitive> {
225 build_ui(root, Size::new(100.0, 100.0), &Runtime::default(), theme)
226 .scene()
227 .primitives()
228 .to_vec()
229 }
230
231 fn rects_of(primitives: &[Primitive], color: Color) -> Vec<Rect> {
233 let mut found = Vec::new();
234 for p in primitives {
235 match p {
236 Primitive::Rect { rect, color: c, .. } if *c == color => found.push(*rect),
237 Primitive::Layer { primitives, .. } => found.extend(rects_of(primitives, color)),
238 _ => {}
239 }
240 }
241 found
242 }
243
244 fn boxed(child: impl Widget<()> + 'static) -> Container<()> {
246 Container::new().width(100.0).height(100.0).child(child)
247 }
248
249 #[test]
252 fn padding_insets_the_child() {
253 let theme = Theme::default();
254 let at = rects_of(&frame(&boxed(Padding::new(10.0, square())), &theme), RED);
255 assert_eq!(at.first().map(|r| (r.x, r.y)), Some((10.0, 10.0)), "{at:?}");
256 let symmetric = Padding::new(Insets::symmetric(24.0, 8.0), square());
257 let at = rects_of(&frame(&boxed(symmetric), &theme), RED);
258 assert_eq!(at.first().map(|r| (r.x, r.y)), Some((24.0, 8.0)), "{at:?}");
259 }
260
261 #[test]
264 fn the_style_without_a_theme_carries_the_padding() {
265 let padded = Padding::<()>::new(InsetsDirectional::only_start(12.0), square());
266 assert_eq!(
267 Widget::<()>::style(&padded).padding,
268 Insets::new(0.0, 0.0, 0.0, 12.0)
269 );
270 }
271
272 #[test]
276 fn a_directional_padding_mirrors_in_rtl() {
277 let lead = || {
278 boxed(Padding::new(
279 InsetsDirectional::only_start(30.0),
280 crate::Center::new(square()),
281 ))
282 };
283 let ltr = rects_of(&frame(&lead(), &Theme::default()), RED);
284 assert_eq!(ltr.first().map(|r| r.x), Some(55.0), "{ltr:?}");
285 let rtl = rects_of(&frame(&lead(), &Theme::default().rtl()), RED);
286 assert_eq!(rtl.first().map(|r| r.x), Some(25.0), "{rtl:?}");
287 }
288
289 #[test]
292 fn a_physical_padding_stays_put_in_rtl() {
293 let left = || {
294 boxed(Padding::new(
295 Insets::new(0.0, 0.0, 0.0, 30.0),
296 crate::Center::new(square()),
297 ))
298 };
299 for theme in [Theme::default(), Theme::default().rtl()] {
300 let at = rects_of(&frame(&left(), &theme), RED);
301 assert_eq!(at.first().map(|r| r.x), Some(55.0), "{at:?}");
302 }
303 }
304
305 #[test]
308 fn a_colored_box_fills_behind_its_child() {
309 let hugging = frame(&boxed(ColoredBox::new(BLUE, square())), &Theme::default());
310 let fill = rects_of(&hugging, BLUE);
311 assert_eq!(
312 fill.first().map(|r| r.width),
313 Some(20.0),
314 "as wide as the square: {fill:?}"
315 );
316 let centred = boxed(ColoredBox::new(BLUE, crate::Center::new(square())));
317 let primitives = frame(¢red, &Theme::default());
318 let fill = rects_of(&primitives, BLUE);
319 assert_eq!(fill, vec![Rect::new(0.0, 0.0, 100.0, 100.0)]);
320 let fill_at = primitives
321 .iter()
322 .position(|p| matches!(p, Primitive::Rect { color, .. } if *color == BLUE));
323 let child_at = primitives
324 .iter()
325 .position(|p| matches!(p, Primitive::Rect { color, .. } if *color == RED));
326 assert!(fill_at < child_at, "the fill is behind the child");
327 }
328
329 #[test]
332 fn a_decoration_goes_behind_or_in_front() {
333 let frame_of = |decorated: DecoratedBox<()>| frame(&boxed(decorated), &Theme::default());
334 let order = |primitives: &[Primitive]| {
335 let blue = primitives
336 .iter()
337 .position(|p| matches!(p, Primitive::Rect { color, .. } if *color == BLUE));
338 let red = primitives
339 .iter()
340 .position(|p| matches!(p, Primitive::Rect { color, .. } if *color == RED));
341 (blue.expect("the decoration"), red.expect("the child"))
342 };
343 let (decoration, child) = order(&frame_of(DecoratedBox::new(
344 BoxDecoration::filled(BLUE),
345 square(),
346 )));
347 assert!(decoration < child, "behind");
348 let tint = BoxDecoration::filled(BLUE);
349 let (decoration, child) = order(&frame_of(DecoratedBox::foreground(tint, square())));
350 assert!(decoration > child, "in front");
351 let bordered = BoxDecoration::default().border(Border::new(2.0, BLUE));
353 assert!(!frame_of(DecoratedBox::new(bordered, square())).is_empty());
354 }
355
356 #[test]
359 fn a_clip_rect_cuts_at_the_box() {
360 let big = Container::<()>::new().width(200.0).height(200.0).color(RED);
361 let clipped = Container::<()>::new()
362 .width(50.0)
363 .height(50.0)
364 .child(ClipRect::new().child(big));
365 let primitives = frame(&clipped, &Theme::default());
366 let layer = primitives.iter().find_map(|p| match p {
367 Primitive::Layer {
368 clip, primitives, ..
369 } if !rects_of(primitives, RED).is_empty() => Some(*clip),
370 _ => None,
371 });
372 let clip = layer.expect("the child is painted in a clipped layer");
373 assert!(clip.width <= 50.0 && clip.height <= 50.0, "{clip:?}");
374 }
375}
376
377#[cfg(test)]
381mod rtl_sides {
382 use super::*;
383 use crate::{build_ui, Center, Flex, Runtime, SafeArea};
384 use frus_core::{Insets, InsetsDirectional, Primitive, Size};
385
386 const RED: Color = Color::rgb(1.0, 0.0, 0.0);
387
388 fn square() -> Center<()> {
389 Center::new(Container::new().width(20.0).height(20.0).color(RED))
390 }
391
392 fn red_x(root: &dyn Widget<()>, theme: &Theme) -> f32 {
393 build_ui(root, Size::new(100.0, 100.0), &Runtime::default(), theme)
394 .scene()
395 .primitives()
396 .iter()
397 .find_map(|p| match p {
398 Primitive::Rect { rect, color, .. } if *color == RED => Some(rect.x),
399 _ => None,
400 })
401 .expect("the square")
402 }
403
404 fn boxed(child: impl Widget<()> + 'static) -> Container<()> {
405 Container::new().width(100.0).height(100.0).child(child)
406 }
407
408 #[test]
411 fn a_containers_physical_padding_stays_and_its_start_padding_moves() {
412 let (ltr, rtl) = (Theme::default(), Theme::default().rtl());
413 let left = || {
414 boxed(
415 Container::new()
416 .padding_each(0.0, 0.0, 0.0, 30.0)
417 .child(square()),
418 )
419 };
420 assert_eq!(red_x(&left(), <r), 55.0);
421 assert_eq!(red_x(&left(), &rtl), 55.0, "still on the left");
422 let start = || {
423 boxed(
424 Container::new()
425 .padding_insets(InsetsDirectional::only_start(30.0))
426 .child(square()),
427 )
428 };
429 assert_eq!(red_x(&start(), <r), 55.0);
430 assert_eq!(red_x(&start(), &rtl), 25.0, "the start is the right");
431 let margin = || {
432 boxed(
433 Container::new()
434 .margin_each(0.0, 0.0, 0.0, 30.0)
435 .width(70.0)
436 .height(100.0)
437 .child(square()),
438 )
439 };
440 assert_eq!(red_x(&margin(), <r), 55.0);
441 assert_eq!(red_x(&margin(), &rtl), 55.0, "the margin stays on the left");
442 }
443
444 #[test]
446 fn a_flexs_physical_padding_stays() {
447 let rtl = Theme::default().rtl();
448 let left = boxed(
449 Flex::column()
450 .padding_each(0.0, 0.0, 0.0, 30.0)
451 .child(square()),
452 );
453 assert_eq!(red_x(&left, &rtl), 55.0);
454 let start = boxed(
455 Flex::column()
456 .padding_insets(InsetsDirectional::only_start(30.0))
457 .child(square()),
458 );
459 assert_eq!(red_x(&start, &rtl), 25.0);
460 }
461
462 #[test]
465 fn a_safe_area_pads_the_side_the_cutout_is_on() {
466 let (ltr, rtl) = (Theme::default(), Theme::default().rtl());
467 let surface = crate::MediaQuery::new(Size::new(100.0, 100.0))
468 .with_insets(crate::WindowInsets::bars(Insets::new(0.0, 0.0, 0.0, 30.0)));
469 let area = surface.scope(|| boxed(SafeArea::new(square())));
470 let _installed = surface.install();
471 assert_eq!(red_x(&area, <r), 55.0);
472 assert_eq!(red_x(&area, &rtl), 55.0, "clear of the cutout, on the left");
473 }
474}