Skip to main content

fret_core/input/
mod.rs

1use crate::{
2    ClipboardToken, ExternalDropToken, FileDialogDataEvent, FileDialogSelection, ImageId,
3    ImageUpdateToken, ImageUploadToken, IncomingOpenDataEvent, IncomingOpenItem, IncomingOpenToken,
4    PointerId, Rect, ShareSheetOutcome, ShareSheetToken, TimerToken, WindowLogicalPosition,
5    geometry::{Point, Px},
6};
7
8mod keyboard;
9pub use keyboard::{KeyCode, keycode_to_ascii_lowercase};
10
11mod viewport;
12pub use viewport::{ViewportInputEvent, ViewportInputGeometry, ViewportInputKind};
13
14#[cfg(test)]
15mod viewport_input_event_tests;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum MouseButton {
19    Left,
20    Right,
21    Middle,
22    Back,
23    Forward,
24    Other(u16),
25}
26
27/// Pointer device classification for unified pointer events.
28///
29/// This is intentionally small and Radix-aligned: it is used by component-layer policies to
30/// distinguish mouse-specific behaviors (e.g. open-on-pointer-down) from touch/pen behaviors
31/// (e.g. open-on-click to avoid scroll-to-open).
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub enum PointerType {
34    #[default]
35    Mouse,
36    Touch,
37    Pen,
38    Unknown,
39}
40
41#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
42pub struct Modifiers {
43    pub shift: bool,
44    pub ctrl: bool,
45    pub alt: bool,
46    /// Alternate Graphics (AltGr / AltGraph) modifier.
47    ///
48    /// This is semantically distinct from `ctrl+alt` for editor-grade shortcut matching.
49    pub alt_gr: bool,
50    pub meta: bool,
51}
52
53#[derive(Debug, Clone, PartialEq)]
54pub enum ImeEvent {
55    Enabled,
56    Disabled,
57    Commit(String),
58    /// `cursor` is a byte-indexed range in the preedit string (begin, end).
59    /// When `None`, the cursor should be hidden.
60    Preedit {
61        text: String,
62        cursor: Option<(usize, usize)>,
63    },
64    /// Delete text surrounding the cursor or selection.
65    ///
66    /// This event does not affect the preedit string. See winit's `Ime::DeleteSurrounding` docs.
67    ///
68    /// Offsets are expressed in UTF-8 bytes.
69    DeleteSurrounding {
70        before_bytes: usize,
71        after_bytes: usize,
72    },
73}
74
75/// Debug snapshot for the wasm textarea IME bridge (ADR 0180).
76///
77/// This is intended for diagnostics/harness views and is not a normative contract surface.
78#[derive(Debug, Clone, Default, PartialEq)]
79pub struct WebImeBridgeDebugSnapshot {
80    pub enabled: bool,
81    pub composing: bool,
82    pub suppress_next_input: bool,
83
84    /// Whether the hidden textarea is currently the document's active element (focused).
85    ///
86    /// This is best-effort and intended for diagnosing browser "user activation" restrictions where
87    /// `focus()` calls can be ignored.
88    pub textarea_has_focus: Option<bool>,
89    /// Tag name of `document.activeElement` when available (e.g. `"TEXTAREA"`, `"CANVAS"`).
90    pub active_element_tag: Option<String>,
91
92    /// Where the hidden textarea is positioned.
93    ///
94    /// This is intentionally stringly-typed to keep the snapshot portable across runners.
95    /// Expected values: `"absolute"`, `"fixed"`, or `None` if not initialized.
96    pub position_mode: Option<String>,
97    /// Describes what the textarea is mounted into.
98    ///
99    /// Expected values: `"overlay"`, `"mount"`, `"body"`, or `None` if not initialized.
100    pub mount_kind: Option<String>,
101    /// Device pixel ratio at the time the bridge was initialized or last updated.
102    pub device_pixel_ratio: Option<f64>,
103
104    /// Debug-only textarea metrics (DOM-reported).
105    ///
106    /// These help diagnose candidate UI jitter and unexpected wrapping/scrolling behaviors across
107    /// browsers and IMEs. Units are CSS pixels unless otherwise noted.
108    pub textarea_value_chars: Option<usize>,
109    pub textarea_selection_start_utf16: Option<u32>,
110    pub textarea_selection_end_utf16: Option<u32>,
111    pub textarea_client_width_px: Option<i32>,
112    pub textarea_client_height_px: Option<i32>,
113    pub textarea_scroll_width_px: Option<i32>,
114    pub textarea_scroll_height_px: Option<i32>,
115
116    pub last_input_type: Option<String>,
117    pub last_beforeinput_data: Option<String>,
118    pub last_input_data: Option<String>,
119
120    pub last_key_code: Option<KeyCode>,
121    pub last_cursor_area: Option<Rect>,
122    /// Where the hidden textarea is anchored (CSS px, relative to its positioning context).
123    ///
124    /// This is derived from `last_cursor_area` by the web runner and helps diagnose candidate UI
125    /// offsets (e.g. top-left vs center anchoring).
126    pub last_cursor_anchor_px: Option<(f32, f32)>,
127
128    /// Truncated preedit text observed during `compositionupdate`.
129    pub last_preedit_text: Option<String>,
130    /// Preedit cursor range in UTF-16 code units (begin, end) as reported by the textarea.
131    pub last_preedit_cursor_utf16: Option<(u32, u32)>,
132    /// Truncated committed text observed during `compositionend` or `input`.
133    pub last_commit_text: Option<String>,
134
135    /// Recent IME-related DOM events (debug-only ring buffer).
136    ///
137    /// Intended to help diagnose ordering differences across browsers/IMEs.
138    pub recent_events: Vec<String>,
139
140    pub beforeinput_seen: u64,
141    pub input_seen: u64,
142    pub suppressed_input_seen: u64,
143    pub composition_start_seen: u64,
144    pub composition_update_seen: u64,
145    pub composition_end_seen: u64,
146    pub cursor_area_set_seen: u64,
147}
148
149#[derive(Debug, Clone, PartialEq)]
150pub enum PointerEvent {
151    Move {
152        pointer_id: PointerId,
153        position: Point,
154        buttons: MouseButtons,
155        modifiers: Modifiers,
156        pointer_type: PointerType,
157    },
158    Down {
159        pointer_id: PointerId,
160        position: Point,
161        button: MouseButton,
162        modifiers: Modifiers,
163        /// Consecutive click count for this button (1 = single click, 2 = double click, ...).
164        ///
165        /// This count is provided by the platform runner and only increments for "true clicks"
166        /// (press + release without exceeding a small drag threshold).
167        click_count: u8,
168        pointer_type: PointerType,
169    },
170    Up {
171        pointer_id: PointerId,
172        position: Point,
173        button: MouseButton,
174        modifiers: Modifiers,
175        /// Whether this pointer-up completes a "true click" (press + release without exceeding
176        /// the runner's click slop threshold).
177        ///
178        /// This signal is computed by the platform runner and is intentionally separate from
179        /// `click_count`: `click_count` can remain stable even when a press turns into a drag.
180        is_click: bool,
181        /// Consecutive click count for this button (1 = single click, 2 = double click, ...).
182        ///
183        /// See `PointerEvent::Down.click_count` for the normalization rules.
184        click_count: u8,
185        pointer_type: PointerType,
186    },
187    Wheel {
188        pointer_id: PointerId,
189        position: Point,
190        delta: Point,
191        modifiers: Modifiers,
192        pointer_type: PointerType,
193    },
194    /// Two-finger pinch gesture, typically produced by touchpads (and some touch platforms).
195    ///
196    /// `delta` is positive for magnification (zoom in) and negative for shrinking (zoom out).
197    /// This value may be NaN depending on the platform backend; callers should guard accordingly.
198    PinchGesture {
199        pointer_id: PointerId,
200        position: Point,
201        delta: f32,
202        modifiers: Modifiers,
203        pointer_type: PointerType,
204    },
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208pub enum PointerCancelReason {
209    /// The pointer left the window (e.g. cursor left the window, or touch tracking was canceled).
210    LeftWindow,
211}
212
213#[derive(Debug, Clone, PartialEq)]
214pub struct PointerCancelEvent {
215    pub pointer_id: PointerId,
216    /// When provided by the platform, this is the last known pointer position (logical pixels).
217    pub position: Option<Point>,
218    pub buttons: MouseButtons,
219    pub modifiers: Modifiers,
220    pub pointer_type: PointerType,
221    pub reason: PointerCancelReason,
222}
223
224#[derive(Debug, Clone, PartialEq)]
225pub enum ExternalDragKind {
226    EnterFiles(ExternalDragFiles),
227    OverFiles(ExternalDragFiles),
228    DropFiles(ExternalDragFiles),
229    Leave,
230}
231
232#[derive(Debug, Clone, PartialEq)]
233pub struct ExternalDragFiles {
234    pub token: ExternalDropToken,
235    pub files: Vec<ExternalDragFile>,
236}
237
238#[derive(Debug, Clone, PartialEq)]
239pub struct ExternalDragFile {
240    pub name: String,
241    /// File size in bytes when known.
242    ///
243    /// Web runners can provide this from the `File` object. Native runners may provide this from
244    /// filesystem metadata.
245    pub size_bytes: Option<u64>,
246    /// MIME type when known (e.g. `"image/png"`).
247    ///
248    /// Web runners can provide this from the `File` object. Native runners may leave this unset.
249    pub media_type: Option<String>,
250}
251
252#[derive(Debug, Clone, PartialEq)]
253pub struct ExternalDragEvent {
254    pub position: Point,
255    pub kind: ExternalDragKind,
256}
257
258#[derive(Debug, Clone, PartialEq)]
259pub struct ExternalDropDataEvent {
260    pub token: ExternalDropToken,
261    pub files: Vec<ExternalDropFileData>,
262    pub errors: Vec<ExternalDropReadError>,
263}
264
265#[derive(Debug, Clone, PartialEq)]
266pub struct ExternalDropFileData {
267    pub name: String,
268    pub bytes: Vec<u8>,
269}
270
271#[derive(Debug, Clone, PartialEq)]
272pub struct ExternalDropReadError {
273    pub name: String,
274    pub message: String,
275}
276
277#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
278pub struct ExternalDropReadLimits {
279    pub max_total_bytes: u64,
280    pub max_file_bytes: u64,
281    pub max_files: usize,
282}
283
284impl ExternalDropReadLimits {
285    pub fn capped_by(self, cap: ExternalDropReadLimits) -> ExternalDropReadLimits {
286        ExternalDropReadLimits {
287            max_total_bytes: self.max_total_bytes.min(cap.max_total_bytes),
288            max_file_bytes: self.max_file_bytes.min(cap.max_file_bytes),
289            max_files: self.max_files.min(cap.max_files),
290        }
291    }
292}
293
294#[derive(Debug, Clone, Copy, PartialEq)]
295pub enum InternalDragKind {
296    Enter,
297    Over,
298    Drop,
299    Leave,
300    Cancel,
301}
302
303#[derive(Debug, Clone, PartialEq)]
304pub struct InternalDragEvent {
305    pub pointer_id: PointerId,
306    pub position: Point,
307    pub kind: InternalDragKind,
308    pub modifiers: Modifiers,
309}
310
311#[derive(Debug, Clone, Copy, PartialEq, Eq)]
312pub enum ClipboardAccessErrorKind {
313    Unavailable,
314    PermissionDenied,
315    UserActivationRequired,
316    Unsupported,
317    BackendError,
318    Unknown,
319}
320
321#[derive(Debug, Clone, PartialEq, Eq)]
322pub struct ClipboardAccessError {
323    pub kind: ClipboardAccessErrorKind,
324    pub message: Option<String>,
325}
326
327#[derive(Debug, Clone, PartialEq, Eq)]
328pub enum ClipboardWriteOutcome {
329    Succeeded,
330    Failed { error: ClipboardAccessError },
331}
332
333#[derive(Debug, Clone, PartialEq)]
334pub enum Event {
335    Pointer(PointerEvent),
336    PointerCancel(PointerCancelEvent),
337    Timer {
338        token: TimerToken,
339    },
340    Ime(ImeEvent),
341    ExternalDrag(ExternalDragEvent),
342    ExternalDropData(ExternalDropDataEvent),
343    InternalDrag(InternalDragEvent),
344    KeyDown {
345        key: KeyCode,
346        modifiers: Modifiers,
347        repeat: bool,
348    },
349    KeyUp {
350        key: KeyCode,
351        modifiers: Modifiers,
352    },
353    TextInput(String),
354    /// Sets the current selection (or caret when `anchor == focus`) in UTF-8 byte offsets
355    /// within the focused widget's text buffer (ADR 0071).
356    ///
357    /// This event is primarily intended for accessibility and automation backends.
358    SetTextSelection {
359        anchor: u32,
360        focus: u32,
361    },
362    /// Clipboard write request completed.
363    ClipboardWriteCompleted {
364        token: ClipboardToken,
365        outcome: ClipboardWriteOutcome,
366    },
367    /// Clipboard text payload delivered to the focused widget (typically as the result of a paste request).
368    ClipboardReadText {
369        token: ClipboardToken,
370        text: String,
371    },
372    /// Clipboard read completed without a text payload (clipboard empty/unavailable/error).
373    ClipboardReadFailed {
374        token: ClipboardToken,
375        error: ClipboardAccessError,
376    },
377    /// Share sheet request completed.
378    ShareSheetCompleted {
379        token: ShareSheetToken,
380        outcome: ShareSheetOutcome,
381    },
382    /// Linux primary selection text payload delivered to the focused widget.
383    ///
384    /// This typically originates from middle-click paste when primary selection is enabled.
385    PrimarySelectionText {
386        token: ClipboardToken,
387        text: String,
388    },
389    /// Primary selection read completed without a text payload (unavailable/empty/error).
390    PrimarySelectionTextUnavailable {
391        token: ClipboardToken,
392    },
393    /// File dialog selection metadata (token + names). Bytes must be requested via effects.
394    FileDialogSelection(FileDialogSelection),
395    /// File dialog data payload, typically produced by `Effect::FileDialogReadAll`.
396    FileDialogData(FileDialogDataEvent),
397    /// A file dialog request completed without a selection (user canceled).
398    FileDialogCanceled,
399    /// Incoming-open request originating from the OS (open-in / share-target).
400    ///
401    /// Apps must treat the token as ephemeral; privileged platform references remain runner-owned.
402    IncomingOpenRequest {
403        token: IncomingOpenToken,
404        items: Vec<IncomingOpenItem>,
405    },
406    /// Incoming-open data payload, typically produced by `Effect::IncomingOpenReadAll`.
407    IncomingOpenData(IncomingOpenDataEvent),
408    /// Incoming-open token became unavailable (denied/revoked/expired).
409    IncomingOpenUnavailable {
410        token: IncomingOpenToken,
411    },
412    /// Image resource registration completed and produced an `ImageId`.
413    ImageRegistered {
414        token: ImageUploadToken,
415        image: ImageId,
416        width: u32,
417        height: u32,
418    },
419    /// Image resource registration failed (e.g. invalid bytes, backend error).
420    ImageRegisterFailed {
421        token: ImageUploadToken,
422        message: String,
423    },
424    /// Optional acknowledgement that a streaming image update was applied.
425    ///
426    /// This is intended for debugging/telemetry surfaces and must be capability-gated by the
427    /// runner to avoid flooding the event loop during video playback (ADR 0124).
428    ImageUpdateApplied {
429        token: ImageUpdateToken,
430        image: ImageId,
431    },
432    /// Optional acknowledgement that a streaming image update was dropped.
433    ///
434    /// See `ImageUpdateApplied` for rationale (ADR 0124).
435    ImageUpdateDropped {
436        token: ImageUpdateToken,
437        image: ImageId,
438        reason: ImageUpdateDropReason,
439    },
440    /// Window close button / OS close request was triggered.
441    ///
442    /// The runner must not close the window immediately; the app/driver may intercept the request
443    /// (e.g. unsaved-changes confirmation) and decide whether to emit `WindowRequest::Close`.
444    WindowCloseRequested,
445    /// Window focus state changed (focused vs blurred).
446    WindowFocusChanged(bool),
447    WindowScaleFactorChanged(f32),
448    WindowMoved(WindowLogicalPosition),
449    WindowResized {
450        width: Px,
451        height: Px,
452    },
453}
454
455#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
456pub enum ImageUpdateDropReason {
457    Coalesced,
458    StagingBudgetExceeded,
459    UnknownImage,
460    InvalidPayload,
461    RendererNotReady,
462    Unsupported,
463}
464
465#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
466pub struct MouseButtons {
467    pub left: bool,
468    pub right: bool,
469    pub middle: bool,
470}