anathema_default_widgets/
position.rs

1use std::ops::ControlFlow;
2
3use anathema_geometry::{Pos, Size};
4use anathema_value_resolver::{AttributeStorage, ValueKind};
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 RELATIVE: &str = "relative";
13const ABSOLUTE: &str = "absolute";
14const PLACEMENT: &str = "placement";
15
16#[derive(Debug, Copy, Clone, PartialEq)]
17pub enum HorzEdge {
18    Left(i32),
19    Right(i32),
20}
21
22#[derive(Debug, Copy, Clone, PartialEq)]
23pub enum VertEdge {
24    Top(i32),
25    Bottom(i32),
26}
27
28#[derive(Debug, Default, Copy, Clone, PartialEq)]
29pub enum Placement {
30    /// Widget is positioned relative to its parent
31    #[default]
32    Relative,
33    /// Absolute position of a widget
34    Absolute,
35}
36
37impl TryFrom<&ValueKind<'_>> for Placement {
38    type Error = ();
39
40    fn try_from(value: &ValueKind<'_>) -> Result<Self, Self::Error> {
41        match value {
42            ValueKind::Str(wrap) => match wrap.as_ref() {
43                RELATIVE => Ok(Placement::Relative),
44                ABSOLUTE => Ok(Placement::Absolute),
45                _ => Err(()),
46            },
47            _ => Err(()),
48        }
49    }
50}
51
52#[derive(Debug)]
53pub struct Position {
54    horz_edge: HorzEdge,
55    vert_edge: VertEdge,
56    placement: Placement,
57}
58
59impl Default for Position {
60    fn default() -> Self {
61        Self {
62            horz_edge: HorzEdge::Left(0),
63            vert_edge: VertEdge::Top(0),
64            placement: Placement::Relative,
65        }
66    }
67}
68
69impl Widget for Position {
70    fn floats(&self) -> bool {
71        true
72    }
73
74    fn layout<'bp>(
75        &mut self,
76        mut children: LayoutForEach<'_, 'bp>,
77        constraints: Constraints,
78        id: WidgetId,
79        ctx: &mut LayoutCtx<'_, 'bp>,
80    ) -> Result<Size> {
81        let attribs = ctx.attribute_storage.get(id);
82        self.placement = attribs.get_as::<Placement>(PLACEMENT).unwrap_or_default();
83
84        self.horz_edge = match attribs.get_as::<i32>(LEFT) {
85            Some(left) => HorzEdge::Left(left),
86            None => match attribs.get_as::<i32>(RIGHT) {
87                Some(right) => HorzEdge::Right(right),
88                None => HorzEdge::Left(0),
89            },
90        };
91
92        self.vert_edge = match attribs.get_as::<i32>(TOP) {
93            Some(top) => VertEdge::Top(top),
94            None => match attribs.get_as::<i32>(BOTTOM) {
95                Some(bottom) => VertEdge::Bottom(bottom),
96                None => VertEdge::Top(0),
97            },
98        };
99
100        let size = constraints.max_size();
101
102        _ = children.each(ctx, |ctx, child, children| {
103            // size is determined by the constraint
104            _ = child.layout(children, ctx.viewport.constraints(), ctx)?;
105            Ok(ControlFlow::Break(()))
106        })?;
107
108        Ok(size)
109    }
110
111    fn position<'bp>(
112        &mut self,
113        mut children: PositionChildren<'_, 'bp>,
114        _: WidgetId,
115        attribute_storage: &AttributeStorage<'bp>,
116        mut ctx: PositionCtx,
117    ) {
118        if let Placement::Absolute = self.placement {
119            ctx.pos = Pos::ZERO;
120        }
121
122        _ = children.each(|child, children| {
123            match self.horz_edge {
124                HorzEdge::Left(left) => ctx.pos.x += left,
125                HorzEdge::Right(right) => {
126                    let offset = ctx.pos.x + ctx.inner_size.width as i32 - child.size().width as i32 - right;
127                    ctx.pos.x = offset;
128                }
129            }
130
131            match self.vert_edge {
132                VertEdge::Top(top) => ctx.pos.y += top,
133                VertEdge::Bottom(bottom) => {
134                    let offset = ctx.pos.y + ctx.inner_size.height as i32 - child.size().height as i32 - bottom;
135                    ctx.pos.y = offset;
136                }
137            }
138
139            child.position(children, ctx.pos, attribute_storage, ctx.viewport);
140            ControlFlow::Break(())
141        });
142    }
143
144    fn paint<'bp>(
145        &mut self,
146        mut children: PaintChildren<'_, 'bp>,
147        _id: WidgetId,
148        attribute_storage: &AttributeStorage<'bp>,
149        mut ctx: PaintCtx<'_, SizePos>,
150    ) {
151        _ = children.each(|child, children| {
152            let mut ctx = ctx.to_unsized();
153            ctx.clip = None;
154            child.paint(children, ctx, attribute_storage);
155            ControlFlow::Continue(())
156        });
157    }
158}
159
160#[cfg(test)]
161mod test {
162    use crate::testing::TestRunner;
163
164    #[test]
165    fn position_top_left() {
166        let tpl = "
167            position [top: 0, left: 0]
168                text 'hi'
169            ";
170
171        let expected = "
172            ╔════╗
173            ║hi  ║
174            ║    ║
175            ╚════╝
176        ";
177
178        TestRunner::new(tpl, (4, 2)).instance().render_assert(expected);
179    }
180
181    #[test]
182    fn position_top() {
183        let tpl = "
184            position [top: 1]
185                text 'hi'
186            ";
187
188        let expected = "
189            ╔════╗
190            ║    ║
191            ║hi  ║
192            ╚════╝
193        ";
194
195        TestRunner::new(tpl, (4, 2)).instance().render_assert(expected);
196    }
197
198    #[test]
199    fn position_top_right() {
200        let tpl = "
201            position [top: 1, right: 0]
202                text 'hi'
203            ";
204
205        let expected = "
206            ╔════╗
207            ║    ║
208            ║  hi║
209            ╚════╝
210        ";
211
212        TestRunner::new(tpl, (4, 2)).instance().render_assert(expected);
213    }
214
215    #[test]
216    fn position_right() {
217        let tpl = "
218            position [right: 0]
219                text 'hi'
220            ";
221
222        let expected = "
223            ╔════╗
224            ║  hi║
225            ║    ║
226            ╚════╝
227        ";
228
229        TestRunner::new(tpl, (4, 2)).instance().render_assert(expected);
230    }
231
232    #[test]
233    fn position_bottom_right() {
234        let tpl = "
235            position [placement: 'relative', bottom: 0, right: 0]
236                text 'hi'
237            ";
238
239        let expected = "
240            ╔════╗
241            ║    ║
242            ║  hi║
243            ╚════╝
244        ";
245
246        TestRunner::new(tpl, (4, 2)).instance().render_assert(expected);
247    }
248
249    #[test]
250    fn position_bottom() {
251        let tpl = "
252            position [bottom: 0]
253                text 'hi'
254            ";
255
256        let expected = "
257            ╔════╗
258            ║    ║
259            ║hi  ║
260            ╚════╝
261        ";
262
263        TestRunner::new(tpl, (4, 2)).instance().render_assert(expected);
264    }
265
266    #[test]
267    fn position_bottom_left() {
268        let tpl = "
269            position [bottom: 0, left: 1]
270                text 'hi'
271            ";
272
273        let expected = "
274            ╔════╗
275            ║    ║
276            ║ hi ║
277            ╚════╝
278        ";
279
280        TestRunner::new(tpl, (4, 2)).instance().render_assert(expected);
281    }
282}