Slices: Lock Slices and Hide Slices deselect the slices and keep them deselected

Squashed from #2841 onto main.
This commit is contained in:
bflatastic
2026-10-24 17:46:19 +07:00
parent 4ca0c26f2f
commit 381b788f56
9 changed files with 351 additions and 54 deletions
+1 -1
View File
@@ -617,7 +617,7 @@ fn lock_guides(s: &mut Session, p: &Value) -> Result<Value> {
if v {
let active = s.active;
for (i, st) in s.docs.iter_mut().enumerate() {
if Some(i) != active && crate::deselect_guides(&st.doc, &mut st.selection) {
if Some(i) != active && crate::deselect_hidden(&s.menu, &st.doc, &mut st.selection) {
st.revision += 1;
}
}
+5 -9
View File
@@ -453,7 +453,7 @@ pub(crate) fn settle_interaction(s: &mut Session) -> Result<()> {
fn undo(s: &mut Session, _: &Value) -> Result<Value> {
settle_interaction(s)?;
let guides_locked = s.menu.guides_locked;
let menu = s.menu.clone();
let st = s.doc_mut()?;
let e = st.history.undo.pop().ok_or_else(|| EngineError::Other("nothing to undo".into()))?;
let label = e.label.clone();
@@ -468,10 +468,8 @@ fn undo(s: &mut Session, _: &Value) -> Result<Value> {
st.history.trim();
st.revision += 1;
st.selection.prune(&st.doc);
// Guides locked since the step was made stay deselected.
if guides_locked {
crate::deselect_guides(&st.doc, &mut st.selection);
}
// Guides locked and slices locked or hidden since the step was made stay deselected.
crate::deselect_hidden(&menu, &st.doc, &mut st.selection);
st.sync_new_art();
super::maskedit::follow_history(st, editing);
leave_isolation_if_gone(st);
@@ -481,7 +479,7 @@ fn undo(s: &mut Session, _: &Value) -> Result<Value> {
fn redo(s: &mut Session, _: &Value) -> Result<Value> {
settle_interaction(s)?;
let guides_locked = s.menu.guides_locked;
let menu = s.menu.clone();
let st = s.doc_mut()?;
let e = st.history.redo.pop().ok_or_else(|| EngineError::Other("nothing to redo".into()))?;
let label = e.label.clone();
@@ -496,9 +494,7 @@ fn redo(s: &mut Session, _: &Value) -> Result<Value> {
st.history.trim();
st.revision += 1;
st.selection.prune(&st.doc);
if guides_locked {
crate::deselect_guides(&st.doc, &mut st.selection);
}
crate::deselect_hidden(&menu, &st.doc, &mut st.selection);
st.sync_new_art();
super::maskedit::follow_history(st, editing);
leave_isolation_if_gone(st);
+3 -4
View File
@@ -99,7 +99,8 @@ fn run_steps(s: &mut Session, label: &str, artboard: Option<u64>, cmds: &mut [Va
/// ones close, closed or replaced ones (`stash`) come back, and each has its art, selection,
/// history, file, format and saved state again (#1697: undoing the batch's interaction alone left
/// a Save As step's new file and an Undo step's history in place). Files the steps wrote stay
/// written. Lock Guides stays as the steps left it, and while it is on the guides are deselected.
/// written. Lock Guides, Lock Slices and Hide Slices stay as the steps left them, and the guides
/// and slices they keep from being selected are deselected.
fn roll_back(s: &mut Session, start: Vec<DocState>, stash: &[DocState]) {
let open = std::mem::take(&mut s.docs);
s.docs = start;
@@ -107,8 +108,6 @@ fn roll_back(s: &mut Session, start: Vec<DocState>, stash: &[DocState]) {
// Past the newest revision the document reached, so the restored one redraws.
let newest = open.iter().chain(stash).filter(|d| d.uid == st.uid).map(|d| d.revision).max().unwrap_or(st.revision);
st.revision = newest.saturating_add(1);
if s.menu.guides_locked {
crate::deselect_guides(&st.doc, &mut st.selection);
}
crate::deselect_hidden(&s.menu, &st.doc, &mut st.selection);
}
}
+2 -4
View File
@@ -1139,16 +1139,14 @@ fn isolate(s: &mut Session, p: &Value) -> Result<Value> {
}
fn exit_isolation(s: &mut Session, _: &Value) -> Result<Value> {
let guides_locked = s.menu.guides_locked;
let menu = s.menu.clone();
let st = s.doc_mut()?;
if let Some(i) = st.isolation.take() {
super::distortcmds::finish_edit_text(st, i);
let target = st.doc.parent_of(i).filter(|p| st.doc.node(*p).is_some_and(|n| n.extras().shaper.is_some())).unwrap_or(i);
st.selection.set([target]);
// A guide path isolated while Lock Guides is on isn't selected (#1534).
if guides_locked {
crate::deselect_guides(&st.doc, &mut st.selection);
}
crate::deselect_hidden(&menu, &st.doc, &mut st.selection);
// The art on a locked artboard can't be selected.
st.protect_locked_artboards(&[], &Default::default(), None);
}
+31 -6
View File
@@ -109,13 +109,21 @@ pub fn specs() -> Vec<CommandSpec> {
has_doc,
clip_to_artboard
),
cmd!(query "view.slices.hide", "Hide Slices", ["View"], None, "{hidden?: bool} toggle (or set) whether the canvas hides the slices (session view state) → {hidden}", has_doc, hide),
cmd!(
query "view.slices.hide",
"Hide Slices",
["View"],
None,
"{hidden?: bool} toggle (or set) whether the canvas hides the slices (session view state); hiding deselects the slices in every open document, and showing them again leaves them deselected and does not restore the selection that was cleared; while they are hidden no command selects one (an object that is an object slice can still be selected as art) → {hidden}",
has_doc,
hide
),
cmd!(
query "view.slices.lock",
"Lock Slices",
["View"],
None,
"{locked?: bool} toggle (or set) the slice lock: locked slices can't be selected or edited with the Slice Selection tool → {locked}",
"{locked?: bool} toggle (or set) the slice lock: locked slices can't be selected or edited with the Slice Selection tool; locking deselects the slices in every open document, and unlocking leaves them deselected and does not restore the selection that was cleared; while it is on no command selects one (an object that is an object slice can still be selected as art) → {locked}",
has_doc,
lock
),
@@ -124,7 +132,7 @@ pub fn specs() -> Vec<CommandSpec> {
"List Slices",
[],
None,
"{} the slices as laid out, numbered left to right and top to bottom → {slices: [{number, id (null for auto slices), source: \"user\"|\"object\"|\"auto\", name, x, y, width, height, options, selected}], clipToArtboard, hidden, locked}",
"{} the slices as laid out, numbered left to right and top to bottom → {slices: [{number, id (null for auto slices), source: \"user\"|\"object\"|\"auto\", name, x, y, width, height, options, selected (selected with the Slice Selection tool or a slice command, or an object slice whose object is selected)}], clipToArtboard, hidden, locked}",
has_doc,
list
),
@@ -589,18 +597,35 @@ fn clip_to_artboard(s: &mut Session, p: &Value) -> Result<Value> {
fn hide(s: &mut Session, p: &Value) -> Result<Value> {
let v = p.get("hidden").and_then(Value::as_bool).unwrap_or(!s.menu.slices_hidden);
s.menu.slices_hidden = v;
if v {
deselect_in_every_document(s);
}
Ok(json!({ "hidden": v }))
}
fn lock(s: &mut Session, p: &Value) -> Result<Value> {
let v = p.get("locked").and_then(Value::as_bool).unwrap_or(!s.menu.slices_locked);
s.menu.slices_locked = v;
if v {
deselect_in_every_document(s);
}
Ok(json!({ "locked": v }))
}
/// Locked or hidden slices can't stay selected, in any open document ([`crate::deselect_hidden`]).
fn deselect_in_every_document(s: &mut Session) {
for st in &mut s.docs {
if crate::deselect_hidden(&s.menu, &st.doc, &mut st.selection) {
st.revision += 1;
}
}
}
fn list(s: &mut Session, _: &Value) -> Result<Value> {
let st = s.doc()?;
let d = &st.doc;
// What the Object → Slice commands act on: the selected objects that are object slices too.
let selected = selected_slices(d, &st.selection);
let slices: Vec<Value> = d
.slice_layout()
.iter()
@@ -609,7 +634,7 @@ fn list(s: &mut Session, _: &Value) -> Result<Value> {
json!({
"number": a.number, "id": a.id.map(|i| i.0), "source": a.source.key(), "name": d.slice_name(a),
"x": a.rect.x0, "y": a.rect.y0, "width": a.rect.width(), "height": a.rect.height(),
"options": options_json(&o), "selected": a.id.is_some_and(|id| st.selection.slices.contains(&id)),
"options": options_json(&o), "selected": a.id.is_some_and(|id| selected.contains(&id)),
})
})
.collect();
@@ -618,9 +643,9 @@ fn list(s: &mut Session, _: &Value) -> Result<Value> {
fn select_all(s: &mut Session, _: &Value) -> Result<Value> {
let ids = s.doc()?.doc.slice_ids();
let n = ids.len();
s.select(|_, sel| sel.set_slices(ids))?;
Ok(json!({ "count": n }))
// None while the slices are locked or hidden.
Ok(json!({ "count": s.doc()?.selection.slices.len() }))
}
// ---------- the Slice and Slice Selection tools ----------
+32 -22
View File
@@ -869,6 +869,19 @@ pub(crate) fn deselect_guides(d: &Document, sel: &mut Selection) -> bool {
any
}
/// Deselect what the View menu keeps from being selected: the guides while Lock Guides is on
/// ([`deselect_guides`]), and the slices that the Slice Selection tool or a slice command selected
/// ([`Selection::slices`]) while Lock Slices or Hide Slices is on. A selected object that is an
/// object slice stays selected. → whether any was selected.
pub(crate) fn deselect_hidden(menu: &cmd::menucmds::MenuState, d: &Document, sel: &mut Selection) -> bool {
let guides = menu.guides_locked && deselect_guides(d, sel);
let slices = (menu.slices_locked || menu.slices_hidden) && !sel.slices.is_empty();
if slices {
sel.slices.clear();
}
guides || slices
}
/// The error of an edit that would nest objects deeper than [`vectorcraft_doc::MAX_DEPTH`].
pub const TOO_DEEP: &str = "objects can't be nested more than 32 deep";
const _: () = assert!(vectorcraft_doc::MAX_DEPTH == 32, "TOO_DEEP (and its catalog rows) names the cap");
@@ -1920,7 +1933,7 @@ impl Session {
if !self.previewing && cmd::typing_in_progress(self) {
cmd::settle_interaction(self)?;
}
let guides_locked = self.menu.guides_locked;
let menu = self.menu.clone();
let st = self.doc_mut()?;
let before = st.doc.clone();
let before_sel = st.selection.clone();
@@ -1977,10 +1990,10 @@ impl Session {
match result {
Ok(v) => {
st.selection.prune(&st.doc);
// An edit that selects what it unlocks, shows or pastes leaves locked guides out.
if guides_locked {
deselect_guides(&st.doc, &mut st.selection);
}
// An edit that selects what it unlocks, shows or pastes leaves locked guides out,
// and one that selects the slices it makes leaves them out while they are locked
// or hidden.
deselect_hidden(&menu, &st.doc, &mut st.selection);
// The art on a locked artboard can't stay selected, however it got there, except
// the new art this edit made and new art still selected.
st.protect_locked_artboards(&before_sel.objects, &before_sel.new_art, Some(&*before));
@@ -2033,16 +2046,15 @@ impl Session {
/// Change only the selection (not an undo step).
pub fn select(&mut self, f: impl FnOnce(&Document, &mut Selection)) -> Result<()> {
self.active_appearance_item = None;
let guides_locked = self.menu.guides_locked;
let menu = self.menu.clone();
let st = self.doc_mut()?;
let before = st.selection.objects.clone();
let new_art = st.selection.new_art.clone();
f(&st.doc, &mut st.selection);
st.selection.prune(&st.doc);
// Lock Guides locks the paths made into guides too (#1534), whatever selects them.
if guides_locked {
deselect_guides(&st.doc, &mut st.selection);
}
// Lock Guides locks the paths made into guides too (#1534), and Lock Slices and Hide
// Slices keep the slices out, whatever selects them.
deselect_hidden(&menu, &st.doc, &mut st.selection);
// The art on a locked artboard can't be selected, whatever selects it, except new art
// that was selected and still is.
st.protect_locked_artboards(&before, &new_art, None);
@@ -2104,6 +2116,7 @@ impl Session {
/// Re-apply `cmd` on top of the interaction snapshot (replacing the previous preview).
pub fn preview(&mut self, cmd: &str, params: &Value) -> Result<Value> {
{
let menu = self.menu.clone();
let st = self.doc_mut()?;
let Some(it) = &st.interaction else { return Err(EngineError::Other("no interaction in progress".into())) };
// Undoing the previous preview counts as a change, so views redraw even when the command
@@ -2116,6 +2129,9 @@ impl Session {
if selection_changed {
st.selection.clone_from(&it.selection);
}
// Guides locked and slices locked or hidden during the interaction stay deselected,
// even when the preview fails.
deselect_hidden(&menu, &st.doc, &mut st.selection);
cmd::globaledit::follow_history(st);
}
// The previous preview's paint is undone with it.
@@ -2191,13 +2207,11 @@ impl Session {
pub fn cancel_interaction(&mut self) -> Result<()> {
self.clear_deferred_overlay_cache();
self.pending_paint = None;
let guides_locked = self.menu.guides_locked;
let menu = self.menu.clone();
let st = self.doc_mut()?;
st.undo_interaction();
// Guides locked during the interaction stay deselected.
if guides_locked {
deselect_guides(&st.doc, &mut st.selection);
}
// Guides locked and slices locked or hidden during the interaction stay deselected.
deselect_hidden(&menu, &st.doc, &mut st.selection);
Ok(())
}
@@ -2269,9 +2283,7 @@ impl Session {
{
st.doc = e.doc;
st.selection = Arc::unwrap_or_clone(e.selection);
if self.menu.guides_locked {
deselect_guides(&st.doc, &mut st.selection);
}
deselect_hidden(&self.menu, &st.doc, &mut st.selection);
st.sync_new_art();
cmd::globaledit::follow_history(st);
st.revision += 1;
@@ -2333,7 +2345,7 @@ impl Session {
self.end_document_undo_group(scope.uid, cancel);
return;
};
let guides_locked = self.menu.guides_locked;
let menu = self.menu.clone();
let Some(st) = self.document_mut(scope.uid) else { return };
let Some(g) = &mut st.undo_group else { return };
let newest = g.first.as_ref().is_some_and(|f| st.history.undo.last().is_some_and(|e| Arc::ptr_eq(&e.doc, f)));
@@ -2352,9 +2364,7 @@ impl Session {
st.doc = doc;
st.selection = selection;
cmd::globaledit::follow_history(st);
if guides_locked {
deselect_guides(&st.doc, &mut st.selection);
}
deselect_hidden(&menu, &st.doc, &mut st.selection);
st.history.recost_last(&st.doc);
st.revision += 1;
self.journal.truncate(scope.journal);
+17
View File
@@ -2417,6 +2417,23 @@ fn a_canceled_interaction_leaves_locked_guides_deselected() {
assert!(selected(&s).is_empty());
}
/// A drag preview that fails puts back the selection from before the drag, without the guide
/// paths that Lock Guides locked during it, so a move after the drag moves nothing.
#[test]
fn a_failed_preview_leaves_locked_guides_deselected() {
let mut s = session();
let g = guide_path(&mut s, 0.0);
sel(&mut s, &[g]);
s.begin_interaction("Move").unwrap();
s.preview("object.move", &json!({"dx": 10, "dy": 0})).unwrap();
s.execute("view.guides.lock", &json!({"locked": true})).unwrap();
assert!(s.preview("object.move", &json!({"dx": "far", "dy": 0})).is_err());
assert!(selected(&s).is_empty());
s.commit_interaction().unwrap();
let _ = s.execute("object.move", &json!({"dx": 10, "dy": 0}));
assert_eq!(bounds(&s, g).x0, 0.0);
}
fn guides(s: &Session) -> Vec<(bool, f64)> {
s.doc().unwrap().doc.guides.iter().map(|g| (g.vertical, g.pos)).collect()
}
+248
View File
@@ -215,6 +215,7 @@ fn clip_to_artboard_hide_lock_select_and_delete_all() {
assert_eq!((l["hidden"].clone(), l["locked"].clone(), l["clipToArtboard"].clone()), (json!(true), json!(true), json!(true)));
run(&mut s, "view.slices.hide", json!({"hidden": false}));
assert!(!s.slices_hidden());
run(&mut s, "view.slices.lock", json!({"locked": false}));
let a = rect(&mut s, 10.0, 10.0, 10.0, 10.0);
run(&mut s, "object.slice.make", json!({}));
@@ -232,6 +233,253 @@ fn clip_to_artboard_hide_lock_select_and_delete_all() {
assert!(s.execute("object.slice.deleteAll", &json!({})).is_err(), "nothing to delete");
}
/// The slices selected with the Slice Selection tool or a slice command.
fn picked(s: &Session) -> Vec<NodeId> {
s.doc().unwrap().selection.slices.clone()
}
/// View → Lock Slices deselects the slices in every open document, and while it is on nothing
/// selects one, so the commands that act on the selected slices leave them alone. Turning it off
/// leaves them deselected.
#[test]
fn lock_slices_deselects_the_slices_and_keeps_them_unselected() {
let mut s = session();
let id = user_slice(&mut s, [0.0, 0.0, 50.0, 50.0]);
run(&mut s, "select.object.slices", json!({}));
run(&mut s, "view.slices.lock", json!({"locked": true}));
assert!(picked(&s).is_empty(), "after Lock Slices");
let _ = s.execute("object.slice.move", &json!({"dx": 10, "dy": 0}));
let _ = s.execute("object.slice.options", &json!({"name": "hero"}));
assert_eq!(rect_of(&mut s, id), [0.0, 0.0, 50.0, 50.0]);
assert_eq!(s.doc().unwrap().doc.slices[0].options.name, "");
assert_eq!(run(&mut s, "select.object.slices", json!({}))["count"], 0);
assert_eq!(run(&mut s, "object.slice.select", json!({"slices": [id]}))["selected"], json!([]));
run(&mut s, "object.slice.create", json!({"x": 100, "y": 100, "width": 20, "height": 20}));
assert!(picked(&s).is_empty(), "after Create Slice");
// Locking from another document deselects this one's slices too.
run(&mut s, "view.slices.lock", json!({"locked": false}));
assert!(picked(&s).is_empty(), "after turning Lock Slices off");
assert_eq!(run(&mut s, "select.object.slices", json!({}))["count"], 2);
run(&mut s, "file.new", json!({"width": 300, "height": 300}));
run(&mut s, "view.slices.lock", json!({"locked": true}));
run(&mut s, "document.activate", json!({"index": 0}));
assert!(picked(&s).is_empty(), "in the other document");
}
/// View → Hide Slices deselects the slices in every open document, and while they are hidden
/// nothing selects one, so the commands that act on the selected slices leave them alone. Turning
/// it off leaves them deselected.
#[test]
fn hide_slices_deselects_the_slices_and_keeps_them_unselected() {
let mut s = session();
let id = user_slice(&mut s, [0.0, 0.0, 50.0, 50.0]);
run(&mut s, "select.object.slices", json!({}));
run(&mut s, "view.slices.hide", json!({"hidden": true}));
assert!(picked(&s).is_empty(), "after Hide Slices");
let _ = s.execute("object.slice.move", &json!({"dx": 10, "dy": 0}));
let _ = s.execute("object.slice.options", &json!({"name": "hero"}));
assert_eq!(rect_of(&mut s, id), [0.0, 0.0, 50.0, 50.0]);
assert_eq!(s.doc().unwrap().doc.slices[0].options.name, "");
assert_eq!(run(&mut s, "select.object.slices", json!({}))["count"], 0);
assert_eq!(run(&mut s, "object.slice.select", json!({"slices": [id]}))["selected"], json!([]));
run(&mut s, "object.slice.create", json!({"x": 100, "y": 100, "width": 20, "height": 20}));
assert!(picked(&s).is_empty(), "after Create Slice");
// Hiding from another document deselects this one's slices too.
run(&mut s, "view.slices.hide", json!({"hidden": false}));
assert!(picked(&s).is_empty(), "after turning Hide Slices off");
assert_eq!(run(&mut s, "select.object.slices", json!({}))["count"], 2);
run(&mut s, "file.new", json!({"width": 300, "height": 300}));
run(&mut s, "view.slices.hide", json!({"hidden": true}));
run(&mut s, "document.activate", json!({"index": 0}));
assert!(picked(&s).is_empty(), "in the other document");
}
/// Undo and Redo bring back a selection without the slices that Lock Slices has locked since, so
/// a move after them leaves the slice where it is.
#[test]
fn undo_and_redo_leave_locked_slices_deselected() {
let mut s = session();
let id = run(&mut s, "object.slice.create", json!({"x": 0, "y": 0, "width": 50, "height": 50}))["id"].as_u64().unwrap();
run(&mut s, "object.slice.move", json!({"dx": 10, "dy": 0}));
run(&mut s, "view.slices.lock", json!({"locked": true}));
run(&mut s, "edit.undo", json!({}));
assert!(picked(&s).is_empty(), "after Undo");
let _ = s.execute("object.slice.move", &json!({"dx": 10, "dy": 0}));
assert_eq!(rect_of(&mut s, id)[0], 0.0);
// Undone with the slice selected, redone once it is locked.
run(&mut s, "view.slices.lock", json!({"locked": false}));
run(&mut s, "object.slice.select", json!({"slices": [id]}));
run(&mut s, "object.slice.move", json!({"dx": 10, "dy": 0}));
run(&mut s, "edit.undo", json!({}));
run(&mut s, "view.slices.lock", json!({"locked": true}));
run(&mut s, "edit.redo", json!({}));
assert!(picked(&s).is_empty(), "after Redo");
let _ = s.execute("object.slice.move", &json!({"dx": 10, "dy": 0}));
assert_eq!(rect_of(&mut s, id)[0], 10.0);
}
/// Undo and Redo bring back a selection without the slices that Hide Slices has hidden since, so
/// a move after them leaves the slice where it is.
#[test]
fn undo_and_redo_leave_hidden_slices_deselected() {
let mut s = session();
let id = run(&mut s, "object.slice.create", json!({"x": 0, "y": 0, "width": 50, "height": 50}))["id"].as_u64().unwrap();
run(&mut s, "object.slice.move", json!({"dx": 10, "dy": 0}));
run(&mut s, "view.slices.hide", json!({"hidden": true}));
run(&mut s, "edit.undo", json!({}));
assert!(picked(&s).is_empty(), "after Undo");
let _ = s.execute("object.slice.move", &json!({"dx": 10, "dy": 0}));
assert_eq!(rect_of(&mut s, id)[0], 0.0);
// Undone with the slice selected, redone once it is hidden.
run(&mut s, "view.slices.hide", json!({"hidden": false}));
run(&mut s, "object.slice.select", json!({"slices": [id]}));
run(&mut s, "object.slice.move", json!({"dx": 10, "dy": 0}));
run(&mut s, "edit.undo", json!({}));
run(&mut s, "view.slices.hide", json!({"hidden": true}));
run(&mut s, "edit.redo", json!({}));
assert!(picked(&s).is_empty(), "after Redo");
let _ = s.execute("object.slice.move", &json!({"dx": 10, "dy": 0}));
assert_eq!(rect_of(&mut s, id)[0], 10.0);
}
/// A failed batch, a canceled undo group, a failed drag preview and a canceled drag bring back the
/// selection from before them, without the slices that Lock Slices locked inside them.
#[test]
fn a_failed_batch_a_canceled_undo_group_and_a_canceled_drag_leave_locked_slices_deselected() {
let mut s = session();
let id = run(&mut s, "object.slice.create", json!({"x": 0, "y": 0, "width": 50, "height": 50}))["id"].as_u64().unwrap();
let steps = json!([{"command": "view.slices.lock", "params": {"locked": true}}, {"command": "object.isolate", "params": {}}]);
assert!(s.execute("command.batch", &json!({"commands": steps})).is_err());
assert!(s.slices_locked());
assert!(picked(&s).is_empty(), "after the failed batch");
run(&mut s, "view.slices.lock", json!({"locked": false}));
run(&mut s, "object.slice.select", json!({"slices": [id]}));
run(&mut s, "edit.undoGroup.begin", json!({}));
run(&mut s, "object.slice.move", json!({"dx": 10, "dy": 0}));
run(&mut s, "view.slices.lock", json!({"locked": true}));
run(&mut s, "edit.undoGroup.end", json!({"cancel": true}));
assert_eq!(rect_of(&mut s, id)[0], 0.0);
assert!(picked(&s).is_empty(), "after the canceled undo group");
run(&mut s, "view.slices.lock", json!({"locked": false}));
run(&mut s, "object.slice.select", json!({"slices": [id]}));
s.begin_interaction("Move Slice").unwrap();
s.preview("object.slice.move", &json!({"dx": 10, "dy": 0})).unwrap();
run(&mut s, "view.slices.lock", json!({"locked": true}));
// A preview that fails puts back the snapshot, without the locked slice.
assert!(s.preview("object.slice.move", &json!({"dx": "far", "dy": 0})).is_err());
assert!(picked(&s).is_empty(), "after a failed preview");
s.cancel_interaction().unwrap();
assert_eq!(rect_of(&mut s, id)[0], 0.0);
assert!(picked(&s).is_empty(), "after the canceled drag");
}
/// A failed batch, a canceled undo group, a failed drag preview and a canceled drag bring back the
/// selection from before them, without the slices that Hide Slices hid inside them.
#[test]
fn a_failed_batch_a_canceled_undo_group_and_a_canceled_drag_leave_hidden_slices_deselected() {
let mut s = session();
let id = run(&mut s, "object.slice.create", json!({"x": 0, "y": 0, "width": 50, "height": 50}))["id"].as_u64().unwrap();
let steps = json!([{"command": "view.slices.hide", "params": {"hidden": true}}, {"command": "object.isolate", "params": {}}]);
assert!(s.execute("command.batch", &json!({"commands": steps})).is_err());
assert!(s.slices_hidden());
assert!(picked(&s).is_empty(), "after the failed batch");
run(&mut s, "view.slices.hide", json!({"hidden": false}));
run(&mut s, "object.slice.select", json!({"slices": [id]}));
run(&mut s, "edit.undoGroup.begin", json!({}));
run(&mut s, "object.slice.move", json!({"dx": 10, "dy": 0}));
run(&mut s, "view.slices.hide", json!({"hidden": true}));
run(&mut s, "edit.undoGroup.end", json!({"cancel": true}));
assert_eq!(rect_of(&mut s, id)[0], 0.0);
assert!(picked(&s).is_empty(), "after the canceled undo group");
run(&mut s, "view.slices.hide", json!({"hidden": false}));
run(&mut s, "object.slice.select", json!({"slices": [id]}));
s.begin_interaction("Move Slice").unwrap();
s.preview("object.slice.move", &json!({"dx": 10, "dy": 0})).unwrap();
run(&mut s, "view.slices.hide", json!({"hidden": true}));
// A preview that fails puts back the snapshot, without the hidden slice.
assert!(s.preview("object.slice.move", &json!({"dx": "far", "dy": 0})).is_err());
assert!(picked(&s).is_empty(), "after a failed preview");
s.cancel_interaction().unwrap();
assert_eq!(rect_of(&mut s, id)[0], 0.0);
assert!(picked(&s).is_empty(), "after the canceled drag");
}
/// A canceled undo scope that joined an open undo group brings back the selection from when the
/// scope began, without the slices that Lock Slices or Hide Slices took out inside it.
#[test]
fn a_canceled_undo_scope_leaves_locked_or_hidden_slices_deselected() {
for (command, key) in [("view.slices.lock", "locked"), ("view.slices.hide", "hidden")] {
let mut s = session();
let id = run(&mut s, "object.slice.create", json!({"x": 0, "y": 0, "width": 50, "height": 50}))["id"].as_u64().unwrap();
run(&mut s, "object.slice.select", json!({"slices": [id]}));
run(&mut s, "edit.undoGroup.begin", json!({}));
run(&mut s, "object.slice.move", json!({"dx": 10, "dy": 0}));
let scope = s.begin_undo_scope().unwrap();
run(&mut s, command, json!({ key: true }));
s.end_undo_scope(scope, true);
assert!(picked(&s).is_empty(), "{command}: after the canceled scope");
let _ = s.execute("object.slice.move", &json!({"dx": 10, "dy": 0}));
assert_eq!(rect_of(&mut s, id)[0], 10.0, "{command}");
}
}
/// Select slice `id` and the first ruler guide together, through [`Session::select`].
fn pick_slice_and_guide(s: &mut Session, id: u64) {
s.select(|_, sel| {
sel.slices = vec![NodeId(id)];
sel.guides = vec![0];
})
.unwrap();
}
/// With Lock Guides, Lock Slices and Hide Slices all on, Undo and a selection deselect both the
/// guide and the slice, which is locked and hidden at once, and the slices stay deselected until
/// both Lock Slices and Hide Slices are off.
#[test]
fn locked_guides_and_a_locked_hidden_slice_are_deselected_together() {
let mut s = session();
let id = user_slice(&mut s, [0.0, 0.0, 50.0, 50.0]);
run(&mut s, "guide.add", json!({"vertical": true, "pos": 100}));
pick_slice_and_guide(&mut s, id);
// A step whose selection before it holds the guide and the slice, for Undo to bring back.
rect(&mut s, 200.0, 200.0, 10.0, 10.0);
run(&mut s, "view.guides.lock", json!({"locked": true}));
run(&mut s, "view.slices.lock", json!({"locked": true}));
run(&mut s, "view.slices.hide", json!({"hidden": true}));
let none = |s: &Session| {
let sel = &s.doc().unwrap().selection;
sel.slices.is_empty() && sel.guides.is_empty()
};
run(&mut s, "edit.undo", json!({}));
assert!(none(&s), "after Undo");
pick_slice_and_guide(&mut s, id);
assert!(none(&s), "after a selection");
// Shown while still locked, or unlocked while still hidden, the slices stay out.
run(&mut s, "view.slices.hide", json!({"hidden": false}));
assert_eq!(run(&mut s, "select.object.slices", json!({}))["count"], 0);
run(&mut s, "view.slices.hide", json!({"hidden": true}));
run(&mut s, "view.slices.lock", json!({"locked": false}));
assert_eq!(run(&mut s, "select.object.slices", json!({}))["count"], 0);
run(&mut s, "view.slices.hide", json!({"hidden": false}));
assert_eq!(run(&mut s, "select.object.slices", json!({}))["count"], 1);
}
/// `slice.list` reports an object slice as selected while its object is selected: the Object →
/// Slice commands act on it then, and the canvas draws it as selected.
#[test]
fn slice_list_reports_an_object_slice_selected_with_its_object() {
let mut s = session();
let a = rect(&mut s, 10.0, 10.0, 20.0, 20.0);
run(&mut s, "object.slice.make", json!({}));
rect(&mut s, 100.0, 100.0, 20.0, 20.0);
run(&mut s, "object.slice.make", json!({}));
let selected = |s: &mut Session| slices(s).into_iter().filter(|v| v["selected"] == true).map(|v| v["id"].clone()).collect::<Vec<_>>();
run(&mut s, "select.set", json!({"ids": [a]}));
assert_eq!(selected(&mut s), [json!(a)]);
run(&mut s, "select.none", json!({}));
assert!(selected(&mut s).is_empty());
}
#[test]
fn slices_survive_a_native_round_trip() {
let mut s = session();
+12 -8
View File
@@ -3545,14 +3545,18 @@ numbered left to right and top to bottom: `{number, id (null for auto slices), s
width, height, options, selected}`, plus `clipToArtboard`, `hidden` and `locked`.
The Object → Slice commands act on the selected slices: the ones `select.object.slices` (Select → Object → Slices)
selects, and the selected objects that are object slices. `object.slice.release` (an object slice leaves its object,
a user slice becomes an unpainted rectangle), `object.slice.fromGuides` (the grid the ruler guides cut the artboards
into), `object.slice.fromSelection`, `object.slice.duplicate {dx?, dy?}`, `object.slice.combine`,
`object.slice.divide {rows? | rowHeight?, columns? | columnWidth?}`, `object.slice.deleteAll`,
`object.slice.options {kind?: image|noImage|htmlText, name?, url?, target?, message?, alt?, text?, background?:
""|matte|#rrggbb, hAlign?, vAlign?}` and `object.slice.clipToArtboard {on?}` are each one undo step.
`view.slices.hide {hidden?}` and `view.slices.lock {locked?}` are session view state; the canvas draws the slices in
the `sliceLineColor` preference, numbered while `showSliceNumbers` is on.
selects, and the selected objects that are object slices. `slice.list` reports both kinds as `selected`.
`object.slice.release` (an object slice leaves its object, a user slice becomes an unpainted rectangle),
`object.slice.fromGuides` (the grid the ruler guides cut the artboards into), `object.slice.fromSelection`,
`object.slice.duplicate {dx?, dy?}`, `object.slice.combine`, `object.slice.divide {rows? | rowHeight?, columns? |
columnWidth?}`, `object.slice.deleteAll`, `object.slice.options {kind?: image|noImage|htmlText, name?, url?, target?,
message?, alt?, text?, background?: ""|matte|#rrggbb, hAlign?, vAlign?}` and `object.slice.clipToArtboard {on?}` are
each one undo step. `view.slices.hide {hidden?}` and `view.slices.lock {locked?}` are session view state; the canvas
draws the slices in the `sliceLineColor` preference, numbered while `showSliceNumbers` is on. Locking or hiding
deselects the slices in every open document; unlocking or showing them again leaves them deselected and does not
restore the selection that was cleared. While the slices are locked or hidden, a command that selects slices, such as
`select.object.slices` (which replies `count: 0`), `object.slice.select`, `object.slice.create`, `edit.undo` or
`edit.redo`, leaves them out; a selected object that is an object slice stays selected.
```json
{"name":"run_command","arguments":{"command":"object.slice.make","params":{}}}