Skip to main content

repose_core/
scroll.rs

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/// Axis (or axes) a scroll modifier operates on.
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum ScrollAxis {
12    Vertical,
13    Horizontal,
14    Both,
15}
16
17// Holds all callbacks that the layout engine needs from a scroll state.
18// Stored in `Modifier.scroll`.
19
20/// Callbacks for a single-axis (vertical or horizontal) scroll container.
21#[derive(Clone, Default)]
22pub struct ScrollAxisBinding {
23    pub on_scroll: Option<Rc<dyn Fn(Vec2) -> Vec2>>,
24    /// Set viewport size along the scroll axis (height for V, width for H).
25    pub set_viewport_main: Option<Rc<dyn Fn(f32)>>,
26    /// Set content size along the scroll axis.
27    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/// Callbacks for a 2D (both-axis) scroll container.
54#[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/// Handles velocity estimation from input deltas, frame-rate-independent
108/// exponential decay, edge snapping, and animation state tracking.
109#[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/// Inertial scroll state (single axis Y).
194#[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: 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: 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(&mut self, enabled: bool) {
229        self.overscroll_enabled = 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 set_show_scrollbar(&self, show: bool) {
241        self.show_scrollbar.set(show);
242    }
243
244    pub fn set_viewport_height(&self, h: f32) {
245        let h = h.max(0.0);
246        if (self.viewport_height.get() - h).abs() > 0.5 {
247            self.viewport_height.set(h);
248            self.clamp_offset();
249        }
250    }
251
252    pub fn set_content_height(&self, h: f32) {
253        let h = h.max(0.0);
254        if (self.content_height.get() - h).abs() > 0.5 {
255            self.content_height.set(h);
256            self.clamp_offset();
257        }
258    }
259
260    pub fn set_overscroll(&self, val: f32) {
261        self.overscroll.set(val);
262    }
263
264    pub fn set_offset(&self, off: f32) {
265        let vh = self.viewport_height.get();
266        let ch = self.content_height.get();
267        let max_off = (ch - vh).max(0.0);
268        self.scroll_offset.set(off.clamp(0.0, max_off));
269    }
270
271    fn clamp_offset(&self) {
272        let vh = self.viewport_height.get();
273        let ch = self.content_height.get();
274        let max_off = (ch - vh).max(0.0);
275        self.scroll_offset.update(|o| {
276            if *o > max_off {
277                *o = max_off;
278            }
279            if *o < 0.0 {
280                *o = 0.0;
281            }
282        });
283    }
284
285    pub fn get(&self) -> f32 {
286        self.scroll_offset.get()
287    }
288
289    pub fn scroll_immediate(&self, dy: f32) -> f32 {
290        let before = self.scroll_offset.get();
291        let vh = self.viewport_height.get();
292        let ch = self.content_height.get();
293        let max_off = (ch - vh).max(0.0);
294
295        let os = self.overscroll.get();
296        if self.overscroll_enabled && os.abs() > 0.5 {
297            if os.signum() * dy < 0.0 {
298                let reduction = dy.abs().min(os.abs());
299                self.overscroll.set(os - os.signum() * reduction);
300                let remainder = dy - dy.signum() * reduction;
301                if remainder.abs() > 0.5 {
302                    let new_off = (before + remainder).clamp(0.0, max_off);
303                    self.scroll_offset.set(new_off);
304                    let consumed = new_off - before;
305                    let leftover = remainder - consumed;
306                    self.physics.borrow_mut().record_input(consumed);
307                    return leftover;
308                }
309                return 0.0;
310            } else {
311                let total = os + dy;
312                let bandied = Self::rubber_band(total, 150.0);
313                self.overscroll.set(bandied);
314                self.physics.borrow_mut().record_input(dy);
315                return 0.0;
316            }
317        }
318
319        let can_overscroll = self.overscroll_enabled && max_off > 5.0;
320        let new_off = (before + dy).clamp(0.0, max_off);
321        self.scroll_offset.set(new_off);
322
323        let consumed = new_off - before;
324        let leftover = dy - consumed;
325
326        if can_overscroll
327            && leftover.abs() > 0.5
328            && ((before <= 0.0 && dy < 0.0) || (before >= max_off && dy > 0.0))
329        {
330            let bandied = Self::rubber_band(leftover, 150.0);
331            self.overscroll.set(os + bandied);
332            self.physics.borrow_mut().record_input(consumed);
333            return 0.0;
334        }
335
336        self.physics.borrow_mut().record_input(consumed);
337        leftover
338    }
339
340    fn rubber_band(amount: f32, max: f32) -> f32 {
341        let sign = amount.signum();
342        let abs_val = amount.abs();
343        if abs_val <= 0.0 {
344            return 0.0;
345        }
346        (1.0 - 1.0 / (1.0 + abs_val / max)) * max * sign
347    }
348
349    pub fn tick(&self) -> bool {
350        let now = Instant::now();
351        let dt = (now - self.prev_tick.get()).as_secs_f32().min(0.1);
352        self.prev_tick.set(now);
353
354        if self.overscroll_enabled {
355            let os = self.overscroll.get();
356            if os.abs() > 0.5 {
357                let decayed = os * OVERSHOOT_DECAY_PER_60HZ.powf(dt * 60.0);
358                if decayed.abs() < 0.5 {
359                    self.overscroll.set(0.0);
360                } else {
361                    self.overscroll.set(decayed);
362                }
363                request_frame();
364                return true;
365            }
366        }
367
368        let vh = self.viewport_height.get();
369        let ch = self.content_height.get();
370        let max_off = (ch - vh).max(0.0);
371
372        let mut p = self.physics.borrow_mut();
373        if let Some(new_off) = p.tick_integrate(self.scroll_offset.get(), 0.0, max_off) {
374            drop(p);
375            self.scroll_offset.set(new_off);
376            request_frame();
377            true
378        } else {
379            false
380        }
381    }
382
383    pub fn show_scrollbar(&self) -> bool {
384        self.show_scrollbar.get()
385    }
386
387    /// Build a `ScrollBinding::Vertical` from this state for use with `Modifier::vertical_scroll`.
388    pub fn to_binding(&self) -> ScrollBinding {
389        let this = Rc::new(self.clone());
390        let pc = Rc::clone(&this.parent_connection);
391        let c_on_scroll = {
392            let state = Rc::clone(&pc);
393            let this = Rc::clone(&this);
394            Rc::new(move |d: Vec2| -> Vec2 {
395                let d = run_pre_scroll(&*state, d);
396                let leftover_y = this.scroll_immediate(d.y);
397                let result = Vec2 {
398                    x: d.x,
399                    y: leftover_y,
400                };
401                run_post_scroll(&*state, result)
402            })
403        } as Rc<dyn Fn(Vec2) -> Vec2>;
404        let c_set_viewport = {
405            let this = Rc::clone(&this);
406            Rc::new(move |h: f32| this.set_viewport_height(h))
407        };
408        let c_set_content = {
409            let this = Rc::clone(&this);
410            Rc::new(move |h: f32| this.set_content_height(h))
411        };
412        let c_get = {
413            let this = Rc::clone(&this);
414            Rc::new(move || this.get() + this.overscroll_offset())
415        };
416        let c_set = {
417            let this = Rc::clone(&this);
418            Rc::new(move |off: f32| this.set_offset(off))
419        };
420        let c_tick = {
421            let this = Rc::clone(&this);
422            Rc::new(move || {
423                this.tick();
424            })
425        };
426        let c_set_nested = {
427            let state = Rc::clone(&pc);
428            Rc::new(move |conn| {
429                *state.borrow_mut() = Some(conn);
430            })
431        };
432        ScrollBinding::Vertical(ScrollAxisBinding {
433            on_scroll: Some(c_on_scroll),
434            set_viewport_main: Some(c_set_viewport),
435            set_content_main: Some(c_set_content),
436            get_offset_main: Some(c_get),
437            set_offset_main: Some(c_set),
438            show_scrollbar: self.show_scrollbar(),
439            tick: Some(c_tick),
440            set_nested_scroll_parent: Some(c_set_nested),
441        })
442    }
443}
444
445/// X-only state
446#[derive(Clone)]
447pub struct HorizontalScrollState {
448    scroll_offset: Signal<f32>,
449    viewport_width: Signal<f32>,
450    content_width: Signal<f32>,
451    physics: RefCell<ScrollPhysics>,
452    overscroll: Signal<f32>,
453    overscroll_enabled: bool,
454    pub(crate) parent_connection: Rc<RefCell<Option<NestedScrollConnection>>>,
455    show_scrollbar: Cell<bool>,
456    prev_tick: Cell<Instant>,
457}
458
459impl Default for HorizontalScrollState {
460    fn default() -> Self {
461        Self::new()
462    }
463}
464
465impl HorizontalScrollState {
466    pub fn new() -> Self {
467        Self {
468            scroll_offset: signal(0.0),
469            viewport_width: signal(0.0),
470            content_width: signal(0.0),
471            physics: RefCell::new(ScrollPhysics::new(0.90, 15.0, 50.0)),
472            overscroll: signal(0.0),
473            overscroll_enabled: true,
474            parent_connection: Rc::new(RefCell::new(None)),
475            show_scrollbar: Cell::new(true),
476            prev_tick: Cell::new(Instant::now()),
477        }
478    }
479
480    pub fn set_overscroll_enabled(&mut self, enabled: bool) {
481        self.overscroll_enabled = enabled;
482    }
483
484    pub fn overscroll_offset(&self) -> f32 {
485        self.overscroll.get()
486    }
487
488    pub fn set_nested_scroll_parent(&self, conn: NestedScrollConnection) {
489        *self.parent_connection.borrow_mut() = Some(conn);
490    }
491
492    pub fn set_show_scrollbar(&self, show: bool) {
493        self.show_scrollbar.set(show);
494    }
495
496    pub fn set_viewport_width(&self, w: f32) {
497        let w = w.max(0.0);
498        if (self.viewport_width.get() - w).abs() > 0.5 {
499            self.viewport_width.set(w);
500            self.clamp();
501        }
502    }
503
504    pub fn set_content_width(&self, w: f32) {
505        let w = w.max(0.0);
506        if (self.content_width.get() - w).abs() > 0.5 {
507            self.content_width.set(w);
508            self.clamp();
509        }
510    }
511
512    pub fn set_overscroll(&self, val: f32) {
513        self.overscroll.set(val);
514    }
515
516    pub fn set_offset(&self, off: f32) {
517        let max_off = (self.content_width.get() - self.viewport_width.get()).max(0.0);
518        self.scroll_offset.set(off.clamp(0.0, max_off));
519    }
520
521    fn clamp(&self) {
522        let max_off = (self.content_width.get() - self.viewport_width.get()).max(0.0);
523        self.scroll_offset.update(|o| {
524            *o = o.clamp(0.0, max_off);
525        });
526    }
527
528    pub fn get(&self) -> f32 {
529        self.scroll_offset.get()
530    }
531
532    pub fn scroll_immediate(&self, dx: f32) -> f32 {
533        let before = self.scroll_offset.get();
534        let max_off = (self.content_width.get() - self.viewport_width.get()).max(0.0);
535
536        let os = self.overscroll.get();
537        if self.overscroll_enabled && os.abs() > 0.5 {
538            if os.signum() * dx < 0.0 {
539                let reduction = dx.abs().min(os.abs());
540                self.overscroll.set(os - os.signum() * reduction);
541                let remainder = dx - dx.signum() * reduction;
542                if remainder.abs() > 0.5 {
543                    let new_off = (before + remainder).clamp(0.0, max_off);
544                    self.scroll_offset.set(new_off);
545                    let consumed = new_off - before;
546                    let leftover = remainder - consumed;
547                    self.physics.borrow_mut().record_input(consumed);
548                    return leftover;
549                }
550                return 0.0;
551            } else {
552                let total = os + dx;
553                let bandied = ScrollState::rubber_band(total, 150.0);
554                self.overscroll.set(bandied);
555                self.physics.borrow_mut().record_input(dx);
556                return 0.0;
557            }
558        }
559
560        let can_overscroll = self.overscroll_enabled && max_off > 5.0;
561        let new_off = (before + dx).clamp(0.0, max_off);
562        self.scroll_offset.set(new_off);
563
564        let consumed = new_off - before;
565        let leftover = dx - consumed;
566
567        if can_overscroll
568            && leftover.abs() > 0.5
569            && ((before <= 0.0 && dx < 0.0) || (before >= max_off && dx > 0.0))
570        {
571            let bandied = ScrollState::rubber_band(leftover, 150.0);
572            self.overscroll.set(os + bandied);
573            self.physics.borrow_mut().record_input(consumed);
574            return 0.0;
575        }
576
577        self.physics.borrow_mut().record_input(consumed);
578        leftover
579    }
580
581    pub fn tick(&self) -> bool {
582        let now = Instant::now();
583        let dt = (now - self.prev_tick.get()).as_secs_f32().min(0.1);
584        self.prev_tick.set(now);
585
586        if self.overscroll_enabled {
587            let os = self.overscroll.get();
588            if os.abs() > 0.5 {
589                let decayed = os * OVERSHOOT_DECAY_PER_60HZ.powf(dt * 60.0);
590                if decayed.abs() < 0.5 {
591                    self.overscroll.set(0.0);
592                } else {
593                    self.overscroll.set(decayed);
594                }
595                request_frame();
596                return true;
597            }
598        }
599
600        let max_off = (self.content_width.get() - self.viewport_width.get()).max(0.0);
601        let mut p = self.physics.borrow_mut();
602        if let Some(new_off) = p.tick_integrate(self.scroll_offset.get(), 0.0, max_off) {
603            drop(p);
604            self.scroll_offset.set(new_off);
605            request_frame();
606            true
607        } else {
608            false
609        }
610    }
611
612    pub fn show_scrollbar(&self) -> bool {
613        self.show_scrollbar.get()
614    }
615
616    pub fn to_binding(&self) -> ScrollBinding {
617        let this = Rc::new(self.clone());
618        let pc = Rc::clone(&this.parent_connection);
619        let c_on_scroll = {
620            let state = Rc::clone(&pc);
621            let this = Rc::clone(&this);
622            Rc::new(move |d: Vec2| -> Vec2 {
623                let d = run_pre_scroll(&*state, d);
624                let leftover_x = this.scroll_immediate(d.x);
625                let result = Vec2 {
626                    x: leftover_x,
627                    y: d.y,
628                };
629                run_post_scroll(&*state, result)
630            })
631        } as Rc<dyn Fn(Vec2) -> Vec2>;
632        let c_set_vp = {
633            let this = Rc::clone(&this);
634            Rc::new(move |w: f32| this.set_viewport_width(w))
635        };
636        let c_set_ct = {
637            let this = Rc::clone(&this);
638            Rc::new(move |w: f32| this.set_content_width(w))
639        };
640        let c_get = {
641            let this = Rc::clone(&this);
642            Rc::new(move || this.get() + this.overscroll_offset())
643        };
644        let c_set = {
645            let this = Rc::clone(&this);
646            Rc::new(move |off: f32| this.set_offset(off))
647        };
648        let c_tick = {
649            let this = Rc::clone(&this);
650            Rc::new(move || {
651                this.tick();
652            })
653        };
654        let c_set_nested = {
655            let state = Rc::clone(&pc);
656            Rc::new(move |conn| {
657                *state.borrow_mut() = Some(conn);
658            })
659        };
660        ScrollBinding::Horizontal(ScrollAxisBinding {
661            on_scroll: Some(c_on_scroll),
662            set_viewport_main: Some(c_set_vp),
663            set_content_main: Some(c_set_ct),
664            get_offset_main: Some(c_get),
665            set_offset_main: Some(c_set),
666            show_scrollbar: this.show_scrollbar(),
667            tick: Some(c_tick),
668            set_nested_scroll_parent: Some(c_set_nested),
669        })
670    }
671}
672
673/// 2D state
674#[derive(Clone)]
675pub struct ScrollStateXY {
676    off_x: Signal<f32>,
677    off_y: Signal<f32>,
678    vp_w: Signal<f32>,
679    vp_h: Signal<f32>,
680    c_w: Signal<f32>,
681    c_h: Signal<f32>,
682    physics_x: RefCell<ScrollPhysics>,
683    physics_y: RefCell<ScrollPhysics>,
684    os_x: Signal<f32>,
685    os_y: Signal<f32>,
686    overscroll_enabled: bool,
687    pub(crate) parent_connection: Rc<RefCell<Option<NestedScrollConnection>>>,
688    show_scrollbar: Cell<bool>,
689    prev_tick: Cell<Instant>,
690}
691
692impl Default for ScrollStateXY {
693    fn default() -> Self {
694        Self::new()
695    }
696}
697
698impl ScrollStateXY {
699    pub fn new() -> Self {
700        Self {
701            off_x: signal(0.0),
702            off_y: signal(0.0),
703            vp_w: signal(0.0),
704            vp_h: signal(0.0),
705            c_w: signal(0.0),
706            c_h: signal(0.0),
707            physics_x: RefCell::new(ScrollPhysics::new(0.90, 15.0, 50.0)),
708            physics_y: RefCell::new(ScrollPhysics::new(0.90, 15.0, 50.0)),
709            os_x: signal(0.0),
710            os_y: signal(0.0),
711            overscroll_enabled: true,
712            parent_connection: Rc::new(RefCell::new(None)),
713            show_scrollbar: Cell::new(true),
714            prev_tick: Cell::new(Instant::now()),
715        }
716    }
717
718    pub fn set_overscroll_enabled(&mut self, enabled: bool) {
719        self.overscroll_enabled = enabled;
720    }
721
722    pub fn set_show_scrollbar(&self, show: bool) {
723        self.show_scrollbar.set(show);
724    }
725
726    pub fn overscroll_offset(&self) -> (f32, f32) {
727        (self.os_x.get(), self.os_y.get())
728    }
729
730    pub fn set_nested_scroll_parent(&self, conn: NestedScrollConnection) {
731        *self.parent_connection.borrow_mut() = Some(conn);
732    }
733
734    pub fn set_viewport(&self, w: f32, h: f32) {
735        let w = w.max(0.0);
736        let h = h.max(0.0);
737        let changed = (self.vp_w.get() - w).abs() > 0.5 || (self.vp_h.get() - h).abs() > 0.5;
738        if changed {
739            self.vp_w.set(w);
740            self.vp_h.set(h);
741            self.clamp();
742        }
743    }
744    pub fn set_content(&self, w: f32, h: f32) {
745        let w = w.max(0.0);
746        let h = h.max(0.0);
747        let changed = (self.c_w.get() - w).abs() > 0.5 || (self.c_h.get() - h).abs() > 0.5;
748        if changed {
749            self.c_w.set(w);
750            self.c_h.set(h);
751            self.clamp();
752        }
753    }
754    pub fn set_offset_xy(&self, x: f32, y: f32) {
755        let max_x = (self.c_w.get() - self.vp_w.get()).max(0.0);
756        let max_y = (self.c_h.get() - self.vp_h.get()).max(0.0);
757        self.off_x.set(x.clamp(0.0, max_x));
758        self.off_y.set(y.clamp(0.0, max_y));
759    }
760    fn clamp(&self) {
761        let max_x = (self.c_w.get() - self.vp_w.get()).max(0.0);
762        let max_y = (self.c_h.get() - self.vp_h.get()).max(0.0);
763        self.off_x.update(|x| *x = x.clamp(0.0, max_x));
764        self.off_y.update(|y| *y = y.clamp(0.0, max_y));
765    }
766    pub fn get(&self) -> (f32, f32) {
767        (self.off_x.get(), self.off_y.get())
768    }
769    fn rubber_band(amount: f32, max: f32) -> f32 {
770        let sign = amount.signum();
771        let abs_val = amount.abs();
772        let result = if abs_val <= 0.0 {
773            0.0
774        } else {
775            (1.0 - 1.0 / (1.0 + abs_val / max)) * max
776        };
777        result * sign
778    }
779    fn os_scroll_axis(
780        off: &Signal<f32>,
781        os: &Signal<f32>,
782        overscroll_enabled: bool,
783        max_off: f32,
784        dx: f32,
785        physics: &mut ScrollPhysics,
786    ) -> f32 {
787        let before = off.get();
788        let os_val = os.get();
789        if overscroll_enabled && os_val.abs() > 0.5 {
790            if os_val.signum() * dx < 0.0 {
791                let reduction = dx.abs().min(os_val.abs());
792                os.set(os_val - os_val.signum() * reduction);
793                let remainder = dx - dx.signum() * reduction;
794                if remainder.abs() > 0.5 {
795                    let new_off = (before + remainder).clamp(0.0, max_off);
796                    let consumed = new_off - before;
797                    off.set(new_off);
798                    let leftover = remainder - consumed;
799                    physics.record_input(consumed);
800                    return leftover;
801                }
802                return 0.0;
803            } else {
804                let total = os_val + dx;
805                let bandied = Self::rubber_band(total, 150.0);
806                os.set(bandied);
807                physics.record_input(dx);
808                return 0.0;
809            }
810        }
811
812        let can_os = overscroll_enabled && max_off > 5.0;
813        let new_off = (before + dx).clamp(0.0, max_off);
814        off.set(new_off);
815        let consumed = new_off - before;
816        let leftover = dx - consumed;
817
818        if can_os
819            && leftover.abs() > 0.5
820            && ((before <= 0.0 && dx < 0.0) || (before >= max_off && dx > 0.0))
821        {
822            let bandied = Self::rubber_band(leftover, 150.0);
823            os.set(os_val + bandied);
824            physics.record_input(consumed);
825            return 0.0;
826        }
827
828        physics.record_input(consumed);
829        leftover
830    }
831    pub fn scroll_immediate(&self, d: Vec2) -> Vec2 {
832        let max_x = (self.c_w.get() - self.vp_w.get()).max(0.0);
833        let max_y = (self.c_h.get() - self.vp_h.get()).max(0.0);
834
835        let mut px = self.physics_x.borrow_mut();
836        let mut py = self.physics_y.borrow_mut();
837        let lx = Self::os_scroll_axis(&self.off_x, &self.os_x, self.overscroll_enabled, max_x, d.x, &mut px);
838        let ly = Self::os_scroll_axis(&self.off_y, &self.os_y, self.overscroll_enabled, max_y, d.y, &mut py);
839        drop((px, py));
840
841        Vec2 { x: lx, y: ly }
842    }
843    fn tick_os_axis(os: &Signal<f32>, enabled: bool, dt: f32) -> bool {
844        if !enabled {
845            return false;
846        }
847        let v = os.get();
848        if v.abs() > 0.5 {
849            let decayed = v * OVERSHOOT_DECAY_PER_60HZ.powf(dt * 60.0);
850            if decayed.abs() < 0.5 {
851                os.set(0.0);
852            } else {
853                os.set(decayed);
854            }
855            request_frame();
856            true
857        } else {
858            false
859        }
860    }
861    pub fn tick(&self) -> bool {
862        let now = Instant::now();
863        let dt = (now - self.prev_tick.get()).as_secs_f32().min(0.1);
864        self.prev_tick.set(now);
865
866        if self.overscroll_enabled {
867            if Self::tick_os_axis(&self.os_x, true, dt) || Self::tick_os_axis(&self.os_y, true, dt)
868            {
869                return true;
870            }
871        }
872
873        let max_x = (self.c_w.get() - self.vp_w.get()).max(0.0);
874        let max_y = (self.c_h.get() - self.vp_h.get()).max(0.0);
875        let (bx, by) = (self.off_x.get(), self.off_y.get());
876
877        let mut px = self.physics_x.borrow_mut();
878        let mut py = self.physics_y.borrow_mut();
879        if !px.animating && !py.animating {
880            return false;
881        }
882        if px.animating {
883            if px.vel.abs() < px.stop_velocity {
884                px.vel = 0.0;
885                px.animating = false;
886            } else {
887                let nx = (bx + px.vel * dt).clamp(0.0, max_x);
888                if (nx - bx).abs() < 0.01 && (bx <= 0.0 || bx >= max_x) {
889                    px.vel = 0.0;
890                    px.animating = false;
891                } else {
892                    let speed = px.vel.abs();
893                    let t = (speed / 4000.0).min(1.0);
894                    let effective_decay = 0.85 + t * 0.10;
895                    px.vel *= effective_decay.powf(dt * 60.0);
896                    if px.vel.abs() < px.stop_velocity {
897                        px.vel = 0.0;
898                        px.animating = false;
899                    } else {
900                        self.off_x.set(nx);
901                    }
902                }
903            }
904        }
905        if py.animating {
906            if py.vel.abs() < py.stop_velocity {
907                py.vel = 0.0;
908                py.animating = false;
909            } else {
910                let ny = (by + py.vel * dt).clamp(0.0, max_y);
911                if (ny - by).abs() < 0.01 && (by <= 0.0 || by >= max_y) {
912                    py.vel = 0.0;
913                    py.animating = false;
914                } else {
915                    let speed = py.vel.abs();
916                    let t = (speed / 4000.0).min(1.0);
917                    let effective_decay = 0.85 + t * 0.10;
918                    py.vel *= effective_decay.powf(dt * 60.0);
919                    if py.vel.abs() < py.stop_velocity {
920                        py.vel = 0.0;
921                        py.animating = false;
922                    } else {
923                        self.off_y.set(ny);
924                    }
925                }
926            }
927        }
928        let running = px.animating || py.animating;
929        if running {
930            request_frame();
931        }
932        running
933    }
934
935    pub fn show_scrollbar(&self) -> bool {
936        self.show_scrollbar.get()
937    }
938
939    pub fn to_binding(&self) -> ScrollBinding {
940        let this = Rc::new(self.clone());
941        let pc = Rc::clone(&this.parent_connection);
942        let c_on_scroll = {
943            let state = Rc::clone(&pc);
944            let this = Rc::clone(&this);
945            Rc::new(move |d: Vec2| -> Vec2 {
946                let d = run_pre_scroll(&*state, d);
947                let result = this.scroll_immediate(d);
948                run_post_scroll(&*state, result)
949            })
950        } as Rc<dyn Fn(Vec2) -> Vec2>;
951        let c_set_vw = {
952            let this = Rc::clone(&this);
953            Rc::new(move |w: f32| this.set_viewport(w, this.vp_h.get()))
954        };
955        let c_set_vh = {
956            let this = Rc::clone(&this);
957            Rc::new(move |h: f32| this.set_viewport(this.vp_w.get(), h))
958        };
959        let c_set_cw = {
960            let this = Rc::clone(&this);
961            Rc::new(move |w: f32| this.set_content(w, this.c_h.get()))
962        };
963        let c_set_ch = {
964            let this = Rc::clone(&this);
965            Rc::new(move |h: f32| this.set_content(this.c_w.get(), h))
966        };
967        let c_get_xy = {
968            let this = Rc::clone(&this);
969            Rc::new(move || {
970                let (ox, oy) = this.get();
971                let (osx, osy) = this.overscroll_offset();
972                (ox + osx, oy + osy)
973            })
974        };
975        let c_set_xy = {
976            let this = Rc::clone(&this);
977            Rc::new(move |x: f32, y: f32| this.set_offset_xy(x, y))
978        };
979        let c_tick = {
980            let this = Rc::clone(&this);
981            Rc::new(move || {
982                this.tick();
983            })
984        };
985        let c_set_nested = {
986            let state = Rc::clone(&pc);
987            Rc::new(move |conn| {
988                *state.borrow_mut() = Some(conn);
989            })
990        };
991        ScrollBinding::Both(ScrollBothBinding {
992            on_scroll: Some(c_on_scroll),
993            set_viewport_width: Some(c_set_vw),
994            set_viewport_height: Some(c_set_vh),
995            set_content_width: Some(c_set_cw),
996            set_content_height: Some(c_set_ch),
997            get_offset_xy: Some(c_get_xy),
998            set_offset_xy: Some(c_set_xy),
999            show_scrollbar: this.show_scrollbar(),
1000            tick: Some(c_tick),
1001            set_nested_scroll_parent: Some(c_set_nested),
1002        })
1003    }
1004}
1005
1006pub fn run_pre_scroll(conn: &RefCell<Option<NestedScrollConnection>>, d: Vec2) -> Vec2 {
1007    if let Some(ref parent) = *conn.borrow() {
1008        let consumed = parent.dispatch_pre_scroll(d, NestedScrollSource::UserInput);
1009        d - consumed
1010    } else {
1011        d
1012    }
1013}
1014
1015pub fn run_post_scroll(
1016    conn: &RefCell<Option<NestedScrollConnection>>,
1017    leftover: Vec2,
1018) -> Vec2 {
1019    if let Some(ref parent) = *conn.borrow() {
1020        let added =
1021            parent.dispatch_post_scroll(Vec2::ZERO, leftover, NestedScrollSource::UserInput);
1022        leftover - added
1023    } else {
1024        leftover
1025    }
1026}