Skip to main content

aetna_core/
event.rs

1//! Event types and the [`App`] trait.
2//!
3//! State-driven rebuilds, routed events, keyboard input, and automatic
4//! hover/press/focus visuals. See `docs/LIBRARY_VISION.md` for the application
5//! model this fits into.
6//!
7//! This module owns the *types* — what the host's `App::on_event` sees
8//! and what gets registered as hotkeys. The state machine that produces
9//! these events lives in [`crate::state::UiState`]; the routing helpers
10//! live in [`mod@crate::hit_test`] and [`mod@crate::focus`].
11//!
12//! # The model
13//!
14//! ```ignore
15//! use aetna_core::prelude::*;
16//!
17//! struct Counter { value: i32 }
18//!
19//! impl App for Counter {
20//!     fn build(&self) -> El {
21//!         column([
22//!             h1(format!("{}", self.value)),
23//!             row([
24//!                 button("-").key("dec"),
25//!                 button("+").key("inc"),
26//!             ]),
27//!         ])
28//!     }
29//!     fn on_event(&mut self, e: UiEvent) {
30//!         if e.is_click_or_activate("inc") {
31//!             self.value += 1;
32//!         } else if e.is_click_or_activate("dec") {
33//!             self.value -= 1;
34//!         }
35//!     }
36//! }
37//! ```
38//!
39//! - **Identity** is `El::key`. Tag a node with `.key("...")` and it's
40//!   hit-testable (and gets automatic hover/press visuals).
41//! - **The build closure is pure.** It reads `&self`, returns a fresh
42//!   tree. The library tracks pointer state, hovered key, pressed key
43//!   internally and applies visual deltas after build but before layout
44//!   completes.
45//! - **Events flow back via `on_event`.** The library hit-tests pointer
46//!   events against the most-recently-laid-out tree and emits
47//!   [`UiEvent`]s when something is clicked. The host's `App::on_event`
48//!   updates state; the renderer reports whether animation state needs
49//!   another redraw.
50
51use crate::tree::{El, Rect};
52
53/// Hit-test target metadata. `key` is the author-facing route, while
54/// `node_id` is the stable laid-out tree path used by artifacts.
55#[derive(Clone, Debug, PartialEq)]
56#[non_exhaustive]
57pub struct UiTarget {
58    pub key: String,
59    pub node_id: String,
60    pub rect: Rect,
61}
62
63/// Which mouse button (or pointer button) generated a pointer event.
64/// The host backend translates its native button id to one of these
65/// before calling `pointer_down` / `pointer_up`.
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67pub enum PointerButton {
68    /// Left mouse, primary touch, or pen tip. Drives `Click`.
69    Primary,
70    /// Right mouse or two-finger touch. Drives `SecondaryClick` —
71    /// typically opens a context menu.
72    Secondary,
73    /// Middle mouse / scroll-wheel click. No library default; surfaced
74    /// as `MiddleClick` for apps that want it (autoscroll, paste-on-X).
75    Middle,
76}
77
78/// Keyboard key values normalized by the core library. This keeps the
79/// core independent from host/windowing crates while covering the
80/// navigation and activation keys the library owns.
81#[derive(Clone, Debug, PartialEq, Eq)]
82pub enum UiKey {
83    Enter,
84    Escape,
85    Tab,
86    Space,
87    ArrowUp,
88    ArrowDown,
89    ArrowLeft,
90    ArrowRight,
91    /// Backspace — deletes the grapheme before the caret.
92    Backspace,
93    /// Forward delete — deletes the grapheme after the caret.
94    Delete,
95    /// Home — caret to start of line.
96    Home,
97    /// End — caret to end of line.
98    End,
99    /// PageUp — coarse-step navigation (sliders adjust by a larger
100    /// amount; lists scroll a viewport).
101    PageUp,
102    /// PageDown — coarse-step navigation (sliders adjust by a larger
103    /// amount; lists scroll a viewport).
104    PageDown,
105    Character(String),
106    Other(String),
107}
108
109/// OS modifier-key mask. The four fields mirror the platform-standard
110/// modifier set; this struct is intentionally **not** `#[non_exhaustive]`
111/// so callers can use struct-literal syntax with `..Default::default()`
112/// to spell precise modifier combinations.
113#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
114pub struct KeyModifiers {
115    pub shift: bool,
116    pub ctrl: bool,
117    pub alt: bool,
118    pub logo: bool,
119}
120
121#[derive(Clone, Debug, PartialEq, Eq)]
122#[non_exhaustive]
123pub struct KeyPress {
124    pub key: UiKey,
125    pub modifiers: KeyModifiers,
126    pub repeat: bool,
127}
128
129/// A keyboard chord for app-level hotkey registration. Match a key with
130/// an exact modifier mask: `KeyChord::ctrl('f')` does not also match
131/// `Ctrl+Shift+F`, and `KeyChord::vim('j')` does not match if any
132/// modifier is held.
133///
134/// Register chords from [`App::hotkeys`]; the library matches them
135/// against incoming key presses ahead of focus activation routing and
136/// emits a [`UiEvent`] with `kind = UiEventKind::Hotkey` and `key`
137/// equal to the registered name.
138#[derive(Clone, Debug, PartialEq, Eq)]
139#[non_exhaustive]
140pub struct KeyChord {
141    pub key: UiKey,
142    pub modifiers: KeyModifiers,
143}
144
145impl KeyChord {
146    /// A bare key with no modifiers (vim-style). `KeyChord::vim('j')`
147    /// matches the `j` key with no Ctrl/Shift/Alt/Logo held.
148    pub fn vim(c: char) -> Self {
149        Self {
150            key: UiKey::Character(c.to_string()),
151            modifiers: KeyModifiers::default(),
152        }
153    }
154
155    /// `Ctrl+<char>`.
156    pub fn ctrl(c: char) -> Self {
157        Self {
158            key: UiKey::Character(c.to_string()),
159            modifiers: KeyModifiers {
160                ctrl: true,
161                ..Default::default()
162            },
163        }
164    }
165
166    /// `Ctrl+Shift+<char>`.
167    pub fn ctrl_shift(c: char) -> Self {
168        Self {
169            key: UiKey::Character(c.to_string()),
170            modifiers: KeyModifiers {
171                ctrl: true,
172                shift: true,
173                ..Default::default()
174            },
175        }
176    }
177
178    /// A named key with no modifiers (e.g. `KeyChord::named(UiKey::Escape)`).
179    pub fn named(key: UiKey) -> Self {
180        Self {
181            key,
182            modifiers: KeyModifiers::default(),
183        }
184    }
185
186    pub fn with_modifiers(mut self, modifiers: KeyModifiers) -> Self {
187        self.modifiers = modifiers;
188        self
189    }
190
191    /// Strict match: keys equal AND modifier mask is identical. Holding
192    /// extra modifiers does not match a chord that didn't request them.
193    pub fn matches(&self, key: &UiKey, modifiers: KeyModifiers) -> bool {
194        key_eq(&self.key, key) && self.modifiers == modifiers
195    }
196}
197
198fn key_eq(a: &UiKey, b: &UiKey) -> bool {
199    match (a, b) {
200        (UiKey::Character(x), UiKey::Character(y)) => x.eq_ignore_ascii_case(y),
201        _ => a == b,
202    }
203}
204
205/// User-facing event. The host's [`App::on_event`] receives one of these
206/// per discrete user action.
207///
208/// Most apps should not destructure every field. Prefer the convenience
209/// methods on this type for common routes:
210///
211/// ```
212/// # use aetna_core::prelude::*;
213/// # struct Counter { value: i32 }
214/// # impl App for Counter {
215/// # fn build(&self) -> El { button("+").key("inc") }
216/// fn on_event(&mut self, event: UiEvent) {
217///     if event.is_click_or_activate("inc") {
218///         self.value += 1;
219///     }
220/// }
221/// # }
222/// ```
223#[derive(Clone, Debug)]
224#[non_exhaustive]
225pub struct UiEvent {
226    /// Route string for this event.
227    ///
228    /// For pointer and focus events, this is the [`El::key`][crate::El::key]
229    /// of the target node. For [`UiEventKind::Hotkey`], this is the
230    /// action name returned from [`App::hotkeys`]. For window-level
231    /// keyboard events such as Escape with no focused target, this is
232    /// `None`.
233    ///
234    /// Prefer [`Self::route`] or [`Self::is_click_or_activate`] in app
235    /// code. The field remains public for direct pattern matching.
236    pub key: Option<String>,
237    /// Full hit-test target for events routed to a concrete element.
238    pub target: Option<UiTarget>,
239    /// Pointer position in logical pixels when the event was emitted.
240    pub pointer: Option<(f32, f32)>,
241    /// Keyboard payload for key events.
242    pub key_press: Option<KeyPress>,
243    /// Composed text payload for [`UiEventKind::TextInput`] events.
244    pub text: Option<String>,
245    /// Modifier mask captured at the moment this event was emitted. For
246    /// keyboard events this duplicates `key_press.modifiers`; for
247    /// pointer events it's the host-tracked modifier state at the time
248    /// of the click / drag (used by widgets like text_input that need
249    /// to detect Shift+click for "extend selection").
250    pub modifiers: KeyModifiers,
251    pub kind: UiEventKind,
252}
253
254impl UiEvent {
255    /// Synthesize a click event for the given route key.
256    ///
257    /// Intended for tests, headless automation, and snapshot
258    /// fixtures that drive UI logic without a real pointer history.
259    /// All optional fields default to `None`; modifiers are empty.
260    pub fn synthetic_click(key: impl Into<String>) -> Self {
261        Self {
262            kind: UiEventKind::Click,
263            key: Some(key.into()),
264            target: None,
265            pointer: None,
266            key_press: None,
267            text: None,
268            modifiers: KeyModifiers::default(),
269        }
270    }
271
272    /// Route string for this event, if any.
273    ///
274    /// For pointer/focus events this is the target element key. For
275    /// hotkeys this is the registered action name.
276    pub fn route(&self) -> Option<&str> {
277        self.key.as_deref()
278    }
279
280    /// Target element key, if this event was routed to an element.
281    ///
282    /// Unlike [`Self::route`], this returns `None` for app-level
283    /// hotkey actions because those do not have a concrete element
284    /// target.
285    pub fn target_key(&self) -> Option<&str> {
286        self.target.as_ref().map(|t| t.key.as_str())
287    }
288
289    /// True when this event's route equals `key`.
290    pub fn is_route(&self, key: &str) -> bool {
291        self.route() == Some(key)
292    }
293
294    /// True for a primary click or keyboard activation on `key`.
295    ///
296    /// This is the most common button/menu route in app code.
297    pub fn is_click_or_activate(&self, key: &str) -> bool {
298        matches!(self.kind, UiEventKind::Click | UiEventKind::Activate) && self.is_route(key)
299    }
300
301    /// True for a registered hotkey action name.
302    pub fn is_hotkey(&self, action: &str) -> bool {
303        self.kind == UiEventKind::Hotkey && self.is_route(action)
304    }
305
306    /// Pointer position in logical pixels, if this event carries one.
307    pub fn pointer_pos(&self) -> Option<(f32, f32)> {
308        self.pointer
309    }
310
311    /// Pointer x coordinate in logical pixels, if this event carries one.
312    pub fn pointer_x(&self) -> Option<f32> {
313        self.pointer.map(|(x, _)| x)
314    }
315
316    /// Pointer y coordinate in logical pixels, if this event carries one.
317    pub fn pointer_y(&self) -> Option<f32> {
318        self.pointer.map(|(_, y)| y)
319    }
320
321    /// Rectangle of the routed target from the last layout pass.
322    pub fn target_rect(&self) -> Option<Rect> {
323        self.target.as_ref().map(|t| t.rect)
324    }
325
326    /// OS-composed text payload for [`UiEventKind::TextInput`].
327    pub fn text(&self) -> Option<&str> {
328        self.text.as_deref()
329    }
330}
331
332/// What kind of event happened.
333///
334/// This enum is non-exhaustive so Aetna can add new input events
335/// without breaking downstream apps. Match the variants you handle and
336/// include a wildcard arm for everything else.
337#[derive(Clone, Copy, Debug, PartialEq, Eq)]
338#[non_exhaustive]
339pub enum UiEventKind {
340    /// Primary-button pointer down + up landed on the same node.
341    Click,
342    /// Secondary-button (right-click) pointer down + up landed on the
343    /// same node. Used for context menus.
344    SecondaryClick,
345    /// Middle-button pointer down + up landed on the same node.
346    MiddleClick,
347    /// Focused element was activated by keyboard (Enter/Space).
348    Activate,
349    /// Escape was pressed. Routed to the focused element when present,
350    /// otherwise emitted as a window-level event.
351    Escape,
352    /// A registered hotkey chord matched. `event.key` is the registered
353    /// name (the second element of the `(KeyChord, String)` pair).
354    Hotkey,
355    /// Other keyboard input.
356    KeyDown,
357    /// Composed text input — printable characters from the OS, after
358    /// dead-key composition / IME / shift mapping. Routed to the
359    /// focused element. Distinct from `KeyDown(Character(_))`: the
360    /// latter is the raw key event used for shortcuts and navigation;
361    /// `TextInput` is the grapheme stream a text field should consume.
362    TextInput,
363    /// Pointer moved while the primary button was held down. Routed
364    /// to the originally pressed target so a widget can extend a
365    /// selection / scrub a slider / move a draggable. `event.pointer`
366    /// carries the current logical-pixel position; `event.target` is
367    /// the node where the drag began.
368    Drag,
369    /// Primary pointer button released. Fires regardless of whether
370    /// the up landed on the same node as the down — paired with
371    /// `Click` (which only fires on a same-node match), this lets
372    /// drag-aware widgets always observe drag-end.
373    /// `event.target` is the originally pressed node;
374    /// `event.pointer` is the up position.
375    PointerUp,
376    /// Primary pointer button pressed on a hit-test target. Routed
377    /// before the eventual `Click` (which fires on up-on-same-target).
378    /// Used by widgets like text_input that need to react at
379    /// down-time — e.g., to set the selection anchor before any drag
380    /// extends it. `event.target` is the down-target,
381    /// `event.pointer` is the down position, and `event.modifiers`
382    /// carries the modifier mask (Shift+click for extend-selection).
383    PointerDown,
384}
385
386/// The application contract. Implement this on your state struct and
387/// pass it to a host runner (e.g., `aetna_winit_wgpu::run`).
388pub trait App {
389    /// Refresh app-owned external state immediately before a frame is
390    /// built.
391    ///
392    /// Hosts call this once per redraw before [`Self::build`]. Use it
393    /// for polling an external source, reconciling optimistic local
394    /// state with a backend snapshot, or advancing host-owned live data
395    /// that should be visible in the next tree. Keep expensive work
396    /// outside the render loop; this hook is still on the frame path.
397    ///
398    /// Default: no-op.
399    fn before_build(&mut self) {}
400
401    /// Project current state into a scene tree. Called whenever the
402    /// host requests a redraw, after [`Self::before_build`]. Prefer to
403    /// keep this pure: read current state and return a fresh tree.
404    fn build(&self) -> El;
405
406    /// Update state in response to a routed event. Default: no-op.
407    fn on_event(&mut self, _event: UiEvent) {}
408
409    /// App-level hotkey registry. The library matches incoming key
410    /// presses against this list before its own focus-activation
411    /// routing; a match emits a [`UiEvent`] with `kind =
412    /// UiEventKind::Hotkey` and `key = Some(name)`.
413    ///
414    /// Called once per build cycle; the host runner snapshots the list
415    /// alongside `build()` so the chords stay in sync with state.
416    /// Default: no hotkeys.
417    fn hotkeys(&self) -> Vec<(KeyChord, String)> {
418        Vec::new()
419    }
420
421    /// Custom shaders this app needs registered. Each tuple is
422    /// `(name, wgsl_source, samples_backdrop)`. The host runner
423    /// registers them once at startup via
424    /// `Runner::register_shader_with(name, wgsl, samples_backdrop)`.
425    ///
426    /// Backends that don't support backdrop sampling skip entries with
427    /// `samples_backdrop=true`; any node bound to such a shader will
428    /// draw nothing on those backends rather than mis-render.
429    ///
430    /// Default: no shaders.
431    fn shaders(&self) -> Vec<AppShader> {
432        Vec::new()
433    }
434
435    /// Runtime paint theme for this app. Hosts apply it to the renderer
436    /// before preparing each frame so stateful apps can switch global
437    /// material routing without backend-specific calls.
438    fn theme(&self) -> crate::Theme {
439        crate::Theme::default()
440    }
441}
442
443/// One custom shader registration, returned from [`App::shaders`].
444#[derive(Clone, Copy, Debug)]
445pub struct AppShader {
446    pub name: &'static str,
447    pub wgsl: &'static str,
448    pub samples_backdrop: bool,
449}