Skip to main content

zest_widget/widget/
scrollable.rs

1//! Drag-to-scroll single-child wrapper (the iced `Scrollable` analog).
2//!
3//! Wraps one child and makes it scrollable with the shared scroll engine: a
4//! 1:1 drag pans the content, a fast release flings with friction,
5//! over-dragging an edge stretches then springs back, and a thumb shows the
6//! position.
7//!
8//! Like the scrollable containers, the host owns a [`ScrollState`] (widgets
9//! are transient) and passes it via [`Scrollable::scroll_state`]; the wrapper
10//! reads it during layout/draw and emits [`ScrollMsg`] through
11//! [`Scrollable::on_scroll`]. The host applies those in `update()` and drives
12//! momentum with [`tick_task`](zest_core::scroll::tick_task). For multi-child
13//! content, make a `Column`/`Row`/`Grid` scrollable directly instead.
14
15use super::{Widget, element::Element, scroll_core};
16use alloc::{boxed::Box, vec::Vec};
17use embedded_graphics::{pixelcolor::PixelColor, prelude::*, primitives::Rectangle};
18use zest_core::{
19    Constraints, GesturePhase, Length, RenderError, Renderer, ScrollDirection, ScrollMsg,
20    ScrollState, ScrollbarMode, SnapMode, TouchPhase, UNBOUNDED, UiAction, WidgetId,
21};
22use zest_theme::Theme;
23
24/// A scrollable viewport wrapping a single child.
25pub struct Scrollable<'a, C: PixelColor, M: Clone> {
26    rect: Rectangle,
27    child: Element<'a, C, M>,
28    dir: ScrollDirection,
29    bar: ScrollbarMode,
30    snap: SnapMode,
31    state: ScrollState,
32    on_scroll: Option<Box<dyn Fn(ScrollMsg) -> M + 'a>>,
33    width: Length,
34    height: Length,
35    /// Scrollable content extent measured this frame.
36    content: Size,
37    /// Un-scrolled child origin + size (for snap-line computation).
38    child_origin: Point,
39    child_size: Size,
40}
41
42impl<'a, C: PixelColor + 'a, M: Clone + 'a> Scrollable<'a, C, M> {
43    /// Wrap `child` in a vertically scrollable viewport. Position and size are
44    /// assigned by the parent via `arrange`.
45    pub fn new<W>(child: W) -> Self
46    where
47        W: Widget<C, M> + 'a,
48    {
49        Self {
50            rect: Rectangle::zero(),
51            child: Element::new(child),
52            dir: ScrollDirection::Vertical,
53            bar: ScrollbarMode::Auto,
54            snap: SnapMode::None,
55            state: ScrollState::new(),
56            on_scroll: None,
57            width: Length::Fill,
58            height: Length::Fill,
59            content: Size::zero(),
60            child_origin: Point::zero(),
61            child_size: Size::zero(),
62        }
63    }
64
65    /// Which axes scroll (default [`ScrollDirection::Vertical`]).
66    #[must_use]
67    pub fn direction(mut self, dir: ScrollDirection) -> Self {
68        self.dir = dir;
69        self
70    }
71
72    /// Supply the host-owned [`ScrollState`] read this frame.
73    #[must_use]
74    pub fn scroll_state(mut self, state: &ScrollState) -> Self {
75        self.state = *state;
76        self
77    }
78
79    /// When the scrollbar is drawn (default [`ScrollbarMode::Auto`]).
80    #[must_use]
81    pub fn scrollbar(mut self, mode: ScrollbarMode) -> Self {
82        self.bar = mode;
83        self
84    }
85
86    /// Snapping mode (default [`SnapMode::None`]).
87    #[must_use]
88    pub fn snap(mut self, mode: SnapMode) -> Self {
89        self.snap = mode;
90        self
91    }
92
93    /// Callback mapping a [`ScrollMsg`] to the host message. Without
94    /// it, scroll offsets are still computed but never reach the host, so the
95    /// position isn't persisted across frames.
96    #[must_use]
97    pub fn on_scroll<F>(mut self, f: F) -> Self
98    where
99        F: Fn(ScrollMsg) -> M + 'a,
100    {
101        self.on_scroll = Some(Box::new(f));
102        self
103    }
104
105    /// Width sizing intent.
106    #[must_use]
107    pub fn width(mut self, w: impl Into<Length>) -> Self {
108        self.width = w.into();
109        self
110    }
111
112    /// Height sizing intent.
113    #[must_use]
114    pub fn height(mut self, h: impl Into<Length>) -> Self {
115        self.height = h.into();
116        self
117    }
118
119    fn snap_lines(&self) -> Vec<i32> {
120        if self.snap == SnapMode::None {
121            return Vec::new();
122        }
123        let offset = scroll_core::render_offset(self.state, self.dir);
124        scroll_core::snap_lines(
125            &[Rectangle::new(self.child_origin, self.child_size)],
126            self.rect.top_left,
127            offset,
128            self.rect.size,
129            self.dir,
130            self.snap,
131        )
132    }
133}
134
135impl<'a, C: PixelColor + 'a, M: Clone + 'a> Widget<C, M> for Scrollable<'a, C, M> {
136    fn measure(&mut self, constraints: Constraints) -> Size {
137        let w = self
138            .width
139            .resolve(constraints.max.width, constraints.max.width);
140        let h = self
141            .height
142            .resolve(constraints.max.height, constraints.max.height);
143        constraints.clamp(Size::new(w, h))
144    }
145
146    fn preferred_size(&self) -> (Length, Length) {
147        (self.width, self.height)
148    }
149
150    fn arrange(&mut self, rect: Rectangle) {
151        self.rect = rect;
152        let dir = self.dir;
153        let (vw, vh) = (rect.size.width, rect.size.height);
154        // Measure the child against UNBOUNDED on the scrolling axis to learn
155        // its intrinsic content extent.
156        let max_w = if dir.scrolls_x() { UNBOUNDED } else { vw };
157        let max_h = if dir.scrolls_y() { UNBOUNDED } else { vh };
158        let m = self
159            .child
160            .measure(Constraints::loose(Size::new(max_w, max_h)));
161        self.content = Size::new(
162            if dir.scrolls_x() { m.width } else { vw },
163            if dir.scrolls_y() { m.height } else { vh },
164        );
165        let off = scroll_core::render_offset(self.state, dir);
166        let size = Size::new(self.content.width.max(vw), self.content.height.max(vh));
167        self.child
168            .arrange(Rectangle::new(rect.top_left - off, size));
169        self.child_origin = rect.top_left;
170        self.child_size = size;
171    }
172
173    fn rect(&self) -> Rectangle {
174        self.rect
175    }
176
177    fn handle_touch(&mut self, point: Point, phase: TouchPhase) -> Option<M> {
178        let state = self.state;
179        let dir = self.dir;
180        let viewport = self.rect;
181        let content = self.content;
182        let lines = self.snap_lines();
183        let on_scroll = self.on_scroll.as_deref();
184        let child = &mut self.child;
185        scroll_core::route_touch(
186            state,
187            dir,
188            viewport,
189            content,
190            point,
191            phase,
192            &lines,
193            on_scroll,
194            |p, ph| child.handle_touch(p, ph),
195        )
196    }
197
198    fn mark_pressed(&mut self, point: Point) {
199        // Cancel any child press once a drag/animation owns the gesture.
200        if matches!(
201            self.state.phase,
202            GesturePhase::Dragging | GesturePhase::Flinging | GesturePhase::Springing
203        ) {
204            return;
205        }
206        self.child.mark_pressed(point);
207    }
208
209    fn draw<'t>(
210        &self,
211        renderer: &mut dyn Renderer<C>,
212        theme: &Theme<'t, C>,
213    ) -> Result<(), RenderError> {
214        renderer.push_clip(self.rect);
215        self.child.draw(renderer, theme)?;
216        renderer.pop_clip();
217        scroll_core::draw_scrollbars(
218            renderer,
219            theme,
220            self.state,
221            self.bar,
222            self.dir,
223            self.rect,
224            self.content,
225        )
226    }
227
228    fn collect_focusable(&self, out: &mut Vec<WidgetId>) {
229        self.child.collect_focusable(out);
230    }
231
232    fn sync_focus(&mut self, focused: Option<WidgetId>) {
233        self.child.sync_focus(focused);
234    }
235
236    fn route_action(&mut self, target: WidgetId, action: UiAction) -> Option<M> {
237        self.child.route_action(target, action)
238    }
239
240    fn navigate_focus(&self, target: WidgetId, action: UiAction) -> Option<WidgetId> {
241        self.child.navigate_focus(target, action)
242    }
243
244    fn focus_rect(&self, target: WidgetId) -> Option<Rectangle> {
245        self.child.focus_rect(target)
246    }
247
248    fn focus_at(&self, point: Point) -> Option<WidgetId> {
249        self.child.focus_at(point)
250    }
251}