Skip to main content

cranpose_navigation/
controller.rs

1use std::{any::Any, panic::Location, rc::Rc};
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, as the host keeps it.
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> Entry<R> {
34    /// Tells this visit apart from every other entry of its back stack.
35    pub(crate) fn id(&self) -> u64 {
36        self.id
37    }
38
39    /// Whether `scope` is this entry's saved state scope or one inside it,
40    /// such as the scope of a host nested in the entry's screen.
41    fn encloses(&self, scope: &SaveableScope) -> bool {
42        let (Some(own), Some(inner)) = (self.saveables.namespace(), scope.namespace()) else {
43            return false;
44        };
45        inner
46            .strip_prefix(own)
47            .is_some_and(|rest| rest.is_empty() || rest.starts_with('/'))
48    }
49}
50
51impl<R> PartialEq for Entry<R> {
52    fn eq(&self, other: &Self) -> bool {
53        self.id == other.id
54    }
55}
56
57#[derive(Clone)]
58pub(crate) struct BackStack<R> {
59    entries: Vec<Entry<R>>,
60    /// The segments that pops with [`NavOptions::save_state`] kept, each
61    /// bottom first, for [`NavOptions::restore_state`] to bring back.
62    saved: Vec<Vec<Entry<R>>>,
63    next_id: u64,
64    /// Where the entries' saved state lives, each entry in a child scope
65    /// named by its id.
66    saveables: SaveableScope,
67}
68
69impl<R: Clone + PartialEq> BackStack<R> {
70    fn new(start: R, saveables: SaveableScope) -> Self {
71        let mut stack = Self {
72            entries: Vec::new(),
73            saved: Vec::new(),
74            next_id: 0,
75            saveables,
76        };
77        stack.push(start);
78        stack
79    }
80
81    fn push(&mut self, route: R) {
82        self.entries
83            .push(Entry::new(self.next_id, route, &self.saveables));
84        self.next_id += 1;
85    }
86
87    /// Removes the entries above the topmost `route`, and that entry too when
88    /// `inclusive`. The removed entries are returned so that their view models
89    /// are dropped after the back stack is written, not while it is borrowed.
90    /// [`forget`] forgets their saved state.
91    fn split_above(&mut self, route: &R, inclusive: bool) -> Vec<Entry<R>> {
92        let Some(index) = self.entries.iter().rposition(|entry| entry.route == *route) else {
93            return Vec::new();
94        };
95        self.entries
96            .split_off(if inclusive { index } else { index + 1 })
97    }
98
99    /// Keeps `popped` for a later [`NavOptions::restore_state`], in place of
100    /// the segments kept before that hold one of its routes. Those are off
101    /// screen, so their view models are dropped here; they are returned for
102    /// [`forget`] to forget their saved state.
103    fn keep(&mut self, popped: Vec<Entry<R>>) -> Vec<Entry<R>> {
104        let (replaced, kept) = std::mem::take(&mut self.saved)
105            .into_iter()
106            .partition::<Vec<_>, _>(|segment| {
107                segment
108                    .iter()
109                    .any(|entry| popped.iter().any(|new| new.route == entry.route))
110            });
111        self.saved = kept;
112        self.saved.push(popped);
113        let replaced: Vec<Entry<R>> = replaced.into_iter().flatten().collect();
114        for entry in &replaced {
115            entry.store.clear();
116        }
117        replaced
118    }
119
120    /// Takes the kept segment that holds `route`.
121    fn take_saved(&mut self, route: &R) -> Option<Vec<Entry<R>>> {
122        let index = self
123            .saved
124            .iter()
125            .position(|segment| segment.iter().any(|entry| entry.route == *route))?;
126        Some(self.saved.swap_remove(index))
127    }
128
129    fn top_route(&self) -> Option<&R> {
130        self.entries.last().map(|entry| &entry.route)
131    }
132
133    /// The entry `id`, on the back stack or kept by a
134    /// [`NavOptions::save_state`] pop.
135    fn find(&self, id: u64) -> Option<&Entry<R>> {
136        self.entries
137            .iter()
138            .chain(self.saved.iter().flatten())
139            .find(|entry| entry.id == id)
140    }
141
142    pub(crate) fn can_pop(&self) -> bool {
143        self.entries.len() > 1
144    }
145
146    /// What a host shows when `dialog` gives the routes that show as
147    /// dialogs their spec: the topmost entry that is no dialog, the screen a
148    /// pop of it shows, and the dialog entries above it with their depth in
149    /// the back stack.
150    pub(crate) fn screens_and_dialogs<S>(
151        &self,
152        dialog: impl Fn(&R) -> Option<S>,
153    ) -> HostView<R, S> {
154        let is_screen = |entry: &Entry<R>| dialog(&entry.route).is_none();
155        let at = self.entries.iter().rposition(is_screen);
156        let toward = at
157            .and_then(|at| self.entries[..at].iter().rposition(is_screen))
158            .map(|index| self.entries[index].clone());
159        let from = at.map_or(0, |at| at + 1);
160        let dialogs = self.entries[from..]
161            .iter()
162            .enumerate()
163            .filter_map(|(offset, entry)| {
164                dialog(&entry.route).map(|spec| (from + offset, entry.clone(), spec))
165            })
166            .collect();
167        HostView {
168            screen: at.map(|at| self.entries[at].clone()),
169            toward,
170            dialogs,
171        }
172    }
173}
174
175/// What a [`NavHost`](crate::NavHost) shows of a back stack.
176pub(crate) struct HostView<R, S> {
177    /// The topmost entry that is no dialog.
178    pub(crate) screen: Option<Entry<R>>,
179    /// The screen beneath it, which a pop shows.
180    pub(crate) toward: Option<Entry<R>>,
181    /// The dialog entries above the screen, bottom first, each with its
182    /// depth in the back stack and its spec.
183    pub(crate) dialogs: Vec<(usize, Entry<R>, S)>,
184}
185
186/// The values that screens left for the entries of one back stack, each
187/// with the id of its entry. An entry holds one value of each type.
188#[derive(Clone, Default)]
189pub(crate) struct EntryResults(Vec<(u64, Rc<dyn Any>)>);
190
191impl EntryResults {
192    /// Where the value of type `T` for `entry` is.
193    fn position<T: 'static>(&self, entry: u64) -> Option<usize> {
194        self.0
195            .iter()
196            .position(|(id, value)| *id == entry && value.is::<T>())
197    }
198
199    fn holds_any(&self, entry: u64) -> bool {
200        self.0.iter().any(|(id, _)| *id == entry)
201    }
202}
203
204/// How [`NavController::navigate_with`] changes the back stack — Android's
205/// `NavOptions`.
206pub struct NavOptions<R> {
207    pop_up_to: Option<(R, bool)>,
208    launch_single_top: bool,
209    save_state: bool,
210    restore_state: bool,
211}
212
213impl<R> Default for NavOptions<R> {
214    fn default() -> Self {
215        Self {
216            pop_up_to: None,
217            launch_single_top: false,
218            save_state: false,
219            restore_state: false,
220        }
221    }
222}
223
224impl<R> NavOptions<R> {
225    /// Options that change nothing: the route is pushed on top.
226    pub fn new() -> Self {
227        Self::default()
228    }
229
230    /// Pops the entries above the topmost `route` before navigating, and that
231    /// entry too when `inclusive` — Android's `popUpTo(route) { inclusive }`.
232    /// Nothing is popped when `route` is not on the back stack.
233    #[must_use]
234    pub fn pop_up_to(mut self, route: R, inclusive: bool) -> Self {
235        self.pop_up_to = Some((route, inclusive));
236        self
237    }
238
239    /// Does not push the route when it is already on top — Android's
240    /// `launchSingleTop = true`.
241    #[must_use]
242    pub fn launch_single_top(mut self) -> Self {
243        self.launch_single_top = true;
244        self
245    }
246
247    /// Keeps the entries that [`Self::pop_up_to`] pops, with their view
248    /// models and saved state, for a later navigation with
249    /// [`Self::restore_state`] to bring back — Android's
250    /// `popUpTo(route) { saveState = true }`. A tab bar uses it to give each
251    /// tab its own back stack.
252    #[must_use]
253    pub fn save_state(mut self) -> Self {
254        self.save_state = true;
255        self
256    }
257
258    /// Brings back the entries a [`Self::save_state`] pop kept for the route,
259    /// bottom first, in place of a new entry for it — Android's
260    /// `restoreState = true`. Without such entries the route is pushed as
261    /// usual.
262    #[must_use]
263    pub fn restore_state(mut self) -> Self {
264        self.restore_state = true;
265        self
266    }
267}
268
269/// The back stack of a [`NavHost`](crate::NavHost) — Android's
270/// `NavController`.
271///
272/// It is `Copy`, so it moves into any number of `move` closures:
273/// `move || nav.navigate(Screen::Detail { id })`. `R` is the route type,
274/// usually an enum whose variants carry the screen's arguments.
275pub struct NavController<R: Clone + 'static> {
276    pub(crate) stack: MutableState<BackStack<R>>,
277    /// Apart from the back stack, so that only the readers of results
278    /// recompose when a result changes.
279    results: MutableState<EntryResults>,
280}
281
282impl<R: Clone + 'static> Clone for NavController<R> {
283    fn clone(&self) -> Self {
284        *self
285    }
286}
287
288impl<R: Clone + 'static> Copy for NavController<R> {}
289
290impl<R: Clone + 'static> PartialEq for NavController<R> {
291    fn eq(&self, other: &Self) -> bool {
292        self.stack == other.stack
293    }
294}
295
296impl<R: Clone + PartialEq + 'static> NavController<R> {
297    /// Pushes `route` on top of the back stack — Android's `navigate(route)`.
298    pub fn navigate(&self, route: R) {
299        self.navigate_with(route, NavOptions::new());
300    }
301
302    /// Navigates to `route` the way `options` say — Android's
303    /// `navigate(route) { popUpTo(...); launchSingleTop = true }`.
304    pub fn navigate_with(&self, route: R, options: NavOptions<R>) {
305        let forgotten = self.stack.update(|stack| {
306            let popped = options
307                .pop_up_to
308                .map(|(target, inclusive)| stack.split_above(&target, inclusive))
309                .unwrap_or_default();
310            let forgotten = if options.save_state && !popped.is_empty() {
311                stack.keep(popped)
312            } else {
313                popped
314            };
315            let restored = options
316                .restore_state
317                .then(|| stack.take_saved(&route))
318                .flatten();
319            if let Some(restored) = restored {
320                stack.entries.extend(restored);
321            } else if !(options.launch_single_top && stack.top_route() == Some(&route)) {
322                stack.push(route);
323            }
324            forgotten
325        });
326        self.forget(forgotten);
327    }
328
329    /// Pops the top entry and returns whether there was one — Android's
330    /// `popBackStack()`. Popping the last entry leaves the host empty.
331    pub fn pop_back_stack(&self) -> bool {
332        self.forget(self.stack.update(|stack| stack.entries.pop()))
333    }
334
335    /// Pops the entries above the topmost `route`, and that entry too when
336    /// `inclusive`, and returns whether anything was popped — Android's
337    /// `popBackStack(route, inclusive)`.
338    pub fn pop_back_stack_to(&self, route: &R, inclusive: bool) -> bool {
339        self.forget(
340            self.stack
341                .update(|stack| stack.split_above(route, inclusive)),
342        )
343    }
344
345    /// Pops the entries above the first `depth`.
346    pub(crate) fn pop_to_depth(&self, depth: usize) {
347        self.forget(self.stack.update(|stack| {
348            let keep = depth.min(stack.entries.len());
349            stack.entries.split_off(keep)
350        }));
351    }
352
353    /// Leaves the bottom entry with `route` above it, the back stack Android
354    /// builds for a deep link; a `route` equal to the bottom one leaves the
355    /// bottom alone.
356    pub(crate) fn open_deep_link(&self, route: R) {
357        self.forget(self.stack.update(|stack| {
358            let popped = stack.entries.split_off(stack.entries.len().min(1));
359            if stack.top_route() != Some(&route) {
360                stack.push(route);
361            }
362            popped
363        }));
364    }
365
366    /// Pops the top entry when there is one beneath it, and returns whether it
367    /// did — Android's `navigateUp()`.
368    pub fn navigate_up(&self) -> bool {
369        self.forget(
370            self.stack
371                .update(|stack| stack.can_pop().then(|| stack.entries.pop()).flatten()),
372        )
373    }
374
375    /// The route on top of the back stack, `None` once everything is popped.
376    /// Reading it in a composable recomposes that composable when it changes —
377    /// Android's `currentBackStackEntryAsState()`.
378    pub fn current_route(&self) -> Option<R> {
379        self.stack.read(|stack| stack.top_route().cloned())
380    }
381
382    /// Every route on the back stack, bottom first. Reading it in a
383    /// composable recomposes that composable when it changes — Android's
384    /// `currentBackStack`.
385    pub fn back_stack(&self) -> Vec<R> {
386        self.stack.read(|stack| {
387            stack
388                .entries
389                .iter()
390                .map(|entry| entry.route.clone())
391                .collect()
392        })
393    }
394
395    /// The entry whose screen or dialog composes the caller, when this
396    /// controller's host shows it — the `NavBackStackEntry` that Android's
397    /// `NavHost` gives each destination. Call it while composing. It does not
398    /// subscribe the caller, and it is `None` once the entry is popped.
399    pub fn composing_entry(&self) -> Option<NavBackStackEntry<R>> {
400        let scope = SaveableScope::current();
401        self.entry_where(|entries| entries.iter().find(|entry| entry.encloses(&scope)))
402    }
403
404    /// The entry beneath the top one, which a pop shows — Android's
405    /// `previousBackStackEntry`. It does not subscribe the caller.
406    pub fn previous_back_stack_entry(&self) -> Option<NavBackStackEntry<R>> {
407        self.entry_where(|entries| entries.iter().nth_back(1))
408    }
409
410    /// The topmost entry of `route` — Android's `getBackStackEntry(route)`.
411    /// It does not subscribe the caller.
412    pub fn back_stack_entry(&self, route: &R) -> Option<NavBackStackEntry<R>> {
413        self.entry_where(|entries| entries.iter().rfind(|entry| entry.route == *route))
414    }
415
416    /// The handle of the entry that `find` picks from the back stack, read
417    /// without subscribing the caller.
418    fn entry_where(
419        &self,
420        find: impl FnOnce(&[Entry<R>]) -> Option<&Entry<R>>,
421    ) -> Option<NavBackStackEntry<R>> {
422        let id = self
423            .stack
424            .try_with(|stack| find(&stack.entries).map(Entry::id))
425            .flatten()?;
426        Some(NavBackStackEntry {
427            controller: *self,
428            id,
429        })
430    }
431
432    /// Forgets the saved state and the results of `popped` entries, and
433    /// returns whether there was one. Their view models drop once the host
434    /// has faded them out.
435    fn forget(&self, popped: impl IntoIterator<Item = Entry<R>>) -> bool {
436        let mut any = false;
437        for entry in popped {
438            entry.saveables.forget();
439            if self
440                .results
441                .try_with(|results| results.holds_any(entry.id))
442                .unwrap_or(false)
443            {
444                self.results
445                    .update(|results| results.0.retain(|(id, _)| *id != entry.id));
446            }
447            any = true;
448        }
449        any
450    }
451}
452
453/// One visit on a [`NavController`]'s back stack — Android's
454/// `NavBackStackEntry`.
455///
456/// Its route carries the screen's arguments. Other screens leave it values
457/// with [`Self::set_result`], as Android's `savedStateHandle` does, and its
458/// screen reads them with [`Self::result`]. A screen returns a result to the
459/// one beneath it this way:
460///
461/// ```
462/// use cranpose_navigation::NavController;
463///
464/// #[derive(Clone, PartialEq)]
465/// enum Screen {
466///     Home,
467///     PickColor,
468/// }
469///
470/// #[derive(Clone)]
471/// struct Picked(u32);
472///
473/// fn pick(nav: NavController<Screen>, color: u32) {
474///     if let Some(home) = nav.previous_back_stack_entry() {
475///         home.set_result(Picked(color));
476///     }
477///     nav.pop_back_stack();
478/// }
479///
480/// // In the home screen, while composing:
481/// fn picked(nav: NavController<Screen>) -> Option<u32> {
482///     let Picked(color) = nav.composing_entry()?.result::<Picked>()?;
483///     Some(color)
484/// }
485/// ```
486///
487/// The type of a value is its key, so an entry holds one value of each type;
488/// a newtype per result keeps two results of one type apart. Results live in
489/// memory: a popped entry drops them, and a restored back stack starts
490/// without them. A screen that shows a result for longer takes it into its
491/// own saved state, such as `rememberSaveable`.
492pub struct NavBackStackEntry<R: Clone + 'static> {
493    controller: NavController<R>,
494    id: u64,
495}
496
497impl<R: Clone + 'static> Clone for NavBackStackEntry<R> {
498    fn clone(&self) -> Self {
499        *self
500    }
501}
502
503impl<R: Clone + 'static> Copy for NavBackStackEntry<R> {}
504
505impl<R: Clone + 'static> PartialEq for NavBackStackEntry<R> {
506    fn eq(&self, other: &Self) -> bool {
507        self.controller == other.controller && self.id == other.id
508    }
509}
510
511impl<R: Clone + PartialEq + 'static> NavBackStackEntry<R> {
512    /// The entry's route, with the screen's arguments; `None` once it is
513    /// popped. It does not subscribe the caller: an entry keeps its route.
514    pub fn route(&self) -> Option<R> {
515        self.controller
516            .stack
517            .try_with(|stack| stack.find(self.id).map(|entry| entry.route.clone()))
518            .flatten()
519    }
520
521    /// The value of type `T` that a screen left for this entry. Reading it in
522    /// a composable recomposes that composable when the value changes —
523    /// Android's `savedStateHandle.getStateFlow(key)`.
524    pub fn result<T: Clone + 'static>(&self) -> Option<T> {
525        self.controller.results.read(|results| {
526            let index = results.position::<T>(self.id)?;
527            results.0[index].1.downcast_ref::<T>().cloned()
528        })
529    }
530
531    /// Leaves `value` for this entry, in place of its earlier value of type
532    /// `T` — Android's `savedStateHandle[key] = value`. Does nothing once the
533    /// entry is popped.
534    pub fn set_result<T: 'static>(&self, value: T) {
535        let live = self
536            .controller
537            .stack
538            .try_with(|stack| stack.find(self.id).is_some())
539            .unwrap_or(false);
540        if !live {
541            return;
542        }
543        let (id, value) = (self.id, Rc::new(value));
544        self.controller
545            .results
546            .update(|results| match results.position::<T>(id) {
547                Some(index) => results.0[index].1 = value,
548                None => results.0.push((id, value)),
549            });
550    }
551
552    /// Removes the value of type `T` and returns it — Android's
553    /// `savedStateHandle.remove(key)`. A screen calls it once it has acted on
554    /// a result, from an effect or a callback.
555    pub fn take_result<T: Clone + 'static>(&self) -> Option<T> {
556        let id = self.id;
557        self.controller
558            .results
559            .try_with(|results| results.position::<T>(id))
560            .flatten()?;
561        let value = self.controller.results.update(|results| {
562            let index = results.position::<T>(id)?;
563            Some(results.0.swap_remove(index).1)
564        })?;
565        Rc::downcast::<T>(value).ok().map(Rc::unwrap_or_clone)
566    }
567}
568
569struct NavControllerState<R: Clone + 'static> {
570    /// Declared first so that it drops first and reads the back stack one
571    /// last time.
572    _saved: Option<SaveableRegistration>,
573    stack: OwnedMutableState<BackStack<R>>,
574    results: OwnedMutableState<EntryResults>,
575}
576
577impl<R: Clone + 'static> NavControllerState<R> {
578    fn new(stack: OwnedMutableState<BackStack<R>>, saved: Option<SaveableRegistration>) -> Self {
579        Self {
580            _saved: saved,
581            stack,
582            results: ownedMutableStateOfNeverEqual(EntryResults::default()),
583        }
584    }
585
586    fn controller(&self) -> NavController<R> {
587        NavController {
588            stack: self.stack.handle(),
589            results: self.results.handle(),
590        }
591    }
592}
593
594/// A [`NavController`] whose back stack starts at `start` — Android's
595/// `rememberNavController()` plus `NavHost(startDestination)`.
596///
597/// The back stack lives in the nearest view model store, so a `NavHost`
598/// nested in a screen keeps its back stack while another screen covers that
599/// screen. It lives only in memory, and so does the `rememberSaveable` state
600/// of its screens: a covered screen gets its state back, and a popped one
601/// forgets it. [`rememberSaveableNavController`] also restores both after the
602/// process restarts.
603#[track_caller]
604pub fn rememberNavController<R: Clone + PartialEq + 'static>(start: R) -> NavController<R> {
605    let location = Location::caller();
606    let state = viewModel(location, |_| {
607        let saveables = SaveableScope::current().child(location).in_memory();
608        saveables.forget();
609        NavControllerState::new(
610            ownedMutableStateOfNeverEqual(BackStack::new(start, saveables)),
611            None,
612        )
613    });
614    state.get().controller()
615}
616
617/// A [`NavController`] whose back stack is saved under `key` at each save
618/// boundary, as `rememberSaveable` state is, and restored when it is next
619/// created, after a process restart too. `saver` gives each route its saved
620/// form; it may read the forms older builds wrote.
621///
622/// The `rememberSaveable` state of each screen is saved with it, apart from
623/// other visits of the same route. A popped screen forgets its state.
624/// Restoring keeps the entries beneath the first route `saver` cannot read;
625/// when it cannot read the bottom one, or nothing was saved, the back stack
626/// starts at `start`.
627#[track_caller]
628pub fn rememberSaveableNavController<R: Clone + PartialEq + 'static>(
629    key: &'static str,
630    start: R,
631    saver: Saver<R>,
632) -> NavController<R> {
633    let state = viewModel(Location::caller(), |_| {
634        let scope = SaveableScope::current();
635        let saveables = scope.child(key);
636        let restored = scope
637            .restore(key)
638            .and_then(|saved| saved_stack::decode(&saved, &saver, &saveables));
639        let stack = match restored {
640            Some(stack) => {
641                saveables.forget_children_except(
642                    stack
643                        .entries
644                        .iter()
645                        .chain(stack.saved.iter().flatten())
646                        .map(|entry| entry.id),
647                );
648                stack
649            }
650            None => {
651                saveables.forget();
652                BackStack::new(start, saveables)
653            }
654        };
655        let stack = ownedMutableStateOfNeverEqual(stack);
656        let handle = stack.handle();
657        let saved = scope.register(key, move || {
658            handle.try_with(|stack| saved_stack::encode(stack, &saver))
659        });
660        NavControllerState::new(stack, Some(saved))
661    });
662    state.get().controller()
663}