Skip to main content

uzor_core/input/
widget_state.rs

1//! Widget input state management
2//!
3//! Centralized state tracking for widget interactions across frames.
4
5pub use crate::types::state::WidgetId;
6
7/// Focus state for widgets
8#[derive(Clone, Debug, Default)]
9pub struct FocusState {
10    /// Currently focused widget ID
11    pub focused: Option<WidgetId>,
12    /// Widget that will receive focus on next frame
13    pub pending_focus: Option<WidgetId>,
14}
15
16impl FocusState {
17    pub fn new() -> Self {
18        Self::default()
19    }
20
21    /// Set focus to a widget
22    pub fn set_focus(&mut self, id: WidgetId) {
23        self.focused = Some(id);
24    }
25
26    /// Clear focus
27    pub fn clear_focus(&mut self) {
28        self.focused = None;
29    }
30
31    /// Check if a widget is focused
32    pub fn is_focused(&self, id: &WidgetId) -> bool {
33        self.focused.as_ref() == Some(id)
34    }
35
36    /// Request focus for next frame
37    pub fn request_focus(&mut self, id: WidgetId) {
38        self.pending_focus = Some(id);
39    }
40
41    /// Process pending focus changes
42    pub fn process_pending(&mut self) {
43        if let Some(id) = self.pending_focus.take() {
44            self.focused = Some(id);
45        }
46    }
47}
48
49/// Hover state for widgets
50#[derive(Clone, Debug, Default)]
51pub struct HoverState {
52    /// Currently hovered widget ID
53    pub hovered: Option<WidgetId>,
54    /// Mouse position
55    pub mouse_pos: (f64, f64),
56    /// Whether mouse is pressed
57    pub mouse_pressed: bool,
58}
59
60impl HoverState {
61    pub fn new() -> Self {
62        Self::default()
63    }
64
65    /// Update mouse position
66    pub fn update_mouse(&mut self, x: f64, y: f64) {
67        self.mouse_pos = (x, y);
68    }
69
70    /// Set hovered widget
71    pub fn set_hovered(&mut self, id: Option<WidgetId>) {
72        self.hovered = id;
73    }
74
75    /// Check if a widget is hovered
76    pub fn is_hovered(&self, id: &WidgetId) -> bool {
77        self.hovered.as_ref() == Some(id)
78    }
79
80    /// Set mouse pressed state
81    pub fn set_pressed(&mut self, pressed: bool) {
82        self.mouse_pressed = pressed;
83    }
84}
85
86/// Widget drag state
87#[derive(Clone, Debug, Default)]
88pub struct DragState {
89    /// Widget being dragged
90    pub dragging: Option<WidgetId>,
91    /// Drag start position
92    pub start_pos: (f64, f64),
93    /// Current drag position
94    pub current_pos: (f64, f64),
95    /// Drag offset from widget origin
96    pub offset: (f64, f64),
97    /// Initial value when drag started (for sliders, scrollbars)
98    pub initial_value: f64,
99}
100
101impl DragState {
102    pub fn new() -> Self {
103        Self::default()
104    }
105
106    /// Start dragging a widget
107    pub fn start(&mut self, id: WidgetId, x: f64, y: f64, offset_x: f64, offset_y: f64) {
108        self.dragging = Some(id);
109        self.start_pos = (x, y);
110        self.current_pos = (x, y);
111        self.offset = (offset_x, offset_y);
112    }
113
114    /// Start dragging with initial value
115    pub fn start_with_value(&mut self, id: WidgetId, x: f64, y: f64, value: f64) {
116        self.dragging = Some(id);
117        self.start_pos = (x, y);
118        self.current_pos = (x, y);
119        self.offset = (0.0, 0.0);
120        self.initial_value = value;
121    }
122
123    /// Update drag position
124    pub fn update(&mut self, x: f64, y: f64) {
125        self.current_pos = (x, y);
126    }
127
128    /// End dragging
129    pub fn end(&mut self) {
130        self.dragging = None;
131    }
132
133    /// Check if a widget is being dragged
134    pub fn is_dragging(&self, id: &WidgetId) -> bool {
135        self.dragging.as_ref() == Some(id)
136    }
137
138    /// Get drag delta from start
139    pub fn delta(&self) -> (f64, f64) {
140        (
141            self.current_pos.0 - self.start_pos.0,
142            self.current_pos.1 - self.start_pos.1,
143        )
144    }
145
146    /// Get drag delta from last frame
147    pub fn delta_from(&self, last_pos: (f64, f64)) -> (f64, f64) {
148        (
149            self.current_pos.0 - last_pos.0,
150            self.current_pos.1 - last_pos.1,
151        )
152    }
153}
154
155/// Widget interaction type
156#[derive(Clone, Copy, Debug, PartialEq, Eq)]
157#[derive(Default)]
158pub enum WidgetInteraction {
159    #[default]
160    None,
161    Hover,
162    Press,
163    Drag,
164    Click,
165    DoubleClick,
166    TripleClick,
167    Focus,
168}
169
170
171/// Combined widget input state
172#[derive(Clone, Debug, Default)]
173pub struct WidgetInputState {
174    /// Focus management
175    pub focus: FocusState,
176    /// Hover tracking
177    pub hover: HoverState,
178    /// Drag tracking
179    pub drag: DragState,
180    /// Active widget (pressed but not yet released)
181    pub active: Option<WidgetId>,
182    /// Last click time for double-click detection
183    pub last_click_time: f64,
184    /// Last click position
185    pub last_click_pos: (f64, f64),
186    /// Last clicked widget
187    pub last_click_widget: Option<WidgetId>,
188    /// Double-click threshold in milliseconds
189    pub double_click_threshold_ms: f64,
190    /// Double-click distance threshold in pixels
191    pub double_click_distance: f64,
192    /// Click count for multi-click detection (1 = single, 2 = double, 3 = triple)
193    pub click_count: u8,
194    /// Triple-click threshold in milliseconds
195    pub triple_click_threshold_ms: f64,
196}
197
198impl WidgetInputState {
199    pub fn new() -> Self {
200        Self {
201            double_click_threshold_ms: 500.0,
202            double_click_distance: 5.0,
203            click_count: 0,
204            triple_click_threshold_ms: 300.0,
205            ..Default::default()
206        }
207    }
208
209    /// Update mouse position
210    pub fn update_mouse(&mut self, x: f64, y: f64) {
211        self.hover.update_mouse(x, y);
212        if self.drag.dragging.is_some() {
213            self.drag.update(x, y);
214        }
215    }
216
217    /// Handle mouse press
218    pub fn mouse_press(&mut self, _x: f64, _y: f64, widget_id: Option<WidgetId>) {
219        self.hover.set_pressed(true);
220        self.active = widget_id;
221    }
222
223    /// Handle mouse release with click/double-click/triple-click detection
224    pub fn mouse_release(&mut self, x: f64, y: f64, now: f64) -> WidgetInteraction {
225        self.hover.set_pressed(false);
226
227        let was_dragging = self.drag.dragging.is_some();
228        self.drag.end();
229
230        if was_dragging {
231            self.active = None;
232            return WidgetInteraction::None;
233        }
234
235        if let Some(ref active_id) = self.active {
236            let is_same_widget = self.last_click_widget.as_ref() == Some(active_id);
237            let time_since_last = now - self.last_click_time;
238            let dist = ((x - self.last_click_pos.0).powi(2) + (y - self.last_click_pos.1).powi(2)).sqrt();
239            let dist_ok = dist < self.double_click_distance;
240
241            let interaction = if is_same_widget && dist_ok {
242                if time_since_last < self.triple_click_threshold_ms && self.click_count == 2 {
243                    self.click_count = 3;
244                    WidgetInteraction::TripleClick
245                } else if time_since_last < self.double_click_threshold_ms && self.click_count == 1 {
246                    self.click_count = 2;
247                    WidgetInteraction::DoubleClick
248                } else {
249                    self.click_count = 1;
250                    WidgetInteraction::Click
251                }
252            } else {
253                self.click_count = 1;
254                WidgetInteraction::Click
255            };
256
257            self.last_click_time = now;
258            self.last_click_pos = (x, y);
259            self.last_click_widget = Some(active_id.clone());
260
261            self.active = None;
262            return interaction;
263        }
264
265        self.active = None;
266        WidgetInteraction::None
267    }
268
269    /// Start dragging a widget
270    pub fn start_drag(&mut self, id: WidgetId, x: f64, y: f64) {
271        self.drag.start(id, x, y, 0.0, 0.0);
272    }
273
274    /// Start dragging with value (for sliders)
275    pub fn start_drag_with_value(&mut self, id: WidgetId, x: f64, y: f64, value: f64) {
276        self.drag.start_with_value(id, x, y, value);
277    }
278
279    /// Process frame end (update pending states)
280    pub fn end_frame(&mut self) {
281        self.focus.process_pending();
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn test_widget_id() {
291        let id1 = WidgetId::new("button1");
292        let id2: WidgetId = "button2".into();
293        assert_ne!(id1, id2);
294    }
295
296    #[test]
297    fn test_focus_state() {
298        let mut focus = FocusState::new();
299        let id = WidgetId::new("input1");
300
301        assert!(!focus.is_focused(&id));
302
303        focus.set_focus(id.clone());
304        assert!(focus.is_focused(&id));
305
306        focus.clear_focus();
307        assert!(!focus.is_focused(&id));
308    }
309
310    #[test]
311    fn test_hover_state() {
312        let mut hover = HoverState::new();
313        let id = WidgetId::new("button1");
314
315        hover.update_mouse(100.0, 50.0);
316        assert_eq!(hover.mouse_pos, (100.0, 50.0));
317
318        hover.set_hovered(Some(id.clone()));
319        assert!(hover.is_hovered(&id));
320
321        hover.set_hovered(None);
322        assert!(!hover.is_hovered(&id));
323    }
324
325    #[test]
326    fn test_drag_state() {
327        let mut drag = DragState::new();
328        let id = WidgetId::new("slider1");
329
330        drag.start(id.clone(), 100.0, 50.0, 5.0, 0.0);
331        assert!(drag.is_dragging(&id));
332
333        drag.update(150.0, 60.0);
334        assert_eq!(drag.delta(), (50.0, 10.0));
335
336        drag.end();
337        assert!(!drag.is_dragging(&id));
338    }
339
340    #[test]
341    fn test_click_detection() {
342        let mut state = WidgetInputState::new();
343        let id = WidgetId::new("button1");
344
345        state.mouse_press(100.0, 50.0, Some(id.clone()));
346        let interaction = state.mouse_release(100.0, 50.0, 1000.0);
347        assert_eq!(interaction, WidgetInteraction::Click);
348    }
349
350    #[test]
351    fn test_double_click_detection() {
352        let mut state = WidgetInputState::new();
353        let id = WidgetId::new("button1");
354
355        state.mouse_press(100.0, 50.0, Some(id.clone()));
356        state.mouse_release(100.0, 50.0, 1000.0);
357
358        state.mouse_press(101.0, 51.0, Some(id.clone()));
359        let interaction = state.mouse_release(101.0, 51.0, 1200.0);
360        assert_eq!(interaction, WidgetInteraction::DoubleClick);
361    }
362
363    #[test]
364    fn test_triple_click_detection() {
365        let mut state = WidgetInputState::new();
366        let id = WidgetId::new("button1");
367
368        state.mouse_press(100.0, 50.0, Some(id.clone()));
369        let interaction1 = state.mouse_release(100.0, 50.0, 1000.0);
370        assert_eq!(interaction1, WidgetInteraction::Click);
371
372        state.mouse_press(101.0, 51.0, Some(id.clone()));
373        let interaction2 = state.mouse_release(101.0, 51.0, 1200.0);
374        assert_eq!(interaction2, WidgetInteraction::DoubleClick);
375
376        state.mouse_press(100.0, 50.0, Some(id.clone()));
377        let interaction3 = state.mouse_release(100.0, 50.0, 1400.0);
378        assert_eq!(interaction3, WidgetInteraction::TripleClick);
379    }
380
381    #[test]
382    fn test_triple_click_timeout() {
383        let mut state = WidgetInputState::new();
384        let id = WidgetId::new("button1");
385
386        state.mouse_press(100.0, 50.0, Some(id.clone()));
387        state.mouse_release(100.0, 50.0, 1000.0);
388
389        state.mouse_press(101.0, 51.0, Some(id.clone()));
390        state.mouse_release(101.0, 51.0, 1200.0);
391
392        state.mouse_press(100.0, 50.0, Some(id.clone()));
393        let interaction = state.mouse_release(100.0, 50.0, 2000.0);
394        assert_eq!(interaction, WidgetInteraction::Click);
395    }
396
397    #[test]
398    fn test_triple_click_different_widget() {
399        let mut state = WidgetInputState::new();
400        let id1 = WidgetId::new("button1");
401        let id2 = WidgetId::new("button2");
402
403        state.mouse_press(100.0, 50.0, Some(id1.clone()));
404        state.mouse_release(100.0, 50.0, 1000.0);
405
406        state.mouse_press(101.0, 51.0, Some(id1.clone()));
407        state.mouse_release(101.0, 51.0, 1200.0);
408
409        state.mouse_press(200.0, 50.0, Some(id2.clone()));
410        let interaction = state.mouse_release(200.0, 50.0, 1400.0);
411        assert_eq!(interaction, WidgetInteraction::Click);
412    }
413
414    #[test]
415    fn test_triple_click_too_far() {
416        let mut state = WidgetInputState::new();
417        let id = WidgetId::new("button1");
418
419        state.mouse_press(100.0, 50.0, Some(id.clone()));
420        state.mouse_release(100.0, 50.0, 1000.0);
421
422        state.mouse_press(101.0, 51.0, Some(id.clone()));
423        state.mouse_release(101.0, 51.0, 1200.0);
424
425        state.mouse_press(200.0, 50.0, Some(id.clone()));
426        let interaction = state.mouse_release(200.0, 50.0, 1400.0);
427        assert_eq!(interaction, WidgetInteraction::Click);
428    }
429
430    #[test]
431    fn test_click_count_reset_after_triple() {
432        let mut state = WidgetInputState::new();
433        let id = WidgetId::new("button1");
434
435        state.mouse_press(100.0, 50.0, Some(id.clone()));
436        state.mouse_release(100.0, 50.0, 1000.0);
437        state.mouse_press(100.0, 50.0, Some(id.clone()));
438        state.mouse_release(100.0, 50.0, 1200.0);
439        state.mouse_press(100.0, 50.0, Some(id.clone()));
440        let interaction = state.mouse_release(100.0, 50.0, 1400.0);
441        assert_eq!(interaction, WidgetInteraction::TripleClick);
442
443        state.mouse_press(100.0, 50.0, Some(id.clone()));
444        let interaction = state.mouse_release(100.0, 50.0, 1600.0);
445        assert_eq!(interaction, WidgetInteraction::Click);
446    }
447}