freya_components/scroll_views/
use_scroll_controller.rs1use 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 }
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 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 utils.click_cursor((15., 480.)).await;
266
267 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 utils.click_cursor((15., 15.)).await;
275
276 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}