Skip to main content

cranpose_navigation/
controller.rs

1use std::panic::Location;
2
3use cranpose_core::{MutableState, OwnedMutableState, ownedMutableStateOfNeverEqual};
4use cranpose_coroflow::{ViewModelStore, viewModel};
5use cranpose_services::{SaveableRegistration, SaveableScope, Saver};
6
7mod saved_stack;
8
9/// One screen on the back stack — Android's `NavBackStackEntry`.
10///
11/// Two entries are equal when they are the same visit, so navigating to the
12/// same route twice gives two entries with their own view models and saved
13/// state.
14#[derive(Clone)]
15pub(crate) struct Entry<R> {
16    id: u64,
17    pub(crate) route: R,
18    pub(crate) store: ViewModelStore,
19    pub(crate) saveables: SaveableScope,
20}
21
22impl<R> Entry<R> {
23    fn new(id: u64, route: R, stack: &SaveableScope) -> Self {
24        Self {
25            id,
26            route,
27            store: ViewModelStore::default(),
28            saveables: stack.child(id),
29        }
30    }
31}
32
33impl<R> PartialEq for Entry<R> {
34    fn eq(&self, other: &Self) -> bool {
35        self.id == other.id
36    }
37}
38
39#[derive(Clone)]
40pub(crate) struct BackStack<R> {
41    entries: Vec<Entry<R>>,
42    next_id: u64,
43    /// Where the entries' saved state lives, each entry in a child scope
44    /// named by its id.
45    saveables: SaveableScope,
46}
47
48impl<R: Clone + PartialEq> BackStack<R> {
49    fn new(start: R, saveables: SaveableScope) -> Self {
50        let mut stack = Self {
51            entries: Vec::new(),
52            next_id: 0,
53            saveables,
54        };
55        stack.push(start);
56        stack
57    }
58
59    fn push(&mut self, route: R) {
60        self.entries
61            .push(Entry::new(self.next_id, route, &self.saveables));
62        self.next_id += 1;
63    }
64
65    /// Removes the entries above the topmost `route`, and that entry too when
66    /// `inclusive`. The removed entries are returned so that their view models
67    /// are dropped after the back stack is written, not while it is borrowed.
68    /// [`forget`] forgets their saved state.
69    fn split_above(&mut self, route: &R, inclusive: bool) -> Vec<Entry<R>> {
70        let Some(index) = self.entries.iter().rposition(|entry| entry.route == *route) else {
71            return Vec::new();
72        };
73        self.entries
74            .split_off(if inclusive { index } else { index + 1 })
75    }
76
77    fn top_route(&self) -> Option<&R> {
78        self.entries.last().map(|entry| &entry.route)
79    }
80
81    pub(crate) fn top(&self) -> Option<Entry<R>> {
82        self.entries.last().cloned()
83    }
84
85    /// The entry a pop would show.
86    pub(crate) fn beneath_top(&self) -> Option<Entry<R>> {
87        let index = self.entries.len().checked_sub(2)?;
88        self.entries.get(index).cloned()
89    }
90
91    pub(crate) fn can_pop(&self) -> bool {
92        self.entries.len() > 1
93    }
94}
95
96/// Forgets the saved state of `popped` entries, and returns whether there
97/// was one. Their view models drop once the host has faded them out.
98fn forget<R>(popped: impl IntoIterator<Item = Entry<R>>) -> bool {
99    let mut any = false;
100    for entry in popped {
101        entry.saveables.forget();
102        any = true;
103    }
104    any
105}
106
107/// How [`NavController::navigate_with`] changes the back stack — Android's
108/// `NavOptions`.
109pub struct NavOptions<R> {
110    pop_up_to: Option<(R, bool)>,
111    launch_single_top: bool,
112}
113
114impl<R> Default for NavOptions<R> {
115    fn default() -> Self {
116        Self {
117            pop_up_to: None,
118            launch_single_top: false,
119        }
120    }
121}
122
123impl<R> NavOptions<R> {
124    /// Options that change nothing: the route is pushed on top.
125    pub fn new() -> Self {
126        Self::default()
127    }
128
129    /// Pops the entries above the topmost `route` before navigating, and that
130    /// entry too when `inclusive` — Android's `popUpTo(route) { inclusive }`.
131    /// Nothing is popped when `route` is not on the back stack.
132    #[must_use]
133    pub fn pop_up_to(mut self, route: R, inclusive: bool) -> Self {
134        self.pop_up_to = Some((route, inclusive));
135        self
136    }
137
138    /// Does not push the route when it is already on top — Android's
139    /// `launchSingleTop = true`.
140    #[must_use]
141    pub fn launch_single_top(mut self) -> Self {
142        self.launch_single_top = true;
143        self
144    }
145}
146
147/// The back stack of a [`NavHost`](crate::NavHost) — Android's
148/// `NavController`.
149///
150/// It is `Copy`, so it moves into any number of `move` closures:
151/// `move || nav.navigate(Screen::Detail { id })`. `R` is the route type,
152/// usually an enum whose variants carry the screen's arguments.
153pub struct NavController<R: Clone + 'static> {
154    pub(crate) stack: MutableState<BackStack<R>>,
155}
156
157impl<R: Clone + 'static> Clone for NavController<R> {
158    fn clone(&self) -> Self {
159        *self
160    }
161}
162
163impl<R: Clone + 'static> Copy for NavController<R> {}
164
165impl<R: Clone + 'static> PartialEq for NavController<R> {
166    fn eq(&self, other: &Self) -> bool {
167        self.stack == other.stack
168    }
169}
170
171impl<R: Clone + PartialEq + 'static> NavController<R> {
172    /// Pushes `route` on top of the back stack — Android's `navigate(route)`.
173    pub fn navigate(&self, route: R) {
174        self.navigate_with(route, NavOptions::new());
175    }
176
177    /// Navigates to `route` the way `options` say — Android's
178    /// `navigate(route) { popUpTo(...); launchSingleTop = true }`.
179    pub fn navigate_with(&self, route: R, options: NavOptions<R>) {
180        let popped = self.stack.update(|stack| {
181            let popped = options
182                .pop_up_to
183                .map(|(target, inclusive)| stack.split_above(&target, inclusive))
184                .unwrap_or_default();
185            if !(options.launch_single_top && stack.top_route() == Some(&route)) {
186                stack.push(route);
187            }
188            popped
189        });
190        forget(popped);
191    }
192
193    /// Pops the top entry and returns whether there was one — Android's
194    /// `popBackStack()`. Popping the last entry leaves the host empty.
195    pub fn pop_back_stack(&self) -> bool {
196        forget(self.stack.update(|stack| stack.entries.pop()))
197    }
198
199    /// Pops the entries above the topmost `route`, and that entry too when
200    /// `inclusive`, and returns whether anything was popped — Android's
201    /// `popBackStack(route, inclusive)`.
202    pub fn pop_back_stack_to(&self, route: &R, inclusive: bool) -> bool {
203        forget(
204            self.stack
205                .update(|stack| stack.split_above(route, inclusive)),
206        )
207    }
208
209    /// Pops the entries above the first `depth`.
210    pub(crate) fn pop_to_depth(&self, depth: usize) {
211        forget(self.stack.update(|stack| {
212            let keep = depth.min(stack.entries.len());
213            stack.entries.split_off(keep)
214        }));
215    }
216
217    /// Pops the top entry when there is one beneath it, and returns whether it
218    /// did — Android's `navigateUp()`.
219    pub fn navigate_up(&self) -> bool {
220        forget(
221            self.stack
222                .update(|stack| stack.can_pop().then(|| stack.entries.pop()).flatten()),
223        )
224    }
225
226    /// The route on top of the back stack, `None` once everything is popped.
227    /// Reading it in a composable recomposes that composable when it changes —
228    /// Android's `currentBackStackEntryAsState()`.
229    pub fn current_route(&self) -> Option<R> {
230        self.stack.read(|stack| stack.top_route().cloned())
231    }
232
233    /// Every route on the back stack, bottom first. Reading it in a
234    /// composable recomposes that composable when it changes — Android's
235    /// `currentBackStack`.
236    pub fn back_stack(&self) -> Vec<R> {
237        self.stack.read(|stack| {
238            stack
239                .entries
240                .iter()
241                .map(|entry| entry.route.clone())
242                .collect()
243        })
244    }
245}
246
247struct NavControllerState<R: Clone + 'static> {
248    /// Declared first so that it drops first and reads the back stack one
249    /// last time.
250    _saved: Option<SaveableRegistration>,
251    stack: OwnedMutableState<BackStack<R>>,
252}
253
254impl<R: Clone + 'static> NavControllerState<R> {
255    fn controller(&self) -> NavController<R> {
256        NavController {
257            stack: self.stack.handle(),
258        }
259    }
260}
261
262/// A [`NavController`] whose back stack starts at `start` — Android's
263/// `rememberNavController()` plus `NavHost(startDestination)`.
264///
265/// The back stack lives in the nearest view model store, so a `NavHost`
266/// nested in a screen keeps its back stack while another screen covers that
267/// screen. It lives only in memory, and so does the `rememberSaveable` state
268/// of its screens: a covered screen gets its state back, and a popped one
269/// forgets it. [`rememberSaveableNavController`] also restores both after the
270/// process restarts.
271#[track_caller]
272pub fn rememberNavController<R: Clone + PartialEq + 'static>(start: R) -> NavController<R> {
273    let location = Location::caller();
274    let state = viewModel(location, |_| {
275        let saveables = SaveableScope::current().child(location).in_memory();
276        saveables.forget();
277        NavControllerState {
278            _saved: None,
279            stack: ownedMutableStateOfNeverEqual(BackStack::new(start, saveables)),
280        }
281    });
282    state.get().controller()
283}
284
285/// A [`NavController`] whose back stack is saved under `key` at each save
286/// boundary, as `rememberSaveable` state is, and restored when it is next
287/// created, after a process restart too. `saver` gives each route its saved
288/// form; it may read the forms older builds wrote.
289///
290/// The `rememberSaveable` state of each screen is saved with it, apart from
291/// other visits of the same route. A popped screen forgets its state.
292/// Restoring keeps the entries beneath the first route `saver` cannot read;
293/// when it cannot read the bottom one, or nothing was saved, the back stack
294/// starts at `start`.
295#[track_caller]
296pub fn rememberSaveableNavController<R: Clone + PartialEq + 'static>(
297    key: &'static str,
298    start: R,
299    saver: Saver<R>,
300) -> NavController<R> {
301    let state = viewModel(Location::caller(), |_| {
302        let scope = SaveableScope::current();
303        let saveables = scope.child(key);
304        let restored = scope
305            .restore(key)
306            .and_then(|saved| saved_stack::decode(&saved, &saver, &saveables));
307        let stack = match restored {
308            Some(stack) => {
309                saveables.forget_children_except(stack.entries.iter().map(|entry| entry.id));
310                stack
311            }
312            None => {
313                saveables.forget();
314                BackStack::new(start, saveables)
315            }
316        };
317        let stack = ownedMutableStateOfNeverEqual(stack);
318        let handle = stack.handle();
319        let saved = scope.register(key, move || {
320            handle.try_with(|stack| saved_stack::encode(stack, &saver))
321        });
322        NavControllerState {
323            _saved: Some(saved),
324            stack,
325        }
326    });
327    state.get().controller()
328}