1use std::sync::Arc;
2
3use crate::binding::RatioLens;
4use crate::prelude::*;
5
6pub(crate) const SCROLL_SENSITIVITY: f32 = 20.0;
7
8pub enum ScrollEvent {
10 SetX(f32),
12 SetY(f32),
14 ScrollX(f32),
16 ScrollY(f32),
18 ChildGeo(f32, f32),
20
21 ScrollToView(Entity),
22}
23
24#[derive(Lens, Data, Clone)]
26pub struct ScrollView {
27 pub scroll_x: f32,
29 pub scroll_y: f32,
31 #[lens(ignore)]
33 pub on_scroll: Option<Arc<dyn Fn(&mut EventContext, f32, f32) + Send + Sync>>,
34 pub inner_width: f32,
36 pub inner_height: f32,
38 pub container_width: f32,
40 pub container_height: f32,
42 pub scroll_to_cursor: bool,
44 pub show_horizontal_scrollbar: bool,
46 pub show_vertical_scrollbar: bool,
48}
49
50impl ScrollView {
51 pub fn new<F>(cx: &mut Context, content: F) -> Handle<Self>
53 where
54 F: 'static + FnOnce(&mut Context),
55 {
56 Self {
57 scroll_to_cursor: false,
58 scroll_x: 0.0,
59 scroll_y: 0.0,
60 on_scroll: None,
61 inner_width: 0.0,
62 inner_height: 0.0,
63 container_width: 0.0,
64 container_height: 0.0,
65 show_horizontal_scrollbar: true,
66 show_vertical_scrollbar: true,
67 }
68 .build(cx, move |cx| {
69 ScrollContent::new(cx, content);
70
71 Binding::new(cx, ScrollView::show_vertical_scrollbar, |cx, show_scrollbar| {
72 if show_scrollbar.get(cx) {
73 Scrollbar::new(
74 cx,
75 ScrollView::scroll_y,
76 RatioLens::new(ScrollView::container_height, ScrollView::inner_height),
77 Orientation::Vertical,
78 |cx, value| {
79 cx.emit(ScrollEvent::SetY(value));
80 },
81 )
82 .position_type(PositionType::Absolute)
83 .scroll_to_cursor(Self::scroll_to_cursor);
84 }
85 });
86
87 Binding::new(cx, ScrollView::show_horizontal_scrollbar, |cx, show_scrollbar| {
88 if show_scrollbar.get(cx) {
89 Scrollbar::new(
90 cx,
91 ScrollView::scroll_x,
92 RatioLens::new(ScrollView::container_width, ScrollView::inner_width),
93 Orientation::Horizontal,
94 |cx, value| {
95 cx.emit(ScrollEvent::SetX(value));
96 },
97 )
98 .position_type(PositionType::Absolute)
99 .scroll_to_cursor(Self::scroll_to_cursor);
100 }
101 });
102 })
103 .bind(ScrollView::root, |mut handle, data| {
104 let data = data.get(&handle);
105 let scale_factor = handle.context().scale_factor();
106 let top = ((data.inner_height - data.container_height) * data.scroll_y).round()
107 / scale_factor;
108 let left =
109 ((data.inner_width - data.container_width) * data.scroll_x).round() / scale_factor;
110 handle.horizontal_scroll(-left.abs()).vertical_scroll(-top.abs());
111 })
112 .toggle_class(
113 "h-scroll",
114 ScrollView::root.map(|data| data.container_width < data.inner_width),
115 )
116 .toggle_class(
117 "v-scroll",
118 ScrollView::root.map(|data| data.container_height < data.inner_height),
119 )
120 }
121
122 fn reset(&mut self) {
123 if self.inner_width == self.container_width {
124 self.scroll_x = 0.0;
125 }
126
127 if self.inner_height == self.container_height {
128 self.scroll_y = 0.0;
129 }
130 }
131}
132
133impl View for ScrollView {
134 fn element(&self) -> Option<&'static str> {
135 Some("scrollview")
136 }
137
138 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
139 event.map(|scroll_update, meta| {
140 match scroll_update {
141 ScrollEvent::ScrollX(f) => {
142 self.scroll_x = (self.scroll_x + *f).clamp(0.0, 1.0);
143
144 if let Some(callback) = &self.on_scroll {
145 (callback)(cx, self.scroll_x, self.scroll_y);
146 }
147 }
148
149 ScrollEvent::ScrollY(f) => {
150 self.scroll_y = (self.scroll_y + *f).clamp(0.0, 1.0);
151 if let Some(callback) = &self.on_scroll {
152 (callback)(cx, self.scroll_x, self.scroll_y);
153 }
154 }
155
156 ScrollEvent::SetX(f) => {
157 self.scroll_x = *f;
158 if let Some(callback) = &self.on_scroll {
159 (callback)(cx, self.scroll_x, self.scroll_y);
160 }
161 }
162
163 ScrollEvent::SetY(f) => {
164 self.scroll_y = *f;
165 if let Some(callback) = &self.on_scroll {
166 (callback)(cx, self.scroll_x, self.scroll_y);
167 }
168 }
169
170 ScrollEvent::ChildGeo(w, h) => {
171 let bounds = cx.bounds();
172 let scale_factor = cx.scale_factor();
173
174 if self.inner_width != 0.0
175 && self.inner_height != 0.0
176 && self.container_width != 0.0
177 && self.container_height != 0.0
178 {
179 let top = ((self.inner_height - self.container_height) * self.scroll_y)
180 .round()
181 / scale_factor;
182 let left = ((self.inner_width - self.container_width) * self.scroll_x)
183 .round()
184 / scale_factor;
185
186 self.container_width = bounds.width();
187 self.container_height = bounds.height();
188 self.inner_width = *w;
189 self.inner_height = *h;
190
191 if self.inner_width != self.container_width {
192 self.scroll_x = ((left * scale_factor)
193 / (self.inner_width - self.container_width))
194 .clamp(0.0, 1.0);
195 } else {
196 self.scroll_x = 0.0;
197 }
198
199 if self.inner_height != self.container_height {
200 self.scroll_y = ((top * scale_factor)
201 / (self.inner_height - self.container_height))
202 .clamp(0.0, 1.0);
203 } else {
204 self.scroll_y = 0.0;
205 }
206
207 if let Some(callback) = &self.on_scroll {
208 (callback)(cx, self.scroll_x, self.scroll_y);
209 }
210
211 self.reset();
212 }
213
214 self.inner_width = *w;
215 self.inner_height = *h;
216 self.reset();
217 }
218
219 ScrollEvent::ScrollToView(entity) => {
220 let view_bounds = cx.cache.get_bounds(*entity);
221
222 let content_bounds = cx.bounds();
223
224 let dx = content_bounds.right() - view_bounds.right();
225 let dy = content_bounds.bottom() - view_bounds.bottom();
226
227 if dx < 0.0 {
229 let sx = (-dx / (self.inner_width - self.container_width)).clamp(0.0, 1.0);
230 self.scroll_x = (self.scroll_x + sx).clamp(0.0, 1.0);
231 }
232
233 if dy < 0.0 {
234 let sy =
235 (-dy / (self.inner_height - self.container_height)).clamp(0.0, 1.0);
236 self.scroll_y = (self.scroll_y + sy).clamp(0.0, 1.0);
237 }
238
239 let dx = view_bounds.left() - content_bounds.left();
240 let dy = view_bounds.top() - content_bounds.top();
241
242 if dx < 0.0 {
243 let sx = (-dx / (self.inner_width - self.container_width)).clamp(0.0, 1.0);
244 self.scroll_x = (self.scroll_x - sx).clamp(0.0, 1.0);
245 }
246
247 if dy < 0.0 {
248 let sy =
249 (-dy / (self.inner_height - self.container_height)).clamp(0.0, 1.0);
250 self.scroll_y = (self.scroll_y - sy).clamp(0.0, 1.0);
251 }
252
253 if let Some(callback) = &self.on_scroll {
254 (callback)(cx, self.scroll_x, self.scroll_y);
255 }
256 }
257 }
258
259 meta.consume();
262 });
263
264 event.map(|window_event, meta| match window_event {
265 WindowEvent::GeometryChanged(geo) => {
266 if geo.contains(GeoChanged::WIDTH_CHANGED)
267 || geo.contains(GeoChanged::HEIGHT_CHANGED)
268 {
269 let bounds = cx.bounds();
270 let scale_factor = cx.scale_factor();
271
272 if self.inner_width != 0.0
273 && self.inner_height != 0.0
274 && self.container_width != 0.0
275 && self.container_height != 0.0
276 {
277 let top = ((self.inner_height - self.container_height) * self.scroll_y)
278 .round()
279 / scale_factor;
280 let left = ((self.inner_width - self.container_width) * self.scroll_x)
281 .round()
282 / scale_factor;
283
284 self.container_width = bounds.width();
285 self.container_height = bounds.height();
286
287 if self.inner_width != self.container_width {
288 self.scroll_x = ((left * scale_factor)
289 / (self.inner_width - self.container_width))
290 .clamp(0.0, 1.0);
291 } else {
292 self.scroll_x = 0.0;
293 }
294
295 if self.inner_height != self.container_height {
296 self.scroll_y = ((top * scale_factor)
297 / (self.inner_height - self.container_height))
298 .clamp(0.0, 1.0);
299 } else {
300 self.scroll_y = 0.0;
301 }
302
303 if let Some(callback) = &self.on_scroll {
304 (callback)(cx, self.scroll_x, self.scroll_y);
305 }
306
307 self.reset();
308 }
309
310 self.container_width = bounds.width();
311 self.container_height = bounds.height();
312 }
313 }
314
315 WindowEvent::MouseScroll(x, y) => {
316 cx.set_active(true);
317 let (x, y) = if cx.modifiers.shift() { (-*y, -*x) } else { (-*x, -*y) };
318
319 if x != 0.0 && self.inner_width > self.container_width {
321 let negative_space = self.inner_width - self.container_width;
322 if negative_space != 0.0 {
323 let logical_delta = x * SCROLL_SENSITIVITY / negative_space;
324 cx.emit(ScrollEvent::ScrollX(logical_delta));
325 }
326 meta.consume();
328 }
329 if y != 0.0 && self.inner_height > self.container_height {
330 let negative_space = self.inner_height - self.container_height;
331 if negative_space != 0.0 {
332 let logical_delta = y * SCROLL_SENSITIVITY / negative_space;
333 cx.emit(ScrollEvent::ScrollY(logical_delta));
334 }
335 meta.consume();
337 }
338 }
339
340 WindowEvent::MouseOut => {
341 cx.set_active(false);
342 }
343
344 _ => {}
345 });
346 }
347}
348
349impl Handle<'_, ScrollView> {
350 pub fn on_scroll(
352 self,
353 callback: impl Fn(&mut EventContext, f32, f32) + 'static + Send + Sync,
354 ) -> Self {
355 self.modify(|scrollview| scrollview.on_scroll = Some(Arc::new(callback)))
356 }
357
358 pub fn scroll_to_cursor(self, scroll_to_cursor: impl Res<bool>) -> Self {
360 self.bind(scroll_to_cursor, |handle, scroll_to_cursor| {
361 let scroll_to_cursor = scroll_to_cursor.get(&handle);
362 handle.modify(|scrollview| scrollview.scroll_to_cursor = scroll_to_cursor);
363 })
364 }
365
366 pub fn scroll_x(self, scrollx: impl Res<f32>) -> Self {
368 self.bind(scrollx, |handle, scrollx| {
369 let sx = scrollx.get(&handle);
370 handle.modify(|scrollview| scrollview.scroll_x = sx);
371 })
372 }
373
374 pub fn scroll_y(self, scrollx: impl Res<f32>) -> Self {
376 self.bind(scrollx, |handle, scrolly| {
377 let sy = scrolly.get(&handle);
378 handle.modify(|scrollview| scrollview.scroll_y = sy);
379 })
380 }
381
382 pub fn show_horizontal_scrollbar(self, flag: impl Res<bool>) -> Self {
384 self.bind(flag, |handle, show_scrollbar| {
385 let s = show_scrollbar.get(&handle);
386 handle.modify(|scrollview| scrollview.show_horizontal_scrollbar = s);
387 })
388 }
389
390 pub fn show_vertical_scrollbar(self, flag: impl Res<bool>) -> Self {
392 self.bind(flag, |handle, show_scrollbar| {
393 let s = show_scrollbar.get(&handle);
394 handle.modify(|scrollview| scrollview.show_vertical_scrollbar = s);
395 })
396 }
397}
398
399struct ScrollContent {}
400
401impl ScrollContent {
402 pub fn new(cx: &mut Context, content: impl FnOnce(&mut Context)) -> Handle<Self> {
403 Self {}.build(cx, content)
404 }
405}
406
407impl View for ScrollContent {
408 fn element(&self) -> Option<&'static str> {
409 Some("scroll-content")
410 }
411
412 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
413 event.map(|window_event, _| match window_event {
414 WindowEvent::GeometryChanged(geo) => {
415 if geo.contains(GeoChanged::WIDTH_CHANGED)
416 || geo.contains(GeoChanged::HEIGHT_CHANGED)
417 {
418 let bounds = cx.bounds();
419 cx.emit(ScrollEvent::ChildGeo(bounds.w, bounds.h));
421 }
422 }
423
424 _ => {}
425 });
426 }
427}