1use std::sync::Arc;
2
3use crate::prelude::*;
4
5pub(crate) const SCROLL_SENSITIVITY: f32 = 20.0;
6
7pub enum ScrollEvent {
9 SetX(f32),
11 SetY(f32),
13 ScrollX(f32),
15 ScrollY(f32),
17 ChildGeo(f32, f32),
19
20 ScrollToView(Entity),
21}
22
23pub struct ScrollView {
25 pub scroll_x: Signal<f32>,
27 pub scroll_y: Signal<f32>,
29 pub on_scroll: Option<Arc<dyn Fn(&mut EventContext, f32, f32) + Send + Sync>>,
31 pub inner_width: Signal<f32>,
33 pub inner_height: Signal<f32>,
35 pub container_width: Signal<f32>,
37 pub container_height: Signal<f32>,
39 pub scroll_to_cursor: Signal<bool>,
41 pub show_horizontal_scrollbar: Signal<bool>,
43 pub show_vertical_scrollbar: Signal<bool>,
45}
46
47impl ScrollView {
48 fn map_scroll_x_to_physical(scroll_x: f32, direction: Direction) -> f32 {
49 if direction == Direction::RightToLeft { 1.0 - scroll_x } else { scroll_x }
50 }
51
52 fn map_scroll_x_from_physical(scroll_x: f32, direction: Direction) -> f32 {
53 if direction == Direction::RightToLeft { 1.0 - scroll_x } else { scroll_x }
54 }
55
56 pub fn new<F>(cx: &mut Context, content: F) -> Handle<Self>
58 where
59 F: 'static + FnOnce(&mut Context),
60 {
61 let scroll_to_cursor = Signal::new(false);
62 let scroll_x = Signal::new(0.0_f32);
63 let scroll_y = Signal::new(0.0_f32);
64 let inner_width = Signal::new(0.0_f32);
65 let inner_height = Signal::new(0.0_f32);
66 let container_width = Signal::new(0.0_f32);
67 let container_height = Signal::new(0.0_f32);
68 let show_horizontal_scrollbar = Signal::new(true);
69 let show_vertical_scrollbar = Signal::new(true);
70 let direction = cx.environment().direction;
71 let vertical_ratio: Memo<f32> = Memo::new(move |_| {
72 let inner = inner_height.get();
73 if inner == 0.0_f32 {
74 0.0_f32
75 } else {
76 (container_height.get() / inner).clamp(0.0_f32, 1.0_f32)
77 }
78 });
79
80 let horizontal_ratio: Memo<f32> = Memo::new(move |_| {
81 let inner = inner_width.get();
82 if inner == 0.0_f32 {
83 0.0_f32
84 } else {
85 (container_width.get() / inner).clamp(0.0_f32, 1.0_f32)
86 }
87 });
88
89 let has_h_scroll = Memo::new(move |_| container_width.get() < inner_width.get());
90 let has_v_scroll = Memo::new(move |_| container_height.get() < inner_height.get());
91
92 let horizontal_scrollbar_value: Memo<f32> = Memo::new(move |_| scroll_x.get());
93
94 let scroll_state = Memo::new(move |_| {
95 (
96 scroll_x.get(),
97 scroll_y.get(),
98 inner_width.get(),
99 inner_height.get(),
100 container_width.get(),
101 container_height.get(),
102 direction.get(),
103 )
104 });
105 let scroll_state_signal = scroll_state;
106
107 Self {
108 scroll_to_cursor,
109 scroll_x,
110 scroll_y,
111 on_scroll: None,
112 inner_width,
113 inner_height,
114 container_width,
115 container_height,
116 show_horizontal_scrollbar,
117 show_vertical_scrollbar,
118 }
119 .build(cx, move |cx| {
120 ScrollContent::new(cx, content);
121
122 Binding::new(cx, show_vertical_scrollbar, move |cx| {
123 if show_vertical_scrollbar.get() {
124 Scrollbar::new(
125 cx,
126 scroll_y,
127 vertical_ratio,
128 Orientation::Vertical,
129 |cx, value| {
130 cx.emit(ScrollEvent::SetY(value));
131 },
132 )
133 .position_type(PositionType::Absolute)
134 .scroll_to_cursor(scroll_to_cursor);
135 }
136 });
137
138 Binding::new(cx, show_horizontal_scrollbar, move |cx| {
139 if show_horizontal_scrollbar.get() {
140 Scrollbar::new(
141 cx,
142 horizontal_scrollbar_value,
143 horizontal_ratio,
144 Orientation::Horizontal,
145 |cx, value| {
146 cx.emit(ScrollEvent::SetX(value));
147 },
148 )
149 .position_type(PositionType::Absolute)
150 .scroll_to_cursor(scroll_to_cursor);
151 }
152 });
153 })
154 .bind(scroll_state, move |mut handle| {
155 let (
156 scroll_x,
157 scroll_y,
158 inner_width,
159 inner_height,
160 container_width,
161 container_height,
162 direction,
163 ) = scroll_state_signal.get();
164 let scale_factor = handle.context().scale_factor();
165 let top = ((inner_height - container_height) * scroll_y).round() / scale_factor;
166 let physical_scroll_x = ScrollView::map_scroll_x_to_physical(scroll_x, direction);
167 let left = ((inner_width - container_width) * physical_scroll_x).round() / scale_factor;
168 handle.horizontal_scroll(-left.abs()).vertical_scroll(-top.abs());
169 })
170 .toggle_class("h-scroll", has_h_scroll)
171 .toggle_class("v-scroll", has_v_scroll)
172 }
173
174 fn reset(&mut self) {
175 if self.inner_width.get() == self.container_width.get() {
176 self.scroll_x.set(0.0);
177 }
178
179 if self.inner_height.get() == self.container_height.get() {
180 self.scroll_y.set(0.0);
181 }
182 }
183}
184
185impl View for ScrollView {
186 fn element(&self) -> Option<&'static str> {
187 Some("scrollview")
188 }
189
190 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
191 event.map(|scroll_update, meta| {
192 match scroll_update {
193 ScrollEvent::ScrollX(f) => {
194 let delta = if cx.environment().direction.get() == Direction::RightToLeft {
195 -*f
196 } else {
197 *f
198 };
199 self.scroll_x.set((self.scroll_x.get() + delta).clamp(0.0, 1.0));
200
201 if let Some(callback) = &self.on_scroll {
202 (callback)(cx, self.scroll_x.get(), self.scroll_y.get());
203 }
204 }
205
206 ScrollEvent::ScrollY(f) => {
207 self.scroll_y.set((self.scroll_y.get() + *f).clamp(0.0, 1.0));
208 if let Some(callback) = &self.on_scroll {
209 (callback)(cx, self.scroll_x.get(), self.scroll_y.get());
210 }
211 }
212
213 ScrollEvent::SetX(f) => {
214 self.scroll_x.set(*f);
215 if let Some(callback) = &self.on_scroll {
216 (callback)(cx, self.scroll_x.get(), self.scroll_y.get());
217 }
218 }
219
220 ScrollEvent::SetY(f) => {
221 self.scroll_y.set(*f);
222 if let Some(callback) = &self.on_scroll {
223 (callback)(cx, self.scroll_x.get(), self.scroll_y.get());
224 }
225 }
226
227 ScrollEvent::ChildGeo(w, h) => {
228 let bounds = cx.bounds();
229 let scale_factor = cx.scale_factor();
230
231 let mut scroll_x = self.scroll_x.get();
232 let mut scroll_y = self.scroll_y.get();
233 let mut inner_width = self.inner_width.get();
234 let mut inner_height = self.inner_height.get();
235 let mut container_width = self.container_width.get();
236 let mut container_height = self.container_height.get();
237
238 if inner_width != 0.0 && inner_height != 0.0 {
239 let top =
240 ((inner_height - container_height) * scroll_y).round() / scale_factor;
241 let physical_scroll_x = ScrollView::map_scroll_x_to_physical(
242 scroll_x,
243 cx.environment().direction.get(),
244 );
245 let left = ((inner_width - container_width) * physical_scroll_x).round()
246 / scale_factor;
247
248 container_width = bounds.width();
249 container_height = bounds.height();
250 inner_width = *w;
251 inner_height = *h;
252
253 if inner_width != container_width {
254 let physical_scroll_x = ((left * scale_factor)
255 / (inner_width - container_width))
256 .clamp(0.0, 1.0);
257 scroll_x = ScrollView::map_scroll_x_from_physical(
258 physical_scroll_x,
259 cx.environment().direction.get(),
260 );
261 } else {
262 scroll_x = 0.0;
263 }
264
265 if inner_height != container_height {
266 scroll_y = ((top * scale_factor) / (inner_height - container_height))
267 .clamp(0.0, 1.0);
268 } else {
269 scroll_y = 0.0;
270 }
271
272 self.scroll_x.set(scroll_x);
273 self.scroll_y.set(scroll_y);
274 self.inner_width.set(inner_width);
275 self.inner_height.set(inner_height);
276 self.container_width.set(container_width);
277 self.container_height.set(container_height);
278
279 if let Some(callback) = &self.on_scroll {
280 (callback)(cx, self.scroll_x.get(), self.scroll_y.get());
281 }
282
283 self.reset();
284 }
285
286 self.inner_width.set(*w);
287 self.inner_height.set(*h);
288 self.reset();
289 }
290
291 ScrollEvent::ScrollToView(entity) => {
292 let view_bounds = cx.cache.get_bounds(*entity);
293
294 let content_bounds = cx.bounds();
295
296 let direction = cx.environment().direction.get();
297 let mut physical_scroll_x =
298 ScrollView::map_scroll_x_to_physical(self.scroll_x.get(), direction);
299
300 let dx = content_bounds.right() - view_bounds.right();
301 let dy = content_bounds.bottom() - view_bounds.bottom();
302
303 if dx < 0.0 {
305 let sx = (-dx / (self.inner_width.get() - self.container_width.get()))
306 .clamp(0.0, 1.0);
307 physical_scroll_x = (physical_scroll_x + sx).clamp(0.0, 1.0);
308 }
309
310 if dy < 0.0 {
311 let sy = (-dy / (self.inner_height.get() - self.container_height.get()))
312 .clamp(0.0, 1.0);
313 self.scroll_y.set((self.scroll_y.get() + sy).clamp(0.0, 1.0));
314 }
315
316 let dx = view_bounds.left() - content_bounds.left();
317 let dy = view_bounds.top() - content_bounds.top();
318
319 if dx < 0.0 {
320 let sx = (-dx / (self.inner_width.get() - self.container_width.get()))
321 .clamp(0.0, 1.0);
322 physical_scroll_x = (physical_scroll_x - sx).clamp(0.0, 1.0);
323 }
324
325 self.scroll_x
326 .set(ScrollView::map_scroll_x_from_physical(physical_scroll_x, direction));
327
328 if dy < 0.0 {
329 let sy = (-dy / (self.inner_height.get() - self.container_height.get()))
330 .clamp(0.0, 1.0);
331 self.scroll_y.set((self.scroll_y.get() - sy).clamp(0.0, 1.0));
332 }
333
334 if let Some(callback) = &self.on_scroll {
335 (callback)(cx, self.scroll_x.get(), self.scroll_y.get());
336 }
337 }
338 }
339
340 meta.consume();
343 });
344
345 event.map(|window_event, meta| match window_event {
346 WindowEvent::GeometryChanged(geo) => {
347 if geo.contains(GeoChanged::WIDTH_CHANGED)
348 || geo.contains(GeoChanged::HEIGHT_CHANGED)
349 {
350 let bounds = cx.bounds();
351 let scale_factor = cx.scale_factor();
352
353 let mut scroll_x = self.scroll_x.get();
354 let mut scroll_y = self.scroll_y.get();
355 let inner_width = self.inner_width.get();
356 let inner_height = self.inner_height.get();
357 let mut container_width = self.container_width.get();
358 let mut container_height = self.container_height.get();
359
360 if inner_width != 0.0 && inner_height != 0.0 {
361 let top =
362 ((inner_height - container_height) * scroll_y).round() / scale_factor;
363 let physical_scroll_x = ScrollView::map_scroll_x_to_physical(
364 scroll_x,
365 cx.environment().direction.get(),
366 );
367 let left = ((inner_width - container_width) * physical_scroll_x).round()
368 / scale_factor;
369
370 container_width = bounds.width();
371 container_height = bounds.height();
372
373 if inner_width != container_width {
374 let physical_scroll_x = ((left * scale_factor)
375 / (inner_width - container_width))
376 .clamp(0.0, 1.0);
377 scroll_x = ScrollView::map_scroll_x_from_physical(
378 physical_scroll_x,
379 cx.environment().direction.get(),
380 );
381 } else {
382 scroll_x = 0.0;
383 }
384
385 if inner_height != container_height {
386 scroll_y = ((top * scale_factor) / (inner_height - container_height))
387 .clamp(0.0, 1.0);
388 } else {
389 scroll_y = 0.0;
390 }
391
392 self.scroll_x.set(scroll_x);
393 self.scroll_y.set(scroll_y);
394 self.container_width.set(container_width);
395 self.container_height.set(container_height);
396
397 if let Some(callback) = &self.on_scroll {
398 (callback)(cx, self.scroll_x.get(), self.scroll_y.get());
399 }
400
401 self.reset();
402 }
403
404 self.container_width.set(bounds.width());
405 self.container_height.set(bounds.height());
406 }
407 }
408
409 WindowEvent::MouseScroll(x, y) => {
410 cx.set_active(true);
411 let (x, y) = if cx.modifiers.shift() { (-*y, -*x) } else { (-*x, -*y) };
412
413 if x != 0.0 && self.inner_width.get() > self.container_width.get() {
415 let negative_space = self.inner_width.get() - self.container_width.get();
416 if negative_space != 0.0 {
417 let logical_delta = x * SCROLL_SENSITIVITY / negative_space;
418 cx.emit(ScrollEvent::ScrollX(logical_delta));
419 }
420 meta.consume();
422 }
423 if y != 0.0 && self.inner_height.get() > self.container_height.get() {
424 let negative_space = self.inner_height.get() - self.container_height.get();
425 if negative_space != 0.0 {
426 let logical_delta = y * SCROLL_SENSITIVITY / negative_space;
427 cx.emit(ScrollEvent::ScrollY(logical_delta));
428 }
429 meta.consume();
431 }
432 }
433
434 WindowEvent::MouseOut => {
435 cx.set_active(false);
436 }
437
438 _ => {}
439 });
440 }
441}
442
443impl Handle<'_, ScrollView> {
444 pub fn on_scroll(
446 self,
447 callback: impl Fn(&mut EventContext, f32, f32) + 'static + Send + Sync,
448 ) -> Self {
449 self.modify(|scrollview| scrollview.on_scroll = Some(Arc::new(callback)))
450 }
451
452 pub fn scroll_to_cursor(self, scroll_to_cursor: impl Res<bool> + 'static) -> Self {
454 let scroll_to_cursor = scroll_to_cursor.to_signal(self.cx);
455 self.bind(scroll_to_cursor, move |handle| {
456 handle.modify(|scrollview| scrollview.scroll_to_cursor.set(scroll_to_cursor.get()));
457 })
458 }
459
460 pub fn scroll_x(self, scrollx: impl Res<f32> + 'static) -> Self {
462 let scrollx = scrollx.to_signal(self.cx);
463 self.bind(scrollx, move |handle| {
464 handle.modify(|scrollview| scrollview.scroll_x.set(scrollx.get()));
465 })
466 }
467
468 pub fn scroll_y(self, scrolly: impl Res<f32> + 'static) -> Self {
470 let scrolly = scrolly.to_signal(self.cx);
471 self.bind(scrolly, move |handle| {
472 handle.modify(|scrollview| scrollview.scroll_y.set(scrolly.get()));
473 })
474 }
475
476 pub fn show_horizontal_scrollbar(self, flag: impl Res<bool> + 'static) -> Self {
478 let flag = flag.to_signal(self.cx);
479 self.bind(flag, move |handle| {
480 handle.modify(|scrollview| scrollview.show_horizontal_scrollbar.set(flag.get()));
481 })
482 }
483
484 pub fn show_vertical_scrollbar(self, flag: impl Res<bool> + 'static) -> Self {
486 let flag = flag.to_signal(self.cx);
487 self.bind(flag, move |handle| {
488 handle.modify(|scrollview| scrollview.show_vertical_scrollbar.set(flag.get()));
489 })
490 }
491}
492
493struct ScrollContent {}
494
495impl ScrollContent {
496 pub fn new(cx: &mut Context, content: impl FnOnce(&mut Context)) -> Handle<Self> {
497 Self {}.build(cx, content)
498 }
499}
500
501impl View for ScrollContent {
502 fn element(&self) -> Option<&'static str> {
503 Some("scroll-content")
504 }
505
506 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
507 event.map(|window_event, _| match window_event {
508 WindowEvent::GeometryChanged(geo) => {
509 if geo.contains(GeoChanged::WIDTH_CHANGED)
510 || geo.contains(GeoChanged::HEIGHT_CHANGED)
511 {
512 let bounds = cx.bounds();
513 cx.emit(ScrollEvent::ChildGeo(bounds.w, bounds.h));
515 }
516 }
517
518 _ => {}
519 });
520 }
521}