consortium_hmi_input/event.rs
1// Copyright 2026 Ethan Wu
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15// SPDX-License-Identifier: Apache-2.0
16
17//! The portable [`InputEvent`] type and its component enums.
18
19use consortium_ipc::IpcSafe;
20use serde::{Deserialize, Serialize};
21
22/// A semantic input event, independent of its hardware or OS source.
23///
24/// Positions are in **logical UI coordinates** — the same units an HMI app
25/// renders at — so the *source* owns device-to-logical calibration and every
26/// backend consumes one coordinate space. `i16` is deliberate: it spans any
27/// realistic panel while staying a fixed, `IpcSafe` width across cores
28/// (unlike `usize`).
29///
30/// The enum is `#[non_exhaustive]`; match with a wildcard arm so new event
31/// kinds can be added without a breaking change.
32#[derive(Clone, Copy, Debug, PartialEq, Eq, IpcSafe, Deserialize, Serialize)]
33#[non_exhaustive]
34pub enum InputEvent {
35 /// A digital button, D-pad direction, or key changed state.
36 Button {
37 /// Which logical control changed.
38 code: ButtonCode,
39 /// `true` on press, `false` on release.
40 pressed: bool,
41 },
42 /// Absolute pointer/cursor motion, in logical UI coordinates.
43 Pointer {
44 /// Logical X, clamped by the source to the UI width.
45 x: i16,
46 /// Logical Y, clamped by the source to the UI height.
47 y: i16,
48 },
49 /// A pointer (mouse) button changed state at the last known position.
50 PointerButton {
51 /// Which pointer button changed.
52 button: PointerButton,
53 /// `true` on press, `false` on release.
54 pressed: bool,
55 },
56 /// A single touch contact, in logical UI coordinates.
57 Touch {
58 /// Stable contact identifier for the lifetime of one touch.
59 id: u8,
60 /// Lifecycle phase of this contact.
61 phase: TouchPhase,
62 /// Logical X of the contact.
63 x: i16,
64 /// Logical Y of the contact.
65 y: i16,
66 },
67 /// A scroll/wheel tick along one axis, in logical detents.
68 Scroll {
69 /// Axis the scroll applies to.
70 axis: ScrollAxis,
71 /// Signed magnitude; sign follows the natural axis direction.
72 delta: i16,
73 },
74 /// A raw keyboard scancode, for text and navigation backends.
75 ///
76 /// The scancode space is backend-defined (e.g. USB HID usage IDs); this
77 /// crate does not enumerate keys, keeping the model small and stable.
78 Key {
79 /// Backend-defined scancode.
80 scancode: u16,
81 /// `true` on press, `false` on release.
82 pressed: bool,
83 },
84}
85
86/// A logical control on a gamepad-style input surface.
87///
88/// Face buttons use direction-neutral `South`/`East`/`West`/`North` names
89/// (the standard-gamepad convention) rather than any one vendor's glyphs; a
90/// backend maps them to its own button identity.
91#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IpcSafe, Deserialize, Serialize)]
92#[non_exhaustive]
93pub enum ButtonCode {
94 /// D-pad up.
95 DpadUp,
96 /// D-pad down.
97 DpadDown,
98 /// D-pad left.
99 DpadLeft,
100 /// D-pad right.
101 DpadRight,
102 /// Bottom face button.
103 South,
104 /// Right face button.
105 East,
106 /// Left face button.
107 West,
108 /// Top face button.
109 North,
110 /// Left shoulder / trigger.
111 L1,
112 /// Right shoulder / trigger.
113 R1,
114 /// Start / menu.
115 Start,
116 /// Select / back.
117 Select,
118}
119
120/// A pointer (mouse) button.
121#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IpcSafe, Deserialize, Serialize)]
122#[non_exhaustive]
123pub enum PointerButton {
124 /// Primary (usually left) button.
125 Left,
126 /// Secondary (usually right) button.
127 Right,
128 /// Middle / wheel button.
129 Middle,
130}
131
132/// Lifecycle phase of a touch contact.
133#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IpcSafe, Deserialize, Serialize)]
134#[non_exhaustive]
135pub enum TouchPhase {
136 /// Contact began.
137 Down,
138 /// Contact moved while held.
139 Move,
140 /// Contact lifted normally.
141 Up,
142 /// Contact aborted by the system (e.g. palm rejection).
143 Cancel,
144}
145
146/// Axis a [`InputEvent::Scroll`] applies to.
147#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IpcSafe, Deserialize, Serialize)]
148#[non_exhaustive]
149pub enum ScrollAxis {
150 /// Vertical scroll.
151 Vertical,
152 /// Horizontal scroll.
153 Horizontal,
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn events_round_trip_through_a_codec() {
162 // Proves the IPC path: an event serialized on one core decodes to an
163 // identical value on another. postcard is Consortium's default codec.
164 let events = [
165 InputEvent::Button {
166 code: ButtonCode::South,
167 pressed: true,
168 },
169 InputEvent::Pointer { x: -12, y: 300 },
170 InputEvent::PointerButton {
171 button: PointerButton::Right,
172 pressed: false,
173 },
174 InputEvent::Touch {
175 id: 2,
176 phase: TouchPhase::Move,
177 x: 120,
178 y: 64,
179 },
180 InputEvent::Scroll {
181 axis: ScrollAxis::Vertical,
182 delta: -3,
183 },
184 InputEvent::Key {
185 scancode: 0x2C,
186 pressed: true,
187 },
188 ];
189
190 let mut buf = [0u8; 32];
191 for event in events {
192 let encoded = postcard::to_slice(&event, &mut buf).expect("encode");
193 let decoded: InputEvent = postcard::from_bytes(encoded).expect("decode");
194 assert_eq!(event, decoded);
195 }
196 }
197}