freya_components/scroll_views/
use_scroll_controller.rs

1use std::collections::HashSet;
2
3use dioxus::prelude::{
4    current_scope_id,
5    schedule_update_any,
6    use_drop,
7    use_hook,
8    Readable,
9    ScopeId,
10    Signal,
11    Writable,
12    WritableVecExt,
13};
14use freya_core::custom_attributes::NodeReferenceLayout;
15
16#[derive(Default, PartialEq, Eq)]
17pub enum ScrollPosition {
18    #[default]
19    Start,
20    End,
21    // Specific
22}
23
24#[derive(Default, PartialEq, Eq)]
25pub enum ScrollDirection {
26    #[default]
27    Vertical,
28    Horizontal,
29}
30
31#[derive(Default)]
32pub struct ScrollConfig {
33    pub default_vertical_position: ScrollPosition,
34    pub default_horizontal_position: ScrollPosition,
35}
36
37pub struct ScrollRequest {
38    pub(crate) position: ScrollPosition,
39    pub(crate) direction: ScrollDirection,
40    pub(crate) init: bool,
41    pub(crate) applied_by: HashSet<ScopeId>,
42}
43
44impl ScrollRequest {
45    pub fn new(position: ScrollPosition, direction: ScrollDirection) -> ScrollRequest {
46        ScrollRequest {
47            position,
48            direction,
49            init: false,
50            applied_by: HashSet::default(),
51        }
52    }
53}
54
55#[derive(PartialEq, Eq, Clone, Copy)]
56pub struct ScrollController {
57    requests_subscribers: Signal<HashSet<ScopeId>>,
58    requests: Signal<Vec<ScrollRequest>>,
59    x: Signal<i32>,
60    y: Signal<i32>,
61    layout: Signal<NodeReferenceLayout>,
62}
63
64impl From<ScrollController> for (Signal<i32>, Signal<i32>) {
65    fn from(val: ScrollController) -> Self {
66        (val.x, val.y)
67    }
68}
69
70impl ScrollController {
71    pub fn new(x: i32, y: i32, initial_requests: Vec<ScrollRequest>) -> Self {
72        Self {
73            x: Signal::new(x),
74            y: Signal::new(y),
75            requests_subscribers: Signal::new(HashSet::new()),
76            requests: Signal::new(initial_requests),
77            layout: Signal::default(),
78        }
79    }
80
81    pub fn x(&self) -> Signal<i32> {
82        self.x
83    }
84
85    pub fn y(&self) -> Signal<i32> {
86        self.y
87    }
88
89    pub fn layout(&self) -> Signal<NodeReferenceLayout> {
90        self.layout
91    }
92
93    pub fn use_apply(&mut self, width: f32, height: f32) {
94        let scope_id = current_scope_id().unwrap();
95
96        if !self.requests_subscribers.peek().contains(&scope_id) {
97            self.requests_subscribers.write().insert(scope_id);
98        }
99
100        let mut requests_subscribers = self.requests_subscribers;
101        use_drop(move || {
102            requests_subscribers.write().remove(&scope_id);
103        });
104
105        self.requests.write().retain_mut(|request| {
106            if request.applied_by.contains(&scope_id) {
107                return true;
108            }
109
110            match request {
111                ScrollRequest {
112                    position: ScrollPosition::Start,
113                    direction: ScrollDirection::Vertical,
114                    ..
115                } => {
116                    *self.y.write() = 0;
117                }
118                ScrollRequest {
119                    position: ScrollPosition::Start,
120                    direction: ScrollDirection::Horizontal,
121                    ..
122                } => {
123                    *self.x.write() = 0;
124                }
125                ScrollRequest {
126                    position: ScrollPosition::End,
127                    direction: ScrollDirection::Vertical,
128                    init,
129                    ..
130                } => {
131                    if *init && height == 0. {
132                        return true;
133                    }
134                    *self.y.write() = -height as i32;
135                }
136                ScrollRequest {
137                    position: ScrollPosition::End,
138                    direction: ScrollDirection::Horizontal,
139                    init,
140                    ..
141                } => {
142                    if *init && width == 0. {
143                        return true;
144                    }
145                    *self.x.write() = -width as i32;
146                }
147            }
148
149            request.applied_by.insert(scope_id);
150
151            *self.requests_subscribers.peek() != request.applied_by
152        });
153    }
154
155    pub fn scroll_to_x(&mut self, to: i32) {
156        self.x.set(to);
157    }
158
159    pub fn scroll_to_y(&mut self, to: i32) {
160        self.y.set(to);
161    }
162
163    pub fn scroll_to(
164        &mut self,
165        scroll_position: ScrollPosition,
166        scroll_direction: ScrollDirection,
167    ) {
168        self.requests
169            .push(ScrollRequest::new(scroll_position, scroll_direction));
170        let schedule = schedule_update_any();
171        for scope_id in self.requests_subscribers.read().iter() {
172            schedule(*scope_id);
173        }
174    }
175}
176
177pub fn use_scroll_controller(init: impl FnOnce() -> ScrollConfig) -> ScrollController {
178    use_hook(|| {
179        let config = init();
180        ScrollController::new(
181            0,
182            0,
183            vec![
184                ScrollRequest {
185                    position: config.default_vertical_position,
186                    direction: ScrollDirection::Vertical,
187                    init: true,
188                    applied_by: HashSet::default(),
189                },
190                ScrollRequest {
191                    position: config.default_horizontal_position,
192                    direction: ScrollDirection::Horizontal,
193                    init: true,
194                    applied_by: HashSet::default(),
195                },
196            ],
197        )
198    })
199}
200
201#[cfg(test)]
202mod test {
203    use freya::prelude::*;
204    use freya_testing::prelude::*;
205
206    #[tokio::test]
207    pub async fn controlled_scroll_view() {
208        fn scroll_view_app() -> Element {
209            let mut scroll_controller = use_scroll_controller(|| ScrollConfig {
210                default_vertical_position: ScrollPosition::End,
211                ..Default::default()
212            });
213
214            rsx!(
215                ScrollView {
216                    scroll_controller,
217                    Button {
218                        onpress: move |_| {
219                            scroll_controller.scroll_to(ScrollPosition::End, ScrollDirection::Vertical);
220                        },
221                        label {
222                            "Scroll Down"
223                        }
224                    }
225                    rect {
226                        height: "200",
227                        width: "200",
228                    }
229                    rect {
230                        height: "200",
231                        width: "200",
232                    }
233                    rect {
234                        height: "200",
235                        width: "200",
236                    }
237                    rect {
238                        height: "200",
239                        width: "200",
240                    }
241                    Button {
242                        onpress: move |_| {
243                            scroll_controller.scroll_to(ScrollPosition::Start, ScrollDirection::Vertical);
244                        },
245                        label {
246                            "Scroll up"
247                        }
248                    }
249                }
250            )
251        }
252
253        let mut utils = launch_test(scroll_view_app);
254        let root = utils.root();
255        let content = root.get(0).get(0).get(0);
256        utils.wait_for_update().await;
257
258        // Only the last three items are visible
259        assert!(!content.get(1).is_visible());
260        assert!(content.get(2).is_visible());
261        assert!(content.get(3).is_visible());
262        assert!(content.get(4).is_visible());
263
264        // Click on the button to scroll up
265        utils.click_cursor((15., 480.)).await;
266
267        // Only the first three items are visible
268        assert!(content.get(1).is_visible());
269        assert!(content.get(2).is_visible());
270        assert!(content.get(3).is_visible());
271        assert!(!content.get(4).is_visible());
272
273        // Click on the button to scroll down
274        utils.click_cursor((15., 15.)).await;
275
276        // Only the first three items are visible
277        assert!(!content.get(1).is_visible());
278        assert!(content.get(2).is_visible());
279        assert!(content.get(3).is_visible());
280        assert!(content.get(4).is_visible());
281    }
282}