Skip to main content

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}