From 6fe71df1cfa016ccf8438e3e7e89bdb6dfab07e5 Mon Sep 17 00:00:00 2001 From: Don Omondi <282319807+mockydevs@users.noreply.github.com> Date: Fri, 30 Oct 2026 00:32:16 +0300 Subject: [PATCH] Fix #4233: Direct Selection drags art onto Asset Export and the other panels Art dragged off the canvas reached the panels (Asset Export, Graphic Styles, Swatches, Symbols) only from the Selection tool: drag_art_out checked for that tool by id. With the Direct Selection or Group Selection tool the move stayed on the canvas, so nothing dropped. Those tools now hand over a move too when every selected object is selected whole (a press on its fill). A drag of anchor points or segments, which they also call Move, stays a reshape and isn't handed to the panels. --- crates/ui-egui/src/canvas/drag.rs | 10 ++++- crates/ui-egui/src/canvas/tests.rs | 65 ++++++++++++++++++++++++++++++ docs/ui-parity.md | 1 + 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/canvas/drag.rs b/crates/ui-egui/src/canvas/drag.rs index bfc3a070f..c35effb6f 100644 --- a/crates/ui-egui/src/canvas/drag.rs +++ b/crates/ui-egui/src/canvas/drag.rs @@ -172,12 +172,18 @@ pub(super) fn drop_press(app: &mut VectorcraftApp, ui: &Ui, view: vectorcraft_en /// the selected art ([`widgets::PanelDrag::Art`]): the art waits where it last moved on the /// canvas, and a panel it is released on takes it (the Graphic Styles panel makes a style of it). /// Back on the canvas, [`Drag::Art`] takes the move up again where the pointer is (#2826). +/// The Direct and Group Selection tools hand over art they move whole too (#4233); a move of +/// anchor points or segments stays a reshape. pub(super) fn drag_art_out(app: &VectorcraftApp, ui: &Ui, resp: &egui::Response, p: Pos2) -> bool { - let Some(st) = app.session.active().filter(|_| app.session.tool_id() == "selection") else { return false }; - // The Selection tool's interaction while it moves (or Alt-copies) the selection. + let tool = app.session.tool_id(); + let Some(st) = app.session.active().filter(|_| matches!(tool, "selection" | "directSelection" | "groupSelection")) else { return false }; + // The tool's interaction while it moves (or Alt-copies) the selection. if !st.interaction.as_ref().is_some_and(|i| i.label == "Move" || i.label == "Copy") { return false; } + if tool != "selection" && st.selection.objects.iter().any(|id| st.selection.partial(*id).is_some() || st.selection.segments.contains_key(id)) { + return false; + } // A floating panel in an OS window of its own takes the art too: craft-ui hands that window // the pointer while it is over it, and this window sees the pointer go. craft_ui::docking::route_app_drag(ui.ctx(), crate::panel_docking::area_id()); diff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs index 6a30f248f..110e3bd5c 100644 --- a/crates/ui-egui/src/canvas/tests.rs +++ b/crates/ui-egui/src/canvas/tests.rs @@ -5298,6 +5298,71 @@ fn art_moved_off_the_canvas_waits_and_drops_on_the_panel() { assert_eq!(st.history.undo.len(), undo); } +/// #4233: the Direct and Group Selection tools hand art they move whole to the panels too (Asset +/// Export, Graphic Styles…), as the Selection tool does; the move waits and nothing is recorded. +#[test] +fn art_moved_whole_off_the_canvas_with_direct_selection_drops_on_the_panel() { + for tool in ["directSelection", "groupSelection"] { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + let id = app.session.execute("shape.rectangle", &json!({"x": 50, "y": 50, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap(); + let id = vectorcraft_doc::NodeId(id); + app.session.execute("select.none", &json!({})).unwrap(); + app.select_tool(tool); + let ctx = egui::Context::default(); + frame(&mut app, &ctx, vec![]); + let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()); + let before = app.session.active().unwrap().doc.clone(); + let undo = app.session.active().unwrap().history.undo.len(); + let button = |pos: Pos2, pressed: bool| egui::Event::PointerButton { + pos, + button: egui::PointerButton::Primary, + pressed, + modifiers: Default::default(), + }; + // Pressed on the fill: the rectangle is selected whole. + let start = xf.to_screen(Point::new(100.0, 100.0)); + frame(&mut app, &ctx, vec![egui::Event::PointerMoved(start), button(start, true)]); + assert_eq!(app.session.active().unwrap().selection.whole(), [id], "{tool}: selected whole"); + frame(&mut app, &ctx, vec![egui::Event::PointerMoved(start + vec2(40.0, 0.0))]); + assert!(app.session.active().unwrap().interaction.is_some(), "{tool}: the move is live"); + frame(&mut app, &ctx, vec![egui::Event::PointerMoved(pos2(850.0, 100.0))]); + assert!(app.session.active().unwrap().interaction.is_some(), "{tool}: the move waits, not cancels"); + assert_eq!(egui::DragAndDrop::payload::(&ctx).as_deref(), Some(&widgets::PanelDrag::Art(vec![id])), "{tool}"); + frame(&mut app, &ctx, vec![button(pos2(850.0, 100.0), false)]); + let st = app.session.active().unwrap(); + assert!(st.interaction.is_none(), "{tool}"); + assert_eq!(*st.doc, *before, "{tool}: the move goes back"); + assert_eq!(st.history.undo.len(), undo, "{tool}: nothing recorded"); + } +} + +/// An anchor point the Direct Selection tool drags off the canvas isn't art for the panels: the +/// reshape goes on and no art drag starts (#4233). +#[test] +fn an_anchor_dragged_off_the_canvas_is_not_handed_to_the_panels() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + let id = app.session.execute("shape.rectangle", &json!({"x": 50, "y": 50, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap(); + app.select_tool("directSelection"); + app.session.execute("select.anchors", &json!({"id": id, "anchors": [[0, 0]]})).unwrap(); + let ctx = egui::Context::default(); + frame(&mut app, &ctx, vec![]); + let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()); + let button = + |pos: Pos2, pressed: bool| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; + let corner = xf.to_screen(Point::new(50.0, 50.0)); + frame(&mut app, &ctx, vec![egui::Event::PointerMoved(corner), button(corner, true)]); + frame(&mut app, &ctx, vec![egui::Event::PointerMoved(corner + vec2(20.0, 0.0))]); + assert!(app.session.active().unwrap().selection.partial(vectorcraft_doc::NodeId(id)).is_some(), "an anchor is picked"); + frame(&mut app, &ctx, vec![egui::Event::PointerMoved(pos2(850.0, 100.0))]); + assert!( + !matches!(egui::DragAndDrop::payload::(&ctx).as_deref(), Some(widgets::PanelDrag::Art(_))), + "no art drag for an anchor" + ); + frame(&mut app, &ctx, vec![button(pos2(850.0, 100.0), false)]); +} + /// #2826: a move taken over a panel waits where it left the canvas, and goes on with the /// pointer when it comes back — it isn't dropped or put back. #[test] diff --git a/docs/ui-parity.md b/docs/ui-parity.md index ae4795c2a..4c615393a 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -363,6 +363,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development |---|---|---| | 2026-10-30 | minor | Character panel menu › Show Snap to Glyph Options shows the Snap to Glyph section (Baseline, x-height, Glyph Bounds, Proximity Guides, Angular Guides, Anchor Point; `view.snapToGlyph.options`) (#4232); scores and estimates unchanged | | 2026-10-30 | minor | Align panel menu › Align to Glyph Bounds › Point Text and Area Text align and distribute type by the ink of its glyphs (#4231); the Character and Properties panels' controls for it are not done; scores and estimates unchanged | +| 2026-10-30 | minor | Art moved whole with the Direct Selection or Group Selection tool and dragged off the canvas goes to the panels (Asset Export, Graphic Styles, Swatches, Symbols) as with the Selection tool; anchor points and segments dragged off stay a reshape (#4233). Tests in `crates/ui-egui/src/canvas/tests.rs`; scores and estimates unchanged | | 2026-10-30 | minor | Blob Brush and Eraser Tool Options: Size follows the pen pressure, Fixed or Pressure with a Variation (#4590, #3961); scores and estimates unchanged | | 2026-10-30 | minor | Blob Brush Tool Options: Size follows the pen pressure, Fixed or Pressure with a Variation (#4590); scores and estimates unchanged | | 2026-10-29 | minor | Pathfinder: live compound shapes (#4522): the panel menu's Make, Release and Expand Compound Shape, Alt-clicking a Shape Mode, a selected member's Shape Mode and the Expand button act; scores and estimates unchanged |