Skip to main content

frust_core/
component.rs

1//! Component: Flutter's `StatefulWidget` analog.
2//!
3//! A [`Component`] is a piece of UI with *retained local state* that lives in
4//! the widget tree, a per-component reactive [`Owner`] (context scoping +
5//! `on_cleanup`), and a state boundary the surrounding view tree never sees. It
6//! is the seam that lets a subtree own state independent of the ambient
7//! application state: a [`ComponentView`] implements `View<Outer>` for **any**
8//! outer state, and the subtree it hosts is diffed against the component's own
9//! `State` instead.
10//!
11//! # The state boundary
12//!
13//! [`ComponentWidget`] builds an inner [`EventCtx`] over its own
14//! `C::State` and dispatches events to its child through the same
15//! capture/focus/IME routing contract single-child containers use (mirroring
16//! `frust-widgets`' `route_event_single`), then mirrors the inner results
17//! (redraw / capture / focus request-release / IME publish) back onto the
18//! *outer* context. The subtree therefore mutates the component's local state
19//! while the outer tree only observes the effects (a redraw, a capture, a focus
20//! change) — exactly as a container observes them from a leaf.
21//!
22//! # Reactive ownership
23//!
24//! Each component owns a child [`Owner`] created under the ambient owner (the
25//! root `Owner` a shell wraps its rebuild in; a nested component inherits its
26//! parent component's owner because build/rebuild run under `owner.with`). The
27//! owner scopes `provide_context`/`use_context` and `on_cleanup`, and is
28//! disposed explicitly at teardown (and defensively on `Drop`).
29//!
30//! # What `Component` does NOT do (yet)
31//!
32//! Per-component tracked/reactive scopes and rebuild skipping are not
33//! implemented: a component **always** re-runs `build` on rebuild because its
34//! local state may have changed even when the component value compares equal.
35//! Root-level components are never torn down (the root widget lives for the
36//! app's life), so a root component's `on_cleanup`/owner disposal never runs —
37//! a known, accepted limitation.
38
39use std::any::Any;
40
41use kurbo::{Point, Size};
42use reactive_graph::owner::Owner;
43
44use crate::event::{EventCtx, EventResult, InputEvent, PointerPhase};
45#[cfg(feature = "hotpatch")]
46use crate::hotpatch::SeamWitness;
47use crate::layout::BoxConstraints;
48use crate::sliver::{SliverConstraints, SliverGeometry, SliverWidget};
49use crate::view::{AnyView, BuildCtx, ChangeFlags, View};
50use crate::widget::{ChildPod, LayoutCtx, PaintCtx, PaintScene, Widget};
51
52/// A piece of UI with retained local state (Flutter's `StatefulWidget` analog).
53///
54/// A `Component` declares an associated [`Component::State`] type, seeds it once
55/// with [`Component::init`], and produces its subtree with [`Component::build`],
56/// which receives `&mut State` so the build can read (and the subtree's events
57/// can mutate) the retained state. The subtree is any `impl View` over the
58/// component's own `State` — the outer state type never appears. Erasure
59/// happens at the API boundary: [`ComponentWidget`] wraps whatever `build`
60/// returns in an [`AnyView`] before storing it, and because [`AnyView::new`] is
61/// idempotent a body that already returns an `AnyView` (e.g. `any(..)`) is not
62/// boxed a second time.
63pub trait Component: 'static {
64    /// The retained local state this component owns across rebuilds.
65    type State: 'static;
66
67    /// Create the component's initial local state. Runs exactly once, when the
68    /// [`ComponentWidget`] is first built, under the component's [`Owner`] (so
69    /// `on_cleanup`/`provide_context` registered here bind to that owner).
70    ///
71    /// A *root* component (the one an entry macro or `frust::run` drives) has
72    /// no enclosing [`ComponentWidget`] and so no per-component owner of its
73    /// own: the shell instead runs its `init`/`build` under the shell's **root**
74    /// [`Owner`], which lives for the whole process and is never disposed. Its
75    /// `on_cleanup`/`provide_context` therefore bind to that root owner — the
76    /// context is visible to the entire tree, and cleanups run at process exit
77    /// rather than on teardown (there is no teardown for the root).
78    fn init(&self) -> Self::State;
79
80    /// Produce the component's subtree from its current local state.
81    ///
82    /// Re-run on every rebuild (local state may have changed even when the
83    /// component value is equal). The returned view is erased into an
84    /// [`AnyView`] by the hosting [`ComponentWidget`] and diffed against the
85    /// previous one exactly like `RenderRoot`'s root view is. Implementations
86    /// declare the return type as `impl View<Self::State>`; returning a concrete
87    /// view type or an `any(..)` value both satisfy it.
88    ///
89    /// The opaque return type captures the `&self` and `&mut State` borrows, so
90    /// a caller that hands the result out of a closure (a root driver's
91    /// `move |state| ..` build closure) erases it first with
92    /// `AnyView::new(root.build(state))`.
93    fn build(&self, state: &mut Self::State) -> impl View<Self::State>;
94}
95
96/// The `View` adapter that hosts a [`Component`] in any surrounding view tree.
97///
98/// `ComponentView<C>` implements `View<Outer>` for **every** `Outer` state:
99/// the hosted subtree is bound to `C::State`, not `Outer`, so a component can
100/// be dropped anywhere regardless of the ambient application state. Build it
101/// with the [`component`] free function.
102pub struct ComponentView<C: Component> {
103    component: C,
104}
105
106/// Host a [`Component`] as a [`View`] — the view-fn spelling of
107/// [`ComponentView`], mirroring the `text(..)`/`button(..)` vocabulary.
108pub fn component<C: Component>(c: C) -> ComponentView<C> {
109    ComponentView { component: c }
110}
111
112/// The retained widget behind a [`ComponentView`].
113///
114/// Owns the component's local `State`, the previous child [`AnyView`] (diffed
115/// each rebuild), the child element wrapped in a [`ChildPod`], the component's
116/// reactive [`Owner`], and a component-local [`BuildCtx`] id counter.
117///
118/// The child element is stored **double-boxed** inside the pod (`ChildPod::new(
119/// Box::new(element))`, where `element: Box<dyn Widget>` is the [`AnyView`]'s
120/// element): the extra box lets a rebuild recover the child as
121/// `&mut Box<dyn Widget>`, the exact type [`AnyView::rebuild`] needs to swap the
122/// widget on a concrete-type change. This mirrors `frust-widgets`'
123/// `build_child`/`rebuild_child` pod convention.
124///
125/// # Under the `hotpatch` feature
126///
127/// The struct is `repr(C)`, starts with the [`SeamWitness`] the creating image
128/// wrote, and holds the state boxed, so every field sits at the same offset in
129/// every image whatever `C::State` looks like there. `rebuild`, `teardown` and
130/// `Drop` check the witness first; on a mismatch nothing `C`-generic touches the
131/// state, and `Drop` leaks the state box (`mem::forget`) rather than let another
132/// image's layout drop or free it, while still disposing the owner.
133#[cfg_attr(feature = "hotpatch", repr(C))]
134pub struct ComponentWidget<C: Component> {
135    /// The layout of `C` and `C::State` in the image that built this widget.
136    #[cfg(feature = "hotpatch")]
137    witness: SeamWitness,
138    /// The component's retained local state; the inner [`EventCtx`] is built
139    /// over this, so the subtree's event handlers mutate it via `state_mut`.
140    /// Always `Some` while the widget is live; `Drop` takes it to leak it on a
141    /// layout mismatch.
142    #[cfg(feature = "hotpatch")]
143    state: Option<Box<C::State>>,
144    /// The component's retained local state; the inner [`EventCtx`] is built
145    /// over this, so the subtree's event handlers mutate it via `state_mut`.
146    #[cfg(not(feature = "hotpatch"))]
147    state: C::State,
148    /// The previous child view, diffed against the freshly built one each
149    /// rebuild (the same reconcile `RenderRoot::rebuild_view` performs).
150    prev: AnyView<C::State>,
151    /// The child element (a double-boxed [`AnyView`] element) plus its geometry
152    /// and capture/focus bookkeeping.
153    child: ChildPod,
154    /// The component's reactive owner — child of the ambient owner, scoping
155    /// context and `on_cleanup`. Disposed explicitly at teardown / drop.
156    owner: Owner,
157    /// Component-local widget-id counter for the child's [`BuildCtx`] (decoupled
158    /// from the arena's monotonic ids because pod-owned children never enter the
159    /// arena).
160    next_id: u64,
161    /// Whether the owner has already been disposed, so teardown + `Drop` never
162    /// run cleanups twice.
163    disposed: bool,
164}
165
166impl<C: Component> ComponentWidget<C> {
167    /// Dispose the component's owner exactly once, running its `on_cleanup`s and
168    /// dropping any arena-allocated reactive values. Idempotent: teardown and a
169    /// later defensive `Drop` both call this, but the cleanups fire once.
170    fn dispose(&mut self) {
171        if !self.disposed {
172            // `Owner::cleanup` runs this owner's (and its children's) registered
173            // `on_cleanup`s and drops its arena nodes; it drains those lists, so
174            // a second call — including the `Drop` fallback below — is a no-op.
175            self.owner.cleanup();
176            self.disposed = true;
177        }
178    }
179
180    /// The retained local state.
181    #[cfg(feature = "hotpatch")]
182    fn state_mut(&mut self) -> &mut C::State {
183        self.state
184            .as_deref_mut()
185            .expect("component state is present while the widget is live")
186    }
187
188    /// The retained local state.
189    #[cfg(not(feature = "hotpatch"))]
190    fn state_mut(&mut self) -> &mut C::State {
191        &mut self.state
192    }
193
194    /// The retained local state, read-only.
195    #[cfg(all(test, feature = "hotpatch"))]
196    fn state(&self) -> &C::State {
197        self.state
198            .as_deref()
199            .expect("component state is present while the widget is live")
200    }
201
202    /// The retained local state, read-only.
203    #[cfg(all(test, not(feature = "hotpatch")))]
204    fn state(&self) -> &C::State {
205        &self.state
206    }
207
208    /// Overwrite the stored witness, standing in for a widget another image
209    /// built.
210    #[cfg(all(test, feature = "hotpatch"))]
211    fn set_witness_for_test(&mut self, witness: SeamWitness) {
212        self.witness = witness;
213    }
214}
215
216impl<C: Component> Drop for ComponentWidget<C> {
217    fn drop(&mut self) {
218        // A mismatched witness means this drop glue belongs to an image whose
219        // `C::State` layout differs from the creator's: leak the state box
220        // without dropping or freeing it. `take` reads and clears only the
221        // pointer, whose layout does not depend on `C::State`.
222        #[cfg(feature = "hotpatch")]
223        if crate::hotpatch::verify_witness::<C>(self.witness).is_err() {
224            std::mem::forget(self.state.take());
225        }
226        // Defensive: a component removed by a path that did not route through
227        // `View::teardown` (or a panic mid-teardown) still disposes its owner.
228        self.dispose();
229    }
230}
231
232impl<Outer: 'static, C: Component> View<Outer> for ComponentView<C> {
233    type Element = ComponentWidget<C>;
234
235    fn build(&self, _ctx: &mut BuildCtx<'_>) -> Self::Element {
236        // A child of the ambient owner: the root owner a shell installs, or the
237        // enclosing component's owner (we build under `owner.with`, below).
238        let owner = Owner::new();
239        // This image runs `init`, so its layout is the creator's.
240        #[cfg(feature = "hotpatch")]
241        let witness = SeamWitness::of::<C>();
242        let (state, prev, child, next_id) = owner.with(|| {
243            #[cfg(feature = "hotpatch")]
244            let mut state = Box::new(self.component.init());
245            #[cfg(not(feature = "hotpatch"))]
246            let mut state = self.component.init();
247            // Erase at the boundary; idempotent, so an `AnyView` body is not
248            // re-boxed. The seam erases inside the hot function; a patch that
249            // already changed `C`'s layout yields an inert child instead.
250            #[cfg(feature = "hotpatch")]
251            let view = crate::hotpatch::built_or_inert(crate::hotpatch::build_erased(
252                &self.component,
253                &mut *state,
254                witness,
255            ));
256            #[cfg(not(feature = "hotpatch"))]
257            let view = AnyView::new(self.component.build(&mut state));
258            // A component-local build counter; the child never enters the arena.
259            let mut next_id = 0u64;
260            let mut inner = BuildCtx::new(&mut next_id);
261            // A freshly built child pod holds no focus link (`ChildPod::new`
262            // starts unfocused), so the chain across this boundary is closed —
263            // and a build tears nothing down, so nothing under it can orphan a
264            // session (see `BuildCtx::has_focus`).
265            inner.set_has_focus(false);
266            let element: Box<dyn Widget> = view.build(&mut inner);
267            // Double-box so a later rebuild can recover `&mut Box<dyn Widget>`.
268            (state, view, ChildPod::new(Box::new(element)), next_id)
269        });
270        ComponentWidget {
271            #[cfg(feature = "hotpatch")]
272            witness,
273            #[cfg(feature = "hotpatch")]
274            state: Some(state),
275            #[cfg(not(feature = "hotpatch"))]
276            state,
277            prev,
278            child,
279            owner,
280            next_id,
281            disposed: false,
282        }
283    }
284
285    fn rebuild(
286        &self,
287        _prev: &Self,
288        element: &mut Self::Element,
289        ctx: &mut BuildCtx<'_>,
290    ) -> ChangeFlags {
291        // Before anything forms `&mut C::State` or reads `prev`/`child`: a
292        // widget another image built with a different layout is left exactly as
293        // it is (the stale subtree stays until the session restarts).
294        #[cfg(feature = "hotpatch")]
295        if crate::hotpatch::verify_witness::<C>(element.witness).is_err() {
296            return ChangeFlags::NONE;
297        }
298        // The outer `_prev` ComponentView is intentionally unused: the component
299        // always re-runs `build` because its retained local state may have
300        // changed even when the component value is equal (tracked-scope skipping
301        // is not implemented).
302        //
303        // The outer `ctx`, by contrast, is read for exactly one thing: the
304        // effective focus chain, which must cross this state boundary or every
305        // reconciler under a component would start a fresh (assumed-live) chain.
306        // Read here because the inner context is built inside the `owner.with`
307        // closure below, which cannot also borrow `ctx`.
308        let outer_has_focus = ctx.has_focus();
309        let owner = element.owner.clone();
310        owner.with(|| {
311            // The seam passes the stored witness, so a patched hot function
312            // compares the creator's layout with its own.
313            #[cfg(feature = "hotpatch")]
314            let new_view = {
315                let witness = element.witness;
316                match crate::hotpatch::build_erased(&self.component, element.state_mut(), witness) {
317                    Ok(view) => view,
318                    // Reported by the seam; nothing below has been touched.
319                    Err(_) => return ChangeFlags::NONE,
320                }
321            };
322            #[cfg(not(feature = "hotpatch"))]
323            let new_view = AnyView::new(self.component.build(element.state_mut()));
324            // Read before the `widget_mut` borrow below.
325            let child_focused = element.child.is_focused();
326            let (flags, swapped) = {
327                // The non-marking accessor: the pod is marked below from the
328                // rebuild's own flags (see `ChildPod::widget_mut_for_rebuild`).
329                let boxed = element
330                    .child
331                    .widget_mut_for_rebuild()
332                    .downcast_mut::<Box<dyn Widget>>()
333                    .expect("component child element is a boxed AnyView widget");
334                let before = {
335                    let any: &dyn Any = &**boxed;
336                    any.type_id()
337                };
338                let mut inner = BuildCtx::new(&mut element.next_id);
339                // Extend the chain by this component's own child link — the
340                // `ChildPod::paint_child` composition, spelled by hand because
341                // the component descends into its pod through a fresh inner
342                // context rather than through the pod plumbing.
343                inner.set_has_focus(outer_has_focus && child_focused);
344                // Run the child rebuild as an element-replaced tracking scope: a
345                // body that is an enum view (`Either`, `EitherOfN`) keeps its
346                // concrete type across an arm swap, so the type-id comparison
347                // below cannot see it, and the enum reports it instead
348                // (`BuildCtx::mark_element_replaced`). A pod inside the body
349                // consumes its own signal, so only a swap of *this* child's
350                // widget reaches here.
351                let (flags, replaced) = inner
352                    .track_element_replaced(|inner| new_view.rebuild(&element.prev, boxed, inner));
353                let after = {
354                    let any: &dyn Any = &**boxed;
355                    any.type_id()
356                };
357                (flags, before != after || replaced)
358            };
359            if swapped {
360                // The child AnyView swapped concrete type (or its enum body
361                // switched arms): the old widget was torn down inside the
362                // child's `rebuild`, so any capture/focus path it recorded is
363                // stale — drop it (mirrors `rebuild_child`).
364                element.child.set_active(false);
365                element.child.set_focused(false);
366                // Dropping the flag is not the load-bearing part: the focused
367                // widget just stopped existing, so a LIVE session died with it
368                // and the root must release it before the frame ends. Gated on
369                // the whole chain — `outer_has_focus && child_focused` is the
370                // same composition `teardown` spells below and the same one
371                // `frust-widgets`' `mark_orphan_if_live` applies at its own
372                // marking sites (this crate sits below that one, so the gate is
373                // spelled by hand rather than shared).
374                if outer_has_focus && child_focused {
375                    crate::event::mark_focus_orphaned();
376                }
377            }
378            // The layout-skip dirty source for the component's own child pod: it
379            // is rebuilt here rather than through a reconciler, so the mark the
380            // reconcilers apply is spelled by hand. The component's *own* pod is
381            // marked by whoever rebuilt it, from the same `flags` returned below.
382            // A LAYOUT rebuild dirties layout (which implies paint); a PAINT-only
383            // rebuild dirties just paint, so a retained layer is not reused stale.
384            if swapped || flags.contains(ChangeFlags::LAYOUT) {
385                element.child.mark_layout_dirty();
386            } else if flags.contains(ChangeFlags::PAINT) {
387                element.child.mark_paint_dirty();
388            }
389            element.prev = new_view;
390            flags
391        })
392    }
393
394    fn teardown(&self, element: &mut Self::Element, ctx: &mut BuildCtx<'_>) {
395        // A widget another image built with a different layout is not torn
396        // down: neither the child teardown through `prev` nor the dispose runs
397        // here (`Drop` disposes the owner and leaks the state).
398        #[cfg(feature = "hotpatch")]
399        if crate::hotpatch::verify_witness::<C>(element.witness).is_err() {
400            return;
401        }
402        // Forward teardown to the child view/element under the component owner,
403        // then dispose the owner (runs `on_cleanup`s) and let the state drop.
404        //
405        // The outer focus chain crosses the boundary here exactly as it does in
406        // `rebuild`: a component torn down *inside a live focus chain* must let
407        // the reconcilers below it mark the orphan, and one torn down under a
408        // cleared link must not (see `BuildCtx::has_focus`).
409        let inner_has_focus = ctx.has_focus() && element.child.is_focused();
410        element.owner.clone().with(|| {
411            let mut next_id = element.next_id;
412            let mut inner = BuildCtx::new(&mut next_id);
413            inner.set_has_focus(inner_has_focus);
414            if let Some(boxed) = element.child.widget_mut().downcast_mut::<Box<dyn Widget>>() {
415                element.prev.teardown(boxed, &mut inner);
416            }
417        });
418        element.dispose();
419    }
420}
421
422impl<C: Component> Widget for ComponentWidget<C> {
423    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
424        // Pass-through: the component contributes no chrome of its own. The
425        // child sits at the component's origin (ZERO in the component's local
426        // space) and receives the incoming constraints unchanged.
427        self.child.set_origin(Point::ZERO);
428        self.child.layout_child(ctx, bc)
429    }
430
431    fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
432        // Pass-through to the pod at origin ZERO: `paint_child` offsets by the
433        // incoming `ctx.origin()`, bubbles `needs_frame`, republishes a focused
434        // editable's IME surface, and composes `has_focus` — exactly like a
435        // single-child container.
436        self.child.paint_child(ctx, scene);
437    }
438
439    fn semantics(&self, ctx: &mut crate::semantics::SemanticsCtx) {
440        // A component contributes no node of its own (the state boundary is
441        // invisible to accessibility); forward to the child pod, exactly like a
442        // transparent single-child container.
443        self.child.semantics_child(ctx);
444    }
445
446    fn visit_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
447        // The component boundary is invisible to tooling for the same reason it
448        // is invisible to accessibility: it contributes no element of its own,
449        // only the child it wraps.
450        visitor(&self.child);
451    }
452
453    /// A component is a sliver exactly when its child is: the boundary is as
454    /// transparent to the sliver protocol as it is to box layout. The sliver
455    /// face is the component itself rather than the child widget, so the
456    /// layout runs through the child pod and leaves it sized and recorded for
457    /// this component's own paint, hit test and semantics.
458    fn as_sliver(&mut self) -> Option<&mut dyn SliverWidget> {
459        if self.child.is_sliver() {
460            Some(self)
461        } else {
462            None
463        }
464    }
465
466    fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
467        // The state boundary: build an inner context over the component's *own*
468        // local state, seeded with the outer focus flag, dispatch through the
469        // pod with the single-child routing contract, then mirror the inner
470        // results back onto the outer context.
471        //
472        // A synthesized `Cancel` (from an outer structural rebuild) arrives over
473        // throwaway `()` outer state, but this handler never touches outer state
474        // — it forwards to the subtree over the component's real state, safe
475        // under the Cancel-never-reads-state contract (which binds the subtree's
476        // handlers, not this carrier).
477        let size = ctx.size();
478        let has_focus = ctx.has_focus();
479        // The hover link, its live epoch, and this pass's claim eligibility all
480        // cross the state boundary unchanged — a component is transparent to hover
481        // exactly as it is to focus, so a claim made below it still reaches the root
482        // and stamps the pod chain on the way (mirrored back through `claim_hover`).
483        let hovered = ctx.is_hovered();
484        let hover_epoch = ctx.hover_epoch();
485        let hover_eligible = ctx.is_hover_eligible();
486        // A cursor request needs no threading here at all, unlike every other
487        // channel this carrier mirrors: it rides one pass-scoped slot rather than a
488        // per-context field (see `crate::event`'s `CURSOR_REQUEST`), so a
489        // `set_cursor` below this boundary already reaches the root, and the
490        // component boundary is transparent to it by construction.
491        let (result, needs_redraw, layout, captured, hover_claimed, focus_req, focus_rel, ime) = {
492            // Spelled per field (not through `state_mut`) so the borrow stays
493            // disjoint from `self.child` below.
494            #[cfg(feature = "hotpatch")]
495            let state_any: &mut dyn Any = self
496                .state
497                .as_deref_mut()
498                .expect("component state is present while the widget is live");
499            #[cfg(not(feature = "hotpatch"))]
500            let state_any: &mut dyn Any = &mut self.state;
501            let mut inner = EventCtx::new(state_any, Point::ZERO, size);
502            inner.set_has_focus(has_focus);
503            inner.set_hovered(hovered);
504            inner.set_hover_epoch(hover_epoch);
505            inner.set_hover_eligible(hover_eligible);
506            let result = route_child(&mut self.child, &mut inner, event);
507            (
508                result,
509                inner.needs_redraw(),
510                (inner.needs_layout(), inner.is_layout_path_dirty()),
511                inner.is_pointer_captured(),
512                inner.is_hover_claimed(),
513                inner.is_focus_requested(),
514                inner.is_focus_released(),
515                inner.take_ime_state(),
516            )
517        };
518        if needs_redraw {
519            ctx.request_redraw();
520        }
521        // The layout request and the layout-dirty path bit cross the state
522        // boundary like the redraw: the inner pod marked itself, and the pods
523        // above this component must mark themselves too.
524        ctx.absorb_layout(layout.0, layout.1);
525        if captured {
526            ctx.capture_pointer();
527        }
528        if hover_claimed {
529            ctx.claim_hover();
530        }
531        if focus_req {
532            ctx.request_focus();
533        }
534        if focus_rel {
535            ctx.release_focus();
536        }
537        if let Some(ime) = ime {
538            ctx.publish_ime_state(ime);
539        }
540        result
541    }
542}
543
544impl<C: Component> SliverWidget for ComponentWidget<C> {
545    /// Pass-through, like [`Widget::layout`]: the child sits at the component's
546    /// origin and receives `sc` unchanged, under the child pod's layout skip.
547    fn layout_sliver(&mut self, ctx: &mut LayoutCtx, sc: &SliverConstraints) -> SliverGeometry {
548        self.child.set_origin(Point::ZERO);
549        // A viewport reaches this only through `ChildPod::layout_sliver_child`,
550        // which asks `as_sliver` first, so `None` here means a caller invoked
551        // `layout_sliver` on a wrapper whose `as_sliver` answered `None`: a
552        // contract breach that fails in debug builds; release builds fall back
553        // to an empty sliver.
554        self.child.layout_sliver_child(ctx, sc).unwrap_or_else(|| {
555            debug_assert!(
556                false,
557                "ComponentWidget::layout_sliver called on a wrapper whose as_sliver answered None"
558            );
559            SliverGeometry::ZERO
560        })
561    }
562}
563
564/// Route an event to the component's single child pod with the recorded-path
565/// semantics single-child containers use (the in-crate mirror of
566/// `frust-widgets`' `route_event_single`).
567///
568/// A broadcast ([`InputEvent::is_broadcast`]) reaches the child unconditionally,
569/// checked **first** so no capture/focus/hit-test branch below can swallow it —
570/// a component sitting between the root and a navigator is the ordinary shape,
571/// so this arm is what lets a deferred pop-result flush reach that navigator at
572/// all. A focus-routed event goes to the child only if it holds the recorded
573/// focus path; a captured (active) child receives pointer events unconditionally
574/// (capture auto-releasing on `Up`/`Cancel`), otherwise the child receives the
575/// event only if it contains the point, and a `Down` that misses a focused
576/// child blurs it.
577fn route_child(pod: &mut ChildPod, ctx: &mut EventCtx<'_>, event: &InputEvent) -> EventResult {
578    if event.is_broadcast() {
579        // Never consumed: forward, discard the result (see the variant's
580        // routing contract).
581        pod.event_child(ctx, event);
582        return EventResult::Ignored;
583    }
584    if event.is_focus_routed() {
585        if pod.is_focused() {
586            return pod.event_child(ctx, event);
587        }
588        return EventResult::Ignored;
589    }
590    if pod.is_active() {
591        let result = pod.event_child(ctx, event);
592        if releases_capture(event) {
593            pod.set_active(false);
594        }
595        return result;
596    }
597    let inside = pod.contains(event.position());
598    let result = if inside {
599        pod.event_child(ctx, event)
600    } else {
601        EventResult::Ignored
602    };
603    if is_pointer_down(event) && !inside && pod.is_focused() {
604        pod.set_focused(false);
605    }
606    result
607}
608
609/// Whether `event` is the phase that auto-releases a recorded capture
610/// (`Up`/`Cancel`).
611fn releases_capture(event: &InputEvent) -> bool {
612    matches!(
613        event,
614        InputEvent::Pointer(p) if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
615    )
616}
617
618/// Whether `event` is a pointer `Down` (opens capture / triggers blur eval).
619fn is_pointer_down(event: &InputEvent) -> bool {
620    matches!(
621        event,
622        InputEvent::Pointer(p) if matches!(p.phase, PointerPhase::Down)
623    )
624}
625
626#[cfg(test)]
627mod tests {
628    use super::*;
629    use crate::event::{
630        CursorIcon, EditingState, ImeState, Key, KeyEvent, Modifiers, NamedKey, PointerButton,
631        PointerEvent, clear_cursor_request, take_cursor_request,
632    };
633    use crate::view::any;
634    use kurbo::Rect;
635    use reactive_graph::owner::{on_cleanup, provide_context, use_context};
636    use std::cell::Cell;
637    use std::rc::Rc;
638    use std::sync::atomic::{AtomicU32, Ordering};
639    use std::sync::{Arc, Mutex};
640
641    // --- Event constructors -------------------------------------------------
642
643    fn pointer(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
644        InputEvent::Pointer(PointerEvent {
645            phase,
646            position: Point::new(x, y),
647            button: PointerButton::Primary,
648        })
649    }
650
651    fn key_enter() -> InputEvent {
652        InputEvent::Key(KeyEvent {
653            key: Key::Named(NamedKey::Enter),
654            modifiers: Modifiers::default(),
655            repeat: false,
656        })
657    }
658
659    // --- A trivial leaf view/widget used to fill component subtrees ---------
660
661    struct Empty;
662    struct EmptyWidget;
663    impl Widget for EmptyWidget {
664        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
665            bc.constrain(Size::new(10.0, 10.0))
666        }
667        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
668    }
669    impl View<()> for Empty {
670        type Element = EmptyWidget;
671        fn build(&self, _ctx: &mut BuildCtx<'_>) -> EmptyWidget {
672            EmptyWidget
673        }
674        fn rebuild(
675            &self,
676            _prev: &Self,
677            _element: &mut EmptyWidget,
678            _ctx: &mut BuildCtx<'_>,
679        ) -> ChangeFlags {
680            ChangeFlags::NONE
681        }
682    }
683
684    /// A second, distinct leaf type — an `AnyView` swap partner for `Empty`.
685    struct OtherLeaf;
686    struct OtherWidget;
687    impl Widget for OtherWidget {
688        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
689            bc.constrain(Size::new(10.0, 10.0))
690        }
691        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
692    }
693    impl View<()> for OtherLeaf {
694        type Element = OtherWidget;
695        fn build(&self, _ctx: &mut BuildCtx<'_>) -> OtherWidget {
696            OtherWidget
697        }
698        fn rebuild(
699            &self,
700            _prev: &Self,
701            _element: &mut OtherWidget,
702            _ctx: &mut BuildCtx<'_>,
703        ) -> ChangeFlags {
704            ChangeFlags::NONE
705        }
706    }
707
708    // --- Criterion 1: local-state retention (a click counter) ---------------
709
710    struct CounterState {
711        count: u32,
712    }
713
714    /// A component whose button increments its own local `count`. Carries a
715    /// `label` prop so a parent-driven rebuild can change props without
716    /// disturbing the retained state.
717    struct ClickCounter {
718        label: &'static str,
719    }
720
721    impl Component for ClickCounter {
722        type State = CounterState;
723        fn init(&self) -> CounterState {
724            CounterState { count: 0 }
725        }
726        fn build(&self, state: &mut CounterState) -> impl View<CounterState> {
727            any(CounterButtonView {
728                count: state.count,
729                label: self.label,
730            })
731        }
732    }
733
734    struct CounterButtonView {
735        count: u32,
736        label: &'static str,
737    }
738    struct CounterButtonWidget {
739        count: u32,
740        label: &'static str,
741    }
742    impl View<CounterState> for CounterButtonView {
743        type Element = CounterButtonWidget;
744        fn build(&self, _ctx: &mut BuildCtx<'_>) -> CounterButtonWidget {
745            CounterButtonWidget {
746                count: self.count,
747                label: self.label,
748            }
749        }
750        fn rebuild(
751            &self,
752            prev: &Self,
753            element: &mut CounterButtonWidget,
754            _ctx: &mut BuildCtx<'_>,
755        ) -> ChangeFlags {
756            let mut flags = ChangeFlags::NONE;
757            if prev.count != self.count || prev.label != self.label {
758                element.count = self.count;
759                element.label = self.label;
760                flags = ChangeFlags::PAINT;
761            }
762            flags
763        }
764    }
765    impl Widget for CounterButtonWidget {
766        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
767            bc.constrain(Size::new(40.0, 20.0))
768        }
769        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
770        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
771            if let InputEvent::Pointer(p) = event
772                && p.phase == PointerPhase::Down
773            {
774                ctx.state_mut::<CounterState>().count += 1;
775                ctx.request_redraw();
776                return EventResult::Handled;
777            }
778            EventResult::Ignored
779        }
780    }
781
782    /// Set (and keep alive) an ambient owner for a test, so components created
783    /// under it become children of a real owner (context/cleanup work).
784    fn ambient() -> Owner {
785        let owner = Owner::new();
786        owner.set();
787        owner
788    }
789
790    fn build_widget<Outer: 'static, C: Component>(view: &ComponentView<C>) -> ComponentWidget<C> {
791        let mut next_id = 0u64;
792        let mut ctx = BuildCtx::new(&mut next_id);
793        View::<Outer>::build(view, &mut ctx)
794    }
795
796    #[test]
797    fn local_state_survives_parent_driven_rebuild() {
798        let _owner = ambient();
799        let v1 = component(ClickCounter { label: "a" });
800        let mut widget = build_widget::<(), _>(&v1);
801        let mut lctx = LayoutCtx::new();
802        widget.layout(&mut lctx, &BoxConstraints::tight(Size::new(40.0, 20.0)));
803
804        // Three clicks land inside the button and increment the component's own
805        // local state through the inner EventCtx.
806        let mut outer = ();
807        for _ in 0..3 {
808            let mut ectx = EventCtx::new(&mut outer, Point::ZERO, Size::new(40.0, 20.0));
809            widget.event(&mut ectx, &pointer(PointerPhase::Down, 5.0, 5.0));
810        }
811        assert_eq!(widget.state().count, 3);
812
813        // A parent-driven rebuild with changed props must not reset the count.
814        let v2 = component(ClickCounter { label: "changed" });
815        let mut next_id = 0u64;
816        let mut ctx = BuildCtx::new(&mut next_id);
817        View::<()>::rebuild(&v2, &v1, &mut widget, &mut ctx);
818        assert_eq!(
819            widget.state().count,
820            3,
821            "local state retained across rebuild"
822        );
823    }
824
825    // --- Criterion 2: context scoping across the component boundary ---------
826
827    #[derive(Clone)]
828    struct Theme(i32);
829
830    /// Provides a `Theme` on its own owner, then hosts a child component that
831    /// reads it — the child (a descendant) must resolve the ancestor's value.
832    struct Provider {
833        sink: Arc<Mutex<Option<i32>>>,
834    }
835    impl Component for Provider {
836        type State = ();
837        fn init(&self) {}
838        fn build(&self, _state: &mut ()) -> impl View<()> {
839            provide_context(Theme(7));
840            any(component(Reader {
841                sink: self.sink.clone(),
842            }))
843        }
844    }
845
846    /// Reads `Theme` from context during build, recording what it resolved.
847    struct Reader {
848        sink: Arc<Mutex<Option<i32>>>,
849    }
850    impl Component for Reader {
851        type State = ();
852        fn init(&self) {}
853        fn build(&self, _state: &mut ()) -> impl View<()> {
854            *self.sink.lock().unwrap() = use_context::<Theme>().map(|t| t.0);
855            any(Empty)
856        }
857    }
858
859    /// Provides a `Theme` but does not descend into a reader — used to prove a
860    /// sibling's provided context is invisible to a cousin.
861    struct SecretProvider;
862    impl Component for SecretProvider {
863        type State = ();
864        fn init(&self) {}
865        fn build(&self, _state: &mut ()) -> impl View<()> {
866            provide_context(Theme(99));
867            any(Empty)
868        }
869    }
870
871    #[test]
872    fn use_context_resolves_ancestor_provided_value() {
873        let _owner = ambient();
874        let sink = Arc::new(Mutex::new(None));
875        let v = component(Provider { sink: sink.clone() });
876        let _widget = build_widget::<(), _>(&v);
877        assert_eq!(*sink.lock().unwrap(), Some(7));
878    }
879
880    #[test]
881    fn sibling_does_not_see_a_cousins_context() {
882        let parent = ambient();
883        let reader_sink = Arc::new(Mutex::new(Some(-1)));
884        // Two siblings under the same parent owner: one provides a secret, the
885        // other reads. The reader (a cousin, not a descendant) must see None.
886        parent.with(|| {
887            let secret = component(SecretProvider);
888            let _s = build_widget::<(), _>(&secret);
889            let reader = component(Reader {
890                sink: reader_sink.clone(),
891            });
892            let _r = build_widget::<(), _>(&reader);
893        });
894        assert_eq!(*reader_sink.lock().unwrap(), None);
895    }
896
897    // --- Criterion 3: on_cleanup / owner disposal ---------------------------
898
899    struct Disposable {
900        probe: Arc<AtomicU32>,
901    }
902    impl Component for Disposable {
903        type State = ();
904        fn init(&self) {
905            let probe = self.probe.clone();
906            // Registered once (init runs once) on this component's owner.
907            on_cleanup(move || {
908                probe.fetch_add(1, Ordering::SeqCst);
909            });
910        }
911        fn build(&self, _state: &mut ()) -> impl View<()> {
912            any(Empty)
913        }
914    }
915
916    #[test]
917    fn anyview_type_swap_disposes_component_owner_once() {
918        let _owner = ambient();
919        let probe = Arc::new(AtomicU32::new(0));
920        let mut next_id = 0u64;
921        let mut ctx = BuildCtx::new(&mut next_id);
922
923        let prev = any::<(), _>(component(Disposable {
924            probe: probe.clone(),
925        }));
926        let mut element: Box<dyn Widget> = prev.build(&mut ctx);
927        assert_eq!(probe.load(Ordering::SeqCst), 0);
928
929        // Swap the AnyView to a different concrete type: the component is torn
930        // down and its `on_cleanup` runs exactly once.
931        let next = any::<(), _>(OtherLeaf);
932        next.rebuild(&prev, &mut element, &mut ctx);
933        assert_eq!(probe.load(Ordering::SeqCst), 1);
934
935        // Dropping the replaced widget (already disposed) must not fire again.
936        drop(element);
937        assert_eq!(probe.load(Ordering::SeqCst), 1);
938    }
939
940    // --- Erasure at the boundary --------------------------------------------
941
942    /// A component whose `build` returns a concrete view (no `any(..)`): the
943    /// host erases it, so the pod's double-boxed element wraps the leaf widget.
944    struct ConcreteComp;
945    impl Component for ConcreteComp {
946        type State = ();
947        fn init(&self) {}
948        fn build(&self, _state: &mut ()) -> impl View<()> {
949            Empty
950        }
951    }
952
953    /// The same leaf, returned already erased: `AnyView::new` is idempotent, so
954    /// the host does not wrap the `AnyView` in a second box.
955    struct ErasedComp;
956    impl Component for ErasedComp {
957        type State = ();
958        fn init(&self) {}
959        fn build(&self, _state: &mut ()) -> impl View<()> {
960            any(Empty)
961        }
962    }
963
964    /// Whether the component's child element is the leaf widget itself (one
965    /// erasure), not a box around another boxed element (two).
966    fn child_is_empty_widget<C: Component>(widget: &mut ComponentWidget<C>) -> bool {
967        let boxed = widget
968            .child
969            .widget_mut()
970            .downcast_mut::<Box<dyn Widget>>()
971            .expect("double-boxed AnyView element");
972        let any: &dyn Any = &**boxed;
973        any.is::<EmptyWidget>()
974    }
975
976    #[test]
977    fn build_result_is_erased_exactly_once_at_the_boundary() {
978        let _owner = ambient();
979
980        let concrete = component(ConcreteComp);
981        let mut widget = build_widget::<(), _>(&concrete);
982        assert!(child_is_empty_widget(&mut widget), "concrete body, build");
983        let mut next_id = 0u64;
984        let mut ctx = BuildCtx::new(&mut next_id);
985        View::<()>::rebuild(&concrete, &concrete, &mut widget, &mut ctx);
986        assert!(child_is_empty_widget(&mut widget), "concrete body, rebuild");
987
988        let erased = component(ErasedComp);
989        let mut widget = build_widget::<(), _>(&erased);
990        assert!(child_is_empty_widget(&mut widget), "any(..) body, build");
991        let mut next_id = 0u64;
992        let mut ctx = BuildCtx::new(&mut next_id);
993        View::<()>::rebuild(&erased, &erased, &mut widget, &mut ctx);
994        assert!(child_is_empty_widget(&mut widget), "any(..) body, rebuild");
995    }
996
997    // --- The swap arm's orphan mark, across the state boundary --------------
998
999    /// A component whose child view *type* flips on a shared flag: `Empty`
1000    /// first, `OtherLeaf` after — the `ComponentView::rebuild` swap arm's
1001    /// fixture (`if editing { field } else { label }` inside a component's
1002    /// `build`).
1003    struct SwapComp {
1004        swapped: Rc<Cell<bool>>,
1005    }
1006    impl Component for SwapComp {
1007        type State = ();
1008        fn init(&self) {}
1009        fn build(&self, _state: &mut ()) -> impl View<()> {
1010            if self.swapped.get() {
1011                any(OtherLeaf)
1012            } else {
1013                any(Empty)
1014            }
1015        }
1016    }
1017
1018    #[test]
1019    fn component_swap_arm_marks_the_orphan_only_on_a_live_chain() {
1020        /// Swap the component's child type with the *outer* chain either live
1021        /// or already broken, reporting `(pod still focused, orphan marked)`.
1022        fn swap_child(outer_live: bool) -> (bool, bool) {
1023            let _ = crate::event::take_focus_orphaned();
1024            let _owner = ambient();
1025            let swapped = Rc::new(Cell::new(false));
1026            let v1 = component(SwapComp {
1027                swapped: swapped.clone(),
1028            });
1029            let mut widget = build_widget::<(), _>(&v1);
1030            // The component's own child pod holds the recorded focus path.
1031            widget.child.set_focused(true);
1032
1033            swapped.set(true);
1034            let v2 = component(SwapComp {
1035                swapped: swapped.clone(),
1036            });
1037            let mut next_id = 0u64;
1038            let mut ctx = BuildCtx::new(&mut next_id);
1039            // The chain the surrounding tree hands this component.
1040            ctx.set_has_focus(outer_live);
1041            View::<()>::rebuild(&v2, &v1, &mut widget, &mut ctx);
1042            (
1043                widget.child.is_focused(),
1044                crate::event::take_focus_orphaned(),
1045            )
1046        }
1047
1048        let (still_focused, marked) = swap_child(true);
1049        assert!(
1050            !still_focused,
1051            "a swapped-in widget must not inherit the old focus link"
1052        );
1053        assert!(
1054            marked,
1055            "a live focus path dying inside the component's swap releases the session"
1056        );
1057
1058        let (still_focused, marked) = swap_child(false);
1059        assert!(!still_focused, "the flag is dropped either way");
1060        assert!(
1061            !marked,
1062            "the same swap under an already-blurred ancestor owns no session to release"
1063        );
1064    }
1065
1066    /// A two-arm enum view in the shape of `frust-widgets`' `Either` (which this
1067    /// crate cannot name): one element type for both arms, the old arm torn down
1068    /// and the new one built on a swap, and the swap reported through
1069    /// `BuildCtx::mark_element_replaced`.
1070    enum Arms {
1071        Left(Empty),
1072        Right(OtherLeaf),
1073    }
1074    enum ArmsWidget {
1075        Left(EmptyWidget),
1076        Right(OtherWidget),
1077    }
1078    impl Widget for ArmsWidget {
1079        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1080            match self {
1081                Self::Left(w) => w.layout(ctx, bc),
1082                Self::Right(w) => w.layout(ctx, bc),
1083            }
1084        }
1085        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1086            match self {
1087                Self::Left(w) => w.paint(ctx, scene),
1088                Self::Right(w) => w.paint(ctx, scene),
1089            }
1090        }
1091    }
1092    impl View<()> for Arms {
1093        type Element = ArmsWidget;
1094        fn build(&self, ctx: &mut BuildCtx<'_>) -> ArmsWidget {
1095            match self {
1096                Self::Left(v) => ArmsWidget::Left(v.build(ctx)),
1097                Self::Right(v) => ArmsWidget::Right(v.build(ctx)),
1098            }
1099        }
1100        fn rebuild(
1101            &self,
1102            prev: &Self,
1103            element: &mut ArmsWidget,
1104            ctx: &mut BuildCtx<'_>,
1105        ) -> ChangeFlags {
1106            match (self, prev, &mut *element) {
1107                (Self::Left(v), Self::Left(p), ArmsWidget::Left(w)) => v.rebuild(p, w, ctx),
1108                (Self::Right(v), Self::Right(p), ArmsWidget::Right(w)) => v.rebuild(p, w, ctx),
1109                _ => {
1110                    ctx.mark_element_replaced();
1111                    *element = self.build(ctx);
1112                    ChangeFlags::LAYOUT | ChangeFlags::PAINT
1113                }
1114            }
1115        }
1116    }
1117
1118    /// A component whose body is the enum view above, not erased by the
1119    /// component itself: the child's boxed widget is `ArmsWidget` on both arms.
1120    struct ArmsComp {
1121        right: Rc<Cell<bool>>,
1122    }
1123    impl Component for ArmsComp {
1124        type State = ();
1125        fn init(&self) {}
1126        fn build(&self, _state: &mut ()) -> impl View<()> {
1127            if self.right.get() {
1128                Arms::Right(OtherLeaf)
1129            } else {
1130                Arms::Left(Empty)
1131            }
1132        }
1133    }
1134
1135    #[test]
1136    fn component_enum_body_arm_swap_clears_the_child_pod_flags() {
1137        /// Rebuild the component with its body on the left arm then `right_now`,
1138        /// the child pod holding a capture and the focus path, reporting
1139        /// `(still active, still focused, orphan marked)`.
1140        fn rebuild_arms(right_now: bool) -> (bool, bool, bool) {
1141            let _ = crate::event::take_focus_orphaned();
1142            let _owner = ambient();
1143            let right = Rc::new(Cell::new(false));
1144            let v1 = component(ArmsComp {
1145                right: right.clone(),
1146            });
1147            let mut widget = build_widget::<(), _>(&v1);
1148            widget.child.set_active(true);
1149            widget.child.set_focused(true);
1150
1151            right.set(right_now);
1152            let v2 = component(ArmsComp {
1153                right: right.clone(),
1154            });
1155            let mut next_id = 0u64;
1156            let mut ctx = BuildCtx::new(&mut next_id);
1157            View::<()>::rebuild(&v2, &v1, &mut widget, &mut ctx);
1158            (
1159                widget.child.is_active(),
1160                widget.child.is_focused(),
1161                crate::event::take_focus_orphaned(),
1162            )
1163        }
1164
1165        let (active, focused, marked) = rebuild_arms(true);
1166        assert!(
1167            !active,
1168            "the fresh arm never saw the Down: the capture is dropped"
1169        );
1170        assert!(
1171            !focused,
1172            "the fresh arm never claimed focus: the link is dropped"
1173        );
1174        assert!(
1175            marked,
1176            "a live focus path dying inside the arm swap releases the session"
1177        );
1178
1179        let (active, focused, marked) = rebuild_arms(false);
1180        assert!(active, "a same-arm rebuild keeps the in-flight capture");
1181        assert!(focused, "a same-arm rebuild keeps the focus path");
1182        assert!(!marked, "a same-arm rebuild orphans nothing");
1183    }
1184
1185    #[test]
1186    fn component_content_only_rebuild_keeps_focus_and_marks_nothing() {
1187        // The negative guard: the component always re-runs `build`, so a
1188        // same-type child rebuild happens every frame a component is on screen
1189        // — marking there would drop the keyboard continuously.
1190        let _ = crate::event::take_focus_orphaned();
1191        let _owner = ambient();
1192        let v1 = component(Labeled { text: "a" });
1193        let mut widget = build_widget::<(), _>(&v1);
1194        widget.child.set_focused(true);
1195
1196        let v2 = component(Labeled { text: "b" });
1197        let mut next_id = 0u64;
1198        let mut ctx = BuildCtx::new(&mut next_id);
1199        View::<()>::rebuild(&v2, &v1, &mut widget, &mut ctx);
1200
1201        assert!(
1202            widget.child.is_focused(),
1203            "a content-only rebuild keeps the recorded focus path"
1204        );
1205        assert!(
1206            !crate::event::take_focus_orphaned(),
1207            "an in-place child rebuild must not orphan the focus session"
1208        );
1209    }
1210
1211    #[test]
1212    fn explicit_teardown_disposes_owner_once_and_drop_is_idempotent() {
1213        let _owner = ambient();
1214        let probe = Arc::new(AtomicU32::new(0));
1215        let view = component(Disposable {
1216            probe: probe.clone(),
1217        });
1218        let mut widget = build_widget::<(), _>(&view);
1219
1220        let mut next_id = 0u64;
1221        let mut ctx = BuildCtx::new(&mut next_id);
1222        View::<()>::teardown(&view, &mut widget, &mut ctx);
1223        assert_eq!(probe.load(Ordering::SeqCst), 1);
1224
1225        // The defensive `Drop` must not run the cleanup a second time.
1226        drop(widget);
1227        assert_eq!(probe.load(Ordering::SeqCst), 1);
1228    }
1229
1230    // --- Criterion 4: event routing across the boundary ---------------------
1231
1232    /// A widget that captures the pointer on `Down` (tracking `pressed`), and
1233    /// clears both on `Up`/`Cancel` — the latter without touching state.
1234    struct CaptureWidget {
1235        pressed: bool,
1236    }
1237    impl Widget for CaptureWidget {
1238        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1239            bc.constrain(Size::new(40.0, 40.0))
1240        }
1241        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
1242        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1243            if let InputEvent::Pointer(p) = event {
1244                match p.phase {
1245                    PointerPhase::Down => {
1246                        self.pressed = true;
1247                        ctx.capture_pointer();
1248                        ctx.request_redraw();
1249                        return EventResult::Handled;
1250                    }
1251                    PointerPhase::Move => return EventResult::Handled,
1252                    // Cancel arm never reads application state.
1253                    PointerPhase::Up | PointerPhase::Cancel => {
1254                        self.pressed = false;
1255                        return EventResult::Handled;
1256                    }
1257                }
1258            }
1259            EventResult::Ignored
1260        }
1261    }
1262    struct CaptureView;
1263    impl View<()> for CaptureView {
1264        type Element = CaptureWidget;
1265        fn build(&self, _ctx: &mut BuildCtx<'_>) -> CaptureWidget {
1266            CaptureWidget { pressed: false }
1267        }
1268        fn rebuild(
1269            &self,
1270            _prev: &Self,
1271            _element: &mut CaptureWidget,
1272            _ctx: &mut BuildCtx<'_>,
1273        ) -> ChangeFlags {
1274            ChangeFlags::NONE
1275        }
1276    }
1277    struct CaptureComp;
1278    impl Component for CaptureComp {
1279        type State = ();
1280        fn init(&self) {}
1281        fn build(&self, _state: &mut ()) -> impl View<()> {
1282            any(CaptureView)
1283        }
1284    }
1285
1286    /// Recover the concrete inner widget from a component widget's pod (the
1287    /// child element is double-boxed).
1288    fn inner_widget<C: Component, W: Widget>(w: &mut ComponentWidget<C>) -> &mut W {
1289        w.child
1290            .widget_mut()
1291            .downcast_mut::<Box<dyn Widget>>()
1292            .expect("double-boxed AnyView element")
1293            .downcast_mut::<W>()
1294            .expect("inner concrete widget")
1295    }
1296
1297    #[test]
1298    fn capture_survives_drag_outside_bounds_and_mirrors_outward() {
1299        let _owner = ambient();
1300        let view = component(CaptureComp);
1301        let mut widget = build_widget::<(), _>(&view);
1302        let mut lctx = LayoutCtx::new();
1303        widget.layout(&mut lctx, &BoxConstraints::tight(Size::new(40.0, 40.0)));
1304
1305        let mut outer = ();
1306        // Down inside: the inner widget captures and the flag mirrors outward.
1307        let captured = {
1308            let mut ectx = EventCtx::new(&mut outer, Point::ZERO, Size::new(40.0, 40.0));
1309            widget.event(&mut ectx, &pointer(PointerPhase::Down, 5.0, 5.0));
1310            ectx.is_pointer_captured()
1311        };
1312        assert!(captured, "inner capture bubbles to the outer context");
1313        assert!(
1314            widget.child.is_active(),
1315            "component pod records the capture"
1316        );
1317
1318        // A Move far outside the child's bounds still reaches it (captured path).
1319        {
1320            let mut ectx = EventCtx::new(&mut outer, Point::ZERO, Size::new(40.0, 40.0));
1321            let r = widget.event(&mut ectx, &pointer(PointerPhase::Move, 500.0, 500.0));
1322            assert_eq!(r, EventResult::Handled, "drag outside bounds still routed");
1323        }
1324        assert!(widget.child.is_active(), "capture survives the drag");
1325
1326        // Up releases the capture at the boundary.
1327        {
1328            let mut ectx = EventCtx::new(&mut outer, Point::ZERO, Size::new(40.0, 40.0));
1329            widget.event(&mut ectx, &pointer(PointerPhase::Up, 500.0, 500.0));
1330        }
1331        assert!(!widget.child.is_active(), "Up auto-releases the capture");
1332        assert!(!inner_widget::<_, CaptureWidget>(&mut widget).pressed);
1333    }
1334
1335    #[test]
1336    fn synthesized_cancel_over_unit_state_is_safe_and_clears_pressed() {
1337        let _owner = ambient();
1338        let view = component(CaptureComp);
1339        let mut widget = build_widget::<(), _>(&view);
1340        let mut lctx = LayoutCtx::new();
1341        widget.layout(&mut lctx, &BoxConstraints::tight(Size::new(40.0, 40.0)));
1342
1343        // Arm the capture first (real state path).
1344        let mut outer = ();
1345        {
1346            let mut ectx = EventCtx::new(&mut outer, Point::ZERO, Size::new(40.0, 40.0));
1347            widget.event(&mut ectx, &pointer(PointerPhase::Down, 5.0, 5.0));
1348        }
1349        assert!(inner_widget::<_, CaptureWidget>(&mut widget).pressed);
1350
1351        // A structural-rebuild `Cancel` arrives over throwaway `()` state — as
1352        // `cancel_pod` synthesizes it — and must neither panic nor touch state.
1353        let mut dummy = ();
1354        {
1355            let mut ectx = EventCtx::new(&mut dummy, Point::ZERO, Size::new(40.0, 40.0));
1356            widget.event(&mut ectx, &pointer(PointerPhase::Cancel, 0.0, 0.0));
1357        }
1358        assert!(
1359            !inner_widget::<_, CaptureWidget>(&mut widget).pressed,
1360            "Cancel cleared pressed across the boundary"
1361        );
1362        assert!(!widget.child.is_active(), "Cancel released the capture");
1363    }
1364
1365    // A leaf that asks for a cursor on every `Move` — the canonical request shape.
1366    struct CursorLeafWidget;
1367    impl Widget for CursorLeafWidget {
1368        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1369            bc.constrain(Size::new(40.0, 40.0))
1370        }
1371        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
1372        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1373            if let InputEvent::Pointer(p) = event
1374                && p.phase == PointerPhase::Move
1375            {
1376                ctx.set_cursor(CursorIcon::Text);
1377            }
1378            EventResult::Ignored
1379        }
1380    }
1381    struct CursorLeafView;
1382    impl View<()> for CursorLeafView {
1383        type Element = CursorLeafWidget;
1384        fn build(&self, _ctx: &mut BuildCtx<'_>) -> CursorLeafWidget {
1385            CursorLeafWidget
1386        }
1387        fn rebuild(
1388            &self,
1389            _prev: &Self,
1390            _element: &mut CursorLeafWidget,
1391            _ctx: &mut BuildCtx<'_>,
1392        ) -> ChangeFlags {
1393            ChangeFlags::NONE
1394        }
1395    }
1396    struct CursorComp;
1397    impl Component for CursorComp {
1398        type State = ();
1399        fn init(&self) {}
1400        fn build(&self, _state: &mut ()) -> impl View<()> {
1401            any(CursorLeafView)
1402        }
1403    }
1404
1405    /// The component boundary is transparent to a cursor request with no mirroring
1406    /// code at all — the request rides one pass-scoped slot rather than a
1407    /// per-context field, so a refactor that turned it into a field (and forgot
1408    /// this carrier, as every other channel here has to be threaded) would drop it.
1409    #[test]
1410    fn a_cursor_request_crosses_the_component_boundary() {
1411        let _owner = ambient();
1412        let view = component(CursorComp);
1413        let mut widget = build_widget::<(), _>(&view);
1414        let mut lctx = LayoutCtx::new();
1415        widget.layout(&mut lctx, &BoxConstraints::tight(Size::new(40.0, 40.0)));
1416
1417        // `RenderRoot::event` clears the slot per pass; this test drives the widget
1418        // directly, so it does the same before asserting on what the pass left.
1419        clear_cursor_request();
1420        let mut outer = ();
1421        {
1422            let mut ectx = EventCtx::new(&mut outer, Point::ZERO, Size::new(40.0, 40.0));
1423            widget.event(&mut ectx, &pointer(PointerPhase::Move, 5.0, 5.0));
1424        }
1425        assert_eq!(
1426            take_cursor_request(),
1427            Some(CursorIcon::Text),
1428            "a request from below the component boundary reaches the root"
1429        );
1430    }
1431
1432    // A focus + IME publishing leaf: focuses on a left-half Down and publishes
1433    // an IME surface; a right-half Down neither focuses nor publishes.
1434    struct ImeLeafWidget;
1435    impl Widget for ImeLeafWidget {
1436        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1437            bc.constrain(Size::new(100.0, 100.0))
1438        }
1439        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
1440        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1441            match event {
1442                InputEvent::Pointer(p) if p.phase == PointerPhase::Down => {
1443                    if p.position.x < 50.0 {
1444                        ctx.request_focus();
1445                        ctx.publish_ime_state(ImeState {
1446                            active: true,
1447                            editing: EditingState {
1448                                text: "abc".to_string(),
1449                                selection_base: 3,
1450                                selection_extent: 3,
1451                                composing_base: -1,
1452                                composing_extent: -1,
1453                            },
1454                            caret: Some(Rect::new(0.0, 0.0, 1.0, 12.0)),
1455                            content_type: Default::default(),
1456                            suppress_soft_keyboard: false,
1457                        });
1458                    }
1459                    EventResult::Handled
1460                }
1461                InputEvent::Key(_) | InputEvent::Ime(_) => EventResult::Handled,
1462                _ => EventResult::Ignored,
1463            }
1464        }
1465    }
1466    struct ImeLeafView;
1467    impl View<()> for ImeLeafView {
1468        type Element = ImeLeafWidget;
1469        fn build(&self, _ctx: &mut BuildCtx<'_>) -> ImeLeafWidget {
1470            ImeLeafWidget
1471        }
1472        fn rebuild(
1473            &self,
1474            _prev: &Self,
1475            _element: &mut ImeLeafWidget,
1476            _ctx: &mut BuildCtx<'_>,
1477        ) -> ChangeFlags {
1478            ChangeFlags::NONE
1479        }
1480    }
1481    struct ImeComp;
1482    impl Component for ImeComp {
1483        type State = ();
1484        fn init(&self) {}
1485        fn build(&self, _state: &mut ()) -> impl View<()> {
1486            any(ImeLeafView)
1487        }
1488    }
1489
1490    #[test]
1491    fn focus_ime_and_blur_surface_through_render_root() {
1492        use crate::app::RenderRoot;
1493        let _owner = ambient();
1494        let mut root: RenderRoot<(), ComponentView<ImeComp>> = RenderRoot::new();
1495        let mut state = ();
1496        root.rebuild(&mut |_s: &mut ()| component(ImeComp), &mut state);
1497        root.layout(Size::new(100.0, 100.0));
1498
1499        assert!(!root.is_focus_active());
1500        assert!(root.ime_state().is_none());
1501
1502        // A left-half Down inside the component focuses the inner leaf and its
1503        // IME surface bubbles across the boundary to the render root.
1504        root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
1505        assert!(root.is_focus_active(), "focus request crossed the boundary");
1506        let ime = root.ime_state().expect("IME surface surfaced at the root");
1507        assert!(ime.active);
1508        assert_eq!(ime.editing.text, "abc");
1509
1510        // A focused Key routes down the focus path across the boundary (handled).
1511        let outcome = root.event(&mut state, &key_enter());
1512        assert!(outcome.handled, "Key routed to the focused inner leaf");
1513
1514        // A right-half Down does not re-establish focus: blur clears both the
1515        // focus state and the IME surface at the root.
1516        root.event(&mut state, &pointer(PointerPhase::Down, 80.0, 10.0));
1517        assert!(!root.is_focus_active(), "blur crossed the boundary");
1518        assert!(root.ime_state().is_none());
1519    }
1520
1521    // --- Criterion 5: ChangeFlags propagation -------------------------------
1522
1523    struct TextLeafView {
1524        text: &'static str,
1525    }
1526    struct TextLeafWidget {
1527        text: &'static str,
1528    }
1529    impl View<()> for TextLeafView {
1530        type Element = TextLeafWidget;
1531        fn build(&self, _ctx: &mut BuildCtx<'_>) -> TextLeafWidget {
1532            TextLeafWidget { text: self.text }
1533        }
1534        fn rebuild(
1535            &self,
1536            prev: &Self,
1537            element: &mut TextLeafWidget,
1538            _ctx: &mut BuildCtx<'_>,
1539        ) -> ChangeFlags {
1540            if prev.text != self.text {
1541                element.text = self.text;
1542                ChangeFlags::PAINT
1543            } else {
1544                ChangeFlags::NONE
1545            }
1546        }
1547    }
1548    impl Widget for TextLeafWidget {
1549        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1550            bc.constrain(Size::new(10.0, 10.0))
1551        }
1552        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
1553    }
1554    struct Labeled {
1555        text: &'static str,
1556    }
1557    impl Component for Labeled {
1558        type State = ();
1559        fn init(&self) {}
1560        fn build(&self, _state: &mut ()) -> impl View<()> {
1561            any(TextLeafView { text: self.text })
1562        }
1563    }
1564
1565    #[test]
1566    fn child_paint_flags_bubble_out_of_component_rebuild() {
1567        let _owner = ambient();
1568        let v1 = component(Labeled { text: "a" });
1569        let mut widget = build_widget::<(), _>(&v1);
1570
1571        // An unchanged rebuild reports nothing.
1572        let v_same = component(Labeled { text: "a" });
1573        let mut next_id = 0u64;
1574        let mut ctx = BuildCtx::new(&mut next_id);
1575        let flags = View::<()>::rebuild(&v_same, &v1, &mut widget, &mut ctx);
1576        assert_eq!(flags, ChangeFlags::NONE);
1577
1578        // A changed prop makes the child view return PAINT; it must bubble out
1579        // of the component rebuild unchanged.
1580        let v2 = component(Labeled { text: "b" });
1581        let flags = View::<()>::rebuild(&v2, &v_same, &mut widget, &mut ctx);
1582        assert_eq!(flags, ChangeFlags::PAINT);
1583    }
1584
1585    // --- Hot-patch seam: the creator-image layout witness --------------------
1586
1587    #[cfg(feature = "hotpatch")]
1588    mod seam {
1589        use super::*;
1590        use crate::hotpatch::{Inert, SeamWitness};
1591        use std::any::type_name;
1592        use std::mem::offset_of;
1593
1594        /// A witness no image computes for `C`: the stored layout of a widget
1595        /// another image built.
1596        fn foreign<C: Component>() -> SeamWitness {
1597            let own = SeamWitness::of::<C>();
1598            SeamWitness {
1599                size_state: own.size_state + 8,
1600                ..own
1601            }
1602        }
1603
1604        /// Whether a mismatch for `S` is pending; clears this test's records.
1605        fn take_reported<S>() -> bool {
1606            let mine: Vec<_> = frust_hotpatch::pending_layout_mismatches()
1607                .into_iter()
1608                .filter(|m| m.type_name == type_name::<S>())
1609                .collect();
1610            frust_hotpatch::mark_layout_mismatches_reported(&mine);
1611            !mine.is_empty()
1612        }
1613
1614        /// A state whose drop is observable. Each test names its own type so
1615        /// the process-global mismatch records never cross tests.
1616        struct Counted<const N: usize> {
1617            value: u32,
1618            drops: Arc<AtomicU32>,
1619        }
1620        impl<const N: usize> Drop for Counted<N> {
1621            fn drop(&mut self) {
1622                self.drops.fetch_add(1, Ordering::SeqCst);
1623            }
1624        }
1625
1626        /// A component over `Counted<N>` counting its builds, its owner's
1627        /// disposal and its child's teardowns.
1628        struct Probe<const N: usize> {
1629            builds: Arc<AtomicU32>,
1630            drops: Arc<AtomicU32>,
1631            disposed: Arc<AtomicU32>,
1632            child_teardowns: Arc<AtomicU32>,
1633        }
1634        impl<const N: usize> Probe<N> {
1635            fn new() -> Self {
1636                Self {
1637                    builds: Arc::new(AtomicU32::new(0)),
1638                    drops: Arc::new(AtomicU32::new(0)),
1639                    disposed: Arc::new(AtomicU32::new(0)),
1640                    child_teardowns: Arc::new(AtomicU32::new(0)),
1641                }
1642            }
1643            fn handle(&self) -> Self {
1644                Self {
1645                    builds: self.builds.clone(),
1646                    drops: self.drops.clone(),
1647                    disposed: self.disposed.clone(),
1648                    child_teardowns: self.child_teardowns.clone(),
1649                }
1650            }
1651        }
1652        impl<const N: usize> Component for Probe<N> {
1653            type State = Counted<N>;
1654            fn init(&self) -> Counted<N> {
1655                let disposed = self.disposed.clone();
1656                on_cleanup(move || {
1657                    disposed.fetch_add(1, Ordering::SeqCst);
1658                });
1659                Counted {
1660                    value: 0,
1661                    drops: self.drops.clone(),
1662                }
1663            }
1664            fn build(&self, state: &mut Counted<N>) -> impl View<Counted<N>> {
1665                self.builds.fetch_add(1, Ordering::SeqCst);
1666                state.value += 1;
1667                TeardownProbe {
1668                    teardowns: self.child_teardowns.clone(),
1669                }
1670            }
1671        }
1672
1673        /// A leaf counting its teardowns.
1674        struct TeardownProbe {
1675            teardowns: Arc<AtomicU32>,
1676        }
1677        impl<S: 'static> View<S> for TeardownProbe {
1678            type Element = EmptyWidget;
1679            fn build(&self, _ctx: &mut BuildCtx<'_>) -> EmptyWidget {
1680                EmptyWidget
1681            }
1682            fn rebuild(
1683                &self,
1684                _prev: &Self,
1685                _element: &mut EmptyWidget,
1686                _ctx: &mut BuildCtx<'_>,
1687            ) -> ChangeFlags {
1688                ChangeFlags::NONE
1689            }
1690            fn teardown(&self, _element: &mut EmptyWidget, _ctx: &mut BuildCtx<'_>) {
1691                self.teardowns.fetch_add(1, Ordering::SeqCst);
1692            }
1693        }
1694
1695        /// Hosts `Probe<1>` as its subtree, so the nested widget sits in its
1696        /// own erased box.
1697        struct Parent {
1698            nested: Probe<1>,
1699        }
1700        impl Component for Parent {
1701            type State = ();
1702            fn init(&self) {}
1703            fn build(&self, _state: &mut ()) -> impl View<()> {
1704                component(self.nested.handle())
1705            }
1706        }
1707
1708        fn nested_of(parent: &mut ComponentWidget<Parent>) -> &mut ComponentWidget<Probe<1>> {
1709            let boxed = parent
1710                .child
1711                .widget_mut()
1712                .downcast_mut::<Box<dyn Widget>>()
1713                .expect("component child element is a boxed AnyView widget");
1714            (**boxed)
1715                .downcast_mut::<ComponentWidget<Probe<1>>>()
1716                .expect("the parent's subtree is the nested component")
1717        }
1718
1719        #[test]
1720        fn a_nested_widget_with_a_foreign_witness_is_never_rebuilt() {
1721            let _owner = ambient();
1722            let nested = Probe::<1>::new();
1723            let v1 = component(Parent {
1724                nested: nested.handle(),
1725            });
1726            let mut widget = build_widget::<(), _>(&v1);
1727            assert_eq!(nested.builds.load(Ordering::SeqCst), 1);
1728
1729            nested_of(&mut widget).set_witness_for_test(foreign::<Probe<1>>());
1730            let v2 = component(Parent {
1731                nested: nested.handle(),
1732            });
1733            let mut next_id = 0u64;
1734            let mut ctx = BuildCtx::new(&mut next_id);
1735            let flags = View::<()>::rebuild(&v2, &v1, &mut widget, &mut ctx);
1736
1737            assert_eq!(flags, ChangeFlags::NONE);
1738            assert_eq!(nested.builds.load(Ordering::SeqCst), 1, "never rebuilt");
1739            assert_eq!(nested_of(&mut widget).state().value, 1, "state untouched");
1740            assert!(take_reported::<Counted<1>>(), "the mismatch is reported");
1741
1742            // Dropping the tree leaks the mismatched state and disposes its owner.
1743            drop(widget);
1744            assert_eq!(nested.drops.load(Ordering::SeqCst), 0);
1745            assert_eq!(nested.disposed.load(Ordering::SeqCst), 1);
1746            assert!(take_reported::<Counted<1>>());
1747        }
1748
1749        /// A two-arm view whose element holds the live arm inline, the shape of
1750        /// `frust-widgets`' `Either` (which this crate cannot depend on).
1751        enum TwoArm<L, R> {
1752            Left(L),
1753            Right(R),
1754        }
1755        enum TwoArmWidget<L, R> {
1756            Left(L),
1757            Right(R),
1758        }
1759        impl<L: Widget, R: Widget> Widget for TwoArmWidget<L, R> {
1760            fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1761                match self {
1762                    Self::Left(w) => w.layout(ctx, bc),
1763                    Self::Right(w) => w.layout(ctx, bc),
1764                }
1765            }
1766            fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1767                match self {
1768                    Self::Left(w) => w.paint(ctx, scene),
1769                    Self::Right(w) => w.paint(ctx, scene),
1770                }
1771            }
1772        }
1773        impl<S: 'static, L: View<S>, R: View<S>> View<S> for TwoArm<L, R> {
1774            type Element = TwoArmWidget<L::Element, R::Element>;
1775            fn build(&self, ctx: &mut BuildCtx<'_>) -> Self::Element {
1776                match self {
1777                    Self::Left(v) => TwoArmWidget::Left(v.build(ctx)),
1778                    Self::Right(v) => TwoArmWidget::Right(v.build(ctx)),
1779                }
1780            }
1781            fn rebuild(
1782                &self,
1783                prev: &Self,
1784                element: &mut Self::Element,
1785                ctx: &mut BuildCtx<'_>,
1786            ) -> ChangeFlags {
1787                match (self, prev, &mut *element) {
1788                    (Self::Left(v), Self::Left(p), TwoArmWidget::Left(w)) => v.rebuild(p, w, ctx),
1789                    (Self::Right(v), Self::Right(p), TwoArmWidget::Right(w)) => {
1790                        v.rebuild(p, w, ctx)
1791                    }
1792                    _ => {
1793                        // Arm swap: tear the old arm down, then build the new
1794                        // one in place, dropping the old element inline.
1795                        prev.teardown(element, ctx);
1796                        *element = self.build(ctx);
1797                        ChangeFlags::LAYOUT | ChangeFlags::PAINT
1798                    }
1799                }
1800            }
1801            fn teardown(&self, element: &mut Self::Element, ctx: &mut BuildCtx<'_>) {
1802                match (self, element) {
1803                    (Self::Left(v), TwoArmWidget::Left(w)) => v.teardown(w, ctx),
1804                    (Self::Right(v), TwoArmWidget::Right(w)) => v.teardown(w, ctx),
1805                    _ => {}
1806                }
1807            }
1808        }
1809
1810        #[test]
1811        fn an_inline_arm_swap_leaks_a_foreign_state_and_disposes_its_owner() {
1812            let _owner = ambient();
1813            let probe = Probe::<2>::new();
1814            let left: TwoArm<ComponentView<Probe<2>>, Empty> =
1815                TwoArm::Left(component(probe.handle()));
1816            let mut next_id = 0u64;
1817            let mut ctx = BuildCtx::new(&mut next_id);
1818            let mut element = View::<()>::build(&left, &mut ctx);
1819            match &mut element {
1820                TwoArmWidget::Left(w) => w.set_witness_for_test(foreign::<Probe<2>>()),
1821                TwoArmWidget::Right(_) => unreachable!("built the left arm"),
1822            }
1823
1824            let right: TwoArm<ComponentView<Probe<2>>, Empty> = TwoArm::Right(Empty);
1825            let flags = View::<()>::rebuild(&right, &left, &mut element, &mut ctx);
1826
1827            assert_eq!(flags, ChangeFlags::LAYOUT | ChangeFlags::PAINT);
1828            assert!(matches!(element, TwoArmWidget::Right(_)), "swapped out");
1829            assert_eq!(
1830                probe.drops.load(Ordering::SeqCst),
1831                0,
1832                "the state is neither dropped nor freed"
1833            );
1834            assert_eq!(
1835                probe.child_teardowns.load(Ordering::SeqCst),
1836                0,
1837                "teardown did not proceed"
1838            );
1839            assert_eq!(probe.disposed.load(Ordering::SeqCst), 1, "owner disposed");
1840            assert!(take_reported::<Counted<2>>(), "the mismatch is reported");
1841        }
1842
1843        #[test]
1844        fn a_matching_widget_tears_down_and_drops_its_state() {
1845            let _owner = ambient();
1846            let probe = Probe::<3>::new();
1847            let view = component(probe.handle());
1848            let mut widget = build_widget::<(), _>(&view);
1849            let mut next_id = 0u64;
1850            let mut ctx = BuildCtx::new(&mut next_id);
1851            View::<()>::rebuild(&view, &view, &mut widget, &mut ctx);
1852            assert_eq!(probe.builds.load(Ordering::SeqCst), 2);
1853            View::<()>::teardown(&view, &mut widget, &mut ctx);
1854            assert_eq!(probe.child_teardowns.load(Ordering::SeqCst), 1);
1855            assert_eq!(probe.disposed.load(Ordering::SeqCst), 1);
1856            drop(widget);
1857            assert_eq!(probe.drops.load(Ordering::SeqCst), 1);
1858            assert!(!take_reported::<Counted<3>>());
1859        }
1860
1861        struct Tiny;
1862        impl Component for Tiny {
1863            type State = u8;
1864            fn init(&self) -> u8 {
1865                0
1866            }
1867            fn build(&self, _state: &mut u8) -> impl View<u8> {
1868                Inert
1869            }
1870        }
1871
1872        struct Huge;
1873        impl Component for Huge {
1874            type State = [u64; 32];
1875            fn init(&self) -> [u64; 32] {
1876                [0; 32]
1877            }
1878            fn build(&self, _state: &mut [u64; 32]) -> impl View<[u64; 32]> {
1879                Inert
1880            }
1881        }
1882
1883        #[test]
1884        fn widget_layout_does_not_depend_on_the_state_layout() {
1885            assert_ne!(size_of::<u8>(), size_of::<[u64; 32]>());
1886            assert_eq!(
1887                size_of::<ComponentWidget<Tiny>>(),
1888                size_of::<ComponentWidget<Huge>>()
1889            );
1890            assert_eq!(
1891                align_of::<ComponentWidget<Tiny>>(),
1892                align_of::<ComponentWidget<Huge>>()
1893            );
1894            macro_rules! same_offset {
1895                ($($field:ident),*) => {$(
1896                    assert_eq!(
1897                        offset_of!(ComponentWidget<Tiny>, $field),
1898                        offset_of!(ComponentWidget<Huge>, $field),
1899                        stringify!($field)
1900                    );
1901                )*};
1902            }
1903            same_offset!(witness, state, prev, child, owner, next_id, disposed);
1904            assert_eq!(offset_of!(ComponentWidget<Tiny>, witness), 0);
1905        }
1906    }
1907}