Skip to main content

teksilo_core/
build_context.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! BuildContext — context available during Widget::build().
5//!
6//! Provides Signal-based APIs for creating reactive state, registering
7//! effects, and adding child widgets during the build lifecycle.
8
9use crate::binding::BindingRegistry;
10use crate::event_source::{SubscriptionHandle, SubscriptionId};
11use crate::signal::{ObserverHandle, Signal};
12use crate::widget_id::WidgetId;
13
14/// Context available during Widget::build().
15pub struct BuildContext<'a> {
16    pub(crate) tree: &'a mut crate::widget_tree::WidgetTree,
17    pub(crate) composite_id: Option<WidgetId>,
18    /// RAII handles for effects registered during this build cycle.
19    /// Transferred to the arena node's `effect_handles` after build returns.
20    pub(crate) effect_handles: Vec<ObserverHandle>,
21    /// Backend-event subscription handles registered during this build
22    /// cycle via `subscribe_event`. Transferred to the arena node's
23    /// `subscription_handles` after build returns.
24    pub(crate) subscription_handles: Vec<(SubscriptionId, SubscriptionHandle)>,
25    /// The `SubscriptionId`s the **previous** build of this same widget used, in the
26    /// order it created them. Empty on a first mount.
27    ///
28    /// A subscription's id is what crosses the thread boundary: the publisher-side
29    /// wrapper captures it by value and posts it, and the UI thread looks it up some
30    /// frames later. Minting a fresh id on every rebuild therefore silently destroys
31    /// every event already in flight, because `rebuild_single_widget` removes the
32    /// previous build's callbacks before `build()` runs and the queued event then names
33    /// an id nothing answers to. Re-using the ids here makes a subscription's identity
34    /// span the rebuilds of one widget, so an event posted before a rebuild is delivered
35    /// to the closure the *new* build installed.
36    ///
37    /// Matched **by position**, which is what makes it cheap and predictable: the Nth
38    /// `subscribe_event`/`subscribe_event_with_ctx` call of this build re-uses the id of
39    /// the Nth call of the last one. A build that subscribes fewer times simply leaves
40    /// the surplus ids unclaimed and they stay torn down; one that subscribes more
41    /// allocates fresh ids for the extras.
42    pub(crate) reusable_sub_ids: Vec<SubscriptionId>,
43}
44
45impl<'a> BuildContext<'a> {
46    /// The WidgetId of the widget being built.
47    pub fn self_id(&self) -> WidgetId {
48        self.composite_id
49            .expect("self_id() called outside of build()")
50    }
51
52    /// Add a widget to the tree.
53    pub fn add(&mut self, widget: impl crate::widget::Widget + 'static) -> WidgetId {
54        self.tree.add(widget)
55    }
56
57    /// Add a pre-boxed widget to the tree.
58    pub fn add_boxed(&mut self, widget: Box<dyn crate::widget::Widget>) -> WidgetId {
59        self.tree.add_boxed(widget)
60    }
61
62    /// Add a **parentless** widget this one owns: pre-built overlay content
63    /// (a dropdown menu, a date picker's calendar, a tooltip's nested cascade
64    /// children) that must not be reached by the child walk.
65    ///
66    /// Use this — never a bare [`add`](Self::add) — for anything built ahead of
67    /// time and parked with [`set_dormant`](Self::set_dormant) to be shown later
68    /// through an overlay. The two differ only in bookkeeping: `add` hands back
69    /// a node nothing owns, so the builder's own teardown cannot reach it and
70    /// every rebuild strands another copy in the arena; this records the
71    /// ownership edge, so the node dies with its owner and the previous
72    /// generation dies with each rebuild.
73    ///
74    /// Content that *can* be a child should be returned from `build()` as one
75    /// instead. This exists for content that cannot: activation and the paint
76    /// walk both descend through `children`, so a dormant popup parked there
77    /// wakes with its host and paints inline at zero size.
78    pub fn add_detached(&mut self, widget: impl crate::widget::Widget + 'static) -> WidgetId {
79        self.add_detached_boxed(Box::new(widget))
80    }
81
82    /// Insert a child whose subtree is **not built until `reveal` first turns
83    /// true**, and is retained from then on. Returns the host's id immediately.
84    ///
85    /// The shape this replaces is `ctx.add(panel)` followed by
86    /// `ctx.set_dormant(id)` — correct, but it builds content the user may never
87    /// open, on every rebuild of the owner. In a virtualized collection the
88    /// owner is a per-row delegate, so that cost is multiplied by the row count:
89    /// on a 40-row table whose cells each carried a four-item menu, the eager
90    /// form cost 325–552 ms per rebuild against 42–46 ms without the column at
91    /// all, and ~85% of it was the `add` rather than constructing the widget
92    /// value. See [`DeferredSubtree`](crate::deferred_subtree::DeferredSubtree)
93    /// for the full contract.
94    ///
95    /// Pass the same signal the content's `visible_when` gate uses. Everything
96    /// downstream of the returned id — `set_dormant` / `activate`,
97    /// `visible_when`, `OverlayRequest::content`, descendant checks,
98    /// dismissal — is unchanged; only when the subtree below it exists moves.
99    pub fn add_deferred(
100        &mut self,
101        reveal: crate::signal::Signal<bool>,
102        widget: impl crate::widget::Widget + 'static,
103    ) -> WidgetId {
104        self.add_deferred_boxed(reveal, Box::new(widget))
105    }
106
107    /// [`add_deferred`](Self::add_deferred) for an already-boxed widget.
108    pub fn add_deferred_boxed(
109        &mut self,
110        reveal: crate::signal::Signal<bool>,
111        widget: Box<dyn crate::widget::Widget>,
112    ) -> WidgetId {
113        self.add(crate::deferred_subtree::DeferredSubtree::new(
114            Some(reveal),
115            widget,
116        ))
117    }
118
119    /// [`add_deferred`](Self::add_deferred) for content the **framework**
120    /// materializes, kept as a child of the builder.
121    ///
122    /// The parented twin of
123    /// [`add_detached_deferred_on_demand`](Self::add_detached_deferred_on_demand),
124    /// for the two rich-tooltip attach paths: they have always parented their
125    /// body on the anchor's owner, and reparenting them to `detached` would move
126    /// which teardown reaps them. Only *when* the body is built changes.
127    ///
128    /// Worth the separate entry point because a rich tooltip is not one widget:
129    /// `RichTooltipWidget::build` eagerly pre-creates a nested tooltip for every
130    /// `:key` link in its body, recursively, so one attached tip expands into a
131    /// cascade. Built eagerly on a data view's row delegate, 29 rows of
132    /// Skribisto's Overview carried 1,305 tooltip widgets inside a 22,737-node
133    /// subtree, and tearing that down cost 5.3 s per arrow-key press — the
134    /// destroy, not the build.
135    pub fn add_deferred_on_demand(
136        &mut self,
137        widget: impl crate::widget::Widget + 'static,
138    ) -> WidgetId {
139        self.add(crate::deferred_subtree::DeferredSubtree::new(
140            None,
141            Box::new(widget),
142        ))
143    }
144
145    /// [`add_deferred`](Self::add_deferred) for content the **framework**
146    /// materializes rather than a widget's own open signal.
147    ///
148    /// The tooltip case: a tooltip body has no open signal a widget could hand
149    /// over — the tree decides, when a dwell matures. `WidgetTree` forces such
150    /// a host just before it consults `Widget::tooltip_has_content`, so the
151    /// body exists by the time anything asks it a question.
152    pub fn add_detached_deferred_on_demand(
153        &mut self,
154        widget: impl crate::widget::Widget + 'static,
155    ) -> WidgetId {
156        self.add_detached(crate::deferred_subtree::DeferredSubtree::new(
157            None,
158            Box::new(widget),
159        ))
160    }
161
162    /// [`add_deferred`](Self::add_deferred), inserted detached — the shape
163    /// overlay content wants, so it is owned by the builder and dies with it
164    /// rather than outliving every menu the user ever opened.
165    pub fn add_detached_deferred_boxed(
166        &mut self,
167        reveal: crate::signal::Signal<bool>,
168        widget: Box<dyn crate::widget::Widget>,
169    ) -> WidgetId {
170        self.add_detached(crate::deferred_subtree::DeferredSubtree::new(
171            Some(reveal),
172            widget,
173        ))
174    }
175
176    /// [`add_detached_deferred_boxed`](Self::add_detached_deferred_boxed) for an
177    /// unboxed widget.
178    pub fn add_detached_deferred(
179        &mut self,
180        reveal: crate::signal::Signal<bool>,
181        widget: impl crate::widget::Widget + 'static,
182    ) -> WidgetId {
183        self.add_detached_deferred_boxed(reveal, Box::new(widget))
184    }
185
186    /// [`add_detached`](Self::add_detached) for an already-boxed widget.
187    pub fn add_detached_boxed(&mut self, widget: Box<dyn crate::widget::Widget>) -> WidgetId {
188        let id = self.tree.add_boxed(widget);
189        let owner = self.self_id();
190        self.tree.record_detached(owner, id);
191        id
192    }
193
194    /// Add a Level 2 widget as a child of another widget.
195    pub fn add_child(
196        &mut self,
197        parent: WidgetId,
198        widget: impl crate::widget::Widget + 'static,
199    ) -> WidgetId {
200        self.tree.add_child(parent, widget)
201    }
202
203    // --- Signal APIs ---
204
205    /// Create a new mutable signal.
206    pub fn signal<T: 'static>(&mut self, value: T) -> Signal<T> {
207        Signal::new(value)
208    }
209
210    /// Create a new `Signal<f32>` that supports `animate_to()`.
211    /// Registered with the animation scheduler automatically. The owning
212    /// widget (`self_id()`) is recorded so that the scheduler can pause
213    /// the animation when the widget is offscreen, dormant, or rebuilt.
214    pub fn animated_signal(&mut self, value: f32) -> Signal<f32> {
215        let signal = Signal::new_animated(value);
216        let owner = self.self_id();
217        self.tree.register_animated_signal(&signal, owner);
218        signal
219    }
220
221    /// Register a pre-existing `Signal<f32>` for animation support.
222    /// Use this when the signal was created outside of `build()` (e.g. in the
223    /// widget constructor) and needs to be registered with the animation scheduler.
224    pub fn register_animated_signal(&mut self, signal: &Signal<f32>) {
225        let owner = self.self_id();
226        self.tree.register_animated_signal(signal, owner);
227    }
228
229    /// Read the OS-level `prefers-reduced-motion` preference. Widgets
230    /// that use looping or decorative animations (spinners, sprite
231    /// icons, marquee text, etc.) should skip starting them when this
232    /// returns `true` so the UI respects accessibility settings and —
233    /// as a bonus — draws no CPU/GPU.
234    pub fn prefers_reduced_motion(&self) -> bool {
235        self.tree.prefers_reduced_motion()
236    }
237
238    /// Build an [`AnimationSpec`](crate::animation_builder::AnimationSpec)
239    /// — the fluent ergonomic façade over `Signal<f32>::animate_to`.
240    /// Captures the theme's `MotionTokens` and the platform
241    /// reduced-motion preference at build time, returns a clonable
242    /// spec that event-handler closures can drive without
243    /// re-threading durations and easing.
244    ///
245    /// ```ignore
246    /// let knob_anim = ctx.animate().fast().standard();
247    /// handlers = handlers.on_tap(move |_, _| {
248    ///     knob_anim.to_or_snap(&knob_position, target);
249    /// });
250    /// ```
251    pub fn animate(&self) -> crate::animation_builder::AnimationSpec {
252        crate::animation_builder::AnimationSpec::from_motion(
253            self.theme().motion.clone(),
254            self.prefers_reduced_motion(),
255        )
256    }
257
258    /// Attach `trigger` to the widget being built, in this window. Call it in
259    /// every `build()`: a rebuild whose `build()` no longer attaches it
260    /// releases it, as does the widget's destruction, and the tree's drop
261    /// releases the rest. A request pending when the widget is rebuilt is
262    /// still pending after it. Attaching the same trigger to the same widget
263    /// twice is a no-op. See [`crate::RepaintTrigger`].
264    pub fn attach_repaint_trigger(&mut self, trigger: &crate::RepaintTrigger) {
265        let id = self.self_id();
266        self.tree.attach_repaint_trigger(id, trigger);
267    }
268
269    /// Attach `source` to the widget being built, in this window, writing its
270    /// size and status into `signals`. Call it in every `build()`: a rebuild
271    /// detaches it and the new `build()` attaches again, the widget's
272    /// destruction detaches it, and the tree's drop detaches the rest.
273    ///
274    /// The source's current size and status are written into `signals`, and
275    /// recorded for the widget's paint, before this returns: a widget built
276    /// after this frame's layout pre-pass, or attached to a source that will
277    /// never commit again, lays out and draws its picture in its first frame.
278    /// Its pixels reach the renderer without passing through the widget: in
279    /// `paint()` the widget hands the attachment's consumer to
280    /// `Canvas::draw_live_image`. See [`teksilo_canvas::live_image`].
281    pub fn attach_live_image(
282        &mut self,
283        source: &teksilo_canvas::live_image::LiveImageSource,
284        signals: &crate::LiveImageSignals,
285    ) -> crate::LiveImageAttachment {
286        let id = self.self_id();
287        self.tree.attach_live_image(id, source, signals)
288    }
289
290    /// Run `hook` on the UI thread in the frame after a `RepaintTrigger`
291    /// attached to the widget being built requests a pull
292    /// ([`crate::RepaintTrigger::request_pull`]), whether or not the widget
293    /// is shown: it takes in what the producer stored and says what changed.
294    /// A widget that is not shown takes it in without its window drawing a
295    /// frame. Set it in every `build()`; it replaces the one an earlier
296    /// build set, and a pull requested before a rebuild runs the new one.
297    pub fn on_trigger_pull(&mut self, hook: impl FnMut() -> crate::PullOutcome + 'static) {
298        let id = self.self_id();
299        self.tree.set_trigger_pull_hook(id, Box::new(hook));
300    }
301
302    /// Opt into the shader-driven animated-quad pipeline. The widget
303    /// paint() emits ONE `canvas.draw_animated_quad(bounds, handle.slot(),
304    /// class)` call; the renderer samples per-slot state from its
305    /// uniform buffer each frame and the widget's paint() does not
306    /// re-run for animation ticks — only on layout changes. The
307    /// returned handle is stable for the widget-mount lifetime and
308    /// should be stashed on `self` to thread to `paint()`.
309    ///
310    /// For decorative motion that isn't a quad (scroll-offset tweens,
311    /// sidebar slide, toggle knob), keep using `ctx.animated_signal` +
312    /// `signal.animate_looping` — both paths coexist.
313    pub fn animated_quad(
314        &mut self,
315        kind: crate::animated_quad::AnimatedQuadKind,
316    ) -> crate::animated_quad::AnimatedQuadHandle {
317        let owner = self.self_id();
318        self.tree.register_animated_quad(owner, kind)
319    }
320
321    /// The per-frame delta-seconds signal. Observe it via
322    /// `ctx.effect(&ctx.frame_tick(), |delta| ...)` to run code once per
323    /// frame **the tree was explicitly asked to pump**. Merely observing
324    /// this signal does not keep the event loop awake — widgets must
325    /// call [`request_frame`](Self::request_frame) (typically from an
326    /// event handler or from inside the tick closure itself) to schedule
327    /// the next wake-up. This preserves Teksilo's draw-when-needed model.
328    pub fn frame_tick(&self) -> Signal<f32> {
329        self.tree.frame_tick()
330    }
331
332    /// Ask the tree to pump exactly one more frame. See
333    /// [`frame_tick`](Self::frame_tick) for the observer side.
334    pub fn request_frame(&self) {
335        self.tree.request_frame();
336    }
337
338    /// Request that the AccessKit tree be re-walked after this build pass.
339    /// Use when `build()` restructured its subtree in a way that changes the
340    /// accessibility tree (relayout alone no longer re-walks AT). `SceneView`
341    /// calls this each build, since it may have materialised or destroyed
342    /// scene widgets or applied a11y-only scene mutations.
343    pub fn request_accessibility_update(&self) {
344        self.tree.request_accessibility_update();
345    }
346
347    /// Speak `message` to the screen reader, politely.
348    ///
349    /// The build-time companion to
350    /// [`EventContext::announce`](crate::widget::EventContext::announce), for a
351    /// widget that discovers during `build()` that something needs saying — an
352    /// error surface appearing, a result count changing. Announcing from
353    /// `build()` announces once per *rebuild*, so guard it on a real change
354    /// rather than on the build itself.
355    pub fn announce(&mut self, message: impl Into<String>) {
356        self.tree.announce(message);
357    }
358
359    /// Speak `message` to the screen reader at the given urgency. See
360    /// [`EventContext::announce_with`](crate::widget::EventContext::announce_with).
361    pub fn announce_with(
362        &mut self,
363        message: impl Into<String>,
364        politeness: crate::announcer::Politeness,
365    ) {
366        self.tree.announce_with(message, politeness);
367    }
368
369    /// Clone the shared "frame requested" flag. Stash it on widget
370    /// state and call `.set(true)` from inside a frame-tick effect
371    /// closure to chain-request another frame without needing
372    /// mutable access to the tree. Used by widgets with continuous
373    /// frame needs (caret blink, drag auto-scroll, smooth
374    /// animations driven from a tick closure).
375    ///
376    /// **Prefer [`subscribe_frame_tick`](Self::subscribe_frame_tick)**
377    /// for visual-only continuous animations (Pulse, Cycle, …): the
378    /// scheduler-backed path automatically pauses the chain when the
379    /// owner widget is hidden, while this raw handle keeps the event
380    /// loop pumping at full frame rate regardless of visibility.
381    pub fn frame_request_handle(&self) -> std::rc::Rc<std::cell::Cell<bool>> {
382        self.tree.frame_request_handle()
383    }
384
385    /// Subscribe the widget being built to the per-frame-effect
386    /// scheduler. The returned RAII guard removes the subscription on
387    /// drop — store it on `self` so its lifetime tracks the widget's.
388    ///
389    /// While at least one subscriber's owner is visible, the framework
390    /// re-arms the frame tick after every render. When all
391    /// subscribers are hidden (e.g. parked inside a non-selected
392    /// `Switcher` branch), no re-arm happens and the chain dies, so
393    /// the event loop sleeps. On a hidden→visible transition the
394    /// `visible_when` binding's relayout dirty triggers a repaint that
395    /// paints the subscriber, which the post-render arm then detects
396    /// and resumes the chain.
397    ///
398    /// Replaces the widget-managed `frame_request.set(true)` re-arm
399    /// pattern for visual-only continuous animations. The widget's
400    /// `frame_tick` effect closure no longer needs to call
401    /// `frame_request.set(true)` itself — the scheduler handles it.
402    pub fn subscribe_frame_tick(&self) -> crate::frame_tick_scheduler::FrameTickSubscription {
403        let sub = self.tree.subscribe_frame_tick(self.self_id());
404        // Bootstrap: ensure at least one frame runs after registration
405        // so the first paint happens. The post-render re-arm takes over
406        // from there. This is also the resume nudge for the case where
407        // a widget rebuilds (e.g. due to a state change) while still
408        // hidden — the parent's relayout dirty will trigger paint, and
409        // post-render arm will pick up the chain.
410        self.tree.request_frame();
411        sub
412    }
413
414    /// Like [`subscribe_frame_tick`](Self::subscribe_frame_tick), but the
415    /// widget only needs to wake **at most once per `interval`** while
416    /// visible. Same visibility gate and RAII guard; between wakes the
417    /// event loop sleeps to the interval deadline rather than rendering
418    /// identical 60 fps frames. Use when the widget's visible output
419    /// changes far less often than 60 Hz — e.g. `Cycle`'s once-per-period
420    /// index advance, or a seconds-granular clock.
421    ///
422    /// The interval delays this widget's own wake and nothing else: a frame
423    /// anything else requests still runs at 60 Hz, and the `frame_tick`
424    /// effect fires on it too. Read the clock in the effect rather than
425    /// counting ticks.
426    pub fn subscribe_frame_tick_throttled(
427        &self,
428        interval: std::time::Duration,
429    ) -> crate::frame_tick_scheduler::FrameTickSubscription {
430        let sub = self
431            .tree
432            .subscribe_frame_tick_throttled(self.self_id(), interval);
433        // Bootstrap the first frame after registration (see
434        // `subscribe_frame_tick`).
435        self.tree.request_frame();
436        sub
437    }
438
439    /// Clone the shared wake-at deadline cell. Stash it on widget
440    /// state and set `Some(instant)` from a frame-tick effect to
441    /// schedule a one-shot deadline wake-up without keeping the event
442    /// loop in `Poll` mode. See `WidgetTree::wake_at_handle` for
443    /// the underlying mechanism.
444    pub fn wake_at_handle(&self) -> std::rc::Rc<std::cell::Cell<Option<std::time::Instant>>> {
445        self.tree.wake_at_handle()
446    }
447
448    /// Register a scoped effect tied to this build cycle.
449    /// The effect fires whenever the signal changes. It is automatically
450    /// cleaned up on rebuild or widget destruction.
451    pub fn effect<T: Clone + 'static>(&mut self, signal: &Signal<T>, f: impl Fn(&T) + 'static) {
452        let handle = signal.observe(f);
453        self.effect_handles.push(handle);
454    }
455
456    /// Register a pre-existing observer handle for lifecycle management.
457    /// The handle will be dropped (and the observer removed) on rebuild
458    /// or widget destruction.
459    pub fn own_handle(&mut self, handle: ObserverHandle) {
460        self.effect_handles.push(handle);
461    }
462
463    /// Get the binding registry.
464    pub fn binding_registry(&self) -> &BindingRegistry {
465        self.tree.binding_registry()
466    }
467
468    /// Get the current theme.
469    pub fn theme(&self) -> &crate::styles::Theme {
470        self.tree.theme()
471    }
472
473    /// The active [`TargetDensity`] — the ladder `ctx.theme().input` was
474    /// projected onto.
475    ///
476    /// Read it in `build()` when a dimension must be chosen once per build
477    /// (a `MinSize` wrapper, a recipe's metrics). For the values themselves
478    /// prefer `ctx.theme().input` plus the
479    /// [`density`](crate::styles::density) helpers, which encode the floors.
480    ///
481    /// [`TargetDensity`]: teksilo_tokens::TargetDensity
482    pub fn density(&self) -> teksilo_tokens::TargetDensity {
483        self.tree.input_density()
484    }
485
486    /// Reactive handle on the current theme. Fires observers when
487    /// `tree.set_theme(...)` is called. Build implementations that want
488    /// theme-driven values to update without a rebuild should use this
489    /// instead of cloning tokens from `self.theme()` — for example,
490    /// `ctx.theme_signal().map(|t| t.colors.accent)` or combining with
491    /// interaction state via `zip(...)`.
492    pub fn theme_signal(&self) -> crate::signal::Signal<crate::styles::Theme> {
493        self.tree.theme_signal().clone()
494    }
495
496    /// Current combined text-scale factor (`user × OS`, `1.0` = 100 %). One-shot
497    /// read for build-time sizing; for a value that updates without a rebuild,
498    /// bind [`text_scale_signal`](Self::text_scale_signal) instead.
499    pub fn text_scale(&self) -> f32 {
500        self.tree.effective_text_scale()
501    }
502
503    /// Reactive handle on the combined text-scale factor. Fires when the user
504    /// scale, theme, or OS text-scale preference changes. Build implementations
505    /// that derive a build-time dimension from the scale (e.g. `Calendar`'s
506    /// fixed cell sizes) bind this — typically at `Rebuild` level so the change
507    /// recomputes the constants — since a scale change relayouts but does not
508    /// rebuild on its own.
509    pub fn text_scale_signal(&self) -> crate::signal::Signal<f32> {
510        self.tree.text_scale_signal()
511    }
512
513    /// The window's device scale (physical px per logical px), as a signal.
514    /// Bind it at `Relayout` when `layout_response` or `place_children` reads
515    /// `LayoutContext::scale_factor`: a scale change with an unchanged logical
516    /// size relayouts nothing on its own.
517    pub fn device_scale_signal(&self) -> crate::signal::Signal<f32> {
518        self.tree.device_scale_signal()
519    }
520
521    /// Whether the host window is currently active (`focused AND not
522    /// occluded`). One-shot read for build-time use; for a value that reacts
523    /// to focus changes, bind [`window_active_signal`](Self::window_active_signal).
524    pub fn window_active(&self) -> bool {
525        self.tree.is_window_active()
526    }
527
528    /// Reactive handle on window-active state. Fires when the host window gains
529    /// or loses active status (`focused AND not occluded`). Build
530    /// implementations that show/hide appearance with window focus — caret
531    /// effects, the selection-colour swap in text fields, `DimWhenInactive` —
532    /// bind this, typically at `RepaintOnly` level (an active-state flip never
533    /// affects geometry). Starts `true`.
534    pub fn window_active_signal(&self) -> crate::signal::Signal<bool> {
535        self.tree.window_active_signal()
536    }
537
538    /// Whether the OS "increase contrast" preference is set. One-shot read for
539    /// build-time use; for a value that follows the preference, bind
540    /// [`high_contrast_signal`](Self::high_contrast_signal).
541    pub fn prefers_high_contrast(&self) -> bool {
542        self.tree.prefers_high_contrast()
543    }
544
545    /// Reactive handle on the high-contrast preference, the companion of
546    /// [`window_active_signal`](Self::window_active_signal). A recipe that
547    /// bakes a colour at build time zips both into a
548    /// [`PaintState`](crate::styles::PaintState) and resolves against
549    /// [`Theme::for_paint_state`](crate::styles::Theme::for_paint_state), so
550    /// it paints what the paint walker paints. Starts `false`.
551    pub fn high_contrast_signal(&self) -> crate::signal::Signal<bool> {
552        self.tree.high_contrast_signal()
553    }
554
555    /// Whether the operating system says a screen reader is running. One-shot
556    /// read for build-time use; for a value that follows it, bind
557    /// [`screen_reader_signal`](Self::screen_reader_signal).
558    pub fn screen_reader_state(&self) -> crate::environment::ScreenReaderState {
559        self.tree.screen_reader_state()
560    }
561
562    /// Reactive handle on the OS screen-reader state, the companion of
563    /// [`high_contrast_signal`](Self::high_contrast_signal). Starts
564    /// [`Unknown`](crate::environment::ScreenReaderState::Unknown).
565    ///
566    /// It is the OS flag, not AccessKit attachment. Only `Active` means a
567    /// reader is present; `Unknown` and `Inactive` both mean the framework
568    /// does not treat one as present, so never remove or disable anything
569    /// unless the state is `Active`. A new window starts with the window
570    /// manager's last reading; it is taken again when a window takes the
571    /// focus, and a detaching AccessKit client forces `Inactive`, which holds
572    /// until the OS flag itself changes. The value is per window tree (two
573    /// windows can briefly disagree). Read it, do not write it, and compare
574    /// against `teksilo_core::ScreenReaderState`
575    /// (`teksilo::core::ScreenReaderState` from the umbrella crate; it is not
576    /// in the prelude).
577    ///
578    /// Nothing is marked dirty when it changes: bind at the level you need
579    /// (`Rebuild` to swap widgets, `AccessibilityOnly` for an accessibility
580    /// property, `RepaintOnly` for colour). [`effect`](Self::effect) does not
581    /// replay the current value: read
582    /// [`screen_reader_state`](Self::screen_reader_state) once in `build()`,
583    /// then subscribe.
584    ///
585    /// Use it for additive presentation (longer status wording, a one-time
586    /// offer the user can refuse), not as a switch for a second, reduced UI,
587    /// not as a gate on semantics, focus or the AccessKit tree, and not in
588    /// place of [`prefers_reduced_motion`](Self::prefers_reduced_motion).
589    /// Many readers never set the OS flag.
590    pub fn screen_reader_signal(
591        &self,
592    ) -> crate::signal::Signal<crate::environment::ScreenReaderState> {
593        self.tree.screen_reader_signal()
594    }
595
596    /// The [`PaintState`](crate::styles::PaintState) the host window paints
597    /// under right now. One-shot read; see
598    /// [`high_contrast_signal`](Self::high_contrast_signal) for the reactive
599    /// form.
600    pub fn paint_state(&self) -> crate::styles::PaintState {
601        self.tree.paint_state()
602    }
603
604    /// Reactive handle on the current locale. Fires observers when
605    /// `tree.set_locale(...)` is called.
606    pub fn locale_signal(&self) -> crate::signal::Signal<Option<String>> {
607        self.tree.locale_signal().clone()
608    }
609
610    /// The [`WindowState`](crate::window::WindowState) for the window
611    /// hosting this tree. `None` only for trees built outside of an
612    /// app (tests, headless scenarios). Use this to bind widgets to
613    /// window-level signals like `placement`, `size`, `focused`.
614    pub fn window(&self) -> Option<&crate::window::WindowState> {
615        self.tree.window_state()
616    }
617
618    /// Retrieve an application-scoped value of type `T` registered via
619    /// `TeksiloAppBuilder::app_state`. Returns `None` if no value of
620    /// that type was registered. The returned reference borrows from
621    /// the framework for the duration of the build pass.
622    pub fn app_state<T: 'static>(&self) -> Option<&T> {
623        self.tree.app_context().app_state::<T>()
624    }
625
626    /// Borrow the [`AppEventPoster`](crate::AppEventPoster) installed by the
627    /// framework, if any. Mirrors [`EventContext::poster`](crate::widget::EventContext::poster).
628    /// Used by integrations that wire a platform callback (e.g. a native menu
629    /// item) to post a typed payload back to the UI loop. Returns `None` for
630    /// trees built outside an app (tests / headless).
631    pub fn poster(&self) -> Option<&std::sync::Arc<dyn crate::AppEventPoster>> {
632        self.tree.app_context().poster()
633    }
634
635    /// Bind a widget's visibility to a boolean prop.
636    pub fn visible_when(&mut self, id: WidgetId, state: impl Into<crate::signal::Prop<bool>>) {
637        self.tree.visible_when(id, state);
638    }
639
640    /// Enqueue a one-shot action to run shortly after this build, with a real
641    /// [`EventContext`](crate::widget::EventContext) — the only place a widget
642    /// can read the OS parent window handle (`ctx.parent_window_handle()`),
643    /// `app_state`, and `poster` *together*, after it is mounted under its
644    /// window. The action runs at most once per enqueue (the app loop drains
645    /// the queue each iteration); a widget that rebuilds must guard against
646    /// enqueuing twice. Built for widgets owning a native OS resource that
647    /// needs a window handle to initialise (a `WebView`'s engine subview);
648    /// ordinary widgets never need it.
649    pub fn run_after_mount(&mut self, f: impl FnOnce(&mut crate::widget::EventContext) + 'static) {
650        self.tree.queue_mount_action(Box::new(f));
651    }
652
653    /// Observe a node's framework activation as a `Signal<bool>` — `true`
654    /// while active, `false` while parked dormant by a `Switcher` /
655    /// `visible_when` gate. Initialised to the node's current state and
656    /// updated only on an actual Active↔Dormant transition.
657    ///
658    /// Ordinary widgets never need this: dormant subtrees are simply not
659    /// painted, so they vanish for free. It exists for the one case where
660    /// "not painted" ≠ "hidden" — a widget owning a native OS resource
661    /// that renders *outside* the wgpu pass (a `WebView`'s engine subview).
662    /// Such a widget does `ctx.effect(&ctx.activation_signal(id), move |a|
663    /// handle.set_visible(*a))` to hide/show the native surface in lockstep.
664    pub fn activation_signal(&mut self, id: WidgetId) -> Signal<bool> {
665        self.tree.activation_signal(id)
666    }
667
668    /// The framework press signal for the widget being built — `true` while it
669    /// holds a pointer press whose visual should show.
670    ///
671    /// The router owns the state, so this is correct for the four cases a
672    /// widget's own `PointerDown`/`PointerUp` bookkeeping gets wrong: a press
673    /// that slides off its target goes `false` and comes back `true` on
674    /// re-entry (WCAG 2.2 SC 2.5.2), a press a pan claimant or an ancestor drag
675    /// wins goes `false` with no release to hang it on, a cancel clears it, and
676    /// a press inside a scrollable withholds the visual for the profile's
677    /// `press_feedback_delay` so a finger that turns out to be scrolling never
678    /// flashes a highlight. The rules are written out in `docs/touch-and-pen.md`
679    /// §7.
680    ///
681    /// Bind it at [`BindingLevel::RepaintOnly`](crate::binding::BindingLevel) —
682    /// a press changes colour, never size.
683    pub fn pressed_signal(&mut self) -> Signal<bool> {
684        let id = self.self_id();
685        self.tree.pressed_signal(id)
686    }
687
688    /// Reactive `Signal<bool>` that is `true` while the *focus scope* containing
689    /// the widget being built — its nearest focusable ancestor, e.g. the
690    /// enclosing `ListView` / `TreeView` — holds keyboard focus. Items outside
691    /// any focusable scope read a constant `true`.
692    ///
693    /// Drives **focus-aware selection**: a selected row renders with the active
694    /// `Selected` chrome while its view has focus and the muted
695    /// `SelectedInactive` chrome when focus moves elsewhere — the standard
696    /// desktop affordance (Qt `SH_ItemView_...`, macOS inactive selection) that
697    /// shows where the keyboard is. The scope is resolved at build time but the
698    /// signal stays live across focus changes.
699    pub fn view_focus_active(&mut self) -> Signal<bool> {
700        // Prefer the scope a containing data view explicitly established for its
701        // rows (deterministic, parenting-independent); else resolve by walking
702        // to the nearest focusable ancestor.
703        if let Some(scope) = self.tree.current_view_focus() {
704            return scope;
705        }
706        let id = self.self_id();
707        self.tree.view_focus_active_for(id)
708    }
709
710    /// Mark the widget being built as a **focus scope** for the rows/items it
711    /// builds next: any descendant's [`view_focus_active`](Self::view_focus_active)
712    /// (and `StandardItem`'s focus-aware selection / focus ring) reads *this*
713    /// widget's keyboard focus. A data view calls this around its row loop, then
714    /// [`end_view_focus`](Self::end_view_focus). Deterministic — unaffected by
715    /// arena parenting, which may not be wired while docked/virtualized rows build.
716    pub fn begin_view_focus(&mut self) -> Signal<bool> {
717        let id = self.self_id();
718        self.tree.begin_view_focus(id)
719    }
720
721    /// Like [`begin_view_focus`](Self::begin_view_focus) but keys the scope on
722    /// an explicit `node_id` rather than the widget being built. A view whose
723    /// rows are built by a **separate body-pane widget** (ListView / TreeView /
724    /// TableView / TreeTableView / GridView) passes its own focusable root id so
725    /// descendant
726    /// items resolve the *root's* keyboard focus — not the pane's, which is a
727    /// child of the root and so never holds focus itself.
728    pub fn begin_view_focus_for(&mut self, node_id: WidgetId) -> Signal<bool> {
729        self.tree.begin_view_focus(node_id)
730    }
731
732    /// End the focus scope opened by [`begin_view_focus`](Self::begin_view_focus).
733    pub fn end_view_focus(&mut self) {
734        self.tree.end_view_focus();
735    }
736
737    /// Input-modality "focus-visible" signal — `true` after keyboard input,
738    /// `false` after pointer input (the standard `:focus-visible` rule). Pair
739    /// with [`view_focus_active`](Self::view_focus_active) to draw a focus
740    /// ring only during keyboard navigation, not on mouse clicks.
741    pub fn focus_visible(&self) -> Signal<bool> {
742        self.tree.focus_visible_signal()
743    }
744
745    /// Bind an opacity multiplier (0..1) to a widget. The render walker
746    /// emits `SetOpacity(value)` before painting the widget's subtree
747    /// and `RestoreOpacity` afterwards, so the multiplier composes
748    /// correctly with ancestor opacity scopes. Bound at `RepaintOnly`:
749    /// opacity changes never trigger relayout. Used by the `Fade`
750    /// wrapper to animate a child between hidden and fully visible.
751    pub fn set_opacity(&mut self, id: WidgetId, opacity: impl Into<crate::signal::Prop<f32>>) {
752        self.tree.set_opacity(id, opacity);
753    }
754
755    /// Bind a 2D affine transform to a widget. The render walker emits
756    /// `PushTransform(value)` before painting the widget's subtree and
757    /// `PopTransform` afterwards, so the transform composes onto the
758    /// renderer's stack with any ancestor transform scopes and with
759    /// the widget's own canvas-level transforms. Bound at `RepaintOnly`:
760    /// visual-only transforms never trigger relayout. Used by `Scale`
761    /// and `Rotate`; reflow-driving wrappers (e.g. `Scale::reflow(true)`)
762    /// must additionally bind their driver signal to themselves at
763    /// `Relayout` to make layout track the value.
764    pub fn set_transform(
765        &mut self,
766        id: WidgetId,
767        transform: impl Into<crate::signal::Prop<teksilo_canvas::Transform2D>>,
768    ) {
769        self.tree.set_transform(id, transform);
770    }
771
772    /// Bind a 2D affine **content** transform to a widget — the transform
773    /// positions the widget's content within its fixed parent-space viewport
774    /// (its bounds) rather than transforming the widget itself. Renders the
775    /// same `PushTransform` / `PopTransform` scope as
776    /// [`set_transform`](Self::set_transform), but hit-testing treats the
777    /// bounds as a fixed viewport so the whole visible area stays interactive
778    /// at any pan / zoom. Used by `SceneView` for its pan/zoom view transform.
779    pub fn set_content_transform(
780        &mut self,
781        id: WidgetId,
782        transform: impl Into<crate::signal::Prop<teksilo_canvas::Transform2D>>,
783    ) {
784        self.tree.set_content_transform(id, transform);
785    }
786
787    /// Bind a Gaussian-equivalent blur radius to a widget. The render
788    /// walker emits `BeginBlurredSubtree { bounds, radius }` before
789    /// painting the widget's subtree and `EndBlurredSubtree` afterwards;
790    /// the renderer redirects drawing into an intermediate texture, runs
791    /// a dual-Kawase blur chain at the requested radius, and composites
792    /// the blurred result back into the parent pass at the widget's
793    /// bounds. Bound at `RepaintOnly`: blur radius changes never trigger
794    /// relayout. Sub-perceptual radii (< 0.5) skip the Begin/End pair
795    /// entirely so animated enable/disable patterns have zero per-frame
796    /// cost when fully off. Used by the `Blur` wrapper.
797    pub fn set_blur(&mut self, id: WidgetId, radius: impl Into<crate::signal::Prop<f32>>) {
798        self.tree.set_blur(id, radius);
799    }
800
801    /// Bind a widget's enabled state to a boolean prop.
802    pub fn enabled_when(&mut self, id: WidgetId, state: impl Into<crate::signal::Prop<bool>>) {
803        self.tree.enabled_when(id, state);
804    }
805
806    /// Reactive view of "is this widget effectively enabled?" — the AND
807    /// of the widget's own `enabled_state` and every ancestor's. The
808    /// arena's [`crate::arena::WidgetArena::is_enabled`] is the
809    /// non-reactive equivalent; this method gives composite widgets a
810    /// `Signal<bool>` they can `.map(...)` / `.zip(...)` against to
811    /// derive other reactive UI state (cursor, custom paint, helper
812    /// signals).
813    ///
814    /// Leaves like `IconWidget` / `TextWidget` / `RectWidget` do NOT
815    /// need this — they get the bool directly via
816    /// [`crate::widget::PaintContext::effective_enabled`] at paint time.
817    /// This method is for composites that need the value at build time
818    /// or want to chain signals.
819    ///
820    /// The signal is node-resident and framework-refreshed (install-or-reuse,
821    /// like [`Self::activation_signal`]), so it tracks ancestors correctly even
822    /// though a widget's parent is not yet wired while its own `build()` runs.
823    /// It is a *mutable* signal, so — unlike the old derived implementation —
824    /// it can be passed to [`Self::effect`].
825    ///
826    /// Returns a signal reading `true` for any node whose entire ancestor
827    /// chain (including itself) has no `enabled_state` bound.
828    pub fn effective_enabled_signal(&mut self, id: WidgetId) -> Signal<bool> {
829        self.tree.effective_enabled_signal(id)
830    }
831
832    /// Whether an intent named `name`, sent from the window's logical focus,
833    /// would be handled, as a signal the tree keeps current while the widget
834    /// being built is active: what a menu row binds to grey itself out, and
835    /// what a toolbar button or any other control bound to a command uses to
836    /// grey itself the same way. "Handled" is what a dispatch would find: an
837    /// enabled action on the route from the focus (the window's own, then
838    /// the application's) that has something to act on
839    /// ([`ActionBuilder::available_when`](crate::action::ActionBuilder::available_when)).
840    pub fn command_available(&mut self, name: &'static str) -> Signal<bool> {
841        let id = self.self_id();
842        self.tree.command_available_signal(id, name)
843    }
844
845    /// Bind a widget's Tab-key participation to a boolean prop.
846    /// When false, the widget is removed
847    /// from Tab / Shift+Tab traversal but remains reachable via
848    /// `request_focus` and arrow-key navigation. Implements the ARIA
849    /// roving-tabindex pattern (HTML `tabindex="-1"` semantics).
850    pub fn set_tab_stop(&mut self, id: WidgetId, state: impl Into<crate::signal::Prop<bool>>) {
851        self.tree.set_tab_stop(id, state);
852    }
853
854    /// Gate Tab traversal over the whole subtree of `id`. See
855    /// [`WidgetTree::set_tab_gate`](crate::widget_tree::WidgetTree::set_tab_gate).
856    #[doc(hidden)]
857    pub fn set_tab_gate(&mut self, id: WidgetId, gate: impl Into<crate::signal::Prop<bool>>) {
858        self.tree.set_tab_gate(id, gate);
859    }
860
861    /// Publish what a data view's `Space` should do when the row containing
862    /// `id` holds the keyboard cursor.
863    ///
864    /// A `ListView` / `TreeView` row is not itself focusable and the view keeps
865    /// the row subtree out of the Tab order — a listbox is one Tab stop — so a
866    /// checkbox inside a row has no keyboard route of its own.
867    /// `StandardListItem` / `StandardTreeItem` publish one for the checkbox
868    /// they embed; a hand-written row delegate calls this to get the same
869    /// behaviour. Without it `Space` keeps meaning "toggle the selection".
870    pub fn set_keyboard_toggle(
871        &mut self,
872        id: WidgetId,
873        f: std::rc::Rc<dyn Fn(&mut crate::widget::EventContext)>,
874    ) {
875        self.tree.set_keyboard_toggle(id, f);
876    }
877
878    /// Declare the widget being built as a **traversal-scope boundary** for
879    /// Tab / Shift+Tab navigation. Descendants' `tab_index` values become
880    /// scoped to this node — they never collide with sibling scopes — and the
881    /// `policy` controls what happens at the scope's ends:
882    ///
883    /// - [`TraversalScopePolicy::Continue`](crate::focus::TraversalScopePolicy::Continue)
884    ///   — Tab flows out into the enclosing scope's next member (groups
885    ///   numbering only).
886    /// - [`TraversalScopePolicy::Cycle`](crate::focus::TraversalScopePolicy::Cycle):
887    ///   Tab wraps within the scope, never exits. For **modal dialogs**, and
888    ///   for an embedded edit scope that advertises its exit key (a list row
889    ///   in action mode, left with Escape). Not for a popover or menu: those
890    ///   are non-modal, and the framework closes one the keyboard walks out of
891    ///   rather than containing focus in it. Trapping such an overlay stops
892    ///   that dismissal from ever firing.
893    ///
894    /// This node is automatically excluded from being a Tab stop itself.
895    /// Prefer the `FocusScope` wrapper widget in `teksilo-widgets` over
896    /// calling this directly.
897    pub fn set_traversal_scope(&mut self, policy: crate::focus::TraversalScopePolicy) {
898        let id = self.self_id();
899        self.tree.set_traversal_scope(id, policy);
900    }
901
902    /// The widget being built receives the release of every key whose press
903    /// it received, even once a modal has opened over it (a key release is
904    /// otherwise dropped then). For a widget that forwards keys to something
905    /// that tracks held keys, a web view's engine, which would keep a key
906    /// held after a page answered its press with a dialog.
907    #[doc(hidden)]
908    pub fn receive_every_key_release(&mut self) {
909        let id = self.self_id();
910        if let Some(node) = self.tree.arena.get_mut(id) {
911            node.receives_every_key_release = true;
912        }
913    }
914
915    /// The widget being built judges a
916    /// [`ScrollIntoView`](crate::event::WidgetEvent::ScrollIntoView) against
917    /// its **live** viewport (its offset now, or where a running glide is
918    /// heading), moves only as far as the target needs and answers `Ignored`
919    /// when it does not move. The reveal walk then always asks it, rather
920    /// than first testing the target against bounds from the last layout,
921    /// which a scroll since (a wheel notch, an offset the application wrote, a
922    /// glide, an earlier reveal of the same turn) has made stale. A container
923    /// not so marked keeps that test. `ScrollArea` and `GridView` set it; a
924    /// `SceneView` does not need it, since the walk tests its target through
925    /// the live camera transform.
926    ///
927    /// Plumbing, hidden pending the maintainer's decision on the public API
928    /// for it.
929    #[doc(hidden)]
930    pub fn judge_reveals_live(&mut self) {
931        let id = self.self_id();
932        if let Some(node) = self.tree.arena.get_mut(id) {
933            node.judges_reveals_live = true;
934        }
935    }
936
937    /// Attach a tooltip to a widget.
938    pub fn attach_tooltip(
939        &mut self,
940        anchor_id: WidgetId,
941        content_id: WidgetId,
942        delay: std::time::Duration,
943    ) {
944        self.tree.attach_tooltip(anchor_id, content_id, delay);
945        self.claim_tooltip_description(anchor_id);
946    }
947
948    /// Attach a tooltip with an explicit
949    /// [`TooltipPlacement`](crate::overlay::TooltipPlacement) — use `Side`
950    /// for anchors stacked vertically (menu items, a vertical tab strip,
951    /// list/tree rows) so the tooltip opens beside the anchor instead of
952    /// covering the next sibling.
953    pub fn attach_tooltip_with_placement(
954        &mut self,
955        anchor_id: WidgetId,
956        content_id: WidgetId,
957        delay: std::time::Duration,
958        placement: crate::overlay::TooltipPlacement,
959    ) {
960        self.tree
961            .attach_tooltip_with_placement(anchor_id, content_id, delay, placement);
962        self.claim_tooltip_description(anchor_id);
963    }
964
965    /// Attach a tooltip that auto-promotes to sticky after a dwell
966    /// timer. Non-None `sticky_after` enables the sticky-on-dwell UX:
967    /// once the tooltip has been shown for `sticky_after`, the tree
968    /// flags the entry sticky and swaps the overlay's dismiss
969    /// behavior to `EscapeOrClickOutside`.
970    pub fn attach_tooltip_with_sticky(
971        &mut self,
972        anchor_id: WidgetId,
973        content_id: WidgetId,
974        delay: std::time::Duration,
975        sticky_after: Option<std::time::Duration>,
976    ) {
977        self.tree
978            .attach_tooltip_with_sticky(anchor_id, content_id, delay, sticky_after);
979        self.claim_tooltip_description(anchor_id);
980    }
981
982    /// Variant of [`attach_tooltip_with_sticky`](Self::attach_tooltip_with_sticky)
983    /// that takes a shared `Rc<Cell<Option<Instant>>>` "sink" the
984    /// tree updates whenever the tooltip is shown / dismissed. The
985    /// tooltip widget reads from this sink to compute its own dwell
986    /// progress reliably, without needing a paint-gap heuristic.
987    pub fn attach_tooltip_with_sticky_sink(
988        &mut self,
989        anchor_id: WidgetId,
990        content_id: WidgetId,
991        delay: std::time::Duration,
992        sticky_after: Option<std::time::Duration>,
993        shown_at_sink: std::rc::Rc<std::cell::Cell<Option<std::time::Instant>>>,
994    ) {
995        self.tree.attach_tooltip_with_sticky_sink(
996            anchor_id,
997            content_id,
998            delay,
999            sticky_after,
1000            shown_at_sink,
1001        );
1002        self.claim_tooltip_description(anchor_id);
1003    }
1004
1005    /// Variant of [`attach_tooltip_with_sticky_sink`](Self::attach_tooltip_with_sticky_sink)
1006    /// that also carries a [`TooltipPlacement`](crate::overlay::TooltipPlacement).
1007    /// The full-featured path used by rich + composite tooltips that want
1008    /// `Side` placement in a vertical context (menu items, list/tree rows).
1009    pub fn attach_tooltip_with_sticky_sink_placement(
1010        &mut self,
1011        anchor_id: WidgetId,
1012        content_id: WidgetId,
1013        delay: std::time::Duration,
1014        sticky_after: Option<std::time::Duration>,
1015        shown_at_sink: std::rc::Rc<std::cell::Cell<Option<std::time::Instant>>>,
1016        placement: crate::overlay::TooltipPlacement,
1017    ) {
1018        self.tree.attach_tooltip_with_sticky_sink_placement(
1019            anchor_id,
1020            content_id,
1021            delay,
1022            sticky_after,
1023            shown_at_sink,
1024            placement,
1025        );
1026        self.claim_tooltip_description(anchor_id);
1027    }
1028
1029    /// Name this widget as the one the tooltip just attached describes.
1030    ///
1031    /// Every `attach_tooltip*` wrapper ends with this, so a composing control
1032    /// gets it for free: `Button`, `Toggle` and the two dozen widgets shaped
1033    /// like them hang the overlay off an inner chrome node -- the thing with
1034    /// the right bounds to open against -- while their role, their name and
1035    /// their focusability sit on their own outer node, which is the node an
1036    /// assistive technology lands on and therefore the node a description has
1037    /// to be on.
1038    ///
1039    /// A widget anchoring its tooltip on itself claims itself, which is what
1040    /// it already had. A widget attaching *many* tooltips in one build -- a
1041    /// list body pane, one per visible row -- claims itself for every one of
1042    /// them, which is a claim that cannot be granted; the accessibility walk
1043    /// is where that is noticed, because it is the only place the whole set
1044    /// is visible at once.
1045    fn claim_tooltip_description(&mut self, anchor_id: WidgetId) {
1046        let owner = self.self_id();
1047        self.tree.set_tooltip_description_owner(anchor_id, owner);
1048    }
1049
1050    /// Promote a shown tooltip to "sticky": removes its auto-dismiss
1051    /// on pointer-leave and swaps the overlay's dismiss behavior to
1052    /// `EscapeOrClickOutside`. Used by rich tooltips that implement a
1053    /// dwell timer.
1054    pub fn promote_tooltip_to_sticky(&mut self, content_id: WidgetId) {
1055        self.tree.promote_tooltip_to_sticky(content_id);
1056    }
1057
1058    /// Set a widget as dormant (inactive). Used to pre-create overlay content
1059    /// that will be activated later via `EventContext::activate()`.
1060    pub fn set_dormant(&mut self, id: WidgetId) {
1061        self.tree.set_dormant(id);
1062    }
1063
1064    /// Destroy a widget and its entire subtree, removing them from the
1065    /// arena and dropping any per-widget subscription / effect handles.
1066    ///
1067    /// Use this to clean up dormant subtrees that the current widget
1068    /// created during a prior build and that live outside its regular
1069    /// arena children — e.g., a pre-built popup panel inserted via
1070    /// `ctx.add(..)` + `ctx.set_dormant(..)` that becomes stale after a
1071    /// rebuild. Regular arena children of the composite (i.e. widgets
1072    /// whose ids are returned from `build`) are destroyed automatically
1073    /// by the framework's rebuild path and do not need this call.
1074    ///
1075    /// If an overlay currently references `id` as its content, the
1076    /// overlay is dismissed first so the manager does not retain a
1077    /// stale content reference.
1078    pub fn destroy_subtree(&mut self, id: WidgetId) {
1079        let overlay_id = self.tree.overlay_manager().find_by_content(id);
1080        if let Some(overlay_id) = overlay_id {
1081            self.tree.dismiss_overlay(overlay_id);
1082        }
1083        self.tree.destroy_subtree(id);
1084    }
1085
1086    /// Apply a `HandlerSet` to the composite widget being built (self).
1087    /// This transfers attached event handlers, focusable flag, cursor, etc.
1088    /// to the widget's arena node, replacing `event()` and `is_focusable()` overrides.
1089    pub fn apply_self_handlers(&mut self, handler_set: crate::widget_builder::HandlerSet) {
1090        let id = self.self_id();
1091        self.tree.apply_self_handler_set(id, handler_set);
1092    }
1093
1094    /// Move keyboard focus to `id`. Mirrors
1095    /// `EventContext::request_focus` for use during `build()` — e.g.
1096    /// when a composing widget pre-builds an editor and needs focus to
1097    /// land on it as soon as the subtree is wired in.
1098    ///
1099    /// A dormant `id` is focused once the next layout pass wakes it, and an
1100    /// `id` that cannot take focus (a composite whose field is inside it) hands
1101    /// focus to its first Tab stop, both as for
1102    /// [`WidgetTree::focus`](crate::widget_tree::WidgetTree::focus).
1103    pub fn focus(&mut self, id: WidgetId) {
1104        self.tree.focus(id);
1105    }
1106
1107    /// The window's focused widget, as a signal: any widget, not the one
1108    /// being built (`None` while nothing in the window has focus). The
1109    /// build-time door to
1110    /// [`WidgetTree::focused_signal`](crate::widget_tree::WidgetTree::focused_signal).
1111    ///
1112    /// For a container that remembers where focus was inside it, to put it
1113    /// back there later: observe it with [`effect`](Self::effect), and check
1114    /// a remembered id with [`is_descendant_of`](Self::is_descendant_of) before
1115    /// using it, since the widget may have been destroyed since.
1116    pub fn focused_signal(&self) -> Signal<Option<WidgetId>> {
1117        self.tree.focused_signal()
1118    }
1119
1120    /// Whether `id` is `ancestor` or one of its descendants. `false` for an id
1121    /// that is no longer in the tree (unless it is `ancestor` itself).
1122    ///
1123    /// A node added during the current `build()` is not linked to the widget
1124    /// being built until that build returns, so ask about the subtree below an
1125    /// id, which is complete, rather than about the chain above it.
1126    pub fn is_descendant_of(&self, id: WidgetId, ancestor: WidgetId) -> bool {
1127        self.tree.is_descendant_of(id, ancestor)
1128    }
1129
1130    /// Find the first focusable widget within the subtree rooted at
1131    /// `root` in depth-first order. Returns `None` when the subtree has
1132    /// no focusable descendant or `root` is not in the arena.
1133    pub fn first_focusable_descendant(&self, root: WidgetId) -> Option<WidgetId> {
1134        self.tree.first_focusable_descendant(root)
1135    }
1136
1137    /// Whether the widget being built takes keyboard focus from a click, as
1138    /// [`focus_on_click`](crate::widget_builder::HandlerSet::focus_on_click)
1139    /// set it on its own node (`true` unless set). The flag holds only for the
1140    /// node it is set on, so a composite whose focusable node is a child of
1141    /// its own (a popover button around its button) reads it here and passes
1142    /// it on to that child.
1143    #[doc(hidden)]
1144    pub fn focus_on_click(&self) -> bool {
1145        self.tree
1146            .arena
1147            .get(self.self_id())
1148            .is_none_or(|node| node.focus_on_click)
1149    }
1150
1151    /// Find the first widget within the subtree rooted at `root`, in
1152    /// depth-first order, that is declared focusable, **whether or not it can
1153    /// take focus right now**: a disabled control, or one kept out of the Tab
1154    /// order, still counts. Dormant subtrees are skipped, as they are by
1155    /// [`first_focusable_descendant`](Self::first_focusable_descendant).
1156    ///
1157    /// For a wrapper asking which control it stands for. A control disabled
1158    /// when it is built is still that control, and taking it for absent would
1159    /// have the wrapper add a focus stop of its own beside it.
1160    pub fn first_focus_capable_descendant(&self, root: WidgetId) -> Option<WidgetId> {
1161        self.tree.first_focus_capable_descendant(root)
1162    }
1163
1164    /// Every widget within the subtree rooted at `root`, `root` included, that
1165    /// can take focus now (active, enabled, declared focusable), in the order
1166    /// Tab visits them, whether or not each is a Tab stop now. Reads only.
1167    ///
1168    /// For a container that steps the keyboard into a part of itself and must
1169    /// name its controls without disturbing a roving group's Tab stop among
1170    /// them (a `ListView` row in action mode).
1171    #[doc(hidden)]
1172    pub fn focusables_in_tab_order(&self, root: WidgetId) -> Vec<WidgetId> {
1173        self.tree.focusables_in_tab_order(root)
1174    }
1175
1176    /// Move keyboard focus **into** the subtree rooted at `id`: its first
1177    /// focusable descendant in tab order, or `id` itself when it is the only
1178    /// focusable thing there. Returns whether focus ended up inside `id`.
1179    ///
1180    /// The build-time twin of
1181    /// [`EventContext::request_focus_into`](crate::widget::EventContext::request_focus_into),
1182    /// and safe here for the same reason [`focus`](Self::focus) is: `add` builds
1183    /// a child's whole subtree synchronously, so by the time a composing widget
1184    /// holds a child's id the focusable descendants of that child already exist.
1185    ///
1186    /// **Idempotent, and that is the point.** `build` runs again on every
1187    /// rebuild, so a bare `focus` here would drag focus back into this subtree
1188    /// every time the owner rebuilt for an unrelated reason — a table body pane
1189    /// rebuilds on selection, on filtering and on scroll. This is a no-op while
1190    /// focus already sits inside `id`, so it expresses "focus belongs in here"
1191    /// rather than "focus here now".
1192    ///
1193    /// A subtree with nothing focusable leaves focus exactly where it was: an
1194    /// empty region never traps it.
1195    ///
1196    /// A **dormant** `id` (a parked page, a hidden branch) is entered once the
1197    /// next layout pass has woken it, as for
1198    /// [`WidgetTree::focus`](crate::widget_tree::WidgetTree::focus); this
1199    /// returns `false` meanwhile, since focus is not inside `id` yet.
1200    ///
1201    /// ⚠ **Ancestor-chain side effects do not run**, and that is a property of
1202    /// focusing from `build` at all, not of this method — [`focus`](Self::focus)
1203    /// has it too. A node added during `build` is not parented until the build
1204    /// that produced it *returns*, so at this moment `id`'s chain stops at
1205    /// whatever the caller has already inserted: `focus_within` signals on
1206    /// enclosing nodes never flip, and `scroll_focused_into_view` finds no
1207    /// scroll container to reveal the target in. Everything **below** `id` is
1208    /// linked (children are parented as each is inserted), so the walk that
1209    /// picks the focusable descendant, and every later key dispatch — which
1210    /// happens after the pass, on a whole tree — are unaffected.
1211    ///
1212    /// Reach for [`EventContext::request_focus_into`](crate::widget::EventContext::request_focus_into)
1213    /// where the difference matters: it is queued and drained after dispatch,
1214    /// against a complete tree.
1215    pub fn focus_into(&mut self, id: WidgetId) -> bool {
1216        if let Some(focused) = self.tree.focused()
1217            && (focused == id || self.tree.is_descendant_of(focused, id))
1218        {
1219            return true;
1220        }
1221        let mut noop = crate::window::NoopWindowOps;
1222        self.tree
1223            .request_focus_with_ops(id, crate::widget_tree::FocusRequestMode::Into, &mut noop);
1224        self.tree
1225            .focused()
1226            .is_some_and(|focused| focused == id || self.tree.is_descendant_of(focused, id))
1227    }
1228
1229    // --- Actions & shortcuts ---
1230
1231    /// Attach an [`Action`](crate::action::Action) to the widget being
1232    /// built. Actions are consulted during intent dispatch as the
1233    /// framework walks source-widget → root; the first matching,
1234    /// enabled action wins (subject to the `IntentResponse` returned
1235    /// by its handler).
1236    ///
1237    /// Actions are cleared on rebuild, mirroring event handlers.
1238    pub fn register_action(&mut self, action: crate::action::Action) {
1239        let id = self.self_id();
1240        self.tree.push_action(id, action);
1241    }
1242
1243    /// Declare that the widget being built **edits text**.
1244    ///
1245    /// Every text widget should call this. It is what lets an application run
1246    /// an edit command — Undo, Copy — from a menu or a toolbar on whatever the
1247    /// caret is in: the host asks
1248    /// [`focused_text_surface`](crate::widget_tree::WidgetTree::focused_text_surface)
1249    /// and either drives this surface or runs its own command. See
1250    /// [`crate::text_surface`] for the whole argument.
1251    ///
1252    /// Owned by the registering widget and torn down on its rebuild or destroy,
1253    /// like [`register_action_global`](Self::register_action_global). Calling it
1254    /// twice from one widget re-points rather than duplicating, so a rebuild
1255    /// that hands over a fresh handle is correct.
1256    pub fn register_text_surface(
1257        &mut self,
1258        surface: std::rc::Rc<dyn crate::text_surface::TextSurface>,
1259    ) {
1260        let id = self.self_id();
1261        self.tree.push_text_surface(id, surface);
1262    }
1263
1264    /// A cloneable view of this tree's registered text surfaces.
1265    ///
1266    /// Take it once, during `build`, and hold it: a view-model refreshed from a
1267    /// frame tick has no `&WidgetTree` to consult, and that is exactly when it
1268    /// needs to know whether the caret is in a text widget.
1269    pub fn text_surfaces(&self) -> crate::text_surface::TextSurfaces {
1270        self.tree.text_surfaces()
1271    }
1272
1273    /// Register a **window-global** [`Action`](crate::action::Action), owned by
1274    /// the widget being built. Unlike [`register_action`](Self::register_action),
1275    /// which only fires when this widget is on the intent's walk to the root, a
1276    /// global action is consulted as a dispatch *fallback*, so it is reachable
1277    /// no matter where in the window the intent originated. An intent sent
1278    /// inside an open modal overlay (a dialog presented in the window, the
1279    /// command palette) is offered the global actions registered by widgets
1280    /// inside that modal first, as a native modal window's tree has its own,
1281    /// and the others only once it has walked on to the page behind the
1282    /// modal; a shortcut pressed inside a modal reaches only those registered
1283    /// inside it.
1284    ///
1285    /// An action on the window's root needs no global form: every walk in the
1286    /// window ends there, a menu's and a popover's included (the walk from an
1287    /// overlay goes on from the widget it is attached to; a modal's, once its
1288    /// content has passed the intent by, from the widget it was opened from),
1289    /// and a menu bar's rows send from the focused widget. Use this when the handler's
1290    /// widget is off to the side of the walk: a popover's open action fired from
1291    /// a menu row or a shortcut, or an action below the root reached by a global
1292    /// shortcut fired with nothing focused. This is the action-side counterpart
1293    /// to [`register_shortcut_global`](Self::register_shortcut_global).
1294    /// Ownership applies: the action is torn down when this widget rebuilds or
1295    /// is destroyed.
1296    pub fn register_action_global(&mut self, action: crate::action::Action) {
1297        let id = self.self_id();
1298        self.tree.push_global_action(id, action);
1299    }
1300
1301    /// Register a [`Shortcut`](crate::shortcut::Shortcut) in the
1302    /// tree's registry, owned by the widget being built.
1303    ///
1304    /// If the shortcut builder left `scope` at the default
1305    /// ([`ShortcutScope::Global`](crate::shortcut::ShortcutScope::Global)),
1306    /// this method rewrites it to `Scoped(self_id)` so the shortcut
1307    /// only fires when focus is inside the registering widget's
1308    /// subtree — the ergonomic default for widget-declared shortcuts.
1309    /// Callers that want an explicit global shortcut should use
1310    /// [`BuildContext::register_shortcut_global`] instead; callers
1311    /// that want to scope to a specific child should set
1312    /// `.scope_to(child_id)` on the builder themselves.
1313    ///
1314    /// Ownership: the shortcut is removed from the registry when the
1315    /// widget is destroyed or rebuilt. User overrides survive across
1316    /// rebuilds (graveyard semantics).
1317    pub fn register_shortcut(&mut self, mut shortcut: crate::shortcut::Shortcut) {
1318        let id = self.self_id();
1319        if shortcut.scope == crate::shortcut::ShortcutScope::Global {
1320            shortcut.scope = crate::shortcut::ShortcutScope::Scoped(id);
1321        }
1322        self.tree
1323            .shortcut_registry_mut()
1324            .register_owned(shortcut, id);
1325    }
1326
1327    /// Register a [`Shortcut`](crate::shortcut::Shortcut) with
1328    /// explicit global scope, owned by the widget being built. Unlike
1329    /// [`BuildContext::register_shortcut`], this does not rewrite the
1330    /// scope: the shortcut fires wherever focus is, except in a modal overlay
1331    /// (a dialog presented in the window, the command palette) this widget is
1332    /// not inside, as a native modal window has only its own widgets'
1333    /// shortcuts. A standard edit shortcut
1334    /// ([`StandardEdit`](crate::shortcut::StandardEdit)) is the exception: an
1335    /// app may re-register one to rename it, and it still applies in every
1336    /// modal.
1337    ///
1338    /// Ownership still applies: the shortcut is torn down when this
1339    /// widget goes away, except a standard edit shortcut, which reverts to
1340    /// the tree's own.
1341    pub fn register_shortcut_global(&mut self, mut shortcut: crate::shortcut::Shortcut) {
1342        let id = self.self_id();
1343        shortcut.scope = crate::shortcut::ShortcutScope::Global;
1344        self.tree
1345            .shortcut_registry_mut()
1346            .register_owned(shortcut, id);
1347    }
1348
1349    /// Register a global [`Shortcut`](crate::shortcut::Shortcut) that every
1350    /// instance of a widget declares together, with the widget being built
1351    /// among its owners. It stays registered while any of them lives, except
1352    /// a standard edit shortcut
1353    /// ([`StandardEdit`](crate::shortcut::StandardEdit)), which reverts to the
1354    /// tree's own when the last goes, and which applies in every modal
1355    /// whoever registered it. See
1356    /// [`ShortcutRegistry::register_shared`](crate::shortcut::ShortcutRegistry::register_shared).
1357    ///
1358    /// For a command each instance runs from an
1359    /// [`Action`](crate::action::Action) of its own, the way
1360    /// `RichTextEditor` runs bold: one binding for the user to change, and the
1361    /// focused instance, the start of the intent's walk, answers it.
1362    pub fn register_shortcut_shared(&mut self, mut shortcut: crate::shortcut::Shortcut) {
1363        let id = self.self_id();
1364        shortcut.scope = crate::shortcut::ShortcutScope::Global;
1365        self.tree
1366            .shortcut_registry_mut()
1367            .register_shared(shortcut, id);
1368    }
1369
1370    /// Pre-declare shortcuts on behalf of a not-yet-mounted child
1371    /// (e.g. a `Switcher` walking its `Pending` slots' static
1372    /// declarations before they're inserted). Each shortcut is owned
1373    /// by the *calling* widget and its declared scope is preserved
1374    /// as-is — unlike [`register_shortcut`](Self::register_shortcut),
1375    /// no rewrite from `Global` to `Scoped(self)` happens, because
1376    /// the child intended its own scope.
1377    ///
1378    /// When the child is eventually mounted, the framework's
1379    /// insert-time walk of `Widget::declare_shortcuts` re-registers
1380    /// the same ids owned by the *child*; the registry's idempotent
1381    /// upsert moves ownership cleanly. If the child never mounts, the
1382    /// pre-declared entries stay alive (owned by the parent) so
1383    /// settings UIs still see them, and they get torn down when the
1384    /// parent goes away.
1385    pub fn register_pending_shortcuts(
1386        &mut self,
1387        shortcuts: impl IntoIterator<Item = crate::shortcut::Shortcut>,
1388    ) {
1389        let id = self.self_id();
1390        let registry = self.tree.shortcut_registry_mut();
1391        for shortcut in shortcuts {
1392            registry.register_owned(shortcut, id);
1393        }
1394    }
1395
1396    /// Read-through access to the tree's shortcut registry. Consumers
1397    /// (menus, tooltips) look up the effective keystroke for a given
1398    /// id here, and observe
1399    /// [`ShortcutRegistry::version`](crate::shortcut::ShortcutRegistry::version)
1400    /// to refresh when the user rebinds.
1401    pub fn shortcut_registry(&self) -> &crate::shortcut::ShortcutRegistry {
1402        self.tree.shortcut_registry()
1403    }
1404
1405    /// Effective view of a shortcut by id, merged with any user
1406    /// override. Returns `None` when no default has been registered
1407    /// for `id`. Typical caller pattern: call from `paint()` so
1408    /// late-registered shortcuts are still picked up without a
1409    /// dedicated build-phase query.
1410    pub fn effective_shortcut<'b>(
1411        &'b self,
1412        id: &str,
1413    ) -> Option<crate::shortcut::EffectiveShortcut<'b>> {
1414        self.shortcut_registry().effective(id)
1415    }
1416
1417    /// Convenience accessor for the reactive version signal. Widgets
1418    /// that render shortcut-derived state (menu labels, tooltips)
1419    /// observe this so the UI refreshes when the user rebinds or a
1420    /// new shortcut is registered.
1421    pub fn shortcut_version(&self) -> &Signal<u64> {
1422        self.shortcut_registry().version()
1423    }
1424
1425    /// A reactive, **per-id** handle to a shortcut's effective primary
1426    /// keystroke — the granular alternative to [`Self::shortcut_version`].
1427    /// Bind this to render one shortcut's accelerator as a *leaf* value
1428    /// (a menu item's trailing label, a tooltip) that refreshes in place
1429    /// when the user rebinds *that* id, without observing — and rebuilding
1430    /// on — every unrelated registry mutation. The signal is created on
1431    /// first request, seeded with the current value, and kept live by the
1432    /// registry across register / unregister / rebind of that id.
1433    pub fn effective_shortcut_signal(
1434        &mut self,
1435        id: &'static str,
1436    ) -> Signal<Option<crate::shortcut::KeyStroke>> {
1437        self.tree
1438            .shortcut_registry_mut()
1439            .effective_primary_signal(id)
1440    }
1441
1442    /// Apply a `HandlerSet` to a child widget created during this build.
1443    /// Use this to attach event handlers to children without wrapping them
1444    /// in `WidgetWithHandlers`.
1445    pub fn apply_handlers(
1446        &mut self,
1447        id: crate::widget_id::WidgetId,
1448        handler_set: crate::widget_builder::HandlerSet,
1449    ) {
1450        // A composing parent attaches handlers to a child — from the
1451        // child's perspective these are external and must survive the
1452        // child's own rebuilds.
1453        self.tree.apply_external_handler_set(id, handler_set);
1454    }
1455
1456    /// The buttons the `on_tap` attached to `id` from outside answers to: the
1457    /// one added by its builder chain (`.on_tap(..)`) or by a composing
1458    /// parent's [`apply_handlers`](Self::apply_handlers). `None` when nothing
1459    /// outside attached an `on_tap` to it.
1460    ///
1461    /// The mask is the one that bucket declared with `accept_tap_buttons`,
1462    /// else the one declared beside the widget's own `on_tap` when the widget
1463    /// has none, else [`ButtonMask::PRIMARY`](crate::event::ButtonMask::PRIMARY)
1464    /// — the rule the node's tap recognizer applies.
1465    ///
1466    /// For a wrapper that routes one tap to several nodes: it reads the
1467    /// buttons its caller chose on the node it was given, and attaches the
1468    /// same tap with the same mask to a node inside it.
1469    pub fn attached_tap_buttons(
1470        &self,
1471        id: crate::widget_id::WidgetId,
1472    ) -> Option<crate::event::ButtonMask> {
1473        self.tree.attached_tap_buttons(id)
1474    }
1475
1476    /// Wire an accessibility `labelled_by` relation from an already-mounted
1477    /// child (`id`) to its label (`label_id`), so assistive tech announces the
1478    /// field by its visible label (WCAG 3.3.2 / EN 301 549 11.5.2.7). Unlike
1479    /// the `.access_labelled_by(..)` builder method, this operates *after* the
1480    /// child is mounted (so a container like `FormLayout` can pair a label and
1481    /// a boxed field once both ids are resolved) and preserves any
1482    /// accessibility overrides the child already carries.
1483    ///
1484    /// Both ends see through layout wrappers, which no screen reader keeps:
1485    ///
1486    /// - When `id` neither takes focus nor hands its node to a proxy
1487    ///   ([`Widget::accessibility_proxy`](crate::widget::Widget::accessibility_proxy))
1488    ///   and its own node is an empty container (a `FixedSize`, a `VStack`),
1489    ///   the relation goes to the one control inside it: the outermost
1490    ///   descendant that takes focus or has a proxy. With none, or several (a
1491    ///   field and a browse button), it stays on `id` and names nothing.
1492    /// - When `label_id` has no name or value of its own, the relation points
1493    ///   at the one descendant that has, such as a text inside a `Padding`.
1494    ///   With none, or several, it points at `label_id`.
1495    ///
1496    /// Only descendants already built are found, so call this once both
1497    /// subtrees exist. The end is decided now: a wrapper that later swaps
1498    /// its control (a `Switcher` flipping branch) leaves the relation on the
1499    /// old one, so link the control itself in that case. The label replaces a control's built-in default name
1500    /// (`DateEdit`'s "Date"); a name the application gave (`access_label`)
1501    /// and a name the control takes from its own visible text (a
1502    /// checkbox's) are kept.
1503    pub fn access_labelled_by(
1504        &mut self,
1505        id: crate::widget_id::WidgetId,
1506        label_id: crate::widget_id::WidgetId,
1507    ) {
1508        self.tree.push_access_labelled_by(id, label_id);
1509    }
1510
1511    /// The widget that paints `id`'s title, when it has one.
1512    ///
1513    /// A container names itself from its visible title by pointing at that
1514    /// node — see [`Widget::accessible_title_node`](crate::widget::Widget::accessible_title_node).
1515    /// `build()` runs eagerly on insertion, so the answer is already there
1516    /// the moment the content is mounted.
1517    pub fn accessible_title_node(
1518        &self,
1519        id: crate::widget_id::WidgetId,
1520    ) -> Option<crate::widget_id::WidgetId> {
1521        self.tree.widget_accessible_title_node(id)
1522    }
1523
1524    /// Wire an accessibility `described_by` relation from an already-mounted
1525    /// child (`id`) to a description/error node (`target_id`) — the
1526    /// post-mount, override-preserving counterpart of the
1527    /// `.access_described_by(..)` builder method (WCAG 3.3.1), whose docs say
1528    /// how the target's text reaches a screen reader: as `id`'s description,
1529    /// which the tree writes, since no AccessKit adapter reads the relation.
1530    pub fn access_described_by(
1531        &mut self,
1532        id: crate::widget_id::WidgetId,
1533        target_id: crate::widget_id::WidgetId,
1534    ) {
1535        self.tree.push_access_described_by(id, target_id);
1536    }
1537
1538    /// The id this subscription should carry: the one the previous build used at this
1539    /// same position, or a fresh one.
1540    ///
1541    /// ⚠ **Position is the whole matching rule**, and it is deliberate. The alternative
1542    /// — matching on the origin — cannot be written here: `origin` reaches the adapter
1543    /// as `Box<dyn Any>`, with no `Eq` and no `Hash` to compare it by, and requiring
1544    /// either would change every `EventSource` in existence. Position is stable for the
1545    /// shape widgets actually have, where `build()` runs the same subscribe calls in the
1546    /// same order every time.
1547    ///
1548    /// What a widget that subscribes *conditionally* gets: if the origin at position N
1549    /// differs between two builds, an event still in flight from the old origin is
1550    /// delivered to the new build's callback rather than being dropped. That is safe by
1551    /// construction rather than by luck — an app registers exactly one `EventSource`, so
1552    /// every subscription in the tree shares one origin type and one event type, and the
1553    /// payload downcast cannot mismatch. The callback receives the whole event and can
1554    /// read its origin, which is what `Origin::LongOperation(..)` handlers already do.
1555    fn next_subscription_id(
1556        &self,
1557        app_context: &crate::event_source::TreeAppContext,
1558    ) -> SubscriptionId {
1559        // `subscription_handles` is pushed to once per subscribe call and starts empty
1560        // for each build, so its length *is* this call's position within the build.
1561        self.reusable_sub_ids
1562            .get(self.subscription_handles.len())
1563            .copied()
1564            .unwrap_or_else(|| app_context.allocate_subscription_id())
1565    }
1566
1567    /// Subscribe to events from the registered application event source.
1568    /// The callback runs on the UI thread when the source publishes an
1569    /// event with a matching origin.
1570    ///
1571    /// The subscription is scoped to the current widget's lifetime: when
1572    /// the widget is rebuilt or destroyed, the framework drops the source
1573    /// handle (unregistering from the source) and removes the UI-side
1574    /// callback.
1575    ///
1576    /// It is scoped to the window it was registered from as well. A closing window's
1577    /// tree is dropped wholesale, with no per-widget destroy pass, so teksilo-app calls
1578    /// [`TreeAppContext::purge_subscriptions_for_window`](crate::event_source::TreeAppContext::purge_subscriptions_for_window)
1579    /// to drop the callbacks that window installed. A registration from a windowless
1580    /// tree (headless / tests) records no window, and only the per-widget path above
1581    /// removes such a callback.
1582    ///
1583    /// # Panics
1584    ///
1585    /// Panics if no event source has been registered on the
1586    /// `TeksiloAppBuilder`. In debug builds, also asserts that the `Origin`
1587    /// and `Event` types match the registered source.
1588    pub fn subscribe_event<O, E, F>(&mut self, origin: O, callback: F)
1589    where
1590        O: 'static,
1591        E: 'static,
1592        F: Fn(&E) + 'static,
1593    {
1594        use std::any::{Any, TypeId};
1595        use std::rc::Rc;
1596        use std::sync::Arc;
1597
1598        // Recorded so `TreeAppContext::purge_subscriptions_for_window` can drop this
1599        // entry when the window closes. A closing window's tree is dropped wholesale,
1600        // with no per-widget destroy pass, so nothing else ever reaches the entry and
1601        // the callback (plus everything it captured) would stay live for the rest of
1602        // the process. `None` from a windowless tree (headless / tests), which no
1603        // window purge touches. Mirrors `subscribe_event_with_ctx` below.
1604        let window_id = self.window().map(|w| w.id());
1605
1606        let app_context = self.tree.app_context.clone();
1607
1608        let adapter = app_context.event_source.as_ref().expect(
1609            "BuildContext::subscribe_event called but no event source was registered \
1610             on TeksiloAppBuilder. Call .event_source(source) on the builder first.",
1611        );
1612
1613        debug_assert_eq!(
1614            adapter.origin_type,
1615            TypeId::of::<O>(),
1616            "subscribe_event origin type mismatch: source uses {}, subscribe call used {}",
1617            adapter.origin_type_name,
1618            std::any::type_name::<O>(),
1619        );
1620        debug_assert_eq!(
1621            adapter.event_type,
1622            TypeId::of::<E>(),
1623            "subscribe_event event type mismatch: source uses {}, subscribe call used {}",
1624            adapter.event_type_name,
1625            std::any::type_name::<E>(),
1626        );
1627
1628        let sub_id = self.next_subscription_id(&app_context);
1629
1630        // The UI-side callback that runs after an event posted from the
1631        // source thread is delivered back to the UI thread. It downcasts
1632        // the type-erased payload back to `&E` and invokes the user's `F`.
1633        let stored_callback: Rc<dyn Fn(&dyn Any)> = Rc::new(move |event_any| {
1634            let event = event_any
1635                .downcast_ref::<E>()
1636                .expect("subscription event downcast failed — framework bug");
1637            callback(event);
1638        });
1639        app_context
1640            .subscription_callbacks
1641            .borrow_mut()
1642            .insert(sub_id, (window_id, stored_callback));
1643
1644        // Build the wrapper that the source will invoke from its
1645        // publisher thread. It carries only the sub_id (Copy) and an
1646        // Arc-clone of the poster (Send + Sync), boxes the typed event
1647        // as Any+Send, and posts an AppEvent::SubscriptionEvent through
1648        // the proxy. Tests that run without a registered poster post
1649        // events into a test queue and dispatch them back into the tree
1650        // via `tree.app_context().dispatch_subscription_event`.
1651        let poster = app_context
1652            .poster
1653            .as_ref()
1654            .expect(
1655                "BuildContext::subscribe_event called but no AppEventPoster \
1656                 is installed on the tree. teksilo-app installs one when an \
1657                 event source is registered on the builder; tests must \
1658                 supply a TestPoster via TreeAppContext::with_source_and_poster.",
1659            )
1660            .clone();
1661        let wrapper: Arc<dyn Fn(Box<dyn Any + Send>) + Send + Sync> =
1662            Arc::new(move |erased_event| {
1663                poster.post_subscription_event(sub_id, erased_event);
1664            });
1665
1666        let handle = (adapter.subscribe_fn)(Box::new(origin), wrapper);
1667        self.subscription_handles.push((sub_id, handle));
1668    }
1669
1670    /// Like [`subscribe_event`](Self::subscribe_event), but the UI-side
1671    /// callback additionally receives a fresh
1672    /// [`EventContext`](crate::widget::EventContext) bound to this widget's
1673    /// window. That lets it react to a backend event *imperatively* — update /
1674    /// replace / dismiss a toast, present a modal, `send_intent`, navigate —
1675    /// none of which a plain (context-free) `subscribe_event` callback can do
1676    /// (it can only poke `Signal`s).
1677    ///
1678    /// This is the supported bridge for **long-operation progress**: a data
1679    /// layer's `Progress | Completed | Cancelled | Failed` event
1680    /// crosses from the operation's background thread to the UI thread and the
1681    /// callback drives an evolving progress toast (percentage in the body, a
1682    /// Cancel action, a success/error replacement on completion) — see the
1683    /// `toast_demo` example.
1684    ///
1685    /// The event is delivered on the UI thread through the same
1686    /// `AppEvent::SubscriptionEvent` path as `subscribe_event`; teksilo-app
1687    /// mints the `EventContext` from this widget's window tree just before the
1688    /// call (mirroring `teksilo-async`'s `spawn_local_with` completion path).
1689    /// The subscription is torn down with the widget, exactly like
1690    /// `subscribe_event`.
1691    ///
1692    /// The `<O, E>` type match against the registered event source is a
1693    /// `debug_assert` (as in [`subscribe_event`](Self::subscribe_event)); a
1694    /// mismatched call site in a release build is not caught here but panics
1695    /// later at the payload downcast.
1696    ///
1697    /// Registering from a tree no window hosts records no window. In a
1698    /// running app that tree is the application tree (`teksilo_app`'s
1699    /// `TeksiloAppBuilder::app_root`), and the event is delivered there,
1700    /// with a context whose `window()` is `None`. A headless tree has no
1701    /// event loop to deliver through; headless code that wants to observe
1702    /// events should use [`subscribe_event`](Self::subscribe_event) and
1703    /// drive `Signal`s instead.
1704    pub fn subscribe_event_with_ctx<O, E, F>(&mut self, origin: O, callback: F)
1705    where
1706        O: 'static,
1707        E: 'static,
1708        F: Fn(&E, &mut crate::widget::EventContext) + 'static,
1709    {
1710        use std::any::{Any, TypeId};
1711        use std::rc::Rc;
1712        use std::sync::Arc;
1713
1714        let window_id = self.window().map(|w| w.id());
1715
1716        let app_context = self.tree.app_context.clone();
1717
1718        let adapter = app_context.event_source.as_ref().expect(
1719            "BuildContext::subscribe_event_with_ctx called but no event source was registered \
1720             on TeksiloAppBuilder. Call .event_source(source) on the builder first.",
1721        );
1722
1723        debug_assert_eq!(
1724            adapter.origin_type,
1725            TypeId::of::<O>(),
1726            "subscribe_event_with_ctx origin type mismatch: source uses {}, subscribe call used {}",
1727            adapter.origin_type_name,
1728            std::any::type_name::<O>(),
1729        );
1730        debug_assert_eq!(
1731            adapter.event_type,
1732            TypeId::of::<E>(),
1733            "subscribe_event_with_ctx event type mismatch: source uses {}, subscribe call used {}",
1734            adapter.event_type_name,
1735            std::any::type_name::<E>(),
1736        );
1737
1738        // Re-used across this widget's rebuilds exactly as in `subscribe_event` — the
1739        // context-bearing path keeps its callbacks in a second map but crosses the very
1740        // same queue, so it loses in-flight events the very same way. See
1741        // [`Self::next_subscription_id`].
1742        let sub_id = self.next_subscription_id(&app_context);
1743
1744        // The UI-side callback, invoked after an event posted from the source
1745        // thread is delivered back to the UI thread and a fresh `EventContext`
1746        // has been minted. Downcasts the type-erased payload back to `&E` and
1747        // forwards it plus the context to the user's `F`. Stored behind `Rc` so
1748        // dispatch can drop the map borrow before invoking it (re-entrancy).
1749        let stored_callback: Rc<dyn Fn(&dyn Any, &mut crate::widget::EventContext)> =
1750            Rc::new(move |event_any, ctx| {
1751                let event = event_any
1752                    .downcast_ref::<E>()
1753                    .expect("subscription event downcast failed — framework bug");
1754                callback(event, ctx);
1755            });
1756        app_context
1757            .subscription_ctx_callbacks
1758            .borrow_mut()
1759            .insert(sub_id, (window_id, stored_callback));
1760
1761        // Same publisher-thread wrapper as `subscribe_event`: carry only the
1762        // sub_id (Copy) + an Arc-clone of the poster, box the typed event, and
1763        // post an `AppEvent::SubscriptionEvent`. The dispatch side (teksilo-app)
1764        // routes context-bearing sub_ids through the fresh-`EventContext` path.
1765        let poster = app_context
1766            .poster
1767            .as_ref()
1768            .expect(
1769                "BuildContext::subscribe_event_with_ctx called but no AppEventPoster \
1770                 is installed on the tree. teksilo-app installs one when an \
1771                 event source is registered on the builder.",
1772            )
1773            .clone();
1774        let wrapper: Arc<dyn Fn(Box<dyn Any + Send>) + Send + Sync> =
1775            Arc::new(move |erased_event| {
1776                poster.post_subscription_event(sub_id, erased_event);
1777            });
1778
1779        let handle = (adapter.subscribe_fn)(Box::new(origin), wrapper);
1780        self.subscription_handles.push((sub_id, handle));
1781    }
1782}
1783
1784#[cfg(test)]
1785mod effect_tests {
1786    use super::*;
1787    use crate::widget::{LayoutContext, Widget};
1788    use crate::widget_id::WidgetId;
1789    use crate::widget_tree::WidgetTree;
1790    use teksilo_canvas::SizeProposal;
1791
1792    /// A leaf widget that registers an effect on one signal to mirror its
1793    /// value into another. Produces no children.
1794    #[derive(Debug)]
1795    struct LeafWithEffect {
1796        source: Signal<i32>,
1797        mirror: Signal<i32>,
1798    }
1799
1800    impl Widget for LeafWithEffect {
1801        fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
1802            let mirror = self.mirror.clone();
1803            ctx.effect(&self.source, move |v| mirror.set(*v));
1804            Vec::new()
1805        }
1806
1807        fn layout_response(
1808            &self,
1809            proposal: SizeProposal,
1810            _ctx: &LayoutContext,
1811        ) -> crate::widget::LayoutResponse {
1812            proposal.resolve(0.0, 0.0).into()
1813        }
1814    }
1815
1816    /// A widget that observes the per-frame tick signal and accumulates the
1817    /// deltas it receives into a shared counter, so a test can verify both
1818    /// that the tick fires at all and that the delta value is non-zero.
1819    #[derive(Debug)]
1820    struct FrameTickListener {
1821        ticks: Signal<u32>,
1822        last_delta: Signal<f32>,
1823    }
1824
1825    impl Widget for FrameTickListener {
1826        fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
1827            let ticks = self.ticks.clone();
1828            let last_delta = self.last_delta.clone();
1829            let tick = ctx.frame_tick();
1830            ctx.effect(&tick, move |delta| {
1831                ticks.set(ticks.get() + 1);
1832                last_delta.set(*delta);
1833            });
1834            Vec::new()
1835        }
1836
1837        fn layout_response(
1838            &self,
1839            proposal: SizeProposal,
1840            _ctx: &LayoutContext,
1841        ) -> crate::widget::LayoutResponse {
1842            proposal.resolve(0.0, 0.0).into()
1843        }
1844    }
1845
1846    #[test]
1847    fn frame_tick_stays_silent_until_explicit_request() {
1848        // The draw-when-needed contract: a widget that merely observes
1849        // frame_tick must NOT keep the tree awake. Only an explicit
1850        // `request_frame()` call pumps a tick.
1851        let mut tree = WidgetTree::new();
1852        let ticks = Signal::new(0_u32);
1853        let last_delta = Signal::new(-1.0_f32);
1854        tree.add(FrameTickListener {
1855            ticks: ticks.clone(),
1856            last_delta: last_delta.clone(),
1857        });
1858
1859        // Flush the initial layout-dirty flag from widget insertion.
1860        tree.layout(teksilo_canvas::SizeProposal::exact(400.0, 300.0));
1861        assert!(
1862            !tree.frame_requested(),
1863            "observing frame_tick does not set the request flag"
1864        );
1865
1866        tree.tick_animations(std::time::Duration::from_millis(16));
1867        assert_eq!(
1868            ticks.get(),
1869            0,
1870            "an un-requested tick_animations must not fire frame_tick observers"
1871        );
1872        assert_eq!(last_delta.get(), -1.0);
1873    }
1874
1875    #[test]
1876    fn frame_tick_fires_once_per_request() {
1877        let mut tree = WidgetTree::new();
1878        let ticks = Signal::new(0_u32);
1879        let last_delta = Signal::new(-1.0_f32);
1880        let id = tree.add(FrameTickListener {
1881            ticks: ticks.clone(),
1882            last_delta: last_delta.clone(),
1883        });
1884
1885        // Flush initial layout-dirty flag so assertions reflect only
1886        // the frame-tick contract.
1887        tree.layout(teksilo_canvas::SizeProposal::exact(400.0, 300.0));
1888
1889        tree.request_frame();
1890        assert!(tree.needs_redraw(), "explicit request marks the tree dirty");
1891        assert!(tree.frame_requested());
1892
1893        tree.tick_animations(std::time::Duration::from_millis(16));
1894        assert_eq!(ticks.get(), 1);
1895        assert!((last_delta.get() - 0.016).abs() < 0.001);
1896        assert!(
1897            !tree.frame_requested(),
1898            "request flag must be cleared after the tick fired"
1899        );
1900
1901        // Second request fires exactly one more tick.
1902        tree.request_frame();
1903        tree.tick_animations(std::time::Duration::from_millis(16));
1904        assert_eq!(ticks.get(), 2);
1905
1906        // Without a request, further ticks silently advance time.
1907        tree.tick_animations(std::time::Duration::from_millis(16));
1908        assert_eq!(ticks.get(), 2);
1909
1910        tree.destroy_subtree(id);
1911        tree.request_frame();
1912        tree.tick_animations(std::time::Duration::from_millis(16));
1913        assert_eq!(
1914            ticks.get(),
1915            2,
1916            "destroyed widget's observer must not resurrect"
1917        );
1918    }
1919
1920    #[test]
1921    fn frame_tick_delta_clamped_against_huge_pauses() {
1922        let mut tree = WidgetTree::new();
1923        let ticks = Signal::new(0_u32);
1924        let last_delta = Signal::new(-1.0_f32);
1925        tree.add(FrameTickListener {
1926            ticks: ticks.clone(),
1927            last_delta: last_delta.clone(),
1928        });
1929
1930        tree.request_frame();
1931        tree.tick_animations(std::time::Duration::from_secs(5));
1932        assert_eq!(ticks.get(), 1);
1933        assert!(
1934            (last_delta.get() - 0.1).abs() < 1e-4,
1935            "frame delta must clamp at 0.1s even after a multi-second pause"
1936        );
1937    }
1938
1939    #[test]
1940    fn leaf_widget_effect_fires_and_is_cleaned_up_on_destroy() {
1941        // Regression guard: before the insert_widget / add_child fix,
1942        // effect_handles for a leaf widget (Vec::new() from build()) were
1943        // dropped the moment BuildContext went out of scope, silently
1944        // unregistering the observer. After the fix, the handle is
1945        // transferred to the arena node and the effect fires on signal
1946        // changes until the widget is destroyed.
1947        let mut tree = WidgetTree::new();
1948        let source = Signal::new(0_i32);
1949        let mirror = Signal::new(0_i32);
1950
1951        let id = tree.add(LeafWithEffect {
1952            source: source.clone(),
1953            mirror: mirror.clone(),
1954        });
1955
1956        // The effect should be live after insertion.
1957        source.set(42);
1958        assert_eq!(
1959            mirror.get(),
1960            42,
1961            "leaf widget effect must survive build() and fire on signal change"
1962        );
1963
1964        source.set(7);
1965        assert_eq!(mirror.get(), 7);
1966
1967        // Destroying the widget drops its effect_handles, which in turn
1968        // drops each ObserverHandle and unregisters the observer.
1969        tree.destroy_subtree(id);
1970        source.set(100);
1971        assert_eq!(
1972            mirror.get(),
1973            7,
1974            "effect must be unregistered after widget destruction"
1975        );
1976    }
1977}
1978
1979#[cfg(test)]
1980mod focus_into_tests {
1981    use super::*;
1982    use crate::widget::{LayoutContext, Widget};
1983    use crate::widget_builder::HandlerSet;
1984    use crate::widget_id::WidgetId;
1985    use crate::widget_tree::WidgetTree;
1986    use teksilo_canvas::SizeProposal;
1987
1988    /// A leaf that is focusable when asked, so the walk has something real to
1989    /// find — or nothing at all.
1990    #[derive(Debug)]
1991    struct Leaf {
1992        focusable: bool,
1993    }
1994
1995    impl Widget for Leaf {
1996        fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
1997            if self.focusable {
1998                ctx.apply_self_handlers(HandlerSet::new().focusable(true));
1999            }
2000            Vec::new()
2001        }
2002        fn layout_response(
2003            &self,
2004            proposal: SizeProposal,
2005            _ctx: &LayoutContext,
2006        ) -> crate::widget::LayoutResponse {
2007            proposal.resolve(10.0, 10.0).into()
2008        }
2009    }
2010
2011    /// Holds `focusable` focusable leaves and publishes their ids.
2012    #[derive(Debug)]
2013    struct Panel {
2014        focusable: usize,
2015        leaves: Signal<Vec<WidgetId>>,
2016    }
2017
2018    impl Widget for Panel {
2019        fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
2020            let kids: Vec<WidgetId> = (0..2)
2021                .map(|i| {
2022                    ctx.add(Leaf {
2023                        focusable: i < self.focusable,
2024                    })
2025                })
2026                .collect();
2027            self.leaves.set(kids.clone());
2028            kids
2029        }
2030        fn layout_response(
2031            &self,
2032            proposal: SizeProposal,
2033            _ctx: &LayoutContext,
2034        ) -> crate::widget::LayoutResponse {
2035            proposal.resolve(10.0, 10.0).into()
2036        }
2037    }
2038
2039    /// Calls `focus_into(panel)` on every one of *its own* builds, which is how
2040    /// a composing widget uses it. Rebuilt on demand through `tick` — and
2041    /// rebuilding it leaves the panel and its leaves alive, which is the whole
2042    /// point: that is the situation the idempotence has to survive.
2043    #[derive(Debug)]
2044    struct Driver {
2045        panel: Signal<Option<WidgetId>>,
2046        tick: Signal<u64>,
2047        moved: Signal<bool>,
2048    }
2049
2050    impl Widget for Driver {
2051        fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
2052            self.tick.bind_to(
2053                ctx.self_id(),
2054                ctx.binding_registry(),
2055                crate::binding::BindingLevel::Rebuild,
2056            );
2057            if let Some(panel) = self.panel.get() {
2058                let moved = ctx.focus_into(panel);
2059                self.moved.set(moved);
2060            }
2061            Vec::new()
2062        }
2063        fn layout_response(
2064            &self,
2065            proposal: SizeProposal,
2066            _ctx: &LayoutContext,
2067        ) -> crate::widget::LayoutResponse {
2068            proposal.resolve(0.0, 0.0).into()
2069        }
2070    }
2071
2072    struct Probe {
2073        tree: WidgetTree,
2074        leaves: Vec<WidgetId>,
2075        tick: Signal<u64>,
2076        moved: Signal<bool>,
2077    }
2078
2079    impl Probe {
2080        fn rebuild_driver(&mut self) {
2081            self.tick.set(self.tick.get() + 1);
2082            self.tree.layout(SizeProposal::exact(100.0, 100.0));
2083        }
2084    }
2085
2086    /// A panel with `focusable` focusable leaves, plus a sibling driver that
2087    /// calls `focus_into` on it from `build`. `outside` is focusable and lives
2088    /// outside the panel, so "focus did not move" is observable.
2089    fn probe(focusable: usize) -> (Probe, WidgetId) {
2090        let leaves = Signal::new(Vec::new());
2091        let panel_id = Signal::new(None);
2092        let tick = Signal::new(0_u64);
2093        let moved = Signal::new(false);
2094
2095        let mut tree = WidgetTree::new();
2096        let outside = tree.add(Leaf { focusable: true });
2097        let panel = tree.add(Panel {
2098            focusable,
2099            leaves: leaves.clone(),
2100        });
2101        panel_id.set(Some(panel));
2102        tree.add(Driver {
2103            panel: panel_id,
2104            tick: tick.clone(),
2105            moved: moved.clone(),
2106        });
2107        tree.layout(SizeProposal::exact(100.0, 100.0));
2108        (
2109            Probe {
2110                tree,
2111                leaves: leaves.get(),
2112                tick,
2113                moved,
2114            },
2115            outside,
2116        )
2117    }
2118
2119    /// It lands on the first focusable descendant, not on the container.
2120    #[test]
2121    fn focus_into_lands_on_the_first_focusable_descendant() {
2122        let (p, _) = probe(2);
2123        assert!(p.moved.get());
2124        assert_eq!(p.tree.focused(), Some(p.leaves[0]));
2125    }
2126
2127    /// **It is a no-op while focus is already inside** — the property that lets
2128    /// it be called from `build`, which re-runs on every rebuild. A bare
2129    /// `focus` on the first focusable descendant would drag focus back to the
2130    /// first field every time the caller rebuilt for an unrelated reason, which
2131    /// mid-edit is the caret jumping to the start of the line.
2132    #[test]
2133    fn focus_into_leaves_focus_alone_when_it_is_already_inside() {
2134        let (mut p, _) = probe(2);
2135        p.tree.focus(p.leaves[1]);
2136        p.rebuild_driver();
2137        assert!(p.moved.get(), "focus is inside, so the answer is still yes");
2138        assert_eq!(
2139            p.tree.focused(),
2140            Some(p.leaves[1]),
2141            "focus was dragged back to the first focusable child"
2142        );
2143    }
2144
2145    /// A subtree with nothing focusable leaves focus exactly where it was: an
2146    /// empty region never traps it, and the caller is told so.
2147    #[test]
2148    fn focus_into_an_unfocusable_subtree_moves_nothing() {
2149        let (mut p, outside) = probe(0);
2150        p.tree.focus(outside);
2151        p.rebuild_driver();
2152        assert!(!p.moved.get());
2153        assert_eq!(p.tree.focused(), Some(outside));
2154    }
2155}