use teksilo_canvas::Point;
use teksilo_tokens::{DragActivation, GestureProfile};
use crate::pointer::touch_action::{Axis, PanClaim, TouchAction};
use crate::pointer::{EventTime, PointerInfo};
use crate::widget_id::WidgetId;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MemberRole {
Gesture,
Pan(PanClaim),
RawDrag,
RawPreview,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MemberState {
Possible,
Held,
Rejected,
Won,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SequenceMember {
pub id: WidgetId,
pub role: MemberRole,
pub eligible_at: Option<EventTime>,
pub state: MemberState,
pub(crate) rejects_on_tap_slop: bool,
pub(crate) held_since: Option<EventTime>,
pub(crate) has_own_drag: bool,
pub(crate) own_drag_eligible_at: Option<EventTime>,
pub(crate) own_drag_withdrawn: bool,
}
impl SequenceMember {
pub(crate) fn new(id: WidgetId, role: MemberRole) -> Self {
Self {
id,
role,
eligible_at: None,
state: MemberState::Possible,
rejects_on_tap_slop: false,
held_since: None,
has_own_drag: false,
own_drag_eligible_at: None,
own_drag_withdrawn: false,
}
}
pub fn is_live(&self) -> bool {
matches!(self.state, MemberState::Possible | MemberState::Held)
}
pub fn is_eligible_at(&self, now: EventTime) -> bool {
self.state == MemberState::Possible
&& self.eligible_at.is_none_or(|deadline| now >= deadline)
}
pub fn own_drag_armed_at(&self, now: EventTime) -> bool {
if !self.has_own_drag {
return true;
}
!self.own_drag_withdrawn && self.own_drag_eligible_at.is_none_or(|at| now >= at)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TapBoundary {
Radius(f32),
Bounds,
}
impl TapBoundary {
pub fn for_pointer(pointer: &PointerInfo, profile: &GestureProfile) -> Self {
if pointer.kind.is_coarse() {
Self::Bounds
} else {
Self::Radius(profile.tap_slop)
}
}
pub fn left(
&self,
origin: Point,
position: Point,
bounds: Option<teksilo_canvas::Rect>,
profile: &GestureProfile,
) -> bool {
match self {
Self::Radius(radius) => super::distance(origin, position) > *radius,
Self::Bounds => match bounds {
Some(rect) if rect.contains(origin) => !rect.contains(position),
Some(rect) => {
!rect.contains(position) && super::distance(origin, position) > profile.tap_slop
}
None => super::distance(origin, position) > profile.tap_slop,
},
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PointerSequence {
pointer: PointerInfo,
path: Vec<WidgetId>,
touch_action: TouchAction,
dead_zone_boundary: Option<WidgetId>,
members: Vec<SequenceMember>,
winner: Option<WidgetId>,
capture: Option<WidgetId>,
press_origin: Point,
last_position: Point,
started_at: EventTime,
pressed_owner: Option<WidgetId>,
terminating: bool,
taps_cancelled: bool,
drag_activation_overrides: Vec<(WidgetId, DragActivation)>,
}
impl PointerSequence {
pub fn new(
pointer: PointerInfo,
path: Vec<WidgetId>,
touch_action: TouchAction,
dead_zone_boundary: Option<WidgetId>,
origin: Point,
started_at: EventTime,
) -> Self {
Self {
pointer,
path,
touch_action,
dead_zone_boundary,
members: Vec::new(),
winner: None,
capture: None,
press_origin: origin,
last_position: origin,
started_at,
pressed_owner: None,
terminating: false,
taps_cancelled: false,
drag_activation_overrides: Vec::new(),
}
}
pub fn pointer(&self) -> PointerInfo {
self.pointer
}
pub fn path(&self) -> &[WidgetId] {
&self.path
}
pub fn touch_action(&self) -> TouchAction {
self.touch_action
}
pub fn dead_zone_boundary(&self) -> Option<WidgetId> {
self.dead_zone_boundary
}
pub fn members(&self) -> &[SequenceMember] {
&self.members
}
pub fn winner(&self) -> Option<WidgetId> {
self.winner
}
pub fn is_decided(&self) -> bool {
self.winner.is_some()
}
pub fn capture(&self) -> Option<WidgetId> {
self.capture
}
pub fn set_capture(&mut self, captor: Option<WidgetId>) {
self.capture = captor;
}
pub fn pressed_owner(&self) -> Option<WidgetId> {
self.pressed_owner
}
pub fn set_pressed_owner(&mut self, owner: Option<WidgetId>) {
self.pressed_owner = owner;
}
pub fn press_origin(&self) -> Point {
self.press_origin
}
pub fn last_position(&self) -> Point {
self.last_position
}
pub fn set_last_position(&mut self, position: Point) {
self.last_position = position;
}
pub fn started_at(&self) -> EventTime {
self.started_at
}
pub fn taps_cancelled(&self) -> bool {
self.taps_cancelled
}
pub fn set_taps_cancelled(&mut self) {
self.taps_cancelled = true;
}
pub fn is_terminating(&self) -> bool {
self.terminating
}
pub fn set_terminating(&mut self, terminating: bool) {
self.terminating = terminating;
}
pub fn travel(&self) -> f32 {
super::distance(self.press_origin, self.last_position)
}
pub fn travel_on(&self, axis: Axis) -> f32 {
match axis {
Axis::X => (self.last_position.x - self.press_origin.x).abs(),
Axis::Y => (self.last_position.y - self.press_origin.y).abs(),
}
}
pub fn latch_slop(&self, profile: &GestureProfile) -> f32 {
if self.pointer.kind.is_direct() && self.touch_action.is_none() {
profile.slop_precise
} else {
profile.drag_slop
}
}
pub fn may_enrol(&self, id: WidgetId) -> bool {
let Some(index) = self.path.iter().position(|p| *p == id) else {
return false;
};
match self.dead_zone_boundary {
Some(boundary) => match self.path.iter().position(|p| *p == boundary) {
Some(boundary_index) => index < boundary_index,
None => true,
},
None => true,
}
}
fn depth_of(&self, id: WidgetId) -> usize {
self.path
.iter()
.position(|p| *p == id)
.unwrap_or(usize::MAX)
}
pub fn has_member(&self, id: WidgetId) -> bool {
self.members.iter().any(|m| m.id == id)
}
pub fn enrol(&mut self, id: WidgetId, role: MemberRole) -> bool {
if self.has_member(id) || !self.may_enrol(id) {
return false;
}
let member = SequenceMember::new(id, role);
let depth = self.depth_of(id);
let at = self
.members
.iter()
.position(|m| self.depth_of(m.id) > depth)
.unwrap_or(self.members.len());
self.members.insert(at, member);
true
}
pub fn enrol_drag(
&mut self,
id: WidgetId,
role: MemberRole,
activation: DragActivation,
profile: &GestureProfile,
) -> bool {
if !self.enrol(id, role) {
return false;
}
if self.resolve_activation(activation) == DragActivation::AfterLongPress
&& let Some(member) = self.members.iter_mut().find(|m| m.id == id)
{
member.eligible_at = Some(self.started_at + profile.long_press);
member.rejects_on_tap_slop = true;
}
true
}
pub fn defer_own_drag(
&mut self,
id: WidgetId,
activation: DragActivation,
profile: &GestureProfile,
) -> bool {
if self.is_decided() {
return false;
}
let resolved = self.resolve_activation(activation);
let started_at = self.started_at;
let Some(member) = self
.members
.iter_mut()
.find(|m| m.id == id && m.is_live() && matches!(m.role, MemberRole::Pan(_)))
else {
return false;
};
member.has_own_drag = true;
if resolved == DragActivation::AfterLongPress {
member.own_drag_eligible_at = Some(started_at + profile.long_press);
}
true
}
pub fn withdraw_own_drag(&mut self, id: WidgetId) {
if let Some(member) = self.members.iter_mut().find(|m| m.id == id) {
member.own_drag_withdrawn = true;
}
}
pub fn own_drag_blocked(&self, id: WidgetId, now: EventTime) -> bool {
self.members
.iter()
.find(|m| m.id == id)
.is_some_and(|m| !m.own_drag_armed_at(now))
}
pub(crate) fn promote_own_drag(&mut self, id: WidgetId) -> bool {
let Some(member) = self
.members
.iter_mut()
.find(|m| m.id == id && m.has_own_drag && !m.own_drag_withdrawn)
else {
return false;
};
member.role = MemberRole::Gesture;
true
}
pub(crate) fn unripe_own_drag_members(&self, now: EventTime) -> Vec<WidgetId> {
self.members
.iter()
.filter(|m| {
m.is_live()
&& m.has_own_drag
&& !m.own_drag_withdrawn
&& m.own_drag_eligible_at.is_some_and(|at| now < at)
})
.map(|m| m.id)
.collect()
}
pub fn set_drag_activation_override(&mut self, id: WidgetId, activation: DragActivation) {
if let Some(slot) = self
.drag_activation_overrides
.iter_mut()
.find(|(other, _)| *other == id)
{
slot.1 = activation;
} else {
self.drag_activation_overrides.push((id, activation));
}
}
pub fn drag_activation_override(&self, id: WidgetId) -> Option<DragActivation> {
self.drag_activation_overrides
.iter()
.find(|(other, _)| *other == id)
.map(|(_, activation)| *activation)
}
pub fn resolve_activation(&self, activation: DragActivation) -> DragActivation {
match activation {
DragActivation::Auto => {
if !self.pointer.kind.is_direct() || self.touch_action.is_none() {
DragActivation::Immediate
} else if self.has_eligible_pan() {
DragActivation::AfterLongPress
} else {
DragActivation::Immediate
}
}
other => other,
}
}
pub fn has_deferred_grab_for(&self, id: WidgetId) -> bool {
self.members.iter().any(|m| {
m.id == id
&& m.is_live()
&& (m.eligible_at.is_some()
|| (m.has_own_drag
&& !m.own_drag_withdrawn
&& m.own_drag_eligible_at.is_some()))
})
}
pub fn has_eligible_pan(&self) -> bool {
self.members
.iter()
.any(|m| m.is_live() && matches!(m.role, MemberRole::Pan(_)))
}
pub fn pan_is_eligible(&self, claim: &PanClaim, profile: &GestureProfile) -> bool {
if profile.pan_slop.is_none() {
return false;
}
if !claim.devices.contains(self.pointer.kind) {
return false;
}
[Axis::X, Axis::Y]
.into_iter()
.any(|axis| claim.axes.contains(axis) && self.touch_action.allows_pan(axis))
}
pub fn pan_axis_past_slop(&self, claim: &PanClaim, profile: &GestureProfile) -> Option<Axis> {
let slop = profile.pan_slop?;
let mut candidates: Vec<(Axis, f32)> = [Axis::X, Axis::Y]
.into_iter()
.filter(|axis| claim.axes.contains(*axis) && self.touch_action.allows_pan(*axis))
.map(|axis| (axis, self.travel_on(axis)))
.filter(|(_, travel)| *travel >= slop)
.collect();
candidates.sort_by(|a, b| b.1.total_cmp(&a.1));
candidates.first().map(|(axis, _)| *axis)
}
pub fn decide(&mut self, id: WidgetId) -> Vec<WidgetId> {
self.winner = Some(id);
let mut losers = Vec::new();
for member in &mut self.members {
if member.id == id {
member.state = MemberState::Won;
} else if member.is_live() {
member.state = MemberState::Rejected;
losers.push(member.id);
}
}
losers
}
pub fn reject(&mut self, id: WidgetId) {
if let Some(member) = self.members.iter_mut().find(|m| m.id == id)
&& member.is_live()
{
member.state = MemberState::Rejected;
}
}
pub fn hold(&mut self, id: WidgetId, now: EventTime) {
if let Some(member) = self.members.iter_mut().find(|m| m.id == id)
&& member.state == MemberState::Possible
{
member.state = MemberState::Held;
member.held_since = Some(now);
}
}
pub fn release_hold(&mut self, id: WidgetId) {
if let Some(member) = self.members.iter_mut().find(|m| m.id == id)
&& member.state == MemberState::Held
{
member.state = MemberState::Possible;
member.held_since = None;
}
}
pub fn expire_holds(&mut self, now: EventTime, profile: &GestureProfile) {
for member in &mut self.members {
if member.state == MemberState::Held
&& let Some(since) = member.held_since
&& now.saturating_since(since) >= profile.max_hold
{
member.state = MemberState::Possible;
member.held_since = None;
}
}
}
pub fn is_held(&self) -> bool {
self.members.iter().any(|m| m.state == MemberState::Held)
}
pub fn next_hold_deadline(&self, profile: &GestureProfile) -> Option<EventTime> {
self.members
.iter()
.filter(|m| m.state == MemberState::Held)
.filter_map(|m| m.held_since.map(|since| since + profile.max_hold))
.min()
}
pub fn revalidate(&mut self, arena: &crate::arena::WidgetArena) -> Vec<WidgetId> {
let mut dead = Vec::new();
self.members.retain(|member| {
if arena.is_active(member.id) {
true
} else {
dead.push(member.id);
false
}
});
dead
}
pub fn lost_owner(&self, arena: &crate::arena::WidgetArena) -> bool {
let owner = self.winner.or(self.capture);
owner.is_some_and(|id| !arena.is_active(id))
}
pub fn member_report(&self) -> Vec<(WidgetId, MemberRole, MemberState)> {
self.members
.iter()
.map(|m| (m.id, m.role, m.state))
.collect()
}
pub(crate) fn live_ids_with<F: Fn(&MemberRole) -> bool>(&self, filter: F) -> Vec<WidgetId> {
self.members
.iter()
.filter(|m| m.is_live() && filter(&m.role))
.map(|m| m.id)
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pointer::{BackendDeviceKey, PointerIdAllocator};
use crate::widget_id::WidgetId;
use slotmap::KeyData;
use teksilo_tokens::{PointerKind, TargetDensity};
fn tokens() -> teksilo_tokens::InputTokens {
teksilo_tokens::InputTokens::for_density(TargetDensity::Compact)
}
fn mouse() -> PointerInfo {
PointerInfo::mouse(EventTime::ZERO)
}
fn finger() -> PointerInfo {
let id = PointerIdAllocator::global().begin(BackendDeviceKey::DEFAULT, 7);
PointerInfo::touch(id, EventTime::ZERO)
}
fn seq(pointer: PointerInfo, action: TouchAction, path: Vec<WidgetId>) -> PointerSequence {
PointerSequence::new(pointer, path, action, None, Point::ZERO, EventTime::ZERO)
}
fn ids(n: u64) -> Vec<WidgetId> {
(0..n)
.map(|i| KeyData::from_ffi((1u64 << 32) | (i + 1)).into())
.collect()
}
#[test]
fn a_mouse_latches_at_five_in_every_configuration() {
let tokens = tokens();
let profile = tokens.profile(PointerKind::Mouse);
for action in [
TouchAction::AUTO,
TouchAction::NONE,
TouchAction::PAN,
TouchAction::PAN_X,
TouchAction::PAN_Y,
TouchAction::PINCH_ZOOM,
TouchAction::MANIPULATION,
] {
let s = seq(mouse(), action, ids(1));
assert_eq!(
s.latch_slop(profile),
5.0,
"a mouse under {action:?} must latch at 5.0"
);
}
}
#[test]
fn slop_precise_reaches_only_a_direct_pointer_under_a_frozen_none() {
let tokens = tokens();
let touch_profile = tokens.profile(PointerKind::Touch);
let none = seq(finger(), TouchAction::NONE, ids(1));
assert_eq!(none.latch_slop(touch_profile), touch_profile.slop_precise);
let auto = seq(finger(), TouchAction::AUTO, ids(1));
assert_eq!(auto.latch_slop(touch_profile), touch_profile.drag_slop);
}
#[test]
fn a_mouse_never_has_an_eligible_pan_member() {
let tokens = tokens();
let profile = tokens.profile(PointerKind::Mouse);
let s = seq(mouse(), TouchAction::AUTO, ids(1));
assert!(!s.pan_is_eligible(&PanClaim::both(), profile));
}
#[test]
fn members_stay_innermost_first_whatever_order_they_enrol_in() {
let path = ids(4);
let mut s = seq(mouse(), TouchAction::AUTO, path.clone());
assert!(s.enrol(path[3], MemberRole::Gesture));
assert!(s.enrol(path[1], MemberRole::RawDrag));
assert!(s.enrol(path[2], MemberRole::Gesture));
let order: Vec<_> = s.members().iter().map(|m| m.id).collect();
assert_eq!(order, vec![path[1], path[2], path[3]]);
}
#[test]
fn the_dead_zone_boundary_refuses_everything_at_or_above_it() {
let path = ids(4);
let mut s = PointerSequence::new(
mouse(),
path.clone(),
TouchAction::AUTO,
Some(path[2]),
Point::ZERO,
EventTime::ZERO,
);
assert!(s.enrol(path[1], MemberRole::Gesture), "below the boundary");
assert!(
!s.enrol(path[2], MemberRole::Gesture),
"the boundary itself"
);
assert!(!s.enrol(path[3], MemberRole::Gesture), "above the boundary");
}
#[test]
fn deciding_rejects_every_other_live_member_exactly_once() {
let path = ids(3);
let mut s = seq(mouse(), TouchAction::AUTO, path.clone());
s.enrol(path[0], MemberRole::Gesture);
s.enrol(path[1], MemberRole::Gesture);
s.enrol(path[2], MemberRole::Gesture);
let losers = s.decide(path[1]);
assert_eq!(losers, vec![path[0], path[2]]);
assert_eq!(s.winner(), Some(path[1]));
assert!(s.decide(path[1]).is_empty());
}
#[test]
fn after_long_press_defers_eligibility_and_arms_self_rejection() {
let tokens = tokens();
let profile = tokens.profile(PointerKind::Touch);
let path = ids(2);
let mut s = seq(finger(), TouchAction::PAN_Y, path.clone());
s.enrol_drag(
path[0],
MemberRole::Gesture,
DragActivation::AfterLongPress,
profile,
);
let member = s.members()[0];
assert_eq!(
member.eligible_at,
Some(EventTime::ZERO + profile.long_press)
);
assert!(member.rejects_on_tap_slop);
assert!(!member.is_eligible_at(EventTime::ZERO));
assert!(member.is_eligible_at(EventTime::ZERO + profile.long_press));
}
#[test]
fn auto_activation_defers_only_a_coarse_pointer_facing_a_pan() {
let path = ids(2);
let mut m = seq(mouse(), TouchAction::AUTO, path.clone());
m.enrol(path[1], MemberRole::Pan(PanClaim::vertical()));
assert_eq!(
m.resolve_activation(DragActivation::Auto),
DragActivation::Immediate
);
let bare = seq(finger(), TouchAction::AUTO, path.clone());
assert_eq!(
bare.resolve_activation(DragActivation::Auto),
DragActivation::Immediate
);
let mut contested = seq(finger(), TouchAction::AUTO, path.clone());
contested.enrol(path[1], MemberRole::Pan(PanClaim::vertical()));
assert_eq!(
contested.resolve_activation(DragActivation::Auto),
DragActivation::AfterLongPress
);
}
#[test]
fn a_pan_wins_on_the_dominant_axis_and_only_where_permitted() {
let tokens = tokens();
let profile = tokens.profile(PointerKind::Touch);
let slop = profile.pan_slop.expect("touch pans");
let path = ids(1);
let mut s = seq(finger(), TouchAction::PAN, path);
s.set_last_position(Point::new(slop + 10.0, slop + 1.0));
assert_eq!(
s.pan_axis_past_slop(&PanClaim::both(), profile),
Some(Axis::X),
"the axis that travelled further wins the diagonal"
);
let mut only_y = seq(finger(), TouchAction::PAN_Y, ids(1));
only_y.set_last_position(Point::new(slop + 10.0, slop + 1.0));
assert_eq!(
only_y.pan_axis_past_slop(&PanClaim::both(), profile),
Some(Axis::Y)
);
}
#[test]
fn a_hold_expires_at_max_hold_and_not_before() {
let tokens = tokens();
let profile = tokens.profile(PointerKind::Mouse);
let path = ids(1);
let mut s = seq(mouse(), TouchAction::AUTO, path.clone());
s.enrol(path[0], MemberRole::Gesture);
s.hold(path[0], EventTime::ZERO);
assert!(s.is_held());
s.expire_holds(EventTime::from_duration(profile.max_hold / 2), profile);
assert!(s.is_held(), "a hold survives until max_hold");
s.expire_holds(EventTime::from_duration(profile.max_hold), profile);
assert!(!s.is_held(), "and is released at it");
assert_eq!(s.members()[0].state, MemberState::Possible);
}
#[test]
fn revalidate_drops_dead_members_one_at_a_time() {
let mut arena = crate::arena::WidgetArena::new();
let live = arena.insert(Box::new(crate::test_widgets::FillWidget::new()));
let doomed = arena.insert(Box::new(crate::test_widgets::FillWidget::new()));
let mut s = seq(mouse(), TouchAction::AUTO, vec![doomed, live]);
s.enrol(doomed, MemberRole::Gesture);
s.enrol(live, MemberRole::Gesture);
s.set_capture(Some(live));
assert!(s.revalidate(&arena).is_empty(), "nothing has died yet");
arena.destroy(doomed);
assert_eq!(s.revalidate(&arena), vec![doomed]);
assert_eq!(
s.members().iter().map(|m| m.id).collect::<Vec<_>>(),
vec![live],
"only the dead member is dropped"
);
assert!(!s.lost_owner(&arena), "the captor is still alive");
arena.destroy(live);
assert!(
s.lost_owner(&arena),
"losing the captor is what cancels the sequence"
);
}
#[test]
fn the_tap_boundary_is_a_radius_for_a_mouse_and_bounds_for_a_finger() {
let tokens = tokens();
let mouse_profile = tokens.profile(PointerKind::Mouse);
let touch_profile = tokens.profile(PointerKind::Touch);
assert_eq!(
TapBoundary::for_pointer(&mouse(), mouse_profile),
TapBoundary::Radius(mouse_profile.tap_slop)
);
assert_eq!(
TapBoundary::for_pointer(&finger(), touch_profile),
TapBoundary::Bounds
);
let bounds = teksilo_canvas::Rect::new(0.0, 0.0, 100.0, 100.0);
assert!(!TapBoundary::Bounds.left(
Point::new(50.0, 50.0),
Point::new(50.0, 80.0),
Some(bounds),
touch_profile,
));
assert!(TapBoundary::Bounds.left(
Point::new(50.0, 50.0),
Point::new(50.0, 120.0),
Some(bounds),
touch_profile,
));
assert!(TapBoundary::Bounds.left(
Point::new(50.0, 50.0),
Point::new(50.0, 80.0),
None,
touch_profile,
));
}
#[test]
fn a_press_that_began_outside_the_node_is_bounded_by_its_own_radius() {
let tokens = tokens();
let touch_profile = tokens.profile(PointerKind::Touch);
let rect = teksilo_canvas::Rect::new(0.0, 0.0, 12.0, 12.0);
let origin = Point::new(16.0, 6.0);
assert!(
!TapBoundary::Bounds.left(origin, origin, Some(rect), touch_profile),
"a press cannot have left the boundary on the sample that opened it",
);
assert!(
!TapBoundary::Bounds.left(origin, Point::new(6.0, 6.0), Some(rect), touch_profile),
"sliding onto the control keeps the press",
);
assert!(
TapBoundary::Bounds.left(
origin,
Point::new(16.0 + touch_profile.tap_slop + 1.0, 6.0),
Some(rect),
touch_profile,
),
"and past the radius it is gone, so the abort gesture still works",
);
}
#[test]
fn sliding_onto_the_control_keeps_a_press_the_radius_alone_would_lose() {
let tokens = tokens();
let touch_profile = tokens.profile(PointerKind::Touch);
let rect = teksilo_canvas::Rect::new(0.0, 0.0, 100.0, 20.0);
let origin = Point::new(104.0, 10.0);
let onto = Point::new(80.0, 10.0);
assert!(
super::super::distance(origin, onto) > touch_profile.tap_slop,
"the probe is only discriminating while the travel exceeds tap_slop",
);
assert!(rect.contains(onto), "…and lands inside the control");
assert!(
!TapBoundary::Bounds.left(origin, onto, Some(rect), touch_profile),
"a finger resting on the control it pressed has not left it",
);
}
#[test]
fn the_boundary_rule_is_chosen_by_where_the_press_began() {
let tokens = tokens();
let touch_profile = tokens.profile(PointerKind::Touch);
let rect = teksilo_canvas::Rect::new(0.0, 0.0, 100.0, 20.0);
let position = Point::new(104.0, 10.0);
let from_inside = Point::new(96.0, 10.0);
let from_outside = Point::new(108.0, 10.0);
assert!(rect.contains(from_inside), "the first press began inside");
assert!(
!rect.contains(from_outside) && !rect.contains(position),
"the second began outside, and neither sample is in the rect",
);
for origin in [from_inside, from_outside] {
assert!(
super::super::distance(origin, position) < touch_profile.tap_slop,
"the probe only discriminates while the travel is inside tap_slop",
);
}
assert!(
TapBoundary::Bounds.left(from_inside, position, Some(rect), touch_profile),
"a press that began inside the node is bounded by the node: crossing \
the edge ends it, with no radius grace outside",
);
assert!(
!TapBoundary::Bounds.left(from_outside, position, Some(rect), touch_profile),
"a press that began outside is bounded by its own radius, and this \
one has barely moved",
);
}
#[test]
fn defer_own_drag_refuses_anything_but_a_live_pan_member() {
let tokens = tokens();
let profile = tokens.profile(PointerKind::Touch);
let ids = ids(3);
let mut s = seq(finger(), TouchAction::AUTO, ids.clone());
assert!(s.enrol(ids[0], MemberRole::Gesture));
assert!(
!s.defer_own_drag(ids[0], DragActivation::Auto, profile),
"a Gesture member is not dual-role"
);
assert!(
!s.defer_own_drag(ids[1], DragActivation::Auto, profile),
"a non-member has nothing to attach a deferral to"
);
let mut s = seq(finger(), TouchAction::AUTO, ids.clone());
assert!(s.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
s.reject(ids[0]);
assert!(
!s.defer_own_drag(ids[0], DragActivation::Auto, profile),
"a member that is out of the running gets no second half"
);
let mut s = seq(finger(), TouchAction::AUTO, ids.clone());
assert!(s.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
s.decide(ids[0]);
assert!(
!s.defer_own_drag(ids[0], DragActivation::Auto, profile),
"arbitration is over"
);
let mut s = seq(finger(), TouchAction::AUTO, ids.clone());
assert!(s.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
assert!(s.defer_own_drag(ids[0], DragActivation::Auto, profile));
}
#[test]
fn a_mouse_can_never_defer_its_own_drag() {
let tokens = tokens();
let profile = tokens.profile(PointerKind::Mouse);
let ids = ids(1);
let mut s = seq(mouse(), TouchAction::AUTO, ids.clone());
assert!(
!s.pan_is_eligible(&PanClaim::both(), profile),
"the mouse profile has no pan_slop, so no claim is eligible"
);
assert!(!s.defer_own_drag(ids[0], DragActivation::Auto, profile));
assert!(!s.has_deferred_grab_for(ids[0]));
}
#[test]
fn defer_own_drag_resolves_auto_the_way_every_other_drag_resolves_it() {
let tokens = tokens();
let profile = tokens.profile(PointerKind::Touch);
let ids = ids(1);
let mut deferred = seq(finger(), TouchAction::AUTO, ids.clone());
assert!(deferred.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
assert!(deferred.defer_own_drag(ids[0], DragActivation::Auto, profile));
assert!(
deferred.own_drag_blocked(ids[0], EventTime::ZERO),
"Auto + an eligible pan means a hold"
);
assert!(
!deferred.own_drag_blocked(ids[0], EventTime::ZERO + profile.long_press),
"…and the hold ends at long_press"
);
assert!(
deferred.has_deferred_grab_for(ids[0]),
"so the hold is spent on the grab and cannot also be a long press"
);
let mut immediate = seq(finger(), TouchAction::AUTO, ids.clone());
assert!(immediate.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
assert!(immediate.defer_own_drag(ids[0], DragActivation::Immediate, profile));
assert!(
!immediate.own_drag_blocked(ids[0], EventTime::ZERO),
"Immediate arms at the press"
);
assert!(
!immediate.has_deferred_grab_for(ids[0]),
"and spends no hold, so a long press on the same node still fires"
);
}
#[test]
fn a_withdrawn_self_drag_stays_blocked_past_its_own_deadline() {
let tokens = tokens();
let profile = tokens.profile(PointerKind::Touch);
let ids = ids(1);
let mut s = seq(finger(), TouchAction::AUTO, ids.clone());
assert!(s.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
assert!(s.defer_own_drag(ids[0], DragActivation::Auto, profile));
s.withdraw_own_drag(ids[0]);
assert!(
s.own_drag_blocked(ids[0], EventTime::ZERO + profile.long_press * 10),
"a withdrawn self-drag does not come back when its timer ripens"
);
assert!(
!s.has_deferred_grab_for(ids[0]),
"and stops spending the hold, so the node's long press is free again"
);
}
#[test]
fn promote_own_drag_renames_the_half_that_won() {
let tokens = tokens();
let profile = tokens.profile(PointerKind::Touch);
let ids = ids(1);
let mut s = seq(finger(), TouchAction::AUTO, ids.clone());
assert!(s.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
assert!(s.defer_own_drag(ids[0], DragActivation::Immediate, profile));
assert!(matches!(s.members()[0].role, MemberRole::Pan(_)));
assert!(s.promote_own_drag(ids[0]));
assert_eq!(s.members()[0].role, MemberRole::Gesture);
let mut plain = seq(finger(), TouchAction::AUTO, ids.clone());
assert!(plain.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
assert!(!plain.promote_own_drag(ids[0]));
assert!(matches!(plain.members()[0].role, MemberRole::Pan(_)));
let mut withdrawn = seq(finger(), TouchAction::AUTO, ids.clone());
assert!(withdrawn.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
assert!(withdrawn.defer_own_drag(ids[0], DragActivation::Auto, profile));
withdrawn.withdraw_own_drag(ids[0]);
assert!(!withdrawn.promote_own_drag(ids[0]));
assert!(matches!(withdrawn.members()[0].role, MemberRole::Pan(_)));
}
#[test]
fn only_a_deferred_self_drag_is_swept_positionally() {
let tokens = tokens();
let profile = tokens.profile(PointerKind::Touch);
let ids = ids(1);
let mut deferred = seq(finger(), TouchAction::AUTO, ids.clone());
assert!(deferred.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
assert!(deferred.defer_own_drag(ids[0], DragActivation::Auto, profile));
assert_eq!(
deferred.unripe_own_drag_members(EventTime::ZERO),
vec![ids[0]]
);
let mut immediate = seq(finger(), TouchAction::AUTO, ids.clone());
assert!(immediate.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
assert!(immediate.defer_own_drag(ids[0], DragActivation::Immediate, profile));
assert!(
immediate
.unripe_own_drag_members(EventTime::ZERO)
.is_empty()
);
let mut plain = seq(finger(), TouchAction::AUTO, ids.clone());
assert!(plain.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
assert!(plain.unripe_own_drag_members(EventTime::ZERO).is_empty());
}
#[test]
fn a_ripe_self_drag_is_no_longer_swept() {
let tokens = tokens();
let profile = tokens.profile(PointerKind::Touch);
let ids = ids(1);
let mut s = seq(finger(), TouchAction::AUTO, ids.clone());
assert!(s.enrol(ids[0], MemberRole::Pan(PanClaim::both())));
assert!(s.defer_own_drag(ids[0], DragActivation::Auto, profile));
let deadline = EventTime::ZERO + profile.long_press;
assert_eq!(
s.unripe_own_drag_members(
EventTime::ZERO + (profile.long_press - std::time::Duration::from_millis(1)),
),
vec![ids[0]],
"still inside the hold"
);
assert!(
s.unripe_own_drag_members(deadline).is_empty(),
"the hold has been served; the grab is live and answers to its own \
recognizer from here"
);
}
#[test]
fn own_drag_armed_is_true_for_a_member_with_no_self_drag() {
let ids = ids(1);
let mut s = seq(mouse(), TouchAction::AUTO, ids.clone());
assert!(s.enrol(ids[0], MemberRole::Gesture));
for at in [
EventTime::ZERO,
EventTime::ZERO + std::time::Duration::from_secs(10),
] {
assert!(s.members()[0].own_drag_armed_at(at));
assert!(!s.own_drag_blocked(ids[0], at));
}
}
#[test]
fn a_drag_activation_override_is_recorded_per_node() {
let ids = ids(2);
let mut s = seq(finger(), TouchAction::AUTO, ids.clone());
assert_eq!(s.drag_activation_override(ids[0]), None);
s.set_drag_activation_override(ids[0], DragActivation::Immediate);
s.set_drag_activation_override(ids[1], DragActivation::AfterLongPress);
assert_eq!(
s.drag_activation_override(ids[0]),
Some(DragActivation::Immediate)
);
assert_eq!(
s.drag_activation_override(ids[1]),
Some(DragActivation::AfterLongPress)
);
s.set_drag_activation_override(ids[0], DragActivation::Auto);
assert_eq!(
s.drag_activation_override(ids[0]),
Some(DragActivation::Auto),
"answering twice on one press means the second answer"
);
}
}