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