Skip to main content

frust_core/
component.rs

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