diff --git a/crates/tools/src/xform/artboard.rs b/crates/tools/src/xform/artboard.rs index ecdb79d43..09933d2a4 100644 --- a/crates/tools/src/xform/artboard.rs +++ b/crates/tools/src/xform/artboard.rs @@ -426,7 +426,7 @@ impl Tool for ArtboardTool { vec![Action::Commit] } Drag::Resize { began: true, .. } => vec![Action::Commit], - Drag::Create { toggle: Some((i, at)), .. } if p.distance(at) < cx.tol(3.0) => { + Drag::Create { toggle: Some((i, at)), .. } if p.distance(at) < cx.tol(5.0) => { self.toggle(cx, i); vec![] } @@ -562,9 +562,14 @@ impl Tool for ArtboardTool { if let Some(Drag::Marquee { start, cur }) = self.drag { o.push(Overlay::Marquee(Rect::from_points(start, cur))); } - // The other selected artboards: dashed bounds (in a drag the document shows them moved). - for r in self.others.iter().filter_map(|i| cx.doc.artboards.get(*i)).map(|a| a.rect) { - o.push(Overlay::Path { path: polygon(&rect_corners(r), true), color: BLUE, width: 1.0, dashed: true }); + // The other selected artboards: dashed bounds and labels. + for &i in &self.others { + if let Some(ab) = cx.doc.artboards.get(i) { + let r = ab.rect; + o.push(Overlay::Path { path: polygon(&rect_corners(r), true), color: BLUE, width: 1.0, dashed: true }); + let at = Point::new(r.x0 - cx.tol(8.0), r.y0 - cx.tol(4.0)); + o.push(Overlay::Label { p: at, text: format!("{:02} - {}", i + 1, ab.name), color: BLUE }); + } } let Some(ab) = cx.doc.artboards.get(self.active) else { return o }; let r = self.preview.unwrap_or(ab.rect); diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 7f2856f48..610771608 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -3,7 +3,7 @@ use egui::emath::GuiRounding as _; use egui::{Color32, CornerRadius, Pos2, Sense, Shape, Stroke, StrokeKind, Ui, Vec2, pos2, vec2}; -use serde_json::json; +use serde_json::{Value, json}; use vectorcraft_doc::text::TextKind; use vectorcraft_doc::{Node, NodeKind, SymbolBoxes, Unit}; use vectorcraft_geom::{Affine, BezPath, PathEl, Point, Rect}; @@ -717,7 +717,11 @@ fn artboard_outlines(app: &VectorcraftApp, ui: &Ui, painter: &egui::Painter, xf: // a move or resize): that one isn't named twice. let active_ab = crate::panels::artboards::selected(app, doc.artboards.len()); let artboard_tool = app.session.tool_id() == "artboard"; - let tool_labelled = artboard_tool.then(|| app.session.tool_options()["active"].as_u64()).flatten(); + let tool_selected: Vec = if artboard_tool { + app.session.tool_options()["selected"].as_array().map(|a| a.iter().filter_map(Value::as_u64).collect()).unwrap_or_default() + } else { + vec![] + }; let editing_name = ui.data(|d| d.get_temp::(name_edit_id()).map(|e| e.index)); for (i, ab) in doc.artboards.iter().enumerate() { let c = if i == active_ab { Color32::from_gray(0) } else { Color32::from_gray(120) }; @@ -729,7 +733,8 @@ fn artboard_outlines(app: &VectorcraftApp, ui: &Ui, painter: &egui::Painter, xf: // Show Center Mark, Show Cross Hairs and Show Video Safe Areas (Artboard Options). Only the // canvas draws them, as it does the edges: exports and print leave them out. artboard_marks(painter, xf, ab, t.artboard_mark); - if editing_name != Some(i) && (artboard_tool || doc.artboards.len() > 1) && tool_labelled != u64::try_from(i).ok() { + let labelled_by_tool = u64::try_from(i).ok().is_some_and(|idx| tool_selected.contains(&idx)); + if editing_name != Some(i) && (artboard_tool || doc.artboards.len() > 1) && !labelled_by_tool { // At the artboard's own top-left corner, which a rotated view turns away from its // bounding box's (#1095). let corner = xf.to_screen(Point::new(ab.rect.x0, ab.rect.y0)); diff --git a/crates/ui-egui/src/canvas/artboards.rs b/crates/ui-egui/src/canvas/artboards.rs index 4f803ef0c..a824868be 100644 --- a/crates/ui-egui/src/canvas/artboards.rs +++ b/crates/ui-egui/src/canvas/artboards.rs @@ -1,4 +1,6 @@ -use super::{Color32, Point, Pos2, Rect, Shape, Stroke, Ui, VectorcraftApp, Xf, json, vec2}; +use serde_json::{Value, json}; + +use super::*; /// The width an artboard needs on screen for its name to show (px): narrower, the name is left /// out rather than spilling over its neighbours. @@ -96,11 +98,16 @@ pub(super) fn artboard_name_at(app: &VectorcraftApp, ui: &Ui, xf: &Xf, screen: P if !artboard_tool && st.doc.artboards.len() <= 1 { return None; } - let tool_labelled = artboard_tool.then(|| app.session.tool_options()["active"].as_u64()).flatten(); + let tool_selected: Vec = if artboard_tool { + app.session.tool_options()["selected"].as_array().map(|a| a.iter().filter_map(Value::as_u64).collect()).unwrap_or_default() + } else { + vec![] + }; let painter = ui.painter(); let mut hit = None; for (i, ab) in st.doc.artboards.iter().enumerate() { - if tool_labelled == u64::try_from(i).ok() { + let is_tool_labelled = u64::try_from(i).ok().is_some_and(|idx| tool_selected.contains(&idx)); + if is_tool_labelled { continue; } let corner = xf.to_screen(Point::new(ab.rect.x0, ab.rect.y0)); @@ -110,16 +117,18 @@ pub(super) fn artboard_name_at(app: &VectorcraftApp, ui: &Ui, xf: &Xf, screen: P hit = Some(i); } } - if let Some(i) = tool_labelled.and_then(|i| usize::try_from(i).ok()) - && let Some(ab) = st.doc.artboards.get(i) - && tool_name_rect(painter, xf, ab.rect, i, &ab.name).contains(screen) - { - return Some(i); + for &idx in &tool_selected { + if let Some(i) = usize::try_from(idx).ok() + && let Some(ab) = st.doc.artboards.get(i) + && tool_name_rect(painter, xf, ab.rect, i, &ab.name).contains(screen) + { + return Some(i); + } } hit } -pub(super) fn select_named_artboard(app: &mut VectorcraftApp, index: usize) { +pub(super) fn select_named_artboard(app: &mut VectorcraftApp, index: usize, shift: bool) { let n = app.session.active().map(|st| st.doc.artboards.len()).unwrap_or(0); if index >= n { return; @@ -128,7 +137,11 @@ pub(super) fn select_named_artboard(app: &mut VectorcraftApp, index: usize) { v.artboard = index; } if app.session.tool_id() == "artboard" { - app.session.set_tool_option("active", &json!(index)); + if shift { + crate::panels::artboards::toggle(app, index, n); + } else { + app.session.set_tool_option("active", &json!(index)); + } } } @@ -195,9 +208,14 @@ pub(super) fn artboard_name_field_rect(app: &VectorcraftApp, ui: &Ui, xf: &Xf, i let st = app.session.active()?; let ab = st.doc.artboards.get(index)?; let artboard_tool = app.session.tool_id() == "artboard"; - let tool_labelled = artboard_tool.then(|| app.session.tool_options()["active"].as_u64()).flatten(); + let tool_selected: Vec = if artboard_tool { + app.session.tool_options()["selected"].as_array().map(|a| a.iter().filter_map(Value::as_u64).collect()).unwrap_or_default() + } else { + vec![] + }; let painter = ui.painter(); - let rect = if tool_labelled == u64::try_from(index).ok() { + let is_tool_labelled = u64::try_from(index).ok().is_some_and(|idx| tool_selected.contains(&idx)); + let rect = if is_tool_labelled { tool_name_rect(painter, xf, ab.rect, index, &ab.name) } else { let corner = xf.to_screen(Point::new(ab.rect.x0, ab.rect.y0)); diff --git a/crates/ui-egui/src/canvas/input.rs b/crates/ui-egui/src/canvas/input.rs index 6e362f9b4..1bf577fd7 100644 --- a/crates/ui-egui/src/canvas/input.rs +++ b/crates/ui-egui/src/canvas/input.rs @@ -91,7 +91,7 @@ pub(super) fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Respo // The artboard's name sits above its corner. A press there renames on a double-click // and does not start a tool gesture (a fitted artboard, a new one, Artboard Options). if let Some(i) = artboard_name_at(app, ui, &xf, p) { - ui.data_mut(|dd| dd.insert_temp(name_press_id(), i)); + ui.data_mut(|dd| dd.insert_temp(name_press_id(), (i, m.shift))); } else { begin_drag(app, ui, &f, p); } @@ -134,9 +134,9 @@ pub(super) fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Respo ui.ctx().request_repaint_after(std::time::Duration::from_millis(16)); } if pointer.primary_released() - && let Some(i) = ui.data_mut(|d| d.remove_temp::(name_press_id())) + && let Some((i, shift)) = ui.data_mut(|d| d.remove_temp::<(usize, bool)>(name_press_id())) { - select_named_artboard(app, i); + select_named_artboard(app, i, shift || m.shift); } if resp.double_clicked() && let Some(p) = at diff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs index 4616338c1..e8e5b3dac 100644 --- a/crates/ui-egui/src/canvas/tests.rs +++ b/crates/ui-egui/src/canvas/tests.rs @@ -705,6 +705,50 @@ fn the_artboard_tool_names_its_active_artboard_once() { assert!((active[0].1 - other[0].1).abs() < 2.0, "on the artboard's top edge like the other name: {active:?} vs {other:?}"); } +#[test] +fn shift_clicking_canvas_artboard_label_toggles_multi_selection() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 200, "height": 100, "artboards": 3})).unwrap(); + app.select_tool("artboard"); + if let Some(v) = app.view_mut() { + v.center = Point::new(320.0, 50.0); + v.zoom = 0.8; + v.fitted = true; + } + let ctx = egui::Context::default(); + frame(&mut app, &ctx, vec![]); + assert_eq!(app.session.tool_options()["selected"], json!([0])); + + let drawn = shapes(&mut app, &ctx); + let label_pos = |name: &str| { + drawn + .iter() + .find_map(|s| match s { + Shape::Text(t) if t.galley.text() == name => Some(t.pos + vec2(10.0, 5.0)), + _ => None, + }) + .unwrap() + }; + let at1 = label_pos("02 - Artboard 2"); + + // Shift-click artboard 2's name label + let shift = egui::Modifiers::SHIFT; + let button = |pressed| egui::Event::PointerButton { pos: at1, button: egui::PointerButton::Primary, pressed, modifiers: shift }; + frame(&mut app, &ctx, vec![egui::Event::ModifiersChanged(shift), egui::Event::PointerMoved(at1), button(true)]); + frame(&mut app, &ctx, vec![egui::Event::ModifiersChanged(shift), button(false)]); + + assert_eq!(app.session.tool_options()["selected"], json!([1, 0]), "Shift-clicking artboard label adds it to selection"); + + // Click artboard 3's name label without Shift + let at2 = label_pos("03 - Artboard 3"); + let plain_btn = + |pressed| egui::Event::PointerButton { pos: at2, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE }; + frame(&mut app, &ctx, vec![egui::Event::ModifiersChanged(egui::Modifiers::NONE), egui::Event::PointerMoved(at2), plain_btn(true)]); + frame(&mut app, &ctx, vec![plain_btn(false)]); + + assert_eq!(app.session.tool_options()["selected"], json!([2]), "Plain click selects that artboard alone"); +} + /// Enable Touch Gestures (#585): a two-finger tap undoes and a three-finger one redoes, and the /// first finger's press (the pointer egui makes of it) draws nothing. Off, a tap does nothing. #[test] diff --git a/crates/ui-egui/src/panels/artboards.rs b/crates/ui-egui/src/panels/artboards.rs index 741365922..695f63fd6 100644 --- a/crates/ui-egui/src/panels/artboards.rs +++ b/crates/ui-egui/src/panels/artboards.rs @@ -308,7 +308,7 @@ fn select_range(app: &mut VectorcraftApp, anchor: Option, i: usize, n: us /// Ctrl/Cmd-click on artboard `i`'s row while the Artboard tool is in use: adds its artboard to the /// tool's selection and makes it the active one, or takes it out and makes the lowest-numbered /// remaining artboard active. The only selected artboard stays selected. -fn toggle(app: &mut VectorcraftApp, i: usize, n: usize) { +pub(crate) fn toggle(app: &mut VectorcraftApp, i: usize, n: usize) { let mut all = picked(app, n); match all.iter().position(|j| *j == i) { Some(at) if all.len() > 1 => {