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}