Skip to main content

cranpose_navigation/
saved_scroll.rs

1//! Scroll positions kept like [`rememberSaveable`] values, so a list, grid,
2//! pager or scrolling column comes back where the user left it: after a
3//! screen that covered it is popped, and after the process restarts.
4//!
5//! Each state saves under its `key` in the [`SaveableScope`] where it is
6//! composed; a navigation entry has its own scope, so two visits of one
7//! screen keep their positions apart. A position is saved at each save
8//! boundary and when the scroller leaves the composition, and restored when
9//! it is composed again.
10//!
11//! [`rememberSaveable`]: cranpose_services::rememberSaveable
12
13use cranpose_core::remember;
14use cranpose_services::{SaveableRegistration, SaveableScope};
15use cranpose_ui::{
16    LazyGridState, LazyListState, LazyStaggeredGridState, PagerState, ScrollState, composable,
17};
18
19/// The stored form of a first visible item and its offset: `"index offset"`.
20fn format_position((index, offset): (usize, f32)) -> String {
21    format!("{index} {offset}")
22}
23
24fn parse_position(stored: &str) -> Option<(usize, f32)> {
25    let (index, offset) = stored.split_once(' ')?;
26    Some((index.parse().ok()?, offset.parse().ok()?))
27}
28
29/// Remembers the state `create` makes from the position saved under `key`,
30/// and saves the state's position under `key`. The state and its
31/// registration share one slot, so the scroller's last position is read
32/// before its states are released.
33fn remember_saved<S, P>(
34    key: &'static str,
35    parse: fn(&str) -> Option<P>,
36    format: fn(P) -> String,
37    create: impl FnOnce(Option<P>) -> S,
38    read: fn(&S) -> Option<P>,
39) -> S
40where
41    S: Clone + 'static,
42    P: 'static,
43{
44    remember(move || {
45        let scope = SaveableScope::current();
46        let state = create(scope.restore(key).as_deref().and_then(parse));
47        let saved = state.clone();
48        let registration: SaveableRegistration =
49            scope.register(key, move || read(&saved).map(format));
50        (state, registration)
51    })
52    .with(|(state, _)| state.clone())
53}
54
55/// A [`LazyListState`] for a `LazyColumn` or `LazyRow` whose position is
56/// saved under `key`.
57#[composable]
58pub fn rememberSaveableLazyListState(key: &'static str) -> LazyListState {
59    remember_saved(
60        key,
61        parse_position,
62        format_position,
63        |restored| {
64            let (index, offset) = restored.unwrap_or_default();
65            LazyListState::new(index, offset)
66        },
67        LazyListState::try_position,
68    )
69}
70
71/// A [`LazyGridState`] whose first visible item is saved under `key`. It
72/// comes back to the same item when the grid has another number of columns,
73/// as after a rotation.
74#[composable]
75pub fn rememberSaveableLazyGridState(key: &'static str) -> LazyGridState {
76    remember_saved(
77        key,
78        parse_position,
79        format_position,
80        |restored| {
81            let state = LazyGridState::new();
82            if let Some((index, offset)) = restored {
83                state.scroll_to_item(index, offset);
84            }
85            state
86        },
87        LazyGridState::try_position,
88    )
89}
90
91/// A [`LazyStaggeredGridState`] whose position is saved under `key`.
92#[composable]
93pub fn rememberSaveableLazyStaggeredGridState(key: &'static str) -> LazyStaggeredGridState {
94    remember_saved(
95        key,
96        parse_position,
97        format_position,
98        |restored| {
99            let (index, offset) = restored.unwrap_or_default();
100            LazyStaggeredGridState::new(index, offset)
101        },
102        LazyStaggeredGridState::try_position,
103    )
104}
105
106/// A [`PagerState`] whose page is saved under `key`; it starts on
107/// `initial_page` when nothing was saved.
108#[composable]
109pub fn rememberSaveablePagerState(key: &'static str, initial_page: usize) -> PagerState {
110    remember_saved(
111        key,
112        |stored| stored.parse().ok(),
113        |page: usize| page.to_string(),
114        |restored| PagerState::new(restored.unwrap_or(initial_page)),
115        PagerState::try_first_page,
116    )
117}
118
119/// A [`ScrollState`] for `vertical_scroll` or `horizontal_scroll` whose
120/// position, in logical pixels, is saved under `key`.
121#[composable]
122pub fn rememberSaveableScrollState(key: &'static str) -> ScrollState {
123    remember_saved(
124        key,
125        |stored| stored.parse().ok(),
126        |value: f32| value.to_string(),
127        |restored| ScrollState::new(restored.unwrap_or_default()),
128        ScrollState::try_value,
129    )
130}