use super::{Scene, DBL_CLICK_MS, MAX_CLICK_DIST, ZOOM_SENSITIVITY};
use crate::event::{Event, EventResult, MouseButton};
use crate::geometry::Point;
use web_time::Instant;
impl Scene {
pub(super) fn handle_event(&mut self, event: &Event) -> EventResult {
match event {
Event::MouseMove { pos } => self.on_mouse_move(*pos),
Event::MouseDown { pos, button, .. } => self.on_mouse_down(*pos, *button),
Event::MouseUp { pos, .. } => self.on_mouse_up(*pos),
Event::MouseWheel { pos, delta_y, .. } => self.on_wheel(*pos, *delta_y),
_ => EventResult::Ignored,
}
}
fn on_mouse_move(&mut self, pos: Point) -> EventResult {
if self.panning {
if !self.pan_moved {
let dx = pos.x - self.pan_press.x;
let dy = pos.y - self.pan_press.y;
if dx * dx + dy * dy > MAX_CLICK_DIST * MAX_CLICK_DIST {
self.pan_moved = true;
}
}
let delta = Point::new(pos.x - self.pan_last.x, pos.y - self.pan_last.y);
self.transform.pan(delta);
self.pan_last = pos;
self.user_interacted = true;
self.publish_scene_rect();
crate::animation::request_draw();
return EventResult::Consumed;
}
EventResult::Ignored
}
pub(super) fn on_mouse_down(&mut self, pos: Point, button: MouseButton) -> EventResult {
match button {
MouseButton::Left | MouseButton::Middle => {
self.begin_pan(pos, button == MouseButton::Left);
EventResult::Consumed
}
_ => EventResult::Ignored,
}
}
fn on_mouse_up(&mut self, _pos: Point) -> EventResult {
if self.panning {
self.panning = false;
if self.pan_is_left && !self.pan_moved {
let now = Instant::now();
let in_time = self
.last_bg_click
.map(|t| now.duration_since(t).as_millis() < DBL_CLICK_MS)
.unwrap_or(false);
let consecutive = self.last_bg_click_epoch
== Some(self.pan_press_epoch.wrapping_sub(1));
if in_time && consecutive {
self.last_bg_click = None;
self.last_bg_click_epoch = None;
self.reset_view();
} else {
self.last_bg_click = Some(now);
self.last_bg_click_epoch = Some(self.pan_press_epoch);
}
} else {
self.last_bg_click = None;
self.last_bg_click_epoch = None;
}
return EventResult::Consumed;
}
EventResult::Ignored
}
fn on_wheel(&mut self, pos: Point, delta_y: f64) -> EventResult {
let factor = (delta_y * ZOOM_SENSITIVITY).exp();
let new_zoom = self.transform.zoom * factor;
self.transform.zoom_at(pos, new_zoom, self.zoom_range);
self.user_interacted = true;
self.publish_scene_rect();
crate::animation::request_draw();
EventResult::Consumed
}
fn begin_pan(&mut self, pos: Point, is_left: bool) {
self.panning = true;
self.pan_last = pos;
self.pan_press = pos;
self.pan_moved = false;
self.pan_is_left = is_left;
self.pan_press_epoch = crate::animation::pointer_press_epoch();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::draw_ctx::DrawCtx;
use crate::event::Modifiers;
use crate::geometry::{Rect, Size};
use crate::widget::Widget;
struct InertContent {
bounds: Rect,
children: Vec<Box<dyn Widget>>,
}
impl Widget for InertContent {
fn type_name(&self) -> &'static str {
"InertContent"
}
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, b: Rect) {
self.bounds = b;
}
fn children(&self) -> &[Box<dyn Widget>] {
&self.children
}
fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
&mut self.children
}
fn layout(&mut self, _available: Size) -> Size {
self.bounds = Rect::new(0.0, 0.0, 100.0, 100.0);
Size::new(100.0, 100.0)
}
fn paint(&mut self, _ctx: &mut dyn DrawCtx) {}
fn on_event(&mut self, _event: &Event) -> EventResult {
EventResult::Ignored
}
}
fn scene() -> Scene {
let mut s = Scene::new(Box::new(InertContent {
bounds: Rect::default(),
children: Vec::new(),
}));
s.layout(Size::new(400.0, 400.0));
s
}
fn down(s: &mut Scene, x: f64, y: f64) {
crate::animation::bump_pointer_press_epoch();
s.on_event(&Event::MouseDown {
pos: Point::new(x, y),
button: MouseButton::Left,
modifiers: Modifiers::default(),
});
}
fn mv(s: &mut Scene, x: f64, y: f64) {
s.on_event(&Event::MouseMove {
pos: Point::new(x, y),
});
}
fn up(s: &mut Scene, x: f64, y: f64) {
s.on_event(&Event::MouseUp {
pos: Point::new(x, y),
button: MouseButton::Left,
modifiers: Modifiers::default(),
});
}
#[test]
fn pan_then_press_does_not_reset_view() {
let mut s = scene();
let fitted = s.transform();
down(&mut s, 200.0, 200.0);
mv(&mut s, 250.0, 260.0);
up(&mut s, 250.0, 260.0);
let panned = s.transform();
assert_ne!(panned, fitted, "drag should have panned the view");
down(&mut s, 250.0, 260.0);
up(&mut s, 250.0, 260.0);
assert_eq!(
s.transform(),
panned,
"press after a drag must not reset the view"
);
}
#[test]
fn genuine_double_click_resets_view() {
let mut s = scene();
let fitted = s.transform();
down(&mut s, 200.0, 200.0);
mv(&mut s, 260.0, 240.0);
up(&mut s, 260.0, 240.0);
assert_ne!(s.transform(), fitted);
down(&mut s, 300.0, 300.0);
up(&mut s, 300.0, 300.0);
down(&mut s, 300.0, 300.0);
up(&mut s, 300.0, 300.0);
assert_eq!(
s.transform(),
fitted,
"double-click on the background must reset to the fitted view"
);
}
#[test]
fn small_jitter_still_counts_as_click() {
let mut s = scene();
let fitted = s.transform();
down(&mut s, 200.0, 200.0);
mv(&mut s, 150.0, 150.0);
up(&mut s, 150.0, 150.0);
let panned = s.transform();
assert_ne!(panned, fitted);
down(&mut s, 300.0, 300.0);
mv(&mut s, 302.0, 301.0);
up(&mut s, 302.0, 301.0);
down(&mut s, 302.0, 301.0);
mv(&mut s, 300.0, 300.0);
up(&mut s, 300.0, 300.0);
assert_eq!(s.transform(), fitted, "jittery double-click should reset");
}
}