Skip to main content

kui_core/
event.rs

1//! Typed readings of the core's own event payloads: drags, buttons,
2//! scrolls, hovers, layouts, key presses and text input.
3//!
4//! A payload is a [`Value`] because it crosses into Lua, Node and C in one
5//! shape. Rather than reading it back field by field (`get_f32("x")`,
6//! `get_str("phase")`), a Rust handler asks for the view it wants; each
7//! view is `None` for an event of another kind.
8//!
9//! ```rust
10//! use kui_core::{Key, OriginId, UiEvent, Value};
11//! use kui_core::event::DragPhase;
12//!
13//! // What an `on_drag` node emits as the pointer moves.
14//! let ev = UiEvent::on(OriginId::HOST, Key::ROOT, Value::map([
15//!     ("kind", Value::str("drag")),
16//!     ("phase", Value::str("move")),
17//!     ("x", Value::float(40.0)), ("y", Value::float(12.0)),
18//!     ("dx", Value::float(8.0)), ("dy", Value::float(0.0)),
19//!     ("tag", Value::str("splitter")),
20//! ]));
21//!
22//! let d = ev.drag().expect("a drag");
23//! assert_eq!(d.phase, DragPhase::Move);
24//! assert_eq!(d.delta.x, 8.0);
25//! assert_eq!(ev.tag().and_then(Value::as_str), Some("splitter"));
26//! assert!(ev.scroll().is_none()); // another kind reads as None
27//! ```
28//!
29//! The app's own tag is still [`UiEvent::message`] or [`UiEvent::tag`];
30//! these are the fields the core adds around it. Nothing here changes what
31//! an event carries.
32
33use crate::geom::{Rect, Vec2};
34use crate::input::{KeyCode, KeyMods, KeyPhase, KeyPress, MouseButton, UiEvent};
35use crate::value::Value;
36
37/// Which part of a drag an event reports.
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum DragPhase {
40    Start,
41    Move,
42    End,
43}
44
45/// A `{kind:"drag"}` event: an `onDrag` node's pointer capture.
46#[derive(Clone, Copy, Debug, PartialEq)]
47pub struct Drag {
48    pub phase: DragPhase,
49    /// Where the pointer is.
50    pub pos: Vec2,
51    /// How far it is from where it pressed — in every phase, so a value
52    /// is `start + delta`, never a sum.
53    pub delta: Vec2,
54    /// The dragged node's parent's rect, for fractions without a query.
55    pub parent: Rect,
56    /// On a `cells` grid: the `(row, col)` under the pointer.
57    pub cell: Option<(u32, u32)>,
58    /// Inside an `onKey` sink that draws `role="line"` rows: the line, the
59    /// byte in its text, and the press's click count.
60    pub line: Option<u32>,
61    pub byte: Option<usize>,
62    pub clicks: Option<u32>,
63}
64
65impl Drag {
66    /// The pointer's place across the parent, 0 at its left or top edge
67    /// and 1 at its right or bottom, clamped: a divider's split, a
68    /// slider's value. 0 on an axis the parent has no size on.
69    pub fn ratio(&self) -> Vec2 {
70        let along = |p: f32, at: f32, len: f32| {
71            if len > 0.0 {
72                ((p - at) / len).clamp(0.0, 1.0)
73            } else {
74                0.0
75            }
76        };
77        Vec2::new(
78            along(self.pos.x, self.parent.x, self.parent.w),
79            along(self.pos.y, self.parent.y, self.parent.h),
80        )
81    }
82}
83
84/// Which part of a held non-primary button an event reports.
85#[derive(Clone, Copy, Debug, PartialEq, Eq)]
86pub enum ButtonPhase {
87    Press,
88    Move,
89    Release,
90}
91
92/// A `{kind:"button"}` event: a non-primary button an `onButton` node
93/// claimed, captured by it from press to release.
94#[derive(Clone, Copy, Debug, PartialEq)]
95pub struct ButtonEvent {
96    pub phase: ButtonPhase,
97    pub button: MouseButton,
98    /// Where the pointer is.
99    pub pos: Vec2,
100    /// The driver's click count, on the press only.
101    pub clicks: Option<u32>,
102    /// On a `cells` grid: the `(row, col)` under the pointer, clamped to
103    /// the grid.
104    pub cell: Option<(u32, u32)>,
105    /// Inside an `onKey` sink that draws `role="line"` rows: the line and
106    /// the byte in its text, as a drag carries them.
107    pub line: Option<u32>,
108    pub byte: Option<usize>,
109}
110
111/// A `{kind:"scroll"}` event: the wheel over an `onScroll` node.
112#[derive(Clone, Copy, Debug, PartialEq)]
113pub struct Scroll {
114    pub pos: Vec2,
115    pub delta: Vec2,
116    /// On a `cells` grid: whole rows the wheel moved, the remainder
117    /// carried to the next event; later history is negative.
118    pub lines: Option<i64>,
119}
120
121/// Which half of a hover an event reports.
122#[derive(Clone, Copy, Debug, PartialEq, Eq)]
123pub enum HoverPhase {
124    Enter,
125    Leave,
126}
127
128/// What moved to change the hover.
129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
130pub enum HoverBy {
131    /// The pointer moved, or left the window.
132    Pointer,
133    /// The pointer stayed and what is under it changed: a list scrolled
134    /// by the wheel or the keys, a row that grew, a float that opened. A
135    /// picker that follows the pointer ignores these.
136    Content,
137}
138
139/// A `{kind:"hover"}` event: an `onHover` node entered or left.
140#[derive(Clone, Copy, Debug, PartialEq, Eq)]
141pub struct Hover {
142    pub phase: HoverPhase,
143    pub by: HoverBy,
144}
145
146/// A `{kind:"layout"}` event: an `onLayout` node's placed rect.
147#[derive(Clone, Copy, Debug, PartialEq)]
148pub struct Layout {
149    pub rect: Rect,
150    pub parent: Rect,
151    pub scale: f32,
152}
153
154/// A `{kind:"text"}` event: what a focused key sink or editor was given
155/// to insert.
156#[derive(Clone, Copy, Debug, PartialEq)]
157pub struct TextInput<'a> {
158    pub text: &'a str,
159    /// The clipboard's answer to a paste the app asked for, not typing.
160    pub pasted: bool,
161    /// Typed under secure keyboard entry: keep it out of logs and history.
162    pub concealed: bool,
163    /// A clipboard manager or a password tool marked it as not for keeping.
164    pub transient: bool,
165}
166
167fn point(p: &Value) -> Option<Vec2> {
168    Some(Vec2::new(p.get_f32("x")?, p.get_f32("y")?))
169}
170
171fn rect(p: &Value) -> Option<Rect> {
172    Some(Rect::new(
173        p.get_f32("x")?,
174        p.get_f32("y")?,
175        p.get_f32("w")?,
176        p.get_f32("h")?,
177    ))
178}
179
180fn small(p: &Value, key: &str) -> Option<u32> {
181    p.get_int(key).and_then(|v| u32::try_from(v).ok())
182}
183
184impl UiEvent {
185    /// The app's tag inside a core event's payload — `onDrag`'s, `onKey`'s
186    /// — as sent; [`UiEvent::message`] reads it typed.
187    pub fn tag(&self) -> Option<&Value> {
188        self.payload.get("tag")
189    }
190
191    /// This event as a drag, if it is one.
192    pub fn drag(&self) -> Option<Drag> {
193        let p = &self.payload;
194        if self.kind()? != "drag" {
195            return None;
196        }
197        let phase = match p.get_str("phase")? {
198            "start" => DragPhase::Start,
199            "move" => DragPhase::Move,
200            "end" => DragPhase::End,
201            _ => return None,
202        };
203        let cell = p
204            .get("cell")
205            .and_then(|c| Some((small(c, "row")?, small(c, "col")?)));
206        Some(Drag {
207            phase,
208            pos: point(p)?,
209            delta: Vec2::new(p.get_f32("dx")?, p.get_f32("dy")?),
210            parent: p.get("parent").and_then(rect).unwrap_or_default(),
211            cell,
212            line: small(p, "line"),
213            byte: p.get_int("byte").and_then(|v| usize::try_from(v).ok()),
214            clicks: small(p, "clicks"),
215        })
216    }
217
218    /// This event as a held non-primary button's press, move or release,
219    /// if it is one.
220    pub fn button(&self) -> Option<ButtonEvent> {
221        let p = &self.payload;
222        if self.kind()? != "button" {
223            return None;
224        }
225        let phase = match p.get_str("phase")? {
226            "press" => ButtonPhase::Press,
227            "move" => ButtonPhase::Move,
228            "release" => ButtonPhase::Release,
229            _ => return None,
230        };
231        let button = match p.get("button")? {
232            Value::Str(name) => MouseButton::from_name(name)?,
233            Value::Int(code) => MouseButton::from_code(u32::try_from(*code).ok()?),
234            _ => return None,
235        };
236        let cell = p
237            .get("cell")
238            .and_then(|c| Some((small(c, "row")?, small(c, "col")?)));
239        Some(ButtonEvent {
240            phase,
241            button,
242            pos: point(p)?,
243            clicks: small(p, "clicks"),
244            cell,
245            line: small(p, "line"),
246            byte: p.get_int("byte").and_then(|v| usize::try_from(v).ok()),
247        })
248    }
249
250    /// This event as a key press or release on a key sink, if it is one
251    /// (`key_press`, since `key` is the node the event is about):
252    /// the [`KeyPress`] the core built it from, so a keymap binds the same
253    /// value a headless `press` sends.
254    pub fn key_press(&self) -> Option<(KeyPhase, KeyPress)> {
255        let p = &self.payload;
256        if self.kind()? != "key" {
257            return None;
258        }
259        let phase = match p.get_str("phase")? {
260            "down" => KeyPhase::Down,
261            "up" => KeyPhase::Up,
262            _ => return None,
263        };
264        let code = KeyCode::from_name(p.get_str("code")?)?;
265        let physical = p
266            .get_str("physical")
267            .and_then(KeyCode::from_name)
268            .unwrap_or(KeyCode::Unknown);
269        let press = KeyPress {
270            code,
271            physical,
272            mods: mods(p),
273            text: p.get_str("text").map(str::to_string),
274            repeat: p.get_bool("repeat").unwrap_or(false),
275            location: p
276                .get_str("location")
277                .and_then(crate::input::KeyLocation::from_name)
278                .unwrap_or_default(),
279            locks: crate::input::KeyLocks {
280                caps: p.get_bool("caps_lock").unwrap_or(false),
281                num: p.get_bool("num_lock").unwrap_or(false),
282            },
283        };
284        Some((phase, press))
285    }
286
287    /// This event as inserted text, if it is some.
288    pub fn text(&self) -> Option<TextInput<'_>> {
289        let p = &self.payload;
290        if self.kind()? != "text" {
291            return None;
292        }
293        Some(TextInput {
294            text: p.get_str("text")?,
295            pasted: p.get_bool("pasted").unwrap_or(false),
296            concealed: p.get_bool("concealed").unwrap_or(false),
297            transient: p.get_bool("transient").unwrap_or(false),
298        })
299    }
300
301    /// This event as a wheel scroll, if it is one.
302    pub fn scroll(&self) -> Option<Scroll> {
303        let p = &self.payload;
304        if self.kind()? != "scroll" {
305            return None;
306        }
307        Some(Scroll {
308            pos: point(p)?,
309            delta: Vec2::new(p.get_f32("dx")?, p.get_f32("dy")?),
310            lines: p.get_int("lines"),
311        })
312    }
313
314    /// This event as a hover edge, if it is one.
315    pub fn hover(&self) -> Option<Hover> {
316        if self.kind()? != "hover" {
317            return None;
318        }
319        let phase = match self.payload.get_str("phase")? {
320            "enter" => HoverPhase::Enter,
321            "leave" => HoverPhase::Leave,
322            _ => return None,
323        };
324        let by = match self.payload.get_str("by") {
325            Some("content") => HoverBy::Content,
326            _ => HoverBy::Pointer,
327        };
328        Some(Hover { phase, by })
329    }
330
331    /// This event as a modifier change, if it is one: the keys now held.
332    pub fn modifiers(&self) -> Option<KeyMods> {
333        if self.kind()? != "modifiers" {
334            return None;
335        }
336        Some(mods(&self.payload))
337    }
338
339    /// This event as an `onLayout` report, if it is one.
340    pub fn layout(&self) -> Option<Layout> {
341        let p = &self.payload;
342        if self.kind()? != "layout" {
343            return None;
344        }
345        Some(Layout {
346            rect: rect(p)?,
347            parent: p.get("parent").and_then(rect).unwrap_or_default(),
348            scale: p.get_f32("scale").unwrap_or(1.0),
349        })
350    }
351}
352
353/// The four modifier flags a `key` and a `modifiers` payload both carry.
354fn mods(p: &Value) -> KeyMods {
355    let on = |k| p.get_bool(k).unwrap_or(false);
356    KeyMods {
357        shift: on("shift"),
358        ctrl: on("ctrl"),
359        alt: on("alt"),
360        super_key: on("super"),
361    }
362}