Skip to main content

incular_runtime/
route_outlet.rs

1//! Hosts one navigator's routes as mounted, identity-tagged content.
2//!
3//! [`RouteOutlet`] is the production seam between a navigation stack and a
4//! window tree: it composes every mounted route's content per an explicit
5//! [`OutletPlacement`] policy, drives focus save/restore/forget and route
6//! task bindings from live navigator state, and derives element ownership
7//! from the mounted tags — applications never maintain element-id oracles.
8//!
9//! Drive contract: mount [`RouteOutlet::widget`] in the window tree,
10//! attach once with [`RouteOutlet::attach`], then drive every cycle with
11//! the single [`RouteOutlet::present_frame`] operation — rebuild, frame,
12//! and reconcile in enforced order, with tree-wide validation before and
13//! after the frame. Covered routes without retention unmount (disposal);
14//! retained ones stay mounted inert while invisible; only permanent
15//! removal ends lifetimes.
16//!
17//! Frame contract: a frame runs build, layout, composite, semantics, then
18//! paint, so restoration — which reconciles after the frame returns —
19//! cannot make that same frame's paint or semantics. The restore applies
20//! to the focus slot synchronously and flags follow-up work through the
21//! existing scheduler (`set_focus` marks another frame); the next frame
22//! then finalizes styling, semantics, and keyboard dispatch from restored
23//! focus. No unconditional extra frame runs: follow-up is flagged when
24//! reconciliation changed focus, or when the presented output is older
25//! than live navigation (mid-build navigation flags exactly once per
26//! revision, nested staleness wakes the root driver, and quiet frames
27//! flag nothing). Errors schedule no follow-up at all, so rejected
28//! stacks cannot spin a retry loop. Deferred content converges through
29//! its own invalidation afterwards.
30//!
31//! Driving states (derived from the retained fields, not tracked
32//! separately — `pending`, `consumed` vs live revision, `needs_frame`,
33//! and the per-revision schedule memo):
34//!
35//! | Situation | pending | consumed | needs_frame | scheduled | exit |
36//! | Unattached, empty | none | none | false | — | navigate → stale |
37//! | Unattached, routes live | per capture | older/none | true | on success | attach → attached |
38//! | Attached, idle | none | current | false | — | navigate → pending |
39//! | Attempt pending | some | just composed | false, true after mid-frame nav | — | commit → consumed; stale → follow-up |
40//! | Failed (error/panic) | kept | older | true | reset, re-evaluates | retry → pending/consumed |
41//! | Successful-but-stale | recaptured/none | older | true | once per rev | follow-up → consumed |
42//! | Detached | kept (own) | frozen | own tree only | own claims | reattach or drive standalone |
43
44use std::{
45    cell::{Cell, RefCell},
46    collections::HashMap,
47    rc::{Rc, Weak},
48    sync::atomic::{AtomicU64, Ordering},
49};
50
51use incular_config::Constraints;
52use incular_navigation::{ModalBarrier, Navigator, Route, RouteId, RoutePresentation};
53use incular_rendering::DisplayList;
54use incular_widgets::{
55    AnimatedModalBarrier, ExcludeFocusTraversal, LayoutBuilder, SizedBox, Stack, Visibility,
56    Widget,
57    internal::{ElementId, Key, TreeError, WidgetTree},
58};
59
60use super::{
61    frame::{FrameStats, Runtime},
62    tasks::TaskScope,
63};
64use crate::{RouteTaskBinding, route_focus::RouteFocusState};
65
66/// Sequence numbering outlet key namespaces so sibling outlets sharing one
67/// tree never tag two routes alike.
68static OUTLET_SEQUENCE: AtomicU64 = AtomicU64::new(1);
69
70/// How the outlet presents one route, derived from its [`RoutePresentation`].
71///
72/// Modes the outlet cannot honor are explicit variants, never silent
73/// behavior: overlay entries are portal-managed and never outlet-mounted.
74#[derive(Clone, Debug, PartialEq, Eq)]
75pub enum OutletPlacement {
76    /// Mounted stack content. `visible` paints, hits, and exposes
77    /// semantics; invisible content stays mounted inert while the route
78    /// retains state. `barrier` veils content below while mounted.
79    Stacked {
80        visible: bool,
81        barrier: Option<ModalBarrier>,
82    },
83    /// Portal-managed overlay entries: host them in an `OverlayPortal`
84    /// separately instead of mounting them here.
85    ExternalOverlay,
86}
87
88impl OutletPlacement {
89    /// Classifies `presentation` given whether any opaque route above
90    /// covers it. Only opaque pages occlude, so translucent veils dim
91    /// painted content rather than blackness.
92    pub fn of(presentation: &RoutePresentation, visible: bool) -> Self {
93        if presentation.is_overlay() {
94            return Self::ExternalOverlay;
95        }
96        Self::Stacked {
97            visible,
98            barrier: presentation.barrier().cloned(),
99        }
100    }
101}
102
103/// The navigation state one frame attempt builds content from: the
104/// authoritative stack revision plus the ordered route identities the
105/// frame composes. Immutable once captured; refreshed (never re-read for
106/// focus) at every build boundary so retries describe the content they
107/// actually present. Presentation is deliberately not snapshotted: it is
108/// fixed for a route identity, so revision plus identities is
109/// authoritative — no boolean approximation.
110#[derive(Clone, Debug, PartialEq, Eq)]
111struct FrameAttempt {
112    revision: u64,
113    active: Option<RouteId>,
114    members: Vec<RouteId>,
115}
116
117impl FrameAttempt {
118    fn capture(navigator: &Navigator) -> Self {
119        Self::of(navigator.revision(), &navigator.routes())
120    }
121
122    /// Snapshots exactly these routes at this revision: [`RouteOutlet::widget`]
123    /// builds the attempt from the same route list it composes children
124    /// from, so acquisition and composition stay together.
125    fn of(revision: u64, routes: &[Route]) -> Self {
126        Self {
127            revision,
128            active: routes.last().map(|route| route.id),
129            members: routes.iter().map(|route| route.id).collect(),
130        }
131    }
132
133    /// Whether the live navigator still matches this attempt closely
134    /// enough to commit. Same revision means nothing moved. Otherwise the
135    /// member set must match under the same active route: order-only
136    /// churn (a lower-route reorder beneath an unchanged top) and
137    /// same-identity child replacement keep the per-route bookkeeping
138    /// valid, while membership changes — pushes, pops, removals —
139    /// abandon. Push-then-pop back to the original top with no pending
140    /// transition never reaches the commit; with one in flight the ids
141    /// differ and the capture abandons cleanly.
142    fn covers(&self, navigator: &Navigator) -> bool {
143        if navigator.revision() == self.revision {
144            return true;
145        }
146        let current = Self::capture(navigator);
147        current.active == self.active
148            && current.members.len() == self.members.len()
149            && self.members.iter().all(|id| current.members.contains(id))
150    }
151}
152
153/// One uncommitted frame transition, tracking four independent concerns:
154/// - the outgoing focus save (`previous`/`saved`), which survives retries
155///   untouched — rebuilding re-records consumption without re-reading;
156/// - the navigation snapshot for this attempt (`attempt`), adopted from
157///   what composition actually consumed (see `consumed`);
158/// - newer-work detection, via the consumed revision versus live state
159///   (see [`RouteOutlet::needs_frame`]);
160/// - lifetime eligibility, checked at commit (incoming) and at restore
161///   (records never restore for removed routes).
162///
163/// The save lives here — not in the records — until the frame presenting
164/// it commits, so failed attempts and navigation mid-frame can never
165/// disturb committed state.
166#[derive(Clone, Debug, PartialEq, Eq)]
167struct PendingTransition {
168    previous: Option<RouteId>,
169    incoming: Option<RouteId>,
170    saved: Option<ElementId>,
171    attempt: FrameAttempt,
172}
173
174/// Identity of one [`RouteOutlet`], for actionable error data and host
175/// bookkeeping. Assigned once per outlet; sibling outlets sharing one
176/// tree never collide.
177#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
178pub struct OutletId(u64);
179
180impl std::fmt::Display for OutletId {
181    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        write!(formatter, "route-outlet-{}", self.0)
183    }
184}
185
186/// A route outlet refusing to present.
187///
188/// Timing differs per variant, and the docs say which: pre-frame
189/// rejections change nothing mounted or committed, while the post-frame
190/// validation can fire after the tree mounted the frame's content — in
191/// that case integration state (records, bindings, tags, saves) is still
192/// untouched, but the tree holds the last frame's stack output. Hosts fix
193/// the stack (portal-mount the listed routes, or keep them out of
194/// outlet-driven navigators) instead of debugging silently omitted
195/// content.
196#[derive(Clone, Debug, PartialEq, Eq)]
197pub enum OutletError {
198    /// One or more routes use portal-managed overlay presentations, which
199    /// the outlet never mounts. Carries the offending outlet alongside
200    /// the route ids, so nested rejections point at the responsible
201    /// navigator instead of the driving root.
202    UnsupportedPresentation {
203        outlet: OutletId,
204        routes: Vec<RouteId>,
205    },
206    /// Another live runtime already drives this outlet: the outlet is
207    /// attached to (or last presented by) `owner`, so presenting through
208    /// `attempted` would split one lifecycle across two frame loops.
209    /// Detach or drop the first runtime to release the claim — teardown
210    /// releases it automatically — then drive here.
211    DriverConflict {
212        outlet: OutletId,
213        owner: u64,
214        attempted: u64,
215    },
216    /// This outlet nests under a live parent: drive the root instead.
217    /// Separate presents would drive the child twice per frame (once
218    /// directly, once through the cascade) and corrupt the pending
219    /// capture. Detach first to drive it standalone.
220    SeparateDrive { outlet: OutletId, parent: OutletId },
221    /// The underlying rebuild or frame failed.
222    Frame(TreeError),
223}
224
225impl std::fmt::Display for OutletError {
226    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227        match self {
228            Self::UnsupportedPresentation { outlet, routes } => {
229                write!(
230                    formatter,
231                    "{outlet} cannot present portal-managed overlay routes: {routes:?} \
232                     (host overlay entries in an OverlayPortal separately)"
233                )
234            }
235            Self::DriverConflict {
236                outlet,
237                owner,
238                attempted,
239            } => {
240                write!(
241                    formatter,
242                    "{outlet} is already driven by runtime-{owner}: refusing runtime-{attempted} \
243                     (one lifecycle per runtime; release the first claim before driving here)"
244                )
245            }
246            Self::SeparateDrive { outlet, parent } => {
247                write!(
248                    formatter,
249                    "{outlet} nests under {parent}: drive the root instead of presenting \
250                     the child separately (detach first to drive it standalone)"
251                )
252            }
253            Self::Frame(error) => {
254                write!(formatter, "route outlet frame failed: {error}")
255            }
256        }
257    }
258}
259
260impl std::error::Error for OutletError {
261    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
262        match self {
263            Self::UnsupportedPresentation { .. }
264            | Self::DriverConflict { .. }
265            | Self::SeparateDrive { .. } => None,
266            Self::Frame(error) => Some(error),
267        }
268    }
269}
270
271impl From<TreeError> for OutletError {
272    fn from(error: TreeError) -> Self {
273        Self::Frame(error)
274    }
275}
276
277/// Owns cleanup for one driven attempt. Armed before the fallible
278/// section; success disarms after flagging, while every early return —
279/// and panic unwind — drops it armed and resets the schedule memos.
280/// What survives a failed attempt, by design: the outgoing save and the
281/// pending capture (untouched — retry needs them), the published
282/// snapshot (no promotion ran), the driver claim (predates the attempt),
283/// and live navigation itself. Only the schedule memo resets, so the
284/// next success re-evaluates outstanding work. Panics resume unwinding
285/// immediately after the reset: integration invariants are restored,
286/// never repaired, and application panics never become successful
287/// frames.
288struct AttemptGuard<'a> {
289    outlet: &'a Rc<RefCell<RouteOutlet>>,
290    armed: bool,
291}
292
293impl<'a> AttemptGuard<'a> {
294    fn arm(outlet: &'a Rc<RefCell<RouteOutlet>>) -> Self {
295        Self {
296            outlet,
297            armed: true,
298        }
299    }
300
301    fn disarm(&mut self) {
302        self.armed = false;
303    }
304}
305
306impl Drop for AttemptGuard<'_> {
307    fn drop(&mut self) {
308        if self.armed {
309            self.outlet.borrow().reset_signaled_tree();
310        }
311    }
312}
313
314/// Why a nested outlet attachment was rejected.
315///
316/// Rejection happens before any mutation, so a failed attach leaves both
317/// outlets exactly as they were. The topology is a forest by design —
318/// like back dispatch it must terminate, but unlike back dispatch (which
319/// follows a single active chain and can share children) cascade driving
320/// visits every registered child, so one child under two parents would
321/// drive twice per frame.
322#[derive(Clone, Copy, Debug, PartialEq, Eq)]
323pub enum OutletAttachError {
324    /// An outlet cannot nest inside itself.
325    SelfAttachment,
326    /// The child already reaches the parent through nested attachments,
327    /// so the edge would close a directed cycle and cascade driving
328    /// could recurse through it forever.
329    Cycle,
330    /// The child is already attached to a different live parent. Detach
331    /// it explicitly first: cascade driving follows registration, so a
332    /// second parent would drive the same outlet twice per frame.
333    AlreadyAttached,
334}
335
336impl std::fmt::Display for OutletAttachError {
337    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
338        match self {
339            Self::SelfAttachment => formatter.write_str("a route outlet cannot nest inside itself"),
340            Self::Cycle => formatter.write_str(
341                "attaching this outlet would cycle cascade driving: \
342                 it already reaches its parent",
343            ),
344            Self::AlreadyAttached => formatter.write_str(
345                "this outlet is already attached to a different parent: \
346                 detach it explicitly before reattaching",
347            ),
348        }
349    }
350}
351
352impl std::error::Error for OutletAttachError {}
353
354/// Mounts one [`Navigator`] as presentation-aware stack content.
355///
356/// The outlet owns the integration state the navigator cannot: per-route
357/// mount tags feeding the focus ownership oracle, a [`RouteFocusState`]
358/// driven across transitions, and one [`RouteTaskBinding`] per live
359/// route (lifetime-owned, including unmounted-but-alive routes). Use one
360/// outlet per navigator; nested navigators get nested
361/// outlets and stay isolated through disjoint tags and states.
362///
363/// Dropping the outlet releases the integration: focus records, bindings,
364/// and tags go with it (bound scopes then follow ordinary [`TaskScope`]
365/// ownership, detached from later removals).
366///
367/// State inventory (owner → invariant): `navigator` (cloned handle —
368/// identity and lifetimes live in navigation); `focus`, `bindings`,
369/// `tags` (outlet — pruned to live routes on every reconcile);
370/// `nested`/`parent` (attachment graph — weak both ways, single-parent,
371/// acyclic); `driver` (driving runtime — claimed on drive, released on
372/// detach or runtime drop); `task_parent` (host scope — never mutated);
373/// `presented`/`pending` (transition bookkeeping — advance only on
374/// commit); `epoch` (attempt context — one per driven frame);
375/// `composing`/`consumed` (composition record — receipted mounting
376/// builders only, promoted on success only); `signaled`
377/// (schedule memo — edge-triggered per revision); `namespace`/`root_key`
378/// (immutable
379/// construction identity); `tag_sequence` (monotonic, never reused);
380/// `revision` (invalidation generation — doubles as the drive counter);
381/// `mount` (attached element — cleared when it vanishes).
382pub struct RouteOutlet {
383    navigator: Navigator,
384    focus: RouteFocusState,
385    bindings: HashMap<RouteId, RouteTaskBinding>,
386    tags: Rc<RefCell<HashMap<RouteId, Key>>>,
387    /// Directly nested outlets, weakest first. Elements inside a nested
388    /// outlet belong to it even when an outer tag sits above them;
389    /// transitivity follows containment, so only direct children register.
390    /// Weak handles release automatically when a nested outlet drops, and
391    /// dead entries prune on every present, so repeated replacement never
392    /// grows metadata. Registration is what cascade driving follows: a
393    /// detached outlet receives no frame work even while retained.
394    nested: Rc<RefCell<Vec<Weak<RefCell<RouteOutlet>>>>>,
395    /// This outlet's parent, if attached. Single-parent by design (see
396    /// [`OutletAttachError`]): set on attach, cleared on detach or when
397    /// the parent drops.
398    parent: Rc<RefCell<Option<Weak<RefCell<RouteOutlet>>>>>,
399    /// The runtime driving this outlet, with its liveness token. Claimed
400    /// on attach and on every driven present (roots and cascade children
401    /// alike); released on detach and automatically when the runtime
402    /// drops. No global registry: ownership lives in this claim plus the
403    /// runtime's own builder registration.
404    driver: RefCell<Option<(u64, Weak<u64>)>>,
405    task_parent: TaskScope,
406    /// Last successfully presented active route. Transitions capture
407    /// against this and advance it only on commit — never on failure.
408    presented: Option<RouteId>,
409    /// Uncommitted capture: `Some` while a frame transition is in flight.
410    pending: Option<PendingTransition>,
411    /// Frame attempt counter: bumped by every [`Self::begin_frame`].
412    /// Necessary context for [`Self::composing`], not proof of mounting:
413    /// only a receipt stamped by a mounting builder during the current
414    /// attempt authorizes adoption.
415    epoch: Cell<u64>,
416    /// Publication slot for in-flight composition: the mounting builders
417    /// write a receipt (attempt plus snapshot) here on every execution
418    /// (see [`Self::compose_and_record`]). Never read directly for
419    /// decisions — reconciliation promotes it (see `consumed`) only on
420    /// the success path for the current attempt, so failed composition
421    /// publishes nothing and discarded descriptors authorize nothing.
422    composing: RefCell<Option<(u64, FrameAttempt)>>,
423    /// Last composition a frame carried through reconciliation for this
424    /// outlet. Comparing it against live state answers whether newer
425    /// navigation still requires a frame (see [`Self::needs_frame`])
426    /// independently of the pending save above. Each outlet promotes its
427    /// own: nested slot builders promote when they execute and reconcile,
428    /// skipped builders keep the previous publication.
429    consumed: Option<FrameAttempt>,
430    /// Live revision last scheduled for. Follow-up scheduling is
431    /// edge-triggered per outlet: a stale revision flags exactly once no
432    /// matter how many frames observe it, so unconsumable staleness
433    /// (content no present can compose) idles instead of spinning. Reset
434    /// whenever a present fails: a failed frame consumes nothing, so the
435    /// next success must re-evaluate from scratch instead of inheriting
436    /// the failure's suppression.
437    signaled: Cell<u64>,
438    namespace: u64,
439    /// Monotonic tag counter. Freed tags are never reassigned (unlike the
440    /// map length, which shrinks on removal), so element identity can never
441    /// alias across removals. Interior so [`Self::widget`] shares by ref.
442    tag_sequence: Cell<u64>,
443    /// Mount tag on the outlet root, used to locate it for [`Self::attach`].
444    root_key: Key,
445    /// Invalidation generation bumped once per driven frame; nested
446    /// stateful slots observe it to rebuild outlet content (builder-owned
447    /// children survive ancestor rebuilds, so nested state and focus
448    /// persist across outer frames). Doubles as the drive counter read by
449    /// [`Self::frame_drive_count`]: one begin per present per outlet.
450    revision: Rc<Cell<u64>>,
451    /// Attached mount element, if [`Self::attach`] located the outlet root.
452    /// Cleared when the mount disappears so later frames degrade to
453    /// frame-plus-reconcile instead of erroring forever.
454    mount: Cell<Option<ElementId>>,
455}
456
457impl RouteOutlet {
458    /// Hosts `navigator`, parenting route task scopes under `task_parent`
459    /// (typically the window scope, so close still cancels through existing
460    /// ownership).
461    pub fn new(navigator: &Navigator, task_parent: &TaskScope) -> Self {
462        let tags: Rc<RefCell<HashMap<RouteId, Key>>> = Rc::default();
463        let tags_for_oracle = tags.clone();
464        let nested: Rc<RefCell<Vec<Weak<RefCell<RouteOutlet>>>>> = Rc::default();
465        let nested_for_oracle = nested.clone();
466        let focus = RouteFocusState::new(move |tree: &WidgetTree, id: ElementId| {
467            // Ancestor chain of the element, self included. Nested outlets
468            // win first: anything at or below a nested outlet root belongs
469            // to that outlet, even with an outer tag above it. Remaining
470            // attribution is by ancestry — sibling route subtrees never
471            // contain each other, so any matched own tag is the nearest.
472            // Scans stay bounded: one ancestor walk plus one reverse lookup
473            // per tag, on transitions and vacant retries only — never a
474            // per-route sweep. Dead handles simply miss.
475            let mut chain = Vec::new();
476            let mut cursor = Some(id);
477            while let Some(current) = cursor {
478                chain.push(current);
479                cursor = tree.parent(current);
480            }
481            for nested in nested_for_oracle.borrow().iter() {
482                let Some(nested) = nested.upgrade() else {
483                    continue;
484                };
485                let nested = nested.borrow();
486                if let Some(root) = tree.element_with_key(&nested.root_key)
487                    && (root == id || chain.contains(&root))
488                {
489                    return None;
490                }
491            }
492            tags_for_oracle.borrow().iter().find_map(|(route, key)| {
493                tree.element_with_key(key)
494                    .is_some_and(|root| chain.contains(&root))
495                    .then_some(*route)
496            })
497        });
498        let namespace = OUTLET_SEQUENCE
499            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
500            .expect("route outlet identity space exhausted");
501        Self {
502            navigator: navigator.clone(),
503            focus,
504            bindings: HashMap::new(),
505            tags,
506            nested,
507            parent: Rc::default(),
508            driver: RefCell::default(),
509            task_parent: task_parent.clone(),
510            presented: None,
511            pending: None,
512            epoch: Cell::new(0),
513            composing: RefCell::default(),
514            consumed: None,
515            signaled: Cell::new(0),
516            namespace,
517            // NOTE: no separate drive counter — `revision` below advances
518            // once per driven frame and serves as the drive count.
519            tag_sequence: Cell::new(0),
520            root_key: Key::String(format!("route-outlet-root-{namespace}")),
521            revision: Rc::new(Cell::new(0)),
522            mount: Cell::new(None),
523        }
524    }
525
526    /// Registers a directly nested outlet whose content mounts inside this
527    /// outlet's routes. Nested elements resolve to the nested outlet, so
528    /// inner navigation can neither overwrite outer saved focus nor be
529    /// restored over by it. Sibling outlets sharing only an ancestor need
530    /// nothing: disjoint subtrees never attribute across. Transitivity
531    /// follows containment — register each outlet with its direct parent.
532    ///
533    /// Topology is enforced before any mutation, borrowing the
534    /// back-dispatch lesson without coupling to it: self-attachment and
535    /// cycles are rejected (cascade driving must terminate), and each
536    /// outlet has at most one live parent (cascade driving visits every
537    /// registered child, so a shared child would drive twice per frame).
538    /// Re-registering with the same parent is an idempotent no-op;
539    /// moving parents requires [`Self::detach_nested`] first.
540    /// Registration holds weakly: dropping the nested outlet releases it,
541    /// and dead entries prune on every present, so repeated replacement
542    /// never grows metadata.
543    pub fn attach_nested(
544        outlet: &Rc<RefCell<Self>>,
545        nested: &Rc<RefCell<Self>>,
546    ) -> Result<(), OutletAttachError> {
547        if Rc::ptr_eq(outlet, nested) {
548            return Err(OutletAttachError::SelfAttachment);
549        }
550        if nested.borrow().reaches(outlet) {
551            return Err(OutletAttachError::Cycle);
552        }
553        {
554            let child = nested.borrow();
555            let mut parent = child.parent.borrow_mut();
556            if let Some(current) = parent.as_ref().and_then(Weak::upgrade) {
557                if Rc::ptr_eq(&current, outlet) {
558                    return Ok(());
559                }
560                return Err(OutletAttachError::AlreadyAttached);
561            }
562            // A dead parent link is just a leftover: detach cleared the
563            // registration but the child outlived this assignment.
564            *parent = Some(Rc::downgrade(outlet));
565        }
566        let outlet = outlet.borrow_mut();
567        let mut attached = outlet.nested.borrow_mut();
568        attached.retain(|existing| {
569            existing
570                .upgrade()
571                .is_some_and(|live| !Rc::ptr_eq(&live, nested))
572        });
573        attached.push(Rc::downgrade(nested));
574        Ok(())
575    }
576
577    /// Detaches a nested outlet. The child keeps its own records, bindings,
578    /// and tags, but the parent's cascade no longer drives it: a detached
579    /// outlet receives no frame work even while application code retains
580    /// its handle. Detach also releases the child's driver claim, so
581    /// ownership transfers cleanly — reattaching elsewhere (or driving it
582    /// standalone, including under another runtime) claims anew.
583    /// Detaching a non-child is a no-op.
584    pub fn detach_nested(outlet: &Rc<RefCell<Self>>, nested: &Rc<RefCell<Self>>) {
585        {
586            let child = nested.borrow_mut();
587            let mut parent = child.parent.borrow_mut();
588            let mine = parent
589                .as_ref()
590                .and_then(Weak::upgrade)
591                .is_some_and(|current| Rc::ptr_eq(&current, outlet));
592            if mine {
593                parent.take();
594                child.driver.borrow_mut().take();
595            }
596        }
597        {
598            let outlet = outlet.borrow_mut();
599            outlet.nested.borrow_mut().retain(|existing| {
600                existing
601                    .upgrade()
602                    .is_some_and(|live| !Rc::ptr_eq(&live, nested))
603            });
604        }
605    }
606
607    /// Whether `target` is reachable from this outlet by following nested
608    /// attachments. Dead entries are skipped, never traversed.
609    fn reaches(&self, target: &Rc<RefCell<Self>>) -> bool {
610        let mut visited: Vec<*const RefCell<Self>> = Vec::new();
611        let mut stack: Vec<Rc<RefCell<Self>>> = self.live_nested();
612        // The outlet itself counts: self-attachment is checked separately
613        // for its error, but a zero-length path must not read as a cycle.
614        while let Some(node) = stack.pop() {
615            if Rc::ptr_eq(&node, target) {
616                return true;
617            }
618            let address = Rc::as_ptr(&node);
619            if visited.contains(&address) {
620                continue;
621            }
622            visited.push(address);
623            stack.extend(node.borrow().live_nested());
624        }
625        false
626    }
627
628    /// Frames driven through this outlet, cascade included. Each
629    /// [`Self::present_frame`] on the driving root advances every
630    /// participating outlet by exactly one. Reads the invalidation
631    /// generation, which advances once per driven frame and needs no
632    /// separate counter.
633    #[must_use]
634    pub fn frame_drive_count(&self) -> u64 {
635        self.revision.get()
636    }
637
638    /// Mounts a nested outlet's widget with the required builder identity:
639    /// a stable keyed boundary driven by the nested revision, so ancestor
640    /// rebuilds retain the slot instead of remounting it. Hosts compose
641    /// this inside outer route content instead of assembling stateful
642    /// builders by hand; no nested `attach` is needed on top. Presenting
643    /// the outer outlet drives the whole tree in one frame — nested
644    /// outlets never need their own `present_frame` calls.
645    pub fn nested_widget(nested: &Rc<RefCell<Self>>) -> Widget {
646        let (revision, namespace) = {
647            let nested = nested.borrow();
648            (nested.revision.clone(), nested.namespace)
649        };
650        let nested_for_build = Rc::clone(nested);
651        Widget::stateful_layout_builder(revision, move |_, _| {
652            nested_for_build.borrow().compose_and_record()
653        })
654        .with_key(Key::String(format!("route-outlet-slot-{namespace}")))
655    }
656
657    /// Number of live nested attachments. Dead handles prune on every
658    /// present; useful for hosts composing nested outlets dynamically.
659    #[must_use]
660    pub fn attached_nested_count(&self) -> usize {
661        self.nested
662            .borrow()
663            .iter()
664            .filter(|weak| weak.upgrade().is_some())
665            .count()
666    }
667
668    /// Live nested outlets for cascade driving.
669    fn live_nested(&self) -> Vec<Rc<RefCell<Self>>> {
670        self.nested
671            .borrow()
672            .iter()
673            .filter_map(Weak::upgrade)
674            .collect()
675    }
676
677    /// The live parent, if this outlet is attached under one.
678    fn live_parent(&self) -> Option<Rc<RefCell<Self>>> {
679        self.parent.borrow().as_ref().and_then(Weak::upgrade)
680    }
681
682    /// The live driver claim's runtime id, if a live runtime owns it. A
683    /// dead token means teardown released the claim: treated as unowned.
684    fn live_driver(&self) -> Option<u64> {
685        self.driver
686            .borrow()
687            .as_ref()
688            .and_then(|(id, token)| token.upgrade().is_some().then_some(*id))
689    }
690
691    /// Claims this outlet for `runtime`, superseding a dead owner's
692    /// leftover. Callers verify conflicts first.
693    fn claim_driver(&self, runtime: &Runtime) {
694        *self.driver.borrow_mut() = Some((runtime.id(), runtime.driver_token()));
695    }
696
697    /// Rejects conflicting drivers for the whole tree before any mutation:
698    /// the root must not nest under a live parent (drive the root
699    /// instead), and every participant must be unclaimed or claimed by
700    /// `runtime` — a live foreign claim fails with both identities.
701    fn check_drivers(&self, runtime: &Runtime, is_root: bool) -> Result<(), OutletError> {
702        if is_root && let Some(parent) = self.live_parent() {
703            return Err(OutletError::SeparateDrive {
704                outlet: self.id(),
705                parent: parent.borrow().id(),
706            });
707        }
708        if let Some(owner) = self.live_driver()
709            && owner != runtime.id()
710        {
711            return Err(OutletError::DriverConflict {
712                outlet: self.id(),
713                owner,
714                attempted: runtime.id(),
715            });
716        }
717        for nested in self.live_nested() {
718            nested.borrow().check_drivers(runtime, false)?;
719        }
720        Ok(())
721    }
722
723    /// Claims the whole tree for `runtime` after [`Self::check_drivers`]
724    /// passes. Refreshes matching claims harmlessly.
725    fn claim_tree(&self, runtime: &Runtime) {
726        self.claim_driver(runtime);
727        for nested in self.live_nested() {
728            nested.borrow().claim_tree(runtime);
729        }
730    }
731
732    /// Returns the navigator this outlet hosts.
733    #[must_use]
734    pub fn navigator(&self) -> &Navigator {
735        &self.navigator
736    }
737
738    /// Returns this outlet's identity, for error data and host bookkeeping.
739    #[must_use]
740    pub fn id(&self) -> OutletId {
741        OutletId(self.namespace)
742    }
743
744    /// Whether navigation arrived that no composition consumed yet — for
745    /// this outlet or any nested outlet. True before the first composition
746    /// with a non-empty stack, and after navigation newer than the last
747    /// consumed snapshot (including navigation during a build, whose paint
748    /// lags one frame behind the bookkeeping). False once composition
749    /// consumes current state. Diagnostics and custom drivers only:
750    /// ordinary hosts must not poll this for mandatory work — the
751    /// scheduler flag from `present_frame` carries that. It does not
752    /// report failed frames (their error return does that).
753    #[must_use]
754    pub fn needs_frame(&self) -> bool {
755        let stale = match &self.consumed {
756            None => !self.navigator.routes().is_empty(),
757            Some(consumed) => consumed.revision != self.navigator.revision(),
758        };
759        stale
760            || self
761                .live_nested()
762                .iter()
763                .any(|nested| nested.borrow().needs_frame())
764    }
765
766    /// Rejects this outlet's stack when it contains portal-managed overlay
767    /// routes, which the outlet never mounts. Pure check: no state changes
768    /// either way.
769    fn preflight(&self) -> Result<(), OutletError> {
770        let unsupported: Vec<RouteId> = self
771            .navigator
772            .routes()
773            .iter()
774            .filter(|route| route.presentation.is_overlay())
775            .map(|route| route.id)
776            .collect();
777        if unsupported.is_empty() {
778            Ok(())
779        } else {
780            Err(OutletError::UnsupportedPresentation {
781                outlet: self.id(),
782                routes: unsupported,
783            })
784        }
785    }
786
787    /// Validates the whole outlet tree top-down: this outlet, then every
788    /// nested outlet transitively. Pure check — no revisions, captures,
789    /// rebuilds, or frames happen on either outcome — so
790    /// [`Self::present_frame`] runs it before driving anything and again
791    /// after the frame (navigation may have moved mid-build) before
792    /// committing anything. Reports the outermost offender first.
793    fn preflight_tree(&self) -> Result<(), OutletError> {
794        self.preflight()?;
795        for nested in self.live_nested() {
796            nested.borrow().preflight_tree()?;
797        }
798        Ok(())
799    }
800
801    /// Attaches the outlet to its mounted widget so [`Self::present_frame`]
802    /// rebuilds outlet content every frame. Call once after mounting
803    /// [`Self::widget`]: the mount element is located through the outlet
804    /// root tag. Re-attaching replaces the previous builder harmlessly.
805    ///
806    /// Attachment claims the outlet for `runtime`'s driver domain: an
807    /// outlet nested under a live parent, or claimed by another live
808    /// runtime, is rejected before mutation. Detach (nested) or drop the
809    /// owning runtime (teardown releases) before attaching elsewhere.
810    ///
811    /// ```rust
812    /// use std::{cell::RefCell, rc::Rc};
813    /// use incular_config::Constraints;
814    /// use incular_core::{Color, Size};
815    /// use incular_navigation::{Navigator, Page};
816    /// use incular_runtime::{RouteOutlet, Runtime};
817    /// use incular_widgets::{Column, Widget};
818    ///
819    /// let navigator = Navigator::new();
820    /// let mut runtime =
821    ///     Runtime::new(Column::new(Vec::<Widget>::new()).into()).unwrap();
822    /// let parent = runtime.spawner().scope();
823    /// let outlet = Rc::new(RefCell::new(RouteOutlet::new(&navigator, &parent)));
824    /// // One-time setup: mount the outlet widget, then attach its builder.
825    /// let root = runtime.tree().root().expect("root");
826    /// let outlet_for_build = outlet.clone();
827    /// runtime
828    ///     .register_builder(root, move || {
829    ///         Column::new(vec![outlet_for_build.borrow().widget()]).into()
830    ///     })
831    ///     .unwrap();
832    /// runtime
833    ///     .run_frame(Constraints::tight(Size::new(200., 200.)))
834    ///     .unwrap();
835    /// RouteOutlet::attach(&outlet, &mut runtime).unwrap();
836    /// navigator.push_page(Page::new("a", Widget::box_(Size::new(40., 40.), Color::WHITE)));
837    /// // The single recurring operation: rebuild, frame, reconcile.
838    /// let _frame =
839    ///     RouteOutlet::present_frame(&outlet, &mut runtime, Constraints::tight(Size::new(200., 200.)))
840    ///         .unwrap();
841    /// assert!(navigator.current().is_some());
842    /// ```
843    pub fn attach(
844        outlet: &Rc<RefCell<RouteOutlet>>,
845        runtime: &mut Runtime,
846    ) -> Result<(), OutletError> {
847        {
848            let outlet = outlet.borrow();
849            if let Some(parent) = outlet.live_parent() {
850                return Err(OutletError::SeparateDrive {
851                    outlet: outlet.id(),
852                    parent: parent.borrow().id(),
853                });
854            }
855            if let Some(owner) = outlet.live_driver()
856                && owner != runtime.id()
857            {
858                return Err(OutletError::DriverConflict {
859                    outlet: outlet.id(),
860                    owner,
861                    attempted: runtime.id(),
862                });
863            }
864        }
865        let mount = {
866            let outlet = outlet.borrow();
867            runtime
868                .tree()
869                .element_with_key(&outlet.root_key)
870                .ok_or_else(|| {
871                    OutletError::Frame(TreeError::InvalidWidgetConfiguration {
872                        widget: "RouteOutlet",
873                        reason: "mount outlet.widget() in the tree before attaching".to_owned(),
874                    })
875                })?
876        };
877        // The builder carries no reactive dependencies: present_frame
878        // rebuilds it explicitly, so content never refreshes behind the
879        // transition bookkeeping's back.
880        let outlet_for_build = Rc::clone(outlet);
881        runtime
882            .register_builder(mount, move || {
883                outlet_for_build.borrow().compose_and_record()
884            })
885            .map_err(OutletError::Frame)?;
886        outlet.borrow().claim_driver(runtime);
887        outlet.borrow_mut().mount.set(Some(mount));
888        Ok(())
889    }
890
891    /// Builds the outlet widget: mounted routes in stack order with each
892    /// route's veil as a direct sibling preceding its content, so barrier
893    /// semantics block every earlier route while the veiled route itself
894    /// stays visible. The visible suffix paints from the top down to the
895    /// first opaque page inclusive; retained but covered routes stay
896    /// Builds the outlet widget for mounting, without touching any
897    /// bookkeeping: safe to call speculatively and discard. Only the
898    /// mounting builders (registered by `attach`, and nested slots)
899    /// acknowledge what they accept. Covered routes without retention
900    /// unmount (disposal); retained ones stay mounted inert while
901    /// invisible; overlay entries are left for their portal host.
902    #[must_use]
903    pub fn widget(&self) -> Widget {
904        self.compose().0
905    }
906
907    /// Composes the widget from the current routes alongside the immutable
908    /// snapshot it was built from. Pure: acquisition and composition stay
909    /// together here, publication happens only where the result mounts.
910    fn compose(&self) -> (Widget, FrameAttempt) {
911        let routes = self.navigator.routes();
912        let attempt = FrameAttempt::of(self.navigator.revision(), &routes);
913        // Top-down visibility: everything paints until (and including) the
914        // first opaque page.
915        let mut visible = vec![false; routes.len()];
916        let mut occluded = false;
917        for (index, route) in routes.iter().enumerate().rev() {
918            visible[index] = !occluded;
919            if route.presentation.is_opaque() {
920                occluded = true;
921            }
922        }
923        let mut children = Vec::new();
924        for (route, visible) in routes.iter().zip(visible) {
925            // Overlay entries never mount here: `present_frame` rejects
926            // their stacks typed (before and after the frame), so reaching
927            // this skip means setup-time composition ahead of validation —
928            // still omitted, never half-mounted, and never silent through
929            // the supported path.
930            let OutletPlacement::Stacked { visible, barrier } =
931                OutletPlacement::of(&route.presentation, visible)
932            else {
933                continue;
934            };
935            if !visible && !route.presentation.maintains_state() {
936                continue;
937            }
938            // Veil before content: semantic blocking hides preceding
939            // siblings only, so the veil must precede its route's content
940            // (paint and hit order still resolve top-down correctly, since
941            // the veil sits above every earlier route). Veils follow
942            // visibility: a covered retained route keeps its content but
943            // not an active veil — the veil would dim and intercept input
944            // for content the user can no longer see.
945            if visible && let Some(barrier) = barrier {
946                children.push(self.veil_widget(route.id, &barrier));
947            }
948            children.push(self.content_widget(route, visible));
949        }
950        // The outlet root carries the mount tag so `attach` locates it;
951        // route tags live on the per-route wrappers inside. The root is
952        // always a stack — even empty — so mounting and unmounting the
953        // last route never flips the element kind.
954        let root: Widget = Stack::new(children).into();
955        (root.with_key(self.root_key.clone()), attempt)
956    }
957
958    /// Composes for mounting and publishes the consumed receipt: the only
959    /// path (besides [`Self::compose`]'s pure construction) that writes
960    /// [`Self::composing`], stamped with the current attempt. Called
961    /// solely by the mounting builders, so a discarded or speculative
962    /// descriptor can never authorize a frame.
963    fn compose_and_record(&self) -> Widget {
964        let (widget, attempt) = self.compose();
965        *self.composing.borrow_mut() = Some((self.epoch.get(), attempt));
966        widget
967    }
968
969    /// Composes one mounted route's content inside the outlet's own
970    /// visibility wrapper carrying the mount tag. The tag sits on the
971    /// outlet-owned wrapper — never on the presented child — so caller
972    /// keys on route content survive navigation untouched, and the tag
973    /// element stays stable across visibility flips.
974    fn content_widget(&self, route: &Route, visible: bool) -> Widget {
975        let content = route.presented_child();
976        let mut wrapped: Widget = Visibility::new(content)
977            .visible(visible)
978            .maintain_state(true)
979            .into();
980        if !visible {
981            wrapped = ExcludeFocusTraversal::new(wrapped).into();
982        }
983        wrapped.with_key(self.tag_for(route.id))
984    }
985
986    /// Veils the outlet area for one route through the shared animated
987    /// barrier mechanism: unconditional pointer interception, background
988    /// semantics blocked for every preceding sibling, and dismissal that
989    /// pops the barrier's own route — never whatever happens to be on top.
990    /// The veil is a direct stack sibling preceding its content (not nested
991    /// inside it) so semantic blocking reaches across routes while the
992    /// route's own content stays visible; it is sized to the outlet area
993    /// because an unsized barrier container collapses and lets taps fall
994    /// through to the supposedly blocked content.
995    fn veil_widget(&self, route: RouteId, barrier: &ModalBarrier) -> Widget {
996        let navigator = self.navigator.clone();
997        let barrier = barrier.clone();
998        Widget::from(LayoutBuilder::new(move |_, constraints| {
999            // Bounded hosts (windows, dialogs) size the veil to the outlet
1000            // area; unbounded heights fall back to the raw veil, which still
1001            // blocks its own content but cannot cover an unknown area.
1002            let size = constraints.biggest();
1003            let navigator = navigator.clone();
1004            let mut veil = AnimatedModalBarrier::with_flat_color(barrier.color)
1005                .dismissible(barrier.dismissible);
1006            if let Some(label) = barrier.label.clone() {
1007                veil = veil.semantics_label(label);
1008            }
1009            let veiled: Widget = veil
1010                .on_dismiss(move || {
1011                    if navigator
1012                        .current()
1013                        .is_some_and(|current| current.id == route)
1014                    {
1015                        navigator.pop();
1016                    }
1017                })
1018                .into();
1019            if size.width.is_finite() && size.height.is_finite() {
1020                SizedBox::from_dimensions(Some(size.width), Some(size.height), Some(veiled)).into()
1021            } else {
1022                veiled
1023            }
1024        }))
1025        .block_semantics()
1026    }
1027
1028    /// Returns the route task scope for spawning route-bound work, if the
1029    /// route is currently mounted and bound.
1030    #[must_use]
1031    pub fn route_task_scope(&self, route: RouteId) -> Option<TaskScope> {
1032        self.bindings
1033            .get(&route)
1034            .map(|binding| binding.scope().clone())
1035    }
1036
1037    /// Presents one production frame through the outlet: validates the
1038    /// tree, captures any pending transition, rebuilds outlet content,
1039    /// runs layout, then commits the transition against the mounted tree.
1040    /// This is the supported recurring operation: the save runs before
1041    /// the frame reconciles (rebuilding may unmount covered content and
1042    /// the drain clears dead focus), while the restore runs after layout
1043    /// when targets are eligible.
1044    ///
1045    /// Takes the outlet shared (rather than `&mut self`) because the
1046    /// attached builder reenters it immutably while the frame runs; short
1047    /// borrows never span the frame. Driving claims the whole tree for
1048    /// `runtime`: presenting a nested child directly, or through a second
1049    /// live runtime, fails before any mutation. A rebuild or frame failure
1050    /// keeps the pending capture for a safe retry instead of consuming the
1051    /// activation, and navigation mid-frame abandons the stale capture
1052    /// without touching committed records.
1053    pub fn present_frame(
1054        outlet: &Rc<RefCell<Self>>,
1055        runtime: &mut Runtime,
1056        constraints: Constraints,
1057    ) -> Result<(DisplayList, FrameStats), OutletError> {
1058        // Unsupported stacks anywhere in the tree fail before revisions,
1059        // capture, rebuild, frame, or reconcile — nothing mounted or
1060        // committed changes on this path. Driver conflicts fail next, also
1061        // before any mutation; the claim lands only on this success path.
1062        outlet.borrow().preflight_tree()?;
1063        outlet.borrow().check_drivers(runtime, true)?;
1064        outlet.borrow().claim_tree(runtime);
1065        {
1066            let mut outlet = outlet.borrow_mut();
1067            outlet.begin_frame(runtime);
1068        }
1069        // The fallible section runs builders, layout, and reconciliation —
1070        // all application-reachable code. The attempt guard owns cleanup:
1071        // every early return — and panic unwind — drops it armed and
1072        // resets the schedule memos, since a failed present consumes
1073        // nothing and the next success must re-evaluate outstanding work.
1074        // Neither path repairs tree state — recovery stays the retry's
1075        // job.
1076        let mut attempt = AttemptGuard::arm(outlet);
1077        // Rebuild outlet content explicitly before framing: reactive
1078        // dependencies alone cannot cover content the host descriptors
1079        // never mention, and an explicit rebuild keeps mounting ordered
1080        // with the capture above. A vanished mount (unmounted content
1081        // prunes its builder) detaches gracefully instead of erroring —
1082        // or panicking — every frame. The existence check precedes the
1083        // rebuild because pruned builders no longer fail with
1084        // `MissingElement`.
1085        let attached = outlet
1086            .borrow()
1087            .mount
1088            .get()
1089            .filter(|mount| runtime.tree().element_exists(*mount));
1090        if attached.is_none() {
1091            outlet.borrow_mut().mount.set(None);
1092        }
1093        if let Some(mount) = attached {
1094            match runtime.rebuild_from_builder(mount) {
1095                Ok(()) => {}
1096                Err(TreeError::MissingElement(_)) => {
1097                    outlet.borrow_mut().mount.set(None);
1098                }
1099                Err(error) => return Err(OutletError::Frame(error)),
1100            }
1101        }
1102        // No post-rebuild sampling here: each outlet's builders publish
1103        // what they actually consume (see `consumed`), and reconciliation
1104        // adopts it. Sampling afterward would credit the rebuild with
1105        // navigation it never saw.
1106        let output = runtime.run_frame(constraints)?;
1107        // A preflight snapshot cannot authorize later content: builders
1108        // may have navigated mid-frame, so the tree re-validates before
1109        // anything commits. On failure the pending capture survives for a
1110        // retry (which preflights first anyway) and no commit, bind,
1111        // prune, or restore runs — the error names the offender instead
1112        // of silently omitting its content.
1113        outlet.borrow().preflight_tree()?;
1114        outlet.borrow_mut().reconcile_tree(runtime);
1115        attempt.disarm();
1116        // Successful-but-stale output schedules its own follow-up: paint
1117        // may lag the bookkeeping by a frame, and the run_frame reset
1118        // wipes anything flagged mid-frame. Quiet frames schedule
1119        // nothing; errors never reach here.
1120        outlet.borrow().flag_stale_frames(runtime);
1121        Ok(output)
1122    }
1123
1124    /// Captures a pending transition when none is outstanding and the
1125    /// active route moved since the last commit. The outgoing focus is
1126    /// read once into the pending slot — never into the records — alongside
1127    /// the attempt snapshot, so a later retry cannot overwrite it with
1128    /// intermediate focus, and abandoning the capture drops it without
1129    /// touching committed state.
1130    fn capture_transition(&mut self, runtime: &Runtime) {
1131        if self.pending.is_some() {
1132            return;
1133        }
1134        let active = self.navigator.current().map(|route| route.id);
1135        if active == self.presented {
1136            return;
1137        }
1138        let saved = self.presented.and_then(|previous| {
1139            self.navigator
1140                .lifetime_of(previous)
1141                .is_some()
1142                .then(|| self.focus.read_owned_focus(runtime, previous))
1143                .flatten()
1144        });
1145        self.pending = Some(PendingTransition {
1146            previous: self.presented,
1147            incoming: active,
1148            saved,
1149            attempt: FrameAttempt::capture(&self.navigator),
1150        });
1151    }
1152
1153    /// Commits the pending transition when the frame actually presented
1154    /// it: the active route still matches the captured incoming route, the
1155    /// incoming lifetime is alive, and the attempt snapshot still covers
1156    /// the navigator — so the mounted content is the transition's own.
1157    /// Anything else — navigation mid-frame beyond order/child-only
1158    /// changes, or a removed incoming route — abandons the capture and
1159    /// re-captures fresh, leaving committed records alone. Returns whether
1160    /// a transition committed.
1161    fn commit_transition(&mut self, runtime: &mut Runtime) -> bool {
1162        let active = self.navigator.current().map(|route| route.id);
1163        let Some(pending) = self.pending.take() else {
1164            return false;
1165        };
1166        if active != pending.incoming
1167            || pending
1168                .incoming
1169                .is_some_and(|incoming| self.navigator.lifetime_of(incoming).is_none())
1170            || !pending.attempt.covers(&self.navigator)
1171        {
1172            // Stale capture (already taken above): re-capture fresh for the
1173            // route that actually won, without writing anything first.
1174            self.capture_transition(runtime);
1175            return false;
1176        }
1177        if let Some(previous) = pending.previous {
1178            self.focus.commit_saved(previous, pending.saved);
1179        }
1180        self.presented = active;
1181        self.pending = None;
1182        true
1183    }
1184
1185    /// Drops records, bindings, and tags for permanently removed routes.
1186    /// Lifetime-end notifications already fired at commit time, so bound
1187    /// scopes were cancelled before their bindings drop here. Unmounted
1188    /// but live routes (covered, retained, disposal-unmounted) keep
1189    /// everything: removal is a lifetime event, not a mount event.
1190    fn prune_removed(&mut self) {
1191        self.focus.retain_mounted(&self.navigator);
1192        self.bindings
1193            .retain(|id, _| self.navigator.lifetime_of(*id).is_some());
1194        self.tags
1195            .borrow_mut()
1196            .retain(|id, _| self.navigator.lifetime_of(*id).is_some());
1197    }
1198
1199    /// Binds every live route lacking a binding. Scope ownership follows
1200    /// the route lifetime — not mounted content: covered, retained, and
1201    /// even disposal-unmounted routes stay bound while alive, so their
1202    /// tasks outlive their pixels; only permanent removal ends them.
1203    /// Focus records are the mounted-content side of the integration
1204    /// (attributed through the live tree); bindings are the lifetime side
1205    /// (keyed by navigator state, including routes with no pixels).
1206    fn ensure_bindings(&mut self) {
1207        for route in self.navigator.routes() {
1208            if !self.bindings.contains_key(&route.id)
1209                && let Some(lifetime) = self.navigator.lifetime_of(route.id)
1210            {
1211                self.bindings.insert(
1212                    route.id,
1213                    RouteTaskBinding::bind(&self.task_parent, &lifetime),
1214                );
1215            }
1216        }
1217    }
1218
1219    /// Restores on committed transitions; otherwise retries a pending
1220    /// restore only when it cannot displace valid focus (deferred content
1221    /// may have mounted or enabled through ordinary invalidation since).
1222    fn restore_or_retry(&mut self, runtime: &mut Runtime, transitioned: bool) {
1223        let Some(id) = self.navigator.current().map(|route| route.id) else {
1224            return;
1225        };
1226        if transitioned {
1227            self.focus.restore_saved(runtime, &self.navigator, id);
1228        } else {
1229            self.focus.restore_if_vacant(runtime, &self.navigator, id);
1230        }
1231    }
1232
1233    /// Promotes the in-flight composition into the published snapshot and
1234    /// adopts it into the pending attempt — without touching the outgoing
1235    /// save — so the commit judges built content, and retries describe
1236    /// their own build. Runs only on the success path (reconciliation),
1237    /// so failed composition publishes nothing; skipped builders simply
1238    /// re-promote their previous publication. Only mounting builders
1239    /// write the slot, and only the current attempt's receipt adopts: a
1240    /// stale receipt (failed attempt, predated capture) authorizes
1241    /// nothing even at a matching revision. Within the attempt, only
1242    /// newer-or-equal snapshots relabel. All values are immutable copies,
1243    /// so application callbacks running later in reconciliation cannot
1244    /// relabel built content.
1245    fn adopt_consumed(&mut self) {
1246        let Some((epoch, composing)) = self.composing.borrow().clone() else {
1247            return;
1248        };
1249        if epoch != self.epoch.get() {
1250            return;
1251        }
1252        if let Some(pending) = self.pending.as_mut()
1253            && composing.revision >= pending.attempt.revision
1254        {
1255            pending.attempt = composing.clone();
1256        }
1257        self.consumed = Some(composing);
1258    }
1259
1260    /// Schedules follow-up frames for stale output through the runtime's
1261    /// own invalidation (`request_frame`) — but only for revisions never
1262    /// scheduled before. Successful output older than live navigation
1263    /// still requires a frame, and ordinary hosts must not poll
1264    /// [`Self::needs_frame`] to discover it; the edge trigger keeps
1265    /// unconsumable staleness (and every quiet frame) from scheduling
1266    /// anything. Errors return before reconciliation, so rejected stacks
1267    /// schedule no retry loop. Nested staleness wakes the root driver
1268    /// through the same cascade. The per-revision suppression loses no
1269    /// outstanding demand: failures and panics reset it, and every new
1270    /// navigation (including slot remounts) carries a new revision.
1271    fn flag_stale_frames(&self, runtime: &mut Runtime) {
1272        let live = self.navigator.revision();
1273        let stale = match &self.consumed {
1274            None => !self.navigator.routes().is_empty(),
1275            Some(consumed) => consumed.revision != live,
1276        };
1277        if stale && self.signaled.get() != live {
1278            self.signaled.set(live);
1279            runtime.request_frame();
1280        }
1281        for nested in self.live_nested() {
1282            nested.borrow().flag_stale_frames(runtime);
1283        }
1284    }
1285
1286    /// Clears schedule memos across the tree. A failed present consumes
1287    /// nothing, so its suppression must not survive: the next success
1288    /// re-evaluates outstanding work from scratch.
1289    fn reset_signaled_tree(&self) {
1290        self.signaled.set(0);
1291        for nested in self.live_nested() {
1292            nested.borrow().reset_signaled_tree();
1293        }
1294    }
1295
1296    /// Post-frame reconciliation: adopt the consumed snapshot, commit the
1297    /// pending transition against it, then prune, bind, and restore or
1298    /// retry. The commit runs before pruning so a just-committed record
1299    /// for a route removed mid-frame is dropped rather than resurrected.
1300    fn reconcile_presented(&mut self, runtime: &mut Runtime) {
1301        self.adopt_consumed();
1302        let transitioned = self.commit_transition(runtime);
1303        self.prune_removed();
1304        self.ensure_bindings();
1305        self.restore_or_retry(runtime, transitioned);
1306    }
1307
1308    /// Bumps this outlet and every nested outlet, then captures pending
1309    /// transitions top-down. One call drives the whole outlet tree, so a
1310    /// single frame presents nested navigation without rendering the
1311    /// runtime once per outlet. The topology guarantees (acyclic,
1312    /// single-parent) make every participant reachable exactly once.
1313    fn begin_frame(&mut self, runtime: &Runtime) {
1314        self.epoch.set(self.epoch.get().wrapping_add(1));
1315        self.revision.set(
1316            self.revision
1317                .get()
1318                .checked_add(1)
1319                .expect("revision exhausted"),
1320        );
1321        self.capture_transition(runtime);
1322        for nested in self.live_nested() {
1323            nested.borrow_mut().begin_frame(runtime);
1324        }
1325    }
1326
1327    /// Reconciles this outlet and every nested outlet after a frame,
1328    /// children first so the outer context wins focus ties
1329    /// deterministically. Dead nested handles prune here, keeping
1330    /// registrations bounded no matter how often hosts replace outlets.
1331    fn reconcile_tree(&mut self, runtime: &mut Runtime) {
1332        for nested in self.live_nested() {
1333            nested.borrow_mut().reconcile_tree(runtime);
1334        }
1335        self.reconcile_presented(runtime);
1336        self.nested
1337            .borrow_mut()
1338            .retain(|weak| weak.upgrade().is_some());
1339    }
1340
1341    /// Stable mount tag for a route, assigned once while the outlet lives.
1342    /// Tags are never reused, so a re-pushed route mounts fresh even under
1343    /// the same page key.
1344    fn tag_for(&self, route: RouteId) -> Key {
1345        if let Some(key) = self.tags.borrow().get(&route) {
1346            return key.clone();
1347        }
1348        let mut tags = self.tags.borrow_mut();
1349        if let Some(key) = tags.get(&route) {
1350            return key.clone();
1351        }
1352        let sequence = self.tag_sequence.get();
1353        self.tag_sequence.set(sequence.wrapping_add(1));
1354        let key = Key::String(format!("route-outlet-{}-{sequence}", self.namespace));
1355        tags.insert(route, key.clone());
1356        key
1357    }
1358}