Skip to main content

teksilo_core/widget/
event_context.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4use crate::widget_id::WidgetId;
5
6use super::CursorIcon;
7
8/// Selects which overlay-dismissal pathway runs after a handler
9/// returns. Last-write-wins: each `dismiss_*_overlays()` setter
10/// overwrites the previous choice. `None` (the default) falls
11/// through to draining individual ids from `overlay_dismissals`.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub(crate) enum DismissScope {
14    /// Dismiss every overlay in the stack, including hosts.
15    All,
16    /// Dismiss every overlay whose content is *not* a host surface
17    /// (`Tooltip`, `Dialog`, `AlertDialog`). Used by popover triggers
18    /// and pre-show cleanup.
19    AllExceptHosts,
20    /// Walk up from the source widget's containing overlay,
21    /// dismissing menu-like overlays and stopping at the first host
22    /// surface. Used by menu / dropdown item activation.
23    SelfChain,
24    /// Dismiss the topmost overlay only.
25    Top,
26}
27
28/// One queued "reveal this rectangle" request, drained after the handler
29/// returns and turned into a [`WidgetEvent::ScrollIntoView`] per clipping
30/// ancestor.
31///
32/// A struct rather than a tuple because the three modifiers (margin, alignment,
33/// motion) are independent and positional tuples of that width stop being
34/// readable at the call site.
35///
36/// [`WidgetEvent::ScrollIntoView`]: crate::event::WidgetEvent::ScrollIntoView
37#[derive(Debug, Clone, Copy, PartialEq)]
38pub(crate) struct ScrollRevealRequest {
39    /// The target, in absolute tree (window) coordinates.
40    pub(crate) rect: teksilo_canvas::Rect,
41    /// Breathing room to keep around the target, in logical pixels.
42    pub(crate) margin: f32,
43    /// Where the target should come to rest vertically.
44    pub(crate) align: crate::event::ScrollAlign,
45    /// Whether to jump or glide.
46    pub(crate) motion: crate::event::ScrollMotion,
47    /// **Whose** ancestors to walk, when that is not the widget whose handler
48    /// queued this.
49    ///
50    /// `None` means the source widget, which is right whenever a widget reveals
51    /// something inside itself. It is wrong — silently — whenever the rect belongs
52    /// to a *different* widget: a find banner's Next button asking for a match in
53    /// the prose, a toolbar revealing a row in the list below it. Those walk the
54    /// button's ancestors, which do not include the scroll container the rect lives
55    /// in, so nothing scrolls and the reveal is a no-op no error reports.
56    pub(crate) from: Option<crate::widget_id::WidgetId>,
57}
58
59/// Context available during event handling.
60pub struct EventContext<'ops> {
61    pub(crate) cursor_request: Option<CursorIcon>,
62    pub(crate) tree_mutations: Vec<TreeMutation>,
63    pub(crate) idle_callbacks: Vec<crate::idle::IdleCallback>,
64    pub(crate) modal_requests: Vec<crate::modal::ModalRequest>,
65    pub(crate) dismiss_modal: bool,
66    pub(crate) overlay_requests: Vec<crate::overlay::OverlayRequest>,
67    pub(crate) overlay_dismissals: Vec<crate::overlay::OverlayId>,
68    /// Content widget ids whose currently-shown overlay (if any) should
69    /// be dismissed. Resolved to an `OverlayId` via
70    /// `OverlayManager::find_by_content` at drain time. Lets a handler
71    /// dismiss an overlay it can only identify by content (e.g. a single
72    /// reusable tooltip surface) — the symmetric companion to
73    /// [`cancel_delayed_overlay`](EventContext::cancel_delayed_overlay).
74    pub(crate) overlay_content_dismissals: Vec<crate::widget_id::WidgetId>,
75    /// Overlay ids whose `auto_dismiss_after` timer should be paused
76    /// or resumed after the handler returns (`true` = pause, `false`
77    /// = resume). Drained by `WidgetTree::collect_from_ctx` against
78    /// `OverlayManager::pause_auto_dismiss` / `resume_auto_dismiss`,
79    /// after the dismissals in the same drain: a pause aimed at an
80    /// overlay the same handler dismissed is silently dropped. Used
81    /// by `ToastHost` for hover-pause.
82    pub(crate) overlay_pause_requests: Vec<(crate::overlay::OverlayId, bool)>,
83    /// The dismissal scope chosen by the handler, if any. Set by
84    /// `dismiss_all_overlays()` / `dismiss_all_except_hosts()` /
85    /// `dismiss_self_overlay_chain()` / `dismiss_top_overlay()` —
86    /// last setter wins. `None` falls through to draining the
87    /// per-id `overlay_dismissals` vec instead.
88    pub(crate) dismiss_scope: Option<DismissScope>,
89    /// Request to capture or release the pointer.
90    pub(crate) pointer_capture: Option<bool>,
91    /// Delayed overlay requests (request, delay, optional focus target,
92    /// whether to dismiss sibling overlays when it finally shows).
93    pub(crate) delayed_overlay_requests: Vec<(
94        crate::overlay::OverlayRequest,
95        std::time::Duration,
96        Option<crate::widget_id::WidgetId>,
97        bool,
98    )>,
99    /// Timed overlay requests (request, auto-dismiss delay).
100    pub(crate) timed_overlay_requests: Vec<(crate::overlay::OverlayRequest, std::time::Duration)>,
101    /// Reveal overlay requests (request, caller-owned animated progress
102    /// signal, tween duration). The framework shows the overlay, then
103    /// drives `progress` 0 → 1 on show and 1 → 0 on dismiss, deferring
104    /// the actual stack removal until the roll-back tween completes —
105    /// the same deferral the fade path uses, minus the opacity scope.
106    /// The caller applies `progress` however it wants (e.g. an `Unroll`
107    /// width). See [`show_overlay_with_reveal`](EventContext::show_overlay_with_reveal).
108    pub(crate) reveal_overlay_requests: Vec<(
109        crate::overlay::OverlayRequest,
110        crate::signal::Signal<f32>,
111        std::time::Duration,
112    )>,
113    /// Dismiss descendant overlays of the source widget's containing overlay.
114    /// Optionally preserve the subtree rooted at a specific content widget ID.
115    pub(crate) dismiss_descendant_overlays: Vec<Option<crate::widget_id::WidgetId>>,
116    /// Cancel pending delayed overlays by content widget ID.
117    pub(crate) cancel_delayed_overlays: Vec<crate::widget_id::WidgetId>,
118    /// Overlays whose safe triangle should be armed at the current
119    /// pointer position once this handler returns. See
120    /// [`EventContext::arm_overlay_safe_region`].
121    pub(crate) safe_region_arm_requests: Vec<crate::widget_id::WidgetId>,
122    /// Widget IDs that need repainting (cross-widget signal propagation).
123    pub(crate) repaint_requests: Vec<crate::widget_id::WidgetId>,
124    /// Synthetic clicks to dispatch on target widgets after event processing.
125    pub(crate) synthetic_clicks: Vec<crate::widget_id::WidgetId>,
126    /// Focus requests — transfer focus to a specific widget (e.g., overlay content on open).
127    pub(crate) focus_requests: Vec<crate::widget_id::WidgetId>,
128    /// Focus-into requests — move focus to the *first focusable descendant* of
129    /// the given widget, with no fallback to the widget itself when the subtree
130    /// has none. The "dive into this region's content" intent (Enter on a tab
131    /// header → into the tab panel), distinct from `focus_requests` which
132    /// focuses the container itself as a last resort.
133    pub(crate) focus_into_requests: Vec<crate::widget_id::WidgetId>,
134    /// Rect-based "scroll this into view" requests, in **absolute tree
135    /// (window) coordinates**. Queued by [`ensure_visible`](EventContext::ensure_visible)
136    /// / [`ensure_visible_with_margin`](EventContext::ensure_visible_with_margin).
137    /// Drained in `collect_from_ctx`, which walks the ancestors of the widget
138    /// whose handler queued the request and dispatches
139    /// [`WidgetEvent::ScrollIntoView`](crate::event::WidgetEvent::ScrollIntoView)
140    /// to every `clips_children` scroll container that doesn't already fully
141    /// contain the rect — the same ancestor-walk the focus path uses, but
142    /// with a caller-supplied rectangle instead of a widget's own bounds
143    /// (so a caret, a virtualized row, or a scrolled-off tab header can be
144    /// revealed even though it is not itself a distinct focused node).
145    pub(crate) scroll_into_view_requests: Vec<ScrollRevealRequest>,
146    /// Widget-id-based "scroll this into view" requests, queued by
147    /// [`ensure_widget_visible`](EventContext::ensure_widget_visible) /
148    /// [`ensure_widget_visible_with_margin`](EventContext::ensure_widget_visible_with_margin).
149    /// Drained in `collect_from_ctx`, which resolves each id to its current
150    /// absolute arena bounds and walks *that widget's* ancestors (the target
151    /// widget itself excluded) dispatching
152    /// [`WidgetEvent::ScrollIntoView`](crate::event::WidgetEvent::ScrollIntoView).
153    /// The convenience form of the rect API for a target that is a real
154    /// mounted, non-virtualized child (a radio tile, a tab header) whose bounds
155    /// the framework already knows — the caller need not compute the rect.
156    pub(crate) scroll_widget_into_view_requests: Vec<(crate::widget_id::WidgetId, f32)>,
157    /// Keyboard-highlight tooltip requests: surface the tooltip of the given
158    /// (menu) item immediately and dismiss the previously-highlighted one.
159    /// Drained after the handler (see `event_dispatch_impl`). Only the last
160    /// entry per handler is honoured — a handler sets one highlight per key.
161    pub(crate) highlight_tooltip_requests: Vec<crate::widget_id::WidgetId>,
162    /// Drag start request: (source_widget_id, payload, optional_preview_widget).
163    pub(crate) drag_start_request: Option<(
164        crate::widget_id::WidgetId,
165        crate::drag_payload::DragPayload,
166        Option<Box<dyn crate::widget::Widget>>,
167    )>,
168    /// Cancel any active drag session.
169    pub(crate) cancel_drag: bool,
170    /// Whether the drag session active while this context is live was
171    /// started by an external (OS) drag. Read via `drag_is_external()`.
172    /// `false` for hand-constructed contexts and when no drag is active.
173    pub(crate) drag_is_external: bool,
174    /// Replace the tree-level theme. Drained after dispatch; triggers a
175    /// composite-widget rebuild and full repaint.
176    pub(crate) theme_request: Option<crate::styles::Theme>,
177    /// Request that the app follow the OS theme (native / system mode).
178    /// Drained after dispatch; the app switches to `ThemeMode::Native` and
179    /// recomputes the theme from the current OS colours. Parameterless so
180    /// `teksilo-widgets` never needs the app-layer `ThemeMode` enum.
181    pub(crate) follow_system_request: bool,
182    /// Replace the tree-level locale identifier. Drained after dispatch;
183    /// triggers a composite-widget rebuild and full repaint.
184    pub(crate) locale_request: Option<String>,
185    /// Set the user-controlled text-scale factor. Drained after dispatch and
186    /// fanned out to every window; grows all text without a rebuild.
187    pub(crate) text_scale_request: Option<f32>,
188    /// Set by `request_frame()`; consumed by the event dispatcher which
189    /// forwards it to `WidgetTree::request_frame()` so the next layout
190    /// pass advances the per-frame tick signal.
191    pub(crate) frame_requested: bool,
192    /// Optional reference to the tree's app-state registry, so handlers
193    /// can look up application-scoped values via `app_state::<T>()`.
194    /// Populated by the dispatcher before running each handler; `None`
195    /// for hand-constructed contexts in tests.
196    pub(crate) app_context: Option<std::rc::Rc<crate::event_source::TreeAppContext>>,
197    /// App-level window-ops sink. Injected by the dispatcher so
198    /// handlers can reach the multi-window API (`open_window`,
199    /// `focus_window`, …) synchronously. For `EventContext`
200    /// instances constructed outside a dispatch (standalone trees,
201    /// tests) this is `None` and the multi-window methods no-op /
202    /// return `None`.
203    pub(crate) window_ops: Option<&'ops mut dyn crate::window::WindowOps>,
204    /// [`WindowState`](crate::window::WindowState) for the window
205    /// this tree belongs to. Cloned from the tree at construction.
206    /// `None` for standalone trees.
207    pub(crate) current_window: Option<crate::window::WindowState>,
208    /// Snapshot of the tree's occlusion-aware window-active state
209    /// (`focused AND not occluded`) at construction time. Distinct from
210    /// `current_window.focused()` (raw OS focus, no occlusion). Read by
211    /// [`window_active`](Self::window_active). Defaults `true` (matches the
212    /// tree's initial value) for standalone / test contexts.
213    pub(crate) tree_window_active: bool,
214    /// Last `PointerMove` position observed by the tree. Snapshotted
215    /// at handler-invocation time so widgets that don't see the live
216    /// pointer event (e.g. an `on_hover` callback that fires on the
217    /// boundary edge) can still query "where is the cursor right
218    /// now". Read by the safe-triangle submenu hover gate.
219    pub(crate) tree_pointer_position: Option<teksilo_canvas::Point>,
220    /// True when the in-flight pointer press's hit target is a strict
221    /// descendant of the widget whose handler is currently running and that
222    /// descendant carries its own tap gesture (a chevron, checkbox, inline
223    /// button). Set per-node by the dispatcher for `PointerDown`/`PointerUp`.
224    /// Read via [`press_claimed_by_interactive_child`](Self::press_claimed_by_interactive_child).
225    pub(crate) press_claimed_by_interactive_child: bool,
226    /// Per-content-widget overlay bounds — and armed safe-triangle apex,
227    /// when the overlay has one — snapshotted at handler invocation. A
228    /// flat vec is fine: open overlays are typically 0–3 per tree. Read
229    /// by [`EventContext::overlay_bounds_for_content`] and
230    /// [`EventContext::overlay_safe_region_armed`].
231    pub(crate) overlay_bounds_snapshot: Vec<(
232        WidgetId,
233        teksilo_canvas::Rect,
234        Option<teksilo_canvas::Point>,
235    )>,
236    /// The widget holding focus when this batch began. Part of the same
237    /// per-dispatch snapshot as the two above, and read by
238    /// [`focused`](EventContext::focused).
239    pub(crate) focused_widget: Option<WidgetId>,
240    /// Intents queued by handlers via `send_intent`. Drained by the
241    /// tree after event dispatch and routed source-widget → root.
242    pub(crate) pending_intents: Vec<crate::intent::Intent>,
243    /// The dispatcher sets this to the appropriate
244    /// [`IntentSource`](crate::telemetry::IntentSource) before
245    /// invoking a typed handler (menu select → `Menu`, AccessKit
246    /// action → `Accessibility`, on_tap / button activation →
247    /// `Handler`, …). `send_intent` reads it and stamps the intent
248    /// before queuing. `None` outside a managed handler — bare
249    /// programmatic sends keep their `Intent::source` value
250    /// (default `Programmatic`).
251    pub(crate) current_source: Option<crate::telemetry::IntentSource>,
252    /// Key-capture callback armed via `ctx.begin_key_capture(...)`.
253    /// The callback + its shared slot are installed on the tree by
254    /// `collect_from_ctx`. Only one per ctx; the last caller wins.
255    pub(crate) pending_key_capture: Option<crate::shortcut::KeyCaptureSlot>,
256    /// Set to request cancellation of any armed key capture.
257    pub(crate) cancel_key_capture: bool,
258    /// Deferred mutations to the tree's [`ShortcutRegistry`](crate::shortcut::ShortcutRegistry),
259    /// typically issued by settings-UI buttons to rebind or clear
260    /// overrides. Applied in `collect_from_ctx` after the handler
261    /// returns.
262    pub(crate) pending_shortcut_mutations: Vec<ShortcutMutation>,
263    /// Requests that the app-level event loop close the window this
264    /// tree belongs to. Drained after dispatch via
265    /// `WidgetTree::take_close_window_request`. Routed through the
266    /// window's close guard (if any) — see
267    /// [`WindowConfig::on_close_requested`](crate::window::WindowConfig::on_close_requested).
268    pub(crate) close_window_requested: bool,
269    /// Like [`close_window_requested`](Self::close_window_requested), but
270    /// **bypasses** the window's close guard. Set by
271    /// [`close_window_forced`](Self::close_window_forced). Drained after
272    /// dispatch via `WidgetTree::take_force_close_request`. The escape
273    /// hatch a confirmation dialog uses once the user confirms.
274    pub(crate) force_close_requested: bool,
275    /// Set by [`request_accessibility_update`](EventContext::request_accessibility_update);
276    /// drained in `collect_from_ctx` to set `WidgetTree::a11y_dirty`, forcing the
277    /// next `sync_accessibility` to re-walk the AccessKit tree. The general lever for a
278    /// composing widget that restructured its subtree in a way that changes the AT tree
279    /// (relayout alone no longer re-walks AT).
280    pub(crate) request_a11y_update: bool,
281    /// Messages queued by [`announce`](EventContext::announce) /
282    /// [`announce_with`](EventContext::announce_with), drained into the tree's
283    /// own live regions by `collect_from_ctx`. See [`crate::announcer`].
284    pub(crate) announcements: Vec<(String, crate::announcer::Politeness)>,
285    /// Layout direction (LTR/RTL) of the hosting tree, snapshotted at
286    /// handler-invocation time by `make_event_context`. Read via
287    /// [`is_rtl`](EventContext::is_rtl) so pointer / keyboard / drag
288    /// handlers can mirror their x-axis logic live — a runtime locale
289    /// switch dirties the tree but does **not** rebuild, so direction
290    /// must be read here rather than captured at `build()` time.
291    /// Defaults to `LeftToRight` for hand-constructed (test) contexts.
292    pub(crate) layout_direction: crate::environment::LayoutDirection,
293    /// Debug-only WCAG 3.2.1 guard: `Some(flag)` where `flag` is set while a
294    /// focus-change dispatch is running. `open_window` / `focus_window` warn if
295    /// invoked while it reads `true` (a focus handler changing context). `None`
296    /// for hand-constructed (test) contexts.
297    pub(crate) in_focus_dispatch: Option<std::rc::Rc<std::cell::Cell<bool>>>,
298}
299
300/// Deferred edit to the tree's shortcut registry, queued on an
301/// `EventContext` and applied in `collect_from_ctx`.
302#[derive(Debug, Clone)]
303pub(crate) enum ShortcutMutation {
304    RebindPrimary {
305        id: String,
306        keystroke: Option<crate::shortcut::KeyStroke>,
307    },
308    RebindSecondary {
309        id: String,
310        keystroke: Option<crate::shortcut::KeyStroke>,
311    },
312    ClearOverride {
313        id: String,
314    },
315}
316
317/// A structural change to the widget tree, deferred until after event dispatch.
318pub(crate) enum TreeMutation {
319    SetDormant(WidgetId),
320    Activate(WidgetId),
321    Destroy(WidgetId),
322    /// Typed mutable access to a mounted widget, applied in
323    /// `apply_tree_mutations` where `&mut arena` is live. The boxed closure
324    /// downcasts the node's `as_any_mut()` to the requested concrete type;
325    /// `dirty` selects the post-mutation re-render level.
326    WithWidgetMut {
327        id: WidgetId,
328        dirty: crate::binding::BindingLevel,
329        apply: Box<dyn FnOnce(&mut dyn std::any::Any)>,
330    },
331    /// Re-run one widget's `build()` **now**, inside `apply_tree_mutations`,
332    /// rather than marking it for the next layout pass. See
333    /// [`EventContext::materialize_now`].
334    MaterializeNow(WidgetId),
335    /// `Space` on a data view's focused row: run the keyboard-toggle action
336    /// published inside `row`, or `fallback` when the row publishes none.
337    ///
338    /// Deferred rather than resolved in the handler because finding the action
339    /// means walking the row's subtree, and `EventContext` is a command buffer
340    /// with no view of the arena. Carrying the fallback keeps the decision in
341    /// one place: whether a row has a checkbox is a fact about the tree, not
342    /// something the key handler can know.
343    RowSpaceActivate {
344        row: WidgetId,
345        fallback: std::rc::Rc<dyn Fn()>,
346    },
347}
348
349// Manual `Debug`: the `WithWidgetMut` closure is not `Debug`.
350impl std::fmt::Debug for TreeMutation {
351    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
352        match self {
353            Self::SetDormant(id) => f.debug_tuple("SetDormant").field(id).finish(),
354            Self::Activate(id) => f.debug_tuple("Activate").field(id).finish(),
355            Self::MaterializeNow(id) => f.debug_tuple("MaterializeNow").field(id).finish(),
356            Self::Destroy(id) => f.debug_tuple("Destroy").field(id).finish(),
357            Self::WithWidgetMut { id, dirty, .. } => f
358                .debug_struct("WithWidgetMut")
359                .field("id", id)
360                .field("dirty", dirty)
361                .finish_non_exhaustive(),
362            Self::RowSpaceActivate { row, .. } => f
363                .debug_struct("RowSpaceActivate")
364                .field("row", row)
365                .finish_non_exhaustive(),
366        }
367    }
368}
369
370impl<'ops> EventContext<'ops> {
371    pub(crate) fn new() -> Self {
372        Self {
373            cursor_request: None,
374            tree_mutations: Vec::new(),
375            idle_callbacks: Vec::new(),
376            modal_requests: Vec::new(),
377            dismiss_modal: false,
378            overlay_requests: Vec::new(),
379            overlay_dismissals: Vec::new(),
380            overlay_content_dismissals: Vec::new(),
381            overlay_pause_requests: Vec::new(),
382            dismiss_scope: None,
383            pointer_capture: None,
384            delayed_overlay_requests: Vec::new(),
385            timed_overlay_requests: Vec::new(),
386            reveal_overlay_requests: Vec::new(),
387            dismiss_descendant_overlays: Vec::new(),
388            cancel_delayed_overlays: Vec::new(),
389            safe_region_arm_requests: Vec::new(),
390            repaint_requests: Vec::new(),
391            synthetic_clicks: Vec::new(),
392            focus_requests: Vec::new(),
393            focus_into_requests: Vec::new(),
394            scroll_into_view_requests: Vec::new(),
395            scroll_widget_into_view_requests: Vec::new(),
396            highlight_tooltip_requests: Vec::new(),
397            drag_start_request: None,
398            cancel_drag: false,
399            drag_is_external: false,
400            theme_request: None,
401            follow_system_request: false,
402            locale_request: None,
403            text_scale_request: None,
404            frame_requested: false,
405            app_context: None,
406            pending_intents: Vec::new(),
407            current_source: None,
408            pending_key_capture: None,
409            cancel_key_capture: false,
410            pending_shortcut_mutations: Vec::new(),
411            close_window_requested: false,
412            force_close_requested: false,
413            request_a11y_update: false,
414            announcements: Vec::new(),
415            window_ops: None,
416            current_window: None,
417            tree_window_active: true,
418            tree_pointer_position: None,
419            press_claimed_by_interactive_child: false,
420            overlay_bounds_snapshot: Vec::new(),
421            focused_widget: None,
422            layout_direction: crate::environment::LayoutDirection::LeftToRight,
423            in_focus_dispatch: None,
424        }
425    }
426
427    /// Snapshot the hosting tree's layout direction. Called by
428    /// `make_event_context` once per event batch so x-axis handlers
429    /// (resize, drag-reorder, arrow-key navigation) can mirror under
430    /// RTL without a rebuild.
431    pub(crate) fn with_layout_direction(
432        mut self,
433        direction: crate::environment::LayoutDirection,
434    ) -> Self {
435        self.layout_direction = direction;
436        self
437    }
438
439    /// Layout direction of the hosting tree at dispatch time.
440    pub fn layout_direction(&self) -> crate::environment::LayoutDirection {
441        self.layout_direction
442    }
443
444    /// Whether the hosting tree is laid out right-to-left. Mirrors
445    /// [`LayoutContext::is_rtl`](crate::widget::LayoutContext::is_rtl)
446    /// for the event-dispatch side.
447    pub fn is_rtl(&self) -> bool {
448        self.layout_direction == crate::environment::LayoutDirection::RightToLeft
449    }
450
451    /// The widget that held focus when this event batch began.
452    ///
453    /// A snapshot, not a live read: it answers what the tree's focus was at
454    /// dispatch time, so a handler that has already called
455    /// [`request_focus`](EventContext::request_focus) still sees the old
456    /// value. That is the useful reading for a handler deciding *whether* to
457    /// act on the focused widget.
458    ///
459    /// `None` when nothing is focused, and also for an `EventContext` built
460    /// outside `WidgetTree::make_event_context`, which is what a hand-made
461    /// test context is. Treat it as `None`-safe, like the other snapshots.
462    ///
463    /// The reason this exists: a widget-scoped shortcut fires before the
464    /// focused widget sees the key, so a container that binds a key which its
465    /// own children also handle has no other way to yield to them.
466    /// `MessageBox` is the case that asked for it, where Enter is bound to the
467    /// default button and must not answer for the button the user has actually
468    /// tabbed to.
469    pub fn focused(&self) -> Option<WidgetId> {
470        self.focused_widget
471    }
472
473    /// Attach a per-dispatch snapshot of read-only tree query state
474    /// (current pointer position, overlay bounds, focus). Called by
475    /// `WidgetTree::make_event_context` once per event batch. Test
476    /// `EventContext`s that don't go through that path stay with
477    /// empty snapshots — handlers must treat every read as `None`-
478    /// safe.
479    pub(crate) fn with_query_snapshot(
480        mut self,
481        pointer: Option<teksilo_canvas::Point>,
482        overlays: Vec<(
483            WidgetId,
484            teksilo_canvas::Rect,
485            Option<teksilo_canvas::Point>,
486        )>,
487        focused: Option<WidgetId>,
488    ) -> Self {
489        self.tree_pointer_position = pointer;
490        self.overlay_bounds_snapshot = overlays;
491        self.focused_widget = focused;
492        self
493    }
494
495    /// Attach the app-level window-ops sink and the hosting tree's
496    /// [`WindowState`](crate::window::WindowState). Called by the
497    /// dispatcher once per event batch so handlers can reach the
498    /// multi-window API synchronously.
499    pub(crate) fn with_window_context(
500        mut self,
501        ops: &'ops mut dyn crate::window::WindowOps,
502        current_window: Option<crate::window::WindowState>,
503    ) -> Self {
504        self.window_ops = Some(ops);
505        self.current_window = current_window;
506        self
507    }
508
509    /// Snapshot the tree's occlusion-aware window-active state. Called by
510    /// `make_event_context` once per event batch so handlers can read
511    /// [`window_active`](Self::window_active).
512    pub(crate) fn with_window_active(mut self, active: bool) -> Self {
513        self.tree_window_active = active;
514        self
515    }
516
517    /// Attach the tree's shared "inside focus dispatch" flag (WCAG 3.2.1
518    /// debug guard). See [`EventContext::open_window`].
519    pub(crate) fn with_focus_dispatch_flag(
520        mut self,
521        flag: std::rc::Rc<std::cell::Cell<bool>>,
522    ) -> Self {
523        self.in_focus_dispatch = Some(flag);
524        self
525    }
526
527    /// Debug-only: warn (once per call) if a context change is being made from
528    /// inside a focus-change dispatch — a WCAG 3.2.1 (On Focus) anti-pattern.
529    /// Compiled out entirely in release builds.
530    #[inline]
531    fn warn_if_context_change_in_focus_dispatch(&self, what: &str) {
532        #[cfg(debug_assertions)]
533        if self.in_focus_dispatch.as_ref().is_some_and(|f| f.get()) {
534            eprintln!(
535                "[teksilo a11y] WCAG 3.2.1 (On Focus): `{what}` was called from \
536                 inside an on_focus handler. Changing context (opening/focusing a \
537                 window, navigating) merely because a control received focus \
538                 surprises keyboard users tabbing through the UI. Move this to an \
539                 explicit activation handler (on_tap / on_activate / a shortcut)."
540            );
541        }
542        let _ = what;
543    }
544
545    /// Attach the tree's app-state registry so handlers can look up
546    /// application-scoped values (`ClipboardHandle`, `SharedTypesetter`,
547    /// …). Called by the dispatcher once per event batch.
548    pub(crate) fn with_app_context(
549        mut self,
550        ctx: std::rc::Rc<crate::event_source::TreeAppContext>,
551    ) -> Self {
552        self.app_context = Some(ctx);
553        self
554    }
555
556    /// Record whether the drag session active while this context is live
557    /// originated from an external (OS) drag. Set by `make_event_context`.
558    pub(crate) fn with_drag_external(mut self, is_external: bool) -> Self {
559        self.drag_is_external = is_external;
560        self
561    }
562
563    /// Whether a drag is currently in flight that was started by an external
564    /// (OS) drag-and-drop (files / text / URLs from another application),
565    /// rather than by an in-app `start_drag`. Useful in `on_drag_leave` /
566    /// `on_drag_tick` handlers, which don't receive the payload directly;
567    /// in `on_drag_hover` / `on_drop` prefer `payload.is_external()`.
568    pub fn drag_is_external(&self) -> bool {
569        self.drag_is_external
570    }
571
572    /// Look up an application-scoped value by type. Mirrors
573    /// `BuildContext::app_state`. Returns `None` when the handler was
574    /// invoked without a registry (hand-constructed `EventContext` in
575    /// tests, or when no value of that type was registered).
576    pub fn app_state<T: 'static>(&self) -> Option<&T> {
577        self.app_context
578            .as_ref()
579            .and_then(|ctx| ctx.app_state::<T>())
580    }
581
582    /// Borrow the [`AppEventPoster`](crate::AppEventPoster) installed
583    /// by the framework. Used by integrations that need to post
584    /// typed payloads back to the UI loop from a worker thread
585    /// (`teksilo_platform::file_dialog`'s `RfdAsyncBackend`, future
586    /// async-result features). Returns `None` for hand-constructed
587    /// `EventContext`s in tests.
588    pub fn poster(&self) -> Option<&std::sync::Arc<dyn crate::AppEventPoster>> {
589        self.app_context.as_ref().and_then(|ctx| ctx.poster())
590    }
591
592    /// Ask the tree to pump one more frame after this handler returns.
593    /// Use from event handlers that kick off per-frame work (pending
594    /// document events to drain, drag-select auto-scroll, caret blink
595    /// restart on focus). See `WidgetTree::request_frame` for the
596    /// draw-when-needed contract.
597    pub fn request_frame(&mut self) {
598        self.frame_requested = true;
599    }
600
601    /// Dispatch an [`Intent`](crate::intent::Intent) as if the source
602    /// widget pressed its keyboard shortcut. The framework walks
603    /// source-widget → root after the current handler returns,
604    /// invoking any matching [`Action`](crate::action::Action) it
605    /// finds. Unmatched intents are silently dropped.
606    ///
607    /// The intent's `source` is overridden by the dispatcher's
608    /// current handler-source label (`current_source`) when one is
609    /// active. This is how the framework distinguishes
610    /// `IntentSource::Handler` (button taps, generic on_tap) from
611    /// `IntentSource::Menu`, `IntentSource::Accessibility`, etc.
612    /// Programmatic callers outside any handler pass through with
613    /// `IntentSource::Programmatic` (the default).
614    pub fn send_intent(&mut self, intent: impl Into<crate::intent::Intent>) {
615        let mut intent: crate::intent::Intent = intent.into();
616        if let Some(src) = self.current_source {
617            intent.source = src;
618        }
619        self.pending_intents.push(intent);
620    }
621
622    /// Run a closure with the given `IntentSource` active. Any
623    /// `ctx.send_intent(...)` issued from within the closure will
624    /// be tagged with this source instead of the dispatcher's
625    /// default (`Handler` / `Shortcut` / `Accessibility`).
626    ///
627    /// The previous source is restored after the closure returns.
628    /// Panic during the closure unwinds the dispatcher's whole
629    /// frame, so the EventContext is destroyed before the next
630    /// dispatch — no need for a panic-safe drop guard.
631    ///
632    /// Used by framework widgets that want a more specific source
633    /// label than the default — `MenuItem` wraps its activation
634    /// handler to emit `IntentSource::Menu`, etc.
635    pub fn with_intent_source<R>(
636        &mut self,
637        source: crate::telemetry::IntentSource,
638        f: impl FnOnce(&mut Self) -> R,
639    ) -> R {
640        let prev = self.current_source.replace(source);
641        let r = f(self);
642        self.current_source = prev;
643        r
644    }
645
646    /// Arm a one-shot key-capture callback, returning a
647    /// [`CaptureHandle`](crate::shortcut::CaptureHandle) whose `Drop`
648    /// cancels the capture if it hasn't fired yet. The next `KeyDown`
649    /// bypasses shortcut resolution and invokes the callback with:
650    /// - the captured [`KeyStroke`](crate::shortcut::KeyStroke)
651    /// - mutable access to the registry (rebinds in-place)
652    /// - a mutable [`EventContext`] (emit commands, send intents,
653    ///   dismiss overlays, …)
654    ///
655    /// The handle must be stored somewhere with an appropriate
656    /// lifetime (typically in the calling widget's state) or the
657    /// capture will be cancelled immediately when the returned
658    /// handle drops at end of scope.
659    pub fn begin_key_capture(
660        &mut self,
661        callback: impl FnOnce(
662            crate::shortcut::KeyStroke,
663            &mut crate::shortcut::ShortcutRegistry,
664            &mut EventContext,
665        ) + 'static,
666    ) -> crate::shortcut::CaptureHandle {
667        let slot: crate::shortcut::KeyCaptureSlot =
668            std::rc::Rc::new(std::cell::RefCell::new(Some(Box::new(callback))));
669        self.pending_key_capture = Some(slot.clone());
670        self.cancel_key_capture = false;
671        crate::shortcut::CaptureHandle::new(slot)
672    }
673
674    /// Cancel any key capture armed earlier in this handler or via
675    /// `WidgetTree::begin_key_capture` before the handler ran.
676    pub fn cancel_key_capture(&mut self) {
677        self.pending_key_capture = None;
678        self.cancel_key_capture = true;
679    }
680
681    /// Queue a deferred rebind of the primary keystroke for the
682    /// registered shortcut with the given id. Applied by the tree
683    /// after the current handler returns. Use `None` to explicitly
684    /// unbind the slot.
685    pub fn rebind_shortcut_primary(
686        &mut self,
687        id: impl Into<String>,
688        keystroke: Option<crate::shortcut::KeyStroke>,
689    ) {
690        self.pending_shortcut_mutations
691            .push(ShortcutMutation::RebindPrimary {
692                id: id.into(),
693                keystroke,
694            });
695    }
696
697    /// Queue a deferred rebind of the secondary keystroke for the
698    /// registered shortcut with the given id.
699    pub fn rebind_shortcut_secondary(
700        &mut self,
701        id: impl Into<String>,
702        keystroke: Option<crate::shortcut::KeyStroke>,
703    ) {
704        self.pending_shortcut_mutations
705            .push(ShortcutMutation::RebindSecondary {
706                id: id.into(),
707                keystroke,
708            });
709    }
710
711    /// Queue a deferred clear of any user override for the given
712    /// shortcut id, restoring its declared defaults.
713    pub fn clear_shortcut_override(&mut self, id: impl Into<String>) {
714        self.pending_shortcut_mutations
715            .push(ShortcutMutation::ClearOverride { id: id.into() });
716    }
717
718    /// Request that the application close the window this tree
719    /// belongs to. Drained by the app event loop after the handler
720    /// returns. Typical use: title-bar close button handlers.
721    ///
722    /// This is a *guarded* close: if the window declared a close guard
723    /// via
724    /// [`WindowConfig::on_close_requested`](crate::window::WindowConfig::on_close_requested)
725    /// or [`can_close`](crate::window::WindowConfig::can_close), that
726    /// guard runs first and may veto the close. To skip the guard (e.g.
727    /// from the confirmation dialog the guard itself opened), use
728    /// [`close_window_forced`](Self::close_window_forced).
729    pub fn close_window(&mut self) {
730        self.close_window_requested = true;
731    }
732
733    /// Request that the application close this tree's window
734    /// **unconditionally**, bypassing any close guard declared via
735    /// [`WindowConfig::on_close_requested`](crate::window::WindowConfig::on_close_requested)
736    /// / [`can_close`](crate::window::WindowConfig::can_close).
737    ///
738    /// This is the second half of the veto-then-reissue pattern: the
739    /// guard returns [`CloseResponse::Veto`](crate::window::CloseResponse::Veto)
740    /// and opens a confirmation dialog; the dialog's "close anyway"
741    /// button calls `close_window_forced` so the window actually closes
742    /// without re-triggering the guard.
743    pub fn close_window_forced(&mut self) {
744        self.force_close_requested = true;
745    }
746
747    // -------------------- Multi-window API --------------------
748
749    /// The [`WindowState`](crate::window::WindowState) for the window
750    /// hosting this handler. `None` only for handlers run outside
751    /// of an app (hand-constructed `EventContext` in tests).
752    /// Cursor position at the moment this handler was invoked. `None`
753    /// when no `PointerMove` has reached the tree yet, or when the
754    /// context was constructed without a tree-side snapshot (e.g.
755    /// hand-built `EventContext`s in tests). Used by the safe-triangle
756    /// submenu hover gate.
757    pub fn tree_pointer_position(&self) -> Option<teksilo_canvas::Point> {
758        self.tree_pointer_position
759    }
760
761    /// True when the in-flight pointer press's hit target is a strict
762    /// descendant of THIS handler's widget that carries its own tap gesture
763    /// (chevron, checkbox, inline button). A row/container that selects on
764    /// press should early-return `EventResponse::Ignored` when this is set, so
765    /// the press belongs to the inner control, not the row. Only meaningful
766    /// inside `on_pointer_event` handlers for `PointerDown`/`PointerUp`.
767    pub fn press_claimed_by_interactive_child(&self) -> bool {
768        self.press_claimed_by_interactive_child
769    }
770
771    /// Look up the bounds rect of an open overlay by its root content
772    /// widget id. Returns `None` when no such overlay is currently
773    /// active. The snapshot is taken once per dispatch; mid-handler
774    /// `show_overlay` calls will not appear here. Used by the
775    /// safe-triangle submenu hover gate.
776    pub fn overlay_bounds_for_content(&self, content_id: WidgetId) -> Option<teksilo_canvas::Rect> {
777        self.overlay_bounds_snapshot
778            .iter()
779            .find(|(cid, _, _)| *cid == content_id)
780            .map(|(_, r, _)| *r)
781    }
782
783    /// Whether a safe-triangle traversal toward the overlay rooted at
784    /// `content_id` is still live — i.e. whether the user may still be on
785    /// their way to that submenu.
786    ///
787    /// A widget whose hover would otherwise tear the overlay down (a
788    /// sibling menu row switching the selection) asks this first and
789    /// stands aside while it is `true`, leaving the dismissal to the
790    /// overlay's own pointer-leave grace — which tests the cone on every
791    /// sample and closes the overlay one `delay` after the pointer stops
792    /// heading there.
793    ///
794    /// **This is deliberately the armed window, not a point-in-cone
795    /// test.** A sibling row's hover fires exactly once, at the instant
796    /// the pointer crosses onto it — a pixel or two from the apex, where
797    /// the cone is a needle — so answering "is this one sample inside the
798    /// cone" made a single quantized step final, and any departure
799    /// steeper than the cone (which is most of them, for a wide menu with
800    /// a short submenu) killed the submenu the moment the pointer left
801    /// the trigger row. Whether *this* sample is inside the cone is the
802    /// framework's question, asked continuously; the widget's question is
803    /// only whether to get out of the way.
804    ///
805    /// `false` when no region is armed and when its budget is spent.
806    ///
807    /// Arm the region with
808    /// [`arm_overlay_safe_region`](Self::arm_overlay_safe_region).
809    pub fn overlay_safe_region_armed(&self, content_id: WidgetId) -> bool {
810        self.overlay_bounds_snapshot
811            .iter()
812            .find(|(cid, _, _)| *cid == content_id)
813            .is_some_and(|(_, _, apex)| apex.is_some())
814    }
815
816    pub fn window(&self) -> Option<&crate::window::WindowState> {
817        self.current_window.as_ref()
818    }
819
820    /// Whether the host window is currently active (`focused AND not
821    /// occluded`) — the occlusion-aware companion to
822    /// `self.window().map(|w| w.focused())` (raw OS focus). Snapshotted at
823    /// context construction. Matches [`BuildContext::window_active`].
824    ///
825    /// [`BuildContext::window_active`]: crate::build_context::BuildContext::window_active
826    pub fn window_active(&self) -> bool {
827        self.tree_window_active
828    }
829
830    /// Open a new window, creating the winit-level surface
831    /// synchronously. The returned id is immediately valid for
832    /// [`focus_window`](Self::focus_window),
833    /// [`window_state`](Self::window_state), and
834    /// [`find_window`](Self::find_window).
835    ///
836    /// Panics when called from a handler on a standalone `WidgetTree`
837    /// (no app context) — tests should not invoke this method.
838    pub fn open_window(
839        &mut self,
840        config: crate::window::WindowConfig,
841    ) -> crate::window::TeksiloWindowId {
842        self.warn_if_context_change_in_focus_dispatch("open_window");
843        self.window_ops
844            .as_deref_mut()
845            .expect("open_window called outside of a dispatch")
846            .open_window(config)
847    }
848
849    /// Find a window by the string id assigned via
850    /// [`WindowConfig::id`](crate::window::WindowConfig::id). Returns
851    /// `None` if no open window carries that id.
852    pub fn find_window(&self, string_id: &str) -> Option<crate::window::TeksiloWindowId> {
853        self.window_ops.as_deref()?.find_window(string_id)
854    }
855
856    /// Read the [`WindowState`](crate::window::WindowState) for a
857    /// specific window.
858    pub fn window_state(
859        &self,
860        id: crate::window::TeksiloWindowId,
861    ) -> Option<crate::window::WindowState> {
862        self.window_ops.as_deref()?.window_state(id)
863    }
864
865    /// Snapshot of every live window's state.
866    pub fn windows(&self) -> Vec<crate::window::WindowState> {
867        self.window_ops
868            .as_deref()
869            .map(|o| o.windows())
870            .unwrap_or_default()
871    }
872
873    /// Raise a window to the front and give it keyboard focus.
874    pub fn focus_window(&mut self, id: crate::window::TeksiloWindowId) {
875        self.warn_if_context_change_in_focus_dispatch("focus_window");
876        if let Some(ops) = self.window_ops.as_deref_mut() {
877            ops.focus_window(id);
878        }
879    }
880
881    /// Request an xdg-activation token for `id` (see
882    /// [`WindowOps::request_activation_token`](crate::window::WindowOps::request_activation_token)).
883    /// `cb` fires once with the token string, or `None` where the platform can't
884    /// provide one — used to hand a token to a child process ("open in new
885    /// window") or an IPC peer that will raise itself on Wayland.
886    pub fn request_activation_token(
887        &mut self,
888        id: crate::window::TeksiloWindowId,
889        cb: Box<dyn FnOnce(Option<String>)>,
890    ) {
891        if let Some(ops) = self.window_ops.as_deref_mut() {
892            ops.request_activation_token(id, cb);
893        } else {
894            cb(None);
895        }
896    }
897
898    /// Request an activation token for the **current** window (see
899    /// [`WindowOps::request_activation_token_self`](crate::window::WindowOps::request_activation_token_self)).
900    /// Use this from a widget handler to mint a token from *this* focused window
901    /// to hand to another window or process — it works mid-dispatch, unlike the
902    /// id-based variant.
903    pub fn request_activation_token_self(&mut self, cb: Box<dyn FnOnce(Option<String>)>) {
904        if let Some(ops) = self.window_ops.as_deref_mut() {
905            ops.request_activation_token_self(cb);
906        } else {
907            cb(None);
908        }
909    }
910
911    /// Close a specific window by id. Equivalent to
912    /// [`close_window`](Self::close_window) when `id` is the current
913    /// window's id.
914    pub fn close_window_by_id(&mut self, id: crate::window::TeksiloWindowId) {
915        if let Some(ops) = self.window_ops.as_deref_mut() {
916            ops.close_window_by_id(id);
917        }
918    }
919
920    /// Report the focused text widget's caret rectangle (window-logical
921    /// pixels) so the platform can position the OS IME candidate window at
922    /// the insertion point. Text-editing widgets call this whenever the
923    /// caret moves. No-op outside a dispatch / on a standalone tree.
924    pub fn set_ime_cursor_area(&mut self, area: teksilo_canvas::Rect) {
925        if let Some(ops) = self.window_ops.as_deref_mut() {
926            ops.set_ime_cursor_area(area);
927        }
928    }
929
930    /// Resolve the platform parent handle of the window currently
931    /// dispatching the event. Used by native-dialog integrations
932    /// (`teksilo_platform::file_dialog`) to parent OS dialogs to the
933    /// originating Teksilo window.
934    ///
935    /// Returns `None` when called from a standalone `WidgetTree` (no
936    /// app-level `WindowOps` sink), or when the platform refuses to
937    /// surface a handle (rare; mostly during teardown).
938    pub fn parent_window_handle(&self) -> Option<crate::raw_handle::ParentHandle> {
939        self.window_ops.as_deref()?.current_parent_handle()
940    }
941
942    /// Request a cursor icon change.
943    pub fn set_cursor(&mut self, cursor: CursorIcon) {
944        self.cursor_request = Some(cursor);
945    }
946
947    /// Set a widget subtree as dormant (preserves state, releases rendering).
948    pub fn set_dormant(&mut self, id: WidgetId) {
949        self.tree_mutations.push(TreeMutation::SetDormant(id));
950    }
951
952    /// Activate a dormant widget subtree.
953    pub fn activate(&mut self, id: WidgetId) {
954        self.tree_mutations.push(TreeMutation::Activate(id));
955    }
956
957    /// Destroy a widget subtree (removes from arena entirely, state is gone).
958    pub fn destroy(&mut self, id: WidgetId) {
959        self.tree_mutations.push(TreeMutation::Destroy(id));
960    }
961
962    /// Imperatively mutate a mounted widget by id, downcasting to the
963    /// concrete type `W`.
964    ///
965    /// The mutation is **deferred**: the closure runs after the handler
966    /// returns, inside `apply_tree_mutations`, where the framework holds
967    /// `&mut` arena access (a handler cannot re-borrow the arena to reach
968    /// another node, so this is the only safe channel — the same model as
969    /// [`destroy`](Self::destroy)). After the closure runs, the target is
970    /// dirty-marked at `dirty` so the mutation takes visual effect.
971    ///
972    /// The target widget must override `Widget::as_any_mut` to return
973    /// `Some(self)`. If the id is gone or is not a `W`, the closure is a
974    /// no-op in release and a `debug_assert` failure in debug — it never
975    /// silently mutates the wrong widget.
976    ///
977    /// Use it for per-view state a handler can't otherwise reach — e.g.
978    /// `SceneView::ensure_visible(...)` (camera) after the view is mounted:
979    /// ```ignore
980    /// ctx.with_widget_mut::<SceneView>(view_id, BindingLevel::Relayout, |v| {
981    ///     v.ensure_visible(card_rect, 40.0);
982    /// });
983    /// ```
984    /// For scene *content*, prefer the shared `SceneModel` handle (`view.model()`)
985    /// — its mutators are `&self`, so a handler holding a clone can drive the
986    /// scene directly and every attached view reconciles, no `with_widget_mut`
987    /// needed.
988    pub fn with_widget_mut<W: 'static>(
989        &mut self,
990        id: WidgetId,
991        dirty: crate::binding::BindingLevel,
992        f: impl FnOnce(&mut W) + 'static,
993    ) {
994        self.tree_mutations.push(TreeMutation::WithWidgetMut {
995            id,
996            dirty,
997            apply: Box::new(move |any| match any.downcast_mut::<W>() {
998                Some(w) => f(w),
999                None => debug_assert!(
1000                    false,
1001                    "with_widget_mut: widget {id:?} is not the requested type (or does not \
1002                     override Widget::as_any_mut)"
1003                ),
1004            }),
1005        });
1006    }
1007
1008    /// Re-run one widget's `build()` **during this handler's drain**, before
1009    /// overlays are shown and before focus requests are applied — rather than
1010    /// dirty-marking it for the next layout pass, which is what every other
1011    /// rebuild trigger does.
1012    ///
1013    /// It exists for one shape:
1014    /// [`DeferredSubtree`](crate::deferred_subtree::DeferredSubtree) content
1015    /// that a handler is *about to depend on*. Opening a popover activates its
1016    /// content, shows an overlay anchored to it, and moves focus into it — all
1017    /// three inside the same drain (see `collect_from_ctx`). A deferred panel
1018    /// marked for rebuild would not exist yet at any of those points: the
1019    /// overlay would be measured against an empty node and
1020    /// `first_focusable_descendant` would find nothing to focus, so the popover
1021    /// would open in the wrong place and swallow the keyboard. Materializing
1022    /// here closes that window, and makes deferred content behave exactly like
1023    /// the eagerly-built content it replaces.
1024    ///
1025    /// Cheap to call redundantly: a `DeferredSubtree` that is already
1026    /// materialized returns its existing child, so a second open costs one
1027    /// `build()` of the host and nothing below it.
1028    ///
1029    /// Not a general "rebuild this widget now" door — reach for
1030    /// [`with_widget_mut`](Self::with_widget_mut) or a `Rebuild` binding for
1031    /// ordinary reactive updates, which are correctly served by the next
1032    /// layout pass.
1033    /// `Space` on a data view's focused row: activate the row's published
1034    /// keyboard toggle — the checkbox `StandardListItem` embeds, most often —
1035    /// or run `fallback` when the row publishes none.
1036    ///
1037    /// A row's controls are out of the Tab order, so this is the only keyboard
1038    /// route to them; `fallback` is what `Space` means on a row without one,
1039    /// which for the data views is "toggle the selection".
1040    pub fn row_space_activate(&mut self, row: WidgetId, fallback: std::rc::Rc<dyn Fn()>) {
1041        self.tree_mutations
1042            .push(TreeMutation::RowSpaceActivate { row, fallback });
1043    }
1044
1045    pub fn materialize_now(&mut self, id: WidgetId) {
1046        self.tree_mutations.push(TreeMutation::MaterializeNow(id));
1047    }
1048
1049    /// Request that the AccessKit tree be re-walked after this handler
1050    /// returns. Use after a mutation that changes the accessibility tree
1051    /// **shape** in a way the framework doesn't already detect (relayout
1052    /// alone no longer re-walks AT; only events that change the AT tree
1053    /// — focus, overlays, locale/shortcut rebinds — set the dirty flag).
1054    /// The companion `BuildContext::request_accessibility_update` covers
1055    /// the build-time path.
1056    pub fn request_accessibility_update(&mut self) {
1057        self.request_a11y_update = true;
1058    }
1059
1060    /// Speak `message` to the screen reader, politely.
1061    ///
1062    /// For anything the user needs told that is not the name of a widget: a
1063    /// completed action, a new count, the result of an undo, a row that moved.
1064    /// Sighted users read those off the screen; a screen-reader user is told
1065    /// only what the framework says out loud.
1066    ///
1067    /// ```ignore
1068    /// ctx.announce(tr!(event_added(title = title.clone())));
1069    /// ```
1070    ///
1071    /// Takes `impl Into<String>`, so `tr!(…)` works directly.
1072    /// `LocalizedString` is deliberately not the parameter type: an
1073    /// announcement is an event, not a label, and
1074    /// re-resolving it on a later language switch would re-speak it. See
1075    /// [`crate::announcer`].
1076    ///
1077    /// **Do not pair this with a toast on the same path.** `Toast` is already a
1078    /// correct live region, so doing both says everything twice.
1079    pub fn announce(&mut self, message: impl Into<String>) {
1080        self.announce_with(message, crate::announcer::Politeness::Polite);
1081    }
1082
1083    /// Speak `message` to the screen reader at the given urgency.
1084    ///
1085    /// [`Politeness::Assertive`](crate::announcer::Politeness::Assertive)
1086    /// interrupts whatever is being spoken. Reserve it for something the user
1087    /// must not miss and cannot recover by re-reading the screen — a failure, a
1088    /// refusal, a destructive result. Everything else is
1089    /// [`Polite`](crate::announcer::Politeness::Polite), which is what
1090    /// [`announce`](Self::announce) uses.
1091    pub fn announce_with(
1092        &mut self,
1093        message: impl Into<String>,
1094        politeness: crate::announcer::Politeness,
1095    ) {
1096        self.announcements.push((message.into(), politeness));
1097    }
1098
1099    /// Show an overlay (tooltip, menu, popover).
1100    pub fn show_overlay(&mut self, request: crate::overlay::OverlayRequest) {
1101        self.overlay_requests.push(request);
1102    }
1103
1104    /// Show an overlay whose reveal/dismiss is animated by a
1105    /// caller-owned progress signal.
1106    ///
1107    /// `progress` must be an animated `Signal<f32>` (created with
1108    /// [`Signal::new_animated`](crate::signal::Signal::new_animated) or
1109    /// [`BuildContext::animated_signal`](crate::build_context::BuildContext::animated_signal)).
1110    /// The framework shows the overlay, tweens `progress` 0 → 1 over
1111    /// `duration`, and on any dismiss path tweens it 1 → 0 while
1112    /// **deferring** the overlay's removal (and its content's dormancy)
1113    /// until the roll-back completes — the same window the fade path
1114    /// uses, but with no opacity applied. The caller binds `progress`
1115    /// to whatever paints the reveal (e.g. an
1116    /// [`Unroll`](https://docs.rs/teksilo) width), and is responsible for
1117    /// resetting it to `0.0` before the show if a prior reveal left it
1118    /// at `1.0`.
1119    ///
1120    /// Under `prefers-reduced-motion`, skip this and use
1121    /// [`show_overlay`](Self::show_overlay) with the progress pinned at
1122    /// `1.0` so there is no tween and dismissal is immediate.
1123    pub fn show_overlay_with_reveal(
1124        &mut self,
1125        request: crate::overlay::OverlayRequest,
1126        progress: crate::signal::Signal<f32>,
1127        duration: std::time::Duration,
1128    ) {
1129        self.reveal_overlay_requests
1130            .push((request, progress, duration));
1131    }
1132
1133    /// Show an overlay that dismisses automatically after `duration`.
1134    pub fn show_overlay_for(
1135        &mut self,
1136        request: crate::overlay::OverlayRequest,
1137        duration: std::time::Duration,
1138    ) {
1139        self.timed_overlay_requests.push((request, duration));
1140    }
1141
1142    /// Dismiss an overlay by ID.
1143    pub fn dismiss_overlay(&mut self, id: crate::overlay::OverlayId) {
1144        self.overlay_dismissals.push(id);
1145    }
1146
1147    /// Dismiss the currently-shown overlay whose content root is
1148    /// `content_id`, if one is active. No-op when no overlay is showing
1149    /// that content. Use this to dismiss an overlay you can only name by
1150    /// its content widget — the symmetric companion to
1151    /// [`cancel_delayed_overlay`](Self::cancel_delayed_overlay), which
1152    /// cancels a *pending* delayed show for the same content. Together
1153    /// they let a caller fully retract a reusable tooltip surface
1154    /// (shown or pending) without tracking the `OverlayId`.
1155    pub fn dismiss_overlay_by_content(&mut self, content_id: crate::widget_id::WidgetId) {
1156        self.overlay_content_dismissals.push(content_id);
1157    }
1158
1159    /// Queue a request to pause an overlay's `auto_dismiss_after`
1160    /// timer. Drained by the framework after this handler returns —
1161    /// equivalent to calling
1162    /// [`OverlayManager::pause_auto_dismiss`](crate::overlay::OverlayManager::pause_auto_dismiss)
1163    /// at the next safe point. Idempotent.
1164    ///
1165    /// Used by `ToastHost` for hover-pause: on pointer-enter the
1166    /// host queues `pause_overlay_auto_dismiss(id)` for every live
1167    /// toast; on pointer-leave it queues `resume_overlay_auto_dismiss`.
1168    pub fn pause_overlay_auto_dismiss(&mut self, id: crate::overlay::OverlayId) {
1169        self.overlay_pause_requests.push((id, true));
1170    }
1171
1172    /// Queue a request to resume an overlay's `auto_dismiss_after`
1173    /// timer paused via
1174    /// [`pause_overlay_auto_dismiss`](Self::pause_overlay_auto_dismiss).
1175    /// Idempotent on un-paused overlays.
1176    pub fn resume_overlay_auto_dismiss(&mut self, id: crate::overlay::OverlayId) {
1177        self.overlay_pause_requests.push((id, false));
1178    }
1179
1180    /// Dismiss all active overlays (e.g., after a menu item is activated).
1181    pub fn dismiss_all_overlays(&mut self) {
1182        self.dismiss_scope = Some(DismissScope::All);
1183    }
1184
1185    /// Dismiss the source widget's containing overlay and any ancestor
1186    /// overlays in the chain that are menu-like (anything that isn't a
1187    /// `Role::Tooltip`, `Role::Dialog`, or `Role::AlertDialog`),
1188    /// preserving an outer composite tooltip or modal hosting the
1189    /// popover. Use for menu / dropdown item activation that wants to
1190    /// close the menu cascade without disturbing the host surface.
1191    pub fn dismiss_self_overlay_chain(&mut self) {
1192        self.dismiss_scope = Some(DismissScope::SelfChain);
1193    }
1194
1195    /// Dismiss every overlay whose content is *not* a host surface
1196    /// (`Role::Tooltip`, `Role::Dialog`, `Role::AlertDialog`),
1197    /// preserving an outer composite tooltip or modal hosting the
1198    /// trigger. Use for popover triggers and pre-show cleanup that
1199    /// want to close stale popovers / menus without taking a hosting
1200    /// surface with them.
1201    pub fn dismiss_all_except_hosts(&mut self) {
1202        self.dismiss_scope = Some(DismissScope::AllExceptHosts);
1203    }
1204
1205    /// Dismiss the topmost overlay only (e.g., closing a submenu while
1206    /// keeping the parent menu open).
1207    pub fn dismiss_top_overlay(&mut self) {
1208        self.dismiss_scope = Some(DismissScope::Top);
1209    }
1210
1211    /// Dismiss descendant overlays of the source widget's containing overlay.
1212    /// Useful for closing sibling submenu branches while keeping the current
1213    /// parent menu open.
1214    pub fn dismiss_child_overlays(&mut self) {
1215        self.dismiss_descendant_overlays.push(None);
1216    }
1217
1218    /// Dismiss descendant overlays of the source widget's containing overlay,
1219    /// preserving the subtree rooted at `content_id` if it is already open.
1220    pub fn dismiss_child_overlays_except(&mut self, content_id: crate::widget_id::WidgetId) {
1221        self.dismiss_descendant_overlays.push(Some(content_id));
1222    }
1223
1224    /// Request an idle callback to be run during the next idle period.
1225    /// Use this for incremental work that takes 5-50ms — too short for a
1226    /// background thread, too long for a single frame.
1227    pub fn request_idle_callback(
1228        &mut self,
1229        callback: impl FnOnce(crate::idle::IdleDeadline) + 'static,
1230    ) {
1231        self.idle_callbacks.push(Box::new(callback));
1232    }
1233
1234    /// Request framework-owned modal presentation.
1235    ///
1236    /// The widget tree records the request together with the originating
1237    /// widget, and the application layer can later resolve `Auto` into a
1238    /// concrete presentation backend.
1239    pub fn present_modal(&mut self, request: crate::modal::ModalRequest) {
1240        self.modal_requests.push(request);
1241    }
1242
1243    /// Synchronously open a modal as a native window — the single
1244    /// unified path for native-window modals. Callers that don't
1245    /// care whether the modal lands in-tree or in a native window
1246    /// use [`present_modal`](Self::present_modal), which routes
1247    /// `ModalPresentation::Auto` through the framework's picker.
1248    ///
1249    /// Returns the new window's id, or `None` when called outside a
1250    /// dispatch context (standalone trees). The window's parent is
1251    /// the current window; focus target and title / size from the
1252    /// request are honored.
1253    ///
1254    /// Only `ModalContent::Deferred` is supported here — an
1255    /// `ExistingWidget` id wouldn't make sense in a fresh tree.
1256    pub fn open_modal(
1257        &mut self,
1258        request: crate::modal::ModalRequest,
1259    ) -> Option<crate::window::TeksiloWindowId> {
1260        let parent = self.current_window.as_ref()?.id();
1261        let crate::modal::ModalContent::Deferred(builder) = request.content else {
1262            return None;
1263        };
1264        let mut config = crate::window::WindowConfig::new().modal(crate::window::ModalConfig {
1265            parent,
1266            focus_target: request.focus_target,
1267        });
1268        if let Some(title) = request.title {
1269            config = config.title(title);
1270        }
1271        if let Some((w, h)) = request.size {
1272            config = config.size(w, h);
1273        }
1274        let config = config.root(move |tree, _state| builder(tree));
1275        Some(self.open_window(config))
1276    }
1277
1278    /// Dismiss the current framework-owned modal presentation.
1279    pub fn dismiss_modal(&mut self) {
1280        self.dismiss_modal = true;
1281    }
1282
1283    /// Show an overlay after a delay. The widget tree checks pending delayed
1284    /// overlays during `layout()` and shows them once the delay elapses.
1285    /// Use this for submenu hover-open delays.
1286    ///
1287    /// The content widget should already be added to the tree (typically
1288    /// dormant). It will be activated automatically when the delay elapses.
1289    pub fn show_overlay_after(
1290        &mut self,
1291        request: crate::overlay::OverlayRequest,
1292        delay: std::time::Duration,
1293    ) {
1294        self.delayed_overlay_requests
1295            .push((request, delay, None, false));
1296    }
1297
1298    /// Show an overlay after a delay and move focus when it opens.
1299    pub fn show_overlay_after_with_focus(
1300        &mut self,
1301        request: crate::overlay::OverlayRequest,
1302        delay: std::time::Duration,
1303        focus_target: crate::widget_id::WidgetId,
1304    ) {
1305        self.delayed_overlay_requests
1306            .push((request, delay, Some(focus_target), false));
1307    }
1308
1309    /// Show an overlay after a delay, move focus when it opens, and
1310    /// dismiss the anchor's sibling overlays **at that moment** rather
1311    /// than when the request was made.
1312    ///
1313    /// This is the hover-switch between two submenu triggers in the same
1314    /// menu. Dismissing eagerly at hover-enter closes the submenu the
1315    /// user is still walking toward as soon as the pointer crosses a
1316    /// neighbouring trigger; deferring the dismissal to the moment the
1317    /// new submenu actually opens means a pointer merely passing through
1318    /// costs nothing, and one that settles gets the swap on the same
1319    /// frame — no window with two submenus on screen.
1320    pub fn show_overlay_after_replacing_siblings(
1321        &mut self,
1322        request: crate::overlay::OverlayRequest,
1323        delay: std::time::Duration,
1324        focus_target: crate::widget_id::WidgetId,
1325    ) {
1326        self.delayed_overlay_requests
1327            .push((request, delay, Some(focus_target), true));
1328    }
1329
1330    /// Request a repaint on a specific widget. Use this when an event handler
1331    /// on one widget changes state that affects a different widget's appearance
1332    /// (e.g., keyboard navigation highlighting items in an overlay).
1333    pub fn request_repaint(&mut self, id: crate::widget_id::WidgetId) {
1334        self.repaint_requests.push(id);
1335    }
1336
1337    /// Programmatically click a widget (synthetic PointerDown + PointerUp at
1338    /// its center). Use this for keyboard activation of a child widget, e.g.,
1339    /// Enter on a keyboard-focused menu item.
1340    pub fn synthetic_click(&mut self, id: crate::widget_id::WidgetId) {
1341        self.synthetic_clicks.push(id);
1342    }
1343
1344    /// Transfer focus to a specific widget. Use this when opening overlay
1345    /// content (menus, dialogs) that should receive keyboard events.
1346    pub fn request_focus(&mut self, id: crate::widget_id::WidgetId) {
1347        self.focus_requests.push(id);
1348    }
1349
1350    /// Move focus **into** the content of `id`: focus its first focusable
1351    /// descendant in tab order. Unlike [`request_focus`](Self::request_focus),
1352    /// this does **not** fall back to focusing `id` itself when the subtree has
1353    /// no focusable descendant — it is a no-op in that case, so an empty region
1354    /// never traps focus on a non-interactive container.
1355    ///
1356    /// Use this for "dive into this region" gestures, e.g. pressing Enter on a
1357    /// focused tab header to move focus into the tab's content panel. A panel
1358    /// with focusable content lands on its first control; a panel that opted
1359    /// into focusability itself (no inner controls) lands on the panel; a bare
1360    /// panel with neither leaves focus where it was.
1361    pub fn request_focus_into(&mut self, id: crate::widget_id::WidgetId) {
1362        self.focus_into_requests.push(id);
1363    }
1364
1365    /// Scroll the given rectangle into view inside every enclosing scroll
1366    /// container, walking outward from the widget whose handler is running.
1367    ///
1368    /// `rect` is in **absolute tree (window) coordinates** — the same space
1369    /// the arena stores widget bounds in. After the handler returns, the
1370    /// framework walks the current widget's ancestors and, for each
1371    /// `clips_children` scroll container whose viewport does not already
1372    /// fully contain `rect`, dispatches
1373    /// [`WidgetEvent::ScrollIntoView`](crate::event::WidgetEvent::ScrollIntoView)
1374    /// so the container adjusts its offset. Nested scroll areas each get a
1375    /// turn (outermost included), exactly like the focus-driven path.
1376    ///
1377    /// Unlike the automatic focus follow — which can only reveal a *focused
1378    /// widget's own bounds* — this lets a widget reveal an arbitrary interior
1379    /// rectangle it computed itself: a text caret, a virtualized list/table
1380    /// row (which is not a distinct focusable node), or a scrolled-off tab
1381    /// header. The widget remains responsible for scrolling its *own* interior
1382    /// viewport; `ensure_visible` handles the enclosing containers. It is a
1383    /// no-op when there is no scroll container above the widget, or when every
1384    /// container already shows the rect.
1385    ///
1386    /// See [`ensure_visible_with_margin`](Self::ensure_visible_with_margin) to
1387    /// keep breathing room around the target.
1388    pub fn ensure_visible(&mut self, rect: teksilo_canvas::Rect) {
1389        self.scroll_into_view_requests.push(ScrollRevealRequest {
1390            rect,
1391            margin: 0.0,
1392            align: crate::event::ScrollAlign::Minimal,
1393            motion: crate::event::ScrollMotion::Instant,
1394            from: None,
1395        });
1396    }
1397
1398    /// [`ensure_visible`](Self::ensure_visible), for a rect that belongs to
1399    /// **another** widget.
1400    ///
1401    /// The framework walks `owner`'s ancestors rather than the handling widget's.
1402    /// That distinction is the whole of it, and getting it wrong fails silently:
1403    /// a find banner's Next button sits *beside* the scrolling page, not inside
1404    /// it, so a reveal walked from the button climbs out through the banner and
1405    /// never meets the scroll container the match is in. The match is selected,
1406    /// the counter moves, and the viewport does not follow.
1407    ///
1408    /// The same reasoning [`ensure_widget_visible`](Self::ensure_widget_visible)
1409    /// already records for the id-based form; this is its rect-based twin, for a
1410    /// target that is an interior span rather than a mounted child.
1411    ///
1412    /// `rect` is in absolute tree (window) coordinates.
1413    pub fn ensure_visible_from(
1414        &mut self,
1415        owner: crate::widget_id::WidgetId,
1416        rect: teksilo_canvas::Rect,
1417    ) {
1418        self.scroll_into_view_requests.push(ScrollRevealRequest {
1419            rect,
1420            margin: 0.0,
1421            align: crate::event::ScrollAlign::Minimal,
1422            motion: crate::event::ScrollMotion::Instant,
1423            from: Some(owner),
1424        });
1425    }
1426
1427    /// Like [`ensure_visible`](Self::ensure_visible), but keeps `margin`
1428    /// logical pixels of breathing room around `rect` on every edge, so the
1429    /// target does not sit flush against the viewport boundary (the caret at
1430    /// the bottom line, the selected row at the fold). `rect` is in absolute
1431    /// tree (window) coordinates.
1432    pub fn ensure_visible_with_margin(&mut self, rect: teksilo_canvas::Rect, margin: f32) {
1433        self.scroll_into_view_requests.push(ScrollRevealRequest {
1434            rect,
1435            margin: margin.max(0.0),
1436            align: crate::event::ScrollAlign::Minimal,
1437            motion: crate::event::ScrollMotion::Instant,
1438            from: None,
1439        });
1440    }
1441
1442    /// **Pin** `rect` at `fraction` of the way down the innermost enclosing
1443    /// scroll container — `0.0` flush with the top, `0.5` centred, `1.0` flush
1444    /// with the bottom — instead of merely revealing it.
1445    ///
1446    /// The difference from [`ensure_visible`](Self::ensure_visible) is that this
1447    /// scrolls **even when the target is already visible**. That is what makes
1448    /// it usable for typewriter scrolling: a caret that only moved the view once
1449    /// it fell off the edge would not be pinned to anything.
1450    ///
1451    /// Only the **innermost** clipping ancestor aligns; any further ancestors
1452    /// out fall back to a minimal reveal, since an outer container's job is to
1453    /// bring the inner viewport on screen, not to align a rectangle it does not
1454    /// own.
1455    ///
1456    /// `fraction` is clamped to `0.0..=1.0`. The container additionally clamps
1457    /// to its own scroll range, so a target near the start or end of the content
1458    /// lands as close to `fraction` as the range permits — see the scroll
1459    /// container's `scroll_past_end` for buying range past the content's end so
1460    /// the last line can still reach the pin.
1461    ///
1462    /// `rect` is in absolute tree (window) coordinates.
1463    pub fn ensure_visible_aligned(
1464        &mut self,
1465        rect: teksilo_canvas::Rect,
1466        fraction: f32,
1467        motion: crate::event::ScrollMotion,
1468    ) {
1469        self.scroll_into_view_requests.push(ScrollRevealRequest {
1470            rect,
1471            margin: 0.0,
1472            align: crate::event::ScrollAlign::Fraction(fraction.clamp(0.0, 1.0)),
1473            motion,
1474            from: None,
1475        });
1476    }
1477
1478    /// [`ensure_visible_aligned`](Self::ensure_visible_aligned), for a rect that
1479    /// belongs to **another** widget — see [`ensure_visible_from`](Self::ensure_visible_from)
1480    /// for why the distinction exists and how it fails when it is missed.
1481    pub fn ensure_visible_aligned_from(
1482        &mut self,
1483        owner: crate::widget_id::WidgetId,
1484        rect: teksilo_canvas::Rect,
1485        fraction: f32,
1486        motion: crate::event::ScrollMotion,
1487    ) {
1488        self.scroll_into_view_requests.push(ScrollRevealRequest {
1489            rect,
1490            margin: 0.0,
1491            align: crate::event::ScrollAlign::Fraction(fraction.clamp(0.0, 1.0)),
1492            motion,
1493            from: Some(owner),
1494        });
1495    }
1496
1497    /// Scroll a specific mounted widget into view inside every enclosing
1498    /// scroll container — the id-based companion to
1499    /// [`ensure_visible`](Self::ensure_visible).
1500    ///
1501    /// The framework resolves `id` to its current absolute bounds after the
1502    /// handler returns and walks *that widget's* ancestors (never `id`
1503    /// itself), dispatching
1504    /// [`WidgetEvent::ScrollIntoView`](crate::event::WidgetEvent::ScrollIntoView)
1505    /// to each `clips_children` container that doesn't already show it.
1506    ///
1507    /// Use this when the target you want revealed is a real, non-virtualized
1508    /// child whose bounds the arena already knows — a selected radio tile, a
1509    /// tab header — so you don't have to compute a rect. For a target that has
1510    /// no distinct node (a text caret) or that may not be realized (a
1511    /// virtualized list/table row), use [`ensure_visible`](Self::ensure_visible)
1512    /// with an analytic rect instead. No-op if `id` is not currently mounted.
1513    pub fn ensure_widget_visible(&mut self, id: crate::widget_id::WidgetId) {
1514        self.scroll_widget_into_view_requests.push((id, 0.0));
1515    }
1516
1517    /// Like [`ensure_widget_visible`](Self::ensure_widget_visible), but keeps
1518    /// `margin` logical pixels of breathing room around the widget.
1519    pub fn ensure_widget_visible_with_margin(
1520        &mut self,
1521        id: crate::widget_id::WidgetId,
1522        margin: f32,
1523    ) {
1524        self.scroll_widget_into_view_requests
1525            .push((id, margin.max(0.0)));
1526    }
1527
1528    /// Surface the tooltip of a keyboard-highlighted item immediately (no
1529    /// dwell), dismissing the previously-highlighted item's tooltip. Used by
1530    /// `MenuList` on arrow-key navigation so a menu item's rich/composite
1531    /// tooltip is reachable by keyboard — real focus stays on the menu panel,
1532    /// so this is keyed on the item id rather than on focus. Pass the item's
1533    /// own widget id; a tooltip-less item simply dismisses the previous one.
1534    pub fn show_highlight_tooltip(&mut self, id: crate::widget_id::WidgetId) {
1535        self.highlight_tooltip_requests.push(id);
1536    }
1537
1538    /// Cancel a pending delayed overlay by its content widget ID.
1539    /// Call this when the hover ends before the delay elapses.
1540    pub fn cancel_delayed_overlay(&mut self, content_id: crate::widget_id::WidgetId) {
1541        self.cancel_delayed_overlays.push(content_id);
1542    }
1543
1544    /// Arm the "safe triangle" of the open overlay rooted at
1545    /// `content_id`, with its apex at the current pointer position.
1546    ///
1547    /// Call this from the anchor's hover-leave handler: the pointer is
1548    /// then exactly at the point the diagonal toward the overlay
1549    /// starts. While the pointer sits inside the triangle spanned by
1550    /// that apex and the overlay's near edge, the overlay's
1551    /// pointer-leave grace is held off; leaving the triangle starts the
1552    /// grace and re-entering it cancels the grace again, so a wobble
1553    /// mid-diagonal costs nothing. Throughout — cone or no cone, until
1554    /// the pointer arrives or the framework's budget runs out —
1555    /// [`overlay_safe_region_armed`](Self::overlay_safe_region_armed)
1556    /// reports `true` so sibling widgets stand aside and let that one
1557    /// re-evaluated grace own the dismissal.
1558    ///
1559    /// No-ops when the overlay is not open (a submenu whose hover-open
1560    /// delay was cancelled before it ever showed) or when no pointer
1561    /// position is known.
1562    pub fn arm_overlay_safe_region(&mut self, content_id: crate::widget_id::WidgetId) {
1563        self.safe_region_arm_requests.push(content_id);
1564    }
1565
1566    /// Capture the pointer: all subsequent `PointerMove` and `PointerUp`
1567    /// events will be routed to the capturing widget until the capture is
1568    /// released. Use this when starting a drag operation.
1569    pub fn capture_pointer(&mut self) {
1570        self.pointer_capture = Some(true);
1571    }
1572
1573    /// Release a previously captured pointer. Pointer events resume normal
1574    /// hit-test dispatch.
1575    pub fn release_pointer(&mut self) {
1576        self.pointer_capture = Some(false);
1577    }
1578
1579    /// Start a drag-and-drop operation from the given source widget.
1580    ///
1581    /// The `payload` carries the data being dragged. During the drag:
1582    /// - `PointerMove` events update the drag position and fire `on_drag_hover`
1583    ///   on widgets under the pointer that have drop handlers
1584    /// - `PointerUp` fires `on_drop` on the target widget (if any)
1585    /// - `Escape` cancels the drag
1586    pub fn start_drag(
1587        &mut self,
1588        source_widget: crate::widget_id::WidgetId,
1589        payload: crate::drag_payload::DragPayload,
1590    ) {
1591        self.drag_start_request = Some((source_widget, payload, None));
1592    }
1593
1594    /// Start a drag-and-drop with a preview widget that follows the pointer.
1595    pub fn start_drag_with_preview(
1596        &mut self,
1597        source_widget: crate::widget_id::WidgetId,
1598        payload: crate::drag_payload::DragPayload,
1599        preview: Box<dyn crate::widget::Widget>,
1600    ) {
1601        self.drag_start_request = Some((source_widget, payload, Some(preview)));
1602    }
1603
1604    /// Cancel the active drag-and-drop session (if any).
1605    pub fn cancel_drag(&mut self) {
1606        self.cancel_drag = true;
1607    }
1608
1609    /// Replace the tree-level theme. Composite widgets are rebuilt so any
1610    /// derived values they captured at build time pick up the new tokens,
1611    /// and all widgets are marked dirty for repaint.
1612    ///
1613    /// An explicit theme also turns **off** OS-following: the app's theme
1614    /// mode is reset to manual, so a later OS light/dark change won't
1615    /// override the chosen theme.
1616    pub fn set_theme(&mut self, theme: crate::styles::Theme) {
1617        self.theme_request = Some(theme);
1618    }
1619
1620    /// Switch the application to follow the OS theme (native / system mode):
1621    /// the app adopts the OS's colours and tracks OS light/dark changes at
1622    /// runtime. On platforms without OS-colour support it falls back to
1623    /// following the built-in light/dark presets.
1624    ///
1625    /// This is the counterpart to [`set_theme`](Self::set_theme): calling
1626    /// `set_theme` pins a fixed theme (manual mode), while this resumes
1627    /// OS-following. Parameterless by design, so widgets need not reference
1628    /// the app-layer theme-mode enum.
1629    pub fn follow_system_theme(&mut self) {
1630        self.follow_system_request = true;
1631    }
1632
1633    /// Replace the tree-level locale identifier. Composite widgets are
1634    /// rebuilt so any tr! lookups picked up at build time are re-evaluated
1635    /// against the new locale.
1636    pub fn set_locale(&mut self, locale: impl Into<String>) {
1637        self.locale_request = Some(locale.into());
1638    }
1639
1640    /// Set the user-controlled global text-scale factor (`1.0` = 100 %).
1641    ///
1642    /// The change is applied app-wide (every window) after the handler returns,
1643    /// mirroring [`set_theme`](Self::set_theme) / [`set_locale`](Self::set_locale).
1644    /// All text grows uniformly without a rebuild. Persist the value through
1645    /// `ctx.settings()` (e.g. `teksilo_settings::TEXT_SCALE_KEY`) so it survives
1646    /// a restart — the `TextScaleControl` widget does both for you.
1647    pub fn set_text_scale(&mut self, factor: f32) {
1648        self.text_scale_request = Some(factor);
1649    }
1650}
1651
1652#[cfg(test)]
1653mod multi_window_tests {
1654    use super::*;
1655    use crate::window::state::WindowStateInit;
1656    use crate::window::{
1657        NoopWindowOps, TeksiloWindowId, WindowConfig, WindowOps, WindowPlacement, WindowState,
1658    };
1659    use std::cell::RefCell;
1660    use std::rc::Rc;
1661
1662    /// Recording implementation of `WindowOps` so tests can assert
1663    /// that `EventContext` routes each method through the trait.
1664    #[derive(Default)]
1665    struct RecordingOps {
1666        open_calls: RefCell<Vec<WindowConfig>>,
1667        focus_calls: RefCell<Vec<TeksiloWindowId>>,
1668        close_calls: RefCell<Vec<TeksiloWindowId>>,
1669        next_id: RefCell<u64>,
1670        // A fake registry so `find_window` / `window_state` / `windows`
1671        // can return values.
1672        states: RefCell<Vec<WindowState>>,
1673    }
1674
1675    impl RecordingOps {
1676        fn alloc_id(&self) -> TeksiloWindowId {
1677            let mut n = self.next_id.borrow_mut();
1678            *n += 1;
1679            TeksiloWindowId::new(*n)
1680        }
1681    }
1682
1683    impl WindowOps for RecordingOps {
1684        fn open_window(&mut self, config: WindowConfig) -> TeksiloWindowId {
1685            let id = self.alloc_id();
1686            let state = WindowState::new(WindowStateInit {
1687                id,
1688                string_id: config.string_id.clone(),
1689                placement: config.initial_placement,
1690                title: config.title.clone(),
1691                size: config.size,
1692                position: config.position.unwrap_or((0, 0)),
1693                focused: true,
1694                resizable: config.resizable,
1695                always_on_top: config.always_on_top,
1696            });
1697            self.states.borrow_mut().push(state);
1698            self.open_calls.borrow_mut().push(config);
1699            id
1700        }
1701
1702        fn find_window(&self, string_id: &str) -> Option<TeksiloWindowId> {
1703            self.states
1704                .borrow()
1705                .iter()
1706                .find(|s| s.string_id() == Some(string_id))
1707                .map(|s| s.id())
1708        }
1709
1710        fn window_state(&self, id: TeksiloWindowId) -> Option<WindowState> {
1711            self.states.borrow().iter().find(|s| s.id() == id).cloned()
1712        }
1713
1714        fn windows(&self) -> Vec<WindowState> {
1715            self.states.borrow().clone()
1716        }
1717
1718        fn focus_window(&mut self, id: TeksiloWindowId) {
1719            self.focus_calls.borrow_mut().push(id);
1720        }
1721
1722        fn close_window_by_id(&mut self, id: TeksiloWindowId) {
1723            self.close_calls.borrow_mut().push(id);
1724        }
1725    }
1726
1727    fn make_state(id: u64, string_id: Option<&str>) -> WindowState {
1728        WindowState::new(WindowStateInit {
1729            id: TeksiloWindowId::new(id),
1730            string_id: string_id.map(String::from),
1731            placement: WindowPlacement::Floating,
1732            title: "Test".into(),
1733            size: (800, 600),
1734            position: (0, 0),
1735            focused: true,
1736            resizable: true,
1737            always_on_top: false,
1738        })
1739    }
1740
1741    #[test]
1742    fn window_returns_current_window_state() {
1743        let state = make_state(1, Some("main"));
1744        let mut noop = NoopWindowOps;
1745        let ctx = EventContext::new().with_window_context(&mut noop, Some(state.clone()));
1746        assert_eq!(ctx.window().unwrap().id(), TeksiloWindowId::new(1));
1747        assert_eq!(ctx.window().unwrap().string_id(), Some("main"));
1748    }
1749
1750    #[test]
1751    fn window_is_none_without_context() {
1752        let ctx = EventContext::new();
1753        assert!(ctx.window().is_none());
1754    }
1755
1756    #[test]
1757    fn open_window_routes_through_ops() {
1758        let mut ops = RecordingOps::default();
1759        let main_state = make_state(1, Some("main"));
1760        let returned_id = {
1761            let mut ctx = EventContext::new().with_window_context(&mut ops, Some(main_state));
1762            ctx.open_window(WindowConfig::new().id("help").title("Help"))
1763        };
1764        assert_eq!(ops.open_calls.borrow().len(), 1);
1765        assert_eq!(
1766            ops.open_calls.borrow()[0].string_id.as_deref(),
1767            Some("help")
1768        );
1769        // Recording ops allocates ids 2+; 1 was reserved for `main`
1770        // only in this test — Recording's counter starts from 0, so the
1771        // first alloc yields 1.
1772        assert_eq!(returned_id, TeksiloWindowId::new(1));
1773    }
1774
1775    #[test]
1776    fn find_window_routes_through_ops() {
1777        let mut ops = RecordingOps::default();
1778        ops.states.borrow_mut().push(make_state(7, Some("foo")));
1779        let main_state = make_state(1, Some("main"));
1780        let ctx = EventContext::new().with_window_context(&mut ops, Some(main_state));
1781        assert_eq!(ctx.find_window("foo"), Some(TeksiloWindowId::new(7)));
1782        assert!(ctx.find_window("missing").is_none());
1783    }
1784
1785    #[test]
1786    fn focus_window_records_via_ops() {
1787        let mut ops = RecordingOps::default();
1788        let main_state = make_state(1, None);
1789        {
1790            let mut ctx = EventContext::new().with_window_context(&mut ops, Some(main_state));
1791            ctx.focus_window(TeksiloWindowId::new(42));
1792        }
1793        assert_eq!(
1794            ops.focus_calls.borrow().as_slice(),
1795            &[TeksiloWindowId::new(42)]
1796        );
1797    }
1798
1799    #[test]
1800    fn close_window_by_id_records_via_ops() {
1801        let mut ops = RecordingOps::default();
1802        let main_state = make_state(1, None);
1803        {
1804            let mut ctx = EventContext::new().with_window_context(&mut ops, Some(main_state));
1805            ctx.close_window_by_id(TeksiloWindowId::new(9));
1806        }
1807        assert_eq!(
1808            ops.close_calls.borrow().as_slice(),
1809            &[TeksiloWindowId::new(9)]
1810        );
1811    }
1812
1813    #[test]
1814    fn close_window_sets_guarded_flag_only() {
1815        let mut ctx = EventContext::new();
1816        ctx.close_window();
1817        assert!(
1818            ctx.close_window_requested,
1819            "close_window must raise the guarded-close flag"
1820        );
1821        assert!(
1822            !ctx.force_close_requested,
1823            "close_window must NOT raise the forced-close flag"
1824        );
1825    }
1826
1827    #[test]
1828    fn close_window_forced_sets_force_flag_only() {
1829        let mut ctx = EventContext::new();
1830        ctx.close_window_forced();
1831        assert!(
1832            ctx.force_close_requested,
1833            "close_window_forced must raise the forced-close flag"
1834        );
1835        assert!(
1836            !ctx.close_window_requested,
1837            "close_window_forced must NOT raise the guarded-close flag"
1838        );
1839    }
1840
1841    /// End-to-end: a handler calling `close_window_forced` during
1842    /// dispatch must transfer the flag onto the `WidgetTree` (the
1843    /// `collect_from_ctx` teardown), where the app loop drains it via
1844    /// `take_force_close_request` — separately from the guarded
1845    /// `take_close_window_request` flag.
1846    #[test]
1847    fn forced_close_flag_propagates_to_tree_and_drains_independently() {
1848        use crate::test_widgets::FillWidget;
1849        use crate::widget_tree::WidgetTree;
1850
1851        // `run_with_event_context` only runs the `collect_from_ctx`
1852        // teardown (which transfers ctx flags onto the tree) when the
1853        // tree has a root to anchor on, so give it one.
1854        let mut tree = WidgetTree::new();
1855        tree.add(FillWidget::new());
1856        tree.run_with_event_context(&mut NoopWindowOps, |ctx| ctx.close_window_forced());
1857        assert!(
1858            tree.take_force_close_request(),
1859            "forced-close flag must reach the tree"
1860        );
1861        assert!(
1862            !tree.take_close_window_request(),
1863            "a forced close must not also raise the guarded flag"
1864        );
1865
1866        // And the guarded path stays on its own channel.
1867        let mut tree = WidgetTree::new();
1868        tree.add(FillWidget::new());
1869        tree.run_with_event_context(&mut NoopWindowOps, |ctx| ctx.close_window());
1870        assert!(tree.take_close_window_request());
1871        assert!(!tree.take_force_close_request());
1872    }
1873
1874    #[test]
1875    fn windows_enumerates_via_ops() {
1876        let mut ops = RecordingOps::default();
1877        ops.states.borrow_mut().push(make_state(1, Some("a")));
1878        ops.states.borrow_mut().push(make_state(2, Some("b")));
1879        let main_state = make_state(1, Some("a"));
1880        let ctx = EventContext::new().with_window_context(&mut ops, Some(main_state));
1881        let ids: Vec<_> = ctx.windows().iter().map(|s| s.id()).collect();
1882        assert_eq!(ids, vec![TeksiloWindowId::new(1), TeksiloWindowId::new(2)]);
1883    }
1884
1885    #[test]
1886    fn standalone_context_returns_empty_windows_and_none_lookups() {
1887        let ctx = EventContext::new();
1888        assert!(ctx.find_window("anything").is_none());
1889        assert!(ctx.window_state(TeksiloWindowId::new(1)).is_none());
1890        assert!(ctx.windows().is_empty());
1891    }
1892
1893    #[test]
1894    #[should_panic(expected = "open_window called outside of a dispatch")]
1895    fn open_window_on_standalone_context_panics() {
1896        let mut ctx = EventContext::new();
1897        let _ = ctx.open_window(WindowConfig::new());
1898    }
1899
1900    #[test]
1901    fn open_modal_builds_window_config_from_request() {
1902        use crate::modal::{ModalContent, ModalRequest};
1903        let mut ops = RecordingOps::default();
1904        let main_state = make_state(1, Some("main"));
1905        let built_widget = Rc::new(RefCell::new(false));
1906        let built_widget_flag = built_widget.clone();
1907        let request = ModalRequest {
1908            content: ModalContent::Deferred(Box::new(move |_tree| {
1909                *built_widget_flag.borrow_mut() = true;
1910                // Return a dummy WidgetId — not used in this test since
1911                // the RecordingOps doesn't actually build the tree.
1912                crate::widget_id::WidgetId::default()
1913            })),
1914            presentation: crate::modal::ModalPresentation::NativeWindow,
1915            close_behavior: crate::modal::ModalCloseBehavior::default(),
1916            title: Some("Confirm".to_string()),
1917            size: Some((420, 180)),
1918            focus_target: None,
1919            on_dismiss: None,
1920        };
1921        {
1922            let mut ctx = EventContext::new().with_window_context(&mut ops, Some(main_state));
1923            let id = ctx.open_modal(request);
1924            assert!(id.is_some());
1925        }
1926        // open_modal is a thin wrapper over open_window — the config
1927        // it built must reach RecordingOps::open_window.
1928        let calls = ops.open_calls.borrow();
1929        assert_eq!(calls.len(), 1);
1930        let cfg = &calls[0];
1931        assert_eq!(cfg.title, "Confirm");
1932        assert_eq!(cfg.size, (420, 180));
1933        assert!(cfg.is_modal());
1934        assert_eq!(cfg.modal_parent(), Some(TeksiloWindowId::new(1)));
1935        // Cell is just to let us observe something reachable via cfg.root_builder;
1936        // the builder hasn't been called yet (RecordingOps records the config
1937        // but doesn't build the tree).
1938        let _ = built_widget;
1939    }
1940
1941    #[test]
1942    fn open_modal_requires_current_window() {
1943        use crate::modal::{ModalContent, ModalRequest};
1944        let mut ops = RecordingOps::default();
1945        let mut ctx = EventContext::new().with_window_context(&mut ops, None);
1946        let request = ModalRequest {
1947            content: ModalContent::Deferred(Box::new(|_tree| {
1948                crate::widget_id::WidgetId::default()
1949            })),
1950            presentation: crate::modal::ModalPresentation::NativeWindow,
1951            close_behavior: crate::modal::ModalCloseBehavior::default(),
1952            title: None,
1953            size: None,
1954            focus_target: None,
1955            on_dismiss: None,
1956        };
1957        assert!(ctx.open_modal(request).is_none());
1958    }
1959}