frust_core/event/ctx.rs
1//! [`EventCtx`], the context a widget's event handler mutates, and the [`EventResult`] /
2//! [`EventOutcome`] a dispatch reports.
3
4use std::any::Any;
5
6use kurbo::{Point, Size};
7
8use super::contact::{
9 current_pointer_id, note_capture_released, note_contacts_requested, under_contact_captor,
10};
11use super::ime::ImeState;
12use super::key::EditCommand;
13use super::pass::{CLIPBOARD_WRITE, CURSOR_REQUEST, EDIT_COMMAND_QUEUE, PASTE_REQUEST};
14use super::pointer::{CursorIcon, PointerId};
15use crate::overlay::OverlayKey;
16
17/// What a widget did with an event: `Handled` stops the container offering it to further
18/// siblings and marks the frame dirty; `Ignored` lets routing continue.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum EventResult {
21 Ignored,
22 Handled,
23}
24
25/// The result of a whole [`crate::app::RenderRoot::event`] pass: whether any widget consumed the
26/// event, and whether the shell should schedule a redraw (the pass itself never repaints).
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
28pub struct EventOutcome {
29 pub handled: bool,
30 pub needs_redraw: bool,
31}
32
33/// A child dispatch's request flags and published IME surface, folded into its parent by
34/// [`EventCtx::absorb_child`].
35#[derive(Debug, Default)]
36pub(crate) struct ChildFold {
37 pub(crate) needs_redraw: bool,
38 pub(crate) captured: bool,
39 pub(crate) contacts_requested: bool,
40 pub(crate) capture_released: bool,
41 pub(crate) hover_claimed: bool,
42 pub(crate) focus_requested: bool,
43 pub(crate) focus_released: bool,
44 pub(crate) ime_state: Option<ImeState>,
45}
46
47/// Context threaded into [`crate::widget::Widget::event`]: mutate the type-erased app state
48/// ([`EventCtx::state_mut`]), request a redraw or layout, capture the pointer or focus, and read
49/// the widget's own geometry (event positions are already local).
50pub struct EventCtx<'a> {
51 state: &'a mut dyn Any,
52 needs_redraw: bool,
53 needs_layout: bool,
54 /// A pod below was marked layout-dirty by this dispatch — the path bit the pod above marks
55 /// itself from, also folded into the root's pending `ChangeFlags::LAYOUT`.
56 layout_path_dirty: bool,
57 capture_requested: bool,
58 contacts_requested: bool,
59 capture_released: bool,
60 pointer_id: PointerId,
61 focus_requested: bool,
62 focus_released: bool,
63 has_focus: bool,
64 hovered: bool,
65 hover_claimed: bool,
66 /// Whether a claim records anything: set only for an uncaptured `Move`, and narrowed per pod so
67 /// a captured pointer never hovers and only the first claim of a pass records.
68 hover_eligible: bool,
69 /// The last completed hover pass's epoch; a claim in this pass records `hover_claim_epoch`.
70 hover_epoch: u64,
71 hover_root: u64,
72 ime_state: Option<ImeState>,
73 origin: Point,
74 size: Size,
75}
76
77impl<'a> EventCtx<'a> {
78 /// Build a root event context over the erased `state` for a widget at `origin` with `size`.
79 pub fn new(state: &'a mut dyn Any, origin: Point, size: Size) -> Self {
80 Self {
81 state,
82 needs_redraw: false,
83 needs_layout: false,
84 layout_path_dirty: false,
85 capture_requested: false,
86 contacts_requested: false,
87 capture_released: false,
88 // The running root pass's contact, so a context built mid-pass (a component's inner
89 // one) reports the same id; the mouse outside any pass.
90 pointer_id: current_pointer_id(),
91 focus_requested: false,
92 focus_released: false,
93 has_focus: false,
94 hovered: false,
95 hover_claimed: false,
96 hover_eligible: false,
97 hover_epoch: 0,
98 hover_root: 0,
99 ime_state: None,
100 origin,
101 size,
102 }
103 }
104
105 /// Recover the application state as `&mut T`. Panics if `T` is not the type the root erased —
106 /// a wiring bug, not a data condition.
107 pub fn state_mut<T: Any>(&mut self) -> &mut T {
108 self.state
109 .downcast_mut::<T>()
110 .expect("event state is not the expected application-state type")
111 }
112
113 /// Request that the shell schedule a repaint after this event pass.
114 pub fn request_redraw(&mut self) {
115 self.needs_redraw = true;
116 }
117
118 /// Whether a redraw was requested during this (sub)dispatch.
119 pub fn needs_redraw(&self) -> bool {
120 self.needs_redraw
121 }
122
123 /// Request layout for state this widget changed itself: marks every pod up to the root
124 /// layout-dirty, reaches the root's pending `ChangeFlags::LAYOUT`, and implies a redraw.
125 pub fn request_layout(&mut self) {
126 self.needs_layout = true;
127 self.needs_redraw = true;
128 self.layout_path_dirty = true;
129 }
130
131 /// Whether a layout was requested during this (sub)dispatch.
132 pub fn needs_layout(&self) -> bool {
133 self.needs_layout
134 }
135
136 /// Whether this (sub)dispatch dirtied a pod below this context.
137 pub(crate) fn is_layout_path_dirty(&self) -> bool {
138 self.layout_path_dirty
139 }
140
141 /// Fold a child dispatch's layout request and dirty-path bit into this context; also how a
142 /// component boundary mirrors its inner context.
143 pub(crate) fn absorb_layout(&mut self, child_needs_layout: bool, child_path_dirty: bool) {
144 if child_needs_layout {
145 self.needs_layout = true;
146 self.needs_redraw = true;
147 }
148 self.layout_path_dirty |= child_path_dirty || child_needs_layout;
149 }
150
151 /// Capture the pointer: later moves/releases route back to this widget along the recorded
152 /// active path until `Up`/`Cancel`. Only a hit-tested `Down` (or an overlay surface's input)
153 /// sets the root's capture mirror, so a capture from a `Move` routes while the root still reads
154 /// uncaptured; a drag wanting hover chrome throughout keeps its own pressed flag.
155 pub fn capture_pointer(&mut self) {
156 self.capture_requested = true;
157 }
158
159 /// Whether the widget requested pointer capture during this (sub)dispatch.
160 pub fn is_pointer_captured(&self) -> bool {
161 self.capture_requested
162 }
163
164 /// Which pointer contact this event comes from: [`PointerId::MOUSE`] unless the root unwrapped
165 /// an [`InputEvent::PointerContact`](crate::event::InputEvent::PointerContact).
166 pub fn pointer_id(&self) -> PointerId {
167 self.pointer_id
168 }
169
170 /// Opt into the gesture's other contacts: call on the `Down` that calls
171 /// [`EventCtx::capture_pointer`]; while that capture lives, each other contact arrives here as
172 /// a plain pointer event with its own [`EventCtx::pointer_id`]. Another contact's `Up`/`Cancel`
173 /// ends nothing; when the claimant's does, drop every other contact being tracked.
174 pub fn capture_contacts(&mut self) {
175 self.contacts_requested = true;
176 note_contacts_requested();
177 }
178
179 /// Whether a widget called [`EventCtx::capture_contacts`] during this (sub)dispatch; bubbles
180 /// like [`EventCtx::is_pointer_captured`].
181 pub fn is_contact_capture_requested(&self) -> bool {
182 self.contacts_requested
183 }
184
185 /// Take the gesture over: clear `child`'s active path (after sending it `Cancel`). If its
186 /// subtree held the contact opt-in, the root stops routing other contacts; the capture stays
187 /// with the claimant. A no-op while `child` is inactive, during a non-claimant delivery, or
188 /// when the opt-in holder is this container or an ancestor.
189 pub fn release_captured_child(&mut self, child: &mut crate::widget::ChildPod) {
190 if !child.is_active() {
191 return;
192 }
193 let held_opt_in = child.holds_contact_opt_in();
194 child.set_active(false);
195 if child.is_active() || !held_opt_in || under_contact_captor() {
196 return;
197 }
198 self.capture_released = true;
199 note_capture_released();
200 }
201
202 /// Whether [`EventCtx::release_captured_child`] voided the opt-in during this (sub)dispatch.
203 pub fn is_capture_released(&self) -> bool {
204 self.capture_released
205 }
206
207 /// Request focus: the enclosing pods record the focus path, and keyboard, IME and edit-command
208 /// events are then delivered down it with no hit test.
209 pub fn request_focus(&mut self) {
210 self.focus_requested = true;
211 }
212
213 /// Release focus: drop the recorded focus path (e.g. Escape / blur).
214 pub fn release_focus(&mut self) {
215 self.focus_released = true;
216 }
217
218 /// Whether the receiving widget holds the focus path (always, for a focus-routed event).
219 pub fn has_focus(&self) -> bool {
220 self.has_focus
221 }
222
223 /// From an editable's owner: forward the keys the focused editable declines to the floated
224 /// pod `key`, as `OverlayEventKind::Key`, before the shortcut table sees them. Recorded on
225 /// the focus session live at the end of this pass; a new claim or a release ends it.
226 pub fn forward_declined_keys_to(&mut self, key: OverlayKey) {
227 crate::shortcuts::request_key_forward(key);
228 }
229
230 /// Consume an `OverlayEventKind::Key` delivery, which stops its walk to the shortcut table:
231 /// the delivery is a broadcast, whose `Handled` no container carries back.
232 pub fn consume_overlay_key(&mut self) {
233 crate::shortcuts::note_handled();
234 }
235
236 /// Claim the hover link for this widget and, via the recorded path, its ancestors; the next
237 /// paint reports [`PaintCtx::is_hovered`](crate::widget::PaintCtx::is_hovered) for them.
238 /// Ignored outside an uncaptured `Move` or after the pass's first claim; requests no redraw.
239 ///
240 /// # The consumer contract
241 /// 1. Claim from the `Move` arm on every move the local hit test passes, not just on entry.
242 /// 2. A container claims *after* routing to its children: the first recorded claim wins.
243 /// 3. Keep an internal hovered flag from that hit test; `request_redraw` only when it changes.
244 /// 4. In `paint`, self-correct that flag from `PaintCtx::is_hovered`, which is authoritative.
245 pub fn claim_hover(&mut self) {
246 if self.hover_eligible {
247 self.hover_claimed = true;
248 }
249 }
250
251 /// Whether this widget or a descendant holds the hover link, as of before this dispatch;
252 /// `PaintCtx::is_hovered` is the authoritative read for hover chrome.
253 pub fn is_hovered(&self) -> bool {
254 self.hovered
255 }
256
257 /// Ask the host to show `icon`; per pass, last writer wins, and a widget that stops asking
258 /// falls back to [`CursorIcon::Default`]. Only a pointer `Move` (captured included) resolves
259 /// it, so a drag re-asks on each captured `Move`, and a container overriding its child asks
260 /// after routing (the reverse of `claim_hover`'s order). Requests no redraw.
261 pub fn set_cursor(&mut self, icon: CursorIcon) {
262 CURSOR_REQUEST.with(|slot| slot.set(Some(icon)));
263 }
264
265 /// Ask the shell to put `text` on the host clipboard — a widget's answer to `EditCommand::Copy`
266 /// or `Cut`. Per pass, last writer wins; drained by
267 /// [`RenderRoot::take_clipboard_write`](crate::app::RenderRoot::take_clipboard_write).
268 pub fn write_clipboard(&mut self, text: String) {
269 CLIPBOARD_WRITE.with(|slot| slot.set(Some(text)));
270 }
271
272 /// Ask the shell to read the host clipboard and dispatch it back as `EditCommand::Paste` on a
273 /// later pass; idempotent per pass. The answer lands wherever focus is at delivery, so an async
274 /// read binds it to [`RenderRoot::focus_epoch`](crate::app::RenderRoot::focus_epoch) and drops
275 /// a mismatch (never `focus_ime_generation`, which edits also move).
276 pub fn request_paste(&mut self) {
277 PASTE_REQUEST.with(|slot| slot.set(true));
278 }
279
280 /// Queue `cmd` for whichever widget drains [`EventCtx::take_edit_commands`] later in this pass
281 /// — the route from a floated toolbar back to its field. A command nobody takes is dropped.
282 pub fn dispatch_edit_command(&mut self, cmd: EditCommand) {
283 EDIT_COMMAND_QUEUE.with(|slot| {
284 let mut queue = slot.take();
285 queue.push(cmd);
286 slot.set(queue);
287 });
288 }
289
290 /// Drain this pass's queued edit commands in dispatch order; an overlay owner calls it right
291 /// after forwarding into its floated pod.
292 pub fn take_edit_commands(&mut self) -> Vec<EditCommand> {
293 EDIT_COMMAND_QUEUE.with(|slot| slot.take())
294 }
295
296 /// Publish this widget's IME surface after an editing change; it bubbles up the focus chain to
297 /// [`crate::app::RenderRoot::ime_state`], carried opaquely.
298 pub fn publish_ime_state(&mut self, state: ImeState) {
299 self.ime_state = Some(state);
300 }
301
302 /// Whether this widget requested focus during this (sub)dispatch (container-side).
303 pub(crate) fn is_focus_requested(&self) -> bool {
304 self.focus_requested
305 }
306
307 /// Whether this widget released focus during this (sub)dispatch (container-side).
308 pub(crate) fn is_focus_released(&self) -> bool {
309 self.focus_released
310 }
311
312 /// Take the IME surface published during this (sub)dispatch, leaving `None`.
313 pub(crate) fn take_ime_state(&mut self) -> Option<ImeState> {
314 self.ime_state.take()
315 }
316
317 /// Seed whether the receiving (root) widget holds focus.
318 pub(crate) fn set_has_focus(&mut self, has_focus: bool) {
319 self.has_focus = has_focus;
320 }
321
322 /// Whether a widget claimed hover during this (sub)dispatch (container-side).
323 pub(crate) fn is_hover_claimed(&self) -> bool {
324 self.hover_claimed
325 }
326
327 /// Whether a claim would record; `ChildPod::event_child` narrows it before handing it down.
328 pub(crate) fn is_hover_eligible(&self) -> bool {
329 self.hover_eligible
330 }
331
332 /// Seed whether the receiving (root) widget holds the hover link.
333 pub(crate) fn set_hovered(&mut self, hovered: bool) {
334 self.hovered = hovered;
335 }
336
337 /// Seed whether this pass may record a claim (only an uncaptured `Move`).
338 pub(crate) fn set_hover_eligible(&mut self, eligible: bool) {
339 self.hover_eligible = eligible;
340 }
341
342 /// Seed the live hover epoch, threaded unchanged into every child.
343 pub(crate) fn set_hover_epoch(&mut self, epoch: u64) {
344 self.hover_epoch = epoch;
345 }
346
347 /// The live hover epoch: a pod whose stamp equals it still holds the link.
348 pub(crate) fn hover_epoch(&self) -> u64 {
349 self.hover_epoch
350 }
351
352 /// Seed the running root's identity, threaded unchanged into every child.
353 pub(crate) fn set_hover_root(&mut self, root: u64) {
354 self.hover_root = root;
355 }
356
357 /// The running root's identity, stamped beside the claim epoch so a dropped pod can qualify it.
358 pub(crate) fn hover_root(&self) -> u64 {
359 self.hover_root
360 }
361
362 /// The epoch a claim in this pass records: one past the live one, as the root advances at pass
363 /// end. Wrapping is harmless; stamps are only compared for equality.
364 pub(crate) fn hover_claim_epoch(&self) -> u64 {
365 self.hover_epoch.wrapping_add(1)
366 }
367
368 /// The widget's origin in its **parent's** space, not window space (unlike `PaintCtx::origin`).
369 /// Event positions are already local; compute window-anchored rects from `PaintCtx` in `paint`.
370 pub fn origin(&self) -> Point {
371 self.origin
372 }
373
374 /// The receiving widget's resolved size.
375 pub fn size(&self) -> Size {
376 self.size
377 }
378
379 /// A sub-context for a child at `origin`/`size` over the same state: request flags start
380 /// clear; hover epoch, root identity and pointer id thread down. Folded back by `absorb_child`.
381 pub(crate) fn child_ctx(
382 &mut self,
383 origin: Point,
384 size: Size,
385 focused: bool,
386 hovered: bool,
387 hover_eligible: bool,
388 ) -> EventCtx<'_> {
389 EventCtx {
390 state: &mut *self.state,
391 needs_redraw: false,
392 needs_layout: false,
393 layout_path_dirty: false,
394 capture_requested: false,
395 contacts_requested: false,
396 capture_released: false,
397 pointer_id: self.pointer_id,
398 focus_requested: false,
399 focus_released: false,
400 has_focus: focused,
401 hovered,
402 hover_claimed: false,
403 hover_eligible,
404 hover_epoch: self.hover_epoch,
405 hover_root: self.hover_root,
406 ime_state: None,
407 origin,
408 size,
409 }
410 }
411
412 /// Drain this (child) context's request flags and IME surface into a [`ChildFold`].
413 pub(crate) fn fold(&mut self) -> ChildFold {
414 ChildFold {
415 needs_redraw: self.needs_redraw,
416 captured: self.capture_requested,
417 contacts_requested: self.contacts_requested,
418 capture_released: self.capture_released,
419 hover_claimed: self.hover_claimed,
420 focus_requested: self.focus_requested,
421 focus_released: self.focus_released,
422 ime_state: self.ime_state.take(),
423 }
424 }
425
426 /// Fold a child dispatch's request flags and any published IME surface back into this context.
427 pub(crate) fn absorb_child(&mut self, child: ChildFold) {
428 self.needs_redraw |= child.needs_redraw;
429 self.capture_requested |= child.captured;
430 self.contacts_requested |= child.contacts_requested;
431 self.capture_released |= child.capture_released;
432 // A claim bubbles like a focus request, so the whole path carries the same stamp.
433 self.hover_claimed |= child.hover_claimed;
434 self.focus_requested |= child.focus_requested;
435 self.focus_released |= child.focus_released;
436 if child.ime_state.is_some() {
437 self.ime_state = child.ime_state;
438 }
439 }
440}