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