1use repose_core::*;
11use std::cell::RefCell;
12use std::rc::Rc;
13use web_time::Instant;
14
15pub struct ScrollState {
17 scroll_offset: Signal<f32>,
18 viewport_height: Signal<f32>,
19 content_height: Signal<f32>,
20
21 vel: RefCell<f32>,
23 last_t: RefCell<Instant>,
24 last_input_t: RefCell<Instant>,
25 animating: RefCell<bool>,
26}
27
28impl Default for ScrollState {
29 fn default() -> Self {
30 Self::new()
31 }
32}
33
34impl ScrollState {
35 pub fn new() -> Self {
36 let now = Instant::now();
37 Self {
38 scroll_offset: signal(0.0),
39 viewport_height: signal(0.0),
40 content_height: signal(0.0),
41 vel: RefCell::new(0.0),
42 last_t: RefCell::new(now),
43 last_input_t: RefCell::new(now),
44 animating: RefCell::new(false),
45 }
46 }
47
48 pub fn set_viewport_height(&self, h: f32) {
49 self.viewport_height.set(h.max(0.0));
50 self.clamp_offset();
51 }
52 pub fn set_content_height(&self, h: f32) {
53 self.content_height.set(h.max(0.0));
54 self.clamp_offset();
55 }
56 pub fn set_offset(&self, off: f32) {
57 let vh = self.viewport_height.get();
58 let ch = self.content_height.get();
59 let max_off = (ch - vh).max(0.0);
60 self.scroll_offset.set(off.clamp(0.0, max_off));
61 }
62
63 fn clamp_offset(&self) {
64 let vh = self.viewport_height.get();
65 let ch = self.content_height.get();
66 let max_off = (ch - vh).max(0.0);
67 self.scroll_offset.update(|o| {
68 if *o > max_off {
69 *o = max_off;
70 }
71 if *o < 0.0 {
72 *o = 0.0;
73 }
74 });
75 }
76
77 pub fn get(&self) -> f32 {
78 self.scroll_offset.get()
79 }
80
81 pub fn scroll_immediate(&self, dy: f32) -> f32 {
83 let before = self.scroll_offset.get();
84 let vh = self.viewport_height.get();
85 let ch = self.content_height.get();
86 let max_off = (ch - vh).max(0.0);
87
88 let new_off = (before + dy).clamp(0.0, max_off);
89 self.scroll_offset.set(new_off);
90
91 let consumed = new_off - before;
92 let leftover = dy - consumed;
93
94 let now = Instant::now();
96 let dt = (now - *self.last_input_t.borrow())
97 .as_secs_f32()
98 .clamp(1.0 / 240.0, 1.0 / 15.0);
99 *self.last_input_t.borrow_mut() = now;
100
101 *self.vel.borrow_mut() = consumed / dt;
102 *self.animating.borrow_mut() = self.vel.borrow().abs() > 10.0;
103
104 leftover
105 }
106
107 pub fn tick(&self) -> bool {
109 if !*self.animating.borrow() {
110 return false;
111 }
112
113 let now = Instant::now();
114 let dt = (now - *self.last_t.borrow()).as_secs_f32().min(0.1);
115 *self.last_t.borrow_mut() = now;
116 if dt <= 0.0 {
117 return false;
118 }
119
120 let vel0 = *self.vel.borrow();
121 if vel0.abs() < 5.0 {
122 *self.vel.borrow_mut() = 0.0;
123 *self.animating.borrow_mut() = false;
124 return false;
125 }
126
127 let before = self.scroll_offset.get();
128 let vh = self.viewport_height.get();
129 let ch = self.content_height.get();
130 let max_off = (ch - vh).max(0.0);
131
132 let new_off = (before + vel0 * dt).clamp(0.0, max_off);
134 self.scroll_offset.set(new_off);
135
136 if (new_off - before).abs() < 0.01 && (before <= 0.0 || before >= max_off) {
138 *self.vel.borrow_mut() = 0.0;
139 *self.animating.borrow_mut() = false;
140 return false;
141 }
142
143 let decay_per_60hz = 0.90f32;
145 let decay = decay_per_60hz.powf(dt * 60.0);
146 *self.vel.borrow_mut() = vel0 * decay;
147
148 request_frame();
149
150 true
151 }
152}
153
154pub struct HorizontalScrollState {
156 scroll_offset: Signal<f32>,
157 viewport_width: Signal<f32>,
158 content_width: Signal<f32>,
159 vel: RefCell<f32>, last_t: RefCell<Instant>,
161 last_input_t: RefCell<Instant>,
162 animating: RefCell<bool>,
163}
164impl Default for HorizontalScrollState {
165 fn default() -> Self {
166 Self::new()
167 }
168}
169
170impl HorizontalScrollState {
171 pub fn new() -> Self {
172 let now = Instant::now();
173 Self {
174 scroll_offset: signal(0.0),
175 viewport_width: signal(0.0),
176 content_width: signal(0.0),
177 vel: RefCell::new(0.0),
178 last_t: RefCell::new(now),
179 last_input_t: RefCell::new(now),
180 animating: RefCell::new(false),
181 }
182 }
183 pub fn set_viewport_width(&self, w: f32) {
184 self.viewport_width.set(w.max(0.0));
185 self.clamp();
186 }
187 pub fn set_content_width(&self, w: f32) {
188 self.content_width.set(w.max(0.0));
189 self.clamp();
190 }
191 pub fn set_offset(&self, off: f32) {
192 let max_off = (self.content_width.get() - self.viewport_width.get()).max(0.0);
193 self.scroll_offset.set(off.clamp(0.0, max_off));
194 }
195 fn clamp(&self) {
196 let max_off = (self.content_width.get() - self.viewport_width.get()).max(0.0);
197 self.scroll_offset.update(|o| {
198 *o = o.clamp(0.0, max_off);
199 });
200 }
201 pub fn get(&self) -> f32 {
202 self.scroll_offset.get()
203 }
204 pub fn scroll_immediate(&self, dx: f32) -> f32 {
205 let before = self.scroll_offset.get();
206 let max_off = (self.content_width.get() - self.viewport_width.get()).max(0.0);
207 let new_off = (before + dx).clamp(0.0, max_off);
208 self.scroll_offset.set(new_off);
209
210 let consumed = new_off - before;
211 let leftover = dx - consumed;
212
213 let now = Instant::now();
214 let dt = (now - *self.last_input_t.borrow())
215 .as_secs_f32()
216 .clamp(1.0 / 240.0, 1.0 / 15.0);
217 *self.last_input_t.borrow_mut() = now;
218
219 *self.vel.borrow_mut() = consumed / dt;
220 *self.animating.borrow_mut() = self.vel.borrow().abs() > 10.0;
221
222 leftover
223 }
224 pub fn tick(&self) -> bool {
225 if !*self.animating.borrow() {
226 return false;
227 }
228
229 let now = Instant::now();
230 let dt = (now - *self.last_t.borrow()).as_secs_f32().min(0.1);
231 *self.last_t.borrow_mut() = now;
232 if dt <= 0.0 {
233 return false;
234 }
235
236 let vel0 = *self.vel.borrow();
237 if vel0.abs() < 5.0 {
238 *self.animating.borrow_mut() = false;
239 *self.vel.borrow_mut() = 0.0;
240 return false;
241 }
242
243 let before = self.scroll_offset.get();
244 let max_off = (self.content_width.get() - self.viewport_width.get()).max(0.0);
245 let new_off = (before + vel0 * dt).clamp(0.0, max_off);
246 self.scroll_offset.set(new_off);
247
248 if (new_off - before).abs() < 0.01 && (before <= 0.0 || before >= max_off) {
249 *self.vel.borrow_mut() = 0.0;
250 *self.animating.borrow_mut() = false;
251 return false;
252 }
253
254 let decay_per_60hz = 0.90f32;
255 let decay = decay_per_60hz.powf(dt * 60.0);
256 *self.vel.borrow_mut() = vel0 * decay;
257
258 request_frame();
259
260 true
261 }
262}
263
264pub struct ScrollStateXY {
266 off_x: Signal<f32>,
267 off_y: Signal<f32>,
268 vp_w: Signal<f32>,
269 vp_h: Signal<f32>,
270 c_w: Signal<f32>,
271 c_h: Signal<f32>,
272 vel_x: RefCell<f32>, vel_y: RefCell<f32>, last_t: RefCell<Instant>,
275 last_input_t: RefCell<Instant>,
276 animating: RefCell<bool>,
277}
278impl Default for ScrollStateXY {
279 fn default() -> Self {
280 Self::new()
281 }
282}
283
284impl ScrollStateXY {
285 pub fn new() -> Self {
286 let now = Instant::now();
287 Self {
288 off_x: signal(0.0),
289 off_y: signal(0.0),
290 vp_w: signal(0.0),
291 vp_h: signal(0.0),
292 c_w: signal(0.0),
293 c_h: signal(0.0),
294 vel_x: RefCell::new(0.0),
295 vel_y: RefCell::new(0.0),
296 last_t: RefCell::new(now),
297 last_input_t: RefCell::new(now),
298 animating: RefCell::new(false),
299 }
300 }
301 pub fn set_viewport(&self, w: f32, h: f32) {
302 self.vp_w.set(w.max(0.0));
303 self.vp_h.set(h.max(0.0));
304 self.clamp();
305 }
306 pub fn set_content(&self, w: f32, h: f32) {
307 self.c_w.set(w.max(0.0));
308 self.c_h.set(h.max(0.0));
309 self.clamp();
310 }
311 pub fn set_offset_xy(&self, x: f32, y: f32) {
312 let max_x = (self.c_w.get() - self.vp_w.get()).max(0.0);
313 let max_y = (self.c_h.get() - self.vp_h.get()).max(0.0);
314 self.off_x.set(x.clamp(0.0, max_x));
315 self.off_y.set(y.clamp(0.0, max_y));
316 }
317 fn clamp(&self) {
318 let max_x = (self.c_w.get() - self.vp_w.get()).max(0.0);
319 let max_y = (self.c_h.get() - self.vp_h.get()).max(0.0);
320 self.off_x.update(|x| *x = x.clamp(0.0, max_x));
321 self.off_y.update(|y| *y = y.clamp(0.0, max_y));
322 }
323 pub fn get(&self) -> (f32, f32) {
324 (self.off_x.get(), self.off_y.get())
325 }
326 pub fn scroll_immediate(&self, d: Vec2) -> Vec2 {
327 let bx = self.off_x.get();
328 let by = self.off_y.get();
329
330 let max_x = (self.c_w.get() - self.vp_w.get()).max(0.0);
331 let max_y = (self.c_h.get() - self.vp_h.get()).max(0.0);
332
333 let nx = (bx + d.x).clamp(0.0, max_x);
334 let ny = (by + d.y).clamp(0.0, max_y);
335
336 self.off_x.set(nx);
337 self.off_y.set(ny);
338
339 let consumed_x = nx - bx;
340 let consumed_y = ny - by;
341
342 let now = Instant::now();
343 let dt = (now - *self.last_input_t.borrow())
344 .as_secs_f32()
345 .clamp(1.0 / 240.0, 1.0 / 15.0);
346 *self.last_input_t.borrow_mut() = now;
347
348 *self.vel_x.borrow_mut() = consumed_x / dt;
349 *self.vel_y.borrow_mut() = consumed_y / dt;
350 *self.animating.borrow_mut() =
351 self.vel_x.borrow().abs() > 10.0 || self.vel_y.borrow().abs() > 10.0;
352
353 Vec2 {
354 x: d.x - consumed_x,
355 y: d.y - consumed_y,
356 }
357 }
358 pub fn tick(&self) -> bool {
359 if !*self.animating.borrow() {
360 return false;
361 }
362
363 let now = Instant::now();
364 let dt = (now - *self.last_t.borrow()).as_secs_f32().min(0.1);
365 *self.last_t.borrow_mut() = now;
366 if dt <= 0.0 {
367 return false;
368 }
369
370 let vx0 = *self.vel_x.borrow();
371 let vy0 = *self.vel_y.borrow();
372 if vx0.abs() < 5.0 && vy0.abs() < 5.0 {
373 *self.animating.borrow_mut() = false;
374 *self.vel_x.borrow_mut() = 0.0;
375 *self.vel_y.borrow_mut() = 0.0;
376 return false;
377 }
378
379 let (bx, by) = (self.off_x.get(), self.off_y.get());
380 let max_x = (self.c_w.get() - self.vp_w.get()).max(0.0);
381 let max_y = (self.c_h.get() - self.vp_h.get()).max(0.0);
382
383 let nx = (bx + vx0 * dt).clamp(0.0, max_x);
384 let ny = (by + vy0 * dt).clamp(0.0, max_y);
385
386 self.off_x.set(nx);
387 self.off_y.set(ny);
388
389 if (nx - bx).abs() < 0.01 && (bx <= 0.0 || bx >= max_x) {
391 *self.vel_x.borrow_mut() = 0.0;
392 }
393 if (ny - by).abs() < 0.01 && (by <= 0.0 || by >= max_y) {
394 *self.vel_y.borrow_mut() = 0.0;
395 }
396
397 let decay_per_60hz = 0.95f32;
398 let decay = decay_per_60hz.powf(dt * 60.0);
399 *self.vel_x.borrow_mut() *= decay;
400 *self.vel_y.borrow_mut() *= decay;
401
402 *self.animating.borrow_mut() =
403 self.vel_x.borrow().abs() > 5.0 || self.vel_y.borrow().abs() > 5.0;
404
405 if *self.animating.borrow() {
406 request_frame();
407 return true;
408 }
409 false
410 }
411}
412
413pub fn remember_scroll_state(key: impl Into<String>) -> Rc<ScrollState> {
415 repose_core::remember_with_key(key.into(), ScrollState::new)
416}
417
418pub fn remember_horizontal_scroll_state(key: impl Into<String>) -> Rc<HorizontalScrollState> {
419 repose_core::remember_with_key(key.into(), HorizontalScrollState::new)
420}
421pub fn remember_scroll_state_xy(key: impl Into<String>) -> Rc<ScrollStateXY> {
422 repose_core::remember_with_key(key.into(), ScrollStateXY::new)
423}
424
425pub fn ScrollArea(modifier: Modifier, state: Rc<ScrollState>, content: View) -> View {
427 let st_clone = state.clone();
428 let on_scroll = {
429 Rc::new(move |d: Vec2| -> Vec2 {
430 Vec2 {
431 x: d.x,
432 y: st_clone.scroll_immediate(d.y),
433 }
434 })
435 };
436 let set_viewport = {
437 let st = state.clone();
438 Rc::new(move |h: f32| st.set_viewport_height(h))
439 };
440 let set_content = {
441 let st = state.clone();
442 Rc::new(move |h: f32| st.set_content_height(h))
443 };
444 let get_scroll = {
445 let st = state.clone();
446 Rc::new(move || {
447 st.tick();
448 st.get()
449 })
450 };
451 let set_scroll = {
452 let st = state.clone();
453 Rc::new(move |off: f32| st.set_offset(off))
454 };
455 View::new(
456 0,
457 ViewKind::ScrollV {
458 on_scroll: Some(on_scroll),
459 set_viewport_height: Some(set_viewport),
460 set_content_height: Some(set_content),
461 get_scroll_offset: Some(get_scroll),
462 set_scroll_offset: Some(set_scroll),
463 },
464 )
465 .modifier(modifier)
466 .with_children(vec![content])
467}
468
469pub fn HorizontalScrollArea(
470 modifier: Modifier,
471 state: Rc<HorizontalScrollState>,
472 content: View,
473) -> View {
474 let st_clone = state.clone();
475 let on_scroll = {
476 Rc::new(move |d: Vec2| -> Vec2 {
477 let use_dx = if d.x.abs() > 0.001 { d.x } else { d.y };
480 let leftover_x = st_clone.scroll_immediate(use_dx);
481 Vec2 {
482 x: leftover_x,
483 y: if d.x.abs() > 0.001 { d.y } else { 0.0 },
484 }
485 })
486 };
487 let set_viewport_w = {
488 let st = state.clone();
489 Rc::new(move |w: f32| st.set_viewport_width(w))
490 };
491 let set_content_w = {
492 let st = state.clone();
493 Rc::new(move |w: f32| st.set_content_width(w))
494 };
495 let get_scroll_xy = {
496 let st = state.clone();
497 Rc::new(move || {
498 st.tick();
499 (st.get(), 0.0)
500 })
501 };
502 let set_xy = {
503 let st = state.clone();
504 Rc::new(move |x: f32, _y: f32| st.set_offset(x))
505 };
506 View::new(
507 0,
508 ViewKind::ScrollXY {
509 on_scroll: Some(on_scroll),
510 set_viewport_width: Some(set_viewport_w),
511 set_viewport_height: None,
512 set_content_width: Some(set_content_w),
513 set_content_height: None,
514 get_scroll_offset_xy: Some(get_scroll_xy),
515 set_scroll_offset_xy: Some(set_xy),
516 },
517 )
518 .modifier(modifier)
519 .with_children(vec![content])
520}
521
522pub fn ScrollAreaXY(modifier: Modifier, state: Rc<ScrollStateXY>, content: View) -> View {
523 let on_scroll = {
524 let st = state.clone();
525 Rc::new(move |d: Vec2| -> Vec2 { st.scroll_immediate(d) })
526 };
527 let set_vw = {
528 let st = state.clone();
529 Rc::new(move |w: f32| st.set_viewport(w, st.vp_h.get()))
530 };
531 let set_vh = {
532 let st = state.clone();
533 Rc::new(move |h: f32| st.set_viewport(st.vp_w.get(), h))
534 };
535 let set_cw = {
536 let st = state.clone();
537 Rc::new(move |w: f32| {
538 st.set_content(w, st.c_h.get());
539 })
540 };
541 let set_ch = {
542 let st = state.clone();
543 Rc::new(move |h: f32| {
544 st.set_content(st.c_w.get(), h);
545 })
546 };
547 let get_xy = {
548 let st = state.clone();
549 Rc::new(move || {
550 st.tick();
551 st.get()
552 })
553 };
554 let set_xy = {
555 let st = state.clone();
556 Rc::new(move |x: f32, y: f32| st.set_offset_xy(x, y))
557 };
558
559 View::new(
560 0,
561 ViewKind::ScrollXY {
562 on_scroll: Some(on_scroll),
563 set_viewport_width: Some(set_vw),
564 set_viewport_height: Some(set_vh),
565 set_content_width: Some(set_cw),
566 set_content_height: Some(set_ch),
567 get_scroll_offset_xy: Some(get_xy),
568 set_scroll_offset_xy: Some(set_xy),
569 },
570 )
571 .modifier(modifier)
572 .with_children(vec![content])
573}