Direct Selection: a further Alt-click adds the path's group, then the next group up

Squashed from #4381 onto main.
This commit is contained in:
dotCooCoo
2026-10-29 20:47:39 +07:00
committed by bflatastic
parent e446a9a69b
commit 85a0adc5b8
3 changed files with 159 additions and 11 deletions
+118
View File
@@ -577,6 +577,124 @@ fn a_double_click_outside_isolation_leaves_it() {
assert!(DirectSelectionTool::new(false).pointer(&cx(&d, &none, &p), &PointerEvent::new(PointerKind::DoubleClick, 700.0, 700.0)).is_empty());
}
/// With Direct Selection, an Alt-click on a path selected whole adds its group on release, and
/// each further Alt-click adds the next group up, as Group Selection's clicks do, on an anchor,
/// a segment and the fill alike. The climb stops below the layer and at the isolated group.
#[test]
fn alt_clicks_on_a_path_selected_whole_add_its_groups() {
let (d, inside, _, inner, outer) = nested_group_doc();
let p = paint();
let alt = Mods { alt: true, ..Default::default() };
let add = |id: NodeId| vec![Action::Exec("select.add".into(), json!({"ids": [id.0]}))];
let click = |cx: &ToolContext, x: f64, y: f64| {
let mut t = DirectSelectionTool::new(false);
let down = t.pointer(cx, &PointerEvent::new(PointerKind::Down, x, y).with_mods(alt));
assert!(down.is_empty(), "({x}, {y}): {down:?}");
t.pointer(cx, &PointerEvent::new(PointerKind::Up, x, y).with_mods(alt))
};
for (x, y) in [(100.0, 100.0), (150.0, 100.0), (150.0, 150.0)] {
let mut s = Selection::default();
s.set([inside]);
assert_eq!(click(&cx(&d, &s, &p), x, y), add(inner), "({x}, {y}): the path's group");
s.set([inside, inner]);
assert_eq!(click(&cx(&d, &s, &p), x, y), add(outer), "({x}, {y}): the group around it");
s.set([inside, inner, outer]);
assert!(click(&cx(&d, &s, &p), x, y).is_empty(), "({x}, {y}): the layer is not added");
s.set([inside, inner]);
let isolated = ToolContext { isolation: Some(inner), ..cx(&d, &s, &p) };
assert!(click(&isolated, x, y).is_empty(), "({x}, {y}): not the group outside isolation");
}
}
/// In a sublayer, a Group Selection click and a Direct Selection Alt-click climb to the group
/// and stop there: the sublayer is not added.
#[test]
fn group_clicks_in_a_sublayer_stop_below_the_sublayer() {
use vectorcraft_doc::{Appearance, Document, LayerColor, Node};
let mut d = Document::new(500.0, 500.0);
let l = d.layers[0].id;
let (sub, g, a) = (d.alloc_id(), d.alloc_id(), d.alloc_id());
d.insert(Some(l), 0, Node::layer(sub, "Sub", LayerColor::Preset(1))).unwrap();
d.insert(Some(sub), 0, Node::group(g, vec![])).unwrap();
d.insert(Some(g), 0, Node::path(a, vectorcraft_geom::shapes::rectangle(Rect::new(100.0, 100.0, 200.0, 200.0)), Appearance::default_art()))
.unwrap();
let p = paint();
let alt = Mods { alt: true, ..Default::default() };
let click = |s: &Selection, group: bool, mods: Mods| {
let c = cx(&d, s, &p);
let mut t = DirectSelectionTool::new(group);
let down = t.pointer(&c, &PointerEvent::new(PointerKind::Down, 150.0, 150.0).with_mods(mods));
let up = t.pointer(&c, &PointerEvent::new(PointerKind::Up, 150.0, 150.0).with_mods(mods));
[down, up].concat()
};
let add = |id: NodeId| vec![Action::Exec("select.add".into(), json!({"ids": [id.0]}))];
let mut s = Selection::default();
s.set([a]);
assert_eq!(click(&s, true, Mods::default()), add(g), "Group Selection adds the group");
assert_eq!(click(&s, false, alt), add(g), "an Alt-click adds the group");
s.set([a, g]);
assert!(click(&s, true, Mods::default()).is_empty(), "Group Selection does not add the sublayer");
assert!(click(&s, false, alt).is_empty(), "an Alt-click does not add the sublayer");
}
/// When a group holds a sublayer, the climb stops at the sublayer: it adds neither the sublayer
/// nor the group above it.
#[test]
fn group_clicks_stop_at_a_sublayer_inside_a_group() {
use vectorcraft_doc::{Appearance, Document, LayerColor, Node};
let mut d = Document::new(500.0, 500.0);
let l = d.layers[0].id;
let (outer, sub, g, a) = (d.alloc_id(), d.alloc_id(), d.alloc_id(), d.alloc_id());
d.insert(Some(l), 0, Node::group(outer, vec![])).unwrap();
d.insert(Some(outer), 0, Node::layer(sub, "Sub", LayerColor::Preset(1))).unwrap();
d.insert(Some(sub), 0, Node::group(g, vec![])).unwrap();
d.insert(Some(g), 0, Node::path(a, vectorcraft_geom::shapes::rectangle(Rect::new(100.0, 100.0, 200.0, 200.0)), Appearance::default_art()))
.unwrap();
let p = paint();
let alt = Mods { alt: true, ..Default::default() };
let click = |s: &Selection, group: bool, mods: Mods| {
let c = cx(&d, s, &p);
let mut t = DirectSelectionTool::new(group);
let down = t.pointer(&c, &PointerEvent::new(PointerKind::Down, 150.0, 150.0).with_mods(mods));
let up = t.pointer(&c, &PointerEvent::new(PointerKind::Up, 150.0, 150.0).with_mods(mods));
[down, up].concat()
};
let add = |id: NodeId| vec![Action::Exec("select.add".into(), json!({"ids": [id.0]}))];
let mut s = Selection::default();
s.set([a]);
assert_eq!(click(&s, true, Mods::default()), add(g), "Group Selection adds the group");
assert_eq!(click(&s, false, alt), add(g), "an Alt-click adds the group");
s.set([a, g]);
assert!(click(&s, true, Mods::default()).is_empty(), "Group Selection does not add the sublayer");
assert!(click(&s, false, alt).is_empty(), "an Alt-click does not add the sublayer");
}
/// An Alt-drag from a path selected whole inside a group copies the path and adds no group, and
/// Alt+Shift-click on a segment or the fill still toggles the path, adding no group either.
#[test]
fn alt_drags_and_alt_shift_clicks_on_a_path_in_a_group_add_no_group() {
let (d, inside, ..) = nested_group_doc();
let p = paint();
let mut s = Selection::default();
s.set([inside]);
let cx = cx(&d, &s, &p);
let alt = Mods { alt: true, ..Default::default() };
for (x, y) in [(100.0, 100.0), (150.0, 100.0), (150.0, 150.0)] {
let mut t = DirectSelectionTool::new(false);
assert!(t.pointer(&cx, &PointerEvent::new(PointerKind::Down, x, y).with_mods(alt)).is_empty());
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, x + 20.0, y).with_mods(alt));
assert!(matches!(a.as_slice(), [Action::Begin(_), Action::Preview(c, v)] if c == "object.transform" && v["copy"] == true), "{a:?}");
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, x + 20.0, y).with_mods(alt)), vec![Action::Commit]);
}
let alt_shift = Mods { alt: true, shift: true, ..Default::default() };
for (x, y) in [(150.0, 100.0), (150.0, 150.0)] {
let mut t = DirectSelectionTool::new(false);
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, x, y).with_mods(alt_shift));
assert_eq!(a, vec![Action::Exec("select.toggle".into(), json!({"id": inside.0}))], "({x}, {y})");
assert!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, x, y).with_mods(alt_shift)).is_empty(), "({x}, {y})");
}
}
/// #927: a marquee across type selects the type object whole, beside the anchors it holds.
#[test]
fn marquee_selects_type_it_touches() {