Skip to main content

zest_widget/widget/
grid.rs

1//! Equal-cell grid container. Children fill cells in row-major order.
2//!
3//! ## Scrolling
4//!
5//! A `Grid` becomes scrollable via [`Grid::scrollable`] plus
6//! [`Grid::scroll_state`]. The host owns a [`ScrollState`] (because widgets
7//! are transient) and passes it by reference each frame. Unlike the linear
8//! containers, a `Grid` supports [`ScrollDirection::Both`] for 2-D panning:
9//! the `cols × rows` slot defines the *visible* window and fixes the cell
10//! size, while any extra children extend the content beyond the viewport on
11//! the scrolling axes. The grid offsets every cell by
12//! [`scroll_core::render_offset`], clips the viewport, and draws scrollbars.
13//! See [`scroll_core`] for the shared engine. When no
14//! scroll is configured the layout/touch/draw paths are identical to a plain
15//! `Grid`.
16
17use 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
26/// Per-grid scroll configuration (present only when scrolling is enabled).
27struct ScrollCore<'a, M> {
28    /// Host-owned scroll state, read each frame.
29    state: ScrollState,
30    /// Which axes scroll.
31    dir: ScrollDirection,
32    /// When the scrollbar is shown.
33    bar: ScrollbarMode,
34    /// How scrolling settles to child boundaries.
35    snap: SnapMode,
36    /// Callback turning a [`ScrollMsg`] into the host message.
37    on_scroll: Option<Box<dyn Fn(ScrollMsg) -> M + 'a>>,
38}
39
40/// Grid of equal-size cells. Children are placed in row-major order.
41pub 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    /// Measured total content size (only meaningful when scrollable).
51    content: Size,
52    /// Un-scrolled child top-left positions, captured for snap lines.
53    child_origins: Vec<Point>,
54    /// Un-scrolled child sizes, captured for snap lines.
55    child_sizes: Vec<Size>,
56}
57
58impl<'a, C: PixelColor + 'a, M: Clone + 'a> Grid<'a, C, M> {
59    /// Create a `cols x rows` grid. Position and size are assigned by
60    /// the parent via `arrange`.
61    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    /// Gap between cells.
78    #[must_use]
79    pub fn spacing(mut self, spacing: u32) -> Self {
80        self.spacing = spacing;
81        self
82    }
83
84    /// Width sizing intent.
85    #[must_use]
86    pub fn width(mut self, width: impl Into<Length>) -> Self {
87        self.width = width.into();
88        self
89    }
90
91    /// Height sizing intent.
92    #[must_use]
93    pub fn height(mut self, height: impl Into<Length>) -> Self {
94        self.height = height.into();
95        self
96    }
97
98    /// Add a child. Excess children beyond `cols × rows` are
99    /// retained but not laid out (unless the grid is scrollable, in which
100    /// case they extend the content along the scrolling axes).
101    #[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    /// Make this grid scrollable on `dir` (including
111    /// [`ScrollDirection::Both`] for 2-D panning). Defaults the scrollbar to
112    /// [`ScrollbarMode::Auto`] and no snapping. Pair with
113    /// [`Grid::scroll_state`] to supply the host's [`ScrollState`].
114    #[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    /// Supply the host-owned [`ScrollState`] read this frame.
128    /// Implies scrolling (defaults to [`ScrollDirection::Both`] if
129    /// [`Grid::scrollable`] was not called first).
130    #[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    /// When the scrollbar is drawn. Implies scrolling.
144    #[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    /// Snapping mode. Implies scrolling.
158    #[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    /// Callback mapping a [`ScrollMsg`] to the host message. Implies
172    /// scrolling.
173    #[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    /// Snap-line candidates for the current layout, in offset space. Empty
190    /// when not snapping.
191    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    // ---- non-scrolling layout ------
215
216    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    // ---- scrolling layout ---------------------------------------------
245
246    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        // The `cols × rows` slot fixes the visible cell size. Extra children
255        // extend the content beyond the viewport on the scrolling axes.
256        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        // Content extent: full grid of all children when scrolling that axis,
265        // otherwise the viewport extent.
266        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        // While dragging/flinging/springing, stop re-asserting a child press
373        // so any button highlighted on Down is cancelled mid-drag.
374        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}