Merge pull request #2093 from TrevorBrown-dev/fix/submenu-opens-without-delay

Menus: a submenu opens as soon as its row is hovered, as measured in Figma
This commit is contained in:
BFlat authored and GitHub committed 2026-11-12 11:20:15 +07:00
commit 46467fb536
3 files changed
+17 -18

No files matched your search

+9 -13
View File
@@ -1,7 +1,7 @@
//! Which submenu the main and context menus show, driven through egui headless: a closed menu
//! reopens with no submenu open (also one level down), hovering another row closes the open
//! submenu at once while a submenu opens only once the pointer stays on its row a moment
//! (sweeping across rows doesn't flash each one), and on the way from a
//! submenu at once and a submenu opens on the first frame its row is hovered (as Figma's does),
//! and on the way from a
//! submenu's row to its submenu the rows the pointer crosses don't take over (the safe triangle),
//! unless it rests on one or turns away.
@@ -214,7 +214,7 @@ fn resting_on_a_crossed_row_hands_over_to_it() {
}
#[test]
fn moving_straight_down_switches_after_a_short_hover() {
fn moving_straight_down_switches_at_once() {
let mut u = Ui::new();
u.open_main_menu();
let file = u.top_row(0);
@@ -227,24 +227,21 @@ fn moving_straight_down_switches_after_a_short_hover() {
p.y = (p.y + 4.0).min(edit.y);
u.hover(p);
}
assert!(u.tops_shown().is_empty(), "File closes at once, Edit waits: {:?}", u.tops_shown());
u.wait(HOVER_DELAY + 0.05);
assert_eq!(u.tops_shown(), ["Edit"]);
u.frame(vec![]);
assert_eq!(u.tops_shown(), ["Edit"], "File closes at once, Edit opens on its first frame");
}
#[test]
fn the_first_submenu_opens_after_a_short_hover() {
fn the_first_submenu_opens_on_the_next_frame() {
let mut u = Ui::new();
u.open_main_menu();
u.hover(u.top_row(0).center());
u.frame(vec![]);
assert!(u.tops_shown().is_empty(), "opened at once");
u.wait(HOVER_DELAY + 0.05);
assert_eq!(u.tops_shown(), ["File"]);
assert_eq!(u.tops_shown(), ["File"], "Figma opens it on the first frame (median 11 ms)");
}
#[test]
fn sweeping_across_rows_opens_only_where_the_pointer_stops() {
fn sweeping_across_rows_shows_only_the_row_under_the_pointer() {
let mut u = Ui::new();
u.open_main_menu();
u.settle(u.top_row(0).center());
@@ -254,9 +251,8 @@ fn sweeping_across_rows_opens_only_where_the_pointer_stops() {
u.hover(pos2(r.min.x + 20.0, r.center().y));
// (A panel drawn above the row the pointer is on goes the next frame.)
u.frame(vec![]);
assert!(u.tops_shown().is_empty(), "{:?} open on row {i}", u.tops_shown());
assert_eq!(u.tops_shown(), [TOPS[i]], "wrong submenu open on row {i}");
}
u.wait(HOVER_DELAY + 0.05);
assert_eq!(u.tops_shown(), ["Arrange"]);
}
+6 -4
View File
@@ -11,8 +11,8 @@ pub(crate) fn submenu<R>(ui: &mut Ui, key: &str, label: &str, t: &Tokens, add: i
// Which submenu is open: hover intent with a safe triangle.
//
// Every menu tree (the main menu, the context menu) keeps, per level, which of its submenus is
// open. Hovering another row closes the open submenu at once; resting on a submenu row for
// `HOVER_DELAY` opens it (sweeping across rows doesn't flash every submenu on the way). Except while the
// open. Hovering a row closes the open submenu at once and opens the row's own submenu on the
// same frame (as Figma's main menu does, measured; `HOVER_DELAY` is zero). Except while the
// pointer is on its way from the open submenu's row to the submenu: while each move lands inside
// the triangle between where it was and the submenu's near edge (it keeps getting nearer), rows
// it crosses don't take over and the submenu's row stays highlighted. Resting (`AIM_REST`) or
@@ -20,8 +20,10 @@ pub(crate) fn submenu<R>(ui: &mut Ui, key: &str, label: &str, t: &Tokens, add: i
// last submenu open.
// Closing a menu tree forgets all of it, so it reopens with no submenu open.
/// How long the pointer stays on a submenu row before its submenu opens.
pub(crate) const HOVER_DELAY: f64 = 0.15;
/// How long the pointer stays on a submenu row before its submenu opens. None: Figma's main menu
/// opens it on the first frame (median 11 ms measured), so the delay is zero; kept as a constant so
/// the pending-row bookkeeping the aim relies on stays as it was.
pub(crate) const HOVER_DELAY: f64 = 0.0;
/// How long the pointer may rest inside the safe triangle before the row under it takes over.
pub(crate) const AIM_REST: f64 = 0.3;
+2 -1
View File
@@ -25,7 +25,7 @@ interaction feel not yet compared.
| Actions palette (Cmd+K, Cmd+/) | done | tabs: All (every command, grouped by menu, or Other; a command that needs parameters only through its menu item, and none twice under one label, #1574), Assets (components and styles) and Plugins & widgets; results ranked prefix first, then shortest, at most 60 rows; Up/Down move a highlight and Enter runs it; Recents list what was run from the palette (matching translated labels); one open/toggle path from every entry point (`palette_tests`). Ranking and the Assets results not compared with Figma's |
| Context menus | done | canvas and layers; submenus as in the main menu below, and a right-click in a new spot starts a fresh menu (Select layer closed); Shift+F10 or the keyboard's Menu key opens them from the keyboard (at the focused layer row or below the canvas selection) and the menu takes the keyboard (`layers_keyboard_tests`) |
| Home screen: Recents, Online and Templates | done (ours) | The sidebar switches between the three (`ui.set {homeSection}`). Online: your online files (see the collaboration checklist). Recents: new file, open, join with a code, open files, recent files (cards with the picture each file carries on its page colour, the file's icon, name and "Edited ... ago" or "File not found", full path on hover; or a Name, Last modified and Created table with sortable headers; sorted by name, creation or last change either way; grid or list and the sort kept in the preferences, `ui.set {homeRecentsView, homeRecentsSort, homeRecentsOrder}`; a click selects, a double-click or Enter opens, arrow keys move the selection; Open, Show in folder and Remove from recents on right-click), a Getting started card (six tips with their shortcuts from the keymap; its close button is remembered in the preferences and Help ▸ Getting started brings it back) and, while there are no recent files, four templates to start from. Templates: the eight starter templates as cards (thumbnail rendered from the document, cached per theme; name, description, kind and size), a search field and a kind filter, an empty state; Tab reaches every card and Enter opens it; cards and tabs are named for screen readers. A first run opens here. No Community files or drafts folder |
| Main menu from the logo | done | generated from `catalog.rs`; popups sized to content. Submenus open after 0.15 s on their row (a click or the keyboard opens one at once) and close at once when another row is hovered; while the pointer heads for an open submenu inside the triangle to its near edge, the rows it crosses don't take over, and resting 0.3 s on one hands over; closing the menu, however it closes, clears which submenus were open, so it reopens with none (`menu_aim_tests`, 10 tests; timings tuned by feel, not measured against Figma). A menu or submenu that would run past the window's bottom moves up to fit (never above the top), and one taller than the window scrolls with the wheel, the keyboard's focus kept in view; the context menu's submenus share this (#2217, `menu_overflow_tests`). Menu and palette items that need a value ask for it first: Create link, Offset path and Move to page in a dialog of the shared controls, the language, user name, server and nudge items in Preferences, Open recent on the home screen's Recents; File ▸ Rename edits the file name in place |
| Main menu from the logo | done | generated from `catalog.rs`; popups sized to content. Submenus open on the first frame their row is hovered (a click or the keyboard opens one at once; Figma's main menu measured 2026-11-07 with synthetic pointer events: median 11 ms to open, 13 ms to switch rows) and close at once when another row is hovered; while the pointer heads for an open submenu inside the triangle to its near edge, the rows it crosses don't take over, and resting 0.3 s on one hands over; closing the menu, however it closes, clears which submenus were open, so it reopens with none (`menu_aim_tests`, 10 tests; the open delay is measured; the safe triangle and its 0.3 s rest are still tuned by feel, and Figma's main menu showed no triangle at all in that measurement, so the aim is kept pending the maintainer's decision). A menu or submenu that would run past the window's bottom moves up to fit (never above the top), and one taller than the window scrolls with the wheel, the keyboard's focus kept in view; the context menu's submenus share this (#2217, `menu_overflow_tests`). Menu and palette items that need a value ask for it first: Create link, Offset path and Move to page in a dialog of the shared controls, the language, user name, server and nudge items in Preferences, Open recent on the home screen's Recents; File ▸ Rename edits the file name in place |
| Keyboard shortcuts panel | done | closes when the home screen shows (#548); docked, tabbed by group like the documentation, searchable, per-platform keycaps; held keys, gestures and presentation keys listed beside the bindings; a label too long for its column ends with "…" and shows in full on hover |
| Several windows | partial | File ▸ New window, Open in new window on a tab, a tab dragged off the strip; screen-reader tree in the main window only; positions not restored |
| Multiplayer avatars, Share button | missing | waits for multiplayer |
@@ -193,6 +193,7 @@ in the table. `crates/ui-egui/src/shortcut_keys_tests.rs` drives the keys a look
| 2026-11-11 | patch | M5, gap 4: marquee selection as measured in Figma's web app (Select layers and objects, Selection marquee): a partly covered top-level frame or section gives the layers in it the marquee touches (it was selected only when enclosed, so nothing came), an empty frame is selected on touch, a rotated shape is touched by its outline and not its box, a drag from inside a frame selects that frame only when the marquee also takes another top-level frame (then whole), and also takes the top-level layers it touches outside it, and selecting a frame whole keeps the marquee at the top level (a partly covered frame then comes whole, not its layers). Flipped: `marquee_selects_what_it_touches`, `cmd_marquee_selects_nested_layers`, `a_box_selects_like_a_marquee`. Tests: `marquee_behaviour_tests`. Counts unchanged |
| 2026-11-08 | minor | Vector edit mode, #1875, measured in Figma (web, real keys): Copy with segments selected takes only those (with just a point selected, the segments touching it) and a paste outside edit mode is a new vector layer in place; Cut removes them, the path splitting into subpaths in the same layer with edit mode staying on, and a paste outside gives a new layer with the cut segments; Paste while editing a vector adds the clipboard's segments, or a copied layer's outline, to the same network in place, selecting the pasted points, with no new layer and the bounds growing. FigCraft copied the whole layer, Cut left nothing to paste and Paste did nothing. `edit.copy`, `edit.cut` and `edit.paste` act so with no `ids`/`parent` given (Ctrl+X is the real Cut again; Shift+Delete still heals); the copy carries the layer's look (fills, strokes, effects, opacity, blend; the width profile only when every segment is copied); layers with no outline are skipped. Tests: `vector_clipboard_tests` |
| 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-07 | patch | Main menu: a submenu opens on the first frame its row is hovered instead of after 0.15 s (Figma's main menu measured with synthetic pointer events and 1 ms polling: median 11 ms to open, 13 ms to switch rows, no hover delay); the safe-triangle aim is unchanged, though the same measurement saw no aim logic in Figma, left for the maintainer to decide |
| 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` |