Skip to main content

cranpose_navigation/
host.rs

1use cranpose_animation::{AnimationType, Easing, tween};
2use cranpose_coroflow::ProvideViewModelStore;
3use cranpose_services::{BackGesture, PredictiveBackHandler, ProvideSaveableScope};
4use cranpose_ui::{ContentSeek, SeekableCrossfade, composable};
5
6use crate::controller::{Entry, NavController};
7
8/// How long Compose's `NavHost` fades between destinations.
9const DEFAULT_TRANSITION_MILLIS: u64 = 700;
10
11/// Shows the screen on top of `controller`'s back stack — Android's
12/// `NavHost`.
13///
14/// `content` receives the route and composes its screen, usually with a
15/// `match`, so the compiler checks that every route has one:
16///
17/// ```
18/// use cranpose_navigation::{NavHost, rememberNavController};
19///
20/// #[derive(Clone, PartialEq)]
21/// enum Screen {
22///     Home,
23///     Detail { id: u32 },
24/// }
25///
26/// # #[allow(non_snake_case)]
27/// fn App() {
28///     let nav = rememberNavController(Screen::Home);
29///     NavHost(nav, move |screen| match screen {
30///         Screen::Home => HomeScreen(move |id| nav.navigate(Screen::Detail { id })),
31///         Screen::Detail { id } => DetailScreen(id),
32///     });
33/// }
34/// # #[allow(non_snake_case)]
35/// # fn HomeScreen(_open: impl Fn(u32) + 'static) {}
36/// # #[allow(non_snake_case)]
37/// # fn DetailScreen(_id: u32) {}
38/// ```
39///
40/// Screens crossfade the way Compose's do. The platform back gesture pops the
41/// back stack while there is a screen beneath the top one. While an
42/// interactive back gesture moves (Android's predictive back, the iOS edge
43/// swipe), the screen beneath shows through as far as the gesture went; when
44/// the user lets go, the fade finishes or goes back from there. Every entry has
45/// its own view model store: `viewModel` in a screen finds the screen's view
46/// models, which are kept while other screens cover it and dropped once it is
47/// popped and has faded out. Every entry also has its own
48/// [`SaveableScope`](cranpose_services::SaveableScope), so `rememberSaveable`
49/// in a screen keeps that visit's state while it is covered and forgets it
50/// once it is popped.
51#[composable(no_skip)]
52pub fn NavHost<R, F>(controller: NavController<R>, content: F)
53where
54    R: Clone + PartialEq + 'static,
55    F: FnMut(R) + 'static,
56{
57    NavHostWith(
58        controller,
59        tween(DEFAULT_TRANSITION_MILLIS, Easing::FastOutSlowInEasing),
60        content,
61    );
62}
63
64/// [`NavHost`] with its own `transition` between screens — Compose's
65/// `NavHost(enterTransition, exitTransition)` for a fade.
66#[composable(no_skip)]
67pub fn NavHostWith<R, F>(controller: NavController<R>, transition: AnimationType, mut content: F)
68where
69    R: Clone + PartialEq + 'static,
70    F: FnMut(R) + 'static,
71{
72    let (top, beneath, can_pop) = controller
73        .stack
74        .read(|stack| (stack.top(), stack.beneath_top(), stack.can_pop()));
75    let gesture = cranpose_core::rememberMutableStateOf(|| None::<f32>);
76    PredictiveBackHandler(
77        can_pop,
78        move |step| {
79            gesture.set(match step {
80                BackGesture::Started(event) | BackGesture::Progressed(event) => {
81                    Some(event.progress)
82                }
83                BackGesture::Cancelled => None,
84            });
85        },
86        move || {
87            gesture.set(None);
88            controller.navigate_up();
89        },
90    );
91    let seek = gesture
92        .get()
93        .zip(beneath)
94        .map(|(fraction, beneath)| ContentSeek {
95            toward: Some(beneath),
96            fraction,
97        });
98    SeekableCrossfade(top, seek, transition, move |entry| {
99        if let Some(Entry {
100            route,
101            store,
102            saveables,
103            ..
104        }) = entry
105        {
106            ProvideSaveableScope(saveables, || {
107                ProvideViewModelStore(store, || content(route));
108            });
109        }
110    });
111}