Skip to main content

uzor_core/input/
state.rs

1//! Platform-agnostic input state
2//!
3//! This module provides `InputState` - a snapshot of user input
4//! that platforms populate and pass to rendering/widget code.
5
6use crate::types::Rect;
7use serde::{Deserialize, Serialize};
8
9use super::touch::TouchState;
10
11// =============================================================================
12// MouseButton
13// =============================================================================
14
15/// Mouse button identifier
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
17pub enum MouseButton {
18    #[default]
19    Left,
20    Right,
21    Middle,
22}
23
24// =============================================================================
25// ModifierKeys
26// =============================================================================
27
28/// Keyboard modifier keys state
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
30pub struct ModifierKeys {
31    /// Shift key is held
32    pub shift: bool,
33
34    /// Ctrl key (or Cmd on Mac) is held
35    pub ctrl: bool,
36
37    /// Alt key (or Option on Mac) is held
38    pub alt: bool,
39
40    /// Meta key (Cmd on Mac, Win on Windows) is held
41    pub meta: bool,
42}
43
44impl ModifierKeys {
45    pub fn none() -> Self {
46        Self::default()
47    }
48
49    pub fn shift() -> Self {
50        Self {
51            shift: true,
52            ..Default::default()
53        }
54    }
55
56    pub fn ctrl() -> Self {
57        Self {
58            ctrl: true,
59            ..Default::default()
60        }
61    }
62
63    pub fn any(&self) -> bool {
64        self.shift || self.ctrl || self.alt || self.meta
65    }
66
67    #[inline]
68    pub fn ctrl_shift(&self) -> bool {
69        self.ctrl && self.shift && !self.alt && !self.meta
70    }
71
72    #[inline]
73    pub fn ctrl_alt(&self) -> bool {
74        self.ctrl && self.alt && !self.shift && !self.meta
75    }
76
77    #[inline]
78    pub fn command(&self) -> bool {
79        #[cfg(target_os = "macos")]
80        {
81            self.meta
82        }
83        #[cfg(not(target_os = "macos"))]
84        {
85            self.ctrl
86        }
87    }
88}
89
90// =============================================================================
91// PointerState
92// =============================================================================
93
94/// Mouse/touch pointer state
95#[derive(Clone, Debug, Default)]
96pub struct PointerState {
97    /// Current pointer position (None if not over canvas)
98    pub pos: Option<(f64, f64)>,
99
100    /// Which button is currently held down (if any)
101    pub button_down: Option<MouseButton>,
102
103    /// Which button was clicked this frame (single click)
104    pub clicked: Option<MouseButton>,
105
106    /// Which button was double-clicked this frame
107    pub double_clicked: Option<MouseButton>,
108
109    /// Which button was triple-clicked this frame
110    pub triple_clicked: Option<MouseButton>,
111
112    /// Previous pointer position (for calculating delta)
113    pub prev_pos: Option<(f64, f64)>,
114}
115
116impl PointerState {
117    pub fn delta(&self) -> (f64, f64) {
118        match (self.pos, self.prev_pos) {
119            (Some((x, y)), Some((px, py))) => (x - px, y - py),
120            _ => (0.0, 0.0),
121        }
122    }
123
124    pub fn is_present(&self) -> bool {
125        self.pos.is_some()
126    }
127}
128
129// =============================================================================
130// DragState
131// =============================================================================
132
133/// Active drag operation state
134#[derive(Clone, Debug)]
135pub struct DragState {
136    pub start: (f64, f64),
137    pub current: (f64, f64),
138    pub delta: (f64, f64),
139    pub total_delta: (f64, f64),
140    pub button: MouseButton,
141    pub initial_value: f64,
142}
143
144impl DragState {
145    pub fn new(start: (f64, f64), current: (f64, f64), button: MouseButton) -> Self {
146        let total_delta = (current.0 - start.0, current.1 - start.1);
147        Self {
148            start,
149            current,
150            delta: (0.0, 0.0),
151            total_delta,
152            button,
153            initial_value: 0.0,
154        }
155    }
156
157    pub fn update(&mut self, x: f64, y: f64) {
158        self.delta = (x - self.current.0, y - self.current.1);
159        self.current = (x, y);
160        self.total_delta = (self.current.0 - self.start.0, self.current.1 - self.start.1);
161    }
162
163    pub fn is_dragging(&self, _id: &crate::types::state::WidgetId) -> bool {
164        // Simple dragging check for now
165        true
166    }
167
168    pub fn delta_tuple(&self) -> (f64, f64) {
169        self.delta
170    }
171}
172
173// =============================================================================
174// InputState - Main Input Snapshot
175// =============================================================================
176
177/// Platform-agnostic input state snapshot
178#[derive(Clone, Debug, Default)]
179pub struct InputState {
180    pub pointer: PointerState,
181    pub modifiers: ModifierKeys,
182    pub scroll_delta: (f64, f64),
183    pub drag: Option<DragState>,
184    pub dt: f64,
185    pub time: f64,
186    pub multi_touch: Option<TouchState>,
187}
188
189impl InputState {
190    pub fn new() -> Self {
191        Self::default()
192    }
193
194    pub fn with_pointer_pos(mut self, x: f64, y: f64) -> Self {
195        self.pointer.pos = Some((x, y));
196        self
197    }
198
199    pub fn pointer_pos(&self) -> Option<(f64, f64)> {
200        self.pointer.pos
201    }
202
203    pub fn is_hovered(&self, rect: &Rect) -> bool {
204        if let Some((px, py)) = self.pointer.pos {
205            rect.contains(px, py)
206        } else {
207            false
208        }
209    }
210
211    pub fn is_clicked(&self) -> bool {
212        self.pointer.clicked == Some(MouseButton::Left)
213    }
214
215    pub fn is_double_clicked(&self) -> bool {
216        self.pointer.double_clicked == Some(MouseButton::Left)
217    }
218
219    pub fn is_middle_clicked(&self) -> bool {
220        self.pointer.clicked == Some(MouseButton::Middle)
221    }
222
223    pub fn is_right_clicked(&self) -> bool {
224        self.pointer.clicked == Some(MouseButton::Right)
225    }
226
227    pub fn is_mouse_down(&self) -> bool {
228        self.pointer.button_down == Some(MouseButton::Left)
229    }
230
231    pub fn is_dragging(&self) -> bool {
232        self.drag.is_some()
233    }
234
235    pub fn drag_delta(&self) -> Option<(f64, f64)> {
236        self.drag.as_ref().map(|d| d.delta)
237    }
238
239    pub fn shift(&self) -> bool {
240        self.modifiers.shift
241    }
242
243    pub fn ctrl(&self) -> bool {
244        self.modifiers.ctrl
245    }
246
247    pub fn alt(&self) -> bool {
248        self.modifiers.alt
249    }
250
251    pub fn consume_click(&mut self) -> bool {
252        if self.pointer.clicked.is_some() {
253            self.pointer.clicked = None;
254            true
255        } else {
256            false
257        }
258    }
259
260    pub fn consume_scroll(&mut self) -> (f64, f64) {
261        let delta = self.scroll_delta;
262        self.scroll_delta = (0.0, 0.0);
263        delta
264    }
265
266    /// Get active touch count
267    pub fn touch_count(&self) -> usize {
268        self.multi_touch
269            .as_ref()
270            .map(|t| t.touch_count())
271            .unwrap_or(0)
272    }
273
274    pub fn primary_pointer(&self) -> Option<(f64, f64)> {
275        self.pointer.pos.or_else(|| {
276            self.multi_touch
277                .as_ref()
278                .and_then(|t| t.primary_touch())
279                .map(|t| t.pos)
280        })
281    }
282
283    pub fn end_frame(&mut self) {
284        self.pointer.clicked = None;
285        self.pointer.double_clicked = None;
286        self.pointer.triple_clicked = None;
287        self.pointer.prev_pos = self.pointer.pos;
288        if let Some(ref mut touch) = self.multi_touch {
289            touch.clear_deltas();
290        }
291    }
292}