ribir_core/builtin_widgets/
box_decoration.rs

1use crate::prelude::*;
2
3/// The BoxDecoration provides a variety of ways to draw a box.
4#[derive(SingleChild, Default, Clone, Query)]
5pub struct BoxDecoration {
6  /// The background of the box.
7  pub background: Option<Brush>,
8  /// A border to draw above the background
9  pub border: Option<Border>,
10  /// The corners of this box are rounded by this `BorderRadius`. The round
11  /// corner only work if the two borders beside it are same style.
12  pub border_radius: Option<Radius>,
13}
14
15impl Declare for BoxDecoration {
16  type Builder = FatObj<()>;
17  #[inline]
18  fn declarer() -> Self::Builder { FatObj::new(()) }
19}
20
21#[derive(Debug, Default, Clone, PartialEq)]
22pub struct Border {
23  pub left: BorderSide,
24  pub right: BorderSide,
25  pub top: BorderSide,
26  pub bottom: BorderSide,
27}
28
29#[derive(Debug, Default, Clone, PartialEq, Lerp)]
30pub struct BorderSide {
31  pub color: Brush,
32  pub width: f32,
33}
34
35impl BorderSide {
36  #[inline]
37  pub fn new(width: f32, color: Brush) -> Self { Self { width, color } }
38}
39
40impl Render for BoxDecoration {
41  #[inline]
42  fn perform_layout(&self, clamp: BoxClamp, ctx: &mut LayoutCtx) -> Size {
43    ctx.assert_perform_single_child_layout(clamp)
44  }
45
46  fn paint(&self, ctx: &mut PaintingCtx) {
47    let size = ctx.box_size().unwrap();
48    if !size.is_empty() {
49      let rect = Rect::from_size(size);
50      let painter = ctx.painter();
51      if let Some(ref background) = self.background {
52        painter.set_brush(background.clone());
53        if let Some(radius) = &self.border_radius {
54          painter.rect_round(&rect, radius);
55        } else {
56          painter.rect(&rect);
57        }
58        painter.fill();
59      }
60      self.paint_border(painter, &rect);
61    }
62  }
63}
64
65impl BoxDecoration {
66  fn paint_border(&self, painter: &mut Painter, rect: &Rect) {
67    if self.border.is_none() {
68      return;
69    }
70    let border = self.border.as_ref().unwrap();
71    if let Some(radius) = &self.border_radius {
72      self.paint_round_border(painter, radius, border, rect);
73    } else {
74      self.paint_rect_border(painter, border, rect);
75    }
76  }
77
78  fn is_border_uniform(&self) -> bool {
79    self.border.as_ref().map_or(true, |border| {
80      border.top == border.left && border.top == border.right && border.top == border.bottom
81    })
82  }
83
84  fn paint_round_border(
85    &self, painter: &mut Painter, radius: &Radius, border: &Border, content_rect: &Rect,
86  ) {
87    assert!(self.is_border_uniform(), "radius can't be setted with different border");
88    let width_half = border.left.width / 2.;
89    let min_x = content_rect.min_x() + width_half;
90    let max_x = content_rect.max_x() - width_half;
91    let min_y = content_rect.min_y() + width_half;
92    let max_y = content_rect.max_y() - width_half;
93    let radius = Radius::new(
94      radius.top_left + width_half,
95      radius.top_right + width_half,
96      radius.bottom_left + width_half,
97      radius.bottom_right + width_half,
98    );
99
100    painter
101      .set_line_width(border.top.width)
102      .set_brush(border.top.color.clone());
103    painter.rect_round(
104      &Rect::new(Point::new(min_x, min_y), Size::new(max_x - min_x, max_y - min_y)),
105      &radius,
106    );
107    painter.stroke();
108  }
109
110  fn paint_rect_border(&self, painter: &mut Painter, border: &Border, content_rect: &Rect) {
111    let min_x = content_rect.min_x() - border.left.width;
112    let max_x = content_rect.max_x() + border.right.width;
113    let min_y = content_rect.min_y() - border.top.width;
114    let max_y = content_rect.max_y() + border.bottom.width;
115    let vertexs = [
116      Point::new(min_x, min_y), // lt
117      Point::new(max_x, min_y), // rt
118      Point::new(max_x, max_y), // rb
119      Point::new(min_x, max_y), // lb
120    ];
121    let edges = [(0, 1), (1, 2), (2, 3), (3, 0)];
122    let borders = [&border.top, &border.right, &border.bottom, &border.left];
123    let borders_offset = [
124      Size::new(0., border.top.width / 2.),
125      Size::new(-border.right.width / 2., 0.),
126      Size::new(0., -border.bottom.width / 2.),
127      Size::new(border.left.width / 2., 0.),
128    ];
129    edges
130      .iter()
131      .zip(borders.iter())
132      .zip(borders_offset.iter())
133      .for_each(|((edge, border), offset)| {
134        if border.is_visible() {
135          painter
136            .set_line_width(border.width)
137            .set_brush(border.color.clone());
138          painter.begin_path(vertexs[edge.0] + *offset);
139          painter.line_to(vertexs[edge.1] + *offset);
140          painter.end_path(false).stroke();
141        }
142      });
143  }
144}
145
146impl BorderSide {
147  fn is_visible(&self) -> bool {
148    match self.color {
149      Brush::Color(color) => color.alpha > 0 && self.width > 0.,
150      _ => true,
151    }
152  }
153}
154
155impl Border {
156  #[inline]
157  pub fn all(side: BorderSide) -> Self {
158    Self { left: side.clone(), right: side.clone(), top: side.clone(), bottom: side }
159  }
160
161  #[inline]
162  pub fn only_left(left: BorderSide) -> Self { Self { left, ..Default::default() } }
163
164  #[inline]
165  pub fn only_right(right: BorderSide) -> Self { Self { right, ..Default::default() } }
166
167  #[inline]
168  pub fn only_bottom(bottom: BorderSide) -> Self { Self { bottom, ..Default::default() } }
169
170  #[inline]
171  pub fn only_top(top: BorderSide) -> Self { Self { top, ..Default::default() } }
172
173  #[inline]
174  pub fn none() -> Self { Self { ..Default::default() } }
175}
176#[cfg(test)]
177mod tests {
178  use ribir_dev_helper::*;
179
180  use super::*;
181  use crate::test_helper::*;
182
183  #[test]
184  fn default_value_is_none() {
185    let dummy = std::mem::MaybeUninit::uninit();
186    // just for test, we know BoxDecoration not use `ctx` to build.
187    let ctx: BuildCtx<'static> = unsafe { dummy.assume_init() };
188    let mut w = BoxDecoration::declarer().finish(&ctx);
189    let w = w.get_box_decoration_widget();
190
191    assert_eq!(w.read().border, None);
192    assert_eq!(w.read().border_radius, None);
193    assert_eq!(w.read().background, None);
194
195    std::mem::forget(ctx);
196  }
197
198  const SIZE: Size = Size::new(100., 100.);
199  fn with_border() -> impl WidgetBuilder {
200    fn_widget! {
201      @MockBox {
202        size: SIZE,
203        border: Border {
204          left: BorderSide::new(1., Color::BLACK.into()),
205          right: BorderSide::new(2., Color::BLACK.into()),
206          top: BorderSide::new(3., Color::BLACK.into()),
207          bottom: BorderSide::new(4., Color::BLACK.into()),
208        },
209      }
210    }
211  }
212  widget_layout_test!(
213    with_border,
214    { path = [0],  width == 100., height == 100., }
215    { path = [0, 0], rect == ribir_geom::rect(0., 0., 100., 100.), }
216  );
217}