Skip to main content

repose_core/
input.rs

1use crate::Vec2;
2use std::cell::Cell;
3use std::rc::Rc;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
6pub struct PointerId(pub u64);
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum PointerKind {
10    Mouse,
11    Touch,
12    Pen,
13}
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
16pub enum PointerButton {
17    Primary,   // Left mouse, touch
18    Secondary, // Right mouse
19    Tertiary,  // Middle mouse
20}
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum PointerEventPass {
24    /// Top-down pass: ancestor -> descendant. Allows ancestors to preview or
25    /// intercept events before descendants see them.
26    Initial,
27    /// Bottom-up pass: descendant -> ancestor. The primary pass where gesture
28    /// handlers react to and consume events. A child that consumes its event
29    /// prevents the parent from reacting (Compose's requireUnconsumed).
30    Main,
31    /// Top-down pass: ancestor -> descendant. Allows descendants to learn
32    /// about events consumed by ancestors during the Main pass.
33    Final,
34}
35
36#[derive(Clone, Copy, Debug)]
37pub enum PointerEventKind {
38    Down(PointerButton),
39    Up(PointerButton),
40    Move,
41    Cancel,
42    Enter,
43    Leave,
44}
45
46#[derive(Clone, Debug)]
47pub struct PointerEvent {
48    pub id: PointerId,
49    pub kind: PointerKind,
50    pub event: PointerEventKind,
51    /// Position relative to `origin` (hit-region top-left), in physical px.
52    pub position: Vec2,
53    /// Top-left of the hit region this event is being delivered to (physical px).
54    pub origin: Vec2,
55    pub pressure: f32,
56    pub modifiers: Modifiers,
57    /// Shared consumed state -> every clone of this event points to the same
58    /// Cell. Calling `consume()` on any clone marks it consumed for all clones.
59    pub consumed: Rc<Cell<bool>>,
60}
61
62impl PointerEvent {
63    pub fn new(
64        id: PointerId,
65        kind: PointerKind,
66        event: PointerEventKind,
67        position: Vec2,
68        pressure: f32,
69        modifiers: Modifiers,
70    ) -> Self {
71        Self {
72            id,
73            kind,
74            event,
75            position,
76            origin: Vec2::ZERO,
77            pressure,
78            modifiers,
79            consumed: Rc::new(Cell::new(false)),
80        }
81    }
82
83    /// Absolute position in window/surface physical pixels.
84    pub fn position_in_window(&self) -> Vec2 {
85        self.position + self.origin
86    }
87
88    /// Mark this event as consumed. Once consumed, subsequent handlers in the
89    /// same pass should skip processing it (equivalent to Compose's
90    /// `PointerInputChange.consume()`).
91    pub fn consume(&self) {
92        self.consumed.set(true);
93    }
94
95    /// Returns `true` if `consume()` was called on this event or any clone of it.
96    pub fn is_consumed(&self) -> bool {
97        self.consumed.get()
98    }
99}
100
101#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
102pub struct Modifiers {
103    pub shift: bool,
104    pub ctrl: bool,
105    pub alt: bool,
106    pub meta: bool,    // Cmd on Mac, Win key on Windows
107    pub command: bool, // egui like (Cmd on macOS, Ctrl elsewhere)
108}
109
110#[derive(Clone, Debug, PartialEq, Eq, Hash)]
111pub enum Key {
112    Character(char),
113    Enter,
114    Tab,
115    Backspace,
116    Delete,
117    Insert,
118    Escape,
119    ArrowLeft,
120    ArrowRight,
121    ArrowUp,
122    ArrowDown,
123    Home,
124    End,
125    PageUp,
126    PageDown,
127    Space,
128    F(u8), // F1-F12
129    Unknown,
130}
131
132#[derive(Clone, Copy, Debug, PartialEq, Eq)]
133pub enum KeyEventType {
134    /// Key pressed down.
135    Down,
136    /// Key released.
137    Up,
138    /// Unknown or unsupported event type.
139    Unknown,
140}
141
142#[derive(Clone, Debug)]
143pub struct KeyEvent {
144    pub key: Key,
145    pub modifiers: Modifiers,
146    pub is_repeat: bool,
147    /// Whether this is a key-down or key-up event.
148    pub event_type: KeyEventType,
149    /// UTF-16 code point for character keys, or 0 for non-characters.
150    /// Matches Compose's `utf16CodePoint`.
151    pub utf16_code_point: u16,
152    /// Physical key position (`KeyW`, `Digit1`, `Space`, ... — winit
153    /// `KeyCode` debug names, layout-independent). `None` for
154    /// synthetic events (gamepad-emulated keys, tests). Games bind by
155    /// position the way GML's `ord("W")` binds the US position, so
156    /// non-US layouts move the same; the `key` glyph stays the
157    /// text-entry/shortcut path.
158    pub physical: Option<String>,
159}
160
161#[derive(Clone, Debug)]
162pub struct TextInputEvent {
163    pub text: String,
164}
165
166#[derive(Clone, Debug)]
167pub enum ImeEvent {
168    /// IME composition started
169    Start,
170    /// Composition text updated
171    Update {
172        text: String,
173        cursor: Option<(usize, usize)>, // (start, end) of composition range
174    },
175    /// Composition committed (finalized)
176    Commit(String),
177    /// Composition cancelled
178    Cancel,
179}
180
181#[derive(Clone, Debug)]
182pub enum InputEvent {
183    Pointer(PointerEvent),
184    Key(KeyEvent),
185    Text(TextInputEvent),
186    Ime(ImeEvent),
187    Gamepad(GamepadEvent),
188}
189
190/// Opaque gamepad handle. Backend-local index, stable for the connection
191/// lifetime. Survives across frames; invalid after [`GamepadEvent::Disconnected`].
192#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
193pub struct GamepadId(pub u32);
194
195/// Standard-layout buttons (SDL gamecontroller mapping positions).
196/// Backends translate hardware codes to these; unknown buttons are dropped.
197#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
198pub enum GamepadButton {
199    /// Bottom face button (A / Cross). UI default: activate.
200    South,
201    /// Right face button (B / Circle). UI default: back.
202    East,
203    /// Left face button (X / Square).
204    West,
205    /// Top face button (Y / Triangle).
206    North,
207    Start,
208    Select,
209    LeftShoulder,
210    RightShoulder,
211    LeftStick,
212    RightStick,
213    DPadUp,
214    DPadDown,
215    DPadLeft,
216    DPadRight,
217}
218
219/// Analog axes, normalized to -1.0..=1.0. Triggers report 0.0..=1.0.
220#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
221pub enum GamepadAxis {
222    LeftStickX,
223    LeftStickY,
224    RightStickX,
225    RightStickY,
226    LeftTrigger,
227    RightTrigger,
228}
229
230#[derive(Clone, Debug)]
231pub enum GamepadEvent {
232    Connected {
233        id: GamepadId,
234        name: String,
235    },
236    Disconnected {
237        id: GamepadId,
238    },
239    Button {
240        id: GamepadId,
241        button: GamepadButton,
242        pressed: bool,
243    },
244    Axis {
245        id: GamepadId,
246        axis: GamepadAxis,
247        /// -1.0..=1.0 (sticks) or 0.0..=1.0 (triggers). Backends deadzone.
248        value: f32,
249    },
250}
251
252impl GamepadEvent {
253    pub fn id(&self) -> GamepadId {
254        match *self {
255            GamepadEvent::Connected { id, .. } => id,
256            GamepadEvent::Disconnected { id } => id,
257            GamepadEvent::Button { id, .. } => id,
258            GamepadEvent::Axis { id, .. } => id,
259        }
260    }
261}
262
263#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
264pub enum InputMode {
265    #[default]
266    Touch,
267    /// Keyboard, Tab, arrow/D-pad, or other non-pointer navigation.
268    Keyboard,
269}
270
271thread_local! {
272    static INPUT_MODE: Cell<InputMode> = const { Cell::new(InputMode::Touch) };
273}
274
275/// Current input mode (Compose `InputModeManager.inputMode`).
276///
277/// Composition-local override ([`crate::locals::with_input_mode`]) wins over
278/// the thread default.
279#[inline]
280pub fn input_mode() -> InputMode {
281    crate::locals::local_input_mode().unwrap_or_else(|| INPUT_MODE.get())
282}
283
284/// Force the global default input mode (no frame request). Prefer
285/// [`request_input_mode`] from event handlers.
286#[inline]
287pub fn set_input_mode_default(mode: InputMode) {
288    INPUT_MODE.set(mode);
289}
290
291/// Request a new input mode. Returns `true` if the mode changed.
292///
293/// On change, requests a frame so focus chrome can appear/disappear.
294pub fn request_input_mode(mode: InputMode) -> bool {
295    let prev = INPUT_MODE.get();
296    if prev == mode {
297        return false;
298    }
299    INPUT_MODE.set(mode);
300    crate::frame_clock::request_frame();
301    true
302}
303
304/// `true` when focus indication should paint (focused **and** keyboard mode).
305#[inline]
306pub fn is_focus_visible(focused: bool) -> bool {
307    focused && input_mode() == InputMode::Keyboard
308}
309
310#[cfg(test)]
311mod input_mode_tests {
312    use super::*;
313    use crate::frame_clock::take_frame_request;
314    use crate::modifier::{Interaction, MutableInteractionSource};
315
316    #[test]
317    fn request_input_mode_changes_and_requests_frame() {
318        set_input_mode_default(InputMode::Touch);
319        let _ = take_frame_request();
320
321        assert!(!request_input_mode(InputMode::Touch));
322        assert!(!take_frame_request());
323
324        assert!(request_input_mode(InputMode::Keyboard));
325        assert_eq!(input_mode(), InputMode::Keyboard);
326        assert!(take_frame_request());
327
328        assert!(request_input_mode(InputMode::Touch));
329        assert_eq!(input_mode(), InputMode::Touch);
330        set_input_mode_default(InputMode::Touch);
331    }
332
333    #[test]
334    fn focus_visible_requires_keyboard_mode() {
335        set_input_mode_default(InputMode::Touch);
336        let src = MutableInteractionSource::new();
337        src.emit(Interaction::Focus);
338        assert!(src.source().collect_is_focused());
339        assert!(!src.source().collect_is_focus_visible());
340
341        set_input_mode_default(InputMode::Keyboard);
342        assert!(src.source().collect_is_focus_visible());
343
344        set_input_mode_default(InputMode::Touch);
345        src.emit(Interaction::Unfocus);
346    }
347
348    #[test]
349    fn with_input_mode_overrides_global() {
350        set_input_mode_default(InputMode::Touch);
351        crate::locals::with_input_mode(InputMode::Keyboard, || {
352            assert_eq!(input_mode(), InputMode::Keyboard);
353        });
354        assert_eq!(input_mode(), InputMode::Touch);
355    }
356}