cranpose_navigation/
controller.rs1use std::panic::Location;
2
3use cranpose_core::{MutableState, OwnedMutableState, ownedMutableStateOfNeverEqual};
4use cranpose_coroflow::{ViewModelStore, viewModel};
5
6#[derive(Clone)]
11pub(crate) struct Entry<R> {
12 id: u64,
13 pub(crate) route: R,
14 pub(crate) store: ViewModelStore,
15}
16
17impl<R> PartialEq for Entry<R> {
18 fn eq(&self, other: &Self) -> bool {
19 self.id == other.id
20 }
21}
22
23#[derive(Clone)]
24pub(crate) struct BackStack<R> {
25 entries: Vec<Entry<R>>,
26 next_id: u64,
27}
28
29impl<R: Clone + PartialEq> BackStack<R> {
30 fn new(start: R) -> Self {
31 let mut stack = Self {
32 entries: Vec::new(),
33 next_id: 0,
34 };
35 stack.push(start);
36 stack
37 }
38
39 fn push(&mut self, route: R) {
40 self.entries.push(Entry {
41 id: self.next_id,
42 route,
43 store: ViewModelStore::default(),
44 });
45 self.next_id += 1;
46 }
47
48 fn split_above(&mut self, route: &R, inclusive: bool) -> Vec<Entry<R>> {
52 let Some(index) = self.entries.iter().rposition(|entry| entry.route == *route) else {
53 return Vec::new();
54 };
55 self.entries
56 .split_off(if inclusive { index } else { index + 1 })
57 }
58
59 fn top_route(&self) -> Option<&R> {
60 self.entries.last().map(|entry| &entry.route)
61 }
62
63 pub(crate) fn top(&self) -> Option<Entry<R>> {
64 self.entries.last().cloned()
65 }
66
67 pub(crate) fn can_pop(&self) -> bool {
68 self.entries.len() > 1
69 }
70}
71
72pub struct NavOptions<R> {
75 pop_up_to: Option<(R, bool)>,
76 launch_single_top: bool,
77}
78
79impl<R> Default for NavOptions<R> {
80 fn default() -> Self {
81 Self {
82 pop_up_to: None,
83 launch_single_top: false,
84 }
85 }
86}
87
88impl<R> NavOptions<R> {
89 pub fn new() -> Self {
91 Self::default()
92 }
93
94 #[must_use]
98 pub fn pop_up_to(mut self, route: R, inclusive: bool) -> Self {
99 self.pop_up_to = Some((route, inclusive));
100 self
101 }
102
103 #[must_use]
106 pub fn launch_single_top(mut self) -> Self {
107 self.launch_single_top = true;
108 self
109 }
110}
111
112pub struct NavController<R: Clone + 'static> {
119 pub(crate) stack: MutableState<BackStack<R>>,
120}
121
122impl<R: Clone + 'static> Clone for NavController<R> {
123 fn clone(&self) -> Self {
124 *self
125 }
126}
127
128impl<R: Clone + 'static> Copy for NavController<R> {}
129
130impl<R: Clone + 'static> PartialEq for NavController<R> {
131 fn eq(&self, other: &Self) -> bool {
132 self.stack == other.stack
133 }
134}
135
136impl<R: Clone + PartialEq + 'static> NavController<R> {
137 pub fn navigate(&self, route: R) {
139 self.navigate_with(route, NavOptions::new());
140 }
141
142 pub fn navigate_with(&self, route: R, options: NavOptions<R>) {
145 let popped = self.stack.update(|stack| {
146 let popped = options
147 .pop_up_to
148 .map(|(target, inclusive)| stack.split_above(&target, inclusive))
149 .unwrap_or_default();
150 if !(options.launch_single_top && stack.top_route() == Some(&route)) {
151 stack.push(route);
152 }
153 popped
154 });
155 drop(popped);
156 }
157
158 pub fn pop_back_stack(&self) -> bool {
161 let popped = self.stack.update(|stack| stack.entries.pop());
162 popped.is_some()
163 }
164
165 pub fn pop_back_stack_to(&self, route: &R, inclusive: bool) -> bool {
169 let popped = self
170 .stack
171 .update(|stack| stack.split_above(route, inclusive));
172 !popped.is_empty()
173 }
174
175 pub fn navigate_up(&self) -> bool {
178 let popped = self
179 .stack
180 .update(|stack| stack.can_pop().then(|| stack.entries.pop()).flatten());
181 popped.is_some()
182 }
183
184 pub fn current_route(&self) -> Option<R> {
188 self.stack.read(|stack| stack.top_route().cloned())
189 }
190
191 pub fn back_stack(&self) -> Vec<R> {
195 self.stack.read(|stack| {
196 stack
197 .entries
198 .iter()
199 .map(|entry| entry.route.clone())
200 .collect()
201 })
202 }
203}
204
205struct NavControllerState<R: Clone + 'static> {
206 stack: OwnedMutableState<BackStack<R>>,
207}
208
209#[track_caller]
216pub fn rememberNavController<R: Clone + PartialEq + 'static>(start: R) -> NavController<R> {
217 let state = viewModel(Location::caller(), |_| NavControllerState {
218 stack: ownedMutableStateOfNeverEqual(BackStack::new(start)),
219 });
220 NavController {
221 stack: state.get().stack.handle(),
222 }
223}