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