use std::panic::Location;
use cranpose_core::{MutableState, OwnedMutableState, ownedMutableStateOfNeverEqual};
use cranpose_coroflow::{ViewModelStore, viewModel};
#[derive(Clone)]
pub(crate) struct Entry<R> {
id: u64,
pub(crate) route: R,
pub(crate) store: ViewModelStore,
}
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,
}
impl<R: Clone + PartialEq> BackStack<R> {
fn new(start: R) -> Self {
let mut stack = Self {
entries: Vec::new(),
next_id: 0,
};
stack.push(start);
stack
}
fn push(&mut self, route: R) {
self.entries.push(Entry {
id: self.next_id,
route,
store: ViewModelStore::default(),
});
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 can_pop(&self) -> bool {
self.entries.len() > 1
}
}
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
});
drop(popped);
}
pub fn pop_back_stack(&self) -> bool {
let popped = self.stack.update(|stack| stack.entries.pop());
popped.is_some()
}
pub fn pop_back_stack_to(&self, route: &R, inclusive: bool) -> bool {
let popped = self
.stack
.update(|stack| stack.split_above(route, inclusive));
!popped.is_empty()
}
pub fn navigate_up(&self) -> bool {
let popped = self
.stack
.update(|stack| stack.can_pop().then(|| stack.entries.pop()).flatten());
popped.is_some()
}
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> {
stack: OwnedMutableState<BackStack<R>>,
}
#[track_caller]
pub fn rememberNavController<R: Clone + PartialEq + 'static>(start: R) -> NavController<R> {
let state = viewModel(Location::caller(), |_| NavControllerState {
stack: ownedMutableStateOfNeverEqual(BackStack::new(start)),
});
NavController {
stack: state.get().stack.handle(),
}
}