1use core::time::Duration;
10
11use blockworx_geom::{Pos2, Vec2};
12use blockworx_paint::{Button, Event, Input, Interaction, Press, Raw, Vantage};
13
14const MAX_CLICK_DIST: f32 = 6.0;
16const MAX_CLICK_DURATION: Duration = Duration::from_millis(800);
18const MAX_DOUBLE_CLICK_DELAY: Duration = Duration::from_millis(300);
20
21#[derive(Clone, Copy, Debug)]
24struct Pressed {
25 origin: Pos2,
26 since: Duration,
27 strayed: bool,
30 phase: Phase,
31}
32
33#[derive(Clone, Copy, PartialEq, Eq, Debug)]
35enum Phase {
36 Held,
38 Dragging,
39 Cancelled,
41}
42
43impl Pressed {
44 fn could_click(&self, now: Duration) -> bool {
45 !self.strayed && now.saturating_sub(self.since) <= MAX_CLICK_DURATION
46 }
47}
48
49#[derive(Clone, Copy, Debug)]
50struct Click {
51 at: Duration,
52 pos: Pos2,
53}
54
55#[derive(Clone, Copy, PartialEq, Eq, Debug)]
58enum Over {
59 Canvas,
60 Elsewhere,
61}
62
63#[derive(Debug)]
65pub struct Resolver {
66 latest: Option<Pos2>,
68 acted: Option<Pos2>,
71 over: Over,
72 press: Option<Pressed>,
73 last_click: Option<Click>,
74}
75
76impl Default for Resolver {
77 fn default() -> Self {
78 Self {
79 latest: None,
80 acted: None,
81 over: Over::Elsewhere,
82 press: None,
83 last_click: None,
84 }
85 }
86}
87
88impl Resolver {
89 pub fn latest(&self) -> Option<Pos2> {
91 self.latest
92 }
93
94 pub fn resolve(
98 &mut self,
99 input: &Input,
100 now: Duration,
101 vantage: Vantage,
102 origin: Pos2,
103 ) -> Interaction {
104 let before = self.latest;
105 let mut resolved = None;
106 let mut panning = false;
107 for raw in &input.raw {
108 match *raw {
109 Raw::Moved(pos) => {
110 self.latest = Some(pos);
111 self.acted = Some(pos);
112 self.over = Over::Canvas;
113 if let Some(press) = &mut self.press
114 && press.origin.distance(pos) > MAX_CLICK_DIST
115 {
116 press.strayed = true;
117 }
118 }
119 Raw::Down { pos, button } => {
120 self.latest = Some(pos);
121 self.acted = Some(pos);
122 if button == Button::Primary {
123 self.press = Some(Pressed {
124 origin: pos,
125 since: now,
126 strayed: false,
127 phase: Phase::Held,
128 });
129 }
130 }
131 Raw::Up { pos, button } => {
132 self.latest = Some(pos);
133 self.acted = Some(pos);
134 if button == Button::Primary
135 && let Some(press) = self.press.take()
136 {
137 resolved = self.released(press, pos, now);
138 }
139 }
140 Raw::Gone => {
141 self.latest = None;
142 self.over = Over::Elsewhere;
143 if let Some(press) = self.press.take()
147 && press.phase == Phase::Dragging
148 {
149 resolved = self.acted.map(|pos| Event::DragStopped { pos });
150 }
151 }
152 Raw::Cancelled => {
153 if let Some(press) = &mut self.press {
154 if press.phase == Phase::Dragging {
155 resolved = self.acted.map(|pos| Event::DragStopped { pos });
156 }
157 press.phase = Phase::Cancelled;
158 }
159 }
160 Raw::Panning => panning = true,
161 }
162 }
163 let mut event = resolved.or_else(|| self.dragged(before, now));
164 if event.is_none()
165 && self.over == Over::Canvas
166 && let Some(pos) = self.latest
167 {
168 event = Some(Event::HoverAt(pos));
169 }
170 let mut press = self.press.map(|press| Press {
171 origin: press.origin,
172 });
173 if panning {
174 if matches!(
175 event,
176 Some(
177 Event::DragStarted { .. } | Event::Dragging { .. } | Event::DragStopped { .. }
178 )
179 ) {
180 event = None;
181 }
182 press = None;
186 }
187 let to_world = |screen: Pos2| vantage.screen_to_world(origin, screen);
188 let event = event.map(|event| match event {
189 Event::HoverAt(pos) => Event::HoverAt(to_world(pos)),
190 Event::DragStarted { pos } => Event::DragStarted { pos: to_world(pos) },
191 Event::Dragging { pos, delta } => Event::Dragging {
192 pos: to_world(pos),
193 delta: delta / vantage.zoom.get(),
194 },
195 Event::DragStopped { pos } => Event::DragStopped { pos: to_world(pos) },
196 Event::Clicked { pos } => Event::Clicked { pos: to_world(pos) },
197 Event::DoubleClicked { pos } => Event::DoubleClicked { pos: to_world(pos) },
198 });
199 let keys = input.keys;
200 Interaction {
201 event,
202 press: press.map(|press| Press {
203 origin: to_world(press.origin),
204 }),
205 text: None,
206 escape_pressed: keys.escape,
207 delete_pressed: keys.delete,
208 shift: keys.shift,
209 }
210 }
211
212 fn released(&mut self, press: Pressed, pos: Pos2, now: Duration) -> Option<Event> {
215 if press.could_click(now) {
216 let double = self.last_click.is_some_and(|last| {
217 now.saturating_sub(last.at) < MAX_DOUBLE_CLICK_DELAY
218 && last.pos.distance_sq(pos) < MAX_CLICK_DIST * MAX_CLICK_DIST
219 });
220 self.last_click = Some(Click { at: now, pos });
221 return Some(if double {
222 Event::DoubleClicked { pos }
223 } else {
224 Event::Clicked { pos }
225 });
226 }
227 (press.phase == Phase::Dragging).then_some(Event::DragStopped { pos })
228 }
229
230 fn dragged(&mut self, before: Option<Pos2>, now: Duration) -> Option<Event> {
234 let press = self.press.as_mut()?;
235 match press.phase {
236 Phase::Dragging => {
237 let pos = self.acted?;
238 let delta = match (before, self.latest) {
239 (Some(was), Some(is)) => is - was,
240 _ => Vec2::ZERO,
241 };
242 Some(Event::Dragging { pos, delta })
243 }
244 Phase::Cancelled => None,
245 Phase::Held => {
246 if press.could_click(now) {
247 return None;
248 }
249 press.phase = Phase::Dragging;
250 Some(Event::DragStarted { pos: press.origin })
251 }
252 }
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259 use blockworx_geom::pos2;
260 use blockworx_paint::Keys;
261
262 fn at(millis: u64) -> Duration {
263 Duration::from_millis(millis)
264 }
265
266 fn input(raw: Vec<Raw>) -> Input {
267 Input {
268 raw,
269 keys: Keys::default(),
270 }
271 }
272
273 fn down(pos: Pos2) -> Raw {
274 Raw::Down {
275 pos,
276 button: Button::Primary,
277 }
278 }
279
280 fn up(pos: Pos2) -> Raw {
281 Raw::Up {
282 pos,
283 button: Button::Primary,
284 }
285 }
286
287 fn frame(resolver: &mut Resolver, millis: u64, raw: Vec<Raw>) -> Interaction {
290 resolver.resolve(&input(raw), at(millis), Vantage::resting(), Pos2::ZERO)
291 }
292
293 #[test]
294 fn a_release_inside_the_click_distance_is_a_click() {
295 let mut resolver = Resolver::default();
296 let pressed = frame(
297 &mut resolver,
298 0,
299 vec![Raw::Moved(pos2(10.0, 10.0)), down(pos2(10.0, 10.0))],
300 );
301 assert_eq!(pressed.event, Some(Event::HoverAt(pos2(10.0, 10.0))));
302 assert_eq!(
303 pressed.press,
304 Some(Press {
305 origin: pos2(10.0, 10.0)
306 }),
307 "a held press is reported while it is held",
308 );
309 let released = frame(
310 &mut resolver,
311 100,
312 vec![Raw::Moved(pos2(13.0, 12.0)), up(pos2(13.0, 12.0))],
313 );
314 assert_eq!(
315 released.event,
316 Some(Event::Clicked {
317 pos: pos2(13.0, 12.0)
318 })
319 );
320 assert!(released.press.is_none());
321 }
322
323 #[test]
324 fn a_press_that_travels_past_the_threshold_drags_from_where_it_began() {
325 let mut resolver = Resolver::default();
326 let origin = pos2(10.0, 10.0);
327 frame(&mut resolver, 0, vec![Raw::Moved(origin), down(origin)]);
328 let started = frame(&mut resolver, 16, vec![Raw::Moved(pos2(20.0, 10.0))]);
329 assert_eq!(
330 started.event,
331 Some(Event::DragStarted { pos: origin }),
332 "the drag starts where the button went down, not where it was noticed",
333 );
334 let dragging = frame(&mut resolver, 32, vec![Raw::Moved(pos2(25.0, 12.0))]);
335 assert_eq!(
336 dragging.event,
337 Some(Event::Dragging {
338 pos: pos2(25.0, 12.0),
339 delta: Vec2::new(5.0, 2.0)
340 })
341 );
342 let stopped = frame(&mut resolver, 48, vec![up(pos2(25.0, 12.0))]);
343 assert_eq!(
344 stopped.event,
345 Some(Event::DragStopped {
346 pos: pos2(25.0, 12.0)
347 })
348 );
349 }
350
351 #[test]
352 fn a_press_held_past_the_click_duration_becomes_a_drag_without_moving() {
353 let mut resolver = Resolver::default();
354 let origin = pos2(10.0, 10.0);
355 frame(&mut resolver, 0, vec![Raw::Moved(origin), down(origin)]);
356 assert_eq!(
357 frame(&mut resolver, 700, vec![]).event,
358 Some(Event::HoverAt(origin)),
359 "still a possible click",
360 );
361 assert_eq!(
362 frame(&mut resolver, 900, vec![]).event,
363 Some(Event::DragStarted { pos: origin })
364 );
365 assert_eq!(
366 frame(&mut resolver, 1000, vec![up(origin)]).event,
367 Some(Event::DragStopped { pos: origin }),
368 "a long press let go is not a click",
369 );
370 }
371
372 #[test]
373 fn two_clicks_within_the_window_at_one_spot_are_a_double_click() {
374 let mut resolver = Resolver::default();
375 let spot = pos2(10.0, 10.0);
376 frame(
377 &mut resolver,
378 0,
379 vec![Raw::Moved(spot), down(spot), up(spot)],
380 );
381 let second = frame(&mut resolver, 200, vec![down(spot), up(spot)]);
382 assert_eq!(second.event, Some(Event::DoubleClicked { pos: spot }));
383
384 let mut resolver = Resolver::default();
385 frame(
386 &mut resolver,
387 0,
388 vec![Raw::Moved(spot), down(spot), up(spot)],
389 );
390 let late = frame(&mut resolver, 400, vec![down(spot), up(spot)]);
391 assert_eq!(
392 late.event,
393 Some(Event::Clicked { pos: spot }),
394 "past the window"
395 );
396
397 let mut resolver = Resolver::default();
398 frame(
399 &mut resolver,
400 0,
401 vec![Raw::Moved(spot), down(spot), up(spot)],
402 );
403 let far = pos2(30.0, 10.0);
404 let elsewhere = frame(&mut resolver, 200, vec![down(far), up(far)]);
405 assert_eq!(
406 elsewhere.event,
407 Some(Event::Clicked { pos: far }),
408 "too far apart"
409 );
410 }
411
412 #[test]
417 fn a_press_ends_when_the_pointer_goes() {
418 let mut resolver = Resolver::default();
419 resolver.resolve(
420 &input(vec![down(pos2(10.0, 10.0))]),
421 at(0),
422 Vantage::resting(),
423 Pos2::ZERO,
424 );
425 let gone = resolver.resolve(
426 &input(vec![Raw::Gone]),
427 at(16),
428 Vantage::resting(),
429 Pos2::ZERO,
430 );
431 assert_eq!(gone.event, None);
432 assert_eq!(gone.press, None, "the press outlived the pointer");
433 let later = resolver.resolve(&input(vec![]), at(1000), Vantage::resting(), Pos2::ZERO);
434 assert_eq!(later.event, None, "a press that had gone started a drag");
435 assert_eq!(later.press, None);
436
437 let mut resolver = Resolver::default();
438 resolver.resolve(
439 &input(vec![down(pos2(10.0, 10.0))]),
440 at(0),
441 Vantage::resting(),
442 Pos2::ZERO,
443 );
444 let dragging = resolver.resolve(
445 &input(vec![Raw::Moved(pos2(40.0, 10.0))]),
446 at(16),
447 Vantage::resting(),
448 Pos2::ZERO,
449 );
450 assert!(matches!(dragging.event, Some(Event::DragStarted { .. })));
451 let gone = resolver.resolve(
452 &input(vec![Raw::Gone]),
453 at(32),
454 Vantage::resting(),
455 Pos2::ZERO,
456 );
457 assert_eq!(
458 gone.event,
459 Some(Event::DragStopped {
460 pos: pos2(40.0, 10.0)
461 }),
462 "a drag the pointer left did not stop",
463 );
464 assert_eq!(gone.press, None);
465 }
466
467 #[test]
468 fn a_hover_ends_when_the_pointer_leaves() {
469 let mut resolver = Resolver::default();
470 let hovering = frame(&mut resolver, 0, vec![Raw::Moved(pos2(10.0, 10.0))]);
471 assert_eq!(hovering.event, Some(Event::HoverAt(pos2(10.0, 10.0))));
472 let still = frame(&mut resolver, 16, vec![]);
473 assert_eq!(
474 still.event,
475 Some(Event::HoverAt(pos2(10.0, 10.0))),
476 "a still pointer still hovers"
477 );
478 let gone = frame(&mut resolver, 32, vec![Raw::Gone]);
479 assert_eq!(gone.event, None);
480 assert_eq!(resolver.latest(), None);
481 }
482
483 #[test]
484 fn a_panning_frame_keeps_the_drag_and_the_press_from_the_tools() {
485 let mut resolver = Resolver::default();
486 let origin = pos2(10.0, 10.0);
487 frame(&mut resolver, 0, vec![Raw::Moved(origin), down(origin)]);
488 let panned = frame(
489 &mut resolver,
490 16,
491 vec![Raw::Moved(pos2(40.0, 10.0)), Raw::Panning],
492 );
493 assert_eq!(panned.event, None);
494 assert_eq!(panned.press, None);
495 let still_panning = frame(&mut resolver, 32, vec![up(pos2(40.0, 10.0)), Raw::Panning]);
496 assert_eq!(still_panning.event, None, "the drag's end is the pan's too");
497 }
498
499 #[test]
500 fn positions_come_out_in_world_space() {
501 let mut resolver = Resolver::default();
502 let vantage = Vantage {
503 zoom: blockworx_paint::Zoom::new(2.0),
504 translation: Vec2::new(100.0, 50.0),
505 };
506 let origin = pos2(10.0, 10.0);
507 let hovered = resolver.resolve(
508 &input(vec![Raw::Moved(pos2(310.0, 260.0))]),
509 at(0),
510 vantage,
511 origin,
512 );
513 assert_eq!(hovered.event, Some(Event::HoverAt(pos2(100.0, 100.0))));
514 resolver.resolve(
515 &input(vec![down(pos2(310.0, 260.0))]),
516 at(16),
517 vantage,
518 origin,
519 );
520 resolver.resolve(
521 &input(vec![Raw::Moved(pos2(330.0, 260.0))]),
522 at(32),
523 vantage,
524 origin,
525 );
526 let dragging = resolver.resolve(
527 &input(vec![Raw::Moved(pos2(340.0, 270.0))]),
528 at(48),
529 vantage,
530 origin,
531 );
532 assert_eq!(
533 dragging.event,
534 Some(Event::Dragging {
535 pos: pos2(115.0, 105.0),
536 delta: Vec2::new(5.0, 5.0)
537 }),
538 "a screen delta is halved at twice the zoom",
539 );
540 }
541}