pixels_graphics_lib/ui/layout/
column.rs

1use crate::ui::prelude::*;
2#[cfg(feature = "serde")]
3use serde::{Deserialize, Serialize};
4
5#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
6#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
7pub enum ColumnGravity {
8    Left,
9    Center,
10    Right,
11}
12
13/// Position a collection of views into a column
14/// This doesn't act as a container or parent and only moves the views and isn't needed after it's been used
15#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
16#[derive(Debug, Clone, Eq, PartialEq)]
17pub struct ColumnLayout {
18    pub padding: usize,
19    pub spacing: usize,
20    pub bounds: Rect,
21    pub gravity: ColumnGravity,
22}
23
24impl ColumnLayout {
25    ///# Arguments
26    ///* `padding` - Pixel distance to move all views from the topleft or bottomright (depending on gravity)
27    ///* `spacing` - Pixel distance to leave between views
28    ///* `bounds` - Depends on gravity, if it's
29    ///
30    /// | Gravity | Effects |
31    /// |---------|---------|
32    /// |Left| bounds.top_left() is the starting point, bottom and right are ignored|
33    /// |Right| bounds.bottom_right() is the starting point, top and left are ignored|
34    /// |Center| bounds.top_left() is the starting point, views will be positioned between left and right|
35    ///
36    ///* `gravity` - Effects how views are positioned, see above
37    pub fn new(padding: usize, spacing: usize, bounds: Rect, gravity: ColumnGravity) -> Self {
38        Self {
39            padding,
40            spacing,
41            bounds,
42            gravity,
43        }
44    }
45
46    pub fn new_from_topleft(topleft: Coord) -> Self {
47        Self::new(
48            0,
49            0,
50            Rect::new_with_size(topleft, 1000000, 100000),
51            ColumnGravity::Left,
52        )
53    }
54
55    pub fn new_bounded(bounds: Rect) -> Self {
56        Self::new(0, 0, bounds, ColumnGravity::Left)
57    }
58}
59
60impl ColumnLayout {
61    /// Reposition views in a column
62    pub fn layout(&self, views: &mut [&mut dyn PixelView]) {
63        let mut y = self.padding;
64        for view in views {
65            let x = match self.gravity {
66                ColumnGravity::Left => self.padding,
67                ColumnGravity::Center => {
68                    (self.bounds.width() / 2).saturating_sub(view.bounds().width() / 2)
69                }
70                ColumnGravity::Right => self
71                    .bounds
72                    .width()
73                    .saturating_sub(view.bounds().width())
74                    .saturating_sub(self.padding),
75            };
76            view.set_position(self.bounds.top_left() + (x, y));
77            y += view.bounds().height();
78            y += self.spacing;
79        }
80    }
81}
82
83#[cfg(test)]
84mod test {
85    use crate::ui::prelude::*;
86
87    #[test]
88    fn column_defaults() {
89        let style = UiStyle::default();
90        let mut view1 = Button::new((0, 0), "Test", None, &style.button);
91        let mut view2 = Button::new((0, 0), "Another", None, &style.button);
92        let layout = ColumnLayout {
93            padding: 0,
94            spacing: 0,
95            bounds: Rect::new((0, 0), (100, 100)),
96            gravity: ColumnGravity::Left,
97        };
98        layout.layout(&mut [&mut view1, &mut view2]);
99
100        assert_eq!(view1.bounds().top_left(), coord!(0, 0));
101        assert_eq!(view2.bounds().top_left(), coord!(0, 16));
102    }
103
104    #[test]
105    fn column_defaults_with_padding() {
106        let style = UiStyle::default();
107        let mut view1 = Button::new((0, 0), "Test", None, &style.button);
108        let mut view2 = Button::new((0, 0), "Another", None, &style.button);
109        let layout = ColumnLayout {
110            padding: 10,
111            spacing: 0,
112            bounds: Rect::new((0, 0), (100, 100)),
113            gravity: ColumnGravity::Left,
114        };
115        layout.layout(&mut [&mut view1, &mut view2]);
116
117        assert_eq!(view1.bounds().top_left(), coord!(10, 10));
118        assert_eq!(view2.bounds().top_left(), coord!(10, 26));
119    }
120
121    #[test]
122    fn column_defaults_with_spacing() {
123        let style = UiStyle::default();
124        let mut view1 = Button::new((0, 0), "Test", None, &style.button);
125        let mut view2 = Button::new((0, 0), "Another", None, &style.button);
126        let layout = ColumnLayout {
127            padding: 0,
128            spacing: 8,
129            bounds: Rect::new((0, 0), (100, 100)),
130            gravity: ColumnGravity::Left,
131        };
132        layout.layout(&mut [&mut view1, &mut view2]);
133
134        assert_eq!(view1.bounds().top_left(), coord!(0, 0));
135        assert_eq!(view2.bounds().top_left(), coord!(0, 24));
136    }
137
138    #[test]
139    fn column_defaults_with_spacing_and_padding() {
140        let style = UiStyle::default();
141        let mut view1 = Button::new((0, 0), "Test", None, &style.button);
142        let mut view2 = Button::new((0, 0), "Another", None, &style.button);
143        let layout = ColumnLayout {
144            padding: 10,
145            spacing: 8,
146            bounds: Rect::new((0, 0), (100, 100)),
147            gravity: ColumnGravity::Left,
148        };
149        layout.layout(&mut [&mut view1, &mut view2]);
150
151        assert_eq!(view1.bounds().top_left(), coord!(10, 10));
152        assert_eq!(view2.bounds().top_left(), coord!(10, 34));
153    }
154
155    #[test]
156    fn column_defaults_with_gravity_right() {
157        let style = UiStyle::default();
158        let mut view1 = Button::new((0, 0), "Test", None, &style.button);
159        let mut view2 = Button::new((0, 0), "Another", None, &style.button);
160        let layout = ColumnLayout {
161            padding: 0,
162            spacing: 0,
163            bounds: Rect::new((0, 0), (100, 100)),
164            gravity: ColumnGravity::Right,
165        };
166        layout.layout(&mut [&mut view1, &mut view2]);
167
168        assert_eq!(view1.bounds().top_left(), coord!(67, 0));
169        assert_eq!(view2.bounds().top_left(), coord!(42, 16));
170    }
171
172    #[test]
173    fn column_defaults_with_gravity_right_and_padding() {
174        let style = UiStyle::default();
175        let mut view1 = Button::new((0, 0), "Test", None, &style.button);
176        let mut view2 = Button::new((0, 0), "Another", None, &style.button);
177        let layout = ColumnLayout {
178            padding: 20,
179            spacing: 0,
180            bounds: Rect::new((0, 0), (100, 100)),
181            gravity: ColumnGravity::Right,
182        };
183        layout.layout(&mut [&mut view1, &mut view2]);
184
185        assert_eq!(view1.bounds().top_left(), coord!(47, 20));
186        assert_eq!(view2.bounds().top_left(), coord!(22, 36));
187    }
188
189    #[test]
190    fn column_defaults_with_gravity_center() {
191        let style = UiStyle::default();
192        let mut view1 = Button::new((0, 0), "Test", None, &style.button);
193        let mut view2 = Button::new((0, 0), "Another", None, &style.button);
194        let mut layout = ColumnLayout {
195            padding: 0,
196            spacing: 0,
197            bounds: Rect::new((0, 0), (100, 100)),
198            gravity: ColumnGravity::Center,
199        };
200        layout.layout(&mut [&mut view1, &mut view2]);
201
202        assert_eq!(view1.bounds().top_left(), coord!(34, 0));
203        assert_eq!(view2.bounds().top_left(), coord!(21, 16));
204
205        layout.padding = 20;
206        layout.layout(&mut [&mut view1, &mut view2]);
207
208        assert_eq!(view1.bounds().top_left(), coord!(34, 20));
209        assert_eq!(view2.bounds().top_left(), coord!(21, 36));
210    }
211}