kozan_platform/event.rs
1//! View event — the top-level message type sent to view threads.
2//!
3//! Like Chrome's separation of input events, lifecycle events, and rendering
4//! signals into different routing paths. `ViewEvent` is the single enum
5//! sent over the mpsc channel, but it's **hierarchical** — input events are
6//! nested under `ViewEvent::Input(InputEvent)`, not flattened.
7//!
8//! # Architecture
9//!
10//! ```text
11//! ViewEvent
12//! ├── Input(InputEvent) ← mouse, keyboard, wheel events
13//! │ ├── MouseMove(...)
14//! │ ├── MouseButton(...)
15//! │ ├── Wheel(...)
16//! │ └── Keyboard(...)
17//! ├── Lifecycle(LifecycleEvent) ← resize, focus, scale factor
18//! │ ├── Resized(...)
19//! │ ├── Focused(...)
20//! │ └── ScaleFactorChanged(...)
21//! ├── Paint ← rendering signal
22//! └── Shutdown ← clean exit
23//! ```
24//!
25//! # Why hierarchical?
26//!
27//! Chrome routes input events through `InputRouterImpl`, lifecycle events
28//! through the frame lifecycle, and paint signals through the compositor.
29//! Different routing paths, different handling. A flat enum forces one
30//! match arm per event — hierarchical lets the view thread match at the
31//! category level first, then delegate to specialized handlers.
32
33use kozan_core::InputEvent;
34use kozan_core::scroll::ScrollOffsets;
35
36/// Top-level event sent from the main thread to a view thread.
37///
38/// Sent over `mpsc::Sender<ViewEvent>` — the single channel per view.
39/// The view thread's event loop matches on the category, then delegates
40/// to specialized handlers.
41///
42/// Chrome equivalent: the dispatch in `WebFrameWidgetImpl::HandleInputEvent()`
43/// for input, and separate IPC messages for lifecycle.
44pub enum ViewEvent {
45 /// An input event (mouse, keyboard, wheel).
46 /// Routed to the view's `EventHandler` for hit testing and DOM dispatch.
47 Input(InputEvent),
48
49 /// A lifecycle event (resize, focus change, scale factor change).
50 /// Handled by the view's layout/rendering pipeline.
51 Lifecycle(LifecycleEvent),
52
53 /// Something changed — schedule a frame.
54 Paint,
55
56 /// Compositor posted updated scroll offsets after compositor-side scroll.
57 /// Chrome: `ProxyImpl::SetNeedsCommitOnImplThread()` posts scroll state back.
58 /// The view thread applies these before the next paint so positions match.
59 ScrollSync(ScrollOffsets),
60
61 /// Clean shutdown — the view thread should exit its event loop.
62 Shutdown,
63}
64
65/// Lifecycle events — window/view state changes.
66///
67/// These are NOT input events — they don't go through hit testing or
68/// DOM event dispatch. They're handled directly by the view's rendering
69/// pipeline (layout invalidation, viewport update, etc.).
70///
71/// Chrome equivalent: separate IPC messages like `WidgetMsg_Resize`,
72/// `WidgetMsg_SetFocus`, `WidgetMsg_UpdateScreenInfo`.
73#[derive(Debug, Clone, Copy)]
74pub enum LifecycleEvent {
75 /// The view's area was resized.
76 /// Triggers layout invalidation.
77 ///
78 /// Width and height in physical pixels.
79 Resized { width: u32, height: u32 },
80
81 /// The view gained or lost focus.
82 /// Triggers focus/blur DOM events and caret visibility.
83 Focused(bool),
84
85 /// The display changed (e.g., moved to a different monitor).
86 /// Triggers re-layout at the new DPI and updates frame budget.
87 ///
88 /// Chrome equivalent: `WidgetMsg_UpdateScreenInfo` with new
89 /// `device_scale_factor` + vsync interval.
90 ScaleFactorChanged {
91 scale_factor: f64,
92 /// Display refresh rate in millihertz (e.g., 144000 = 144Hz).
93 /// `None` = unknown → keep current budget.
94 refresh_rate_millihertz: Option<u32>,
95 },
96}
97
98#[cfg(test)]
99mod tests {
100 use kozan_core::{Modifiers, input::MouseMoveEvent};
101
102 use super::*;
103 use std::time::Instant;
104
105 #[test]
106 fn view_event_is_send() {
107 fn assert_send<T: Send>() {}
108 assert_send::<ViewEvent>();
109 }
110
111 #[test]
112 fn lifecycle_event_is_copy() {
113 fn assert_copy<T: Copy>() {}
114 assert_copy::<LifecycleEvent>();
115 }
116
117 #[test]
118 fn hierarchical_matching() {
119 let evt = ViewEvent::Input(InputEvent::MouseMove(MouseMoveEvent {
120 x: 10.0,
121 y: 20.0,
122 modifiers: Modifiers::EMPTY,
123 timestamp: Instant::now(),
124 }));
125
126 match evt {
127 ViewEvent::Input(InputEvent::MouseMove(m)) => {
128 assert!((m.x - 10.0).abs() < f64::EPSILON);
129 assert!((m.y - 20.0).abs() < f64::EPSILON);
130 }
131 _ => panic!("wrong variant"),
132 }
133 }
134
135 #[test]
136 fn lifecycle_resize() {
137 let evt = ViewEvent::Lifecycle(LifecycleEvent::Resized {
138 width: 1920,
139 height: 1080,
140 });
141 match evt {
142 ViewEvent::Lifecycle(LifecycleEvent::Resized { width, height }) => {
143 assert_eq!(width, 1920);
144 assert_eq!(height, 1080);
145 }
146 _ => panic!("wrong variant"),
147 }
148 }
149}