Merge pull request #2107 from TrevorBrown-dev/fix/escape-deselects-measured

Selection: Escape clears the selection instead of stepping out to the parent, as in Figma
This commit is contained in:
BFlat authored and GitHub committed 2026-11-12 09:18:27 +07:00
commit 0fd45a2eb2
5 files changed
+36 -32

No files matched your search

+4 -16
View File
@@ -1,4 +1,4 @@
use super::{Document, Gesture, Node, NodeId, Overlay, Point, PointerEvent, PointerKind, Result, Session, ShapeHandle, ToolId, json, textedit};
use super::{Document, Gesture, NodeId, Overlay, Point, PointerEvent, PointerKind, Result, Session, ShapeHandle, ToolId, json, textedit};
impl Session {
/// Keys the current mode handles before menu shortcuts (vector edit mode's tools and point
@@ -146,21 +146,9 @@ impl Session {
self.ds_mut()?.focus = None;
return Ok(());
}
// Escape selects the parent, then nothing.
let doc = self.doc()?;
let parents: Vec<NodeId> =
sel.iter().filter_map(|i| doc.node(*i).and_then(|n| n.parent)).filter(|p| !doc.node(*p).is_some_and(Node::is_page)).collect();
if !parents.is_empty() {
let mut uniq = Vec::new();
for p in parents {
if !uniq.contains(&p) {
uniq.push(p);
}
}
self.set_selection(uniq);
} else {
self.set_selection(vec![]);
}
// Escape clears the selection whatever its nesting (measured in Figma: it never selects the
// parent).
self.set_selection(vec![]);
Ok(())
}
+9 -3
View File
@@ -84,14 +84,20 @@ fn only_the_focused_frame_is_hit_and_escape_leaves() {
click(&mut s, 75.0, 75.0);
assert_eq!(s.selection(), vec![in_a], "the layer on top of it isn't hit either");
assert_ne!(s.selection(), vec![over]);
// Escape: selection up to the frame, then out of the focus view.
// Escape: the selection goes, then out of the focus view.
s.escape().unwrap();
assert_eq!(s.selection(), vec![a]);
assert!(s.selection().is_empty(), "Escape selects nothing, not the parent frame");
assert_eq!(focused(&s), Some(a));
s.escape().unwrap();
assert_eq!(focused(&s), None, "Escape with the frame selected leaves");
assert_eq!(focused(&s), None, "Escape with nothing selected leaves");
click(&mut s, 350.0, 100.0);
assert_eq!(s.selection(), vec![b], "everything is hit again");
// With the focused frame itself selected, one Escape leaves the focus view.
ex(&mut s, "dev.focus", json!({"id": a}));
ex(&mut s, "select.set", json!({"ids": [a]}));
assert_eq!(focused(&s), Some(a));
s.escape().unwrap();
assert_eq!(focused(&s), None, "Escape with the focused frame selected leaves at once");
}
#[test]
+17 -5
View File
@@ -271,14 +271,26 @@ fn keyboard_selection() {
ex(&mut s, cmd, json!({}));
assert_eq!(sorted(s.selection()), sorted(after), "{why}");
}
// Esc steps out to the parent, then deselects.
// Esc clears the selection at once, whatever the nesting (measured in Figma).
ex(&mut s, "select.set", json!({"ids": [k.a]}));
s.escape().unwrap();
assert_eq!(s.selection(), vec![k.g], "Esc selects the parent");
assert_eq!(s.selection(), Vec::<NodeId>::new(), "Esc from a nested layer selects nothing, not the parent");
s.escape().unwrap();
assert_eq!(s.selection(), vec![k.f]);
s.escape().unwrap();
assert_eq!(s.selection(), Vec::<NodeId>::new(), "Esc on a top-level layer deselects");
assert_eq!(s.selection(), Vec::<NodeId>::new());
// A top-level layer, and several layers, nested or not: one Esc selects nothing.
let cases: Vec<(Vec<NodeId>, &str)> = vec![
(vec![k.f], "a top-level frame"),
(vec![k.r], "a top-level layer"),
(vec![k.a, k.b], "two layers in one group (not their group)"),
(vec![k.a, k.d], "layers in different parents (not the parents)"),
(vec![k.a, k.r], "a nested layer and a top-level one"),
];
for (ids, why) in cases {
ex(&mut s, "select.set", json!({"ids": ids}));
assert_eq!(sorted(s.selection()), sorted(ids.clone()), "{why}: selected");
s.escape().unwrap();
assert_eq!(s.selection(), Vec::<NodeId>::new(), "Esc on {why} selects nothing");
}
}
#[test]
+4 -8
View File
@@ -328,8 +328,8 @@ fn edit_delete_from_the_menu_removes_the_open_interaction_not_the_layer() {
}
#[test]
fn escape_closes_prototype_editor_without_deselecting_layer() {
let (s, a, _b, btn) = setup();
fn escape_closes_prototype_editor_and_the_next_deselects() {
let (s, _a, _b, btn) = setup();
let mut u = Ui::new(s);
u.app.ui.right_tab = crate::RightTab::Prototype;
u.run("select.set", json!({"ids": [btn]}));
@@ -344,13 +344,9 @@ fn escape_closes_prototype_editor_without_deselecting_layer() {
assert_eq!(crate::proto_panel::editor(&u.ctx), None);
assert_eq!(u.app.session.selection(), vec![btn], "layer remains selected");
// Second Escape steps out to parent frame.
// Second Escape clears the selection, not stepping out to the parent frame.
u.key(pos2(100.0, 100.0), egui::Key::Escape);
assert_eq!(u.app.session.selection(), vec![a], "steps out to parent frame");
// Third Escape deselects all.
u.key(pos2(100.0, 100.0), egui::Key::Escape);
assert!(u.app.session.selection().is_empty(), "selection cleared on third escape");
assert!(u.app.session.selection().is_empty(), "the second Escape deselects");
}
/// After a click in the Pages list, Delete that removes an interaction ends the list's hold on the
+2
View File
@@ -59,6 +59,7 @@ interaction feel not yet compared.
| Click, Shift-click, marquee, Shift-marquee toggle | done | Shift-drag on a selected layer moves the selection, kept to one axis (#498); a marquee selects what it touches; from a top-level frame's background, the layers inside it; locked and hidden layers are skipped (`selection_behaviour_tests`) (a locked layer selected from the panel ignores the arrow keys and the nudge commands; its properties still edit; measured in Figma's web app, 2026-11-10) |
| Deep select (Cmd/Ctrl-click, Cmd/Ctrl-marquee) | done | |
| Select layer menu (every layer under the click) | done | |
| Escape with layers selected | done | clears the selection at once, whatever the nesting; it never selects the parent. Measured in the Figma web app (2026-11-08) with a rectangle in a group in a frame, selected from the canvas (a double-click into the group), from the Layers panel, and after Tab moved the selection to its sibling: one Escape selects nothing, a second leaves nothing selected. Escape still leaves text and vector editing, ends gestures and the pen first, and ends the focus view once nothing or the focused frame is selected (`selection_behaviour_tests`, `devfocus_tests`) |
| Enter / Shift+Enter (children / parent), Tab / Shift+Tab (siblings) | done | Tab renames the next layer while renaming |
| Resize handles: Shift keeps proportions, Alt from centre, constraints-aware | done | Alt shrinks too; past an edge flips (several layers mirror); groups and mixed rotations scale each layer along its own axes, keeping rotation and flip (no skew: children turned off a right angle are approximate); Cmd/Ctrl ignores a frame's constraints (`transform_behaviour_tests`) |
| Rotate just outside a corner; Shift in 15° steps | done | rotated handle box when layers share a rotation |
@@ -187,6 +188,7 @@ in the table. `crates/ui-egui/src/shortcut_keys_tests.rs` drives the keys a look
| Date | Change | Summary |
|---|---|---|
| 2026-11-08 | patch | Selection: Escape with layers selected clears the selection instead of stepping out to the parent first. Measured in the Figma web app with a rectangle in a group in a frame, selected from the canvas, from the Layers panel and after Tab: one Escape selects nothing. Editing, gestures and the pen still end first |
| 2026-11-10 | patch | M5, #1560: locked layers follow Figma's rules (Lock and unlock layers; measured in Figma's web app): a locked layer selected in the Layers panel ignores the arrow keys and the nudge commands while its properties still edit, and a child of a locked layer keeps its padlock inert (the Layers panel padlock, Lock/Unlock and `object.toggleLock` without a value no longer lock the child on its own) |
| 2026-11-10 | patch | #2217: the main menu's submenus (Object, say) ran past the window's bottom, behind the taskbar, with the lowest commands out of reach, because submenus were not constrained to the window (to keep them beside their row). Every menu panel now scrolls when taller than the window less 8 pt margins, and a submenu is held within the window's height (moved up, never above the top) while keeping its place beside its row, so menu-aim is unchanged. Tests: `menu_overflow_tests.rs`. Numbers unchanged. |
| 2026-11-02 | patch | Shortcuts: a Cmd/Ctrl-click on a padding field (the horizontal or vertical pair, or a side with Individual padding open) makes the value typed go to all four sides, in one undo step (Guide to auto layout, Keyboard shortcut guide); a plain click still edits that axis or side alone. A documented gesture row: 134 documented, 133 bound. Test: `padding_individual_tests` |