iris_ui/
grid.rs

1use crate::geom::{Bounds, Point};
2use crate::view::Flex::{Intrinsic, Resize};
3use crate::view::{Align, View, ViewId};
4use crate::{DrawEvent, LayoutEvent};
5use alloc::boxed::Box;
6use embedded_graphics::pixelcolor::{Rgb565, RgbColor};
7use hashbrown::HashMap;
8
9pub struct GridLayoutState {
10    pub constraints: HashMap<ViewId, LayoutConstraint>,
11    row_count: usize,
12    col_count: usize,
13    col_width: usize,
14    row_height: usize,
15    pub debug: bool,
16}
17
18impl GridLayoutState {
19    pub fn new_row_column(
20        row_count: usize,
21        row_height: usize,
22        col_count: usize,
23        col_width: usize,
24    ) -> GridLayoutState {
25        GridLayoutState {
26            constraints: HashMap::new(),
27            col_count,
28            row_count,
29            col_width,
30            row_height,
31            debug: false,
32        }
33    }
34}
35
36impl GridLayoutState {
37    pub fn place_at_row_column(
38        &mut self,
39        name: &ViewId,
40        row: usize,
41        col: usize,
42    ) -> Option<LayoutConstraint> {
43        self.constraints
44            .insert(name.clone(), LayoutConstraint::at_row_column(row, col))
45    }
46}
47
48pub struct LayoutConstraint {
49    pub col: usize,
50    pub row: usize,
51    pub col_span: usize,
52    pub row_span: usize,
53}
54
55impl LayoutConstraint {
56    pub fn at_row_column(row: usize, col: usize) -> LayoutConstraint {
57        LayoutConstraint {
58            col,
59            row,
60            col_span: 1,
61            row_span: 1,
62        }
63    }
64}
65
66pub fn make_grid_panel(name: &ViewId) -> View {
67    View {
68        name: name.clone(),
69        title: name.as_str().into(),
70        state: Some(Box::new(GridLayoutState {
71            constraints: HashMap::new(),
72            col_count: 2,
73            row_count: 2,
74            col_width: 100,
75            row_height: 30,
76            debug: false,
77        })),
78        layout: Some(layout_grid),
79        draw: Some(draw_grid),
80        visible: true,
81        ..Default::default()
82    }
83}
84
85fn draw_grid(evt: &mut DrawEvent) {
86    let bounds = evt.view.bounds;
87    evt.ctx.fill_rect(&evt.view.bounds, &evt.theme.bg);
88    evt.ctx.stroke_rect(&evt.view.bounds, &evt.theme.fg);
89    let padding = evt.view.padding;
90    if let Some(state) = evt.view.get_state::<GridLayoutState>() {
91        if state.debug {
92            for i in 0..state.col_count + 1 {
93                let x = (i * state.col_width) as i32 + bounds.x() + padding.left;
94                let y = bounds.y() + padding.top;
95                let y2 = bounds.y() + bounds.h() - padding.top * 2;
96                evt.ctx
97                    .line(&Point::new(x, y), &Point::new(x, y2), &Rgb565::RED);
98            }
99            for j in 0..state.row_count + 1 {
100                let y = (j * state.row_height) as i32 + bounds.y() + padding.top;
101                let x = bounds.x() + padding.left;
102                let x2 = bounds.x() + bounds.w() - padding.left * 2;
103                evt.ctx
104                    .line(&Point::new(x, y), &Point::new(x2, y), &Rgb565::RED);
105            }
106        }
107    }
108}
109
110fn layout_grid(pass: &mut LayoutEvent) {
111    if let Some(view) = pass.scene.get_view_mut(pass.target) {
112        if view.h_flex == Resize {
113            view.bounds.size.w = pass.space.w;
114        }
115        if view.h_flex == Intrinsic {}
116        if view.v_flex == Resize {
117            view.bounds.size.h = pass.space.h;
118        }
119        if view.v_flex == Intrinsic {}
120
121        let parent_bounds = view.bounds.clone();
122        let padding = view.padding.clone();
123        let kids = pass.scene.get_children_ids(pass.target);
124        let space = parent_bounds.size.clone() - padding;
125        for kid in kids {
126            pass.layout_child(&kid, space);
127            if let Some(state) = pass.scene.get_view_state::<GridLayoutState>(pass.target) {
128                let cell_bounds = if let Some(cons) = &state.constraints.get(&kid) {
129                    let x = (cons.col * state.col_width) as i32 + padding.left;
130                    let y = (cons.row * state.row_height) as i32 + padding.top;
131                    let w = state.col_width as i32 * cons.col_span as i32;
132                    let h = state.row_height as i32 * cons.row_span as i32;
133                    Bounds::new(x, y, w, h)
134                } else {
135                    Bounds::new(0, 0, 0, 0)
136                };
137                if let Some(view) = pass.scene.get_view_mut(&kid) {
138                    view.bounds.position.x = match &view.h_align {
139                        Align::Start => cell_bounds.x(),
140                        Align::Center => cell_bounds.x() + (cell_bounds.w() - view.bounds.w()) / 2,
141                        Align::End => cell_bounds.x() + cell_bounds.w() - view.bounds.w(),
142                    };
143                    view.bounds.position.y = match &view.v_align {
144                        Align::Start => cell_bounds.y(),
145                        Align::Center => cell_bounds.y() + (cell_bounds.h() - view.bounds.h()) / 2,
146                        Align::End => cell_bounds.y() + cell_bounds.h() - view.bounds.h(),
147                    };
148                }
149            }
150        }
151    }
152}
153
154impl Into<ViewId> for &'static str {
155    fn into(self) -> ViewId {
156        ViewId::new(self)
157    }
158}
159
160mod tests {
161    use crate::button::make_button;
162    use crate::geom::Bounds;
163    use crate::grid::{GridLayoutState, LayoutConstraint, make_grid_panel};
164    use crate::label::make_label;
165    use crate::scene::{Scene, draw_scene, layout_scene};
166    use crate::test::MockDrawingContext;
167    use crate::view::Align::Start;
168    use crate::view::ViewId;
169    use alloc::boxed::Box;
170
171    #[test]
172    fn test_grid_layout() {
173        let theme = MockDrawingContext::make_mock_theme();
174
175        let mut grid = make_grid_panel(&ViewId::new("grid"));
176        grid.bounds = Bounds::new(40, 40, 200, 200);
177        let mut grid_layout = GridLayoutState::new_row_column(2, 30, 2, 100);
178
179        let mut scene = Scene::new_with_bounds(Bounds::new(0, 0, 320, 240));
180
181        let mut label1 = make_label("label1", "Label 1");
182        label1.h_align = Start;
183        label1.v_align = Start;
184        grid_layout.place_at_row_column(&label1.name, 0, 0);
185        scene.add_view_to_parent(label1, &grid.name);
186
187        let mut label2 = make_label("label2", "Label 2");
188        label2.h_align = Start;
189        label2.v_align = Start;
190        grid_layout.place_at_row_column(&label2.name, 0, 1);
191        scene.add_view_to_parent(label2, &grid.name);
192
193        let mut label3 = make_label("label3", "Label 3");
194        label3.h_align = Start;
195        label3.v_align = Start;
196        grid_layout.place_at_row_column(&label3.name, 1, 0);
197        scene.add_view_to_parent(label3, &grid.name);
198
199        grid.state = Some(Box::new(grid_layout));
200        scene.add_view_to_root(grid);
201
202        layout_scene(&mut scene, &theme);
203
204        {
205            let label1 = scene.get_view(&ViewId::new("label1")).unwrap();
206            assert_eq!(label1.name, ViewId::new("label1"));
207            assert_eq!(label1.bounds, Bounds::new(0, 0, 54, 20));
208
209            let label2 = scene.get_view(&ViewId::new("label2")).unwrap();
210            assert_eq!(label2.name, ViewId::new("label2"));
211            assert_eq!(label2.bounds, Bounds::new(100, 0, 54, 20));
212
213            let label3 = scene.get_view(&ViewId::new("label3")).unwrap();
214            assert_eq!(label3.name, ViewId::new("label3"));
215            assert_eq!(label3.bounds, Bounds::new(0, 30, 54, 20));
216        }
217
218        let mut ctx = MockDrawingContext::new(&scene);
219
220        assert_eq!(scene.dirty, true);
221        draw_scene(&mut scene, &mut ctx, &theme);
222        assert_eq!(scene.dirty, false);
223    }
224
225    #[test]
226    fn col_span() {
227        let theme = MockDrawingContext::make_mock_theme();
228        let mut grid = make_grid_panel(&ViewId::new("grid"))
229            .position_at(0, 0)
230            .with_size(200, 200);
231        let mut layout = GridLayoutState::new_row_column(2, 30, 2, 100);
232        layout.debug = true;
233        let mut scene = Scene::new_with_bounds(Bounds::new(0, 0, 320, 240));
234
235        let button = make_button(&"b1".into(), "b1");
236        layout.constraints.insert(
237            button.name.clone(),
238            LayoutConstraint {
239                col: 0,
240                row: 0,
241                col_span: 2,
242                row_span: 1,
243            },
244        );
245
246        grid.state = Some(Box::new(layout));
247        scene.add_view_to_parent(button, &grid.name);
248        scene.add_view_to_root(grid);
249        layout_scene(&mut scene, &theme);
250
251        if let Some(view) = scene.get_view(&ViewId::new("b1")) {
252            assert_eq!(view.bounds, Bounds::new(86, 2, 28, 25));
253        }
254    }
255}