mod consumed;
mod density;
mod focus;
mod hit;
mod pointer;
mod response;
mod sense;
mod shortcuts;
pub use consumed::Consumed;
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 alloc::vec::Vec;
use core::time::Duration;
use retroglyph_core::app::Frame;
use retroglyph_core::event::{Event, KeyCode, MouseButton};
use retroglyph_core::grid::{Pos, Rect};
use retroglyph_core::surface::Surface;
use crate::animate::Tween;
use crate::ui::Ui;
pub const DEFAULT_DRAG_THRESHOLD: u16 = 1;
pub const DEFAULT_DOUBLE_CLICK_WINDOW: u16 = 30;
#[derive(Debug, Clone)]
pub struct Interaction<Id> {
pointer: Pointer,
hits: HitTester<Id>,
prev_hits: HitTester<Id>,
focus: FocusRing<Id>,
resolved_hover: Option<Id>,
resolved: Pointer,
active: Option<Id>,
secondary_active: Option<Id>,
drag_origin: Option<Pos>,
drag_threshold: u16,
activate_focused: bool,
frame_count: u64,
last_click: Option<(Id, u64)>,
double_click_window: u16,
prev_focused: Option<Id>,
tweens: Vec<(Id, Tween)>,
}
impl<Id> Interaction<Id> {
#[must_use]
pub const fn new() -> Self {
Self {
pointer: Pointer::new(),
hits: HitTester::new(),
prev_hits: HitTester::new(),
focus: FocusRing::new(),
resolved_hover: None,
resolved: Pointer::new(),
active: None,
secondary_active: None,
drag_origin: None,
drag_threshold: DEFAULT_DRAG_THRESHOLD,
activate_focused: false,
frame_count: 0,
last_click: None,
double_click_window: DEFAULT_DOUBLE_CLICK_WINDOW,
prev_focused: None,
tweens: Vec::new(),
}
}
#[must_use]
pub const fn with_drag_threshold(mut self, cells: u16) -> Self {
self.drag_threshold = cells;
self
}
#[must_use]
pub const fn with_double_click_window(mut self, frames: u16) -> Self {
self.double_click_window = frames;
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> {
#[must_use]
pub const fn hovered(&self) -> Option<Id> {
self.resolved_hover
}
pub fn frame<R>(
&mut self,
surface: &mut Surface<'_>,
f: impl FnOnce(&mut Ui<'_, '_, Id>) -> R,
) -> R {
self.begin_frame();
let result = f(&mut Ui::new(surface, self));
self.end_frame();
result
}
pub fn begin_frame(&mut self) {
self.frame_count += 1;
self.prev_focused = self.focus.focused();
self.resolved = self.pointer;
self.resolved_hover = self
.resolved
.pos()
.and_then(|pos| self.hits.topmost_at(pos));
if self.resolved.pressed(MouseButton::Left) {
self.active = self.resolved_hover;
self.drag_origin = self.resolved.pos();
}
if self.resolved.pressed(MouseButton::Right) {
self.secondary_active = self.resolved_hover;
}
core::mem::swap(&mut self.hits, &mut self.prev_hits);
self.hits.clear();
self.focus.begin_frame();
self.pointer.end_frame();
}
#[must_use]
pub fn handle_event(&mut self, event: &Event) -> Consumed {
let claimed = match event {
Event::Mouse(mouse) => self.prev_hits.topmost_at(mouse.position).is_some(),
Event::Key(key)
if key.is_down() && matches!(key.code, KeyCode::Tab | KeyCode::BackTab) =>
{
self.focus.has_order()
}
Event::Key(_) => is_activation_key(event) && self.focus.focused().is_some(),
Event::Resize(..) | Event::Paste(_) | Event::FocusGained | Event::FocusLost | _ => {
false
}
};
self.pointer.handle_event(event);
self.focus.handle_event(event);
self.activate_focused |= is_activation_key(event);
claimed.into()
}
#[must_use]
pub fn wants_pointer(&self) -> bool {
self.pointer
.pos()
.is_some_and(|pos| self.prev_hits.topmost_at(pos).is_some())
}
#[must_use]
pub const fn wants_keyboard(&self) -> bool {
self.focus.focused().is_some()
}
pub fn animate(&mut self, id: Id, target: bool, duration: Duration, frame: &Frame) -> f32 {
let target_value = f32::from(target);
let index = if let Some(index) = self
.tweens
.iter()
.position(|(existing, ..)| *existing == id)
{
let (_, tween) = &mut self.tweens[index];
#[allow(clippy::float_cmp)]
if tween.target() != target_value {
tween.retarget(target_value);
}
tween.update(frame.delta);
index
} else {
self.tweens.push((
id,
Tween::new(target_value, target_value).duration(duration),
));
self.tweens.len() - 1
};
let (_, tween) = &self.tweens[index];
let value = tween.value();
if tween.is_finished() {
self.tweens.remove(index);
}
value
}
pub fn push_barrier(&mut self, rect: Rect) {
self.hits.push_barrier(rect);
}
pub fn interact(&mut self, rect: Rect, id: Id, sense: Sense) -> Response<Id> {
let disabled = sense.is_disabled();
if sense.wants_pointer() {
self.hits.push(rect, id);
}
if sense.contains(Sense::FOCUSABLE) && !disabled {
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) && !disabled;
let is_focused = !disabled && self.focus.is_focused(id);
let key_activated =
senses_click && sense.contains(Sense::FOCUSABLE) && is_focused && self.activate_focused;
let released_here = senses_click && is_active && self.resolved.released(MouseButton::Left);
let dragging =
is_active && sense.contains(Sense::DRAG) && !disabled && 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 && sense.contains(Sense::FOCUSABLE) && released_here && hovered && !dragging
{
self.focus.request(id);
}
let scrollable_here = !disabled
&& sense.wants_pointer()
&& sense.contains(Sense::SCROLL)
&& self
.resolved
.pos()
.is_some_and(|pos| rect.contains_pos(pos));
let secondary_is_active = !disabled && self.secondary_active == Some(id);
let secondary_clicked = sense.contains(Sense::SECONDARY_CLICK)
&& !disabled
&& secondary_is_active
&& self.resolved.released(MouseButton::Right)
&& hovered;
let clicked = (senses_click && released_here && hovered && !dragging) || key_activated;
let double_clicked = clicked
&& self.last_click.is_some_and(|(last_id, last_frame)| {
last_id == id
&& self.frame_count.saturating_sub(last_frame)
<= u64::from(self.double_click_window)
});
if clicked {
self.last_click = if double_clicked {
None
} else {
Some((id, self.frame_count))
};
}
let press_origin = (senses_click && is_active)
.then_some(self.drag_origin)
.flatten();
let drag_delta = press_origin.zip(self.pointer.pos()).map(|(origin, pos)| {
(
i32::from(pos.x) - i32::from(origin.x),
i32::from(pos.y) - i32::from(origin.y),
)
});
let gained_focus = is_focused && self.prev_focused != Some(id);
let lost_focus = !is_focused && self.prev_focused == Some(id);
Response {
id,
hovered,
pressed: (senses_click && is_active && self.resolved.pressed(MouseButton::Left))
|| key_activated,
released: released_here || key_activated,
clicked,
double_clicked,
held,
dragging,
focused: is_focused,
gained_focus,
lost_focus,
secondary_clicked,
disabled,
scroll_delta: if scrollable_here {
self.resolved.scroll_delta()
} else {
0
},
pointer_pos: hovered.then_some(self.resolved.pos()).flatten(),
press_origin,
drag_delta,
rect,
}
}
pub const fn end_frame(&mut self) {
if self.resolved.released(MouseButton::Left) {
self.active = None;
self.drag_origin = None;
}
if self.resolved.released(MouseButton::Right) {
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 alloc::borrow::ToOwned as _;
use retroglyph_core::event::{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) {
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Down(MouseButton::Left),
pos,
KeyModifiers::NONE,
)));
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Up(MouseButton::Left),
pos,
KeyModifiers::NONE,
)));
}
fn frame_with_events(
interaction: &mut Interaction<Id>,
events: &[Event],
) -> (Response<Id>, Response<Id>) {
interaction.begin_frame();
for event in events {
let _ = 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<Id>, Response<Id>) {
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::new(
MouseEventKind::Down(MouseButton::Left),
Pos::new(2, 0),
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::new(
MouseEventKind::Up(MouseButton::Left),
Pos::new(2, 0),
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);
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Moved,
Pos::new(7, 0),
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);
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Down(MouseButton::Left),
Pos::new(2, 0),
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();
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Moved,
Pos::new(4, 0), 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();
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Up(MouseButton::Left),
Pos::new(4, 0),
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);
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Down(MouseButton::Left),
Pos::new(2, 0), KeyModifiers::NONE,
)));
let (save, _) = frame(&mut interaction);
assert!(save.held());
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Moved,
Pos::new(20, 0), 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());
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Moved,
Pos::new(2, 0), 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());
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Up(MouseButton::Left),
Pos::new(2, 0),
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();
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Down(MouseButton::Left),
Pos::new(2, 0), 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 hover_only_sense_must_not_report_pressed_or_released() {
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.pressed());
assert!(!save.released());
assert_eq!(save.press_origin(), None);
assert_eq!(save.drag_delta(), None);
}
#[test]
fn sense_none_returns_response_default() {
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::NONE);
interaction.end_frame();
assert!(!save.hovered());
assert!(!save.pressed());
assert!(!save.released());
assert!(!save.clicked());
assert!(!save.double_clicked());
assert!(!save.held());
assert!(!save.dragging());
assert!(!save.focused());
assert!(!save.gained_focus());
assert!(!save.lost_focus());
assert!(!save.secondary_clicked());
assert!(!save.disabled());
assert_eq!(save.scroll_delta(), 0);
assert_eq!(save.pointer_pos(), None);
assert_eq!(save.press_origin(), None);
assert_eq!(save.drag_delta(), None);
}
#[test]
fn pointer_pos_matches_hover_and_is_none_elsewhere() {
let mut interaction = Interaction::<Id>::new();
let _ = frame(&mut interaction);
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Moved,
Pos::new(2, 0),
KeyModifiers::NONE,
)));
let (save, cancel) = frame(&mut interaction);
assert_eq!(save.pointer_pos(), Some(Pos::new(2, 0)));
assert_eq!(cancel.pointer_pos(), None);
}
#[test]
fn press_origin_stays_put_for_the_duration_of_a_drag() {
let mut interaction = Interaction::<Id>::new().with_drag_threshold(1);
let _ = frame(&mut interaction);
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Down(MouseButton::Left),
Pos::new(2, 0),
KeyModifiers::NONE,
)));
interaction.begin_frame();
let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::drag());
let cancel = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::drag());
interaction.end_frame();
assert_eq!(save.press_origin(), Some(Pos::new(2, 0)));
assert_eq!(cancel.press_origin(), None);
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Moved,
Pos::new(4, 0),
KeyModifiers::NONE,
)));
interaction.begin_frame();
let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::drag());
interaction.end_frame();
assert_eq!(save.press_origin(), Some(Pos::new(2, 0)));
}
#[test]
fn drag_delta_keeps_reporting_once_the_pointer_leaves_the_rect() {
let mut interaction = Interaction::<Id>::new().with_drag_threshold(1);
let _ = frame(&mut interaction);
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Down(MouseButton::Left),
Pos::new(2, 0),
KeyModifiers::NONE,
)));
interaction.begin_frame();
let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::drag());
let cancel = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::drag());
interaction.end_frame();
assert_eq!(save.drag_delta(), Some((0, 0))); assert_eq!(cancel.drag_delta(), None);
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Moved,
Pos::new(20, 3),
KeyModifiers::NONE,
)));
interaction.begin_frame();
let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::drag());
interaction.end_frame();
assert_eq!(save.pointer_pos(), None); assert_eq!(save.drag_delta(), Some((18, 3)));
}
#[test]
fn scroll_reports_only_while_hovered_and_sensed() {
let mut interaction = Interaction::<Id>::new();
let _ = frame(&mut interaction);
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Moved,
Pos::new(2, 0),
KeyModifiers::NONE,
)));
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Scroll { dx: 0.0, dy: -1.0 },
Pos::new(2, 0),
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();
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Scroll { dx: 0.0, dy: -1.0 },
Pos::new(2, 0),
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) {
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Down(MouseButton::Right),
pos,
KeyModifiers::NONE,
)));
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Up(MouseButton::Right),
pos,
KeyModifiers::NONE,
)));
}
#[test]
fn secondary_click_is_independent_of_the_primary_button() {
fn frame_secondary(interaction: &mut Interaction<Id>) -> (Response<Id>, Response<Id>) {
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 mut interaction = Interaction::<Id>::new();
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()); }
fn frame_disabled(interaction: &mut Interaction<Id>) -> Response<Id> {
interaction.begin_frame();
let save = interaction.interact(
Rect::new(0, 0, 5, 1),
Id::Save,
Sense::click() | Sense::SECONDARY_CLICK | Sense::DISABLED,
);
interaction.end_frame();
save
}
#[test]
fn disabled_widget_never_reports_click_press_or_held() {
let mut interaction = Interaction::<Id>::new();
let _ = frame_disabled(&mut interaction);
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Down(MouseButton::Left),
Pos::new(2, 0), KeyModifiers::NONE,
)));
let save = frame_disabled(&mut interaction); assert!(!save.pressed());
assert!(!save.held());
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Up(MouseButton::Left),
Pos::new(2, 0),
KeyModifiers::NONE,
)));
let save = frame_disabled(&mut interaction); assert!(!save.released());
assert!(!save.clicked());
}
#[test]
fn disabled_widget_never_reports_dragging_or_scroll() {
let mut interaction = Interaction::<Id>::new().with_drag_threshold(1);
let sense = Sense::drag() | Sense::SCROLL | Sense::DISABLED;
interaction.begin_frame();
let _ = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, sense);
interaction.end_frame();
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Down(MouseButton::Left),
Pos::new(2, 0), KeyModifiers::NONE,
)));
interaction.begin_frame();
let _ = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, sense);
interaction.end_frame();
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Moved,
Pos::new(4, 0), KeyModifiers::NONE,
)));
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Scroll { dx: 0.0, dy: -1.0 },
Pos::new(4, 0),
KeyModifiers::NONE,
)));
interaction.begin_frame();
let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, sense);
interaction.end_frame();
assert!(!save.dragging());
assert_eq!(save.scroll_delta(), 0);
assert!(save.hovered()); }
#[test]
fn disabled_widget_still_reports_hover() {
let mut interaction = Interaction::<Id>::new();
let _ = frame_disabled(&mut interaction);
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Moved,
Pos::new(2, 0), KeyModifiers::NONE,
)));
let save = frame_disabled(&mut interaction);
assert!(save.hovered()); }
#[test]
fn disabled_widget_never_reports_secondary_click() {
let mut interaction = Interaction::<Id>::new();
let _ = frame_disabled(&mut interaction);
right_click_at(&mut interaction, Pos::new(2, 0));
let save = frame_disabled(&mut interaction);
assert!(!save.secondary_clicked());
}
#[test]
fn disabled_widget_is_skipped_by_focus_ring() {
let mut interaction = Interaction::<Id>::new();
interaction.begin_frame();
let _ = interaction.interact(
Rect::new(0, 0, 5, 1),
Id::Save,
Sense::click() | Sense::DISABLED,
);
let _ = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::click());
interaction.end_frame();
let tab = Event::Key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
interaction.begin_frame(); let _ = interaction.handle_event(&tab);
assert_eq!(interaction.focus().focused(), Some(Id::Cancel));
}
#[test]
fn disabling_a_focused_widget_clears_its_response_and_self_heals_on_the_next_tab() {
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());
interaction.begin_frame();
let save = interaction.interact(
Rect::new(0, 0, 5, 1),
Id::Save,
Sense::click() | Sense::DISABLED,
);
let _ = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::click());
interaction.end_frame();
assert!(!save.focused());
let tab = Event::Key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
let (_, cancel) = frame_with_events(&mut interaction, &[tab]);
assert!(cancel.focused());
}
#[test]
fn disabled_bit_reports_via_response_even_on_a_hover_only_sense() {
let mut interaction = Interaction::<Id>::new();
interaction.begin_frame();
let disabled = interaction.interact(
Rect::new(0, 0, 5, 1),
Id::Save,
Sense::hover() | Sense::DISABLED,
);
let plain = interaction.interact(Rect::new(6, 0, 5, 1), Id::Cancel, Sense::hover());
interaction.end_frame();
assert!(disabled.disabled());
assert!(!plain.disabled());
assert_eq!(disabled.hovered(), plain.hovered()); }
#[test]
fn wants_pointer_is_false_before_any_frame_has_registered_anything() {
let interaction = Interaction::<Id>::new();
assert!(!interaction.wants_pointer());
}
#[test]
fn a_pointer_event_on_a_registered_rect_is_consumed() {
let mut interaction = Interaction::<Id>::new();
let _ = frame(&mut interaction); let _ = frame(&mut interaction);
let consumed = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Moved,
Pos::new(2, 0), KeyModifiers::NONE,
)));
assert_eq!(consumed, Consumed::Yes);
assert!(interaction.wants_pointer());
}
#[test]
fn a_pointer_event_on_empty_space_is_not_consumed() {
let mut interaction = Interaction::<Id>::new();
let _ = frame(&mut interaction); let _ = frame(&mut interaction);
let consumed = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Moved,
Pos::new(20, 20), KeyModifiers::NONE,
)));
assert_eq!(consumed, Consumed::No);
assert!(!interaction.wants_pointer());
}
#[test]
fn unclaimed_events_are_never_consumed_even_while_focused_and_active() {
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());
let _ = interaction.handle_event(&Event::Mouse(MouseEvent::new(
MouseEventKind::Down(MouseButton::Left),
Pos::new(2, 0), KeyModifiers::NONE,
)));
assert_eq!(
interaction.handle_event(&Event::Resize(80, 24)),
Consumed::No
);
assert_eq!(
interaction.handle_event(&Event::Paste("hello".to_owned())),
Consumed::No
);
assert_eq!(interaction.handle_event(&Event::FocusLost), Consumed::No);
assert_eq!(
interaction.handle_event(&Event::Key(KeyEvent::new(
KeyCode::Char('a'),
KeyModifiers::NONE
))),
Consumed::No
);
assert_eq!(
interaction.handle_event(&Event::Key(KeyEvent::new(
KeyCode::Escape,
KeyModifiers::NONE
))),
Consumed::No
);
assert_eq!(interaction.handle_event(&Event::FocusGained), Consumed::No);
}
#[test]
fn tab_is_consumed_only_when_something_is_registered_as_focusable() {
let mut interaction = Interaction::<Id>::new();
let tab = Event::Key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
assert_eq!(interaction.handle_event(&tab), Consumed::No);
let _ = frame(&mut interaction); let _ = frame(&mut interaction); assert_eq!(interaction.handle_event(&tab), Consumed::Yes);
}
#[test]
fn rect_echoes_back_this_frames_area() {
let mut interaction = Interaction::<Id>::new();
interaction.begin_frame();
let save = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::hover());
interaction.end_frame();
assert_eq!(save.rect(), Rect::new(0, 0, 5, 1));
}
#[test]
fn a_second_click_within_the_window_reports_double_clicked() {
let mut interaction = Interaction::<Id>::new().with_double_click_window(5);
let _ = frame(&mut interaction);
click_at(&mut interaction, Pos::new(2, 0));
let (save, _) = frame(&mut interaction); assert!(save.clicked());
assert!(!save.double_clicked());
click_at(&mut interaction, Pos::new(2, 0));
let (save, _) = frame(&mut interaction); assert!(save.clicked());
assert!(save.double_clicked());
click_at(&mut interaction, Pos::new(2, 0));
let (save, _) = frame(&mut interaction);
assert!(save.clicked());
assert!(!save.double_clicked());
}
#[test]
fn a_second_click_outside_the_window_does_not_pair() {
let mut interaction = Interaction::<Id>::new().with_double_click_window(1);
let _ = frame(&mut interaction);
click_at(&mut interaction, Pos::new(2, 0));
let _ = frame(&mut interaction); let _ = frame(&mut interaction);
click_at(&mut interaction, Pos::new(2, 0));
let (save, _) = frame(&mut interaction); assert!(save.clicked());
assert!(!save.double_clicked());
}
#[test]
fn a_second_click_on_a_different_widget_does_not_pair() {
let mut interaction = Interaction::<Id>::new().with_double_click_window(5);
let _ = frame(&mut interaction);
click_at(&mut interaction, Pos::new(2, 0)); let _ = frame(&mut interaction);
click_at(&mut interaction, Pos::new(7, 0)); let (save, cancel) = frame(&mut interaction);
assert!(!save.double_clicked());
assert!(cancel.clicked());
assert!(!cancel.double_clicked());
}
#[test]
fn gained_and_lost_focus_are_one_shot_edges_around_level_focused() {
let mut interaction = Interaction::<Id>::new();
let _ = frame(&mut interaction);
let tab = Event::Key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
let (save, cancel) = frame_with_events(&mut interaction, core::slice::from_ref(&tab));
assert!(save.focused());
assert!(save.gained_focus()); assert!(!save.lost_focus());
assert!(!cancel.gained_focus());
let (save, _) = frame(&mut interaction);
assert!(save.focused());
assert!(!save.gained_focus());
assert!(!save.lost_focus());
let (save, cancel) = frame_with_events(&mut interaction, core::slice::from_ref(&tab));
assert!(!save.focused());
assert!(save.lost_focus());
assert!(!save.gained_focus());
assert!(cancel.focused());
assert!(cancel.gained_focus());
}
#[test]
fn clicking_a_non_focusable_widget_must_not_steal_focus() {
let mut interaction = Interaction::<Id>::new();
interaction.begin_frame();
let _ = interaction.interact(Rect::new(0, 0, 5, 1), Id::Save, Sense::click());
let _ = interaction.interact(
Rect::new(6, 0, 5, 1),
Id::Cancel,
Sense::HOVER | Sense::CLICK,
);
interaction.end_frame();
let tab = Event::Key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
interaction.begin_frame();
let _ = interaction.handle_event(&tab);
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::HOVER | Sense::CLICK,
);
interaction.end_frame();
assert!(save.focused());
click_at(&mut interaction, Pos::new(7, 0));
interaction.begin_frame();
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::HOVER | Sense::CLICK,
);
interaction.end_frame();
assert!(cancel.clicked());
assert!(!cancel.focused()); assert!(save.focused()); }
fn frame_with_delta(delta: Duration) -> Frame {
Frame::new(delta, 0)
}
#[test]
#[allow(clippy::float_cmp)]
fn animate_starts_at_rest_on_whichever_side_target_begins_on() {
let mut interaction = Interaction::<Id>::new();
let frame = frame_with_delta(Duration::ZERO);
assert_eq!(
interaction.animate(Id::Save, false, Duration::from_millis(100), &frame),
0.0
);
assert_eq!(
interaction.animate(Id::Cancel, true, Duration::from_millis(100), &frame),
1.0
);
}
#[test]
#[allow(clippy::float_cmp)]
fn animate_eases_toward_the_target_once_it_flips() {
let mut interaction = Interaction::<Id>::new();
let duration = Duration::from_millis(100);
let value =
interaction.animate(Id::Save, false, duration, &frame_with_delta(Duration::ZERO));
assert_eq!(value, 0.0);
let value = interaction.animate(
Id::Save,
true,
duration,
&frame_with_delta(Duration::from_millis(50)),
);
assert_eq!(value, 0.5); assert!(value < 1.0);
let value = interaction.animate(
Id::Save,
true,
duration,
&frame_with_delta(Duration::from_millis(50)),
);
assert_eq!(value, 1.0);
}
#[test]
fn animate_prunes_the_entry_once_it_settles_at_rest() {
let mut interaction = Interaction::<Id>::new();
let duration = Duration::from_millis(100);
let _ = interaction.animate(Id::Save, false, duration, &frame_with_delta(Duration::ZERO));
assert_eq!(interaction.tweens.len(), 1);
let _ = interaction.animate(Id::Save, false, duration, &frame_with_delta(duration));
assert!(
interaction.tweens.is_empty(),
"a settled tween must be dropped, not accumulate"
);
}
#[test]
#[allow(clippy::float_cmp)]
fn animate_tracks_each_id_independently() {
let mut interaction = Interaction::<Id>::new();
let duration = Duration::from_millis(100);
let save = interaction.animate(Id::Save, true, duration, &frame_with_delta(Duration::ZERO));
let cancel = interaction.animate(
Id::Cancel,
false,
duration,
&frame_with_delta(Duration::ZERO),
);
assert_eq!(save, 1.0);
assert_eq!(cancel, 0.0);
let save = interaction.animate(
Id::Save,
false,
duration,
&frame_with_delta(Duration::from_millis(50)),
);
let cancel = interaction.animate(
Id::Cancel,
false,
duration,
&frame_with_delta(Duration::from_millis(50)),
);
assert_eq!(save, 0.5);
assert_eq!(cancel, 0.0);
}
}