Skip to main content

incular_core/
input.rs

1//! Platform-neutral input values.
2
3use crate::{Offset, Size};
4
5/// Pressure normalized to the closed interval `[0, 1]`.
6///
7/// Native adapters normalize once at their boundary. Values outside a native
8/// device's documented range are clamped, while non-finite values and invalid
9/// ranges remain unavailable instead of entering retained input state.
10#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
11pub struct NormalizedPressure(f32);
12
13impl NormalizedPressure {
14    #[must_use]
15    pub fn new(value: f64) -> Option<Self> {
16        value
17            .is_finite()
18            .then(|| Self(value.clamp(0.0, 1.0) as f32))
19    }
20
21    #[must_use]
22    pub fn from_range(value: f64, maximum: f64) -> Option<Self> {
23        (value.is_finite() && maximum.is_finite() && maximum > 0.0)
24            .then(|| Self((value / maximum).clamp(0.0, 1.0) as f32))
25    }
26
27    #[must_use]
28    pub const fn get(self) -> f32 {
29        self.0
30    }
31}
32
33/// Portable stylus orientation derived from native pen tilt.
34///
35/// `altitude` is radians above the tablet plane: `0` is parallel and `PI/2`
36/// is perpendicular. `azimuth` is radians in `[0, TAU)`, clockwise from the
37/// positive logical X axis in Incular's screen coordinate system (+Y down).
38#[derive(Clone, Copy, Debug, PartialEq)]
39pub struct StylusOrientation {
40    pub altitude: f32,
41    pub azimuth: f32,
42}
43
44impl StylusOrientation {
45    /// Converts Windows-style signed X/Y tilt angles (degrees from the surface
46    /// normal) into Incular's altitude/azimuth representation.
47    #[must_use]
48    pub fn from_tilt_degrees(tilt_x: f64, tilt_y: f64) -> Option<Self> {
49        if !tilt_x.is_finite()
50            || !tilt_y.is_finite()
51            || !(-90.0..=90.0).contains(&tilt_x)
52            || !(-90.0..=90.0).contains(&tilt_y)
53        {
54            return None;
55        }
56        let x = tilt_x.to_radians().tan();
57        let y = tilt_y.to_radians().tan();
58        if !x.is_finite() || !y.is_finite() {
59            return None;
60        }
61        let altitude = 1.0_f64.atan2(x.hypot(y));
62        let azimuth = y.atan2(x).rem_euclid(std::f64::consts::TAU);
63        Some(Self {
64            altitude: altitude as f32,
65            azimuth: azimuth as f32,
66        })
67    }
68}
69
70/// Stylus-only sample data with semantics shared by supported native backends.
71#[derive(Clone, Copy, Debug, Default, PartialEq)]
72pub struct StylusMetadata {
73    pub orientation: Option<StylusOrientation>,
74    /// Whether the pen's barrel/secondary switch is currently depressed.
75    pub barrel_button: bool,
76}
77
78/// Optional high-fidelity data attached to one pointer sample.
79#[derive(Clone, Copy, Debug, Default, PartialEq)]
80pub struct PointerSampleMetadata {
81    pub pressure: Option<NormalizedPressure>,
82    pub stylus: Option<StylusMetadata>,
83}
84
85impl PointerSampleMetadata {
86    pub const EMPTY: Self = Self {
87        pressure: None,
88        stylus: None,
89    };
90}
91
92#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
93pub enum TrackpadGesturePhase {
94    Started,
95    Updated,
96    Ended,
97    Cancelled,
98}
99
100#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
101pub enum TrackpadGestureKind {
102    Pinch,
103    Rotation,
104    Pan,
105    SmartMagnify,
106    Pressure,
107}
108
109/// Aggregate native trackpad gesture. These events deliberately do not contain
110/// synthetic touch contacts: platforms that expose only magnification,
111/// rotation, or force keep those aggregate semantics intact.
112#[derive(Clone, Copy, Debug, PartialEq)]
113pub enum TrackpadGesture {
114    Pinch {
115        device: u64,
116        phase: TrackpadGesturePhase,
117        magnification_delta: f32,
118    },
119    Rotation {
120        device: u64,
121        phase: TrackpadGesturePhase,
122        /// Counter-clockwise rotation in radians for this native update.
123        delta_radians: f32,
124    },
125    Pan {
126        device: u64,
127        phase: TrackpadGesturePhase,
128        delta: Offset,
129    },
130    SmartMagnify {
131        device: u64,
132    },
133    Pressure {
134        device: u64,
135        pressure: NormalizedPressure,
136        stage: i64,
137    },
138}
139
140impl TrackpadGesture {
141    #[must_use]
142    pub const fn device(self) -> u64 {
143        match self {
144            Self::Pinch { device, .. }
145            | Self::Rotation { device, .. }
146            | Self::Pan { device, .. }
147            | Self::SmartMagnify { device }
148            | Self::Pressure { device, .. } => device,
149        }
150    }
151
152    #[must_use]
153    pub const fn kind(self) -> TrackpadGestureKind {
154        match self {
155            Self::Pinch { .. } => TrackpadGestureKind::Pinch,
156            Self::Rotation { .. } => TrackpadGestureKind::Rotation,
157            Self::Pan { .. } => TrackpadGestureKind::Pan,
158            Self::SmartMagnify { .. } => TrackpadGestureKind::SmartMagnify,
159            Self::Pressure { .. } => TrackpadGestureKind::Pressure,
160        }
161    }
162
163    #[must_use]
164    pub const fn phase(self) -> Option<TrackpadGesturePhase> {
165        match self {
166            Self::Pinch { phase, .. } | Self::Rotation { phase, .. } | Self::Pan { phase, .. } => {
167                Some(phase)
168            }
169            Self::SmartMagnify { .. } | Self::Pressure { .. } => None,
170        }
171    }
172}
173
174/// Standardized keyboard values shared by every platform adapter.
175///
176/// [`Code`] identifies the physical key position; [`Key`] is the logical key
177/// value after the operating system's layout processing.  Incular does not
178/// maintain a parallel keyboard vocabulary.
179pub use keyboard_types::{Code, Key, KeyState, KeyboardEvent, Location, Modifiers, NamedKey};
180
181#[derive(Clone, Copy, Debug, PartialEq)]
182pub enum PointerPhase {
183    /// A hover-capable pointer entered the native surface. No widget-local
184    /// position is implied until a subsequent [`Self::Move`] arrives.
185    Enter,
186    Move,
187    Down,
188    Up,
189    Cancel,
190    /// A hover-capable pointer left the native surface.
191    Exit,
192}
193
194/// Physical device category for pointer input shared by platform adapters and
195/// raw widget routing.
196#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
197pub enum PointerDeviceKind {
198    #[default]
199    Mouse,
200    Touch,
201    Stylus,
202    InvertedStylus,
203    Trackpad,
204    Unknown,
205}
206
207/// Edge/corner selected for a compositor-owned native window resize gesture.
208///
209/// This lives in `incular-core` because retained widgets can describe resize
210/// hit regions without depending on a platform window implementation.
211#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
212pub enum WindowResizeDirection {
213    East,
214    North,
215    NorthEast,
216    NorthWest,
217    South,
218    SouthEast,
219    SouthWest,
220    West,
221}
222
223/// Primary-button bit in Incular's portable pointer-button mask.
224///
225/// Raw pointer metadata follows Flutter's button-mask convention so the value
226/// is stable across platform adapters. Zero remains valid for legacy adapters
227/// that do not publish button metadata.
228pub const PRIMARY_POINTER_BUTTON: u32 = 1 << 0;
229/// Secondary/right mouse button.
230pub const SECONDARY_POINTER_BUTTON: u32 = 1 << 1;
231/// Tertiary/middle mouse button.
232pub const TERTIARY_POINTER_BUTTON: u32 = 1 << 2;
233/// Conventional browser/navigation back button.
234pub const BACK_POINTER_BUTTON: u32 = 1 << 3;
235/// Conventional browser/navigation forward button.
236pub const FORWARD_POINTER_BUTTON: u32 = 1 << 4;
237
238/// Returns the portable mask for an additional mouse button.
239///
240/// `index == 0` is the first button beyond primary/secondary/tertiary/back/
241/// forward. The `u32` event mask has room for 27 such buttons; larger native
242/// button indices are intentionally unrepresentable instead of colliding with
243/// an existing bit.
244#[must_use]
245pub const fn additional_pointer_button_mask(index: u16) -> Option<u32> {
246    let bit = 5_u32 + index as u32;
247    if bit < u32::BITS {
248        Some(1_u32 << bit)
249    } else {
250        None
251    }
252}
253
254/// IME composition is separate from committed text. Byte ranges always refer
255/// to valid UTF-8 boundaries in the preedit string when supplied.
256#[derive(Clone, Debug, PartialEq, Eq)]
257pub enum ImeEvent {
258    Preedit {
259        text: String,
260        selection: Option<(usize, usize)>,
261    },
262    Commit(String),
263    End,
264}
265
266#[derive(Clone, Debug, PartialEq)]
267pub enum InputEvent {
268    Pointer {
269        phase: PointerPhase,
270        position: Offset,
271    },
272    /// Identified pointer input for touch, pen, and multi-contact touchpads.
273    /// The existing [`Self::Pointer`] form is retained as pointer `0` for
274    /// source compatibility with mouse-only adapters.
275    PointerWithId {
276        /// Stable for the duration of one pointer sequence.
277        pointer: u64,
278        phase: PointerPhase,
279        position: Offset,
280    },
281    /// Fully identified pointer input. The two legacy pointer forms remain
282    /// available for adapters that do not expose device/button metadata.
283    PointerWithMetadata {
284        /// Stable for the duration of one pointer sequence.
285        pointer: u64,
286        /// Stable identifier for the physical device, when available.
287        device: u64,
288        /// Physical category of the source device.
289        kind: PointerDeviceKind,
290        /// Pressed-button bit mask; zero is used for hover.
291        buttons: u32,
292        /// Button whose state changed for a Down/Up event. `None` is used for
293        /// movement, surface enter/exit, cancellation, and adapters that do not
294        /// expose the changed native button.
295        button: Option<u32>,
296        /// Optional pressure/orientation data supplied by the native device.
297        sample: PointerSampleMetadata,
298        phase: PointerPhase,
299        position: Offset,
300    },
301    /// Native aggregate trackpad gesture. No fake pointer contacts are created
302    /// from this event.
303    TrackpadGesture(TrackpadGesture),
304    Scroll {
305        delta: Offset,
306    },
307    Key(KeyboardEvent),
308    Text(String),
309    Ime(ImeEvent),
310    WindowResized {
311        size: Size,
312        scale_factor: f64,
313    },
314}