use crate::draggable::DraggableState;
use crate::{collect_modifier_slices, Modifier};
use cranpose_core::{DefaultScheduler, Runtime};
use cranpose_foundation::{
BasicModifierNodeContext, ModifierNodeChain, PointerButton, PointerButtons, PointerEvent,
PointerEventKind, DRAG_THRESHOLD,
};
use cranpose_ui_graphics::Point;
use cranpose_ui_layout::Axis;
use std::cell::Cell;
use std::rc::Rc;
use std::sync::Arc;
fn with_test_runtime<T>(body: impl FnOnce() -> T) -> T {
let _runtime = Runtime::new(Arc::new(DefaultScheduler));
body()
}
fn pointer_handler_for(modifier: Modifier) -> (Rc<dyn Fn(PointerEvent)>, ModifierNodeChain) {
let elements = modifier.elements();
let mut chain = ModifierNodeChain::new();
let mut context = BasicModifierNodeContext::new();
chain.update_from_slice(&elements, &mut context);
let slices = collect_modifier_slices(&chain);
let handler = slices
.pointer_inputs()
.first()
.cloned()
.expect("draggable modifier should provide a pointer input handler");
(handler, chain)
}
fn event(kind: PointerEventKind, x: f32, y: f32) -> PointerEvent {
PointerEvent::new(kind, Point { x, y }, Point { x, y })
.with_buttons(PointerButtons::new().with(PointerButton::Primary))
}
fn recording_state() -> (DraggableState, Rc<Cell<f32>>) {
let total = Rc::new(Cell::new(0.0));
let recorder = Rc::clone(&total);
let state = DraggableState::new(move |delta| recorder.set(recorder.get() + delta));
(state, total)
}
#[test]
fn a_vertical_drag_follows_the_finger() {
let _app_context = crate::render_state::app_context_test_scope();
with_test_runtime(|| {
let (state, total) = recording_state();
let (handler, _chain) =
pointer_handler_for(Modifier::empty().draggable(Axis::Vertical, state.clone()));
handler(event(PointerEventKind::Down, 0.0, 100.0));
handler(event(PointerEventKind::Move, 0.0, 160.0));
handler(event(PointerEventKind::Up, 0.0, 160.0));
assert!(
(total.get() - 60.0).abs() < 0.001,
"a finger moved 60 down must report +60, got {}",
total.get()
);
assert_eq!(state.offset(), total.get());
});
}
#[test]
fn a_horizontal_drag_reports_the_other_axis() {
let _app_context = crate::render_state::app_context_test_scope();
with_test_runtime(|| {
let (state, total) = recording_state();
let (handler, _chain) =
pointer_handler_for(Modifier::empty().draggable(Axis::Horizontal, state));
handler(event(PointerEventKind::Down, 100.0, 0.0));
handler(event(PointerEventKind::Move, 40.0, 0.0));
handler(event(PointerEventKind::Up, 40.0, 0.0));
assert!(
(total.get() + 60.0).abs() < 0.001,
"a finger moved 60 left must report -60, got {}",
total.get()
);
});
}
#[test]
fn a_movement_inside_the_touch_slop_is_not_a_drag() {
let _app_context = crate::render_state::app_context_test_scope();
with_test_runtime(|| {
let (state, total) = recording_state();
let (handler, _chain) =
pointer_handler_for(Modifier::empty().draggable(Axis::Vertical, state.clone()));
handler(event(PointerEventKind::Down, 0.0, 100.0));
handler(event(
PointerEventKind::Move,
0.0,
100.0 + DRAG_THRESHOLD - 1.0,
));
assert_eq!(total.get(), 0.0, "a finger inside the slop has not dragged");
assert!(!state.is_dragging());
});
}
#[test]
fn a_drag_along_the_other_axis_never_reaches_this_one() {
let _app_context = crate::render_state::app_context_test_scope();
with_test_runtime(|| {
let (state, total) = recording_state();
let (handler, _chain) =
pointer_handler_for(Modifier::empty().draggable(Axis::Vertical, state));
handler(event(PointerEventKind::Down, 100.0, 100.0));
handler(event(PointerEventKind::Move, 200.0, 104.0));
handler(event(PointerEventKind::Move, 240.0, 180.0));
assert_eq!(
total.get(),
0.0,
"a vertical draggable must not steal a horizontal gesture"
);
});
}
#[test]
fn an_event_another_modifier_consumed_ends_the_drag() {
let _app_context = crate::render_state::app_context_test_scope();
with_test_runtime(|| {
let (state, total) = recording_state();
let (handler, _chain) =
pointer_handler_for(Modifier::empty().draggable(Axis::Vertical, state.clone()));
handler(event(PointerEventKind::Down, 0.0, 100.0));
handler(event(PointerEventKind::Move, 0.0, 160.0));
assert!((total.get() - 60.0).abs() < 0.001);
let claimed = event(PointerEventKind::Move, 0.0, 220.0);
claimed.consume();
handler(claimed);
handler(event(PointerEventKind::Move, 0.0, 280.0));
assert!(
(total.get() - 60.0).abs() < 0.001,
"a consumed event ends the gesture rather than being applied twice, got {}",
total.get()
);
assert!(!state.is_dragging());
});
}
#[test]
fn dragging_is_reported_for_the_length_of_the_gesture() {
let _app_context = crate::render_state::app_context_test_scope();
with_test_runtime(|| {
let (state, _total) = recording_state();
let (handler, _chain) =
pointer_handler_for(Modifier::empty().draggable(Axis::Vertical, state.clone()));
assert!(!state.is_dragging());
handler(event(PointerEventKind::Down, 0.0, 100.0));
assert!(!state.is_dragging(), "a press alone is not a drag");
handler(event(PointerEventKind::Move, 0.0, 160.0));
assert!(state.is_dragging());
handler(event(PointerEventKind::Up, 0.0, 160.0));
assert!(!state.is_dragging());
});
}
#[test]
fn a_cancelled_drag_stops_reporting() {
let _app_context = crate::render_state::app_context_test_scope();
with_test_runtime(|| {
let (state, total) = recording_state();
let (handler, _chain) =
pointer_handler_for(Modifier::empty().draggable(Axis::Vertical, state.clone()));
handler(event(PointerEventKind::Down, 0.0, 100.0));
handler(event(PointerEventKind::Move, 0.0, 160.0));
handler(event(PointerEventKind::Cancel, 0.0, 160.0));
handler(event(PointerEventKind::Move, 0.0, 220.0));
assert!(
(total.get() - 60.0).abs() < 0.001,
"a cancelled gesture reports nothing further, got {}",
total.get()
);
assert!(!state.is_dragging());
});
}
#[test]
fn a_guard_that_says_no_declines_the_gesture() {
let _app_context = crate::render_state::app_context_test_scope();
with_test_runtime(|| {
let (state, total) = recording_state();
let enabled = Rc::new(Cell::new(false));
let guard = Rc::clone(&enabled);
let (handler, _chain) = pointer_handler_for(Modifier::empty().draggable_guarded(
Axis::Vertical,
state,
move || guard.get(),
));
handler(event(PointerEventKind::Down, 0.0, 100.0));
handler(event(PointerEventKind::Move, 0.0, 160.0));
assert_eq!(total.get(), 0.0, "a declined gesture reports nothing");
enabled.set(true);
handler(event(PointerEventKind::Down, 0.0, 100.0));
handler(event(PointerEventKind::Move, 0.0, 160.0));
assert!((total.get() - 60.0).abs() < 0.001);
});
}
#[test]
fn releasing_a_fast_drag_places_the_control_rather_than_flinging_it() {
let _app_context = crate::render_state::app_context_test_scope();
with_test_runtime(|| {
let (state, total) = recording_state();
let (handler, _chain) =
pointer_handler_for(Modifier::empty().draggable(Axis::Vertical, state));
handler(
PointerEvent::new(
PointerEventKind::Down,
Point { x: 0.0, y: 0.0 },
Point { x: 0.0, y: 0.0 },
)
.with_buttons(PointerButtons::new().with(PointerButton::Primary))
.with_time_ms(Some(0)),
);
for step in 1..=6 {
let y = step as f32 * 40.0;
handler(
PointerEvent::new(
PointerEventKind::Move,
Point { x: 0.0, y },
Point { x: 0.0, y },
)
.with_buttons(PointerButtons::new().with(PointerButton::Primary))
.with_time_ms(Some(step * 8)),
);
}
let placed = total.get();
handler(
PointerEvent::new(
PointerEventKind::Up,
Point { x: 0.0, y: 240.0 },
Point { x: 0.0, y: 240.0 },
)
.with_buttons(PointerButtons::new().with(PointerButton::Primary))
.with_time_ms(Some(48)),
);
assert_eq!(
total.get(),
placed,
"releasing a drag must not carry the control any further"
);
});
}