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