mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-03 00:39:44 -06:00
fix: 修复 PR #1457 的选择与拖拽评审问题
- 标签在选择边界外也参与收窄,Dev Mode 单击已选图层只选它 - 区分点击命中图层与描边空白处的拖拽回退目标 - 重排圆环按按下时的修饰键判断点击,画布外松手视为拖拽结束 - 修复 clippy unnecessary_get_then_check
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user