mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 13:17:19 -06:00
Merge pull request #4672 from storytold/fix/touch-second-finger
Touch: a second finger cancels a tool gesture the first one started; the pinch pans and zooms (#4198)
This commit is contained in:
@@ -81,9 +81,12 @@ pub(super) fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Respo
|
||||
let middle_pan = matches!(drag, Some(Drag::Pan { middle: true, .. }));
|
||||
// egui counts a press a few pixels outside the canvas as on it (its interaction radius): only a
|
||||
// press on the canvas itself reaches the tools, else it would land at the canvas's centre.
|
||||
// A finger pressing with others down, or with another landing in its frame, presses nothing
|
||||
// (`touch_gestures` made the press a gesture, #4198).
|
||||
if pointer.primary_pressed()
|
||||
&& resp.hovered()
|
||||
&& !middle_pan
|
||||
&& drag != Some(Drag::Gesture)
|
||||
&& let Some(p) = at
|
||||
{
|
||||
ui.ctx().memory_mut(|mem| mem.stop_text_input());
|
||||
|
||||
@@ -4195,6 +4195,83 @@ fn two_fingers_dragged_together_pan_the_canvas() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #4198: egui-winit presses with the first finger as the pointer, in the frame the second lands
|
||||
/// or a frame or more before (the Pencil has begun drawing, the Selection tool moving the art
|
||||
/// under it). The second finger cancels that press, as it does the press of a finger lifted and
|
||||
/// landed again mid-pinch: the pinch zooms, nothing is drawn or moved, nothing enters the undo
|
||||
/// history, and lifting the fingers commits nothing. One finger still draws afterwards.
|
||||
#[test]
|
||||
fn a_second_finger_cancels_the_tool_gesture_the_first_began() {
|
||||
for (tool, together) in [("pencil", false), ("selection", false), ("pencil", true), ("selection", true)] {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
app.session.execute("shape.rectangle", &json!({"x": 50, "y": 50, "width": 100, "height": 100})).unwrap();
|
||||
app.select_tool(tool);
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap());
|
||||
let (doc, undo, zoom) = {
|
||||
let st = app.session.active().unwrap();
|
||||
(st.doc.clone(), st.history.undo.len(), app.view().unwrap().zoom)
|
||||
};
|
||||
let touch = |n: u64, phase, pos| egui::Event::Touch { device_id: egui::TouchDeviceId(1), id: egui::TouchId(n), phase, pos, force: None };
|
||||
let button = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
let press = |n: u64, pos| vec![touch(n, egui::TouchPhase::Start, pos), egui::Event::PointerMoved(pos), button(pos, true)];
|
||||
let lift = |n: u64, pos| vec![touch(n, egui::TouchPhase::End, pos), button(pos, false), egui::Event::PointerGone];
|
||||
let apart = |p: Pos2, q: Pos2| vec![touch(1, egui::TouchPhase::Move, p), egui::Event::PointerMoved(p), touch(2, egui::TouchPhase::Move, q)];
|
||||
// The first finger presses on the rectangle, and drags a little on its own unless the
|
||||
// second lands in the same frame.
|
||||
let mut p = xf.to_screen(Point::new(100.0, 100.0));
|
||||
let mut q = p + vec2(80.0, 0.0);
|
||||
if together {
|
||||
frame(&mut app, &ctx, [press(1, p), vec![touch(2, egui::TouchPhase::Start, q)]].concat());
|
||||
} else {
|
||||
frame(&mut app, &ctx, press(1, p));
|
||||
for _ in 0..2 {
|
||||
p += vec2(12.0, 6.0);
|
||||
frame(&mut app, &ctx, vec![touch(1, egui::TouchPhase::Move, p), egui::Event::PointerMoved(p)]);
|
||||
}
|
||||
assert!(matches!(ctx.data(|d| d.get_temp::<Drag>(drag_id())), Some(Drag::Tool | Drag::Art)), "{tool}: the tool's drag began");
|
||||
q = p + vec2(80.0, 0.0);
|
||||
frame(&mut app, &ctx, vec![touch(2, egui::TouchPhase::Start, q)]);
|
||||
}
|
||||
// The two pinch apart; the first lifts, the second moves alone (egui-winit's pointer
|
||||
// now), and the first lands again and pinches on.
|
||||
for _ in 0..3 {
|
||||
(p, q) = (p - vec2(15.0, 0.0), q + vec2(15.0, 0.0));
|
||||
frame(&mut app, &ctx, apart(p, q));
|
||||
}
|
||||
frame(&mut app, &ctx, lift(1, p));
|
||||
q += vec2(10.0, 0.0);
|
||||
frame(&mut app, &ctx, vec![touch(2, egui::TouchPhase::Move, q), egui::Event::PointerMoved(q)]);
|
||||
frame(&mut app, &ctx, press(1, p));
|
||||
for _ in 0..3 {
|
||||
(p, q) = (p - vec2(10.0, 5.0), q + vec2(10.0, 5.0));
|
||||
frame(&mut app, &ctx, apart(p, q));
|
||||
}
|
||||
frame(&mut app, &ctx, lift(1, p));
|
||||
frame(&mut app, &ctx, lift(2, q));
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let st = app.session.active().unwrap();
|
||||
assert!(st.interaction.is_none(), "{tool}: no tool interaction is left");
|
||||
assert_eq!(*st.doc, *doc, "{tool}: nothing drawn or moved");
|
||||
assert_eq!(st.history.undo.len(), undo, "{tool}: no undo entry");
|
||||
assert!(app.view().unwrap().zoom > zoom * 1.2, "{tool}: the pinch zoomed in");
|
||||
if tool == "pencil" {
|
||||
// One finger alone still draws.
|
||||
let mut p = app.canvas_rect.unwrap().center() - vec2(30.0, 20.0);
|
||||
frame(&mut app, &ctx, press(3, p));
|
||||
for _ in 0..4 {
|
||||
p += vec2(15.0, 10.0);
|
||||
frame(&mut app, &ctx, vec![touch(3, egui::TouchPhase::Move, p), egui::Event::PointerMoved(p)]);
|
||||
}
|
||||
frame(&mut app, &ctx, lift(3, p));
|
||||
let children = app.session.active().unwrap().doc.layers[0].children().unwrap().len();
|
||||
assert_eq!(children, 2, "one finger draws with the Pencil");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A canvas frame at input time `t` (seconds) with `events`, for the trackpad rotation tests.
|
||||
fn timed_frame(app: &mut VectorcraftApp, ctx: &egui::Context, t: f64, events: Vec<egui::Event>) {
|
||||
let raw =
|
||||
|
||||
@@ -63,7 +63,8 @@ const TAP_SLOP: f32 = 16.0;
|
||||
/// What a touch event made of the gesture.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Touch {
|
||||
/// A second finger landed: the gesture isn't the first finger's press.
|
||||
/// A finger landed while another is down: the gesture isn't the first finger's press, nor a
|
||||
/// lifted finger's press as it lands again.
|
||||
Fingers,
|
||||
/// All fingers lifted from a tap of this many fingers (two or more).
|
||||
Tap(usize),
|
||||
@@ -93,9 +94,8 @@ impl Taps {
|
||||
if !self.down.iter().any(|(d, _)| *d == id) {
|
||||
self.down.push((id, pos));
|
||||
}
|
||||
let before = self.most;
|
||||
self.most = self.most.max(self.down.len());
|
||||
(before < 2 && self.most >= 2).then_some(Touch::Fingers)
|
||||
(self.down.len() >= 2).then_some(Touch::Fingers)
|
||||
}
|
||||
TouchPhase::Move => {
|
||||
if self.down.iter().any(|(d, at)| *d == id && at.distance(pos) > TAP_SLOP) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Hardware parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (pen pressure and the pen's eraser end on macOS, read from AppKit's tablet events, #852, not yet tried on a Mac; previously: patch: web pen buttons/modifiers preserved, touch-only input left to eframe, session-only palm rejection; tool cursors painted into the window on Wayland, #1652, not run on Linux; borderless title-bar window on Windows, #1758, awaiting confirmation; a window surface larger than 8192 px keeps working, #1459 in part; Linux Display Server preference opens the window through XWayland, #1151) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** patch (a second finger cancels the tool press the first finger began, and a finger landing while another is down presses nothing; the pinch only navigates, #4198) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
What hardware Illustrator uses, per platform, against VectorCraft. Illustrator's side is from its public
|
||||
documentation (GPU Performance, touch workspace, pen pressure pages); ours from the code
|
||||
@@ -25,7 +25,7 @@ reads the pen's eraser end on Windows, on macOS and on Linux under X11 and XWayl
|
||||
| Pen tilt, bearing, barrel rotation (6D Art Pen) | yes, drives brushes | no | no | no | no | missing: 4–8 h after the input layer (#372) |
|
||||
| Pen eraser end | yes | the eraser end (AppKit's tablet proximity events) selects the Eraser tool and the tip brings back the tool used before, as on Linux (#852; not yet tried on a Mac) | no | under X11 and XWayland, the eraser end selects the Eraser tool and the tip brings back the tool used before (#764); **no** on native Wayland | a press of the eraser end (Pointer Events eraser button, `button` 5) selects the Eraser tool, and that press erases; a press of the tip brings back the tool used before; Pointer Events report no end while the pen hovers | partial |
|
||||
| Pen eraser end | yes | the eraser end (AppKit's tablet proximity events) selects the Eraser tool and the tip brings back the tool used before, as on Linux (#852; not yet tried on a Mac) | with Windows Ink, an inverted pen or its eraser button (`PEN_FLAG_INVERTED`, `PEN_FLAG_ERASER` in the pen's pointer messages) selects the Eraser tool and the tip brings back the tool used before, as on Linux (not yet tried with a pen) | under X11 and XWayland, the eraser end selects the Eraser tool and the tip brings back the tool used before (#764); **no** on native Wayland | no | partial |
|
||||
| Touch gestures (pinch zoom, two-finger pan, rotate view, Touch workspace) | yes (Windows touch, Surface) | trackpad pinch, scroll and rotate | pinch, pan; two-finger tap undo, three-finger redo (#585) | same where winit reports touch | eframe until the first pen contact, then Pointer Events (pen and fingers told apart); long press opens the context menu | partial: touch workspace and touch-screen rotation missing, 3–5 h |
|
||||
| Touch gestures (pinch zoom, two-finger pan, rotate view, Touch workspace) | yes (Windows touch, Surface) | trackpad pinch, scroll and rotate | pinch, pan (a second finger cancels the first finger's tool press, #4198); two-finger tap undo, three-finger redo (#585) | same where winit reports touch | eframe until the first pen contact, then Pointer Events (pen and fingers told apart); long press opens the context menu | partial: touch workspace and touch-screen rotation missing, 3–5 h |
|
||||
| Trackpad (smooth scroll, pinch) | yes | yes | precision touchpads | yes | yes | done |
|
||||
| HiDPI / Retina, per-monitor scale | yes | yes | yes | yes | yes | done; a window dragged between monitors of different scale can still grow on Windows (#1459, open); the custom title bar's window on Windows stays truly borderless so the pointer lines up with what is drawn (#1791, #1758; awaiting the reporter's confirmation) |
|
||||
| Multiple monitors: panels and document windows on another screen | yes | no (one OS window) | no | no | — | missing: 6–10 h (with multiple windows, gap G7) |
|
||||
@@ -36,6 +36,7 @@ reads the pen's eraser end on Windows, on macOS and on Linux under X11 and XWayl
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | patch | Touch (#4198): a second finger landing cancels the tool press the first finger began, also in the same frame (the Pencil's stroke and a Selection move are rolled back, nothing enters the undo history), and a finger landing while another is down presses nothing, so a pinch or two-finger pan only navigates. Headless canvas test `a_second_finger_cancels_the_tool_gesture_the_first_began`; not tried on a touch screen; score unchanged (estimated) |
|
||||
| 2026-10-24 | minor | macOS: an AppKit local event monitor reads each mouse and tablet event's pen pressure and the pen's end before the window gets it and writes them into the canvas's pen feed (#852): each pointer move of a stroke keeps its own pressure, mouse event coalescing is off, and the eraser end selects the Eraser tool. The mapping is unit-tested on every platform and the macOS code type-checks for `aarch64-apple-darwin`; not yet tried on a Mac with a tablet, so the score is unchanged (estimated) |
|
||||
| 2026-10-15 | patch | Web review: preserve pen buttons and modifiers; leave fingers to eframe before the first pen contact; remove persistent pen policy. Canvas regression tests cover intermediate moves and long-touch cancellation on native and web input. Physical-device QA pending. |
|
||||
| 2026-10-14 | minor | Tool cursors are painted into the window on Wayland (#1652; not run on Linux, #1645 stays open until confirmed); On Windows the custom title bar's window stays truly borderless, so the pointer lines up with what is drawn on systems where the hidden caption offset it (#1791, #1758; awaiting the reporter's confirmation); neither is confirmed on the hardware that reported it, so nothing is marked done |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Parity with Adobe Illustrator
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (File formats: SVG export writes simple shapes as `<rect>`, `<circle>`, `<ellipse>`, `<line>`, `<polygon>` and `<polyline>` (#4652); scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Hardware: a second finger cancels the tool press the first finger began, so a pinch only navigates (#4198); scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
|
||||
|
||||
## Feature submission evidence
|
||||
|
||||
@@ -783,6 +783,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
While the desktop app's render worker renders the frame for a new view, the parts of the canvas the last frame does not cover after a pan, a zoom-out or a turn show the art from a frame that a second worker renders at a quarter of the resolution, covering twice the canvas's width and height. That frame draws images only from pixels already decoded (an image not yet decoded shows as a light gray box), and it renders large raster effects at the lower resolution a drag uses. Until it is ready, those parts show the pasteboard and the artboards. They used to show only the pasteboard and the artboards until the frame for the new view was ready.
|
||||
With the view rotated, a second Escape less than a second after the first resets the rotation, as Reset Rotate View does. The first Escape may end something else, such as isolation mode or a path being drawn. The second resets the view only when it has nothing else to end, and an Escape that closes a menu does not count. After a click, or a press of another key that is not a modifier (Shift, Control, Alt/Option or Command), the next Escape counts as the first. With the Rotate View tool chosen, one Escape resets the view.
|
||||
A two-finger rotation on a Mac trackpad turns the view about the pointer once it reaches 5° (Preferences › General › Trackpad Gesture to Rotate View, on by default).
|
||||
On a touch screen a second finger cancels the tool press the first finger began, in the frame it lands or later: the Pencil's stroke or a Selection move is rolled back with no undo entry, and a finger that lands while another is down presses nothing, so a pinch or two-finger pan only zooms and pans (#4198; canvas test `a_second_finger_cancels_the_tool_gesture_the_first_began`).
|
||||
The status bar navigator's field shows the active artboard's number and name, and its drop-down lists every artboard by number and name, as the Artboards panel does. Choosing an artboard in the list makes it the active artboard and fits it in the window (`view.goToArtboard`). The list opens on the active artboard's row and lays out only the rows in view and at most one more.
|
||||
- **Geometry and effects:**
|
||||
- **Open-path anchor Delete:** Delete/Backspace removes selected anchors and their adjoining segments, leaving drawable runs as separate open subpaths of the original object. Surviving curves and appearance stay intact; one Undo restores the document and selection, and a second Delete removes the selected remainder. Explicit Remove Anchor Points still reconnects/refits. Four core regressions, one full-frame Delete/Backspace regression and three existing removal/segment controls pass, covering these distinctions, multiple deletions, transformed geometry and mixed whole-object selection. Behaviour references: the legacy public [Illustrator reference guide, Add or delete an anchor point](https://helpx.adobe.com/content/dam/help/en/pdf/illustrator_reference.pdf) distinguishes keyboard deletion from dedicated removal; the current [Add or remove anchor points](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/modify-paths/add-or-remove-anchor-points.html) describes the dedicated tools. Other G11 gaps and scores/hours remain unchanged.
|
||||
@@ -1209,6 +1210,7 @@ applies to fills). Tests: `color::gradient::tests` (perceptual midpoint, expansi
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | Hardware: a second finger cancels the tool press the first finger began, and a finger landing while another is down presses nothing, so a pinch only navigates (#4198); evidence added; scores unchanged (estimated). |
|
||||
| 2026-10-30 | minor | File formats: SVG export writes axis-aligned rectangles as `<rect>`, circles and ellipses as `<circle>`/`<ellipse>`, straight lines as `<line>` and other straight-segment paths as `<polygon>`/`<polyline>`, keeping `<path>` where a shape element would draw differently (#4652); evidence added; scores and hours unchanged (estimated). |
|
||||
| 2026-10-30 | minor | UI chrome: with Preferences › Performance › Real-time Drawing and Editing off, a ruler guide dragged out of a ruler or moved shows while it is dragged (#4646); evidence added; score and hours unchanged (estimated). |
|
||||
| 2026-10-30 | minor | Direct Selection: a marquee over part of a path segment, with neither of its anchors inside, selects the segment (#4654); scores unchanged |
|
||||
|
||||
Reference in New Issue
Block a user