Skip to main content

frust_core/event/
input.rs

1//! The [`InputEvent`] enum the tree receives, plus its overlay and file-drop payloads.
2
3use std::path::PathBuf;
4
5use kurbo::{Affine, Point, Vec2};
6
7use crate::overlay::OverlayKey;
8use crate::shortcuts::ShortcutId;
9
10use super::ime::ImeEvent;
11use super::key::{EditCommand, KeyEvent};
12use super::pointer::{PointerEvent, PointerId, ScaleEvent, ScalePhase, ScrollDelta};
13
14/// What an [`InputEvent::Overlay`] carries: the owner's key and the input, in window space.
15#[derive(Clone, Debug, PartialEq)]
16pub struct OverlayEvent {
17    pub key: OverlayKey,
18    pub kind: OverlayEventKind,
19}
20
21/// An [`OverlayEvent`]'s input, in window space (the owner subtracts its rect origin itself).
22/// [`InputEvent::translated`] leaves an `Overlay` payload unchanged, so it stays in window space.
23#[derive(Clone, Debug, PartialEq)]
24pub enum OverlayEventKind {
25    Pointer(PointerEvent),
26    Scroll {
27        position: Point,
28        delta: ScrollDelta,
29    },
30    Scale {
31        focal: Point,
32        phase: ScalePhase,
33        scale_delta: f64,
34        velocity: f64,
35    },
36    /// A primary press outside every surface (light dismiss), sent only to `Notify` entries.
37    OutsideDown,
38    /// A shortcut the pod declared; the owner forwards it as [`InputEvent::Shortcut`].
39    Shortcut(ShortcutId),
40    /// A key the focus path declined (see [`crate::overlay`]); answer via `consume_overlay_key`.
41    Key(KeyEvent),
42}
43
44/// The phase of a desktop OS file drag over the window. A shell publishes `Hover`, `Drop` and
45/// `Cancel`; [`Ended`](FileDropPhase::Ended) is the root's own broadcast follow-up.
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub enum FileDropPhase {
48    Hover,
49    Drop,
50    Cancel,
51    /// Broadcast-only, after every `Drop`/`Cancel`: the drag is over, so end any state it opened.
52    Ended,
53}
54
55/// A desktop OS file drag/drop event, hit-tested and bubbling like [`InputEvent::Scroll`].
56///
57/// # Source
58/// Only `frust-shell-desktop` publishes it, mapping winit's `HoveredFile`/`DroppedFile`/
59/// `HoveredFileCancelled`; Android, iOS and web publish nothing (see `docs/LIMITATIONS.md`).
60///
61/// # Broadcast follow-up
62/// After the hit-tested dispatch of every `Drop`/`Cancel`, handled or not, the root broadcasts
63/// [`FileDropPhase::Ended`] to every widget in the same call; answering it must be idempotent.
64#[derive(Clone, Debug, PartialEq)]
65pub struct FileDropEvent {
66    pub phase: FileDropPhase,
67    /// Where the drag is, in the receiving widget's local logical space.
68    pub position: Point,
69    /// The dropped files, populated only for `Drop`. Untrusted OS input: resolve symlinks, check
70    /// type and size before opening, and sanitise a name before showing it.
71    pub paths: Vec<PathBuf>,
72}
73
74/// An input event delivered to the widget tree. Pointer, scroll, scale and file-drop events are
75/// hit-tested; key, IME and edit-command events are focus-routed; the rest are broadcasts.
76#[derive(Clone, Debug, PartialEq)]
77pub enum InputEvent {
78    /// A pointer gesture event — the only pointer form a widget receives; from a shell, the mouse.
79    Pointer(PointerEvent),
80    /// One identified pointer contact from a shell. Widgets never receive it: the root dispatches
81    /// `event` as [`InputEvent::Pointer`] and reports `pointer_id` via `EventCtx::pointer_id`.
82    ///
83    /// # Multi-contact contract
84    /// * **(a)** With no live capture, slot `0` is hit-tested exactly like `Pointer`; a capture
85    ///   taken on its `Down` latches this id as the gesture's **claimant**.
86    /// * **(b)** With no live capture, slot `1` and above is dropped at the root.
87    /// * **(c)** While a capture is live, the claimant's events take the captured path; any other
88    ///   id reaches only the captor (enclosing containers forward it unhandled), and only if it
89    ///   called `EventCtx::capture_contacts` on its capturing `Down`; otherwise it is dropped.
90    ///   Only the claimant's `Up`/`Cancel` releases the capture, and a container takeover
91    ///   (`EventCtx::release_captured_child`) ends the opt-in.
92    PointerContact {
93        pointer_id: PointerId,
94        event: PointerEvent,
95    },
96    /// A scroll event at `position` carrying `delta`.
97    Scroll {
98        /// Where the scroll occurred, in the receiving widget's local logical space.
99        position: Point,
100        delta: ScrollDelta,
101    },
102    /// A pinch/zoom event, hit-tested by [`ScaleEvent::focal`] and bubbling until handled.
103    Scale(ScaleEvent),
104    /// A keyboard key event, routed down the focus path.
105    Key(KeyEvent),
106    /// An IME event, routed down the focus path.
107    Ime(ImeEvent),
108    /// A decoded clipboard/selection command, routed down the focus path; with nothing focused it
109    /// is dropped, so a shell may answer a stale paste request unconditionally.
110    EditCommand(EditCommand),
111    /// Not user input: a broadcast, never consumed, that lets a widget run a callback it queued
112    /// needing `&mut State` during a state-free build pass, on the frame that queued it.
113    Housekeeping,
114    /// A floated overlay surface's own input: a broadcast only the owner holding the matching
115    /// [`OverlayKey`] acts on; every other widget ignores it.
116    Overlay(OverlayEvent),
117    /// A desktop OS file drag; see [`FileDropEvent`]. Not routed through the overlay pre-pass.
118    FileDrop(FileDropEvent),
119    /// A broadcast naming the shortcut a key pressed (`crate::shortcuts`); the declarer answers.
120    Shortcut(ShortcutId),
121}
122
123impl InputEvent {
124    /// The event's location in the receiving widget's local space; [`Point::ZERO`] for
125    /// focus-routed events and broadcasts, which callers must never hit-test on.
126    pub fn position(&self) -> Point {
127        match self {
128            InputEvent::Pointer(p) | InputEvent::PointerContact { event: p, .. } => p.position,
129            InputEvent::Scroll { position, .. } => *position,
130            InputEvent::Scale(scale) => scale.focal,
131            InputEvent::FileDrop(drop) if drop.phase == FileDropPhase::Ended => Point::ZERO,
132            InputEvent::FileDrop(drop) => drop.position,
133            InputEvent::Key(_)
134            | InputEvent::Ime(_)
135            | InputEvent::EditCommand(_)
136            | InputEvent::Housekeeping
137            | InputEvent::Overlay(_)
138            | InputEvent::Shortcut(_) => Point::ZERO,
139        }
140    }
141
142    /// A copy with its position shifted by `offset` (a container passes `-child_origin`).
143    /// Positionless events and the window-space `Overlay` payload are returned unchanged.
144    pub fn translated(&self, offset: Vec2) -> InputEvent {
145        match self {
146            InputEvent::Pointer(p) => InputEvent::Pointer(PointerEvent {
147                position: p.position + offset,
148                ..*p
149            }),
150            InputEvent::PointerContact { pointer_id, event } => InputEvent::PointerContact {
151                pointer_id: *pointer_id,
152                event: PointerEvent {
153                    position: event.position + offset,
154                    ..*event
155                },
156            },
157            InputEvent::Scroll { position, delta } => InputEvent::Scroll {
158                position: *position + offset,
159                delta: *delta,
160            },
161            InputEvent::Scale(scale) => InputEvent::Scale(ScaleEvent {
162                focal: scale.focal + offset,
163                ..*scale
164            }),
165            InputEvent::FileDrop(drop) if drop.phase == FileDropPhase::Ended => self.clone(),
166            InputEvent::FileDrop(drop) => InputEvent::FileDrop(FileDropEvent {
167                position: drop.position + offset,
168                phase: drop.phase,
169                paths: drop.paths.clone(),
170            }),
171            InputEvent::Key(_)
172            | InputEvent::Ime(_)
173            | InputEvent::EditCommand(_)
174            | InputEvent::Housekeeping
175            | InputEvent::Overlay(_)
176            | InputEvent::Shortcut(_) => self.clone(),
177        }
178    }
179
180    /// A copy with exactly the positions `translated` shifts mapped through `affine` (the inverse
181    /// of the child's transform); deltas and velocities are carried over as-is.
182    pub fn transformed(&self, affine: &Affine) -> InputEvent {
183        match self {
184            InputEvent::Pointer(p) => InputEvent::Pointer(PointerEvent {
185                position: *affine * p.position,
186                ..*p
187            }),
188            InputEvent::PointerContact { pointer_id, event } => InputEvent::PointerContact {
189                pointer_id: *pointer_id,
190                event: PointerEvent {
191                    position: *affine * event.position,
192                    ..*event
193                },
194            },
195            InputEvent::Scroll { position, delta } => InputEvent::Scroll {
196                position: *affine * *position,
197                delta: *delta,
198            },
199            InputEvent::Scale(scale) => InputEvent::Scale(ScaleEvent {
200                focal: *affine * scale.focal,
201                ..*scale
202            }),
203            InputEvent::FileDrop(drop) if drop.phase == FileDropPhase::Ended => self.clone(),
204            InputEvent::FileDrop(drop) => InputEvent::FileDrop(FileDropEvent {
205                position: *affine * drop.position,
206                phase: drop.phase,
207                paths: drop.paths.clone(),
208            }),
209            InputEvent::Key(_)
210            | InputEvent::Ime(_)
211            | InputEvent::EditCommand(_)
212            | InputEvent::Housekeeping
213            | InputEvent::Overlay(_)
214            | InputEvent::Shortcut(_) => self.clone(),
215        }
216    }
217
218    /// Whether this event is focus-routed (key, IME, edit command) rather than hit-tested.
219    pub fn is_focus_routed(&self) -> bool {
220        matches!(
221            self,
222            InputEvent::Key(_) | InputEvent::Ime(_) | InputEvent::EditCommand(_)
223        )
224    }
225
226    /// Whether this event is a broadcast — `Housekeeping`, `Overlay`, `Shortcut`, or a file drop's
227    /// `Ended` — forwarded to every child before any capture/focus/hit-test branch.
228    pub fn is_broadcast(&self) -> bool {
229        matches!(
230            self,
231            InputEvent::Housekeeping
232                | InputEvent::Overlay(_)
233                | InputEvent::Shortcut(_)
234                | InputEvent::FileDrop(FileDropEvent {
235                    phase: FileDropPhase::Ended,
236                    ..
237                })
238        )
239    }
240}