Skip to main content

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}