1use std::cell::{Cell, RefCell};
2use std::rc::Rc;
3
4use web_time::Instant;
5
6use crate::nested_scroll::{NestedScrollConnection, NestedScrollSource};
7use crate::{ControlVisualSet, Dp, Signal, Vec2, request_frame, signal};
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum ScrollAxis {
12 Vertical,
13 Horizontal,
14 Both,
15}
16
17#[derive(Clone, Debug, PartialEq)]
21pub struct ScrollbarStyle {
22 pub thickness: Dp,
23 pub track_inset: Dp,
24 pub min_thumb_length: Dp,
25 pub fixed_thumb_length: Option<Dp>,
26 pub radius: Option<Dp>,
27 pub track_visuals: ControlVisualSet,
28 pub thumb_visuals: ControlVisualSet,
29}
30
31impl Default for ScrollbarStyle {
32 fn default() -> Self {
33 Self {
34 thickness: Dp(4.0),
35 track_inset: Dp(2.0),
36 min_thumb_length: Dp(24.0),
37 fixed_thumb_length: None,
38 radius: None,
39 track_visuals: ControlVisualSet::default(),
40 thumb_visuals: ControlVisualSet::default(),
41 }
42 }
43}
44
45impl ScrollbarStyle {
46 pub fn with_thickness(mut self, thickness: Dp) -> Self {
47 self.thickness = thickness;
48 self
49 }
50
51 pub fn with_track_inset(mut self, track_inset: Dp) -> Self {
52 self.track_inset = track_inset;
53 self
54 }
55
56 pub fn with_min_thumb_length(mut self, min_thumb_length: Dp) -> Self {
57 self.min_thumb_length = min_thumb_length;
58 self
59 }
60
61 pub fn with_fixed_thumb_length(mut self, length: Option<Dp>) -> Self {
62 self.fixed_thumb_length = length;
63 self
64 }
65
66 pub fn with_radius(mut self, radius: Option<Dp>) -> Self {
67 self.radius = radius;
68 self
69 }
70
71 pub fn with_track_visuals(mut self, visuals: ControlVisualSet) -> Self {
72 self.track_visuals = visuals;
73 self
74 }
75
76 pub fn with_thumb_visuals(mut self, visuals: ControlVisualSet) -> Self {
77 self.thumb_visuals = visuals;
78 self
79 }
80}
81
82#[derive(Clone, Default)]
84pub struct ScrollAxisBinding {
85 pub on_scroll: Option<Rc<dyn Fn(Vec2) -> Vec2>>,
86 pub set_viewport_main: Option<Rc<dyn Fn(f32)>>,
88 pub set_content_main: Option<Rc<dyn Fn(f32)>>,
90 pub get_offset_main: Option<Rc<dyn Fn() -> f32>>,
91 pub set_offset_main: Option<Rc<dyn Fn(f32)>>,
92 pub show_scrollbar: bool,
93 pub tick: Option<Rc<dyn Fn()>>,
94 pub set_nested_scroll_parent: Option<Rc<dyn Fn(NestedScrollConnection)>>,
95}
96
97impl std::fmt::Debug for ScrollAxisBinding {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 f.debug_struct("ScrollAxisBinding")
100 .field("has_on_scroll", &self.on_scroll.is_some())
101 .field("set_viewport_main", &self.set_viewport_main.is_some())
102 .field("set_content_main", &self.set_content_main.is_some())
103 .field("get_offset_main", &self.get_offset_main.is_some())
104 .field("set_offset_main", &self.set_offset_main.is_some())
105 .field("show_scrollbar", &self.show_scrollbar)
106 .field("tick", &self.tick.is_some())
107 .field(
108 "set_nested_scroll_parent",
109 &self.set_nested_scroll_parent.is_some(),
110 )
111 .finish()
112 }
113}
114
115#[derive(Clone, Default)]
117pub struct ScrollBothBinding {
118 pub on_scroll: Option<Rc<dyn Fn(Vec2) -> Vec2>>,
119 pub set_viewport_width: Option<Rc<dyn Fn(f32)>>,
120 pub set_viewport_height: Option<Rc<dyn Fn(f32)>>,
121 pub set_content_width: Option<Rc<dyn Fn(f32)>>,
122 pub set_content_height: Option<Rc<dyn Fn(f32)>>,
123 pub get_offset_xy: Option<Rc<dyn Fn() -> (f32, f32)>>,
124 pub set_offset_xy: Option<Rc<dyn Fn(f32, f32)>>,
125 pub show_scrollbar: bool,
126 pub tick: Option<Rc<dyn Fn()>>,
127 pub set_nested_scroll_parent: Option<Rc<dyn Fn(NestedScrollConnection)>>,
128}
129
130impl std::fmt::Debug for ScrollBothBinding {
131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 f.debug_struct("ScrollBothBinding")
133 .field("has_on_scroll", &self.on_scroll.is_some())
134 .field("set_viewport_width", &self.set_viewport_width.is_some())
135 .field("set_viewport_height", &self.set_viewport_height.is_some())
136 .field("set_content_width", &self.set_content_width.is_some())
137 .field("set_content_height", &self.set_content_height.is_some())
138 .field("get_offset_xy", &self.get_offset_xy.is_some())
139 .field("set_offset_xy", &self.set_offset_xy.is_some())
140 .field("show_scrollbar", &self.show_scrollbar)
141 .field("tick", &self.tick.is_some())
142 .field(
143 "set_nested_scroll_parent",
144 &self.set_nested_scroll_parent.is_some(),
145 )
146 .finish()
147 }
148}
149
150#[derive(Clone, Debug)]
151pub enum ScrollBinding {
152 Vertical(ScrollAxisBinding),
153 Horizontal(ScrollAxisBinding),
154 Both(ScrollBothBinding),
155}
156
157impl ScrollBinding {
158 pub fn axis(&self) -> ScrollAxis {
159 match self {
160 ScrollBinding::Vertical(_) => ScrollAxis::Vertical,
161 ScrollBinding::Horizontal(_) => ScrollAxis::Horizontal,
162 ScrollBinding::Both(_) => ScrollAxis::Both,
163 }
164 }
165}
166
167const OVERSHOOT_DECAY_PER_60HZ: f32 = 0.78;
168
169pub const SCROLL_DECAY_PER_60HZ: f32 = 0.85;
171pub const SCROLL_STOP_VELOCITY: f32 = 15.0;
173pub const SCROLL_FLING_ACTIVATE_VELOCITY: f32 = 50.0;
175pub const SCROLL_OVERSROLL_BAND_MAX: f32 = 150.0;
177
178fn clamp_offset(off: f32, max: f32) -> f32 {
183 let max = if max.is_finite() { max.max(0.0) } else { 0.0 };
184 if off.is_finite() {
185 off.clamp(0.0, max)
186 } else {
187 0.0
188 }
189}
190
191pub fn rubber_band(amount: f32, max: f32) -> f32 {
194 let abs_val = amount.abs();
195 if abs_val <= 0.0 {
196 return 0.0;
197 }
198 (1.0 - 1.0 / (1.0 + abs_val / max)) * max * amount.signum()
199}
200
201pub fn os_scroll_axis(
204 off: &Signal<f32>,
205 os: &Signal<f32>,
206 overscroll_enabled: bool,
207 max_off: f32,
208 before: f32,
209 leftover: f32,
210 physics: &RefCell<ScrollPhysics>,
211) -> f32 {
212 if !overscroll_enabled || leftover.abs() < 0.001 {
213 return leftover;
214 }
215 let os_val = os.get();
216 if os_val.abs() > 0.5 && os_val.signum() * leftover < 0.0 {
218 let reduction = leftover.abs().min(os_val.abs());
219 os.set(os_val - os_val.signum() * reduction);
220 let remainder = leftover - leftover.signum() * reduction;
221 if remainder.abs() > 0.5 {
222 let new_off = clamp_offset(before + remainder, max_off);
223 off.set(new_off);
224 let consumed = new_off - before;
225 physics.borrow_mut().record_nested_input(consumed);
226 return remainder - consumed;
227 }
228 return 0.0;
229 }
230
231 let at_edge = (before <= 0.0 && leftover < 0.0) || (before >= max_off && leftover > 0.0);
233 if max_off > 5.0 && at_edge && leftover.abs() > 0.5 {
234 os.set(os_val + rubber_band(leftover, SCROLL_OVERSROLL_BAND_MAX));
235 physics.borrow_mut().record_nested_input(0.0);
236 return 0.0;
237 }
238 leftover
239}
240
241pub fn tick_os_axis(os: &Signal<f32>, enabled: bool, dt: f32) -> bool {
243 if !enabled {
244 return false;
245 }
246 let v = os.get();
247 if v.abs() > 0.5 {
248 let decayed = v * OVERSHOOT_DECAY_PER_60HZ.powf(dt * 60.0);
249 if decayed.abs() < 0.5 {
250 os.set(0.0);
251 } else {
252 os.set(decayed);
253 }
254 request_frame();
255 true
256 } else {
257 false
258 }
259}
260
261#[derive(Clone)]
264pub struct ScrollPhysics {
265 pub(crate) vel: f32,
266 last_t: Instant,
267 last_input_t: Instant,
268 pub(crate) animating: bool,
269 input_pending: bool,
271 last_dt: f32,
272 decay_per_60hz: f32,
273 stop_velocity: f32,
274 input_activate_velocity: f32,
275}
276
277impl Default for ScrollPhysics {
278 fn default() -> Self {
279 Self::new()
280 }
281}
282
283impl ScrollPhysics {
284 pub fn new() -> Self {
285 Self::with_tuning(
286 SCROLL_DECAY_PER_60HZ,
287 SCROLL_STOP_VELOCITY,
288 SCROLL_FLING_ACTIVATE_VELOCITY,
289 )
290 }
291
292 pub fn with_tuning(
293 decay_per_60hz: f32,
294 stop_velocity: f32,
295 input_activate_velocity: f32,
296 ) -> Self {
297 let now = Instant::now();
298 Self {
299 vel: 0.0,
300 last_t: now,
301 last_input_t: now,
302 animating: false,
303 input_pending: false,
304 last_dt: 1.0 / 60.0,
305 decay_per_60hz,
306 stop_velocity,
307 input_activate_velocity,
308 }
309 }
310
311 pub fn record_user_input(&mut self, consumed: f32) {
320 let (instant_vel, fresh) = self.sample(consumed);
321 if fresh || (self.animating && instant_vel * self.vel < 0.0) {
322 self.vel = instant_vel;
323 } else {
324 const SMOOTHING: f32 = 0.35;
325 self.vel = self.vel * (1.0 - SMOOTHING) + instant_vel * SMOOTHING;
326 }
327 self.finish_sample();
328 }
329
330 pub fn record_nested_input(&mut self, consumed: f32) {
334 let (instant_vel, _) = self.sample(consumed);
335 const SMOOTHING: f32 = 0.35;
336 self.vel = self.vel * (1.0 - SMOOTHING) + instant_vel * SMOOTHING;
337 self.finish_sample();
338 }
339
340 fn sample(&mut self, consumed: f32) -> (f32, bool) {
341 let now = Instant::now();
342 let raw_dt = (now - self.last_input_t).as_secs_f32();
343 let dt = raw_dt.clamp(1.0 / 240.0, 1.0 / 15.0);
344 self.last_dt = dt;
345 let fresh = raw_dt > 0.05;
347 self.last_input_t = now;
348 if raw_dt > 0.1 {
349 self.vel = 0.0;
350 }
351 (consumed / dt, fresh)
352 }
353
354 fn finish_sample(&mut self) {
355 const MAX_VEL: f32 = 8000.0;
356 self.vel = self.vel.clamp(-MAX_VEL, MAX_VEL);
357 self.animating = self.vel.abs() > self.input_activate_velocity;
358 self.input_pending = true;
359 }
360
361 fn dt(&mut self) -> f32 {
362 let now = Instant::now();
363 let dt = (now - self.last_t).as_secs_f32().min(0.1);
364 self.last_t = now;
365 dt
366 }
367
368 pub fn tick_integrate(&mut self, current: f32, min: f32, max: f32) -> Option<f32> {
369 let dt = self.dt();
374 if self.input_pending {
375 self.input_pending = false;
381 return None;
382 }
383 if !self.animating {
384 return None;
385 }
386 if dt <= 0.0 {
387 return None;
388 }
389 let vel0 = self.vel;
390 if vel0.abs() < self.stop_velocity {
391 self.vel = 0.0;
392 self.animating = false;
393 return None;
394 }
395 let new = {
396 let raw = current + vel0 * dt;
397 let raw = if raw.is_finite() { raw } else { current };
398 raw.clamp(min, max.max(min))
399 };
400 if (new - current).abs() < 0.01 && (current <= min || current >= max) {
401 self.vel = 0.0;
402 self.animating = false;
403 return None;
404 }
405 let speed = self.vel.abs();
406 let t = (speed / 4000.0).min(1.0);
407 let effective_decay = (self.decay_per_60hz + t * 0.10).min(0.999);
408 let decay = effective_decay.powf(dt * 60.0);
409 self.vel = vel0 * decay;
410 if self.vel.abs() < self.stop_velocity {
411 self.vel = 0.0;
412 self.animating = false;
413 return None;
414 }
415 Some(new)
416 }
417
418 pub fn is_animating(&self) -> bool {
419 self.animating
420 }
421}
422
423#[derive(Clone)]
425pub struct ScrollState {
426 scroll_offset: Signal<f32>,
427 viewport_height: Signal<f32>,
428 content_height: Signal<f32>,
429 physics: Rc<RefCell<ScrollPhysics>>,
430 overscroll: Signal<f32>,
431 overscroll_enabled: Cell<bool>,
432 pub(crate) parent_connection: Rc<RefCell<Option<NestedScrollConnection>>>,
433 show_scrollbar: Cell<bool>,
434 prev_tick: Cell<Instant>,
435}
436
437impl Default for ScrollState {
438 fn default() -> Self {
439 Self::new()
440 }
441}
442
443impl ScrollState {
444 pub fn new() -> Self {
445 Self {
446 scroll_offset: signal(0.0),
447 viewport_height: signal(0.0),
448 content_height: signal(0.0),
449 physics: Rc::new(RefCell::new(ScrollPhysics::new())),
450 overscroll: signal(0.0),
451 overscroll_enabled: Cell::new(true),
452 parent_connection: Rc::new(RefCell::new(None)),
453 show_scrollbar: Cell::new(true),
454 prev_tick: Cell::new(Instant::now()),
455 }
456 }
457
458 pub fn set_overscroll_enabled(&self, enabled: bool) {
459 self.overscroll_enabled.set(enabled);
460 }
461
462 pub fn overscroll_offset(&self) -> f32 {
463 self.overscroll.get()
464 }
465
466 pub fn set_nested_scroll_parent(&self, conn: NestedScrollConnection) {
467 *self.parent_connection.borrow_mut() = Some(conn);
468 }
469
470 pub fn connection(&self) -> NestedScrollConnection {
473 let this = Rc::new(self.clone());
474 let pc = Rc::clone(&this.parent_connection);
475 NestedScrollConnection::new().on_post_scroll(
476 move |_consumed: Vec2, available: Vec2, _source: NestedScrollSource| -> Vec2 {
477 if available.x.abs() < 0.001 && available.y.abs() < 0.001 {
478 return Vec2::ZERO;
479 }
480 let leftover_y = this.scroll_nested(available.y);
481 let after = run_post_scroll(
482 &pc,
483 Vec2 {
484 x: available.x,
485 y: leftover_y,
486 },
487 );
488 Vec2 {
489 x: available.x - after.x,
490 y: available.y - after.y,
491 }
492 },
493 )
494 }
495
496 pub fn set_show_scrollbar(&self, show: bool) {
497 self.show_scrollbar.set(show);
498 }
499
500 pub fn set_viewport_height(&self, h: f32) {
501 let h = h.max(0.0);
502 if (self.viewport_height.get() - h).abs() > 0.5 {
503 self.viewport_height.set(h);
504 self.clamp_offset();
505 }
506 }
507
508 pub fn set_content_height(&self, h: f32) {
509 let h = h.max(0.0);
510 if (self.content_height.get() - h).abs() > 0.5 {
511 self.content_height.set(h);
512 self.clamp_offset();
513 }
514 }
515
516 pub fn set_overscroll(&self, val: f32) {
517 self.overscroll.set(val);
518 }
519
520 pub fn set_offset(&self, off: f32) {
521 let off = if off.is_finite() { off } else { 0.0 };
522 let vh = self.viewport_height.get();
523 let ch = self.content_height.get();
524 let max_off = (ch - vh).max(0.0);
525 self.scroll_offset.set(clamp_offset(off, max_off));
526 }
527
528 fn clamp_offset(&self) {
529 let vh = self.viewport_height.get();
530 let ch = self.content_height.get();
531 let max_off = (ch - vh).max(0.0);
532 self.scroll_offset.update(|o| {
533 if *o > max_off {
534 *o = max_off;
535 }
536 if *o < 0.0 {
537 *o = 0.0;
538 }
539 });
540 }
541
542 pub fn get(&self) -> f32 {
543 self.scroll_offset.get()
544 }
545
546 pub fn scroll_immediate(&self, dy: f32) -> f32 {
550 self.apply_delta(dy, true)
551 }
552
553 pub fn scroll_nested(&self, dy: f32) -> f32 {
557 self.apply_delta(dy, false)
558 }
559
560 fn apply_delta(&self, dy: f32, user: bool) -> f32 {
561 let dy = if dy.is_finite() { dy } else { 0.0 };
562 let before = self.scroll_offset.get();
563 let vh = self.viewport_height.get();
564 let ch = self.content_height.get();
565 let max_off = (ch - vh).max(0.0);
566
567 let new_off = clamp_offset(before + dy, max_off);
568 self.scroll_offset.set(new_off);
569 let consumed = new_off - before;
570 if user {
571 self.physics.borrow_mut().record_user_input(consumed);
572 } else {
573 self.physics.borrow_mut().record_nested_input(consumed);
574 }
575 dy - consumed
576 }
577
578 pub fn apply_overscroll(&self, leftover: f32) -> f32 {
582 let vh = self.viewport_height.get();
583 let ch = self.content_height.get();
584 os_scroll_axis(
585 &self.scroll_offset,
586 &self.overscroll,
587 self.overscroll_enabled.get(),
588 (ch - vh).max(0.0),
589 self.scroll_offset.get(),
590 leftover,
591 &self.physics,
592 )
593 }
594
595 pub fn tick(&self) -> bool {
596 let now = Instant::now();
597 let dt = (now - self.prev_tick.get()).as_secs_f32().min(0.1);
598 self.prev_tick.set(now);
599
600 if tick_os_axis(&self.overscroll, self.overscroll_enabled.get(), dt) {
601 return true;
602 }
603
604 let vh = self.viewport_height.get();
605 let ch = self.content_height.get();
606 let max_off = (ch - vh).max(0.0);
607
608 let mut p = self.physics.borrow_mut();
609 if let Some(new_off) = p.tick_integrate(self.scroll_offset.get(), 0.0, max_off) {
610 drop(p);
611 self.scroll_offset.set(new_off);
612 request_frame();
613 true
614 } else {
615 false
616 }
617 }
618
619 pub fn show_scrollbar(&self) -> bool {
620 self.show_scrollbar.get()
621 }
622
623 pub fn to_binding(&self) -> ScrollBinding {
625 let this = Rc::new(self.clone());
626 let pc = Rc::clone(&this.parent_connection);
627 let c_on_scroll = {
628 let state = Rc::clone(&pc);
629 let this = Rc::clone(&this);
630 Rc::new(move |d: Vec2| -> Vec2 {
631 let d = run_pre_scroll(&state, d);
632 let leftover_y = this.scroll_immediate(d.y);
635 let result = Vec2 {
636 x: d.x,
637 y: leftover_y,
638 };
639 let after_parent = run_post_scroll(&state, result);
640 let final_y = this.apply_overscroll(after_parent.y);
641 Vec2 {
642 x: after_parent.x,
643 y: final_y,
644 }
645 })
646 } as Rc<dyn Fn(Vec2) -> Vec2>;
647 let c_set_viewport = {
648 let this = Rc::clone(&this);
649 Rc::new(move |h: f32| this.set_viewport_height(h))
650 };
651 let c_set_content = {
652 let this = Rc::clone(&this);
653 Rc::new(move |h: f32| this.set_content_height(h))
654 };
655 let c_get = {
656 let this = Rc::clone(&this);
657 Rc::new(move || this.get() + this.overscroll_offset())
658 };
659 let c_set = {
660 let this = Rc::clone(&this);
661 Rc::new(move |off: f32| this.set_offset(off))
662 };
663 let c_tick = {
664 let this = Rc::clone(&this);
665 Rc::new(move || {
666 this.tick();
667 })
668 };
669 let c_set_nested = {
670 let state = Rc::clone(&pc);
671 Rc::new(move |conn| {
672 *state.borrow_mut() = Some(conn);
673 })
674 };
675 ScrollBinding::Vertical(ScrollAxisBinding {
676 on_scroll: Some(c_on_scroll),
677 set_viewport_main: Some(c_set_viewport),
678 set_content_main: Some(c_set_content),
679 get_offset_main: Some(c_get),
680 set_offset_main: Some(c_set),
681 show_scrollbar: self.show_scrollbar(),
682 tick: Some(c_tick),
683 set_nested_scroll_parent: Some(c_set_nested),
684 })
685 }
686}
687
688#[derive(Clone)]
690pub struct HorizontalScrollState {
691 scroll_offset: Signal<f32>,
692 viewport_width: Signal<f32>,
693 content_width: Signal<f32>,
694 physics: Rc<RefCell<ScrollPhysics>>,
695 overscroll: Signal<f32>,
696 overscroll_enabled: Cell<bool>,
697 pub(crate) parent_connection: Rc<RefCell<Option<NestedScrollConnection>>>,
698 show_scrollbar: Cell<bool>,
699 prev_tick: Cell<Instant>,
700}
701
702impl Default for HorizontalScrollState {
703 fn default() -> Self {
704 Self::new()
705 }
706}
707
708impl HorizontalScrollState {
709 pub fn new() -> Self {
710 Self {
711 scroll_offset: signal(0.0),
712 viewport_width: signal(0.0),
713 content_width: signal(0.0),
714 physics: Rc::new(RefCell::new(ScrollPhysics::new())),
715 overscroll: signal(0.0),
716 overscroll_enabled: Cell::new(true),
717 parent_connection: Rc::new(RefCell::new(None)),
718 show_scrollbar: Cell::new(true),
719 prev_tick: Cell::new(Instant::now()),
720 }
721 }
722
723 pub fn set_overscroll_enabled(&self, enabled: bool) {
724 self.overscroll_enabled.set(enabled);
725 }
726
727 pub fn overscroll_offset(&self) -> f32 {
728 self.overscroll.get()
729 }
730
731 pub fn set_nested_scroll_parent(&self, conn: NestedScrollConnection) {
732 *self.parent_connection.borrow_mut() = Some(conn);
733 }
734
735 pub fn connection(&self) -> NestedScrollConnection {
739 let this = Rc::new(self.clone());
740 let pc = Rc::clone(&this.parent_connection);
741 NestedScrollConnection::new().on_post_scroll(
742 move |_consumed: Vec2, available: Vec2, _source: NestedScrollSource| -> Vec2 {
743 if available.x.abs() < 0.001 && available.y.abs() < 0.001 {
744 return Vec2::ZERO;
745 }
746 let leftover_x = this.scroll_nested(available.x);
747 let after = run_post_scroll(
748 &pc,
749 Vec2 {
750 x: leftover_x,
751 y: available.y,
752 },
753 );
754 Vec2 {
755 x: available.x - after.x,
756 y: available.y - after.y,
757 }
758 },
759 )
760 }
761
762 pub fn set_show_scrollbar(&self, show: bool) {
763 self.show_scrollbar.set(show);
764 }
765
766 pub fn set_viewport_width(&self, w: f32) {
767 let w = w.max(0.0);
768 if (self.viewport_width.get() - w).abs() > 0.5 {
769 self.viewport_width.set(w);
770 self.clamp();
771 }
772 }
773
774 pub fn set_content_width(&self, w: f32) {
775 let w = w.max(0.0);
776 if (self.content_width.get() - w).abs() > 0.5 {
777 self.content_width.set(w);
778 self.clamp();
779 }
780 }
781
782 pub fn set_overscroll(&self, val: f32) {
783 self.overscroll.set(val);
784 }
785
786 pub fn set_offset(&self, off: f32) {
787 let max_off = (self.content_width.get() - self.viewport_width.get()).max(0.0);
788 self.scroll_offset.set(clamp_offset(off, max_off));
789 }
790
791 fn clamp(&self) {
792 let max_off = (self.content_width.get() - self.viewport_width.get()).max(0.0);
793 self.scroll_offset.update(|o| {
794 *o = clamp_offset(*o, max_off);
795 });
796 }
797
798 pub fn get(&self) -> f32 {
799 self.scroll_offset.get()
800 }
801
802 pub fn scroll_immediate(&self, dx: f32) -> f32 {
805 self.apply_delta(dx, true)
806 }
807
808 pub fn scroll_nested(&self, dx: f32) -> f32 {
811 self.apply_delta(dx, false)
812 }
813
814 fn apply_delta(&self, dx: f32, user: bool) -> f32 {
815 let dx = if dx.is_finite() { dx } else { 0.0 };
816 let before = self.scroll_offset.get();
817 let max_off = (self.content_width.get() - self.viewport_width.get()).max(0.0);
818
819 let new_off = clamp_offset(before + dx, max_off);
820 self.scroll_offset.set(new_off);
821 let consumed = new_off - before;
822 if user {
823 self.physics.borrow_mut().record_user_input(consumed);
824 } else {
825 self.physics.borrow_mut().record_nested_input(consumed);
826 }
827 dx - consumed
828 }
829
830 pub fn apply_overscroll(&self, leftover: f32) -> f32 {
832 let max_off = (self.content_width.get() - self.viewport_width.get()).max(0.0);
833 os_scroll_axis(
834 &self.scroll_offset,
835 &self.overscroll,
836 self.overscroll_enabled.get(),
837 max_off,
838 self.scroll_offset.get(),
839 leftover,
840 &self.physics,
841 )
842 }
843
844 pub fn tick(&self) -> bool {
845 let now = Instant::now();
846 let dt = (now - self.prev_tick.get()).as_secs_f32().min(0.1);
847 self.prev_tick.set(now);
848
849 if tick_os_axis(&self.overscroll, self.overscroll_enabled.get(), dt) {
850 return true;
851 }
852
853 let max_off = (self.content_width.get() - self.viewport_width.get()).max(0.0);
854 let mut p = self.physics.borrow_mut();
855 if let Some(new_off) = p.tick_integrate(self.scroll_offset.get(), 0.0, max_off) {
856 drop(p);
857 self.scroll_offset.set(new_off);
858 request_frame();
859 true
860 } else {
861 false
862 }
863 }
864
865 pub fn show_scrollbar(&self) -> bool {
866 self.show_scrollbar.get()
867 }
868
869 pub fn to_binding(&self) -> ScrollBinding {
870 let this = Rc::new(self.clone());
871 let pc = Rc::clone(&this.parent_connection);
872 let c_on_scroll = {
873 let state = Rc::clone(&pc);
874 let this = Rc::clone(&this);
875 Rc::new(move |d: Vec2| -> Vec2 {
876 let d = run_pre_scroll(&state, d);
877 let leftover_x = this.scroll_immediate(d.x);
878 let result = Vec2 {
879 x: leftover_x,
880 y: d.y,
881 };
882 let after_parent = run_post_scroll(&state, result);
883 let final_x = this.apply_overscroll(after_parent.x);
884 Vec2 {
885 x: final_x,
886 y: after_parent.y,
887 }
888 })
889 } as Rc<dyn Fn(Vec2) -> Vec2>;
890 let c_set_vp = {
891 let this = Rc::clone(&this);
892 Rc::new(move |w: f32| this.set_viewport_width(w))
893 };
894 let c_set_ct = {
895 let this = Rc::clone(&this);
896 Rc::new(move |w: f32| this.set_content_width(w))
897 };
898 let c_get = {
899 let this = Rc::clone(&this);
900 Rc::new(move || this.get() + this.overscroll_offset())
901 };
902 let c_set = {
903 let this = Rc::clone(&this);
904 Rc::new(move |off: f32| this.set_offset(off))
905 };
906 let c_tick = {
907 let this = Rc::clone(&this);
908 Rc::new(move || {
909 this.tick();
910 })
911 };
912 let c_set_nested = {
913 let state = Rc::clone(&pc);
914 Rc::new(move |conn| {
915 *state.borrow_mut() = Some(conn);
916 })
917 };
918 ScrollBinding::Horizontal(ScrollAxisBinding {
919 on_scroll: Some(c_on_scroll),
920 set_viewport_main: Some(c_set_vp),
921 set_content_main: Some(c_set_ct),
922 get_offset_main: Some(c_get),
923 set_offset_main: Some(c_set),
924 show_scrollbar: this.show_scrollbar(),
925 tick: Some(c_tick),
926 set_nested_scroll_parent: Some(c_set_nested),
927 })
928 }
929}
930
931#[derive(Clone)]
933pub struct ScrollStateXY {
934 off_x: Signal<f32>,
935 off_y: Signal<f32>,
936 vp_w: Signal<f32>,
937 vp_h: Signal<f32>,
938 c_w: Signal<f32>,
939 c_h: Signal<f32>,
940 physics_x: Rc<RefCell<ScrollPhysics>>,
941 physics_y: Rc<RefCell<ScrollPhysics>>,
942 os_x: Signal<f32>,
943 os_y: Signal<f32>,
944 overscroll_enabled: Cell<bool>,
945 pub(crate) parent_connection: Rc<RefCell<Option<NestedScrollConnection>>>,
946 show_scrollbar: Cell<bool>,
947 prev_tick: Cell<Instant>,
948}
949
950impl Default for ScrollStateXY {
951 fn default() -> Self {
952 Self::new()
953 }
954}
955
956impl ScrollStateXY {
957 pub fn new() -> Self {
958 Self {
959 off_x: signal(0.0),
960 off_y: signal(0.0),
961 vp_w: signal(0.0),
962 vp_h: signal(0.0),
963 c_w: signal(0.0),
964 c_h: signal(0.0),
965 physics_x: Rc::new(RefCell::new(ScrollPhysics::new())),
966 physics_y: Rc::new(RefCell::new(ScrollPhysics::new())),
967 os_x: signal(0.0),
968 os_y: signal(0.0),
969 overscroll_enabled: Cell::new(true),
970 parent_connection: Rc::new(RefCell::new(None)),
971 show_scrollbar: Cell::new(true),
972 prev_tick: Cell::new(Instant::now()),
973 }
974 }
975
976 pub fn set_overscroll_enabled(&self, enabled: bool) {
977 self.overscroll_enabled.set(enabled);
978 }
979
980 pub fn set_show_scrollbar(&self, show: bool) {
981 self.show_scrollbar.set(show);
982 }
983
984 pub fn overscroll_offset(&self) -> (f32, f32) {
985 (self.os_x.get(), self.os_y.get())
986 }
987
988 pub fn set_nested_scroll_parent(&self, conn: NestedScrollConnection) {
989 *self.parent_connection.borrow_mut() = Some(conn);
990 }
991
992 pub fn connection(&self) -> NestedScrollConnection {
996 let this = Rc::new(self.clone());
997 let pc = Rc::clone(&this.parent_connection);
998 NestedScrollConnection::new().on_post_scroll(
999 move |_consumed: Vec2, available: Vec2, _source: NestedScrollSource| -> Vec2 {
1000 if available.x.abs() < 0.001 && available.y.abs() < 0.001 {
1001 return Vec2::ZERO;
1002 }
1003 let after = this.scroll_nested(available);
1004 let after2 = run_post_scroll(&pc, after);
1005 Vec2 {
1006 x: available.x - after2.x,
1007 y: available.y - after2.y,
1008 }
1009 },
1010 )
1011 }
1012
1013 pub fn set_viewport(&self, w: f32, h: f32) {
1014 let w = w.max(0.0);
1015 let h = h.max(0.0);
1016 let changed = (self.vp_w.get() - w).abs() > 0.5 || (self.vp_h.get() - h).abs() > 0.5;
1017 if changed {
1018 self.vp_w.set(w);
1019 self.vp_h.set(h);
1020 self.clamp();
1021 }
1022 }
1023 pub fn set_content(&self, w: f32, h: f32) {
1024 let w = w.max(0.0);
1025 let h = h.max(0.0);
1026 let changed = (self.c_w.get() - w).abs() > 0.5 || (self.c_h.get() - h).abs() > 0.5;
1027 if changed {
1028 self.c_w.set(w);
1029 self.c_h.set(h);
1030 self.clamp();
1031 }
1032 }
1033 pub fn set_offset_xy(&self, x: f32, y: f32) {
1034 let max_x = (self.c_w.get() - self.vp_w.get()).max(0.0);
1035 let max_y = (self.c_h.get() - self.vp_h.get()).max(0.0);
1036 self.off_x.set(clamp_offset(x, max_x));
1037 self.off_y.set(clamp_offset(y, max_y));
1038 }
1039 fn clamp(&self) {
1040 let max_x = (self.c_w.get() - self.vp_w.get()).max(0.0);
1041 let max_y = (self.c_h.get() - self.vp_h.get()).max(0.0);
1042 self.off_x.update(|x| *x = clamp_offset(*x, max_x));
1043 self.off_y.update(|y| *y = clamp_offset(*y, max_y));
1044 }
1045 pub fn get(&self) -> (f32, f32) {
1046 (self.off_x.get(), self.off_y.get())
1047 }
1048
1049 fn os_scroll_axis(
1050 off: &Signal<f32>,
1051 os: &Signal<f32>,
1052 overscroll_enabled: bool,
1053 max_off: f32,
1054 before: f32,
1055 leftover: f32,
1056 physics: &RefCell<ScrollPhysics>,
1057 ) -> f32 {
1058 if !overscroll_enabled || leftover.abs() < 0.001 {
1059 return leftover;
1060 }
1061 let os_val = os.get();
1062 if os_val.abs() > 0.5 && os_val.signum() * leftover < 0.0 {
1064 let reduction = leftover.abs().min(os_val.abs());
1065 os.set(os_val - os_val.signum() * reduction);
1066 let remainder = leftover - leftover.signum() * reduction;
1067 if remainder.abs() > 0.5 {
1068 let new_off = clamp_offset(before + remainder, max_off);
1069 off.set(new_off);
1070 let consumed = new_off - before;
1071 physics.borrow_mut().record_nested_input(consumed);
1072 return remainder - consumed;
1073 }
1074 return 0.0;
1075 }
1076
1077 let at_edge = (before <= 0.0 && leftover < 0.0) || (before >= max_off && leftover > 0.0);
1079 if max_off > 5.0 && at_edge && leftover.abs() > 0.5 {
1080 os.set(os_val + rubber_band(leftover, SCROLL_OVERSROLL_BAND_MAX));
1081 physics.borrow_mut().record_nested_input(0.0);
1082 return 0.0;
1083 }
1084 leftover
1085 }
1086 pub fn scroll_immediate(&self, d: Vec2) -> Vec2 {
1089 self.apply_delta(d, true)
1090 }
1091
1092 pub fn scroll_nested(&self, d: Vec2) -> Vec2 {
1095 self.apply_delta(d, false)
1096 }
1097
1098 fn apply_delta(&self, d: Vec2, user: bool) -> Vec2 {
1099 let d = Vec2 {
1100 x: if d.x.is_finite() { d.x } else { 0.0 },
1101 y: if d.y.is_finite() { d.y } else { 0.0 },
1102 };
1103 let max_x = (self.c_w.get() - self.vp_w.get()).max(0.0);
1104 let max_y = (self.c_h.get() - self.vp_h.get()).max(0.0);
1105 let (bx, by) = (self.off_x.get(), self.off_y.get());
1106
1107 let nx = clamp_offset(bx + d.x, max_x);
1108 let ny = clamp_offset(by + d.y, max_y);
1109 self.off_x.set(nx);
1110 self.off_y.set(ny);
1111 let (cx, cy) = (nx - bx, ny - by);
1112
1113 let mut px = self.physics_x.borrow_mut();
1114 let mut py = self.physics_y.borrow_mut();
1115 if user {
1116 px.record_user_input(cx);
1117 py.record_user_input(cy);
1118 } else {
1119 px.record_nested_input(cx);
1120 py.record_nested_input(cy);
1121 }
1122 drop((px, py));
1123
1124 Vec2 {
1125 x: d.x - cx,
1126 y: d.y - cy,
1127 }
1128 }
1129
1130 pub fn apply_overscroll(&self, d: Vec2) -> Vec2 {
1132 let max_x = (self.c_w.get() - self.vp_w.get()).max(0.0);
1133 let max_y = (self.c_h.get() - self.vp_h.get()).max(0.0);
1134 let (bx, by) = (self.off_x.get(), self.off_y.get());
1135
1136 let lx = Self::os_scroll_axis(
1137 &self.off_x,
1138 &self.os_x,
1139 self.overscroll_enabled.get(),
1140 max_x,
1141 bx,
1142 d.x,
1143 &self.physics_x,
1144 );
1145 let ly = Self::os_scroll_axis(
1146 &self.off_y,
1147 &self.os_y,
1148 self.overscroll_enabled.get(),
1149 max_y,
1150 by,
1151 d.y,
1152 &self.physics_y,
1153 );
1154 Vec2 { x: lx, y: ly }
1155 }
1156 pub fn tick(&self) -> bool {
1157 let now = Instant::now();
1158 let dt = (now - self.prev_tick.get()).as_secs_f32().min(0.1);
1159 self.prev_tick.set(now);
1160
1161 if self.overscroll_enabled.get()
1162 && (tick_os_axis(&self.os_x, true, dt) || tick_os_axis(&self.os_y, true, dt))
1163 {
1164 return true;
1165 }
1166
1167 let max_x = (self.c_w.get() - self.vp_w.get()).max(0.0);
1168 let max_y = (self.c_h.get() - self.vp_h.get()).max(0.0);
1169 let (bx, by) = (self.off_x.get(), self.off_y.get());
1170
1171 let mut px = self.physics_x.borrow_mut();
1172 let mut py = self.physics_y.borrow_mut();
1173 if !px.animating && !py.animating {
1174 return false;
1175 }
1176 if px.animating {
1177 if px.vel.abs() < px.stop_velocity {
1178 px.vel = 0.0;
1179 px.animating = false;
1180 } else {
1181 let nx = clamp_offset(bx + px.vel * dt, max_x);
1182 if (nx - bx).abs() < 0.01 && (bx <= 0.0 || bx >= max_x) {
1183 px.vel = 0.0;
1184 px.animating = false;
1185 } else {
1186 let speed = px.vel.abs();
1187 let t = (speed / 4000.0).min(1.0);
1188 let effective_decay = 0.85 + t * 0.10;
1189 px.vel *= effective_decay.powf(dt * 60.0);
1190 if px.vel.abs() < px.stop_velocity {
1191 px.vel = 0.0;
1192 px.animating = false;
1193 } else {
1194 self.off_x.set(nx);
1195 }
1196 }
1197 }
1198 }
1199 if py.animating {
1200 if py.vel.abs() < py.stop_velocity {
1201 py.vel = 0.0;
1202 py.animating = false;
1203 } else {
1204 let ny = clamp_offset(by + py.vel * dt, max_y);
1205 if (ny - by).abs() < 0.01 && (by <= 0.0 || by >= max_y) {
1206 py.vel = 0.0;
1207 py.animating = false;
1208 } else {
1209 let speed = py.vel.abs();
1210 let t = (speed / 4000.0).min(1.0);
1211 let effective_decay = 0.85 + t * 0.10;
1212 py.vel *= effective_decay.powf(dt * 60.0);
1213 if py.vel.abs() < py.stop_velocity {
1214 py.vel = 0.0;
1215 py.animating = false;
1216 } else {
1217 self.off_y.set(ny);
1218 }
1219 }
1220 }
1221 }
1222 let running = px.animating || py.animating;
1223 if running {
1224 request_frame();
1225 }
1226 running
1227 }
1228
1229 pub fn show_scrollbar(&self) -> bool {
1230 self.show_scrollbar.get()
1231 }
1232
1233 pub fn to_binding(&self) -> ScrollBinding {
1234 let this = Rc::new(self.clone());
1235 let pc = Rc::clone(&this.parent_connection);
1236 let c_on_scroll = {
1237 let state = Rc::clone(&pc);
1238 let this = Rc::clone(&this);
1239 Rc::new(move |d: Vec2| -> Vec2 {
1240 let d = run_pre_scroll(&state, d);
1241 let result = this.scroll_immediate(d);
1242 let after_parent = run_post_scroll(&state, result);
1243 this.apply_overscroll(after_parent)
1244 })
1245 } as Rc<dyn Fn(Vec2) -> Vec2>;
1246 let c_set_vw = {
1247 let this = Rc::clone(&this);
1248 Rc::new(move |w: f32| this.set_viewport(w, this.vp_h.get()))
1249 };
1250 let c_set_vh = {
1251 let this = Rc::clone(&this);
1252 Rc::new(move |h: f32| this.set_viewport(this.vp_w.get(), h))
1253 };
1254 let c_set_cw = {
1255 let this = Rc::clone(&this);
1256 Rc::new(move |w: f32| this.set_content(w, this.c_h.get()))
1257 };
1258 let c_set_ch = {
1259 let this = Rc::clone(&this);
1260 Rc::new(move |h: f32| this.set_content(this.c_w.get(), h))
1261 };
1262 let c_get_xy = {
1263 let this = Rc::clone(&this);
1264 Rc::new(move || {
1265 let (ox, oy) = this.get();
1266 let (osx, osy) = this.overscroll_offset();
1267 (ox + osx, oy + osy)
1268 })
1269 };
1270 let c_set_xy = {
1271 let this = Rc::clone(&this);
1272 Rc::new(move |x: f32, y: f32| this.set_offset_xy(x, y))
1273 };
1274 let c_tick = {
1275 let this = Rc::clone(&this);
1276 Rc::new(move || {
1277 this.tick();
1278 })
1279 };
1280 let c_set_nested = {
1281 let state = Rc::clone(&pc);
1282 Rc::new(move |conn| {
1283 *state.borrow_mut() = Some(conn);
1284 })
1285 };
1286 ScrollBinding::Both(ScrollBothBinding {
1287 on_scroll: Some(c_on_scroll),
1288 set_viewport_width: Some(c_set_vw),
1289 set_viewport_height: Some(c_set_vh),
1290 set_content_width: Some(c_set_cw),
1291 set_content_height: Some(c_set_ch),
1292 get_offset_xy: Some(c_get_xy),
1293 set_offset_xy: Some(c_set_xy),
1294 show_scrollbar: this.show_scrollbar(),
1295 tick: Some(c_tick),
1296 set_nested_scroll_parent: Some(c_set_nested),
1297 })
1298 }
1299}
1300
1301pub fn run_pre_scroll(conn: &RefCell<Option<NestedScrollConnection>>, d: Vec2) -> Vec2 {
1302 if let Some(ref parent) = *conn.borrow() {
1303 let consumed = parent.dispatch_pre_scroll(d, NestedScrollSource::UserInput);
1304 d - consumed
1305 } else {
1306 d
1307 }
1308}
1309
1310pub fn run_post_scroll(conn: &RefCell<Option<NestedScrollConnection>>, leftover: Vec2) -> Vec2 {
1311 if let Some(ref parent) = *conn.borrow() {
1312 let added =
1313 parent.dispatch_post_scroll(Vec2::ZERO, leftover, NestedScrollSource::UserInput);
1314 leftover - added
1315 } else {
1316 leftover
1317 }
1318}
1319
1320#[cfg(test)]
1321mod fling_tests {
1322 use super::*;
1323
1324 const FRAME: std::time::Duration = std::time::Duration::from_millis(16);
1325
1326 fn seeded() -> ScrollState {
1327 let st = ScrollState::new();
1328 st.set_viewport_height(100.0);
1329 st.set_content_height(4000.0);
1330 st
1331 }
1332
1333 fn frame() {
1337 std::thread::sleep(FRAME);
1338 }
1339
1340 #[test]
1344 fn drag_does_not_double_count() {
1345 let st = seeded();
1346 let (frames, step) = (10, 30.0);
1347 for _ in 0..frames {
1348 st.scroll_immediate(step);
1349 frame();
1350 st.tick();
1351 }
1352 let expected = step * frames as f32;
1353 assert!(
1354 (st.get() - expected).abs() < 1.0,
1355 "drag of {expected} over {frames} frames moved {}",
1356 st.get()
1357 );
1358 }
1359
1360 #[test]
1363 fn fling_continues_after_input_stops() {
1364 let st = seeded();
1365 st.scroll_immediate(200.0);
1366 frame();
1367 st.tick();
1368 let at_release = st.get();
1369 let mut coasted = 0.0;
1370 for _ in 0..10 {
1371 frame();
1372 if st.tick() {
1373 coasted += 1.0;
1374 }
1375 }
1376 assert!(coasted > 0.0, "fling stopped immediately after release");
1377 assert!(st.get() > at_release, "fling must travel further");
1378 }
1379
1380 #[test]
1385 fn release_is_continuous_with_the_drag() {
1386 for step in [8.0f32, 30.0, 70.0] {
1387 let mut st = seeded();
1388 for _ in 0..10 {
1389 st.scroll_immediate(step);
1390 frame();
1391 st.tick();
1392 }
1393 let released = st.get();
1394 frame();
1395 st.tick();
1396 let first = st.get() - released;
1397 assert!(
1398 (first - step).abs() < step * 0.25,
1399 "drag at {step}px/frame released into a {first}px step"
1400 );
1401 }
1402 }
1403
1404 #[test]
1407 fn drag_cancels_stale_fling_direction() {
1408 let st = seeded();
1409 for _ in 0..6 {
1410 st.scroll_immediate(200.0);
1411 frame();
1412 st.tick();
1413 }
1414 let before = st.get();
1415 for _ in 0..6 {
1416 st.scroll_immediate(-20.0);
1417 frame();
1418 st.tick();
1419 }
1420 assert!(
1421 st.get() < before,
1422 "reverse drag must win over the old fling ({} -> {})",
1423 before,
1424 st.get()
1425 );
1426 }
1427
1428 #[test]
1430 fn parent_flings_after_taking_child_leftover() {
1431 let st = seeded();
1432 let conn = st.connection();
1433 let consumed = conn.dispatch_post_scroll(
1434 Vec2::ZERO,
1435 Vec2 { x: 0.0, y: 200.0 },
1436 NestedScrollSource::UserInput,
1437 );
1438 assert!(
1440 (consumed.y - 200.0).abs() < 0.001,
1441 "consumed {}",
1442 consumed.y
1443 );
1444 assert!(st.get() > 0.0, "parent scrolled");
1445 assert!(st.physics.borrow().is_animating(), "parent has velocity");
1446 frame();
1447 st.tick();
1448 let before_coast = st.get();
1449 frame();
1450 st.tick();
1451 assert!(
1452 st.get() > before_coast,
1453 "parent must coast: nested velocity reached the driven physics"
1454 );
1455 }
1456}