mod density;
mod focus;
mod hit;
mod pointer;
mod response;
mod sense;
mod shortcuts;
pub use density::Density;
pub use focus::FocusRing;
pub use hit::HitTester;
pub use pointer::Pointer;
pub use response::Response;
pub use sense::Sense;
pub use shortcuts::Shortcuts;
use retroglyph_core::{Event, KeyCode, MouseButton, Pos, Rect};
pub const DEFAULT_DRAG_THRESHOLD: u16 = 1;
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)]
pub struct Interaction<Id> {
pointer: Pointer,
hits: HitTester<Id>,
focus: FocusRing<Id>,
resolved_hover: Option<Id>,
resolved_pos: Option<Pos>,
resolved_press: bool,
resolved_release: bool,
resolved_secondary_press: bool,
resolved_secondary_release: bool,
resolved_scroll: i32,
active: Option<Id>,
secondary_active: Option<Id>,
drag_origin: Option<Pos>,
drag_threshold: u16,
activate_focused: bool,
}
impl<Id> Interaction<Id> {
#[must_use]
pub const fn new() -> Self {
Self {
pointer: Pointer::new(),
hits: HitTester::new(),
focus: FocusRing::new(),
resolved_hover: None,
resolved_pos: None,
resolved_press: false,
resolved_release: false,
resolved_secondary_press: false,
resolved_secondary_release: false,
resolved_scroll: 0,
active: None,
secondary_active: None,
drag_origin: None,
drag_threshold: DEFAULT_DRAG_THRESHOLD,
activate_focused: false,
}
}
#[must_use]
pub const fn with_drag_threshold(mut self, cells: u16) -> Self {
self.drag_threshold = cells;
self
}
#[must_use]
pub const fn pointer(&self) -> &Pointer {
&self.pointer
}
#[must_use]
pub const fn focus(&self) -> &FocusRing<Id> {
&self.focus
}
pub const fn focus_mut(&mut self) -> &mut FocusRing<Id> {
&mut self.focus
}
}
impl<Id: Copy + PartialEq> Interaction<Id> {
pub fn begin_frame(&mut self) {
self.resolved_pos = self.pointer.pos();
self.resolved_hover = self.resolved_pos.and_then(|pos| self.hits.topmost_at(pos));
self.resolved_press = self.pointer.pressed(MouseButton::Left);
self.resolved_release = self.pointer.released(MouseButton::Left);
self.resolved_secondary_press = self.pointer.pressed(MouseButton::Right);
self.resolved_secondary_release = self.pointer.released(MouseButton::Right);
self.resolved_scroll = self.pointer.scroll_delta();
if self.resolved_press {
self.active = self.resolved_hover;
self.drag_origin = self.resolved_pos;
}
if self.resolved_secondary_press {
self.secondary_active = self.resolved_hover;
}
self.hits.clear();
self.focus.begin_frame();
self.pointer.end_frame();
}
pub fn handle_event(&mut self, event: &Event) {
self.pointer.handle_event(event);
self.focus.handle_event(event);
self.activate_focused |= is_activation_key(event);
}
pub fn interact(&mut self, rect: Rect, id: Id, sense: Sense) -> Response {
if sense.wants_pointer() {
self.hits.push(rect, id);
}
if sense.contains(Sense::FOCUSABLE) {
self.focus.register(id);
}
let hovered = sense.wants_pointer() && self.resolved_hover == Some(id);
let is_active = self.active == Some(id);
let senses_click = sense.contains(Sense::CLICK);
let key_activated = senses_click
&& sense.contains(Sense::FOCUSABLE)
&& self.focus.is_focused(id)
&& self.activate_focused;
let released_here = is_active && self.resolved_release;
let dragging = is_active && sense.contains(Sense::DRAG) && self.past_drag_threshold();
let held = senses_click
&& is_active
&& self.pointer.is_down(MouseButton::Left)
&& self.pointer.pos().is_some_and(|pos| rect.contains_pos(pos));
if senses_click && released_here && hovered && !dragging {
self.focus.request(id);
}
let scrollable_here = sense.wants_pointer()
&& sense.contains(Sense::SCROLL)
&& self.resolved_pos.is_some_and(|pos| rect.contains_pos(pos));
let secondary_is_active = self.secondary_active == Some(id);
let secondary_clicked = sense.contains(Sense::SECONDARY_CLICK)
&& secondary_is_active
&& self.resolved_secondary_release
&& hovered;
Response {
hovered,
pressed: (is_active && self.resolved_press) || key_activated,
released: released_here || key_activated,
clicked: (senses_click && released_here && hovered && !dragging) || key_activated,
held,
dragging,
focused: self.focus.is_focused(id),
secondary_clicked,
scroll_delta: if scrollable_here {
self.resolved_scroll
} else {
0
},
}
}
pub const fn end_frame(&mut self) {
if self.resolved_release {
self.active = None;
self.drag_origin = None;
}
if self.resolved_secondary_release {
self.secondary_active = None;
}
self.activate_focused = false;
}
fn past_drag_threshold(&self) -> bool {
let (Some(origin), Some(pos)) = (self.drag_origin, self.pointer.pos()) else {
return false;
};
origin.x.abs_diff(pos.x).max(origin.y.abs_diff(pos.y)) > self.drag_threshold
}
}
impl<Id> Default for Interaction<Id> {
fn default() -> Self {
Self::new()
}
}
const fn is_activation_key(event: &Event) -> bool {
let Event::Key(key) = event else {
return false;
};
key.is_down() && matches!(key.code, KeyCode::Enter | KeyCode::Char(' '))
}
#[cfg(test)]
mod tests {
use retroglyph_core::{KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Id {
Save,
Cancel,
}
fn click_at(interaction: &mut Interaction<Id>, pos: Pos) {
interaction.handle_event(&Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
position: pos,
pixel_position: None,
modifiers: KeyModifiers::NONE,
}));
interaction.handle_event(&Event::Mouse(MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
position: pos,
pixel_position: None,
modifiers: KeyModifiers::NONE,
}));
}
fn frame_with_events(
interaction: &mut Interaction<Id>,
events: &[Event],
) -> (Response, Response) {
interaction.begin_frame();
for event in events {
interaction.handle_event(event);
}
let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::click());
let cancel = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::click());
interaction.end_frame();
(save, cancel)
}
fn frame(interaction: &mut Interaction<Id>) -> (Response, Response) {
frame_with_events(interaction, &[])
}
#[test]
fn click_is_resolved_one_frame_after_the_pointer_event() {
let mut interaction = Interaction::<Id>::new();
let (save1, _) = frame(&mut interaction);
assert!(!save1.clicked());
click_at(&mut interaction, Pos::new(2, 0));
let (save2, cancel2) = frame(&mut interaction);
assert!(save2.clicked());
assert!(!cancel2.clicked());
}
#[test]
fn press_and_release_in_separate_frames_still_resolves_a_click() {
let mut interaction = Interaction::<Id>::new();
let _ = frame(&mut interaction);
let down = Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
position: Pos::new(2, 0),
pixel_position: None,
modifiers: KeyModifiers::NONE,
});
let (save2, _) = frame_with_events(&mut interaction, &[down]);
assert!(!save2.pressed());
let (save3, _) = frame(&mut interaction);
assert!(save3.pressed());
let up = Event::Mouse(MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
position: Pos::new(2, 0),
pixel_position: None,
modifiers: KeyModifiers::NONE,
});
let _ = frame_with_events(&mut interaction, &[up]);
let (save5, _) = frame(&mut interaction);
assert!(save5.clicked());
}
#[test]
fn hover_follows_the_pointer_without_a_click() {
let mut interaction = Interaction::<Id>::new();
let _ = frame(&mut interaction);
interaction.handle_event(&Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
position: Pos::new(7, 0),
pixel_position: None,
modifiers: KeyModifiers::NONE,
}));
let (save, cancel) = frame(&mut interaction);
assert!(!save.hovered());
assert!(cancel.hovered());
assert!(!cancel.clicked());
}
#[test]
fn tab_focuses_then_enter_activates_without_any_pointer() {
let mut interaction = Interaction::<Id>::new();
let _ = frame(&mut interaction);
let tab = Event::Key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
let (save, _) = frame_with_events(&mut interaction, &[tab]);
assert!(save.focused());
assert!(!save.clicked());
let enter = Event::Key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
let (save, cancel) = frame_with_events(&mut interaction, &[enter]);
assert!(save.clicked());
assert!(!cancel.clicked());
}
#[test]
fn drag_past_threshold_suppresses_the_click() {
let mut interaction = Interaction::<Id>::new().with_drag_threshold(1);
let _ = frame(&mut interaction);
interaction.handle_event(&Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
position: Pos::new(2, 0),
pixel_position: None,
modifiers: KeyModifiers::NONE,
}));
interaction.begin_frame();
let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::drag());
assert!(!save.dragging()); interaction.end_frame();
interaction.handle_event(&Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
position: Pos::new(4, 0), pixel_position: None,
modifiers: KeyModifiers::NONE,
}));
interaction.begin_frame();
let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::drag());
assert!(save.dragging());
interaction.end_frame();
interaction.handle_event(&Event::Mouse(MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
position: Pos::new(4, 0),
pixel_position: None,
modifiers: KeyModifiers::NONE,
}));
interaction.begin_frame();
let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::drag());
assert!(!save.clicked()); assert!(save.released());
}
#[test]
fn held_is_true_while_pressed_and_hovering_and_false_once_the_pointer_slides_off() {
let mut interaction = Interaction::<Id>::new();
let _ = frame(&mut interaction);
interaction.handle_event(&Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
position: Pos::new(2, 0), pixel_position: None,
modifiers: KeyModifiers::NONE,
}));
let (save, _) = frame(&mut interaction);
assert!(save.held());
interaction.handle_event(&Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
position: Pos::new(20, 0), pixel_position: None,
modifiers: KeyModifiers::NONE,
}));
interaction.begin_frame();
let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::click());
let _ = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::click());
interaction.end_frame();
assert!(!save.held());
interaction.handle_event(&Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
position: Pos::new(2, 0), pixel_position: None,
modifiers: KeyModifiers::NONE,
}));
interaction.begin_frame();
let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::click());
let _ = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::click());
interaction.end_frame();
assert!(save.held());
interaction.handle_event(&Event::Mouse(MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
position: Pos::new(2, 0),
pixel_position: None,
modifiers: KeyModifiers::NONE,
}));
interaction.begin_frame();
let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::click());
let _ = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::click());
interaction.end_frame();
assert!(!save.held());
assert!(save.released());
assert!(save.clicked());
}
#[test]
fn held_requires_click_sense() {
let mut interaction = Interaction::<Id>::new();
interaction.begin_frame();
let _ = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::hover());
interaction.end_frame();
interaction.handle_event(&Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
position: Pos::new(2, 0), pixel_position: None,
modifiers: KeyModifiers::NONE,
}));
interaction.begin_frame();
let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::hover());
interaction.end_frame();
assert!(!save.held());
}
#[test]
fn scroll_reports_only_while_hovered_and_sensed() {
let mut interaction = Interaction::<Id>::new();
let _ = frame(&mut interaction);
interaction.handle_event(&Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
position: Pos::new(2, 0),
pixel_position: None,
modifiers: KeyModifiers::NONE,
}));
interaction.handle_event(&Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollDown,
position: Pos::new(2, 0),
pixel_position: None,
modifiers: KeyModifiers::NONE,
}));
interaction.begin_frame();
let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::scroll());
let cancel = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::scroll());
interaction.end_frame();
assert_eq!(save.scroll_delta(), 1);
assert_eq!(cancel.scroll_delta(), 0); }
#[test]
fn scroll_reaches_a_container_through_an_overlapping_child() {
let mut interaction = Interaction::<Id>::new();
interaction.begin_frame();
let _ = interaction.interact(Rect::new(0, 0, 10, 1), Id::Save, Sense::scroll());
let _ = interaction.interact(
Rect::new(0, 0, 10, 1),
Id::Cancel,
Sense::HOVER | Sense::CLICK,
);
interaction.end_frame();
interaction.handle_event(&Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollDown,
position: Pos::new(2, 0),
pixel_position: None,
modifiers: KeyModifiers::NONE,
}));
interaction.begin_frame();
let container = interaction.interact(Rect::new(0, 0, 10, 1), Id::Save, Sense::scroll());
let child = interaction.interact(
Rect::new(0, 0, 10, 1),
Id::Cancel,
Sense::HOVER | Sense::CLICK,
);
interaction.end_frame();
assert_eq!(container.scroll_delta(), 1);
assert!(child.hovered()); }
#[test]
fn hover_only_sense_never_reports_clicked() {
let mut interaction = Interaction::<Id>::new();
let _ = frame(&mut interaction);
click_at(&mut interaction, Pos::new(2, 0));
interaction.begin_frame();
let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::hover());
interaction.end_frame();
assert!(save.hovered());
assert!(!save.clicked());
}
fn right_click_at(interaction: &mut Interaction<Id>, pos: Pos) {
interaction.handle_event(&Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Right),
position: pos,
pixel_position: None,
modifiers: KeyModifiers::NONE,
}));
interaction.handle_event(&Event::Mouse(MouseEvent {
kind: MouseEventKind::Up(MouseButton::Right),
position: pos,
pixel_position: None,
modifiers: KeyModifiers::NONE,
}));
}
#[test]
fn secondary_click_is_independent_of_the_primary_button() {
let mut interaction = Interaction::<Id>::new();
fn frame_secondary(interaction: &mut Interaction<Id>) -> (Response, Response) {
interaction.begin_frame();
let save = interaction.interact(
Rect::new(0, 0, 5, 1),
Id::Save,
Sense::click() | Sense::SECONDARY_CLICK,
);
let cancel = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::click());
interaction.end_frame();
(save, cancel)
}
let _ = frame_secondary(&mut interaction); right_click_at(&mut interaction, Pos::new(2, 0));
let (save, cancel) = frame_secondary(&mut interaction); assert!(save.secondary_clicked());
assert!(!save.clicked()); assert!(!cancel.secondary_clicked());
}
#[test]
fn secondary_click_not_sensed_never_reports_even_when_right_clicked() {
let mut interaction = Interaction::<Id>::new();
let _ = frame(&mut interaction); right_click_at(&mut interaction, Pos::new(2, 0));
let (save, _) = frame(&mut interaction);
assert!(!save.secondary_clicked()); }
}