use std::{any::Any, panic::Location, rc::Rc};
use cranpose_core::{MutableState, OwnedMutableState, ownedMutableStateOfNeverEqual};
use cranpose_coroflow::{ViewModelStore, viewModel};
use cranpose_services::{SaveableRegistration, SaveableScope, Saver};
mod saved_stack;
#[derive(Clone)]
pub(crate) struct Entry<R> {
id: u64,
pub(crate) route: R,
pub(crate) store: ViewModelStore,
pub(crate) saveables: SaveableScope,
}
impl<R> Entry<R> {
fn new(id: u64, route: R, stack: &SaveableScope) -> Self {
Self {
id,
route,
store: ViewModelStore::default(),
saveables: stack.child(id),
}
}
}
impl<R> Entry<R> {
pub(crate) fn id(&self) -> u64 {
self.id
}
fn encloses(&self, scope: &SaveableScope) -> bool {
let (Some(own), Some(inner)) = (self.saveables.namespace(), scope.namespace()) else {
return false;
};
inner
.strip_prefix(own)
.is_some_and(|rest| rest.is_empty() || rest.starts_with('/'))
}
}
impl<R> PartialEq for Entry<R> {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
#[derive(Clone)]
pub(crate) struct BackStack<R> {
entries: Vec<Entry<R>>,
saved: Vec<Vec<Entry<R>>>,
next_id: u64,
saveables: SaveableScope,
}
impl<R: Clone + PartialEq> BackStack<R> {
fn new(start: R, saveables: SaveableScope) -> Self {
let mut stack = Self {
entries: Vec::new(),
saved: Vec::new(),
next_id: 0,
saveables,
};
stack.push(start);
stack
}
fn push(&mut self, route: R) {
self.entries
.push(Entry::new(self.next_id, route, &self.saveables));
self.next_id += 1;
}
fn split_above(&mut self, route: &R, inclusive: bool) -> Vec<Entry<R>> {
let Some(index) = self.entries.iter().rposition(|entry| entry.route == *route) else {
return Vec::new();
};
self.entries
.split_off(if inclusive { index } else { index + 1 })
}
fn keep(&mut self, popped: Vec<Entry<R>>) -> Vec<Entry<R>> {
let (replaced, kept) = std::mem::take(&mut self.saved)
.into_iter()
.partition::<Vec<_>, _>(|segment| {
segment
.iter()
.any(|entry| popped.iter().any(|new| new.route == entry.route))
});
self.saved = kept;
self.saved.push(popped);
let replaced: Vec<Entry<R>> = replaced.into_iter().flatten().collect();
for entry in &replaced {
entry.store.clear();
}
replaced
}
fn take_saved(&mut self, route: &R) -> Option<Vec<Entry<R>>> {
let index = self
.saved
.iter()
.position(|segment| segment.iter().any(|entry| entry.route == *route))?;
Some(self.saved.swap_remove(index))
}
fn top_route(&self) -> Option<&R> {
self.entries.last().map(|entry| &entry.route)
}
fn find(&self, id: u64) -> Option<&Entry<R>> {
self.entries
.iter()
.chain(self.saved.iter().flatten())
.find(|entry| entry.id == id)
}
pub(crate) fn can_pop(&self) -> bool {
self.entries.len() > 1
}
pub(crate) fn screens_and_dialogs<S>(
&self,
dialog: impl Fn(&R) -> Option<S>,
) -> HostView<R, S> {
let is_screen = |entry: &Entry<R>| dialog(&entry.route).is_none();
let at = self.entries.iter().rposition(is_screen);
let toward = at
.and_then(|at| self.entries[..at].iter().rposition(is_screen))
.map(|index| self.entries[index].clone());
let from = at.map_or(0, |at| at + 1);
let dialogs = self.entries[from..]
.iter()
.enumerate()
.filter_map(|(offset, entry)| {
dialog(&entry.route).map(|spec| (from + offset, entry.clone(), spec))
})
.collect();
HostView {
screen: at.map(|at| self.entries[at].clone()),
toward,
dialogs,
}
}
}
pub(crate) struct HostView<R, S> {
pub(crate) screen: Option<Entry<R>>,
pub(crate) toward: Option<Entry<R>>,
pub(crate) dialogs: Vec<(usize, Entry<R>, S)>,
}
#[derive(Clone, Default)]
pub(crate) struct EntryResults(Vec<(u64, Rc<dyn Any>)>);
impl EntryResults {
fn position<T: 'static>(&self, entry: u64) -> Option<usize> {
self.0
.iter()
.position(|(id, value)| *id == entry && value.is::<T>())
}
fn holds_any(&self, entry: u64) -> bool {
self.0.iter().any(|(id, _)| *id == entry)
}
}
pub struct NavOptions<R> {
pop_up_to: Option<(R, bool)>,
launch_single_top: bool,
save_state: bool,
restore_state: bool,
}
impl<R> Default for NavOptions<R> {
fn default() -> Self {
Self {
pop_up_to: None,
launch_single_top: false,
save_state: false,
restore_state: false,
}
}
}
impl<R> NavOptions<R> {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn pop_up_to(mut self, route: R, inclusive: bool) -> Self {
self.pop_up_to = Some((route, inclusive));
self
}
#[must_use]
pub fn launch_single_top(mut self) -> Self {
self.launch_single_top = true;
self
}
#[must_use]
pub fn save_state(mut self) -> Self {
self.save_state = true;
self
}
#[must_use]
pub fn restore_state(mut self) -> Self {
self.restore_state = true;
self
}
}
pub struct NavController<R: Clone + 'static> {
pub(crate) stack: MutableState<BackStack<R>>,
results: MutableState<EntryResults>,
}
impl<R: Clone + 'static> Clone for NavController<R> {
fn clone(&self) -> Self {
*self
}
}
impl<R: Clone + 'static> Copy for NavController<R> {}
impl<R: Clone + 'static> PartialEq for NavController<R> {
fn eq(&self, other: &Self) -> bool {
self.stack == other.stack
}
}
impl<R: Clone + PartialEq + 'static> NavController<R> {
pub fn navigate(&self, route: R) {
self.navigate_with(route, NavOptions::new());
}
pub fn navigate_with(&self, route: R, options: NavOptions<R>) {
let forgotten = self.stack.update(|stack| {
let popped = options
.pop_up_to
.map(|(target, inclusive)| stack.split_above(&target, inclusive))
.unwrap_or_default();
let forgotten = if options.save_state && !popped.is_empty() {
stack.keep(popped)
} else {
popped
};
let restored = options
.restore_state
.then(|| stack.take_saved(&route))
.flatten();
if let Some(restored) = restored {
stack.entries.extend(restored);
} else if !(options.launch_single_top && stack.top_route() == Some(&route)) {
stack.push(route);
}
forgotten
});
self.forget(forgotten);
}
pub fn pop_back_stack(&self) -> bool {
self.forget(self.stack.update(|stack| stack.entries.pop()))
}
pub fn pop_back_stack_to(&self, route: &R, inclusive: bool) -> bool {
self.forget(
self.stack
.update(|stack| stack.split_above(route, inclusive)),
)
}
pub(crate) fn pop_to_depth(&self, depth: usize) {
self.forget(self.stack.update(|stack| {
let keep = depth.min(stack.entries.len());
stack.entries.split_off(keep)
}));
}
pub(crate) fn open_deep_link(&self, route: R) {
self.forget(self.stack.update(|stack| {
let popped = stack.entries.split_off(stack.entries.len().min(1));
if stack.top_route() != Some(&route) {
stack.push(route);
}
popped
}));
}
pub fn navigate_up(&self) -> bool {
self.forget(
self.stack
.update(|stack| stack.can_pop().then(|| stack.entries.pop()).flatten()),
)
}
pub fn current_route(&self) -> Option<R> {
self.stack.read(|stack| stack.top_route().cloned())
}
pub fn back_stack(&self) -> Vec<R> {
self.stack.read(|stack| {
stack
.entries
.iter()
.map(|entry| entry.route.clone())
.collect()
})
}
pub fn composing_entry(&self) -> Option<NavBackStackEntry<R>> {
let scope = SaveableScope::current();
self.entry_where(|entries| entries.iter().find(|entry| entry.encloses(&scope)))
}
pub fn previous_back_stack_entry(&self) -> Option<NavBackStackEntry<R>> {
self.entry_where(|entries| entries.iter().nth_back(1))
}
pub fn back_stack_entry(&self, route: &R) -> Option<NavBackStackEntry<R>> {
self.entry_where(|entries| entries.iter().rfind(|entry| entry.route == *route))
}
fn entry_where(
&self,
find: impl FnOnce(&[Entry<R>]) -> Option<&Entry<R>>,
) -> Option<NavBackStackEntry<R>> {
let id = self
.stack
.try_with(|stack| find(&stack.entries).map(Entry::id))
.flatten()?;
Some(NavBackStackEntry {
controller: *self,
id,
})
}
fn forget(&self, popped: impl IntoIterator<Item = Entry<R>>) -> bool {
let mut any = false;
for entry in popped {
entry.saveables.forget();
if self
.results
.try_with(|results| results.holds_any(entry.id))
.unwrap_or(false)
{
self.results
.update(|results| results.0.retain(|(id, _)| *id != entry.id));
}
any = true;
}
any
}
}
pub struct NavBackStackEntry<R: Clone + 'static> {
controller: NavController<R>,
id: u64,
}
impl<R: Clone + 'static> Clone for NavBackStackEntry<R> {
fn clone(&self) -> Self {
*self
}
}
impl<R: Clone + 'static> Copy for NavBackStackEntry<R> {}
impl<R: Clone + 'static> PartialEq for NavBackStackEntry<R> {
fn eq(&self, other: &Self) -> bool {
self.controller == other.controller && self.id == other.id
}
}
impl<R: Clone + PartialEq + 'static> NavBackStackEntry<R> {
pub fn route(&self) -> Option<R> {
self.controller
.stack
.try_with(|stack| stack.find(self.id).map(|entry| entry.route.clone()))
.flatten()
}
pub fn result<T: Clone + 'static>(&self) -> Option<T> {
self.controller.results.read(|results| {
let index = results.position::<T>(self.id)?;
results.0[index].1.downcast_ref::<T>().cloned()
})
}
pub fn set_result<T: 'static>(&self, value: T) {
let live = self
.controller
.stack
.try_with(|stack| stack.find(self.id).is_some())
.unwrap_or(false);
if !live {
return;
}
let (id, value) = (self.id, Rc::new(value));
self.controller
.results
.update(|results| match results.position::<T>(id) {
Some(index) => results.0[index].1 = value,
None => results.0.push((id, value)),
});
}
pub fn take_result<T: Clone + 'static>(&self) -> Option<T> {
let id = self.id;
self.controller
.results
.try_with(|results| results.position::<T>(id))
.flatten()?;
let value = self.controller.results.update(|results| {
let index = results.position::<T>(id)?;
Some(results.0.swap_remove(index).1)
})?;
Rc::downcast::<T>(value).ok().map(Rc::unwrap_or_clone)
}
}
struct NavControllerState<R: Clone + 'static> {
_saved: Option<SaveableRegistration>,
stack: OwnedMutableState<BackStack<R>>,
results: OwnedMutableState<EntryResults>,
}
impl<R: Clone + 'static> NavControllerState<R> {
fn new(stack: OwnedMutableState<BackStack<R>>, saved: Option<SaveableRegistration>) -> Self {
Self {
_saved: saved,
stack,
results: ownedMutableStateOfNeverEqual(EntryResults::default()),
}
}
fn controller(&self) -> NavController<R> {
NavController {
stack: self.stack.handle(),
results: self.results.handle(),
}
}
}
#[track_caller]
pub fn rememberNavController<R: Clone + PartialEq + 'static>(start: R) -> NavController<R> {
let location = Location::caller();
let state = viewModel(location, |_| {
let saveables = SaveableScope::current().child(location).in_memory();
saveables.forget();
NavControllerState::new(
ownedMutableStateOfNeverEqual(BackStack::new(start, saveables)),
None,
)
});
state.get().controller()
}
#[track_caller]
pub fn rememberSaveableNavController<R: Clone + PartialEq + 'static>(
key: &'static str,
start: R,
saver: Saver<R>,
) -> NavController<R> {
let state = viewModel(Location::caller(), |_| {
let scope = SaveableScope::current();
let saveables = scope.child(key);
let restored = scope
.restore(key)
.and_then(|saved| saved_stack::decode(&saved, &saver, &saveables));
let stack = match restored {
Some(stack) => {
saveables.forget_children_except(
stack
.entries
.iter()
.chain(stack.saved.iter().flatten())
.map(|entry| entry.id),
);
stack
}
None => {
saveables.forget();
BackStack::new(start, saveables)
}
};
let stack = ownedMutableStateOfNeverEqual(stack);
let handle = stack.handle();
let saved = scope.register(key, move || {
handle.try_with(|stack| saved_stack::encode(stack, &saver))
});
NavControllerState::new(stack, Some(saved))
});
state.get().controller()
}