use std::rc::Rc;
use frust_core::{
AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, FrameTime,
InputEvent, LayoutCtx, PaintCtx, PaintScene, PointerPhase, SemanticsCtx, TOUCH_SLOP, View,
Widget, any,
};
use kurbo::{Point, Size};
use crate::authoring::presses;
pub(crate) const LONG_PRESS_MS: f64 = 500.0;
pub(crate) const DOUBLE_TAP_MS: f64 = 300.0;
type GestureCallback<State> = Rc<dyn Fn(&mut State)>;
pub struct GestureDetectorView<State: 'static> {
child: AnyView<State>,
on_tap: Option<GestureCallback<State>>,
on_long_press: Option<GestureCallback<State>>,
on_hold_progress: Option<crate::authoring::TypedArgCallback<State, f64>>,
hold_threshold_ms: Option<u64>,
}
#[allow(non_snake_case)]
pub fn GestureDetector<State: 'static, V: View<State>>(child: V) -> GestureDetectorView<State> {
GestureDetectorView {
child: any(child),
on_tap: None,
on_long_press: None,
on_hold_progress: None,
hold_threshold_ms: None,
}
}
impl<State: 'static> GestureDetectorView<State> {
pub fn on_tap<F: Fn(&mut State) + 'static>(mut self, on_tap: F) -> Self {
self.on_tap = Some(Rc::new(on_tap));
self
}
pub fn on_long_press<F: Fn(&mut State) + 'static>(mut self, on_long_press: F) -> Self {
self.on_long_press = Some(Rc::new(on_long_press));
self
}
pub fn on_hold_progress<F: Fn(&mut State, f64) + 'static>(
mut self,
on_hold_progress: F,
) -> Self {
self.on_hold_progress = Some(Rc::new(on_hold_progress));
self
}
pub fn hold_threshold_ms(mut self, ms: u64) -> Self {
self.hold_threshold_ms = Some(ms);
self
}
}
#[derive(Clone, Copy)]
enum Recognizer {
Idle,
Pressed {
down_pos: Point,
press_start: Option<FrameTime>,
elapsed: bool,
progress: f64,
},
Dragged,
Fired,
}
pub struct GestureDetectorWidget {
child: ChildPod,
state: Recognizer,
on_tap: Option<crate::authoring::ErasedCallback>,
on_long_press: Option<crate::authoring::ErasedCallback>,
on_hold_progress: Option<crate::authoring::ErasedArgCallback<f64>>,
threshold_ms: f64,
}
impl<State: 'static> View<State> for GestureDetectorView<State> {
type Element = GestureDetectorWidget;
fn build(&self, ctx: &mut BuildCtx<'_>) -> GestureDetectorWidget {
GestureDetectorWidget {
child: crate::authoring::build_child(&self.child, ctx),
state: Recognizer::Idle,
on_tap: self.on_tap.as_ref().map(crate::authoring::erase_callback),
on_long_press: self
.on_long_press
.as_ref()
.map(crate::authoring::erase_callback),
on_hold_progress: self
.on_hold_progress
.as_ref()
.map(crate::authoring::erase_callback_arg),
threshold_ms: self
.hold_threshold_ms
.map(|ms| ms.max(1) as f64)
.unwrap_or(LONG_PRESS_MS),
}
}
fn rebuild(
&self,
prev: &Self,
element: &mut GestureDetectorWidget,
ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
element.on_tap = self.on_tap.as_ref().map(crate::authoring::erase_callback);
element.on_long_press = self
.on_long_press
.as_ref()
.map(crate::authoring::erase_callback);
element.on_hold_progress = self
.on_hold_progress
.as_ref()
.map(crate::authoring::erase_callback_arg);
element.threshold_ms = self
.hold_threshold_ms
.map(|ms| ms.max(1) as f64)
.unwrap_or(LONG_PRESS_MS);
crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx)
}
fn teardown(&self, element: &mut GestureDetectorWidget, ctx: &mut BuildCtx<'_>) {
crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
}
}
impl Widget for GestureDetectorWidget {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
let size = self.child.layout_child(ctx, bc);
self.child.set_origin(Point::ZERO);
bc.constrain(size)
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
if (self.on_long_press.is_some() || self.on_hold_progress.is_some())
&& let Recognizer::Pressed {
press_start,
elapsed,
progress,
..
} = &mut self.state
{
let start = *press_start.get_or_insert(ctx.frame_time());
let elapsed_ms = ctx.frame_time().saturating_sub(start).as_secs_f64() * 1000.0;
*progress = (elapsed_ms / self.threshold_ms).clamp(0.0, 1.0);
if !*elapsed {
if elapsed_ms >= self.threshold_ms {
*elapsed = true;
if self.on_long_press.is_some() {
frust_core::mark_pending_result_flush();
ctx.request_frame();
}
} else {
ctx.request_frame();
}
}
}
self.child.paint_child(ctx, scene);
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
if let InputEvent::Pointer(p) = event {
match p.phase {
PointerPhase::Down => {
if !presses(p) {
return self.child.event_child(ctx, event);
}
self.state = Recognizer::Pressed {
down_pos: p.position,
press_start: None,
elapsed: false,
progress: 0.0,
};
ctx.capture_pointer();
ctx.request_redraw();
self.child.event_child(ctx, event);
return EventResult::Handled;
}
PointerPhase::Move => {
if let Recognizer::Pressed {
down_pos,
elapsed,
progress,
..
} = self.state
{
if (p.position - down_pos).hypot() > TOUCH_SLOP {
self.state = Recognizer::Dragged;
if let Some(cb) = self.on_hold_progress.as_mut() {
cb(ctx, 0.0);
ctx.request_redraw();
}
} else {
if let Some(cb) = self.on_hold_progress.as_mut() {
cb(ctx, progress);
ctx.request_redraw();
}
if elapsed {
self.state = Recognizer::Fired;
self.child.event_child(ctx, event);
if let Some(cb) = self.on_long_press.as_mut() {
cb(ctx);
ctx.request_redraw();
} else if let Some(cb) = self.on_tap.as_mut() {
cb(ctx);
ctx.request_redraw();
}
return EventResult::Handled;
}
}
}
self.child.event_child(ctx, event);
return EventResult::Handled;
}
PointerPhase::Up => {
let fire_long = matches!(self.state, Recognizer::Pressed { elapsed: true, .. });
let fire_tap = matches!(self.state, Recognizer::Pressed { elapsed: false, .. });
let final_progress = match self.state {
Recognizer::Pressed { elapsed: true, .. } => Some(1.0),
Recognizer::Pressed { elapsed: false, .. } => Some(0.0),
_ => None,
};
self.state = Recognizer::Idle;
self.child.event_child(ctx, event);
self.child.set_active(false);
if let Some(progress) = final_progress
&& let Some(cb) = self.on_hold_progress.as_mut()
{
cb(ctx, progress);
ctx.request_redraw();
}
if fire_long && let Some(cb) = self.on_long_press.as_mut() {
cb(ctx);
ctx.request_redraw();
} else if (fire_tap || fire_long)
&& let Some(cb) = self.on_tap.as_mut()
{
cb(ctx);
ctx.request_redraw();
}
return EventResult::Handled;
}
PointerPhase::Cancel => {
self.state = Recognizer::Idle;
self.child.event_child(ctx, event);
self.child.set_active(false);
return EventResult::Handled;
}
}
}
if event.is_broadcast() {
if self.on_long_press.is_some()
&& let Recognizer::Pressed {
elapsed: true,
progress,
..
} = self.state
{
self.state = Recognizer::Fired;
if let Some(cb) = self.on_long_press.as_mut() {
cb(ctx);
ctx.request_redraw();
}
if let Some(cb) = self.on_hold_progress.as_mut() {
cb(ctx, progress);
ctx.request_redraw();
}
}
self.child.event_child(ctx, event);
return EventResult::Ignored;
}
self.child.event_child(ctx, event)
}
fn semantics(&self, ctx: &mut SemanticsCtx) {
self.child.semantics_child(ctx);
}
crate::authoring::visit_children!(child);
}
#[cfg(test)]
mod tests {
use super::*;
use frust_core::RenderRoot;
use std::any::Any;
#[derive(Default)]
struct TapState {
taps: u32,
long_presses: u32,
}
struct Blank;
struct BlankWidget;
impl<State: 'static> View<State> for Blank {
type Element = BlankWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> BlankWidget {
BlankWidget
}
fn rebuild(
&self,
_prev: &Self,
_element: &mut BlankWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
ChangeFlags::NONE
}
}
impl Widget for BlankWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.max()
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
}
fn widget(with_tap: bool) -> GestureDetectorWidget {
let mut view = GestureDetector::<TapState, _>(Blank);
if with_tap {
view = view.on_tap(|s: &mut TapState| s.taps += 1);
}
let mut counter = 0u64;
View::<TapState>::build(&view, &mut BuildCtx::new(&mut counter))
}
fn ev(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
InputEvent::Pointer(frust_core::PointerEvent {
phase,
position: Point::new(x, y),
button: frust_core::PointerButton::Primary,
})
}
fn dispatch(w: &mut GestureDetectorWidget, state: &mut TapState, event: &InputEvent) {
let state_any: &mut dyn Any = state;
let mut ctx = EventCtx::new(state_any, Point::ZERO, Size::new(100.0, 100.0));
w.event(&mut ctx, event);
}
#[test]
fn press_release_within_slop_is_a_tap() {
let mut w = widget(true);
let mut state = TapState::default();
dispatch(&mut w, &mut state, &ev(PointerPhase::Down, 10.0, 10.0));
dispatch(&mut w, &mut state, &ev(PointerPhase::Up, 15.0, 12.0));
assert_eq!(state.taps, 1);
}
#[test]
fn drag_past_slop_disarms_the_tap() {
let mut w = widget(true);
let mut state = TapState::default();
dispatch(&mut w, &mut state, &ev(PointerPhase::Down, 10.0, 10.0));
dispatch(&mut w, &mut state, &ev(PointerPhase::Move, 10.0, 40.0));
dispatch(&mut w, &mut state, &ev(PointerPhase::Up, 10.0, 40.0));
assert_eq!(state.taps, 0);
}
#[test]
fn cancel_disarms_the_tap() {
let mut w = widget(true);
let mut state = TapState::default();
dispatch(&mut w, &mut state, &ev(PointerPhase::Down, 10.0, 10.0));
dispatch(&mut w, &mut state, &ev(PointerPhase::Cancel, 10.0, 10.0));
dispatch(&mut w, &mut state, &ev(PointerPhase::Up, 10.0, 10.0));
assert_eq!(state.taps, 0);
}
#[test]
fn no_tap_callback_is_a_benign_noop() {
let mut w = widget(false);
let mut state = TapState::default();
dispatch(&mut w, &mut state, &ev(PointerPhase::Down, 10.0, 10.0));
dispatch(&mut w, &mut state, &ev(PointerPhase::Up, 10.0, 10.0));
assert_eq!(state.taps, 0);
}
struct NullScene;
impl PaintScene for NullScene {
fn fill_rect(&mut self, _o: Point, _s: Size, _c: peniko::Color) {}
fn draw_text(&mut self, _o: Point, _t: &str) {}
}
fn ft(ms: f64) -> FrameTime {
FrameTime::from_nanos((ms * 1_000_000.0) as u64)
}
fn root() -> RenderRoot<TapState, GestureDetectorView<TapState>> {
fn logic(_: &mut TapState) -> GestureDetectorView<TapState> {
GestureDetector::<TapState, _>(Blank)
.on_tap(|s: &mut TapState| s.taps += 1)
.on_long_press(|s: &mut TapState| s.long_presses += 1)
}
let mut root = RenderRoot::new();
let mut state = TapState::default();
root.rebuild(&mut logic, &mut state);
root.layout(Size::new(100.0, 100.0));
root
}
#[test]
fn hold_past_threshold_then_up_fires_long_press_only() {
let mut root = root();
let mut state = TapState::default();
let mut sink = NullScene;
root.event(&mut state, &ev(PointerPhase::Down, 10.0, 10.0));
root.paint(&mut sink, ft(0.0)); root.paint(&mut sink, ft(600.0)); root.event(&mut state, &ev(PointerPhase::Up, 11.0, 11.0));
assert_eq!(state.long_presses, 1, "held press releases as a long-press");
assert_eq!(state.taps, 0, "a long-press must not also fire on_tap");
}
fn long_press_logic(_: &mut TapState) -> GestureDetectorView<TapState> {
GestureDetector::<TapState, _>(Blank)
.on_tap(|s: &mut TapState| s.taps += 1)
.on_long_press(|s: &mut TapState| s.long_presses += 1)
}
#[test]
fn stationary_hold_fires_on_long_press_at_the_threshold_with_no_pointer_event() {
let mut root = root();
let mut state = TapState::default();
let mut sink = NullScene;
root.event(&mut state, &ev(PointerPhase::Down, 10.0, 10.0));
root.paint(&mut sink, ft(0.0)); let crossing = root.paint(&mut sink, ft(600.0)); assert!(
crossing.needs_frame,
"the threshold-crossing paint requests a follow-up frame so a \
dirty-driven desktop shell actually reaches the rebuild that fires"
);
assert_eq!(state.long_presses, 0, "paint only latches; it never fires");
let flags = root.rebuild(&mut long_press_logic, &mut state);
assert_eq!(
state.long_presses, 1,
"a stationary hold fires on_long_press at the threshold, in \
wall time, with no pointer event"
);
assert_eq!(state.taps, 0);
assert!(
flags.needs_paint(),
"the fire's `request_redraw` folds into the rebuild's flags, which \
is what the mobile frame gate reads"
);
let after = root.paint(&mut sink, ft(620.0));
assert!(
after.needs_frame,
"and surfaces as `needs_frame` for the desktop `Wait` loop — the \
widget itself stops requesting frames once it has fired"
);
}
#[test]
fn housekeeping_fire_wins_the_race_and_a_later_move_or_up_is_inert() {
let mut root = root();
let mut state = TapState::default();
let mut sink = NullScene;
root.event(&mut state, &ev(PointerPhase::Down, 10.0, 10.0));
root.paint(&mut sink, ft(0.0));
root.paint(&mut sink, ft(600.0)); root.rebuild(&mut long_press_logic, &mut state); assert_eq!(state.long_presses, 1);
root.event(&mut state, &ev(PointerPhase::Move, 11.0, 11.0));
assert_eq!(
state.long_presses, 1,
"a post-fire Move must not double-fire"
);
assert_eq!(state.taps, 0);
root.event(&mut state, &ev(PointerPhase::Up, 11.0, 11.0));
assert_eq!(state.long_presses, 1, "a post-fire Up must not double-fire");
assert_eq!(
state.taps, 0,
"a post-fire Up must not fall through to on_tap"
);
}
#[test]
fn drag_before_threshold_then_rebuild_never_fires_via_housekeeping() {
let mut root = root();
let mut state = TapState::default();
let mut sink = NullScene;
root.event(&mut state, &ev(PointerPhase::Down, 10.0, 10.0));
root.paint(&mut sink, ft(0.0));
root.paint(&mut sink, ft(100.0)); root.event(&mut state, &ev(PointerPhase::Move, 10.0, 40.0)); root.event(&mut state, &ev(PointerPhase::Up, 10.0, 40.0));
root.rebuild(&mut long_press_logic, &mut state);
assert_eq!(
state.long_presses, 0,
"a pre-threshold drag never fires via Housekeeping"
);
assert_eq!(state.taps, 0, "a dragged press is not a tap either");
}
#[test]
fn quick_tap_before_threshold_fires_tap_only() {
let mut root = root();
let mut state = TapState::default();
let mut sink = NullScene;
root.event(&mut state, &ev(PointerPhase::Down, 10.0, 10.0));
root.paint(&mut sink, ft(0.0));
root.paint(&mut sink, ft(100.0)); root.event(&mut state, &ev(PointerPhase::Up, 12.0, 12.0));
assert_eq!(state.taps, 1, "a quick release is a tap");
assert_eq!(state.long_presses, 0, "under threshold must not long-press");
}
#[test]
fn quick_tap_then_rebuild_never_retroactively_long_presses() {
let mut root = root();
let mut state = TapState::default();
let mut sink = NullScene;
root.event(&mut state, &ev(PointerPhase::Down, 10.0, 10.0));
root.paint(&mut sink, ft(0.0));
root.paint(&mut sink, ft(100.0)); root.event(&mut state, &ev(PointerPhase::Up, 12.0, 12.0));
root.rebuild(&mut long_press_logic, &mut state);
assert_eq!(state.taps, 1, "the tap already resolved on Up");
assert_eq!(
state.long_presses, 0,
"a subsequent rebuild must not retroactively long-press a released tap"
);
}
#[test]
fn move_after_threshold_fires_long_press_before_release() {
let mut root = root();
let mut state = TapState::default();
let mut sink = NullScene;
root.event(&mut state, &ev(PointerPhase::Down, 10.0, 10.0));
root.paint(&mut sink, ft(0.0));
root.paint(&mut sink, ft(600.0)); root.event(&mut state, &ev(PointerPhase::Move, 12.0, 11.0));
assert_eq!(state.long_presses, 1, "fires on the post-threshold move");
root.event(&mut state, &ev(PointerPhase::Up, 12.0, 11.0));
assert_eq!(
state.long_presses, 1,
"Up after a fired long-press is inert"
);
assert_eq!(state.taps, 0);
}
#[test]
fn hold_then_drag_then_up_fires_neither() {
let mut root = root();
let mut state = TapState::default();
let mut sink = NullScene;
root.event(&mut state, &ev(PointerPhase::Down, 10.0, 10.0));
root.paint(&mut sink, ft(0.0));
root.paint(&mut sink, ft(600.0)); root.event(&mut state, &ev(PointerPhase::Move, 10.0, 40.0));
root.event(&mut state, &ev(PointerPhase::Up, 10.0, 40.0));
assert_eq!(state.long_presses, 0, "a drag disarms the long-press");
assert_eq!(state.taps, 0, "a drag disarms the tap");
}
#[test]
fn cancel_mid_hold_fires_neither_and_leaves_state_untouched() {
let mut root = root();
let mut state = TapState::default();
let mut sink = NullScene;
root.event(&mut state, &ev(PointerPhase::Down, 10.0, 10.0));
root.paint(&mut sink, ft(0.0));
root.paint(&mut sink, ft(600.0)); root.event(&mut state, &ev(PointerPhase::Cancel, 10.0, 10.0));
root.event(&mut state, &ev(PointerPhase::Up, 10.0, 10.0));
assert_eq!(state.long_presses, 0, "Cancel fires no long-press");
assert_eq!(state.taps, 0, "Cancel fires no tap");
}
#[test]
fn paint_requests_frames_while_pressed_and_stops_after_up() {
let mut root = root();
let mut state = TapState::default();
let mut sink = NullScene;
root.event(&mut state, &ev(PointerPhase::Down, 10.0, 10.0));
assert!(
root.paint(&mut sink, ft(0.0)).needs_frame,
"a pending long-press timer requests continuation frames"
);
root.event(&mut state, &ev(PointerPhase::Up, 10.0, 10.0));
assert!(
!root.paint(&mut sink, ft(16.0)).needs_frame,
"the timer stops requesting frames after Up"
);
}
#[test]
fn only_on_tap_still_behaves_as_before() {
let mut w = widget(true);
let mut state = TapState::default();
dispatch(&mut w, &mut state, &ev(PointerPhase::Down, 10.0, 10.0));
dispatch(&mut w, &mut state, &ev(PointerPhase::Up, 12.0, 12.0));
assert_eq!(state.taps, 1);
assert_eq!(state.long_presses, 0);
}
fn tap_only_root() -> RenderRoot<TapState, GestureDetectorView<TapState>> {
fn logic(_: &mut TapState) -> GestureDetectorView<TapState> {
GestureDetector::<TapState, _>(Blank).on_tap(|s: &mut TapState| s.taps += 1)
}
let mut root = RenderRoot::new();
let mut state = TapState::default();
root.rebuild(&mut logic, &mut state);
root.layout(Size::new(100.0, 100.0));
root
}
#[test]
fn tap_only_hold_past_threshold_then_up_still_fires_tap() {
let mut root = tap_only_root();
let mut state = TapState::default();
let mut sink = NullScene;
root.event(&mut state, &ev(PointerPhase::Down, 10.0, 10.0));
root.paint(&mut sink, ft(0.0)); root.paint(&mut sink, ft(600.0)); root.event(&mut state, &ev(PointerPhase::Up, 11.0, 11.0));
assert_eq!(
state.taps, 1,
"a held press with no on_long_press still taps on release"
);
assert_eq!(state.long_presses, 0);
}
#[test]
fn tap_only_hold_then_within_slop_move_taps_on_release_not_on_move() {
let mut root = tap_only_root();
let mut state = TapState::default();
let mut sink = NullScene;
root.event(&mut state, &ev(PointerPhase::Down, 10.0, 10.0));
root.paint(&mut sink, ft(0.0));
root.paint(&mut sink, ft(600.0));
root.event(&mut state, &ev(PointerPhase::Move, 12.0, 11.0));
assert_eq!(
state.taps, 0,
"a tap-only detector fires nothing on the move"
);
assert_eq!(state.long_presses, 0);
root.event(&mut state, &ev(PointerPhase::Up, 12.0, 11.0));
assert_eq!(state.taps, 1, "the tap fires on the in-bounds release");
assert_eq!(state.long_presses, 0);
}
#[test]
fn tap_only_press_requests_no_continuation_frames() {
let mut root = tap_only_root();
let mut state = TapState::default();
let mut sink = NullScene;
root.event(&mut state, &ev(PointerPhase::Down, 10.0, 10.0));
assert!(
!root.paint(&mut sink, ft(0.0)).needs_frame,
"a tap-only press has no timer, so it requests no continuation frames"
);
}
#[test]
fn both_handlers_long_press_behavior_unchanged() {
let mut root = root();
let mut state = TapState::default();
let mut sink = NullScene;
root.event(&mut state, &ev(PointerPhase::Down, 10.0, 10.0));
root.paint(&mut sink, ft(0.0));
root.paint(&mut sink, ft(600.0));
root.event(&mut state, &ev(PointerPhase::Up, 11.0, 11.0));
assert_eq!(state.long_presses, 1, "both wired: hold still long-presses");
assert_eq!(state.taps, 0, "both wired: a long-press never also taps");
}
#[derive(Default)]
struct HoldState {
taps: u32,
long_presses: u32,
progress: Vec<f64>,
}
fn hold_ev(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
InputEvent::Pointer(frust_core::PointerEvent {
phase,
position: Point::new(x, y),
button: frust_core::PointerButton::Primary,
})
}
fn progress_root() -> RenderRoot<HoldState, GestureDetectorView<HoldState>> {
fn logic(_: &mut HoldState) -> GestureDetectorView<HoldState> {
GestureDetector::<HoldState, _>(Blank)
.on_tap(|s: &mut HoldState| s.taps += 1)
.on_long_press(|s: &mut HoldState| s.long_presses += 1)
.on_hold_progress(|s: &mut HoldState, p| s.progress.push(p))
}
let mut root = RenderRoot::new();
let mut state = HoldState::default();
root.rebuild(&mut logic, &mut state);
root.layout(Size::new(100.0, 100.0));
root
}
fn progress_root_with_threshold(
ms: u64,
) -> RenderRoot<HoldState, GestureDetectorView<HoldState>> {
fn logic(state: &mut HoldState, ms: u64) -> GestureDetectorView<HoldState> {
let _ = state;
GestureDetector::<HoldState, _>(Blank)
.on_long_press(|s: &mut HoldState| s.long_presses += 1)
.on_hold_progress(|s: &mut HoldState, p| s.progress.push(p))
.hold_threshold_ms(ms)
}
let mut root = RenderRoot::new();
let mut state = HoldState::default();
root.rebuild(&mut |s: &mut HoldState| logic(s, ms), &mut state);
root.layout(Size::new(100.0, 100.0));
root
}
#[test]
fn hold_progress_increases_monotonically_and_long_press_still_fires() {
let mut root = progress_root();
let mut state = HoldState::default();
let mut sink = NullScene;
root.event(&mut state, &hold_ev(PointerPhase::Down, 10.0, 10.0));
root.paint(&mut sink, ft(0.0));
root.event(&mut state, &hold_ev(PointerPhase::Move, 10.0, 10.0)); root.paint(&mut sink, ft(150.0));
root.event(&mut state, &hold_ev(PointerPhase::Move, 11.0, 10.0));
root.paint(&mut sink, ft(300.0));
root.event(&mut state, &hold_ev(PointerPhase::Move, 10.0, 11.0));
root.paint(&mut sink, ft(500.0)); root.event(&mut state, &hold_ev(PointerPhase::Move, 11.0, 11.0));
assert_eq!(
state.progress,
vec![0.0, 0.3, 0.6, 1.0],
"progress observations increase monotonically 0..1"
);
assert_eq!(state.long_presses, 1, "threshold reached: long-press fires");
assert_eq!(state.taps, 0);
root.event(&mut state, &hold_ev(PointerPhase::Up, 11.0, 11.0));
assert_eq!(state.long_presses, 1);
assert_eq!(
state.progress.len(),
4,
"Up after a fired long-press adds no observation"
);
}
fn progress_logic(_: &mut HoldState) -> GestureDetectorView<HoldState> {
GestureDetector::<HoldState, _>(Blank)
.on_tap(|s: &mut HoldState| s.taps += 1)
.on_long_press(|s: &mut HoldState| s.long_presses += 1)
.on_hold_progress(|s: &mut HoldState, p| s.progress.push(p))
}
#[test]
fn housekeeping_fire_delivers_the_paired_final_progress_and_a_later_drag_is_inert() {
let mut root = progress_root();
let mut state = HoldState::default();
let mut sink = NullScene;
root.event(&mut state, &hold_ev(PointerPhase::Down, 10.0, 10.0));
root.paint(&mut sink, ft(0.0));
root.paint(&mut sink, ft(500.0)); root.rebuild(&mut progress_logic, &mut state); assert_eq!(state.long_presses, 1);
assert_eq!(
state.progress,
vec![1.0],
"the paired final progress observation rides the same fire"
);
root.event(&mut state, &hold_ev(PointerPhase::Move, 10.0, 40.0)); assert_eq!(state.long_presses, 1, "no double-fire on a post-fire drag");
assert_eq!(
state.progress,
vec![1.0],
"a post-fire Move delivers no further progress observation"
);
}
#[test]
fn early_lift_delivers_a_final_zero_observation_and_no_long_press() {
let mut root = progress_root();
let mut state = HoldState::default();
let mut sink = NullScene;
root.event(&mut state, &hold_ev(PointerPhase::Down, 10.0, 10.0));
root.paint(&mut sink, ft(0.0));
root.paint(&mut sink, ft(250.0)); root.event(&mut state, &hold_ev(PointerPhase::Up, 11.0, 11.0));
assert_eq!(
state.progress.last().copied(),
Some(0.0),
"an early lift's final observation resets to exactly 0.0"
);
assert_eq!(state.long_presses, 0, "early lift never long-presses");
}
#[test]
fn move_past_slop_mid_hold_delivers_a_final_zero_and_no_long_press() {
let mut root = progress_root();
let mut state = HoldState::default();
let mut sink = NullScene;
root.event(&mut state, &hold_ev(PointerPhase::Down, 10.0, 10.0));
root.paint(&mut sink, ft(0.0));
root.paint(&mut sink, ft(300.0)); root.event(&mut state, &hold_ev(PointerPhase::Move, 10.0, 40.0));
root.event(&mut state, &hold_ev(PointerPhase::Up, 10.0, 40.0));
assert_eq!(
state.progress.last().copied(),
Some(0.0),
"a drag past slop delivers a final 0.0 observation"
);
assert_eq!(state.long_presses, 0);
assert_eq!(state.taps, 0);
}
#[test]
fn hold_threshold_ms_override_is_respected_by_both_callbacks() {
let mut root = progress_root_with_threshold(700);
let mut state = HoldState::default();
let mut sink = NullScene;
root.event(&mut state, &hold_ev(PointerPhase::Down, 10.0, 10.0));
root.paint(&mut sink, ft(0.0));
root.paint(&mut sink, ft(600.0)); root.event(&mut state, &hold_ev(PointerPhase::Move, 10.0, 10.0));
assert_eq!(
state.long_presses, 0,
"the 700ms override must not fire long-press at 600ms"
);
assert!(
(state.progress.last().copied().unwrap() - 600.0 / 700.0).abs() < 1e-9,
"progress reflects the overridden 700ms threshold, not the 500ms default"
);
root.paint(&mut sink, ft(700.0)); root.event(&mut state, &hold_ev(PointerPhase::Move, 11.0, 10.0));
assert_eq!(
state.long_presses, 1,
"the 700ms override still fires long-press once reached"
);
assert_eq!(state.progress.last().copied(), Some(1.0));
}
#[test]
fn no_hold_progress_handler_is_a_benign_noop() {
let mut w = widget(true);
let mut state = TapState::default();
dispatch(&mut w, &mut state, &ev(PointerPhase::Down, 10.0, 10.0));
dispatch(&mut w, &mut state, &ev(PointerPhase::Move, 11.0, 11.0));
dispatch(&mut w, &mut state, &ev(PointerPhase::Up, 12.0, 12.0));
assert_eq!(state.taps, 1);
assert_eq!(state.long_presses, 0);
}
#[test]
fn cancel_leaves_progress_stale_until_fresh_down_cycle_delivers_clean_value() {
let mut root = progress_root();
let mut state = HoldState::default();
let mut sink = NullScene;
root.event(&mut state, &hold_ev(PointerPhase::Down, 10.0, 10.0));
root.paint(&mut sink, ft(0.0));
root.event(&mut state, &hold_ev(PointerPhase::Move, 10.0, 10.0)); root.paint(&mut sink, ft(150.0)); root.event(&mut state, &hold_ev(PointerPhase::Move, 11.0, 10.0)); let before_cancel_len = state.progress.len();
let stale_value = *state.progress.last().unwrap();
assert!(
stale_value > 0.0,
"sanity: mid-hold progress is non-zero before Cancel"
);
root.event(&mut state, &hold_ev(PointerPhase::Cancel, 11.0, 10.0));
assert_eq!(
state.progress.len(),
before_cancel_len,
"Cancel delivers no observation — the Cancel-never-mutates-state convention"
);
root.event(&mut state, &hold_ev(PointerPhase::Down, 10.0, 10.0));
root.paint(&mut sink, ft(150.0)); root.event(&mut state, &hold_ev(PointerPhase::Move, 10.0, 10.0));
let fresh_value = *state.progress.last().unwrap();
assert!(
fresh_value < stale_value,
"a fresh Down-cycle's first observation starts low, self-correcting the stale \
ring (fresh={fresh_value}, stale={stale_value})"
);
assert_eq!(
fresh_value, 0.0,
"a fresh press-start yields exactly 0.0 progress"
);
}
#[test]
fn zero_threshold_guard_yields_finite_progress_never_nan() {
let mut root = progress_root_with_threshold(0);
let mut state = HoldState::default();
let mut sink = NullScene;
root.event(&mut state, &hold_ev(PointerPhase::Down, 10.0, 10.0));
root.paint(&mut sink, ft(0.0));
root.event(&mut state, &hold_ev(PointerPhase::Move, 10.0, 10.0)); let p0 = state.progress[0];
assert!(!p0.is_nan(), "zero threshold must never yield NaN progress");
assert!(
(0.0..=1.0).contains(&p0),
"progress stays in the defined [0.0, 1.0] range"
);
root.paint(&mut sink, ft(1.0));
root.event(&mut state, &hold_ev(PointerPhase::Move, 11.0, 10.0));
assert_eq!(
state.long_presses, 1,
"long-press fires almost immediately at the 1ms floor"
);
}
}