From 4af26f085280f1e3a889400b443592f881081445 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 17 Dec 2025 13:42:43 +0100 Subject: [PATCH] Fix tab bar button flickering when opening menus (#45098) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #33018 ### Problem When opening a `PopoverMenu` or `RightClickMenu`, the pane's tab bar buttons would flicker (disappear for a couple frames then reappear). This happened because: 1. The menu is created and `window.focus()` was called immediately 2. However, menus are rendered using `deferred()`, so their focus handles aren't connected in the dispatch tree until after the deferred draw callback runs 3. When the pane checks `has_focus()`, it calls `contains_focused()` which walks up the focus hierarchy — but the menu's focus handle isn't linked yet 4. `has_focus()` returns false → tab bar buttons disappear 5. Next frame, the menu is rendered and linked → `has_focus()` returns true → buttons reappear ### Solution Delay the focus transfer by 2 frames using nested `on_next_frame()` calls before focusing the menu. **Why 2 frames instead of 1?** The frame lifecycle in GPUI runs `next_frame_callbacks` BEFORE `draw()`: ``` on_request_frame: 1. Run next_frame_callbacks 2. window.draw() ← menu rendered here via deferred() 3. Present ``` So: - **Frame 1**: First `on_next_frame` callback runs, queues second callback. Then `draw()` renders the menu and connects its focus handle to the dispatch tree. - **Frame 2**: Second `on_next_frame` callback runs and focuses the menu. Now the focus handle is connected (from Frame 1's draw), so `contains_focused()` returns true. With only 1 frame, the focus would happen BEFORE `draw()`, when the menu's focus handle isn't connected yet. This follows the same pattern established in b709996ec6 which fixed the identical issue for the editor's `MouseContextMenu`. --- crates/ui/src/components/popover_menu.rs | 14 +++++++++++++- crates/ui/src/components/right_click_menu.rs | 14 +++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs index b1a52bec8fdf1f7030b5b321bed7702d602ff212..7fdb39126b5244e367a4646c6ef6df1547a8a52f 100644 --- a/crates/ui/src/components/popover_menu.rs +++ b/crates/ui/src/components/popover_menu.rs @@ -287,7 +287,19 @@ fn show_menu( window.refresh(); }) .detach(); - window.focus(&new_menu.focus_handle(cx)); + + // Since menus are rendered in a deferred fashion, their focus handles are + // not linked in the dispatch tree until after the deferred draw callback + // runs. We need to wait for that to happen before focusing it, so that + // calling `contains_focused` on the parent's focus handle returns `true` + // when the menu is focused. This prevents the pane's tab bar buttons from + // flickering when opening popover menus. + let focus_handle = new_menu.focus_handle(cx); + window.on_next_frame(move |window, _cx| { + window.on_next_frame(move |window, _cx| { + window.focus(&focus_handle); + }); + }); *menu.borrow_mut() = Some(new_menu); window.refresh(); diff --git a/crates/ui/src/components/right_click_menu.rs b/crates/ui/src/components/right_click_menu.rs index dff423073710121bb0bc0fafdb8ab3108b746bde..5b654c295e8c9721cd38af8b4807ba5d8e6d6cb9 100644 --- a/crates/ui/src/components/right_click_menu.rs +++ b/crates/ui/src/components/right_click_menu.rs @@ -259,7 +259,19 @@ impl Element for RightClickMenu { window.refresh(); }) .detach(); - window.focus(&new_menu.focus_handle(cx)); + + // Since menus are rendered in a deferred fashion, their focus handles are + // not linked in the dispatch tree until after the deferred draw callback + // runs. We need to wait for that to happen before focusing it, so that + // calling `contains_focused` on the parent's focus handle returns `true` + // when the menu is focused. This prevents the pane's tab bar buttons from + // flickering when opening menus. + let focus_handle = new_menu.focus_handle(cx); + window.on_next_frame(move |window, _cx| { + window.on_next_frame(move |window, _cx| { + window.focus(&focus_handle); + }); + }); *menu.borrow_mut() = Some(new_menu); *position.borrow_mut() = if let Some(child_bounds) = child_bounds { if let Some(attach) = attach {