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};
45use crate::layout::BoxConstraints;
46use crate::view::{AnyView, BuildCtx, ChangeFlags, View};
47use crate::widget::{ChildPod, LayoutCtx, PaintCtx, PaintScene, Widget};
48
49/// A piece of UI with retained local state (Flutter's `StatefulWidget` analog).
50///
51/// A `Component` declares an associated [`Component::State`] type, seeds it once
52/// with [`Component::init`], and produces its subtree with [`Component::build`],
53/// which receives `&mut State` so the build can read (and the subtree's events
54/// can mutate) the retained state. The subtree is an [`AnyView`] over the
55/// component's own `State` — the outer state type never appears.
56pub trait Component: 'static {
57    /// The retained local state this component owns across rebuilds.
58    type State: 'static;
59
60    /// Create the component's initial local state. Runs exactly once, when the
61    /// [`ComponentWidget`] is first built, under the component's [`Owner`] (so
62    /// `on_cleanup`/`provide_context` registered here bind to that owner).
63    ///
64    /// A *root* component (the one an entry macro or `frust::run` drives) has
65    /// no enclosing [`ComponentWidget`] and so no per-component owner of its
66    /// own: the shell instead runs its `init`/`build` under the shell's **root**
67    /// [`Owner`], which lives for the whole process and is never disposed. Its
68    /// `on_cleanup`/`provide_context` therefore bind to that root owner — the
69    /// context is visible to the entire tree, and cleanups run at process exit
70    /// rather than on teardown (there is no teardown for the root).
71    fn init(&self) -> Self::State;
72
73    /// Produce the component's subtree from its current local state.
74    ///
75    /// Re-run on every rebuild (local state may have changed even when the
76    /// component value is equal). The returned [`AnyView`] is diffed against the
77    /// previous one exactly like `RenderRoot`'s root view is.
78    fn build(&self, state: &mut Self::State) -> AnyView<Self::State>;
79}
80
81/// The `View` adapter that hosts a [`Component`] in any surrounding view tree.
82///
83/// `ComponentView<C>` implements `View<Outer>` for **every** `Outer` state:
84/// the hosted subtree is bound to `C::State`, not `Outer`, so a component can
85/// be dropped anywhere regardless of the ambient application state. Build it
86/// with the [`component`] free function.
87pub struct ComponentView<C: Component> {
88    component: C,
89}
90
91/// Host a [`Component`] as a [`View`] — the view-fn spelling of
92/// [`ComponentView`], mirroring the `text(..)`/`button(..)` vocabulary.
93pub fn component<C: Component>(c: C) -> ComponentView<C> {
94    ComponentView { component: c }
95}
96
97/// The retained widget behind a [`ComponentView`].
98///
99/// Owns the component's local `State`, the previous child [`AnyView`] (diffed
100/// each rebuild), the child element wrapped in a [`ChildPod`], the component's
101/// reactive [`Owner`], and a component-local [`BuildCtx`] id counter.
102///
103/// The child element is stored **double-boxed** inside the pod (`ChildPod::new(
104/// Box::new(element))`, where `element: Box<dyn Widget>` is the [`AnyView`]'s
105/// element): the extra box lets a rebuild recover the child as
106/// `&mut Box<dyn Widget>`, the exact type [`AnyView::rebuild`] needs to swap the
107/// widget on a concrete-type change. This mirrors `frust-widgets`'
108/// `build_child`/`rebuild_child` pod convention.
109pub struct ComponentWidget<C: Component> {
110    /// The component's retained local state; the inner [`EventCtx`] is built
111    /// over this, so the subtree's event handlers mutate it via `state_mut`.
112    state: C::State,
113    /// The previous child view, diffed against the freshly built one each
114    /// rebuild (the same reconcile `RenderRoot::rebuild_view` performs).
115    prev: AnyView<C::State>,
116    /// The child element (a double-boxed [`AnyView`] element) plus its geometry
117    /// and capture/focus bookkeeping.
118    child: ChildPod,
119    /// The component's reactive owner — child of the ambient owner, scoping
120    /// context and `on_cleanup`. Disposed explicitly at teardown / drop.
121    owner: Owner,
122    /// Component-local widget-id counter for the child's [`BuildCtx`] (decoupled
123    /// from the arena's monotonic ids because pod-owned children never enter the
124    /// arena).
125    next_id: u64,
126    /// Whether the owner has already been disposed, so teardown + `Drop` never
127    /// run cleanups twice.
128    disposed: bool,
129}
130
131impl<C: Component> ComponentWidget<C> {
132    /// Dispose the component's owner exactly once, running its `on_cleanup`s and
133    /// dropping any arena-allocated reactive values. Idempotent: teardown and a
134    /// later defensive `Drop` both call this, but the cleanups fire once.
135    fn dispose(&mut self) {
136        if !self.disposed {
137            // `Owner::cleanup` runs this owner's (and its children's) registered
138            // `on_cleanup`s and drops its arena nodes; it drains those lists, so
139            // a second call — including the `Drop` fallback below — is a no-op.
140            self.owner.cleanup();
141            self.disposed = true;
142        }
143    }
144}
145
146impl<C: Component> Drop for ComponentWidget<C> {
147    fn drop(&mut self) {
148        // Defensive: a component removed by a path that did not route through
149        // `View::teardown` (or a panic mid-teardown) still disposes its owner.
150        self.dispose();
151    }
152}
153
154impl<Outer: 'static, C: Component> View<Outer> for ComponentView<C> {
155    type Element = ComponentWidget<C>;
156
157    fn build(&self, _ctx: &mut BuildCtx<'_>) -> Self::Element {
158        // A child of the ambient owner: the root owner a shell installs, or the
159        // enclosing component's owner (we build under `owner.with`, below).
160        let owner = Owner::new();
161        let (state, prev, child, next_id) = owner.with(|| {
162            let mut state = self.component.init();
163            let view = self.component.build(&mut state);
164            // A component-local build counter; the child never enters the arena.
165            let mut next_id = 0u64;
166            let mut inner = BuildCtx::new(&mut next_id);
167            // A freshly built child pod holds no focus link (`ChildPod::new`
168            // starts unfocused), so the chain across this boundary is closed —
169            // and a build tears nothing down, so nothing under it can orphan a
170            // session (see `BuildCtx::has_focus`).
171            inner.set_has_focus(false);
172            let element: Box<dyn Widget> = view.build(&mut inner);
173            // Double-box so a later rebuild can recover `&mut Box<dyn Widget>`.
174            (state, view, ChildPod::new(Box::new(element)), next_id)
175        });
176        ComponentWidget {
177            state,
178            prev,
179            child,
180            owner,
181            next_id,
182            disposed: false,
183        }
184    }
185
186    fn rebuild(
187        &self,
188        _prev: &Self,
189        element: &mut Self::Element,
190        ctx: &mut BuildCtx<'_>,
191    ) -> ChangeFlags {
192        // The outer `_prev` ComponentView is intentionally unused: the component
193        // always re-runs `build` because its retained local state may have
194        // changed even when the component value is equal (tracked-scope skipping
195        // is not implemented).
196        //
197        // The outer `ctx`, by contrast, is read for exactly one thing: the
198        // effective focus chain, which must cross this state boundary or every
199        // reconciler under a component would start a fresh (assumed-live) chain.
200        // Read here because the inner context is built inside the `owner.with`
201        // closure below, which cannot also borrow `ctx`.
202        let outer_has_focus = ctx.has_focus();
203        let owner = element.owner.clone();
204        owner.with(|| {
205            let new_view = self.component.build(&mut element.state);
206            // Read before the `widget_mut` borrow below.
207            let child_focused = element.child.is_focused();
208            let (flags, swapped) = {
209                let boxed = element
210                    .child
211                    .widget_mut()
212                    .downcast_mut::<Box<dyn Widget>>()
213                    .expect("component child element is a boxed AnyView widget");
214                let before = {
215                    let any: &dyn Any = &**boxed;
216                    any.type_id()
217                };
218                let mut inner = BuildCtx::new(&mut element.next_id);
219                // Extend the chain by this component's own child link — the
220                // `ChildPod::paint_child` composition, spelled by hand because
221                // the component descends into its pod through a fresh inner
222                // context rather than through the pod plumbing.
223                inner.set_has_focus(outer_has_focus && child_focused);
224                let flags = new_view.rebuild(&element.prev, boxed, &mut inner);
225                let after = {
226                    let any: &dyn Any = &**boxed;
227                    any.type_id()
228                };
229                (flags, before != after)
230            };
231            if swapped {
232                // The child AnyView swapped concrete type: the old widget was
233                // torn down inside `AnyView::rebuild`, so any capture/focus path
234                // it recorded is stale — drop it (mirrors `rebuild_child`).
235                element.child.set_active(false);
236                element.child.set_focused(false);
237                // Dropping the flag is not the load-bearing part: the focused
238                // widget just stopped existing, so a LIVE session died with it
239                // and the root must release it before the frame ends. Gated on
240                // the whole chain — `outer_has_focus && child_focused` is the
241                // same composition `teardown` spells below and the same one
242                // `frust-widgets`' `mark_orphan_if_live` applies at its own
243                // marking sites (this crate sits below that one, so the gate is
244                // spelled by hand rather than shared).
245                if outer_has_focus && child_focused {
246                    crate::event::mark_focus_orphaned();
247                }
248            }
249            element.prev = new_view;
250            flags
251        })
252    }
253
254    fn teardown(&self, element: &mut Self::Element, ctx: &mut BuildCtx<'_>) {
255        // Forward teardown to the child view/element under the component owner,
256        // then dispose the owner (runs `on_cleanup`s) and let the state drop.
257        //
258        // The outer focus chain crosses the boundary here exactly as it does in
259        // `rebuild`: a component torn down *inside a live focus chain* must let
260        // the reconcilers below it mark the orphan, and one torn down under a
261        // cleared link must not (see `BuildCtx::has_focus`).
262        let inner_has_focus = ctx.has_focus() && element.child.is_focused();
263        element.owner.clone().with(|| {
264            let mut next_id = element.next_id;
265            let mut inner = BuildCtx::new(&mut next_id);
266            inner.set_has_focus(inner_has_focus);
267            if let Some(boxed) = element.child.widget_mut().downcast_mut::<Box<dyn Widget>>() {
268                element.prev.teardown(boxed, &mut inner);
269            }
270        });
271        element.dispose();
272    }
273}
274
275impl<C: Component> Widget for ComponentWidget<C> {
276    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
277        // Pass-through: the component contributes no chrome of its own. The
278        // child sits at the component's origin (ZERO in the component's local
279        // space) and receives the incoming constraints unchanged.
280        self.child.set_origin(Point::ZERO);
281        self.child.layout_child(ctx, bc)
282    }
283
284    fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
285        // Pass-through to the pod at origin ZERO: `paint_child` offsets by the
286        // incoming `ctx.origin()`, bubbles `needs_frame`, republishes a focused
287        // editable's IME surface, and composes `has_focus` — exactly like a
288        // single-child container.
289        self.child.paint_child(ctx, scene);
290    }
291
292    fn semantics(&self, ctx: &mut crate::semantics::SemanticsCtx) {
293        // A component contributes no node of its own (the state boundary is
294        // invisible to accessibility); forward to the child pod, exactly like a
295        // transparent single-child container.
296        self.child.semantics_child(ctx);
297    }
298
299    fn visit_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
300        // The component boundary is invisible to tooling for the same reason it
301        // is invisible to accessibility: it contributes no element of its own,
302        // only the child it wraps.
303        visitor(&self.child);
304    }
305
306    fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
307        // The state boundary: build an inner context over the component's *own*
308        // local state, seeded with the outer focus flag, dispatch through the
309        // pod with the single-child routing contract, then mirror the inner
310        // results back onto the outer context.
311        //
312        // A synthesized `Cancel` (from an outer structural rebuild) arrives over
313        // throwaway `()` outer state, but this handler never touches outer state
314        // — it forwards to the subtree over the component's real state, safe
315        // under the Cancel-never-reads-state contract (which binds the subtree's
316        // handlers, not this carrier).
317        let size = ctx.size();
318        let has_focus = ctx.has_focus();
319        // The hover link, its live epoch, and this pass's claim eligibility all
320        // cross the state boundary unchanged — a component is transparent to hover
321        // exactly as it is to focus, so a claim made below it still reaches the root
322        // and stamps the pod chain on the way (mirrored back through `claim_hover`).
323        let hovered = ctx.is_hovered();
324        let hover_epoch = ctx.hover_epoch();
325        let hover_eligible = ctx.is_hover_eligible();
326        // A cursor request needs no threading here at all, unlike every other
327        // channel this carrier mirrors: it rides one pass-scoped slot rather than a
328        // per-context field (see `crate::event`'s `CURSOR_REQUEST`), so a
329        // `set_cursor` below this boundary already reaches the root, and the
330        // component boundary is transparent to it by construction.
331        let (result, needs_redraw, captured, hover_claimed, focus_req, focus_rel, ime) = {
332            let state_any: &mut dyn Any = &mut self.state;
333            let mut inner = EventCtx::new(state_any, Point::ZERO, size);
334            inner.set_has_focus(has_focus);
335            inner.set_hovered(hovered);
336            inner.set_hover_epoch(hover_epoch);
337            inner.set_hover_eligible(hover_eligible);
338            let result = route_child(&mut self.child, &mut inner, event);
339            (
340                result,
341                inner.needs_redraw(),
342                inner.is_pointer_captured(),
343                inner.is_hover_claimed(),
344                inner.is_focus_requested(),
345                inner.is_focus_released(),
346                inner.take_ime_state(),
347            )
348        };
349        if needs_redraw {
350            ctx.request_redraw();
351        }
352        if captured {
353            ctx.capture_pointer();
354        }
355        if hover_claimed {
356            ctx.claim_hover();
357        }
358        if focus_req {
359            ctx.request_focus();
360        }
361        if focus_rel {
362            ctx.release_focus();
363        }
364        if let Some(ime) = ime {
365            ctx.publish_ime_state(ime);
366        }
367        result
368    }
369}
370
371/// Route an event to the component's single child pod with the recorded-path
372/// semantics single-child containers use (the in-crate mirror of
373/// `frust-widgets`' `route_event_single`).
374///
375/// A broadcast ([`InputEvent::is_broadcast`]) reaches the child unconditionally,
376/// checked **first** so no capture/focus/hit-test branch below can swallow it —
377/// a component sitting between the root and a navigator is the ordinary shape,
378/// so this arm is what lets a deferred pop-result flush reach that navigator at
379/// all. A focus-routed event goes to the child only if it holds the recorded
380/// focus path; a captured (active) child receives pointer events unconditionally
381/// (capture auto-releasing on `Up`/`Cancel`), otherwise the child receives the
382/// event only if it contains the point, and a `Down` that misses a focused
383/// child blurs it.
384fn route_child(pod: &mut ChildPod, ctx: &mut EventCtx<'_>, event: &InputEvent) -> EventResult {
385    if event.is_broadcast() {
386        // Never consumed: forward, discard the result (see the variant's
387        // routing contract).
388        pod.event_child(ctx, event);
389        return EventResult::Ignored;
390    }
391    if event.is_focus_routed() {
392        if pod.is_focused() {
393            return pod.event_child(ctx, event);
394        }
395        return EventResult::Ignored;
396    }
397    if pod.is_active() {
398        let result = pod.event_child(ctx, event);
399        if releases_capture(event) {
400            pod.set_active(false);
401        }
402        return result;
403    }
404    let inside = pod.contains(event.position());
405    let result = if inside {
406        pod.event_child(ctx, event)
407    } else {
408        EventResult::Ignored
409    };
410    if is_pointer_down(event) && !inside && pod.is_focused() {
411        pod.set_focused(false);
412    }
413    result
414}
415
416/// Whether `event` is the phase that auto-releases a recorded capture
417/// (`Up`/`Cancel`).
418fn releases_capture(event: &InputEvent) -> bool {
419    matches!(
420        event,
421        InputEvent::Pointer(p) if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
422    )
423}
424
425/// Whether `event` is a pointer `Down` (opens capture / triggers blur eval).
426fn is_pointer_down(event: &InputEvent) -> bool {
427    matches!(
428        event,
429        InputEvent::Pointer(p) if matches!(p.phase, PointerPhase::Down)
430    )
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436    use crate::event::{
437        CursorIcon, EditingState, ImeState, Key, KeyEvent, Modifiers, NamedKey, PointerButton,
438        PointerEvent, clear_cursor_request, take_cursor_request,
439    };
440    use crate::view::any;
441    use kurbo::Rect;
442    use reactive_graph::owner::{on_cleanup, provide_context, use_context};
443    use std::cell::Cell;
444    use std::rc::Rc;
445    use std::sync::atomic::{AtomicU32, Ordering};
446    use std::sync::{Arc, Mutex};
447
448    // --- Event constructors -------------------------------------------------
449
450    fn pointer(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
451        InputEvent::Pointer(PointerEvent {
452            phase,
453            position: Point::new(x, y),
454            button: PointerButton::Primary,
455        })
456    }
457
458    fn key_enter() -> InputEvent {
459        InputEvent::Key(KeyEvent {
460            key: Key::Named(NamedKey::Enter),
461            modifiers: Modifiers::default(),
462            repeat: false,
463        })
464    }
465
466    // --- A trivial leaf view/widget used to fill component subtrees ---------
467
468    struct Empty;
469    struct EmptyWidget;
470    impl Widget for EmptyWidget {
471        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
472            bc.constrain(Size::new(10.0, 10.0))
473        }
474        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
475    }
476    impl View<()> for Empty {
477        type Element = EmptyWidget;
478        fn build(&self, _ctx: &mut BuildCtx<'_>) -> EmptyWidget {
479            EmptyWidget
480        }
481        fn rebuild(
482            &self,
483            _prev: &Self,
484            _element: &mut EmptyWidget,
485            _ctx: &mut BuildCtx<'_>,
486        ) -> ChangeFlags {
487            ChangeFlags::NONE
488        }
489    }
490
491    /// A second, distinct leaf type — an `AnyView` swap partner for `Empty`.
492    struct OtherLeaf;
493    struct OtherWidget;
494    impl Widget for OtherWidget {
495        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
496            bc.constrain(Size::new(10.0, 10.0))
497        }
498        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
499    }
500    impl View<()> for OtherLeaf {
501        type Element = OtherWidget;
502        fn build(&self, _ctx: &mut BuildCtx<'_>) -> OtherWidget {
503            OtherWidget
504        }
505        fn rebuild(
506            &self,
507            _prev: &Self,
508            _element: &mut OtherWidget,
509            _ctx: &mut BuildCtx<'_>,
510        ) -> ChangeFlags {
511            ChangeFlags::NONE
512        }
513    }
514
515    // --- Criterion 1: local-state retention (a click counter) ---------------
516
517    struct CounterState {
518        count: u32,
519    }
520
521    /// A component whose button increments its own local `count`. Carries a
522    /// `label` prop so a parent-driven rebuild can change props without
523    /// disturbing the retained state.
524    struct ClickCounter {
525        label: &'static str,
526    }
527
528    impl Component for ClickCounter {
529        type State = CounterState;
530        fn init(&self) -> CounterState {
531            CounterState { count: 0 }
532        }
533        fn build(&self, state: &mut CounterState) -> AnyView<CounterState> {
534            any(CounterButtonView {
535                count: state.count,
536                label: self.label,
537            })
538        }
539    }
540
541    struct CounterButtonView {
542        count: u32,
543        label: &'static str,
544    }
545    struct CounterButtonWidget {
546        count: u32,
547        label: &'static str,
548    }
549    impl View<CounterState> for CounterButtonView {
550        type Element = CounterButtonWidget;
551        fn build(&self, _ctx: &mut BuildCtx<'_>) -> CounterButtonWidget {
552            CounterButtonWidget {
553                count: self.count,
554                label: self.label,
555            }
556        }
557        fn rebuild(
558            &self,
559            prev: &Self,
560            element: &mut CounterButtonWidget,
561            _ctx: &mut BuildCtx<'_>,
562        ) -> ChangeFlags {
563            let mut flags = ChangeFlags::NONE;
564            if prev.count != self.count || prev.label != self.label {
565                element.count = self.count;
566                element.label = self.label;
567                flags = ChangeFlags::PAINT;
568            }
569            flags
570        }
571    }
572    impl Widget for CounterButtonWidget {
573        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
574            bc.constrain(Size::new(40.0, 20.0))
575        }
576        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
577        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
578            if let InputEvent::Pointer(p) = event
579                && p.phase == PointerPhase::Down
580            {
581                ctx.state_mut::<CounterState>().count += 1;
582                ctx.request_redraw();
583                return EventResult::Handled;
584            }
585            EventResult::Ignored
586        }
587    }
588
589    /// Set (and keep alive) an ambient owner for a test, so components created
590    /// under it become children of a real owner (context/cleanup work).
591    fn ambient() -> Owner {
592        let owner = Owner::new();
593        owner.set();
594        owner
595    }
596
597    fn build_widget<Outer: 'static, C: Component>(view: &ComponentView<C>) -> ComponentWidget<C> {
598        let mut next_id = 0u64;
599        let mut ctx = BuildCtx::new(&mut next_id);
600        View::<Outer>::build(view, &mut ctx)
601    }
602
603    #[test]
604    fn local_state_survives_parent_driven_rebuild() {
605        let _owner = ambient();
606        let v1 = component(ClickCounter { label: "a" });
607        let mut widget = build_widget::<(), _>(&v1);
608        let mut lctx = LayoutCtx::new();
609        widget.layout(&mut lctx, &BoxConstraints::tight(Size::new(40.0, 20.0)));
610
611        // Three clicks land inside the button and increment the component's own
612        // local state through the inner EventCtx.
613        let mut outer = ();
614        for _ in 0..3 {
615            let mut ectx = EventCtx::new(&mut outer, Point::ZERO, Size::new(40.0, 20.0));
616            widget.event(&mut ectx, &pointer(PointerPhase::Down, 5.0, 5.0));
617        }
618        assert_eq!(widget.state.count, 3);
619
620        // A parent-driven rebuild with changed props must not reset the count.
621        let v2 = component(ClickCounter { label: "changed" });
622        let mut next_id = 0u64;
623        let mut ctx = BuildCtx::new(&mut next_id);
624        View::<()>::rebuild(&v2, &v1, &mut widget, &mut ctx);
625        assert_eq!(widget.state.count, 3, "local state retained across rebuild");
626    }
627
628    // --- Criterion 2: context scoping across the component boundary ---------
629
630    #[derive(Clone)]
631    struct Theme(i32);
632
633    /// Provides a `Theme` on its own owner, then hosts a child component that
634    /// reads it — the child (a descendant) must resolve the ancestor's value.
635    struct Provider {
636        sink: Arc<Mutex<Option<i32>>>,
637    }
638    impl Component for Provider {
639        type State = ();
640        fn init(&self) {}
641        fn build(&self, _state: &mut ()) -> AnyView<()> {
642            provide_context(Theme(7));
643            any(component(Reader {
644                sink: self.sink.clone(),
645            }))
646        }
647    }
648
649    /// Reads `Theme` from context during build, recording what it resolved.
650    struct Reader {
651        sink: Arc<Mutex<Option<i32>>>,
652    }
653    impl Component for Reader {
654        type State = ();
655        fn init(&self) {}
656        fn build(&self, _state: &mut ()) -> AnyView<()> {
657            *self.sink.lock().unwrap() = use_context::<Theme>().map(|t| t.0);
658            any(Empty)
659        }
660    }
661
662    /// Provides a `Theme` but does not descend into a reader — used to prove a
663    /// sibling's provided context is invisible to a cousin.
664    struct SecretProvider;
665    impl Component for SecretProvider {
666        type State = ();
667        fn init(&self) {}
668        fn build(&self, _state: &mut ()) -> AnyView<()> {
669            provide_context(Theme(99));
670            any(Empty)
671        }
672    }
673
674    #[test]
675    fn use_context_resolves_ancestor_provided_value() {
676        let _owner = ambient();
677        let sink = Arc::new(Mutex::new(None));
678        let v = component(Provider { sink: sink.clone() });
679        let _widget = build_widget::<(), _>(&v);
680        assert_eq!(*sink.lock().unwrap(), Some(7));
681    }
682
683    #[test]
684    fn sibling_does_not_see_a_cousins_context() {
685        let parent = ambient();
686        let reader_sink = Arc::new(Mutex::new(Some(-1)));
687        // Two siblings under the same parent owner: one provides a secret, the
688        // other reads. The reader (a cousin, not a descendant) must see None.
689        parent.with(|| {
690            let secret = component(SecretProvider);
691            let _s = build_widget::<(), _>(&secret);
692            let reader = component(Reader {
693                sink: reader_sink.clone(),
694            });
695            let _r = build_widget::<(), _>(&reader);
696        });
697        assert_eq!(*reader_sink.lock().unwrap(), None);
698    }
699
700    // --- Criterion 3: on_cleanup / owner disposal ---------------------------
701
702    struct Disposable {
703        probe: Arc<AtomicU32>,
704    }
705    impl Component for Disposable {
706        type State = ();
707        fn init(&self) {
708            let probe = self.probe.clone();
709            // Registered once (init runs once) on this component's owner.
710            on_cleanup(move || {
711                probe.fetch_add(1, Ordering::SeqCst);
712            });
713        }
714        fn build(&self, _state: &mut ()) -> AnyView<()> {
715            any(Empty)
716        }
717    }
718
719    #[test]
720    fn anyview_type_swap_disposes_component_owner_once() {
721        let _owner = ambient();
722        let probe = Arc::new(AtomicU32::new(0));
723        let mut next_id = 0u64;
724        let mut ctx = BuildCtx::new(&mut next_id);
725
726        let prev = any::<(), _>(component(Disposable {
727            probe: probe.clone(),
728        }));
729        let mut element: Box<dyn Widget> = prev.build(&mut ctx);
730        assert_eq!(probe.load(Ordering::SeqCst), 0);
731
732        // Swap the AnyView to a different concrete type: the component is torn
733        // down and its `on_cleanup` runs exactly once.
734        let next = any::<(), _>(OtherLeaf);
735        next.rebuild(&prev, &mut element, &mut ctx);
736        assert_eq!(probe.load(Ordering::SeqCst), 1);
737
738        // Dropping the replaced widget (already disposed) must not fire again.
739        drop(element);
740        assert_eq!(probe.load(Ordering::SeqCst), 1);
741    }
742
743    // --- The swap arm's orphan mark, across the state boundary --------------
744
745    /// A component whose child view *type* flips on a shared flag: `Empty`
746    /// first, `OtherLeaf` after — the `ComponentView::rebuild` swap arm's
747    /// fixture (`if editing { field } else { label }` inside a component's
748    /// `build`).
749    struct SwapComp {
750        swapped: Rc<Cell<bool>>,
751    }
752    impl Component for SwapComp {
753        type State = ();
754        fn init(&self) {}
755        fn build(&self, _state: &mut ()) -> AnyView<()> {
756            if self.swapped.get() {
757                any(OtherLeaf)
758            } else {
759                any(Empty)
760            }
761        }
762    }
763
764    #[test]
765    fn component_swap_arm_marks_the_orphan_only_on_a_live_chain() {
766        /// Swap the component's child type with the *outer* chain either live
767        /// or already broken, reporting `(pod still focused, orphan marked)`.
768        fn swap_child(outer_live: bool) -> (bool, bool) {
769            let _ = crate::event::take_focus_orphaned();
770            let _owner = ambient();
771            let swapped = Rc::new(Cell::new(false));
772            let v1 = component(SwapComp {
773                swapped: swapped.clone(),
774            });
775            let mut widget = build_widget::<(), _>(&v1);
776            // The component's own child pod holds the recorded focus path.
777            widget.child.set_focused(true);
778
779            swapped.set(true);
780            let v2 = component(SwapComp {
781                swapped: swapped.clone(),
782            });
783            let mut next_id = 0u64;
784            let mut ctx = BuildCtx::new(&mut next_id);
785            // The chain the surrounding tree hands this component.
786            ctx.set_has_focus(outer_live);
787            View::<()>::rebuild(&v2, &v1, &mut widget, &mut ctx);
788            (
789                widget.child.is_focused(),
790                crate::event::take_focus_orphaned(),
791            )
792        }
793
794        let (still_focused, marked) = swap_child(true);
795        assert!(
796            !still_focused,
797            "a swapped-in widget must not inherit the old focus link"
798        );
799        assert!(
800            marked,
801            "a live focus path dying inside the component's swap releases the session"
802        );
803
804        let (still_focused, marked) = swap_child(false);
805        assert!(!still_focused, "the flag is dropped either way");
806        assert!(
807            !marked,
808            "the same swap under an already-blurred ancestor owns no session to release"
809        );
810    }
811
812    #[test]
813    fn component_content_only_rebuild_keeps_focus_and_marks_nothing() {
814        // The negative guard: the component always re-runs `build`, so a
815        // same-type child rebuild happens every frame a component is on screen
816        // — marking there would drop the keyboard continuously.
817        let _ = crate::event::take_focus_orphaned();
818        let _owner = ambient();
819        let v1 = component(Labeled { text: "a" });
820        let mut widget = build_widget::<(), _>(&v1);
821        widget.child.set_focused(true);
822
823        let v2 = component(Labeled { text: "b" });
824        let mut next_id = 0u64;
825        let mut ctx = BuildCtx::new(&mut next_id);
826        View::<()>::rebuild(&v2, &v1, &mut widget, &mut ctx);
827
828        assert!(
829            widget.child.is_focused(),
830            "a content-only rebuild keeps the recorded focus path"
831        );
832        assert!(
833            !crate::event::take_focus_orphaned(),
834            "an in-place child rebuild must not orphan the focus session"
835        );
836    }
837
838    #[test]
839    fn explicit_teardown_disposes_owner_once_and_drop_is_idempotent() {
840        let _owner = ambient();
841        let probe = Arc::new(AtomicU32::new(0));
842        let view = component(Disposable {
843            probe: probe.clone(),
844        });
845        let mut widget = build_widget::<(), _>(&view);
846
847        let mut next_id = 0u64;
848        let mut ctx = BuildCtx::new(&mut next_id);
849        View::<()>::teardown(&view, &mut widget, &mut ctx);
850        assert_eq!(probe.load(Ordering::SeqCst), 1);
851
852        // The defensive `Drop` must not run the cleanup a second time.
853        drop(widget);
854        assert_eq!(probe.load(Ordering::SeqCst), 1);
855    }
856
857    // --- Criterion 4: event routing across the boundary ---------------------
858
859    /// A widget that captures the pointer on `Down` (tracking `pressed`), and
860    /// clears both on `Up`/`Cancel` — the latter without touching state.
861    struct CaptureWidget {
862        pressed: bool,
863    }
864    impl Widget for CaptureWidget {
865        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
866            bc.constrain(Size::new(40.0, 40.0))
867        }
868        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
869        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
870            if let InputEvent::Pointer(p) = event {
871                match p.phase {
872                    PointerPhase::Down => {
873                        self.pressed = true;
874                        ctx.capture_pointer();
875                        ctx.request_redraw();
876                        return EventResult::Handled;
877                    }
878                    PointerPhase::Move => return EventResult::Handled,
879                    // Cancel arm never reads application state.
880                    PointerPhase::Up | PointerPhase::Cancel => {
881                        self.pressed = false;
882                        return EventResult::Handled;
883                    }
884                }
885            }
886            EventResult::Ignored
887        }
888    }
889    struct CaptureView;
890    impl View<()> for CaptureView {
891        type Element = CaptureWidget;
892        fn build(&self, _ctx: &mut BuildCtx<'_>) -> CaptureWidget {
893            CaptureWidget { pressed: false }
894        }
895        fn rebuild(
896            &self,
897            _prev: &Self,
898            _element: &mut CaptureWidget,
899            _ctx: &mut BuildCtx<'_>,
900        ) -> ChangeFlags {
901            ChangeFlags::NONE
902        }
903    }
904    struct CaptureComp;
905    impl Component for CaptureComp {
906        type State = ();
907        fn init(&self) {}
908        fn build(&self, _state: &mut ()) -> AnyView<()> {
909            any(CaptureView)
910        }
911    }
912
913    /// Recover the concrete inner widget from a component widget's pod (the
914    /// child element is double-boxed).
915    fn inner_widget<C: Component, W: Widget>(w: &mut ComponentWidget<C>) -> &mut W {
916        w.child
917            .widget_mut()
918            .downcast_mut::<Box<dyn Widget>>()
919            .expect("double-boxed AnyView element")
920            .downcast_mut::<W>()
921            .expect("inner concrete widget")
922    }
923
924    #[test]
925    fn capture_survives_drag_outside_bounds_and_mirrors_outward() {
926        let _owner = ambient();
927        let view = component(CaptureComp);
928        let mut widget = build_widget::<(), _>(&view);
929        let mut lctx = LayoutCtx::new();
930        widget.layout(&mut lctx, &BoxConstraints::tight(Size::new(40.0, 40.0)));
931
932        let mut outer = ();
933        // Down inside: the inner widget captures and the flag mirrors outward.
934        let captured = {
935            let mut ectx = EventCtx::new(&mut outer, Point::ZERO, Size::new(40.0, 40.0));
936            widget.event(&mut ectx, &pointer(PointerPhase::Down, 5.0, 5.0));
937            ectx.is_pointer_captured()
938        };
939        assert!(captured, "inner capture bubbles to the outer context");
940        assert!(
941            widget.child.is_active(),
942            "component pod records the capture"
943        );
944
945        // A Move far outside the child's bounds still reaches it (captured path).
946        {
947            let mut ectx = EventCtx::new(&mut outer, Point::ZERO, Size::new(40.0, 40.0));
948            let r = widget.event(&mut ectx, &pointer(PointerPhase::Move, 500.0, 500.0));
949            assert_eq!(r, EventResult::Handled, "drag outside bounds still routed");
950        }
951        assert!(widget.child.is_active(), "capture survives the drag");
952
953        // Up releases the capture at the boundary.
954        {
955            let mut ectx = EventCtx::new(&mut outer, Point::ZERO, Size::new(40.0, 40.0));
956            widget.event(&mut ectx, &pointer(PointerPhase::Up, 500.0, 500.0));
957        }
958        assert!(!widget.child.is_active(), "Up auto-releases the capture");
959        assert!(!inner_widget::<_, CaptureWidget>(&mut widget).pressed);
960    }
961
962    #[test]
963    fn synthesized_cancel_over_unit_state_is_safe_and_clears_pressed() {
964        let _owner = ambient();
965        let view = component(CaptureComp);
966        let mut widget = build_widget::<(), _>(&view);
967        let mut lctx = LayoutCtx::new();
968        widget.layout(&mut lctx, &BoxConstraints::tight(Size::new(40.0, 40.0)));
969
970        // Arm the capture first (real state path).
971        let mut outer = ();
972        {
973            let mut ectx = EventCtx::new(&mut outer, Point::ZERO, Size::new(40.0, 40.0));
974            widget.event(&mut ectx, &pointer(PointerPhase::Down, 5.0, 5.0));
975        }
976        assert!(inner_widget::<_, CaptureWidget>(&mut widget).pressed);
977
978        // A structural-rebuild `Cancel` arrives over throwaway `()` state — as
979        // `cancel_pod` synthesizes it — and must neither panic nor touch state.
980        let mut dummy = ();
981        {
982            let mut ectx = EventCtx::new(&mut dummy, Point::ZERO, Size::new(40.0, 40.0));
983            widget.event(&mut ectx, &pointer(PointerPhase::Cancel, 0.0, 0.0));
984        }
985        assert!(
986            !inner_widget::<_, CaptureWidget>(&mut widget).pressed,
987            "Cancel cleared pressed across the boundary"
988        );
989        assert!(!widget.child.is_active(), "Cancel released the capture");
990    }
991
992    // A leaf that asks for a cursor on every `Move` — the canonical request shape.
993    struct CursorLeafWidget;
994    impl Widget for CursorLeafWidget {
995        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
996            bc.constrain(Size::new(40.0, 40.0))
997        }
998        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
999        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1000            if let InputEvent::Pointer(p) = event
1001                && p.phase == PointerPhase::Move
1002            {
1003                ctx.set_cursor(CursorIcon::Text);
1004            }
1005            EventResult::Ignored
1006        }
1007    }
1008    struct CursorLeafView;
1009    impl View<()> for CursorLeafView {
1010        type Element = CursorLeafWidget;
1011        fn build(&self, _ctx: &mut BuildCtx<'_>) -> CursorLeafWidget {
1012            CursorLeafWidget
1013        }
1014        fn rebuild(
1015            &self,
1016            _prev: &Self,
1017            _element: &mut CursorLeafWidget,
1018            _ctx: &mut BuildCtx<'_>,
1019        ) -> ChangeFlags {
1020            ChangeFlags::NONE
1021        }
1022    }
1023    struct CursorComp;
1024    impl Component for CursorComp {
1025        type State = ();
1026        fn init(&self) {}
1027        fn build(&self, _state: &mut ()) -> AnyView<()> {
1028            any(CursorLeafView)
1029        }
1030    }
1031
1032    /// The component boundary is transparent to a cursor request with no mirroring
1033    /// code at all — the request rides one pass-scoped slot rather than a
1034    /// per-context field, so a refactor that turned it into a field (and forgot
1035    /// this carrier, as every other channel here has to be threaded) would drop it.
1036    #[test]
1037    fn a_cursor_request_crosses_the_component_boundary() {
1038        let _owner = ambient();
1039        let view = component(CursorComp);
1040        let mut widget = build_widget::<(), _>(&view);
1041        let mut lctx = LayoutCtx::new();
1042        widget.layout(&mut lctx, &BoxConstraints::tight(Size::new(40.0, 40.0)));
1043
1044        // `RenderRoot::event` clears the slot per pass; this test drives the widget
1045        // directly, so it does the same before asserting on what the pass left.
1046        clear_cursor_request();
1047        let mut outer = ();
1048        {
1049            let mut ectx = EventCtx::new(&mut outer, Point::ZERO, Size::new(40.0, 40.0));
1050            widget.event(&mut ectx, &pointer(PointerPhase::Move, 5.0, 5.0));
1051        }
1052        assert_eq!(
1053            take_cursor_request(),
1054            Some(CursorIcon::Text),
1055            "a request from below the component boundary reaches the root"
1056        );
1057    }
1058
1059    // A focus + IME publishing leaf: focuses on a left-half Down and publishes
1060    // an IME surface; a right-half Down neither focuses nor publishes.
1061    struct ImeLeafWidget;
1062    impl Widget for ImeLeafWidget {
1063        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1064            bc.constrain(Size::new(100.0, 100.0))
1065        }
1066        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
1067        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1068            match event {
1069                InputEvent::Pointer(p) if p.phase == PointerPhase::Down => {
1070                    if p.position.x < 50.0 {
1071                        ctx.request_focus();
1072                        ctx.publish_ime_state(ImeState {
1073                            active: true,
1074                            editing: EditingState {
1075                                text: "abc".to_string(),
1076                                selection_base: 3,
1077                                selection_extent: 3,
1078                                composing_base: -1,
1079                                composing_extent: -1,
1080                            },
1081                            caret: Some(Rect::new(0.0, 0.0, 1.0, 12.0)),
1082                            content_type: Default::default(),
1083                            suppress_soft_keyboard: false,
1084                        });
1085                    }
1086                    EventResult::Handled
1087                }
1088                InputEvent::Key(_) | InputEvent::Ime(_) => EventResult::Handled,
1089                _ => EventResult::Ignored,
1090            }
1091        }
1092    }
1093    struct ImeLeafView;
1094    impl View<()> for ImeLeafView {
1095        type Element = ImeLeafWidget;
1096        fn build(&self, _ctx: &mut BuildCtx<'_>) -> ImeLeafWidget {
1097            ImeLeafWidget
1098        }
1099        fn rebuild(
1100            &self,
1101            _prev: &Self,
1102            _element: &mut ImeLeafWidget,
1103            _ctx: &mut BuildCtx<'_>,
1104        ) -> ChangeFlags {
1105            ChangeFlags::NONE
1106        }
1107    }
1108    struct ImeComp;
1109    impl Component for ImeComp {
1110        type State = ();
1111        fn init(&self) {}
1112        fn build(&self, _state: &mut ()) -> AnyView<()> {
1113            any(ImeLeafView)
1114        }
1115    }
1116
1117    #[test]
1118    fn focus_ime_and_blur_surface_through_render_root() {
1119        use crate::app::RenderRoot;
1120        let _owner = ambient();
1121        let mut root: RenderRoot<(), ComponentView<ImeComp>> = RenderRoot::new();
1122        let mut state = ();
1123        root.rebuild(&mut |_s: &mut ()| component(ImeComp), &mut state);
1124        root.layout(Size::new(100.0, 100.0));
1125
1126        assert!(!root.is_focus_active());
1127        assert!(root.ime_state().is_none());
1128
1129        // A left-half Down inside the component focuses the inner leaf and its
1130        // IME surface bubbles across the boundary to the render root.
1131        root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
1132        assert!(root.is_focus_active(), "focus request crossed the boundary");
1133        let ime = root.ime_state().expect("IME surface surfaced at the root");
1134        assert!(ime.active);
1135        assert_eq!(ime.editing.text, "abc");
1136
1137        // A focused Key routes down the focus path across the boundary (handled).
1138        let outcome = root.event(&mut state, &key_enter());
1139        assert!(outcome.handled, "Key routed to the focused inner leaf");
1140
1141        // A right-half Down does not re-establish focus: blur clears both the
1142        // focus state and the IME surface at the root.
1143        root.event(&mut state, &pointer(PointerPhase::Down, 80.0, 10.0));
1144        assert!(!root.is_focus_active(), "blur crossed the boundary");
1145        assert!(root.ime_state().is_none());
1146    }
1147
1148    // --- Criterion 5: ChangeFlags propagation -------------------------------
1149
1150    struct TextLeafView {
1151        text: &'static str,
1152    }
1153    struct TextLeafWidget {
1154        text: &'static str,
1155    }
1156    impl View<()> for TextLeafView {
1157        type Element = TextLeafWidget;
1158        fn build(&self, _ctx: &mut BuildCtx<'_>) -> TextLeafWidget {
1159            TextLeafWidget { text: self.text }
1160        }
1161        fn rebuild(
1162            &self,
1163            prev: &Self,
1164            element: &mut TextLeafWidget,
1165            _ctx: &mut BuildCtx<'_>,
1166        ) -> ChangeFlags {
1167            if prev.text != self.text {
1168                element.text = self.text;
1169                ChangeFlags::PAINT
1170            } else {
1171                ChangeFlags::NONE
1172            }
1173        }
1174    }
1175    impl Widget for TextLeafWidget {
1176        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1177            bc.constrain(Size::new(10.0, 10.0))
1178        }
1179        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
1180    }
1181    struct Labeled {
1182        text: &'static str,
1183    }
1184    impl Component for Labeled {
1185        type State = ();
1186        fn init(&self) {}
1187        fn build(&self, _state: &mut ()) -> AnyView<()> {
1188            any(TextLeafView { text: self.text })
1189        }
1190    }
1191
1192    #[test]
1193    fn child_paint_flags_bubble_out_of_component_rebuild() {
1194        let _owner = ambient();
1195        let v1 = component(Labeled { text: "a" });
1196        let mut widget = build_widget::<(), _>(&v1);
1197
1198        // An unchanged rebuild reports nothing.
1199        let v_same = component(Labeled { text: "a" });
1200        let mut next_id = 0u64;
1201        let mut ctx = BuildCtx::new(&mut next_id);
1202        let flags = View::<()>::rebuild(&v_same, &v1, &mut widget, &mut ctx);
1203        assert_eq!(flags, ChangeFlags::NONE);
1204
1205        // A changed prop makes the child view return PAINT; it must bubble out
1206        // of the component rebuild unchanged.
1207        let v2 = component(Labeled { text: "b" });
1208        let flags = View::<()>::rebuild(&v2, &v_same, &mut widget, &mut ctx);
1209        assert_eq!(flags, ChangeFlags::PAINT);
1210    }
1211}