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: 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    /// Use it to wire a child scrollable to a parent scroll container
241    /// explicitly: `child_state.set_nested_scroll_parent(parent_state.connection())`.
242    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    /// Feed leftover (after the nested parent chain has had its chance via
329    /// post-scroll) into the rubber-band overscroll. Returns the amount still
330    /// unconsumed. Overscroll deliberately runs LAST so parents get first dibs.
331    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        // Recovering from an active rubber band: absorb the reverse delta first.
342        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        // At the edge, pushing further: stretch the rubber band.
357        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    /// Build a `ScrollBinding::Vertical` from this state for use with `Modifier::vertical_scroll`.
415    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                // Clamp this scroller first; leftover goes to the nested parent
424                // chain (post-scroll) BEFORE rubber-band overscroll.
425                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/// X-only state
480#[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    /// A `NestedScrollConnection` that consumes leftover scroll by scrolling
527    /// THIS container, then chains anything still unabsorbed up to this
528    /// container's own parent connection.
529    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    /// Feed leftover (after the nested parent chain) into rubber-band overscroll.
605    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/// 2D state
735#[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    /// A `NestedScrollConnection` that consumes leftover scroll by scrolling
796    /// THIS container, then chains anything still unabsorbed up to this
797    /// container's own parent connection.
798    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        // Recovering from an active rubber band: absorb the reverse delta first.
875        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        // At the edge, pushing further: stretch the rubber band.
890        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    /// Feed leftover (after the nested parent chain) into rubber-band overscroll.
920    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}