cranpose_ui/widgets/
animate_scroll.rs1use std::rc::Rc;
5
6use cranpose_foundation::lazy::LazyListState;
7
8use crate::fling_animation::{SettleAnimation, SpringParams};
9
10pub trait AnimateScroll {
13 fn animate_scroll_by(&self, distance: f32);
15
16 fn animate_scroll_to_item(&self, index: usize, offset: f32);
19}
20
21impl AnimateScroll for LazyListState {
22 fn animate_scroll_by(&self, distance: f32) {
23 animate(*self, distance, None);
24 }
25
26 fn animate_scroll_to_item(&self, index: usize, offset: f32) {
27 animate(
28 *self,
29 distance_to_item(self, index) + offset,
30 Some((index, offset)),
31 );
32 }
33}
34
35fn distance_to_item(state: &LazyListState, index: usize) -> f32 {
39 let shown = state.with_layout_info(|info| {
40 let item = info
41 .visible_items_info
42 .iter()
43 .find(|item| item.index == index)?;
44 let leading = if index == 0 {
45 info.before_content_padding
46 } else {
47 0.0
48 };
49 Some(item.offset - leading)
50 });
51 if let Some(distance) = shown.flatten() {
52 return distance;
53 }
54 let first = state.first_visible_item_index_non_reactive();
55 let scrolled = state.first_visible_item_scroll_offset_non_reactive();
56 (index as f32 - first as f32) * state.average_item_size() - scrolled
57}
58
59fn animate(state: LazyListState, distance: f32, land: Option<(usize, f32)>) {
62 let Some(runtime) = cranpose_core::current_runtime_handle() else {
63 match land {
64 Some((index, offset)) => state.scroll_to_item(index, offset),
65 None => {
66 state.dispatch_scroll_delta(-distance);
67 }
68 }
69 return;
70 };
71 let settle = SettleAnimation::new(runtime, SpringParams::SETTLE_POLICY);
72 let stop = settle.clone();
73 let id = state.start_scroll_animation(Rc::new(move || stop.cancel()));
74 settle.start_settle(
75 0.0,
76 0.0,
77 distance,
78 move |delta| -state.dispatch_scroll_delta(-delta),
79 move |_| {
80 state.end_scroll_animation(id);
81 if let Some((index, offset)) = land {
82 state.scroll_to_item(index, offset);
83 }
84 },
85 );
86}