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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum ScrollAxis {
12 Vertical,
13 Horizontal,
14 Both,
15}
16
17#[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#[derive(Clone, Default)]
84pub struct ScrollAxisBinding {
85 pub on_scroll: Option<Rc<dyn Fn(Vec2) -> Vec2>>,
86 pub set_viewport_main: Option<Rc<dyn Fn(f32)>>,
88 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#[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
169fn 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#[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#[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 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 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 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 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 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 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#[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 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 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#[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 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 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 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 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}