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    /// On a node that names `scroll_mods`: the modifiers held when the
120    /// gesture began.
121    pub mods: Option<KeyMods>,
122}
123
124/// Which half of a hover an event reports.
125#[derive(Clone, Copy, Debug, PartialEq, Eq)]
126pub enum HoverPhase {
127    Enter,
128    Leave,
129}
130
131/// What moved to change the hover.
132#[derive(Clone, Copy, Debug, PartialEq, Eq)]
133pub enum HoverBy {
134    /// The pointer moved, or left the window.
135    Pointer,
136    /// The pointer stayed and what is under it changed: a list scrolled
137    /// by the wheel or the keys, a row that grew, a float that opened. A
138    /// picker that follows the pointer ignores these.
139    Content,
140}
141
142/// A `{kind:"hover"}` event: an `onHover` node entered or left.
143#[derive(Clone, Copy, Debug, PartialEq, Eq)]
144pub struct Hover {
145    pub phase: HoverPhase,
146    pub by: HoverBy,
147}
148
149/// A `{kind:"layout"}` event: an `onLayout` node's placed rect.
150#[derive(Clone, Copy, Debug, PartialEq)]
151pub struct Layout {
152    pub rect: Rect,
153    pub parent: Rect,
154    pub scale: f32,
155}
156
157/// A `{kind:"text"}` event: what a focused key sink or editor was given
158/// to insert.
159#[derive(Clone, Copy, Debug, PartialEq)]
160pub struct TextInput<'a> {
161    pub text: &'a str,
162    /// The clipboard's answer to a paste the app asked for, not typing.
163    pub pasted: bool,
164    /// Typed under secure keyboard entry: keep it out of logs and history.
165    pub concealed: bool,
166    /// A clipboard manager or a password tool marked it as not for keeping.
167    pub transient: bool,
168}
169
170fn point(p: &Value) -> Option<Vec2> {
171    Some(Vec2::new(p.get_f32("x")?, p.get_f32("y")?))
172}
173
174fn rect(p: &Value) -> Option<Rect> {
175    Some(Rect::new(
176        p.get_f32("x")?,
177        p.get_f32("y")?,
178        p.get_f32("w")?,
179        p.get_f32("h")?,
180    ))
181}
182
183fn small(p: &Value, key: &str) -> Option<u32> {
184    p.get_int(key).and_then(|v| u32::try_from(v).ok())
185}
186
187impl UiEvent {
188    /// The app's tag inside a core event's payload — `onDrag`'s, `onKey`'s
189    /// — as sent; [`UiEvent::message`] reads it typed.
190    pub fn tag(&self) -> Option<&Value> {
191        self.payload.get("tag")
192    }
193
194    /// This event as a drag, if it is one.
195    pub fn drag(&self) -> Option<Drag> {
196        let p = &self.payload;
197        if self.kind()? != "drag" {
198            return None;
199        }
200        let phase = match p.get_str("phase")? {
201            "start" => DragPhase::Start,
202            "move" => DragPhase::Move,
203            "end" => DragPhase::End,
204            _ => return None,
205        };
206        let cell = p
207            .get("cell")
208            .and_then(|c| Some((small(c, "row")?, small(c, "col")?)));
209        Some(Drag {
210            phase,
211            pos: point(p)?,
212            delta: Vec2::new(p.get_f32("dx")?, p.get_f32("dy")?),
213            parent: p.get("parent").and_then(rect).unwrap_or_default(),
214            cell,
215            line: small(p, "line"),
216            byte: p.get_int("byte").and_then(|v| usize::try_from(v).ok()),
217            clicks: small(p, "clicks"),
218        })
219    }
220
221    /// This event as a held non-primary button's press, move or release,
222    /// if it is one.
223    pub fn button(&self) -> Option<ButtonEvent> {
224        let p = &self.payload;
225        if self.kind()? != "button" {
226            return None;
227        }
228        let phase = match p.get_str("phase")? {
229            "press" => ButtonPhase::Press,
230            "move" => ButtonPhase::Move,
231            "release" => ButtonPhase::Release,
232            _ => return None,
233        };
234        let button = match p.get("button")? {
235            Value::Str(name) => MouseButton::from_name(name)?,
236            Value::Int(code) => MouseButton::from_code(u32::try_from(*code).ok()?),
237            _ => return None,
238        };
239        let cell = p
240            .get("cell")
241            .and_then(|c| Some((small(c, "row")?, small(c, "col")?)));
242        Some(ButtonEvent {
243            phase,
244            button,
245            pos: point(p)?,
246            clicks: small(p, "clicks"),
247            cell,
248            line: small(p, "line"),
249            byte: p.get_int("byte").and_then(|v| usize::try_from(v).ok()),
250        })
251    }
252
253    /// This event as a key press or release on a key sink, if it is one
254    /// (`key_press`, since `key` is the node the event is about):
255    /// the [`KeyPress`] the core built it from, so a keymap binds the same
256    /// value a headless `press` sends.
257    pub fn key_press(&self) -> Option<(KeyPhase, KeyPress)> {
258        let p = &self.payload;
259        if self.kind()? != "key" {
260            return None;
261        }
262        let phase = match p.get_str("phase")? {
263            "down" => KeyPhase::Down,
264            "up" => KeyPhase::Up,
265            _ => return None,
266        };
267        let code = KeyCode::from_name(p.get_str("code")?)?;
268        let physical = p
269            .get_str("physical")
270            .and_then(KeyCode::from_name)
271            .unwrap_or(KeyCode::Unknown);
272        let press = KeyPress {
273            code,
274            physical,
275            mods: mods(p),
276            text: p.get_str("text").map(str::to_string),
277            repeat: p.get_bool("repeat").unwrap_or(false),
278            location: p
279                .get_str("location")
280                .and_then(crate::input::KeyLocation::from_name)
281                .unwrap_or_default(),
282            locks: crate::input::KeyLocks {
283                caps: p.get_bool("caps_lock").unwrap_or(false),
284                num: p.get_bool("num_lock").unwrap_or(false),
285            },
286        };
287        Some((phase, press))
288    }
289
290    /// This event as inserted text, if it is some.
291    pub fn text(&self) -> Option<TextInput<'_>> {
292        let p = &self.payload;
293        if self.kind()? != "text" {
294            return None;
295        }
296        Some(TextInput {
297            text: p.get_str("text")?,
298            pasted: p.get_bool("pasted").unwrap_or(false),
299            concealed: p.get_bool("concealed").unwrap_or(false),
300            transient: p.get_bool("transient").unwrap_or(false),
301        })
302    }
303
304    /// This event as a wheel scroll, if it is one.
305    pub fn scroll(&self) -> Option<Scroll> {
306        let p = &self.payload;
307        if self.kind()? != "scroll" {
308            return None;
309        }
310        Some(Scroll {
311            pos: point(p)?,
312            delta: Vec2::new(p.get_f32("dx")?, p.get_f32("dy")?),
313            lines: p.get_int("lines"),
314            mods: p.get("mods").map(mods),
315        })
316    }
317
318    /// This event as a hover edge, if it is one.
319    pub fn hover(&self) -> Option<Hover> {
320        if self.kind()? != "hover" {
321            return None;
322        }
323        let phase = match self.payload.get_str("phase")? {
324            "enter" => HoverPhase::Enter,
325            "leave" => HoverPhase::Leave,
326            _ => return None,
327        };
328        let by = match self.payload.get_str("by") {
329            Some("content") => HoverBy::Content,
330            _ => HoverBy::Pointer,
331        };
332        Some(Hover { phase, by })
333    }
334
335    /// This event as a modifier change, if it is one: the keys now held.
336    pub fn modifiers(&self) -> Option<KeyMods> {
337        if self.kind()? != "modifiers" {
338            return None;
339        }
340        Some(mods(&self.payload))
341    }
342
343    /// This event as an `onLayout` report, if it is one.
344    pub fn layout(&self) -> Option<Layout> {
345        let p = &self.payload;
346        if self.kind()? != "layout" {
347            return None;
348        }
349        Some(Layout {
350            rect: rect(p)?,
351            parent: p.get("parent").and_then(rect).unwrap_or_default(),
352            scale: p.get_f32("scale").unwrap_or(1.0),
353        })
354    }
355}
356
357/// The four modifier flags a `key` and a `modifiers` payload both carry.
358fn mods(p: &Value) -> KeyMods {
359    let on = |k| p.get_bool(k).unwrap_or(false);
360    KeyMods {
361        shift: on("shift"),
362        ctrl: on("ctrl"),
363        alt: on("alt"),
364        super_key: on("super"),
365    }
366}