cranpose_navigation/
controller.rs1use 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#[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 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 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 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
96fn 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
107pub 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 pub fn new() -> Self {
126 Self::default()
127 }
128
129 #[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 #[must_use]
141 pub fn launch_single_top(mut self) -> Self {
142 self.launch_single_top = true;
143 self
144 }
145}
146
147pub 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 pub fn navigate(&self, route: R) {
174 self.navigate_with(route, NavOptions::new());
175 }
176
177 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 pub fn pop_back_stack(&self) -> bool {
196 forget(self.stack.update(|stack| stack.entries.pop()))
197 }
198
199 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 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 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 pub fn current_route(&self) -> Option<R> {
230 self.stack.read(|stack| stack.top_route().cloned())
231 }
232
233 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 _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#[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#[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}