mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Canvas: the pointer's document point follows a wheel scroll or view drag in the same frame
handle_input set hover_doc, the document point under the pointer, from the view as handle_input found it. The wheel's zoom, scroll and trackpad turn, a held pan, a Rotate View drag, the Zoom tool and a Presentation Mode click moved the view later in the same call, and show drew with the moved view. In a frame where a wheel scroll moved the view, the rulers' pointer marks were drawn off the pointer by that frame's scroll, the Info panel showed the earlier point, and the hover highlight and the tool cursor were looked up there. A pan, a Rotate View drag, a Zoom tool marquee and an Animated Zoom scrub left hover_doc behind the view in the same way. A tool drag held still while the wheel scrolled was sent the old view's point. The wheel block now runs before handle_input takes its view, so the tool's events in that frame use the view the wheel moved to. show sets hover_doc right after handle_input returns, from the view it left.
This commit is contained in:
+125
-25
@@ -265,6 +265,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
egui::Popup::close_all(ui.ctx());
|
||||
} else {
|
||||
handle_input(app, ui, &resp, rect);
|
||||
// `hover_doc` is the document point under the pointer in the view `handle_input` left. The
|
||||
// wheel, a pan, a Rotate View drag, the Zoom tool and a Presentation Mode click move the view
|
||||
// there.
|
||||
let moved = Xf::new(rect, app.view().unwrap_or(&View::default()));
|
||||
app.hover_doc = ui.input(|i| i.pointer.hover_pos()).filter(|p| rect.contains(*p)).map(|p| moved.to_doc(p));
|
||||
}
|
||||
let v = *app.view().unwrap_or(&View::default());
|
||||
let xf = Xf::new(rect, &v);
|
||||
@@ -762,33 +767,10 @@ fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect:
|
||||
if turn.glide != 0.0 {
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
let v = *app.view().unwrap_or(&View::default());
|
||||
let xf = Xf::new(rect, &v);
|
||||
let hover = pointer.hover_pos().filter(|p| rect.contains(*p));
|
||||
// A lifted pen or finger takes the pointer away in the frame it lifts (egui's `PointerGone`),
|
||||
// so a tap whose press and lift come in one frame has no hover position: it presses (and
|
||||
// double-taps) where it was seen last (#491).
|
||||
let at = hover.or_else(|| pointer.interact_pos().filter(|p| rect.contains(*p)));
|
||||
app.hover_doc = hover.map(|p| xf.to_doc(p));
|
||||
let view = app.view_info();
|
||||
touch_gestures(app, ui, rect, view);
|
||||
// Pressed and held still, a pen or a finger opens the context menu (egui's long touch): the
|
||||
// tool's press is dropped, as a right-click presses nothing.
|
||||
if resp.long_touched() {
|
||||
drop_press(app, ui, view);
|
||||
}
|
||||
let drag: Option<Drag> = ui.data(|d| d.get_temp(drag_id()));
|
||||
// A modifier pressed or released over the canvas re-hovers the tool, so what it changes shows
|
||||
// without moving the mouse (Alt switches the Shape Builder to erase mode).
|
||||
let mods_changed = ui.data_mut(|d| {
|
||||
let id = egui::Id::new("canvas-mods");
|
||||
let prev = d.get_temp::<egui::Modifiers>(id);
|
||||
d.insert_temp(id, m);
|
||||
prev.is_some_and(|prev| prev != m)
|
||||
});
|
||||
|
||||
// Zoom around the pointer, and scroll ([`wheel`]): fingers pinching on a touch screen may also
|
||||
// slide. Then turn the view about the pointer for a trackpad rotation ([`trackpad_turn`]).
|
||||
// slide. Then turn the view about the pointer for a trackpad rotation ([`trackpad_turn`]). The
|
||||
// tool's events below take their document points from the view this moves to.
|
||||
if resp.hovered() {
|
||||
if (factor - 1.0).abs() > 1e-6
|
||||
&& let (Some(p), Some(vm)) = (hover, app.view_mut())
|
||||
@@ -808,6 +790,28 @@ fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect:
|
||||
rotate_about(vm, rect, p, deg);
|
||||
}
|
||||
}
|
||||
let v = *app.view().unwrap_or(&View::default());
|
||||
let xf = Xf::new(rect, &v);
|
||||
// A lifted pen or finger takes the pointer away in the frame it lifts (egui's `PointerGone`),
|
||||
// so a tap whose press and lift come in one frame has no hover position: it presses (and
|
||||
// double-taps) where it was seen last (#491).
|
||||
let at = hover.or_else(|| pointer.interact_pos().filter(|p| rect.contains(*p)));
|
||||
let view = app.view_info();
|
||||
touch_gestures(app, ui, rect, view);
|
||||
// Pressed and held still, a pen or a finger opens the context menu (egui's long touch): the
|
||||
// tool's press is dropped, as a right-click presses nothing.
|
||||
if resp.long_touched() {
|
||||
drop_press(app, ui, view);
|
||||
}
|
||||
let drag: Option<Drag> = ui.data(|d| d.get_temp(drag_id()));
|
||||
// A modifier pressed or released over the canvas re-hovers the tool, so what it changes shows
|
||||
// without moving the mouse (Alt switches the Shape Builder to erase mode).
|
||||
let mods_changed = ui.data_mut(|d| {
|
||||
let id = egui::Id::new("canvas-mods");
|
||||
let prev = d.get_temp::<egui::Modifiers>(id);
|
||||
d.insert_temp(id, m);
|
||||
prev.is_some_and(|prev| prev != m)
|
||||
});
|
||||
|
||||
// In Presentation Mode a click anywhere goes to the next artboard. The tool receives no press,
|
||||
// drag or hover, and a drag does not pan the view. A drag that began before the mode was turned
|
||||
@@ -6313,6 +6317,102 @@ mod tests {
|
||||
assert_eq!(app.view().unwrap().zoom / before, done, "and it stops");
|
||||
}
|
||||
|
||||
/// In each frame where a wheel scroll moves the view, the hover point (read by the rulers'
|
||||
/// pointer marks, the Info panel, the hover highlight and the tool cursor) is the document point
|
||||
/// under the pointer in the view the canvas draws in that frame.
|
||||
#[test]
|
||||
fn the_hover_point_follows_a_wheel_scroll_in_the_frame_it_moves() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let rect = app.canvas_rect.unwrap();
|
||||
let at = rect.center() + vec2(60.0, 40.0);
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(at)]);
|
||||
let under = |app: &VectorcraftApp| Xf::new(rect, app.view().unwrap()).to_doc(at);
|
||||
let wheel = egui::Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: vec2(0.0, -60.0),
|
||||
phase: egui::TouchPhase::Move,
|
||||
modifiers: Default::default(),
|
||||
};
|
||||
let mut events = vec![wheel];
|
||||
let mut moved = 0;
|
||||
for k in 0..30 {
|
||||
let before = app.view().unwrap().center;
|
||||
frame(&mut app, &ctx, std::mem::take(&mut events));
|
||||
moved += usize::from(app.view().unwrap().center != before);
|
||||
let hover = app.hover_doc.unwrap();
|
||||
assert!(hover.distance(under(&app)) < 1e-9, "frame {k}: {hover:?}, under the pointer {:?}", under(&app));
|
||||
}
|
||||
assert!(moved > 0, "the wheel scrolls the view");
|
||||
}
|
||||
|
||||
/// While a middle-button pan or a Rotate View drag moves the view, the hover point is the
|
||||
/// document point under the pointer in the view the canvas draws in that frame.
|
||||
#[test]
|
||||
fn the_hover_point_follows_a_view_drag_in_the_frame_it_moves() {
|
||||
for (tool, button) in [("rectangle", egui::PointerButton::Middle), ("rotateView", egui::PointerButton::Primary)] {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
app.select_tool(tool);
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let rect = app.canvas_rect.unwrap();
|
||||
let press = |pos, pressed| egui::Event::PointerButton { pos, button, pressed, modifiers: Default::default() };
|
||||
let a = rect.center() + vec2(100.0, 20.0);
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(a)]);
|
||||
frame(&mut app, &ctx, vec![press(a, true)]);
|
||||
let mut p = a;
|
||||
for k in 1..=3 {
|
||||
p = a + vec2(-20.0, 30.0) * k as f32;
|
||||
let before = *app.view().unwrap();
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(p)]);
|
||||
let after = *app.view().unwrap();
|
||||
assert!(after.center != before.center || after.rotation != before.rotation, "{tool}: the drag moves the view");
|
||||
let under = Xf::new(rect, &after).to_doc(p);
|
||||
let hover = app.hover_doc.unwrap();
|
||||
assert!(hover.distance(under) < 1e-9, "{tool}, step {k}: {hover:?}, under the pointer {under:?}");
|
||||
}
|
||||
frame(&mut app, &ctx, vec![press(p, false)]);
|
||||
}
|
||||
}
|
||||
|
||||
/// A tool drag goes on at the document point under the pointer in the view a wheel scroll moved
|
||||
/// to, in the frame the wheel scrolls.
|
||||
#[test]
|
||||
fn a_tool_drag_follows_a_wheel_scroll_in_the_frame_it_moves() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
// With Zoom with Mouse Wheel on, Shift-wheel scrolls up and down in the frame it comes in.
|
||||
app.session.execute("prefs.set", &json!({"key": "zoomWithMouseWheel", "value": true})).unwrap();
|
||||
app.ui.view.smart_guides = false;
|
||||
app.select_tool("rectangle");
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let rect = app.canvas_rect.unwrap();
|
||||
let (a, b) = (rect.center() - vec2(80.0, 60.0), rect.center() + vec2(40.0, 30.0));
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(a)]);
|
||||
frame(&mut app, &ctx, vec![primary(a, true)]);
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(b)]);
|
||||
let before = *app.view().unwrap();
|
||||
let wheel = egui::Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: vec2(0.0, -40.0),
|
||||
phase: egui::TouchPhase::Move,
|
||||
modifiers: egui::Modifiers::SHIFT,
|
||||
};
|
||||
frame(&mut app, &ctx, vec![wheel]);
|
||||
let after = *app.view().unwrap();
|
||||
assert!(after.center != before.center, "Shift-wheel scrolls the view");
|
||||
let want = Xf::new(rect, &after).to_doc(b);
|
||||
let bounds = |app: &VectorcraftApp| app.session.active().unwrap().doc.layers[0].children().unwrap()[0].geometric_bounds().unwrap();
|
||||
let r = bounds(&app);
|
||||
assert!((r.x1 - want.x).abs() < 1e-6 && ((r.y0 - want.y).abs() < 1e-6 || (r.y1 - want.y).abs() < 1e-6), "{r:?} reaches {want:?}");
|
||||
frame(&mut app, &ctx, vec![primary(b, false)]);
|
||||
assert_eq!(bounds(&app), r, "the rectangle drawn");
|
||||
}
|
||||
|
||||
/// Two fingers dragged together on a touch screen pan the canvas with them, under either
|
||||
/// Zoom with Mouse Wheel setting, and draw nothing (#449).
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user