Skip to main content

agpu/
event.rs

1//! Event system and winit conversion for agpu.
2//!
3//! Provides a unified event type and translates keyboard, mouse, window,
4//! and text input events from winit into agpu's own `Event` enum.
5
6use crate::core::{Position, Rect, Size};
7use serde::{Deserialize, Serialize};
8use winit::event::{ElementState, MouseButton as WinitButton, WindowEvent};
9use winit::keyboard::{Key, NamedKey};
10
11// ── Event Types ─────────────────────────────────────────────────────
12
13/// A GUI event.
14#[derive(Debug, Clone, PartialEq)]
15pub enum Event {
16    /// Keyboard event.
17    Key(KeyEvent),
18    /// Mouse event.
19    Mouse(MouseEvent),
20    /// Text input (already composed characters).
21    TextInput(String),
22    /// Window was resized to the given logical size.
23    Resize(Size),
24    /// Window gained focus.
25    FocusGained,
26    /// Window lost focus.
27    FocusLost,
28    /// Window close requested.
29    CloseRequested,
30    /// A scheduled tick from the runtime (for animations).
31    Tick,
32    /// File(s) dropped onto the window.
33    FileDrop(Vec<String>),
34    /// File(s) hovering over the window.
35    FileHover(Vec<String>),
36    /// File hover cancelled.
37    FileHoverCancelled,
38    /// Drag-and-drop event.
39    DragDrop(DragDropEvent),
40    /// An agent action dispatched via `Command::AgentAction`.
41    AgentAction {
42        agent_id: String,
43        action: String,
44        params: serde_json::Value,
45    },
46    /// Input method editor (IME) preedit text (for CJK input).
47    ImePreedit {
48        /// Current composition text (empty when composition ends).
49        text: String,
50        /// Cursor position within the preedit text, if known.
51        cursor: Option<(usize, usize)>,
52    },
53    /// IME composition committed — final text to insert.
54    ImeCommit(String),
55}
56
57/// A keyboard event.
58#[derive(Debug, Clone, PartialEq, Eq, Hash)]
59pub struct KeyEvent {
60    pub code: KeyCode,
61    pub modifiers: KeyModifiers,
62    pub kind: KeyEventKind,
63}
64
65impl KeyEvent {
66    pub fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {
67        Self {
68            code,
69            modifiers,
70            kind: KeyEventKind::Press,
71        }
72    }
73
74    pub fn is_ctrl(&self, code: KeyCode) -> bool {
75        self.code == code && self.modifiers.contains(KeyModifiers::CONTROL)
76    }
77
78    pub fn is_key(&self, code: KeyCode) -> bool {
79        self.code == code && self.modifiers.is_empty()
80    }
81}
82
83/// The kind of key event.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
85pub enum KeyEventKind {
86    Press,
87    Release,
88    Repeat,
89}
90
91/// Key code.
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
93pub enum KeyCode {
94    Char(char),
95    F(u8),
96    Backspace,
97    Enter,
98    Tab,
99    BackTab,
100    Esc,
101    Left,
102    Right,
103    Up,
104    Down,
105    Home,
106    End,
107    PageUp,
108    PageDown,
109    Insert,
110    Delete,
111    Null,
112}
113
114/// Key modifier flags.
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
116pub struct KeyModifiers(u8);
117
118impl KeyModifiers {
119    pub const NONE: Self = Self(0);
120    pub const SHIFT: Self = Self(1 << 0);
121    pub const CONTROL: Self = Self(1 << 1);
122    pub const ALT: Self = Self(1 << 2);
123    pub const SUPER: Self = Self(1 << 3);
124
125    pub const fn empty() -> Self {
126        Self(0)
127    }
128
129    pub const fn contains(self, other: Self) -> bool {
130        self.0 & other.0 == other.0
131    }
132
133    pub const fn is_empty(self) -> bool {
134        self.0 == 0
135    }
136
137    pub const fn union(self, other: Self) -> Self {
138        Self(self.0 | other.0)
139    }
140}
141
142impl std::ops::BitOr for KeyModifiers {
143    type Output = Self;
144    fn bitor(self, rhs: Self) -> Self::Output {
145        Self(self.0 | rhs.0)
146    }
147}
148
149impl std::ops::BitOrAssign for KeyModifiers {
150    fn bitor_assign(&mut self, rhs: Self) {
151        self.0 |= rhs.0;
152    }
153}
154
155/// A mouse event.
156#[derive(Debug, Clone, PartialEq)]
157pub struct MouseEvent {
158    pub kind: MouseEventKind,
159    pub position: Position,
160    pub modifiers: KeyModifiers,
161}
162
163impl MouseEvent {
164    pub fn is_click(&self) -> bool {
165        matches!(self.kind, MouseEventKind::Click(_))
166    }
167
168    pub fn is_drag(&self) -> bool {
169        matches!(self.kind, MouseEventKind::Drag(_))
170    }
171
172    pub fn is_scroll(&self) -> bool {
173        matches!(self.kind, MouseEventKind::Scroll { .. })
174    }
175
176    /// Whether the given rect was clicked.
177    pub fn clicked_in(&self, area: Rect) -> bool {
178        self.is_click() && area.contains(self.position)
179    }
180}
181
182/// The kind of mouse event.
183#[derive(Debug, Clone, Copy, PartialEq)]
184pub enum MouseEventKind {
185    Click(MouseButton),
186    Release(MouseButton),
187    Drag(MouseButton),
188    Move,
189    Scroll { delta_x: f32, delta_y: f32 },
190}
191
192/// Mouse button.
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
194pub enum MouseButton {
195    Left,
196    Right,
197    Middle,
198}
199
200/// A drag-and-drop event.
201#[derive(Debug, Clone, PartialEq)]
202pub struct DragDropEvent {
203    /// The kind of drag-drop event.
204    pub kind: DragDropKind,
205    /// Current pointer position during drag.
206    pub position: Position,
207}
208
209/// The kind of drag-and-drop event.
210#[derive(Debug, Clone, PartialEq)]
211pub enum DragDropKind {
212    /// A drag operation started from a widget.
213    DragStart {
214        source_id: String,
215        payload: DragPayload,
216    },
217    /// The dragged item is hovering over a potential drop target.
218    DragOver { target_id: String },
219    /// The dragged item left a potential drop target.
220    DragLeave { target_id: String },
221    /// The dragged item was dropped on a target.
222    Drop {
223        source_id: String,
224        target_id: String,
225        payload: DragPayload,
226    },
227    /// The drag operation was cancelled.
228    DragCancel,
229}
230
231/// Data carried during a drag-and-drop operation.
232#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
233pub enum DragPayload {
234    /// Plain text content.
235    Text(String),
236    /// An item index (e.g. from a list or table).
237    Index(usize),
238    /// A path in a tree widget.
239    Path(Vec<String>),
240    /// Arbitrary JSON data.
241    Json(serde_json::Value),
242}
243
244/// A hit-test map that resolves positions to widget IDs.
245///
246/// Widgets register their bounds during rendering; the runtime uses this
247/// to route mouse events to the correct widget.
248#[derive(Debug, Default)]
249pub struct HitMap {
250    entries: Vec<HitEntry>,
251}
252
253#[derive(Debug)]
254struct HitEntry {
255    agent_id: String,
256    bounds: Rect,
257    z_order: u32,
258}
259
260impl HitMap {
261    pub fn new() -> Self {
262        Self::default()
263    }
264
265    /// Clear all entries (called at the start of each frame).
266    pub fn clear(&mut self) {
267        self.entries.clear();
268    }
269
270    /// Register a widget's clickable bounds.
271    pub fn register(&mut self, agent_id: impl Into<String>, bounds: Rect, z_order: u32) {
272        self.entries.push(HitEntry {
273            agent_id: agent_id.into(),
274            bounds,
275            z_order,
276        });
277    }
278
279    /// Find the widget at the given position (highest z-order wins).
280    pub fn hit_test(&self, pos: Position) -> Option<&str> {
281        self.entries
282            .iter()
283            .filter(|e| e.bounds.contains(pos))
284            .max_by_key(|e| e.z_order)
285            .map(|e| e.agent_id.as_str())
286    }
287}
288
289// ── Winit Conversion ────────────────────────────────────────────────
290
291/// Convert a single winit `WindowEvent` into zero or more agpu events.
292pub fn convert_window_event(event: &WindowEvent) -> Vec<Event> {
293    let mut out = Vec::new();
294
295    match event {
296        WindowEvent::KeyboardInput { event: key_ev, .. } => {
297            if let Some(code) = convert_key(&key_ev.logical_key) {
298                let kind = match key_ev.state {
299                    ElementState::Pressed => KeyEventKind::Press,
300                    ElementState::Released => KeyEventKind::Release,
301                };
302                let mods = KeyModifiers::empty(); // winit modifiers handled below
303                out.push(Event::Key(KeyEvent {
304                    code,
305                    modifiers: mods,
306                    kind,
307                }));
308            }
309            // Generate TextInput for printable characters on press
310            if key_ev.state == ElementState::Pressed {
311                if let Key::Character(ch) = &key_ev.logical_key {
312                    out.push(Event::TextInput(ch.to_string()));
313                }
314            }
315        }
316
317        WindowEvent::CursorMoved { position, .. } => {
318            out.push(Event::Mouse(MouseEvent {
319                kind: MouseEventKind::Move,
320                position: Position::new(position.x as f32, position.y as f32),
321                modifiers: KeyModifiers::empty(),
322            }));
323        }
324
325        WindowEvent::MouseInput { state, button, .. } => {
326            let btn = match button {
327                WinitButton::Left => MouseButton::Left,
328                WinitButton::Right => MouseButton::Right,
329                WinitButton::Middle => MouseButton::Middle,
330                _ => MouseButton::Left,
331            };
332            let kind = match state {
333                ElementState::Pressed => MouseEventKind::Click(btn),
334                ElementState::Released => MouseEventKind::Release(btn),
335            };
336            out.push(Event::Mouse(MouseEvent {
337                kind,
338                position: Position::ZERO, // updated by cursor position tracking
339                modifiers: KeyModifiers::empty(),
340            }));
341        }
342
343        WindowEvent::MouseWheel { delta, .. } => {
344            let (dx, dy) = match delta {
345                winit::event::MouseScrollDelta::LineDelta(x, y) => (*x, *y),
346                winit::event::MouseScrollDelta::PixelDelta(p) => (p.x as f32, p.y as f32),
347            };
348            out.push(Event::Mouse(MouseEvent {
349                kind: MouseEventKind::Scroll {
350                    delta_x: dx,
351                    delta_y: dy,
352                },
353                position: Position::ZERO,
354                modifiers: KeyModifiers::empty(),
355            }));
356        }
357
358        WindowEvent::Resized(size) => {
359            out.push(Event::Resize(Size::new(
360                size.width as f32,
361                size.height as f32,
362            )));
363        }
364
365        WindowEvent::Focused(focused) => {
366            out.push(if *focused {
367                Event::FocusGained
368            } else {
369                Event::FocusLost
370            });
371        }
372
373        WindowEvent::CloseRequested => {
374            out.push(Event::CloseRequested);
375        }
376
377        WindowEvent::DroppedFile(path) => {
378            out.push(Event::FileDrop(vec![path.display().to_string()]));
379        }
380
381        WindowEvent::HoveredFile(path) => {
382            out.push(Event::FileHover(vec![path.display().to_string()]));
383        }
384
385        WindowEvent::HoveredFileCancelled => {
386            out.push(Event::FileHoverCancelled);
387        }
388
389        WindowEvent::Ime(ime) => match ime {
390            winit::event::Ime::Preedit(text, cursor) => {
391                out.push(Event::ImePreedit {
392                    text: text.clone(),
393                    cursor: *cursor,
394                });
395            }
396            winit::event::Ime::Commit(text) => {
397                out.push(Event::ImeCommit(text.clone()));
398            }
399            _ => {}
400        },
401
402        _ => {}
403    }
404
405    out
406}
407
408/// Convert winit logical key to agpu `KeyCode`.
409fn convert_key(key: &Key) -> Option<KeyCode> {
410    match key {
411        Key::Named(named) => match named {
412            NamedKey::Enter => Some(KeyCode::Enter),
413            NamedKey::Tab => Some(KeyCode::Tab),
414            NamedKey::Backspace => Some(KeyCode::Backspace),
415            NamedKey::Escape => Some(KeyCode::Esc),
416            NamedKey::ArrowLeft => Some(KeyCode::Left),
417            NamedKey::ArrowRight => Some(KeyCode::Right),
418            NamedKey::ArrowUp => Some(KeyCode::Up),
419            NamedKey::ArrowDown => Some(KeyCode::Down),
420            NamedKey::Home => Some(KeyCode::Home),
421            NamedKey::End => Some(KeyCode::End),
422            NamedKey::PageUp => Some(KeyCode::PageUp),
423            NamedKey::PageDown => Some(KeyCode::PageDown),
424            NamedKey::Insert => Some(KeyCode::Insert),
425            NamedKey::Delete => Some(KeyCode::Delete),
426            NamedKey::F1 => Some(KeyCode::F(1)),
427            NamedKey::F2 => Some(KeyCode::F(2)),
428            NamedKey::F3 => Some(KeyCode::F(3)),
429            NamedKey::F4 => Some(KeyCode::F(4)),
430            NamedKey::F5 => Some(KeyCode::F(5)),
431            NamedKey::F6 => Some(KeyCode::F(6)),
432            NamedKey::F7 => Some(KeyCode::F(7)),
433            NamedKey::F8 => Some(KeyCode::F(8)),
434            NamedKey::F9 => Some(KeyCode::F(9)),
435            NamedKey::F10 => Some(KeyCode::F(10)),
436            NamedKey::F11 => Some(KeyCode::F(11)),
437            NamedKey::F12 => Some(KeyCode::F(12)),
438            _ => None,
439        },
440        Key::Character(ch) => {
441            let c = ch.chars().next()?;
442            Some(KeyCode::Char(c))
443        }
444        _ => None,
445    }
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451
452    // ── KeyModifiers ─────────────────────────────────────────────
453
454    #[test]
455    fn key_modifiers_empty() {
456        let m = KeyModifiers::empty();
457        assert!(m.is_empty());
458        assert!(!m.contains(KeyModifiers::SHIFT));
459    }
460
461    #[test]
462    fn key_modifiers_contains() {
463        let m = KeyModifiers::SHIFT | KeyModifiers::CONTROL;
464        assert!(m.contains(KeyModifiers::SHIFT));
465        assert!(m.contains(KeyModifiers::CONTROL));
466        assert!(!m.contains(KeyModifiers::ALT));
467    }
468
469    #[test]
470    fn key_modifiers_union() {
471        let a = KeyModifiers::SHIFT;
472        let b = KeyModifiers::ALT;
473        let u = a.union(b);
474        assert!(u.contains(KeyModifiers::SHIFT));
475        assert!(u.contains(KeyModifiers::ALT));
476    }
477
478    #[test]
479    fn key_modifiers_bitor_assign() {
480        let mut m = KeyModifiers::NONE;
481        m |= KeyModifiers::SUPER;
482        assert!(m.contains(KeyModifiers::SUPER));
483    }
484
485    // ── KeyEvent ─────────────────────────────────────────────────
486
487    #[test]
488    fn key_event_new() {
489        let ke = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
490        assert_eq!(ke.code, KeyCode::Enter);
491        assert!(ke.modifiers.is_empty());
492        assert_eq!(ke.kind, KeyEventKind::Press);
493    }
494
495    #[test]
496    fn key_event_is_ctrl() {
497        let ke = KeyEvent::new(KeyCode::Char('s'), KeyModifiers::CONTROL);
498        assert!(ke.is_ctrl(KeyCode::Char('s')));
499        assert!(!ke.is_ctrl(KeyCode::Char('c')));
500    }
501
502    #[test]
503    fn key_event_is_key() {
504        let ke = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
505        assert!(ke.is_key(KeyCode::Esc));
506
507        let with_mod = KeyEvent::new(KeyCode::Esc, KeyModifiers::SHIFT);
508        assert!(!with_mod.is_key(KeyCode::Esc));
509    }
510
511    // ── MouseEvent ───────────────────────────────────────────────
512
513    #[test]
514    fn mouse_event_is_click() {
515        let me = MouseEvent {
516            kind: MouseEventKind::Click(MouseButton::Left),
517            position: Position::ZERO,
518            modifiers: KeyModifiers::NONE,
519        };
520        assert!(me.is_click());
521        assert!(!me.is_drag());
522        assert!(!me.is_scroll());
523    }
524
525    #[test]
526    fn mouse_event_clicked_in() {
527        let area = Rect::new(10.0, 10.0, 100.0, 100.0);
528        let inside = MouseEvent {
529            kind: MouseEventKind::Click(MouseButton::Left),
530            position: Position::new(50.0, 50.0),
531            modifiers: KeyModifiers::NONE,
532        };
533        let outside = MouseEvent {
534            kind: MouseEventKind::Click(MouseButton::Left),
535            position: Position::new(5.0, 5.0),
536            modifiers: KeyModifiers::NONE,
537        };
538        let not_click = MouseEvent {
539            kind: MouseEventKind::Move,
540            position: Position::new(50.0, 50.0),
541            modifiers: KeyModifiers::NONE,
542        };
543        assert!(inside.clicked_in(area));
544        assert!(!outside.clicked_in(area));
545        assert!(!not_click.clicked_in(area));
546    }
547
548    // ── HitMap ───────────────────────────────────────────────────
549
550    #[test]
551    fn hit_map_empty() {
552        let hm = HitMap::new();
553        assert!(hm.hit_test(Position::ZERO).is_none());
554    }
555
556    #[test]
557    fn hit_map_register_and_hit() {
558        let mut hm = HitMap::new();
559        hm.register("btn-1", Rect::new(0.0, 0.0, 50.0, 50.0), 0);
560        assert_eq!(hm.hit_test(Position::new(25.0, 25.0)), Some("btn-1"));
561        assert!(hm.hit_test(Position::new(60.0, 60.0)).is_none());
562    }
563
564    #[test]
565    fn hit_map_z_order() {
566        let mut hm = HitMap::new();
567        hm.register("back", Rect::new(0.0, 0.0, 100.0, 100.0), 0);
568        hm.register("front", Rect::new(0.0, 0.0, 100.0, 100.0), 10);
569        assert_eq!(hm.hit_test(Position::new(50.0, 50.0)), Some("front"));
570    }
571
572    #[test]
573    fn hit_map_clear() {
574        let mut hm = HitMap::new();
575        hm.register("widget", Rect::new(0.0, 0.0, 50.0, 50.0), 0);
576        assert!(hm.hit_test(Position::new(25.0, 25.0)).is_some());
577        hm.clear();
578        assert!(hm.hit_test(Position::new(25.0, 25.0)).is_none());
579    }
580
581    // ── DragPayload ──────────────────────────────────────────────
582
583    #[test]
584    fn drag_payload_serialize_roundtrip() {
585        let payloads = vec![
586            DragPayload::Text("hello".into()),
587            DragPayload::Index(42),
588            DragPayload::Path(vec!["a".into(), "b".into()]),
589            DragPayload::Json(serde_json::json!({"key": "value"})),
590        ];
591        for p in payloads {
592            let json = serde_json::to_string(&p).unwrap();
593            let p2: DragPayload = serde_json::from_str(&json).unwrap();
594            assert_eq!(p, p2);
595        }
596    }
597
598    // ── KeyCode serialize ────────────────────────────────────────
599
600    #[test]
601    fn keycode_serialize_roundtrip() {
602        let codes = vec![
603            KeyCode::Char('a'),
604            KeyCode::F(5),
605            KeyCode::Enter,
606            KeyCode::Esc,
607            KeyCode::Null,
608        ];
609        for code in codes {
610            let json = serde_json::to_string(&code).unwrap();
611            let code2: KeyCode = serde_json::from_str(&json).unwrap();
612            assert_eq!(code, code2);
613        }
614    }
615}