Selection: Escape clears the selection instead of stepping out to the parent, as in Figma

Measured in Figma: with a layer inside a group inside a frame selected (from the canvas, from the Layers panel, or after Tab), one Escape selects nothing; FigCraft selected the parent first. Escape still leaves text and vector editing, ends gestures and the pen first. Three tests updated to the measured rule (selection_behaviour, devfocus, proto_editor); the rest of the suites pass.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Opus 5.5 briefed; Sonnet 5.5 wrote at low
This commit is contained in:
Trevor BrownandClaude Sonnet 5.5 committed 2026-11-08 00:11:49 -05:00
1 parent 3635c23070
commit a285fd293b
5 files changed
+16 -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(())
}
+3 -3
View File
@@ -84,12 +84,12 @@ 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");
}
@@ -271,14 +271,12 @@ 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());
}
#[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`) |
| 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 |
@@ -186,6 +187,7 @@ in the table. `crates/ui-egui/src/shortcut_keys_tests.rs` drives the keys a look
## Revision history
| 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-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` |
| 2026-11-01 | patch | Number fields: dragging the field itself with Alt held scrubs it ("You can also hover the input field itself and hold down the modifier key: Mac: ⌥ Option Windows: Alt", Adjust alignment, rotation, position, and dimensions), with the resize cursor and the label's rates (Alt is the enabler there, so it isn't the fine rate); a click with Alt doesn't open the editor. Found by a documentation audit of the ui-parity rows. Test: `field_tests::alt_held_over_a_field_scrubs_it`. Counts unchanged |