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::pointer::touch_action::TouchAction;
5use crate::widget_id::WidgetId;
6
7use super::CursorIcon;
8
9/// Selects which overlay-dismissal pathway runs after a handler
10/// returns. Last-write-wins: each `dismiss_*_overlays()` setter
11/// overwrites the previous choice. `None` (the default) falls
12/// through to draining individual ids from `overlay_dismissals`.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub(crate) enum DismissScope {
15    /// Dismiss every overlay in the stack, including hosts.
16    All,
17    /// Dismiss every overlay whose content is *not* a host surface
18    /// (`Tooltip`, `Dialog`, `AlertDialog`). Used by popover triggers
19    /// and pre-show cleanup.
20    AllExceptHosts,
21    /// Walk up from the source widget's containing overlay,
22    /// dismissing menu-like overlays and stopping at the first host
23    /// surface. Used by menu / dropdown item activation.
24    SelfChain,
25    /// Dismiss the topmost overlay only.
26    Top,
27}
28
29/// One queued "reveal this rectangle" request, drained after the handler
30/// returns and turned into a [`WidgetEvent::ScrollIntoView`] per clipping
31/// ancestor.
32///
33/// A struct rather than a tuple because the three modifiers (margin, alignment,
34/// motion) are independent and positional tuples of that width stop being
35/// readable at the call site.
36///
37/// [`WidgetEvent::ScrollIntoView`]: crate::event::WidgetEvent::ScrollIntoView
38#[derive(Debug, Clone, Copy, PartialEq)]
39pub(crate) struct ScrollRevealRequest {
40    /// The target, in absolute tree (window) coordinates.
41    pub(crate) rect: teksilo_canvas::Rect,
42    /// Breathing room to keep around the target, in logical pixels.
43    pub(crate) margin: f32,
44    /// Where the target should come to rest vertically.
45    pub(crate) align: crate::event::ScrollAlign,
46    /// Whether to jump or glide.
47    pub(crate) motion: crate::event::ScrollMotion,
48    /// **Whose** ancestors to walk, when that is not the widget whose handler
49    /// queued this.
50    ///
51    /// `None` means the source widget, which is right whenever a widget reveals
52    /// something inside itself. It is wrong — silently — whenever the rect belongs
53    /// to a *different* widget: a find banner's Next button asking for a match in
54    /// the prose, a toolbar revealing a row in the list below it. Those walk the
55    /// button's ancestors, which do not include the scroll container the rect lives
56    /// in, so nothing scrolls and the reveal is a no-op no error reports.
57    pub(crate) from: Option<crate::widget_id::WidgetId>,
58}
59
60/// What a handler said about the cursor during one dispatch.
61///
62/// A handler either names a cursor or withdraws, and the two are distinct
63/// answers: withdrawing is not "show `Default`", it is "the node's own
64/// declaration applies again". One slot, so the last call in a dispatch wins,
65/// the way it did when this was a bare `Option<CursorIcon>`.
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67pub(crate) enum CursorRequest {
68    /// Override the cursor with this icon.
69    Set(CursorIcon),
70    /// Give the cursor back to whatever the hovered node declared.
71    Release,
72}
73
74/// Context available during event handling.
75pub struct EventContext<'ops> {
76    /// A handler's word on the cursor for this dispatch, if it said one.
77    pub(crate) cursor_request: Option<CursorRequest>,
78    /// The cursor the **node** declared, written only by the router's
79    /// `PointerEnter` / `PointerLeave` arms. Kept apart from
80    /// [`cursor_request`](Self::cursor_request) so that a handler which also
81    /// speaks during the same dispatch overrides the declared cursor without
82    /// erasing the tree's memory of what it was — which is the value
83    /// [`release_cursor`](Self::release_cursor) hands back to.
84    pub(crate) declared_cursor_request: Option<CursorIcon>,
85    pub(crate) tree_mutations: Vec<TreeMutation>,
86    pub(crate) idle_callbacks: Vec<crate::idle::IdleCallback>,
87    pub(crate) modal_requests: Vec<crate::modal::ModalRequest>,
88    pub(crate) dismiss_modal: bool,
89    pub(crate) overlay_requests: Vec<crate::overlay::OverlayRequest>,
90    pub(crate) overlay_dismissals: Vec<crate::overlay::OverlayId>,
91    /// Content widget ids whose currently-shown overlay (if any) should
92    /// be dismissed. Resolved to an `OverlayId` via
93    /// `OverlayManager::find_by_content` at drain time. Lets a handler
94    /// dismiss an overlay it can only identify by content (e.g. a single
95    /// reusable tooltip surface) — the symmetric companion to
96    /// [`cancel_delayed_overlay`](EventContext::cancel_delayed_overlay).
97    pub(crate) overlay_content_dismissals: Vec<crate::widget_id::WidgetId>,
98    /// Overlay requests that name a z-band other than the default. Kept apart
99    /// from [`overlay_requests`](Self::overlay_requests) rather than carried on
100    /// `OverlayRequest` itself: the band is a property of the *show*, not of
101    /// the request, and every existing construction site of the struct would
102    /// otherwise have to name it.
103    pub(crate) overlay_band_requests:
104        Vec<(crate::overlay::OverlayRequest, crate::overlay::OverlayBand)>,
105    /// New placements for overlays named by their content root. A
106    /// caret-anchored overlay has to be re-placed as the caret moves, and
107    /// `position_overlays` re-reads the placement it was shown with — so
108    /// without this an affordance follows nothing.
109    pub(crate) overlay_placement_updates:
110        Vec<(crate::widget_id::WidgetId, crate::overlay::OverlayPlacement)>,
111    /// Placement functions for overlays named by their content root, re-run
112    /// against the anchor on every layout pass. See
113    /// [`track_overlay_placement_by_content`](EventContext::track_overlay_placement_by_content).
114    pub(crate) overlay_placement_sources: Vec<(
115        crate::widget_id::WidgetId,
116        crate::overlay::OverlayPlacementSource,
117    )>,
118    /// Overlay ids whose `auto_dismiss_after` timer should be paused
119    /// or resumed after the handler returns (`true` = pause, `false`
120    /// = resume). Drained by `WidgetTree::collect_from_ctx` against
121    /// `OverlayManager::pause_auto_dismiss` / `resume_auto_dismiss`,
122    /// after the dismissals in the same drain: a pause aimed at an
123    /// overlay the same handler dismissed is silently dropped. Used
124    /// by `ToastHost` for hover-pause.
125    pub(crate) overlay_pause_requests: Vec<(crate::overlay::OverlayId, bool)>,
126    /// The dismissal scope chosen by the handler, if any. Set by
127    /// `dismiss_all_overlays()` / `dismiss_all_except_hosts()` /
128    /// `dismiss_self_overlay_chain()` / `dismiss_top_overlay()` —
129    /// last setter wins. `None` falls through to draining the
130    /// per-id `overlay_dismissals` vec instead.
131    pub(crate) dismiss_scope: Option<DismissScope>,
132    /// Request to capture (`true`) or release (`false`) a pointer, and which
133    /// one. `None` for the pointer means the one whose sample this handler is
134    /// serving — the default, and what every pre-multi-touch call site means.
135    pub(crate) pointer_capture: Option<(Option<crate::pointer::PointerId>, bool)>,
136    /// The widget currently holding the capture of the pointer being
137    /// dispatched, as the tree knew it when this context was made. Read by
138    /// [`owns_pointer`](EventContext::owns_pointer).
139    pub(crate) pointer_captor: Option<WidgetId>,
140    /// Whether the capture request above came from a **widget handler** rather
141    /// than from framework plumbing.
142    ///
143    /// The distinction is the whole of A4's "explicit capture is an
144    /// arbitration act": the gesture arena and the drag pipeline both capture
145    /// the pointer for their own bookkeeping, and neither is a widget staking
146    /// a claim. Only a `capture_pointer()` written in a handler enrols its
147    /// caller as a [`MemberRole::RawDrag`](crate::gesture::MemberRole::RawDrag)
148    /// competitor.
149    pub(crate) explicit_capture: bool,
150    /// A recognizer on this node produced a gesture that **owns the rest of
151    /// the press** — a drag or a swipe, as opposed to a tap, which completes
152    /// the press rather than claiming it. Set by `dispatch_recognized_gesture`
153    /// and read by `collect_from_ctx`, which decides the pointer's sequence in
154    /// the recognizer's favour.
155    pub(crate) recognized_owning_gesture: bool,
156    /// A press-time [`DragActivation`](teksilo_tokens::DragActivation) chosen
157    /// for this node by its own press handler, overriding its build-time
158    /// declaration for this press alone. Applied by `collect_from_ctx` onto the
159    /// pointer's sequence, which the enrolment walk reads immediately
160    /// afterwards. See [`EventContext::set_drag_activation`].
161    pub(crate) drag_activation_override: Option<teksilo_tokens::DragActivation>,
162    /// Arbitration acts the handler performed on the sequence owning the
163    /// pointer it is serving, in the order it performed them. Applied by
164    /// `WidgetTree::collect_from_ctx` against that sequence.
165    pub(crate) gesture_acts: Vec<GestureAct>,
166    /// The handler asked for its pointer's whole interaction to be revoked.
167    /// Queued by `WidgetTree::collect_from_ctx` onto the cancel funnel, so it
168    /// runs after this dispatch rather than under it. Last reason wins.
169    pub(crate) cancel_pointer_request: Option<crate::pointer::CancelReason>,
170    /// The node whose handler is running, when the dispatcher knows it.
171    /// `None` for a context made outside per-node dispatch (a gesture timer, a
172    /// key-capture callback, an async completion).
173    pub(crate) dispatch_node: Option<WidgetId>,
174    /// The handler space of `dispatch_node`: the frame its pointer positions
175    /// were localised into. Read by [`to_local`](EventContext::to_local).
176    pub(crate) local_frame: Option<crate::arena::LocalFrame>,
177    /// The node the router resolved as this dispatch's **target** — the
178    /// innermost node the arena's hit walk accepted. Read by
179    /// [`dispatch_target`](EventContext::dispatch_target).
180    pub(crate) dispatch_target: Option<WidgetId>,
181    /// How the focus being delivered was acquired, set only while an
182    /// `on_focus(true, …)` handler runs. Read by
183    /// [`focus_origin`](EventContext::focus_origin).
184    pub(crate) focus_origin: Option<crate::focus::FocusOrigin>,
185    /// Delayed overlay requests (request, delay, optional focus target,
186    /// whether to dismiss sibling overlays when it finally shows).
187    pub(crate) delayed_overlay_requests: Vec<(
188        crate::overlay::OverlayRequest,
189        std::time::Duration,
190        Option<crate::widget_id::WidgetId>,
191        bool,
192    )>,
193    /// Timed overlay requests (request, auto-dismiss delay).
194    pub(crate) timed_overlay_requests: Vec<(crate::overlay::OverlayRequest, std::time::Duration)>,
195    /// Reveal overlay requests (request, caller-owned animated progress
196    /// signal, tween duration). The framework shows the overlay, then
197    /// drives `progress` 0 → 1 on show and 1 → 0 on dismiss, deferring
198    /// the actual stack removal until the roll-back tween completes —
199    /// the same deferral the fade path uses, minus the opacity scope.
200    /// The caller applies `progress` however it wants (e.g. an `Unroll`
201    /// width). See [`show_overlay_with_reveal`](EventContext::show_overlay_with_reveal).
202    pub(crate) reveal_overlay_requests: Vec<(
203        crate::overlay::OverlayRequest,
204        crate::signal::Signal<f32>,
205        std::time::Duration,
206    )>,
207    /// Dismiss descendant overlays of the source widget's containing overlay.
208    /// Optionally preserve the subtree rooted at a specific content widget ID.
209    pub(crate) dismiss_descendant_overlays: Vec<Option<crate::widget_id::WidgetId>>,
210    /// Cancel pending delayed overlays by content widget ID.
211    pub(crate) cancel_delayed_overlays: Vec<crate::widget_id::WidgetId>,
212    /// Overlays whose safe triangle should be armed at the current
213    /// pointer position once this handler returns. See
214    /// [`EventContext::arm_overlay_safe_region`].
215    pub(crate) safe_region_arm_requests: Vec<crate::widget_id::WidgetId>,
216    /// Widget IDs that need repainting (cross-widget signal propagation).
217    pub(crate) repaint_requests: Vec<crate::widget_id::WidgetId>,
218    /// Synthetic clicks to dispatch on target widgets after event processing.
219    pub(crate) synthetic_clicks: Vec<crate::widget_id::WidgetId>,
220    /// Focus requests — transfer focus to a specific widget (e.g., overlay content on open).
221    pub(crate) focus_requests: Vec<crate::widget_id::WidgetId>,
222    /// Focus-into requests — move focus to the *first focusable descendant* of
223    /// the given widget, with no fallback to the widget itself when the subtree
224    /// has none. The "dive into this region's content" intent (Enter on a tab
225    /// header → into the tab panel), distinct from `focus_requests` which
226    /// focuses the container itself as a last resort.
227    pub(crate) focus_into_requests: Vec<crate::widget_id::WidgetId>,
228    /// Drop keyboard focus once this handler returns. Applied before
229    /// `focus_requests` / `focus_into_requests`, which
230    /// [`clear_focus`](EventContext::clear_focus) empties as it is queued, so
231    /// the order of the calls is the order of the effects.
232    pub(crate) clear_focus_requested: bool,
233    /// Rect-based "scroll this into view" requests, stated in the **queuing
234    /// widget's own bounds space** — window coordinates everywhere except
235    /// inside a content transform (a `SceneView`'s scene coordinates), which
236    /// the ancestor walk carries outward one space at a time.
237    /// Queued by [`ensure_visible`](EventContext::ensure_visible)
238    /// / [`ensure_visible_with_margin`](EventContext::ensure_visible_with_margin).
239    /// Drained in `collect_from_ctx`, which walks the ancestors of the widget
240    /// whose handler queued the request and dispatches
241    /// [`WidgetEvent::ScrollIntoView`](crate::event::WidgetEvent::ScrollIntoView)
242    /// to every `clips_children` scroll container that doesn't already fully
243    /// contain the rect — the same ancestor-walk the focus path uses, but
244    /// with a caller-supplied rectangle instead of a widget's own bounds
245    /// (so a caret, a virtualized row, or a scrolled-off tab header can be
246    /// revealed even though it is not itself a distinct focused node).
247    pub(crate) scroll_into_view_requests: Vec<ScrollRevealRequest>,
248    /// Widget-id-based "scroll this into view" requests, queued by
249    /// [`ensure_widget_visible`](EventContext::ensure_widget_visible) /
250    /// [`ensure_widget_visible_with_margin`](EventContext::ensure_widget_visible_with_margin).
251    /// Drained in `collect_from_ctx`, which resolves each id to its current
252    /// absolute arena bounds and walks *that widget's* ancestors (the target
253    /// widget itself excluded) dispatching
254    /// [`WidgetEvent::ScrollIntoView`](crate::event::WidgetEvent::ScrollIntoView).
255    /// The convenience form of the rect API for a target that is a real
256    /// mounted, non-virtualized child (a radio tile, a tab header) whose bounds
257    /// the framework already knows — the caller need not compute the rect.
258    pub(crate) scroll_widget_into_view_requests: Vec<(crate::widget_id::WidgetId, f32)>,
259    /// Keyboard-highlight tooltip requests: surface the tooltip of the given
260    /// (menu) item immediately and dismiss the previously-highlighted one.
261    /// Drained after the handler (see `pointer_router::collect_from_ctx`).
262    /// Only the last entry per handler is honoured — a handler sets one
263    /// highlight per key.
264    pub(crate) highlight_tooltip_requests: Vec<crate::widget_id::WidgetId>,
265    /// Drag start request: (source_widget_id, payload, optional_preview_widget).
266    pub(crate) drag_start_request: Option<(
267        crate::widget_id::WidgetId,
268        crate::drag_payload::DragPayload,
269        Option<Box<dyn crate::widget::Widget>>,
270    )>,
271    /// Cancel any active drag session.
272    pub(crate) cancel_drag: bool,
273    /// Whether the drag session active while this context is live was
274    /// started by an external (OS) drag. Read via `drag_is_external()`.
275    /// `false` for hand-constructed contexts and when no drag is active.
276    pub(crate) drag_is_external: bool,
277    /// The identity of the key event being dispatched (the physical key when
278    /// the window manager noted one). Read via `key_identity()`.
279    pub(crate) key_identity: Option<u64>,
280    /// Replace the tree-level theme. Drained after dispatch; triggers a
281    /// composite-widget rebuild and full repaint.
282    pub(crate) theme_request: Option<crate::styles::Theme>,
283    /// Request that the app follow the OS theme (native / system mode).
284    /// Drained after dispatch; the app switches to `ThemeMode::Native` and
285    /// recomputes the theme from the current OS colours. Parameterless so
286    /// `teksilo-widgets` never needs the app-layer `ThemeMode` enum.
287    pub(crate) follow_system_request: bool,
288    /// Replace the tree-level locale identifier. Drained after dispatch;
289    /// triggers a composite-widget rebuild and full repaint.
290    pub(crate) locale_request: Option<String>,
291    /// Set the user-controlled text-scale factor. Drained after dispatch and
292    /// fanned out to every window; grows all text without a rebuild.
293    pub(crate) text_scale_request: Option<f32>,
294    /// Set by `request_frame()`; consumed by the event dispatcher which
295    /// forwards it to `WidgetTree::request_frame()` so the next layout
296    /// pass advances the per-frame tick signal.
297    pub(crate) frame_requested: bool,
298    /// Optional reference to the tree's app-state registry, so handlers
299    /// can look up application-scoped values via `app_state::<T>()`.
300    /// Populated by the dispatcher before running each handler; `None`
301    /// for hand-constructed contexts in tests.
302    pub(crate) app_context: Option<std::rc::Rc<crate::event_source::TreeAppContext>>,
303    /// App-level window-ops sink. Injected by the dispatcher so
304    /// handlers can reach the multi-window API (`open_window`,
305    /// `focus_window`, …) synchronously. For `EventContext`
306    /// instances constructed outside a dispatch (standalone trees,
307    /// tests) this is `None` and the multi-window methods no-op /
308    /// return `None`.
309    pub(crate) window_ops: Option<&'ops mut dyn crate::window::WindowOps>,
310    /// [`WindowState`](crate::window::WindowState) for the window
311    /// this tree belongs to. Cloned from the tree at construction.
312    /// `None` for standalone trees.
313    pub(crate) current_window: Option<crate::window::WindowState>,
314    /// Snapshot of the tree's occlusion-aware window-active state
315    /// (`focused AND not occluded`) at construction time. Distinct from
316    /// `current_window.focused()` (raw OS focus, no occlusion). Read by
317    /// [`window_active`](Self::window_active). Defaults `true` (matches the
318    /// tree's initial value) for standalone / test contexts.
319    pub(crate) tree_window_active: bool,
320    /// Snapshot of the tree's OS screen-reader state at construction time.
321    /// Read by [`screen_reader_state`](Self::screen_reader_state). `Unknown`
322    /// for standalone / test contexts.
323    pub(crate) tree_screen_reader: crate::environment::ScreenReaderState,
324    /// Last `PointerMove` position observed by the tree. Snapshotted
325    /// at handler-invocation time so widgets that don't see the live
326    /// pointer event (e.g. an `on_hover` callback that fires on the
327    /// boundary edge) can still query "where is the cursor right
328    /// now". Read by the safe-triangle submenu hover gate.
329    pub(crate) tree_pointer_position: Option<teksilo_canvas::Point>,
330    /// True when the in-flight pointer press's hit target is a strict
331    /// descendant of the widget whose handler is currently running and that
332    /// descendant carries its own tap gesture (a chevron, checkbox, inline
333    /// button). Set per-node by the dispatcher for `PointerDown`/`PointerUp`.
334    /// Read via [`press_claimed_by_interactive_child`](Self::press_claimed_by_interactive_child).
335    pub(crate) press_claimed_by_interactive_child: bool,
336    /// Per-content-widget overlay bounds — and armed safe-triangle apex,
337    /// when the overlay has one — snapshotted at handler invocation. A
338    /// flat vec is fine: open overlays are typically 0–3 per tree. Read
339    /// by [`EventContext::overlay_bounds_for_content`] and
340    /// [`EventContext::overlay_safe_region_armed`].
341    pub(crate) overlay_bounds_snapshot: Vec<(
342        WidgetId,
343        teksilo_canvas::Rect,
344        Option<teksilo_canvas::Point>,
345    )>,
346    /// The widget holding focus when this batch began. Part of the same
347    /// per-dispatch snapshot as the two above, and read by
348    /// [`focused`](EventContext::focused).
349    pub(crate) focused_widget: Option<WidgetId>,
350    /// Intents queued by handlers via `send_intent`. Drained by the
351    /// tree after event dispatch and routed source-widget → root.
352    pub(crate) pending_intents: Vec<crate::intent::Intent>,
353    /// Intents queued via `send_intent_from_focus`: routed from the focus
354    /// the tree holds once this handler's effects, a modal dismissal among
355    /// them, have been applied.
356    pub(crate) pending_focus_intents: Vec<crate::intent::Intent>,
357    /// The name of the intent whose action is running this handler, if it
358    /// is an action's. See [`handling_intent`](EventContext::handling_intent).
359    pub(crate) handling_intent: Option<&'static str>,
360    /// The dispatcher sets this to the appropriate
361    /// [`IntentSource`](crate::telemetry::IntentSource) before
362    /// invoking a typed handler (menu select → `Menu`, AccessKit
363    /// action → `Accessibility`, on_tap / button activation →
364    /// `Handler`, …). `send_intent` reads it and stamps the intent
365    /// before queuing. `None` outside a managed handler — bare
366    /// programmatic sends keep their `Intent::source` value
367    /// (default `Programmatic`).
368    pub(crate) current_source: Option<crate::telemetry::IntentSource>,
369    /// Key-capture callback armed via `ctx.begin_key_capture(...)`.
370    /// The callback + its shared slot are installed on the tree by
371    /// `collect_from_ctx`. Only one per ctx; the last caller wins.
372    pub(crate) pending_key_capture: Option<crate::shortcut::KeyCaptureSlot>,
373    /// Set to request cancellation of any armed key capture.
374    pub(crate) cancel_key_capture: bool,
375    /// Deferred mutations to the tree's [`ShortcutRegistry`](crate::shortcut::ShortcutRegistry),
376    /// typically issued by settings-UI buttons to rebind or clear
377    /// overrides. Applied in `collect_from_ctx` after the handler
378    /// returns.
379    pub(crate) pending_shortcut_mutations: Vec<ShortcutMutation>,
380    /// Requests that the app-level event loop close the window this
381    /// tree belongs to. Drained after dispatch via
382    /// `WidgetTree::take_close_window_request`. Routed through the
383    /// window's close guard (if any) — see
384    /// [`WindowConfig::on_close_requested`](crate::window::WindowConfig::on_close_requested).
385    pub(crate) close_window_requested: bool,
386    /// Like [`close_window_requested`](Self::close_window_requested), but
387    /// **bypasses** the window's close guard. Set by
388    /// [`close_window_forced`](Self::close_window_forced). Drained after
389    /// dispatch via `WidgetTree::take_force_close_request`. The escape
390    /// hatch a confirmation dialog uses once the user confirms.
391    pub(crate) force_close_requested: bool,
392    /// Whether an in-window modal was open when this context was made. Read
393    /// by [`give_close_request_to_modal`](Self::give_close_request_to_modal).
394    pub(crate) modal_open: bool,
395    /// Set by [`give_close_request_to_modal`](Self::give_close_request_to_modal);
396    /// answered in `collect_from_ctx` as a close request with a modal open.
397    pub(crate) close_request_to_modal: bool,
398    /// Set by [`request_accessibility_update`](EventContext::request_accessibility_update);
399    /// drained in `collect_from_ctx` to set `WidgetTree::a11y_dirty`, forcing the
400    /// next `sync_accessibility` to re-walk the AccessKit tree. The general lever for a
401    /// composing widget that restructured its subtree in a way that changes the AT tree
402    /// (relayout alone no longer re-walks AT).
403    pub(crate) request_a11y_update: bool,
404    /// Set by [`request_soft_keyboard`](EventContext::request_soft_keyboard);
405    /// drained in `collect_from_ctx` onto the tree, from where the app layer
406    /// takes it once per dispatch — after the IME-allowance reconcile, which
407    /// is the only place that knows whether re-asserting would cancel a live
408    /// composition.
409    pub(crate) soft_keyboard_request: Option<bool>,
410    /// Messages queued by [`announce`](EventContext::announce) /
411    /// [`announce_with`](EventContext::announce_with), drained into the tree's
412    /// own live regions by `collect_from_ctx`. See [`crate::announcer`].
413    pub(crate) announcements: Vec<(String, crate::announcer::Politeness)>,
414    /// Layout direction (LTR/RTL) of the hosting tree, snapshotted at
415    /// handler-invocation time by `make_event_context`. Read via
416    /// [`is_rtl`](EventContext::is_rtl) so pointer / keyboard / drag
417    /// handlers can mirror their x-axis logic live — a runtime locale
418    /// switch dirties the tree but does **not** rebuild, so direction
419    /// must be read here rather than captured at `build()` time.
420    /// Defaults to `LeftToRight` for hand-constructed (test) contexts.
421    pub(crate) layout_direction: crate::environment::LayoutDirection,
422    /// What the tree knows about the sample being dispatched: which pointer
423    /// produced it, where it was, and — for a scroll — its phase and source.
424    /// Snapshotted by `make_event_context` from the tree's in-flight sample.
425    /// Holds its default (a mouse at the epoch) for hand-constructed contexts
426    /// and for handlers run outside a pointer dispatch (a timer, an
427    /// accessibility action).
428    pub(crate) input: crate::pointer::InputSnapshot,
429    /// The frozen [`TouchAction`] for the gesture being handled. Populated by
430    /// `make_event_context` from the pressed pointer's sequence; defaults to
431    /// [`TouchAction::AUTO`] for a hand-constructed context and for a handler
432    /// running outside a press — see
433    /// [`touch_action`](EventContext::touch_action) and
434    /// `crate::pointer::touch_action`.
435    pub(crate) touch_action: TouchAction,
436    /// The framework press held by the pointer being dispatched, as the router
437    /// tracks it: `(inside, pending)`. `None` when that pointer holds no press
438    /// — every handler outside a press, and every hand-constructed context.
439    /// Read by [`is_pressed`](EventContext::is_pressed) and its two siblings.
440    pub(crate) press: Option<(bool, bool)>,
441    /// Debug-only WCAG 3.2.1 guard: `Some(flag)` where `flag` is set while a
442    /// focus-change dispatch is running. `open_window` / `focus_window` warn if
443    /// invoked while it reads `true` (a focus handler changing context). `None`
444    /// for hand-constructed (test) contexts.
445    pub(crate) in_focus_dispatch: Option<std::rc::Rc<std::cell::Cell<bool>>>,
446    /// What asked for the context menu whose factory this context is serving.
447    /// Set by `show_context_menu_for` only; `None` everywhere else. Read by
448    /// [`context_menu_trigger`](EventContext::context_menu_trigger).
449    pub(crate) context_menu_trigger: Option<crate::widget_builder::ContextMenuTrigger>,
450}
451
452/// One arbitration act a handler performed on its pointer's sequence.
453///
454/// Queued on the context and applied in order by
455/// `WidgetTree::collect_from_ctx`, so a handler that claims and then rejects
456/// leaves the sequence in the state its last word describes.
457#[derive(Debug, Clone, Copy, PartialEq, Eq)]
458pub(crate) enum GestureAct {
459    /// [`EventContext::claim_gesture`].
460    Claim,
461    /// [`EventContext::reject_gesture`].
462    Reject,
463    /// [`EventContext::hold_gesture`].
464    Hold,
465    /// [`EventContext::release_gesture`].
466    Release,
467}
468
469/// Deferred edit to the tree's shortcut registry, queued on an
470/// `EventContext` and applied in `collect_from_ctx`.
471#[derive(Debug, Clone)]
472pub(crate) enum ShortcutMutation {
473    RebindPrimary {
474        id: String,
475        keystroke: Option<crate::shortcut::KeyStroke>,
476    },
477    RebindSecondary {
478        id: String,
479        keystroke: Option<crate::shortcut::KeyStroke>,
480    },
481    ClearOverride {
482        id: String,
483    },
484}
485
486/// A structural change to the widget tree, deferred until after event dispatch.
487pub(crate) enum TreeMutation {
488    SetDormant(WidgetId),
489    Activate(WidgetId),
490    Destroy(WidgetId),
491    /// Typed mutable access to a mounted widget, applied in
492    /// `apply_tree_mutations` where `&mut arena` is live. The boxed closure
493    /// downcasts the node's `as_any_mut()` to the requested concrete type;
494    /// `dirty` selects the post-mutation re-render level.
495    WithWidgetMut {
496        id: WidgetId,
497        dirty: crate::binding::BindingLevel,
498        apply: Box<dyn FnOnce(&mut dyn std::any::Any)>,
499    },
500    /// Re-run one widget's `build()` **now**, inside `apply_tree_mutations`,
501    /// rather than marking it for the next layout pass. See
502    /// [`EventContext::materialize_now`].
503    MaterializeNow(WidgetId),
504    /// `Space` on a data view's focused row: run the keyboard-toggle action
505    /// published inside `row`, or `fallback` when the row publishes none.
506    ///
507    /// Deferred rather than resolved in the handler because finding the action
508    /// means walking the row's subtree, and `EventContext` is a command buffer
509    /// with no view of the arena. Carrying the fallback keeps the decision in
510    /// one place: whether a row has a checkbox is a fact about the tree, not
511    /// something the key handler can know.
512    RowSpaceActivate {
513        row: WidgetId,
514        fallback: std::rc::Rc<dyn Fn(&mut EventContext)>,
515    },
516}
517
518// Manual `Debug`: the `WithWidgetMut` closure is not `Debug`.
519impl std::fmt::Debug for TreeMutation {
520    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
521        match self {
522            Self::SetDormant(id) => f.debug_tuple("SetDormant").field(id).finish(),
523            Self::Activate(id) => f.debug_tuple("Activate").field(id).finish(),
524            Self::MaterializeNow(id) => f.debug_tuple("MaterializeNow").field(id).finish(),
525            Self::Destroy(id) => f.debug_tuple("Destroy").field(id).finish(),
526            Self::WithWidgetMut { id, dirty, .. } => f
527                .debug_struct("WithWidgetMut")
528                .field("id", id)
529                .field("dirty", dirty)
530                .finish_non_exhaustive(),
531            Self::RowSpaceActivate { row, .. } => f
532                .debug_struct("RowSpaceActivate")
533                .field("row", row)
534                .finish_non_exhaustive(),
535        }
536    }
537}
538
539impl<'ops> EventContext<'ops> {
540    pub(crate) fn new() -> Self {
541        Self {
542            cursor_request: None,
543            declared_cursor_request: None,
544            tree_mutations: Vec::new(),
545            idle_callbacks: Vec::new(),
546            modal_requests: Vec::new(),
547            dismiss_modal: false,
548            overlay_requests: Vec::new(),
549            overlay_dismissals: Vec::new(),
550            overlay_content_dismissals: Vec::new(),
551            overlay_band_requests: Vec::new(),
552            overlay_placement_updates: Vec::new(),
553            overlay_placement_sources: Vec::new(),
554            overlay_pause_requests: Vec::new(),
555            dismiss_scope: None,
556            pointer_capture: None,
557            pointer_captor: None,
558            dispatch_node: None,
559            local_frame: None,
560            dispatch_target: None,
561            focus_origin: None,
562            delayed_overlay_requests: Vec::new(),
563            timed_overlay_requests: Vec::new(),
564            reveal_overlay_requests: Vec::new(),
565            dismiss_descendant_overlays: Vec::new(),
566            cancel_delayed_overlays: Vec::new(),
567            safe_region_arm_requests: Vec::new(),
568            repaint_requests: Vec::new(),
569            synthetic_clicks: Vec::new(),
570            focus_requests: Vec::new(),
571            focus_into_requests: Vec::new(),
572            clear_focus_requested: false,
573            scroll_into_view_requests: Vec::new(),
574            scroll_widget_into_view_requests: Vec::new(),
575            highlight_tooltip_requests: Vec::new(),
576            drag_start_request: None,
577            cancel_drag: false,
578            drag_is_external: false,
579            key_identity: None,
580            theme_request: None,
581            follow_system_request: false,
582            locale_request: None,
583            text_scale_request: None,
584            frame_requested: false,
585            app_context: None,
586            pending_intents: Vec::new(),
587            pending_focus_intents: Vec::new(),
588            handling_intent: None,
589            current_source: None,
590            pending_key_capture: None,
591            cancel_key_capture: false,
592            pending_shortcut_mutations: Vec::new(),
593            close_window_requested: false,
594            force_close_requested: false,
595            modal_open: false,
596            close_request_to_modal: false,
597            request_a11y_update: false,
598            soft_keyboard_request: None,
599            announcements: Vec::new(),
600            window_ops: None,
601            current_window: None,
602            tree_window_active: true,
603            tree_screen_reader: crate::environment::ScreenReaderState::Unknown,
604            tree_pointer_position: None,
605            press_claimed_by_interactive_child: false,
606            overlay_bounds_snapshot: Vec::new(),
607            focused_widget: None,
608            layout_direction: crate::environment::LayoutDirection::LeftToRight,
609            input: crate::pointer::InputSnapshot::default(),
610            touch_action: TouchAction::AUTO,
611            press: None,
612            in_focus_dispatch: None,
613            context_menu_trigger: None,
614            explicit_capture: false,
615            recognized_owning_gesture: false,
616            drag_activation_override: None,
617            gesture_acts: Vec::new(),
618            cancel_pointer_request: None,
619        }
620    }
621
622    /// Record the [`TouchAction`] frozen at press for the sequence owning the
623    /// pointer being dispatched. Called by `make_event_context`.
624    pub(crate) fn with_touch_action(mut self, action: TouchAction) -> Self {
625        self.touch_action = action;
626        self
627    }
628
629    /// Record the framework press held by the pointer being dispatched, as
630    /// `(inside, pending)`. Called by `make_event_context`.
631    pub(crate) fn with_press(mut self, press: Option<(bool, bool)>) -> Self {
632        self.press = press;
633        self
634    }
635
636    /// Whether the pointer being dispatched holds a press whose visual is
637    /// showing — inside its tap boundary and past any press-feedback delay.
638    ///
639    /// The framework already drives the pressed node's own
640    /// [`pressed_signal`](crate::BuildContext::pressed_signal) from the same
641    /// state; this is for a handler that has to *branch* on the press rather
642    /// than paint it. `false` outside a press.
643    pub fn is_pressed(&self) -> bool {
644        matches!(self.press, Some((true, false)))
645    }
646
647    /// Whether the pointer being dispatched holds a press that has not left
648    /// its tap boundary. Unlike [`is_pressed`](Self::is_pressed) this is still
649    /// true during the press-feedback delay: the press is real, only its
650    /// visual is being withheld.
651    pub fn press_is_inside(&self) -> bool {
652        matches!(self.press, Some((true, _)))
653    }
654
655    /// Whether the pointer being dispatched holds a press whose feedback delay
656    /// has not elapsed — the press is inside a pan claimant and the framework
657    /// is waiting to see whether it becomes a scroll.
658    pub fn press_pending(&self) -> bool {
659        matches!(self.press, Some((_, true)))
660    }
661
662    /// Record what the tree knows about the sample being dispatched. Called by
663    /// `make_event_context` once per event batch.
664    pub(crate) fn with_input_snapshot(mut self, input: crate::pointer::InputSnapshot) -> Self {
665        self.input = input;
666        self
667    }
668
669    /// Record who holds the capture of the pointer being dispatched, so
670    /// [`owns_pointer`](Self::owns_pointer) can answer without a tree lookup.
671    pub(crate) fn with_pointer_captor(mut self, captor: Option<WidgetId>) -> Self {
672        self.pointer_captor = captor;
673        self
674    }
675
676    /// Record the node the arena picked for this event — the *target*, which is
677    /// not in general the node whose handler is running (a preview fires on
678    /// every strict ancestor of it).
679    pub(crate) fn with_dispatch_target(mut self, target: WidgetId) -> Self {
680        self.dispatch_target = Some(target);
681        self
682    }
683
684    /// Record which node's handler is about to run.
685    pub(crate) fn with_dispatch_node(mut self, node: WidgetId) -> Self {
686        self.dispatch_node = Some(node);
687        self
688    }
689
690    /// Record the handler space of the node whose handler is about to run —
691    /// the frame its event was localised into.
692    pub(crate) fn with_local_frame(mut self, frame: crate::arena::LocalFrame) -> Self {
693        self.local_frame = Some(frame);
694        self
695    }
696
697    /// A window-logical point in the space this handler receives positions
698    /// in: the conversion the dispatcher applied to the event's own position.
699    ///
700    /// For the positions a handler is handed in window space —
701    /// [`WidgetEvent::Scroll`](crate::event::WidgetEvent::Scroll)'s and
702    /// [`WidgetEvent::PointerCancel`](crate::event::WidgetEvent::PointerCancel)'s
703    /// `window_position`, the [`coalesced`](Self::coalesced) samples,
704    /// [`tree_pointer_position`](Self::tree_pointer_position) — when the
705    /// handler works in its own: exact under a `Scale`, a `Rotate` or a
706    /// `SceneView`, where subtracting the widget's origin is not.
707    /// [`WidgetTree::window_to_local`](crate::WidgetTree::window_to_local) is
708    /// the same conversion outside a handler.
709    ///
710    /// `None` for a context made outside per-node dispatch (a gesture timer, a
711    /// key-capture callback, an async completion): there is no widget to be
712    /// local to.
713    pub fn to_local(&self, window_point: teksilo_canvas::Point) -> Option<teksilo_canvas::Point> {
714        self.local_frame.map(|frame| frame.to_local(window_point))
715    }
716
717    /// What asked for the context menu, inside a
718    /// [`ContextMenuFactory`](crate::widget_builder::ContextMenuFactory); `None`
719    /// in any other handler.
720    ///
721    /// The factory's point is where the user pointed only for
722    /// [`Pointer`](crate::widget_builder::ContextMenuTrigger::Pointer). A menu
723    /// the keyboard or an assistive technology asked for is anchored on the
724    /// widget it is about, so a factory that moves a caret or picks an item
725    /// from the point does that for a pointer alone.
726    pub fn context_menu_trigger(&self) -> Option<crate::widget_builder::ContextMenuTrigger> {
727        self.context_menu_trigger
728    }
729
730    /// The pointer that produced the event being handled.
731    ///
732    /// Two dispatches have a pointer without having a sample, and both report
733    /// it: a gesture the timer recognised — a hold — reports the **contact that
734    /// held**, and a drag-and-drop handler (`on_drag_hover` / `on_drag_tick` /
735    /// `on_drag_leave` / `on_drop`) reports the pointer **that started the
736    /// drag**, which is what makes it right inside a tick fired from a layout
737    /// pass or an OS drag phase delivered from a platform thread. Outside any
738    /// pointer, scroll, gesture or drag dispatch — an assistive-technology
739    /// action, a hand-constructed test context — this is the mouse at the tree
740    /// epoch, which is the same answer every such handler got before pointers
741    /// were distinguishable.
742    pub fn pointer(&self) -> crate::pointer::PointerInfo {
743        self.input.pointer
744    }
745
746    /// What kind of device is pointing: mouse, finger, stylus.
747    ///
748    /// The one question most handlers actually need — it is what decides
749    /// whether a hover affordance is reachable, whether a target needs slop,
750    /// and which gesture profile governs.
751    pub fn pointer_kind(&self) -> teksilo_tokens::PointerKind {
752        self.input.pointer.kind
753    }
754
755    /// Where the pointer was, in window-logical coordinates, when the event
756    /// being handled was produced.
757    ///
758    /// `None` for an event that carries no position — a keyboard-driven
759    /// scroll, a wheel notch (which routes by hover rather than by position),
760    /// anything dispatched outside a pointer sample. Distinct from
761    /// [`tree_pointer_position`](Self::tree_pointer_position), which reports
762    /// where the pointer is *at this instant* regardless of what is being
763    /// dispatched.
764    pub fn pointer_position(&self) -> Option<teksilo_canvas::Point> {
765        self.input.position
766    }
767
768    /// The positions the OS batched into the packet being dispatched, oldest
769    /// first and **excluding** the packet's own
770    /// ([`pointer_position`](Self::pointer_position), which is the newest).
771    ///
772    /// A backend whose device outruns the window's message rate hands over one
773    /// sample per *message* and puts the intermediate positions here, each with
774    /// the [`PointerAxes`](crate::pointer::PointerAxes) it was sampled at and
775    /// its own [`EventTime`](crate::pointer::EventTime). A surface that must
776    /// see every position a stylus produced — an ink tool is the case this
777    /// exists for — iterates these and *then* handles
778    /// [`pointer_position`](Self::pointer_position), and is correct whether or
779    /// not the backend batches, because a backend that does not returns an
780    /// empty slice.
781    ///
782    /// **Window**-logical coordinates, like
783    /// [`WidgetEvent::Scroll`](crate::event::WidgetEvent::Scroll)'s `window_position` and
784    /// for the same reason: a batch has no single widget to localise against.
785    /// A handler working in its own space converts at the use site.
786    ///
787    /// Empty on every dispatch that is not a pointer sample — a gesture the
788    /// timer recognised, a drag-and-drop tick, an assistive-technology action.
789    /// Those batched nothing, and reporting whichever sample arrived last would
790    /// attribute its positions to a gesture that did not produce them.
791    pub fn coalesced(&self) -> &[crate::pointer::CoalescedSample] {
792        &self.input.coalesced
793    }
794
795    /// Where in a continuous scroll gesture the event being handled sits.
796    ///
797    /// [`ScrollPhase::Discrete`](crate::pointer::ScrollPhase::Discrete) — a
798    /// self-contained wheel notch — for everything that is not a phased
799    /// gesture, which is every scroll Teksilo produced before the touch
800    /// programme.
801    pub fn scroll_phase(&self) -> crate::pointer::ScrollPhase {
802        self.input.scroll_phase
803    }
804
805    /// What produced the scroll being handled: a notched wheel, a precision
806    /// trackpad, a synthesised touch pan, or the app itself.
807    pub fn scroll_source(&self) -> crate::pointer::ScrollSource {
808        self.input.scroll_source
809    }
810
811    /// The [`TouchAction`] governing the gesture being handled.
812    ///
813    /// The value is **frozen at press** for the whole gesture's lifetime: the
814    /// router computes it once, from `WidgetTree::effective_touch_action` of
815    /// the pressed target, and stores it on that pointer's
816    /// [`PointerSequence`](crate::gesture::PointerSequence), so a handler never
817    /// re-reads a subtree that may have rebuilt mid-gesture.
818    ///
819    /// [`TouchAction::AUTO`] — the neutral value — outside a press, and for a
820    /// hand-constructed context. A mouse never consults this at all. See
821    /// `crate::pointer::touch_action`.
822    pub fn touch_action(&self) -> TouchAction {
823        self.touch_action
824    }
825
826    /// Snapshot the hosting tree's layout direction. Called by
827    /// `make_event_context` once per event batch so x-axis handlers
828    /// (resize, drag-reorder, arrow-key navigation) can mirror under
829    /// RTL without a rebuild.
830    pub(crate) fn with_layout_direction(
831        mut self,
832        direction: crate::environment::LayoutDirection,
833    ) -> Self {
834        self.layout_direction = direction;
835        self
836    }
837
838    /// Layout direction of the hosting tree at dispatch time.
839    pub fn layout_direction(&self) -> crate::environment::LayoutDirection {
840        self.layout_direction
841    }
842
843    /// Whether the hosting tree is laid out right-to-left. Mirrors
844    /// [`LayoutContext::is_rtl`](crate::widget::LayoutContext::is_rtl)
845    /// for the event-dispatch side.
846    pub fn is_rtl(&self) -> bool {
847        self.layout_direction == crate::environment::LayoutDirection::RightToLeft
848    }
849
850    /// The widget that held focus when this event batch began.
851    ///
852    /// A snapshot, not a live read: it answers what the tree's focus was at
853    /// dispatch time, so a handler that has already called
854    /// [`request_focus`](EventContext::request_focus) still sees the old
855    /// value. That is the useful reading for a handler deciding *whether* to
856    /// act on the focused widget.
857    ///
858    /// `None` when nothing is focused, and also for an `EventContext` built
859    /// outside `WidgetTree::make_event_context`, which is what a hand-made
860    /// test context is. Treat it as `None`-safe, like the other snapshots.
861    ///
862    /// The reason this exists: a widget-scoped shortcut fires before the
863    /// focused widget sees the key, so a container that binds a key which its
864    /// own children also handle has no other way to yield to them.
865    /// `MessageBox` is the case that asked for it, where Enter is bound to the
866    /// default button and must not answer for the button the user has actually
867    /// tabbed to.
868    pub fn focused(&self) -> Option<WidgetId> {
869        self.focused_widget
870    }
871
872    /// Attach a per-dispatch snapshot of read-only tree query state
873    /// (current pointer position, overlay bounds, focus). Called by
874    /// `WidgetTree::make_event_context` once per event batch. Test
875    /// `EventContext`s that don't go through that path stay with
876    /// empty snapshots — handlers must treat every read as `None`-
877    /// safe.
878    pub(crate) fn with_query_snapshot(
879        mut self,
880        pointer: Option<teksilo_canvas::Point>,
881        overlays: Vec<(
882            WidgetId,
883            teksilo_canvas::Rect,
884            Option<teksilo_canvas::Point>,
885        )>,
886        focused: Option<WidgetId>,
887    ) -> Self {
888        self.tree_pointer_position = pointer;
889        self.overlay_bounds_snapshot = overlays;
890        self.focused_widget = focused;
891        self
892    }
893
894    /// Attach the app-level window-ops sink and the hosting tree's
895    /// [`WindowState`](crate::window::WindowState). Called by the
896    /// dispatcher once per event batch so handlers can reach the
897    /// multi-window API synchronously.
898    pub(crate) fn with_window_context(
899        mut self,
900        ops: &'ops mut dyn crate::window::WindowOps,
901        current_window: Option<crate::window::WindowState>,
902    ) -> Self {
903        self.window_ops = Some(ops);
904        self.current_window = current_window;
905        self
906    }
907
908    /// Snapshot the tree's occlusion-aware window-active state. Called by
909    /// `make_event_context` once per event batch so handlers can read
910    /// [`window_active`](Self::window_active).
911    pub(crate) fn with_window_active(mut self, active: bool) -> Self {
912        self.tree_window_active = active;
913        self
914    }
915
916    /// Snapshot the tree's OS screen-reader state. Called by
917    /// `make_event_context` once per event batch so handlers can read
918    /// [`screen_reader_state`](Self::screen_reader_state).
919    pub(crate) fn with_screen_reader_state(
920        mut self,
921        state: crate::environment::ScreenReaderState,
922    ) -> Self {
923        self.tree_screen_reader = state;
924        self
925    }
926
927    /// Record whether an in-window modal is open in the tree.
928    pub(crate) fn with_modal_open(mut self, open: bool) -> Self {
929        self.modal_open = open;
930        self
931    }
932
933    /// Attach the tree's shared "inside focus dispatch" flag (WCAG 3.2.1
934    /// debug guard). See [`EventContext::open_window`].
935    pub(crate) fn with_focus_dispatch_flag(
936        mut self,
937        flag: std::rc::Rc<std::cell::Cell<bool>>,
938    ) -> Self {
939        self.in_focus_dispatch = Some(flag);
940        self
941    }
942
943    /// Debug-only: warn (once per call) if a context change is being made from
944    /// inside a focus-change dispatch — a WCAG 3.2.1 (On Focus) anti-pattern.
945    /// Compiled out entirely in release builds.
946    #[inline]
947    fn warn_if_context_change_in_focus_dispatch(&self, what: &str) {
948        #[cfg(debug_assertions)]
949        if self.in_focus_dispatch.as_ref().is_some_and(|f| f.get()) {
950            eprintln!(
951                "[teksilo a11y] WCAG 3.2.1 (On Focus): `{what}` was called from \
952                 inside an on_focus handler. Changing context (opening/focusing a \
953                 window, navigating) merely because a control received focus \
954                 surprises keyboard users tabbing through the UI. Move this to an \
955                 explicit activation handler (on_tap / on_activate / a shortcut)."
956            );
957        }
958        let _ = what;
959    }
960
961    /// Attach the tree's app-state registry so handlers can look up
962    /// application-scoped values (`ClipboardHandle`, `SharedTypesetter`,
963    /// …). Called by the dispatcher once per event batch.
964    pub(crate) fn with_app_context(
965        mut self,
966        ctx: std::rc::Rc<crate::event_source::TreeAppContext>,
967    ) -> Self {
968        self.app_context = Some(ctx);
969        self
970    }
971
972    /// Record whether the drag session active while this context is live
973    /// originated from an external (OS) drag. Set by `make_event_context`.
974    pub(crate) fn with_drag_external(mut self, is_external: bool) -> Self {
975        self.drag_is_external = is_external;
976        self
977    }
978
979    pub(crate) fn with_key_identity(mut self, identity: Option<u64>) -> Self {
980        self.key_identity = identity;
981        self
982    }
983
984    /// The identity of the key whose `KeyDown` or `KeyUp` is being handled:
985    /// the physical key when the window manager reported one, so a press
986    /// and its release match even when their logical keys differ ("€"
987    /// pressed, "2" released once Shift is up), else a hash of the logical
988    /// key. `None` outside a key event. For a widget that forwards keys and
989    /// must release what it pressed (a web view).
990    #[doc(hidden)]
991    pub fn key_identity(&self) -> Option<u64> {
992        self.key_identity
993    }
994
995    /// Whether a drag is currently in flight that was started by an external
996    /// (OS) drag-and-drop (files / text / URLs from another application),
997    /// rather than by an in-app `start_drag`. Useful in `on_drag_leave` /
998    /// `on_drag_tick` handlers, which don't receive the payload directly;
999    /// in `on_drag_hover` / `on_drop` prefer `payload.is_external()`.
1000    pub fn drag_is_external(&self) -> bool {
1001        self.drag_is_external
1002    }
1003
1004    /// Look up an application-scoped value by type. Mirrors
1005    /// `BuildContext::app_state`. Returns `None` when the handler was
1006    /// invoked without a registry (hand-constructed `EventContext` in
1007    /// tests, or when no value of that type was registered).
1008    pub fn app_state<T: 'static>(&self) -> Option<&T> {
1009        self.app_context
1010            .as_ref()
1011            .and_then(|ctx| ctx.app_state::<T>())
1012    }
1013
1014    /// Borrow the [`AppEventPoster`](crate::AppEventPoster) installed
1015    /// by the framework. Used by integrations that need to post
1016    /// typed payloads back to the UI loop from a worker thread
1017    /// (`teksilo_platform::file_dialog`'s `RfdAsyncBackend`, future
1018    /// async-result features). Returns `None` for hand-constructed
1019    /// `EventContext`s in tests.
1020    pub fn poster(&self) -> Option<&std::sync::Arc<dyn crate::AppEventPoster>> {
1021        self.app_context.as_ref().and_then(|ctx| ctx.poster())
1022    }
1023
1024    /// Ask the tree to pump one more frame after this handler returns.
1025    /// Use from event handlers that kick off per-frame work (pending
1026    /// document events to drain, drag-select auto-scroll, caret blink
1027    /// restart on focus). See `WidgetTree::request_frame` for the
1028    /// draw-when-needed contract.
1029    pub fn request_frame(&mut self) {
1030        self.frame_requested = true;
1031    }
1032
1033    /// Dispatch an [`Intent`](crate::intent::Intent) as if the source
1034    /// widget pressed its keyboard shortcut. The framework walks
1035    /// source-widget → root after the current handler returns,
1036    /// invoking any matching [`Action`](crate::action::Action) it
1037    /// finds. From inside an open modal presented in the window, what
1038    /// nothing in the modal takes walks on to the page behind it, from the
1039    /// widget the modal was opened from. Unmatched intents are silently
1040    /// dropped.
1041    ///
1042    /// The intent's `source` is overridden by the dispatcher's
1043    /// current handler-source label (`current_source`) when one is
1044    /// active. This is how the framework distinguishes
1045    /// `IntentSource::Handler` (button taps, generic on_tap) from
1046    /// `IntentSource::Menu`, `IntentSource::Accessibility`, etc.
1047    /// Programmatic callers outside any handler pass through with
1048    /// `IntentSource::Programmatic` (the default).
1049    pub fn send_intent(&mut self, intent: impl Into<crate::intent::Intent>) {
1050        let mut intent: crate::intent::Intent = intent.into();
1051        if let Some(src) = self.current_source {
1052            intent.source = src;
1053        }
1054        self.pending_intents.push(intent);
1055    }
1056
1057    /// Queue `intent` to be dispatched from the widget that holds keyboard
1058    /// focus once this handler's other effects have been applied, the way a
1059    /// keyboard shortcut is dispatched. [`send_intent`](Self::send_intent)
1060    /// dispatches from the widget whose handler is running instead.
1061    ///
1062    /// The difference matters for a surface that runs commands on behalf of
1063    /// whatever the user was working in and then closes, such as a command
1064    /// palette: it calls [`dismiss_modal`](Self::dismiss_modal), focus goes
1065    /// back to the editor the palette was opened over, and the intent then
1066    /// walks from that editor, so the actions it registered answer. Sent with
1067    /// `send_intent`, the same intent would start from the palette, which is
1068    /// gone by the time the walk runs, and walk on from the widget the palette
1069    /// was attached to rather than from the editor.
1070    ///
1071    /// A menu row is the other such surface. It closes its menu
1072    /// ([`dismiss_self_overlay_chain`](Self::dismiss_self_overlay_chain)),
1073    /// which gives focus back to the widget the menu was opened over, and its
1074    /// command starts there: a menu bar's Edit ▸ Copy copies in the focused
1075    /// field. The focus is read after every overlay this handler closes has
1076    /// given focus back, whatever order the handler asked for things in.
1077    ///
1078    /// With nothing focused, it starts in the topmost modal overlay that is
1079    /// up when this handler is not in one (as if focus were there: a dialog
1080    /// is in front of the page), and otherwise falls back to this handler's
1081    /// widget, its walk crossing the overlays it was in as they were when the
1082    /// handler ran; when the handler closed the modal it was in (a palette
1083    /// opened with nothing focused), to the window's root instead, where an
1084    /// intent with no source widget starts.
1085    pub fn send_intent_from_focus(&mut self, intent: impl Into<crate::intent::Intent>) {
1086        let mut intent: crate::intent::Intent = intent.into();
1087        if let Some(src) = self.current_source {
1088            intent.source = src;
1089        }
1090        self.pending_focus_intents.push(intent);
1091    }
1092
1093    /// The name of the intent this handler is the action of, `None` outside
1094    /// an [`Action`](crate::action::Action)'s handler. What a surface opened
1095    /// by a command reads to know which command opened it: the command
1096    /// palette closes again on the chord of the command that presented it.
1097    #[doc(hidden)]
1098    pub fn handling_intent(&self) -> Option<&'static str> {
1099        self.handling_intent
1100    }
1101
1102    /// Run a closure with the given `IntentSource` active. Any
1103    /// `ctx.send_intent(...)` issued from within the closure will
1104    /// be tagged with this source instead of the dispatcher's
1105    /// default (`Handler` / `Shortcut` / `Accessibility`).
1106    ///
1107    /// The previous source is restored after the closure returns.
1108    /// Panic during the closure unwinds the dispatcher's whole
1109    /// frame, so the EventContext is destroyed before the next
1110    /// dispatch — no need for a panic-safe drop guard.
1111    ///
1112    /// Used by framework widgets that want a more specific source
1113    /// label than the default — `MenuItem` wraps its activation
1114    /// handler to emit `IntentSource::Menu`, etc.
1115    pub fn with_intent_source<R>(
1116        &mut self,
1117        source: crate::telemetry::IntentSource,
1118        f: impl FnOnce(&mut Self) -> R,
1119    ) -> R {
1120        let prev = self.current_source.replace(source);
1121        let r = f(self);
1122        self.current_source = prev;
1123        r
1124    }
1125
1126    /// Arm a one-shot key-capture callback, returning a
1127    /// [`CaptureHandle`](crate::shortcut::CaptureHandle) whose `Drop`
1128    /// cancels the capture if it hasn't fired yet. The next `KeyDown`
1129    /// bypasses shortcut resolution and invokes the callback with:
1130    /// - the captured [`KeyStroke`](crate::shortcut::KeyStroke)
1131    /// - mutable access to the registry (rebinds in-place)
1132    /// - a mutable [`EventContext`] (emit commands, send intents,
1133    ///   dismiss overlays, …)
1134    ///
1135    /// The handle must be stored somewhere with an appropriate
1136    /// lifetime (typically in the calling widget's state) or the
1137    /// capture will be cancelled immediately when the returned
1138    /// handle drops at end of scope.
1139    pub fn begin_key_capture(
1140        &mut self,
1141        callback: impl FnOnce(
1142            crate::shortcut::KeyStroke,
1143            &mut crate::shortcut::ShortcutRegistry,
1144            &mut EventContext,
1145        ) + 'static,
1146    ) -> crate::shortcut::CaptureHandle {
1147        let slot: crate::shortcut::KeyCaptureSlot =
1148            std::rc::Rc::new(std::cell::RefCell::new(Some(Box::new(callback))));
1149        self.pending_key_capture = Some(slot.clone());
1150        self.cancel_key_capture = false;
1151        crate::shortcut::CaptureHandle::new(slot)
1152    }
1153
1154    /// Cancel any key capture armed earlier in this handler or via
1155    /// `WidgetTree::begin_key_capture` before the handler ran.
1156    pub fn cancel_key_capture(&mut self) {
1157        self.pending_key_capture = None;
1158        self.cancel_key_capture = true;
1159    }
1160
1161    /// Queue a deferred rebind of the primary keystroke for the
1162    /// registered shortcut with the given id. Applied by the tree
1163    /// after the current handler returns. Use `None` to explicitly
1164    /// unbind the slot.
1165    pub fn rebind_shortcut_primary(
1166        &mut self,
1167        id: impl Into<String>,
1168        keystroke: Option<crate::shortcut::KeyStroke>,
1169    ) {
1170        self.pending_shortcut_mutations
1171            .push(ShortcutMutation::RebindPrimary {
1172                id: id.into(),
1173                keystroke,
1174            });
1175    }
1176
1177    /// Queue a deferred rebind of the secondary keystroke for the
1178    /// registered shortcut with the given id.
1179    pub fn rebind_shortcut_secondary(
1180        &mut self,
1181        id: impl Into<String>,
1182        keystroke: Option<crate::shortcut::KeyStroke>,
1183    ) {
1184        self.pending_shortcut_mutations
1185            .push(ShortcutMutation::RebindSecondary {
1186                id: id.into(),
1187                keystroke,
1188            });
1189    }
1190
1191    /// Queue a deferred clear of any user override for the given
1192    /// shortcut id, restoring its declared defaults.
1193    pub fn clear_shortcut_override(&mut self, id: impl Into<String>) {
1194        self.pending_shortcut_mutations
1195            .push(ShortcutMutation::ClearOverride { id: id.into() });
1196    }
1197
1198    /// Request that the application close the window this tree
1199    /// belongs to. Drained by the app event loop after the handler
1200    /// returns. Typical use: title-bar close button handlers.
1201    ///
1202    /// This is a *guarded* close: if the window declared a close guard
1203    /// via
1204    /// [`WindowConfig::on_close_requested`](crate::window::WindowConfig::on_close_requested)
1205    /// or [`can_close`](crate::window::WindowConfig::can_close), that
1206    /// guard runs first and may veto the close or defer its answer. While a
1207    /// modal dialog window blocks this window, the request brings the dialog
1208    /// forward and closes nothing, as a click on its close button does. To
1209    /// skip the guard, use [`close_window_forced`](Self::close_window_forced).
1210    pub fn close_window(&mut self) {
1211        self.close_window_requested = true;
1212    }
1213
1214    /// Request that the application close this tree's window
1215    /// **unconditionally**, bypassing any close guard declared via
1216    /// [`WindowConfig::on_close_requested`](crate::window::WindowConfig::on_close_requested)
1217    /// / [`can_close`](crate::window::WindowConfig::can_close).
1218    ///
1219    /// It closes the window this context belongs to, which for the answer
1220    /// of a dialog is not always the window it is about: a `MessageBox` is
1221    /// a native modal window of its own on macOS and Windows, and its answer
1222    /// runs there. A close guard that asks the user answers through its
1223    /// [`CloseDecision`](crate::window::CloseDecision) instead, which knows
1224    /// the window it is for; to close another window from an answer, name it
1225    /// with [`close_window_by_id`](Self::close_window_by_id).
1226    pub fn close_window_forced(&mut self) {
1227        self.force_close_requested = true;
1228    }
1229
1230    /// Whether a modal overlay is open in this window: a dialog presented in
1231    /// the window or the command palette. For a handler or a post-mount
1232    /// action that would move focus programmatically: moving it under the
1233    /// modal's scrim would leave the user in a page they cannot reach.
1234    // Shared plumbing for teksilo-widgets (`DockingModel::focus_dock`);
1235    // whether to document it is open.
1236    #[doc(hidden)]
1237    pub fn is_modal_open(&self) -> bool {
1238        self.modal_open
1239    }
1240
1241    /// Give the window's close button's request to the in-window modal open
1242    /// in it, if one is: the modal does what Escape does there, and the window
1243    /// is not asked to close, as with Alt+F4 or the OS close button. Returns
1244    /// `false`, doing nothing, when no in-window modal is open, and the caller
1245    /// closes the window as it would.
1246    ///
1247    /// For a title bar's close button, which stays live under a modal's scrim;
1248    /// [`close_window`](Self::close_window) asks the window's guard with the
1249    /// modal still up.
1250    #[doc(hidden)]
1251    pub fn give_close_request_to_modal(&mut self) -> bool {
1252        self.close_request_to_modal |= self.modal_open;
1253        self.modal_open
1254    }
1255
1256    // -------------------- Multi-window API --------------------
1257
1258    /// Cursor position at the moment this handler was invoked. `None`
1259    /// when no `PointerMove` has reached the tree yet, or when the
1260    /// context was constructed without a tree-side snapshot (e.g.
1261    /// hand-built `EventContext`s in tests). Used by the safe-triangle
1262    /// submenu hover gate.
1263    pub fn tree_pointer_position(&self) -> Option<teksilo_canvas::Point> {
1264        self.tree_pointer_position
1265    }
1266
1267    /// True when the in-flight pointer press's hit target is a strict
1268    /// descendant of THIS handler's widget that carries its own tap gesture
1269    /// (chevron, checkbox, inline button). A row/container that selects on
1270    /// press should early-return `EventResponse::Ignored` when this is set, so
1271    /// the press belongs to the inner control, not the row. Only meaningful
1272    /// inside `on_pointer_event` handlers for `PointerDown`/`PointerUp`.
1273    pub fn press_claimed_by_interactive_child(&self) -> bool {
1274        self.press_claimed_by_interactive_child
1275    }
1276
1277    /// The node this dispatch is **addressed to** — the innermost node the
1278    /// arena's hit walk accepted for this pointer sample, with
1279    /// [`hit_transparent`](crate::widget_builder::HandlerSet::hit_transparent),
1280    /// [`Widget::hit_shape`](crate::widget::Widget::hit_shape),
1281    /// [`Widget::accepts_child_hit`](crate::widget::Widget::accepts_child_hit)
1282    /// and `event_pass_through` all already applied.
1283    ///
1284    /// The point of it is the **preview** pass. `on_pointer_event` fires on
1285    /// every strict ancestor of the target during preview and on the target
1286    /// itself during the bubble, so an ancestor handler cannot otherwise tell
1287    /// "a descendant won the walk" from "I am the one that was hit". A widget
1288    /// that owns a second picking system over the same area needs exactly that
1289    /// distinction, to know whether to yield: comparing this against its own id
1290    /// reads the arena's verdict instead of re-deriving it from a rectangle,
1291    /// which is what keeps the two pickers from answering in different orders.
1292    ///
1293    /// `None` for a context made outside pointer dispatch (a gesture timer, a
1294    /// key-capture callback, an async completion). Treat `None` as "no verdict
1295    /// available", not as "not the target".
1296    pub fn dispatch_target(&self) -> Option<WidgetId> {
1297        self.dispatch_target
1298    }
1299
1300    /// How the focus an `on_focus(true, …)` handler is being told about was
1301    /// acquired: the origin [`FocusGained`](crate::event::WidgetEvent::FocusGained)
1302    /// carries, which the `bool` handler signature does not.
1303    ///
1304    /// `None` everywhere else, including focus *loss*. The case that needs it
1305    /// is [`FocusOrigin::Restored`](crate::focus::FocusOrigin::Restored): a
1306    /// widget whose focus arrived because a rebuild replaced its predecessor
1307    /// must not run its arrival behaviour (a text field selecting all of its
1308    /// text) as if the user had just tabbed in.
1309    pub fn focus_origin(&self) -> Option<crate::focus::FocusOrigin> {
1310        self.focus_origin
1311    }
1312
1313    /// Look up the bounds rect of an open overlay by its root content
1314    /// widget id. Returns `None` when no such overlay is currently
1315    /// active. The snapshot is taken once per dispatch; mid-handler
1316    /// `show_overlay` calls will not appear here. Used by the
1317    /// safe-triangle submenu hover gate.
1318    pub fn overlay_bounds_for_content(&self, content_id: WidgetId) -> Option<teksilo_canvas::Rect> {
1319        self.overlay_bounds_snapshot
1320            .iter()
1321            .find(|(cid, _, _)| *cid == content_id)
1322            .map(|(_, r, _)| *r)
1323    }
1324
1325    /// Whether a safe-triangle traversal toward the overlay rooted at
1326    /// `content_id` is still live — i.e. whether the user may still be on
1327    /// their way to that submenu.
1328    ///
1329    /// A widget whose hover would otherwise tear the overlay down (a
1330    /// sibling menu row switching the selection) asks this first and
1331    /// stands aside while it is `true`, leaving the dismissal to the
1332    /// overlay's own pointer-leave grace — which tests the cone on every
1333    /// sample and closes the overlay one `delay` after the pointer stops
1334    /// heading there.
1335    ///
1336    /// **This is deliberately the armed window, not a point-in-cone
1337    /// test.** A sibling row's hover fires exactly once, at the instant
1338    /// the pointer crosses onto it — a pixel or two from the apex, where
1339    /// the cone is a needle — so answering "is this one sample inside the
1340    /// cone" made a single quantized step final, and any departure
1341    /// steeper than the cone (which is most of them, for a wide menu with
1342    /// a short submenu) killed the submenu the moment the pointer left
1343    /// the trigger row. Whether *this* sample is inside the cone is the
1344    /// framework's question, asked continuously; the widget's question is
1345    /// only whether to get out of the way.
1346    ///
1347    /// `false` when no region is armed and when its budget is spent.
1348    ///
1349    /// Arm the region with
1350    /// [`arm_overlay_safe_region`](Self::arm_overlay_safe_region).
1351    pub fn overlay_safe_region_armed(&self, content_id: WidgetId) -> bool {
1352        self.overlay_bounds_snapshot
1353            .iter()
1354            .find(|(cid, _, _)| *cid == content_id)
1355            .is_some_and(|(_, _, apex)| apex.is_some())
1356    }
1357
1358    pub fn window(&self) -> Option<&crate::window::WindowState> {
1359        self.current_window.as_ref()
1360    }
1361
1362    /// Whether the host window is currently active (`focused AND not
1363    /// occluded`) — the occlusion-aware companion to
1364    /// `self.window().map(|w| w.focused())` (raw OS focus). Snapshotted at
1365    /// context construction. Matches [`BuildContext::window_active`].
1366    ///
1367    /// [`BuildContext::window_active`]: crate::build_context::BuildContext::window_active
1368    pub fn window_active(&self) -> bool {
1369        self.tree_window_active
1370    }
1371
1372    /// Whether the operating system says a screen reader is running, as it
1373    /// was when this event batch was dispatched (a snapshot). Only `Active`
1374    /// means a reader is present. See
1375    /// [`BuildContext::screen_reader_signal`] for the contract and the
1376    /// reactive form.
1377    ///
1378    /// [`BuildContext::screen_reader_signal`]: crate::build_context::BuildContext::screen_reader_signal
1379    pub fn screen_reader_state(&self) -> crate::environment::ScreenReaderState {
1380        self.tree_screen_reader
1381    }
1382
1383    /// Open a new window, creating the winit-level surface
1384    /// synchronously. The returned id is immediately valid for
1385    /// [`focus_window`](Self::focus_window),
1386    /// [`window_state`](Self::window_state), and
1387    /// [`find_window`](Self::find_window).
1388    ///
1389    /// Panics when called from a handler on a standalone `WidgetTree`
1390    /// (no app context) — tests should not invoke this method.
1391    pub fn open_window(
1392        &mut self,
1393        config: crate::window::WindowConfig,
1394    ) -> crate::window::TeksiloWindowId {
1395        self.warn_if_context_change_in_focus_dispatch("open_window");
1396        self.window_ops
1397            .as_deref_mut()
1398            .expect("open_window called outside of a dispatch")
1399            .open_window(config)
1400    }
1401
1402    /// Find a window by the string id assigned via
1403    /// [`WindowConfig::id`](crate::window::WindowConfig::id). Returns
1404    /// `None` if no open window carries that id.
1405    pub fn find_window(&self, string_id: &str) -> Option<crate::window::TeksiloWindowId> {
1406        self.window_ops.as_deref()?.find_window(string_id)
1407    }
1408
1409    /// Read the [`WindowState`](crate::window::WindowState) for a
1410    /// specific window.
1411    pub fn window_state(
1412        &self,
1413        id: crate::window::TeksiloWindowId,
1414    ) -> Option<crate::window::WindowState> {
1415        self.window_ops.as_deref()?.window_state(id)
1416    }
1417
1418    /// Snapshot of every live window's state.
1419    pub fn windows(&self) -> Vec<crate::window::WindowState> {
1420        self.window_ops
1421            .as_deref()
1422            .map(|o| o.windows())
1423            .unwrap_or_default()
1424    }
1425
1426    /// Ask the application to quit, the way a Quit menu item does: the
1427    /// guarded quit, as [`close_window`](Self::close_window) is the guarded
1428    /// close.
1429    ///
1430    /// The application's quit guard (`TeksiloAppBuilder::on_quit_requested`)
1431    /// is asked first, then every open window in turn, as if the user closed
1432    /// it: its close guard
1433    /// ([`WindowConfig::on_close_requested`](crate::window::WindowConfig::on_close_requested),
1434    /// [`can_close`](crate::window::WindowConfig::can_close)) runs, and a
1435    /// window that agrees closes. Modal windows are asked before the windows
1436    /// they block, then the rest from the most recently used. The
1437    /// application quits once no window is left, whatever its quit policy
1438    /// (`teksilo_app::QuitPolicy`).
1439    ///
1440    /// A guard that vetoes abandons the quit at once. A guard that defers
1441    /// ([`CloseResponse::Defer`](crate::window::CloseResponse::Defer)) holds
1442    /// the quit at its window, brought to the front, until its
1443    /// [`CloseDecision`](crate::window::CloseDecision) is answered: `proceed`
1444    /// closes the window and the quit goes on, `cancel` abandons the quit.
1445    /// A window whose own close is already waiting on such an answer is not
1446    /// asked again; the quit waits on that answer. [`cancel_quit`](Self::cancel_quit)
1447    /// abandons it from anywhere.
1448    ///
1449    /// Requesting a quit while one is under way changes nothing; use
1450    /// [`quit_forced`](Self::quit_forced) to stop asking.
1451    pub fn quit(&mut self) {
1452        match self.window_ops.as_deref_mut() {
1453            Some(ops) => ops.quit(),
1454            None => Self::warn_without_ops("quit"),
1455        }
1456    }
1457
1458    /// Quit the application without asking any close guard: every window
1459    /// closes, and the application quits. The forced quit, as
1460    /// [`close_window_forced`](Self::close_window_forced) is the forced
1461    /// close. Also turns a [`quit`](Self::quit) under way into this one,
1462    /// which is what a guard's "Quit anyway" answer calls.
1463    pub fn quit_forced(&mut self) {
1464        match self.window_ops.as_deref_mut() {
1465            Some(ops) => ops.quit_forced(),
1466            None => Self::warn_without_ops("quit_forced"),
1467        }
1468    }
1469
1470    /// Whether a quit is under way, requested and neither finished nor
1471    /// abandoned. A close guard reads it to word its question for the whole
1472    /// application ("Quit anyway?") rather than for its window.
1473    pub fn quit_in_progress(&self) -> bool {
1474        self.window_ops
1475            .as_deref()
1476            .is_some_and(|ops| ops.quit_in_progress())
1477    }
1478
1479    /// The source of the quit under way, `None` when no quit is under way
1480    /// (or in a context with no application behind it). A window the user
1481    /// closes by its own button while a quit is under way reads the quit's
1482    /// source too.
1483    ///
1484    /// A close guard, or the guard of
1485    /// [`on_quit_requested`](https://docs.rs/teksilo/latest/teksilo/struct.TeksiloAppBuilder.html#method.on_quit_requested),
1486    /// reads it to decide whether to ask. When the session is ending
1487    /// ([`SessionEnd`](crate::window::QuitSource::SessionEnd)) or the
1488    /// process was told to terminate
1489    /// ([`Signal`](crate::window::QuitSource::Signal)) nobody is there to
1490    /// answer a "Save changes?" dialog, so the guard can save directly and
1491    /// return [`Close`](crate::window::CloseResponse::Close), as AppKit and
1492    /// Qt applications do.
1493    ///
1494    /// ```ignore
1495    /// .on_close_requested(move |ctx| {
1496    ///     if !dirty.get() {
1497    ///         return CloseResponse::Close;
1498    ///     }
1499    ///     if matches!(ctx.quit_source(), Some(QuitSource::SessionEnd | QuitSource::Signal)) {
1500    ///         save();
1501    ///         return CloseResponse::Close;
1502    ///     }
1503    ///     // ... ask the user, and defer
1504    /// })
1505    /// ```
1506    pub fn quit_source(&self) -> Option<crate::window::QuitSource> {
1507        self.window_ops.as_deref().and_then(|ops| ops.quit_source())
1508    }
1509
1510    /// Abandon the quit under way, if it is a guarded one
1511    /// ([`quit`](Self::quit), the App menu's Quit, the macOS Dock's Quit or
1512    /// a logout). A quit no guard may refuse goes on: a forced one
1513    /// ([`quit_forced`](Self::quit_forced)) and one for a source that leaves
1514    /// no time to ask (a termination signal, a Windows session ending now),
1515    /// so a guard, or a dialog's Cancel arm run as its window closes, cannot
1516    /// stop it half done. A close guard's own dialog answers through its
1517    /// [`CloseDecision`](crate::window::CloseDecision) instead.
1518    pub fn cancel_quit(&mut self) {
1519        if let Some(ops) = self.window_ops.as_deref_mut() {
1520            ops.cancel_quit();
1521        }
1522    }
1523
1524    /// A quit asked of a context with no application behind it goes
1525    /// nowhere; say so in a debug build rather than lose it silently.
1526    fn warn_without_ops(what: &str) {
1527        #[cfg(debug_assertions)]
1528        eprintln!(
1529            "teksilo-core: EventContext::{what} called outside a dispatch with \
1530             window operations; ignored"
1531        );
1532        #[cfg(not(debug_assertions))]
1533        let _ = what;
1534    }
1535
1536    /// Raise a window to the front and give it keyboard focus, restoring it
1537    /// first if it is minimised.
1538    pub fn focus_window(&mut self, id: crate::window::TeksiloWindowId) {
1539        self.warn_if_context_change_in_focus_dispatch("focus_window");
1540        if let Some(ops) = self.window_ops.as_deref_mut() {
1541            ops.focus_window(id);
1542        }
1543    }
1544
1545    /// Request an xdg-activation token for `id` (see
1546    /// [`WindowOps::request_activation_token`](crate::window::WindowOps::request_activation_token)).
1547    /// `cb` fires once with the token string, or `None` where the platform can't
1548    /// provide one — used to hand a token to a child process ("open in new
1549    /// window") or an IPC peer that will raise itself on Wayland.
1550    pub fn request_activation_token(
1551        &mut self,
1552        id: crate::window::TeksiloWindowId,
1553        cb: Box<dyn FnOnce(Option<String>)>,
1554    ) {
1555        if let Some(ops) = self.window_ops.as_deref_mut() {
1556            ops.request_activation_token(id, cb);
1557        } else {
1558            cb(None);
1559        }
1560    }
1561
1562    /// Request an activation token for the **current** window (see
1563    /// [`WindowOps::request_activation_token_self`](crate::window::WindowOps::request_activation_token_self)).
1564    /// Use this from a widget handler to mint a token from *this* focused window
1565    /// to hand to another window or process — it works mid-dispatch, unlike the
1566    /// id-based variant.
1567    pub fn request_activation_token_self(&mut self, cb: Box<dyn FnOnce(Option<String>)>) {
1568        if let Some(ops) = self.window_ops.as_deref_mut() {
1569            ops.request_activation_token_self(cb);
1570        } else {
1571            cb(None);
1572        }
1573    }
1574
1575    /// Close a specific window by id, unconditionally: its close guard is
1576    /// not asked, as for [`close_window_forced`](Self::close_window_forced).
1577    pub fn close_window_by_id(&mut self, id: crate::window::TeksiloWindowId) {
1578        if let Some(ops) = self.window_ops.as_deref_mut() {
1579            ops.close_window_by_id(id);
1580        }
1581    }
1582
1583    /// Report the focused text widget's caret rectangle (window-logical
1584    /// pixels) so the platform can position the OS IME candidate window at
1585    /// the insertion point. Text-editing widgets call this whenever the
1586    /// caret moves. No-op outside a dispatch / on a standalone tree.
1587    pub fn set_ime_cursor_area(&mut self, area: teksilo_canvas::Rect) {
1588        if let Some(ops) = self.window_ops.as_deref_mut() {
1589            ops.set_ime_cursor_area(area);
1590        }
1591    }
1592
1593    /// Tell the platform's input method that the focused field's composition
1594    /// is over (a click elsewhere, a focus change), and get the text the input
1595    /// method committed for it, which the field keeps in place of what the
1596    /// composition showed; `None` keeps what it showed. Call it while the
1597    /// field still holds the composition, and keep the text right after: see
1598    /// [`WindowOps::finish_ime_composition`](crate::window::WindowOps::finish_ime_composition).
1599    /// `None` outside a dispatch / on a standalone tree.
1600    pub fn finish_ime_composition(&mut self) -> Option<String> {
1601        self.window_ops.as_deref_mut()?.finish_ime_composition()
1602    }
1603
1604    /// Resolve the platform parent handle of the window currently
1605    /// dispatching the event. Used by native-dialog integrations
1606    /// (`teksilo_platform::file_dialog`) to parent OS dialogs to the
1607    /// originating Teksilo window.
1608    ///
1609    /// Returns `None` when called from a standalone `WidgetTree` (no
1610    /// app-level `WindowOps` sink), or when the platform refuses to
1611    /// surface a handle (rare; mostly during teardown).
1612    pub fn parent_window_handle(&self) -> Option<crate::raw_handle::ParentHandle> {
1613        self.window_ops.as_deref()?.current_parent_handle()
1614    }
1615
1616    /// Request a cursor icon change.
1617    ///
1618    /// This is an **override**: it outranks the cursor the hovered node
1619    /// declared with [`WidgetBuilder::cursor`], and it outlives the dispatch
1620    /// that set it. The tree's cursor moves only when something writes to it,
1621    /// and the node-declared cursor is written on `PointerEnter` /
1622    /// `PointerLeave` alone — so a handler that sets a cursor while the
1623    /// pointer is inside a node owns the cursor until the pointer leaves that
1624    /// node, or until the handler gives it back with
1625    /// [`release_cursor`](Self::release_cursor).
1626    ///
1627    /// [`WidgetBuilder::cursor`]: crate::widget_builder::WidgetBuilder::cursor
1628    pub fn set_cursor(&mut self, cursor: CursorIcon) {
1629        self.cursor_request = Some(CursorRequest::Set(cursor));
1630    }
1631
1632    /// Withdraw this handler's cursor override, so the cursor the hovered
1633    /// node declared applies again.
1634    ///
1635    /// The cursor counterpart of returning [`EventResponse::Ignored`]: it says
1636    /// "I have nothing to say about the cursor", which is *not* the same as
1637    /// saying [`CursorIcon::Default`] and not the same as staying silent.
1638    ///
1639    /// Silence is only safe for a handler that has never spoken. A handler
1640    /// which re-decides the cursor on every move — a scene arbitrating a
1641    /// lightweight item's cursor against the card underneath it, a chart's
1642    /// overlay marks, a terminal's link layer — reaches points where it has no
1643    /// answer while the pointer is still inside the same node. No hover
1644    /// transition fires there, so nothing re-applies the node's declared
1645    /// cursor, and going quiet leaves the handler's *last* word standing. This
1646    /// is how it takes that word back.
1647    ///
1648    /// Restores exactly what the `PointerEnter` walk resolved for the current
1649    /// hover chain (or [`CursorIcon::Default`] if that chain declared none),
1650    /// so it can never disagree with the declared-cursor mechanism it defers
1651    /// to. Calling it when nothing was overridden is a no-op.
1652    ///
1653    /// [`EventResponse::Ignored`]: crate::event::EventResponse::Ignored
1654    pub fn release_cursor(&mut self) {
1655        self.cursor_request = Some(CursorRequest::Release);
1656    }
1657
1658    /// Set a widget subtree as dormant (preserves state, releases rendering).
1659    pub fn set_dormant(&mut self, id: WidgetId) {
1660        self.tree_mutations.push(TreeMutation::SetDormant(id));
1661    }
1662
1663    /// Activate a dormant widget subtree.
1664    pub fn activate(&mut self, id: WidgetId) {
1665        self.tree_mutations.push(TreeMutation::Activate(id));
1666    }
1667
1668    /// Destroy a widget subtree (removes from arena entirely, state is gone).
1669    pub fn destroy(&mut self, id: WidgetId) {
1670        self.tree_mutations.push(TreeMutation::Destroy(id));
1671    }
1672
1673    /// Imperatively mutate a mounted widget by id, downcasting to the
1674    /// concrete type `W`.
1675    ///
1676    /// The mutation is **deferred**: the closure runs after the handler
1677    /// returns, inside `apply_tree_mutations`, where the framework holds
1678    /// `&mut` arena access (a handler cannot re-borrow the arena to reach
1679    /// another node, so this is the only safe channel — the same model as
1680    /// [`destroy`](Self::destroy)). After the closure runs, the target is
1681    /// dirty-marked at `dirty` so the mutation takes visual effect.
1682    ///
1683    /// The target widget must override `Widget::as_any_mut` to return
1684    /// `Some(self)`. If the id is gone or is not a `W`, the closure is a
1685    /// no-op in release and a `debug_assert` failure in debug — it never
1686    /// silently mutates the wrong widget.
1687    ///
1688    /// Use it for per-view state a handler can't otherwise reach — e.g.
1689    /// `SceneView::ensure_visible(...)` (camera) after the view is mounted:
1690    /// ```ignore
1691    /// ctx.with_widget_mut::<SceneView>(view_id, BindingLevel::Relayout, |v| {
1692    ///     v.ensure_visible(card_rect, 40.0);
1693    /// });
1694    /// ```
1695    /// For scene *content*, prefer the shared `SceneModel` handle (`view.model()`)
1696    /// — its mutators are `&self`, so a handler holding a clone can drive the
1697    /// scene directly and every attached view reconciles, no `with_widget_mut`
1698    /// needed.
1699    pub fn with_widget_mut<W: 'static>(
1700        &mut self,
1701        id: WidgetId,
1702        dirty: crate::binding::BindingLevel,
1703        f: impl FnOnce(&mut W) + 'static,
1704    ) {
1705        self.tree_mutations.push(TreeMutation::WithWidgetMut {
1706            id,
1707            dirty,
1708            apply: Box::new(move |any| match any.downcast_mut::<W>() {
1709                Some(w) => f(w),
1710                None => debug_assert!(
1711                    false,
1712                    "with_widget_mut: widget {id:?} is not the requested type (or does not \
1713                     override Widget::as_any_mut)"
1714                ),
1715            }),
1716        });
1717    }
1718
1719    /// Re-run one widget's `build()` **during this handler's drain**, before
1720    /// overlays are shown and before focus requests are applied — rather than
1721    /// dirty-marking it for the next layout pass, which is what every other
1722    /// rebuild trigger does.
1723    ///
1724    /// It exists for one shape:
1725    /// [`DeferredSubtree`](crate::deferred_subtree::DeferredSubtree) content
1726    /// that a handler is *about to depend on*. Opening a popover activates its
1727    /// content, shows an overlay anchored to it, and moves focus into it — all
1728    /// three inside the same drain (see `collect_from_ctx`). A deferred panel
1729    /// marked for rebuild would not exist yet at any of those points: the
1730    /// overlay would be measured against an empty node and
1731    /// `first_focusable_descendant` would find nothing to focus, so the popover
1732    /// would open in the wrong place and swallow the keyboard. Materializing
1733    /// here closes that window, and makes deferred content behave exactly like
1734    /// the eagerly-built content it replaces.
1735    ///
1736    /// Cheap to call redundantly: a `DeferredSubtree` that is already
1737    /// materialized returns its existing child, so a second open costs one
1738    /// `build()` of the host and nothing below it.
1739    ///
1740    /// Not a general "rebuild this widget now" door — reach for
1741    /// [`with_widget_mut`](Self::with_widget_mut) or a `Rebuild` binding for
1742    /// ordinary reactive updates, which are correctly served by the next
1743    /// layout pass.
1744    /// `Space` on a data view's focused row: activate the row's published
1745    /// keyboard toggle — the checkbox `StandardListItem` embeds, most often —
1746    /// or run `fallback` when the row publishes none.
1747    ///
1748    /// A row's controls are out of the Tab order, so this is the only keyboard
1749    /// route to them; `fallback` is what `Space` means on a row without one,
1750    /// which for the data views is "toggle the selection".
1751    pub fn row_space_activate(
1752        &mut self,
1753        row: WidgetId,
1754        fallback: std::rc::Rc<dyn Fn(&mut EventContext)>,
1755    ) {
1756        self.tree_mutations
1757            .push(TreeMutation::RowSpaceActivate { row, fallback });
1758    }
1759
1760    pub fn materialize_now(&mut self, id: WidgetId) {
1761        self.tree_mutations.push(TreeMutation::MaterializeNow(id));
1762    }
1763
1764    /// Request that the AccessKit tree be re-walked after this handler
1765    /// returns. Use after a mutation that changes the accessibility tree
1766    /// **shape** in a way the framework doesn't already detect (relayout
1767    /// alone no longer re-walks AT; only events that change the AT tree
1768    /// — focus, overlays, locale/shortcut rebinds — set the dirty flag).
1769    /// The companion `BuildContext::request_accessibility_update` covers
1770    /// the build-time path.
1771    pub fn request_accessibility_update(&mut self) {
1772        self.request_a11y_update = true;
1773    }
1774
1775    /// Ask the platform to raise its on-screen keyboard.
1776    ///
1777    /// For the case the desktop convention has no answer for: a *finger*
1778    /// landing in a text field, where there is no physical keyboard and no
1779    /// focus change the accessibility layer would notice on its own.
1780    ///
1781    /// The request is honoured **only where it can do no harm**. Where the
1782    /// platform's keyboard follows the framework's IME-allowance reconcile
1783    /// ([`SoftKeyboardSupport::ViaAccessibility`](crate::window::SoftKeyboardSupport::ViaAccessibility)),
1784    /// the request resolves to nothing — always, not merely while a composition
1785    /// happens to be live. That reconcile *is* the request, and the only thing
1786    /// an explicit ask could add is a re-assertion of IME allowance, which is
1787    /// what cancels a composition mid-word. Nothing on this path calls
1788    /// `set_ime_allowed`, and that is what makes placing a caret with a finger
1789    /// mid-composition safe. Where the framework has no keyboard request to
1790    /// send at all the request is dropped; ask
1791    /// [`soft_keyboard_support`](Self::soft_keyboard_support) first if the
1792    /// widget needs to offer a fallback.
1793    pub fn request_soft_keyboard(&mut self) {
1794        self.soft_keyboard_request = Some(true);
1795    }
1796
1797    /// Ask the platform to dismiss its on-screen keyboard.
1798    ///
1799    /// Only a platform reporting
1800    /// [`SoftKeyboardSupport::Explicit`](crate::window::SoftKeyboardSupport::Explicit)
1801    /// can honour this; elsewhere there is no dismiss request to send, and a
1802    /// keyboard that rose on the IME enable goes away on the matching disable
1803    /// when focus leaves the text surface.
1804    pub fn dismiss_soft_keyboard(&mut self) {
1805        self.soft_keyboard_request = Some(false);
1806    }
1807
1808    /// What the host platform can do about an on-screen keyboard.
1809    ///
1810    /// [`SoftKeyboardSupport::None`](crate::window::SoftKeyboardSupport::None)
1811    /// on a standalone tree and on every platform the framework has no keyboard
1812    /// request to send on — which, on the desktop, is most of them.
1813    pub fn soft_keyboard_support(&self) -> crate::window::SoftKeyboardSupport {
1814        self.window_ops
1815            .as_deref()
1816            .map(|ops| ops.soft_keyboard_support())
1817            .unwrap_or_default()
1818    }
1819
1820    /// Speak `message` to the screen reader, politely.
1821    ///
1822    /// For anything the user needs told that is not the name of a widget: a
1823    /// completed action, a new count, the result of an undo, a row that moved.
1824    /// Sighted users read those off the screen; a screen-reader user is told
1825    /// only what the framework says out loud.
1826    ///
1827    /// ```ignore
1828    /// ctx.announce(tr!(event_added(title = title.clone())));
1829    /// ```
1830    ///
1831    /// Takes `impl Into<String>`, so `tr!(…)` works directly.
1832    /// `LocalizedString` is deliberately not the parameter type: an
1833    /// announcement is an event, not a label, and
1834    /// re-resolving it on a later language switch would re-speak it. See
1835    /// [`crate::announcer`].
1836    ///
1837    /// **Do not pair this with a toast on the same path.** `Toast` is already a
1838    /// correct live region, so doing both says everything twice.
1839    pub fn announce(&mut self, message: impl Into<String>) {
1840        self.announce_with(message, crate::announcer::Politeness::Polite);
1841    }
1842
1843    /// Speak `message` to the screen reader at the given urgency.
1844    ///
1845    /// [`Politeness::Assertive`](crate::announcer::Politeness::Assertive)
1846    /// interrupts whatever is being spoken. Reserve it for something the user
1847    /// must not miss and cannot recover by re-reading the screen — a failure, a
1848    /// refusal, a destructive result. Everything else is
1849    /// [`Polite`](crate::announcer::Politeness::Polite), which is what
1850    /// [`announce`](Self::announce) uses.
1851    pub fn announce_with(
1852        &mut self,
1853        message: impl Into<String>,
1854        politeness: crate::announcer::Politeness,
1855    ) {
1856        self.announcements.push((message.into(), politeness));
1857    }
1858
1859    /// Show an overlay (tooltip, menu, popover).
1860    pub fn show_overlay(&mut self, request: crate::overlay::OverlayRequest) {
1861        self.overlay_requests.push(request);
1862    }
1863
1864    /// Show an overlay in an explicit z-band.
1865    ///
1866    /// [`show_overlay`](Self::show_overlay) is this with
1867    /// [`Standard`](crate::overlay::OverlayBand::Standard). The other band is
1868    /// for the touch text affordances, which must render above the editor's
1869    /// `clips_children` ancestor, below every menu, and outside the
1870    /// outside-press dismissal that every caret-moving tap would otherwise
1871    /// trigger. Their lifetime is the controller's — see
1872    /// [`TouchSelection::dismiss`](crate::text_touch::TouchSelection::dismiss).
1873    ///
1874    /// Showing content that is already up is a no-op, so a host may call this
1875    /// on every raise without tracking whether it has.
1876    pub fn show_overlay_in_band(
1877        &mut self,
1878        request: crate::overlay::OverlayRequest,
1879        band: crate::overlay::OverlayBand,
1880    ) {
1881        self.overlay_band_requests.push((request, band));
1882    }
1883
1884    /// Re-place the currently-shown overlay whose content root is `content_id`.
1885    ///
1886    /// Content-keyed for the same reason
1887    /// [`dismiss_overlay_by_content`](Self::dismiss_overlay_by_content) is:
1888    /// [`show_overlay`](Self::show_overlay) returns nothing, so a handler
1889    /// cannot learn the [`OverlayId`](crate::overlay::OverlayId) it created. A
1890    /// no-op when no overlay is showing that content.
1891    pub fn update_overlay_placement_by_content(
1892        &mut self,
1893        content_id: crate::widget_id::WidgetId,
1894        placement: crate::overlay::OverlayPlacement,
1895    ) {
1896        self.overlay_placement_updates.push((content_id, placement));
1897    }
1898
1899    /// Keep the currently-shown overlay whose content root is `content_id`
1900    /// placed by `placement`, re-run on every layout pass for as long as that
1901    /// overlay is up.
1902    ///
1903    /// `placement` receives the anchor's bounds, in window coordinates, as the
1904    /// pass placing the overlay laid the anchor out, and the layout direction.
1905    /// It is for an overlay that hangs off a *part* of its anchor that no
1906    /// [`OverlayPlacement`](crate::overlay::OverlayPlacement) names — a
1907    /// slider's thumb — and so is shown with a rectangle the handler computed:
1908    /// [`update_overlay_placement_by_content`](Self::update_overlay_placement_by_content)
1909    /// moves such an overlay only when a handler runs, and a scroll, a resize
1910    /// or a reflow moves the anchor with no handler running at all.
1911    ///
1912    /// Applied after the shows of the same dispatch, like a placement update.
1913    /// A no-op when no overlay is showing that content; a later call replaces
1914    /// the function.
1915    pub fn track_overlay_placement_by_content(
1916        &mut self,
1917        content_id: crate::widget_id::WidgetId,
1918        placement: impl Fn(
1919            teksilo_canvas::Rect,
1920            crate::environment::LayoutDirection,
1921        ) -> crate::overlay::OverlayPlacement
1922        + 'static,
1923    ) {
1924        self.overlay_placement_sources
1925            .push((content_id, std::rc::Rc::new(placement)));
1926    }
1927
1928    /// Show an overlay whose reveal/dismiss is animated by a
1929    /// caller-owned progress signal.
1930    ///
1931    /// `progress` must be an animated `Signal<f32>` (created with
1932    /// [`Signal::new_animated`](crate::signal::Signal::new_animated) or
1933    /// [`BuildContext::animated_signal`](crate::build_context::BuildContext::animated_signal)).
1934    /// The framework shows the overlay, tweens `progress` 0 → 1 over
1935    /// `duration`, and on any dismiss path tweens it 1 → 0 while
1936    /// **deferring** the overlay's removal (and its content's dormancy)
1937    /// until the roll-back completes — the same window the fade path
1938    /// uses, but with no opacity applied. The caller binds `progress`
1939    /// to whatever paints the reveal (e.g. an
1940    /// [`Unroll`](https://docs.rs/teksilo) width), and is responsible for
1941    /// resetting it to `0.0` before the show if a prior reveal left it
1942    /// at `1.0`.
1943    ///
1944    /// Under `prefers-reduced-motion`, skip this and use
1945    /// [`show_overlay`](Self::show_overlay) with the progress pinned at
1946    /// `1.0` so there is no tween and dismissal is immediate.
1947    pub fn show_overlay_with_reveal(
1948        &mut self,
1949        request: crate::overlay::OverlayRequest,
1950        progress: crate::signal::Signal<f32>,
1951        duration: std::time::Duration,
1952    ) {
1953        self.reveal_overlay_requests
1954            .push((request, progress, duration));
1955    }
1956
1957    /// Show an overlay that dismisses automatically after `duration`.
1958    pub fn show_overlay_for(
1959        &mut self,
1960        request: crate::overlay::OverlayRequest,
1961        duration: std::time::Duration,
1962    ) {
1963        self.timed_overlay_requests.push((request, duration));
1964    }
1965
1966    /// Dismiss an overlay by ID.
1967    pub fn dismiss_overlay(&mut self, id: crate::overlay::OverlayId) {
1968        self.overlay_dismissals.push(id);
1969    }
1970
1971    /// Dismiss the currently-shown overlay whose content root is
1972    /// `content_id`, if one is active. No-op when no overlay is showing
1973    /// that content. Use this to dismiss an overlay you can only name by
1974    /// its content widget — the symmetric companion to
1975    /// [`cancel_delayed_overlay`](Self::cancel_delayed_overlay), which
1976    /// cancels a *pending* delayed show for the same content. Together
1977    /// they let a caller fully retract a reusable tooltip surface
1978    /// (shown or pending) without tracking the `OverlayId`.
1979    pub fn dismiss_overlay_by_content(&mut self, content_id: crate::widget_id::WidgetId) {
1980        self.overlay_content_dismissals.push(content_id);
1981    }
1982
1983    /// Queue a request to pause an overlay's `auto_dismiss_after`
1984    /// timer. Drained by the framework after this handler returns —
1985    /// equivalent to calling
1986    /// [`OverlayManager::pause_auto_dismiss`](crate::overlay::OverlayManager::pause_auto_dismiss)
1987    /// at the next safe point. Idempotent.
1988    ///
1989    /// Used by `ToastHost` for hover-pause: on pointer-enter the
1990    /// host queues `pause_overlay_auto_dismiss(id)` for every live
1991    /// toast; on pointer-leave it queues `resume_overlay_auto_dismiss`.
1992    pub fn pause_overlay_auto_dismiss(&mut self, id: crate::overlay::OverlayId) {
1993        self.overlay_pause_requests.push((id, true));
1994    }
1995
1996    /// Queue a request to resume an overlay's `auto_dismiss_after`
1997    /// timer paused via
1998    /// [`pause_overlay_auto_dismiss`](Self::pause_overlay_auto_dismiss).
1999    /// Idempotent on un-paused overlays.
2000    pub fn resume_overlay_auto_dismiss(&mut self, id: crate::overlay::OverlayId) {
2001        self.overlay_pause_requests.push((id, false));
2002    }
2003
2004    /// Dismiss all active overlays (e.g., after a menu item is activated).
2005    pub fn dismiss_all_overlays(&mut self) {
2006        self.dismiss_scope = Some(DismissScope::All);
2007    }
2008
2009    /// Dismiss the source widget's containing overlay and any ancestor
2010    /// overlays in the chain that are menu-like (anything that isn't a
2011    /// `Role::Tooltip`, `Role::Dialog`, or `Role::AlertDialog`),
2012    /// preserving an outer composite tooltip or modal hosting the
2013    /// popover. Use for menu / dropdown item activation that wants to
2014    /// close the menu cascade without disturbing the host surface.
2015    pub fn dismiss_self_overlay_chain(&mut self) {
2016        self.dismiss_scope = Some(DismissScope::SelfChain);
2017    }
2018
2019    /// Dismiss every overlay whose content is *not* a host surface
2020    /// (`Role::Tooltip`, `Role::Dialog`, `Role::AlertDialog`),
2021    /// preserving an outer composite tooltip or modal hosting the
2022    /// trigger. Use for popover triggers and pre-show cleanup that
2023    /// want to close stale popovers / menus without taking a hosting
2024    /// surface with them.
2025    pub fn dismiss_all_except_hosts(&mut self) {
2026        self.dismiss_scope = Some(DismissScope::AllExceptHosts);
2027    }
2028
2029    /// Dismiss the topmost overlay only (e.g., closing a submenu while
2030    /// keeping the parent menu open).
2031    pub fn dismiss_top_overlay(&mut self) {
2032        self.dismiss_scope = Some(DismissScope::Top);
2033    }
2034
2035    /// Dismiss descendant overlays of the source widget's containing overlay.
2036    /// Useful for closing sibling submenu branches while keeping the current
2037    /// parent menu open.
2038    pub fn dismiss_child_overlays(&mut self) {
2039        self.dismiss_descendant_overlays.push(None);
2040    }
2041
2042    /// Dismiss descendant overlays of the source widget's containing overlay,
2043    /// preserving the subtree rooted at `content_id` if it is already open.
2044    pub fn dismiss_child_overlays_except(&mut self, content_id: crate::widget_id::WidgetId) {
2045        self.dismiss_descendant_overlays.push(Some(content_id));
2046    }
2047
2048    /// Request an idle callback to be run during the next idle period.
2049    /// Use this for incremental work that takes 5-50ms — too short for a
2050    /// background thread, too long for a single frame.
2051    pub fn request_idle_callback(
2052        &mut self,
2053        callback: impl FnOnce(crate::idle::IdleDeadline) + 'static,
2054    ) {
2055        self.idle_callbacks.push(Box::new(callback));
2056    }
2057
2058    /// Request framework-owned modal presentation.
2059    ///
2060    /// The widget tree records the request together with the originating
2061    /// widget, and the application layer can later resolve `Auto` into a
2062    /// concrete presentation backend.
2063    pub fn present_modal(&mut self, request: crate::modal::ModalRequest) {
2064        self.modal_requests.push(request);
2065    }
2066
2067    /// Synchronously open a modal as a native window — the single
2068    /// unified path for native-window modals. Callers that don't
2069    /// care whether the modal lands in-tree or in a native window
2070    /// use [`present_modal`](Self::present_modal), which routes
2071    /// `ModalPresentation::Auto` through the framework's picker.
2072    ///
2073    /// Returns the new window's id, or `None` when called outside a
2074    /// dispatch context (standalone trees). The window's parent is
2075    /// the current window; focus target and title / size from the
2076    /// request are honored.
2077    ///
2078    /// Only `ModalContent::Deferred` is supported here — an
2079    /// `ExistingWidget` id wouldn't make sense in a fresh tree.
2080    pub fn open_modal(
2081        &mut self,
2082        request: crate::modal::ModalRequest,
2083    ) -> Option<crate::window::TeksiloWindowId> {
2084        let parent = self.current_window.as_ref()?.id();
2085        let crate::modal::ModalContent::Deferred(builder) = request.content else {
2086            return None;
2087        };
2088        let mut config = crate::window::WindowConfig::new().modal(crate::window::ModalConfig {
2089            parent,
2090            focus_target: request.focus_target,
2091        });
2092        if let Some(title) = request.title {
2093            config = config.title(title);
2094        }
2095        if let Some((w, h)) = request.size {
2096            config = config.size(w, h);
2097        }
2098        let config = config.root(move |tree, _state| builder(tree));
2099        Some(self.open_window(config))
2100    }
2101
2102    /// Dismiss the current framework-owned modal presentation.
2103    ///
2104    /// For a modal presented in the window, an intent this handler sends is
2105    /// dispatched once the modal has closed, so the modal's own actions no
2106    /// longer take it: [`send_intent`](Self::send_intent) walks on from the
2107    /// widget the modal was attached to, as it would from the open modal once
2108    /// nothing in it had taken it, and
2109    /// [`send_intent_from_focus`](Self::send_intent_from_focus) starts at the
2110    /// focus the modal gave back.
2111    pub fn dismiss_modal(&mut self) {
2112        self.dismiss_modal = true;
2113    }
2114
2115    /// Show an overlay after a delay. The widget tree checks pending delayed
2116    /// overlays during `layout()` and shows them once the delay elapses.
2117    /// Use this for submenu hover-open delays.
2118    ///
2119    /// The content widget should already be added to the tree (typically
2120    /// dormant). It will be activated automatically when the delay elapses.
2121    pub fn show_overlay_after(
2122        &mut self,
2123        request: crate::overlay::OverlayRequest,
2124        delay: std::time::Duration,
2125    ) {
2126        self.delayed_overlay_requests
2127            .push((request, delay, None, false));
2128    }
2129
2130    /// Show an overlay after a delay and move focus when it opens.
2131    ///
2132    /// `focus_target` is focused as
2133    /// [`WidgetTree::focus`](crate::widget_tree::WidgetTree::focus) focuses a
2134    /// node: one that cannot take focus hands it to its first Tab stop. A
2135    /// `focus_target` that is not a live, active node when the overlay opens
2136    /// moves nothing, which is what the null id (`WidgetId::default()`) asks
2137    /// for.
2138    pub fn show_overlay_after_with_focus(
2139        &mut self,
2140        request: crate::overlay::OverlayRequest,
2141        delay: std::time::Duration,
2142        focus_target: crate::widget_id::WidgetId,
2143    ) {
2144        self.delayed_overlay_requests
2145            .push((request, delay, Some(focus_target), false));
2146    }
2147
2148    /// Show an overlay after a delay, move focus when it opens, and
2149    /// dismiss the anchor's sibling overlays **at that moment** rather
2150    /// than when the request was made.
2151    ///
2152    /// This is the hover-switch between two submenu triggers in the same
2153    /// menu. Dismissing eagerly at hover-enter closes the submenu the
2154    /// user is still walking toward as soon as the pointer crosses a
2155    /// neighbouring trigger; deferring the dismissal to the moment the
2156    /// new submenu actually opens means a pointer merely passing through
2157    /// costs nothing, and one that settles gets the swap on the same
2158    /// frame — no window with two submenus on screen.
2159    ///
2160    /// `focus_target` follows the rule of
2161    /// [`show_overlay_after_with_focus`](Self::show_overlay_after_with_focus):
2162    /// a node that cannot take focus hands it to its first Tab stop, and one
2163    /// that is not a live, active node when the overlay opens (the null id,
2164    /// `WidgetId::default()`) moves nothing. A submenu opened by hover passes
2165    /// the null id, so the keyboard stays in the menu the pointer is in.
2166    pub fn show_overlay_after_replacing_siblings(
2167        &mut self,
2168        request: crate::overlay::OverlayRequest,
2169        delay: std::time::Duration,
2170        focus_target: crate::widget_id::WidgetId,
2171    ) {
2172        self.delayed_overlay_requests
2173            .push((request, delay, Some(focus_target), true));
2174    }
2175
2176    /// Request a repaint on a specific widget. Use this when an event handler
2177    /// on one widget changes state that affects a different widget's appearance
2178    /// (e.g., keyboard navigation highlighting items in an overlay).
2179    pub fn request_repaint(&mut self, id: crate::widget_id::WidgetId) {
2180        self.repaint_requests.push(id);
2181    }
2182
2183    /// Programmatically click a widget (synthetic PointerDown + PointerUp at
2184    /// its center). Use this for keyboard activation of a child widget, e.g.,
2185    /// Enter on a keyboard-focused menu item.
2186    pub fn synthetic_click(&mut self, id: crate::widget_id::WidgetId) {
2187        self.synthetic_clicks.push(id);
2188    }
2189
2190    /// Transfer focus to a specific widget. Use this when opening overlay
2191    /// content (menus, dialogs) that should receive keyboard events.
2192    ///
2193    /// The request is applied when the handler returns. If `id` is
2194    /// **dormant** at that moment — a `Switcher` page or a `visible_when`
2195    /// branch the same handler has just asked to show — focus moves when the
2196    /// next layout pass has woken it, and the request is dropped if that pass
2197    /// leaves it hidden. A later focus move, or
2198    /// [`clear_focus`](Self::clear_focus), cancels a request still waiting.
2199    pub fn request_focus(&mut self, id: crate::widget_id::WidgetId) {
2200        self.focus_requests.push(id);
2201    }
2202
2203    /// Drop keyboard focus once this handler returns, so that nothing is
2204    /// focused.
2205    ///
2206    /// A click on space that holds no focusable widget leaves focus where it
2207    /// was — the desktop convention (Qt, AppKit), and what keeps a caret alive
2208    /// while the user clicks a label or a toolbar's empty stretch. An app that
2209    /// wants the browser behaviour instead, where a click on empty space drops
2210    /// focus, puts this on its background:
2211    ///
2212    /// ```ignore
2213    /// background.on_tap(|_, ctx| ctx.clear_focus())
2214    /// ```
2215    ///
2216    /// It is a focus move to nothing, with everything a move carries:
2217    /// `FocusLost` on the node that had focus, `focus_within` signals turned
2218    /// `false`, the focus ring hidden, a non-modal overlay focus was in closed,
2219    /// the accessibility tree updated. It also cancels a focus request still
2220    /// waiting for a dormant target (see [`request_focus`](Self::request_focus)),
2221    /// and any `request_focus` / `request_focus_into` this handler queued
2222    /// before it; one queued after it still applies.
2223    ///
2224    /// The immediate form, for tests and code outside a handler, is
2225    /// [`WidgetTree::clear_focus`](crate::widget_tree::WidgetTree::clear_focus).
2226    pub fn clear_focus(&mut self) {
2227        self.focus_requests.clear();
2228        self.focus_into_requests.clear();
2229        self.clear_focus_requested = true;
2230    }
2231
2232    /// Move focus **into** the content of `id`: focus its first focusable
2233    /// descendant in tab order. Unlike [`request_focus`](Self::request_focus),
2234    /// this does **not** fall back to focusing `id` itself when the subtree has
2235    /// no focusable descendant — it is a no-op in that case, so an empty region
2236    /// never traps focus on a non-interactive container.
2237    ///
2238    /// Use this for "dive into this region" gestures, e.g. pressing Enter on a
2239    /// focused tab header to move focus into the tab's content panel. A panel
2240    /// with focusable content lands on its first control; a panel that opted
2241    /// into focusability itself (no inner controls) lands on the panel; a bare
2242    /// panel with neither leaves focus where it was.
2243    ///
2244    /// A dormant `id` is handled as in [`request_focus`](Self::request_focus):
2245    /// the first focusable descendant is chosen once the next layout pass has
2246    /// woken it.
2247    pub fn request_focus_into(&mut self, id: crate::widget_id::WidgetId) {
2248        self.focus_into_requests.push(id);
2249    }
2250
2251    /// Scroll the given rectangle into view inside every enclosing scroll
2252    /// container, walking outward from the widget whose handler is running.
2253    ///
2254    /// `rect` is in **absolute tree (window) coordinates** — the same space
2255    /// the arena stores widget bounds in. It is where the **last layout** put
2256    /// the target: built from arena bounds, or from a widget's own laid-out
2257    /// origin plus its own interior scroll. Not from an ancestor container's
2258    /// live offset, which a scroll since that layout may have moved. After the
2259    /// handler returns, the framework walks the current widget's ancestors and,
2260    /// for each `clips_children` scroll container whose viewport does not
2261    /// already fully contain `rect`, or that a scroll or a reveal has moved
2262    /// since the last layout, dispatches
2263    /// [`WidgetEvent::ScrollIntoView`](crate::event::WidgetEvent::ScrollIntoView)
2264    /// so the container adjusts its offset. Nested scroll areas each get a
2265    /// turn (outermost included), exactly like the focus-driven path.
2266    ///
2267    /// Unlike the automatic focus follow — which can only reveal a *focused
2268    /// widget's own bounds* — this lets a widget reveal an arbitrary interior
2269    /// rectangle it computed itself: a text caret, a virtualized list/table
2270    /// row (which is not a distinct focusable node), or a scrolled-off tab
2271    /// header. The widget remains responsible for scrolling its *own* interior
2272    /// viewport; `ensure_visible` handles the enclosing containers. It is a
2273    /// no-op when there is no scroll container above the widget, or when every
2274    /// container already shows the rect.
2275    ///
2276    /// See [`ensure_visible_with_margin`](Self::ensure_visible_with_margin) to
2277    /// keep breathing room around the target.
2278    pub fn ensure_visible(&mut self, rect: teksilo_canvas::Rect) {
2279        self.scroll_into_view_requests.push(ScrollRevealRequest {
2280            rect,
2281            margin: 0.0,
2282            align: crate::event::ScrollAlign::Minimal,
2283            motion: crate::event::ScrollMotion::Instant,
2284            from: None,
2285        });
2286    }
2287
2288    /// [`ensure_visible`](Self::ensure_visible), for a rect that belongs to
2289    /// **another** widget.
2290    ///
2291    /// The framework walks `owner`'s ancestors rather than the handling widget's.
2292    /// That distinction is the whole of it, and getting it wrong fails silently:
2293    /// a find banner's Next button sits *beside* the scrolling page, not inside
2294    /// it, so a reveal walked from the button climbs out through the banner and
2295    /// never meets the scroll container the match is in. The match is selected,
2296    /// the counter moves, and the viewport does not follow.
2297    ///
2298    /// The same reasoning [`ensure_widget_visible`](Self::ensure_widget_visible)
2299    /// already records for the id-based form; this is its rect-based twin, for a
2300    /// target that is an interior span rather than a mounted child.
2301    ///
2302    /// `rect` is in absolute tree (window) coordinates.
2303    pub fn ensure_visible_from(
2304        &mut self,
2305        owner: crate::widget_id::WidgetId,
2306        rect: teksilo_canvas::Rect,
2307    ) {
2308        self.scroll_into_view_requests.push(ScrollRevealRequest {
2309            rect,
2310            margin: 0.0,
2311            align: crate::event::ScrollAlign::Minimal,
2312            motion: crate::event::ScrollMotion::Instant,
2313            from: Some(owner),
2314        });
2315    }
2316
2317    /// Like [`ensure_visible`](Self::ensure_visible), but keeps `margin`
2318    /// logical pixels of breathing room around `rect` on every edge, so the
2319    /// target does not sit flush against the viewport boundary (the caret at
2320    /// the bottom line, the selected row at the fold). `rect` is in absolute
2321    /// tree (window) coordinates.
2322    pub fn ensure_visible_with_margin(&mut self, rect: teksilo_canvas::Rect, margin: f32) {
2323        self.scroll_into_view_requests.push(ScrollRevealRequest {
2324            rect,
2325            margin: margin.max(0.0),
2326            align: crate::event::ScrollAlign::Minimal,
2327            motion: crate::event::ScrollMotion::Instant,
2328            from: None,
2329        });
2330    }
2331
2332    /// **Pin** `rect` at `fraction` of the way down the innermost enclosing
2333    /// scroll container — `0.0` flush with the top, `0.5` centred, `1.0` flush
2334    /// with the bottom — instead of merely revealing it.
2335    ///
2336    /// The difference from [`ensure_visible`](Self::ensure_visible) is that this
2337    /// scrolls **even when the target is already visible**. That is what makes
2338    /// it usable for typewriter scrolling: a caret that only moved the view once
2339    /// it fell off the edge would not be pinned to anything.
2340    ///
2341    /// Only the **innermost** scroll container aligns; any further ancestors
2342    /// out fall back to a minimal reveal, since an outer container's job is to
2343    /// bring the inner viewport on screen, not to align a rectangle it does not
2344    /// own. A clipping wrapper that does not scroll (a `MaxSize` capping the
2345    /// column, say) sits between them without taking the pin.
2346    ///
2347    /// `fraction` is clamped to `0.0..=1.0`. The container additionally clamps
2348    /// to its own scroll range, so a target near the start or end of the content
2349    /// lands as close to `fraction` as the range permits — see the scroll
2350    /// container's `scroll_past_end` for buying range past the content's end so
2351    /// the last line can still reach the pin.
2352    ///
2353    /// `rect` is in absolute tree (window) coordinates.
2354    pub fn ensure_visible_aligned(
2355        &mut self,
2356        rect: teksilo_canvas::Rect,
2357        fraction: f32,
2358        motion: crate::event::ScrollMotion,
2359    ) {
2360        self.scroll_into_view_requests.push(ScrollRevealRequest {
2361            rect,
2362            margin: 0.0,
2363            align: crate::event::ScrollAlign::Fraction(fraction.clamp(0.0, 1.0)),
2364            motion,
2365            from: None,
2366        });
2367    }
2368
2369    /// [`ensure_visible_aligned`](Self::ensure_visible_aligned), for a rect that
2370    /// belongs to **another** widget — see [`ensure_visible_from`](Self::ensure_visible_from)
2371    /// for why the distinction exists and how it fails when it is missed.
2372    pub fn ensure_visible_aligned_from(
2373        &mut self,
2374        owner: crate::widget_id::WidgetId,
2375        rect: teksilo_canvas::Rect,
2376        fraction: f32,
2377        motion: crate::event::ScrollMotion,
2378    ) {
2379        self.scroll_into_view_requests.push(ScrollRevealRequest {
2380            rect,
2381            margin: 0.0,
2382            align: crate::event::ScrollAlign::Fraction(fraction.clamp(0.0, 1.0)),
2383            motion,
2384            from: Some(owner),
2385        });
2386    }
2387
2388    /// Scroll a specific mounted widget into view inside every enclosing
2389    /// scroll container — the id-based companion to
2390    /// [`ensure_visible`](Self::ensure_visible).
2391    ///
2392    /// The framework resolves `id` to its current absolute bounds after the
2393    /// handler returns and walks *that widget's* ancestors (never `id`
2394    /// itself), dispatching
2395    /// [`WidgetEvent::ScrollIntoView`](crate::event::WidgetEvent::ScrollIntoView)
2396    /// to each `clips_children` container that doesn't already show it.
2397    ///
2398    /// Use this when the target you want revealed is a real, non-virtualized
2399    /// child whose bounds the arena already knows — a selected radio tile, a
2400    /// tab header — so you don't have to compute a rect. For a target that has
2401    /// no distinct node (a text caret) or that may not be realized (a
2402    /// virtualized list/table row), use [`ensure_visible`](Self::ensure_visible)
2403    /// with an analytic rect instead. No-op if `id` is not currently mounted.
2404    pub fn ensure_widget_visible(&mut self, id: crate::widget_id::WidgetId) {
2405        self.scroll_widget_into_view_requests.push((id, 0.0));
2406    }
2407
2408    /// Like [`ensure_widget_visible`](Self::ensure_widget_visible), but keeps
2409    /// `margin` logical pixels of breathing room around the widget.
2410    pub fn ensure_widget_visible_with_margin(
2411        &mut self,
2412        id: crate::widget_id::WidgetId,
2413        margin: f32,
2414    ) {
2415        self.scroll_widget_into_view_requests
2416            .push((id, margin.max(0.0)));
2417    }
2418
2419    /// Surface the tooltip of a keyboard-highlighted item immediately (no
2420    /// dwell), dismissing the previously-highlighted item's tooltip. Used by
2421    /// `MenuList` on arrow-key navigation so a menu item's rich/composite
2422    /// tooltip is reachable by keyboard — real focus stays on the menu panel,
2423    /// so this is keyed on the item id rather than on focus. Pass the item's
2424    /// own widget id; a tooltip-less item simply dismisses the previous one.
2425    pub fn show_highlight_tooltip(&mut self, id: crate::widget_id::WidgetId) {
2426        self.highlight_tooltip_requests.push(id);
2427    }
2428
2429    /// Cancel a pending delayed overlay by its content widget ID.
2430    /// Call this when the hover ends before the delay elapses.
2431    pub fn cancel_delayed_overlay(&mut self, content_id: crate::widget_id::WidgetId) {
2432        self.cancel_delayed_overlays.push(content_id);
2433    }
2434
2435    /// Arm the "safe triangle" of the open overlay rooted at
2436    /// `content_id`, with its apex at the current pointer position.
2437    ///
2438    /// Call this from the anchor's hover-leave handler: the pointer is
2439    /// then exactly at the point the diagonal toward the overlay
2440    /// starts. While the pointer sits inside the triangle spanned by
2441    /// that apex and the overlay's near edge, the overlay's
2442    /// pointer-leave grace is held off; leaving the triangle starts the
2443    /// grace and re-entering it cancels the grace again, so a wobble
2444    /// mid-diagonal costs nothing. Throughout — cone or no cone, until
2445    /// the pointer arrives or the framework's budget runs out —
2446    /// [`overlay_safe_region_armed`](Self::overlay_safe_region_armed)
2447    /// reports `true` so sibling widgets stand aside and let that one
2448    /// re-evaluated grace own the dismissal.
2449    ///
2450    /// No-ops when the overlay is not open (a submenu whose hover-open
2451    /// delay was cancelled before it ever showed) or when no pointer
2452    /// position is known.
2453    pub fn arm_overlay_safe_region(&mut self, content_id: crate::widget_id::WidgetId) {
2454        self.safe_region_arm_requests.push(content_id);
2455    }
2456
2457    /// Choose, **from this press's `PointerDown` handler**, when this node's own
2458    /// drag may begin — overriding its declared
2459    /// [`DragActivation`](teksilo_tokens::DragActivation) for this press alone.
2460    ///
2461    /// `.drag_activation(..)` is a node property, decided at build time. That is
2462    /// the right grain when a node's `on_drag` means one thing. It is the wrong
2463    /// grain when one handler means several: a scene viewport's single `on_drag`
2464    /// is its marquee *and* its item grab *and* its magnet port drag, and which
2465    /// of the three a press is cannot be known until the press has been
2466    /// hit-tested. This is the per-press door — the press handler has already
2467    /// done that hit test, so it can say "this one landed on an item, arm it
2468    /// immediately" while leaving an empty-space press to defer to the pan.
2469    ///
2470    /// Stashed on the pointer's sequence, **not** written back onto the node, so
2471    /// it dies with the press that chose it. That matters here more than
2472    /// hygiene usually does: `on_pointer_event` previews root-first over every
2473    /// strict ancestor of the press target, so a node that answers from it also
2474    /// answers for presses an interactive descendant owns, and a node write
2475    /// would leave the declaration changed for the *next* press.
2476    ///
2477    /// Read by the enrolment walk, which runs immediately after the press
2478    /// dispatch. Called from anything but a press handler it is inert for the
2479    /// press in flight — there is no enrolment left to read it — and applies to
2480    /// nothing else.
2481    ///
2482    /// Last writer wins: answering twice on one press means the second answer.
2483    pub fn set_drag_activation(&mut self, activation: teksilo_tokens::DragActivation) {
2484        self.drag_activation_override = Some(activation);
2485    }
2486
2487    /// Capture **the pointer this handler is serving**: its subsequent
2488    /// `PointerMove` and `PointerUp` are routed to this widget regardless of
2489    /// hit test, until the capture is released.
2490    ///
2491    /// Capture is per pointer. Two fingers pressing two widgets hold two
2492    /// independent captures, and each is released only by its own Up or
2493    /// Cancel — so a second contact lifting can no longer steal the first
2494    /// one's stream. A mouse call site is unaffected: there is one mouse, and
2495    /// this captures it.
2496    /// **Also an arbitration act.** Taking the pointer from an undecided
2497    /// [`PointerSequence`](crate::gesture::PointerSequence) enrols this widget
2498    /// as a [`MemberRole::RawDrag`](crate::gesture::MemberRole::RawDrag)
2499    /// competitor, and for a precise pointer with no eligible pan competitor
2500    /// it decides the sequence outright — which is what makes the splitter
2501    /// handle, the dock resize handle and the table column grip (all of which
2502    /// answer `Ignored` from `on_pointer_event` and work from `PointerMove`
2503    /// with no recognizer at all) first-class competitors rather than widgets
2504    /// the arbitration cannot see.
2505    pub fn capture_pointer(&mut self) {
2506        self.pointer_capture = Some((None, true));
2507        self.explicit_capture = true;
2508    }
2509
2510    /// Capture a *named* pointer, for a handler driving a pointer other than
2511    /// the one whose sample it is serving.
2512    pub fn capture_pointer_id(&mut self, pointer: crate::pointer::PointerId) {
2513        self.pointer_capture = Some((Some(pointer), true));
2514        self.explicit_capture = true;
2515    }
2516
2517    /// Capture the pointer as **framework plumbing**, without staking an
2518    /// arbitration claim.
2519    ///
2520    /// The gesture arena takes the pointer for the Down..Up window so a
2521    /// recognizer keeps seeing moves that leave the widget's bounds, and the
2522    /// drag pipeline takes it for the life of a drag. Neither is a widget
2523    /// saying "this press is mine"; routing them through the public
2524    /// [`capture_pointer`](Self::capture_pointer) would enrol every
2525    /// arena-bearing node as a `RawDrag` member and decide every mouse
2526    /// sequence at press.
2527    pub(crate) fn capture_pointer_implicit(&mut self) {
2528        self.pointer_capture = Some((None, true));
2529    }
2530
2531    /// Claim the pointer sequence for the widget whose handler is running:
2532    /// arbitration ends, every other competitor is cancelled.
2533    ///
2534    /// The explicit form of what a recognizer does when it recognizes. Use it
2535    /// from an application recognizer that decides by its own rules.
2536    pub fn claim_gesture(&mut self) {
2537        self.gesture_acts.push(GestureAct::Claim);
2538    }
2539
2540    /// Withdraw the widget whose handler is running from the sequence. It can
2541    /// no longer win this press; its peers carry on.
2542    pub fn reject_gesture(&mut self) {
2543        self.gesture_acts.push(GestureAct::Reject);
2544    }
2545
2546    /// Defer this widget's own decision without withdrawing: no peer may win
2547    /// while a member is holding.
2548    ///
2549    /// **The framework never holds.** This exists for an application
2550    /// recognizer awaiting an answer it does not have yet (a hit test against
2551    /// an off-thread model, a network round trip). The hold auto-releases at
2552    /// [`GestureProfile::max_hold`](teksilo_tokens::GestureProfile::max_hold)
2553    /// — 250 ms — so a holder that never answers cannot strand the press.
2554    pub fn hold_gesture(&mut self) {
2555        self.gesture_acts.push(GestureAct::Hold);
2556    }
2557
2558    /// End this widget's hold, putting it back in the running.
2559    pub fn release_gesture(&mut self) {
2560        self.gesture_acts.push(GestureAct::Release);
2561    }
2562
2563    /// Revoke the whole interaction of the pointer this handler is serving,
2564    /// for `reason`.
2565    ///
2566    /// The widget's own way into the cancel funnel, for a widget that knows
2567    /// the interaction can no longer mean anything — the document under a text
2568    /// drag was reloaded, the row being reordered was deleted by a peer. Every
2569    /// competitor is cancelled, the capture is given back, and a
2570    /// [`PointerCancel`](crate::event::WidgetEvent::PointerCancel) is
2571    /// delivered, all **after** this handler returns: a cancel taken inline
2572    /// would unwind the very sample the handler is standing on.
2573    ///
2574    /// Distinct from [`reject_gesture`](Self::reject_gesture), which withdraws
2575    /// only *this* widget and lets its peers carry on with a pointer that is
2576    /// still perfectly alive.
2577    pub fn cancel_pointer_sequence(&mut self, reason: crate::pointer::CancelReason) {
2578        self.cancel_pointer_request = Some(reason);
2579    }
2580
2581    /// Release the capture of the pointer this handler is serving. Its events
2582    /// resume normal hit-test dispatch.
2583    pub fn release_pointer(&mut self) {
2584        self.pointer_capture = Some((None, false));
2585    }
2586
2587    /// Whether the widget whose handler is running already holds the capture
2588    /// of the pointer it is serving.
2589    ///
2590    /// `true` also immediately after a [`capture_pointer`](Self::capture_pointer)
2591    /// in the same handler, even though the tree does not apply the request
2592    /// until the handler returns — asking "do I own this pointer?" after
2593    /// claiming it must not answer no.
2594    pub fn owns_pointer(&self) -> bool {
2595        match self.pointer_capture {
2596            Some((None, capture)) => capture,
2597            _ => self.dispatch_node.is_some() && self.dispatch_node == self.pointer_captor,
2598        }
2599    }
2600
2601    /// Start a drag-and-drop operation from the given source widget.
2602    ///
2603    /// The `payload` carries the data being dragged. During the drag:
2604    /// - `PointerMove` events update the drag position and fire `on_drag_hover`
2605    ///   on widgets under the pointer that have drop handlers
2606    /// - `PointerUp` fires `on_drop` on the target widget (if any)
2607    /// - `Escape` cancels the drag
2608    pub fn start_drag(
2609        &mut self,
2610        source_widget: crate::widget_id::WidgetId,
2611        payload: crate::drag_payload::DragPayload,
2612    ) {
2613        self.drag_start_request = Some((source_widget, payload, None));
2614    }
2615
2616    /// Start a drag-and-drop with a preview widget that follows the pointer.
2617    pub fn start_drag_with_preview(
2618        &mut self,
2619        source_widget: crate::widget_id::WidgetId,
2620        payload: crate::drag_payload::DragPayload,
2621        preview: Box<dyn crate::widget::Widget>,
2622    ) {
2623        self.drag_start_request = Some((source_widget, payload, Some(preview)));
2624    }
2625
2626    /// Cancel the active drag-and-drop session (if any).
2627    pub fn cancel_drag(&mut self) {
2628        self.cancel_drag = true;
2629    }
2630
2631    /// Replace the tree-level theme. All widgets are marked dirty for
2632    /// relayout and repaint; nothing is rebuilt (see
2633    /// `WidgetTree::set_theme`), so focus, scroll offsets and other
2634    /// interaction state survive.
2635    ///
2636    /// An explicit theme also turns **off** OS-following: the app's theme
2637    /// mode is reset to manual, so a later OS light/dark change won't
2638    /// override the chosen theme.
2639    pub fn set_theme(&mut self, theme: crate::styles::Theme) {
2640        self.theme_request = Some(theme);
2641    }
2642
2643    /// Switch the application to follow the OS theme (native / system mode):
2644    /// the app adopts the OS's colours and tracks OS light/dark changes at
2645    /// runtime. On platforms without OS-colour support it falls back to
2646    /// following the built-in light/dark presets.
2647    ///
2648    /// This is the counterpart to [`set_theme`](Self::set_theme): calling
2649    /// `set_theme` pins a fixed theme (manual mode), while this resumes
2650    /// OS-following. Parameterless by design, so widgets need not reference
2651    /// the app-layer theme-mode enum.
2652    pub fn follow_system_theme(&mut self) {
2653        self.follow_system_request = true;
2654    }
2655
2656    /// Replace the tree-level locale identifier. Composite widgets are
2657    /// rebuilt so any tr! lookups picked up at build time are re-evaluated
2658    /// against the new locale.
2659    pub fn set_locale(&mut self, locale: impl Into<String>) {
2660        self.locale_request = Some(locale.into());
2661    }
2662
2663    /// Set the user-controlled global text-scale factor (`1.0` = 100 %).
2664    ///
2665    /// The change is applied app-wide (every window) after the handler returns,
2666    /// mirroring [`set_theme`](Self::set_theme) / [`set_locale`](Self::set_locale).
2667    /// All text grows uniformly without a rebuild. Persist the value through
2668    /// `ctx.settings()` (e.g. `teksilo_settings::TEXT_SCALE_KEY`) so it survives
2669    /// a restart — the `TextScaleControl` widget does both for you.
2670    pub fn set_text_scale(&mut self, factor: f32) {
2671        self.text_scale_request = Some(factor);
2672    }
2673}
2674
2675#[cfg(test)]
2676mod multi_window_tests {
2677    use super::*;
2678    use crate::window::state::WindowStateInit;
2679    use crate::window::{
2680        NoopWindowOps, TeksiloWindowId, WindowConfig, WindowOps, WindowPlacement, WindowState,
2681    };
2682    use std::cell::RefCell;
2683    use std::rc::Rc;
2684
2685    /// Recording implementation of `WindowOps` so tests can assert
2686    /// that `EventContext` routes each method through the trait.
2687    #[derive(Default)]
2688    struct RecordingOps {
2689        open_calls: RefCell<Vec<WindowConfig>>,
2690        focus_calls: RefCell<Vec<TeksiloWindowId>>,
2691        close_calls: RefCell<Vec<TeksiloWindowId>>,
2692        /// Each quit asked for, `true` for a forced one, in order.
2693        quit_calls: RefCell<Vec<bool>>,
2694        cancel_quit_calls: RefCell<usize>,
2695        source: RefCell<Option<crate::window::QuitSource>>,
2696        next_id: RefCell<u64>,
2697        // A fake registry so `find_window` / `window_state` / `windows`
2698        // can return values.
2699        states: RefCell<Vec<WindowState>>,
2700    }
2701
2702    impl RecordingOps {
2703        fn alloc_id(&self) -> TeksiloWindowId {
2704            let mut n = self.next_id.borrow_mut();
2705            *n += 1;
2706            TeksiloWindowId::new(*n)
2707        }
2708    }
2709
2710    impl WindowOps for RecordingOps {
2711        fn open_window(&mut self, config: WindowConfig) -> TeksiloWindowId {
2712            let id = self.alloc_id();
2713            let state = WindowState::new(WindowStateInit {
2714                id,
2715                string_id: config.string_id.clone(),
2716                placement: config.initial_placement,
2717                title: config.title.clone(),
2718                size: config.size,
2719                position: config.position.unwrap_or((0, 0)),
2720                focused: true,
2721                resizable: config.resizable,
2722                always_on_top: config.always_on_top,
2723            });
2724            self.states.borrow_mut().push(state);
2725            self.open_calls.borrow_mut().push(config);
2726            id
2727        }
2728
2729        fn find_window(&self, string_id: &str) -> Option<TeksiloWindowId> {
2730            self.states
2731                .borrow()
2732                .iter()
2733                .find(|s| s.string_id() == Some(string_id))
2734                .map(|s| s.id())
2735        }
2736
2737        fn window_state(&self, id: TeksiloWindowId) -> Option<WindowState> {
2738            self.states.borrow().iter().find(|s| s.id() == id).cloned()
2739        }
2740
2741        fn windows(&self) -> Vec<WindowState> {
2742            self.states.borrow().clone()
2743        }
2744
2745        fn focus_window(&mut self, id: TeksiloWindowId) {
2746            self.focus_calls.borrow_mut().push(id);
2747        }
2748
2749        fn close_window_by_id(&mut self, id: TeksiloWindowId) {
2750            self.close_calls.borrow_mut().push(id);
2751        }
2752
2753        fn quit(&mut self) {
2754            self.quit_calls.borrow_mut().push(false);
2755        }
2756
2757        fn quit_forced(&mut self) {
2758            self.quit_calls.borrow_mut().push(true);
2759        }
2760
2761        fn quit_in_progress(&self) -> bool {
2762            let quits = self.quit_calls.borrow().len();
2763            quits > *self.cancel_quit_calls.borrow()
2764        }
2765
2766        fn quit_source(&self) -> Option<crate::window::QuitSource> {
2767            *self.source.borrow()
2768        }
2769
2770        fn cancel_quit(&mut self) {
2771            *self.cancel_quit_calls.borrow_mut() += 1;
2772        }
2773    }
2774
2775    /// `quit` asks with the guards, `quit_forced` without, as `close_window`
2776    /// and `close_window_forced` do, and both reach the application through
2777    /// the ops sink, as do the query and the cancel.
2778    #[test]
2779    fn quit_requests_route_through_ops() {
2780        let mut ops = RecordingOps::default();
2781        {
2782            let mut ctx =
2783                EventContext::new().with_window_context(&mut ops, Some(make_state(1, None)));
2784            assert!(!ctx.quit_in_progress());
2785            ctx.quit();
2786            assert!(ctx.quit_in_progress());
2787            ctx.cancel_quit();
2788            assert!(!ctx.quit_in_progress());
2789            ctx.quit_forced();
2790        }
2791        assert_eq!(*ops.quit_calls.borrow(), [false, true]);
2792        assert_eq!(*ops.cancel_quit_calls.borrow(), 1);
2793    }
2794
2795    /// `quit_source` reads the source the sink reports, and is `None` when
2796    /// no quit is under way or no application stands behind the context.
2797    #[test]
2798    fn quit_source_reads_what_the_ops_sink_reports() {
2799        let mut ops = RecordingOps::default();
2800        *ops.source.borrow_mut() = Some(crate::window::QuitSource::SessionEnd);
2801        let ctx = EventContext::new().with_window_context(&mut ops, Some(make_state(1, None)));
2802        assert_eq!(
2803            ctx.quit_source(),
2804            Some(crate::window::QuitSource::SessionEnd)
2805        );
2806        let mut ops = RecordingOps::default();
2807        let ctx = EventContext::new().with_window_context(&mut ops, Some(make_state(1, None)));
2808        assert_eq!(ctx.quit_source(), None);
2809        assert_eq!(EventContext::new().quit_source(), None);
2810    }
2811
2812    /// With no application behind the context there is nothing to quit, and
2813    /// nothing to panic about either.
2814    #[test]
2815    fn quit_without_ops_is_a_harmless_no_op() {
2816        let mut ctx = EventContext::new();
2817        ctx.quit();
2818        ctx.quit_forced();
2819        ctx.cancel_quit();
2820        assert!(!ctx.quit_in_progress());
2821    }
2822
2823    fn make_state(id: u64, string_id: Option<&str>) -> WindowState {
2824        WindowState::new(WindowStateInit {
2825            id: TeksiloWindowId::new(id),
2826            string_id: string_id.map(String::from),
2827            placement: WindowPlacement::Floating,
2828            title: "Test".into(),
2829            size: (800, 600),
2830            position: (0, 0),
2831            focused: true,
2832            resizable: true,
2833            always_on_top: false,
2834        })
2835    }
2836
2837    #[test]
2838    fn window_returns_current_window_state() {
2839        let state = make_state(1, Some("main"));
2840        let mut noop = NoopWindowOps;
2841        let ctx = EventContext::new().with_window_context(&mut noop, Some(state.clone()));
2842        assert_eq!(ctx.window().unwrap().id(), TeksiloWindowId::new(1));
2843        assert_eq!(ctx.window().unwrap().string_id(), Some("main"));
2844    }
2845
2846    #[test]
2847    fn window_is_none_without_context() {
2848        let ctx = EventContext::new();
2849        assert!(ctx.window().is_none());
2850    }
2851
2852    #[test]
2853    fn open_window_routes_through_ops() {
2854        let mut ops = RecordingOps::default();
2855        let main_state = make_state(1, Some("main"));
2856        let returned_id = {
2857            let mut ctx = EventContext::new().with_window_context(&mut ops, Some(main_state));
2858            ctx.open_window(WindowConfig::new().id("help").title("Help"))
2859        };
2860        assert_eq!(ops.open_calls.borrow().len(), 1);
2861        assert_eq!(
2862            ops.open_calls.borrow()[0].string_id.as_deref(),
2863            Some("help")
2864        );
2865        // Recording ops allocates ids 2+; 1 was reserved for `main`
2866        // only in this test — Recording's counter starts from 0, so the
2867        // first alloc yields 1.
2868        assert_eq!(returned_id, TeksiloWindowId::new(1));
2869    }
2870
2871    #[test]
2872    fn find_window_routes_through_ops() {
2873        let mut ops = RecordingOps::default();
2874        ops.states.borrow_mut().push(make_state(7, Some("foo")));
2875        let main_state = make_state(1, Some("main"));
2876        let ctx = EventContext::new().with_window_context(&mut ops, Some(main_state));
2877        assert_eq!(ctx.find_window("foo"), Some(TeksiloWindowId::new(7)));
2878        assert!(ctx.find_window("missing").is_none());
2879    }
2880
2881    #[test]
2882    fn focus_window_records_via_ops() {
2883        let mut ops = RecordingOps::default();
2884        let main_state = make_state(1, None);
2885        {
2886            let mut ctx = EventContext::new().with_window_context(&mut ops, Some(main_state));
2887            ctx.focus_window(TeksiloWindowId::new(42));
2888        }
2889        assert_eq!(
2890            ops.focus_calls.borrow().as_slice(),
2891            &[TeksiloWindowId::new(42)]
2892        );
2893    }
2894
2895    #[test]
2896    fn close_window_by_id_records_via_ops() {
2897        let mut ops = RecordingOps::default();
2898        let main_state = make_state(1, None);
2899        {
2900            let mut ctx = EventContext::new().with_window_context(&mut ops, Some(main_state));
2901            ctx.close_window_by_id(TeksiloWindowId::new(9));
2902        }
2903        assert_eq!(
2904            ops.close_calls.borrow().as_slice(),
2905            &[TeksiloWindowId::new(9)]
2906        );
2907    }
2908
2909    #[test]
2910    fn close_window_sets_guarded_flag_only() {
2911        let mut ctx = EventContext::new();
2912        ctx.close_window();
2913        assert!(
2914            ctx.close_window_requested,
2915            "close_window must raise the guarded-close flag"
2916        );
2917        assert!(
2918            !ctx.force_close_requested,
2919            "close_window must NOT raise the forced-close flag"
2920        );
2921    }
2922
2923    #[test]
2924    fn close_window_forced_sets_force_flag_only() {
2925        let mut ctx = EventContext::new();
2926        ctx.close_window_forced();
2927        assert!(
2928            ctx.force_close_requested,
2929            "close_window_forced must raise the forced-close flag"
2930        );
2931        assert!(
2932            !ctx.close_window_requested,
2933            "close_window_forced must NOT raise the guarded-close flag"
2934        );
2935    }
2936
2937    /// End-to-end: a handler calling `close_window_forced` during
2938    /// dispatch must transfer the flag onto the `WidgetTree` (the
2939    /// `collect_from_ctx` teardown), where the app loop drains it via
2940    /// `take_force_close_request` — separately from the guarded
2941    /// `take_close_window_request` flag.
2942    #[test]
2943    fn forced_close_flag_propagates_to_tree_and_drains_independently() {
2944        use crate::test_widgets::FillWidget;
2945        use crate::widget_tree::WidgetTree;
2946
2947        // `run_with_event_context` only runs the `collect_from_ctx`
2948        // teardown (which transfers ctx flags onto the tree) when the
2949        // tree has a root to anchor on, so give it one.
2950        let mut tree = WidgetTree::new();
2951        tree.add(FillWidget::new());
2952        tree.run_with_event_context(&mut NoopWindowOps, |ctx| ctx.close_window_forced());
2953        assert!(
2954            tree.take_force_close_request(),
2955            "forced-close flag must reach the tree"
2956        );
2957        assert!(
2958            !tree.take_close_window_request(),
2959            "a forced close must not also raise the guarded flag"
2960        );
2961
2962        // And the guarded path stays on its own channel.
2963        let mut tree = WidgetTree::new();
2964        tree.add(FillWidget::new());
2965        tree.run_with_event_context(&mut NoopWindowOps, |ctx| ctx.close_window());
2966        assert!(tree.take_close_window_request());
2967        assert!(!tree.take_force_close_request());
2968    }
2969
2970    #[test]
2971    fn windows_enumerates_via_ops() {
2972        let mut ops = RecordingOps::default();
2973        ops.states.borrow_mut().push(make_state(1, Some("a")));
2974        ops.states.borrow_mut().push(make_state(2, Some("b")));
2975        let main_state = make_state(1, Some("a"));
2976        let ctx = EventContext::new().with_window_context(&mut ops, Some(main_state));
2977        let ids: Vec<_> = ctx.windows().iter().map(|s| s.id()).collect();
2978        assert_eq!(ids, vec![TeksiloWindowId::new(1), TeksiloWindowId::new(2)]);
2979    }
2980
2981    #[test]
2982    fn standalone_context_returns_empty_windows_and_none_lookups() {
2983        let ctx = EventContext::new();
2984        assert!(ctx.find_window("anything").is_none());
2985        assert!(ctx.window_state(TeksiloWindowId::new(1)).is_none());
2986        assert!(ctx.windows().is_empty());
2987    }
2988
2989    #[test]
2990    #[should_panic(expected = "open_window called outside of a dispatch")]
2991    fn open_window_on_standalone_context_panics() {
2992        let mut ctx = EventContext::new();
2993        let _ = ctx.open_window(WindowConfig::new());
2994    }
2995
2996    #[test]
2997    fn open_modal_builds_window_config_from_request() {
2998        use crate::modal::{ModalContent, ModalRequest};
2999        let mut ops = RecordingOps::default();
3000        let main_state = make_state(1, Some("main"));
3001        let built_widget = Rc::new(RefCell::new(false));
3002        let built_widget_flag = built_widget.clone();
3003        let request = ModalRequest {
3004            content: ModalContent::Deferred(Box::new(move |_tree| {
3005                *built_widget_flag.borrow_mut() = true;
3006                // Return a dummy WidgetId — not used in this test since
3007                // the RecordingOps doesn't actually build the tree.
3008                crate::widget_id::WidgetId::default()
3009            })),
3010            presentation: crate::modal::ModalPresentation::NativeWindow,
3011            close_behavior: crate::modal::ModalCloseBehavior::default(),
3012            title: Some("Confirm".to_string()),
3013            size: Some((420, 180)),
3014            placement: crate::modal::ModalPlacement::Centered,
3015            focus_target: None,
3016            on_dismiss: None,
3017        };
3018        {
3019            let mut ctx = EventContext::new().with_window_context(&mut ops, Some(main_state));
3020            let id = ctx.open_modal(request);
3021            assert!(id.is_some());
3022        }
3023        // open_modal is a thin wrapper over open_window — the config
3024        // it built must reach RecordingOps::open_window.
3025        let calls = ops.open_calls.borrow();
3026        assert_eq!(calls.len(), 1);
3027        let cfg = &calls[0];
3028        assert_eq!(cfg.title, "Confirm");
3029        assert_eq!(cfg.size, (420, 180));
3030        assert!(cfg.is_modal());
3031        assert_eq!(cfg.modal_parent(), Some(TeksiloWindowId::new(1)));
3032        // Cell is just to let us observe something reachable via cfg.root_builder;
3033        // the builder hasn't been called yet (RecordingOps records the config
3034        // but doesn't build the tree).
3035        let _ = built_widget;
3036    }
3037
3038    #[test]
3039    fn open_modal_requires_current_window() {
3040        use crate::modal::{ModalContent, ModalRequest};
3041        let mut ops = RecordingOps::default();
3042        let mut ctx = EventContext::new().with_window_context(&mut ops, None);
3043        let request = ModalRequest {
3044            content: ModalContent::Deferred(Box::new(|_tree| {
3045                crate::widget_id::WidgetId::default()
3046            })),
3047            presentation: crate::modal::ModalPresentation::NativeWindow,
3048            close_behavior: crate::modal::ModalCloseBehavior::default(),
3049            title: None,
3050            size: None,
3051            placement: crate::modal::ModalPlacement::Centered,
3052            focus_target: None,
3053            on_dismiss: None,
3054        };
3055        assert!(ctx.open_modal(request).is_none());
3056    }
3057}