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",
|
"Double-click the canvas to add a note",
|
||||||
"Drag a dot on a node's edge to connect it",
|
"Drag a dot on a node's edge to connect it",
|
||||||
"Middle- or right-drag to pan, wheel to zoom",
|
"Middle- or right-drag to pan, wheel to zoom",
|
||||||
|
"Swipe to pan, Ctrl+swipe to zoom",
|
||||||
"Hold Shift to add to the selection",
|
"Hold Shift to add to the selection",
|
||||||
] {
|
] {
|
||||||
ui.label(
|
ui.label(
|
||||||
|
|||||||
+52
-8
@@ -299,6 +299,9 @@ pub struct CanvasView {
|
|||||||
viewport: Option<Rect>,
|
viewport: Option<Rect>,
|
||||||
/// Framing asked for before the viewport size was known.
|
/// Framing asked for before the viewport size was known.
|
||||||
pending_fit: Option<PendingFit>,
|
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.
|
/// The text of the file behind the file node being edited.
|
||||||
@@ -352,6 +355,7 @@ impl CanvasView {
|
|||||||
menu_at: None,
|
menu_at: None,
|
||||||
viewport: None,
|
viewport: None,
|
||||||
pending_fit: None,
|
pending_fit: None,
|
||||||
|
scroll_is_touchpad: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -775,7 +779,8 @@ impl CanvasView {
|
|||||||
|
|
||||||
// -- input ------------------------------------------------------------
|
// -- 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) {
|
fn handle_pan_and_zoom(&mut self, ui: &egui::Ui, background: &egui::Response) {
|
||||||
if background.dragged_by(PointerButton::Middle)
|
if background.dragged_by(PointerButton::Middle)
|
||||||
|| background.dragged_by(PointerButton::Secondary)
|
|| background.dragged_by(PointerButton::Secondary)
|
||||||
@@ -784,19 +789,32 @@ impl CanvasView {
|
|||||||
ui.ctx().set_cursor_icon(CursorIcon::Grabbing);
|
ui.ctx().set_cursor_icon(CursorIcon::Grabbing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.note_scroll_device(ui);
|
||||||
|
|
||||||
if !background.contains_pointer() {
|
if !background.contains_pointer() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(pointer) = ui.input(|i| i.pointer.latest_pos()) else {
|
let Some(pointer) = ui.input(|i| i.pointer.latest_pos()) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
// A bare wheel arrives as scroll and is turned into a zoom by the
|
// Held down, the zoom modifier turns scrolling into zooming before it
|
||||||
// same rule egui uses for the modifier it expects, so that the two
|
// ever reaches here: egui reports it as a zoom and leaves no scroll
|
||||||
// zoom at one speed. Nothing else on the canvas wants the wheel: it is
|
// behind. A pinch on a touchscreen arrives the same way.
|
||||||
// panned by dragging, and node contents are cropped rather than
|
let (scroll, mut zoom_delta) = ui.input(|i| (i.smooth_scroll_delta, i.zoom_delta()));
|
||||||
// scrolled.
|
if self.scroll_is_touchpad {
|
||||||
let speed = ui.ctx().options(|o| o.input_options.scroll_zoom_speed);
|
// Two fingers sweep the canvas about, in whatever direction they
|
||||||
let zoom_delta = ui.input(|i| i.zoom_delta() * (speed * i.smooth_scroll_delta.y).exp());
|
// 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 {
|
if zoom_delta != 1.0 {
|
||||||
let factor = (self.to_global.scaling * zoom_delta).clamp(MIN_ZOOM, MAX_ZOOM)
|
let factor = (self.to_global.scaling * zoom_delta).clamp(MIN_ZOOM, MAX_ZOOM)
|
||||||
/ self.to_global.scaling;
|
/ 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 ---------------------------------------------------------
|
// -- painting ---------------------------------------------------------
|
||||||
|
|
||||||
fn paint_grid(&self, ui: &egui::Ui, visible: Rect, palette: &Palette) {
|
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,
|
&mut doc,
|
||||||
screen,
|
screen,
|
||||||
vec![egui::Event::MouseWheel {
|
vec![egui::Event::MouseWheel {
|
||||||
unit: egui::MouseWheelUnit::Point,
|
unit: egui::MouseWheelUnit::Line,
|
||||||
delta: vec2(0.0, 50.0),
|
delta: vec2(0.0, 1.0),
|
||||||
modifiers: egui::Modifiers::default(),
|
modifiers: egui::Modifiers::default(),
|
||||||
phase: egui::TouchPhase::Move,
|
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");
|
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]
|
#[test]
|
||||||
fn a_click_in_the_inspector_stays_in_the_inspector() {
|
fn a_click_in_the_inspector_stays_in_the_inspector() {
|
||||||
let ctx = egui::Context::default();
|
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