diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 177697839..f0b7f4b60 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -203,6 +203,9 @@ pub struct Session { /// The pointer button is held: the last pointer event was a press or a drag, not a release or /// a move ([`Session::pointer_held`]). pub(crate) pointer_held: bool, + /// The id and persistent options of the tool when the press under way began, which a dropped + /// press puts back ([`Session::drop_tool_press`]). + pub(crate) press_options: Option<(String, serde_json::Map)>, /// A guide being dragged out of a ruler ([`Session::ruler_guide`]). pub(crate) ruler_guide: Option, /// Which selected type overflows its frame, for the ports every tool but the Type tools shows. diff --git a/crates/engine/src/session/documents.rs b/crates/engine/src/session/documents.rs index 51b5b5776..da2c65988 100644 --- a/crates/engine/src/session/documents.rs +++ b/crates/engine/src/session/documents.rs @@ -92,6 +92,7 @@ impl Session { liquify_stroke: None, plane_widget_press: false, pointer_held: false, + press_options: None, ruler_guide: None, port_marks: Default::default(), font_search: None, diff --git a/crates/engine/src/tests/toolsettings.rs b/crates/engine/src/tests/toolsettings.rs index 34f85f822..3e522a552 100644 --- a/crates/engine/src/tests/toolsettings.rs +++ b/crates/engine/src/tests/toolsettings.rs @@ -91,11 +91,11 @@ 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. +/// A dropped press (a long touch, a canceled touch, a second finger) keeps nothing it began: the +/// tool ends at rest with the options it had before the press, 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() { +fn a_dropped_press_does_not_keep_what_its_drag_changed() { let mut s = session(); select(&mut s, "polygon"); let v = ViewInfo::default(); @@ -104,20 +104,100 @@ fn a_dropped_press_does_not_store_what_its_drag_changed() { 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(); + assert_eq!(s.tool_options()["sides"], json!(6)); 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:?}"); + assert_eq!(s.tool_options()["sides"], json!(6), "the tool has its sides from before the press"); + assert_eq!(s.prefs.tool_settings.get("polygon").and_then(|o| o.get("sides")), None, "the dropped drag's sides are not 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"); + assert_eq!(s.prefs.tool_settings["polygon"]["sides"], json!(7), "and stores its sides"); +} + +/// A dropped Pen press drops only the anchor it was placing: the path being drawn goes on with +/// the next click. +#[test] +fn a_dropped_pen_press_keeps_the_path_being_drawn() { + let mut s = session(); + select(&mut s, "pen"); + let v = ViewInfo::default(); + let click = |s: &mut Session, x: f64, y: f64| { + s.pointer(&PointerEvent::new(PointerKind::Down, x, y), v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Up, x, y), v).unwrap(); + }; + click(&mut s, 10.0, 10.0); + click(&mut s, 100.0, 10.0); + s.pointer(&PointerEvent::new(PointerKind::Down, 100.0, 100.0), v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Drag, 140.0, 120.0), v).unwrap(); + s.drop_tool_press(v).unwrap(); + click(&mut s, 10.0, 100.0); + let d = &s.active().unwrap().doc; + let paths = d.layers[0].children().unwrap(); + assert_eq!(paths.len(), 1, "no new path starts"); + assert_eq!(paths[0].path_data().unwrap().anchor_count(), 3, "the dropped anchor is not placed"); +} + +/// A dropped Pen press on the first anchor does not close the path or end it. +#[test] +fn a_dropped_pen_close_keeps_the_path_open_and_being_drawn() { + let mut s = session(); + select(&mut s, "pen"); + let v = ViewInfo::default(); + let click = |s: &mut Session, x: f64, y: f64| { + s.pointer(&PointerEvent::new(PointerKind::Down, x, y), v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Up, x, y), v).unwrap(); + }; + click(&mut s, 10.0, 10.0); + click(&mut s, 100.0, 10.0); + click(&mut s, 100.0, 100.0); + s.pointer(&PointerEvent::new(PointerKind::Down, 10.0, 10.0), v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Drag, 40.0, 30.0), v).unwrap(); + s.drop_tool_press(v).unwrap(); + click(&mut s, 10.0, 100.0); + let d = &s.active().unwrap().doc; + let paths = d.layers[0].children().unwrap(); + assert_eq!(paths.len(), 1, "no new path starts"); + let pd = paths[0].path_data().unwrap(); + assert_eq!(pd.anchor_count(), 4, "the next click goes on the path"); + assert!(!pd.subpaths[0].closed, "the path is not closed"); +} + +/// A dropped marquee selects nothing: the selection stays as it was before the press. +#[test] +fn a_dropped_marquee_leaves_the_selection_alone() { + let mut s = session(); + s.execute("shape.rectangle", &json!({"x": 600, "y": 400, "width": 100, "height": 100})).unwrap(); + let before = s.doc().unwrap().selection.objects.clone(); + assert_eq!(before.len(), 1); + select(&mut s, "selection"); + let v = ViewInfo::default(); + s.pointer(&PointerEvent::new(PointerKind::Down, 300.0, 300.0), v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Drag, 350.0, 350.0), v).unwrap(); + s.drop_tool_press(v).unwrap(); + assert!(!s.tool_busy(), "the tool is at rest"); + assert_eq!(s.doc().unwrap().selection.objects, before); +} + +/// A dropped Shift-drag marquee of the Artboard tool toggles no artboard: the tool takes Escape, +/// as during a drag, before its release. +#[test] +fn a_dropped_artboard_marquee_toggles_nothing() { + let mut s = session(); + s.execute("artboard.new", &json!({"x": 1000, "y": 0, "width": 400, "height": 400})).unwrap(); + select(&mut s, "artboard"); + let before = s.tool_options()["selected"].clone(); + let v = ViewInfo::default(); + let shift = Mods { shift: true, ..Mods::default() }; + s.pointer(&PointerEvent::new(PointerKind::Down, 1200.0, 500.0).with_mods(shift), v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Drag, 1250.0, 550.0).with_mods(shift), v).unwrap(); + s.drop_tool_press(v).unwrap(); + assert!(!s.tool_busy(), "the tool is at rest"); + assert_eq!(s.tool_options()["selected"], before, "no artboard is toggled"); } #[test] diff --git a/crates/engine/src/tooling.rs b/crates/engine/src/tooling.rs index f8eb5c1f6..0a959959e 100644 --- a/crates/engine/src/tooling.rs +++ b/crates/engine/src/tooling.rs @@ -361,20 +361,44 @@ impl Session { 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. + /// Drop the press in progress (a long touch, a canceled touch, a second finger): nothing it + /// began is kept. Its interaction is undone, the tool drops the press ([`Tool::drop_press`]: + /// Escape, as during a drag, for most tools), and a release returns the tool to rest. Of what + /// the tool asks for along the way, only a cancel and a notify run, so a marquee selects + /// nothing and a click opens no size dialog. The tool gets back the persistent options it had + /// when the press began, and none of the press's changes to them are stored. 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. + // Nothing to roll back without a document; the tool still drops the press and gets the release. let _ = self.cancel_interaction(); - let esc = self.tool_key(ToolKey::Escape, Mods::default(), view); + let before = self.press_options.take(); + let mut acts = self.with_tool_cx(view, |t, cx| t.drop_press(cx)); + acts.retain(|a| matches!(a, Action::Cancel | Action::Notify(_))); + let dropped = self.take_tool_panic().and_then(|()| self.apply_actions(acts)); 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) + let r = dropped.and(self.pointer_event(&up, view, false)); + if let Some((id, opts)) = before + && id == self.tool.id() + { + let now = self.tool.options(); + for (k, v) in &opts { + if now.get(k) != Some(v) { + self.tool.set_option(k, v); + } + } + } + r.map(drop) } - /// [`Self::pointer`]; a release stores the tool's options only with `keep`. + /// The options of `t` that [`Self::store_tool_settings`] stores. + fn persistent_options(t: &dyn Tool) -> Map { + let opts = t.options(); + let radius = (t.id() == ROUNDED_RECTANGLE).then_some("cornerRadius"); + let keys = settings::stores(t.id()).flat_map(|(_, keys)| keys.iter().copied()).chain(radius); + keys.filter_map(|k| opts.get(k).map(|v| (k.to_string(), v.clone()))).collect() + } + + /// [`Self::pointer`]. Without `keep` (a dropped press's release), the tool's options are not + /// stored and only the tool's `Cancel` and `Notify` actions run. fn pointer_event(&mut self, ev: &PointerEvent, view: ViewInfo, keep: bool) -> Result> { self.last_view = view; match ev.kind { @@ -382,13 +406,21 @@ impl Session { PointerKind::Up | PointerKind::Move => self.pointer_held = false, PointerKind::DoubleClick => {} } + match ev.kind { + PointerKind::Down => self.press_options = Some((self.tool.id().to_string(), Self::persistent_options(self.tool.as_ref()))), + PointerKind::Up => self.press_options = None, + _ => {} + } // The Plane Switching Widget takes its clicks whatever the tool. if let Some(r) = self.plane_widget_pointer(ev, view) { return r; } self.leave_draw_inside(ev, view)?; let ev = &self.lend_selection_tool(ev); - let acts = self.with_tool_cx(view, |t, cx| t.pointer(cx, ev)); + let mut acts = self.with_tool_cx(view, |t, cx| t.pointer(cx, ev)); + if !keep { + acts.retain(|a| matches!(a, Action::Cancel | Action::Notify(_))); + } let r = self.take_tool_panic().and_then(|()| { // A gesture may change options (Alt-drag sizes a Liquify brush): keep them. if keep && ev.kind == PointerKind::Up { diff --git a/crates/tools/src/addons/smart_pen.rs b/crates/tools/src/addons/smart_pen.rs index f2b52219e..90c73620a 100644 --- a/crates/tools/src/addons/smart_pen.rs +++ b/crates/tools/src/addons/smart_pen.rs @@ -534,6 +534,15 @@ impl Tool for SmartPenTool { } } + /// A dropped press does not end the path being drawn, as Escape would: the press and its + /// marquee are forgotten, so the release does nothing. + fn drop_press(&mut self, _cx: &ToolContext) -> Vec { + self.press = None; + self.marquee = None; + self.snap.clear(); + vec![] + } + fn claims_key(&self, cx: &ToolContext, key: ToolKey) -> bool { key == ToolKey::Tab && (self.press.is_some() || self.active(cx).is_some()) } diff --git a/crates/tools/src/draw2/curvature.rs b/crates/tools/src/draw2/curvature.rs index 8301c108c..70ad9a7e4 100644 --- a/crates/tools/src/draw2/curvature.rs +++ b/crates/tools/src/draw2/curvature.rs @@ -35,6 +35,9 @@ pub struct CurvatureTool { id: Option, /// The point being dragged, the pointer at the press, and whether the drag began. drag: Option<(Grab, Point, bool)>, + /// Where the point of the path being drawn that the press grabbed was at the press, and the + /// current point before the press. + grabbed: Option<(Point, Option)>, /// Last touched point of the path being drawn (Backspace removes it). current: Option, hover: Option, @@ -564,7 +567,7 @@ impl Tool for CurvatureTool { PointerKind::Down => { if let Some(id) = active { if let Some(i) = self.hit_point(cx, p) { - self.current = Some(i); + let was_current = self.current.replace(i); if ev.mods.alt { self.pts[i].1 = !self.pts[i].1; return vec![Action::Exec("path.curvature".into(), self.params())]; @@ -576,6 +579,7 @@ impl Tool for CurvatureTool { // The point dragged and the curve through it move: the rest pull. hold_targets(&mut self.snap, cx, id, 0, &[i]); self.drag = Some((Grab::Drawn(i), p, false)); + self.grabbed = self.pts.get(i).map(|pt| (pt.0, was_current)); return vec![]; } if !self.closed { @@ -650,6 +654,18 @@ impl Tool for CurvatureTool { && self.drag.is_none() && if self.active(cx).is_some() { self.pts.len() > 1 } else { current_point(cx).is_some() } } + /// A dropped press does not end the path being drawn, as Escape would: a point it grabbed + /// goes back to where it was at the press, and the current point to the one before the press. + fn drop_press(&mut self, _cx: &ToolContext) -> Vec { + if let (Some((Grab::Drawn(i), _, _)), Some((at, current))) = (self.drag.take(), self.grabbed.take()) + && let Some(pt) = self.pts.get_mut(i) + { + pt.0 = at; + self.current = current; + } + self.snap.clear(); + vec![] + } fn key(&mut self, cx: &ToolContext, key: ToolKey, _m: Mods) -> Vec { match key { ToolKey::Escape | ToolKey::Enter => { diff --git a/crates/tools/src/draw2/curvature/tests.rs b/crates/tools/src/draw2/curvature/tests.rs index 412292ef9..05593a3dc 100644 --- a/crates/tools/src/draw2/curvature/tests.rs +++ b/crates/tools/src/draw2/curvature/tests.rs @@ -370,6 +370,34 @@ fn a_press_on_a_point_selects_it_and_a_drag_moves_it() { assert_eq!(press(&mut t, &cx, 100.0, 0.0, false).len(), 1); } +/// A dropped drag of a point of the path being drawn puts the point back where it was at the +/// press, and the path goes on: the next click adds a point to it. +#[test] +fn a_dropped_drag_puts_the_point_back_and_keeps_drawing() { + let (d, id) = doc_with(SubPath::polyline(&[pt(100.0, 100.0), pt(200.0, 100.0), pt(300.0, 100.0)], false)); + let (s, p) = (selected(id, None), paint()); + let cx = quiet(&d, &s, &p); + let mut t = CurvatureTool { + pts: vec![(pt(100.0, 100.0), false), (pt(200.0, 100.0), false), (pt(300.0, 100.0), false)], + id: Some(id), + current: Some(2), + ..Default::default() + }; + t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 200.0, 100.0)); + let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 220.0, 160.0)); + assert_eq!(a[0], Action::Begin("Curvature".into())); + assert_eq!(t.pts[1].0, pt(220.0, 160.0)); + assert!(t.drop_press(&cx).is_empty(), "the engine has undone the preview"); + assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 0.0, 0.0)), vec![]); + assert_eq!(t.pts[1].0, pt(200.0, 100.0), "the point is back"); + assert_eq!(t.current, Some(2), "the current point is the one before the press"); + let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 400.0, 200.0)); + assert!( + matches!(&a[0], Action::Exec(_, v) if v["id"] == id.0 && v["points"][1]["y"] == 100.0 && v["points"].as_array().unwrap().len() == 4), + "{a:?}" + ); +} + /// #2120: the dragged end of a slanted straight path keeps to its extension (Smart Guides). #[test] fn a_dragged_end_of_a_straight_path_keeps_to_its_extension() { diff --git a/crates/tools/src/lib.rs b/crates/tools/src/lib.rs index dd5977f5a..7aa338ab4 100644 --- a/crates/tools/src/lib.rs +++ b/crates/tools/src/lib.rs @@ -124,6 +124,12 @@ pub trait Tool: Send { fn key(&mut self, _cx: &ToolContext, _key: ToolKey, _mods: Mods) -> Vec { vec![] } + /// The press in progress is dropped (a long touch, a canceled touch, a second finger): the + /// engine has undone its interaction and sends a release after this. By default the tool takes + /// Escape, as during a drag. + fn drop_press(&mut self, cx: &ToolContext) -> Vec { + self.key(cx, ToolKey::Escape, Mods::default()) + } fn overlays(&self, _cx: &ToolContext) -> Vec { vec![] } diff --git a/crates/tools/src/pen.rs b/crates/tools/src/pen.rs index a2e9d3881..e5906db59 100644 --- a/crates/tools/src/pen.rs +++ b/crates/tools/src/pen.rs @@ -445,6 +445,15 @@ impl Tool for PenTool { _ => vec![], } } + /// A dropped press drops only the anchor it was placing, a handle it was dragging or the + /// close it was making: the path being drawn goes on with the next click. + fn drop_press(&mut self, _cx: &ToolContext) -> Vec { + self.drag = None; + self.handle = None; + self.convert = None; + self.snap.clear(); + vec![] + } /// Deactivate the pen tool, committing in-flight operations and restoring compound path selection. fn deactivate(&mut self, cx: &ToolContext) -> Vec { let path = self.path; diff --git a/crates/ui-egui/src/canvas/drag.rs b/crates/ui-egui/src/canvas/drag.rs index ef4cc2ad2..3f3c156ed 100644 --- a/crates/ui-egui/src/canvas/drag.rs +++ b/crates/ui-egui/src/canvas/drag.rs @@ -230,8 +230,8 @@ 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_id())) { Some(Drag::Tool | Drag::Art) => { - // 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 + // The tool drops the press, and a mouse-up returns it to rest with the options it had + // when the press began. 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![])); diff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs index f0a594f2a..efb608e74 100644 --- a/crates/ui-egui/src/canvas/tests.rs +++ b/crates/ui-egui/src/canvas/tests.rs @@ -6728,10 +6728,10 @@ fn long_touch_drops_a_tool_press_without_committing_art() { 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. +/// A canceled touch drops the tool's drag: the options the drag changed (the Polygon tool's +/// sides) are put back and not stored, and nothing is drawn. #[test] -fn a_cancelled_touch_drops_the_drag_without_storing_its_options() { +fn a_canceled_touch_drops_the_drag_without_keeping_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(); @@ -6745,7 +6745,7 @@ fn a_cancelled_touch_drops_the_drag_without_storing_its_options() { assert!(matches!(ctx.data(|d| d.get_temp::(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(); + assert_eq!(app.session.tool_options()["sides"], json!(6)); 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)); @@ -6753,9 +6753,9 @@ fn a_cancelled_touch_drops_the_drag_without_storing_its_options() { 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. + assert_eq!(app.session.tool_options()["sides"], json!(6), "the tool has its sides from before the press"); 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:?}"); + assert_eq!(stored, None, "the dropped drag's sides are not stored"); } /// #2826: a second finger drops a move waiting over a panel, so its interaction doesn't diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 1ce35dbe3..8fd362eab 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -840,7 +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`). + A tool press dropped by a second finger, a long touch or a canceled touch keeps nothing it began: the tool gets back the options it had when the press began (the Polygon tool's sides, set with ↑ and ↓) and they are not stored, a press held still opens no size dialog, a Selection or Artboard tool marquee selects nothing, and the Pen goes on drawing its path without the dropped anchor (canvas tests `a_canceled_touch_drops_the_drag_without_keeping_its_options` and `long_touch_drops_a_tool_press_without_committing_art`; engine tests in `tests::toolsettings`). 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.