1use std::fmt::{self, Debug};
4use std::hash::{Hash, Hasher};
5use std::rc::Rc;
6use std::time::Duration;
7
8use inset_animation::{Curve, Curves};
9use inset_foundation::{
10 App, ChangeNotifier, ChangeNotifierData, CompleterFuture, Handle, HandleId, Listenable,
11 ListenableObject, Listener,
12};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum ScrollDirection {
17 Idle,
19
20 Forward,
22
23 Reverse,
25}
26
27pub fn flip_scroll_direction(direction: ScrollDirection) -> ScrollDirection {
31 match direction {
32 ScrollDirection::Idle => ScrollDirection::Idle,
33 ScrollDirection::Forward => ScrollDirection::Reverse,
34 ScrollDirection::Reverse => ScrollDirection::Forward,
35 }
36}
37
38pub trait ViewportOffset: ChangeNotifier + Sized {
51 fn pixels(self: Handle<Self>, app: &App) -> f64;
61
62 fn has_pixels(self: Handle<Self>, app: &App) -> bool;
64
65 fn apply_viewport_dimension(self: Handle<Self>, app: &mut App, viewport_dimension: f64)
83 -> bool;
84
85 fn apply_content_dimensions(
105 self: Handle<Self>,
106 app: &mut App,
107 min_scroll_extent: f64,
108 max_scroll_extent: f64,
109 ) -> bool;
110
111 fn correct_by(self: Handle<Self>, app: &mut App, correction: f64);
124
125 fn jump_to(self: Handle<Self>, app: &mut App, pixels: f64);
133
134 fn animate_to(
142 self: Handle<Self>,
143 app: &mut App,
144 to: f64,
145 duration: Duration,
146 curve: Rc<dyn Curve>,
147 ) -> CompleterFuture<()>;
148
149 fn move_to(
157 self: Handle<Self>,
158 app: &mut App,
159 to: f64,
160 duration: Option<Duration>,
161 curve: Option<Rc<dyn Curve>>,
162 clamp: Option<bool>,
163 ) -> CompleterFuture<()> {
164 ViewportOffsetBase::move_to(self, app, to, duration, curve, clamp)
165 }
166
167 fn user_scroll_direction(self: Handle<Self>, app: &App) -> ScrollDirection;
178
179 fn allow_implicit_scrolling(self: Handle<Self>, app: &App) -> bool;
187
188 fn debug_fill_description(self: Handle<Self>, app: &App, description: &mut Vec<String>) {
193 if self.has_pixels(app) {
194 description.push(format!("offset: {:.1}", self.pixels(app)));
195 }
196 }
197
198 fn as_viewport_offset(self: Handle<Self>) -> AnyViewportOffset {
201 AnyViewportOffset {
202 id: self.id(),
203 vtable: const { &ViewportOffsetVTable::of::<Self>() },
204 }
205 }
206}
207
208pub struct ViewportOffsetBase;
210
211impl ViewportOffsetBase {
212 pub fn move_to<T: ViewportOffset>(
214 this: Handle<T>,
215 app: &mut App,
216 to: f64,
217 duration: Option<Duration>,
218 curve: Option<Rc<dyn Curve>>,
219 clamp: Option<bool>,
220 ) -> CompleterFuture<()> {
221 let _ = clamp;
222 let Some(duration) = duration.filter(|duration| !duration.is_zero()) else {
223 this.jump_to(app, to);
224 return CompleterFuture::ready(());
225 };
226 this.animate_to(app, to, duration, curve.unwrap_or(Curves::ease()))
227 }
228}
229
230type AnimateToFn = fn(&mut App, HandleId, f64, Duration, Rc<dyn Curve>) -> CompleterFuture<()>;
232
233type MoveToFn = fn(
235 &mut App,
236 HandleId,
237 f64,
238 Option<Duration>,
239 Option<Rc<dyn Curve>>,
240 Option<bool>,
241) -> CompleterFuture<()>;
242
243struct ViewportOffsetVTable {
246 add_listener: fn(&mut App, HandleId, Listener),
247 remove_listener: fn(&mut App, HandleId, &Listener),
248 pixels: fn(&App, HandleId) -> f64,
249 has_pixels: fn(&App, HandleId) -> bool,
250 apply_viewport_dimension: fn(&mut App, HandleId, f64) -> bool,
251 apply_content_dimensions: fn(&mut App, HandleId, f64, f64) -> bool,
252 correct_by: fn(&mut App, HandleId, f64),
253 jump_to: fn(&mut App, HandleId, f64),
254 animate_to: AnimateToFn,
255 move_to: MoveToFn,
256 user_scroll_direction: fn(&App, HandleId) -> ScrollDirection,
257 allow_implicit_scrolling: fn(&App, HandleId) -> bool,
258}
259
260fn resolve<T: 'static>(id: HandleId) -> Handle<T> {
262 Handle::from_id(id)
263}
264
265impl ViewportOffsetVTable {
266 const fn of<T: ViewportOffset>() -> ViewportOffsetVTable {
267 ViewportOffsetVTable {
268 add_listener: |app, id, listener| {
269 ListenableObject::add_listener(resolve::<T>(id), app, listener)
270 },
271 remove_listener: |app, id, listener| {
272 ListenableObject::remove_listener(resolve::<T>(id), app, listener)
273 },
274 pixels: |app, id| T::pixels(resolve(id), app),
275 has_pixels: |app, id| T::has_pixels(resolve(id), app),
276 apply_viewport_dimension: |app, id, dimension| {
277 T::apply_viewport_dimension(resolve(id), app, dimension)
278 },
279 apply_content_dimensions: |app, id, min, max| {
280 T::apply_content_dimensions(resolve(id), app, min, max)
281 },
282 correct_by: |app, id, correction| T::correct_by(resolve(id), app, correction),
283 jump_to: |app, id, pixels| T::jump_to(resolve(id), app, pixels),
284 animate_to: |app, id, to, duration, curve| {
285 T::animate_to(resolve(id), app, to, duration, curve)
286 },
287 move_to: |app, id, to, duration, curve, clamp| {
288 T::move_to(resolve(id), app, to, duration, curve, clamp)
289 },
290 user_scroll_direction: |app, id| T::user_scroll_direction(resolve(id), app),
291 allow_implicit_scrolling: |app, id| T::allow_implicit_scrolling(resolve(id), app),
292 }
293 }
294}
295
296#[derive(Clone, Copy)]
300pub struct AnyViewportOffset {
301 id: HandleId,
302 vtable: &'static ViewportOffsetVTable,
303}
304
305impl AnyViewportOffset {
306 pub fn pixels(self, app: &App) -> f64 {
308 (self.vtable.pixels)(app, self.id)
309 }
310
311 pub fn has_pixels(self, app: &App) -> bool {
313 (self.vtable.has_pixels)(app, self.id)
314 }
315
316 pub fn apply_viewport_dimension(self, app: &mut App, viewport_dimension: f64) -> bool {
318 (self.vtable.apply_viewport_dimension)(app, self.id, viewport_dimension)
319 }
320
321 pub fn apply_content_dimensions(
323 self,
324 app: &mut App,
325 min_scroll_extent: f64,
326 max_scroll_extent: f64,
327 ) -> bool {
328 (self.vtable.apply_content_dimensions)(app, self.id, min_scroll_extent, max_scroll_extent)
329 }
330
331 pub fn correct_by(self, app: &mut App, correction: f64) {
333 (self.vtable.correct_by)(app, self.id, correction)
334 }
335
336 pub fn jump_to(self, app: &mut App, pixels: f64) {
338 (self.vtable.jump_to)(app, self.id, pixels)
339 }
340
341 pub fn animate_to(
343 self,
344 app: &mut App,
345 to: f64,
346 duration: Duration,
347 curve: Rc<dyn Curve>,
348 ) -> CompleterFuture<()> {
349 (self.vtable.animate_to)(app, self.id, to, duration, curve)
350 }
351
352 pub fn move_to(
354 self,
355 app: &mut App,
356 to: f64,
357 duration: Option<Duration>,
358 curve: Option<Rc<dyn Curve>>,
359 clamp: Option<bool>,
360 ) -> CompleterFuture<()> {
361 (self.vtable.move_to)(app, self.id, to, duration, curve, clamp)
362 }
363
364 pub fn user_scroll_direction(self, app: &App) -> ScrollDirection {
366 (self.vtable.user_scroll_direction)(app, self.id)
367 }
368
369 pub fn allow_implicit_scrolling(self, app: &App) -> bool {
371 (self.vtable.allow_implicit_scrolling)(app, self.id)
372 }
373}
374
375impl PartialEq for AnyViewportOffset {
376 fn eq(&self, other: &AnyViewportOffset) -> bool {
377 self.id == other.id
378 }
379}
380
381impl Eq for AnyViewportOffset {}
382
383impl Hash for AnyViewportOffset {
384 fn hash<H: Hasher>(&self, state: &mut H) {
385 self.id.hash(state);
386 }
387}
388
389impl Debug for AnyViewportOffset {
390 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391 write!(f, "AnyViewportOffset({:?})", self.id)
392 }
393}
394
395impl Listenable for AnyViewportOffset {
396 fn add_listener(&self, app: &mut App, listener: Listener) {
397 (self.vtable.add_listener)(app, self.id, listener)
398 }
399
400 fn remove_listener(&self, app: &mut App, listener: &Listener) {
401 (self.vtable.remove_listener)(app, self.id, listener)
402 }
403}
404
405#[derive(Debug, Default)]
411pub struct FixedViewportOffset {
412 change_notifier: ChangeNotifierData,
413 pixels: f64,
414}
415
416impl FixedViewportOffset {
417 pub fn new(app: &mut App, value: f64) -> Handle<FixedViewportOffset> {
421 app.create(FixedViewportOffset {
422 change_notifier: ChangeNotifierData::new(),
423 pixels: value,
424 })
425 }
426
427 pub fn zero(app: &mut App) -> Handle<FixedViewportOffset> {
431 FixedViewportOffset::new(app, 0.0)
432 }
433}
434
435impl ChangeNotifier for FixedViewportOffset {
436 fn change_notifier_data(&self) -> &ChangeNotifierData {
437 &self.change_notifier
438 }
439
440 fn change_notifier_data_mut(&mut self) -> &mut ChangeNotifierData {
441 &mut self.change_notifier
442 }
443}
444
445impl ViewportOffset for FixedViewportOffset {
446 fn pixels(self: Handle<Self>, app: &App) -> f64 {
447 app.get(self).pixels
448 }
449
450 fn has_pixels(self: Handle<Self>, _app: &App) -> bool {
451 true
452 }
453
454 fn apply_viewport_dimension(
455 self: Handle<Self>,
456 _app: &mut App,
457 _viewport_dimension: f64,
458 ) -> bool {
459 true
460 }
461
462 fn apply_content_dimensions(
463 self: Handle<Self>,
464 _app: &mut App,
465 _min_scroll_extent: f64,
466 _max_scroll_extent: f64,
467 ) -> bool {
468 true
469 }
470
471 fn correct_by(self: Handle<Self>, app: &mut App, correction: f64) {
472 app.get_mut(self).pixels += correction;
473 }
474
475 fn jump_to(self: Handle<Self>, _app: &mut App, _pixels: f64) {
476 }
478
479 fn animate_to(
480 self: Handle<Self>,
481 _app: &mut App,
482 _to: f64,
483 _duration: Duration,
484 _curve: Rc<dyn Curve>,
485 ) -> CompleterFuture<()> {
486 CompleterFuture::ready(())
487 }
488
489 fn user_scroll_direction(self: Handle<Self>, _app: &App) -> ScrollDirection {
490 ScrollDirection::Idle
491 }
492
493 fn allow_implicit_scrolling(self: Handle<Self>, _app: &App) -> bool {
494 false
495 }
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501 use inset_foundation::AppCell;
502
503 #[test]
504 fn a_fixed_offset_only_moves_by_correction() {
505 let cell = AppCell::new();
506 let mut app = cell.borrow_mut();
507 let offset = FixedViewportOffset::new(&mut app, 12.0);
508 assert_eq!(offset.pixels(&app), 12.0);
509 assert!(offset.has_pixels(&app));
510 offset.jump_to(&mut app, 40.0);
511 assert_eq!(offset.pixels(&app), 12.0);
512 offset.correct_by(&mut app, -5.0);
513 assert_eq!(offset.pixels(&app), 7.0);
514 assert_eq!(offset.user_scroll_direction(&app), ScrollDirection::Idle);
515 assert!(!offset.allow_implicit_scrolling(&app));
516 }
517
518 #[test]
519 fn the_erased_edge_dispatches_to_the_concrete_offset() {
520 let cell = AppCell::new();
521 let mut app = cell.borrow_mut();
522 let offset = FixedViewportOffset::zero(&mut app).as_viewport_offset();
523 assert_eq!(offset.pixels(&app), 0.0);
524 assert!(offset.apply_viewport_dimension(&mut app, 100.0));
525 assert!(offset.apply_content_dimensions(&mut app, 0.0, 200.0));
526 let moved = offset.move_to(&mut app, 30.0, None, None, None);
527 assert_eq!(offset.pixels(&app), 0.0);
528 assert!(
529 moved.is_completed(),
530 "a jump hands back an already completed future"
531 );
532 let animated = offset.animate_to(&mut app, 30.0, Duration::from_millis(10), Curves::ease());
533 assert!(
534 animated.is_completed(),
535 "a fixed offset's animation is over before it starts"
536 );
537 }
538}