Press-and-drag selection windows (PICKDRAG = 2) (#63)

Selection windows only worked click-then-click: the canvas reacts to clicks,
and a press that moves before release is a drag, not a click, so a drag was
ignored entirely.

Now, while objects are being selected (no command running, or a command
asking for objects), a drag opens the window where the button went down and
letting go closes it, as with AutoCAD's default PICKDRAG = 2. Dragging right
is a window, dragging left a crossing window, Shift still removes, and
click-click works as before. Drags that start on a grip or while a command
asks for a point are left alone.

Session::begin_window opens the window at the drag's start, even over an
object (a click there would pick the object instead), with an engine test.


Claude-Session: https://claude.ai/code/session_019uMCkRAB8DMd8MEmoY9Vyr

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
by Truitt 2026-10-10 12:15:20 -05:00 committed by GitHub
parent 1dfa3660a7
commit d87950afe8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 71 additions and 0 deletions

View File

@ -863,6 +863,19 @@ impl Session {
// ---------------- picking without a command ----------------
/// Open a selection window with one corner at `corner`, for press-and-drag selection
/// (AutoCAD's PICKDRAG = 2: a drag opens the window wherever it starts, even over an object;
/// the next point, where the drag ends, closes it). Only while objects are being selected: no
/// command running, or a command asking for objects. Returns whether a window was opened.
pub fn begin_window(&mut self, corner: Vec2) -> bool {
let during_command = self.running.is_some();
if !corner.is_finite() || (during_command && !self.current_prompt().is_some_and(|p| p.accept.select)) {
return false;
}
self.pending_window = Some(PendingWindow { corner, during_command });
true
}
/// A click with no command running: pick/toggle objects or start a selection window.
///
/// PICKADD on (the default): picks add to the selection and Shift removes. PICKADD off: each pick

View File

@ -994,3 +994,34 @@ fn pline_width_answers_taper_the_next_segment() {
let p = typed_pline("pline 0,0 10,0 w 1 0.2 20,0 u");
assert_eq!(widths(&p), (0.0, vec![(0.0, 0.0), (0.0, 0.0)]));
}
#[test]
fn press_and_drag_selection_window() {
let mut s = Session::new();
s.cmdline("line 0,0 10,0").unwrap();
s.cmdline("").unwrap();
s.cmdline("circle 50,50 1").unwrap();
// No command: a drag from (-1,-1) to (11,1) is a window around the line only.
assert!(s.begin_window(Vec2::new(-1.0, -1.0)));
s.idle_click(Vec2::new(11.0, 1.0), false).unwrap();
assert_eq!(s.selection().len(), 1);
assert!(s.pending_window.is_none());
// Dragging right to left is a crossing window: it also catches the circle it touches.
s.set_selection(Vec::new());
assert!(s.begin_window(Vec2::new(60.0, 60.0)));
s.idle_click(Vec2::new(50.0, 40.0), false).unwrap();
assert_eq!(s.selection().len(), 1);
// A command asking for objects: the window feeds its selection.
s.set_selection(Vec::new());
s.cmdline("erase").unwrap();
assert!(s.begin_window(Vec2::new(-1.0, -1.0)));
s.input(Input::Point(Vec2::new(11.0, 1.0))).unwrap();
s.input(Input::Enter).unwrap();
assert_eq!(kinds(&s), vec!["Circle"]);
// A drawing command never opens one, and neither does a hostile point.
s.cmdline("line").unwrap();
assert!(!s.begin_window(Vec2::ZERO));
s.cancel();
assert!(!s.begin_window(Vec2::new(f64::NAN, 0.0)));
assert!(s.pending_window.is_none());
}

View File

@ -112,6 +112,8 @@ pub struct CanvasState {
hover_at: Option<Pos2>,
/// Constraint glyphs for the parametric overlay (cached per drawing revision).
pub param: crate::parametric::Cache,
/// A selection window opened by a press-and-drag, to close where the button is let go.
drag_window: bool,
}
/// Primitives of `list` sorted by entity handle, built on the first highlight after each list
@ -648,6 +650,31 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
app.session.echo(e.to_string());
}
}
// Press-and-drag selection window (AutoCAD's PICKDRAG = 2; click-click still works): a drag
// that starts while objects are being selected opens a window where the button went down,
// and letting go closes it. Drags on grips and drags while drawing are left alone.
if resp.drag_started_by(egui::PointerButton::Primary)
&& !middle_down
&& app.canvas.hot_grip.is_none()
&& app.session.pending_window.is_none()
&& let Some(origin) = ui.input(|i| i.pointer.press_origin())
&& rect.contains(origin)
&& grip_at(app, &xf, Some(origin)).is_none()
{
app.canvas.drag_window = app.session.begin_window(xf.to_world(origin));
}
if app.canvas.drag_window && resp.drag_stopped_by(egui::PointerButton::Primary) {
app.canvas.drag_window = false;
if app.session.pending_window.is_some()
&& let Some(end) = ui.input(|i| i.pointer.interact_pos())
{
let end = xf.to_world(end);
let done = if app.session.running.is_some() { app.session.input(Input::Point(end)) } else { app.session.idle_click(end, mods.shift) };
if let Err(e) = done {
app.session.echo(e.to_string());
}
}
}
if inside && pressed_secondary {
// Right-click acts as Enter while a command runs (the classic CAD default).
if app.session.running.is_some() {