Skip to main content

yog_ui/
event.rs

1//! The raw UI events a Yog screen's `register_ui` handler receives, parsed
2//! once into [`UiEvent`].
3//!
4//! | String | Event |
5//! |---|---|
6//! | `click:X:Y` | [`UiEvent::Click`] |
7//! | `drag:X:Y` | [`UiEvent::Drag`] |
8//! | `release:X:Y` | [`UiEvent::Release`] |
9//! | `move:X:Y` | [`UiEvent::Move`] (the cursor moved; newer hosts) |
10//! | `scroll:DY` | [`UiEvent::Scroll`] (positive = up) |
11//! | `key:KEY:MODS` | [`UiEvent::Key`] (GLFW key code and modifier bits) |
12//! | `char:CODEPOINT` | [`UiEvent::Char`] |
13//! | `open:W:H` | [`UiEvent::Open`] (the screen was opened; newer hosts) |
14//! | `close` | [`UiEvent::Close`] (the screen was closed; newer hosts) |
15//!
16//! Anything else is [`UiEvent::Other`].
17
18/// One parsed UI event — see the [module docs](self).
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub enum UiEvent<'a> {
21    Click { x: f32, y: f32 },
22    Drag { x: f32, y: f32 },
23    Release { x: f32, y: f32 },
24    Move { x: f32, y: f32 },
25    Scroll { dy: f32 },
26    Key { key: i32, mods: i32 },
27    Char(char),
28    Open { w: f32, h: f32 },
29    Close,
30    Other(&'a str),
31}
32
33/// GLFW key codes and modifier bits.
34pub mod keys {
35    pub const SPACE: i32 = 32;
36    pub const A: i32 = 65;
37    pub const ESCAPE: i32 = 256;
38    pub const ENTER: i32 = 257;
39    pub const TAB: i32 = 258;
40    pub const BACKSPACE: i32 = 259;
41    pub const DELETE: i32 = 261;
42    pub const RIGHT: i32 = 262;
43    pub const LEFT: i32 = 263;
44    pub const DOWN: i32 = 264;
45    pub const UP: i32 = 265;
46    pub const PAGE_UP: i32 = 266;
47    pub const PAGE_DOWN: i32 = 267;
48    pub const HOME: i32 = 268;
49    pub const END: i32 = 269;
50    pub const KP_ENTER: i32 = 335;
51    pub const MOD_SHIFT: i32 = 0x1;
52    pub const MOD_CONTROL: i32 = 0x2;
53    pub const MOD_ALT: i32 = 0x4;
54    pub const MOD_SUPER: i32 = 0x8;
55}
56
57fn xy(rest: &str) -> Option<(f32, f32)> {
58    let mut it = rest.split(':');
59    let x = it.next()?.parse().ok()?;
60    let y = it.next()?.parse().ok()?;
61    it.next().is_none().then_some((x, y))
62}
63
64impl<'a> UiEvent<'a> {
65    /// Parses a host event string; malformed or unknown ones are `Other`.
66    pub fn parse(raw: &'a str) -> Self {
67        Self::try_parse(raw).unwrap_or(UiEvent::Other(raw))
68    }
69
70    fn try_parse(raw: &'a str) -> Option<Self> {
71        if raw == "close" {
72            return Some(UiEvent::Close);
73        }
74        let (head, rest) = raw.split_once(':')?;
75        Some(match head {
76            "click" => xy(rest).map(|(x, y)| UiEvent::Click { x, y })?,
77            "drag" => xy(rest).map(|(x, y)| UiEvent::Drag { x, y })?,
78            "release" => xy(rest).map(|(x, y)| UiEvent::Release { x, y })?,
79            "move" => xy(rest).map(|(x, y)| UiEvent::Move { x, y })?,
80            "open" => xy(rest).map(|(w, h)| UiEvent::Open { w, h })?,
81            "scroll" => UiEvent::Scroll {
82                dy: rest.parse().ok()?,
83            },
84            "key" => {
85                let mut it = rest.split(':');
86                let key = it.next()?.parse().ok()?;
87                let mods = match it.next() {
88                    Some(m) => m.parse().ok()?,
89                    None => 0,
90                };
91                UiEvent::Key { key, mods }
92            }
93            "char" => UiEvent::Char(rest.parse::<u32>().ok().and_then(char::from_u32)?),
94            _ => return None,
95        })
96    }
97
98    /// The cursor position of a pointer event.
99    pub fn pos(&self) -> Option<(f32, f32)> {
100        match *self {
101            UiEvent::Click { x, y }
102            | UiEvent::Drag { x, y }
103            | UiEvent::Release { x, y }
104            | UiEvent::Move { x, y } => Some((x, y)),
105            _ => None,
106        }
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn parses_every_host_format() {
116        assert_eq!(
117            UiEvent::parse("click:1.5:2.0"),
118            UiEvent::Click { x: 1.5, y: 2.0 }
119        );
120        assert_eq!(UiEvent::parse("drag:3:4"), UiEvent::Drag { x: 3.0, y: 4.0 });
121        assert_eq!(
122            UiEvent::parse("release:3:4"),
123            UiEvent::Release { x: 3.0, y: 4.0 }
124        );
125        assert_eq!(
126            UiEvent::parse("move:3.0:4.5"),
127            UiEvent::Move { x: 3.0, y: 4.5 }
128        );
129        assert_eq!(UiEvent::parse("scroll:-2.00"), UiEvent::Scroll { dy: -2.0 });
130        assert_eq!(
131            UiEvent::parse("key:256:0"),
132            UiEvent::Key { key: 256, mods: 0 }
133        );
134        assert_eq!(UiEvent::parse("key:65"), UiEvent::Key { key: 65, mods: 0 });
135        assert_eq!(UiEvent::parse("char:1078"), UiEvent::Char('ж'));
136        assert_eq!(
137            UiEvent::parse("open:320:240"),
138            UiEvent::Open { w: 320.0, h: 240.0 }
139        );
140        assert_eq!(UiEvent::parse("close"), UiEvent::Close);
141    }
142
143    #[test]
144    fn garbage_is_other() {
145        for raw in [
146            "",
147            "click",
148            "click:x:1",
149            "click:1:2:3",
150            "scroll:",
151            "char:-1",
152            "done",
153            "key:a:0",
154        ] {
155            assert_eq!(UiEvent::parse(raw), UiEvent::Other(raw), "{raw}");
156        }
157    }
158}