1use std::cell::{Cell, RefCell};
2use std::rc::Rc;
3
4use web_time::Instant;
5
6use crate::nested_scroll::{NestedScrollConnection, NestedScrollSource};
7use crate::{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, Default)]
22pub struct ScrollAxisBinding {
23 pub on_scroll: Option<Rc<dyn Fn(Vec2) -> Vec2>>,
24 pub set_viewport_main: Option<Rc<dyn Fn(f32)>>,
26 pub set_content_main: Option<Rc<dyn Fn(f32)>>,
28 pub get_offset_main: Option<Rc<dyn Fn() -> f32>>,
29 pub set_offset_main: Option<Rc<dyn Fn(f32)>>,
30 pub show_scrollbar: bool,
31 pub tick: Option<Rc<dyn Fn()>>,
32 pub set_nested_scroll_parent: Option<Rc<dyn Fn(NestedScrollConnection)>>,
33}
34
35impl std::fmt::Debug for ScrollAxisBinding {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 f.debug_struct("ScrollAxisBinding")
38 .field("has_on_scroll", &self.on_scroll.is_some())
39 .field("set_viewport_main", &self.set_viewport_main.is_some())
40 .field("set_content_main", &self.set_content_main.is_some())
41 .field("get_offset_main", &self.get_offset_main.is_some())
42 .field("set_offset_main", &self.set_offset_main.is_some())
43 .field("show_scrollbar", &self.show_scrollbar)
44 .field("tick", &self.tick.is_some())
45 .field(
46 "set_nested_scroll_parent",
47 &self.set_nested_scroll_parent.is_some(),
48 )
49 .finish()
50 }
51}
52
53#[derive(Clone, Default)]
55pub struct ScrollBothBinding {
56 pub on_scroll: Option<Rc<dyn Fn(Vec2) -> Vec2>>,
57 pub set_viewport_width: Option<Rc<dyn Fn(f32)>>,
58 pub set_viewport_height: Option<Rc<dyn Fn(f32)>>,
59 pub set_content_width: Option<Rc<dyn Fn(f32)>>,
60 pub set_content_height: Option<Rc<dyn Fn(f32)>>,
61 pub get_offset_xy: Option<Rc<dyn Fn() -> (f32, f32)>>,
62 pub set_offset_xy: Option<Rc<dyn Fn(f32, f32)>>,
63 pub show_scrollbar: bool,
64 pub tick: Option<Rc<dyn Fn()>>,
65 pub set_nested_scroll_parent: Option<Rc<dyn Fn(NestedScrollConnection)>>,
66}
67
68impl std::fmt::Debug for ScrollBothBinding {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 f.debug_struct("ScrollBothBinding")
71 .field("has_on_scroll", &self.on_scroll.is_some())
72 .field("set_viewport_width", &self.set_viewport_width.is_some())
73 .field("set_viewport_height", &self.set_viewport_height.is_some())
74 .field("set_content_width", &self.set_content_width.is_some())
75 .field("set_content_height", &self.set_content_height.is_some())
76 .field("get_offset_xy", &self.get_offset_xy.is_some())
77 .field("set_offset_xy", &self.set_offset_xy.is_some())
78 .field("show_scrollbar", &self.show_scrollbar)
79 .field("tick", &self.tick.is_some())
80 .field(
81 "set_nested_scroll_parent",
82 &self.set_nested_scroll_parent.is_some(),
83 )
84 .finish()
85 }
86}
87
88#[derive(Clone, Debug)]
89pub enum ScrollBinding {
90 Vertical(ScrollAxisBinding),
91 Horizontal(ScrollAxisBinding),
92 Both(ScrollBothBinding),
93}
94
95impl ScrollBinding {
96 pub fn axis(&self) -> ScrollAxis {
97 match self {
98 ScrollBinding::Vertical(_) => ScrollAxis::Vertical,
99 ScrollBinding::Horizontal(_) => ScrollAxis::Horizontal,
100 ScrollBinding::Both(_) => ScrollAxis::Both,
101 }
102 }
103}
104
105const OVERSHOOT_DECAY_PER_60HZ: f32 = 0.78;
106
107fn clamp_offset(off: f32, max: f32) -> f32 {
112 let max = if max.is_finite() { max.max(0.0) } else { 0.0 };
113 if off.is_finite() {
114 off.clamp(0.0, max)
115 } else {
116 0.0
117 }
118}
119
120#[derive(Clone)]
123pub struct ScrollPhysics {
124 pub(crate) vel: f32,
125 last_t: Instant,
126 last_input_t: Instant,
127 pub(crate) animating: bool,
128 stop_velocity: f32,
129 input_activate_velocity: f32,
130}
131
132impl ScrollPhysics {
133 pub fn new(_decay_per_60hz: f32, stop_velocity: f32, input_activate_velocity: f32) -> Self {
134 let now = Instant::now();
135 Self {
136 vel: 0.0,
137 last_t: now,
138 last_input_t: now,
139 animating: false,
140 stop_velocity,
141 input_activate_velocity,
142 }
143 }
144
145 pub fn record_input(&mut self, consumed: f32) {
146 let now = Instant::now();
147 let raw_dt = (now - self.last_input_t).as_secs_f32();
148 let dt = raw_dt.clamp(1.0 / 240.0, 1.0 / 15.0);
149 self.last_input_t = now;
150 if raw_dt > 0.1 {
151 self.vel = 0.0;
152 }
153 let instant_vel = consumed / dt;
154 const SMOOTHING: f32 = 0.35;
155 self.vel = self.vel * (1.0 - SMOOTHING) + instant_vel * SMOOTHING;
156 const MAX_VEL: f32 = 8000.0;
157 self.vel = self.vel.clamp(-MAX_VEL, MAX_VEL);
158 self.animating = self.vel.abs() > self.input_activate_velocity;
159 }
160
161 fn dt(&mut self) -> f32 {
162 let now = Instant::now();
163 let dt = (now - self.last_t).as_secs_f32().min(0.1);
164 self.last_t = now;
165 dt
166 }
167
168 pub fn tick_integrate(&mut self, current: f32, min: f32, max: f32) -> Option<f32> {
169 if !self.animating {
170 return None;
171 }
172 let dt = self.dt();
173 if dt <= 0.0 {
174 return None;
175 }
176 let vel0 = self.vel;
177 if vel0.abs() < self.stop_velocity {
178 self.vel = 0.0;
179 self.animating = false;
180 return None;
181 }
182 let new = {
183 let raw = current + vel0 * dt;
184 let raw = if raw.is_finite() { raw } else { current };
185 raw.clamp(min, max.max(min))
186 };
187 if (new - current).abs() < 0.01 && (current <= min || current >= max) {
188 self.vel = 0.0;
189 self.animating = false;
190 return None;
191 }
192 let speed = self.vel.abs();
193 let t = (speed / 4000.0).min(1.0);
194 let effective_decay = 0.85 + t * 0.10;
195 let decay = effective_decay.powf(dt * 60.0);
196 self.vel = vel0 * decay;
197 if self.vel.abs() < self.stop_velocity {
198 self.vel = 0.0;
199 self.animating = false;
200 return None;
201 }
202 Some(new)
203 }
204
205 pub fn is_animating(&self) -> bool {
206 self.animating
207 }
208}
209
210#[derive(Clone)]
212pub struct ScrollState {
213 scroll_offset: Signal<f32>,
214 viewport_height: Signal<f32>,
215 content_height: Signal<f32>,
216 physics: RefCell<ScrollPhysics>,
217 overscroll: Signal<f32>,
218 overscroll_enabled: Cell<bool>,
219 pub(crate) parent_connection: Rc<RefCell<Option<NestedScrollConnection>>>,
220 show_scrollbar: Cell<bool>,
221 prev_tick: Cell<Instant>,
222}
223
224impl Default for ScrollState {
225 fn default() -> Self {
226 Self::new()
227 }
228}
229
230impl ScrollState {
231 pub fn new() -> Self {
232 Self {
233 scroll_offset: signal(0.0),
234 viewport_height: signal(0.0),
235 content_height: signal(0.0),
236 physics: RefCell::new(ScrollPhysics::new(0.90, 15.0, 50.0)),
237 overscroll: signal(0.0),
238 overscroll_enabled: Cell::new(true),
239 parent_connection: Rc::new(RefCell::new(None)),
240 show_scrollbar: Cell::new(true),
241 prev_tick: Cell::new(Instant::now()),
242 }
243 }
244
245 pub fn set_overscroll_enabled(&self, enabled: bool) {
246 self.overscroll_enabled.set(enabled);
247 }
248
249 pub fn overscroll_offset(&self) -> f32 {
250 self.overscroll.get()
251 }
252
253 pub fn set_nested_scroll_parent(&self, conn: NestedScrollConnection) {
254 *self.parent_connection.borrow_mut() = Some(conn);
255 }
256
257 pub fn connection(&self) -> NestedScrollConnection {
260 let this = Rc::new(self.clone());
261 let pc = Rc::clone(&this.parent_connection);
262 NestedScrollConnection::new().on_post_scroll(
263 move |_consumed: Vec2, available: Vec2, _source: NestedScrollSource| -> Vec2 {
264 if available.x.abs() < 0.001 && available.y.abs() < 0.001 {
265 return Vec2::ZERO;
266 }
267 let leftover_y = this.scroll_immediate(available.y);
268 let after = run_post_scroll(
269 &pc,
270 Vec2 {
271 x: available.x,
272 y: leftover_y,
273 },
274 );
275 Vec2 {
276 x: available.x - after.x,
277 y: available.y - after.y,
278 }
279 },
280 )
281 }
282
283 pub fn set_show_scrollbar(&self, show: bool) {
284 self.show_scrollbar.set(show);
285 }
286
287 pub fn set_viewport_height(&self, h: f32) {
288 let h = h.max(0.0);
289 if (self.viewport_height.get() - h).abs() > 0.5 {
290 self.viewport_height.set(h);
291 self.clamp_offset();
292 }
293 }
294
295 pub fn set_content_height(&self, h: f32) {
296 let h = h.max(0.0);
297 if (self.content_height.get() - h).abs() > 0.5 {
298 self.content_height.set(h);
299 self.clamp_offset();
300 }
301 }
302
303 pub fn set_overscroll(&self, val: f32) {
304 self.overscroll.set(val);
305 }
306
307 pub fn set_offset(&self, off: f32) {
308 let off = if off.is_finite() { off } else { 0.0 };
309 let vh = self.viewport_height.get();
310 let ch = self.content_height.get();
311 let max_off = (ch - vh).max(0.0);
312 self.scroll_offset.set(clamp_offset(off, max_off));
313 }
314
315 fn clamp_offset(&self) {
316 let vh = self.viewport_height.get();
317 let ch = self.content_height.get();
318 let max_off = (ch - vh).max(0.0);
319 self.scroll_offset.update(|o| {
320 if *o > max_off {
321 *o = max_off;
322 }
323 if *o < 0.0 {
324 *o = 0.0;
325 }
326 });
327 }
328
329 pub fn get(&self) -> f32 {
330 self.scroll_offset.get()
331 }
332
333 pub fn scroll_immediate(&self, dy: f32) -> f32 {
334 let dy = if dy.is_finite() { dy } else { 0.0 };
335 let before = self.scroll_offset.get();
336 let vh = self.viewport_height.get();
337 let ch = self.content_height.get();
338 let max_off = (ch - vh).max(0.0);
339
340 let new_off = clamp_offset(before + dy, max_off);
341 self.scroll_offset.set(new_off);
342 let consumed = new_off - before;
343 self.physics.borrow_mut().record_input(consumed);
344 dy - consumed
345 }
346
347 pub fn apply_overscroll(&self, leftover: f32) -> f32 {
351 if !self.overscroll_enabled.get() || leftover.abs() < 0.001 {
352 return leftover;
353 }
354 let os = self.overscroll.get();
355 let vh = self.viewport_height.get();
356 let ch = self.content_height.get();
357 let max_off = (ch - vh).max(0.0);
358 let before = self.scroll_offset.get();
359
360 if os.abs() > 0.5 && os.signum() * leftover < 0.0 {
362 let reduction = leftover.abs().min(os.abs());
363 self.overscroll.set(os - os.signum() * reduction);
364 let remainder = leftover - leftover.signum() * reduction;
365 if remainder.abs() > 0.5 {
366 let new_off = clamp_offset(before + remainder, max_off);
367 self.scroll_offset.set(new_off);
368 let consumed = new_off - before;
369 self.physics.borrow_mut().record_input(consumed);
370 return remainder - consumed;
371 }
372 return 0.0;
373 }
374
375 let at_edge = (before <= 0.0 && leftover < 0.0) || (before >= max_off && leftover > 0.0);
377 if max_off > 5.0 && at_edge && leftover.abs() > 0.5 {
378 let bandied = Self::rubber_band(leftover, 150.0);
379 self.overscroll.set(os + bandied);
380 self.physics.borrow_mut().record_input(0.0);
381 return 0.0;
382 }
383 leftover
384 }
385
386 fn rubber_band(amount: f32, max: f32) -> f32 {
387 let sign = amount.signum();
388 let abs_val = amount.abs();
389 if abs_val <= 0.0 {
390 return 0.0;
391 }
392 (1.0 - 1.0 / (1.0 + abs_val / max)) * max * sign
393 }
394
395 pub fn tick(&self) -> bool {
396 let now = Instant::now();
397 let dt = (now - self.prev_tick.get()).as_secs_f32().min(0.1);
398 self.prev_tick.set(now);
399
400 if self.overscroll_enabled.get() {
401 let os = self.overscroll.get();
402 if os.abs() > 0.5 {
403 let decayed = os * OVERSHOOT_DECAY_PER_60HZ.powf(dt * 60.0);
404 if decayed.abs() < 0.5 {
405 self.overscroll.set(0.0);
406 } else {
407 self.overscroll.set(decayed);
408 }
409 request_frame();
410 return true;
411 }
412 }
413
414 let vh = self.viewport_height.get();
415 let ch = self.content_height.get();
416 let max_off = (ch - vh).max(0.0);
417
418 let mut p = self.physics.borrow_mut();
419 if let Some(new_off) = p.tick_integrate(self.scroll_offset.get(), 0.0, max_off) {
420 drop(p);
421 self.scroll_offset.set(new_off);
422 request_frame();
423 true
424 } else {
425 false
426 }
427 }
428
429 pub fn show_scrollbar(&self) -> bool {
430 self.show_scrollbar.get()
431 }
432
433 pub fn to_binding(&self) -> ScrollBinding {
435 let this = Rc::new(self.clone());
436 let pc = Rc::clone(&this.parent_connection);
437 let c_on_scroll = {
438 let state = Rc::clone(&pc);
439 let this = Rc::clone(&this);
440 Rc::new(move |d: Vec2| -> Vec2 {
441 let d = run_pre_scroll(&state, d);
442 let leftover_y = this.scroll_immediate(d.y);
445 let result = Vec2 {
446 x: d.x,
447 y: leftover_y,
448 };
449 let after_parent = run_post_scroll(&state, result);
450 let final_y = this.apply_overscroll(after_parent.y);
451 Vec2 {
452 x: after_parent.x,
453 y: final_y,
454 }
455 })
456 } as Rc<dyn Fn(Vec2) -> Vec2>;
457 let c_set_viewport = {
458 let this = Rc::clone(&this);
459 Rc::new(move |h: f32| this.set_viewport_height(h))
460 };
461 let c_set_content = {
462 let this = Rc::clone(&this);
463 Rc::new(move |h: f32| this.set_content_height(h))
464 };
465 let c_get = {
466 let this = Rc::clone(&this);
467 Rc::new(move || this.get() + this.overscroll_offset())
468 };
469 let c_set = {
470 let this = Rc::clone(&this);
471 Rc::new(move |off: f32| this.set_offset(off))
472 };
473 let c_tick = {
474 let this = Rc::clone(&this);
475 Rc::new(move || {
476 this.tick();
477 })
478 };
479 let c_set_nested = {
480 let state = Rc::clone(&pc);
481 Rc::new(move |conn| {
482 *state.borrow_mut() = Some(conn);
483 })
484 };
485 ScrollBinding::Vertical(ScrollAxisBinding {
486 on_scroll: Some(c_on_scroll),
487 set_viewport_main: Some(c_set_viewport),
488 set_content_main: Some(c_set_content),
489 get_offset_main: Some(c_get),
490 set_offset_main: Some(c_set),
491 show_scrollbar: self.show_scrollbar(),
492 tick: Some(c_tick),
493 set_nested_scroll_parent: Some(c_set_nested),
494 })
495 }
496}
497
498#[derive(Clone)]
500pub struct HorizontalScrollState {
501 scroll_offset: Signal<f32>,
502 viewport_width: Signal<f32>,
503 content_width: Signal<f32>,
504 physics: RefCell<ScrollPhysics>,
505 overscroll: Signal<f32>,
506 overscroll_enabled: Cell<bool>,
507 pub(crate) parent_connection: Rc<RefCell<Option<NestedScrollConnection>>>,
508 show_scrollbar: Cell<bool>,
509 prev_tick: Cell<Instant>,
510}
511
512impl Default for HorizontalScrollState {
513 fn default() -> Self {
514 Self::new()
515 }
516}
517
518impl HorizontalScrollState {
519 pub fn new() -> Self {
520 Self {
521 scroll_offset: signal(0.0),
522 viewport_width: signal(0.0),
523 content_width: signal(0.0),
524 physics: RefCell::new(ScrollPhysics::new(0.90, 15.0, 50.0)),
525 overscroll: signal(0.0),
526 overscroll_enabled: Cell::new(true),
527 parent_connection: Rc::new(RefCell::new(None)),
528 show_scrollbar: Cell::new(true),
529 prev_tick: Cell::new(Instant::now()),
530 }
531 }
532
533 pub fn set_overscroll_enabled(&self, enabled: bool) {
534 self.overscroll_enabled.set(enabled);
535 }
536
537 pub fn overscroll_offset(&self) -> f32 {
538 self.overscroll.get()
539 }
540
541 pub fn set_nested_scroll_parent(&self, conn: NestedScrollConnection) {
542 *self.parent_connection.borrow_mut() = Some(conn);
543 }
544
545 pub fn connection(&self) -> NestedScrollConnection {
549 let this = Rc::new(self.clone());
550 let pc = Rc::clone(&this.parent_connection);
551 NestedScrollConnection::new().on_post_scroll(
552 move |_consumed: Vec2, available: Vec2, _source: NestedScrollSource| -> Vec2 {
553 if available.x.abs() < 0.001 && available.y.abs() < 0.001 {
554 return Vec2::ZERO;
555 }
556 let leftover_x = this.scroll_immediate(available.x);
557 let after = run_post_scroll(
558 &pc,
559 Vec2 {
560 x: leftover_x,
561 y: available.y,
562 },
563 );
564 Vec2 {
565 x: available.x - after.x,
566 y: available.y - after.y,
567 }
568 },
569 )
570 }
571
572 pub fn set_show_scrollbar(&self, show: bool) {
573 self.show_scrollbar.set(show);
574 }
575
576 pub fn set_viewport_width(&self, w: f32) {
577 let w = w.max(0.0);
578 if (self.viewport_width.get() - w).abs() > 0.5 {
579 self.viewport_width.set(w);
580 self.clamp();
581 }
582 }
583
584 pub fn set_content_width(&self, w: f32) {
585 let w = w.max(0.0);
586 if (self.content_width.get() - w).abs() > 0.5 {
587 self.content_width.set(w);
588 self.clamp();
589 }
590 }
591
592 pub fn set_overscroll(&self, val: f32) {
593 self.overscroll.set(val);
594 }
595
596 pub fn set_offset(&self, off: f32) {
597 let max_off = (self.content_width.get() - self.viewport_width.get()).max(0.0);
598 self.scroll_offset.set(clamp_offset(off, max_off));
599 }
600
601 fn clamp(&self) {
602 let max_off = (self.content_width.get() - self.viewport_width.get()).max(0.0);
603 self.scroll_offset.update(|o| {
604 *o = clamp_offset(*o, max_off);
605 });
606 }
607
608 pub fn get(&self) -> f32 {
609 self.scroll_offset.get()
610 }
611
612 pub fn scroll_immediate(&self, dx: f32) -> f32 {
613 let dx = if dx.is_finite() { dx } else { 0.0 };
614 let before = self.scroll_offset.get();
615 let max_off = (self.content_width.get() - self.viewport_width.get()).max(0.0);
616
617 let new_off = clamp_offset(before + dx, max_off);
618 self.scroll_offset.set(new_off);
619 let consumed = new_off - before;
620 self.physics.borrow_mut().record_input(consumed);
621 dx - consumed
622 }
623
624 pub fn apply_overscroll(&self, leftover: f32) -> f32 {
626 if !self.overscroll_enabled.get() || leftover.abs() < 0.001 {
627 return leftover;
628 }
629 let os = self.overscroll.get();
630 let max_off = (self.content_width.get() - self.viewport_width.get()).max(0.0);
631 let before = self.scroll_offset.get();
632
633 if os.abs() > 0.5 && os.signum() * leftover < 0.0 {
634 let reduction = leftover.abs().min(os.abs());
635 self.overscroll.set(os - os.signum() * reduction);
636 let remainder = leftover - leftover.signum() * reduction;
637 if remainder.abs() > 0.5 {
638 let new_off = clamp_offset(before + remainder, max_off);
639 self.scroll_offset.set(new_off);
640 let consumed = new_off - before;
641 self.physics.borrow_mut().record_input(consumed);
642 return remainder - consumed;
643 }
644 return 0.0;
645 }
646
647 let at_edge = (before <= 0.0 && leftover < 0.0) || (before >= max_off && leftover > 0.0);
648 if max_off > 5.0 && at_edge && leftover.abs() > 0.5 {
649 let bandied = ScrollState::rubber_band(leftover, 150.0);
650 self.overscroll.set(os + bandied);
651 self.physics.borrow_mut().record_input(0.0);
652 return 0.0;
653 }
654 leftover
655 }
656
657 pub fn tick(&self) -> bool {
658 let now = Instant::now();
659 let dt = (now - self.prev_tick.get()).as_secs_f32().min(0.1);
660 self.prev_tick.set(now);
661
662 if self.overscroll_enabled.get() {
663 let os = self.overscroll.get();
664 if os.abs() > 0.5 {
665 let decayed = os * OVERSHOOT_DECAY_PER_60HZ.powf(dt * 60.0);
666 if decayed.abs() < 0.5 {
667 self.overscroll.set(0.0);
668 } else {
669 self.overscroll.set(decayed);
670 }
671 request_frame();
672 return true;
673 }
674 }
675
676 let max_off = (self.content_width.get() - self.viewport_width.get()).max(0.0);
677 let mut p = self.physics.borrow_mut();
678 if let Some(new_off) = p.tick_integrate(self.scroll_offset.get(), 0.0, max_off) {
679 drop(p);
680 self.scroll_offset.set(new_off);
681 request_frame();
682 true
683 } else {
684 false
685 }
686 }
687
688 pub fn show_scrollbar(&self) -> bool {
689 self.show_scrollbar.get()
690 }
691
692 pub fn to_binding(&self) -> ScrollBinding {
693 let this = Rc::new(self.clone());
694 let pc = Rc::clone(&this.parent_connection);
695 let c_on_scroll = {
696 let state = Rc::clone(&pc);
697 let this = Rc::clone(&this);
698 Rc::new(move |d: Vec2| -> Vec2 {
699 let d = run_pre_scroll(&state, d);
700 let leftover_x = this.scroll_immediate(d.x);
701 let result = Vec2 {
702 x: leftover_x,
703 y: d.y,
704 };
705 let after_parent = run_post_scroll(&state, result);
706 let final_x = this.apply_overscroll(after_parent.x);
707 Vec2 {
708 x: final_x,
709 y: after_parent.y,
710 }
711 })
712 } as Rc<dyn Fn(Vec2) -> Vec2>;
713 let c_set_vp = {
714 let this = Rc::clone(&this);
715 Rc::new(move |w: f32| this.set_viewport_width(w))
716 };
717 let c_set_ct = {
718 let this = Rc::clone(&this);
719 Rc::new(move |w: f32| this.set_content_width(w))
720 };
721 let c_get = {
722 let this = Rc::clone(&this);
723 Rc::new(move || this.get() + this.overscroll_offset())
724 };
725 let c_set = {
726 let this = Rc::clone(&this);
727 Rc::new(move |off: f32| this.set_offset(off))
728 };
729 let c_tick = {
730 let this = Rc::clone(&this);
731 Rc::new(move || {
732 this.tick();
733 })
734 };
735 let c_set_nested = {
736 let state = Rc::clone(&pc);
737 Rc::new(move |conn| {
738 *state.borrow_mut() = Some(conn);
739 })
740 };
741 ScrollBinding::Horizontal(ScrollAxisBinding {
742 on_scroll: Some(c_on_scroll),
743 set_viewport_main: Some(c_set_vp),
744 set_content_main: Some(c_set_ct),
745 get_offset_main: Some(c_get),
746 set_offset_main: Some(c_set),
747 show_scrollbar: this.show_scrollbar(),
748 tick: Some(c_tick),
749 set_nested_scroll_parent: Some(c_set_nested),
750 })
751 }
752}
753
754#[derive(Clone)]
756pub struct ScrollStateXY {
757 off_x: Signal<f32>,
758 off_y: Signal<f32>,
759 vp_w: Signal<f32>,
760 vp_h: Signal<f32>,
761 c_w: Signal<f32>,
762 c_h: Signal<f32>,
763 physics_x: RefCell<ScrollPhysics>,
764 physics_y: RefCell<ScrollPhysics>,
765 os_x: Signal<f32>,
766 os_y: Signal<f32>,
767 overscroll_enabled: Cell<bool>,
768 pub(crate) parent_connection: Rc<RefCell<Option<NestedScrollConnection>>>,
769 show_scrollbar: Cell<bool>,
770 prev_tick: Cell<Instant>,
771}
772
773impl Default for ScrollStateXY {
774 fn default() -> Self {
775 Self::new()
776 }
777}
778
779impl ScrollStateXY {
780 pub fn new() -> Self {
781 Self {
782 off_x: signal(0.0),
783 off_y: signal(0.0),
784 vp_w: signal(0.0),
785 vp_h: signal(0.0),
786 c_w: signal(0.0),
787 c_h: signal(0.0),
788 physics_x: RefCell::new(ScrollPhysics::new(0.90, 15.0, 50.0)),
789 physics_y: RefCell::new(ScrollPhysics::new(0.90, 15.0, 50.0)),
790 os_x: signal(0.0),
791 os_y: signal(0.0),
792 overscroll_enabled: Cell::new(true),
793 parent_connection: Rc::new(RefCell::new(None)),
794 show_scrollbar: Cell::new(true),
795 prev_tick: Cell::new(Instant::now()),
796 }
797 }
798
799 pub fn set_overscroll_enabled(&self, enabled: bool) {
800 self.overscroll_enabled.set(enabled);
801 }
802
803 pub fn set_show_scrollbar(&self, show: bool) {
804 self.show_scrollbar.set(show);
805 }
806
807 pub fn overscroll_offset(&self) -> (f32, f32) {
808 (self.os_x.get(), self.os_y.get())
809 }
810
811 pub fn set_nested_scroll_parent(&self, conn: NestedScrollConnection) {
812 *self.parent_connection.borrow_mut() = Some(conn);
813 }
814
815 pub fn connection(&self) -> NestedScrollConnection {
819 let this = Rc::new(self.clone());
820 let pc = Rc::clone(&this.parent_connection);
821 NestedScrollConnection::new().on_post_scroll(
822 move |_consumed: Vec2, available: Vec2, _source: NestedScrollSource| -> Vec2 {
823 if available.x.abs() < 0.001 && available.y.abs() < 0.001 {
824 return Vec2::ZERO;
825 }
826 let after = this.scroll_immediate(available);
827 let after2 = run_post_scroll(&pc, after);
828 Vec2 {
829 x: available.x - after2.x,
830 y: available.y - after2.y,
831 }
832 },
833 )
834 }
835
836 pub fn set_viewport(&self, w: f32, h: f32) {
837 let w = w.max(0.0);
838 let h = h.max(0.0);
839 let changed = (self.vp_w.get() - w).abs() > 0.5 || (self.vp_h.get() - h).abs() > 0.5;
840 if changed {
841 self.vp_w.set(w);
842 self.vp_h.set(h);
843 self.clamp();
844 }
845 }
846 pub fn set_content(&self, w: f32, h: f32) {
847 let w = w.max(0.0);
848 let h = h.max(0.0);
849 let changed = (self.c_w.get() - w).abs() > 0.5 || (self.c_h.get() - h).abs() > 0.5;
850 if changed {
851 self.c_w.set(w);
852 self.c_h.set(h);
853 self.clamp();
854 }
855 }
856 pub fn set_offset_xy(&self, x: f32, y: f32) {
857 let max_x = (self.c_w.get() - self.vp_w.get()).max(0.0);
858 let max_y = (self.c_h.get() - self.vp_h.get()).max(0.0);
859 self.off_x.set(clamp_offset(x, max_x));
860 self.off_y.set(clamp_offset(y, max_y));
861 }
862 fn clamp(&self) {
863 let max_x = (self.c_w.get() - self.vp_w.get()).max(0.0);
864 let max_y = (self.c_h.get() - self.vp_h.get()).max(0.0);
865 self.off_x.update(|x| *x = clamp_offset(*x, max_x));
866 self.off_y.update(|y| *y = clamp_offset(*y, max_y));
867 }
868 pub fn get(&self) -> (f32, f32) {
869 (self.off_x.get(), self.off_y.get())
870 }
871
872 fn rubber_band(amount: f32, max: f32) -> f32 {
873 let sign = amount.signum();
874 let abs_val = amount.abs();
875 let result = if abs_val <= 0.0 {
876 0.0
877 } else {
878 (1.0 - 1.0 / (1.0 + abs_val / max)) * max
879 };
880 result * sign
881 }
882 fn os_scroll_axis(
883 off: &Signal<f32>,
884 os: &Signal<f32>,
885 overscroll_enabled: bool,
886 max_off: f32,
887 before: f32,
888 leftover: f32,
889 physics: &RefCell<ScrollPhysics>,
890 ) -> f32 {
891 if !overscroll_enabled || leftover.abs() < 0.001 {
892 return leftover;
893 }
894 let os_val = os.get();
895 if os_val.abs() > 0.5 && os_val.signum() * leftover < 0.0 {
897 let reduction = leftover.abs().min(os_val.abs());
898 os.set(os_val - os_val.signum() * reduction);
899 let remainder = leftover - leftover.signum() * reduction;
900 if remainder.abs() > 0.5 {
901 let new_off = clamp_offset(before + remainder, max_off);
902 off.set(new_off);
903 let consumed = new_off - before;
904 physics.borrow_mut().record_input(consumed);
905 return remainder - consumed;
906 }
907 return 0.0;
908 }
909
910 let at_edge = (before <= 0.0 && leftover < 0.0) || (before >= max_off && leftover > 0.0);
912 if max_off > 5.0 && at_edge && leftover.abs() > 0.5 {
913 let bandied = Self::rubber_band(leftover, 150.0);
914 os.set(os_val + bandied);
915 physics.borrow_mut().record_input(0.0);
916 return 0.0;
917 }
918 leftover
919 }
920 pub fn scroll_immediate(&self, d: Vec2) -> Vec2 {
921 let d = Vec2 {
922 x: if d.x.is_finite() { d.x } else { 0.0 },
923 y: if d.y.is_finite() { d.y } else { 0.0 },
924 };
925 let max_x = (self.c_w.get() - self.vp_w.get()).max(0.0);
926 let max_y = (self.c_h.get() - self.vp_h.get()).max(0.0);
927 let (bx, by) = (self.off_x.get(), self.off_y.get());
928
929 let nx = clamp_offset(bx + d.x, max_x);
930 let ny = clamp_offset(by + d.y, max_y);
931 self.off_x.set(nx);
932 self.off_y.set(ny);
933 let (cx, cy) = (nx - bx, ny - by);
934
935 let mut px = self.physics_x.borrow_mut();
936 let mut py = self.physics_y.borrow_mut();
937 px.record_input(cx);
938 py.record_input(cy);
939 drop((px, py));
940
941 Vec2 {
942 x: d.x - cx,
943 y: d.y - cy,
944 }
945 }
946
947 pub fn apply_overscroll(&self, d: Vec2) -> Vec2 {
949 let max_x = (self.c_w.get() - self.vp_w.get()).max(0.0);
950 let max_y = (self.c_h.get() - self.vp_h.get()).max(0.0);
951 let (bx, by) = (self.off_x.get(), self.off_y.get());
952
953 let lx = Self::os_scroll_axis(
954 &self.off_x,
955 &self.os_x,
956 self.overscroll_enabled.get(),
957 max_x,
958 bx,
959 d.x,
960 &self.physics_x,
961 );
962 let ly = Self::os_scroll_axis(
963 &self.off_y,
964 &self.os_y,
965 self.overscroll_enabled.get(),
966 max_y,
967 by,
968 d.y,
969 &self.physics_y,
970 );
971 Vec2 { x: lx, y: ly }
972 }
973 fn tick_os_axis(os: &Signal<f32>, enabled: bool, dt: f32) -> bool {
974 if !enabled {
975 return false;
976 }
977 let v = os.get();
978 if v.abs() > 0.5 {
979 let decayed = v * OVERSHOOT_DECAY_PER_60HZ.powf(dt * 60.0);
980 if decayed.abs() < 0.5 {
981 os.set(0.0);
982 } else {
983 os.set(decayed);
984 }
985 request_frame();
986 true
987 } else {
988 false
989 }
990 }
991 pub fn tick(&self) -> bool {
992 let now = Instant::now();
993 let dt = (now - self.prev_tick.get()).as_secs_f32().min(0.1);
994 self.prev_tick.set(now);
995
996 if self.overscroll_enabled.get()
997 && (Self::tick_os_axis(&self.os_x, true, dt)
998 || Self::tick_os_axis(&self.os_y, true, dt))
999 {
1000 return true;
1001 }
1002
1003 let max_x = (self.c_w.get() - self.vp_w.get()).max(0.0);
1004 let max_y = (self.c_h.get() - self.vp_h.get()).max(0.0);
1005 let (bx, by) = (self.off_x.get(), self.off_y.get());
1006
1007 let mut px = self.physics_x.borrow_mut();
1008 let mut py = self.physics_y.borrow_mut();
1009 if !px.animating && !py.animating {
1010 return false;
1011 }
1012 if px.animating {
1013 if px.vel.abs() < px.stop_velocity {
1014 px.vel = 0.0;
1015 px.animating = false;
1016 } else {
1017 let nx = clamp_offset(bx + px.vel * dt, max_x);
1018 if (nx - bx).abs() < 0.01 && (bx <= 0.0 || bx >= max_x) {
1019 px.vel = 0.0;
1020 px.animating = false;
1021 } else {
1022 let speed = px.vel.abs();
1023 let t = (speed / 4000.0).min(1.0);
1024 let effective_decay = 0.85 + t * 0.10;
1025 px.vel *= effective_decay.powf(dt * 60.0);
1026 if px.vel.abs() < px.stop_velocity {
1027 px.vel = 0.0;
1028 px.animating = false;
1029 } else {
1030 self.off_x.set(nx);
1031 }
1032 }
1033 }
1034 }
1035 if py.animating {
1036 if py.vel.abs() < py.stop_velocity {
1037 py.vel = 0.0;
1038 py.animating = false;
1039 } else {
1040 let ny = clamp_offset(by + py.vel * dt, max_y);
1041 if (ny - by).abs() < 0.01 && (by <= 0.0 || by >= max_y) {
1042 py.vel = 0.0;
1043 py.animating = false;
1044 } else {
1045 let speed = py.vel.abs();
1046 let t = (speed / 4000.0).min(1.0);
1047 let effective_decay = 0.85 + t * 0.10;
1048 py.vel *= effective_decay.powf(dt * 60.0);
1049 if py.vel.abs() < py.stop_velocity {
1050 py.vel = 0.0;
1051 py.animating = false;
1052 } else {
1053 self.off_y.set(ny);
1054 }
1055 }
1056 }
1057 }
1058 let running = px.animating || py.animating;
1059 if running {
1060 request_frame();
1061 }
1062 running
1063 }
1064
1065 pub fn show_scrollbar(&self) -> bool {
1066 self.show_scrollbar.get()
1067 }
1068
1069 pub fn to_binding(&self) -> ScrollBinding {
1070 let this = Rc::new(self.clone());
1071 let pc = Rc::clone(&this.parent_connection);
1072 let c_on_scroll = {
1073 let state = Rc::clone(&pc);
1074 let this = Rc::clone(&this);
1075 Rc::new(move |d: Vec2| -> Vec2 {
1076 let d = run_pre_scroll(&state, d);
1077 let result = this.scroll_immediate(d);
1078 let after_parent = run_post_scroll(&state, result);
1079 this.apply_overscroll(after_parent)
1080 })
1081 } as Rc<dyn Fn(Vec2) -> Vec2>;
1082 let c_set_vw = {
1083 let this = Rc::clone(&this);
1084 Rc::new(move |w: f32| this.set_viewport(w, this.vp_h.get()))
1085 };
1086 let c_set_vh = {
1087 let this = Rc::clone(&this);
1088 Rc::new(move |h: f32| this.set_viewport(this.vp_w.get(), h))
1089 };
1090 let c_set_cw = {
1091 let this = Rc::clone(&this);
1092 Rc::new(move |w: f32| this.set_content(w, this.c_h.get()))
1093 };
1094 let c_set_ch = {
1095 let this = Rc::clone(&this);
1096 Rc::new(move |h: f32| this.set_content(this.c_w.get(), h))
1097 };
1098 let c_get_xy = {
1099 let this = Rc::clone(&this);
1100 Rc::new(move || {
1101 let (ox, oy) = this.get();
1102 let (osx, osy) = this.overscroll_offset();
1103 (ox + osx, oy + osy)
1104 })
1105 };
1106 let c_set_xy = {
1107 let this = Rc::clone(&this);
1108 Rc::new(move |x: f32, y: f32| this.set_offset_xy(x, y))
1109 };
1110 let c_tick = {
1111 let this = Rc::clone(&this);
1112 Rc::new(move || {
1113 this.tick();
1114 })
1115 };
1116 let c_set_nested = {
1117 let state = Rc::clone(&pc);
1118 Rc::new(move |conn| {
1119 *state.borrow_mut() = Some(conn);
1120 })
1121 };
1122 ScrollBinding::Both(ScrollBothBinding {
1123 on_scroll: Some(c_on_scroll),
1124 set_viewport_width: Some(c_set_vw),
1125 set_viewport_height: Some(c_set_vh),
1126 set_content_width: Some(c_set_cw),
1127 set_content_height: Some(c_set_ch),
1128 get_offset_xy: Some(c_get_xy),
1129 set_offset_xy: Some(c_set_xy),
1130 show_scrollbar: this.show_scrollbar(),
1131 tick: Some(c_tick),
1132 set_nested_scroll_parent: Some(c_set_nested),
1133 })
1134 }
1135}
1136
1137pub fn run_pre_scroll(conn: &RefCell<Option<NestedScrollConnection>>, d: Vec2) -> Vec2 {
1138 if let Some(ref parent) = *conn.borrow() {
1139 let consumed = parent.dispatch_pre_scroll(d, NestedScrollSource::UserInput);
1140 d - consumed
1141 } else {
1142 d
1143 }
1144}
1145
1146pub fn run_post_scroll(conn: &RefCell<Option<NestedScrollConnection>>, leftover: Vec2) -> Vec2 {
1147 if let Some(ref parent) = *conn.borrow() {
1148 let added =
1149 parent.dispatch_post_scroll(Vec2::ZERO, leftover, NestedScrollSource::UserInput);
1150 leftover - added
1151 } else {
1152 leftover
1153 }
1154}