Skip to main content

uzor_core/input/
response.rs

1//! Unified widget response pattern
2//!
3//! Provides `WidgetResponse` - a unified structure returned by all interactive
4//! widgets containing information about user interactions during the current frame.
5
6use super::sense::Sense;
7use super::state::InputState;
8use super::widget_state::WidgetId;
9use crate::types::WidgetRect;
10
11/// Unified response from widget interaction
12///
13/// Every widget should return a WidgetResponse containing information
14/// about user interactions with that widget during the current frame.
15#[derive(Clone, Debug)]
16pub struct WidgetResponse {
17    /// Widget identifier
18    pub id: WidgetId,
19    /// Widget's bounding rectangle
20    pub rect: WidgetRect,
21    /// What interactions this widget senses
22    pub sense: Sense,
23
24    // Hover state
25    /// Mouse is hovering over widget
26    pub hovered: bool,
27    /// Mouse entered widget this frame
28    pub hover_started: bool,
29    /// Mouse left widget this frame
30    pub hover_ended: bool,
31
32    // Click state
33    /// Left mouse button clicked this frame
34    pub clicked: bool,
35    /// Left mouse button double-clicked this frame
36    pub double_clicked: bool,
37    /// Left mouse button triple-clicked this frame
38    pub triple_clicked: bool,
39    /// Right mouse button clicked this frame
40    pub right_clicked: bool,
41    /// Middle mouse button clicked this frame
42    pub middle_clicked: bool,
43
44    // Drag state
45    /// Drag operation started this frame
46    pub drag_started: bool,
47    /// Widget is currently being dragged
48    pub dragged: bool,
49    /// Drag operation ended this frame
50    pub drag_stopped: bool,
51    /// Drag delta since last frame
52    pub drag_delta: (f64, f64),
53    /// Total drag delta since drag started
54    pub drag_total: (f64, f64),
55
56    // Focus state
57    /// Widget has keyboard focus
58    pub has_focus: bool,
59    /// Widget gained focus this frame
60    pub gained_focus: bool,
61    /// Widget lost focus this frame
62    pub lost_focus: bool,
63
64    // Value change (for input widgets)
65    /// Underlying value changed
66    pub changed: bool,
67
68    // Widget state
69    /// Widget is enabled (not disabled/grayed)
70    pub enabled: bool,
71}
72
73impl Default for WidgetResponse {
74    fn default() -> Self {
75        Self {
76            id: WidgetId::new(""),
77            rect: WidgetRect::default(),
78            sense: Sense::NONE,
79            hovered: false,
80            hover_started: false,
81            hover_ended: false,
82            clicked: false,
83            double_clicked: false,
84            triple_clicked: false,
85            right_clicked: false,
86            middle_clicked: false,
87            drag_started: false,
88            dragged: false,
89            drag_stopped: false,
90            drag_delta: (0.0, 0.0),
91            drag_total: (0.0, 0.0),
92            has_focus: false,
93            gained_focus: false,
94            lost_focus: false,
95            changed: false,
96            enabled: true,
97        }
98    }
99}
100
101impl WidgetResponse {
102    /// Create new response for widget
103    pub fn new(id: WidgetId, rect: WidgetRect, sense: Sense) -> Self {
104        Self {
105            id,
106            rect,
107            sense,
108            ..Default::default()
109        }
110    }
111
112    /// Set hover state
113    pub fn with_hover(mut self, hovered: bool) -> Self {
114        self.hovered = hovered;
115        self
116    }
117
118    /// Set clicked state
119    pub fn with_click(mut self) -> Self {
120        self.clicked = true;
121        self
122    }
123
124    /// Set focus state
125    pub fn with_focus(mut self, has_focus: bool) -> Self {
126        self.has_focus = has_focus;
127        self
128    }
129
130    /// Mark as changed
131    pub fn with_changed(mut self) -> Self {
132        self.changed = true;
133        self
134    }
135
136    /// Mark as disabled
137    pub fn disabled(mut self) -> Self {
138        self.enabled = false;
139        self
140    }
141}
142
143// Query helper methods
144impl WidgetResponse {
145    /// Check if any click occurred (left, right, or middle)
146    pub fn any_click(&self) -> bool {
147        self.clicked || self.right_clicked || self.middle_clicked
148    }
149
150    /// Check if widget was interacted with this frame (click, drag start, or gained focus)
151    pub fn interacted(&self) -> bool {
152        self.any_click() || self.drag_started || self.gained_focus
153    }
154
155    /// Check if widget is active (being dragged or has focus)
156    pub fn is_active(&self) -> bool {
157        self.dragged || self.has_focus
158    }
159
160    /// Check if pointer is over widget or dragging it
161    pub fn is_pointer_over(&self) -> bool {
162        self.hovered || self.dragged
163    }
164}
165
166// Response combination
167impl WidgetResponse {
168    /// Combine two responses (logical OR of interaction states)
169    ///
170    /// Uses the rect and id of `self`.
171    pub fn union(self, other: WidgetResponse) -> WidgetResponse {
172        WidgetResponse {
173            id: self.id,
174            rect: self.rect,
175            sense: self.sense.union(other.sense),
176            hovered: self.hovered || other.hovered,
177            hover_started: self.hover_started || other.hover_started,
178            hover_ended: self.hover_ended || other.hover_ended,
179            clicked: self.clicked || other.clicked,
180            double_clicked: self.double_clicked || other.double_clicked,
181            triple_clicked: self.triple_clicked || other.triple_clicked,
182            right_clicked: self.right_clicked || other.right_clicked,
183            middle_clicked: self.middle_clicked || other.middle_clicked,
184            drag_started: self.drag_started || other.drag_started,
185            dragged: self.dragged || other.dragged,
186            drag_stopped: self.drag_stopped || other.drag_stopped,
187            drag_delta: if self.dragged {
188                self.drag_delta
189            } else {
190                other.drag_delta
191            },
192            drag_total: if self.dragged {
193                self.drag_total
194            } else {
195                other.drag_total
196            },
197            has_focus: self.has_focus || other.has_focus,
198            gained_focus: self.gained_focus || other.gained_focus,
199            lost_focus: self.lost_focus || other.lost_focus,
200            changed: self.changed || other.changed,
201            enabled: self.enabled && other.enabled,
202        }
203    }
204}
205
206impl std::ops::BitOr for WidgetResponse {
207    type Output = WidgetResponse;
208
209    /// Combine two responses using the `|` operator (equivalent to union)
210    fn bitor(self, rhs: Self) -> Self::Output {
211        self.union(rhs)
212    }
213}
214
215/// Create a WidgetResponse by testing InputState against a widget rect
216pub fn create_response(
217    id: WidgetId,
218    rect: WidgetRect,
219    sense: Sense,
220    input: &InputState,
221    prev_hovered: bool,
222    prev_focused: bool,
223) -> WidgetResponse {
224    let hovered = if sense.hover {
225        input.is_hovered(&rect)
226    } else {
227        false
228    };
229
230    let hover_started = hovered && !prev_hovered;
231    let hover_ended = !hovered && prev_hovered;
232
233    let clicked = sense.click && hovered && input.is_clicked();
234    let double_clicked = sense.click && hovered && input.is_double_clicked();
235    let right_clicked = sense.click && hovered && input.is_right_clicked();
236    let middle_clicked = sense.click && hovered && input.is_middle_clicked();
237
238    let (dragged, drag_delta, drag_started, drag_stopped) = if sense.drag {
239        let is_dragging = input.is_dragging();
240        let delta = input.drag_delta().unwrap_or((0.0, 0.0));
241        (is_dragging, delta, false, false)
242    } else {
243        (false, (0.0, 0.0), false, false)
244    };
245
246    WidgetResponse {
247        id,
248        rect,
249        sense,
250        hovered,
251        hover_started,
252        hover_ended,
253        clicked,
254        double_clicked,
255        triple_clicked: false,
256        right_clicked,
257        middle_clicked,
258        drag_started,
259        dragged,
260        drag_stopped,
261        drag_delta,
262        drag_total: (0.0, 0.0),
263        has_focus: prev_focused,
264        gained_focus: false,
265        lost_focus: false,
266        changed: false,
267        enabled: true,
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn test_default_response() {
277        let response = WidgetResponse::default();
278        assert!(!response.clicked);
279        assert!(!response.hovered);
280        assert!(response.enabled);
281        assert_eq!(response.sense, Sense::NONE);
282    }
283
284    #[test]
285    fn test_response_new() {
286        let id = WidgetId::new("test_button");
287        let rect = WidgetRect::new(10.0, 10.0, 100.0, 40.0);
288        let response = WidgetResponse::new(id.clone(), rect, Sense::CLICK);
289
290        assert_eq!(response.id, id);
291        assert_eq!(response.rect.x, 10.0);
292        assert_eq!(response.sense, Sense::CLICK);
293    }
294
295    #[test]
296    fn test_builder_methods() {
297        let response = WidgetResponse::new(
298            WidgetId::new("test"),
299            WidgetRect::default(),
300            Sense::CLICK,
301        )
302        .with_hover(true)
303        .with_click()
304        .with_focus(true)
305        .with_changed();
306
307        assert!(response.hovered);
308        assert!(response.clicked);
309        assert!(response.has_focus);
310        assert!(response.changed);
311    }
312
313    #[test]
314    fn test_disabled_builder() {
315        let response = WidgetResponse::new(
316            WidgetId::new("test"),
317            WidgetRect::default(),
318            Sense::CLICK,
319        )
320        .disabled();
321
322        assert!(!response.enabled);
323    }
324
325    #[test]
326    fn test_any_click() {
327        let mut response = WidgetResponse::default();
328        assert!(!response.any_click());
329
330        response.clicked = true;
331        assert!(response.any_click());
332
333        response.clicked = false;
334        response.right_clicked = true;
335        assert!(response.any_click());
336
337        response.right_clicked = false;
338        response.middle_clicked = true;
339        assert!(response.any_click());
340    }
341
342    #[test]
343    fn test_interacted() {
344        let mut response = WidgetResponse::default();
345        assert!(!response.interacted());
346
347        response.clicked = true;
348        assert!(response.interacted());
349
350        response = WidgetResponse::default();
351        response.drag_started = true;
352        assert!(response.interacted());
353
354        response = WidgetResponse::default();
355        response.gained_focus = true;
356        assert!(response.interacted());
357    }
358
359    #[test]
360    fn test_is_active() {
361        let mut response = WidgetResponse::default();
362        assert!(!response.is_active());
363
364        response.dragged = true;
365        assert!(response.is_active());
366
367        response.dragged = false;
368        response.has_focus = true;
369        assert!(response.is_active());
370    }
371
372    #[test]
373    fn test_is_pointer_over() {
374        let mut response = WidgetResponse::default();
375        assert!(!response.is_pointer_over());
376
377        response.hovered = true;
378        assert!(response.is_pointer_over());
379
380        response.hovered = false;
381        response.dragged = true;
382        assert!(response.is_pointer_over());
383    }
384
385    #[test]
386    fn test_response_union() {
387        let response1 = WidgetResponse::new(
388            WidgetId::new("widget1"),
389            WidgetRect::new(0.0, 0.0, 100.0, 100.0),
390            Sense::CLICK,
391        )
392        .with_hover(true);
393
394        let response2 = WidgetResponse::new(
395            WidgetId::new("widget2"),
396            WidgetRect::new(50.0, 50.0, 100.0, 100.0),
397            Sense::DRAG,
398        )
399        .with_click();
400
401        let combined = response1.clone().union(response2);
402
403        assert_eq!(combined.id, response1.id);
404        assert_eq!(combined.rect.x, response1.rect.x);
405        assert!(combined.hovered);
406        assert!(combined.clicked);
407        assert!(combined.sense.hover);
408        assert!(combined.sense.click);
409        assert!(combined.sense.drag);
410    }
411
412    #[test]
413    fn test_response_bitor_operator() {
414        let response1 = WidgetResponse::default().with_hover(true);
415        let response2 = WidgetResponse::default().with_click();
416
417        let combined = response1 | response2;
418
419        assert!(combined.hovered);
420        assert!(combined.clicked);
421    }
422
423    #[test]
424    fn test_union_preserves_drag_data() {
425        let mut response1 = WidgetResponse::default();
426        response1.dragged = true;
427        response1.drag_delta = (10.0, 20.0);
428        response1.drag_total = (30.0, 40.0);
429
430        let mut response2 = WidgetResponse::default();
431        response2.drag_delta = (5.0, 5.0);
432        response2.drag_total = (15.0, 15.0);
433
434        let combined = response1.clone().union(response2);
435
436        assert!(combined.dragged);
437        assert_eq!(combined.drag_delta, (10.0, 20.0));
438        assert_eq!(combined.drag_total, (30.0, 40.0));
439    }
440
441    #[test]
442    fn test_union_enabled_state() {
443        let response1 = WidgetResponse::default();
444        let response2 = WidgetResponse::default().disabled();
445
446        let combined = response1.union(response2);
447        assert!(!combined.enabled);
448    }
449
450    #[test]
451    fn test_create_response_hover() {
452        let mut input = InputState::new();
453        input.pointer.pos = Some((50.0, 50.0));
454
455        let rect = WidgetRect::new(10.0, 10.0, 100.0, 100.0);
456        let response = create_response(
457            WidgetId::new("test"),
458            rect,
459            Sense::HOVER,
460            &input,
461            false,
462            false,
463        );
464
465        assert!(response.hovered);
466        assert!(response.hover_started);
467    }
468
469    #[test]
470    fn test_create_response_hover_ended() {
471        let mut input = InputState::new();
472        input.pointer.pos = Some((5.0, 5.0));
473
474        let rect = WidgetRect::new(10.0, 10.0, 100.0, 100.0);
475        let response = create_response(
476            WidgetId::new("test"),
477            rect,
478            Sense::HOVER,
479            &input,
480            true,
481            false,
482        );
483
484        assert!(!response.hovered);
485        assert!(response.hover_ended);
486    }
487
488    #[test]
489    fn test_create_response_click() {
490        let mut input = InputState::new();
491        input.pointer.pos = Some((50.0, 50.0));
492        input.pointer.clicked = Some(crate::input::state::MouseButton::Left);
493
494        let rect = WidgetRect::new(10.0, 10.0, 100.0, 100.0);
495        let response = create_response(
496            WidgetId::new("test"),
497            rect,
498            Sense::CLICK,
499            &input,
500            false,
501            false,
502        );
503
504        assert!(response.hovered);
505        assert!(response.clicked);
506    }
507
508    #[test]
509    fn test_create_response_no_sense() {
510        let mut input = InputState::new();
511        input.pointer.pos = Some((50.0, 50.0));
512        input.pointer.clicked = Some(crate::input::state::MouseButton::Left);
513
514        let rect = WidgetRect::new(10.0, 10.0, 100.0, 100.0);
515        let response = create_response(
516            WidgetId::new("test"),
517            rect,
518            Sense::NONE,
519            &input,
520            false,
521            false,
522        );
523
524        assert!(!response.hovered);
525        assert!(!response.clicked);
526    }
527
528    #[test]
529    fn test_create_response_right_click() {
530        let mut input = InputState::new();
531        input.pointer.pos = Some((50.0, 50.0));
532        input.pointer.clicked = Some(crate::input::state::MouseButton::Right);
533
534        let rect = WidgetRect::new(10.0, 10.0, 100.0, 100.0);
535        let response = create_response(
536            WidgetId::new("test"),
537            rect,
538            Sense::CLICK,
539            &input,
540            false,
541            false,
542        );
543
544        assert!(response.right_clicked);
545        assert!(!response.clicked);
546    }
547}