mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
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:
1 parent
cf775072be
commit
3f1f3383d3
2 files changed
+58
-8
No files matched your search
+2
-2
@@ -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 |
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user