Improve touchpad scrolling
This commit is contained in:
@@ -642,6 +642,7 @@ fn canvas_section(ui: &mut Ui, doc: &mut Document, palette: &Palette, out: &mut
|
||||
"Double-click the canvas to add a note",
|
||||
"Drag a dot on a node's edge to connect it",
|
||||
"Middle- or right-drag to pan, wheel to zoom",
|
||||
"Swipe to pan, Ctrl+swipe to zoom",
|
||||
"Hold Shift to add to the selection",
|
||||
] {
|
||||
ui.label(
|
||||
|
||||
+52
-8
@@ -299,6 +299,9 @@ pub struct CanvasView {
|
||||
viewport: Option<Rect>,
|
||||
/// Framing asked for before the viewport size was known.
|
||||
pending_fit: Option<PendingFit>,
|
||||
/// Whether the scrolling now under way came from a touchpad rather than a
|
||||
/// mouse wheel. See [`CanvasView::note_scroll_device`].
|
||||
scroll_is_touchpad: bool,
|
||||
}
|
||||
|
||||
/// The text of the file behind the file node being edited.
|
||||
@@ -352,6 +355,7 @@ impl CanvasView {
|
||||
menu_at: None,
|
||||
viewport: None,
|
||||
pending_fit: None,
|
||||
scroll_is_touchpad: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,7 +779,8 @@ impl CanvasView {
|
||||
|
||||
// -- input ------------------------------------------------------------
|
||||
|
||||
/// Panning is a drag; the wheel zooms, bare or with a modifier held.
|
||||
/// Panning is a drag or a two-finger swipe; the wheel zooms, and so does
|
||||
/// a swipe with the zoom modifier held.
|
||||
fn handle_pan_and_zoom(&mut self, ui: &egui::Ui, background: &egui::Response) {
|
||||
if background.dragged_by(PointerButton::Middle)
|
||||
|| background.dragged_by(PointerButton::Secondary)
|
||||
@@ -784,19 +789,32 @@ impl CanvasView {
|
||||
ui.ctx().set_cursor_icon(CursorIcon::Grabbing);
|
||||
}
|
||||
|
||||
self.note_scroll_device(ui);
|
||||
|
||||
if !background.contains_pointer() {
|
||||
return;
|
||||
}
|
||||
let Some(pointer) = ui.input(|i| i.pointer.latest_pos()) else {
|
||||
return;
|
||||
};
|
||||
// A bare wheel arrives as scroll and is turned into a zoom by the
|
||||
// same rule egui uses for the modifier it expects, so that the two
|
||||
// zoom at one speed. Nothing else on the canvas wants the wheel: it is
|
||||
// panned by dragging, and node contents are cropped rather than
|
||||
// scrolled.
|
||||
let speed = ui.ctx().options(|o| o.input_options.scroll_zoom_speed);
|
||||
let zoom_delta = ui.input(|i| i.zoom_delta() * (speed * i.smooth_scroll_delta.y).exp());
|
||||
// Held down, the zoom modifier turns scrolling into zooming before it
|
||||
// ever reaches here: egui reports it as a zoom and leaves no scroll
|
||||
// behind. A pinch on a touchscreen arrives the same way.
|
||||
let (scroll, mut zoom_delta) = ui.input(|i| (i.smooth_scroll_delta, i.zoom_delta()));
|
||||
if self.scroll_is_touchpad {
|
||||
// Two fingers sweep the canvas about, in whatever direction they
|
||||
// are moving: the delta carries both axes, and points the way the
|
||||
// contents of a scroll area would go.
|
||||
self.to_global.translation += scroll;
|
||||
} else {
|
||||
// A bare wheel arrives as scroll and is turned into a zoom by the
|
||||
// same rule egui uses for the modifier it expects, so that the two
|
||||
// zoom at one speed. Nothing else on the canvas wants the wheel: it
|
||||
// is panned by dragging, and node contents are cropped rather than
|
||||
// scrolled.
|
||||
let speed = ui.ctx().options(|o| o.input_options.scroll_zoom_speed);
|
||||
zoom_delta *= (speed * scroll.y).exp();
|
||||
}
|
||||
if zoom_delta != 1.0 {
|
||||
let factor = (self.to_global.scaling * zoom_delta).clamp(MIN_ZOOM, MAX_ZOOM)
|
||||
/ self.to_global.scaling;
|
||||
@@ -806,6 +824,32 @@ impl CanvasView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Notes whether the scrolling now under way is a touchpad swipe or a
|
||||
/// mouse wheel, since the one pans and the other zooms.
|
||||
///
|
||||
/// The unit the event carries tells them apart: a touchpad reports points,
|
||||
/// a wheel reports lines. X11 is the exception, reporting lines for both,
|
||||
/// so a fraction of a line is taken for a touchpad as well — there a
|
||||
/// wheel notch is always a whole one.
|
||||
///
|
||||
/// The answer is kept because egui spreads a long scroll over the frames
|
||||
/// after it, and those frames carry no event to read it from.
|
||||
fn note_scroll_device(&mut self, ui: &egui::Ui) {
|
||||
ui.input(|i| {
|
||||
for event in &i.events {
|
||||
if let egui::Event::MouseWheel { unit, delta, .. } = event {
|
||||
self.scroll_is_touchpad = match unit {
|
||||
egui::MouseWheelUnit::Point => true,
|
||||
egui::MouseWheelUnit::Line => {
|
||||
delta.x.fract() != 0.0 || delta.y.fract() != 0.0
|
||||
}
|
||||
egui::MouseWheelUnit::Page => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// -- painting ---------------------------------------------------------
|
||||
|
||||
fn paint_grid(&self, ui: &egui::Ui, visible: Rect, palette: &Palette) {
|
||||
|
||||
+97
-2
@@ -546,8 +546,8 @@ fn the_wheel_zooms_over_a_node() {
|
||||
&mut doc,
|
||||
screen,
|
||||
vec![egui::Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: vec2(0.0, 50.0),
|
||||
unit: egui::MouseWheelUnit::Line,
|
||||
delta: vec2(0.0, 1.0),
|
||||
modifiers: egui::Modifiers::default(),
|
||||
phase: egui::TouchPhase::Move,
|
||||
}],
|
||||
@@ -563,6 +563,101 @@ fn the_wheel_zooms_over_a_node() {
|
||||
assert!(moved < 1.0, "the canvas slid {moved} under the pointer");
|
||||
}
|
||||
|
||||
/// A two-finger swipe on a touchpad slides the canvas about, sideways as
|
||||
/// readily as up and down, rather than zooming the way the wheel does.
|
||||
#[test]
|
||||
fn a_two_finger_swipe_pans() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut doc = Document::new();
|
||||
doc.canvas = canvas_with_two_nodes();
|
||||
let mut view = CanvasView::new();
|
||||
|
||||
let screen = Rect::from_min_size(Pos2::ZERO, vec2(1200.0, 900.0));
|
||||
let at = pos2(600.0, 450.0);
|
||||
pass(
|
||||
&ctx,
|
||||
&mut view,
|
||||
&mut doc,
|
||||
screen,
|
||||
vec![egui::Event::PointerMoved(at)],
|
||||
);
|
||||
|
||||
let before = view.to_global;
|
||||
// Steps small enough that egui hands each one over the frame it arrives,
|
||||
// with none of the smoothing it saves for the coarser wheel.
|
||||
let swipe = vec2(4.0, 3.0);
|
||||
let steps = 5;
|
||||
for _ in 0..steps {
|
||||
pass(
|
||||
&ctx,
|
||||
&mut view,
|
||||
&mut doc,
|
||||
screen,
|
||||
vec![egui::Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: swipe,
|
||||
modifiers: egui::Modifiers::default(),
|
||||
phase: egui::TouchPhase::Move,
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
view.zoom(),
|
||||
before.scaling,
|
||||
"the swipe zoomed instead of panning"
|
||||
);
|
||||
let moved = view.to_global.translation - before.translation;
|
||||
let wanted = swipe * steps as f32;
|
||||
assert!(
|
||||
(moved - wanted).length() < 1.0,
|
||||
"the swipe panned by {moved:?}, wanted {wanted:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// With the zoom modifier down the same swipe zooms instead, at the pointer.
|
||||
#[test]
|
||||
fn ctrl_and_a_two_finger_swipe_zooms() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut doc = Document::new();
|
||||
doc.canvas = canvas_with_two_nodes();
|
||||
let mut view = CanvasView::new();
|
||||
|
||||
let screen = Rect::from_min_size(Pos2::ZERO, vec2(1200.0, 900.0));
|
||||
let at = pos2(50.0, 50.0);
|
||||
let under_pointer = view.to_global.inverse() * at;
|
||||
pass(
|
||||
&ctx,
|
||||
&mut view,
|
||||
&mut doc,
|
||||
screen,
|
||||
vec![egui::Event::PointerMoved(at)],
|
||||
);
|
||||
|
||||
for _ in 0..5 {
|
||||
pass(
|
||||
&ctx,
|
||||
&mut view,
|
||||
&mut doc,
|
||||
screen,
|
||||
vec![egui::Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: vec2(0.0, 4.0),
|
||||
modifiers: egui::Modifiers::COMMAND,
|
||||
phase: egui::TouchPhase::Move,
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
assert!(
|
||||
view.zoom() > 1.0,
|
||||
"the swipe did not zoom in: {}",
|
||||
view.zoom()
|
||||
);
|
||||
let moved = (view.to_global.inverse() * at).distance(under_pointer);
|
||||
assert!(moved < 1.0, "the canvas slid {moved} under the pointer");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_click_in_the_inspector_stays_in_the_inspector() {
|
||||
let ctx = egui::Context::default();
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 84 KiB |
Reference in New Issue
Block a user