diff --git a/crates/engine/src/cmd/globaledit.rs b/crates/engine/src/cmd/globaledit.rs index 56ae30140..dc54cf792 100644 --- a/crates/engine/src/cmd/globaledit.rs +++ b/crates/engine/src/cmd/globaledit.rs @@ -716,8 +716,9 @@ pub(crate) fn propagate(before: &Document, d: &mut Document, ge: &GlobalEdit, se edited } -/// The outlines of the members of the active document's Global Edit that are not selected, in -/// their layer's colour: the canvas highlights them while Global Edit is on. +/// The outlines of the members of the active document's Global Edit that are not selected, hold +/// no selected object and sit inside no selected object, in their layer's colour: the canvas +/// highlights them while Global Edit is on. pub fn highlights(s: &Session) -> Outlines { let Some(st) = s.active() else { return vec![] }; let Some(ge) = &st.global_edit else { return vec![] }; @@ -732,6 +733,9 @@ pub fn highlights(s: &Session) -> Outlines { .iter() .filter_map(|(id, _)| { let n = st.doc.node(*id).filter(|n| !holds(n, &st.selection.objects))?; + if st.doc.ancestry(*id).is_some_and(|a| a.iter().any(|p| st.selection.objects.contains(p))) { + return None; + } let path = match n.stroke_path() { Some(p) => p, None => n.geometric_bounds_with(&symbols)?.to_path(0.1), diff --git a/crates/engine/src/tests/globaledit.rs b/crates/engine/src/tests/globaledit.rs index b73efd0df..786a351ac 100644 --- a/crates/engine/src/tests/globaledit.rs +++ b/crates/engine/src/tests/globaledit.rs @@ -472,6 +472,20 @@ fn a_shift_click_inside_a_matched_group_leaves_the_whole_group_out() { assert_eq!(members(&s), Some(vec![g, small])); } +#[test] +fn a_shift_click_on_a_member_inside_a_selected_group_deselects_the_group() { + let mut s = session(); + let [a, b, c, wide, _] = squares(&mut s); + let pair = group_of(&mut s, &[b, wide]); + let all = start(&mut s, a, json!({})); + assert_eq!(all, [a, c, b], "the copy in the group comes after the small square"); + sel(&mut s, &[pair]); + assert_eq!(crate::cmd::globaledit::highlights(&s).len(), 2, "the copy inside the selected group is not highlighted"); + shift_click(&mut s, 350.0, 50.0); + assert_eq!(members(&s), Some(all), "nothing is left out"); + assert!(s.doc().unwrap().selection.objects.is_empty(), "the group is deselected"); +} + #[test] fn leaving_out_all_but_one_ends_global_edit_and_the_first_object_stays() { let mut s = session(); diff --git a/crates/tools/src/select.rs b/crates/tools/src/select.rs index 2819c3369..36aca6092 100644 --- a/crates/tools/src/select.rs +++ b/crates/tools/src/select.rs @@ -994,13 +994,15 @@ fn shift_press(cx: &ToolContext, h: &Hit, top: NodeId, out: &mut Vec) -> } /// The object of the Global Edit in progress that the click `h` lands on or inside, when it is -/// highlighted: neither the object the edit started from nor one that is selected or holds a -/// selected object (`select.globalEditExclude` leaves it out of the edit). +/// highlighted: neither the object the edit started from nor one that is selected, holds a +/// selected object or sits inside a selected object (`select.globalEditExclude` leaves it out of +/// the edit). fn global_edit_member(cx: &ToolContext, h: &Hit) -> Option { let others = cx.global_edit.get(1..)?; let member = h.ancestry.iter().chain([&h.leaf]).find(|id| others.iter().any(|(m, _)| m == *id)).copied()?; - let selected = cx.selection.objects.iter().any(|s| cx.doc.ancestry(*s).is_some_and(|a| a.contains(&member))); - (!selected).then_some(member) + let holds = cx.selection.objects.iter().any(|s| cx.doc.ancestry(*s).is_some_and(|a| a.contains(&member))); + let inside = cx.doc.ancestry(member).is_some_and(|a| a.iter().any(|id| cx.selection.contains(*id))); + (!holds && !inside).then_some(member) } /// Can a click make `id` the key object? `select.key` takes only a selected top-level object. In