Isolation mode: breadcrumbs are clickable to navigate hierarchy levels (#4468)

This commit is contained in:
el-j
2026-10-29 19:54:35 +07:00
committed by bflatastic
parent 9534a36416
commit 6e2e0740b0
4 changed files with 207 additions and 28 deletions
+33 -17
View File
@@ -269,22 +269,34 @@ pub(super) fn isolate(s: &mut Session, p: &Value) -> Result<Value> {
.or_else(|| s.active().and_then(|d| d.selection.objects.first().copied()))
.ok_or_else(|| bad("object.isolate", "missing id"))?;
let guides_locked = s.menu.guides_locked;
let (isolation, prev) = {
let st = s.doc_mut()?;
// Groups, layers and single objects (a path, a compound path, an image…); type isn't
// isolated: a double-click on it edits its text.
if matches!(st.doc.node(id).ok_or(EngineError::NoNode(id))?.kind, NodeKind::Text(_)) {
return Err(bad("object.isolate", "type objects can't be isolated"));
}
let isolation =
st.doc.node(id).filter(|n| n.extras().shaper.is_some()).and_then(|n| n.children()).and_then(|c| c.first()).map_or(id, |n| n.id);
// A hidden or locked object, one in a hidden or locked group or layer, and a guide path while
// Lock Guides is on aren't isolated either: a click with the Selection tool doesn't select them.
// A Shaper Group isolates its source group, so that group is the one checked.
if !st.doc.is_editable(isolation) {
return Err(bad("object.isolate", "the object is hidden or locked"));
}
if guides_locked && crate::is_guide_path(&st.doc, isolation) {
return Err(EngineError::Other("guides are locked".into()));
}
(isolation, st.isolation)
};
if let Some(prev) = prev
&& prev != isolation
{
crate::cmd::settle_interaction(s)?;
let st = s.doc_mut()?;
crate::cmd::distortcmds::finish_edit_text(st, prev);
}
let st = s.doc_mut()?;
// Groups, layers and single objects (a path, a compound path, an image…); type isn't
// isolated: a double-click on it edits its text.
if matches!(st.doc.node(id).ok_or(EngineError::NoNode(id))?.kind, NodeKind::Text(_)) {
return Err(bad("object.isolate", "type objects can't be isolated"));
}
let isolation = st.doc.node(id).filter(|n| n.extras().shaper.is_some()).and_then(|n| n.children()).and_then(|c| c.first()).map_or(id, |n| n.id);
// A hidden or locked object, one in a hidden or locked group or layer, and a guide path while
// Lock Guides is on aren't isolated either: a click with the Selection tool doesn't select them.
// A Shaper Group isolates its source group, so that group is the one checked.
if !st.doc.is_editable(isolation) {
return Err(bad("object.isolate", "the object is hidden or locked"));
}
if guides_locked && crate::is_guide_path(&st.doc, isolation) {
return Err(EngineError::Other("guides are locked".into()));
}
st.isolation = Some(isolation);
st.selection.clear();
// New art on a locked artboard, now deselected, is protected.
@@ -312,8 +324,11 @@ pub(super) fn exit_isolation(s: &mut Session, p: &Value) -> Result<Value> {
return ok();
}
let menu = s.menu.clone();
let st = s.doc_mut()?;
if let Some(i) = st.isolation.take() {
let prev = s.active().and_then(|d| d.isolation);
if let Some(i) = prev {
crate::cmd::settle_interaction(s)?;
let st = s.doc_mut()?;
st.isolation = None;
crate::cmd::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]);
@@ -322,6 +337,7 @@ pub(super) fn exit_isolation(s: &mut Session, p: &Value) -> Result<Value> {
// The art on a locked artboard can't be selected.
st.protect_locked_artboards(&[], &Default::default(), None);
}
let st = s.doc_mut()?;
st.revision += 1;
ok()
}
+44
View File
@@ -158,6 +158,50 @@ fn edit_text_is_off_while_an_opacity_mask_is_edited() {
assert_eq!(s.doc().unwrap().isolation, Some(id));
}
#[test]
fn isolating_ancestor_after_undo_clears_editing_from_redo_history() {
let (mut s, rect) = persp_session();
let id = persp_text(&mut s);
s.execute("select.set", &json!({"ids": [id.0, rect.0]})).unwrap();
let g = s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap();
// Start editing text in perspective and make an edit so undo/redo has entries
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
s.execute("perspective.editText", &json!({})).unwrap();
s.execute("text.editRange", &json!({"id": id.0, "start": 0, "end": 0, "insert": "Hello "})).unwrap();
// Undo the text edit so redo stack holds an entry while doc is not actively editing
s.execute("edit.undo", &json!({})).unwrap();
// Isolating the parent group must clear editing flag in redo entries too
s.execute("object.isolate", &json!({"id": g})).unwrap();
for e in &s.doc().unwrap().history.redo {
if let Some(p) = e.doc.node(id).and_then(|n| n.extras().perspective.as_deref()) {
assert!(!p.editing, "redo entry must not retain editing=true");
}
}
}
#[test]
fn isolate_settles_pending_typing_interaction() {
use vectorcraft_tools::{PointerEvent, PointerKind};
let (mut s, rect) = persp_session();
let v = crate::ViewInfo::default();
s.select_tool("type", v).unwrap();
for k in [PointerKind::Down, PointerKind::Up] {
s.pointer(&PointerEvent::new(k, 200.0, 200.0), v).unwrap();
}
let id = s.doc().unwrap().selection.objects[0];
s.tool_text("abc", v).unwrap();
assert!(s.in_interaction());
s.execute("object.isolate", &json!({"id": rect.0})).unwrap();
assert!(!s.in_interaction(), "isolating another object settles typing interaction");
let n = node(&s, id);
let NodeKind::Text(ref t) = n.kind else { panic!("not text") };
assert_eq!(t.plain_text(), "abc");
}
/// Edit Text ending while a background save writes the file leaves the document saved once the
/// file is written: the flat view is never saved. Saved while editing and edited after: undoing
/// the edit once Edit Text has ended is back at what the file holds.