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