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