mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 04:37:29 -06:00
Merge pull request #4485 from el-j/fix/artboard-multiselect-4484
fix(canvas): allow multi-selecting artboards via Shift-click on canvas label (#4484)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -713,11 +713,11 @@ fn reproject_frame(app: &VectorcraftApp, painter: &egui::Painter, xf: &Xf, rect:
|
||||
fn artboard_outlines(app: &VectorcraftApp, ui: &Ui, painter: &egui::Painter, xf: &Xf, t: &Tokens) {
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let doc = &st.doc;
|
||||
// Artboard edges and names. The Artboard tool labels its active artboard itself (following
|
||||
// a move or resize): that one isn't named twice.
|
||||
// Artboard edges and names. The Artboard tool labels its selected artboards itself (following
|
||||
// a move or resize): those aren'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 = artboards::tool_labelled(app);
|
||||
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 +729,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));
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use super::{Color32, Point, Pos2, Rect, Shape, Stroke, Ui, VectorcraftApp, Xf, json, vec2};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{Color32, Point, Pos2, Rect, Shape, Stroke, Ui, VectorcraftApp, Xf, vec2};
|
||||
|
||||
/// 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.
|
||||
@@ -89,6 +91,15 @@ pub(super) fn tool_name_rect(painter: &egui::Painter, xf: &Xf, rect: Rect, index
|
||||
egui::Rect::from_min_size(sp, galley.size()).expand(2.0)
|
||||
}
|
||||
|
||||
/// The artboards the Artboard tool labels itself, following a move or resize (its selected
|
||||
/// ones): the canvas doesn't name them again. Empty with another tool.
|
||||
pub(super) fn tool_labelled(app: &VectorcraftApp) -> Vec<u64> {
|
||||
if app.session.tool_id() != "artboard" {
|
||||
return vec![];
|
||||
}
|
||||
app.session.tool_options()["selected"].as_array().map(|a| a.iter().filter_map(Value::as_u64).collect()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The artboard whose on-canvas name contains `screen`, if that name is being drawn.
|
||||
pub(super) fn artboard_name_at(app: &VectorcraftApp, ui: &Ui, xf: &Xf, screen: Pos2) -> Option<usize> {
|
||||
let st = app.session.active()?;
|
||||
@@ -96,11 +107,12 @@ 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 = tool_labelled(app);
|
||||
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 +122,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 +142,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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,10 +212,10 @@ pub(super) fn artboard_name_editor(app: &mut VectorcraftApp, ui: &mut Ui, xf: &X
|
||||
pub(super) fn artboard_name_field_rect(app: &VectorcraftApp, ui: &Ui, xf: &Xf, index: usize) -> Option<egui::Rect> {
|
||||
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 = tool_labelled(app);
|
||||
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));
|
||||
|
||||
@@ -91,7 +91,15 @@ 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));
|
||||
let press_shift = ui
|
||||
.input(|inp| {
|
||||
inp.events.iter().rev().find_map(|event| match event {
|
||||
egui::Event::PointerButton { button: egui::PointerButton::Primary, pressed: true, modifiers, .. } => Some(modifiers.shift),
|
||||
_ => None,
|
||||
})
|
||||
})
|
||||
.unwrap_or(m.shift);
|
||||
ui.data_mut(|dd| dd.insert_temp(name_press_id(), (i, press_shift)));
|
||||
} else {
|
||||
begin_drag(app, ui, &f, p);
|
||||
}
|
||||
@@ -134,9 +142,17 @@ 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);
|
||||
let release_shift = ui
|
||||
.input(|inp| {
|
||||
inp.events.iter().rev().find_map(|event| match event {
|
||||
egui::Event::PointerButton { button: egui::PointerButton::Primary, pressed: false, modifiers, .. } => Some(modifiers.shift),
|
||||
_ => None,
|
||||
})
|
||||
})
|
||||
.unwrap_or(m.shift);
|
||||
select_named_artboard(app, i, shift || release_shift);
|
||||
}
|
||||
if resp.double_clicked()
|
||||
&& let Some(p) = at
|
||||
|
||||
@@ -705,6 +705,57 @@ 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 (Shift held on press, released before mouse release)
|
||||
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(egui::Modifiers::NONE),
|
||||
egui::Event::PointerButton { pos: at1, button: egui::PointerButton::Primary, pressed: false, modifiers: egui::Modifiers::NONE },
|
||||
],
|
||||
);
|
||||
|
||||
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]
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user