From 980cc4fe442e2f350a973a3a2cfbebe9b491f7e2 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 10 Oct 2026 12:55:21 +0700 Subject: [PATCH] Fix #901: Shift-click a target circle in the Layers panel to add or remove it Clicking an object's target circle in the Layers panel always replaced the selection, so several objects couldn't be picked there. Shift-click now adds the object (a layer: its art) to the selection, or takes it out when it is selected already, as in Illustrator; nothing stays targeted then. layer.target takes add: true for it. Co-Authored-By: Claude Opus 5.5 --- crates/doc/src/selection.rs | 14 ++++++++++++++ crates/engine/src/cmd/layer.rs | 8 ++++++-- crates/ui-egui/src/panels/layers.rs | 9 ++++++++- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/crates/doc/src/selection.rs b/crates/doc/src/selection.rs index 6400c3ad..a64b17aa 100644 --- a/crates/doc/src/selection.rs +++ b/crates/doc/src/selection.rs @@ -128,6 +128,20 @@ impl Selection { } self.target = Some(id); } + /// Shift-clicking `id`'s target circle: it (a layer: its art) joins the selection, or leaves it + /// when it is all selected already. Several objects are then selected, none targeted. + pub fn toggle_target(&mut self, doc: &Document, id: NodeId) { + let ids = match doc.node(id) { + Some(n) if n.is_layer() => n.layer_art(true), + _ => vec![id], + }; + if ids.iter().all(|i| self.contains(*i)) { + ids.iter().for_each(|i| self.remove(*i)); + } else { + ids.into_iter().for_each(|i| self.add(i)); + } + self.target = None; + } /// What appearance and transparency edits act on: the targeted object, group or layer, else /// the selected objects. pub fn subjects(&self) -> &[NodeId] { diff --git a/crates/engine/src/cmd/layer.rs b/crates/engine/src/cmd/layer.rs index 2bae10ab..2d69434a 100644 --- a/crates/engine/src/cmd/layer.rs +++ b/crates/engine/src/cmd/layer.rs @@ -163,7 +163,7 @@ pub fn specs() -> Vec { "Target", [], None, - "{id} target a layer, group or object as clicking its target circle in the Layers panel does: a layer gets its visible, unlocked art (its sublayers' too) selected and becomes the current layer, and appearance.*, effect.*, transparency.* and the opacity-mask commands without `ids` then act on the layer itself; anything else is selected. Any other selection change ends the targeting (`document.inspect` → target) → {id, selected: [..]}", + "{id, add?: bool (Shift-click: the item joins the selection, or leaves it when all selected; nothing is targeted)} target a layer, group or object as clicking its target circle in the Layers panel does: a layer gets its visible, unlocked art (its sublayers' too) selected and becomes the current layer, and appearance.*, effect.*, transparency.* and the opacity-mask commands without `ids` then act on the layer itself; anything else is selected. Any other selection change ends the targeting (`document.inspect` → target) → {id, selected: [..]}", has_doc, target ), @@ -362,7 +362,11 @@ fn target(s: &mut Session, p: &Value) -> Result { } let layer = n.is_layer(); let rows = st.layer_rows.clone(); - s.select(|d, sel| sel.set_target(d, id))?; + if p.get("add").and_then(Value::as_bool).unwrap_or(false) { + s.select(|d, sel| sel.toggle_target(d, id))?; + } else { + s.select(|d, sel| sel.set_target(d, id))?; + } let st = s.doc_mut()?; // Targeting doesn't change which rows are highlighted. st.layer_rows = rows; diff --git a/crates/ui-egui/src/panels/layers.rs b/crates/ui-egui/src/panels/layers.rs index 51a30339..87c6a0fc 100644 --- a/crates/ui-egui/src/panels/layers.rs +++ b/crates/ui-egui/src/panels/layers.rs @@ -664,7 +664,7 @@ fn row(ui: &mut Ui, view: &View, item: &Row, out: &mut Out) { } let m = ui.input(|i| i.modifiers); if tresp.clicked() { - out.actions.push(("layer.target".into(), json!({"id": n.id.0}))); + out.actions.push(("layer.target".into(), json!({"id": n.id.0, "add": m.shift}))); } else if sq_resp.clicked() { out.actions.push(("layer.selectAll".into(), json!({"id": n.id.0, "add": m.shift}))); } else if resp.clicked() { @@ -1239,6 +1239,13 @@ mod tests { } let st = app.session.active().unwrap(); assert_eq!((st.selection.target, st.selection.objects.clone()), (Some(a), vec![a])); + // Shift-clicking another object's circle adds it to the selection (#901), and again takes + // it out; nothing is targeted meanwhile. + click_with(&mut app, &ctx, c[1], egui::Modifiers::SHIFT); + let st = app.session.active().unwrap(); + assert_eq!((st.selection.target, st.selection.objects.clone()), (None, vec![a, b])); + click_with(&mut app, &ctx, c[1], egui::Modifiers::SHIFT); + assert_eq!(app.session.active().unwrap().selection.objects, vec![a]); } #[test]