fix(canvas): allow multi-selecting artboards via Shift-click on canvas label (#4484)

This commit is contained in:
el-j
2026-10-30 00:15:54 +07:00
committed by bflatastic
parent 401706ff8b
commit 49a05508ab
6 changed files with 95 additions and 23 deletions
+9 -4
View File
@@ -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);
+8 -3
View File
@@ -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<u64> = 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::<ArtboardNameEdit>(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));
+30 -12
View File
@@ -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<u64> = 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<u64> = 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));
+3 -3
View File
@@ -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::<usize>(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
+44
View File
@@ -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]
+1 -1
View File
@@ -308,7 +308,7 @@ fn select_range(app: &mut VectorcraftApp, anchor: Option<usize>, 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 => {