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                let flags = new_view.rebuild(&element.prev, boxed, &mut inner);
342                let after = {
343                    let any: &dyn Any = &**boxed;
344                    any.type_id()
345                };
346                (flags, before != after)
347            };
348            if swapped {
349                // The child AnyView swapped concrete type: the old widget was
350                // torn down inside `AnyView::rebuild`, so any capture/focus path
351                // it recorded is stale — drop it (mirrors `rebuild_child`).
352                element.child.set_active(false);
353                element.child.set_focused(false);
354                // Dropping the flag is not the load-bearing part: the focused
355                // widget just stopped existing, so a LIVE session died with it
356                // and the root must release it before the frame ends. Gated on
357                // the whole chain — `outer_has_focus && child_focused` is the
358                // same composition `teardown` spells below and the same one
359                // `frust-widgets`' `mark_orphan_if_live` applies at its own
360                // marking sites (this crate sits below that one, so the gate is
361                // spelled by hand rather than shared).
362                if outer_has_focus && child_focused {
363                    crate::event::mark_focus_orphaned();
364                }
365            }
366            element.prev = new_view;
367            flags
368        })
369    }
370
371    fn teardown(&self, element: &mut Self::Element, ctx: &mut BuildCtx<'_>) {
372        // A widget another image built with a different layout is not torn
373        // down: neither the child teardown through `prev` nor the dispose runs
374        // here (`Drop` disposes the owner and leaks the state).
375        #[cfg(feature = "hotpatch")]
376        if crate::hotpatch::verify_witness::<C>(element.witness).is_err() {
377            return;
378        }
379        // Forward teardown to the child view/element under the component owner,
380        // then dispose the owner (runs `on_cleanup`s) and let the state drop.
381        //
382        // The outer focus chain crosses the boundary here exactly as it does in
383        // `rebuild`: a component torn down *inside a live focus chain* must let
384        // the reconcilers below it mark the orphan, and one torn down under a
385        // cleared link must not (see `BuildCtx::has_focus`).
386        let inner_has_focus = ctx.has_focus() && element.child.is_focused();
387        element.owner.clone().with(|| {
388            let mut next_id = element.next_id;
389            let mut inner = BuildCtx::new(&mut next_id);
390            inner.set_has_focus(inner_has_focus);
391            if let Some(boxed) = element.child.widget_mut().downcast_mut::<Box<dyn Widget>>() {
392                element.prev.teardown(boxed, &mut inner);
393            }
394        });
395        element.dispose();
396    }
397}
398
399impl<C: Component> Widget for ComponentWidget<C> {
400    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
401        // Pass-through: the component contributes no chrome of its own. The
402        // child sits at the component's origin (ZERO in the component's local
403        // space) and receives the incoming constraints unchanged.
404        self.child.set_origin(Point::ZERO);
405        self.child.layout_child(ctx, bc)
406    }
407
408    fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
409        // Pass-through to the pod at origin ZERO: `paint_child` offsets by the
410        // incoming `ctx.origin()`, bubbles `needs_frame`, republishes a focused
411        // editable's IME surface, and composes `has_focus` — exactly like a
412        // single-child container.
413        self.child.paint_child(ctx, scene);
414    }
415
416    fn semantics(&self, ctx: &mut crate::semantics::SemanticsCtx) {
417        // A component contributes no node of its own (the state boundary is
418        // invisible to accessibility); forward to the child pod, exactly like a
419        // transparent single-child container.
420        self.child.semantics_child(ctx);
421    }
422
423    fn visit_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
424        // The component boundary is invisible to tooling for the same reason it
425        // is invisible to accessibility: it contributes no element of its own,
426        // only the child it wraps.
427        visitor(&self.child);
428    }
429
430    fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
431        // The state boundary: build an inner context over the component's *own*
432        // local state, seeded with the outer focus flag, dispatch through the
433        // pod with the single-child routing contract, then mirror the inner
434        // results back onto the outer context.
435        //
436        // A synthesized `Cancel` (from an outer structural rebuild) arrives over
437        // throwaway `()` outer state, but this handler never touches outer state
438        // — it forwards to the subtree over the component's real state, safe
439        // under the Cancel-never-reads-state contract (which binds the subtree's
440        // handlers, not this carrier).
441        let size = ctx.size();
442        let has_focus = ctx.has_focus();
443        // The hover link, its live epoch, and this pass's claim eligibility all
444        // cross the state boundary unchanged — a component is transparent to hover
445        // exactly as it is to focus, so a claim made below it still reaches the root
446        // and stamps the pod chain on the way (mirrored back through `claim_hover`).
447        let hovered = ctx.is_hovered();
448        let hover_epoch = ctx.hover_epoch();
449        let hover_eligible = ctx.is_hover_eligible();
450        // A cursor request needs no threading here at all, unlike every other
451        // channel this carrier mirrors: it rides one pass-scoped slot rather than a
452        // per-context field (see `crate::event`'s `CURSOR_REQUEST`), so a
453        // `set_cursor` below this boundary already reaches the root, and the
454        // component boundary is transparent to it by construction.
455        let (result, needs_redraw, captured, hover_claimed, focus_req, focus_rel, ime) = {
456            // Spelled per field (not through `state_mut`) so the borrow stays
457            // disjoint from `self.child` below.
458            #[cfg(feature = "hotpatch")]
459            let state_any: &mut dyn Any = self
460                .state
461                .as_deref_mut()
462                .expect("component state is present while the widget is live");
463            #[cfg(not(feature = "hotpatch"))]
464            let state_any: &mut dyn Any = &mut self.state;
465            let mut inner = EventCtx::new(state_any, Point::ZERO, size);
466            inner.set_has_focus(has_focus);
467            inner.set_hovered(hovered);
468            inner.set_hover_epoch(hover_epoch);
469            inner.set_hover_eligible(hover_eligible);
470            let result = route_child(&mut self.child, &mut inner, event);
471            (
472                result,
473                inner.needs_redraw(),
474                inner.is_pointer_captured(),
475                inner.is_hover_claimed(),
476                inner.is_focus_requested(),
477                inner.is_focus_released(),
478                inner.take_ime_state(),
479            )
480        };
481        if needs_redraw {
482            ctx.request_redraw();
483        }
484        if captured {
485            ctx.capture_pointer();
486        }
487        if hover_claimed {
488            ctx.claim_hover();
489        }
490        if focus_req {
491            ctx.request_focus();
492        }
493        if focus_rel {
494            ctx.release_focus();
495        }
496        if let Some(ime) = ime {
497            ctx.publish_ime_state(ime);
498        }
499        result
500    }
501}
502
503/// Route an event to the component's single child pod with the recorded-path
504/// semantics single-child containers use (the in-crate mirror of
505/// `frust-widgets`' `route_event_single`).
506///
507/// A broadcast ([`InputEvent::is_broadcast`]) reaches the child unconditionally,
508/// checked **first** so no capture/focus/hit-test branch below can swallow it —
509/// a component sitting between the root and a navigator is the ordinary shape,
510/// so this arm is what lets a deferred pop-result flush reach that navigator at
511/// all. A focus-routed event goes to the child only if it holds the recorded
512/// focus path; a captured (active) child receives pointer events unconditionally
513/// (capture auto-releasing on `Up`/`Cancel`), otherwise the child receives the
514/// event only if it contains the point, and a `Down` that misses a focused
515/// child blurs it.
516fn route_child(pod: &mut ChildPod, ctx: &mut EventCtx<'_>, event: &InputEvent) -> EventResult {
517    if event.is_broadcast() {
518        // Never consumed: forward, discard the result (see the variant's
519        // routing contract).
520        pod.event_child(ctx, event);
521        return EventResult::Ignored;
522    }
523    if event.is_focus_routed() {
524        if pod.is_focused() {
525            return pod.event_child(ctx, event);
526        }
527        return EventResult::Ignored;
528    }
529    if pod.is_active() {
530        let result = pod.event_child(ctx, event);
531        if releases_capture(event) {
532            pod.set_active(false);
533        }
534        return result;
535    }
536    let inside = pod.contains(event.position());
537    let result = if inside {
538        pod.event_child(ctx, event)
539    } else {
540        EventResult::Ignored
541    };
542    if is_pointer_down(event) && !inside && pod.is_focused() {
543        pod.set_focused(false);
544    }
545    result
546}
547
548/// Whether `event` is the phase that auto-releases a recorded capture
549/// (`Up`/`Cancel`).
550fn releases_capture(event: &InputEvent) -> bool {
551    matches!(
552        event,
553        InputEvent::Pointer(p) if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
554    )
555}
556
557/// Whether `event` is a pointer `Down` (opens capture / triggers blur eval).
558fn is_pointer_down(event: &InputEvent) -> bool {
559    matches!(
560        event,
561        InputEvent::Pointer(p) if matches!(p.phase, PointerPhase::Down)
562    )
563}
564
565#[cfg(test)]
566mod tests {
567    use super::*;
568    use crate::event::{
569        CursorIcon, EditingState, ImeState, Key, KeyEvent, Modifiers, NamedKey, PointerButton,
570        PointerEvent, clear_cursor_request, take_cursor_request,
571    };
572    use crate::view::any;
573    use kurbo::Rect;
574    use reactive_graph::owner::{on_cleanup, provide_context, use_context};
575    use std::cell::Cell;
576    use std::rc::Rc;
577    use std::sync::atomic::{AtomicU32, Ordering};
578    use std::sync::{Arc, Mutex};
579
580    // --- Event constructors -------------------------------------------------
581
582    fn pointer(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
583        InputEvent::Pointer(PointerEvent {
584            phase,
585            position: Point::new(x, y),
586            button: PointerButton::Primary,
587        })
588    }
589
590    fn key_enter() -> InputEvent {
591        InputEvent::Key(KeyEvent {
592            key: Key::Named(NamedKey::Enter),
593            modifiers: Modifiers::default(),
594            repeat: false,
595        })
596    }
597
598    // --- A trivial leaf view/widget used to fill component subtrees ---------
599
600    struct Empty;
601    struct EmptyWidget;
602    impl Widget for EmptyWidget {
603        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
604            bc.constrain(Size::new(10.0, 10.0))
605        }
606        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
607    }
608    impl View<()> for Empty {
609        type Element = EmptyWidget;
610        fn build(&self, _ctx: &mut BuildCtx<'_>) -> EmptyWidget {
611            EmptyWidget
612        }
613        fn rebuild(
614            &self,
615            _prev: &Self,
616            _element: &mut EmptyWidget,
617            _ctx: &mut BuildCtx<'_>,
618        ) -> ChangeFlags {
619            ChangeFlags::NONE
620        }
621    }
622
623    /// A second, distinct leaf type — an `AnyView` swap partner for `Empty`.
624    struct OtherLeaf;
625    struct OtherWidget;
626    impl Widget for OtherWidget {
627        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
628            bc.constrain(Size::new(10.0, 10.0))
629        }
630        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
631    }
632    impl View<()> for OtherLeaf {
633        type Element = OtherWidget;
634        fn build(&self, _ctx: &mut BuildCtx<'_>) -> OtherWidget {
635            OtherWidget
636        }
637        fn rebuild(
638            &self,
639            _prev: &Self,
640            _element: &mut OtherWidget,
641            _ctx: &mut BuildCtx<'_>,
642        ) -> ChangeFlags {
643            ChangeFlags::NONE
644        }
645    }
646
647    // --- Criterion 1: local-state retention (a click counter) ---------------
648
649    struct CounterState {
650        count: u32,
651    }
652
653    /// A component whose button increments its own local `count`. Carries a
654    /// `label` prop so a parent-driven rebuild can change props without
655    /// disturbing the retained state.
656    struct ClickCounter {
657        label: &'static str,
658    }
659
660    impl Component for ClickCounter {
661        type State = CounterState;
662        fn init(&self) -> CounterState {
663            CounterState { count: 0 }
664        }
665        fn build(&self, state: &mut CounterState) -> impl View<CounterState> {
666            any(CounterButtonView {
667                count: state.count,
668                label: self.label,
669            })
670        }
671    }
672
673    struct CounterButtonView {
674        count: u32,
675        label: &'static str,
676    }
677    struct CounterButtonWidget {
678        count: u32,
679        label: &'static str,
680    }
681    impl View<CounterState> for CounterButtonView {
682        type Element = CounterButtonWidget;
683        fn build(&self, _ctx: &mut BuildCtx<'_>) -> CounterButtonWidget {
684            CounterButtonWidget {
685                count: self.count,
686                label: self.label,
687            }
688        }
689        fn rebuild(
690            &self,
691            prev: &Self,
692            element: &mut CounterButtonWidget,
693            _ctx: &mut BuildCtx<'_>,
694        ) -> ChangeFlags {
695            let mut flags = ChangeFlags::NONE;
696            if prev.count != self.count || prev.label != self.label {
697                element.count = self.count;
698                element.label = self.label;
699                flags = ChangeFlags::PAINT;
700            }
701            flags
702        }
703    }
704    impl Widget for CounterButtonWidget {
705        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
706            bc.constrain(Size::new(40.0, 20.0))
707        }
708        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
709        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
710            if let InputEvent::Pointer(p) = event
711                && p.phase == PointerPhase::Down
712            {
713                ctx.state_mut::<CounterState>().count += 1;
714                ctx.request_redraw();
715                return EventResult::Handled;
716            }
717            EventResult::Ignored
718        }
719    }
720
721    /// Set (and keep alive) an ambient owner for a test, so components created
722    /// under it become children of a real owner (context/cleanup work).
723    fn ambient() -> Owner {
724        let owner = Owner::new();
725        owner.set();
726        owner
727    }
728
729    fn build_widget<Outer: 'static, C: Component>(view: &ComponentView<C>) -> ComponentWidget<C> {
730        let mut next_id = 0u64;
731        let mut ctx = BuildCtx::new(&mut next_id);
732        View::<Outer>::build(view, &mut ctx)
733    }
734
735    #[test]
736    fn local_state_survives_parent_driven_rebuild() {
737        let _owner = ambient();
738        let v1 = component(ClickCounter { label: "a" });
739        let mut widget = build_widget::<(), _>(&v1);
740        let mut lctx = LayoutCtx::new();
741        widget.layout(&mut lctx, &BoxConstraints::tight(Size::new(40.0, 20.0)));
742
743        // Three clicks land inside the button and increment the component's own
744        // local state through the inner EventCtx.
745        let mut outer = ();
746        for _ in 0..3 {
747            let mut ectx = EventCtx::new(&mut outer, Point::ZERO, Size::new(40.0, 20.0));
748            widget.event(&mut ectx, &pointer(PointerPhase::Down, 5.0, 5.0));
749        }
750        assert_eq!(widget.state().count, 3);
751
752        // A parent-driven rebuild with changed props must not reset the count.
753        let v2 = component(ClickCounter { label: "changed" });
754        let mut next_id = 0u64;
755        let mut ctx = BuildCtx::new(&mut next_id);
756        View::<()>::rebuild(&v2, &v1, &mut widget, &mut ctx);
757        assert_eq!(
758            widget.state().count,
759            3,
760            "local state retained across rebuild"
761        );
762    }
763
764    // --- Criterion 2: context scoping across the component boundary ---------
765
766    #[derive(Clone)]
767    struct Theme(i32);
768
769    /// Provides a `Theme` on its own owner, then hosts a child component that
770    /// reads it — the child (a descendant) must resolve the ancestor's value.
771    struct Provider {
772        sink: Arc<Mutex<Option<i32>>>,
773    }
774    impl Component for Provider {
775        type State = ();
776        fn init(&self) {}
777        fn build(&self, _state: &mut ()) -> impl View<()> {
778            provide_context(Theme(7));
779            any(component(Reader {
780                sink: self.sink.clone(),
781            }))
782        }
783    }
784
785    /// Reads `Theme` from context during build, recording what it resolved.
786    struct Reader {
787        sink: Arc<Mutex<Option<i32>>>,
788    }
789    impl Component for Reader {
790        type State = ();
791        fn init(&self) {}
792        fn build(&self, _state: &mut ()) -> impl View<()> {
793            *self.sink.lock().unwrap() = use_context::<Theme>().map(|t| t.0);
794            any(Empty)
795        }
796    }
797
798    /// Provides a `Theme` but does not descend into a reader — used to prove a
799    /// sibling's provided context is invisible to a cousin.
800    struct SecretProvider;
801    impl Component for SecretProvider {
802        type State = ();
803        fn init(&self) {}
804        fn build(&self, _state: &mut ()) -> impl View<()> {
805            provide_context(Theme(99));
806            any(Empty)
807        }
808    }
809
810    #[test]
811    fn use_context_resolves_ancestor_provided_value() {
812        let _owner = ambient();
813        let sink = Arc::new(Mutex::new(None));
814        let v = component(Provider { sink: sink.clone() });
815        let _widget = build_widget::<(), _>(&v);
816        assert_eq!(*sink.lock().unwrap(), Some(7));
817    }
818
819    #[test]
820    fn sibling_does_not_see_a_cousins_context() {
821        let parent = ambient();
822        let reader_sink = Arc::new(Mutex::new(Some(-1)));
823        // Two siblings under the same parent owner: one provides a secret, the
824        // other reads. The reader (a cousin, not a descendant) must see None.
825        parent.with(|| {
826            let secret = component(SecretProvider);
827            let _s = build_widget::<(), _>(&secret);
828            let reader = component(Reader {
829                sink: reader_sink.clone(),
830            });
831            let _r = build_widget::<(), _>(&reader);
832        });
833        assert_eq!(*reader_sink.lock().unwrap(), None);
834    }
835
836    // --- Criterion 3: on_cleanup / owner disposal ---------------------------
837
838    struct Disposable {
839        probe: Arc<AtomicU32>,
840    }
841    impl Component for Disposable {
842        type State = ();
843        fn init(&self) {
844            let probe = self.probe.clone();
845            // Registered once (init runs once) on this component's owner.
846            on_cleanup(move || {
847                probe.fetch_add(1, Ordering::SeqCst);
848            });
849        }
850        fn build(&self, _state: &mut ()) -> impl View<()> {
851            any(Empty)
852        }
853    }
854
855    #[test]
856    fn anyview_type_swap_disposes_component_owner_once() {
857        let _owner = ambient();
858        let probe = Arc::new(AtomicU32::new(0));
859        let mut next_id = 0u64;
860        let mut ctx = BuildCtx::new(&mut next_id);
861
862        let prev = any::<(), _>(component(Disposable {
863            probe: probe.clone(),
864        }));
865        let mut element: Box<dyn Widget> = prev.build(&mut ctx);
866        assert_eq!(probe.load(Ordering::SeqCst), 0);
867
868        // Swap the AnyView to a different concrete type: the component is torn
869        // down and its `on_cleanup` runs exactly once.
870        let next = any::<(), _>(OtherLeaf);
871        next.rebuild(&prev, &mut element, &mut ctx);
872        assert_eq!(probe.load(Ordering::SeqCst), 1);
873
874        // Dropping the replaced widget (already disposed) must not fire again.
875        drop(element);
876        assert_eq!(probe.load(Ordering::SeqCst), 1);
877    }
878
879    // --- Erasure at the boundary --------------------------------------------
880
881    /// A component whose `build` returns a concrete view (no `any(..)`): the
882    /// host erases it, so the pod's double-boxed element wraps the leaf widget.
883    struct ConcreteComp;
884    impl Component for ConcreteComp {
885        type State = ();
886        fn init(&self) {}
887        fn build(&self, _state: &mut ()) -> impl View<()> {
888            Empty
889        }
890    }
891
892    /// The same leaf, returned already erased: `AnyView::new` is idempotent, so
893    /// the host does not wrap the `AnyView` in a second box.
894    struct ErasedComp;
895    impl Component for ErasedComp {
896        type State = ();
897        fn init(&self) {}
898        fn build(&self, _state: &mut ()) -> impl View<()> {
899            any(Empty)
900        }
901    }
902
903    /// Whether the component's child element is the leaf widget itself (one
904    /// erasure), not a box around another boxed element (two).
905    fn child_is_empty_widget<C: Component>(widget: &mut ComponentWidget<C>) -> bool {
906        let boxed = widget
907            .child
908            .widget_mut()
909            .downcast_mut::<Box<dyn Widget>>()
910            .expect("double-boxed AnyView element");
911        let any: &dyn Any = &**boxed;
912        any.is::<EmptyWidget>()
913    }
914
915    #[test]
916    fn build_result_is_erased_exactly_once_at_the_boundary() {
917        let _owner = ambient();
918
919        let concrete = component(ConcreteComp);
920        let mut widget = build_widget::<(), _>(&concrete);
921        assert!(child_is_empty_widget(&mut widget), "concrete body, build");
922        let mut next_id = 0u64;
923        let mut ctx = BuildCtx::new(&mut next_id);
924        View::<()>::rebuild(&concrete, &concrete, &mut widget, &mut ctx);
925        assert!(child_is_empty_widget(&mut widget), "concrete body, rebuild");
926
927        let erased = component(ErasedComp);
928        let mut widget = build_widget::<(), _>(&erased);
929        assert!(child_is_empty_widget(&mut widget), "any(..) body, build");
930        let mut next_id = 0u64;
931        let mut ctx = BuildCtx::new(&mut next_id);
932        View::<()>::rebuild(&erased, &erased, &mut widget, &mut ctx);
933        assert!(child_is_empty_widget(&mut widget), "any(..) body, rebuild");
934    }
935
936    // --- The swap arm's orphan mark, across the state boundary --------------
937
938    /// A component whose child view *type* flips on a shared flag: `Empty`
939    /// first, `OtherLeaf` after — the `ComponentView::rebuild` swap arm's
940    /// fixture (`if editing { field } else { label }` inside a component's
941    /// `build`).
942    struct SwapComp {
943        swapped: Rc<Cell<bool>>,
944    }
945    impl Component for SwapComp {
946        type State = ();
947        fn init(&self) {}
948        fn build(&self, _state: &mut ()) -> impl View<()> {
949            if self.swapped.get() {
950                any(OtherLeaf)
951            } else {
952                any(Empty)
953            }
954        }
955    }
956
957    #[test]
958    fn component_swap_arm_marks_the_orphan_only_on_a_live_chain() {
959        /// Swap the component's child type with the *outer* chain either live
960        /// or already broken, reporting `(pod still focused, orphan marked)`.
961        fn swap_child(outer_live: bool) -> (bool, bool) {
962            let _ = crate::event::take_focus_orphaned();
963            let _owner = ambient();
964            let swapped = Rc::new(Cell::new(false));
965            let v1 = component(SwapComp {
966                swapped: swapped.clone(),
967            });
968            let mut widget = build_widget::<(), _>(&v1);
969            // The component's own child pod holds the recorded focus path.
970            widget.child.set_focused(true);
971
972            swapped.set(true);
973            let v2 = component(SwapComp {
974                swapped: swapped.clone(),
975            });
976            let mut next_id = 0u64;
977            let mut ctx = BuildCtx::new(&mut next_id);
978            // The chain the surrounding tree hands this component.
979            ctx.set_has_focus(outer_live);
980            View::<()>::rebuild(&v2, &v1, &mut widget, &mut ctx);
981            (
982                widget.child.is_focused(),
983                crate::event::take_focus_orphaned(),
984            )
985        }
986
987        let (still_focused, marked) = swap_child(true);
988        assert!(
989            !still_focused,
990            "a swapped-in widget must not inherit the old focus link"
991        );
992        assert!(
993            marked,
994            "a live focus path dying inside the component's swap releases the session"
995        );
996
997        let (still_focused, marked) = swap_child(false);
998        assert!(!still_focused, "the flag is dropped either way");
999        assert!(
1000            !marked,
1001            "the same swap under an already-blurred ancestor owns no session to release"
1002        );
1003    }
1004
1005    #[test]
1006    fn component_content_only_rebuild_keeps_focus_and_marks_nothing() {
1007        // The negative guard: the component always re-runs `build`, so a
1008        // same-type child rebuild happens every frame a component is on screen
1009        // — marking there would drop the keyboard continuously.
1010        let _ = crate::event::take_focus_orphaned();
1011        let _owner = ambient();
1012        let v1 = component(Labeled { text: "a" });
1013        let mut widget = build_widget::<(), _>(&v1);
1014        widget.child.set_focused(true);
1015
1016        let v2 = component(Labeled { text: "b" });
1017        let mut next_id = 0u64;
1018        let mut ctx = BuildCtx::new(&mut next_id);
1019        View::<()>::rebuild(&v2, &v1, &mut widget, &mut ctx);
1020
1021        assert!(
1022            widget.child.is_focused(),
1023            "a content-only rebuild keeps the recorded focus path"
1024        );
1025        assert!(
1026            !crate::event::take_focus_orphaned(),
1027            "an in-place child rebuild must not orphan the focus session"
1028        );
1029    }
1030
1031    #[test]
1032    fn explicit_teardown_disposes_owner_once_and_drop_is_idempotent() {
1033        let _owner = ambient();
1034        let probe = Arc::new(AtomicU32::new(0));
1035        let view = component(Disposable {
1036            probe: probe.clone(),
1037        });
1038        let mut widget = build_widget::<(), _>(&view);
1039
1040        let mut next_id = 0u64;
1041        let mut ctx = BuildCtx::new(&mut next_id);
1042        View::<()>::teardown(&view, &mut widget, &mut ctx);
1043        assert_eq!(probe.load(Ordering::SeqCst), 1);
1044
1045        // The defensive `Drop` must not run the cleanup a second time.
1046        drop(widget);
1047        assert_eq!(probe.load(Ordering::SeqCst), 1);
1048    }
1049
1050    // --- Criterion 4: event routing across the boundary ---------------------
1051
1052    /// A widget that captures the pointer on `Down` (tracking `pressed`), and
1053    /// clears both on `Up`/`Cancel` — the latter without touching state.
1054    struct CaptureWidget {
1055        pressed: bool,
1056    }
1057    impl Widget for CaptureWidget {
1058        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1059            bc.constrain(Size::new(40.0, 40.0))
1060        }
1061        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
1062        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1063            if let InputEvent::Pointer(p) = event {
1064                match p.phase {
1065                    PointerPhase::Down => {
1066                        self.pressed = true;
1067                        ctx.capture_pointer();
1068                        ctx.request_redraw();
1069                        return EventResult::Handled;
1070                    }
1071                    PointerPhase::Move => return EventResult::Handled,
1072                    // Cancel arm never reads application state.
1073                    PointerPhase::Up | PointerPhase::Cancel => {
1074                        self.pressed = false;
1075                        return EventResult::Handled;
1076                    }
1077                }
1078            }
1079            EventResult::Ignored
1080        }
1081    }
1082    struct CaptureView;
1083    impl View<()> for CaptureView {
1084        type Element = CaptureWidget;
1085        fn build(&self, _ctx: &mut BuildCtx<'_>) -> CaptureWidget {
1086            CaptureWidget { pressed: false }
1087        }
1088        fn rebuild(
1089            &self,
1090            _prev: &Self,
1091            _element: &mut CaptureWidget,
1092            _ctx: &mut BuildCtx<'_>,
1093        ) -> ChangeFlags {
1094            ChangeFlags::NONE
1095        }
1096    }
1097    struct CaptureComp;
1098    impl Component for CaptureComp {
1099        type State = ();
1100        fn init(&self) {}
1101        fn build(&self, _state: &mut ()) -> impl View<()> {
1102            any(CaptureView)
1103        }
1104    }
1105
1106    /// Recover the concrete inner widget from a component widget's pod (the
1107    /// child element is double-boxed).
1108    fn inner_widget<C: Component, W: Widget>(w: &mut ComponentWidget<C>) -> &mut W {
1109        w.child
1110            .widget_mut()
1111            .downcast_mut::<Box<dyn Widget>>()
1112            .expect("double-boxed AnyView element")
1113            .downcast_mut::<W>()
1114            .expect("inner concrete widget")
1115    }
1116
1117    #[test]
1118    fn capture_survives_drag_outside_bounds_and_mirrors_outward() {
1119        let _owner = ambient();
1120        let view = component(CaptureComp);
1121        let mut widget = build_widget::<(), _>(&view);
1122        let mut lctx = LayoutCtx::new();
1123        widget.layout(&mut lctx, &BoxConstraints::tight(Size::new(40.0, 40.0)));
1124
1125        let mut outer = ();
1126        // Down inside: the inner widget captures and the flag mirrors outward.
1127        let captured = {
1128            let mut ectx = EventCtx::new(&mut outer, Point::ZERO, Size::new(40.0, 40.0));
1129            widget.event(&mut ectx, &pointer(PointerPhase::Down, 5.0, 5.0));
1130            ectx.is_pointer_captured()
1131        };
1132        assert!(captured, "inner capture bubbles to the outer context");
1133        assert!(
1134            widget.child.is_active(),
1135            "component pod records the capture"
1136        );
1137
1138        // A Move far outside the child's bounds still reaches it (captured path).
1139        {
1140            let mut ectx = EventCtx::new(&mut outer, Point::ZERO, Size::new(40.0, 40.0));
1141            let r = widget.event(&mut ectx, &pointer(PointerPhase::Move, 500.0, 500.0));
1142            assert_eq!(r, EventResult::Handled, "drag outside bounds still routed");
1143        }
1144        assert!(widget.child.is_active(), "capture survives the drag");
1145
1146        // Up releases the capture at the boundary.
1147        {
1148            let mut ectx = EventCtx::new(&mut outer, Point::ZERO, Size::new(40.0, 40.0));
1149            widget.event(&mut ectx, &pointer(PointerPhase::Up, 500.0, 500.0));
1150        }
1151        assert!(!widget.child.is_active(), "Up auto-releases the capture");
1152        assert!(!inner_widget::<_, CaptureWidget>(&mut widget).pressed);
1153    }
1154
1155    #[test]
1156    fn synthesized_cancel_over_unit_state_is_safe_and_clears_pressed() {
1157        let _owner = ambient();
1158        let view = component(CaptureComp);
1159        let mut widget = build_widget::<(), _>(&view);
1160        let mut lctx = LayoutCtx::new();
1161        widget.layout(&mut lctx, &BoxConstraints::tight(Size::new(40.0, 40.0)));
1162
1163        // Arm the capture first (real state path).
1164        let mut outer = ();
1165        {
1166            let mut ectx = EventCtx::new(&mut outer, Point::ZERO, Size::new(40.0, 40.0));
1167            widget.event(&mut ectx, &pointer(PointerPhase::Down, 5.0, 5.0));
1168        }
1169        assert!(inner_widget::<_, CaptureWidget>(&mut widget).pressed);
1170
1171        // A structural-rebuild `Cancel` arrives over throwaway `()` state — as
1172        // `cancel_pod` synthesizes it — and must neither panic nor touch state.
1173        let mut dummy = ();
1174        {
1175            let mut ectx = EventCtx::new(&mut dummy, Point::ZERO, Size::new(40.0, 40.0));
1176            widget.event(&mut ectx, &pointer(PointerPhase::Cancel, 0.0, 0.0));
1177        }
1178        assert!(
1179            !inner_widget::<_, CaptureWidget>(&mut widget).pressed,
1180            "Cancel cleared pressed across the boundary"
1181        );
1182        assert!(!widget.child.is_active(), "Cancel released the capture");
1183    }
1184
1185    // A leaf that asks for a cursor on every `Move` — the canonical request shape.
1186    struct CursorLeafWidget;
1187    impl Widget for CursorLeafWidget {
1188        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1189            bc.constrain(Size::new(40.0, 40.0))
1190        }
1191        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
1192        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1193            if let InputEvent::Pointer(p) = event
1194                && p.phase == PointerPhase::Move
1195            {
1196                ctx.set_cursor(CursorIcon::Text);
1197            }
1198            EventResult::Ignored
1199        }
1200    }
1201    struct CursorLeafView;
1202    impl View<()> for CursorLeafView {
1203        type Element = CursorLeafWidget;
1204        fn build(&self, _ctx: &mut BuildCtx<'_>) -> CursorLeafWidget {
1205            CursorLeafWidget
1206        }
1207        fn rebuild(
1208            &self,
1209            _prev: &Self,
1210            _element: &mut CursorLeafWidget,
1211            _ctx: &mut BuildCtx<'_>,
1212        ) -> ChangeFlags {
1213            ChangeFlags::NONE
1214        }
1215    }
1216    struct CursorComp;
1217    impl Component for CursorComp {
1218        type State = ();
1219        fn init(&self) {}
1220        fn build(&self, _state: &mut ()) -> impl View<()> {
1221            any(CursorLeafView)
1222        }
1223    }
1224
1225    /// The component boundary is transparent to a cursor request with no mirroring
1226    /// code at all — the request rides one pass-scoped slot rather than a
1227    /// per-context field, so a refactor that turned it into a field (and forgot
1228    /// this carrier, as every other channel here has to be threaded) would drop it.
1229    #[test]
1230    fn a_cursor_request_crosses_the_component_boundary() {
1231        let _owner = ambient();
1232        let view = component(CursorComp);
1233        let mut widget = build_widget::<(), _>(&view);
1234        let mut lctx = LayoutCtx::new();
1235        widget.layout(&mut lctx, &BoxConstraints::tight(Size::new(40.0, 40.0)));
1236
1237        // `RenderRoot::event` clears the slot per pass; this test drives the widget
1238        // directly, so it does the same before asserting on what the pass left.
1239        clear_cursor_request();
1240        let mut outer = ();
1241        {
1242            let mut ectx = EventCtx::new(&mut outer, Point::ZERO, Size::new(40.0, 40.0));
1243            widget.event(&mut ectx, &pointer(PointerPhase::Move, 5.0, 5.0));
1244        }
1245        assert_eq!(
1246            take_cursor_request(),
1247            Some(CursorIcon::Text),
1248            "a request from below the component boundary reaches the root"
1249        );
1250    }
1251
1252    // A focus + IME publishing leaf: focuses on a left-half Down and publishes
1253    // an IME surface; a right-half Down neither focuses nor publishes.
1254    struct ImeLeafWidget;
1255    impl Widget for ImeLeafWidget {
1256        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1257            bc.constrain(Size::new(100.0, 100.0))
1258        }
1259        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
1260        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1261            match event {
1262                InputEvent::Pointer(p) if p.phase == PointerPhase::Down => {
1263                    if p.position.x < 50.0 {
1264                        ctx.request_focus();
1265                        ctx.publish_ime_state(ImeState {
1266                            active: true,
1267                            editing: EditingState {
1268                                text: "abc".to_string(),
1269                                selection_base: 3,
1270                                selection_extent: 3,
1271                                composing_base: -1,
1272                                composing_extent: -1,
1273                            },
1274                            caret: Some(Rect::new(0.0, 0.0, 1.0, 12.0)),
1275                            content_type: Default::default(),
1276                            suppress_soft_keyboard: false,
1277                        });
1278                    }
1279                    EventResult::Handled
1280                }
1281                InputEvent::Key(_) | InputEvent::Ime(_) => EventResult::Handled,
1282                _ => EventResult::Ignored,
1283            }
1284        }
1285    }
1286    struct ImeLeafView;
1287    impl View<()> for ImeLeafView {
1288        type Element = ImeLeafWidget;
1289        fn build(&self, _ctx: &mut BuildCtx<'_>) -> ImeLeafWidget {
1290            ImeLeafWidget
1291        }
1292        fn rebuild(
1293            &self,
1294            _prev: &Self,
1295            _element: &mut ImeLeafWidget,
1296            _ctx: &mut BuildCtx<'_>,
1297        ) -> ChangeFlags {
1298            ChangeFlags::NONE
1299        }
1300    }
1301    struct ImeComp;
1302    impl Component for ImeComp {
1303        type State = ();
1304        fn init(&self) {}
1305        fn build(&self, _state: &mut ()) -> impl View<()> {
1306            any(ImeLeafView)
1307        }
1308    }
1309
1310    #[test]
1311    fn focus_ime_and_blur_surface_through_render_root() {
1312        use crate::app::RenderRoot;
1313        let _owner = ambient();
1314        let mut root: RenderRoot<(), ComponentView<ImeComp>> = RenderRoot::new();
1315        let mut state = ();
1316        root.rebuild(&mut |_s: &mut ()| component(ImeComp), &mut state);
1317        root.layout(Size::new(100.0, 100.0));
1318
1319        assert!(!root.is_focus_active());
1320        assert!(root.ime_state().is_none());
1321
1322        // A left-half Down inside the component focuses the inner leaf and its
1323        // IME surface bubbles across the boundary to the render root.
1324        root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
1325        assert!(root.is_focus_active(), "focus request crossed the boundary");
1326        let ime = root.ime_state().expect("IME surface surfaced at the root");
1327        assert!(ime.active);
1328        assert_eq!(ime.editing.text, "abc");
1329
1330        // A focused Key routes down the focus path across the boundary (handled).
1331        let outcome = root.event(&mut state, &key_enter());
1332        assert!(outcome.handled, "Key routed to the focused inner leaf");
1333
1334        // A right-half Down does not re-establish focus: blur clears both the
1335        // focus state and the IME surface at the root.
1336        root.event(&mut state, &pointer(PointerPhase::Down, 80.0, 10.0));
1337        assert!(!root.is_focus_active(), "blur crossed the boundary");
1338        assert!(root.ime_state().is_none());
1339    }
1340
1341    // --- Criterion 5: ChangeFlags propagation -------------------------------
1342
1343    struct TextLeafView {
1344        text: &'static str,
1345    }
1346    struct TextLeafWidget {
1347        text: &'static str,
1348    }
1349    impl View<()> for TextLeafView {
1350        type Element = TextLeafWidget;
1351        fn build(&self, _ctx: &mut BuildCtx<'_>) -> TextLeafWidget {
1352            TextLeafWidget { text: self.text }
1353        }
1354        fn rebuild(
1355            &self,
1356            prev: &Self,
1357            element: &mut TextLeafWidget,
1358            _ctx: &mut BuildCtx<'_>,
1359        ) -> ChangeFlags {
1360            if prev.text != self.text {
1361                element.text = self.text;
1362                ChangeFlags::PAINT
1363            } else {
1364                ChangeFlags::NONE
1365            }
1366        }
1367    }
1368    impl Widget for TextLeafWidget {
1369        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1370            bc.constrain(Size::new(10.0, 10.0))
1371        }
1372        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
1373    }
1374    struct Labeled {
1375        text: &'static str,
1376    }
1377    impl Component for Labeled {
1378        type State = ();
1379        fn init(&self) {}
1380        fn build(&self, _state: &mut ()) -> impl View<()> {
1381            any(TextLeafView { text: self.text })
1382        }
1383    }
1384
1385    #[test]
1386    fn child_paint_flags_bubble_out_of_component_rebuild() {
1387        let _owner = ambient();
1388        let v1 = component(Labeled { text: "a" });
1389        let mut widget = build_widget::<(), _>(&v1);
1390
1391        // An unchanged rebuild reports nothing.
1392        let v_same = component(Labeled { text: "a" });
1393        let mut next_id = 0u64;
1394        let mut ctx = BuildCtx::new(&mut next_id);
1395        let flags = View::<()>::rebuild(&v_same, &v1, &mut widget, &mut ctx);
1396        assert_eq!(flags, ChangeFlags::NONE);
1397
1398        // A changed prop makes the child view return PAINT; it must bubble out
1399        // of the component rebuild unchanged.
1400        let v2 = component(Labeled { text: "b" });
1401        let flags = View::<()>::rebuild(&v2, &v_same, &mut widget, &mut ctx);
1402        assert_eq!(flags, ChangeFlags::PAINT);
1403    }
1404
1405    // --- Hot-patch seam: the creator-image layout witness --------------------
1406
1407    #[cfg(feature = "hotpatch")]
1408    mod seam {
1409        use super::*;
1410        use crate::hotpatch::{Inert, SeamWitness};
1411        use std::any::type_name;
1412        use std::mem::offset_of;
1413
1414        /// A witness no image computes for `C`: the stored layout of a widget
1415        /// another image built.
1416        fn foreign<C: Component>() -> SeamWitness {
1417            let own = SeamWitness::of::<C>();
1418            SeamWitness {
1419                size_state: own.size_state + 8,
1420                ..own
1421            }
1422        }
1423
1424        /// Whether a mismatch for `S` is pending; clears this test's records.
1425        fn take_reported<S>() -> bool {
1426            let mine: Vec<_> = frust_hotpatch::pending_layout_mismatches()
1427                .into_iter()
1428                .filter(|m| m.type_name == type_name::<S>())
1429                .collect();
1430            frust_hotpatch::mark_layout_mismatches_reported(&mine);
1431            !mine.is_empty()
1432        }
1433
1434        /// A state whose drop is observable. Each test names its own type so
1435        /// the process-global mismatch records never cross tests.
1436        struct Counted<const N: usize> {
1437            value: u32,
1438            drops: Arc<AtomicU32>,
1439        }
1440        impl<const N: usize> Drop for Counted<N> {
1441            fn drop(&mut self) {
1442                self.drops.fetch_add(1, Ordering::SeqCst);
1443            }
1444        }
1445
1446        /// A component over `Counted<N>` counting its builds, its owner's
1447        /// disposal and its child's teardowns.
1448        struct Probe<const N: usize> {
1449            builds: Arc<AtomicU32>,
1450            drops: Arc<AtomicU32>,
1451            disposed: Arc<AtomicU32>,
1452            child_teardowns: Arc<AtomicU32>,
1453        }
1454        impl<const N: usize> Probe<N> {
1455            fn new() -> Self {
1456                Self {
1457                    builds: Arc::new(AtomicU32::new(0)),
1458                    drops: Arc::new(AtomicU32::new(0)),
1459                    disposed: Arc::new(AtomicU32::new(0)),
1460                    child_teardowns: Arc::new(AtomicU32::new(0)),
1461                }
1462            }
1463            fn handle(&self) -> Self {
1464                Self {
1465                    builds: self.builds.clone(),
1466                    drops: self.drops.clone(),
1467                    disposed: self.disposed.clone(),
1468                    child_teardowns: self.child_teardowns.clone(),
1469                }
1470            }
1471        }
1472        impl<const N: usize> Component for Probe<N> {
1473            type State = Counted<N>;
1474            fn init(&self) -> Counted<N> {
1475                let disposed = self.disposed.clone();
1476                on_cleanup(move || {
1477                    disposed.fetch_add(1, Ordering::SeqCst);
1478                });
1479                Counted {
1480                    value: 0,
1481                    drops: self.drops.clone(),
1482                }
1483            }
1484            fn build(&self, state: &mut Counted<N>) -> impl View<Counted<N>> {
1485                self.builds.fetch_add(1, Ordering::SeqCst);
1486                state.value += 1;
1487                TeardownProbe {
1488                    teardowns: self.child_teardowns.clone(),
1489                }
1490            }
1491        }
1492
1493        /// A leaf counting its teardowns.
1494        struct TeardownProbe {
1495            teardowns: Arc<AtomicU32>,
1496        }
1497        impl<S: 'static> View<S> for TeardownProbe {
1498            type Element = EmptyWidget;
1499            fn build(&self, _ctx: &mut BuildCtx<'_>) -> EmptyWidget {
1500                EmptyWidget
1501            }
1502            fn rebuild(
1503                &self,
1504                _prev: &Self,
1505                _element: &mut EmptyWidget,
1506                _ctx: &mut BuildCtx<'_>,
1507            ) -> ChangeFlags {
1508                ChangeFlags::NONE
1509            }
1510            fn teardown(&self, _element: &mut EmptyWidget, _ctx: &mut BuildCtx<'_>) {
1511                self.teardowns.fetch_add(1, Ordering::SeqCst);
1512            }
1513        }
1514
1515        /// Hosts `Probe<1>` as its subtree, so the nested widget sits in its
1516        /// own erased box.
1517        struct Parent {
1518            nested: Probe<1>,
1519        }
1520        impl Component for Parent {
1521            type State = ();
1522            fn init(&self) {}
1523            fn build(&self, _state: &mut ()) -> impl View<()> {
1524                component(self.nested.handle())
1525            }
1526        }
1527
1528        fn nested_of(parent: &mut ComponentWidget<Parent>) -> &mut ComponentWidget<Probe<1>> {
1529            let boxed = parent
1530                .child
1531                .widget_mut()
1532                .downcast_mut::<Box<dyn Widget>>()
1533                .expect("component child element is a boxed AnyView widget");
1534            (**boxed)
1535                .downcast_mut::<ComponentWidget<Probe<1>>>()
1536                .expect("the parent's subtree is the nested component")
1537        }
1538
1539        #[test]
1540        fn a_nested_widget_with_a_foreign_witness_is_never_rebuilt() {
1541            let _owner = ambient();
1542            let nested = Probe::<1>::new();
1543            let v1 = component(Parent {
1544                nested: nested.handle(),
1545            });
1546            let mut widget = build_widget::<(), _>(&v1);
1547            assert_eq!(nested.builds.load(Ordering::SeqCst), 1);
1548
1549            nested_of(&mut widget).set_witness_for_test(foreign::<Probe<1>>());
1550            let v2 = component(Parent {
1551                nested: nested.handle(),
1552            });
1553            let mut next_id = 0u64;
1554            let mut ctx = BuildCtx::new(&mut next_id);
1555            let flags = View::<()>::rebuild(&v2, &v1, &mut widget, &mut ctx);
1556
1557            assert_eq!(flags, ChangeFlags::NONE);
1558            assert_eq!(nested.builds.load(Ordering::SeqCst), 1, "never rebuilt");
1559            assert_eq!(nested_of(&mut widget).state().value, 1, "state untouched");
1560            assert!(take_reported::<Counted<1>>(), "the mismatch is reported");
1561
1562            // Dropping the tree leaks the mismatched state and disposes its owner.
1563            drop(widget);
1564            assert_eq!(nested.drops.load(Ordering::SeqCst), 0);
1565            assert_eq!(nested.disposed.load(Ordering::SeqCst), 1);
1566            assert!(take_reported::<Counted<1>>());
1567        }
1568
1569        /// A two-arm view whose element holds the live arm inline, the shape of
1570        /// `frust-widgets`' `Either` (which this crate cannot depend on).
1571        enum TwoArm<L, R> {
1572            Left(L),
1573            Right(R),
1574        }
1575        enum TwoArmWidget<L, R> {
1576            Left(L),
1577            Right(R),
1578        }
1579        impl<L: Widget, R: Widget> Widget for TwoArmWidget<L, R> {
1580            fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1581                match self {
1582                    Self::Left(w) => w.layout(ctx, bc),
1583                    Self::Right(w) => w.layout(ctx, bc),
1584                }
1585            }
1586            fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1587                match self {
1588                    Self::Left(w) => w.paint(ctx, scene),
1589                    Self::Right(w) => w.paint(ctx, scene),
1590                }
1591            }
1592        }
1593        impl<S: 'static, L: View<S>, R: View<S>> View<S> for TwoArm<L, R> {
1594            type Element = TwoArmWidget<L::Element, R::Element>;
1595            fn build(&self, ctx: &mut BuildCtx<'_>) -> Self::Element {
1596                match self {
1597                    Self::Left(v) => TwoArmWidget::Left(v.build(ctx)),
1598                    Self::Right(v) => TwoArmWidget::Right(v.build(ctx)),
1599                }
1600            }
1601            fn rebuild(
1602                &self,
1603                prev: &Self,
1604                element: &mut Self::Element,
1605                ctx: &mut BuildCtx<'_>,
1606            ) -> ChangeFlags {
1607                match (self, prev, &mut *element) {
1608                    (Self::Left(v), Self::Left(p), TwoArmWidget::Left(w)) => v.rebuild(p, w, ctx),
1609                    (Self::Right(v), Self::Right(p), TwoArmWidget::Right(w)) => {
1610                        v.rebuild(p, w, ctx)
1611                    }
1612                    _ => {
1613                        // Arm swap: tear the old arm down, then build the new
1614                        // one in place, dropping the old element inline.
1615                        prev.teardown(element, ctx);
1616                        *element = self.build(ctx);
1617                        ChangeFlags::LAYOUT | ChangeFlags::PAINT
1618                    }
1619                }
1620            }
1621            fn teardown(&self, element: &mut Self::Element, ctx: &mut BuildCtx<'_>) {
1622                match (self, element) {
1623                    (Self::Left(v), TwoArmWidget::Left(w)) => v.teardown(w, ctx),
1624                    (Self::Right(v), TwoArmWidget::Right(w)) => v.teardown(w, ctx),
1625                    _ => {}
1626                }
1627            }
1628        }
1629
1630        #[test]
1631        fn an_inline_arm_swap_leaks_a_foreign_state_and_disposes_its_owner() {
1632            let _owner = ambient();
1633            let probe = Probe::<2>::new();
1634            let left: TwoArm<ComponentView<Probe<2>>, Empty> =
1635                TwoArm::Left(component(probe.handle()));
1636            let mut next_id = 0u64;
1637            let mut ctx = BuildCtx::new(&mut next_id);
1638            let mut element = View::<()>::build(&left, &mut ctx);
1639            match &mut element {
1640                TwoArmWidget::Left(w) => w.set_witness_for_test(foreign::<Probe<2>>()),
1641                TwoArmWidget::Right(_) => unreachable!("built the left arm"),
1642            }
1643
1644            let right: TwoArm<ComponentView<Probe<2>>, Empty> = TwoArm::Right(Empty);
1645            let flags = View::<()>::rebuild(&right, &left, &mut element, &mut ctx);
1646
1647            assert_eq!(flags, ChangeFlags::LAYOUT | ChangeFlags::PAINT);
1648            assert!(matches!(element, TwoArmWidget::Right(_)), "swapped out");
1649            assert_eq!(
1650                probe.drops.load(Ordering::SeqCst),
1651                0,
1652                "the state is neither dropped nor freed"
1653            );
1654            assert_eq!(
1655                probe.child_teardowns.load(Ordering::SeqCst),
1656                0,
1657                "teardown did not proceed"
1658            );
1659            assert_eq!(probe.disposed.load(Ordering::SeqCst), 1, "owner disposed");
1660            assert!(take_reported::<Counted<2>>(), "the mismatch is reported");
1661        }
1662
1663        #[test]
1664        fn a_matching_widget_tears_down_and_drops_its_state() {
1665            let _owner = ambient();
1666            let probe = Probe::<3>::new();
1667            let view = component(probe.handle());
1668            let mut widget = build_widget::<(), _>(&view);
1669            let mut next_id = 0u64;
1670            let mut ctx = BuildCtx::new(&mut next_id);
1671            View::<()>::rebuild(&view, &view, &mut widget, &mut ctx);
1672            assert_eq!(probe.builds.load(Ordering::SeqCst), 2);
1673            View::<()>::teardown(&view, &mut widget, &mut ctx);
1674            assert_eq!(probe.child_teardowns.load(Ordering::SeqCst), 1);
1675            assert_eq!(probe.disposed.load(Ordering::SeqCst), 1);
1676            drop(widget);
1677            assert_eq!(probe.drops.load(Ordering::SeqCst), 1);
1678            assert!(!take_reported::<Counted<3>>());
1679        }
1680
1681        struct Tiny;
1682        impl Component for Tiny {
1683            type State = u8;
1684            fn init(&self) -> u8 {
1685                0
1686            }
1687            fn build(&self, _state: &mut u8) -> impl View<u8> {
1688                Inert
1689            }
1690        }
1691
1692        struct Huge;
1693        impl Component for Huge {
1694            type State = [u64; 32];
1695            fn init(&self) -> [u64; 32] {
1696                [0; 32]
1697            }
1698            fn build(&self, _state: &mut [u64; 32]) -> impl View<[u64; 32]> {
1699                Inert
1700            }
1701        }
1702
1703        #[test]
1704        fn widget_layout_does_not_depend_on_the_state_layout() {
1705            assert_ne!(size_of::<u8>(), size_of::<[u64; 32]>());
1706            assert_eq!(
1707                size_of::<ComponentWidget<Tiny>>(),
1708                size_of::<ComponentWidget<Huge>>()
1709            );
1710            assert_eq!(
1711                align_of::<ComponentWidget<Tiny>>(),
1712                align_of::<ComponentWidget<Huge>>()
1713            );
1714            macro_rules! same_offset {
1715                ($($field:ident),*) => {$(
1716                    assert_eq!(
1717                        offset_of!(ComponentWidget<Tiny>, $field),
1718                        offset_of!(ComponentWidget<Huge>, $field),
1719                        stringify!($field)
1720                    );
1721                )*};
1722            }
1723            same_offset!(witness, state, prev, child, owner, next_id, disposed);
1724            assert_eq!(offset_of!(ComponentWidget<Tiny>, witness), 0);
1725        }
1726    }
1727}