use std::cell::Cell;
use geometry_core::Rect;
use platform_core::{Event, ModifiersState, PointerButton};
use ui_tree::EventResult;
use crate::pointer::PointerButtons;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DragStart {
pub button: PointerButton,
pub modifiers: ModifiersState,
}
thread_local! {
static ACTIVE: Cell<Option<(DragStart, f32)>> = const { Cell::new(None) };
}
pub fn drag_start() -> Option<DragStart> {
ACTIVE.with(|a| a.get()).map(|(start, _)| start)
}
pub fn drag_travel() -> f32 {
ACTIVE.with(|a| a.get()).map_or(0.0, |(_, travel)| travel)
}
pub(crate) struct DragGesture {
on_drag: Option<Box<dyn Fn(f32, f32)>>,
on_drag_end: Option<Box<dyn Fn(f32, f32)>>,
arms: PointerButtons,
origin: Option<((f32, f32), DragStart)>,
threshold: f32,
travel: f32,
started: bool,
last: (f32, f32),
}
impl Default for DragGesture {
fn default() -> Self {
Self {
on_drag: None,
on_drag_end: None,
arms: PointerButtons {
primary: true,
..PointerButtons::default()
},
origin: None,
threshold: 0.0,
travel: 0.0,
started: false,
last: (0.0, 0.0),
}
}
}
impl DragGesture {
pub(crate) fn set(&mut self, f: impl Fn(f32, f32) + 'static) {
self.on_drag = Some(Box::new(f));
}
pub(crate) fn set_end(&mut self, f: impl Fn(f32, f32) + 'static) {
self.on_drag_end = Some(Box::new(f));
}
pub(crate) fn arm_with(&mut self, button: &PointerButton) {
self.arms = self.arms.with(button);
}
pub(crate) fn arms(&self, button: &PointerButton) -> bool {
self.arms.holds(button)
}
pub(crate) fn is_set(&self) -> bool {
self.on_drag.is_some() || self.on_drag_end.is_some()
}
pub(crate) fn set_threshold(&mut self, px: f32) {
self.threshold = px.max(0.0);
}
pub(crate) fn has_started(&self) -> bool {
self.started
}
pub(crate) fn has_threshold(&self) -> bool {
self.threshold > 0.0
}
pub(crate) fn press(&mut self, event: &Event, rect: Rect) -> EventResult {
if let Event::PointerPressed { x, y, button, .. } = event
&& self.arms(button)
&& rect.contains(*x as f32, *y as f32)
{
let local = (*x as f32 - rect.x, *y as f32 - rect.y);
self.origin = Some((
local,
DragStart {
button: *button,
modifiers: crate::keyboard::modifiers(),
},
));
self.travel = 0.0;
self.started = !self.has_threshold();
if self.started {
self.report(local.0, local.1);
}
return EventResult::Handled;
}
EventResult::Ignored
}
pub(crate) fn moved(&mut self, event: &Event, rect: Rect) -> EventResult {
let (Some((press, _)), Event::PointerMoved { x, y, .. }) = (self.origin, event) else {
return EventResult::Ignored;
};
let local = (*x as f32 - rect.x, *y as f32 - rect.y);
let (dx, dy) = (local.0 - press.0, local.1 - press.1);
self.travel = self.travel.max(dx.hypot(dy));
self.started |= self.travel > self.threshold;
if !self.started {
return EventResult::Ignored;
}
self.report(local.0, local.1);
EventResult::Handled
}
fn report(&mut self, x: f32, y: f32) {
self.last = (x, y);
let Some((_, start)) = self.origin else {
return;
};
if let Some(cb) = &self.on_drag {
in_drag(start, self.travel, || cb(x, y));
}
}
pub(crate) fn end(&mut self, at: Option<(f32, f32)>) -> bool {
let Some((_, start)) = self.origin.take() else {
return false;
};
let was_dragging = std::mem::take(&mut self.started);
if was_dragging {
let (x, y) = at.unwrap_or(self.last);
self.last = (x, y);
if let Some(cb) = &self.on_drag_end {
in_drag(start, self.travel, || cb(x, y));
}
}
was_dragging
}
}
fn in_drag<R>(start: DragStart, travel: f32, f: impl FnOnce() -> R) -> R {
let outer = ACTIVE.with(|a| a.replace(Some((start, travel))));
let out = f();
ACTIVE.with(|a| a.set(outer));
out
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::rc::Rc;
use platform_core::PointerSource;
use super::*;
const RECT: Rect = Rect {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
};
fn press_at(x: f32, y: f32) -> Event {
Event::PointerPressed {
x: x as f64,
y: y as f64,
button: PointerButton::Primary,
source: PointerSource::Mouse,
}
}
fn move_to(x: f32, y: f32) -> Event {
Event::PointerMoved {
x: x as f64,
y: y as f64,
source: PointerSource::Mouse,
}
}
type Log = Rc<RefCell<Vec<((f32, f32), Option<DragStart>)>>>;
fn logging(threshold: f32) -> (DragGesture, Log) {
let log: Log = Rc::new(RefCell::new(Vec::new()));
let mut drag = DragGesture::default();
drag.set_threshold(threshold);
let sink = log.clone();
drag.set(move |x, y| sink.borrow_mut().push(((x, y), drag_start())));
(drag, log)
}
#[test]
fn without_a_threshold_the_press_itself_reports() {
let (mut drag, log) = logging(0.0);
assert_eq!(
drag.press(&press_at(30.0, 40.0), RECT),
EventResult::Handled
);
assert_eq!(log.borrow().len(), 1, "the press reported straight away");
assert_eq!(log.borrow()[0].0, (30.0, 40.0));
}
#[test]
fn a_press_that_never_travels_is_not_a_drag() {
let (mut drag, log) = logging(4.0);
drag.press(&press_at(30.0, 40.0), RECT);
drag.moved(&move_to(32.0, 41.0), RECT);
assert!(log.borrow().is_empty(), "two pixels is not a drag");
assert!(!drag.end(None), "so nothing was dragged to end");
}
#[test]
fn crossing_the_threshold_starts_the_drag_where_it_crossed() {
let (mut drag, log) = logging(4.0);
drag.press(&press_at(30.0, 40.0), RECT);
drag.moved(&move_to(32.0, 40.0), RECT);
drag.moved(&move_to(50.0, 40.0), RECT);
assert_eq!(log.borrow().len(), 1, "only the move that cleared it");
assert_eq!(log.borrow()[0].0, (50.0, 40.0), "and not back at the press");
assert!(drag.end(None), "this one really was a drag");
}
#[test]
fn a_drag_reports_what_armed_it_and_not_what_is_held_now() {
crate::keyboard::reset();
crate::keyboard::observe(&Event::ModifiersChanged {
modifiers: ModifiersState {
is_shift: true,
..Default::default()
},
});
let (mut drag, log) = logging(0.0);
drag.press(&press_at(10.0, 10.0), RECT);
crate::keyboard::observe(&Event::ModifiersChanged {
modifiers: ModifiersState::default(),
});
drag.moved(&move_to(40.0, 10.0), RECT);
crate::keyboard::reset();
let entries = log.borrow();
assert!(
entries.iter().all(|(_, start)| start
.is_some_and(|s| s.modifiers.is_shift && s.button == PointerButton::Primary)),
"every report names the press, including the one after Shift was released: {entries:?}"
);
assert_eq!(
drag_start(),
None,
"and nothing leaks out of the callback it was scoped to"
);
}
}