M1: Alt pressed during a drag turns the move into a copy (#1385)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Muamer Mustafic
2026-10-27 12:21:44 +01:00
co-authored by Cursor
parent 43af145a12
commit a02c4abefc
4 changed files with 79 additions and 2 deletions
+18
View File
@@ -138,6 +138,24 @@ impl Targets {
self.boxes.push(r);
}
/// Take back what [`Targets::add_box`] added for `r`.
pub fn remove_box(&mut self, r: Rect) {
let take = |v: &mut Vec<Span>, s: Span| {
if let Some(i) = v.iter().rposition(|x| *x == s) {
v.remove(i);
}
};
for x in [r.x0, r.center().x, r.x1] {
take(&mut self.xs, (x, r.y0, r.y1));
}
for y in [r.y0, r.center().y, r.y1] {
take(&mut self.ys, (y, r.x0, r.x1));
}
if let Some(i) = self.boxes.iter().rposition(|b| *b == r) {
self.boxes.remove(i);
}
}
/// A point to snap onto (a vector point, a segment midpoint): lines through it on both axes,
/// and a marker when a moving point lands on it.
pub fn add_point(&mut self, p: Point) {
+14 -1
View File
@@ -159,7 +159,20 @@ impl Session {
}
pub(super) fn drag(&mut self, ev: &PointerEvent) -> Result<()> {
let g = std::mem::take(&mut self.tool.gesture);
let mut g = std::mem::take(&mut self.tool.gesture);
// Alt makes a move a copy whenever it goes down, before the drag or during it, and letting
// go of it moves the original again; a copy snaps to the layers it came from too.
if let Gesture::Move { targets, duplicate, .. } = &mut g
&& *duplicate != ev.alt
{
*duplicate = ev.alt;
if let Some(b) = targets.moving {
if ev.alt { targets.add_box(b) } else { targets.remove_box(b) }
}
if let Some(it) = self.ds_mut().ok().and_then(|ds| ds.interaction.as_mut()) {
it.label = if ev.alt { "Duplicate" } else { "Move" }.into();
}
}
let res = match &g {
Gesture::None => return self.hover(ev),
Gesture::Marquee { start, additive, base, within } => {
@@ -169,6 +169,51 @@ fn drag_move(s: &mut Session, from: (f64, f64), by: (f64, f64)) {
s.pointer(&ev(PointerKind::Up, (from.0 + by.0, from.1 + by.1))).unwrap();
}
/// Drag from `from` to `to` in four steps, Alt held at step `k` when `alt(k)` (0: the press, 1–4:
/// the moves, 5: the release).
fn drag_alt_when(s: &mut Session, from: (f64, f64), to: (f64, f64), alt: impl Fn(u8) -> bool) {
let ev = |k: u8, kind, (x, y): (f64, f64)| PointerEvent { alt: alt(k), ..PointerEvent::at(kind, x, y, 1.0) };
s.pointer(&ev(0, PointerKind::Down, from)).unwrap();
for k in 1..=4 {
let t = f64::from(k) / 4.0;
s.pointer(&ev(k, PointerKind::Move, (from.0 + (to.0 - from.0) * t, from.1 + (to.1 - from.1) * t))).unwrap();
}
s.pointer(&ev(5, PointerKind::Up, to)).unwrap();
}
fn layers(s: &Session) -> usize {
s.doc().unwrap().node(s.page().unwrap()).unwrap().children.len()
}
fn last_step(s: &Session) -> String {
s.ds().unwrap().history.undo.last().map(|e| e.label.clone()).unwrap_or_default()
}
#[test]
fn alt_pressed_during_a_drag_turns_the_move_into_a_copy() {
// #1385: as in other design apps, Alt counts whenever it goes down, not only at the press.
let mut s = Session::new();
let a = rect(&mut s, 0.0, 0.0);
// Alt from the third step: the original goes back, a copy lands where the drag ends and snaps
// to the original (2 px below its top edge).
drag_alt_when(&mut s, (50.0, 50.0), (250.0, 52.0), |k| k >= 3);
let copy = s.selection()[0];
assert_ne!(copy, a);
assert_eq!((at(&s, a), at(&s, copy)), ((0.0, 0.0), (200.0, 0.0)));
assert_eq!((layers(&s), last_step(&s).as_str()), (2, "Duplicate"));
assert!(s.undo().unwrap());
assert_eq!((layers(&s), at(&s, a)), (1, (0.0, 0.0)), "one undo step");
// Let go of Alt during the drag: no copy, the original moves (and no longer snaps to where it
// started, as only a copy does).
drag_alt_when(&mut s, (50.0, 50.0), (250.0, 52.0), |k| k < 3);
assert_eq!((layers(&s), s.selection(), at(&s, a)), (1, vec![a], (200.0, 2.0)));
assert_eq!(last_step(&s), "Move");
// Down and up again during one drag: a copy, as Alt is held at the end.
s.undo().unwrap();
drag_alt_when(&mut s, (50.0, 50.0), (250.0, 52.0), |k| k == 1 || k >= 4);
assert_eq!((layers(&s), at(&s, a)), (2, (0.0, 0.0)));
}
/// The reported path (#567): Cmd+D, drag the copy with the mouse, Cmd+D again. The other tests
/// here set the copy's position with `node.set` or Alt-drag it; dragging is how it is usually done,
/// and the step is measured from where the copy ended up, whichever way it got there.