1use super::{Widget, element::Element, scroll_core};
18use alloc::{boxed::Box, vec::Vec};
19use embedded_graphics::{pixelcolor::PixelColor, prelude::*, primitives::Rectangle};
20use zest_core::{
21 Constraints, Length, RenderError, Renderer, ScrollDirection, ScrollMsg, ScrollState,
22 ScrollbarMode, SnapMode, TouchPhase,
23};
24use zest_theme::Theme;
25
26struct ScrollCore<'a, M> {
28 state: ScrollState,
30 dir: ScrollDirection,
32 bar: ScrollbarMode,
34 snap: SnapMode,
36 on_scroll: Option<Box<dyn Fn(ScrollMsg) -> M + 'a>>,
38}
39
40pub struct Grid<'a, C: PixelColor, M: Clone> {
42 rect: Rectangle,
43 children: Vec<Element<'a, C, M>>,
44 cols: u32,
45 rows: u32,
46 spacing: u32,
47 width: Length,
48 height: Length,
49 scroll: Option<ScrollCore<'a, M>>,
50 content: Size,
52 child_origins: Vec<Point>,
54 child_sizes: Vec<Size>,
56}
57
58impl<'a, C: PixelColor + 'a, M: Clone + 'a> Grid<'a, C, M> {
59 pub fn new(cols: u32, rows: u32) -> Self {
62 Self {
63 rect: Rectangle::zero(),
64 children: Vec::new(),
65 cols,
66 rows,
67 spacing: 0,
68 width: Length::Fill,
69 height: Length::Fill,
70 scroll: None,
71 content: Size::zero(),
72 child_origins: Vec::new(),
73 child_sizes: Vec::new(),
74 }
75 }
76
77 #[must_use]
79 pub fn spacing(mut self, spacing: u32) -> Self {
80 self.spacing = spacing;
81 self
82 }
83
84 #[must_use]
86 pub fn width(mut self, width: impl Into<Length>) -> Self {
87 self.width = width.into();
88 self
89 }
90
91 #[must_use]
93 pub fn height(mut self, height: impl Into<Length>) -> Self {
94 self.height = height.into();
95 self
96 }
97
98 #[must_use]
102 pub fn push<W>(mut self, child: W) -> Self
103 where
104 W: Widget<C, M> + 'a,
105 {
106 self.children.push(Element::new(child));
107 self
108 }
109
110 #[must_use]
115 pub fn scrollable(mut self, dir: ScrollDirection) -> Self {
116 let core = self.scroll.get_or_insert(ScrollCore {
117 state: ScrollState::new(),
118 dir,
119 bar: ScrollbarMode::Auto,
120 snap: SnapMode::None,
121 on_scroll: None,
122 });
123 core.dir = dir;
124 self
125 }
126
127 #[must_use]
131 pub fn scroll_state(mut self, state: &ScrollState) -> Self {
132 let core = self.scroll.get_or_insert(ScrollCore {
133 state: *state,
134 dir: ScrollDirection::Both,
135 bar: ScrollbarMode::Auto,
136 snap: SnapMode::None,
137 on_scroll: None,
138 });
139 core.state = *state;
140 self
141 }
142
143 #[must_use]
145 pub fn scrollbar(mut self, mode: ScrollbarMode) -> Self {
146 let core = self.scroll.get_or_insert(ScrollCore {
147 state: ScrollState::new(),
148 dir: ScrollDirection::Both,
149 bar: mode,
150 snap: SnapMode::None,
151 on_scroll: None,
152 });
153 core.bar = mode;
154 self
155 }
156
157 #[must_use]
159 pub fn snap(mut self, mode: SnapMode) -> Self {
160 let core = self.scroll.get_or_insert(ScrollCore {
161 state: ScrollState::new(),
162 dir: ScrollDirection::Both,
163 bar: ScrollbarMode::Auto,
164 snap: mode,
165 on_scroll: None,
166 });
167 core.snap = mode;
168 self
169 }
170
171 #[must_use]
174 pub fn on_scroll<F>(mut self, f: F) -> Self
175 where
176 F: Fn(ScrollMsg) -> M + 'a,
177 {
178 let core = self.scroll.get_or_insert(ScrollCore {
179 state: ScrollState::new(),
180 dir: ScrollDirection::Both,
181 bar: ScrollbarMode::Auto,
182 snap: SnapMode::None,
183 on_scroll: None,
184 });
185 core.on_scroll = Some(Box::new(f));
186 self
187 }
188
189 fn snap_lines(&self) -> Vec<i32> {
192 match &self.scroll {
193 Some(core) if core.snap != SnapMode::None => {
194 let rects: Vec<Rectangle> = self
195 .child_origins
196 .iter()
197 .zip(self.child_sizes.iter())
198 .map(|(p, s)| Rectangle::new(*p, *s))
199 .collect();
200 let offset = scroll_core::render_offset(core.state, core.dir);
201 scroll_core::snap_lines(
202 &rects,
203 self.rect.top_left,
204 offset,
205 self.rect.size,
206 core.dir,
207 core.snap,
208 )
209 }
210 _ => Vec::new(),
211 }
212 }
213
214 fn relayout(&mut self) {
217 if self.cols == 0 || self.rows == 0 {
218 return;
219 }
220
221 let h_spacing = self.spacing * (self.cols.saturating_sub(1));
222 let v_spacing = self.spacing * (self.rows.saturating_sub(1));
223 let cell_width = self.rect.size.width.saturating_sub(h_spacing) / self.cols;
224 let cell_height = self.rect.size.height.saturating_sub(v_spacing) / self.rows;
225
226 for (idx, child) in self.children.iter_mut().enumerate() {
227 let row = (idx as u32) / self.cols;
228 let col = (idx as u32) % self.cols;
229
230 if row >= self.rows {
231 break;
232 }
233
234 let x = self.rect.top_left.x + (col * (cell_width + self.spacing)) as i32;
235 let y = self.rect.top_left.y + (row * (cell_height + self.spacing)) as i32;
236
237 child.arrange(Rectangle::new(
238 Point::new(x, y),
239 Size::new(cell_width, cell_height),
240 ));
241 }
242 }
243
244 fn relayout_scroll(&mut self, dir: ScrollDirection) {
247 self.child_origins.clear();
248 self.child_sizes.clear();
249 if self.cols == 0 || self.rows == 0 {
250 self.content = Size::zero();
251 return;
252 }
253
254 let h_spacing = self.spacing * (self.cols.saturating_sub(1));
257 let v_spacing = self.spacing * (self.rows.saturating_sub(1));
258 let cell_width = self.rect.size.width.saturating_sub(h_spacing) / self.cols.max(1);
259 let cell_height = self.rect.size.height.saturating_sub(v_spacing) / self.rows.max(1);
260
261 let n = self.children.len() as u32;
262 let total_rows = if n == 0 { 0 } else { n.div_ceil(self.cols) };
263
264 let content_w = if dir.scrolls_x() {
267 (cell_width.saturating_add(self.spacing))
268 .saturating_mul(self.cols)
269 .saturating_sub(self.spacing)
270 } else {
271 self.rect.size.width
272 };
273 let content_h = if dir.scrolls_y() {
274 (cell_height.saturating_add(self.spacing))
275 .saturating_mul(total_rows)
276 .saturating_sub(self.spacing)
277 } else {
278 self.rect.size.height
279 };
280 self.content = Size::new(content_w, content_h.max(self.rect.size.height));
281
282 let off = self
283 .scroll
284 .as_ref()
285 .map_or(Point::zero(), |c| scroll_core::render_offset(c.state, dir));
286 let base_x = self.rect.top_left.x - off.x;
287 let base_y = self.rect.top_left.y - off.y;
288
289 for (idx, child) in self.children.iter_mut().enumerate() {
290 let row = (idx as u32) / self.cols;
291 let col = (idx as u32) % self.cols;
292
293 let x = base_x + (col * (cell_width + self.spacing)) as i32;
294 let y = base_y + (row * (cell_height + self.spacing)) as i32;
295 let origin = Point::new(x, y);
296 let size = Size::new(cell_width, cell_height);
297 child.arrange(Rectangle::new(origin, size));
298 self.child_origins.push(origin);
299 self.child_sizes.push(size);
300 }
301 }
302}
303
304impl<'a, C: PixelColor + 'a, M: Clone + 'a> Widget<C, M> for Grid<'a, C, M> {
305 fn measure(&mut self, constraints: Constraints) -> Size {
306 let w = self
307 .width
308 .resolve(constraints.max.width, constraints.max.width);
309 let h = self
310 .height
311 .resolve(constraints.max.height, constraints.max.height);
312 constraints.clamp(Size::new(w, h))
313 }
314
315 fn preferred_size(&self) -> (Length, Length) {
316 (self.width, self.height)
317 }
318
319 fn arrange(&mut self, rect: Rectangle) {
320 self.rect = rect;
321 match self.scroll.as_ref().map(|c| c.dir) {
322 Some(dir) if dir != ScrollDirection::None => self.relayout_scroll(dir),
323 _ => self.relayout(),
324 }
325 }
326
327 fn rect(&self) -> Rectangle {
328 self.rect
329 }
330
331 fn handle_touch(&mut self, point: Point, phase: TouchPhase) -> Option<M> {
332 match self.scroll.as_ref() {
333 Some(core) if core.dir != ScrollDirection::None => {
334 let dir = core.dir;
335 let state = core.state;
336 let viewport = self.rect;
337 let content = self.content;
338 let lines = self.snap_lines();
339 let on_scroll = self.scroll.as_ref().and_then(|c| c.on_scroll.as_deref());
340 let children = &mut self.children;
341 scroll_core::route_touch(
342 state,
343 dir,
344 viewport,
345 content,
346 point,
347 phase,
348 &lines,
349 on_scroll,
350 |p, ph| {
351 for child in children.iter_mut().rev() {
352 if let Some(msg) = child.handle_touch(p, ph) {
353 return Some(msg);
354 }
355 }
356 None
357 },
358 )
359 }
360 _ => {
361 for child in self.children.iter_mut().rev() {
362 if let Some(msg) = child.handle_touch(point, phase) {
363 return Some(msg);
364 }
365 }
366 None
367 }
368 }
369 }
370
371 fn mark_pressed(&mut self, point: Point) {
372 if let Some(core) = self.scroll.as_ref() {
375 if matches!(
376 core.state.phase,
377 zest_core::GesturePhase::Dragging
378 | zest_core::GesturePhase::Flinging
379 | zest_core::GesturePhase::Springing
380 ) {
381 return;
382 }
383 }
384 for child in &mut self.children {
385 child.mark_pressed(point);
386 }
387 }
388
389 fn draw<'t>(
390 &self,
391 renderer: &mut dyn Renderer<C>,
392 theme: &Theme<'t, C>,
393 ) -> Result<(), RenderError> {
394 match self.scroll.as_ref() {
395 Some(core) if core.dir != ScrollDirection::None => {
396 let viewport = self.rect;
397 renderer.push_clip(viewport);
398 for child in &self.children {
399 child.draw(renderer, theme)?;
400 }
401 renderer.pop_clip();
402 scroll_core::draw_scrollbars(
403 renderer,
404 theme,
405 core.state,
406 core.bar,
407 core.dir,
408 viewport,
409 self.content,
410 )?;
411 Ok(())
412 }
413 _ => {
414 for child in &self.children {
415 child.draw(renderer, theme)?;
416 }
417 Ok(())
418 }
419 }
420 }
421}