1use super::widget_state::{WidgetId, WidgetInputState, WidgetInteraction};
6use crate::types::WidgetRect;
7
8#[derive(Clone, Debug, PartialEq)]
10#[derive(Default)]
11pub enum WidgetHitResult {
12 #[default]
14 None,
15 Widget { id: WidgetId },
17 CloseButton { parent_id: WidgetId },
19 DropdownItem { dropdown_id: WidgetId, item_index: usize },
21 ToolbarItem { toolbar_id: WidgetId, item_id: String },
23 SliderTrack { id: WidgetId },
25 SliderHandle { id: WidgetId },
27 ScrollbarTrack { id: WidgetId },
29 ScrollbarHandle { id: WidgetId },
31 Tab { parent_id: WidgetId, tab_index: usize },
33}
34
35
36pub fn widget_hit_test(rect: &WidgetRect, x: f64, y: f64) -> bool {
38 rect.contains(x, y)
39}
40
41#[derive(Clone, Debug, Default)]
47pub struct ButtonInputResult {
48 pub clicked: bool,
50 pub hovered: bool,
52 pub pressed: bool,
54 pub interaction: WidgetInteraction,
56}
57
58pub 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#[derive(Clone, Debug, Default)]
96pub struct CheckboxInputResult {
97 pub toggled: bool,
99 pub new_checked: bool,
101 pub hovered: bool,
103 pub interaction: WidgetInteraction,
105}
106
107pub 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#[derive(Clone, Debug, Default)]
148pub struct SliderInputResult {
149 pub changed: bool,
151 pub value: f64,
153 pub hovered: bool,
155 pub dragging: bool,
157 pub interaction: WidgetInteraction,
159}
160
161pub 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#[derive(Clone, Debug, Default)]
245pub struct ScrollbarInputResult {
246 pub changed: bool,
248 pub position: f64,
250 pub hovered: bool,
252 pub dragging: bool,
254 pub page_direction: Option<i32>,
256 pub interaction: WidgetInteraction,
258}
259
260#[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}