use std::cell::{Cell, RefCell};
use std::rc::Rc;
use frust_core::{BuildCtx, ChangeFlags, View};
use frust_reactive::{CanPopRegistration, back_presses, set_can_pop_provider, set_handles_back};
use frust_widgets::{NavigatorController, NavigatorId};
use reactive_graph::traits::{Get, GetUntracked};
thread_local! {
static NEXT_FALLBACK_ID: Cell<u64> = const { Cell::new(0) };
}
thread_local! {
static SHARED: RefCell<SharedBack> = const { RefCell::new(SharedBack::new()) };
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
enum Role {
Host,
Navigator,
Fallback,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum RegistrantId {
Navigator(NavigatorId),
Fallback(u64),
}
struct Registrant {
id: RegistrantId,
role: Role,
birth: u64,
seq: u64,
mounted: Box<dyn Fn() -> bool>,
interest: Rc<dyn Fn() -> bool>,
request_back: Rc<dyn Fn()>,
}
impl Registrant {
fn key(&self) -> (Role, u64) {
match self.role {
Role::Host | Role::Fallback => (self.role, self.birth),
Role::Navigator => (self.role, u64::MAX - self.birth),
}
}
}
struct SharedBack {
consumed: Option<u64>,
registrants: Vec<Registrant>,
seq: u64,
provider: Option<CanPopRegistration>,
}
impl SharedBack {
const fn new() -> Self {
Self {
consumed: None,
registrants: Vec::new(),
seq: 0,
provider: None,
}
}
}
fn any_interest() -> bool {
interest_probes().iter().any(|interest| interest())
}
fn interest_probes() -> Vec<Rc<dyn Fn() -> bool>> {
SHARED.with(|shared| {
shared
.borrow()
.registrants
.iter()
.map(|r| Rc::clone(&r.interest))
.collect()
})
}
fn refresh_interest<State: 'static>(controller: &NavigatorController<State>, role: Role) {
let mounted_probe = controller.clone();
let interest_probe = controller.clone();
let request_probe = controller.clone();
register(
RegistrantId::Navigator(controller.id()),
role,
Box::new(move || mounted_probe.is_mounted()),
Rc::new(move || interest_probe.back_interest()),
Rc::new(move || request_probe.request_back()),
);
}
fn register(
id: RegistrantId,
role: Role,
mounted: Box<dyn Fn() -> bool>,
interest: Rc<dyn Fn() -> bool>,
request_back: Rc<dyn Fn()>,
) {
SHARED.with(|shared| {
let mut shared = shared.borrow_mut();
shared.seq += 1;
let seq = shared.seq;
let (role, birth) = match shared.registrants.iter().position(|r| r.id == id) {
Some(index) => {
let previous = shared.registrants[index].seq;
let carried = (
shared.registrants[index].role.min(role),
shared.registrants[index].birth,
);
shared
.registrants
.retain(|r| r.id == id || r.seq >= previous || (r.mounted)());
carried
}
None => (role, seq),
};
let entry = Registrant {
id,
role,
birth,
seq,
mounted,
interest,
request_back,
};
shared.registrants.retain(|r| r.id != id);
let at = shared
.registrants
.iter()
.position(|r| r.key() > entry.key())
.unwrap_or(shared.registrants.len());
shared.registrants.insert(at, entry);
if shared.provider.is_none() {
shared.provider = Some(set_can_pop_provider(Box::new(any_interest)));
}
});
set_handles_back(any_interest());
}
fn route_back() {
let claimant = SHARED
.with(|shared| {
shared
.borrow()
.registrants
.iter()
.map(|r| (Rc::clone(&r.interest), Rc::clone(&r.request_back)))
.collect::<Vec<_>>()
})
.into_iter()
.find(|(interest, _)| interest())
.map(|(_, request_back)| request_back);
if let Some(request_back) = claimant {
request_back();
}
}
fn consume_press() -> bool {
let count = back_presses().count.get();
SHARED.with(|shared| {
let mut shared = shared.borrow_mut();
match shared.consumed {
None => {
shared.consumed = Some(count);
false
}
Some(prev) if prev == count => false,
Some(_) => {
shared.consumed = Some(count);
true
}
}
})
}
fn consume_back() {
if consume_press() {
route_back();
}
}
fn seed_consumed() {
let count = back_presses().count.get_untracked();
SHARED.with(|shared| {
let mut shared = shared.borrow_mut();
if shared.consumed.is_none() {
shared.consumed = Some(count);
}
});
}
pub(crate) fn auto_wire<State: 'static>(controller: &NavigatorController<State>) {
consume_back();
refresh_interest(controller, Role::Navigator);
}
pub(crate) fn auto_wire_overlay_host<State: 'static>(controller: &NavigatorController<State>) {
consume_back();
refresh_interest(controller, Role::Host);
}
pub struct BackHandler<State: 'static> {
controller: NavigatorController<State>,
}
impl<State: 'static> BackHandler<State> {
pub fn new(controller: NavigatorController<State>) -> Self {
seed_consumed();
refresh_interest(&controller, Role::Navigator);
Self { controller }
}
pub fn track(&self) {
consume_back();
refresh_interest(&self.controller, Role::Navigator);
}
pub fn controller(&self) -> &NavigatorController<State> {
&self.controller
}
}
pub fn attach_back_handler<State: 'static>(
controller: NavigatorController<State>,
) -> BackHandler<State> {
BackHandler::new(controller)
}
#[derive(Clone)]
pub struct BackFallback {
inner: Rc<FallbackInner>,
}
struct FallbackInner {
id: u64,
mounted: Cell<usize>,
awaiting_build: Cell<bool>,
interest: Box<dyn Fn() -> bool>,
on_back: Box<dyn Fn()>,
}
impl FallbackInner {
fn live(&self) -> bool {
self.mounted.get() > 0 || self.awaiting_build.get()
}
fn reconciled(&self) {
self.awaiting_build.set(false);
}
}
impl BackFallback {
pub fn new(interest: impl Fn() -> bool + 'static, on_back: impl Fn() + 'static) -> Self {
seed_consumed();
let id = NEXT_FALLBACK_ID.with(|next| {
let id = next.get();
next.set(id + 1);
id
});
Self {
inner: Rc::new(FallbackInner {
id,
mounted: Cell::new(0),
awaiting_build: Cell::new(false),
interest: Box::new(interest),
on_back: Box::new(on_back),
}),
}
}
#[cfg(test)]
fn is_live(&self) -> bool {
self.inner.live()
}
fn wire(&self) {
self.inner.awaiting_build.set(true);
let mounted_probe = Rc::downgrade(&self.inner);
let interest_probe = Rc::downgrade(&self.inner);
let request_probe = Rc::downgrade(&self.inner);
register(
RegistrantId::Fallback(self.inner.id),
Role::Fallback,
Box::new(move || mounted_probe.upgrade().is_some_and(|inner| inner.live())),
Rc::new(move || {
interest_probe
.upgrade()
.is_some_and(|inner| inner.live() && (inner.interest)())
}),
Rc::new(move || {
if let Some(inner) = request_probe.upgrade() {
(inner.on_back)();
}
}),
);
}
}
pub fn back_fallback<V>(fallback: &BackFallback, child: V) -> BackFallbackView<V> {
if consume_press() {
route_back();
}
fallback.wire();
BackFallbackView {
fallback: fallback.clone(),
child,
}
}
pub struct BackFallbackView<V> {
fallback: BackFallback,
child: V,
}
impl<State: 'static, V: View<State>> View<State> for BackFallbackView<V> {
type Element = V::Element;
fn build(&self, ctx: &mut BuildCtx<'_>) -> Self::Element {
let inner = &self.fallback.inner;
inner.mounted.set(inner.mounted.get() + 1);
inner.reconciled();
self.child.build(ctx)
}
fn rebuild(
&self,
prev: &Self,
element: &mut Self::Element,
ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
let (next, old) = (&self.fallback.inner, &prev.fallback.inner);
if !Rc::ptr_eq(next, old) {
next.mounted.set(next.mounted.get() + 1);
old.mounted.set(old.mounted.get().saturating_sub(1));
old.reconciled();
}
next.reconciled();
self.child.rebuild(&prev.child, element, ctx)
}
fn teardown(&self, element: &mut Self::Element, ctx: &mut BuildCtx<'_>) {
self.child.teardown(element, ctx);
let inner = &self.fallback.inner;
inner.mounted.set(inner.mounted.get().saturating_sub(1));
inner.reconciled();
}
fn message(&self, element: &mut Self::Element, state: &mut State) {
self.child.message(element, state);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::motion::patterns::SharedAxis;
use crate::motion::switcher::PatternSwitcherView;
use frust_core::{AnyView, RenderRoot, any};
use frust_reactive::{
ReactiveRuntime, clear_can_pop_provider, handles_back, push_back_press, set_handles_back,
};
use frust_widgets::{
BackPolicy, NavigatorController, NavigatorView, PushOptions, navigator as raw_navigator,
overlay_host as raw_overlay_host,
};
use std::sync::{Arc, Mutex};
static TEST_LOCK: Mutex<()> = Mutex::new(());
fn reset() {
clear_can_pop_provider();
set_handles_back(false);
SHARED.with(|shared| *shared.borrow_mut() = SharedBack::new());
}
fn page() -> AnyView<()> {
any(frust_widgets::text("x"))
}
type AppLogic = Box<dyn FnMut(&mut ()) -> NavigatorView<()>>;
struct Harness {
controller: NavigatorController<()>,
root: RenderRoot<(), NavigatorView<()>>,
app: AppLogic,
wire: Box<dyn FnMut()>,
}
impl Harness {
fn new(wire: impl FnMut(&NavigatorController<()>) + 'static) -> Self {
Self::with_controller(NavigatorController::new(), wire)
}
fn with_controller(
controller: NavigatorController<()>,
mut wire: impl FnMut(&NavigatorController<()>) + 'static,
) -> Self {
let app_controller = controller.clone();
let app: AppLogic = Box::new(move |_: &mut ()| raw_navigator(&app_controller, page));
let wire_controller = controller.clone();
let wire: Box<dyn FnMut()> = Box::new(move || wire(&wire_controller));
let mut harness = Harness {
controller,
root: RenderRoot::new(),
app,
wire,
};
harness.rebuild();
harness
}
fn rebuild(&mut self) {
(self.wire)();
self.root.rebuild(&mut self.app, &mut ());
}
fn depth(&self) -> usize {
self.controller.depth()
}
}
#[test]
fn auto_wire_pops_without_a_backhandler() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let mut h = Harness::new(auto_wire);
assert_eq!(h.depth(), 1, "starts at the root page");
assert!(!handles_back(), "depth 1 + no overlay: handles_back false");
h.controller.push(page);
h.rebuild();
assert_eq!(h.depth(), 2, "pushed to depth 2");
assert!(
handles_back(),
"poppable stack: handles_back true, same rebuild"
);
push_back_press();
h.rebuild();
assert_eq!(h.depth(), 1, "auto-wired back press pops one page");
assert!(
!handles_back(),
"back at the root: handles_back false again"
);
}
#[test]
fn veto_overlay_claims_back_but_does_not_pop() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let mut h = Harness::new(auto_wire);
h.controller
.push_with_options(page, PushOptions::transparent().back(BackPolicy::Veto));
h.rebuild();
assert!(handles_back(), "a Veto overlay claims the back press");
let before = h.depth();
push_back_press();
h.rebuild();
assert_eq!(
h.depth(),
before,
"Veto consumes the press via request_back: stack unchanged (not popped)"
);
assert!(handles_back(), "still claiming back (overlay still on top)");
}
#[test]
fn explicit_backhandler_pops_and_tracks_handles_back() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let controller: NavigatorController<()> = NavigatorController::new();
let back = BackHandler::new(controller.clone());
let mut h = Harness::with_controller(controller, move |_c| back.track());
assert_eq!(h.depth(), 1, "starts at the root");
assert!(
!handles_back(),
"root-level back bubbles: handles_back false"
);
h.controller.push(page);
h.rebuild();
assert_eq!(h.depth(), 2, "push published to depth immediately");
assert!(
handles_back(),
"poppable stack reports handles_back true on the SAME rebuild, no settle frame"
);
push_back_press();
h.rebuild();
assert_eq!(h.depth(), 1, "one back press pops one page");
assert!(
!handles_back(),
"root again: handles_back false immediately"
);
push_back_press();
h.rebuild();
assert_eq!(h.depth(), 1, "pop-at-root is a no-op");
clear_can_pop_provider();
assert!(
!handles_back(),
"after clear, handles_back reads the polled fallback"
);
}
#[test]
fn manual_backhandler_plus_auto_wire_pops_once() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let controller: NavigatorController<()> = NavigatorController::new();
let back = BackHandler::new(controller.clone());
let mut h = Harness::with_controller(controller, move |c| {
back.track();
auto_wire(c);
});
h.controller.push(page);
h.rebuild();
h.controller.push(page);
h.rebuild();
assert_eq!(h.depth(), 3, "pushed to depth 3");
push_back_press();
h.rebuild();
assert_eq!(
h.depth(),
2,
"manual + auto share one consumption source: one press pops once"
);
h.rebuild();
assert_eq!(h.depth(), 2, "no new press -> no extra pop");
}
struct HostHarness {
host: NavigatorController<()>,
inner: NavigatorController<()>,
root: RenderRoot<(), NavigatorView<()>>,
app: AppLogic,
}
impl HostHarness {
fn new() -> Self {
let host: NavigatorController<()> = NavigatorController::new();
let inner: NavigatorController<()> = NavigatorController::new();
let app: AppLogic = {
let host = host.clone();
let inner = inner.clone();
Box::new(move |_: &mut ()| {
auto_wire_overlay_host(&host);
let inner = inner.clone();
raw_overlay_host(&host, move || {
auto_wire(&inner);
any(raw_navigator(&inner, page))
})
})
};
let mut harness = Self {
host,
inner,
root: RenderRoot::new(),
app,
};
harness.rebuild();
harness
}
fn rebuild(&mut self) {
self.root.rebuild(&mut self.app, &mut ());
}
}
fn registrant_ids() -> Vec<frust_widgets::NavigatorId> {
registrants()
.into_iter()
.filter_map(|id| match id {
RegistrantId::Navigator(id) => Some(id),
RegistrantId::Fallback(_) => None,
})
.collect()
}
fn registrants() -> Vec<RegistrantId> {
SHARED.with(|shared| shared.borrow().registrants.iter().map(|r| r.id).collect())
}
#[test]
fn a_root_overlay_takes_the_back_press_from_the_inner_navigator() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let mut h = HostHarness::new();
assert_eq!(
registrant_ids(),
vec![h.host.id(), h.inner.id()],
"outermost first"
);
h.inner.push(page);
h.rebuild();
h.host
.push_with_options(page, PushOptions::transparent().back(BackPolicy::Pop));
h.rebuild();
assert_eq!((h.host.depth(), h.inner.depth()), (2, 2));
assert!(handles_back(), "some registrant claims the press");
push_back_press();
h.rebuild();
assert_eq!(h.host.depth(), 1, "the press went to the root overlay host");
assert_eq!(
h.inner.depth(),
2,
"and NOT to the inner navigator (the root-modal back bug)"
);
}
#[test]
fn a_host_with_no_overlay_defers_the_back_press_to_the_inner_navigator() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let mut h = HostHarness::new();
assert!(
!handles_back(),
"two navigators, both at the root: back bubbles to the platform"
);
h.inner.push(page);
h.rebuild();
assert_eq!(h.host.depth(), 1, "the host stays empty");
assert!(
handles_back(),
"the inner navigator's interest answers for the whole app"
);
push_back_press();
h.rebuild();
assert_eq!(h.inner.depth(), 1, "the press popped the inner navigator");
assert_eq!(h.host.depth(), 1, "the empty host was untouched");
assert!(!handles_back(), "back at the root of both: bubbles again");
push_back_press();
h.rebuild();
assert_eq!((h.host.depth(), h.inner.depth()), (1, 1));
}
#[test]
fn a_root_overlay_claims_back_even_with_the_inner_navigator_at_its_root() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let mut h = HostHarness::new();
h.host
.push_with_options(page, PushOptions::transparent().back(BackPolicy::Pop));
h.rebuild();
assert_eq!((h.host.depth(), h.inner.depth()), (2, 1));
assert!(
handles_back(),
"the root overlay claims the press though the inner navigator cannot pop"
);
push_back_press();
h.rebuild();
assert_eq!(h.host.depth(), 1, "the press dismissed the root overlay");
assert!(!handles_back(), "nothing left to claim it");
}
#[test]
fn a_navigator_that_stops_wiring_is_pruned_after_one_full_cycle() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let outer: NavigatorController<()> = NavigatorController::new();
let inner: NavigatorController<()> = NavigatorController::new();
let nested: NavigatorController<()> = NavigatorController::new();
refresh_interest(&outer, Role::Navigator);
refresh_interest(&inner, Role::Navigator);
refresh_interest(&nested, Role::Navigator);
assert_eq!(
registrant_ids(),
vec![nested.id(), inner.id(), outer.id()],
"innermost/last-wired first: the reverse of build order"
);
refresh_interest(&outer, Role::Navigator);
refresh_interest(&inner, Role::Navigator);
assert_eq!(
registrant_ids(),
vec![nested.id(), inner.id(), outer.id()],
"not yet: a full cycle has not elapsed without it"
);
refresh_interest(&outer, Role::Navigator);
assert_eq!(
registrant_ids(),
vec![inner.id(), outer.id()],
"the departed navigator is pruned, and its controller clone released"
);
refresh_interest(&inner, Role::Navigator);
refresh_interest(&outer, Role::Navigator);
refresh_interest(&inner, Role::Navigator);
assert_eq!(registrant_ids(), vec![inner.id(), outer.id()]);
}
#[test]
fn an_overlay_host_survives_an_inner_navigator_wiring_twice_per_pass() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let host: NavigatorController<()> = NavigatorController::new();
let inner: NavigatorController<()> = NavigatorController::new();
let back = Rc::new(BackHandler::new(inner.clone()));
let mut app: AppLogic = {
let host = host.clone();
let inner = inner.clone();
Box::new(move |_: &mut ()| {
auto_wire_overlay_host(&host);
let inner = inner.clone();
let back = Rc::clone(&back);
raw_overlay_host(&host, move || {
back.track();
auto_wire(&inner);
any(raw_navigator(&inner, page))
})
})
};
let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
root.rebuild(&mut app, &mut ());
assert_eq!(
registrant_ids(),
vec![host.id(), inner.id()],
"the host outranks the inner navigator despite wiring once to its twice"
);
inner.push(page);
host.push_with_options(page, PushOptions::transparent().back(BackPolicy::Pop));
for _ in 0..2 {
root.rebuild(&mut app, &mut ());
}
assert_eq!((host.depth(), inner.depth()), (2, 2));
assert_eq!(
registrant_ids(),
vec![host.id(), inner.id()],
"still outermost-first after repeated passes"
);
assert!(
handles_back(),
"the shell is told the app handles back (a root modal is open)"
);
push_back_press();
root.rebuild(&mut app, &mut ());
assert_eq!(host.depth(), 1, "the press dismissed the root overlay");
assert_eq!(
inner.depth(),
2,
"and NOT the page under it (the root-modal back bug)"
);
}
#[test]
fn an_init_registered_backhandler_does_not_outrank_a_later_overlay_host() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let host: NavigatorController<()> = NavigatorController::new();
let inner: NavigatorController<()> = NavigatorController::new();
let back = BackHandler::new(inner.clone());
assert_eq!(registrant_ids(), vec![inner.id()], "init wired it alone");
auto_wire_overlay_host(&host);
assert_eq!(
registrant_ids(),
vec![host.id(), inner.id()],
"the host claims the outermost slot even though it wired second"
);
auto_wire_overlay_host(&host);
back.track();
assert_eq!(registrant_ids(), vec![host.id(), inner.id()]);
}
#[test]
fn a_torn_down_navigator_stops_vetoing_the_prune_and_is_released() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let outer: NavigatorController<()> = NavigatorController::new();
let nested: NavigatorController<()> = NavigatorController::new();
let show_nested = Rc::new(std::cell::Cell::new(true));
let mid_pass: Rc<RefCell<Vec<NavigatorId>>> = Rc::new(RefCell::new(Vec::new()));
let mut app: AppLogic = {
let outer = outer.clone();
let nested = nested.clone();
let show_nested = Rc::clone(&show_nested);
let mid_pass = Rc::clone(&mid_pass);
Box::new(move |_: &mut ()| {
auto_wire(&outer);
auto_wire(&outer);
let nested = nested.clone();
let show_nested = Rc::clone(&show_nested);
let mid_pass = Rc::clone(&mid_pass);
raw_navigator(&outer, move || {
*mid_pass.borrow_mut() = registrant_ids();
if show_nested.get() {
auto_wire(&nested);
any(raw_navigator(&nested, page))
} else {
page()
}
})
})
};
let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
root.rebuild(&mut app, &mut ());
assert!(nested.is_mounted(), "the nested navigator is in the tree");
assert_eq!(registrant_ids(), vec![nested.id(), outer.id()]);
root.rebuild(&mut app, &mut ());
root.rebuild(&mut app, &mut ());
assert_eq!(
registrant_ids(),
vec![nested.id(), outer.id()],
"a mounted navigator is in the tree by definition: never pruned"
);
assert_eq!(
*mid_pass.borrow(),
vec![nested.id(), outer.id()],
"and it is never MISSING mid-pass either — the window in which a \
spurious prune would mis-answer handles_back / mis-route a press"
);
show_nested.set(false);
root.rebuild(&mut app, &mut ());
assert!(!nested.is_mounted(), "teardown dropped the liveness");
root.rebuild(&mut app, &mut ());
root.rebuild(&mut app, &mut ());
assert_eq!(
registrant_ids(),
vec![outer.id()],
"the departed navigator is released, not kept alive by the veto"
);
}
#[test]
fn a_nested_navigator_inside_a_pushed_page_gets_the_back_press() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let outer: NavigatorController<()> = NavigatorController::new();
let inner: NavigatorController<()> = NavigatorController::new();
let mut app: AppLogic = {
let outer_c = outer.clone();
Box::new(move |_: &mut ()| {
auto_wire(&outer_c);
raw_navigator(&outer_c, page)
})
};
let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
root.rebuild(&mut app, &mut ());
assert_eq!(outer.depth(), 1, "outer starts at its own root");
assert!(
!handles_back(),
"root-level, no nested navigator yet: back bubbles"
);
{
let inner_for_push = inner.clone();
outer.push(move || {
auto_wire(&inner_for_push);
any(raw_navigator(&inner_for_push, page))
});
}
root.rebuild(&mut app, &mut ());
assert_eq!(
outer.depth(),
2,
"outer pushed to depth 2: the page hosting the nested navigator"
);
assert_eq!(
inner.depth(),
1,
"the nested navigator starts at its own root"
);
inner.push(page);
root.rebuild(&mut app, &mut ());
assert_eq!((outer.depth(), inner.depth()), (2, 2));
assert!(handles_back(), "some registrant claims the press");
push_back_press();
root.rebuild(&mut app, &mut ());
assert_eq!(
inner.depth(),
1,
"the back press popped the NESTED navigator's stack"
);
assert_eq!(
outer.depth(),
2,
"and left the outer navigator's depth UNCHANGED — a bare \
build-order (outermost-first) arbitration would instead have \
popped the outer stack, discarding the whole page the nested \
navigator lives on"
);
}
struct NestedHarness {
outer: NavigatorController<()>,
nested: NavigatorController<()>,
root: RenderRoot<(), NavigatorView<()>>,
app: AppLogic,
}
impl NestedHarness {
fn new(cull: bool) -> Self {
let outer: NavigatorController<()> = NavigatorController::new();
let nested: NavigatorController<()> = NavigatorController::new();
let app: AppLogic = {
let outer = outer.clone();
let nested = nested.clone();
Box::new(move |_: &mut ()| {
auto_wire(&outer);
let nested = nested.clone();
raw_navigator(&outer, move || {
auto_wire(&nested);
any(raw_navigator(&nested, page))
})
.cull_covered_builds(cull)
})
};
let mut harness = Self {
outer,
nested,
root: RenderRoot::new(),
app,
};
harness.rebuild();
harness
}
fn rebuild(&mut self) {
self.root.rebuild(&mut self.app, &mut ());
}
}
#[test]
fn a_nested_navigator_on_a_covered_page_does_not_take_the_back_press() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
for cull in [false, true] {
reset();
let mut h = NestedHarness::new(cull);
h.nested.push(page);
h.rebuild();
assert_eq!((h.outer.depth(), h.nested.depth()), (1, 2));
assert!(
handles_back(),
"the nested navigator claims the press while its page is current (cull={cull})"
);
h.outer.push(page);
h.rebuild();
assert_eq!((h.outer.depth(), h.nested.depth()), (2, 2));
for _ in 0..3 {
h.rebuild();
}
assert!(
handles_back(),
"the outer navigator claims it (cull={cull})"
);
push_back_press();
h.rebuild();
assert_eq!(
h.outer.depth(),
1,
"the press popped the OUTER stack — the page the user is \
actually looking at came off (cull={cull})"
);
assert_eq!(
h.nested.depth(),
2,
"and the covered navigator's invisible stack is UNCHANGED \
(cull={cull})"
);
push_back_press();
h.rebuild();
assert_eq!(
(h.outer.depth(), h.nested.depth()),
(1, 1),
"back reaches the nested navigator again once its page is \
current (cull={cull})"
);
}
}
#[test]
fn a_nested_navigator_under_a_transparent_overlay_does_not_take_the_back_press() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let mut h = NestedHarness::new(false);
h.nested.push(page);
h.rebuild();
h.outer
.push_with_options(page, PushOptions::transparent().back(BackPolicy::Pop));
h.rebuild();
assert_eq!((h.outer.depth(), h.nested.depth()), (2, 2));
push_back_press();
h.rebuild();
assert_eq!(h.outer.depth(), 1, "the overlay came off");
assert_eq!(
h.nested.depth(),
2,
"the painted-but-inert page's navigator kept its stack"
);
}
#[test]
#[ignore = "pins a design case that FAILS against current ranking: an inner \
navigator whose explicit BackHandler wires (and keeps tracking) \
before its outer host's first view exists (the Component::init \
shape) loses the innermost-first tie-break to the outer \
navigator instead of winning it — Registrant::key ranks \
Role::Navigator peers by raw wire sequence (birth), not by \
structural nesting depth. Expected to start passing once \
ranking accounts for nesting depth rather than birth order \
alone; rerun with `cargo test -p frust-ui --lib \
wiring_order_does_not_flip -- --ignored` to observe the \
current failure."]
fn wiring_order_does_not_flip_the_innermost_navigator_when_it_is_still_poppable() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let outer: NavigatorController<()> = NavigatorController::new();
let inner: NavigatorController<()> = NavigatorController::new();
let inner_back = BackHandler::new(inner.clone());
assert_eq!(registrant_ids(), vec![inner.id()], "inner wired alone");
let mut app: AppLogic = {
let outer_c = outer.clone();
Box::new(move |_: &mut ()| {
inner_back.track();
auto_wire(&outer_c);
raw_navigator(&outer_c, page)
})
};
let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
root.rebuild(&mut app, &mut ());
assert_eq!(outer.depth(), 1, "outer starts at its own root");
{
let inner_for_push = inner.clone();
outer.push(move || {
auto_wire(&inner_for_push);
any(raw_navigator(&inner_for_push, page))
});
}
root.rebuild(&mut app, &mut ());
assert_eq!(outer.depth(), 2, "outer pushed the page hosting inner");
inner.push(page);
root.rebuild(&mut app, &mut ());
assert_eq!((outer.depth(), inner.depth()), (2, 2));
push_back_press();
root.rebuild(&mut app, &mut ());
assert_eq!(
inner.depth(),
1,
"the structurally-innermost navigator should win the tie \
regardless of which one registered first"
);
assert_eq!(
outer.depth(),
2,
"and the outer stack should stay untouched"
);
}
#[test]
fn wiring_order_is_irrelevant_when_the_inner_navigator_has_no_interest() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let outer: NavigatorController<()> = NavigatorController::new();
let inner: NavigatorController<()> = NavigatorController::new();
let inner_back = BackHandler::new(inner.clone());
let mut app: AppLogic = {
let outer_c = outer.clone();
Box::new(move |_: &mut ()| {
inner_back.track();
auto_wire(&outer_c);
raw_navigator(&outer_c, page)
})
};
let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
root.rebuild(&mut app, &mut ());
{
let inner_for_push = inner.clone();
outer.push(move || {
auto_wire(&inner_for_push);
any(raw_navigator(&inner_for_push, page))
});
}
root.rebuild(&mut app, &mut ());
assert_eq!(
(outer.depth(), inner.depth()),
(2, 1),
"outer poppable, inner still at its own root"
);
push_back_press();
root.rebuild(&mut app, &mut ());
assert_eq!(outer.depth(), 1, "outer popped its own shell page");
}
type TabsApp = Box<dyn FnMut(&mut ()) -> AnyView<()>>;
struct TabsHarness {
tab0: NavigatorController<()>,
tab1: NavigatorController<()>,
host: NavigatorController<()>,
selected: std::rc::Rc<std::cell::Cell<Option<usize>>>,
root: RenderRoot<(), AnyView<()>>,
app: TabsApp,
}
impl TabsHarness {
fn new(selected: usize, last_wired_is_tab1: bool, with_host: bool) -> Self {
Self::build(selected, last_wired_is_tab1, with_host, |_, _, _| None)
}
fn with_fallback(
selected: usize,
make_fallback: impl FnOnce(
&NavigatorController<()>,
&NavigatorController<()>,
&std::rc::Rc<std::cell::Cell<Option<usize>>>,
) -> BackFallback,
) -> Self {
Self::build(selected, true, false, |tab0, tab1, sel| {
Some(make_fallback(tab0, tab1, sel))
})
}
fn build(
selected: usize,
last_wired_is_tab1: bool,
with_host: bool,
make_fallback: impl FnOnce(
&NavigatorController<()>,
&NavigatorController<()>,
&std::rc::Rc<std::cell::Cell<Option<usize>>>,
) -> Option<BackFallback>,
) -> Self {
let tab0: NavigatorController<()> = NavigatorController::new();
let tab1: NavigatorController<()> = NavigatorController::new();
let host: NavigatorController<()> = NavigatorController::new();
let sel = std::rc::Rc::new(std::cell::Cell::new(Some(selected)));
let fallback = make_fallback(&tab0, &tab1, &sel);
let app: TabsApp = {
let (tab0, tab1, host, sel) =
(tab0.clone(), tab1.clone(), host.clone(), sel.clone());
Box::new(move |_: &mut ()| {
let tabs = {
let (tab0, tab1, sel) = (tab0.clone(), tab1.clone(), sel.clone());
move || {
let (n0, n1);
if last_wired_is_tab1 {
n0 = crate::navigator(&tab0, page);
n1 = crate::navigator(&tab1, page);
} else {
n1 = crate::navigator(&tab1, page);
n0 = crate::navigator(&tab0, page);
}
frust_widgets::IndexedStack(sel.get(), (n0, n1))
}
};
if with_host {
auto_wire_overlay_host(&host);
any(raw_overlay_host(&host, move || any(tabs())))
} else if let Some(fallback) = &fallback {
any(back_fallback(fallback, tabs()))
} else {
any(tabs())
}
})
};
let mut h = Self {
tab0,
tab1,
host,
selected: sel,
root: RenderRoot::new(),
app,
};
h.rebuild();
h
}
fn rebuild(&mut self) {
self.root.rebuild(&mut self.app, &mut ());
}
fn select(&mut self, i: usize) {
self.selected.set(Some(i));
self.rebuild();
}
fn depths(&self) -> (usize, usize) {
(self.tab0.depth(), self.tab1.depth())
}
fn deepen(&mut self, d0: usize, d1: usize) {
for _ in self.tab0.depth()..d0 {
self.tab0.push(page);
}
for _ in self.tab1.depth()..d1 {
self.tab1.push(page);
}
self.rebuild();
assert_eq!(self.depths(), (d0, d1));
}
}
#[test]
fn a_selected_tab_at_its_root_exits_even_if_a_hidden_tab_is_deep() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let mut h = TabsHarness::new(0, true, false);
h.deepen(1, 3);
assert!(
!handles_back(),
"selected tab at its root: the app would exit"
);
push_back_press();
h.rebuild();
assert_eq!(h.depths(), (1, 3), "the hidden tab was not popped");
}
#[test]
fn switching_tabs_hands_the_back_press_to_the_newly_selected_tab() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let mut h = TabsHarness::new(0, true, false);
h.deepen(1, 3);
h.select(1);
assert!(handles_back(), "selected tab 1 is poppable");
push_back_press();
h.rebuild();
assert_eq!(h.depths(), (1, 2), "tab 1 popped one page, tab 0 untouched");
}
#[test]
fn the_back_press_follows_selection_not_wire_order() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
for last_wired_is_tab1 in [true, false] {
reset();
let mut h = TabsHarness::new(0, last_wired_is_tab1, false);
h.deepen(2, 3);
assert!(handles_back());
push_back_press();
h.rebuild();
assert_eq!(
h.depths(),
(1, 3),
"selected tab 0 took the press (tab1 wired last: {last_wired_is_tab1})"
);
h.select(1);
h.tab0.push(page);
h.rebuild();
assert_eq!(h.depths(), (2, 3));
push_back_press();
h.rebuild();
assert_eq!(
h.depths(),
(2, 2),
"selected tab 1 took the press (tab1 wired last: {last_wired_is_tab1})"
);
}
}
#[test]
fn a_root_overlay_outranks_the_selected_tab() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let mut h = TabsHarness::new(1, false, true);
h.deepen(1, 2);
h.host
.push_with_options(page, PushOptions::transparent().back(BackPolicy::Pop));
h.rebuild();
assert_eq!(h.host.depth(), 2);
assert!(handles_back());
push_back_press();
h.rebuild();
assert_eq!(h.host.depth(), 1, "the overlay was dismissed first");
assert_eq!(h.depths(), (1, 2), "the selected tab was untouched");
push_back_press();
h.rebuild();
assert_eq!(
h.depths(),
(1, 1),
"the next press reaches the selected tab"
);
}
#[test]
fn a_hidden_tab_keeps_its_stack_and_registration() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let mut h = TabsHarness::new(0, true, false);
h.deepen(1, 3);
h.select(1);
h.select(0);
for _ in 0..3 {
h.rebuild();
}
assert_eq!(h.depths(), (1, 3), "the hidden tab's depth survived");
let ids = registrant_ids();
assert!(
ids.contains(&h.tab0.id()) && ids.contains(&h.tab1.id()),
"both tab controllers stay registered while hidden"
);
h.select(1);
assert!(handles_back());
push_back_press();
h.rebuild();
assert_eq!(h.depths(), (1, 2), "tab 1 resumes from its kept stack");
}
struct Driver<V: View<()>, F: FnMut(&mut ()) -> V> {
root: RenderRoot<(), V>,
app: F,
}
impl<V: View<()>, F: FnMut(&mut ()) -> V> Driver<V, F> {
fn new(app: F) -> Self {
let mut driver = Self {
root: RenderRoot::new(),
app,
};
driver.rebuild();
driver
}
fn rebuild(&mut self) {
self.root.rebuild(&mut self.app, &mut ());
}
}
fn counting() -> (Rc<Cell<usize>>, impl Fn() + 'static) {
let hits = Rc::new(Cell::new(0));
let counter = Rc::clone(&hits);
(hits, move || counter.set(counter.get() + 1))
}
fn fallback_id(fallback: &BackFallback) -> RegistrantId {
RegistrantId::Fallback(fallback.inner.id)
}
fn is_registered(fallback: &BackFallback) -> bool {
registrants().contains(&fallback_id(fallback))
}
type WrappedNavigator = BackFallbackView<NavigatorView<()>>;
type WrappedDriver = Driver<WrappedNavigator, Box<dyn FnMut(&mut ()) -> WrappedNavigator>>;
fn wrapped_navigator(
interest: bool,
) -> (
NavigatorController<()>,
BackFallback,
Rc<Cell<usize>>,
WrappedDriver,
) {
let nav: NavigatorController<()> = NavigatorController::new();
let (hits, on_back) = counting();
let fallback = BackFallback::new(move || interest, on_back);
let driver = {
let (nav, fallback) = (nav.clone(), fallback.clone());
Driver::new(Box::new(move |_: &mut ()| {
back_fallback(&fallback, crate::navigator(&nav, page))
})
as Box<dyn FnMut(&mut ()) -> WrappedNavigator>)
};
(nav, fallback, hits, driver)
}
#[test]
fn a_poppable_navigator_outranks_a_fallback() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let (nav, _fallback, hits, mut d) = wrapped_navigator(true);
nav.push(page);
d.rebuild();
assert_eq!(nav.depth(), 2);
push_back_press();
d.rebuild();
assert_eq!(nav.depth(), 1, "the navigator took the press");
assert_eq!(hits.get(), 0, "and the fallback did not");
}
#[test]
fn a_fallback_claims_the_press_when_no_navigator_does() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let (nav, _fallback, hits, mut d) = wrapped_navigator(true);
assert_eq!(nav.depth(), 1);
assert!(
handles_back(),
"the fallback claims the press ahead of time"
);
push_back_press();
d.rebuild();
assert_eq!(hits.get(), 1, "on_back ran once");
assert_eq!(nav.depth(), 1, "the navigator is unchanged");
d.rebuild();
assert_eq!(hits.get(), 1, "a rebuild with no new press does not refire");
}
#[test]
fn an_uninterested_fallback_lets_the_press_exit() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let (nav, _fallback, hits, mut d) = wrapped_navigator(false);
assert!(!handles_back(), "nobody claims: the app would exit");
push_back_press();
d.rebuild();
assert_eq!((hits.get(), nav.depth()), (0, 1));
}
#[test]
fn fallbacks_rank_after_the_navigator_whatever_the_wire_order() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let nav: NavigatorController<()> = NavigatorController::new();
let (before_hits, before_on_back) = counting();
let (after_hits, after_on_back) = counting();
let before = BackFallback::new(|| true, before_on_back);
let after = BackFallback::new(|| true, after_on_back);
let mut d = {
let (nav, before, after) = (nav.clone(), before.clone(), after.clone());
Driver::new(move |_: &mut ()| {
frust_widgets::stack()
.child(back_fallback(&before, frust_widgets::text("x")))
.child(back_fallback(&after, crate::navigator(&nav, page)))
})
};
assert_eq!(
registrants(),
vec![
RegistrantId::Navigator(nav.id()),
fallback_id(&before),
fallback_id(&after),
],
"the navigator first, then the fallbacks in first-wire order"
);
nav.push(page);
d.rebuild();
push_back_press();
d.rebuild();
assert_eq!(nav.depth(), 1, "the navigator took the press");
assert_eq!((before_hits.get(), after_hits.get()), (0, 0));
push_back_press();
d.rebuild();
assert_eq!(
(before_hits.get(), after_hits.get()),
(1, 0),
"with the navigator at its root, the first-wired fallback took it"
);
assert_eq!(
registrants(),
vec![
RegistrantId::Navigator(nav.id()),
fallback_id(&before),
fallback_id(&after),
],
"re-wiring never re-orders"
);
}
#[test]
fn an_unmounted_fallback_stops_claiming_and_is_pruned() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let nav: NavigatorController<()> = NavigatorController::new();
let (hits, on_back) = counting();
let fallback = BackFallback::new(|| true, on_back);
let show = Rc::new(Cell::new(true));
let mut d = {
let (nav, fallback, show) = (nav.clone(), fallback.clone(), Rc::clone(&show));
Driver::new(move |_: &mut ()| {
frust_widgets::stack()
.child(crate::navigator(&nav, page))
.when(show.get(), |s| {
s.child(back_fallback(&fallback, frust_widgets::text("x")))
})
})
};
assert!(handles_back(), "mounted and interested");
show.set(false);
d.rebuild();
assert!(!fallback.is_live(), "the wrapper was torn down");
assert!(!handles_back(), "an unmounted fallback claims nothing");
d.rebuild();
d.rebuild();
assert!(!is_registered(&fallback), "rule 2 pruned it");
assert_eq!(registrant_ids(), vec![nav.id()]);
push_back_press();
d.rebuild();
assert_eq!(hits.get(), 0, "a press after the prune does not reach it");
show.set(true);
d.rebuild();
assert!(
is_registered(&fallback),
"re-adding the wrapper re-registers it"
);
assert!(handles_back());
push_back_press();
d.rebuild();
assert_eq!(hits.get(), 1, "and it claims presses again");
}
#[test]
fn a_press_burst_calls_on_back_once() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let (_nav, _fallback, hits, mut d) = wrapped_navigator(true);
for _ in 0..3 {
push_back_press();
}
d.rebuild();
assert_eq!(hits.get(), 1, "three presses, one rebuild: one on_back");
d.rebuild();
assert_eq!(hits.get(), 1);
}
#[test]
fn back_at_a_tab_root_returns_home_then_exits() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
const HOME: usize = 0;
let mut h = TabsHarness::with_fallback(1, |tab0, tab1, sel| {
let (tab0, tab1) = (tab0.clone(), tab1.clone());
let (probe, set) = (Rc::clone(sel), Rc::clone(sel));
BackFallback::new(
move || match probe.get() {
Some(HOME) | None => false,
Some(i) => [&tab0, &tab1][i].depth() <= 1,
},
move || set.set(Some(HOME)),
)
});
h.deepen(1, 2);
push_back_press();
h.rebuild();
assert_eq!(h.depths(), (1, 1), "tab B popped");
assert_eq!(h.selected.get(), Some(1), "and the selection stayed");
assert!(
handles_back(),
"off home at a tab root: the fallback claims"
);
push_back_press();
h.rebuild();
assert_eq!(
h.selected.get(),
Some(HOME),
"the press selected the home tab"
);
h.rebuild();
assert_eq!(h.depths(), (1, 1), "no stack was popped");
assert!(!handles_back(), "home at its root: back falls through");
push_back_press();
h.rebuild();
assert_eq!(h.selected.get(), Some(HOME));
assert_eq!(h.depths(), (1, 1));
}
#[test]
fn a_mounted_fallback_survives_a_double_wiring_navigator() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let nav: NavigatorController<()> = NavigatorController::new();
let back = BackHandler::new(nav.clone());
let (hits, on_back) = counting();
let fallback = BackFallback::new(|| true, on_back);
let mut d = {
let (nav, fallback) = (nav.clone(), fallback.clone());
Driver::new(move |_: &mut ()| {
back.track();
back_fallback(&fallback, crate::navigator(&nav, page))
})
};
for pass in 0..4 {
d.rebuild();
assert!(is_registered(&fallback), "never pruned (pass {pass})");
assert!(handles_back(), "handles_back stays true (pass {pass})");
}
push_back_press();
d.rebuild();
assert_eq!(
(hits.get(), nav.depth()),
(1, 1),
"the fallback took it once"
);
}
#[test]
fn first_wire_grace_keeps_an_unbuilt_fallback_through_a_double_wire() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let nav: NavigatorController<()> = NavigatorController::new();
let back = BackHandler::new(nav.clone());
let fallback = BackFallback::new(|| true, || {});
let mid_pass = Rc::new(Cell::new((false, usize::MAX)));
let d = {
let (nav, fallback, mid_pass) = (nav.clone(), fallback.clone(), Rc::clone(&mid_pass));
Driver::new(move |_: &mut ()| {
let wrapped = back_fallback(&fallback, frust_widgets::text("x"));
back.track();
let navigator = crate::navigator(&nav, page);
mid_pass.set((is_registered(&fallback), fallback.inner.mounted.get()));
frust_widgets::stack().child(wrapped).child(navigator)
})
};
assert_eq!(
mid_pass.get(),
(true, 0),
"unbuilt (mount count 0) yet not pruned by the navigator's double wire"
);
assert!(is_registered(&fallback), "still registered after the pass");
assert_eq!(fallback.inner.mounted.get(), 1, "and now mounted");
assert!(
handles_back(),
"handles_back reflects it on the first frame"
);
drop(d);
}
#[test]
fn a_fallback_shared_by_two_wrappers_lives_until_both_unmount() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let nav: NavigatorController<()> = NavigatorController::new();
let fallback = BackFallback::new(|| true, || {});
let show = Rc::new(Cell::new((true, true)));
let mut d = {
let (nav, fallback, show) = (nav.clone(), fallback.clone(), Rc::clone(&show));
Driver::new(move |_: &mut ()| {
let (a, b) = show.get();
frust_widgets::stack()
.child(crate::navigator(&nav, page))
.when(a, |s| {
s.child(back_fallback(&fallback, frust_widgets::text("a")))
})
.when(b, |s| {
s.child(back_fallback(&fallback, frust_widgets::text("b")))
})
})
};
assert_eq!(fallback.inner.mounted.get(), 2);
show.set((true, false));
for _ in 0..3 {
d.rebuild();
}
assert_eq!(fallback.inner.mounted.get(), 1);
assert!(is_registered(&fallback), "one wrapper still mounted: live");
assert!(handles_back());
show.set((false, false));
d.rebuild();
assert!(!handles_back(), "both unmounted: claims nothing");
d.rebuild();
d.rebuild();
assert!(!is_registered(&fallback), "and rule 2 pruned it");
}
#[test]
fn a_wrapper_rebuilt_against_another_handle_moves_its_mount() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let nav: NavigatorController<()> = NavigatorController::new();
let first = BackFallback::new(|| true, || {});
let second = BackFallback::new(|| true, || {});
let use_first = Rc::new(Cell::new(true));
let mut d = {
let (nav, first, second, use_first) = (
nav.clone(),
first.clone(),
second.clone(),
Rc::clone(&use_first),
);
Driver::new(move |_: &mut ()| {
let fallback = if use_first.get() { &first } else { &second };
back_fallback(fallback, crate::navigator(&nav, page))
})
};
assert_eq!(
(first.inner.mounted.get(), second.inner.mounted.get()),
(1, 0)
);
use_first.set(false);
for _ in 0..3 {
d.rebuild();
}
assert_eq!(
(first.inner.mounted.get(), second.inner.mounted.get()),
(0, 1)
);
assert!(!is_registered(&first), "the old handle was released");
assert!(is_registered(&second), "the new one is live");
}
#[test]
fn an_unclaimed_press_pops_nothing_when_the_hidden_deep_tab_wires_first() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let mut h = TabsHarness::new(0, false, false);
h.deepen(1, 3);
assert!(!handles_back(), "selected tab at its root: nothing claims");
push_back_press();
h.rebuild();
assert_eq!(h.depths(), (1, 3), "the hidden deep tab was not popped");
h.rebuild();
assert_eq!(h.depths(), (1, 3));
}
#[test]
fn a_claim_lost_before_the_press_is_consumed_pops_no_hidden_tab() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
const HOME: usize = 0;
let mut h = TabsHarness::with_fallback(1, |tab0, tab1, sel| {
let (tab0, tab1) = (tab0.clone(), tab1.clone());
let probe = Rc::clone(sel);
BackFallback::new(
move || match probe.get() {
Some(HOME) | None => false,
Some(i) => [&tab0, &tab1][i].depth() <= 1,
},
|| {},
)
});
h.deepen(3, 1);
assert!(handles_back(), "tab B at its root: the fallback claims");
h.selected.set(Some(HOME));
push_back_press();
h.rebuild();
assert_eq!(h.depths(), (3, 1), "the hidden-then-selected Home tab held");
h.rebuild();
assert_eq!(h.depths(), (3, 1));
}
type SwitchedApp = Box<dyn FnMut(&mut ()) -> PatternSwitcherView<(), SharedAxis>>;
struct SwitchHarness {
tab0: NavigatorController<()>,
tab1: NavigatorController<()>,
key: Rc<Cell<usize>>,
hits: Rc<Cell<usize>>,
driver: Driver<PatternSwitcherView<(), SharedAxis>, SwitchedApp>,
}
impl SwitchHarness {
fn new(with_fallback: bool) -> Self {
let tab0: NavigatorController<()> = NavigatorController::new();
let tab1: NavigatorController<()> = NavigatorController::new();
let key = Rc::new(Cell::new(0usize));
let (hits, on_back) = counting();
let fallback = {
let tab1 = tab1.clone();
BackFallback::new(move || tab1.depth() <= 1, on_back)
};
let app: SwitchedApp = {
let (tab0, tab1, key) = (tab0.clone(), tab1.clone(), key.clone());
Box::new(move |_: &mut ()| {
let child: AnyView<()> = if key.get() == 0 {
let n0 = crate::navigator(&tab0, page);
let n1 = crate::navigator(&tab1, page);
let tabs = frust_widgets::IndexedStack(Some(1), (n0, n1));
if with_fallback {
any(back_fallback(&fallback, tabs))
} else {
any(tabs)
}
} else {
any(frust_widgets::text("no navigator here"))
};
crate::motion::switcher::pattern_switcher(key.get(), SharedAxis::X, child)
})
};
Self {
tab0,
tab1,
key,
hits,
driver: Driver::new(app),
}
}
fn deepen_b(&mut self, depth: usize) {
for _ in self.tab1.depth()..depth {
self.tab1.push(page);
}
self.driver.rebuild();
assert_eq!(self.tab1.depth(), depth);
}
fn switch_away(&mut self) {
self.key.set(1);
self.driver.rebuild();
}
}
#[test]
fn a_switcher_teardown_leaves_no_claim_on_a_deep_dead_tab() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let mut h = SwitchHarness::new(true);
h.deepen_b(3);
assert!(
handles_back(),
"deep selected tab B claims before the switch"
);
h.switch_away();
assert!(!handles_back(), "no claim on the switch rebuild itself");
push_back_press();
h.driver.rebuild();
assert_eq!(h.tab1.depth(), 3, "the dead tab B was not popped");
assert_eq!(h.tab0.depth(), 1);
assert_eq!(h.hits.get(), 0, "the fallback did not fire either");
assert!(!handles_back());
}
#[test]
fn a_switcher_teardown_silences_an_interested_fallback() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let mut h = SwitchHarness::new(true);
assert!(handles_back(), "tab B at its root: the fallback claims");
h.switch_away();
assert!(!handles_back(), "the torn-down fallback stops claiming");
push_back_press();
h.driver.rebuild();
assert_eq!(h.hits.get(), 0, "the fallback did not fire");
}
#[test]
fn a_torn_down_navigator_stops_claiming_without_an_extra_rebuild() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _rt = ReactiveRuntime::init(Arc::new(|| {}));
reset();
let mut h = SwitchHarness::new(false);
h.deepen_b(3);
assert!(h.tab1.is_mounted());
assert!(handles_back());
h.switch_away();
assert!(!h.tab1.is_mounted(), "torn down at the switch");
assert!(
!handles_back(),
"and no longer claiming, with no extra pass"
);
}
}