Skip to main content

frust/
back_glue.rs

1//! Back-press ⇄ navigator glue (predictive-back + facade auto-wiring): the
2//! facade is the only crate seeing both `frust-widgets`'
3//! [`NavigatorController`] and `frust-reactive`'s back-press source, mirroring
4//! [`router_glue`](crate::router_glue) — `frust-widgets` stays reactive-free,
5//! `frust-reactive` navigator-free.
6//!
7//! # The Android back contract
8//!
9//! Shell delivers a hardware/gesture back press → framework attempts an async
10//! back → framework maintains a "handles back" boolean the shell reads, so a
11//! *root-level* press falls through to the platform (activity finish). This
12//! module is the framework half: a press is consumed exactly once and routed
13//! through [`NavigatorController::request_back`] — which applies the top page's
14//! `BackPolicy` (`Pop` pops, `DismissAnimated` fires the overlay's dismiss
15//! signal, `Veto` swallows it) — never a bare [`pop`](NavigatorController::pop).
16//! "Handles back" reads [`back_interest`](NavigatorController::back_interest),
17//! not [`can_pop`](NavigatorController::can_pop), so a dismissable/veto overlay
18//! at the *root* claims the press ahead-of-time (predictive-back parity) instead
19//! of exiting the app.
20//!
21//! # Two entry points, one consumption source
22//!
23//! [`auto_wire`], called from the facade's [`navigator`](crate::navigator)
24//! wrapper every rebuild (back handling with ZERO back-specific app code), and
25//! the explicit [`BackHandler`]/[`attach_back_handler`], constructed in
26//! `Component::init` and driven by [`track`](BackHandler::track) from every
27//! `Component::build` (Huddle's shape). Both funnel through one process-wide
28//! (UI-thread-affine) [`SHARED`] marker, so a press is consumed **exactly
29//! once**: whichever entry point runs first in a rebuild consumes it, the other
30//! sees the same already-consumed count and no-ops — never a double-pop.
31//!
32//! # R44-back: arbitration across more than one navigator
33//!
34//! An app with a root [`overlay_host`](crate::overlay_host) wires **two**
35//! navigators — the host and the navigator inside it — and both want the press.
36//! `frust-reactive`'s live-provider slot stays single-registrant (see
37//! `frust_reactive::back`); this module owns that registration and multiplexes:
38//!
39//! > **R44-back — a root overlay host outranks every plain navigator; among
40//! > [`Role::Navigator`] peers the innermost (last-wired) ranks first, LIFO —
41//! > mirroring Android's `OnBackPressedDispatcher`; a [`BackFallback`]
42//! > ([`Role::Fallback`]) ranks after every navigator, so it claims a press
43//! > only when no navigator does; a back press goes to the first registrant
44//! > reporting interest
45//! > ([`back_interest()`](NavigatorController::back_interest)` == true` for a
46//! > navigator, the app's interest closure for a fallback).**
47//!
48//! [`SharedBack::registrants`] is that ordered list, sorted by ([`Role`], wire
49//! sequence): build order among hosts, *reversed* among navigators so the
50//! innermost wins, build order again among fallbacks. **Rank comes from the
51//! call site, not wire timing** — [`auto_wire_overlay_host`] registers
52//! [`Role::Host`], [`back_fallback`] registers [`Role::Fallback`], every other
53//! entry point [`Role::Navigator`] — because both *when* and *how often* a
54//! controller wires
55//! genuinely vary: a [`BackHandler`] built in `Component::init` wires before any
56//! view exists, and Huddle's documented pattern wires one controller **twice in
57//! one build pass** (`state.back.track()` beside
58//! `frust::navigator(&controller, …)`), so neither first-wire order nor "one
59//! wire per pass" is a premise anything here may rest on. `handles_back`/the
60//! live provider answer `any(interest)`, and a consumed press goes to the
61//! *first* registrant with interest — so one registrant (or a host with no
62//! overlays, `compute_back_interest` `false` at depth 1) behaves exactly as if
63//! arbitration did not exist.
64//!
65//! ## Releasing a navigator that went away
66//!
67//! A navigator that goes away (a screen with its own nested navigator, popped)
68//! must stop claiming presses and release its controller clone. Two rules, in
69//! priority order:
70//!
71//! 1. **Mounted is live.** [`NavigatorController::is_mounted`] is exact (the
72//!    navigator's `View::build`/`teardown` write it), so a *mounted* registrant
73//!    is in the retained tree by definition and is never pruned, however the
74//!    wire sequence looks — the load-bearing half: a root overlay host is
75//!    mounted from its own `build`, which runs before its page builder and hence
76//!    before the inner navigator wires at all.
77//! 2. **The one-cycle rule, for registrants never mounted.** A wire from an
78//!    already-registered controller ends a window; an unmounted registrant that
79//!    did not wire within it is dropped — the one case rule 1 cannot see (a
80//!    `BackHandler` whose navigator view never made it into the tree).
81//!
82//! **Rule 2 must not be trusted for a mounted navigator**: it infers a build
83//! cycle from a repeat wire, and the double-wire-per-pass pattern above breaks
84//! that inference (the second wire of one pass looks exactly like the first of
85//! the next). Before rule 1, that inference pruned the root overlay host
86//! mid-pass and re-created the root-modal double-claim bug.
87//!
88//! **A [`BackFallback`] obeys the same two rules**, which is why it is
89//! view-scoped: its liveness is the mount count of its [`back_fallback`]
90//! wrappers (rule 1 — `build` increments it, `teardown` decrements it, as a
91//! navigator's own `build`/`teardown` do), and rule 2 removes it once every
92//! wrapper is unmounted. An unmounted fallback also stops claiming at once:
93//! its interest is gated on that same liveness, so it never claims a press in
94//! the window before rule 2 reaches it.
95//!
96//! **First-wire grace (a fallback only).** [`back_fallback`] wires during view
97//! *construction*, before its wrapper's `build` runs — unlike a navigator,
98//! whose `build` publishes liveness before anything nested wires. So in the
99//! very pass that first mounts a wrapper, a navigator double-wiring later in
100//! that pass would see an unbuilt, hence unmounted, fallback and prune it.
101//! Rule: **a fallback wired but not yet built, rebuilt or torn down counts as
102//! mounted until one of those happens** (each clears the grace; the next
103//! [`back_fallback`] call over that handle sets it again). A wrapper is always
104//! reconciled in the pass that constructed it, so the grace never outlives that
105//! pass in practice; the one residual is a [`back_fallback`] view constructed
106//! and then discarded unbuilt, which keeps its fallback live until a wrapper
107//! over the same handle is reconciled or the handle is dropped.
108//!
109//! ## Reachability is upstream of arbitration, by design (R23)
110//!
111//! Ranking decides who wins *among the claimants*, never who may claim. A
112//! navigator input cannot reach — one nested inside a page the outer navigator
113//! has covered — reports `back_interest() == false` at the source:
114//! `frust-widgets` gates it on the hosting page being in the host navigator's
115//! `input_routed_pages()` (rule **R23**: navigator reach follows input routing,
116//! exactly; see `frust_widgets::NavigatorController::back_interest`). So nothing
117//! here filters on visibility and there is **no second notion of reachability**
118//! to keep in sync with the widget's — load-bearing for innermost-first ranking,
119//! since an off-screen innermost navigator would otherwise outrank the one the
120//! user is actually looking at and swallow every press. Reach says *whether* a
121//! navigator is in the running, rank *which* of those gets it.
122//!
123//! # Timing: the live provider closes the stale window
124//!
125//! Wiring runs during a rebuild's *build* pass, **before** the navigator's
126//! `apply_ops` publishes the new stack depth, so the polled [`set_handles_back`]
127//! flag lags a stack change by one frame — and a frame gate skipping the settle
128//! frame lets that stale value persist. So the wiring also registers a live
129//! provider ([`set_can_pop_provider`]) reading `controller.back_interest()`,
130//! which `frust_reactive::handles_back` prefers to the flag: it reads CURRENT
131//! interest at press time, after `apply_ops` published it. UI-thread-affine (it
132//! reads an `Rc`-backed controller through [`SHARED`]); `frust_reactive::back`'s
133//! module docs carry the source-side contract.
134
135use std::cell::{Cell, RefCell};
136use std::rc::Rc;
137
138use frust_core::{BuildCtx, ChangeFlags, View};
139use frust_reactive::{CanPopRegistration, back_presses, set_can_pop_provider, set_handles_back};
140use frust_widgets::{NavigatorController, NavigatorId};
141use reactive_graph::traits::{Get, GetUntracked};
142
143thread_local! {
144    /// The UI-thread monotonic source of [`BackFallback`] ids
145    /// ([`RegistrantId::Fallback`]). Kept apart from [`SHARED`] so nothing that
146    /// resets the arbitration state can ever hand out an id twice.
147    static NEXT_FALLBACK_ID: Cell<u64> = const { Cell::new(0) };
148}
149
150thread_local! {
151    /// The process-wide (UI-thread-affine) back-press wiring shared by BOTH the
152    /// automatic [`navigator`](crate::navigator) path and the explicit
153    /// [`BackHandler`]. A single shared marker is the "single consumption
154    /// source" that makes an auto-wired `navigator()` and a manual
155    /// `BackHandler` on the same controller consume a press EXACTLY once — no
156    /// double-pop — and a single ordered registrant list is what arbitrates a
157    /// press across a root overlay host and the navigator inside it (R44-back;
158    /// see the module docs).
159    ///
160    /// UI-thread-affine because every [`Registrant`] holds an `Rc`-backed
161    /// `NavigatorController` or [`BackFallback`] (`!Send`), exactly like
162    /// frust-reactive's live provider slot it feeds.
163    static SHARED: RefCell<SharedBack> = const { RefCell::new(SharedBack::new()) };
164}
165
166/// What a registrant *is*, which is what R44-back ranks it by — taken from the
167/// facade entry point that wired it, never inferred from wire timing.
168///
169/// `Ord` is the arbitration order: [`Host`](Self::Host) sorts first,
170/// [`Fallback`](Self::Fallback) last.
171#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
172enum Role {
173    /// A root [`overlay_host`](crate::overlay_host). Its overlays are drawn over
174    /// every piece of chrome, including any inner navigator, so an open one owns
175    /// the back press outright — R44-back's host-outranks-navigator half.
176    Host,
177    /// Any other wired navigator: [`navigator`](crate::navigator)'s auto-wiring
178    /// or an explicit [`BackHandler`].
179    Navigator,
180    /// A [`BackFallback`] wired by [`back_fallback`]: not a navigator at all,
181    /// but an app rule ("back at a tab root returns to the home tab"). Declared
182    /// last so it sorts after every navigator **whatever the wire order** — a
183    /// fallback claims a press only when no navigator does. Its liveness is its
184    /// wrappers' mount count (rule 1), and rule 2 removes it once unmounted (see
185    /// the module docs' release rules).
186    Fallback,
187}
188
189/// Who a [`Registrant`] stands for — the identity a re-wire matches on to
190/// refresh an entry in place instead of appending a duplicate.
191#[derive(Clone, Copy, PartialEq, Eq, Debug)]
192enum RegistrantId {
193    /// A navigator, by its controller's id.
194    Navigator(NavigatorId),
195    /// A [`BackFallback`], by the id [`NEXT_FALLBACK_ID`] gave it.
196    Fallback(u64),
197}
198
199/// One wired navigator or fallback in [`SharedBack::registrants`] — the unit
200/// R44-back arbitrates over. A navigator's closures hold a controller clone, so
201/// a registrant keeps its navigator's op queue alive until it is pruned; a
202/// fallback's hold only a weak reference to its [`BackFallback`].
203struct Registrant {
204    /// Which controller (or fallback) this entry stands for, so a re-wire
205    /// refreshes it in place instead of appending a duplicate.
206    id: RegistrantId,
207    /// This entry's arbitration rank (see [`Role`]). Sticky-upgrades to
208    /// [`Role::Host`]: a controller wired *once* as an overlay host is one, even
209    /// on a re-wire that names it through a different entry point.
210    role: Role,
211    /// The wire sequence number of this entry's FIRST wire — the tiebreak within
212    /// a role, so two navigators of equal rank keep build order.
213    birth: u64,
214    /// The wire sequence number of this entry's most recent refresh; the
215    /// one-cycle prune compares against it (see [`refresh_interest`]).
216    seq: u64,
217    /// `controller.is_mounted()` — is this navigator in the retained tree right
218    /// now? For a fallback, its wrappers' mount count > 0 or the first-wire
219    /// grace (see the module docs). A `true` here vetoes the one-cycle prune
220    /// (see the module docs' release rules). Only ever reads `Cell`s, so it is
221    /// safe to call under the [`SHARED`] borrow.
222    mounted: Box<dyn Fn() -> bool>,
223    /// `controller.back_interest()` — does this navigator claim the next press?
224    /// Already `false` for a navigator whose hosting page input cannot reach
225    /// (the R23 gate in `frust-widgets`; see the module docs), which is why
226    /// nothing here filters on visibility. For a fallback, the app's interest
227    /// closure, gated on the fallback's liveness. An `Rc` so it is cloned out
228    /// and called with **no** [`SHARED`] borrow outstanding — a fallback's
229    /// probe runs app code.
230    interest: Rc<dyn Fn() -> bool>,
231    /// `controller.request_back()` — deliver the press to this navigator,
232    /// honoring its top page's `BackPolicy` (a fallback's `on_back`). An `Rc`
233    /// rather than a `Box` so [`route_back`] can clone it out and call it with
234    /// **no** [`SHARED`] borrow outstanding.
235    request_back: Rc<dyn Fn()>,
236}
237
238impl Registrant {
239    /// The sort key R44-back orders the arbitration list by: rank first
240    /// ([`Role`], `Host` sorts ahead of every `Navigator`, `Fallback` after
241    /// every one), then within `Role::Navigator` **innermost-first** (highest
242    /// `birth` — a nested navigator always wires after the navigator hosting
243    /// it, so the highest `birth` among `Navigator` peers is the innermost
244    /// one); `Role::Host` and `Role::Fallback` peers keep first-wire order
245    /// (`birth` ascending). A fallback's rank against a navigator never
246    /// depends on wire order: `Role` alone decides it.
247    ///
248    /// Innermost-first is what lets a nested navigator take the press at all:
249    /// ordering every rank by first wire (`birth` ascending) would leave an
250    /// outer navigator at depth > 1 — exactly the case where a pushed page hosts
251    /// a nested navigator — permanently outranking the nested navigator inside
252    /// it, so back would pop the outer stack (discarding the whole page the
253    /// nested navigator lives on) instead of reaching in. `Role::Host` peers
254    /// keep first-wire order: R44-back only ever requires Host to rank above
255    /// every Navigator, never a particular order among multiple hosts.
256    fn key(&self) -> (Role, u64) {
257        match self.role {
258            Role::Host | Role::Fallback => (self.role, self.birth),
259            // `u64::MAX - birth`: ascending sort on this puts the HIGHEST
260            // birth (innermost, wired last) first among `Navigator` peers —
261            // LIFO, mirroring Android's `OnBackPressedDispatcher`.
262            Role::Navigator => (self.role, u64::MAX - self.birth),
263        }
264    }
265}
266
267/// The shared back state behind [`SHARED`] (see its docs).
268struct SharedBack {
269    /// The last back-press count consumed by *any* entry point; `None` until
270    /// the first observation. A press up to this count has already been routed
271    /// through `request_back`, so a second entry point (or a re-run rebuild)
272    /// observing the same count does not re-fire (the `RouterDeepLinks`
273    /// consumed-marker pattern, shared across both back entry points).
274    consumed: Option<u64>,
275    /// Every wired navigator and fallback, sorted by [`Registrant::key`] —
276    /// [`Role::Host`] first (build order among hosts, unchanged), then
277    /// [`Role::Navigator`] peers innermost-first (LIFO: last-wired sorts first,
278    /// the reverse of build order), then [`Role::Fallback`] peers in build
279    /// order, after every navigator — the R44-back arbitration list. Kept
280    /// sorted on every wire, so a registrant's position never depends on how
281    /// often it wires per pass; an unmounted registrant that misses a build
282    /// cycle is pruned (see [`register`]), a fallback included once all its
283    /// wrappers are unmounted. The live provider (below) reads through this
284    /// list, so a press is decided against the CURRENT registrants' interest.
285    registrants: Vec<Registrant>,
286    /// Monotonic wire counter stamped into [`Registrant::birth`]/
287    /// [`Registrant::seq`]; ordering and the one-cycle prune are both expressed
288    /// against it.
289    seq: u64,
290    /// The live can-pop provider registration, made once on the first wire and
291    /// kept for the process lifetime. Its closure reads
292    /// [`registrants`](Self::registrants) live, so `handles_back()` answers
293    /// against the current navigators with no one-frame lag (see the module
294    /// docs' timing note). `None` until the first wire registers it.
295    provider: Option<CanPopRegistration>,
296}
297
298impl SharedBack {
299    const fn new() -> Self {
300        Self {
301            consumed: None,
302            registrants: Vec::new(),
303            seq: 0,
304            provider: None,
305        }
306    }
307}
308
309/// Whether **any** wired registrant claims the next back press — the value both
310/// the live provider and the polled `handles_back` flag publish.
311///
312/// The interest probes are CLONED OUT of the list and called with **no**
313/// [`SHARED`] borrow outstanding: a navigator's probe is a plain `Cell` read,
314/// but a [`BackFallback`]'s runs the app's interest closure, which must be free
315/// to read anything — this module included — without a `RefCell` panic.
316fn any_interest() -> bool {
317    interest_probes().iter().any(|interest| interest())
318}
319
320/// Every registrant's interest probe, in arbitration order, cloned out of
321/// [`SHARED`] so the caller can run them with no borrow outstanding.
322fn interest_probes() -> Vec<Rc<dyn Fn() -> bool>> {
323    SHARED.with(|shared| {
324        shared
325            .borrow()
326            .registrants
327            .iter()
328            .map(|r| Rc::clone(&r.interest))
329            .collect()
330    })
331}
332
333/// Register (or refresh) `controller` in the R44-back arbitration list at
334/// `role`'s rank — the controller-flavoured wrapper over [`register`]. Called on
335/// every wire from both navigator entry points
336/// ([`auto_wire`]/[`auto_wire_overlay_host`] and
337/// [`BackHandler::new`]/[`track`](BackHandler::track)).
338///
339/// # Panics
340///
341/// As [`register`].
342fn refresh_interest<State: 'static>(controller: &NavigatorController<State>, role: Role) {
343    let mounted_probe = controller.clone();
344    let interest_probe = controller.clone();
345    let request_probe = controller.clone();
346    register(
347        RegistrantId::Navigator(controller.id()),
348        role,
349        Box::new(move || mounted_probe.is_mounted()),
350        Rc::new(move || interest_probe.back_interest()),
351        Rc::new(move || request_probe.request_back()),
352    );
353}
354
355/// Register (or refresh) the registrant `id` in the R44-back arbitration list at
356/// `role`'s rank and republish the polled `handles_back` flag, registering the
357/// live provider once. The core every entry point funnels through — navigators
358/// via [`refresh_interest`], fallbacks via [`back_fallback`].
359///
360/// Ordering and pruning are the whole mechanism — see the module docs' R44-back
361/// section. In short: the entry is (re-)inserted at its ([`Role`], first-wire)
362/// sort position, so neither *when* nor *how often* a registrant wires can move
363/// it; and a repeat wire drops every **unmounted** registrant that did not wire
364/// since this registrant's previous wire, a mounted one being in the tree by
365/// definition.
366///
367/// # Panics
368///
369/// The first call registers the live provider via [`set_can_pop_provider`],
370/// which panics off the UI thread — always the case here (a rebuild / a
371/// `Component::init` runs on the UI thread).
372fn register(
373    id: RegistrantId,
374    role: Role,
375    mounted: Box<dyn Fn() -> bool>,
376    interest: Rc<dyn Fn() -> bool>,
377    request_back: Rc<dyn Fn()>,
378) {
379    SHARED.with(|shared| {
380        let mut shared = shared.borrow_mut();
381        shared.seq += 1;
382        let seq = shared.seq;
383        // Carry the existing entry's identity forward (rank sticky-upgrades to
384        // `Host`, first-wire order is preserved), and prune against its previous
385        // wire while it is still in the list.
386        let (role, birth) = match shared.registrants.iter().position(|r| r.id == id) {
387            Some(index) => {
388                let previous = shared.registrants[index].seq;
389                let carried = (
390                    // `Host` sorts first, so `min` IS the sticky upgrade: a
391                    // controller ever wired as an overlay host stays one. (A
392                    // fallback id only ever wires as `Fallback`, so `min` is a
393                    // no-op for it.)
394                    shared.registrants[index].role.min(role),
395                    shared.registrants[index].birth,
396                );
397                // A repeat wire closes a window. Any registrant that neither
398                // wired within it nor is currently mounted is no longer in the
399                // tree — drop it rather than let it keep claiming presses. The
400                // mounted probe only reads `Cell`s (a navigator's controller, a
401                // fallback's mount count and grace) and never re-enters this
402                // module, so calling it under the `SHARED` borrow is safe.
403                shared
404                    .registrants
405                    .retain(|r| r.id == id || r.seq >= previous || (r.mounted)());
406                carried
407            }
408            // First wire: this seq is the entry's permanent tiebreak.
409            None => (role, seq),
410        };
411        let entry = Registrant {
412            id,
413            role,
414            birth,
415            seq,
416            mounted,
417            interest,
418            request_back,
419        };
420        // Re-insert at the sort position, never "wherever it already was": that
421        // is what keeps arbitration order a function of what a registrant IS
422        // (its role and first wire) rather than of the wire sequence.
423        shared.registrants.retain(|r| r.id != id);
424        let at = shared
425            .registrants
426            .iter()
427            .position(|r| r.key() > entry.key())
428            .unwrap_or(shared.registrants.len());
429        shared.registrants.insert(at, entry);
430        if shared.provider.is_none() {
431            // Register the stable live provider ONCE. It reads whatever
432            // registrants are currently wired from `SHARED`, so a new
433            // registrant joining the list needs no re-registration.
434            shared.provider = Some(set_can_pop_provider(Box::new(any_interest)));
435        }
436    });
437    // Refresh the polled fallback flag (the live provider is authoritative; this
438    // only keeps the no-provider flag roughly in sync — see the module docs).
439    set_handles_back(any_interest());
440}
441
442/// Deliver a consumed back press under **R44-back**: to the first registrant in
443/// arbitration order (hosts first, then plain navigators innermost-first, then
444/// fallbacks) that claims it.
445///
446/// No visibility/reachability filter here on purpose: `back_interest()` is
447/// already gated on the claimant's hosting page being input-routed (see the
448/// module docs' R23 note), so a navigator the user cannot reach is never in this
449/// list's answer to begin with.
450///
451/// A press no registrant claims at this point is dropped: delivering it to the
452/// navigator whose wire consumed it would not be a no-op (that navigator may be
453/// deep yet unreachable, e.g. a hidden `IndexedStack` tab, or torn down). The
454/// shell already consumed the press via `handles_back`, so if every interest
455/// lapsed between that decision and delivery the press is swallowed, not
456/// returned to the platform.
457fn route_back() {
458    // Both the interest probes and the claimant's `request_back` are CLONED OUT
459    // of the list (that is what the `Rc`s are for) and called with NO `SHARED`
460    // borrow outstanding. A navigator's are a `Cell` read and an op enqueue, but
461    // a fallback's are app closures, and a re-entry into this module under a
462    // borrow would panic on the already-borrowed `RefCell`.
463    let claimant = SHARED
464        .with(|shared| {
465            shared
466                .borrow()
467                .registrants
468                .iter()
469                .map(|r| (Rc::clone(&r.interest), Rc::clone(&r.request_back)))
470                .collect::<Vec<_>>()
471        })
472        .into_iter()
473        .find(|(interest, _)| interest())
474        .map(|(_, request_back)| request_back);
475    if let Some(request_back) = claimant {
476        request_back();
477    }
478}
479
480/// Consume any new back press against the shared marker, reporting whether
481/// this call is the one that consumed it (and so must route it). The tracked
482/// read of the back-press counter subscribes THIS rebuild, so a later
483/// `push_back_press` wakes it (State & Reactivity: the track-per-rebuild
484/// contract).
485///
486/// The shared marker is the single consumption source (see the module docs): if
487/// several entry points run in the same rebuild, the first consumes the press
488/// and every later one no-ops on the already-consumed count.
489fn consume_press() -> bool {
490    // Tracked read — subscribes the rebuild to the back-press counter.
491    let count = back_presses().count.get();
492    SHARED.with(|shared| {
493        let mut shared = shared.borrow_mut();
494        match shared.consumed {
495            // A fresh process/marker: seed to the current count WITHOUT firing,
496            // so a press delivered before any handler existed does not trigger a
497            // spurious back on the first wire.
498            None => {
499                shared.consumed = Some(count);
500                false
501            }
502            // Already caught up — a re-run rebuild or the second entry point.
503            Some(prev) if prev == count => false,
504            // A new press (a burst collapses to one `request_back`: catch up to
505            // the current depth, don't replay each event — the back semantics).
506            Some(_) => {
507                shared.consumed = Some(count);
508                true
509            }
510        }
511    })
512}
513
514/// Consume any new back press ([`consume_press`]), routing it through
515/// [`NavigatorController::request_back`] (applies the top page's `BackPolicy`)
516/// on the registrant [`route_back`] arbitrates to.
517///
518/// Under R44-back the press goes to the first registrant claiming it; the
519/// wiring controller gets no special delivery, and an unclaimed press is
520/// dropped.
521fn consume_back() {
522    if consume_press() {
523        route_back();
524    }
525}
526
527/// Seed the shared consumption marker to the current back-press count if it is
528/// not seeded yet, so a press delivered before a [`BackHandler`] existed does
529/// not trigger a spurious `request_back` on the first [`track`](BackHandler::track).
530/// Idempotent (only-if-unseeded), so it composes with a `navigator()` that
531/// already seeded the marker on an earlier rebuild.
532fn seed_consumed() {
533    let count = back_presses().count.get_untracked();
534    SHARED.with(|shared| {
535        let mut shared = shared.borrow_mut();
536        if shared.consumed.is_none() {
537            shared.consumed = Some(count);
538        }
539    });
540}
541
542/// Auto-wire back handling for `controller`: the entry point the
543/// facade's [`navigator`](crate::navigator) wrapper calls on every rebuild.
544/// Consumes a new back press (routing it through `request_back`) and refreshes
545/// the `handles_back` interest — so an app using `frust::navigator` gets the
546/// full Android back contract (overlay dismiss → pop → app exit) with ZERO
547/// back-specific app code, and with no double-pop against a manual
548/// [`BackHandler`] on the same controller (see the module docs).
549///
550/// # Panics
551///
552/// Runs during a rebuild's build pass; the first call registers the live
553/// provider, which panics off the UI thread (always the UI thread here).
554pub(crate) fn auto_wire<State: 'static>(controller: &NavigatorController<State>) {
555    consume_back();
556    refresh_interest(controller, Role::Navigator);
557}
558
559/// [`auto_wire`] for a root [`overlay_host`](crate::overlay_host): identical,
560/// except the registrant takes [`Role::Host`] — the rank that puts an open
561/// root overlay ahead of the inner navigator whatever the wire sequence looks
562/// like (R44-back; see the module docs).
563///
564/// # Panics
565///
566/// As [`auto_wire`].
567pub(crate) fn auto_wire_overlay_host<State: 'static>(controller: &NavigatorController<State>) {
568    consume_back();
569    refresh_interest(controller, Role::Host);
570}
571
572/// A [`NavigatorController`] wired to the process-wide back-press source (see
573/// the module docs) — the **explicit** back-wiring surface predating the
574/// automatic [`navigator`](crate::navigator) auto-wiring.
575///
576/// App code using `frust::navigator` no longer needs this: back handling is
577/// automatic. It stays fully supported for apps that want an explicit handle
578/// (Huddle constructs one), and now shares the same single consumption source
579/// and `request_back`/`back_interest` routing as the automatic path — so
580/// constructing a `BackHandler` *and* calling `frust::navigator` on the same
581/// controller still consumes each press exactly once.
582///
583/// Construct once with [`attach_back_handler`] (or [`BackHandler::new`]) —
584/// typically from `Component::init`, storing the result in `Component::State` —
585/// then call [`track`](Self::track) from every `Component::build`.
586pub struct BackHandler<State: 'static> {
587    controller: NavigatorController<State>,
588}
589
590impl<State: 'static> BackHandler<State> {
591    /// Wire `controller` to the back-press source. Seeds the shared consumption
592    /// marker to the current back-press count (so a press delivered before this
593    /// handler existed does not trigger a spurious back on the first
594    /// [`track`](Self::track)), publishes the initial `handles_back` fallback
595    /// flag, and registers the live `back_interest` provider (see the module
596    /// docs' timing note).
597    ///
598    /// Call this once per controller (e.g. from `Component::init`) — from the UI
599    /// thread, since the provider slot is UI-thread-affine (it reads the
600    /// `Rc`-backed controller). A `Component::init` always runs on the UI thread.
601    ///
602    /// **Registration position does not depend on when you construct this**
603    /// (see the module docs' R44-back section): a handler built in
604    /// `Component::init` wires before any view exists, and a root
605    /// [`overlay_host`](crate::overlay_host) still outranks the navigator it
606    /// wraps — the host's rank comes from its own entry point, not from wire
607    /// order. Where you call [`track`](Self::track) from is likewise free.
608    pub fn new(controller: NavigatorController<State>) -> Self {
609        seed_consumed();
610        refresh_interest(&controller, Role::Navigator);
611        Self { controller }
612    }
613
614    /// Consume a new back press (routing it through
615    /// [`request_back`](NavigatorController::request_back)) and refresh the
616    /// framework's `handles_back` interest from the current stack. Call from
617    /// every `Component::build`.
618    ///
619    /// Shares the single consumption source with the automatic
620    /// [`navigator`](crate::navigator) path (see the module docs): a rebuild
621    /// re-run that observes the same already-consumed count neither re-fires nor
622    /// double-counts, and a `frust::navigator` on the same controller in the
623    /// same rebuild cannot double-pop.
624    pub fn track(&self) {
625        consume_back();
626        refresh_interest(&self.controller, Role::Navigator);
627    }
628
629    /// The wired controller — hand it to
630    /// [`navigator`](frust_widgets::navigator), or drive it directly.
631    pub fn controller(&self) -> &NavigatorController<State> {
632        &self.controller
633    }
634}
635
636/// Convenience constructor equivalent to [`BackHandler::new`] — see its docs and
637/// [`RouterDeepLinks`](crate::RouterDeepLinks)/[`router_with_deep_links`](crate::router_with_deep_links)
638/// for the mirrored API shape.
639///
640/// Note that back handling is **automatic** for any app using
641/// [`frust::navigator`](crate::navigator); this explicit surface is
642/// only needed when an app wants a handle it drives directly (e.g. Huddle).
643///
644/// ```no_run
645/// use frust::{
646///     BackHandler, Component, NavigatorController, View, attach_back_handler, navigator,
647///     text,
648/// };
649///
650/// #[derive(Default)]
651/// struct App;
652///
653/// struct AppState {
654///     back: BackHandler<AppState>,
655/// }
656///
657/// impl Component for App {
658///     type State = AppState;
659///
660///     fn init(&self) -> AppState {
661///         let nav: NavigatorController<AppState> = NavigatorController::new();
662///         AppState {
663///             back: attach_back_handler(nav),
664///         }
665///     }
666///
667///     fn build(&self, state: &mut AppState) -> impl View<AppState> {
668///         // Called every rebuild: consumes a new back press (via `request_back`)
669///         // and keeps the framework's handles-back interest in sync. The
670///         // `frust::navigator` call auto-wires the SAME controller — one press
671///         // still pops exactly once (shared consumption source).
672///         state.back.track();
673///         let controller = state.back.controller();
674///         navigator(controller, || text("home"))
675///     }
676/// }
677///
678/// frust::app!(App);
679/// # fn main() {}
680/// ```
681pub fn attach_back_handler<State: 'static>(
682    controller: NavigatorController<State>,
683) -> BackHandler<State> {
684    BackHandler::new(controller)
685}
686
687/// An app-level back rule that claims a press only when **no navigator does** —
688/// the Android/Material "back at a tab root returns to the home tab; back at the
689/// home tab root exits" convention for per-tab navigator stacks.
690///
691/// Not a navigator: it ranks after every navigator in the back arbitration
692/// (R44-back's fallback rank), whatever the wire order, so a selected tab
693/// that can still pop (or a root overlay) always takes the press first.
694///
695/// Construct once — in `Component::init` or app state — and wrap the subtree it
696/// belongs to with [`back_fallback`] from every `Component::build`. Cheaply
697/// [`Clone`] (`Rc`-backed); every clone is the same fallback.
698///
699/// **Liveness is view-scoped.** The fallback claims presses only while at least
700/// one [`back_fallback`] wrapper over it is mounted: leaving the screen unmounts
701/// the wrapper, the fallback stops claiming at once, and the arbitration list
702/// releases it (see the module docs' release rules). Merely holding the handle
703/// claims nothing.
704///
705/// The facade's re-export docs carry a back-to-home-tab example and the
706/// accepted residual (a wrapper inside a hidden branch still counts as live).
707#[derive(Clone)]
708pub struct BackFallback {
709    inner: Rc<FallbackInner>,
710}
711
712/// The shared state behind a [`BackFallback`] and all its clones.
713struct FallbackInner {
714    /// This fallback's arbitration identity ([`RegistrantId::Fallback`]).
715    id: u64,
716    /// How many [`BackFallbackView`] wrappers over this fallback are mounted
717    /// right now — `build` increments it, `teardown` decrements it. The
718    /// fallback's rule-1 liveness: `> 0` is "in the retained tree".
719    mounted: Cell<usize>,
720    /// The first-wire grace (see the module docs): set by every
721    /// [`back_fallback`] wire, cleared by any wrapper's `build`, `rebuild` or
722    /// `teardown`. While set, the fallback counts as mounted even with
723    /// `mounted == 0` — it was wired by a wrapper that has not been built yet.
724    awaiting_build: Cell<bool>,
725    /// The app's "do I claim the next press?" closure.
726    interest: Box<dyn Fn() -> bool>,
727    /// The app's back action, run when this fallback receives a press.
728    on_back: Box<dyn Fn()>,
729}
730
731impl FallbackInner {
732    /// Rule-1 liveness plus the first-wire grace: a mounted wrapper, or a wire
733    /// whose wrapper has not been built yet.
734    fn live(&self) -> bool {
735        self.mounted.get() > 0 || self.awaiting_build.get()
736    }
737
738    /// One wrapper reconciled (built, rebuilt or torn down): the grace ends.
739    fn reconciled(&self) {
740        self.awaiting_build.set(false);
741    }
742}
743
744impl BackFallback {
745    /// Create a fallback that claims a back press while `interest()` is true
746    /// (and a [`back_fallback`] wrapper over it is mounted), running `on_back`
747    /// for each press it receives.
748    ///
749    /// Call once — from `Component::init` or wherever app state is built, on
750    /// the UI thread. Seeds the shared consumption marker to the current
751    /// back-press count, like [`BackHandler::new`], so a press delivered before
752    /// this fallback existed does not fire it. Registers nothing yet: a
753    /// fallback joins the arbitration when [`back_fallback`] first wraps a view
754    /// with it.
755    ///
756    /// `interest` is polled whenever the shell asks whether the app handles
757    /// back, and before each press is routed, with no back-arbitration borrow
758    /// outstanding; keep it a cheap, untracked read (`get_untracked`,
759    /// [`NavigatorController::depth`]). `on_back` runs during a rebuild's build
760    /// pass, like a navigator's `request_back`.
761    pub fn new(interest: impl Fn() -> bool + 'static, on_back: impl Fn() + 'static) -> Self {
762        seed_consumed();
763        let id = NEXT_FALLBACK_ID.with(|next| {
764            let id = next.get();
765            next.set(id + 1);
766            id
767        });
768        Self {
769            inner: Rc::new(FallbackInner {
770                id,
771                mounted: Cell::new(0),
772                awaiting_build: Cell::new(false),
773                interest: Box::new(interest),
774                on_back: Box::new(on_back),
775            }),
776        }
777    }
778
779    /// Whether this fallback currently counts as live: a wrapper over it is
780    /// mounted, or one was just wired and is awaiting its first build.
781    #[cfg(test)]
782    fn is_live(&self) -> bool {
783        self.inner.live()
784    }
785
786    /// Register (or refresh) this fallback in the R44-back arbitration list at
787    /// [`Role::Fallback`], and open the first-wire grace (see the module docs).
788    /// The probes hold only a [`Weak`](std::rc::Weak) reference, so the
789    /// arbitration list never keeps the app's closures alive.
790    fn wire(&self) {
791        self.inner.awaiting_build.set(true);
792        let mounted_probe = Rc::downgrade(&self.inner);
793        let interest_probe = Rc::downgrade(&self.inner);
794        let request_probe = Rc::downgrade(&self.inner);
795        register(
796            RegistrantId::Fallback(self.inner.id),
797            Role::Fallback,
798            Box::new(move || mounted_probe.upgrade().is_some_and(|inner| inner.live())),
799            Rc::new(move || {
800                interest_probe
801                    .upgrade()
802                    .is_some_and(|inner| inner.live() && (inner.interest)())
803            }),
804            Rc::new(move || {
805                if let Some(inner) = request_probe.upgrade() {
806                    (inner.on_back)();
807                }
808            }),
809        );
810    }
811}
812
813/// Wrap `child` so `fallback` takes part in back arbitration while this view is
814/// mounted — the view-scoped half of [`BackFallback`].
815///
816/// Call from every `Component::build`, exactly like
817/// [`navigator`](crate::navigator): at call time it consumes any new back press
818/// (the tracked read that subscribes the rebuild, routed under R44-back, so the
819/// fallback receives it only if no navigator claims it) and then registers or
820/// refreshes the fallback at the fallback rank. The returned
821/// [`BackFallbackView`] is a pass-through: it renders `child` unchanged and
822/// counts its own mounts, which is the fallback's liveness. It is a [`View`]
823/// for every `State` its child is one for, so the function itself names no
824/// `State` and a state-agnostic child (a `text`) needs no annotation.
825///
826/// The facade's re-export docs carry a back-to-home-tab example.
827///
828/// # Panics
829///
830/// Runs during a rebuild's build pass; the first wire registers the live
831/// `handles_back` provider, which panics off the UI thread (always the UI
832/// thread here).
833pub fn back_fallback<V>(fallback: &BackFallback, child: V) -> BackFallbackView<V> {
834    if consume_press() {
835        route_back();
836    }
837    fallback.wire();
838    BackFallbackView {
839        fallback: fallback.clone(),
840        child,
841    }
842}
843
844/// The pass-through view [`back_fallback`] returns: renders its child unchanged
845/// (`Element` is the child's own) and counts its own mounts into the
846/// [`BackFallback`]'s liveness — `build` increments it before the child builds,
847/// `teardown` decrements it after the child is torn down.
848pub struct BackFallbackView<V> {
849    fallback: BackFallback,
850    child: V,
851}
852
853impl<State: 'static, V: View<State>> View<State> for BackFallbackView<V> {
854    type Element = V::Element;
855
856    fn build(&self, ctx: &mut BuildCtx<'_>) -> Self::Element {
857        // Publish liveness FIRST, like a navigator's `build`: anything wiring
858        // from inside the child already sees this fallback as mounted.
859        let inner = &self.fallback.inner;
860        inner.mounted.set(inner.mounted.get() + 1);
861        inner.reconciled();
862        self.child.build(ctx)
863    }
864
865    fn rebuild(
866        &self,
867        prev: &Self,
868        element: &mut Self::Element,
869        ctx: &mut BuildCtx<'_>,
870    ) -> ChangeFlags {
871        let (next, old) = (&self.fallback.inner, &prev.fallback.inner);
872        if !Rc::ptr_eq(next, old) {
873            // The slot now wraps a DIFFERENT fallback: move this mount from the
874            // old handle to the new one, so `teardown` (which runs through the
875            // latest view) decrements the count it pairs with.
876            next.mounted.set(next.mounted.get() + 1);
877            old.mounted.set(old.mounted.get().saturating_sub(1));
878            old.reconciled();
879        }
880        next.reconciled();
881        self.child.rebuild(&prev.child, element, ctx)
882    }
883
884    fn teardown(&self, element: &mut Self::Element, ctx: &mut BuildCtx<'_>) {
885        self.child.teardown(element, ctx);
886        let inner = &self.fallback.inner;
887        inner.mounted.set(inner.mounted.get().saturating_sub(1));
888        inner.reconciled();
889    }
890
891    fn message(&self, element: &mut Self::Element, state: &mut State) {
892        self.child.message(element, state);
893    }
894}
895
896#[cfg(test)]
897mod tests {
898    use super::*;
899    use crate::motion::patterns::SharedAxis;
900    use crate::motion::switcher::PatternSwitcherView;
901    use frust_core::{AnyView, RenderRoot, any};
902    use frust_reactive::{
903        ReactiveRuntime, clear_can_pop_provider, handles_back, push_back_press, set_handles_back,
904    };
905    use frust_widgets::{
906        BackPolicy, NavigatorController, NavigatorView, PushOptions, navigator as raw_navigator,
907        overlay_host as raw_overlay_host,
908    };
909    use std::sync::{Arc, Mutex};
910
911    /// Serializes every test in this module: they all touch `frust-reactive`'s
912    /// process-wide back-press counter / `handles_back` flag / live-provider slot
913    /// AND this module's `SHARED` marker, so concurrent runs would race the
914    /// shared counter (one test's `push_back_press` advancing it under another's
915    /// feet). `reset()` (below) additionally clears the per-thread state a reused
916    /// harness thread would otherwise carry between tests.
917    static TEST_LOCK: Mutex<()> = Mutex::new(());
918
919    /// Reset the per-thread wiring so each test starts clean: force-clear
920    /// frust-reactive's live provider, drop the shared marker/interest/provider,
921    /// and reset the polled flag. Call at the top of every test (after taking
922    /// [`TEST_LOCK`], under an initialized runtime).
923    fn reset() {
924        clear_can_pop_provider();
925        set_handles_back(false);
926        SHARED.with(|shared| *shared.borrow_mut() = SharedBack::new());
927    }
928
929    fn page() -> AnyView<()> {
930        any(frust_widgets::text("x"))
931    }
932
933    /// The build closure type [`RenderRoot::rebuild`] drives, boxed so
934    /// [`Harness`] can store it (mirroring `router_glue`'s `AppLogic`).
935    type AppLogic = Box<dyn FnMut(&mut ()) -> NavigatorView<()>>;
936
937    /// A `RenderRoot`/app closure driving a controller's `navigator`. The
938    /// `wire` closure runs each rebuild BEFORE reconciliation to exercise a back
939    /// entry point (the automatic `auto_wire`, an explicit `BackHandler::track`,
940    /// or both) exactly as a `Component::build` would.
941    struct Harness {
942        controller: NavigatorController<()>,
943        root: RenderRoot<(), NavigatorView<()>>,
944        app: AppLogic,
945        wire: Box<dyn FnMut()>,
946    }
947
948    impl Harness {
949        /// A fresh-controller harness whose per-rebuild wiring runs `wire`
950        /// (given a clone of that controller) — the seam each test uses to pick
951        /// its back entry point.
952        fn new(wire: impl FnMut(&NavigatorController<()>) + 'static) -> Self {
953            Self::with_controller(NavigatorController::new(), wire)
954        }
955
956        /// As [`new`](Self::new) but drives an EXISTING `controller` — so a test
957        /// can share one controller between a `BackHandler` and the navigator
958        /// (the manual + auto criterion).
959        fn with_controller(
960            controller: NavigatorController<()>,
961            mut wire: impl FnMut(&NavigatorController<()>) + 'static,
962        ) -> Self {
963            let app_controller = controller.clone();
964            let app: AppLogic = Box::new(move |_: &mut ()| raw_navigator(&app_controller, page));
965            let wire_controller = controller.clone();
966            let wire: Box<dyn FnMut()> = Box::new(move || wire(&wire_controller));
967            let mut harness = Harness {
968                controller,
969                root: RenderRoot::new(),
970                app,
971                wire,
972            };
973            harness.rebuild();
974            harness
975        }
976
977        fn rebuild(&mut self) {
978            // Wire under the build pass, exactly as a Component::build would,
979            // then reconcile (which applies the navigator's queued ops).
980            (self.wire)();
981            self.root.rebuild(&mut self.app, &mut ());
982        }
983
984        fn depth(&self) -> usize {
985            self.controller.depth()
986        }
987    }
988
989    /// Criterion 1: an app using the AUTOMATIC `frust::navigator` auto-wiring
990    /// (no `BackHandler` in app code) pops one page on a back press. Also
991    /// covers criterion 2 (depth 1, no overlay → `handles_back()` false: the
992    /// press falls through to the platform).
993    #[test]
994    fn auto_wire_pops_without_a_backhandler() {
995        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
996        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
997        reset();
998
999        let mut h = Harness::new(auto_wire);
1000
1001        // Criterion 2: at the root (depth 1, no overlay) the framework does NOT
1002        // handle back — a root-level press bubbles to the platform.
1003        assert_eq!(h.depth(), 1, "starts at the root page");
1004        assert!(!handles_back(), "depth 1 + no overlay: handles_back false");
1005
1006        // Push to depth 2 (a normal opaque push → BackPolicy::Pop).
1007        h.controller.push(page);
1008        h.rebuild();
1009        assert_eq!(h.depth(), 2, "pushed to depth 2");
1010        assert!(
1011            handles_back(),
1012            "poppable stack: handles_back true, same rebuild"
1013        );
1014
1015        // Criterion 1: one back press pops one page — with NO BackHandler.
1016        push_back_press();
1017        h.rebuild();
1018        assert_eq!(h.depth(), 1, "auto-wired back press pops one page");
1019        assert!(
1020            !handles_back(),
1021            "back at the root: handles_back false again"
1022        );
1023    }
1024
1025    /// Criterion 3: a `Veto` overlay claims the back press (`handles_back()`
1026    /// true — predictive-back parity via `back_interest`), the press is
1027    /// consumed, and the stack is UNCHANGED — the routing-through-`request_back`
1028    /// (not a bare `pop`) contract. This is the meaningful facade-reachable
1029    /// form of "depth 1 + Veto overlay": the overlay sits over the root, and
1030    /// even though a raw `pop()` *would* remove it (the stack is poppable),
1031    /// `request_back` honors the Veto policy and pops nothing. (A Veto policy on
1032    /// the depth-1 *root* itself is not expressible through the public push API
1033    /// — the root is always `BackPolicy::Pop` — and is covered by a
1034    /// widget-level `compute_back_interest` test.)
1035    #[test]
1036    fn veto_overlay_claims_back_but_does_not_pop() {
1037        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1038        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
1039        reset();
1040
1041        let mut h = Harness::new(auto_wire);
1042
1043        // Push a transparent Veto overlay over the root.
1044        h.controller
1045            .push_with_options(page, PushOptions::transparent().back(BackPolicy::Veto));
1046        h.rebuild();
1047
1048        // The overlay claims back ahead-of-time (facade reads `back_interest`).
1049        assert!(handles_back(), "a Veto overlay claims the back press");
1050
1051        let before = h.depth();
1052        // A back press is consumed but Veto swallows it — the stack is unchanged.
1053        // A bare `pop()` here would have removed the overlay (regression guard
1054        // for the request_back-not-pop switch).
1055        push_back_press();
1056        h.rebuild();
1057        assert_eq!(
1058            h.depth(),
1059            before,
1060            "Veto consumes the press via request_back: stack unchanged (not popped)"
1061        );
1062        assert!(handles_back(), "still claiming back (overlay still on top)");
1063    }
1064
1065    /// The EXPLICIT `BackHandler` path still works on its own (regression for
1066    /// the original surface, now routing through `request_back`): a press
1067    /// pops one page and `handles_back()` tracks the depth with no settle frame
1068    /// (the live provider closes the stale-window).
1069    #[test]
1070    fn explicit_backhandler_pops_and_tracks_handles_back() {
1071        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1072        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
1073        reset();
1074
1075        let controller: NavigatorController<()> = NavigatorController::new();
1076        let back = BackHandler::new(controller.clone());
1077        // Only the explicit handler drives back here — on the SAME controller the
1078        // harness's navigator uses (a raw navigator view, no auto-wiring).
1079        let mut h = Harness::with_controller(controller, move |_c| back.track());
1080
1081        assert_eq!(h.depth(), 1, "starts at the root");
1082        assert!(
1083            !handles_back(),
1084            "root-level back bubbles: handles_back false"
1085        );
1086
1087        h.controller.push(page);
1088        h.rebuild();
1089        assert_eq!(h.depth(), 2, "push published to depth immediately");
1090        assert!(
1091            handles_back(),
1092            "poppable stack reports handles_back true on the SAME rebuild, no settle frame"
1093        );
1094
1095        push_back_press();
1096        h.rebuild();
1097        assert_eq!(h.depth(), 1, "one back press pops one page");
1098        assert!(
1099            !handles_back(),
1100            "root again: handles_back false immediately"
1101        );
1102
1103        // A back press at the root is a safe no-op (consumed, pops nothing).
1104        push_back_press();
1105        h.rebuild();
1106        assert_eq!(h.depth(), 1, "pop-at-root is a no-op");
1107
1108        // After a force-clear the polled fallback flag answers (depth-1 false).
1109        clear_can_pop_provider();
1110        assert!(
1111            !handles_back(),
1112            "after clear, handles_back reads the polled fallback"
1113        );
1114    }
1115
1116    /// Criterion 4 (regression): a manual `BackHandler` AND the automatic
1117    /// `navigator()` auto-wiring on the SAME controller consume one press
1118    /// exactly once — no double-pop. Both run every rebuild (as Huddle does:
1119    /// `state.back.track()` then `frust::navigator(&controller, ...)`).
1120    #[test]
1121    fn manual_backhandler_plus_auto_wire_pops_once() {
1122        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1123        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
1124        reset();
1125
1126        // Construct the manual handler first (as an app would, in init).
1127        let controller: NavigatorController<()> = NavigatorController::new();
1128        let back = BackHandler::new(controller.clone());
1129
1130        // The harness's navigator AND both back entry points drive the SAME
1131        // controller. Each rebuild runs BOTH entry points in Huddle's order: the
1132        // manual `track()` then the auto-wiring (`c` is the harness controller).
1133        let mut h = Harness::with_controller(controller, move |c| {
1134            back.track();
1135            auto_wire(c);
1136        });
1137
1138        // Push to depth 3 so a single-vs-double pop is unambiguous.
1139        h.controller.push(page);
1140        h.rebuild();
1141        h.controller.push(page);
1142        h.rebuild();
1143        assert_eq!(h.depth(), 3, "pushed to depth 3");
1144
1145        // ONE back press: with a shared consumption source this pops exactly one
1146        // page (depth 2). A double-consumption bug would pop two (depth 1).
1147        push_back_press();
1148        h.rebuild();
1149        assert_eq!(
1150            h.depth(),
1151            2,
1152            "manual + auto share one consumption source: one press pops once"
1153        );
1154
1155        // A rebuild with no new press does not re-pop (dedupe).
1156        h.rebuild();
1157        assert_eq!(h.depth(), 2, "no new press -> no extra pop");
1158    }
1159
1160    // -----------------------------------------------------------------------
1161    // R44-back — arbitration across a root overlay host and an inner navigator.
1162    // -----------------------------------------------------------------------
1163
1164    /// A root `overlay_host` wrapping an inner `navigator`, each auto-wired
1165    /// where its view is constructed: the host from the app's build pass, the
1166    /// inner navigator from the host's root-page builder — which runs inside
1167    /// the host's own reconcile, i.e. strictly later. That *is* the build order
1168    /// R44-back arbitrates on, so the harness reproduces the real ordering
1169    /// rather than hand-declaring it.
1170    struct HostHarness {
1171        host: NavigatorController<()>,
1172        inner: NavigatorController<()>,
1173        root: RenderRoot<(), NavigatorView<()>>,
1174        app: AppLogic,
1175    }
1176
1177    impl HostHarness {
1178        fn new() -> Self {
1179            let host: NavigatorController<()> = NavigatorController::new();
1180            let inner: NavigatorController<()> = NavigatorController::new();
1181            let app: AppLogic = {
1182                let host = host.clone();
1183                let inner = inner.clone();
1184                Box::new(move |_: &mut ()| {
1185                    // Outermost: exactly what `frust::overlay_host` does.
1186                    auto_wire_overlay_host(&host);
1187                    let inner = inner.clone();
1188                    raw_overlay_host(&host, move || {
1189                        // Innermost: exactly what `frust::navigator` does.
1190                        auto_wire(&inner);
1191                        any(raw_navigator(&inner, page))
1192                    })
1193                })
1194            };
1195            let mut harness = Self {
1196                host,
1197                inner,
1198                root: RenderRoot::new(),
1199                app,
1200            };
1201            harness.rebuild();
1202            harness
1203        }
1204
1205        fn rebuild(&mut self) {
1206            self.root.rebuild(&mut self.app, &mut ());
1207        }
1208    }
1209
1210    /// The R44-back arbitration list, in order — the whitebox view the ordering
1211    /// and pruning rules are pinned against.
1212    fn registrant_ids() -> Vec<frust_widgets::NavigatorId> {
1213        registrants()
1214            .into_iter()
1215            .filter_map(|id| match id {
1216                RegistrantId::Navigator(id) => Some(id),
1217                RegistrantId::Fallback(_) => None,
1218            })
1219            .collect()
1220    }
1221
1222    /// The whole arbitration list, fallbacks included, in order.
1223    fn registrants() -> Vec<RegistrantId> {
1224        SHARED.with(|shared| shared.borrow().registrants.iter().map(|r| r.id).collect())
1225    }
1226
1227    /// **R44-back**: with a root overlay open, the press goes to the HOST even
1228    /// though the inner navigator is poppable and wired *later*. Without the
1229    /// rank the last wire would win outright and this press would pop the inner
1230    /// navigator — a page vanishing under an open modal.
1231    #[test]
1232    fn a_root_overlay_takes_the_back_press_from_the_inner_navigator() {
1233        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1234        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
1235        reset();
1236
1237        let mut h = HostHarness::new();
1238        // Registration order is build order: the host first, then the navigator
1239        // its root page builds.
1240        assert_eq!(
1241            registrant_ids(),
1242            vec![h.host.id(), h.inner.id()],
1243            "outermost first"
1244        );
1245
1246        // Both stacks are poppable: the inner navigator has a page pushed, and
1247        // an overlay sits on the host.
1248        h.inner.push(page);
1249        h.rebuild();
1250        h.host
1251            .push_with_options(page, PushOptions::transparent().back(BackPolicy::Pop));
1252        h.rebuild();
1253        assert_eq!((h.host.depth(), h.inner.depth()), (2, 2));
1254        assert!(handles_back(), "some registrant claims the press");
1255
1256        push_back_press();
1257        h.rebuild();
1258        assert_eq!(h.host.depth(), 1, "the press went to the root overlay host");
1259        assert_eq!(
1260            h.inner.depth(),
1261            2,
1262            "and NOT to the inner navigator (the root-modal back bug)"
1263        );
1264    }
1265
1266    /// **The degenerate case, bit for bit.** A host with no overlays sits at
1267    /// depth 1 with the default `BackPolicy::Pop`, so `compute_back_interest`
1268    /// is false and it claims nothing — the press falls straight through to the
1269    /// inner navigator, exactly as before the host existed. This is the
1270    /// single-navigator (huddle) regression guard.
1271    #[test]
1272    fn a_host_with_no_overlay_defers_the_back_press_to_the_inner_navigator() {
1273        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1274        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
1275        reset();
1276
1277        let mut h = HostHarness::new();
1278        assert!(
1279            !handles_back(),
1280            "two navigators, both at the root: back bubbles to the platform"
1281        );
1282
1283        h.inner.push(page);
1284        h.rebuild();
1285        assert_eq!(h.host.depth(), 1, "the host stays empty");
1286        assert!(
1287            handles_back(),
1288            "the inner navigator's interest answers for the whole app"
1289        );
1290
1291        push_back_press();
1292        h.rebuild();
1293        assert_eq!(h.inner.depth(), 1, "the press popped the inner navigator");
1294        assert_eq!(h.host.depth(), 1, "the empty host was untouched");
1295        assert!(!handles_back(), "back at the root of both: bubbles again");
1296
1297        // A press with nothing to do stays a safe no-op (nobody claims it, so it
1298        // is dropped).
1299        push_back_press();
1300        h.rebuild();
1301        assert_eq!((h.host.depth(), h.inner.depth()), (1, 1));
1302    }
1303
1304    /// The predictive-back half of R44-back: an overlay on the host claims the
1305    /// press even when the inner navigator has nothing to pop. `handles_back`
1306    /// answers `any(interest)`, not "the last navigator wired" — under the
1307    /// latter the shell would be told nobody handles back and would *exit the
1308    /// app* with a root modal open.
1309    #[test]
1310    fn a_root_overlay_claims_back_even_with_the_inner_navigator_at_its_root() {
1311        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1312        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
1313        reset();
1314
1315        let mut h = HostHarness::new();
1316        h.host
1317            .push_with_options(page, PushOptions::transparent().back(BackPolicy::Pop));
1318        h.rebuild();
1319        assert_eq!((h.host.depth(), h.inner.depth()), (2, 1));
1320        assert!(
1321            handles_back(),
1322            "the root overlay claims the press though the inner navigator cannot pop"
1323        );
1324
1325        push_back_press();
1326        h.rebuild();
1327        assert_eq!(h.host.depth(), 1, "the press dismissed the root overlay");
1328        assert!(!handles_back(), "nothing left to claim it");
1329    }
1330
1331    /// The one-cycle prune: a navigator that stops wiring (its screen torn down)
1332    /// is dropped from the arbitration list rather than claiming presses
1333    /// forever from a stack nothing renders. These controllers are never mounted
1334    /// (no widget builds them), so the mounted veto never applies and this is
1335    /// the pure one-cycle path.
1336    ///
1337    /// All three controllers here register as plain `Role::Navigator` peers —
1338    /// despite the `outer`/`inner`/`nested` names, nothing wires one *inside*
1339    /// another — so this test pins only the birth tiebreak among equal-rank
1340    /// registrants (innermost/last-wire first), not `Role::Host`-first ranking
1341    /// (that is
1342    /// `an_init_registered_backhandler_does_not_outrank_a_later_overlay_host`).
1343    /// The expected order below is therefore the REVERSE of build order, by
1344    /// design.
1345    #[test]
1346    fn a_navigator_that_stops_wiring_is_pruned_after_one_full_cycle() {
1347        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1348        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
1349        reset();
1350
1351        let outer: NavigatorController<()> = NavigatorController::new();
1352        let inner: NavigatorController<()> = NavigatorController::new();
1353        let nested: NavigatorController<()> = NavigatorController::new();
1354
1355        // Frame 1: three navigators wire, in build order.
1356        refresh_interest(&outer, Role::Navigator);
1357        refresh_interest(&inner, Role::Navigator);
1358        refresh_interest(&nested, Role::Navigator);
1359        assert_eq!(
1360            registrant_ids(),
1361            vec![nested.id(), inner.id(), outer.id()],
1362            "innermost/last-wired first: the reverse of build order"
1363        );
1364
1365        // Frame 2: the nested navigator's screen is gone — it never wires again.
1366        refresh_interest(&outer, Role::Navigator);
1367        refresh_interest(&inner, Role::Navigator);
1368        assert_eq!(
1369            registrant_ids(),
1370            vec![nested.id(), inner.id(), outer.id()],
1371            "not yet: a full cycle has not elapsed without it"
1372        );
1373
1374        // Frame 3: the outer wire now spans a whole cycle in which `nested`
1375        // never appeared.
1376        refresh_interest(&outer, Role::Navigator);
1377        assert_eq!(
1378            registrant_ids(),
1379            vec![inner.id(), outer.id()],
1380            "the departed navigator is pruned, and its controller clone released"
1381        );
1382
1383        // Re-wiring an existing registrant never re-orders it.
1384        refresh_interest(&inner, Role::Navigator);
1385        refresh_interest(&outer, Role::Navigator);
1386        refresh_interest(&inner, Role::Navigator);
1387        assert_eq!(registrant_ids(), vec![inner.id(), outer.id()]);
1388    }
1389
1390    // -----------------------------------------------------------------------
1391    // The two ways a "a repeat wire proves a build cycle" premise breaks — a
1392    // controller wiring TWICE per pass, and a `BackHandler` wiring from
1393    // `Component::init` before any view exists.
1394    // -----------------------------------------------------------------------
1395
1396    /// The REAL Huddle shape under a root overlay host: the inner navigator is
1397    /// wired **twice in one build pass** — an explicit `BackHandler` built in
1398    /// `Component::init` and `track()`ed from the host's page builder, plus the
1399    /// automatic `navigator()` wiring right beside it
1400    /// (`examples/huddle/src/lib.rs` does exactly this pair).
1401    ///
1402    /// Without the mounted veto, the second wire's one-cycle prune reads the
1403    /// FIRST wire of the same pass as a cycle boundary and drops the host, which
1404    /// then re-appends *after* the inner navigator — permanently inverting
1405    /// arbitration and re-creating the root-modal back bug (a press with a root
1406    /// modal open popping the page underneath it).
1407    #[test]
1408    fn an_overlay_host_survives_an_inner_navigator_wiring_twice_per_pass() {
1409        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1410        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
1411        reset();
1412
1413        let host: NavigatorController<()> = NavigatorController::new();
1414        let inner: NavigatorController<()> = NavigatorController::new();
1415        // `Component::init`: the explicit handler exists before any view does.
1416        let back = Rc::new(BackHandler::new(inner.clone()));
1417
1418        let mut app: AppLogic = {
1419            let host = host.clone();
1420            let inner = inner.clone();
1421            Box::new(move |_: &mut ()| {
1422                // The app's build pass: `frust::overlay_host`.
1423                auto_wire_overlay_host(&host);
1424                let inner = inner.clone();
1425                let back = Rc::clone(&back);
1426                raw_overlay_host(&host, move || {
1427                    // The host's page builder — Huddle's own build body, moved
1428                    // inside the host: `state.back.track()` then
1429                    // `frust::navigator(&controller, …)`, one pass, one
1430                    // controller, TWO wires.
1431                    back.track();
1432                    auto_wire(&inner);
1433                    any(raw_navigator(&inner, page))
1434                })
1435            })
1436        };
1437        let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
1438        root.rebuild(&mut app, &mut ());
1439
1440        assert_eq!(
1441            registrant_ids(),
1442            vec![host.id(), inner.id()],
1443            "the host outranks the inner navigator despite wiring once to its twice"
1444        );
1445
1446        // An overlay over the whole app, and a poppable inner stack under it.
1447        inner.push(page);
1448        host.push_with_options(page, PushOptions::transparent().back(BackPolicy::Pop));
1449        // Two more passes: the ordering must survive repeated double-wiring.
1450        for _ in 0..2 {
1451            root.rebuild(&mut app, &mut ());
1452        }
1453        assert_eq!((host.depth(), inner.depth()), (2, 2));
1454        assert_eq!(
1455            registrant_ids(),
1456            vec![host.id(), inner.id()],
1457            "still outermost-first after repeated passes"
1458        );
1459        assert!(
1460            handles_back(),
1461            "the shell is told the app handles back (a root modal is open)"
1462        );
1463
1464        push_back_press();
1465        root.rebuild(&mut app, &mut ());
1466        assert_eq!(host.depth(), 1, "the press dismissed the root overlay");
1467        assert_eq!(
1468            inner.depth(),
1469            2,
1470            "and NOT the page under it (the root-modal back bug)"
1471        );
1472    }
1473
1474    /// The second ordering break: `BackHandler::new` wires from
1475    /// `Component::init`, which runs before ANY view is constructed — so a
1476    /// first-wire-order rule alone would give the inner navigator position 0
1477    /// from frame 1, and no amount of "track from inside the host's page
1478    /// builder" advice could fix a registration that already happened. Rank
1479    /// comes from the entry point instead, so the host still sorts first.
1480    #[test]
1481    fn an_init_registered_backhandler_does_not_outrank_a_later_overlay_host() {
1482        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1483        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
1484        reset();
1485
1486        let host: NavigatorController<()> = NavigatorController::new();
1487        let inner: NavigatorController<()> = NavigatorController::new();
1488
1489        // `Component::init` — the inner navigator's handler registers FIRST.
1490        let back = BackHandler::new(inner.clone());
1491        assert_eq!(registrant_ids(), vec![inner.id()], "init wired it alone");
1492
1493        // The first `Component::build`: the host wires only now.
1494        auto_wire_overlay_host(&host);
1495        assert_eq!(
1496            registrant_ids(),
1497            vec![host.id(), inner.id()],
1498            "the host claims the outermost slot even though it wired second"
1499        );
1500
1501        // And a repeat pass keeps it there.
1502        auto_wire_overlay_host(&host);
1503        back.track();
1504        assert_eq!(registrant_ids(), vec![host.id(), inner.id()]);
1505    }
1506
1507    /// The mounted veto's other half: it only ever *delays* release. A
1508    /// navigator whose widget is torn down (its page rebuilt without it) reports
1509    /// unmounted, so the one-cycle rule drops it and releases the controller
1510    /// clone the registrant holds.
1511    ///
1512    /// `nested` genuinely lives inside `outer`'s own page here, so it wires
1513    /// after `outer` and — under innermost-first `Role::Navigator` order — sorts
1514    /// AHEAD of it; that ordering is incidental to this test's own point
1515    /// (mount-liveness pruning).
1516    #[test]
1517    fn a_torn_down_navigator_stops_vetoing_the_prune_and_is_released() {
1518        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1519        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
1520        reset();
1521
1522        let outer: NavigatorController<()> = NavigatorController::new();
1523        let nested: NavigatorController<()> = NavigatorController::new();
1524        // The nested navigator lives inside the outer navigator's root page,
1525        // until this flag turns it off.
1526        let show_nested = Rc::new(std::cell::Cell::new(true));
1527        // The arbitration list as seen MID-PASS, at the moment the outer
1528        // navigator's page rebuilds — i.e. inside the window a spurious prune
1529        // opens, when the shell may read `handles_back` or a press may route.
1530        let mid_pass: Rc<RefCell<Vec<NavigatorId>>> = Rc::new(RefCell::new(Vec::new()));
1531
1532        let mut app: AppLogic = {
1533            let outer = outer.clone();
1534            let nested = nested.clone();
1535            let show_nested = Rc::clone(&show_nested);
1536            let mid_pass = Rc::clone(&mid_pass);
1537            Box::new(move |_: &mut ()| {
1538                // The outer navigator wires TWICE per pass (Huddle's
1539                // `track()` + `navigator()` pair), so the one-cycle rule alone
1540                // would prune the nested navigator on every second wire — the
1541                // mounted veto is the only reason it survives below.
1542                auto_wire(&outer);
1543                auto_wire(&outer);
1544                let nested = nested.clone();
1545                let show_nested = Rc::clone(&show_nested);
1546                let mid_pass = Rc::clone(&mid_pass);
1547                raw_navigator(&outer, move || {
1548                    *mid_pass.borrow_mut() = registrant_ids();
1549                    if show_nested.get() {
1550                        auto_wire(&nested);
1551                        any(raw_navigator(&nested, page))
1552                    } else {
1553                        page()
1554                    }
1555                })
1556            })
1557        };
1558        let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
1559        root.rebuild(&mut app, &mut ());
1560        assert!(nested.is_mounted(), "the nested navigator is in the tree");
1561        assert_eq!(registrant_ids(), vec![nested.id(), outer.id()]);
1562
1563        // While mounted it survives the outer navigator's double wire — the
1564        // exact sequence the one-cycle rule alone misreads as a build cycle.
1565        root.rebuild(&mut app, &mut ());
1566        root.rebuild(&mut app, &mut ());
1567        assert_eq!(
1568            registrant_ids(),
1569            vec![nested.id(), outer.id()],
1570            "a mounted navigator is in the tree by definition: never pruned"
1571        );
1572        assert_eq!(
1573            *mid_pass.borrow(),
1574            vec![nested.id(), outer.id()],
1575            "and it is never MISSING mid-pass either — the window in which a \
1576             spurious prune would mis-answer handles_back / mis-route a press"
1577        );
1578
1579        // Its page rebuilds without it: the widget tears down.
1580        show_nested.set(false);
1581        root.rebuild(&mut app, &mut ());
1582        assert!(!nested.is_mounted(), "teardown dropped the liveness");
1583
1584        // Now the one-cycle rule reaches it (the outer wire that spans a whole
1585        // cycle in which the nested navigator never appeared).
1586        root.rebuild(&mut app, &mut ());
1587        root.rebuild(&mut app, &mut ());
1588        assert_eq!(
1589            registrant_ids(),
1590            vec![outer.id()],
1591            "the departed navigator is released, not kept alive by the veto"
1592        );
1593    }
1594
1595    // -----------------------------------------------------------------------
1596    // Innermost-first: a nested navigator must receive the back press.
1597    // -----------------------------------------------------------------------
1598
1599    /// An outer navigator PUSHES a page that hosts a nested navigator (the outer
1600    /// navigator is at depth > 1 — exactly what makes `compute_back_interest`
1601    /// true for it too), and the nested navigator's own stack is also poppable.
1602    /// A back press must reach the INNERMOST (nested) navigator, popping its
1603    /// stack, and must leave the outer navigator's depth untouched — the reverse
1604    /// of what plain build-order (outermost-first) arbitration gives, where the
1605    /// outer navigator, wired first, claims the press and pops the whole page
1606    /// hosting the nested navigator instead of reaching into it.
1607    #[test]
1608    fn a_nested_navigator_inside_a_pushed_page_gets_the_back_press() {
1609        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1610        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
1611        reset();
1612
1613        let outer: NavigatorController<()> = NavigatorController::new();
1614        let inner: NavigatorController<()> = NavigatorController::new();
1615
1616        // The outer navigator's ROOT page is plain — no nested navigator
1617        // exists at depth 1, so `compute_back_interest` starts false for it
1618        // (matching the module docs: the bug only appears once the outer
1619        // navigator itself is poppable).
1620        let mut app: AppLogic = {
1621            let outer_c = outer.clone();
1622            Box::new(move |_: &mut ()| {
1623                auto_wire(&outer_c);
1624                raw_navigator(&outer_c, page)
1625            })
1626        };
1627        let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
1628        root.rebuild(&mut app, &mut ());
1629        assert_eq!(outer.depth(), 1, "outer starts at its own root");
1630        assert!(
1631            !handles_back(),
1632            "root-level, no nested navigator yet: back bubbles"
1633        );
1634
1635        // Push a page onto the OUTER navigator that hosts the nested
1636        // navigator — outer is now at depth 2, so it claims back interest too:
1637        // the shape the ranking has to arbitrate.
1638        {
1639            let inner_for_push = inner.clone();
1640            outer.push(move || {
1641                auto_wire(&inner_for_push);
1642                any(raw_navigator(&inner_for_push, page))
1643            });
1644        }
1645        root.rebuild(&mut app, &mut ());
1646        assert_eq!(
1647            outer.depth(),
1648            2,
1649            "outer pushed to depth 2: the page hosting the nested navigator"
1650        );
1651        assert_eq!(
1652            inner.depth(),
1653            1,
1654            "the nested navigator starts at its own root"
1655        );
1656
1657        // Push inside the nested navigator too, so BOTH stacks are poppable —
1658        // the exact ambiguity R44-back must arbitrate between two
1659        // `Role::Navigator` peers.
1660        inner.push(page);
1661        root.rebuild(&mut app, &mut ());
1662        assert_eq!((outer.depth(), inner.depth()), (2, 2));
1663        assert!(handles_back(), "some registrant claims the press");
1664
1665        push_back_press();
1666        root.rebuild(&mut app, &mut ());
1667        assert_eq!(
1668            inner.depth(),
1669            1,
1670            "the back press popped the NESTED navigator's stack"
1671        );
1672        assert_eq!(
1673            outer.depth(),
1674            2,
1675            "and left the outer navigator's depth UNCHANGED — a bare \
1676             build-order (outermost-first) arbitration would instead have \
1677             popped the outer stack, discarding the whole page the nested \
1678             navigator lives on"
1679        );
1680    }
1681
1682    // -----------------------------------------------------------------------
1683    // R23, the converse of innermost-first: a nested navigator whose hosting
1684    // page input can no longer reach must NOT take the press.
1685    // -----------------------------------------------------------------------
1686
1687    /// An outer navigator whose ROOT page hosts a nested navigator, each
1688    /// auto-wired where its view is constructed (the outer from the app's build
1689    /// pass, the nested from the page builder the outer runs during its own
1690    /// reconcile) — the canonical tab/section-stack shape. `cull` sets the outer
1691    /// navigator's [`cull_covered_builds`], the switch that decides whether the
1692    /// covered page keeps re-running its builder at all.
1693    struct NestedHarness {
1694        outer: NavigatorController<()>,
1695        nested: NavigatorController<()>,
1696        root: RenderRoot<(), NavigatorView<()>>,
1697        app: AppLogic,
1698    }
1699
1700    impl NestedHarness {
1701        fn new(cull: bool) -> Self {
1702            let outer: NavigatorController<()> = NavigatorController::new();
1703            let nested: NavigatorController<()> = NavigatorController::new();
1704            let app: AppLogic = {
1705                let outer = outer.clone();
1706                let nested = nested.clone();
1707                Box::new(move |_: &mut ()| {
1708                    // Exactly what `frust::navigator` does, outermost first.
1709                    auto_wire(&outer);
1710                    let nested = nested.clone();
1711                    raw_navigator(&outer, move || {
1712                        auto_wire(&nested);
1713                        any(raw_navigator(&nested, page))
1714                    })
1715                    .cull_covered_builds(cull)
1716                })
1717            };
1718            let mut harness = Self {
1719                outer,
1720                nested,
1721                root: RenderRoot::new(),
1722                app,
1723            };
1724            harness.rebuild();
1725            harness
1726        }
1727
1728        fn rebuild(&mut self) {
1729            self.root.rebuild(&mut self.app, &mut ());
1730        }
1731    }
1732
1733    /// The outer navigator pushes a page OVER the one hosting the nested
1734    /// navigator. Input can reach only the outer navigator's top page
1735    /// (`input_routed_pages`), so back must follow: the press pops the OUTER
1736    /// stack, and the nested (invisible) stack is untouched.
1737    ///
1738    /// Without the R23 reach gate at the source, `route_back`'s
1739    /// innermost-first `find(|r| interest())` would hand the press to the nested
1740    /// navigator — the covered page keeps reconciling (the default) or stays
1741    /// `is_mounted()` (under the cull) either way — popping a stack nobody can
1742    /// see while the back button appears to do nothing.
1743    ///
1744    /// Run under BOTH `cull_covered_builds` settings: they are exactly the two
1745    /// ways the covered page's builder does or does not keep running, and the
1746    /// answer must not depend on that.
1747    #[test]
1748    fn a_nested_navigator_on_a_covered_page_does_not_take_the_back_press() {
1749        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1750        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
1751
1752        for cull in [false, true] {
1753            reset();
1754            let mut h = NestedHarness::new(cull);
1755
1756            // The nested stack is poppable while its page is still current —
1757            // the innermost-first case, which must keep working (below too).
1758            h.nested.push(page);
1759            h.rebuild();
1760            assert_eq!((h.outer.depth(), h.nested.depth()), (1, 2));
1761            assert!(
1762                handles_back(),
1763                "the nested navigator claims the press while its page is current (cull={cull})"
1764            );
1765
1766            // Now push a page over it on the OUTER navigator: the nested
1767            // navigator's page is covered and routed no input.
1768            h.outer.push(page);
1769            h.rebuild();
1770            assert_eq!((h.outer.depth(), h.nested.depth()), (2, 2));
1771            // A few idle frames: under `cull_covered_builds(true)` the covered
1772            // page stops rebuilding entirely here, so this is where a
1773            // wire-refreshed (rather than live) reachability flag would go stale.
1774            for _ in 0..3 {
1775                h.rebuild();
1776            }
1777            assert!(
1778                handles_back(),
1779                "the outer navigator claims it (cull={cull})"
1780            );
1781
1782            push_back_press();
1783            h.rebuild();
1784            assert_eq!(
1785                h.outer.depth(),
1786                1,
1787                "the press popped the OUTER stack — the page the user is \
1788                 actually looking at came off (cull={cull})"
1789            );
1790            assert_eq!(
1791                h.nested.depth(),
1792                2,
1793                "and the covered navigator's invisible stack is UNCHANGED \
1794                 (cull={cull})"
1795            );
1796
1797            // Revealed again, the nested navigator takes the next press — the
1798            // innermost-first behaviour, unregressed, in the same harness.
1799            push_back_press();
1800            h.rebuild();
1801            assert_eq!(
1802                (h.outer.depth(), h.nested.depth()),
1803                (1, 1),
1804                "back reaches the nested navigator again once its page is \
1805                 current (cull={cull})"
1806            );
1807        }
1808    }
1809
1810    /// The reach gate is **input routing**, not painting: a *transparent*
1811    /// overlay pushed on the outer navigator leaves the page below it painted
1812    /// (`PageVisibility::Visible`) but routes it no input, so the nested
1813    /// navigator on that page must not take the press either. This is the same
1814    /// divergence R23 pins for semantics.
1815    #[test]
1816    fn a_nested_navigator_under_a_transparent_overlay_does_not_take_the_back_press() {
1817        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1818        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
1819        reset();
1820
1821        let mut h = NestedHarness::new(false);
1822        h.nested.push(page);
1823        h.rebuild();
1824        h.outer
1825            .push_with_options(page, PushOptions::transparent().back(BackPolicy::Pop));
1826        h.rebuild();
1827        assert_eq!((h.outer.depth(), h.nested.depth()), (2, 2));
1828
1829        push_back_press();
1830        h.rebuild();
1831        assert_eq!(h.outer.depth(), 1, "the overlay came off");
1832        assert_eq!(
1833            h.nested.depth(),
1834            2,
1835            "the painted-but-inert page's navigator kept its stack"
1836        );
1837    }
1838
1839    // -----------------------------------------------------------------------
1840    // A design-claim stress test: wiring-order independence between two
1841    // `Role::Navigator` peers. `Registrant::key`'s own doc states the
1842    // premise plainly — "a nested navigator always wires after the navigator
1843    // hosting it, so the highest `birth` among `Navigator` peers is the
1844    // innermost one" — and the two tests above this section
1845    // (`an_overlay_host_survives_an_inner_navigator_wiring_twice_per_pass`,
1846    // `an_init_registered_backhandler_does_not_outrank_a_later_overlay_host`)
1847    // already prove that premise does NOT hold for wire order in general
1848    // (`Role::Host` is decoupled from it on purpose), but only for a `Host`
1849    // vs. a `Navigator`. Nothing pins the analogous case for two `Navigator`
1850    // peers — this section does. `refresh_interest`/`Role`/`registrant_ids`
1851    // are crate-private, so this composition cannot be built from an
1852    // integration test the way the rest of the shell matrix is.
1853    // -----------------------------------------------------------------------
1854
1855    /// `inner` gets an explicit [`BackHandler`], built and FIRST wired before
1856    /// `outer` (its eventual host) has any view at all — the
1857    /// `Component::init` shape — and `track()`ed every rebuild from then on,
1858    /// so its earliest `birth` is never dropped by the one-cycle prune even
1859    /// while its own navigator widget doesn't exist yet (a bare, never-tracked
1860    /// pre-registration IS pruned before outer's page ever mounts inner for
1861    /// real, which self-heals the ordering — tried first, and not what this
1862    /// test pins). The REST of the composition is the honest structural
1863    /// nesting the shell matrix uses elsewhere: outer's plain root, then a
1864    /// page it pushes that hosts `inner`'s real navigator. With BOTH
1865    /// navigators poppable, the innermost-first rule says `inner` must win.
1866    ///
1867    /// **Observed vs. designed:** a `Role::Navigator` peer's sort key is
1868    /// `u64::MAX - birth`, and `birth` never changes after a registrant's
1869    /// FIRST wire (only `seq` refreshes) — so pinning `inner`'s birth ahead of
1870    /// `outer`'s gives `inner` the numerically LARGER key, and `route_back`'s
1871    /// first-match search finds `outer` first. `outer` wins, inverting the
1872    /// documented "structurally innermost wins" rule for this ordering. This
1873    /// is a real, reachable divergence between the documented rule (nesting
1874    /// depth) and its actual implementation (raw wire sequence), not a test
1875    /// bug — see the `#[ignore]` reason for the acceptance criterion this
1876    /// pins for future ranking work.
1877    #[test]
1878    #[ignore = "pins a design case that FAILS against current ranking: an inner \
1879                navigator whose explicit BackHandler wires (and keeps tracking) \
1880                before its outer host's first view exists (the Component::init \
1881                shape) loses the innermost-first tie-break to the outer \
1882                navigator instead of winning it — Registrant::key ranks \
1883                Role::Navigator peers by raw wire sequence (birth), not by \
1884                structural nesting depth. Expected to start passing once \
1885                ranking accounts for nesting depth rather than birth order \
1886                alone; rerun with `cargo test -p frust-ui --lib \
1887                wiring_order_does_not_flip -- --ignored` to observe the \
1888                current failure."]
1889    fn wiring_order_does_not_flip_the_innermost_navigator_when_it_is_still_poppable() {
1890        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1891        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
1892        reset();
1893
1894        let outer: NavigatorController<()> = NavigatorController::new();
1895        let inner: NavigatorController<()> = NavigatorController::new();
1896
1897        // `Component::init`: inner's explicit handler wires FIRST, before
1898        // outer's own view exists.
1899        let inner_back = BackHandler::new(inner.clone());
1900        assert_eq!(registrant_ids(), vec![inner.id()], "inner wired alone");
1901
1902        let mut app: AppLogic = {
1903            let outer_c = outer.clone();
1904            Box::new(move |_: &mut ()| {
1905                // Tracked every pass, exactly like Huddle's own
1906                // `state.back.track()` — this is what keeps inner's early
1907                // birth from ever being pruned as stale.
1908                inner_back.track();
1909                auto_wire(&outer_c);
1910                raw_navigator(&outer_c, page)
1911            })
1912        };
1913        let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
1914        root.rebuild(&mut app, &mut ());
1915        assert_eq!(outer.depth(), 1, "outer starts at its own root");
1916
1917        {
1918            let inner_for_push = inner.clone();
1919            outer.push(move || {
1920                auto_wire(&inner_for_push);
1921                any(raw_navigator(&inner_for_push, page))
1922            });
1923        }
1924        root.rebuild(&mut app, &mut ());
1925        assert_eq!(outer.depth(), 2, "outer pushed the page hosting inner");
1926
1927        // The case-5 shape: BOTH navigators poppable.
1928        inner.push(page);
1929        root.rebuild(&mut app, &mut ());
1930        assert_eq!((outer.depth(), inner.depth()), (2, 2));
1931
1932        push_back_press();
1933        root.rebuild(&mut app, &mut ());
1934        assert_eq!(
1935            inner.depth(),
1936            1,
1937            "the structurally-innermost navigator should win the tie \
1938             regardless of which one registered first"
1939        );
1940        assert_eq!(
1941            outer.depth(),
1942            2,
1943            "and the outer stack should stay untouched"
1944        );
1945    }
1946
1947    /// The harmless half of the same stress test: at inner depth 1 (default
1948    /// `BackPolicy::Pop`, no interest either way), wire order cannot matter
1949    /// because ranking is never even consulted — `route_back` only compares
1950    /// registrants that both claim interest, and `inner` claims none here.
1951    /// Unlike its sibling above, this one is expected to (and does) pass
1952    /// against unchanged code.
1953    #[test]
1954    fn wiring_order_is_irrelevant_when_the_inner_navigator_has_no_interest() {
1955        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
1956        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
1957        reset();
1958
1959        let outer: NavigatorController<()> = NavigatorController::new();
1960        let inner: NavigatorController<()> = NavigatorController::new();
1961
1962        let inner_back = BackHandler::new(inner.clone());
1963
1964        let mut app: AppLogic = {
1965            let outer_c = outer.clone();
1966            Box::new(move |_: &mut ()| {
1967                inner_back.track();
1968                auto_wire(&outer_c);
1969                raw_navigator(&outer_c, page)
1970            })
1971        };
1972        let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
1973        root.rebuild(&mut app, &mut ());
1974
1975        {
1976            let inner_for_push = inner.clone();
1977            outer.push(move || {
1978                auto_wire(&inner_for_push);
1979                any(raw_navigator(&inner_for_push, page))
1980            });
1981        }
1982        root.rebuild(&mut app, &mut ());
1983        assert_eq!(
1984            (outer.depth(), inner.depth()),
1985            (2, 1),
1986            "outer poppable, inner still at its own root"
1987        );
1988
1989        push_back_press();
1990        root.rebuild(&mut app, &mut ());
1991        assert_eq!(outer.depth(), 1, "outer popped its own shell page");
1992    }
1993
1994    // -----------------------------------------------------------------------
1995    // Per-tab navigators kept mounted under an `IndexedStack`: only the
1996    // selected tab may claim the back press, whatever the wire order.
1997    // -----------------------------------------------------------------------
1998
1999    type TabsApp = Box<dyn FnMut(&mut ()) -> AnyView<()>>;
2000
2001    /// Two tab navigators inside `IndexedStack(selected, (tab0, tab1))`,
2002    /// optionally under a root `overlay_host`. `last_wired_is_tab1` picks
2003    /// which tab's `crate::navigator` call (and so its wiring) runs last.
2004    struct TabsHarness {
2005        tab0: NavigatorController<()>,
2006        tab1: NavigatorController<()>,
2007        host: NavigatorController<()>,
2008        selected: std::rc::Rc<std::cell::Cell<Option<usize>>>,
2009        root: RenderRoot<(), AnyView<()>>,
2010        app: TabsApp,
2011    }
2012
2013    impl TabsHarness {
2014        fn new(selected: usize, last_wired_is_tab1: bool, with_host: bool) -> Self {
2015            Self::build(selected, last_wired_is_tab1, with_host, |_, _, _| None)
2016        }
2017
2018        /// As [`new`](Self::new) (no host, tab 1 wired last), with the tabs
2019        /// wrapped in `back_fallback` over the fallback `make_fallback` builds
2020        /// from the two tab controllers and the selection cell.
2021        fn with_fallback(
2022            selected: usize,
2023            make_fallback: impl FnOnce(
2024                &NavigatorController<()>,
2025                &NavigatorController<()>,
2026                &std::rc::Rc<std::cell::Cell<Option<usize>>>,
2027            ) -> BackFallback,
2028        ) -> Self {
2029            Self::build(selected, true, false, |tab0, tab1, sel| {
2030                Some(make_fallback(tab0, tab1, sel))
2031            })
2032        }
2033
2034        fn build(
2035            selected: usize,
2036            last_wired_is_tab1: bool,
2037            with_host: bool,
2038            make_fallback: impl FnOnce(
2039                &NavigatorController<()>,
2040                &NavigatorController<()>,
2041                &std::rc::Rc<std::cell::Cell<Option<usize>>>,
2042            ) -> Option<BackFallback>,
2043        ) -> Self {
2044            let tab0: NavigatorController<()> = NavigatorController::new();
2045            let tab1: NavigatorController<()> = NavigatorController::new();
2046            let host: NavigatorController<()> = NavigatorController::new();
2047            let sel = std::rc::Rc::new(std::cell::Cell::new(Some(selected)));
2048            let fallback = make_fallback(&tab0, &tab1, &sel);
2049            let app: TabsApp = {
2050                let (tab0, tab1, host, sel) =
2051                    (tab0.clone(), tab1.clone(), host.clone(), sel.clone());
2052                Box::new(move |_: &mut ()| {
2053                    let tabs = {
2054                        let (tab0, tab1, sel) = (tab0.clone(), tab1.clone(), sel.clone());
2055                        move || {
2056                            let (n0, n1);
2057                            if last_wired_is_tab1 {
2058                                n0 = crate::navigator(&tab0, page);
2059                                n1 = crate::navigator(&tab1, page);
2060                            } else {
2061                                n1 = crate::navigator(&tab1, page);
2062                                n0 = crate::navigator(&tab0, page);
2063                            }
2064                            frust_widgets::IndexedStack(sel.get(), (n0, n1))
2065                        }
2066                    };
2067                    if with_host {
2068                        auto_wire_overlay_host(&host);
2069                        any(raw_overlay_host(&host, move || any(tabs())))
2070                    } else if let Some(fallback) = &fallback {
2071                        any(back_fallback(fallback, tabs()))
2072                    } else {
2073                        any(tabs())
2074                    }
2075                })
2076            };
2077            let mut h = Self {
2078                tab0,
2079                tab1,
2080                host,
2081                selected: sel,
2082                root: RenderRoot::new(),
2083                app,
2084            };
2085            h.rebuild();
2086            h
2087        }
2088
2089        fn rebuild(&mut self) {
2090            self.root.rebuild(&mut self.app, &mut ());
2091        }
2092
2093        fn select(&mut self, i: usize) {
2094            self.selected.set(Some(i));
2095            self.rebuild();
2096        }
2097
2098        fn depths(&self) -> (usize, usize) {
2099            (self.tab0.depth(), self.tab1.depth())
2100        }
2101
2102        /// Tab 0 to depth `d0`, tab 1 to depth `d1`.
2103        fn deepen(&mut self, d0: usize, d1: usize) {
2104            // Depth only updates on rebuild, so push by count from the
2105            // current (settled) depth.
2106            for _ in self.tab0.depth()..d0 {
2107                self.tab0.push(page);
2108            }
2109            for _ in self.tab1.depth()..d1 {
2110                self.tab1.push(page);
2111            }
2112            self.rebuild();
2113            assert_eq!(self.depths(), (d0, d1));
2114        }
2115    }
2116
2117    /// (a) Selected tab 0 at its root: back bubbles to the platform even though
2118    /// the hidden tab 1 (wired last) is deep.
2119    #[test]
2120    fn a_selected_tab_at_its_root_exits_even_if_a_hidden_tab_is_deep() {
2121        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2122        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
2123        reset();
2124
2125        let mut h = TabsHarness::new(0, true, false);
2126        h.deepen(1, 3);
2127        assert!(
2128            !handles_back(),
2129            "selected tab at its root: the app would exit"
2130        );
2131
2132        push_back_press();
2133        h.rebuild();
2134        assert_eq!(h.depths(), (1, 3), "the hidden tab was not popped");
2135    }
2136
2137    /// (b) Switching to tab 1 hands it the press; tab 0 is untouched.
2138    #[test]
2139    fn switching_tabs_hands_the_back_press_to_the_newly_selected_tab() {
2140        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2141        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
2142        reset();
2143
2144        let mut h = TabsHarness::new(0, true, false);
2145        h.deepen(1, 3);
2146        h.select(1);
2147        assert!(handles_back(), "selected tab 1 is poppable");
2148
2149        push_back_press();
2150        h.rebuild();
2151        assert_eq!(h.depths(), (1, 2), "tab 1 popped one page, tab 0 untouched");
2152    }
2153
2154    /// (c) Wire-order independence, both orders: the deep NON-selected tab is
2155    /// wired last, yet the press goes to the selected tab.
2156    #[test]
2157    fn the_back_press_follows_selection_not_wire_order() {
2158        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2159        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
2160
2161        for last_wired_is_tab1 in [true, false] {
2162            reset();
2163            // Selected tab 0 (depth 2) vs hidden tab 1 (depth 3).
2164            let mut h = TabsHarness::new(0, last_wired_is_tab1, false);
2165            h.deepen(2, 3);
2166            assert!(handles_back());
2167            push_back_press();
2168            h.rebuild();
2169            assert_eq!(
2170                h.depths(),
2171                (1, 3),
2172                "selected tab 0 took the press (tab1 wired last: {last_wired_is_tab1})"
2173            );
2174
2175            // And the mirror: select tab 1, which now wins whichever wired last.
2176            h.select(1);
2177            h.tab0.push(page);
2178            h.rebuild();
2179            assert_eq!(h.depths(), (2, 3));
2180            push_back_press();
2181            h.rebuild();
2182            assert_eq!(
2183                h.depths(),
2184                (2, 2),
2185                "selected tab 1 took the press (tab1 wired last: {last_wired_is_tab1})"
2186            );
2187        }
2188    }
2189
2190    /// (d) A root overlay host with an open overlay still outranks the
2191    /// selected tab.
2192    #[test]
2193    fn a_root_overlay_outranks_the_selected_tab() {
2194        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2195        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
2196        reset();
2197
2198        let mut h = TabsHarness::new(1, false, true);
2199        h.deepen(1, 2);
2200        h.host
2201            .push_with_options(page, PushOptions::transparent().back(BackPolicy::Pop));
2202        h.rebuild();
2203        assert_eq!(h.host.depth(), 2);
2204        assert!(handles_back());
2205
2206        push_back_press();
2207        h.rebuild();
2208        assert_eq!(h.host.depth(), 1, "the overlay was dismissed first");
2209        assert_eq!(h.depths(), (1, 2), "the selected tab was untouched");
2210
2211        push_back_press();
2212        h.rebuild();
2213        assert_eq!(
2214            h.depths(),
2215            (1, 1),
2216            "the next press reaches the selected tab"
2217        );
2218    }
2219
2220    /// (e) Hidden tabs stay mounted and registered: depth survives a round
2221    /// trip with no re-push.
2222    #[test]
2223    fn a_hidden_tab_keeps_its_stack_and_registration() {
2224        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2225        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
2226        reset();
2227
2228        let mut h = TabsHarness::new(0, true, false);
2229        h.deepen(1, 3);
2230        h.select(1);
2231        h.select(0);
2232        for _ in 0..3 {
2233            h.rebuild();
2234        }
2235        assert_eq!(h.depths(), (1, 3), "the hidden tab's depth survived");
2236        let ids = registrant_ids();
2237        assert!(
2238            ids.contains(&h.tab0.id()) && ids.contains(&h.tab1.id()),
2239            "both tab controllers stay registered while hidden"
2240        );
2241
2242        h.select(1);
2243        assert!(handles_back());
2244        push_back_press();
2245        h.rebuild();
2246        assert_eq!(h.depths(), (1, 2), "tab 1 resumes from its kept stack");
2247    }
2248
2249    // -----------------------------------------------------------------------
2250    // `BackFallback`: a non-navigator registrant ranked after every navigator,
2251    // live while a `back_fallback` wrapper over it is mounted.
2252    // -----------------------------------------------------------------------
2253
2254    /// A `RenderRoot` driving a concretely typed app closure — no erasure
2255    /// needed when the root view's type is fixed.
2256    struct Driver<V: View<()>, F: FnMut(&mut ()) -> V> {
2257        root: RenderRoot<(), V>,
2258        app: F,
2259    }
2260
2261    impl<V: View<()>, F: FnMut(&mut ()) -> V> Driver<V, F> {
2262        fn new(app: F) -> Self {
2263            let mut driver = Self {
2264                root: RenderRoot::new(),
2265                app,
2266            };
2267            driver.rebuild();
2268            driver
2269        }
2270
2271        fn rebuild(&mut self) {
2272            self.root.rebuild(&mut self.app, &mut ());
2273        }
2274    }
2275
2276    /// An `on_back` that counts its calls, and the count.
2277    fn counting() -> (Rc<Cell<usize>>, impl Fn() + 'static) {
2278        let hits = Rc::new(Cell::new(0));
2279        let counter = Rc::clone(&hits);
2280        (hits, move || counter.set(counter.get() + 1))
2281    }
2282
2283    /// `fallback`'s arbitration identity.
2284    fn fallback_id(fallback: &BackFallback) -> RegistrantId {
2285        RegistrantId::Fallback(fallback.inner.id)
2286    }
2287
2288    /// Whether `fallback` is in the arbitration list at all.
2289    fn is_registered(fallback: &BackFallback) -> bool {
2290        registrants().contains(&fallback_id(fallback))
2291    }
2292
2293    /// The root view [`wrapped_navigator`] drives, and its driver.
2294    type WrappedNavigator = BackFallbackView<NavigatorView<()>>;
2295    type WrappedDriver = Driver<WrappedNavigator, Box<dyn FnMut(&mut ()) -> WrappedNavigator>>;
2296
2297    /// A navigator (at its root) wrapped by `back_fallback`, plus the fallback
2298    /// and its call count, with the fallback's interest fixed to `interest`.
2299    fn wrapped_navigator(
2300        interest: bool,
2301    ) -> (
2302        NavigatorController<()>,
2303        BackFallback,
2304        Rc<Cell<usize>>,
2305        WrappedDriver,
2306    ) {
2307        let nav: NavigatorController<()> = NavigatorController::new();
2308        let (hits, on_back) = counting();
2309        let fallback = BackFallback::new(move || interest, on_back);
2310        let driver = {
2311            let (nav, fallback) = (nav.clone(), fallback.clone());
2312            Driver::new(Box::new(move |_: &mut ()| {
2313                back_fallback(&fallback, crate::navigator(&nav, page))
2314            })
2315                as Box<dyn FnMut(&mut ()) -> WrappedNavigator>)
2316        };
2317        (nav, fallback, hits, driver)
2318    }
2319
2320    /// (a) A navigator that can pop outranks a mounted, interested fallback.
2321    #[test]
2322    fn a_poppable_navigator_outranks_a_fallback() {
2323        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2324        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
2325        reset();
2326
2327        let (nav, _fallback, hits, mut d) = wrapped_navigator(true);
2328        nav.push(page);
2329        d.rebuild();
2330        assert_eq!(nav.depth(), 2);
2331
2332        push_back_press();
2333        d.rebuild();
2334        assert_eq!(nav.depth(), 1, "the navigator took the press");
2335        assert_eq!(hits.get(), 0, "and the fallback did not");
2336    }
2337
2338    /// (b) With the navigator at its root, an interested fallback claims the
2339    /// press: `handles_back()` is true, `on_back` runs once, and the navigator
2340    /// is untouched.
2341    #[test]
2342    fn a_fallback_claims_the_press_when_no_navigator_does() {
2343        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2344        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
2345        reset();
2346
2347        let (nav, _fallback, hits, mut d) = wrapped_navigator(true);
2348        assert_eq!(nav.depth(), 1);
2349        assert!(
2350            handles_back(),
2351            "the fallback claims the press ahead of time"
2352        );
2353
2354        push_back_press();
2355        d.rebuild();
2356        assert_eq!(hits.get(), 1, "on_back ran once");
2357        assert_eq!(nav.depth(), 1, "the navigator is unchanged");
2358
2359        d.rebuild();
2360        assert_eq!(hits.get(), 1, "a rebuild with no new press does not refire");
2361    }
2362
2363    /// (c) A fallback with no interest lets a root-level press exit.
2364    #[test]
2365    fn an_uninterested_fallback_lets_the_press_exit() {
2366        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2367        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
2368        reset();
2369
2370        let (nav, _fallback, hits, mut d) = wrapped_navigator(false);
2371        assert!(!handles_back(), "nobody claims: the app would exit");
2372
2373        push_back_press();
2374        d.rebuild();
2375        assert_eq!((hits.get(), nav.depth()), (0, 1));
2376    }
2377
2378    /// (d) Rank comes from `Role`, never from wire order: a fallback wired
2379    /// BEFORE the navigator and one wired AFTER it both rank after it, and
2380    /// among themselves keep first-wire order.
2381    #[test]
2382    fn fallbacks_rank_after_the_navigator_whatever_the_wire_order() {
2383        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2384        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
2385        reset();
2386
2387        let nav: NavigatorController<()> = NavigatorController::new();
2388        let (before_hits, before_on_back) = counting();
2389        let (after_hits, after_on_back) = counting();
2390        let before = BackFallback::new(|| true, before_on_back);
2391        let after = BackFallback::new(|| true, after_on_back);
2392        let mut d = {
2393            let (nav, before, after) = (nav.clone(), before.clone(), after.clone());
2394            Driver::new(move |_: &mut ()| {
2395                // Argument order is wire order: `before`, the navigator, `after`.
2396                frust_widgets::stack()
2397                    .child(back_fallback(&before, frust_widgets::text("x")))
2398                    .child(back_fallback(&after, crate::navigator(&nav, page)))
2399            })
2400        };
2401        assert_eq!(
2402            registrants(),
2403            vec![
2404                RegistrantId::Navigator(nav.id()),
2405                fallback_id(&before),
2406                fallback_id(&after),
2407            ],
2408            "the navigator first, then the fallbacks in first-wire order"
2409        );
2410
2411        nav.push(page);
2412        d.rebuild();
2413        push_back_press();
2414        d.rebuild();
2415        assert_eq!(nav.depth(), 1, "the navigator took the press");
2416        assert_eq!((before_hits.get(), after_hits.get()), (0, 0));
2417
2418        push_back_press();
2419        d.rebuild();
2420        assert_eq!(
2421            (before_hits.get(), after_hits.get()),
2422            (1, 0),
2423            "with the navigator at its root, the first-wired fallback took it"
2424        );
2425        assert_eq!(
2426            registrants(),
2427            vec![
2428                RegistrantId::Navigator(nav.id()),
2429                fallback_id(&before),
2430                fallback_id(&after),
2431            ],
2432            "re-wiring never re-orders"
2433        );
2434    }
2435
2436    /// (e) Leaving the screen: the wrapper unmounts, the fallback stops
2437    /// claiming at once, rule 2 prunes it, and a press does not reach it.
2438    /// Re-adding the wrapper re-registers it.
2439    #[test]
2440    fn an_unmounted_fallback_stops_claiming_and_is_pruned() {
2441        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2442        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
2443        reset();
2444
2445        let nav: NavigatorController<()> = NavigatorController::new();
2446        let (hits, on_back) = counting();
2447        let fallback = BackFallback::new(|| true, on_back);
2448        let show = Rc::new(Cell::new(true));
2449        let mut d = {
2450            let (nav, fallback, show) = (nav.clone(), fallback.clone(), Rc::clone(&show));
2451            Driver::new(move |_: &mut ()| {
2452                frust_widgets::stack()
2453                    .child(crate::navigator(&nav, page))
2454                    .when(show.get(), |s| {
2455                        s.child(back_fallback(&fallback, frust_widgets::text("x")))
2456                    })
2457            })
2458        };
2459        assert!(handles_back(), "mounted and interested");
2460
2461        show.set(false);
2462        d.rebuild();
2463        assert!(!fallback.is_live(), "the wrapper was torn down");
2464        assert!(!handles_back(), "an unmounted fallback claims nothing");
2465        d.rebuild();
2466        d.rebuild();
2467        assert!(!is_registered(&fallback), "rule 2 pruned it");
2468        assert_eq!(registrant_ids(), vec![nav.id()]);
2469
2470        push_back_press();
2471        d.rebuild();
2472        assert_eq!(hits.get(), 0, "a press after the prune does not reach it");
2473
2474        show.set(true);
2475        d.rebuild();
2476        assert!(
2477            is_registered(&fallback),
2478            "re-adding the wrapper re-registers it"
2479        );
2480        assert!(handles_back());
2481        push_back_press();
2482        d.rebuild();
2483        assert_eq!(hits.get(), 1, "and it claims presses again");
2484    }
2485
2486    /// (f) A burst of presses before a rebuild collapses to one `on_back`.
2487    #[test]
2488    fn a_press_burst_calls_on_back_once() {
2489        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2490        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
2491        reset();
2492
2493        let (_nav, _fallback, hits, mut d) = wrapped_navigator(true);
2494        for _ in 0..3 {
2495            push_back_press();
2496        }
2497        d.rebuild();
2498        assert_eq!(hits.get(), 1, "three presses, one rebuild: one on_back");
2499        d.rebuild();
2500        assert_eq!(hits.get(), 1);
2501    }
2502
2503    /// (g) Per-tab stacks under an `IndexedStack`, wrapped by a back-to-home
2504    /// fallback: back at a non-home tab root switches to the home tab, and
2505    /// back at the home tab root is not consumed (the app exits).
2506    #[test]
2507    fn back_at_a_tab_root_returns_home_then_exits() {
2508        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2509        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
2510        reset();
2511
2512        const HOME: usize = 0;
2513        let mut h = TabsHarness::with_fallback(1, |tab0, tab1, sel| {
2514            let (tab0, tab1) = (tab0.clone(), tab1.clone());
2515            let (probe, set) = (Rc::clone(sel), Rc::clone(sel));
2516            BackFallback::new(
2517                move || match probe.get() {
2518                    Some(HOME) | None => false,
2519                    Some(i) => [&tab0, &tab1][i].depth() <= 1,
2520                },
2521                move || set.set(Some(HOME)),
2522            )
2523        });
2524
2525        // A poppable selected tab still takes the press first.
2526        h.deepen(1, 2);
2527        push_back_press();
2528        h.rebuild();
2529        assert_eq!(h.depths(), (1, 1), "tab B popped");
2530        assert_eq!(h.selected.get(), Some(1), "and the selection stayed");
2531
2532        // Tab B at its root: the fallback switches to the home tab.
2533        assert!(
2534            handles_back(),
2535            "off home at a tab root: the fallback claims"
2536        );
2537        push_back_press();
2538        h.rebuild();
2539        assert_eq!(
2540            h.selected.get(),
2541            Some(HOME),
2542            "the press selected the home tab"
2543        );
2544        h.rebuild();
2545        assert_eq!(h.depths(), (1, 1), "no stack was popped");
2546
2547        // Home tab at its root: nobody claims, so the press would exit.
2548        assert!(!handles_back(), "home at its root: back falls through");
2549        push_back_press();
2550        h.rebuild();
2551        assert_eq!(h.selected.get(), Some(HOME));
2552        assert_eq!(h.depths(), (1, 1));
2553    }
2554
2555    /// (h) A navigator that wires twice per pass (`BackHandler::track` +
2556    /// `navigator()` on one controller) never prunes a mounted fallback: rule 1
2557    /// holds for a fallback exactly as for a navigator.
2558    #[test]
2559    fn a_mounted_fallback_survives_a_double_wiring_navigator() {
2560        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2561        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
2562        reset();
2563
2564        let nav: NavigatorController<()> = NavigatorController::new();
2565        let back = BackHandler::new(nav.clone());
2566        let (hits, on_back) = counting();
2567        let fallback = BackFallback::new(|| true, on_back);
2568        let mut d = {
2569            let (nav, fallback) = (nav.clone(), fallback.clone());
2570            Driver::new(move |_: &mut ()| {
2571                back.track();
2572                back_fallback(&fallback, crate::navigator(&nav, page))
2573            })
2574        };
2575        for pass in 0..4 {
2576            d.rebuild();
2577            assert!(is_registered(&fallback), "never pruned (pass {pass})");
2578            assert!(handles_back(), "handles_back stays true (pass {pass})");
2579        }
2580
2581        push_back_press();
2582        d.rebuild();
2583        assert_eq!(
2584            (hits.get(), nav.depth()),
2585            (1, 1),
2586            "the fallback took it once"
2587        );
2588    }
2589
2590    /// (i) First-wire grace: `back_fallback()` wires before its wrapper is
2591    /// built, and a double-wiring navigator constructed AFTER it in the same
2592    /// first pass must not prune it while its mount count is still 0.
2593    #[test]
2594    fn first_wire_grace_keeps_an_unbuilt_fallback_through_a_double_wire() {
2595        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2596        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
2597        reset();
2598
2599        let nav: NavigatorController<()> = NavigatorController::new();
2600        let back = BackHandler::new(nav.clone());
2601        let fallback = BackFallback::new(|| true, || {});
2602        // What the double wire left behind, seen mid-pass: (registered, mount
2603        // count) right after the navigator's second wire, before any build.
2604        let mid_pass = Rc::new(Cell::new((false, usize::MAX)));
2605        let d = {
2606            let (nav, fallback, mid_pass) = (nav.clone(), fallback.clone(), Rc::clone(&mid_pass));
2607            Driver::new(move |_: &mut ()| {
2608                let wrapped = back_fallback(&fallback, frust_widgets::text("x"));
2609                back.track();
2610                let navigator = crate::navigator(&nav, page);
2611                mid_pass.set((is_registered(&fallback), fallback.inner.mounted.get()));
2612                frust_widgets::stack().child(wrapped).child(navigator)
2613            })
2614        };
2615        assert_eq!(
2616            mid_pass.get(),
2617            (true, 0),
2618            "unbuilt (mount count 0) yet not pruned by the navigator's double wire"
2619        );
2620        assert!(is_registered(&fallback), "still registered after the pass");
2621        assert_eq!(fallback.inner.mounted.get(), 1, "and now mounted");
2622        assert!(
2623            handles_back(),
2624            "handles_back reflects it on the first frame"
2625        );
2626        drop(d);
2627    }
2628
2629    /// (j) Two wrappers sharing one handle: the fallback stays live while
2630    /// either is mounted, and is pruned only once both are gone.
2631    #[test]
2632    fn a_fallback_shared_by_two_wrappers_lives_until_both_unmount() {
2633        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2634        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
2635        reset();
2636
2637        let nav: NavigatorController<()> = NavigatorController::new();
2638        let fallback = BackFallback::new(|| true, || {});
2639        let show = Rc::new(Cell::new((true, true)));
2640        let mut d = {
2641            let (nav, fallback, show) = (nav.clone(), fallback.clone(), Rc::clone(&show));
2642            Driver::new(move |_: &mut ()| {
2643                let (a, b) = show.get();
2644                frust_widgets::stack()
2645                    .child(crate::navigator(&nav, page))
2646                    .when(a, |s| {
2647                        s.child(back_fallback(&fallback, frust_widgets::text("a")))
2648                    })
2649                    .when(b, |s| {
2650                        s.child(back_fallback(&fallback, frust_widgets::text("b")))
2651                    })
2652            })
2653        };
2654        assert_eq!(fallback.inner.mounted.get(), 2);
2655
2656        show.set((true, false));
2657        for _ in 0..3 {
2658            d.rebuild();
2659        }
2660        assert_eq!(fallback.inner.mounted.get(), 1);
2661        assert!(is_registered(&fallback), "one wrapper still mounted: live");
2662        assert!(handles_back());
2663
2664        show.set((false, false));
2665        d.rebuild();
2666        assert!(!handles_back(), "both unmounted: claims nothing");
2667        d.rebuild();
2668        d.rebuild();
2669        assert!(!is_registered(&fallback), "and rule 2 pruned it");
2670    }
2671
2672    /// A slot that rebuilds against a DIFFERENT handle moves its mount: the
2673    /// old fallback unmounts (and is pruned), the new one is live.
2674    #[test]
2675    fn a_wrapper_rebuilt_against_another_handle_moves_its_mount() {
2676        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2677        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
2678        reset();
2679
2680        let nav: NavigatorController<()> = NavigatorController::new();
2681        let first = BackFallback::new(|| true, || {});
2682        let second = BackFallback::new(|| true, || {});
2683        let use_first = Rc::new(Cell::new(true));
2684        let mut d = {
2685            let (nav, first, second, use_first) = (
2686                nav.clone(),
2687                first.clone(),
2688                second.clone(),
2689                Rc::clone(&use_first),
2690            );
2691            Driver::new(move |_: &mut ()| {
2692                let fallback = if use_first.get() { &first } else { &second };
2693                back_fallback(fallback, crate::navigator(&nav, page))
2694            })
2695        };
2696        assert_eq!(
2697            (first.inner.mounted.get(), second.inner.mounted.get()),
2698            (1, 0)
2699        );
2700
2701        use_first.set(false);
2702        for _ in 0..3 {
2703            d.rebuild();
2704        }
2705        assert_eq!(
2706            (first.inner.mounted.get(), second.inner.mounted.get()),
2707            (0, 1)
2708        );
2709        assert!(!is_registered(&first), "the old handle was released");
2710        assert!(is_registered(&second), "the new one is live");
2711    }
2712
2713    // -----------------------------------------------------------------------
2714    // Unclaimed presses are dropped; tab navigators + fallback under a real
2715    // `pattern_switcher`.
2716    // -----------------------------------------------------------------------
2717
2718    /// (b) The opposite wire order of the first tabs test: the hidden deep tab
2719    /// wires FIRST, the selected tab (wired last) is at its root, no fallback.
2720    /// Nothing claims, and nothing pops.
2721    #[test]
2722    fn an_unclaimed_press_pops_nothing_when_the_hidden_deep_tab_wires_first() {
2723        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2724        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
2725        reset();
2726
2727        let mut h = TabsHarness::new(0, false, false);
2728        h.deepen(1, 3);
2729        assert!(!handles_back(), "selected tab at its root: nothing claims");
2730
2731        push_back_press();
2732        h.rebuild();
2733        assert_eq!(h.depths(), (1, 3), "the hidden deep tab was not popped");
2734        h.rebuild();
2735        assert_eq!(h.depths(), (1, 3));
2736    }
2737
2738    /// (c) Lost-claim race: the fallback claims at poll time, the selection
2739    /// flips to Home before the rebuild that consumes the press, and the
2740    /// hidden Home tab is deep. The press pops nothing.
2741    #[test]
2742    fn a_claim_lost_before_the_press_is_consumed_pops_no_hidden_tab() {
2743        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2744        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
2745        reset();
2746
2747        const HOME: usize = 0;
2748        let mut h = TabsHarness::with_fallback(1, |tab0, tab1, sel| {
2749            let (tab0, tab1) = (tab0.clone(), tab1.clone());
2750            let probe = Rc::clone(sel);
2751            BackFallback::new(
2752                move || match probe.get() {
2753                    Some(HOME) | None => false,
2754                    Some(i) => [&tab0, &tab1][i].depth() <= 1,
2755                },
2756                || {},
2757            )
2758        });
2759        h.deepen(3, 1);
2760        assert!(handles_back(), "tab B at its root: the fallback claims");
2761
2762        // The selection flips before the rebuild that consumes the press.
2763        h.selected.set(Some(HOME));
2764        push_back_press();
2765        h.rebuild();
2766        assert_eq!(h.depths(), (3, 1), "the hidden-then-selected Home tab held");
2767        h.rebuild();
2768        assert_eq!(h.depths(), (3, 1));
2769    }
2770
2771    type SwitchedApp = Box<dyn FnMut(&mut ()) -> PatternSwitcherView<(), SharedAxis>>;
2772
2773    /// Two tab navigators (`IndexedStack`, optionally under a `back_fallback`)
2774    /// inside the page of a real `frust::motion` `pattern_switcher` keyed 0;
2775    /// key 1 is a page with no navigator.
2776    struct SwitchHarness {
2777        tab0: NavigatorController<()>,
2778        tab1: NavigatorController<()>,
2779        key: Rc<Cell<usize>>,
2780        hits: Rc<Cell<usize>>,
2781        driver: Driver<PatternSwitcherView<(), SharedAxis>, SwitchedApp>,
2782    }
2783
2784    impl SwitchHarness {
2785        fn new(with_fallback: bool) -> Self {
2786            let tab0: NavigatorController<()> = NavigatorController::new();
2787            let tab1: NavigatorController<()> = NavigatorController::new();
2788            let key = Rc::new(Cell::new(0usize));
2789            let (hits, on_back) = counting();
2790            let fallback = {
2791                let tab1 = tab1.clone();
2792                BackFallback::new(move || tab1.depth() <= 1, on_back)
2793            };
2794            let app: SwitchedApp = {
2795                let (tab0, tab1, key) = (tab0.clone(), tab1.clone(), key.clone());
2796                Box::new(move |_: &mut ()| {
2797                    let child: AnyView<()> = if key.get() == 0 {
2798                        let n0 = crate::navigator(&tab0, page);
2799                        let n1 = crate::navigator(&tab1, page);
2800                        let tabs = frust_widgets::IndexedStack(Some(1), (n0, n1));
2801                        if with_fallback {
2802                            any(back_fallback(&fallback, tabs))
2803                        } else {
2804                            any(tabs)
2805                        }
2806                    } else {
2807                        any(frust_widgets::text("no navigator here"))
2808                    };
2809                    crate::motion::switcher::pattern_switcher(key.get(), SharedAxis::X, child)
2810                })
2811            };
2812            Self {
2813                tab0,
2814                tab1,
2815                key,
2816                hits,
2817                driver: Driver::new(app),
2818            }
2819        }
2820
2821        /// Tab B (the selected one) to `depth`, by a counted number of pushes.
2822        fn deepen_b(&mut self, depth: usize) {
2823            for _ in self.tab1.depth()..depth {
2824                self.tab1.push(page);
2825            }
2826            self.driver.rebuild();
2827            assert_eq!(self.tab1.depth(), depth);
2828        }
2829
2830        fn switch_away(&mut self) {
2831            self.key.set(1);
2832            self.driver.rebuild();
2833        }
2834    }
2835
2836    /// (a) The real switcher path, deep tab: switching to a page with no
2837    /// navigator tears the tabs down on the switch rebuild itself, so nothing
2838    /// claims and the press is not swallowed by the dead tab B.
2839    #[test]
2840    fn a_switcher_teardown_leaves_no_claim_on_a_deep_dead_tab() {
2841        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2842        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
2843        reset();
2844
2845        let mut h = SwitchHarness::new(true);
2846        h.deepen_b(3);
2847        assert!(
2848            handles_back(),
2849            "deep selected tab B claims before the switch"
2850        );
2851
2852        h.switch_away();
2853        assert!(!handles_back(), "no claim on the switch rebuild itself");
2854
2855        push_back_press();
2856        h.driver.rebuild();
2857        assert_eq!(h.tab1.depth(), 3, "the dead tab B was not popped");
2858        assert_eq!(h.tab0.depth(), 1);
2859        assert_eq!(h.hits.get(), 0, "the fallback did not fire either");
2860        assert!(!handles_back());
2861    }
2862
2863    /// (a, cont.) The real switcher path, tab B at its root with the fallback
2864    /// interested: after the switch the fallback is unmounted and does not fire.
2865    #[test]
2866    fn a_switcher_teardown_silences_an_interested_fallback() {
2867        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2868        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
2869        reset();
2870
2871        let mut h = SwitchHarness::new(true);
2872        assert!(handles_back(), "tab B at its root: the fallback claims");
2873
2874        h.switch_away();
2875        assert!(!handles_back(), "the torn-down fallback stops claiming");
2876
2877        push_back_press();
2878        h.driver.rebuild();
2879        assert_eq!(h.hits.get(), 0, "the fallback did not fire");
2880    }
2881
2882    /// (d) A torn-down navigator stops claiming immediately: no extra rebuild
2883    /// cycle is needed before `handles_back` turns false.
2884    #[test]
2885    fn a_torn_down_navigator_stops_claiming_without_an_extra_rebuild() {
2886        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2887        let _rt = ReactiveRuntime::init(Arc::new(|| {}));
2888        reset();
2889
2890        let mut h = SwitchHarness::new(false);
2891        h.deepen_b(3);
2892        assert!(h.tab1.is_mounted());
2893        assert!(handles_back());
2894
2895        h.switch_away();
2896        assert!(!h.tab1.is_mounted(), "torn down at the switch");
2897        assert!(
2898            !handles_back(),
2899            "and no longer claiming, with no extra pass"
2900        );
2901    }
2902}