use frus_core::{
BorderRadius, BoxDecoration, ClipShape, Color, Curve, Insets, InsetsGeometry, Rect, Scene, Size,
};
use frus_layout::Style;
use crate::container::Container;
use crate::interaction::Status;
use crate::theme::Theme;
use crate::widget::Widget;
pub struct Padding<Msg = crate::callback::Callback> {
padding: InsetsGeometry,
children: Vec<Box<dyn Widget<Msg>>>,
}
impl<Msg> Padding<Msg> {
pub fn new(padding: impl Into<InsetsGeometry>, child: impl Widget<Msg> + 'static) -> Self {
Self {
padding: padding.into(),
children: vec![Box::new(child)],
}
}
}
impl<Msg> Padding<Msg> {
fn laid_out(&self, direction: frus_core::TextDirection) -> Insets {
self.padding.laid_out(direction)
}
}
impl<Msg: Clone> Widget<Msg> for Padding<Msg> {
fn style(&self) -> Style {
Style {
padding: self.laid_out(frus_core::TextDirection::Ltr),
..Style::default()
}
}
fn style_themed(&self, theme: &Theme) -> Style {
Style {
padding: self.laid_out(theme.direction),
..Style::default()
}
}
fn children(&self) -> &[Box<dyn Widget<Msg>>] {
&self.children
}
fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {
}
fn on_click(&self) -> Option<Msg> {
None
}
fn debug_name(&self) -> &'static str {
"Padding"
}
}
pub struct ColoredBox<Msg = crate::callback::Callback> {
inner: Container<Msg>,
}
impl<Msg: Clone + 'static> ColoredBox<Msg> {
pub fn new(color: Color, child: impl Widget<Msg> + 'static) -> Self {
Self {
inner: Container::new().color(color).child(child),
}
}
}
crate::animated::forward_to_container!(ColoredBox);
pub struct DecoratedBox<Msg = crate::callback::Callback> {
inner: Container<Msg>,
}
impl<Msg: Clone + 'static> DecoratedBox<Msg> {
pub fn new(decoration: BoxDecoration, child: impl Widget<Msg> + 'static) -> Self {
Self {
inner: Container::new().decoration(decoration).child(child),
}
}
pub fn foreground(decoration: BoxDecoration, child: impl Widget<Msg> + 'static) -> Self {
Self {
inner: Container::new().foreground(decoration).child(child),
}
}
}
crate::animated::forward_to_container!(DecoratedBox);
pub struct ClipRect<Msg = crate::callback::Callback> {
children: Vec<Box<dyn Widget<Msg>>>,
}
impl<Msg> ClipRect<Msg> {
pub fn new() -> Self {
Self {
children: Vec::new(),
}
}
pub fn child(mut self, child: impl Widget<Msg> + 'static) -> Self {
self.children.clear();
self.children.push(Box::new(child));
self
}
}
impl<Msg> Default for ClipRect<Msg> {
fn default() -> Self {
Self::new()
}
}
impl<Msg: Clone> Widget<Msg> for ClipRect<Msg> {
fn style(&self) -> Style {
Style::default()
}
fn children(&self) -> &[Box<dyn Widget<Msg>>] {
&self.children
}
fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
fn on_click(&self) -> Option<Msg> {
None
}
fn clip_shape(&self) -> Option<ClipShape> {
Some(ClipShape::Rect)
}
fn debug_name(&self) -> &'static str {
"ClipRect"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{build_ui, Runtime};
use frus_core::{Border, InsetsDirectional, Primitive};
const RED: Color = Color::rgb(1.0, 0.0, 0.0);
const BLUE: Color = Color::rgb(0.0, 0.0, 1.0);
fn square() -> Container<()> {
Container::new().width(20.0).height(20.0).color(RED)
}
fn frame(root: &dyn Widget<()>, theme: &Theme) -> Vec<Primitive> {
build_ui(root, Size::new(100.0, 100.0), &Runtime::default(), theme)
.scene()
.primitives()
.to_vec()
}
fn rects_of(primitives: &[Primitive], color: Color) -> Vec<Rect> {
let mut found = Vec::new();
for p in primitives {
match p {
Primitive::Rect { rect, color: c, .. } if *c == color => found.push(*rect),
Primitive::Layer { primitives, .. } => found.extend(rects_of(primitives, color)),
_ => {}
}
}
found
}
fn boxed(child: impl Widget<()> + 'static) -> Container<()> {
Container::new().width(100.0).height(100.0).child(child)
}
#[test]
fn padding_insets_the_child() {
let theme = Theme::default();
let at = rects_of(&frame(&boxed(Padding::new(10.0, square())), &theme), RED);
assert_eq!(at.first().map(|r| (r.x, r.y)), Some((10.0, 10.0)), "{at:?}");
let symmetric = Padding::new(Insets::symmetric(24.0, 8.0), square());
let at = rects_of(&frame(&boxed(symmetric), &theme), RED);
assert_eq!(at.first().map(|r| (r.x, r.y)), Some((24.0, 8.0)), "{at:?}");
}
#[test]
fn the_style_without_a_theme_carries_the_padding() {
let padded = Padding::<()>::new(InsetsDirectional::only_start(12.0), square());
assert_eq!(
Widget::<()>::style(&padded).padding,
Insets::new(0.0, 0.0, 0.0, 12.0)
);
}
#[test]
fn a_directional_padding_mirrors_in_rtl() {
let lead = || {
boxed(Padding::new(
InsetsDirectional::only_start(30.0),
crate::Center::new(square()),
))
};
let ltr = rects_of(&frame(&lead(), &Theme::default()), RED);
assert_eq!(ltr.first().map(|r| r.x), Some(55.0), "{ltr:?}");
let rtl = rects_of(&frame(&lead(), &Theme::default().rtl()), RED);
assert_eq!(rtl.first().map(|r| r.x), Some(25.0), "{rtl:?}");
}
#[test]
fn a_physical_padding_stays_put_in_rtl() {
let left = || {
boxed(Padding::new(
Insets::new(0.0, 0.0, 0.0, 30.0),
crate::Center::new(square()),
))
};
for theme in [Theme::default(), Theme::default().rtl()] {
let at = rects_of(&frame(&left(), &theme), RED);
assert_eq!(at.first().map(|r| r.x), Some(55.0), "{at:?}");
}
}
#[test]
fn a_colored_box_fills_behind_its_child() {
let hugging = frame(&boxed(ColoredBox::new(BLUE, square())), &Theme::default());
let fill = rects_of(&hugging, BLUE);
assert_eq!(
fill.first().map(|r| r.width),
Some(20.0),
"as wide as the square: {fill:?}"
);
let centred = boxed(ColoredBox::new(BLUE, crate::Center::new(square())));
let primitives = frame(¢red, &Theme::default());
let fill = rects_of(&primitives, BLUE);
assert_eq!(fill, vec![Rect::new(0.0, 0.0, 100.0, 100.0)]);
let fill_at = primitives
.iter()
.position(|p| matches!(p, Primitive::Rect { color, .. } if *color == BLUE));
let child_at = primitives
.iter()
.position(|p| matches!(p, Primitive::Rect { color, .. } if *color == RED));
assert!(fill_at < child_at, "the fill is behind the child");
}
#[test]
fn a_decoration_goes_behind_or_in_front() {
let frame_of = |decorated: DecoratedBox<()>| frame(&boxed(decorated), &Theme::default());
let order = |primitives: &[Primitive]| {
let blue = primitives
.iter()
.position(|p| matches!(p, Primitive::Rect { color, .. } if *color == BLUE));
let red = primitives
.iter()
.position(|p| matches!(p, Primitive::Rect { color, .. } if *color == RED));
(blue.expect("the decoration"), red.expect("the child"))
};
let (decoration, child) = order(&frame_of(DecoratedBox::new(
BoxDecoration::filled(BLUE),
square(),
)));
assert!(decoration < child, "behind");
let tint = BoxDecoration::filled(BLUE);
let (decoration, child) = order(&frame_of(DecoratedBox::foreground(tint, square())));
assert!(decoration > child, "in front");
let bordered = BoxDecoration::default().border(Border::new(2.0, BLUE));
assert!(!frame_of(DecoratedBox::new(bordered, square())).is_empty());
}
#[test]
fn a_clip_rect_cuts_at_the_box() {
let big = Container::<()>::new().width(200.0).height(200.0).color(RED);
let clipped = Container::<()>::new()
.width(50.0)
.height(50.0)
.child(ClipRect::new().child(big));
let primitives = frame(&clipped, &Theme::default());
let layer = primitives.iter().find_map(|p| match p {
Primitive::Layer {
clip, primitives, ..
} if !rects_of(primitives, RED).is_empty() => Some(*clip),
_ => None,
});
let clip = layer.expect("the child is painted in a clipped layer");
assert!(clip.width <= 50.0 && clip.height <= 50.0, "{clip:?}");
}
}
#[cfg(test)]
mod rtl_sides {
use super::*;
use crate::{build_ui, Center, Flex, Runtime, SafeArea};
use frus_core::{Insets, InsetsDirectional, Primitive, Size};
const RED: Color = Color::rgb(1.0, 0.0, 0.0);
fn square() -> Center<()> {
Center::new(Container::new().width(20.0).height(20.0).color(RED))
}
fn red_x(root: &dyn Widget<()>, theme: &Theme) -> f32 {
build_ui(root, Size::new(100.0, 100.0), &Runtime::default(), theme)
.scene()
.primitives()
.iter()
.find_map(|p| match p {
Primitive::Rect { rect, color, .. } if *color == RED => Some(rect.x),
_ => None,
})
.expect("the square")
}
fn boxed(child: impl Widget<()> + 'static) -> Container<()> {
Container::new().width(100.0).height(100.0).child(child)
}
#[test]
fn a_containers_physical_padding_stays_and_its_start_padding_moves() {
let (ltr, rtl) = (Theme::default(), Theme::default().rtl());
let left = || {
boxed(
Container::new()
.padding_each(0.0, 0.0, 0.0, 30.0)
.child(square()),
)
};
assert_eq!(red_x(&left(), <r), 55.0);
assert_eq!(red_x(&left(), &rtl), 55.0, "still on the left");
let start = || {
boxed(
Container::new()
.padding_insets(InsetsDirectional::only_start(30.0))
.child(square()),
)
};
assert_eq!(red_x(&start(), <r), 55.0);
assert_eq!(red_x(&start(), &rtl), 25.0, "the start is the right");
let margin = || {
boxed(
Container::new()
.margin_each(0.0, 0.0, 0.0, 30.0)
.width(70.0)
.height(100.0)
.child(square()),
)
};
assert_eq!(red_x(&margin(), <r), 55.0);
assert_eq!(red_x(&margin(), &rtl), 55.0, "the margin stays on the left");
}
#[test]
fn a_flexs_physical_padding_stays() {
let rtl = Theme::default().rtl();
let left = boxed(
Flex::column()
.padding_each(0.0, 0.0, 0.0, 30.0)
.child(square()),
);
assert_eq!(red_x(&left, &rtl), 55.0);
let start = boxed(
Flex::column()
.padding_insets(InsetsDirectional::only_start(30.0))
.child(square()),
);
assert_eq!(red_x(&start, &rtl), 25.0);
}
#[test]
fn a_safe_area_pads_the_side_the_cutout_is_on() {
let (ltr, rtl) = (Theme::default(), Theme::default().rtl());
let surface = crate::MediaQuery::new(Size::new(100.0, 100.0))
.with_insets(crate::WindowInsets::bars(Insets::new(0.0, 0.0, 0.0, 30.0)));
let area = surface.scope(|| boxed(SafeArea::new(square())));
let _installed = surface.install();
assert_eq!(red_x(&area, <r), 55.0);
assert_eq!(red_x(&area, &rtl), 55.0, "clear of the cutout, on the left");
}
}