Skip to main content

uzor_core/input/
handlers.rs

1//! Widget input handlers
2//!
3//! Platform-agnostic input handling functions for various widgets.
4
5use super::widget_state::{WidgetId, WidgetInputState, WidgetInteraction};
6use crate::types::WidgetRect;
7
8/// Widget hit test result
9#[derive(Clone, Debug, PartialEq)]
10#[derive(Default)]
11pub enum WidgetHitResult {
12    /// No hit
13    #[default]
14    None,
15    /// Hit a widget body
16    Widget { id: WidgetId },
17    /// Hit a close button
18    CloseButton { parent_id: WidgetId },
19    /// Hit a dropdown item
20    DropdownItem { dropdown_id: WidgetId, item_index: usize },
21    /// Hit a toolbar item
22    ToolbarItem { toolbar_id: WidgetId, item_id: String },
23    /// Hit a slider track
24    SliderTrack { id: WidgetId },
25    /// Hit a slider handle
26    SliderHandle { id: WidgetId },
27    /// Hit a scrollbar track
28    ScrollbarTrack { id: WidgetId },
29    /// Hit a scrollbar handle
30    ScrollbarHandle { id: WidgetId },
31    /// Hit a tab
32    Tab { parent_id: WidgetId, tab_index: usize },
33}
34
35
36/// Generic widget hit test
37pub fn widget_hit_test(rect: &WidgetRect, x: f64, y: f64) -> bool {
38    rect.contains(x, y)
39}
40
41// =============================================================================
42// Button Input
43// =============================================================================
44
45/// Button input result
46#[derive(Clone, Debug, Default)]
47pub struct ButtonInputResult {
48    /// Whether button was clicked
49    pub clicked: bool,
50    /// Whether button is hovered
51    pub hovered: bool,
52    /// Whether button is pressed (mouse down)
53    pub pressed: bool,
54    /// Current interaction state
55    pub interaction: WidgetInteraction,
56}
57
58/// Handle button input
59pub fn handle_button_input(
60    state: &WidgetInputState,
61    id: &WidgetId,
62    rect: &WidgetRect,
63    disabled: bool,
64) -> ButtonInputResult {
65    if disabled {
66        return ButtonInputResult::default();
67    }
68
69    let (mx, my) = state.hover.mouse_pos;
70    let hovered = rect.contains(mx, my);
71    let pressed = hovered && state.hover.mouse_pressed && state.active.as_ref() == Some(id);
72    let clicked = hovered && state.active.as_ref() == Some(id) && !state.hover.mouse_pressed;
73
74    let interaction = if pressed {
75        WidgetInteraction::Press
76    } else if hovered {
77        WidgetInteraction::Hover
78    } else {
79        WidgetInteraction::None
80    };
81
82    ButtonInputResult {
83        clicked,
84        hovered,
85        pressed,
86        interaction,
87    }
88}
89
90// =============================================================================
91// Checkbox Input
92// =============================================================================
93
94/// Checkbox input result
95#[derive(Clone, Debug, Default)]
96pub struct CheckboxInputResult {
97    /// Whether checkbox was toggled
98    pub toggled: bool,
99    /// New checked state (if toggled)
100    pub new_checked: bool,
101    /// Whether checkbox is hovered
102    pub hovered: bool,
103    /// Current interaction state
104    pub interaction: WidgetInteraction,
105}
106
107/// Handle checkbox input
108pub fn handle_checkbox_input(
109    state: &WidgetInputState,
110    id: &WidgetId,
111    rect: &WidgetRect,
112    current_checked: bool,
113    disabled: bool,
114) -> CheckboxInputResult {
115    if disabled {
116        return CheckboxInputResult::default();
117    }
118
119    let (mx, my) = state.hover.mouse_pos;
120    let hovered = rect.contains(mx, my);
121    let clicked = hovered && state.active.as_ref() == Some(id) && !state.hover.mouse_pressed;
122
123    let toggled = clicked;
124    let new_checked = if toggled { !current_checked } else { current_checked };
125
126    let interaction = if hovered && state.hover.mouse_pressed {
127        WidgetInteraction::Press
128    } else if hovered {
129        WidgetInteraction::Hover
130    } else {
131        WidgetInteraction::None
132    };
133
134    CheckboxInputResult {
135        toggled,
136        new_checked,
137        hovered,
138        interaction,
139    }
140}
141
142// =============================================================================
143// Slider Input
144// =============================================================================
145
146/// Slider input result
147#[derive(Clone, Debug, Default)]
148pub struct SliderInputResult {
149    /// Whether value changed
150    pub changed: bool,
151    /// New value (normalized 0.0 - 1.0)
152    pub value: f64,
153    /// Whether slider is hovered
154    pub hovered: bool,
155    /// Whether slider handle is being dragged
156    pub dragging: bool,
157    /// Current interaction state
158    pub interaction: WidgetInteraction,
159}
160
161/// Handle slider input
162pub fn handle_slider_input(
163    state: &WidgetInputState,
164    id: &WidgetId,
165    track_rect: &WidgetRect,
166    handle_rect: &WidgetRect,
167    current_value: f64,
168    horizontal: bool,
169    disabled: bool,
170) -> SliderInputResult {
171    if disabled {
172        return SliderInputResult {
173            value: current_value,
174            ..Default::default()
175        };
176    }
177
178    let (mx, my) = state.hover.mouse_pos;
179    let hovered = track_rect.contains(mx, my);
180    let handle_hovered = handle_rect.contains(mx, my);
181    let dragging = state.drag.is_dragging(id);
182
183    let mut value = current_value;
184    let mut changed = false;
185
186    if dragging {
187        let (dx, dy) = state.drag.delta();
188        let range = if horizontal {
189            track_rect.width - handle_rect.width
190        } else {
191            track_rect.height - handle_rect.height
192        };
193
194        if range > 0.0 {
195            let delta = if horizontal { dx } else { dy };
196            let delta_normalized = delta / range;
197            value = (state.drag.initial_value + delta_normalized).clamp(0.0, 1.0);
198            changed = (value - current_value).abs() > 0.0001;
199        }
200    }
201
202    if hovered && !handle_hovered && state.hover.mouse_pressed && state.active.as_ref() == Some(id) && !dragging {
203        let range = if horizontal {
204            track_rect.width - handle_rect.width
205        } else {
206            track_rect.height - handle_rect.height
207        };
208
209        if range > 0.0 {
210            let pos = if horizontal {
211                mx - track_rect.x - handle_rect.width / 2.0
212            } else {
213                my - track_rect.y - handle_rect.height / 2.0
214            };
215            value = (pos / range).clamp(0.0, 1.0);
216            changed = true;
217        }
218    }
219
220    let interaction = if dragging {
221        WidgetInteraction::Drag
222    } else if handle_hovered && state.hover.mouse_pressed {
223        WidgetInteraction::Press
224    } else if hovered || handle_hovered {
225        WidgetInteraction::Hover
226    } else {
227        WidgetInteraction::None
228    };
229
230    SliderInputResult {
231        changed,
232        value,
233        hovered: hovered || handle_hovered,
234        dragging,
235        interaction,
236    }
237}
238
239// =============================================================================
240// Scrollbar Input
241// =============================================================================
242
243/// Scrollbar input result
244#[derive(Clone, Debug, Default)]
245pub struct ScrollbarInputResult {
246    /// Whether scroll position changed
247    pub changed: bool,
248    /// New scroll position (normalized 0.0 - 1.0)
249    pub position: f64,
250    /// Whether scrollbar is hovered
251    pub hovered: bool,
252    /// Whether handle is being dragged
253    pub dragging: bool,
254    /// Track click (page up/down)
255    pub page_direction: Option<i32>,
256    /// Current interaction state
257    pub interaction: WidgetInteraction,
258}
259
260/// Handle scrollbar input
261#[allow(clippy::too_many_arguments)]
262pub fn handle_scrollbar_input(
263    state: &WidgetInputState,
264    id: &WidgetId,
265    track_rect: &WidgetRect,
266    handle_rect: &WidgetRect,
267    current_position: f64,
268    handle_size_ratio: f64,
269    horizontal: bool,
270    disabled: bool,
271) -> ScrollbarInputResult {
272    if disabled {
273        return ScrollbarInputResult {
274            position: current_position,
275            ..Default::default()
276        };
277    }
278
279    let (mx, my) = state.hover.mouse_pos;
280    let hovered = track_rect.contains(mx, my);
281    let handle_hovered = handle_rect.contains(mx, my);
282    let dragging = state.drag.is_dragging(id);
283
284    let mut position = current_position;
285    let mut changed = false;
286    let mut page_direction = None;
287
288    if dragging {
289        let (dx, dy) = state.drag.delta();
290        let track_size = if horizontal { track_rect.width } else { track_rect.height };
291        let usable_range = track_size * (1.0 - handle_size_ratio);
292
293        if usable_range > 0.0 {
294            let delta = if horizontal { dx } else { dy };
295            let delta_normalized = delta / usable_range;
296            position = (state.drag.initial_value + delta_normalized).clamp(0.0, 1.0);
297            changed = (position - current_position).abs() > 0.0001;
298        }
299    }
300
301    if hovered && !handle_hovered && state.hover.mouse_pressed && state.active.as_ref() == Some(id) && !dragging {
302        let handle_pos = if horizontal { handle_rect.x } else { handle_rect.y };
303        let mouse_pos = if horizontal { mx } else { my };
304
305        if mouse_pos < handle_pos {
306            page_direction = Some(-1);
307        } else {
308            page_direction = Some(1);
309        }
310    }
311
312    let interaction = if dragging {
313        WidgetInteraction::Drag
314    } else if handle_hovered && state.hover.mouse_pressed {
315        WidgetInteraction::Press
316    } else if hovered || handle_hovered {
317        WidgetInteraction::Hover
318    } else {
319        WidgetInteraction::None
320    };
321
322    ScrollbarInputResult {
323        changed,
324        position,
325        hovered: hovered || handle_hovered,
326        dragging,
327        page_direction,
328        interaction,
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335
336    fn make_state() -> WidgetInputState {
337        WidgetInputState::new()
338    }
339
340    #[test]
341    fn test_button_input_hover() {
342        let mut state = make_state();
343        let id = WidgetId::new("btn1");
344        let rect = WidgetRect::new(10.0, 10.0, 100.0, 40.0);
345
346        state.update_mouse(50.0, 30.0);
347        let result = handle_button_input(&state, &id, &rect, false);
348        assert!(result.hovered);
349        assert!(!result.clicked);
350    }
351
352    #[test]
353    fn test_button_input_disabled() {
354        let mut state = make_state();
355        let id = WidgetId::new("btn1");
356        let rect = WidgetRect::new(10.0, 10.0, 100.0, 40.0);
357
358        state.update_mouse(50.0, 30.0);
359        let result = handle_button_input(&state, &id, &rect, true);
360        assert!(!result.hovered);
361        assert!(!result.clicked);
362    }
363
364    #[test]
365    fn test_checkbox_toggle() {
366        let mut state = make_state();
367        let id = WidgetId::new("chk1");
368        let rect = WidgetRect::new(10.0, 10.0, 20.0, 20.0);
369
370        state.update_mouse(15.0, 15.0);
371        state.mouse_press(15.0, 15.0, Some(id.clone()));
372        state.mouse_release(15.0, 15.0, 1000.0);
373
374        let result = handle_checkbox_input(&state, &id, &rect, false, false);
375        assert!(result.hovered);
376    }
377
378    #[test]
379    fn test_slider_drag() {
380        let mut state = make_state();
381        let id = WidgetId::new("slider1");
382        let track_rect = WidgetRect::new(10.0, 10.0, 200.0, 20.0);
383        let handle_rect = WidgetRect::new(10.0, 10.0, 20.0, 20.0);
384
385        state.start_drag_with_value(id.clone(), 20.0, 20.0, 0.0);
386        state.update_mouse(110.0, 20.0);
387
388        let result = handle_slider_input(&state, &id, &track_rect, &handle_rect, 0.0, true, false);
389        assert!(result.dragging);
390        assert!(result.changed);
391        assert!(result.value > 0.4 && result.value < 0.6);
392    }
393
394    #[test]
395    fn test_widget_hit_test() {
396        let rect = WidgetRect::new(10.0, 10.0, 100.0, 50.0);
397        assert!(widget_hit_test(&rect, 50.0, 30.0));
398        assert!(!widget_hit_test(&rect, 5.0, 30.0));
399        assert!(!widget_hit_test(&rect, 50.0, 65.0));
400    }
401}