Skip to main content

anathema_default_widgets/
overflow.rs

1use std::ops::ControlFlow;
2
3use anathema_geometry::{Pos, 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::layout::many::Many;
11use crate::layout::{AXIS, Axis, DIRECTION, Direction};
12use crate::{HEIGHT, WIDTH};
13
14const UNCONSTRAINED: &str = "unconstrained";
15const CLAMP: &str = "clamp";
16
17#[derive(Debug, Default)]
18pub struct Overflow {
19    offset: Pos,
20    // The size of the children since the last layout call
21    inner_size: Size,
22
23    direction: Direction,
24    is_dirty: bool,
25}
26
27impl Overflow {
28    pub fn scroll(&mut self, direction: Direction, amount: Pos) {
29        self.is_dirty = true;
30
31        match (self.direction, direction) {
32            (Direction::Forward, Direction::Forward) => self.offset += amount,
33            (Direction::Forward, Direction::Backward) => self.offset -= amount,
34            (Direction::Backward, Direction::Backward) => self.offset += amount,
35            (Direction::Backward, Direction::Forward) => self.offset -= amount,
36        }
37    }
38
39    pub fn scroll_up(&mut self) {
40        self.scroll(Direction::Backward, Pos { x: 0, y: 1 });
41    }
42
43    pub fn scroll_up_by(&mut self, amount: i32) {
44        self.scroll(Direction::Backward, Pos { x: 0, y: amount });
45    }
46
47    pub fn scroll_down(&mut self) {
48        self.scroll(Direction::Forward, Pos { x: 0, y: 1 });
49    }
50
51    pub fn scroll_down_by(&mut self, amount: i32) {
52        self.scroll(Direction::Forward, Pos { x: 0, y: amount });
53    }
54
55    pub fn scroll_right(&mut self) {
56        self.scroll(Direction::Forward, Pos { x: 1, y: 0 });
57    }
58
59    pub fn scroll_right_by(&mut self, amount: i32) {
60        self.scroll(Direction::Forward, Pos { x: amount, y: 0 });
61    }
62
63    pub fn scroll_left(&mut self) {
64        self.scroll(Direction::Backward, Pos { x: 1, y: 0 });
65    }
66
67    pub fn scroll_left_by(&mut self, amount: i32) {
68        self.scroll(Direction::Backward, Pos { x: amount, y: 0 });
69    }
70
71    pub fn scroll_to(&mut self, pos: Pos) {
72        self.offset = pos;
73    }
74
75    pub fn offset(&self) -> Pos {
76        self.offset
77    }
78
79    fn clamp(&mut self, children: Size, parent: Size) {
80        if self.offset.x < 0 {
81            self.offset.x = 0;
82        }
83
84        if self.offset.y < 0 {
85            self.offset.y = 0;
86        }
87
88        if children.height <= parent.height {
89            self.offset.y = 0;
90        } else {
91            let max_y = children.height as i32 - parent.height as i32;
92            if self.offset.y > max_y {
93                self.offset.y = max_y;
94            }
95        }
96
97        if children.width <= parent.width {
98            self.offset.x = 0;
99        } else {
100            let max_x = children.width as i32 - parent.width as i32;
101            if self.offset.x > max_x {
102                self.offset.x = max_x
103            }
104        }
105    }
106}
107
108impl Widget for Overflow {
109    fn layout<'bp>(
110        &mut self,
111        children: LayoutForEach<'_, 'bp>,
112        mut constraints: Constraints,
113        id: WidgetId,
114        ctx: &mut LayoutCtx<'_, 'bp>,
115    ) -> Result<Size> {
116        let attributes = ctx.attribute_storage.get(id);
117        let axis = attributes.get_as(AXIS).unwrap_or(Axis::Vertical);
118
119        let output_size: Size = (constraints.max_width(), constraints.max_height()).into();
120
121        match axis {
122            Axis::Horizontal => constraints.unbound_width(),
123            Axis::Vertical => constraints.unbound_height(),
124        }
125
126        if attributes.get_as::<bool>(UNCONSTRAINED).unwrap_or_default() {
127            constraints.unbound_width();
128            constraints.unbound_height();
129        }
130
131        if let Some(width) = attributes.get_as::<u16>(WIDTH) {
132            constraints.make_width_tight(width);
133        }
134
135        if let Some(height) = attributes.get_as::<u16>(HEIGHT) {
136            constraints.make_height_tight(height);
137        }
138
139        self.direction = attributes.get_as(DIRECTION).unwrap_or_default();
140
141        // Make `unconstrained` an enum instead of a `bool`
142        let unconstrained = true;
143        let mut many = Many::new(self.direction, axis, unconstrained);
144
145        // NOTE: we use the inner size here from many.layout
146        _ = many.layout(children, constraints, ctx)?;
147
148        self.inner_size = many.used_size.inner_size();
149
150        Ok(output_size)
151    }
152
153    fn position<'bp>(
154        &mut self,
155        mut children: PositionChildren<'_, 'bp>,
156        id: WidgetId,
157        attribute_storage: &AttributeStorage<'bp>,
158        ctx: PositionCtx,
159    ) {
160        let attributes = attribute_storage.get(id);
161        let direction = attributes.get_as(DIRECTION).unwrap_or_default();
162        let axis = attributes.get_as(AXIS).unwrap_or(Axis::Vertical);
163        let mut pos = ctx.pos;
164
165        // If the value is clamped, update the offset
166        match attributes.get_as::<bool>(CLAMP).unwrap_or_default() {
167            false => (),
168            true => self.clamp(self.inner_size, ctx.inner_size),
169        }
170
171        if let Direction::Backward = direction {
172            match axis {
173                Axis::Horizontal => pos.x += ctx.inner_size.width as i32,
174                Axis::Vertical => pos.y += ctx.inner_size.height as i32,
175            }
176        }
177
178        let mut pos = match direction {
179            Direction::Forward => pos - self.offset,
180            Direction::Backward => pos + self.offset,
181        };
182
183        let mut count = 0;
184        _ = children.each(|node, children| {
185            match direction {
186                Direction::Forward => {
187                    node.position(children, pos, attribute_storage, ctx.viewport);
188                    match axis {
189                        Axis::Horizontal => pos.x += node.size().width as i32,
190                        Axis::Vertical => pos.y += node.size().height as i32,
191                    }
192                }
193                Direction::Backward => {
194                    match axis {
195                        Axis::Horizontal => pos.x -= node.size().width as i32,
196                        Axis::Vertical => pos.y -= node.size().height as i32,
197                    }
198                    node.position(children, pos, attribute_storage, ctx.viewport);
199                }
200            }
201
202            count += 1;
203            ControlFlow::Continue(())
204        });
205    }
206
207    fn paint<'bp>(
208        &mut self,
209        mut children: PaintChildren<'_, 'bp>,
210        _: WidgetId,
211        attribute_storage: &AttributeStorage<'bp>,
212        mut ctx: PaintCtx<'_, SizePos>,
213    ) {
214        let region = ctx.create_region();
215
216        _ = children.each(|widget, children| {
217            ctx.set_clip_region(region);
218            let ctx = ctx.to_unsized();
219            widget.paint(children, ctx, attribute_storage);
220            ControlFlow::Continue(())
221        });
222    }
223
224    fn needs_reflow(&mut self) -> bool {
225        let needs_reflow = self.is_dirty;
226        self.is_dirty = false;
227        needs_reflow
228    }
229}
230
231#[cfg(test)]
232mod test {
233
234    use crate::Overflow;
235    use crate::testing::TestRunner;
236
237    #[test]
238    fn overflow() {
239        let tpl = "
240    overflow
241        for i in [0, 1, 2]
242            border
243                text i
244";
245
246        let expected_first = "
247    ╔═══╗
248    ║┌─┐║
249    ║│0│║
250    ║└─┘║
251    ║┌─┐║
252    ║│1│║
253    ║└─┘║
254    ╚═══╝
255";
256
257        let expected_second = "
258    ╔═══╗
259    ║│0│║
260    ║└─┘║
261    ║┌─┐║
262    ║│1│║
263    ║└─┘║
264    ║┌─┐║
265    ╚═══╝
266";
267
268        TestRunner::new(tpl, (3, 6))
269            .instance()
270            .render_assert(expected_first)
271            .with_widget(|mut query| {
272                query.by_tag("overflow").first(|el, _| {
273                    let overflow = el.to::<Overflow>();
274                    overflow.scroll_down();
275                });
276            })
277            .render_assert(expected_second);
278    }
279
280    #[test]
281    fn clamp_prevents_scrolling() {
282        let tpl = "
283    overflow
284        text '0'";
285
286        let expected_first = "
287    ╔═══╗
288    ║0  ║
289    ║   ║
290    ╚═══╝
291";
292
293        TestRunner::new(tpl, (3, 2))
294            .instance()
295            .with_widget(|mut query| {
296                query.by_tag("overflow").first(|el, _| {
297                    let overflow = el.to::<Overflow>();
298                    overflow.scroll_down();
299                });
300            })
301            .render_assert(expected_first);
302    }
303}