fix: 修复 PR #1457 的选择与拖拽评审问题

- 标签在选择边界外也参与收窄,Dev Mode 单击已选图层只选它
- 区分点击命中图层与描边空白处的拖拽回退目标
- 重排圆环按按下时的修饰键判断点击,画布外松手视为拖拽结束
- 修复 clippy unnecessary_get_then_check
This commit is contained in:
Nicek
2026-10-29 09:33:55 +08:00
committed by trae-solo-agent
parent f8ddb5a0a8
commit dd93a6a2a8
7 changed files with 94 additions and 10 deletions
+14 -4
View File
@@ -94,6 +94,10 @@ impl Session {
let behind_sel = |t: Option<NodeId>| {
t.is_none_or(|t| sel.iter().any(|s| *s != t && (doc.is_ancestor(t, *s) || (in_own_box(*s) && paints_below(&doc, t, *s)))))
};
// What the press hit (a label or a painted layer): a click that doesn't become a drag
// selects just it. A press that reaches nothing above the selection uses the selection as
// its drag target instead; that fallback is not a clicked layer.
let clicked = target;
let target = match sel.iter().copied().find(|s| !locked_in(&doc, *s)) {
Some(first) if label.is_none() && !ev.shift && behind_sel(target) && selection_box(&doc, &sel).is_some_and(|q| in_quad(&q, ev.p())) => {
Some(first)
@@ -109,7 +113,10 @@ impl Session {
// drag; a drag moves the selection with it. Where no drag can start, it goes now.
let deselect = (ev.shift && sel.contains(&target)).then_some(target);
let without_target = || sel.iter().copied().filter(|x| *x != target).collect::<Vec<_>>();
let keep = (in_sel_box && !ev.cmd && (sel.contains(&target) || sel.iter().any(|s| doc.is_ancestor(*s, target)))).then_some(target);
// A release without a drag narrows the selection to the layer the press hit. A
// selected frame's name label sits outside the selection bounds, so it counts too.
let keep = clicked
.filter(|c| (label.is_some() || in_sel_box) && !ev.cmd && (sel.contains(c) || sel.iter().any(|s| doc.is_ancestor(*s, *c))));
let new_sel = if keep.is_some() {
sel.clone()
} else if ev.shift && deselect.is_none() {
@@ -122,10 +129,13 @@ impl Session {
vec![target]
};
self.set_selection(new_sel.clone());
// Dev Mode only selects (Shift-click still deselects): nothing moves.
// Dev Mode only selects (Shift-click still deselects): nothing moves. No move
// gesture starts, so a click selects the layer it hit now instead of on release.
if !edits {
if deselect.is_some() {
self.set_selection(without_target());
} else if let Some(k) = keep.filter(|_| sel.len() > 1) {
self.set_selection(vec![k]);
}
return Ok(());
}
@@ -134,9 +144,9 @@ impl Session {
if new_sel.is_empty() || doc.node(target).is_some_and(|n| n.locked) || ids.is_empty() {
if deselect.is_some() {
self.set_selection(without_target());
} else if keep.is_some() {
} else if let Some(k) = keep {
// No drag can start here, so the click narrows the selection now.
self.set_selection(vec![target]);
self.set_selection(vec![k]);
}
return Ok(());
}
@@ -38,6 +38,19 @@ fn status(s: &mut Session, ids: &[NodeId], st: &str) {
ex(s, "node.set", json!({"ids": ids, "props": {"devStatus": st}}));
}
#[test]
fn a_dev_mode_click_on_a_selected_layer_selects_just_it() {
let mut s = Session::new();
let a = idv(&ex(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 40, "height": 40})));
let b = idv(&ex(&mut s, "shape.rectangle", json!({"x": 80, "y": 0, "width": 40, "height": 40})));
s.tool.dev_mode = true;
ex(&mut s, "select.set", json!({"ids": [a, b]}));
// Dev Mode starts no move gesture, so the click narrows the selection at the press itself.
s.pointer(&ev(PointerKind::Down, 20.0, 20.0)).unwrap();
s.pointer(&ev(PointerKind::Up, 20.0, 20.0)).unwrap();
assert_eq!(s.selection(), vec![a], "a click on one of several selected layers selects it");
}
#[test]
fn the_dev_mode_canvas_selects_and_pans_but_never_edits() {
let mut s = Session::new();
+15
View File
@@ -62,6 +62,21 @@ fn pressing_a_rotated_frames_label_selects_and_drags_the_frame() {
assert!(s.selection().is_empty());
}
#[test]
fn a_click_on_a_selected_frames_label_selects_only_it() {
let (mut s, f, q) = setup();
let other = id_of(&ex(&mut s, "shape.rectangle", json!({"x": 500, "y": 500, "width": 40, "height": 40}))["id"]).unwrap();
ex(&mut s, "select.set", json!({"ids": [f, other]}));
// A point on the label's left edge, clear of the selection box's resize handles, lies outside
// the selection's bounds (the strip starts left of the frame's left edge there).
let p = q[0] + (q[3] - q[0]) * 0.2 + (q[1] - q[0]) * 0.02;
assert!(s.doc().unwrap().bounds_of(&[f, other]).is_some_and(|b| !b.contains(p)), "the label point is outside the selection bounds");
assert_eq!(label_at(s.doc().unwrap(), s.page().unwrap(), &s.tool.labels, p), Some(f), "the point is on the label");
s.pointer(&ev(PointerKind::Down, p)).unwrap();
s.pointer(&ev(PointerKind::Up, p)).unwrap();
assert_eq!(s.selection(), vec![f], "the label outside the bounds still narrows the selection to its frame");
}
#[test]
fn labels_of_frames_that_cant_be_picked_do_nothing() {
let (mut s, f, q) = setup();
+1 -1
View File
@@ -114,7 +114,7 @@ fn a_copied_image_fill_brings_its_image_to_another_file() {
// Another file has no image under that key: the copy brings the data with it.
ex(&mut s, "file.new", json!({}));
let b = rect(&mut s);
assert!(s.doc().unwrap().images.get("img-key").is_none());
assert!(!s.doc().unwrap().images.contains_key("img-key"));
ex(&mut s, "fill.paste", json!({"ids": [b], "index": 0}));
assert_eq!(s.doc().unwrap().images.get("img-key"), Some(&data));
assert!(matches!(node(&s, b).fills[0].kind, figcraft_color::PaintKind::Image(_)));
@@ -456,6 +456,21 @@ fn a_layer_stacked_below_the_selection_doesnt_take_a_drag_from_inside_it() {
assert_eq!(at(&s, gap), Point::new(home.x + 10.0, home.y + 10.0));
}
#[test]
fn a_click_where_nothing_is_hit_inside_a_multiple_selection_keeps_it() {
let mut s = Session::new();
let a = rect(&mut s, 100.0, 300.0, None);
let b = rect(&mut s, 220.0, 300.0, None);
for i in [a, b] {
ex(&mut s, "node.set", json!({"ids": [i], "props": {"fills": [], "strokes": ["#000000"], "strokeWeight": 1}}));
}
ex(&mut s, "select.set", json!({"ids": [a, b]}));
// The press is inside `a` but its unfilled inside hits nothing: `a` is only the drag fallback,
// not a clicked layer, so a click keeps the selection.
click(&mut s, 120.0, 320.0, NONE);
assert_eq!(sorted(s.selection()), sorted(vec![a, b]));
}
#[test]
fn alt_shift_drag_on_a_selected_layer_duplicates_it() {
let mut s = Session::new();