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