From db7efb79231d66d904b8c715090e2193a03e6f79 Mon Sep 17 00:00:00 2001 From: zakariayacine Date: Sun, 11 Oct 2026 16:23:01 +0100 Subject: [PATCH] Direct Selection: a click on an already selected anchor selects it alone Clicking an anchor of a path selected whole (or one of several selected anchors) with the Direct Selection tool left the selection as it was, so the clicked anchor never became the selected one and Live Corners and the anchor controls kept acting on every corner. A plain click now selects that anchor alone, as the public documentation describes (a click selects the anchor point, Shift-click adds others): https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/modify-paths/select-anchor-points-in-paths.html The press keeps the selection so a drag still moves every selected anchor with the one held; only the release without a drag narrows it. Shift still toggles, Alt still picks the whole path, and an anchor that wasn't selected is still selected on the press. Test: direct::tests::a_click_on_a_selected_anchor_selects_it_alone. Co-Authored-By: Claude Fable 5.1 --- crates/tools/src/direct.rs | 45 +++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/crates/tools/src/direct.rs b/crates/tools/src/direct.rs index b897b38c..f6d18727 100644 --- a/crates/tools/src/direct.rs +++ b/crates/tools/src/direct.rs @@ -136,6 +136,9 @@ pub struct DirectSelectionTool { /// The anchor under the pointer (Highlight anchors on mouse over): its path, where it is. hover: Option<(NodeId, Point)>, guide: GuideEdit, + /// The already selected anchor the press landed on: a click (no drag) selects it alone, as a + /// drag moves every selected anchor with it. + click: Option<(NodeId, usize, usize)>, } impl DirectSelectionTool { @@ -152,6 +155,7 @@ impl DirectSelectionTool { move_snap: None, hover: None, guide: GuideEdit::default(), + click: None, } } } @@ -360,6 +364,7 @@ impl Tool for DirectSelectionTool { vec![] } (PointerKind::Down, _) => { + self.click = None; if let Some(c) = CornerDrag::hit(cx, ev, true) { self.state = State::Corner(c); return vec![]; @@ -412,6 +417,9 @@ impl Tool for DirectSelectionTool { if ev.mods.shift { return vec![Action::Exec("select.anchors".into(), json!({"id": id.0, "anchors": [[si, ai]], "mode": "toggle"}))]; } + // Pressed among other selected anchors: a click selects this one alone (the + // drag, which moves them all, keeps them). + self.click = already.then_some((id, si, ai)); if !already { return vec![Action::Exec("select.anchors".into(), json!({"id": id.0, "anchors": [[si, ai]], "mode": "set"}))]; } @@ -628,7 +636,13 @@ impl Tool for DirectSelectionTool { self.state = State::Idle; self.guides.clear(); (self.anchor_snap, self.copy_snap, self.move_snap) = (None, None, None); - if began { vec![Action::Commit] } else { vec![] } + match (began, self.click.take()) { + (true, _) => vec![Action::Commit], + (false, Some((id, si, ai))) => { + vec![Action::Exec("select.anchors".into(), json!({"id": id.0, "anchors": [[si, ai]], "mode": "set"}))] + } + (false, None) => vec![], + } } (PointerKind::Up, State::Handle { .. } | State::SpineHandle { .. }) => { self.state = State::Idle; @@ -969,6 +983,35 @@ mod tests { assert_eq!(a, vec![Action::Exec("select.anchors".into(), json!({"id": id.0, "anchors": [[0, 0]], "mode": "toggle"}))]); } + /// A click (no drag) on an anchor that is selected with others, whether the whole path is or + /// some of its anchors, selects that anchor alone, as in Illustrator: a press keeps the + /// selection so a drag moves it all, the release without a drag narrows it. Shift still + /// toggles, and a click on a path's only selected anchor changes nothing visible. + #[test] + fn a_click_on_a_selected_anchor_selects_it_alone() { + let (d, id) = doc_with_rect(); + let p = paint(); + let alone = vec![Action::Exec("select.anchors".into(), json!({"id": id.0, "anchors": [[0, 0]], "mode": "set"}))]; + let mut whole = Selection::default(); + whole.set([id]); + for s in [whole, anchors_of(id, &[(0, 0), (0, 1)])] { + let cx = cx(&d, &s, &p); + let mut t = DirectSelectionTool::new(false); + assert!(t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 100.0, 100.0)).is_empty(), "{s:?}"); + assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 100.0, 100.0)), alone, "{s:?}"); + // Dragged instead: moved, selection kept. + assert!(t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 100.0, 100.0)).is_empty()); + t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 120.0, 100.0)); + assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 120.0, 100.0)), vec![Action::Commit], "{s:?}"); + } + // An anchor not yet selected is selected on the press, and the release adds nothing. + let none = Selection::default(); + let cx = cx(&d, &none, &p); + let mut t = DirectSelectionTool::new(false); + assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 100.0, 100.0)), alone); + assert!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 100.0, 100.0)).is_empty()); + } + /// Alt-pressing an anchor picks the whole path, as Alt on a segment does, and the drag copies /// it as the Selection tool's Alt-drag does. #[test]