anathema_default_widgets/
padding.rs

1use std::ops::ControlFlow;
2
3use anathema_geometry::{Pos, Region, Size};
4use anathema_value_resolver::AttributeStorage;
5use anathema_widgets::error::Result;
6use anathema_widgets::layout::{Constraints, LayoutCtx, PositionCtx};
7use anathema_widgets::paint::{PaintCtx, SizePos};
8use anathema_widgets::{LayoutForEach, PaintChildren, PositionChildren, Widget, WidgetId};
9
10use crate::{BOTTOM, LEFT, RIGHT, TOP};
11
12const PADDING: &str = "padding";
13
14#[derive(Default)]
15struct PaddingValues {
16    top: u16,
17    right: u16,
18    bottom: u16,
19    left: u16,
20}
21
22impl PaddingValues {
23    fn size(&self) -> Size {
24        Size::new(self.left + self.right, self.top + self.bottom)
25    }
26}
27
28#[derive(Default)]
29pub struct Padding(PaddingValues);
30
31impl Widget for Padding {
32    fn layout<'bp>(
33        &mut self,
34        mut children: LayoutForEach<'_, 'bp>,
35        constraints: Constraints,
36        id: WidgetId,
37        ctx: &mut LayoutCtx<'_, 'bp>,
38    ) -> Result<Size> {
39        let attributes = ctx.attribute_storage.get(id);
40        let mut size = Size::ZERO;
41        let padding = attributes.get_as::<u16>(PADDING).unwrap_or(0);
42
43        self.0.top = attributes
44            .get_as::<usize>(TOP)
45            .and_then(|v| v.try_into().ok())
46            .unwrap_or(padding);
47        self.0.right = attributes
48            .get_as::<usize>(RIGHT)
49            .and_then(|v| v.try_into().ok())
50            .unwrap_or(padding);
51        self.0.bottom = attributes
52            .get_as::<usize>(BOTTOM)
53            .and_then(|v| v.try_into().ok())
54            .unwrap_or(padding);
55        self.0.left = attributes
56            .get_as::<usize>(LEFT)
57            .and_then(|v| v.try_into().ok())
58            .unwrap_or(padding);
59
60        let padding_size = self.0.size();
61
62        _ = children.each(ctx, |ctx, child, children| {
63            let mut child_constraints = constraints;
64            child_constraints.sub_max_width(padding_size.width);
65            child_constraints.sub_max_height(padding_size.height);
66            let mut child_size = Size::from(child.layout(children, child_constraints, ctx)?);
67            child_size += padding_size;
68            size.width = child_size.width.max(size.width);
69            size.height = child_size.height.max(size.height);
70
71            Ok(ControlFlow::Break(()))
72        })?;
73
74        size.width = constraints.min_width.max(size.width).min(constraints.max_width());
75        size.height = constraints.min_height.max(size.height).min(constraints.max_height());
76
77        Ok(size)
78    }
79
80    fn position<'bp>(
81        &mut self,
82        mut children: PositionChildren<'_, 'bp>,
83        _: WidgetId,
84        attribute_storage: &AttributeStorage<'bp>,
85        mut ctx: PositionCtx,
86    ) {
87        _ = children.each(|child, children| {
88            ctx.pos.y += self.0.top as i32;
89            ctx.pos.x += self.0.left as i32;
90
91            child.position(children, ctx.pos, attribute_storage, ctx.viewport);
92            ControlFlow::Break(())
93        });
94    }
95
96    fn paint<'bp>(
97        &mut self,
98        mut children: PaintChildren<'_, 'bp>,
99        _id: WidgetId,
100        attribute_storage: &AttributeStorage<'bp>,
101        mut ctx: PaintCtx<'_, SizePos>,
102    ) {
103        _ = children.each(|child, children| {
104            let mut ctx = ctx.to_unsized();
105            if let Some(clip) = ctx.clip.as_mut() {
106                clip.from.x += self.0.left as i32;
107                clip.from.y += self.0.top as i32;
108                clip.to.x -= self.0.right as i32;
109                clip.to.y -= self.0.bottom as i32;
110            }
111            child.paint(children, ctx, attribute_storage);
112            ControlFlow::Break(())
113        });
114    }
115
116    fn inner_bounds(&self, mut pos: Pos, mut size: Size) -> Region {
117        pos.x += self.0.left as i32;
118        pos.y += self.0.top as i32;
119        size.width = size.width.saturating_sub(self.0.right);
120        size.height = size.height.saturating_sub(self.0.bottom);
121        Region::from((pos, size))
122    }
123}
124
125#[cfg(test)]
126mod test {
127
128    use crate::testing::TestRunner;
129
130    #[test]
131    fn padding_all() {
132        let tpl = "
133            padding [padding: 1]
134                text 'a'
135        ";
136
137        let expected = "
138            ╔═══╗
139            ║   ║
140            ║ a ║
141            ║   ║
142            ╚═══╝
143        ";
144
145        TestRunner::new(tpl, (3, 3)).instance().render_assert(expected);
146    }
147
148    #[test]
149    fn padding_top_inclusive() {
150        let tpl = "
151            padding [padding: 1, top: 2]
152                text 'a'
153        ";
154
155        let expected = "
156            ╔════╗
157            ║    ║
158            ║    ║
159            ║ a  ║
160            ║    ║
161            ╚════╝
162        ";
163
164        TestRunner::new(tpl, (4, 4)).instance().render_assert(expected);
165    }
166
167    #[test]
168    fn padding_top() {
169        let tpl = "
170            padding [top: 2]
171                text 'a'
172        ";
173
174        let expected = "
175            ╔════╗
176            ║    ║
177            ║    ║
178            ║a   ║
179            ╚════╝
180        ";
181
182        TestRunner::new(tpl, (4, 3)).instance().render_assert(expected);
183    }
184
185    #[test]
186    fn padding_right_inclusive() {
187        let tpl = "
188            padding [padding: 1, right: 2]
189                text 'a'
190        ";
191
192        let expected = "
193            ╔════╗
194            ║    ║
195            ║ a  ║
196            ║    ║
197            ╚════╝
198        ";
199
200        TestRunner::new(tpl, (4, 3)).instance().render_assert(expected);
201    }
202
203    #[test]
204    fn padding_right() {
205        let tpl = "
206            padding [right: 2]
207                text 'a'
208        ";
209
210        let expected = "
211            ╔════╗
212            ║a   ║
213            ║    ║
214            ║    ║
215            ╚════╝
216        ";
217
218        TestRunner::new(tpl, (4, 3)).instance().render_assert(expected);
219    }
220
221    #[test]
222    fn padding_bottom_inclusive() {
223        let tpl = "
224            padding [padding: 1, bottom: 2]
225                text 'a'
226        ";
227
228        let expected = "
229            ╔════╗
230            ║    ║
231            ║ a  ║
232            ║    ║
233            ║    ║
234            ╚════╝
235        ";
236
237        TestRunner::new(tpl, (4, 4)).instance().render_assert(expected);
238    }
239
240    #[test]
241    fn padding_bottom() {
242        let tpl = "
243            padding [bottom: 2]
244                text 'a'
245        ";
246
247        let expected = "
248            ╔════╗
249            ║a   ║
250            ║    ║
251            ║    ║
252            ╚════╝
253        ";
254
255        TestRunner::new(tpl, (4, 3)).instance().render_assert(expected);
256    }
257
258    #[test]
259    fn padding_left_inclusive() {
260        let tpl = "
261            padding [padding: 1, left: 2]
262                text 'a'
263        ";
264
265        let expected = "
266            ╔════╗
267            ║    ║
268            ║  a ║
269            ║    ║
270            ╚════╝
271        ";
272
273        TestRunner::new(tpl, (4, 3)).instance().render_assert(expected);
274    }
275
276    #[test]
277    fn padding_left() {
278        let tpl = "
279            padding [left: 2]
280                text 'a'
281        ";
282
283        let expected = "
284            ╔════╗
285            ║  a ║
286            ║    ║
287            ║    ║
288            ╚════╝
289        ";
290
291        TestRunner::new(tpl, (4, 3)).instance().render_assert(expected);
292    }
293}