Skip to main content

zest_widget/widget/
container.rs

1//! Bounds-aware single-child wrapper. Applies padding and forwards
2//! the touch / draw protocol to its inner widget.
3//!
4//! ## Scrolling
5//!
6//! A `Container` becomes scrollable via [`Container::scrollable`] plus
7//! [`Container::scroll_state`]. The host owns a [`ScrollState`] (because
8//! widgets are transient) and passes it by reference each frame; the
9//! container reads it during layout/draw and emits [`ScrollMsg`] through
10//! [`Container::on_scroll`]. The single child is measured against
11//! `UNBOUNDED` on the scrolling axis so its intrinsic content extent is
12//! known, then offset by [`scroll_core::render_offset`]. See
13//! [`scroll_core`] for the shared engine. When no
14//! scroll is configured the layout/touch/draw paths are identical to a
15//! plain `Container`.
16
17use super::{Widget, element::Element, scroll_core};
18use alloc::boxed::Box;
19use embedded_graphics::{pixelcolor::PixelColor, prelude::*, primitives::Rectangle};
20use zest_core::{
21    Constraints, Length, RenderError, Renderer, ScrollDirection, ScrollMsg, ScrollState,
22    ScrollbarMode, SnapMode, TouchPhase, UiAction, WidgetId,
23};
24use zest_theme::Theme;
25
26/// Per-container 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/// Holds one child inside a bounded region with optional padding.
41pub struct Container<'a, C: PixelColor, M: Clone> {
42    rect: Rectangle,
43    padding: u32,
44    child: Option<Element<'a, C, M>>,
45    width: Length,
46    height: Length,
47    scroll: Option<ScrollCore<'a, M>>,
48    /// Measured content size of the child (only meaningful when scrollable).
49    content: Size,
50}
51
52impl<'a, C: PixelColor + 'a, M: Clone + 'a> Container<'a, C, M> {
53    /// Create a new empty container. The parent assigns position and
54    /// size via `arrange`; use `.width(...)` / `.height(...)` to
55    /// constrain the slot.
56    pub fn new() -> Self {
57        Self {
58            rect: Rectangle::zero(),
59            padding: 0,
60            child: None,
61            width: Length::Fill,
62            height: Length::Fill,
63            scroll: None,
64            content: Size::zero(),
65        }
66    }
67
68    /// Padding inset on all sides.
69    #[must_use]
70    pub fn padding(mut self, padding: u32) -> Self {
71        self.padding = padding;
72        self
73    }
74
75    /// Width sizing intent.
76    #[must_use]
77    pub fn width(mut self, width: impl Into<Length>) -> Self {
78        self.width = width.into();
79        self
80    }
81
82    /// Height sizing intent.
83    #[must_use]
84    pub fn height(mut self, height: impl Into<Length>) -> Self {
85        self.height = height.into();
86        self
87    }
88
89    /// Set the child widget. The parent will call `arrange`
90    /// later, which propagates the inner rect to the child.
91    #[must_use]
92    pub fn child<W>(mut self, child: W) -> Self
93    where
94        W: Widget<C, M> + 'a,
95    {
96        self.child = Some(Element::new(child));
97        self
98    }
99
100    /// Make this container scrollable on `dir`. Defaults the
101    /// scrollbar to [`ScrollbarMode::Auto`] and no snapping. Pair with
102    /// [`Container::scroll_state`] to supply the host's [`ScrollState`].
103    #[must_use]
104    pub fn scrollable(mut self, dir: ScrollDirection) -> Self {
105        let core = self.scroll.get_or_insert(ScrollCore {
106            state: ScrollState::new(),
107            dir,
108            bar: ScrollbarMode::Auto,
109            snap: SnapMode::None,
110            on_scroll: None,
111        });
112        core.dir = dir;
113        self
114    }
115
116    /// Supply the host-owned [`ScrollState`] read this frame.
117    /// Implies scrolling (defaults to [`ScrollDirection::Vertical`] if
118    /// [`Container::scrollable`] was not called first).
119    #[must_use]
120    pub fn scroll_state(mut self, state: &ScrollState) -> Self {
121        let core = self.scroll.get_or_insert(ScrollCore {
122            state: *state,
123            dir: ScrollDirection::Vertical,
124            bar: ScrollbarMode::Auto,
125            snap: SnapMode::None,
126            on_scroll: None,
127        });
128        core.state = *state;
129        self
130    }
131
132    /// When the scrollbar is drawn. Implies scrolling.
133    #[must_use]
134    pub fn scrollbar(mut self, mode: ScrollbarMode) -> Self {
135        let core = self.scroll.get_or_insert(ScrollCore {
136            state: ScrollState::new(),
137            dir: ScrollDirection::Vertical,
138            bar: mode,
139            snap: SnapMode::None,
140            on_scroll: None,
141        });
142        core.bar = mode;
143        self
144    }
145
146    /// Snapping mode. Implies scrolling.
147    #[must_use]
148    pub fn snap(mut self, mode: SnapMode) -> Self {
149        let core = self.scroll.get_or_insert(ScrollCore {
150            state: ScrollState::new(),
151            dir: ScrollDirection::Vertical,
152            bar: ScrollbarMode::Auto,
153            snap: mode,
154            on_scroll: None,
155        });
156        core.snap = mode;
157        self
158    }
159
160    /// Callback mapping a [`ScrollMsg`] to the host message. Implies
161    /// scrolling.
162    #[must_use]
163    pub fn on_scroll<F>(mut self, f: F) -> Self
164    where
165        F: Fn(ScrollMsg) -> M + 'a,
166    {
167        let core = self.scroll.get_or_insert(ScrollCore {
168            state: ScrollState::new(),
169            dir: ScrollDirection::Vertical,
170            bar: ScrollbarMode::Auto,
171            snap: SnapMode::None,
172            on_scroll: None,
173        });
174        core.on_scroll = Some(Box::new(f));
175        self
176    }
177
178    fn inner_rect(&self) -> Rectangle {
179        let pad = self.padding as i32;
180        let pad_u = self.padding;
181        Rectangle::new(
182            self.rect.top_left + Point::new(pad, pad),
183            Size::new(
184                self.rect.size.width.saturating_sub(pad_u * 2),
185                self.rect.size.height.saturating_sub(pad_u * 2),
186            ),
187        )
188    }
189
190    /// Snap-line candidates for the current layout, in offset space. Empty
191    /// when not snapping.
192    fn snap_lines(&self) -> alloc::vec::Vec<i32> {
193        match &self.scroll {
194            Some(core) if core.snap != SnapMode::None => {
195                let viewport = self.inner_rect();
196                let off = scroll_core::render_offset(core.state, core.dir);
197                // Reconstruct the un-scrolled child rect from cached content.
198                let rect = Rectangle::new(viewport.top_left, self.content);
199                scroll_core::snap_lines(
200                    &[rect],
201                    viewport.top_left,
202                    off,
203                    viewport.size,
204                    core.dir,
205                    core.snap,
206                )
207            }
208            _ => alloc::vec::Vec::new(),
209        }
210    }
211
212    /// Layout the child for the scrolling path: measure against UNBOUNDED on
213    /// the scrolling axis, record `content`, and offset by `-render_offset`.
214    fn arrange_scroll(&mut self, dir: ScrollDirection) {
215        let viewport = self.inner_rect();
216        let measure_w = if dir.scrolls_x() {
217            zest_core::UNBOUNDED
218        } else {
219            viewport.size.width
220        };
221        let measure_h = if dir.scrolls_y() {
222            zest_core::UNBOUNDED
223        } else {
224            viewport.size.height
225        };
226        let off = self
227            .scroll
228            .as_ref()
229            .map_or(Point::zero(), |c| scroll_core::render_offset(c.state, dir));
230        if let Some(child) = self.child.as_mut() {
231            let measured = child.measure(Constraints::loose(Size::new(measure_w, measure_h)));
232            // The child fills the viewport on a non-scrolling axis and uses
233            // its intrinsic size on a scrolling one.
234            let content = Size::new(
235                if dir.scrolls_x() {
236                    measured.width.max(viewport.size.width)
237                } else {
238                    viewport.size.width
239                },
240                if dir.scrolls_y() {
241                    measured.height.max(viewport.size.height)
242                } else {
243                    viewport.size.height
244                },
245            );
246            self.content = content;
247            child.arrange(Rectangle::new(viewport.top_left - off, content));
248        } else {
249            self.content = Size::zero();
250        }
251    }
252}
253
254impl<'a, C: PixelColor + 'a, M: Clone + 'a> Default for Container<'a, C, M> {
255    fn default() -> Self {
256        Self::new()
257    }
258}
259
260impl<'a, C: PixelColor + 'a, M: Clone + 'a> Widget<C, M> for Container<'a, C, M> {
261    fn measure(&mut self, constraints: Constraints) -> Size {
262        let dx = 2 * self.padding;
263        let dy = 2 * self.padding;
264        let inner = constraints.shrink(dx, dy);
265        let child_size = self
266            .child
267            .as_mut()
268            .map_or(Size::zero(), |c| c.measure(inner));
269        let intrinsic_w = child_size.width.saturating_add(dx);
270        let intrinsic_h = child_size.height.saturating_add(dy);
271        let w = self.width.resolve(intrinsic_w, constraints.max.width);
272        let h = self.height.resolve(intrinsic_h, constraints.max.height);
273        constraints.clamp(Size::new(w, h))
274    }
275
276    fn preferred_size(&self) -> (Length, Length) {
277        (self.width, self.height)
278    }
279
280    fn arrange(&mut self, rect: Rectangle) {
281        self.rect = rect;
282        match self.scroll.as_ref().map(|c| c.dir) {
283            Some(dir) if dir != ScrollDirection::None => self.arrange_scroll(dir),
284            _ => {
285                let inner = self.inner_rect();
286                if let Some(child) = self.child.as_mut() {
287                    child.arrange(inner);
288                }
289            }
290        }
291    }
292
293    fn rect(&self) -> Rectangle {
294        self.rect
295    }
296
297    fn handle_touch(&mut self, point: Point, phase: TouchPhase) -> Option<M> {
298        match self.scroll.as_ref() {
299            Some(core) if core.dir != ScrollDirection::None => {
300                let dir = core.dir;
301                let state = core.state;
302                let viewport = self.inner_rect();
303                let content = self.content;
304                let lines = self.snap_lines();
305                let on_scroll = self.scroll.as_ref().and_then(|c| c.on_scroll.as_deref());
306                let child = &mut self.child;
307                scroll_core::route_touch(
308                    state,
309                    dir,
310                    viewport,
311                    content,
312                    point,
313                    phase,
314                    &lines,
315                    on_scroll,
316                    |p, ph| child.as_mut().and_then(|c| c.handle_touch(p, ph)),
317                )
318            }
319            _ => self
320                .child
321                .as_mut()
322                .and_then(|child| child.handle_touch(point, phase)),
323        }
324    }
325
326    fn mark_pressed(&mut self, point: Point) {
327        // While dragging/flinging/springing, stop re-asserting a child press
328        // so any button highlighted on Down is cancelled mid-drag.
329        if let Some(core) = self.scroll.as_ref() {
330            if matches!(
331                core.state.phase,
332                zest_core::GesturePhase::Dragging
333                    | zest_core::GesturePhase::Flinging
334                    | zest_core::GesturePhase::Springing
335            ) {
336                return;
337            }
338        }
339        if let Some(child) = self.child.as_mut() {
340            child.mark_pressed(point);
341        }
342    }
343
344    fn collect_focusable(&self, out: &mut alloc::vec::Vec<WidgetId>) {
345        if let Some(child) = self.child.as_ref() {
346            child.collect_focusable(out);
347        }
348    }
349
350    fn sync_focus(&mut self, focused: Option<WidgetId>) {
351        if let Some(child) = self.child.as_mut() {
352            child.sync_focus(focused);
353        }
354    }
355
356    fn route_action(&mut self, target: WidgetId, action: UiAction) -> Option<M> {
357        self.child
358            .as_mut()
359            .and_then(|child| child.route_action(target, action))
360    }
361
362    fn navigate_focus(&self, target: WidgetId, action: UiAction) -> Option<WidgetId> {
363        self.child
364            .as_ref()
365            .and_then(|child| child.navigate_focus(target, action))
366    }
367
368    fn focus_rect(&self, target: WidgetId) -> Option<Rectangle> {
369        self.child
370            .as_ref()
371            .and_then(|child| child.focus_rect(target))
372    }
373
374    fn focus_at(&self, point: Point) -> Option<WidgetId> {
375        self.child.as_ref().and_then(|child| child.focus_at(point))
376    }
377
378    fn draw<'t>(
379        &self,
380        renderer: &mut dyn Renderer<C>,
381        theme: &Theme<'t, C>,
382    ) -> Result<(), RenderError> {
383        match self.scroll.as_ref() {
384            Some(core) if core.dir != ScrollDirection::None => {
385                let viewport = self.inner_rect();
386                renderer.push_clip(viewport);
387                if let Some(child) = &self.child {
388                    child.draw(renderer, theme)?;
389                }
390                renderer.pop_clip();
391                scroll_core::draw_scrollbars(
392                    renderer,
393                    theme,
394                    core.state,
395                    core.bar,
396                    core.dir,
397                    viewport,
398                    self.content,
399                )?;
400                Ok(())
401            }
402            _ => {
403                if let Some(child) = &self.child {
404                    child.draw(renderer, theme)?;
405                }
406                Ok(())
407            }
408        }
409    }
410}