use super::widget_state::{WidgetId, WidgetInputState, WidgetInteraction};
use crate::types::WidgetRect;
#[derive(Clone, Debug, PartialEq)]
#[derive(Default)]
pub enum WidgetHitResult {
#[default]
None,
Widget { id: WidgetId },
CloseButton { parent_id: WidgetId },
DropdownItem { dropdown_id: WidgetId, item_index: usize },
ToolbarItem { toolbar_id: WidgetId, item_id: String },
SliderTrack { id: WidgetId },
SliderHandle { id: WidgetId },
ScrollbarTrack { id: WidgetId },
ScrollbarHandle { id: WidgetId },
Tab { parent_id: WidgetId, tab_index: usize },
}
pub fn widget_hit_test(rect: &WidgetRect, x: f64, y: f64) -> bool {
rect.contains(x, y)
}
#[derive(Clone, Debug, Default)]
pub struct ButtonInputResult {
pub clicked: bool,
pub hovered: bool,
pub pressed: bool,
pub interaction: WidgetInteraction,
}
pub fn handle_button_input(
state: &WidgetInputState,
id: &WidgetId,
rect: &WidgetRect,
disabled: bool,
) -> ButtonInputResult {
if disabled {
return ButtonInputResult::default();
}
let (mx, my) = state.hover.mouse_pos;
let hovered = rect.contains(mx, my);
let pressed = hovered && state.hover.mouse_pressed && state.active.as_ref() == Some(id);
let clicked = hovered && state.active.as_ref() == Some(id) && !state.hover.mouse_pressed;
let interaction = if pressed {
WidgetInteraction::Press
} else if hovered {
WidgetInteraction::Hover
} else {
WidgetInteraction::None
};
ButtonInputResult {
clicked,
hovered,
pressed,
interaction,
}
}
#[derive(Clone, Debug, Default)]
pub struct CheckboxInputResult {
pub toggled: bool,
pub new_checked: bool,
pub hovered: bool,
pub interaction: WidgetInteraction,
}
pub fn handle_checkbox_input(
state: &WidgetInputState,
id: &WidgetId,
rect: &WidgetRect,
current_checked: bool,
disabled: bool,
) -> CheckboxInputResult {
if disabled {
return CheckboxInputResult::default();
}
let (mx, my) = state.hover.mouse_pos;
let hovered = rect.contains(mx, my);
let clicked = hovered && state.active.as_ref() == Some(id) && !state.hover.mouse_pressed;
let toggled = clicked;
let new_checked = if toggled { !current_checked } else { current_checked };
let interaction = if hovered && state.hover.mouse_pressed {
WidgetInteraction::Press
} else if hovered {
WidgetInteraction::Hover
} else {
WidgetInteraction::None
};
CheckboxInputResult {
toggled,
new_checked,
hovered,
interaction,
}
}
#[derive(Clone, Debug, Default)]
pub struct SliderInputResult {
pub changed: bool,
pub value: f64,
pub hovered: bool,
pub dragging: bool,
pub interaction: WidgetInteraction,
}
pub fn handle_slider_input(
state: &WidgetInputState,
id: &WidgetId,
track_rect: &WidgetRect,
handle_rect: &WidgetRect,
current_value: f64,
horizontal: bool,
disabled: bool,
) -> SliderInputResult {
if disabled {
return SliderInputResult {
value: current_value,
..Default::default()
};
}
let (mx, my) = state.hover.mouse_pos;
let hovered = track_rect.contains(mx, my);
let handle_hovered = handle_rect.contains(mx, my);
let dragging = state.drag.is_dragging(id);
let mut value = current_value;
let mut changed = false;
if dragging {
let (dx, dy) = state.drag.delta();
let range = if horizontal {
track_rect.width - handle_rect.width
} else {
track_rect.height - handle_rect.height
};
if range > 0.0 {
let delta = if horizontal { dx } else { dy };
let delta_normalized = delta / range;
value = (state.drag.initial_value + delta_normalized).clamp(0.0, 1.0);
changed = (value - current_value).abs() > 0.0001;
}
}
if hovered && !handle_hovered && state.hover.mouse_pressed && state.active.as_ref() == Some(id) && !dragging {
let range = if horizontal {
track_rect.width - handle_rect.width
} else {
track_rect.height - handle_rect.height
};
if range > 0.0 {
let pos = if horizontal {
mx - track_rect.x - handle_rect.width / 2.0
} else {
my - track_rect.y - handle_rect.height / 2.0
};
value = (pos / range).clamp(0.0, 1.0);
changed = true;
}
}
let interaction = if dragging {
WidgetInteraction::Drag
} else if handle_hovered && state.hover.mouse_pressed {
WidgetInteraction::Press
} else if hovered || handle_hovered {
WidgetInteraction::Hover
} else {
WidgetInteraction::None
};
SliderInputResult {
changed,
value,
hovered: hovered || handle_hovered,
dragging,
interaction,
}
}
#[derive(Clone, Debug, Default)]
pub struct ScrollbarInputResult {
pub changed: bool,
pub position: f64,
pub hovered: bool,
pub dragging: bool,
pub page_direction: Option<i32>,
pub interaction: WidgetInteraction,
}
#[allow(clippy::too_many_arguments)]
pub fn handle_scrollbar_input(
state: &WidgetInputState,
id: &WidgetId,
track_rect: &WidgetRect,
handle_rect: &WidgetRect,
current_position: f64,
handle_size_ratio: f64,
horizontal: bool,
disabled: bool,
) -> ScrollbarInputResult {
if disabled {
return ScrollbarInputResult {
position: current_position,
..Default::default()
};
}
let (mx, my) = state.hover.mouse_pos;
let hovered = track_rect.contains(mx, my);
let handle_hovered = handle_rect.contains(mx, my);
let dragging = state.drag.is_dragging(id);
let mut position = current_position;
let mut changed = false;
let mut page_direction = None;
if dragging {
let (dx, dy) = state.drag.delta();
let track_size = if horizontal { track_rect.width } else { track_rect.height };
let usable_range = track_size * (1.0 - handle_size_ratio);
if usable_range > 0.0 {
let delta = if horizontal { dx } else { dy };
let delta_normalized = delta / usable_range;
position = (state.drag.initial_value + delta_normalized).clamp(0.0, 1.0);
changed = (position - current_position).abs() > 0.0001;
}
}
if hovered && !handle_hovered && state.hover.mouse_pressed && state.active.as_ref() == Some(id) && !dragging {
let handle_pos = if horizontal { handle_rect.x } else { handle_rect.y };
let mouse_pos = if horizontal { mx } else { my };
if mouse_pos < handle_pos {
page_direction = Some(-1);
} else {
page_direction = Some(1);
}
}
let interaction = if dragging {
WidgetInteraction::Drag
} else if handle_hovered && state.hover.mouse_pressed {
WidgetInteraction::Press
} else if hovered || handle_hovered {
WidgetInteraction::Hover
} else {
WidgetInteraction::None
};
ScrollbarInputResult {
changed,
position,
hovered: hovered || handle_hovered,
dragging,
page_direction,
interaction,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_state() -> WidgetInputState {
WidgetInputState::new()
}
#[test]
fn test_button_input_hover() {
let mut state = make_state();
let id = WidgetId::new("btn1");
let rect = WidgetRect::new(10.0, 10.0, 100.0, 40.0);
state.update_mouse(50.0, 30.0);
let result = handle_button_input(&state, &id, &rect, false);
assert!(result.hovered);
assert!(!result.clicked);
}
#[test]
fn test_button_input_disabled() {
let mut state = make_state();
let id = WidgetId::new("btn1");
let rect = WidgetRect::new(10.0, 10.0, 100.0, 40.0);
state.update_mouse(50.0, 30.0);
let result = handle_button_input(&state, &id, &rect, true);
assert!(!result.hovered);
assert!(!result.clicked);
}
#[test]
fn test_checkbox_toggle() {
let mut state = make_state();
let id = WidgetId::new("chk1");
let rect = WidgetRect::new(10.0, 10.0, 20.0, 20.0);
state.update_mouse(15.0, 15.0);
state.mouse_press(15.0, 15.0, Some(id.clone()));
state.mouse_release(15.0, 15.0, 1000.0);
let result = handle_checkbox_input(&state, &id, &rect, false, false);
assert!(result.hovered);
}
#[test]
fn test_slider_drag() {
let mut state = make_state();
let id = WidgetId::new("slider1");
let track_rect = WidgetRect::new(10.0, 10.0, 200.0, 20.0);
let handle_rect = WidgetRect::new(10.0, 10.0, 20.0, 20.0);
state.start_drag_with_value(id.clone(), 20.0, 20.0, 0.0);
state.update_mouse(110.0, 20.0);
let result = handle_slider_input(&state, &id, &track_rect, &handle_rect, 0.0, true, false);
assert!(result.dragging);
assert!(result.changed);
assert!(result.value > 0.4 && result.value < 0.6);
}
#[test]
fn test_widget_hit_test() {
let rect = WidgetRect::new(10.0, 10.0, 100.0, 50.0);
assert!(widget_hit_test(&rect, 50.0, 30.0));
assert!(!widget_hit_test(&rect, 5.0, 30.0));
assert!(!widget_hit_test(&rect, 50.0, 65.0));
}
}