mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 17:40:14 -06:00
Canvas: a dropped press cancels the tool's drag instead of ending it
A long touch, a canceled touch or a second finger drops the tool press in progress. The tool used to get a plain mouse-up, which it cannot tell from a release: the options the drag changed (the Polygon tool's sides set with the arrow keys) were stored, and a shape tool pressed and held still opened its size dialog. Session::drop_tool_press undoes the press's interaction, sends the tool Escape as during a drag, and then a release that returns it to rest without storing the tool's options. What the tool asks of the UI along the way is dropped.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
//! Tool options that persist across tool switches, documents and launches (`Prefs::tool_settings`).
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey};
|
||||
|
||||
use crate::*;
|
||||
|
||||
@@ -90,6 +91,35 @@ fn a_gesture_keeps_what_it_changed_and_a_document_switch_keeps_options() {
|
||||
assert_eq!(s.tool_options()["segments"], json!(14));
|
||||
}
|
||||
|
||||
/// A dropped press (a long touch, a cancelled touch, a second finger) keeps nothing it began: the
|
||||
/// tool takes Escape and ends at rest, and the options the drag changed are not stored. A release
|
||||
/// stores them.
|
||||
#[test]
|
||||
fn a_dropped_press_does_not_store_what_its_drag_changed() {
|
||||
let mut s = session();
|
||||
select(&mut s, "polygon");
|
||||
let v = ViewInfo::default();
|
||||
let drag_with_one_more_side = |s: &mut Session| {
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 100.0, 100.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 200.0, 150.0), v).unwrap();
|
||||
s.tool_key(ToolKey::Up, Mods::default(), v).unwrap();
|
||||
};
|
||||
let before = s.tool_options()["sides"].clone();
|
||||
drag_with_one_more_side(&mut s);
|
||||
assert_eq!(s.tool_options()["sides"], json!(7));
|
||||
s.drop_tool_press(v).unwrap();
|
||||
assert!(!s.tool_busy(), "the tool is at rest");
|
||||
let st = s.active().unwrap();
|
||||
assert!(st.interaction.is_none() && st.doc.art_bounds().is_none(), "the polygon is not drawn");
|
||||
// A tool whose Escape puts back the options the drag changed stores those; nothing else is stored.
|
||||
let stored = s.prefs.tool_settings.get("polygon").and_then(|o| o.get("sides"));
|
||||
assert!(stored.is_none_or(|v| *v == before), "the dropped drag's sides are not stored: {stored:?}");
|
||||
drag_with_one_more_side(&mut s);
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 200.0, 150.0), v).unwrap();
|
||||
assert!(s.active().unwrap().doc.art_bounds().is_some(), "a release draws the polygon");
|
||||
assert_eq!(s.prefs.tool_settings["polygon"]["sides"], s.tool_options()["sides"], "and stores its sides");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn options_are_saved_with_the_preferences() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -358,6 +358,24 @@ impl Session {
|
||||
|
||||
/// Feed a pointer event to the active tool. Returns requests for the UI.
|
||||
pub fn pointer(&mut self, ev: &PointerEvent, view: ViewInfo) -> Result<Vec<UiRequest>> {
|
||||
self.pointer_event(ev, view, true)
|
||||
}
|
||||
|
||||
/// Drop the press in progress (a long touch, a cancelled touch, a second finger): nothing it
|
||||
/// began is kept. Its interaction is undone, the tool takes Escape as it does during a drag,
|
||||
/// and then a release that returns it to rest. The options the press changed are not stored,
|
||||
/// and what the tool asks of the UI along the way (a size dialog for a click) is dropped.
|
||||
pub fn drop_tool_press(&mut self, view: ViewInfo) -> Result<()> {
|
||||
// Nothing to roll back without a document; the tool still takes Escape and the release.
|
||||
let _ = self.cancel_interaction();
|
||||
let esc = self.tool_key(ToolKey::Escape, Mods::default(), view);
|
||||
let up = PointerEvent { kind: PointerKind::Up, pos: Point::ZERO, mods: Mods::default(), backquote: false, pressure: 1.0, pen: None };
|
||||
let up = self.pointer_event(&up, view, false);
|
||||
esc.and(up).map(drop)
|
||||
}
|
||||
|
||||
/// [`Self::pointer`]; a release stores the tool's options only with `keep`.
|
||||
fn pointer_event(&mut self, ev: &PointerEvent, view: ViewInfo, keep: bool) -> Result<Vec<UiRequest>> {
|
||||
self.last_view = view;
|
||||
match ev.kind {
|
||||
PointerKind::Down | PointerKind::Drag => self.pointer_held = true,
|
||||
@@ -373,7 +391,7 @@ impl Session {
|
||||
let acts = self.with_tool_cx(view, |t, cx| t.pointer(cx, ev));
|
||||
let r = self.take_tool_panic().and_then(|()| {
|
||||
// A gesture may change options (Alt-drag sizes a Liquify brush): keep them.
|
||||
if ev.kind == PointerKind::Up {
|
||||
if keep && ev.kind == PointerKind::Up {
|
||||
self.keep_tool_settings();
|
||||
}
|
||||
self.apply_actions(acts)
|
||||
|
||||
@@ -230,15 +230,11 @@ pub fn mods(m: egui::Modifiers, space: bool) -> Mods {
|
||||
pub(super) fn drop_press(app: &mut VectorcraftApp, ui: &Ui, view: vectorcraft_engine::ViewInfo) {
|
||||
match ui.data(|d| d.get_temp::<Drag>(drag_id())) {
|
||||
Some(Drag::Tool | Drag::Art) => {
|
||||
// The tool's mouse-up returns it to rest. A move waiting over a panel is still live
|
||||
// and would otherwise outlive the drag that owns it (#2826).
|
||||
// Nothing to roll back without a document; the tool still gets its mouse-up.
|
||||
let _ = app.session.cancel_interaction();
|
||||
dispatch(
|
||||
app,
|
||||
&PointerEvent { kind: PointerKind::Up, pos: Point::ZERO, mods: Mods::default(), backquote: false, pressure: 1.0, pen: None },
|
||||
view,
|
||||
);
|
||||
// The tool takes Escape, as during a drag, and then a mouse-up that returns it to rest.
|
||||
// A move waiting over a panel is still live and would otherwise outlive the drag that
|
||||
// owns it (#2826).
|
||||
let r = app.session.drop_tool_press(view);
|
||||
apply_requests(app, r.map(|()| vec![]));
|
||||
egui::DragAndDrop::clear_payload(ui.ctx());
|
||||
}
|
||||
Some(_) => {}
|
||||
|
||||
@@ -6723,10 +6723,41 @@ fn long_touch_drops_a_tool_press_without_committing_art() {
|
||||
tick(&mut app, 1.5, vec![]);
|
||||
assert!(matches!(ctx.data(|d| d.get_temp::<Drag>(drag_id())), Some(Drag::Gesture)), "long touch drops the tool drag");
|
||||
assert!(app.session.active().unwrap().interaction.is_none());
|
||||
assert!(app.ui.dialog.is_none(), "the dropped press is not a click that opens the size dialog");
|
||||
tick(&mut app, 1.6, pen(egui::TouchPhase::End, pos, 0.5));
|
||||
assert!(app.session.active().unwrap().doc.art_bounds().is_none(), "releasing cannot commit the cancelled press");
|
||||
}
|
||||
|
||||
/// A cancelled touch drops the tool's drag as Escape does: the options the drag changed (the
|
||||
/// Polygon tool's sides) are not stored, and nothing is drawn.
|
||||
#[test]
|
||||
fn a_cancelled_touch_drops_the_drag_without_storing_its_options() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.prefs.touch_gestures = true;
|
||||
app.session.execute("file.new", &json!({"width": 800, "height": 600})).unwrap();
|
||||
app.select_tool("polygon");
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap());
|
||||
let start = xf.to_screen(Point::new(200.0, 200.0));
|
||||
frame(&mut app, &ctx, pen(egui::TouchPhase::Start, start, 0.5));
|
||||
frame(&mut app, &ctx, pen(egui::TouchPhase::Move, start + vec2(60.0, 40.0), 0.5));
|
||||
assert!(matches!(ctx.data(|d| d.get_temp::<Drag>(drag_id())), Some(Drag::Tool)));
|
||||
assert!(app.session.active().unwrap().interaction.is_some(), "the drag previews the polygon");
|
||||
let view = app.view_info();
|
||||
let before = app.session.tool_options()["sides"].clone();
|
||||
app.session.tool_key(vectorcraft_tools::ToolKey::Up, Mods::default(), view).unwrap();
|
||||
assert_eq!(app.session.tool_options()["sides"], json!(7));
|
||||
frame(&mut app, &ctx, pen(egui::TouchPhase::Cancel, start + vec2(60.0, 40.0), 0.5));
|
||||
assert!(!app.session.tool_busy(), "the tool is at rest");
|
||||
let st = app.session.active().unwrap();
|
||||
assert!(st.interaction.is_none() && st.doc.art_bounds().is_none(), "nothing is drawn");
|
||||
assert!(app.ui.dialog.is_none());
|
||||
// A tool whose Escape puts back the options the drag changed stores those; nothing else is stored.
|
||||
let stored = app.session.prefs.tool_settings.get("polygon").and_then(|o| o.get("sides"));
|
||||
assert!(stored.is_none_or(|v| *v == before), "the dropped drag's sides are not stored: {stored:?}");
|
||||
}
|
||||
|
||||
/// #2826: a second finger drops a move waiting over a panel, so its interaction doesn't
|
||||
/// outlive the drag that began it.
|
||||
#[test]
|
||||
|
||||
@@ -840,6 +840,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
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`).
|
||||
A tool press dropped by a second finger, a long touch or a cancelled touch reaches the tool as Escape does during a drag, followed by a release: the options the drag changed (the Polygon tool's sides, set with ↑ and ↓) are not stored, and a press held still opens no size dialog (canvas tests `a_cancelled_touch_drops_the_drag_without_storing_its_options` and `long_touch_drops_a_tool_press_without_committing_art`).
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user