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