mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 17:56:50 -06:00
Slices: Lock Slices and Hide Slices deselect the slices and keep them deselected
Squashed from #2841 onto main.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
@@ -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":{}}}
|
||||
|
||||
Reference in New Issue
Block a user