Merge pull request #1237 from zakariayacine/fix-direct-click-anchor

Direct Selection: a click on an already selected anchor selects it alone
This commit is contained in:
BFlat 2026-10-12 01:25:50 +07:00 committed by GitHub
commit 9fe75fa85c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 44 additions and 1 deletions

View File

@ -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]