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}