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 /// The [`PaintState`](crate::styles::PaintState) the host window paints
556 /// under right now. One-shot read; see
557 /// [`high_contrast_signal`](Self::high_contrast_signal) for the reactive
558 /// form.
559 pub fn paint_state(&self) -> crate::styles::PaintState {
560 self.tree.paint_state()
561 }
562
563 /// Reactive handle on the current locale. Fires observers when
564 /// `tree.set_locale(...)` is called.
565 pub fn locale_signal(&self) -> crate::signal::Signal<Option<String>> {
566 self.tree.locale_signal().clone()
567 }
568
569 /// The [`WindowState`](crate::window::WindowState) for the window
570 /// hosting this tree. `None` only for trees built outside of an
571 /// app (tests, headless scenarios). Use this to bind widgets to
572 /// window-level signals like `placement`, `size`, `focused`.
573 pub fn window(&self) -> Option<&crate::window::WindowState> {
574 self.tree.window_state()
575 }
576
577 /// Retrieve an application-scoped value of type `T` registered via
578 /// `TeksiloAppBuilder::app_state`. Returns `None` if no value of
579 /// that type was registered. The returned reference borrows from
580 /// the framework for the duration of the build pass.
581 pub fn app_state<T: 'static>(&self) -> Option<&T> {
582 self.tree.app_context().app_state::<T>()
583 }
584
585 /// Borrow the [`AppEventPoster`](crate::AppEventPoster) installed by the
586 /// framework, if any. Mirrors [`EventContext::poster`](crate::widget::EventContext::poster).
587 /// Used by integrations that wire a platform callback (e.g. a native menu
588 /// item) to post a typed payload back to the UI loop. Returns `None` for
589 /// trees built outside an app (tests / headless).
590 pub fn poster(&self) -> Option<&std::sync::Arc<dyn crate::AppEventPoster>> {
591 self.tree.app_context().poster()
592 }
593
594 /// Bind a widget's visibility to a boolean prop.
595 pub fn visible_when(&mut self, id: WidgetId, state: impl Into<crate::signal::Prop<bool>>) {
596 self.tree.visible_when(id, state);
597 }
598
599 /// Enqueue a one-shot action to run shortly after this build, with a real
600 /// [`EventContext`](crate::widget::EventContext) — the only place a widget
601 /// can read the OS parent window handle (`ctx.parent_window_handle()`),
602 /// `app_state`, and `poster` *together*, after it is mounted under its
603 /// window. The action runs at most once per enqueue (the app loop drains
604 /// the queue each iteration); a widget that rebuilds must guard against
605 /// enqueuing twice. Built for widgets owning a native OS resource that
606 /// needs a window handle to initialise (a `WebView`'s engine subview);
607 /// ordinary widgets never need it.
608 pub fn run_after_mount(&mut self, f: impl FnOnce(&mut crate::widget::EventContext) + 'static) {
609 self.tree.queue_mount_action(Box::new(f));
610 }
611
612 /// Observe a node's framework activation as a `Signal<bool>` — `true`
613 /// while active, `false` while parked dormant by a `Switcher` /
614 /// `visible_when` gate. Initialised to the node's current state and
615 /// updated only on an actual Active↔Dormant transition.
616 ///
617 /// Ordinary widgets never need this: dormant subtrees are simply not
618 /// painted, so they vanish for free. It exists for the one case where
619 /// "not painted" ≠ "hidden" — a widget owning a native OS resource
620 /// that renders *outside* the wgpu pass (a `WebView`'s engine subview).
621 /// Such a widget does `ctx.effect(&ctx.activation_signal(id), move |a|
622 /// handle.set_visible(*a))` to hide/show the native surface in lockstep.
623 pub fn activation_signal(&mut self, id: WidgetId) -> Signal<bool> {
624 self.tree.activation_signal(id)
625 }
626
627 /// The framework press signal for the widget being built — `true` while it
628 /// holds a pointer press whose visual should show.
629 ///
630 /// The router owns the state, so this is correct for the four cases a
631 /// widget's own `PointerDown`/`PointerUp` bookkeeping gets wrong: a press
632 /// that slides off its target goes `false` and comes back `true` on
633 /// re-entry (WCAG 2.2 SC 2.5.2), a press a pan claimant or an ancestor drag
634 /// wins goes `false` with no release to hang it on, a cancel clears it, and
635 /// a press inside a scrollable withholds the visual for the profile's
636 /// `press_feedback_delay` so a finger that turns out to be scrolling never
637 /// flashes a highlight. The rules are written out in `docs/touch-and-pen.md`
638 /// §7.
639 ///
640 /// Bind it at [`BindingLevel::RepaintOnly`](crate::binding::BindingLevel) —
641 /// a press changes colour, never size.
642 pub fn pressed_signal(&mut self) -> Signal<bool> {
643 let id = self.self_id();
644 self.tree.pressed_signal(id)
645 }
646
647 /// Reactive `Signal<bool>` that is `true` while the *focus scope* containing
648 /// the widget being built — its nearest focusable ancestor, e.g. the
649 /// enclosing `ListView` / `TreeView` — holds keyboard focus. Items outside
650 /// any focusable scope read a constant `true`.
651 ///
652 /// Drives **focus-aware selection**: a selected row renders with the active
653 /// `Selected` chrome while its view has focus and the muted
654 /// `SelectedInactive` chrome when focus moves elsewhere — the standard
655 /// desktop affordance (Qt `SH_ItemView_...`, macOS inactive selection) that
656 /// shows where the keyboard is. The scope is resolved at build time but the
657 /// signal stays live across focus changes.
658 pub fn view_focus_active(&mut self) -> Signal<bool> {
659 // Prefer the scope a containing data view explicitly established for its
660 // rows (deterministic, parenting-independent); else resolve by walking
661 // to the nearest focusable ancestor.
662 if let Some(scope) = self.tree.current_view_focus() {
663 return scope;
664 }
665 let id = self.self_id();
666 self.tree.view_focus_active_for(id)
667 }
668
669 /// Mark the widget being built as a **focus scope** for the rows/items it
670 /// builds next: any descendant's [`view_focus_active`](Self::view_focus_active)
671 /// (and `StandardItem`'s focus-aware selection / focus ring) reads *this*
672 /// widget's keyboard focus. A data view calls this around its row loop, then
673 /// [`end_view_focus`](Self::end_view_focus). Deterministic — unaffected by
674 /// arena parenting, which may not be wired while docked/virtualized rows build.
675 pub fn begin_view_focus(&mut self) -> Signal<bool> {
676 let id = self.self_id();
677 self.tree.begin_view_focus(id)
678 }
679
680 /// Like [`begin_view_focus`](Self::begin_view_focus) but keys the scope on
681 /// an explicit `node_id` rather than the widget being built. A view whose
682 /// rows are built by a **separate body-pane widget** (ListView / TreeView /
683 /// TableView / TreeTableView / GridView) passes its own focusable root id so
684 /// descendant
685 /// items resolve the *root's* keyboard focus — not the pane's, which is a
686 /// child of the root and so never holds focus itself.
687 pub fn begin_view_focus_for(&mut self, node_id: WidgetId) -> Signal<bool> {
688 self.tree.begin_view_focus(node_id)
689 }
690
691 /// End the focus scope opened by [`begin_view_focus`](Self::begin_view_focus).
692 pub fn end_view_focus(&mut self) {
693 self.tree.end_view_focus();
694 }
695
696 /// Input-modality "focus-visible" signal — `true` after keyboard input,
697 /// `false` after pointer input (the standard `:focus-visible` rule). Pair
698 /// with [`view_focus_active`](Self::view_focus_active) to draw a focus
699 /// ring only during keyboard navigation, not on mouse clicks.
700 pub fn focus_visible(&self) -> Signal<bool> {
701 self.tree.focus_visible_signal()
702 }
703
704 /// Bind an opacity multiplier (0..1) to a widget. The render walker
705 /// emits `SetOpacity(value)` before painting the widget's subtree
706 /// and `RestoreOpacity` afterwards, so the multiplier composes
707 /// correctly with ancestor opacity scopes. Bound at `RepaintOnly`:
708 /// opacity changes never trigger relayout. Used by the `Fade`
709 /// wrapper to animate a child between hidden and fully visible.
710 pub fn set_opacity(&mut self, id: WidgetId, opacity: impl Into<crate::signal::Prop<f32>>) {
711 self.tree.set_opacity(id, opacity);
712 }
713
714 /// Bind a 2D affine transform to a widget. The render walker emits
715 /// `PushTransform(value)` before painting the widget's subtree and
716 /// `PopTransform` afterwards, so the transform composes onto the
717 /// renderer's stack with any ancestor transform scopes and with
718 /// the widget's own canvas-level transforms. Bound at `RepaintOnly`:
719 /// visual-only transforms never trigger relayout. Used by `Scale`
720 /// and `Rotate`; reflow-driving wrappers (e.g. `Scale::reflow(true)`)
721 /// must additionally bind their driver signal to themselves at
722 /// `Relayout` to make layout track the value.
723 pub fn set_transform(
724 &mut self,
725 id: WidgetId,
726 transform: impl Into<crate::signal::Prop<teksilo_canvas::Transform2D>>,
727 ) {
728 self.tree.set_transform(id, transform);
729 }
730
731 /// Bind a 2D affine **content** transform to a widget — the transform
732 /// positions the widget's content within its fixed parent-space viewport
733 /// (its bounds) rather than transforming the widget itself. Renders the
734 /// same `PushTransform` / `PopTransform` scope as
735 /// [`set_transform`](Self::set_transform), but hit-testing treats the
736 /// bounds as a fixed viewport so the whole visible area stays interactive
737 /// at any pan / zoom. Used by `SceneView` for its pan/zoom view transform.
738 pub fn set_content_transform(
739 &mut self,
740 id: WidgetId,
741 transform: impl Into<crate::signal::Prop<teksilo_canvas::Transform2D>>,
742 ) {
743 self.tree.set_content_transform(id, transform);
744 }
745
746 /// Bind a Gaussian-equivalent blur radius to a widget. The render
747 /// walker emits `BeginBlurredSubtree { bounds, radius }` before
748 /// painting the widget's subtree and `EndBlurredSubtree` afterwards;
749 /// the renderer redirects drawing into an intermediate texture, runs
750 /// a dual-Kawase blur chain at the requested radius, and composites
751 /// the blurred result back into the parent pass at the widget's
752 /// bounds. Bound at `RepaintOnly`: blur radius changes never trigger
753 /// relayout. Sub-perceptual radii (< 0.5) skip the Begin/End pair
754 /// entirely so animated enable/disable patterns have zero per-frame
755 /// cost when fully off. Used by the `Blur` wrapper.
756 pub fn set_blur(&mut self, id: WidgetId, radius: impl Into<crate::signal::Prop<f32>>) {
757 self.tree.set_blur(id, radius);
758 }
759
760 /// Bind a widget's enabled state to a boolean prop.
761 pub fn enabled_when(&mut self, id: WidgetId, state: impl Into<crate::signal::Prop<bool>>) {
762 self.tree.enabled_when(id, state);
763 }
764
765 /// Reactive view of "is this widget effectively enabled?" — the AND
766 /// of the widget's own `enabled_state` and every ancestor's. The
767 /// arena's [`crate::arena::WidgetArena::is_enabled`] is the
768 /// non-reactive equivalent; this method gives composite widgets a
769 /// `Signal<bool>` they can `.map(...)` / `.zip(...)` against to
770 /// derive other reactive UI state (cursor, custom paint, helper
771 /// signals).
772 ///
773 /// Leaves like `IconWidget` / `TextWidget` / `RectWidget` do NOT
774 /// need this — they get the bool directly via
775 /// [`crate::widget::PaintContext::effective_enabled`] at paint time.
776 /// This method is for composites that need the value at build time
777 /// or want to chain signals.
778 ///
779 /// The signal is node-resident and framework-refreshed (install-or-reuse,
780 /// like [`Self::activation_signal`]), so it tracks ancestors correctly even
781 /// though a widget's parent is not yet wired while its own `build()` runs.
782 /// It is a *mutable* signal, so — unlike the old derived implementation —
783 /// it can be passed to [`Self::effect`].
784 ///
785 /// Returns a signal reading `true` for any node whose entire ancestor
786 /// chain (including itself) has no `enabled_state` bound.
787 pub fn effective_enabled_signal(&mut self, id: WidgetId) -> Signal<bool> {
788 self.tree.effective_enabled_signal(id)
789 }
790
791 /// Whether an intent named `name`, sent from the window's logical focus,
792 /// would be handled, as a signal the tree keeps current while the widget
793 /// being built is active: what a menu row binds to grey itself out, and
794 /// what a toolbar button or any other control bound to a command uses to
795 /// grey itself the same way. "Handled" is what a dispatch would find: an
796 /// enabled action on the route from the focus (the window's own, then
797 /// the application's) that has something to act on
798 /// ([`ActionBuilder::available_when`](crate::action::ActionBuilder::available_when)).
799 pub fn command_available(&mut self, name: &'static str) -> Signal<bool> {
800 let id = self.self_id();
801 self.tree.command_available_signal(id, name)
802 }
803
804 /// Bind a widget's Tab-key participation to a boolean prop.
805 /// When false, the widget is removed
806 /// from Tab / Shift+Tab traversal but remains reachable via
807 /// `request_focus` and arrow-key navigation. Implements the ARIA
808 /// roving-tabindex pattern (HTML `tabindex="-1"` semantics).
809 pub fn set_tab_stop(&mut self, id: WidgetId, state: impl Into<crate::signal::Prop<bool>>) {
810 self.tree.set_tab_stop(id, state);
811 }
812
813 /// Publish what a data view's `Space` should do when the row containing
814 /// `id` holds the keyboard cursor.
815 ///
816 /// A `ListView` / `TreeView` row is not itself focusable and the view keeps
817 /// the row subtree out of the Tab order — a listbox is one Tab stop — so a
818 /// checkbox inside a row has no keyboard route of its own.
819 /// `StandardListItem` / `StandardTreeItem` publish one for the checkbox
820 /// they embed; a hand-written row delegate calls this to get the same
821 /// behaviour. Without it `Space` keeps meaning "toggle the selection".
822 pub fn set_keyboard_toggle(
823 &mut self,
824 id: WidgetId,
825 f: std::rc::Rc<dyn Fn(&mut crate::widget::EventContext)>,
826 ) {
827 self.tree.set_keyboard_toggle(id, f);
828 }
829
830 /// Declare the widget being built as a **traversal-scope boundary** for
831 /// Tab / Shift+Tab navigation. Descendants' `tab_index` values become
832 /// scoped to this node — they never collide with sibling scopes — and the
833 /// `policy` controls what happens at the scope's ends:
834 ///
835 /// - [`TraversalScopePolicy::Continue`](crate::focus::TraversalScopePolicy::Continue)
836 /// — Tab flows out into the enclosing scope's next member (groups
837 /// numbering only).
838 /// - [`TraversalScopePolicy::Cycle`](crate::focus::TraversalScopePolicy::Cycle)
839 /// — Tab wraps within the scope, never exits. For **modal dialogs only**:
840 /// a popover or menu is non-modal, and the framework closes one the
841 /// keyboard walks out of rather than containing focus in it. Trapping such
842 /// an overlay stops that dismissal from ever firing.
843 ///
844 /// This node is automatically excluded from being a Tab stop itself.
845 /// Prefer the `FocusScope` wrapper widget in `teksilo-widgets` over
846 /// calling this directly.
847 pub fn set_traversal_scope(&mut self, policy: crate::focus::TraversalScopePolicy) {
848 let id = self.self_id();
849 self.tree.set_traversal_scope(id, policy);
850 }
851
852 /// Attach a tooltip to a widget.
853 pub fn attach_tooltip(
854 &mut self,
855 anchor_id: WidgetId,
856 content_id: WidgetId,
857 delay: std::time::Duration,
858 ) {
859 self.tree.attach_tooltip(anchor_id, content_id, delay);
860 self.claim_tooltip_description(anchor_id);
861 }
862
863 /// Attach a tooltip with an explicit
864 /// [`TooltipPlacement`](crate::overlay::TooltipPlacement) — use `Side`
865 /// for anchors stacked vertically (menu items, a vertical tab strip,
866 /// list/tree rows) so the tooltip opens beside the anchor instead of
867 /// covering the next sibling.
868 pub fn attach_tooltip_with_placement(
869 &mut self,
870 anchor_id: WidgetId,
871 content_id: WidgetId,
872 delay: std::time::Duration,
873 placement: crate::overlay::TooltipPlacement,
874 ) {
875 self.tree
876 .attach_tooltip_with_placement(anchor_id, content_id, delay, placement);
877 self.claim_tooltip_description(anchor_id);
878 }
879
880 /// Attach a tooltip that auto-promotes to sticky after a dwell
881 /// timer. Non-None `sticky_after` enables the sticky-on-dwell UX:
882 /// once the tooltip has been shown for `sticky_after`, the tree
883 /// flags the entry sticky and swaps the overlay's dismiss
884 /// behavior to `EscapeOrClickOutside`.
885 pub fn attach_tooltip_with_sticky(
886 &mut self,
887 anchor_id: WidgetId,
888 content_id: WidgetId,
889 delay: std::time::Duration,
890 sticky_after: Option<std::time::Duration>,
891 ) {
892 self.tree
893 .attach_tooltip_with_sticky(anchor_id, content_id, delay, sticky_after);
894 self.claim_tooltip_description(anchor_id);
895 }
896
897 /// Variant of [`attach_tooltip_with_sticky`](Self::attach_tooltip_with_sticky)
898 /// that takes a shared `Rc<Cell<Option<Instant>>>` "sink" the
899 /// tree updates whenever the tooltip is shown / dismissed. The
900 /// tooltip widget reads from this sink to compute its own dwell
901 /// progress reliably, without needing a paint-gap heuristic.
902 pub fn attach_tooltip_with_sticky_sink(
903 &mut self,
904 anchor_id: WidgetId,
905 content_id: WidgetId,
906 delay: std::time::Duration,
907 sticky_after: Option<std::time::Duration>,
908 shown_at_sink: std::rc::Rc<std::cell::Cell<Option<std::time::Instant>>>,
909 ) {
910 self.tree.attach_tooltip_with_sticky_sink(
911 anchor_id,
912 content_id,
913 delay,
914 sticky_after,
915 shown_at_sink,
916 );
917 self.claim_tooltip_description(anchor_id);
918 }
919
920 /// Variant of [`attach_tooltip_with_sticky_sink`](Self::attach_tooltip_with_sticky_sink)
921 /// that also carries a [`TooltipPlacement`](crate::overlay::TooltipPlacement).
922 /// The full-featured path used by rich + composite tooltips that want
923 /// `Side` placement in a vertical context (menu items, list/tree rows).
924 pub fn attach_tooltip_with_sticky_sink_placement(
925 &mut self,
926 anchor_id: WidgetId,
927 content_id: WidgetId,
928 delay: std::time::Duration,
929 sticky_after: Option<std::time::Duration>,
930 shown_at_sink: std::rc::Rc<std::cell::Cell<Option<std::time::Instant>>>,
931 placement: crate::overlay::TooltipPlacement,
932 ) {
933 self.tree.attach_tooltip_with_sticky_sink_placement(
934 anchor_id,
935 content_id,
936 delay,
937 sticky_after,
938 shown_at_sink,
939 placement,
940 );
941 self.claim_tooltip_description(anchor_id);
942 }
943
944 /// Name this widget as the one the tooltip just attached describes.
945 ///
946 /// Every `attach_tooltip*` wrapper ends with this, so a composing control
947 /// gets it for free: `Button`, `Toggle` and the two dozen widgets shaped
948 /// like them hang the overlay off an inner chrome node -- the thing with
949 /// the right bounds to open against -- while their role, their name and
950 /// their focusability sit on their own outer node, which is the node an
951 /// assistive technology lands on and therefore the node a description has
952 /// to be on.
953 ///
954 /// A widget anchoring its tooltip on itself claims itself, which is what
955 /// it already had. A widget attaching *many* tooltips in one build -- a
956 /// list body pane, one per visible row -- claims itself for every one of
957 /// them, which is a claim that cannot be granted; the accessibility walk
958 /// is where that is noticed, because it is the only place the whole set
959 /// is visible at once.
960 fn claim_tooltip_description(&mut self, anchor_id: WidgetId) {
961 let owner = self.self_id();
962 self.tree.set_tooltip_description_owner(anchor_id, owner);
963 }
964
965 /// Promote a shown tooltip to "sticky": removes its auto-dismiss
966 /// on pointer-leave and swaps the overlay's dismiss behavior to
967 /// `EscapeOrClickOutside`. Used by rich tooltips that implement a
968 /// dwell timer.
969 pub fn promote_tooltip_to_sticky(&mut self, content_id: WidgetId) {
970 self.tree.promote_tooltip_to_sticky(content_id);
971 }
972
973 /// Set a widget as dormant (inactive). Used to pre-create overlay content
974 /// that will be activated later via `EventContext::activate()`.
975 pub fn set_dormant(&mut self, id: WidgetId) {
976 self.tree.set_dormant(id);
977 }
978
979 /// Destroy a widget and its entire subtree, removing them from the
980 /// arena and dropping any per-widget subscription / effect handles.
981 ///
982 /// Use this to clean up dormant subtrees that the current widget
983 /// created during a prior build and that live outside its regular
984 /// arena children — e.g., a pre-built popup panel inserted via
985 /// `ctx.add(..)` + `ctx.set_dormant(..)` that becomes stale after a
986 /// rebuild. Regular arena children of the composite (i.e. widgets
987 /// whose ids are returned from `build`) are destroyed automatically
988 /// by the framework's rebuild path and do not need this call.
989 ///
990 /// If an overlay currently references `id` as its content, the
991 /// overlay is dismissed first so the manager does not retain a
992 /// stale content reference.
993 pub fn destroy_subtree(&mut self, id: WidgetId) {
994 let overlay_id = self.tree.overlay_manager().find_by_content(id);
995 if let Some(overlay_id) = overlay_id {
996 self.tree.dismiss_overlay(overlay_id);
997 }
998 self.tree.destroy_subtree(id);
999 }
1000
1001 /// Apply a `HandlerSet` to the composite widget being built (self).
1002 /// This transfers attached event handlers, focusable flag, cursor, etc.
1003 /// to the widget's arena node, replacing `event()` and `is_focusable()` overrides.
1004 pub fn apply_self_handlers(&mut self, handler_set: crate::widget_builder::HandlerSet) {
1005 let id = self.self_id();
1006 self.tree.apply_self_handler_set(id, handler_set);
1007 }
1008
1009 /// Move keyboard focus to `id`. Mirrors
1010 /// `EventContext::request_focus` for use during `build()` — e.g.
1011 /// when a composing widget pre-builds an editor and needs focus to
1012 /// land on it as soon as the subtree is wired in.
1013 ///
1014 /// A dormant `id` is focused once the next layout pass wakes it, as for
1015 /// [`WidgetTree::focus`](crate::widget_tree::WidgetTree::focus).
1016 pub fn focus(&mut self, id: WidgetId) {
1017 self.tree.focus(id);
1018 }
1019
1020 /// Find the first focusable widget within the subtree rooted at
1021 /// `root` in depth-first order. Returns `None` when the subtree has
1022 /// no focusable descendant or `root` is not in the arena.
1023 pub fn first_focusable_descendant(&self, root: WidgetId) -> Option<WidgetId> {
1024 self.tree.first_focusable_descendant(root)
1025 }
1026
1027 /// Whether the widget being built takes keyboard focus from a click, as
1028 /// [`focus_on_click`](crate::widget_builder::HandlerSet::focus_on_click)
1029 /// set it on its own node (`true` unless set). The flag holds only for the
1030 /// node it is set on, so a composite whose focusable node is a child of
1031 /// its own (a popover button around its button) reads it here and passes
1032 /// it on to that child.
1033 #[doc(hidden)]
1034 pub fn focus_on_click(&self) -> bool {
1035 self.tree
1036 .arena
1037 .get(self.self_id())
1038 .is_none_or(|node| node.focus_on_click)
1039 }
1040
1041 /// Find the first widget within the subtree rooted at `root`, in
1042 /// depth-first order, that is declared focusable, **whether or not it can
1043 /// take focus right now**: a disabled control, or one kept out of the Tab
1044 /// order, still counts. Dormant subtrees are skipped, as they are by
1045 /// [`first_focusable_descendant`](Self::first_focusable_descendant).
1046 ///
1047 /// For a wrapper asking which control it stands for. A control disabled
1048 /// when it is built is still that control, and taking it for absent would
1049 /// have the wrapper add a focus stop of its own beside it.
1050 pub fn first_focus_capable_descendant(&self, root: WidgetId) -> Option<WidgetId> {
1051 self.tree.first_focus_capable_descendant(root)
1052 }
1053
1054 /// Move keyboard focus **into** the subtree rooted at `id`: its first
1055 /// focusable descendant in tab order, or `id` itself when it is the only
1056 /// focusable thing there. Returns whether focus ended up inside `id`.
1057 ///
1058 /// The build-time twin of
1059 /// [`EventContext::request_focus_into`](crate::widget::EventContext::request_focus_into),
1060 /// and safe here for the same reason [`focus`](Self::focus) is: `add` builds
1061 /// a child's whole subtree synchronously, so by the time a composing widget
1062 /// holds a child's id the focusable descendants of that child already exist.
1063 ///
1064 /// **Idempotent, and that is the point.** `build` runs again on every
1065 /// rebuild, so a bare `focus` here would drag focus back into this subtree
1066 /// every time the owner rebuilt for an unrelated reason — a table body pane
1067 /// rebuilds on selection, on filtering and on scroll. This is a no-op while
1068 /// focus already sits inside `id`, so it expresses "focus belongs in here"
1069 /// rather than "focus here now".
1070 ///
1071 /// A subtree with nothing focusable leaves focus exactly where it was: an
1072 /// empty region never traps it.
1073 ///
1074 /// A **dormant** `id` (a parked page, a hidden branch) is entered once the
1075 /// next layout pass has woken it, as for
1076 /// [`WidgetTree::focus`](crate::widget_tree::WidgetTree::focus); this
1077 /// returns `false` meanwhile, since focus is not inside `id` yet.
1078 ///
1079 /// ⚠ **Ancestor-chain side effects do not run**, and that is a property of
1080 /// focusing from `build` at all, not of this method — [`focus`](Self::focus)
1081 /// has it too. A node added during `build` is not parented until the build
1082 /// that produced it *returns*, so at this moment `id`'s chain stops at
1083 /// whatever the caller has already inserted: `focus_within` signals on
1084 /// enclosing nodes never flip, and `scroll_focused_into_view` finds no
1085 /// scroll container to reveal the target in. Everything **below** `id` is
1086 /// linked (children are parented as each is inserted), so the walk that
1087 /// picks the focusable descendant, and every later key dispatch — which
1088 /// happens after the pass, on a whole tree — are unaffected.
1089 ///
1090 /// Reach for [`EventContext::request_focus_into`](crate::widget::EventContext::request_focus_into)
1091 /// where the difference matters: it is queued and drained after dispatch,
1092 /// against a complete tree.
1093 pub fn focus_into(&mut self, id: WidgetId) -> bool {
1094 if let Some(focused) = self.tree.focused()
1095 && (focused == id || self.tree.is_descendant_of(focused, id))
1096 {
1097 return true;
1098 }
1099 let mut noop = crate::window::NoopWindowOps;
1100 self.tree
1101 .request_focus_with_ops(id, crate::widget_tree::FocusRequestMode::Into, &mut noop);
1102 self.tree
1103 .focused()
1104 .is_some_and(|focused| focused == id || self.tree.is_descendant_of(focused, id))
1105 }
1106
1107 // --- Actions & shortcuts ---
1108
1109 /// Attach an [`Action`](crate::action::Action) to the widget being
1110 /// built. Actions are consulted during intent dispatch as the
1111 /// framework walks source-widget → root; the first matching,
1112 /// enabled action wins (subject to the `IntentResponse` returned
1113 /// by its handler).
1114 ///
1115 /// Actions are cleared on rebuild, mirroring event handlers.
1116 pub fn register_action(&mut self, action: crate::action::Action) {
1117 let id = self.self_id();
1118 self.tree.push_action(id, action);
1119 }
1120
1121 /// Declare that the widget being built **edits text**.
1122 ///
1123 /// Every text widget should call this. It is what lets an application run
1124 /// an edit command — Undo, Copy — from a menu or a toolbar on whatever the
1125 /// caret is in: the host asks
1126 /// [`focused_text_surface`](crate::widget_tree::WidgetTree::focused_text_surface)
1127 /// and either drives this surface or runs its own command. See
1128 /// [`crate::text_surface`] for the whole argument.
1129 ///
1130 /// Owned by the registering widget and torn down on its rebuild or destroy,
1131 /// like [`register_action_global`](Self::register_action_global). Calling it
1132 /// twice from one widget re-points rather than duplicating, so a rebuild
1133 /// that hands over a fresh handle is correct.
1134 pub fn register_text_surface(
1135 &mut self,
1136 surface: std::rc::Rc<dyn crate::text_surface::TextSurface>,
1137 ) {
1138 let id = self.self_id();
1139 self.tree.push_text_surface(id, surface);
1140 }
1141
1142 /// A cloneable view of this tree's registered text surfaces.
1143 ///
1144 /// Take it once, during `build`, and hold it: a view-model refreshed from a
1145 /// frame tick has no `&WidgetTree` to consult, and that is exactly when it
1146 /// needs to know whether the caret is in a text widget.
1147 pub fn text_surfaces(&self) -> crate::text_surface::TextSurfaces {
1148 self.tree.text_surfaces()
1149 }
1150
1151 /// Register a **window-global** [`Action`](crate::action::Action), owned by
1152 /// the widget being built. Unlike [`register_action`](Self::register_action),
1153 /// which only fires when this widget is on the intent's walk to the root, a
1154 /// global action is consulted as a dispatch *fallback*, so it is reachable
1155 /// no matter where in the window the intent originated. An intent sent
1156 /// inside an open modal overlay (a dialog presented in the window, the
1157 /// command palette) is offered the global actions registered by widgets
1158 /// inside that modal first, as a native modal window's tree has its own,
1159 /// and the others only once it has walked on to the page behind the
1160 /// modal; a shortcut pressed inside a modal reaches only those registered
1161 /// inside it.
1162 ///
1163 /// An action on the window's root needs no global form: every walk in the
1164 /// window ends there, a menu's and a popover's included (the walk from an
1165 /// overlay goes on from the widget it is attached to; a modal's, once its
1166 /// content has passed the intent by, from the widget it was opened from),
1167 /// and a menu bar's rows send from the focused widget. Use this when the handler's
1168 /// widget is off to the side of the walk: a popover's open action fired from
1169 /// a menu row or a shortcut, or an action below the root reached by a global
1170 /// shortcut fired with nothing focused. This is the action-side counterpart
1171 /// to [`register_shortcut_global`](Self::register_shortcut_global).
1172 /// Ownership applies: the action is torn down when this widget rebuilds or
1173 /// is destroyed.
1174 pub fn register_action_global(&mut self, action: crate::action::Action) {
1175 let id = self.self_id();
1176 self.tree.push_global_action(id, action);
1177 }
1178
1179 /// Register a [`Shortcut`](crate::shortcut::Shortcut) in the
1180 /// tree's registry, owned by the widget being built.
1181 ///
1182 /// If the shortcut builder left `scope` at the default
1183 /// ([`ShortcutScope::Global`](crate::shortcut::ShortcutScope::Global)),
1184 /// this method rewrites it to `Scoped(self_id)` so the shortcut
1185 /// only fires when focus is inside the registering widget's
1186 /// subtree — the ergonomic default for widget-declared shortcuts.
1187 /// Callers that want an explicit global shortcut should use
1188 /// [`BuildContext::register_shortcut_global`] instead; callers
1189 /// that want to scope to a specific child should set
1190 /// `.scope_to(child_id)` on the builder themselves.
1191 ///
1192 /// Ownership: the shortcut is removed from the registry when the
1193 /// widget is destroyed or rebuilt. User overrides survive across
1194 /// rebuilds (graveyard semantics).
1195 pub fn register_shortcut(&mut self, mut shortcut: crate::shortcut::Shortcut) {
1196 let id = self.self_id();
1197 if shortcut.scope == crate::shortcut::ShortcutScope::Global {
1198 shortcut.scope = crate::shortcut::ShortcutScope::Scoped(id);
1199 }
1200 self.tree
1201 .shortcut_registry_mut()
1202 .register_owned(shortcut, id);
1203 }
1204
1205 /// Register a [`Shortcut`](crate::shortcut::Shortcut) with
1206 /// explicit global scope, owned by the widget being built. Unlike
1207 /// [`BuildContext::register_shortcut`], this does not rewrite the
1208 /// scope: the shortcut fires wherever focus is, except in a modal overlay
1209 /// (a dialog presented in the window, the command palette) this widget is
1210 /// not inside, as a native modal window has only its own widgets'
1211 /// shortcuts.
1212 ///
1213 /// Ownership still applies: the shortcut is torn down when this
1214 /// widget goes away.
1215 pub fn register_shortcut_global(&mut self, mut shortcut: crate::shortcut::Shortcut) {
1216 let id = self.self_id();
1217 shortcut.scope = crate::shortcut::ShortcutScope::Global;
1218 self.tree
1219 .shortcut_registry_mut()
1220 .register_owned(shortcut, id);
1221 }
1222
1223 /// Register a global [`Shortcut`](crate::shortcut::Shortcut) that every
1224 /// instance of a widget declares together, with the widget being built
1225 /// among its owners. It stays registered while any of them lives. See
1226 /// [`ShortcutRegistry::register_shared`](crate::shortcut::ShortcutRegistry::register_shared).
1227 ///
1228 /// For a command each instance runs from an
1229 /// [`Action`](crate::action::Action) of its own, the way
1230 /// `RichTextEditor` runs bold: one binding for the user to change, and the
1231 /// focused instance, the start of the intent's walk, answers it.
1232 pub fn register_shortcut_shared(&mut self, mut shortcut: crate::shortcut::Shortcut) {
1233 let id = self.self_id();
1234 shortcut.scope = crate::shortcut::ShortcutScope::Global;
1235 self.tree
1236 .shortcut_registry_mut()
1237 .register_shared(shortcut, id);
1238 }
1239
1240 /// Pre-declare shortcuts on behalf of a not-yet-mounted child
1241 /// (e.g. a `Switcher` walking its `Pending` slots' static
1242 /// declarations before they're inserted). Each shortcut is owned
1243 /// by the *calling* widget and its declared scope is preserved
1244 /// as-is — unlike [`register_shortcut`](Self::register_shortcut),
1245 /// no rewrite from `Global` to `Scoped(self)` happens, because
1246 /// the child intended its own scope.
1247 ///
1248 /// When the child is eventually mounted, the framework's
1249 /// insert-time walk of `Widget::declare_shortcuts` re-registers
1250 /// the same ids owned by the *child*; the registry's idempotent
1251 /// upsert moves ownership cleanly. If the child never mounts, the
1252 /// pre-declared entries stay alive (owned by the parent) so
1253 /// settings UIs still see them, and they get torn down when the
1254 /// parent goes away.
1255 pub fn register_pending_shortcuts(
1256 &mut self,
1257 shortcuts: impl IntoIterator<Item = crate::shortcut::Shortcut>,
1258 ) {
1259 let id = self.self_id();
1260 let registry = self.tree.shortcut_registry_mut();
1261 for shortcut in shortcuts {
1262 registry.register_owned(shortcut, id);
1263 }
1264 }
1265
1266 /// Read-through access to the tree's shortcut registry. Consumers
1267 /// (menus, tooltips) look up the effective keystroke for a given
1268 /// id here, and observe
1269 /// [`ShortcutRegistry::version`](crate::shortcut::ShortcutRegistry::version)
1270 /// to refresh when the user rebinds.
1271 pub fn shortcut_registry(&self) -> &crate::shortcut::ShortcutRegistry {
1272 self.tree.shortcut_registry()
1273 }
1274
1275 /// Effective view of a shortcut by id, merged with any user
1276 /// override. Returns `None` when no default has been registered
1277 /// for `id`. Typical caller pattern: call from `paint()` so
1278 /// late-registered shortcuts are still picked up without a
1279 /// dedicated build-phase query.
1280 pub fn effective_shortcut<'b>(
1281 &'b self,
1282 id: &str,
1283 ) -> Option<crate::shortcut::EffectiveShortcut<'b>> {
1284 self.shortcut_registry().effective(id)
1285 }
1286
1287 /// Convenience accessor for the reactive version signal. Widgets
1288 /// that render shortcut-derived state (menu labels, tooltips)
1289 /// observe this so the UI refreshes when the user rebinds or a
1290 /// new shortcut is registered.
1291 pub fn shortcut_version(&self) -> &Signal<u64> {
1292 self.shortcut_registry().version()
1293 }
1294
1295 /// A reactive, **per-id** handle to a shortcut's effective primary
1296 /// keystroke — the granular alternative to [`Self::shortcut_version`].
1297 /// Bind this to render one shortcut's accelerator as a *leaf* value
1298 /// (a menu item's trailing label, a tooltip) that refreshes in place
1299 /// when the user rebinds *that* id, without observing — and rebuilding
1300 /// on — every unrelated registry mutation. The signal is created on
1301 /// first request, seeded with the current value, and kept live by the
1302 /// registry across register / unregister / rebind of that id.
1303 pub fn effective_shortcut_signal(
1304 &mut self,
1305 id: &'static str,
1306 ) -> Signal<Option<crate::shortcut::KeyStroke>> {
1307 self.tree
1308 .shortcut_registry_mut()
1309 .effective_primary_signal(id)
1310 }
1311
1312 /// Apply a `HandlerSet` to a child widget created during this build.
1313 /// Use this to attach event handlers to children without wrapping them
1314 /// in `WidgetWithHandlers`.
1315 pub fn apply_handlers(
1316 &mut self,
1317 id: crate::widget_id::WidgetId,
1318 handler_set: crate::widget_builder::HandlerSet,
1319 ) {
1320 // A composing parent attaches handlers to a child — from the
1321 // child's perspective these are external and must survive the
1322 // child's own rebuilds.
1323 self.tree.apply_external_handler_set(id, handler_set);
1324 }
1325
1326 /// The buttons the `on_tap` attached to `id` from outside answers to: the
1327 /// one added by its builder chain (`.on_tap(..)`) or by a composing
1328 /// parent's [`apply_handlers`](Self::apply_handlers). `None` when nothing
1329 /// outside attached an `on_tap` to it.
1330 ///
1331 /// The mask is the one that bucket declared with `accept_tap_buttons`,
1332 /// else the one declared beside the widget's own `on_tap` when the widget
1333 /// has none, else [`ButtonMask::PRIMARY`](crate::event::ButtonMask::PRIMARY)
1334 /// — the rule the node's tap recognizer applies.
1335 ///
1336 /// For a wrapper that routes one tap to several nodes: it reads the
1337 /// buttons its caller chose on the node it was given, and attaches the
1338 /// same tap with the same mask to a node inside it.
1339 pub fn attached_tap_buttons(
1340 &self,
1341 id: crate::widget_id::WidgetId,
1342 ) -> Option<crate::event::ButtonMask> {
1343 self.tree.attached_tap_buttons(id)
1344 }
1345
1346 /// Wire an accessibility `labelled_by` relation from an already-mounted
1347 /// child (`id`) to its label (`label_id`), so assistive tech announces the
1348 /// field by its visible label (WCAG 3.3.2 / EN 301 549 11.5.2.7). Unlike
1349 /// the `.access_labelled_by(..)` builder method, this operates *after* the
1350 /// child is mounted (so a container like `FormLayout` can pair a label and
1351 /// a boxed field once both ids are resolved) and preserves any
1352 /// accessibility overrides the child already carries.
1353 pub fn access_labelled_by(
1354 &mut self,
1355 id: crate::widget_id::WidgetId,
1356 label_id: crate::widget_id::WidgetId,
1357 ) {
1358 self.tree.push_access_labelled_by(id, label_id);
1359 }
1360
1361 /// The widget that paints `id`'s title, when it has one.
1362 ///
1363 /// A container names itself from its visible title by pointing at that
1364 /// node — see [`Widget::accessible_title_node`](crate::widget::Widget::accessible_title_node).
1365 /// `build()` runs eagerly on insertion, so the answer is already there
1366 /// the moment the content is mounted.
1367 pub fn accessible_title_node(
1368 &self,
1369 id: crate::widget_id::WidgetId,
1370 ) -> Option<crate::widget_id::WidgetId> {
1371 self.tree.widget_accessible_title_node(id)
1372 }
1373
1374 /// Wire an accessibility `described_by` relation from an already-mounted
1375 /// child (`id`) to a description/error node (`target_id`) — the
1376 /// post-mount, override-preserving counterpart of the
1377 /// `.access_described_by(..)` builder method (WCAG 3.3.1), whose docs say
1378 /// how the target's text reaches a screen reader: as `id`'s description,
1379 /// which the tree writes, since no AccessKit adapter reads the relation.
1380 pub fn access_described_by(
1381 &mut self,
1382 id: crate::widget_id::WidgetId,
1383 target_id: crate::widget_id::WidgetId,
1384 ) {
1385 self.tree.push_access_described_by(id, target_id);
1386 }
1387
1388 /// The id this subscription should carry: the one the previous build used at this
1389 /// same position, or a fresh one.
1390 ///
1391 /// ⚠ **Position is the whole matching rule**, and it is deliberate. The alternative
1392 /// — matching on the origin — cannot be written here: `origin` reaches the adapter
1393 /// as `Box<dyn Any>`, with no `Eq` and no `Hash` to compare it by, and requiring
1394 /// either would change every `EventSource` in existence. Position is stable for the
1395 /// shape widgets actually have, where `build()` runs the same subscribe calls in the
1396 /// same order every time.
1397 ///
1398 /// What a widget that subscribes *conditionally* gets: if the origin at position N
1399 /// differs between two builds, an event still in flight from the old origin is
1400 /// delivered to the new build's callback rather than being dropped. That is safe by
1401 /// construction rather than by luck — an app registers exactly one `EventSource`, so
1402 /// every subscription in the tree shares one origin type and one event type, and the
1403 /// payload downcast cannot mismatch. The callback receives the whole event and can
1404 /// read its origin, which is what `Origin::LongOperation(..)` handlers already do.
1405 fn next_subscription_id(
1406 &self,
1407 app_context: &crate::event_source::TreeAppContext,
1408 ) -> SubscriptionId {
1409 // `subscription_handles` is pushed to once per subscribe call and starts empty
1410 // for each build, so its length *is* this call's position within the build.
1411 self.reusable_sub_ids
1412 .get(self.subscription_handles.len())
1413 .copied()
1414 .unwrap_or_else(|| app_context.allocate_subscription_id())
1415 }
1416
1417 /// Subscribe to events from the registered application event source.
1418 /// The callback runs on the UI thread when the source publishes an
1419 /// event with a matching origin.
1420 ///
1421 /// The subscription is scoped to the current widget's lifetime: when
1422 /// the widget is rebuilt or destroyed, the framework drops the source
1423 /// handle (unregistering from the source) and removes the UI-side
1424 /// callback.
1425 ///
1426 /// It is scoped to the window it was registered from as well. A closing window's
1427 /// tree is dropped wholesale, with no per-widget destroy pass, so teksilo-app calls
1428 /// [`TreeAppContext::purge_subscriptions_for_window`](crate::event_source::TreeAppContext::purge_subscriptions_for_window)
1429 /// to drop the callbacks that window installed. A registration from a windowless
1430 /// tree (headless / tests) records no window, and only the per-widget path above
1431 /// removes such a callback.
1432 ///
1433 /// # Panics
1434 ///
1435 /// Panics if no event source has been registered on the
1436 /// `TeksiloAppBuilder`. In debug builds, also asserts that the `Origin`
1437 /// and `Event` types match the registered source.
1438 pub fn subscribe_event<O, E, F>(&mut self, origin: O, callback: F)
1439 where
1440 O: 'static,
1441 E: 'static,
1442 F: Fn(&E) + 'static,
1443 {
1444 use std::any::{Any, TypeId};
1445 use std::rc::Rc;
1446 use std::sync::Arc;
1447
1448 // Recorded so `TreeAppContext::purge_subscriptions_for_window` can drop this
1449 // entry when the window closes. A closing window's tree is dropped wholesale,
1450 // with no per-widget destroy pass, so nothing else ever reaches the entry and
1451 // the callback (plus everything it captured) would stay live for the rest of
1452 // the process. `None` from a windowless tree (headless / tests), which no
1453 // window purge touches. Mirrors `subscribe_event_with_ctx` below.
1454 let window_id = self.window().map(|w| w.id());
1455
1456 let app_context = self.tree.app_context.clone();
1457
1458 let adapter = app_context.event_source.as_ref().expect(
1459 "BuildContext::subscribe_event called but no event source was registered \
1460 on TeksiloAppBuilder. Call .event_source(source) on the builder first.",
1461 );
1462
1463 debug_assert_eq!(
1464 adapter.origin_type,
1465 TypeId::of::<O>(),
1466 "subscribe_event origin type mismatch: source uses {}, subscribe call used {}",
1467 adapter.origin_type_name,
1468 std::any::type_name::<O>(),
1469 );
1470 debug_assert_eq!(
1471 adapter.event_type,
1472 TypeId::of::<E>(),
1473 "subscribe_event event type mismatch: source uses {}, subscribe call used {}",
1474 adapter.event_type_name,
1475 std::any::type_name::<E>(),
1476 );
1477
1478 let sub_id = self.next_subscription_id(&app_context);
1479
1480 // The UI-side callback that runs after an event posted from the
1481 // source thread is delivered back to the UI thread. It downcasts
1482 // the type-erased payload back to `&E` and invokes the user's `F`.
1483 let stored_callback: Rc<dyn Fn(&dyn Any)> = Rc::new(move |event_any| {
1484 let event = event_any
1485 .downcast_ref::<E>()
1486 .expect("subscription event downcast failed — framework bug");
1487 callback(event);
1488 });
1489 app_context
1490 .subscription_callbacks
1491 .borrow_mut()
1492 .insert(sub_id, (window_id, stored_callback));
1493
1494 // Build the wrapper that the source will invoke from its
1495 // publisher thread. It carries only the sub_id (Copy) and an
1496 // Arc-clone of the poster (Send + Sync), boxes the typed event
1497 // as Any+Send, and posts an AppEvent::SubscriptionEvent through
1498 // the proxy. Tests that run without a registered poster post
1499 // events into a test queue and dispatch them back into the tree
1500 // via `tree.app_context().dispatch_subscription_event`.
1501 let poster = app_context
1502 .poster
1503 .as_ref()
1504 .expect(
1505 "BuildContext::subscribe_event called but no AppEventPoster \
1506 is installed on the tree. teksilo-app installs one when an \
1507 event source is registered on the builder; tests must \
1508 supply a TestPoster via TreeAppContext::with_source_and_poster.",
1509 )
1510 .clone();
1511 let wrapper: Arc<dyn Fn(Box<dyn Any + Send>) + Send + Sync> =
1512 Arc::new(move |erased_event| {
1513 poster.post_subscription_event(sub_id, erased_event);
1514 });
1515
1516 let handle = (adapter.subscribe_fn)(Box::new(origin), wrapper);
1517 self.subscription_handles.push((sub_id, handle));
1518 }
1519
1520 /// Like [`subscribe_event`](Self::subscribe_event), but the UI-side
1521 /// callback additionally receives a fresh
1522 /// [`EventContext`](crate::widget::EventContext) bound to this widget's
1523 /// window. That lets it react to a backend event *imperatively* — update /
1524 /// replace / dismiss a toast, present a modal, `send_intent`, navigate —
1525 /// none of which a plain (context-free) `subscribe_event` callback can do
1526 /// (it can only poke `Signal`s).
1527 ///
1528 /// This is the supported bridge for **long-operation progress**: a data
1529 /// layer's `Progress | Completed | Cancelled | Failed` event
1530 /// crosses from the operation's background thread to the UI thread and the
1531 /// callback drives an evolving progress toast (percentage in the body, a
1532 /// Cancel action, a success/error replacement on completion) — see the
1533 /// `toast_demo` example.
1534 ///
1535 /// The event is delivered on the UI thread through the same
1536 /// `AppEvent::SubscriptionEvent` path as `subscribe_event`; teksilo-app
1537 /// mints the `EventContext` from this widget's window tree just before the
1538 /// call (mirroring `teksilo-async`'s `spawn_local_with` completion path).
1539 /// The subscription is torn down with the widget, exactly like
1540 /// `subscribe_event`.
1541 ///
1542 /// The `<O, E>` type match against the registered event source is a
1543 /// `debug_assert` (as in [`subscribe_event`](Self::subscribe_event)); a
1544 /// mismatched call site in a release build is not caught here but panics
1545 /// later at the payload downcast.
1546 ///
1547 /// Registering from a tree no window hosts records no window. In a
1548 /// running app that tree is the application tree (`teksilo_app`'s
1549 /// `TeksiloAppBuilder::app_root`), and the event is delivered there,
1550 /// with a context whose `window()` is `None`. A headless tree has no
1551 /// event loop to deliver through; headless code that wants to observe
1552 /// events should use [`subscribe_event`](Self::subscribe_event) and
1553 /// drive `Signal`s instead.
1554 pub fn subscribe_event_with_ctx<O, E, F>(&mut self, origin: O, callback: F)
1555 where
1556 O: 'static,
1557 E: 'static,
1558 F: Fn(&E, &mut crate::widget::EventContext) + 'static,
1559 {
1560 use std::any::{Any, TypeId};
1561 use std::rc::Rc;
1562 use std::sync::Arc;
1563
1564 let window_id = self.window().map(|w| w.id());
1565
1566 let app_context = self.tree.app_context.clone();
1567
1568 let adapter = app_context.event_source.as_ref().expect(
1569 "BuildContext::subscribe_event_with_ctx called but no event source was registered \
1570 on TeksiloAppBuilder. Call .event_source(source) on the builder first.",
1571 );
1572
1573 debug_assert_eq!(
1574 adapter.origin_type,
1575 TypeId::of::<O>(),
1576 "subscribe_event_with_ctx origin type mismatch: source uses {}, subscribe call used {}",
1577 adapter.origin_type_name,
1578 std::any::type_name::<O>(),
1579 );
1580 debug_assert_eq!(
1581 adapter.event_type,
1582 TypeId::of::<E>(),
1583 "subscribe_event_with_ctx event type mismatch: source uses {}, subscribe call used {}",
1584 adapter.event_type_name,
1585 std::any::type_name::<E>(),
1586 );
1587
1588 // Re-used across this widget's rebuilds exactly as in `subscribe_event` — the
1589 // context-bearing path keeps its callbacks in a second map but crosses the very
1590 // same queue, so it loses in-flight events the very same way. See
1591 // [`Self::next_subscription_id`].
1592 let sub_id = self.next_subscription_id(&app_context);
1593
1594 // The UI-side callback, invoked after an event posted from the source
1595 // thread is delivered back to the UI thread and a fresh `EventContext`
1596 // has been minted. Downcasts the type-erased payload back to `&E` and
1597 // forwards it plus the context to the user's `F`. Stored behind `Rc` so
1598 // dispatch can drop the map borrow before invoking it (re-entrancy).
1599 let stored_callback: Rc<dyn Fn(&dyn Any, &mut crate::widget::EventContext)> =
1600 Rc::new(move |event_any, ctx| {
1601 let event = event_any
1602 .downcast_ref::<E>()
1603 .expect("subscription event downcast failed — framework bug");
1604 callback(event, ctx);
1605 });
1606 app_context
1607 .subscription_ctx_callbacks
1608 .borrow_mut()
1609 .insert(sub_id, (window_id, stored_callback));
1610
1611 // Same publisher-thread wrapper as `subscribe_event`: carry only the
1612 // sub_id (Copy) + an Arc-clone of the poster, box the typed event, and
1613 // post an `AppEvent::SubscriptionEvent`. The dispatch side (teksilo-app)
1614 // routes context-bearing sub_ids through the fresh-`EventContext` path.
1615 let poster = app_context
1616 .poster
1617 .as_ref()
1618 .expect(
1619 "BuildContext::subscribe_event_with_ctx called but no AppEventPoster \
1620 is installed on the tree. teksilo-app installs one when an \
1621 event source is registered on the builder.",
1622 )
1623 .clone();
1624 let wrapper: Arc<dyn Fn(Box<dyn Any + Send>) + Send + Sync> =
1625 Arc::new(move |erased_event| {
1626 poster.post_subscription_event(sub_id, erased_event);
1627 });
1628
1629 let handle = (adapter.subscribe_fn)(Box::new(origin), wrapper);
1630 self.subscription_handles.push((sub_id, handle));
1631 }
1632}
1633
1634#[cfg(test)]
1635mod effect_tests {
1636 use super::*;
1637 use crate::widget::{LayoutContext, Widget};
1638 use crate::widget_id::WidgetId;
1639 use crate::widget_tree::WidgetTree;
1640 use teksilo_canvas::SizeProposal;
1641
1642 /// A leaf widget that registers an effect on one signal to mirror its
1643 /// value into another. Produces no children.
1644 #[derive(Debug)]
1645 struct LeafWithEffect {
1646 source: Signal<i32>,
1647 mirror: Signal<i32>,
1648 }
1649
1650 impl Widget for LeafWithEffect {
1651 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
1652 let mirror = self.mirror.clone();
1653 ctx.effect(&self.source, move |v| mirror.set(*v));
1654 Vec::new()
1655 }
1656
1657 fn layout_response(
1658 &self,
1659 proposal: SizeProposal,
1660 _ctx: &LayoutContext,
1661 ) -> crate::widget::LayoutResponse {
1662 proposal.resolve(0.0, 0.0).into()
1663 }
1664 }
1665
1666 /// A widget that observes the per-frame tick signal and accumulates the
1667 /// deltas it receives into a shared counter, so a test can verify both
1668 /// that the tick fires at all and that the delta value is non-zero.
1669 #[derive(Debug)]
1670 struct FrameTickListener {
1671 ticks: Signal<u32>,
1672 last_delta: Signal<f32>,
1673 }
1674
1675 impl Widget for FrameTickListener {
1676 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
1677 let ticks = self.ticks.clone();
1678 let last_delta = self.last_delta.clone();
1679 let tick = ctx.frame_tick();
1680 ctx.effect(&tick, move |delta| {
1681 ticks.set(ticks.get() + 1);
1682 last_delta.set(*delta);
1683 });
1684 Vec::new()
1685 }
1686
1687 fn layout_response(
1688 &self,
1689 proposal: SizeProposal,
1690 _ctx: &LayoutContext,
1691 ) -> crate::widget::LayoutResponse {
1692 proposal.resolve(0.0, 0.0).into()
1693 }
1694 }
1695
1696 #[test]
1697 fn frame_tick_stays_silent_until_explicit_request() {
1698 // The draw-when-needed contract: a widget that merely observes
1699 // frame_tick must NOT keep the tree awake. Only an explicit
1700 // `request_frame()` call pumps a tick.
1701 let mut tree = WidgetTree::new();
1702 let ticks = Signal::new(0_u32);
1703 let last_delta = Signal::new(-1.0_f32);
1704 tree.add(FrameTickListener {
1705 ticks: ticks.clone(),
1706 last_delta: last_delta.clone(),
1707 });
1708
1709 // Flush the initial layout-dirty flag from widget insertion.
1710 tree.layout(teksilo_canvas::SizeProposal::exact(400.0, 300.0));
1711 assert!(
1712 !tree.frame_requested(),
1713 "observing frame_tick does not set the request flag"
1714 );
1715
1716 tree.tick_animations(std::time::Duration::from_millis(16));
1717 assert_eq!(
1718 ticks.get(),
1719 0,
1720 "an un-requested tick_animations must not fire frame_tick observers"
1721 );
1722 assert_eq!(last_delta.get(), -1.0);
1723 }
1724
1725 #[test]
1726 fn frame_tick_fires_once_per_request() {
1727 let mut tree = WidgetTree::new();
1728 let ticks = Signal::new(0_u32);
1729 let last_delta = Signal::new(-1.0_f32);
1730 let id = tree.add(FrameTickListener {
1731 ticks: ticks.clone(),
1732 last_delta: last_delta.clone(),
1733 });
1734
1735 // Flush initial layout-dirty flag so assertions reflect only
1736 // the frame-tick contract.
1737 tree.layout(teksilo_canvas::SizeProposal::exact(400.0, 300.0));
1738
1739 tree.request_frame();
1740 assert!(tree.needs_redraw(), "explicit request marks the tree dirty");
1741 assert!(tree.frame_requested());
1742
1743 tree.tick_animations(std::time::Duration::from_millis(16));
1744 assert_eq!(ticks.get(), 1);
1745 assert!((last_delta.get() - 0.016).abs() < 0.001);
1746 assert!(
1747 !tree.frame_requested(),
1748 "request flag must be cleared after the tick fired"
1749 );
1750
1751 // Second request fires exactly one more tick.
1752 tree.request_frame();
1753 tree.tick_animations(std::time::Duration::from_millis(16));
1754 assert_eq!(ticks.get(), 2);
1755
1756 // Without a request, further ticks silently advance time.
1757 tree.tick_animations(std::time::Duration::from_millis(16));
1758 assert_eq!(ticks.get(), 2);
1759
1760 tree.destroy_subtree(id);
1761 tree.request_frame();
1762 tree.tick_animations(std::time::Duration::from_millis(16));
1763 assert_eq!(
1764 ticks.get(),
1765 2,
1766 "destroyed widget's observer must not resurrect"
1767 );
1768 }
1769
1770 #[test]
1771 fn frame_tick_delta_clamped_against_huge_pauses() {
1772 let mut tree = WidgetTree::new();
1773 let ticks = Signal::new(0_u32);
1774 let last_delta = Signal::new(-1.0_f32);
1775 tree.add(FrameTickListener {
1776 ticks: ticks.clone(),
1777 last_delta: last_delta.clone(),
1778 });
1779
1780 tree.request_frame();
1781 tree.tick_animations(std::time::Duration::from_secs(5));
1782 assert_eq!(ticks.get(), 1);
1783 assert!(
1784 (last_delta.get() - 0.1).abs() < 1e-4,
1785 "frame delta must clamp at 0.1s even after a multi-second pause"
1786 );
1787 }
1788
1789 #[test]
1790 fn leaf_widget_effect_fires_and_is_cleaned_up_on_destroy() {
1791 // Regression guard: before the insert_widget / add_child fix,
1792 // effect_handles for a leaf widget (Vec::new() from build()) were
1793 // dropped the moment BuildContext went out of scope, silently
1794 // unregistering the observer. After the fix, the handle is
1795 // transferred to the arena node and the effect fires on signal
1796 // changes until the widget is destroyed.
1797 let mut tree = WidgetTree::new();
1798 let source = Signal::new(0_i32);
1799 let mirror = Signal::new(0_i32);
1800
1801 let id = tree.add(LeafWithEffect {
1802 source: source.clone(),
1803 mirror: mirror.clone(),
1804 });
1805
1806 // The effect should be live after insertion.
1807 source.set(42);
1808 assert_eq!(
1809 mirror.get(),
1810 42,
1811 "leaf widget effect must survive build() and fire on signal change"
1812 );
1813
1814 source.set(7);
1815 assert_eq!(mirror.get(), 7);
1816
1817 // Destroying the widget drops its effect_handles, which in turn
1818 // drops each ObserverHandle and unregisters the observer.
1819 tree.destroy_subtree(id);
1820 source.set(100);
1821 assert_eq!(
1822 mirror.get(),
1823 7,
1824 "effect must be unregistered after widget destruction"
1825 );
1826 }
1827}
1828
1829#[cfg(test)]
1830mod focus_into_tests {
1831 use super::*;
1832 use crate::widget::{LayoutContext, Widget};
1833 use crate::widget_builder::HandlerSet;
1834 use crate::widget_id::WidgetId;
1835 use crate::widget_tree::WidgetTree;
1836 use teksilo_canvas::SizeProposal;
1837
1838 /// A leaf that is focusable when asked, so the walk has something real to
1839 /// find — or nothing at all.
1840 #[derive(Debug)]
1841 struct Leaf {
1842 focusable: bool,
1843 }
1844
1845 impl Widget for Leaf {
1846 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
1847 if self.focusable {
1848 ctx.apply_self_handlers(HandlerSet::new().focusable(true));
1849 }
1850 Vec::new()
1851 }
1852 fn layout_response(
1853 &self,
1854 proposal: SizeProposal,
1855 _ctx: &LayoutContext,
1856 ) -> crate::widget::LayoutResponse {
1857 proposal.resolve(10.0, 10.0).into()
1858 }
1859 }
1860
1861 /// Holds `focusable` focusable leaves and publishes their ids.
1862 #[derive(Debug)]
1863 struct Panel {
1864 focusable: usize,
1865 leaves: Signal<Vec<WidgetId>>,
1866 }
1867
1868 impl Widget for Panel {
1869 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
1870 let kids: Vec<WidgetId> = (0..2)
1871 .map(|i| {
1872 ctx.add(Leaf {
1873 focusable: i < self.focusable,
1874 })
1875 })
1876 .collect();
1877 self.leaves.set(kids.clone());
1878 kids
1879 }
1880 fn layout_response(
1881 &self,
1882 proposal: SizeProposal,
1883 _ctx: &LayoutContext,
1884 ) -> crate::widget::LayoutResponse {
1885 proposal.resolve(10.0, 10.0).into()
1886 }
1887 }
1888
1889 /// Calls `focus_into(panel)` on every one of *its own* builds, which is how
1890 /// a composing widget uses it. Rebuilt on demand through `tick` — and
1891 /// rebuilding it leaves the panel and its leaves alive, which is the whole
1892 /// point: that is the situation the idempotence has to survive.
1893 #[derive(Debug)]
1894 struct Driver {
1895 panel: Signal<Option<WidgetId>>,
1896 tick: Signal<u64>,
1897 moved: Signal<bool>,
1898 }
1899
1900 impl Widget for Driver {
1901 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
1902 self.tick.bind_to(
1903 ctx.self_id(),
1904 ctx.binding_registry(),
1905 crate::binding::BindingLevel::Rebuild,
1906 );
1907 if let Some(panel) = self.panel.get() {
1908 let moved = ctx.focus_into(panel);
1909 self.moved.set(moved);
1910 }
1911 Vec::new()
1912 }
1913 fn layout_response(
1914 &self,
1915 proposal: SizeProposal,
1916 _ctx: &LayoutContext,
1917 ) -> crate::widget::LayoutResponse {
1918 proposal.resolve(0.0, 0.0).into()
1919 }
1920 }
1921
1922 struct Probe {
1923 tree: WidgetTree,
1924 leaves: Vec<WidgetId>,
1925 tick: Signal<u64>,
1926 moved: Signal<bool>,
1927 }
1928
1929 impl Probe {
1930 fn rebuild_driver(&mut self) {
1931 self.tick.set(self.tick.get() + 1);
1932 self.tree.layout(SizeProposal::exact(100.0, 100.0));
1933 }
1934 }
1935
1936 /// A panel with `focusable` focusable leaves, plus a sibling driver that
1937 /// calls `focus_into` on it from `build`. `outside` is focusable and lives
1938 /// outside the panel, so "focus did not move" is observable.
1939 fn probe(focusable: usize) -> (Probe, WidgetId) {
1940 let leaves = Signal::new(Vec::new());
1941 let panel_id = Signal::new(None);
1942 let tick = Signal::new(0_u64);
1943 let moved = Signal::new(false);
1944
1945 let mut tree = WidgetTree::new();
1946 let outside = tree.add(Leaf { focusable: true });
1947 let panel = tree.add(Panel {
1948 focusable,
1949 leaves: leaves.clone(),
1950 });
1951 panel_id.set(Some(panel));
1952 tree.add(Driver {
1953 panel: panel_id,
1954 tick: tick.clone(),
1955 moved: moved.clone(),
1956 });
1957 tree.layout(SizeProposal::exact(100.0, 100.0));
1958 (
1959 Probe {
1960 tree,
1961 leaves: leaves.get(),
1962 tick,
1963 moved,
1964 },
1965 outside,
1966 )
1967 }
1968
1969 /// It lands on the first focusable descendant, not on the container.
1970 #[test]
1971 fn focus_into_lands_on_the_first_focusable_descendant() {
1972 let (p, _) = probe(2);
1973 assert!(p.moved.get());
1974 assert_eq!(p.tree.focused(), Some(p.leaves[0]));
1975 }
1976
1977 /// **It is a no-op while focus is already inside** — the property that lets
1978 /// it be called from `build`, which re-runs on every rebuild. A bare
1979 /// `focus` on the first focusable descendant would drag focus back to the
1980 /// first field every time the caller rebuilt for an unrelated reason, which
1981 /// mid-edit is the caret jumping to the start of the line.
1982 #[test]
1983 fn focus_into_leaves_focus_alone_when_it_is_already_inside() {
1984 let (mut p, _) = probe(2);
1985 p.tree.focus(p.leaves[1]);
1986 p.rebuild_driver();
1987 assert!(p.moved.get(), "focus is inside, so the answer is still yes");
1988 assert_eq!(
1989 p.tree.focused(),
1990 Some(p.leaves[1]),
1991 "focus was dragged back to the first focusable child"
1992 );
1993 }
1994
1995 /// A subtree with nothing focusable leaves focus exactly where it was: an
1996 /// empty region never traps it, and the caller is told so.
1997 #[test]
1998 fn focus_into_an_unfocusable_subtree_moves_nothing() {
1999 let (mut p, outside) = probe(0);
2000 p.tree.focus(outside);
2001 p.rebuild_driver();
2002 assert!(!p.moved.get());
2003 assert_eq!(p.tree.focused(), Some(outside));
2004 }
2005}