Skip to main content

waterui_layout/collections/
scroll.rs

1//! Scroll containers that defer behaviour to the active renderer backend.
2
3use nami::{Binding, Computed};
4use waterui_core::{AnyView, View, raw_view};
5
6use crate::{Point, StretchAxis};
7
8/// Explicit programmatic control for a scrollable view.
9///
10/// A controller owns a reactive target and a monotonically increasing request
11/// generation. The generation makes repeated requests to the same target
12/// observable after the user has scrolled elsewhere.
13#[derive(Clone, Debug)]
14pub struct ScrollController<T: Clone + 'static> {
15    target: Binding<T>,
16    generation: Binding<i32>,
17}
18
19impl<T: Clone + 'static> ScrollController<T> {
20    /// Creates a controller whose first target is `initial_target`.
21    #[must_use]
22    pub fn new(initial_target: T) -> Self {
23        Self {
24            target: Binding::container(initial_target),
25            generation: Binding::container(0),
26        }
27    }
28
29    /// Requests an immediate jump to `target`.
30    ///
31    /// # Panics
32    ///
33    /// Panics if the request generation exceeds [`i32::MAX`].
34    pub fn scroll_to(&self, target: T) {
35        self.target.set(target);
36        self.generation.set(
37            self.generation
38                .get()
39                .checked_add(1)
40                .expect("scroll request generation overflow"),
41        );
42    }
43
44    /// Returns the current requested target as a read-only signal.
45    #[must_use]
46    pub fn target(&self) -> Computed<T> {
47        self.target.clone().into()
48    }
49
50    /// Returns the request generation as a read-only signal.
51    #[must_use]
52    pub fn generation(&self) -> Computed<i32> {
53        self.generation.clone().into()
54    }
55}
56
57impl<T> Default for ScrollController<T>
58where
59    T: Clone + Default + 'static,
60{
61    fn default() -> Self {
62        Self::new(T::default())
63    }
64}
65
66/// A scrollable view that displays content larger than its frame.
67///
68/// Use a `ScrollView` when you have content that might not fit in the available space.
69/// The view automatically enables scrolling in the specified direction.
70///
71/// ```rust
72/// # use waterui::prelude::*;
73/// # fn feed() -> impl View {
74/// scroll(
75///     vstack((
76///         text("Item 1"),
77///         text("Item 2"),
78///         text("Item 3"),
79///         // ... many more items
80///     ))
81/// )
82/// # }
83/// ```
84///
85/// By default, `ScrollView` scrolls vertically. For horizontal scrolling:
86///
87/// ```rust
88/// # use waterui::layout::scroll::scroll_horizontal;
89/// # use waterui::prelude::*;
90/// # fn carousel(long_content: impl View) -> impl View {
91/// scroll_horizontal(long_content)
92/// # }
93/// ```
94///
95/// Or both directions:
96///
97/// ```rust
98/// # use waterui::layout::scroll::scroll_both;
99/// # use waterui::prelude::*;
100/// # fn canvas(large_image: impl View) -> impl View {
101/// scroll_both(large_image)
102/// # }
103/// ```
104#[derive(Debug)]
105pub struct ScrollView {
106    axis: Axis,
107    content: AnyView,
108    controller: Option<ScrollController<Point>>,
109}
110
111/// Defines the scrolling directions supported by `ScrollView`.
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
113#[non_exhaustive]
114pub enum Axis {
115    /// Allow horizontal scrolling only.
116    Horizontal,
117    /// Allow vertical scrolling only (default).
118    #[default]
119    Vertical,
120    /// Allow scrolling in both directions.
121    All,
122}
123
124impl ScrollView {
125    /// Creates a new `ScrollView` with the specified scroll axis and content.
126    #[must_use]
127    pub const fn new(axis: Axis, content: AnyView) -> Self {
128        Self {
129            axis,
130            content,
131            controller: None,
132        }
133    }
134
135    /// Decomposes the `ScrollView` into its axis, content, and controller.
136    pub fn into_inner(self) -> (Axis, AnyView, Option<ScrollController<Point>>) {
137        (self.axis, self.content, self.controller)
138    }
139
140    /// Returns borrowed access to axis, content, and controller.
141    #[must_use = "this borrows the scroll view's parts without consuming it"]
142    pub const fn as_parts(&self) -> (Axis, &AnyView, Option<&ScrollController<Point>>) {
143        (self.axis, &self.content, self.controller.as_ref())
144    }
145
146    /// Connects a programmatic scroll controller.
147    #[must_use]
148    pub fn scroll_controller(mut self, controller: &ScrollController<Point>) -> Self {
149        self.controller = Some(controller.clone());
150        self
151    }
152
153    /// Creates a `ScrollView` with horizontal scrolling.
154    pub fn horizontal(content: impl View) -> Self {
155        Self::new(Axis::Horizontal, AnyView::new(content))
156    }
157
158    /// Creates a `ScrollView` with vertical scrolling.
159    pub fn vertical(content: impl View) -> Self {
160        Self::new(Axis::Vertical, AnyView::new(content))
161    }
162
163    /// Creates a `ScrollView` with scrolling in both directions.
164    pub fn both(content: impl View) -> Self {
165        Self::new(Axis::All, AnyView::new(content))
166    }
167}
168
169raw_view!(ScrollView, StretchAxis::Both);
170
171/// Creates a vertical `ScrollView` with the given content.
172///
173/// This is the most common scroll direction for lists and long content.
174/// The actual scrolling behavior is implemented by the renderer backend.
175pub fn scroll(content: impl View) -> ScrollView {
176    ScrollView::vertical(content)
177}
178
179/// Creates a horizontal `ScrollView` with the given content.
180///
181/// Useful for wide content that needs to scroll left-right.
182/// The actual scrolling behavior is implemented by the renderer backend.
183pub fn scroll_horizontal(content: impl View) -> ScrollView {
184    ScrollView::horizontal(content)
185}
186
187/// Creates a `ScrollView` that can scroll in both directions.
188///
189/// Useful for large content like images or tables that may need both horizontal and vertical scrolling.
190/// The actual scrolling behavior is implemented by the renderer backend.
191pub fn scroll_both(content: impl View) -> ScrollView {
192    ScrollView::both(content)
193}
194
195#[cfg(test)]
196mod tests {
197    use super::ScrollController;
198    use crate::Point;
199    use nami::Signal;
200
201    #[test]
202    fn repeated_target_requests_advance_generation() {
203        let controller = ScrollController::new(Point::zero());
204
205        controller.scroll_to(Point::new(0.0, 240.0));
206        assert_eq!(controller.target().get(), Point::new(0.0, 240.0));
207        assert_eq!(controller.generation().get(), 1);
208
209        controller.scroll_to(Point::new(0.0, 240.0));
210        assert_eq!(controller.target().get(), Point::new(0.0, 240.0));
211        assert_eq!(controller.generation().get(), 2);
212    }
213}