M11.6: middle-button drag pans the view with any tool (no tool events, grabbing cursor while panning; a left press during the pan is ignored); headless canvas test

This commit is contained in:
bflatastic committed 2026-10-02 22:42:50 +07:00
1 parent cf775072be
commit 3f1f3383d3
2 files changed
+58 -8

No files matched your search

+2 -2
View File
@@ -28,7 +28,7 @@ _Last updated: 2026-10-02._
- **UI:** Illustrator 2026 layout restyled to measured values:
- Medium Dark theme, categorized and Advanced toolbars, 35 pt document tabs, 33 pt panel tabs.
- Hint bar, contextual task bar, 19 dock panels with ≡ menus.
- Native macOS menu bar, vector tool cursors, a ⌘K command palette.
- Native macOS menu bar, vector tool cursors, a ⌘K command palette, middle-button panning with any tool.
- Four brightness themes, persistent preferences.
- **Tools:**
- **Selection:** Selection, Direct/Group Selection, Magic Wand, Lasso.
@@ -68,7 +68,7 @@ _Last updated: 2026-10-02._
| M8 | Transform & distort (Puppet Warp, Liquify tools, Envelopes, Blends, Perspective Grid) | 🟡 live Blends, Envelopes (warp/mesh/top object), Width tool, Liquify tools, Puppet Warp and Perspective Grid landed; fidelity pass pending | 8–12 |
| M9 | Live effects (+ 3D & Materials) | 🟡 2D effects done incl. Effect → Pathfinder; SVG Filters, Document Raster Effects Settings, 3D pending | 86–135 |
| M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG `<pattern>`/PDF export) and live Repeat (radial/grid/mirror) done; brushes/symbols in progress | 23–37 |
| M11 | Artboards & views (artboard panel/tool done, Trim View; print tiling, multiple windows, presentation polish) | 🟡 | 20–33 |
| M11 | Artboards & views (artboard panel/tool done, Trim View, middle-button pan; print tiling, multiple windows, presentation polish) | 🟡 | 20–33 |
| M12 | Advanced color & art (CMYK/ICC, separations, Gradient Mesh, Live Paint, Image Trace, Graphs) | 🟡 Gradient Mesh, Live Paint, Image Trace (12 presets, 18 ms/1k² image), Recolor Artwork, colour management (ICC, soft proofing, separations preview), Graphs (all 9 tools, Graph Data/Type, regenerate in place) done; graph Design/Column/Marker designs pending | 6–10 |
| M13 | Automation (Actions ✅ record/playback, persisted; variables, scripting, batch) | 🟡 | 18–27 |
| M14 | 1.0 polish (preferences, shortcut editor, workspaces, accessibility, packaging for all OSes) | 🟡 Preferences, shortcut editor, workspaces done; accessibility, Windows/Linux packaging pending | 20–30 |
+56 -6
View File
@@ -63,9 +63,11 @@ impl Xf {
#[derive(Clone, Copy, Debug, PartialEq)]
enum Drag {
Tool,
/// Hand tool / Space drag, or a middle-button drag with any tool (`middle`).
Pan {
start: Pos2,
center: Point,
middle: bool,
},
ZoomBox {
start: Pos2,
@@ -291,8 +293,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
if resp.hovered() {
let m = ui.input(|i| i.modifiers);
let space = ui.input(|i| i.key_down(egui::Key::Space));
let cur = if space || app.session.tool_id() == "hand" {
if ui.input(|i| i.pointer.primary_down()) { egui::CursorIcon::Grabbing } else { egui::CursorIcon::Grab }
let panning = matches!(ui.data(|d| d.get_temp::<Drag>(drag_id())), Some(Drag::Pan { .. }));
let cur = if panning {
egui::CursorIcon::Grabbing
} else if space || app.session.tool_id() == "hand" {
egui::CursorIcon::Grab
} else if app.session.tool_id() == "zoom" {
if m.alt { egui::CursorIcon::ZoomOut } else { egui::CursorIcon::ZoomIn }
} else if let Some(p) = app.hover_doc {
@@ -368,12 +373,13 @@ fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect:
let tool = app.session.tool_id();
let pan_mode = space || tool == "hand";
if pointer.primary_pressed() && resp.hovered() {
let middle_pan = matches!(drag, Some(Drag::Pan { middle: true, .. }));
if pointer.primary_pressed() && resp.hovered() && !middle_pan {
ui.ctx().memory_mut(|mem| mem.stop_text_input());
app.ui.flyout = None;
let p = hover.unwrap_or(rect.center());
let d = if pan_mode {
Drag::Pan { start: p, center: v.center }
Drag::Pan { start: p, center: v.center, middle: false }
} else if tool == "zoom" {
Drag::ZoomBox { start: p }
} else if tool == "rotateView" {
@@ -392,11 +398,16 @@ fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect:
kind
};
ui.data_mut(|dd| dd.insert_temp(drag_id(), d));
} else if drag.is_none() && resp.hovered() && pointer.button_pressed(egui::PointerButton::Middle) {
// Middle-button drag pans the view whatever the tool.
let start = hover.unwrap_or(rect.center());
ui.data_mut(|dd| dd.insert_temp(drag_id(), Drag::Pan { start, center: v.center, middle: true }));
} else if let Some(d) = drag {
let p = pointer.interact_pos().unwrap_or(rect.center());
if pointer.primary_down() {
let held = if middle_pan { pointer.button_down(egui::PointerButton::Middle) } else { pointer.primary_down() };
if held {
match d {
Drag::Pan { start, center } => {
Drag::Pan { start, center, .. } => {
let d = xf.delta_to_doc(p - start);
if let Some(vm) = app.view_mut() {
vm.center = center - d;
@@ -1019,3 +1030,42 @@ fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
crate::menus::invoke(app, &c, json!({}));
}
}
#[cfg(test)]
mod tests {
use super::*;
use vectorcraft_engine::Session;
/// One headless canvas frame on an 800 × 600 window.
fn frame(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec<egui::Event>) {
let raw = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))), events, ..Default::default() };
let mut out = ctx.run_ui(raw, |ui| show(app, ui));
out.textures_delta.clear();
}
fn middle(pos: Pos2, pressed: bool) -> egui::Event {
egui::Event::PointerButton { pos, button: egui::PointerButton::Middle, pressed, modifiers: Default::default() }
}
#[test]
fn middle_drag_pans_the_view_with_any_tool() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
// A drawing tool: the middle button must pan, never draw.
app.select_tool("rectangle");
let ctx = egui::Context::default();
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(pos2(400.0, 300.0))]);
let before = *app.view().unwrap();
frame(&mut app, &ctx, vec![middle(pos2(400.0, 300.0), true)]);
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(pos2(450.0, 330.0))]);
frame(&mut app, &ctx, vec![middle(pos2(450.0, 330.0), false)]);
let after = *app.view().unwrap();
assert_eq!(after.zoom, before.zoom);
assert!((after.center.x - (before.center.x - 50.0 / before.zoom)).abs() < 1e-6);
assert!((after.center.y - (before.center.y - 30.0 / before.zoom)).abs() < 1e-6);
// Released: moving no longer pans, and nothing was drawn.
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(pos2(300.0, 200.0))]);
assert_eq!(app.view().unwrap().center, after.center);
assert_eq!(app.session.active().unwrap().doc.art_bounds(), None);
}
}