//! Selection state (objects and, for direct selection, individual anchors; ruler guides). use std::collections::{BTreeMap, BTreeSet}; use serde::{Deserialize, Serialize}; use crate::{Document, NodeId}; /// (subpath index, anchor index) inside a path. pub type AnchorRef = (usize, usize); #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct Selection { /// Selected objects in selection order. pub objects: Vec, /// Direct-selected anchors per path. A path in `objects` with no entry here is fully selected. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub anchors: BTreeMap>, /// Key object for Align, one object of a selection of two or more. [`Selection::prune`] clears /// it when every selected object is the key or lies inside it, as when one object is left /// selected or when the key is a group selected with only its own members. #[serde(default, skip_serializing_if = "Option::is_none")] pub key: Option, /// The object, group or layer targeted through its target circle in the Layers panel, which /// appearance, transparency and opacity-mask commands then act on (a targeted layer has its /// art selected). Any other selection change clears it. #[serde(default, skip_serializing_if = "Option::is_none")] pub target: Option, /// Slices selected with the Slice Selection tool: user slice ids and the ids of objects whose /// object slice is selected (see [`crate::slices`]). Any change to the object selection /// clears them. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub slices: Vec, /// Ruler guides selected with a selection tool, as indexes into [`Document::guides`]. A new /// selection ([`Self::set`]) clears them; Shift-adding or -removing objects keeps them, so /// guides and objects can be selected together, in either order. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub guides: Vec, } impl Selection { pub fn is_empty(&self) -> bool { self.objects.is_empty() } /// Is anything selected that Delete and the arrow keys act on: objects or ruler guides? pub fn has_objects_or_guides(&self) -> bool { !self.objects.is_empty() || !self.guides.is_empty() } pub fn len(&self) -> usize { self.objects.len() } pub fn contains(&self, id: NodeId) -> bool { self.objects.contains(&id) } pub fn clear(&mut self) { self.objects.clear(); self.anchors.clear(); self.key = None; self.target = None; self.slices.clear(); self.guides.clear(); } pub fn set(&mut self, ids: impl IntoIterator) { self.clear(); for id in ids { self.add(id); } } pub fn add(&mut self, id: NodeId) { self.target = None; self.slices.clear(); if !self.objects.contains(&id) { self.objects.push(id); } } pub fn remove(&mut self, id: NodeId) { self.target = None; self.slices.clear(); self.objects.retain(|x| *x != id); self.anchors.remove(&id); if self.key == Some(id) { self.key = None; } } /// Select slices `ids` (see [`Selection::slices`]) and nothing else. pub fn set_slices(&mut self, ids: impl IntoIterator) { self.clear(); for id in ids { if !self.slices.contains(&id) { self.slices.push(id); } } } /// Select ruler guides `indexes` (see [`Selection::guides`]) and nothing else. pub fn set_guides(&mut self, indexes: impl IntoIterator) { self.clear(); for i in indexes { if !self.guides.contains(&i) { self.guides.push(i); } } } /// Add ruler guide `i` to the selection, or take it out, keeping the selected objects (a /// Shift-click on a guide). pub fn toggle_guide(&mut self, i: usize) { if let Some(k) = self.guides.iter().position(|g| *g == i) { self.guides.remove(k); } else { self.guides.push(i); } } pub fn toggle(&mut self, id: NodeId) { if self.contains(id) { self.remove(id) } else { self.add(id) } } /// Are some (not all) anchors of `id` direct-selected? pub fn partial(&self, id: NodeId) -> Option<&BTreeSet> { self.anchors.get(&id) } /// Drop ids that no longer exist or are no longer editable. pub fn prune(&mut self, doc: &Document) { self.objects.retain(|id| doc.node(*id).is_some()); self.anchors.retain(|id, _| doc.node(*id).is_some()); // The key object stays while another selected object lies outside it. let keep = self.key.and_then(|k| doc.node(k)).is_some_and(|n| { let mut inside = BTreeSet::new(); n.walk(&mut |c| { inside.insert(c.id); }); self.objects.iter().any(|o| !inside.contains(o)) }); if !keep { self.key = None; } if self.target.is_some_and(|t| doc.node(t).is_none()) { self.target = None; } self.slices.retain(|id| doc.is_slice(*id)); // Guides on a hidden or locked layer can't stay selected. self.guides.retain(|i| doc.guides.get(*i).is_some_and(|g| doc.guide_editable(g))); } /// Deselect the objects that are hidden or locked, themselves or through a group or layer /// around them (and so the key object when it is one): hidden or locked art can't stay /// selected. pub fn deselect_uneditable(&mut self, doc: &Document) { let out: Vec = self.objects.iter().copied().filter(|o| !doc.is_editable(*o)).collect(); for o in out { self.remove(o); } } /// Target `id` (see [`Selection::target`]): a layer gets its visible, unlocked art selected /// (the art of its sublayers too), anything else is selected itself. pub fn set_target(&mut self, doc: &Document, id: NodeId) { match doc.node(id) { Some(n) if n.is_layer() => self.set(n.layer_art(true)), _ => self.set([id]), } 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] { match &self.target { Some(t) => std::slice::from_ref(t), None => &self.objects, } } /// Top-level ordering: selected ids sorted by paint order (bottom first). pub fn in_paint_order(&self, doc: &Document) -> Vec { doc.paint_order(self.objects.iter().copied()) } } #[cfg(test)] mod tests { use super::*; #[test] fn basic_ops() { let mut s = Selection::default(); s.add(NodeId(1)); s.add(NodeId(1)); s.toggle(NodeId(2)); assert_eq!(s.objects, vec![NodeId(1), NodeId(2)]); s.toggle(NodeId(1)); assert_eq!(s.objects, vec![NodeId(2)]); s.clear(); assert!(s.is_empty()); } #[test] fn guides_are_selected_on_their_own() { let mut s = Selection::default(); s.add(NodeId(1)); s.set_guides([2, 0, 2]); assert_eq!((s.objects.clone(), s.guides.clone()), (vec![], vec![2, 0])); assert!(s.has_objects_or_guides() && s.is_empty()); // Shift-adding an object keeps them; a new selection doesn't. s.add(NodeId(1)); assert_eq!(s.guides, vec![2, 0]); s.set([NodeId(1)]); assert!(s.guides.is_empty()); // Toggled, a guide joins (or leaves) the selected objects. s.toggle_guide(0); assert_eq!((s.objects.clone(), s.guides.clone()), (vec![NodeId(1)], vec![0])); s.toggle_guide(0); assert_eq!((s.objects.clone(), s.guides.clone()), (vec![NodeId(1)], vec![])); // Pruning drops guides the document no longer has. let mut d = Document::new(100.0, 100.0); d.guides.push(crate::Guide::new(true, 10.0)); s.set_guides([0, 1]); s.prune(&d); assert_eq!(s.guides, vec![0]); } #[test] fn guides_show_and_lock_with_their_layer() { let mut d = Document::new(100.0, 100.0); let layer = d.layers[0].id; let g = crate::Guide { layer: Some(layer), ..crate::Guide::new(true, 10.0) }; assert!(d.guide_shown(&g) && d.guide_editable(&g)); d.node_mut(layer).unwrap().locked = true; assert!(d.guide_shown(&g) && !d.guide_editable(&g), "locked: shown, not editable"); d.node_mut(layer).unwrap().visible = false; assert!(!d.guide_shown(&g) && !d.guide_editable(&g), "hidden"); // On no layer, or a layer that's gone: always there. assert!(d.guide_shown(&crate::Guide::new(false, 5.0))); let gone = crate::Guide { layer: Some(NodeId(9999)), ..crate::Guide::new(true, 1.0) }; assert!(d.guide_shown(&gone) && d.guide_editable(&gone)); d.guides.push(g); assert_eq!(d.guides_on(layer).count(), 1); // Guides on no layer, or a gone one, are adopted by the top visible, unlocked layer. d.node_mut(layer).unwrap().visible = true; d.node_mut(layer).unwrap().locked = false; d.guides.push(crate::Guide::new(false, 5.0)); d.guides.push(gone); assert_eq!(d.adopt_guides(), 2); assert!(d.guides.iter().all(|g| g.layer == Some(layer))); assert_eq!(d.adopt_guides(), 0, "once"); // A guide saved before guides had layers reads as one on no layer. let old: crate::Guide = serde_json::from_str(r#"{"vertical": true, "pos": 5.0}"#).unwrap(); assert_eq!(old.layer, None); assert!(!serde_json::to_string(&crate::Guide::new(true, 5.0)).unwrap().contains("layer"), "nothing new written"); } #[test] fn selection_changes_clear_the_target() { let mut s = Selection::default(); s.set([NodeId(2), NodeId(3)]); s.target = Some(NodeId(1)); assert_eq!(s.subjects(), &[NodeId(1)]); s.add(NodeId(4)); assert_eq!(s.target, None); assert_eq!(s.subjects(), &[NodeId(2), NodeId(3), NodeId(4)]); s.target = Some(NodeId(1)); s.remove(NodeId(2)); assert_eq!(s.target, None); s.target = Some(NodeId(1)); s.set([NodeId(3)]); assert_eq!(s.target, None); } #[test] fn a_key_object_needs_two_or_more_selected_objects() { let mut s = Selection::default(); s.set([NodeId(1), NodeId(2)]); s.key = Some(NodeId(2)); s.remove(NodeId(1)); assert_eq!(s.key, Some(NodeId(2)), "another object left: remove keeps the key"); s.remove(NodeId(2)); assert_eq!(s.key, None, "the key object itself left"); // Pruning drops objects the document no longer has and clears the key once no other // selected object lies outside it. let mut d = Document::new(100.0, 100.0); let layer = d.layers[0].id; let mut ids = vec![]; for _ in 0..4 { let id = d.alloc_id(); ids.push(d.insert(Some(layer), usize::MAX, crate::Node::group(id, vec![])).unwrap()); } s.set(ids.iter().copied()); s.key = Some(ids[3]); d.remove(ids[0]).unwrap(); s.prune(&d); assert_eq!(s.key, Some(ids[3]), "three objects left: the key stays"); s.toggle(ids[1]); s.prune(&d); assert_eq!(s.key, Some(ids[3]), "two objects left: the key stays"); s.toggle(ids[2]); s.prune(&d); assert_eq!((s.objects.clone(), s.key), (vec![ids[3]], None), "one object left"); // A group key selected with only its own member is cleared too; another selected object // outside the group keeps it. let member = d.alloc_id(); d.insert(Some(ids[3]), usize::MAX, crate::Node::group(member, vec![])).unwrap(); s.set([member, ids[3]]); s.key = Some(ids[3]); s.prune(&d); assert_eq!(s.key, None, "a group and its own member"); s.set([member, ids[3], ids[2]]); s.key = Some(ids[3]); s.prune(&d); assert_eq!(s.key, Some(ids[3]), "another object outside the group"); } }