use std::panic::Location;
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> 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>>,
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(),
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 top_route(&self) -> Option<&R> {
self.entries.last().map(|entry| &entry.route)
}
pub(crate) fn top(&self) -> Option<Entry<R>> {
self.entries.last().cloned()
}
pub(crate) fn beneath_top(&self) -> Option<Entry<R>> {
let index = self.entries.len().checked_sub(2)?;
self.entries.get(index).cloned()
}
pub(crate) fn can_pop(&self) -> bool {
self.entries.len() > 1
}
}
fn forget<R>(popped: impl IntoIterator<Item = Entry<R>>) -> bool {
let mut any = false;
for entry in popped {
entry.saveables.forget();
any = true;
}
any
}
pub struct NavOptions<R> {
pop_up_to: Option<(R, bool)>,
launch_single_top: bool,
}
impl<R> Default for NavOptions<R> {
fn default() -> Self {
Self {
pop_up_to: None,
launch_single_top: 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
}
}
pub struct NavController<R: Clone + 'static> {
pub(crate) stack: MutableState<BackStack<R>>,
}
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 popped = self.stack.update(|stack| {
let popped = options
.pop_up_to
.map(|(target, inclusive)| stack.split_above(&target, inclusive))
.unwrap_or_default();
if !(options.launch_single_top && stack.top_route() == Some(&route)) {
stack.push(route);
}
popped
});
forget(popped);
}
pub fn pop_back_stack(&self) -> bool {
forget(self.stack.update(|stack| stack.entries.pop()))
}
pub fn pop_back_stack_to(&self, route: &R, inclusive: bool) -> bool {
forget(
self.stack
.update(|stack| stack.split_above(route, inclusive)),
)
}
pub(crate) fn pop_to_depth(&self, depth: usize) {
forget(self.stack.update(|stack| {
let keep = depth.min(stack.entries.len());
stack.entries.split_off(keep)
}));
}
pub fn navigate_up(&self) -> bool {
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()
})
}
}
struct NavControllerState<R: Clone + 'static> {
_saved: Option<SaveableRegistration>,
stack: OwnedMutableState<BackStack<R>>,
}
impl<R: Clone + 'static> NavControllerState<R> {
fn controller(&self) -> NavController<R> {
NavController {
stack: self.stack.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 {
_saved: None,
stack: ownedMutableStateOfNeverEqual(BackStack::new(start, saveables)),
}
});
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().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 {
_saved: Some(saved),
stack,
}
});
state.get().controller()
}