diff --git a/crates/tools/src/select.rs b/crates/tools/src/select.rs index d8c6ad12e..76a81d8f2 100644 --- a/crates/tools/src/select.rs +++ b/crates/tools/src/select.rs @@ -279,6 +279,8 @@ pub struct SelectionTool { guides: Vec, /// Selected item to toggle if this Shift press never becomes a drag. shift_release: Option, + /// Selected anchor point to take away if this Shift press never becomes a drag. + anchor_release: Option<(ItemId, usize, usize)>, /// The loaded text cursor. loaded: Option, /// Frame whose live corners are being edited (diamonds shown). @@ -287,7 +289,7 @@ pub struct SelectionTool { impl SelectionTool { pub fn new(direct: bool) -> Self { - Self { direct, drag: Drag::None, hover_handle: None, guides: vec![], shift_release: None, loaded: None, live: None } + Self { direct, drag: Drag::None, hover_handle: None, guides: vec![], shift_release: None, anchor_release: None, loaded: None, live: None } } /// [`bracket_at`] for this tool. With Direct Selection an anchor under the pointer wins: the @@ -811,7 +813,9 @@ impl Tool for SelectionTool { let mut points = cx.selection.anchors.clone(); let was = points.contains(&me); if ev.mods.shift && was { - points.retain(|a| *a != me); + // Kept selected so a Shift-drag can constrain it; a click still takes it + // away, on release, as for objects. + self.anchor_release = Some(me); } else if ev.mods.shift { points.push(me); } else if !was { @@ -820,10 +824,6 @@ impl Tool for SelectionTool { if points != cx.selection.anchors { out.push(Action::Exec("selection.set".into(), anchor_selection(cx, &points))); } - if ev.mods.shift && was { - self.drag = Drag::None; - return out; - } others = points.iter().filter(|a| **a != me).map(|(i, s, a)| (i.0, *s, *a)).collect(); } let label = if handle.is_some() { @@ -1165,6 +1165,9 @@ impl Tool for SelectionTool { } Drag::Anchor { id, si, ai, handle, start, at, others } => { let mut d = p - start; + if d.hypot() >= cx.tol(3.0) { + self.anchor_release = None; + } // Shift: horizontal, vertical or at 45°. if ev.mods.shift { d = constrain_45(d); @@ -1196,13 +1199,15 @@ impl Tool for SelectionTool { }, ); // dx and dy stay in canvas space. Snap.delta is spread space. - let corr = spread_delta_to_canvas(cx.layout.xf(spread), hit.delta); - if corr.x.is_finite() { - dx += corr.x; - } - if corr.y.is_finite() { - dy += corr.y; + let mut corr = spread_delta_to_canvas(cx.layout.xf(spread), hit.delta); + corr = Vec2::new(if corr.x.is_finite() { corr.x } else { 0.0 }, if corr.y.is_finite() { corr.y } else { 0.0 }); + // Shift: the snap moves the points along the constrained direction only. + if ev.mods.shift && d.hypot() > 1e-9 { + let dir = d / d.hypot(); + corr = dir * corr.dot(dir); } + dx += corr.x; + dy += corr.y; self.guides = hit.guides; } let mut params = json!({"id": id, "anchors": [[si, ai]], "dx": dx, "dy": dy}); @@ -1219,8 +1224,14 @@ impl Tool for SelectionTool { PointerKind::Up => { self.guides.clear(); let release = self.shift_release.take(); + let anchor_release = self.anchor_release.take(); let d = std::mem::replace(&mut self.drag, Drag::None); match d { + // A Shift-click on a selected point that never became a drag takes it away. + Drag::Anchor { .. } if anchor_release.is_some() => { + let points: Vec<_> = cx.selection.anchors.iter().copied().filter(|a| Some(*a) != anchor_release).collect(); + vec![Action::Commit, Action::Exec("selection.set".into(), anchor_selection(cx, &points))] + } Drag::Move { .. } | Drag::Resize { .. } | Drag::ContentResize { .. } diff --git a/crates/tools/src/tests.rs b/crates/tools/src/tests.rs index a8f98b7e1..10267224a 100644 --- a/crates/tools/src/tests.rs +++ b/crates/tools/src/tests.rs @@ -1980,9 +1980,19 @@ fn direct_selection_marquee_selects_points_of_several_frames_and_drags_them_toge // Shift keeps the move horizontal. assert_eq!((p["dx"].as_f64(), p["dy"].as_f64()), (Some(36.0), Some(0.0))); assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, at.x + 36.0, at.y)), vec![Action::Commit]); - // Shift+click on a selected point takes it out of the selection; the arrow keys move the points. + // A Shift-drag that starts on a selected point keeps it: all four move, constrained. + t.pointer(&cx, &PointerEvent::new(PointerKind::Down, at.x, at.y).with_mods(shift)); + let drag = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, at.x + 3.0, at.y + 30.0).with_mods(shift)); + let Some(Action::Preview(_, p)) = drag.last() else { panic!("{drag:?}") }; + assert_eq!(p["points"].as_array().map(Vec::len), Some(4), "{p}"); + assert!(p["dx"].as_f64().unwrap().abs() < 1e-9 && (p["dy"].as_f64().unwrap() - 30.0).abs() < 1e-9, "vertical: {p}"); + assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, at.x, at.y + 30.0).with_mods(shift)), vec![Action::Commit]); + // Shift+click on a selected point takes it out of the selection, on release; the arrow keys + // move the points. let out = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, at.x, at.y).with_mods(shift)); - let Some(Action::Exec(cmd, p)) = out.first() else { panic!("{out:?}") }; + assert!(!out.iter().any(|a| matches!(a, Action::Exec(..))), "kept while it may become a drag: {out:?}"); + let out = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, at.x, at.y).with_mods(shift)); + let Some(Action::Exec(cmd, p)) = out.last() else { panic!("{out:?}") }; assert_eq!(cmd, "selection.set"); assert_eq!(p["anchors"].as_array().map(Vec::len), Some(3), "{p}"); let keys = t.key(&cx, ToolKey::Right, Mods::default()); diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index d20327cee..6172a5948 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1279,6 +1279,8 @@ fn draw_selection(app: &DesignApp, painter: &egui::Painter, xf: &Xf, doc: &Docum } let direct = matches!(app.session.tool_id(), "directSelection" | "pen" | "addAnchor" | "deleteAnchor" | "convertDirection"); if direct { + // Looked up once per anchor drawn: a large marquee selection stays linear. + let chosen_set: std::collections::HashSet<(designcraft_doc::ItemId, usize, usize)> = sel.anchors.iter().copied().collect(); for id in &sel.items { let (Some(it), Some((a, _))) = (doc.item(*id), item_canvas_xf(doc, layout, *id)) else { continue }; let m = a * it.xf; @@ -1287,7 +1289,7 @@ fn draw_selection(app: &DesignApp, painter: &egui::Painter, xf: &Xf, doc: &Docum for (ai, an) in sp.anchors.iter().enumerate() { let p = xf.to_screen(m * an.p); // A selected anchor point is solid, the others hollow (InDesign). - let chosen = sel.anchors.contains(&(*id, si, ai)); + let chosen = chosen_set.contains(&(*id, si, ai)); for (h, has) in [(an.h_in, an.has_in()), (an.h_out, an.has_out())] { if has { let hp = xf.to_screen(m * h);