use crate::event::{MouseButton, UiEvent};
use crate::types::WidgetId;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct InteractionState {
hovered: Option<WidgetId>,
pressed: Option<WidgetId>,
}
impl InteractionState {
pub fn new() -> Self {
Self::default()
}
pub fn from_parts(hovered: Option<WidgetId>, pressed: Option<WidgetId>) -> Self {
Self { hovered, pressed }
}
pub fn hovered(&self) -> Option<&WidgetId> {
self.hovered.as_ref()
}
pub fn pressed(&self) -> Option<&WidgetId> {
self.pressed.as_ref()
}
pub fn is_hovered(&self, id: &WidgetId) -> bool {
self.hovered.as_ref() == Some(id)
}
pub fn is_pressed(&self, id: &WidgetId) -> bool {
self.pressed.as_ref() == Some(id)
}
pub fn set_hovered(&mut self, id: Option<WidgetId>) -> bool {
if self.hovered != id {
self.hovered = id;
true
} else {
false
}
}
pub fn set_pressed(&mut self, id: Option<WidgetId>) -> bool {
if self.pressed != id {
self.pressed = id;
true
} else {
false
}
}
pub fn clear(&mut self) -> bool {
let changed = self.hovered.is_some() || self.pressed.is_some();
self.hovered = None;
self.pressed = None;
changed
}
pub fn handle_mouse(
&mut self,
event: &UiEvent,
hit_test: impl FnOnce(f32, f32) -> Option<WidgetId>,
) -> bool {
match event {
UiEvent::MouseMoved { position, .. } => {
self.set_hovered(hit_test(position.x, position.y))
}
UiEvent::MouseDown {
widget,
position,
button: MouseButton::Left,
..
} => {
let id = (*widget).or_else(|| hit_test(position.x, position.y));
self.set_pressed(id)
}
UiEvent::MouseUp {
button: MouseButton::Left,
..
} => self.set_pressed(None),
_ => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::Point;
fn mouse_moved(x: f32, y: f32) -> UiEvent {
UiEvent::MouseMoved {
position: Point::new(x, y),
buttons: Default::default(),
}
}
fn mouse_down(widget: Option<WidgetId>, x: f32, y: f32) -> UiEvent {
UiEvent::MouseDown {
widget,
button: MouseButton::Left,
position: Point::new(x, y),
modifiers: Default::default(),
}
}
fn mouse_up(x: f32, y: f32) -> UiEvent {
UiEvent::MouseUp {
widget: None,
button: MouseButton::Left,
position: Point::new(x, y),
}
}
#[test]
fn new_state_has_nothing_hovered_or_pressed() {
let state = InteractionState::new();
assert_eq!(state.hovered(), None);
assert_eq!(state.pressed(), None);
}
#[test]
fn set_hovered_reports_change() {
let mut state = InteractionState::new();
assert!(state.set_hovered(Some(WidgetId::new("a"))));
assert_eq!(state.hovered(), Some(&WidgetId::new("a")));
assert!(!state.set_hovered(Some(WidgetId::new("a"))));
assert!(state.set_hovered(Some(WidgetId::new("b"))));
assert_eq!(state.hovered(), Some(&WidgetId::new("b")));
}
#[test]
fn is_hovered_and_is_pressed_key_by_widget_id() {
let mut state = InteractionState::new();
state.set_hovered(Some(WidgetId::new("btn-a")));
state.set_pressed(Some(WidgetId::new("btn-b")));
assert!(state.is_hovered(&WidgetId::new("btn-a")));
assert!(!state.is_hovered(&WidgetId::new("btn-b")));
assert!(state.is_pressed(&WidgetId::new("btn-b")));
assert!(!state.is_pressed(&WidgetId::new("btn-a")));
}
#[test]
fn clear_resets_both_and_reports_whether_anything_changed() {
let mut state = InteractionState::new();
assert!(!state.clear(), "clearing an empty state is a no-op");
state.set_hovered(Some(WidgetId::new("a")));
state.set_pressed(Some(WidgetId::new("a")));
assert!(state.clear());
assert_eq!(state.hovered(), None);
assert_eq!(state.pressed(), None);
}
#[test]
fn handle_mouse_moved_resolves_hover_via_hit_test() {
let mut state = InteractionState::new();
let changed = state.handle_mouse(&mouse_moved(5.0, 0.0), |x, _y| {
if x < 10.0 {
Some(WidgetId::new("hit"))
} else {
None
}
});
assert!(changed);
assert_eq!(state.hovered(), Some(&WidgetId::new("hit")));
let changed = state.handle_mouse(&mouse_moved(50.0, 0.0), |x, _y| {
if x < 10.0 {
Some(WidgetId::new("hit"))
} else {
None
}
});
assert!(changed);
assert_eq!(state.hovered(), None);
}
#[test]
fn handle_mouse_down_prefers_the_events_own_resolved_widget() {
let mut state = InteractionState::new();
let changed = state.handle_mouse(
&mouse_down(Some(WidgetId::new("resolved")), 0.0, 0.0),
|_x, _y| Some(WidgetId::new("from-hit-test")),
);
assert!(changed);
assert_eq!(state.pressed(), Some(&WidgetId::new("resolved")));
}
#[test]
fn handle_mouse_down_falls_back_to_hit_test_when_unresolved() {
let mut state = InteractionState::new();
let changed = state.handle_mouse(&mouse_down(None, 3.0, 0.0), |x, _y| {
if x < 10.0 {
Some(WidgetId::new("fallback"))
} else {
None
}
});
assert!(changed);
assert_eq!(state.pressed(), Some(&WidgetId::new("fallback")));
}
#[test]
fn handle_mouse_ignores_non_left_mouse_down() {
let mut state = InteractionState::new();
for button in [MouseButton::Right, MouseButton::Middle] {
let event = UiEvent::MouseDown {
widget: Some(WidgetId::new("btn")),
button,
position: Point::new(1.0, 0.0),
modifiers: Default::default(),
};
let changed = state.handle_mouse(&event, |_, _| Some(WidgetId::new("btn")));
assert!(
!changed,
"{button:?} MouseDown must not change pressed state"
);
assert_eq!(state.pressed(), None, "{button:?} must not press anything");
}
}
#[test]
fn handle_mouse_down_on_empty_space_clears_stale_pressed() {
let mut state = InteractionState::new();
state.set_pressed(Some(WidgetId::new("stale")));
let changed = state.handle_mouse(&mouse_down(None, 999.0, 999.0), |_, _| None);
assert!(changed, "clearing a stale pressed id is a change");
assert_eq!(state.pressed(), None);
}
#[test]
fn from_parts_round_trips_the_positional_pair() {
let state =
InteractionState::from_parts(Some(WidgetId::new("hov")), Some(WidgetId::new("prs")));
assert_eq!(state.hovered(), Some(&WidgetId::new("hov")));
assert_eq!(state.pressed(), Some(&WidgetId::new("prs")));
assert_eq!(
InteractionState::from_parts(None, None),
InteractionState::new()
);
}
#[test]
fn handle_mouse_up_clears_pressed() {
let mut state = InteractionState::new();
state.set_pressed(Some(WidgetId::new("held")));
let changed = state.handle_mouse(&mouse_up(0.0, 0.0), |_, _| None);
assert!(changed);
assert_eq!(state.pressed(), None);
}
#[test]
fn handle_mouse_ignores_unrelated_events() {
let mut state = InteractionState::new();
let changed = state.handle_mouse(
&UiEvent::KeyPressed {
key: crate::event::Key::Char('a'),
modifiers: Default::default(),
repeat: false,
},
|_, _| Some(WidgetId::new("irrelevant")),
);
assert!(!changed);
assert_eq!(state.hovered(), None);
}
}