ui: Use popover menus for tab bar in panes (#16497)

Piotr Osiewicz created

Closes #ISSUE

Release Notes:

- N/A

Change summary

crates/activity_indicator/src/activity_indicator.rs | 157 +++---
crates/assistant/src/assistant_panel.rs             |  37 
crates/quick_action_bar/src/quick_action_bar.rs     | 320 ++++++--------
crates/terminal_view/src/terminal_panel.rs          |  63 +-
crates/ui/src/components/popover_menu.rs            |  24 +
crates/workspace/src/pane.rs                        |  81 +--
6 files changed, 325 insertions(+), 357 deletions(-)

Detailed changes

crates/activity_indicator/src/activity_indicator.rs 🔗

@@ -3,10 +3,9 @@ use editor::Editor;
 use extension::ExtensionStore;
 use futures::StreamExt;
 use gpui::{
-    actions, anchored, deferred, percentage, Animation, AnimationExt as _, AppContext, CursorStyle,
-    DismissEvent, EventEmitter, InteractiveElement as _, Model, ParentElement as _, Render,
-    SharedString, StatefulInteractiveElement, Styled, Transformation, View, ViewContext,
-    VisualContext as _,
+    actions, percentage, Animation, AnimationExt as _, AppContext, CursorStyle, EventEmitter,
+    InteractiveElement as _, Model, ParentElement as _, Render, SharedString,
+    StatefulInteractiveElement, Styled, Transformation, View, ViewContext, VisualContext as _,
 };
 use language::{
     LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId, LanguageServerName,
@@ -14,7 +13,7 @@ use language::{
 use project::{LanguageServerProgress, Project};
 use smallvec::SmallVec;
 use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
-use ui::{prelude::*, ContextMenu};
+use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle};
 use workspace::{item::ItemHandle, StatusItemView, Workspace};
 
 actions!(activity_indicator, [ShowErrorMessage]);
@@ -27,7 +26,7 @@ pub struct ActivityIndicator {
     statuses: Vec<LspStatus>,
     project: Model<Project>,
     auto_updater: Option<Model<AutoUpdater>>,
-    context_menu: Option<View<ContextMenu>>,
+    context_menu_handle: PopoverMenuHandle<ContextMenu>,
 }
 
 struct LspStatus {
@@ -79,7 +78,7 @@ impl ActivityIndicator {
                 statuses: Default::default(),
                 project: project.clone(),
                 auto_updater,
-                context_menu: None,
+                context_menu_handle: Default::default(),
             }
         });
 
@@ -368,72 +367,7 @@ impl ActivityIndicator {
     }
 
     fn toggle_language_server_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
-        if self.context_menu.take().is_some() {
-            return;
-        }
-
-        self.build_lsp_work_context_menu(cx);
-        cx.notify();
-    }
-
-    fn build_lsp_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
-        let mut has_work = false;
-        let this = cx.view().downgrade();
-        let context_menu = ContextMenu::build(cx, |mut menu, cx| {
-            for work in self.pending_language_server_work(cx) {
-                has_work = true;
-
-                let this = this.clone();
-                let title = SharedString::from(
-                    work.progress
-                        .title
-                        .as_deref()
-                        .unwrap_or(work.progress_token)
-                        .to_string(),
-                );
-                if work.progress.is_cancellable {
-                    let language_server_id = work.language_server_id;
-                    let token = work.progress_token.to_string();
-                    menu = menu.custom_entry(
-                        move |_| {
-                            h_flex()
-                                .w_full()
-                                .justify_between()
-                                .child(Label::new(title.clone()))
-                                .child(Icon::new(IconName::XCircle))
-                                .into_any_element()
-                        },
-                        move |cx| {
-                            this.update(cx, |this, cx| {
-                                this.project.update(cx, |project, cx| {
-                                    project.cancel_language_server_work(
-                                        language_server_id,
-                                        Some(token.clone()),
-                                        cx,
-                                    );
-                                });
-                                this.context_menu.take();
-                            })
-                            .ok();
-                        },
-                    );
-                } else {
-                    menu = menu.label(title.clone());
-                }
-            }
-            menu
-        });
-
-        if has_work {
-            cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
-                this.context_menu.take();
-                cx.notify();
-            })
-            .detach();
-            cx.focus_view(&context_menu);
-            self.context_menu = Some(context_menu);
-            cx.notify();
-        }
+        self.context_menu_handle.toggle(cx);
     }
 }
 
@@ -455,19 +389,72 @@ impl Render for ActivityIndicator {
                     on_click(this, cx);
                 }))
         }
-
-        result
-            .gap_2()
-            .children(content.icon)
-            .child(Label::new(SharedString::from(content.message)).size(LabelSize::Small))
-            .children(self.context_menu.as_ref().map(|menu| {
-                deferred(
-                    anchored()
-                        .anchor(gpui::AnchorCorner::BottomLeft)
-                        .child(menu.clone()),
+        let this = cx.view().downgrade();
+        result.gap_2().child(
+            PopoverMenu::new("activity-indicator-popover")
+                .trigger(
+                    ButtonLike::new("activity-indicator-trigger").child(
+                        h_flex()
+                            .gap_2()
+                            .children(content.icon)
+                            .child(Label::new(content.message).size(LabelSize::Small)),
+                    ),
                 )
-                .with_priority(1)
-            }))
+                .anchor(gpui::AnchorCorner::BottomLeft)
+                .menu(move |cx| {
+                    let strong_this = this.upgrade()?;
+                    ContextMenu::build(cx, |mut menu, cx| {
+                        for work in strong_this.read(cx).pending_language_server_work(cx) {
+                            let this = this.clone();
+                            let mut title = work
+                                .progress
+                                .title
+                                .as_deref()
+                                .unwrap_or(work.progress_token)
+                                .to_owned();
+
+                            if work.progress.is_cancellable {
+                                let language_server_id = work.language_server_id;
+                                let token = work.progress_token.to_string();
+                                let title = SharedString::from(title);
+                                menu = menu.custom_entry(
+                                    move |_| {
+                                        h_flex()
+                                            .w_full()
+                                            .justify_between()
+                                            .child(Label::new(title.clone()))
+                                            .child(Icon::new(IconName::XCircle))
+                                            .into_any_element()
+                                    },
+                                    move |cx| {
+                                        this.update(cx, |this, cx| {
+                                            this.project.update(cx, |project, cx| {
+                                                project.cancel_language_server_work(
+                                                    language_server_id,
+                                                    Some(token.clone()),
+                                                    cx,
+                                                );
+                                            });
+                                            this.context_menu_handle.hide(cx);
+                                            cx.notify();
+                                        })
+                                        .ok();
+                                    },
+                                );
+                            } else {
+                                if let Some(progress_message) = work.progress.message.as_ref() {
+                                    title.push_str(": ");
+                                    title.push_str(progress_message);
+                                }
+
+                                menu = menu.label(title);
+                            }
+                        }
+                        menu
+                    })
+                    .into()
+                }),
+        )
     }
 }
 

crates/assistant/src/assistant_panel.rs 🔗

@@ -36,10 +36,10 @@ use fs::Fs;
 use gpui::{
     canvas, div, img, percentage, point, pulsating_between, size, Action, Animation, AnimationExt,
     AnyElement, AnyView, AppContext, AsyncWindowContext, ClipboardEntry, ClipboardItem,
-    Context as _, DismissEvent, Empty, Entity, EntityId, EventEmitter, FocusHandle, FocusableView,
-    FontWeight, InteractiveElement, IntoElement, Model, ParentElement, Pixels, ReadGlobal, Render,
-    RenderImage, SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task,
-    Transformation, UpdateGlobal, View, VisualContext, WeakView, WindowContext,
+    Context as _, Empty, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, FontWeight,
+    InteractiveElement, IntoElement, Model, ParentElement, Pixels, ReadGlobal, Render, RenderImage,
+    SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
+    UpdateGlobal, View, VisualContext, WeakView, WindowContext,
 };
 use indexed_docs::IndexedDocsStore;
 use language::{
@@ -349,6 +349,7 @@ impl AssistantPanel {
                 model_summary_editor.clone(),
             )
         });
+
         let pane = cx.new_view(|cx| {
             let mut pane = Pane::new(
                 workspace.weak_handle(),
@@ -385,6 +386,7 @@ impl AssistantPanel {
                         pane.active_item()
                             .map_or(false, |item| item.downcast::<ContextHistory>().is_some()),
                     );
+                let _pane = cx.view().clone();
                 let right_children = h_flex()
                     .gap(Spacing::Small.rems(cx))
                     .child(
@@ -395,32 +397,27 @@ impl AssistantPanel {
                             .tooltip(|cx| Tooltip::for_action("New Context", &NewFile, cx)),
                     )
                     .child(
-                        IconButton::new("menu", IconName::Menu)
-                            .icon_size(IconSize::Small)
-                            .on_click(cx.listener(|pane, _, cx| {
-                                let zoom_label = if pane.is_zoomed() {
+                        PopoverMenu::new("assistant-panel-popover-menu")
+                            .trigger(
+                                IconButton::new("menu", IconName::Menu).icon_size(IconSize::Small),
+                            )
+                            .menu(move |cx| {
+                                let zoom_label = if _pane.read(cx).is_zoomed() {
                                     "Zoom Out"
                                 } else {
                                     "Zoom In"
                                 };
-                                let menu = ContextMenu::build(cx, |menu, cx| {
-                                    menu.context(pane.focus_handle(cx))
+                                let focus_handle = _pane.focus_handle(cx);
+                                Some(ContextMenu::build(cx, move |menu, _| {
+                                    menu.context(focus_handle.clone())
                                         .action("New Context", Box::new(NewFile))
                                         .action("History", Box::new(DeployHistory))
                                         .action("Prompt Library", Box::new(DeployPromptLibrary))
                                         .action("Configure", Box::new(ShowConfiguration))
                                         .action(zoom_label, Box::new(ToggleZoom))
-                                });
-                                cx.subscribe(&menu, |pane, _, _: &DismissEvent, _| {
-                                    pane.new_item_menu = None;
-                                })
-                                .detach();
-                                pane.new_item_menu = Some(menu);
-                            })),
+                                }))
+                            }),
                     )
-                    .when_some(pane.new_item_menu.as_ref(), |el, new_item_menu| {
-                        el.child(Pane::render_menu_overlay(new_item_menu))
-                    })
                     .into_any_element()
                     .into();
 

crates/quick_action_bar/src/quick_action_bar.rs 🔗

@@ -8,13 +8,14 @@ use editor::actions::{
 use editor::{Editor, EditorSettings};
 
 use gpui::{
-    anchored, deferred, Action, AnchorCorner, ClickEvent, DismissEvent, ElementId, EventEmitter,
-    InteractiveElement, ParentElement, Render, Styled, Subscription, View, ViewContext, WeakView,
+    Action, AnchorCorner, ClickEvent, ElementId, EventEmitter, InteractiveElement, ParentElement,
+    Render, Styled, Subscription, View, ViewContext, WeakView,
 };
 use search::{buffer_search, BufferSearchBar};
 use settings::{Settings, SettingsStore};
 use ui::{
-    prelude::*, ButtonStyle, ContextMenu, IconButton, IconButtonShape, IconName, IconSize, Tooltip,
+    prelude::*, ButtonStyle, ContextMenu, IconButton, IconButtonShape, IconName, IconSize,
+    PopoverMenu, PopoverMenuHandle, Tooltip,
 };
 use workspace::{
     item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
@@ -27,10 +28,9 @@ pub struct QuickActionBar {
     _inlay_hints_enabled_subscription: Option<Subscription>,
     active_item: Option<Box<dyn ItemHandle>>,
     buffer_search_bar: View<BufferSearchBar>,
-    repl_menu: Option<View<ContextMenu>>,
     show: bool,
-    toggle_selections_menu: Option<View<ContextMenu>>,
-    toggle_settings_menu: Option<View<ContextMenu>>,
+    toggle_selections_handle: PopoverMenuHandle<ContextMenu>,
+    toggle_settings_handle: PopoverMenuHandle<ContextMenu>,
     workspace: WeakView<Workspace>,
 }
 
@@ -44,10 +44,9 @@ impl QuickActionBar {
             _inlay_hints_enabled_subscription: None,
             active_item: None,
             buffer_search_bar,
-            repl_menu: None,
             show: true,
-            toggle_selections_menu: None,
-            toggle_settings_menu: None,
+            toggle_selections_handle: Default::default(),
+            toggle_settings_handle: Default::default(),
             workspace: workspace.weak_handle(),
         };
         this.apply_settings(cx);
@@ -79,17 +78,6 @@ impl QuickActionBar {
             ToolbarItemLocation::Hidden
         }
     }
-
-    fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
-        div().absolute().bottom_0().right_0().size_0().child(
-            deferred(
-                anchored()
-                    .anchor(AnchorCorner::TopRight)
-                    .child(menu.clone()),
-            )
-            .with_priority(1),
-        )
-    }
 }
 
 impl Render for QuickActionBar {
@@ -158,150 +146,155 @@ impl Render for QuickActionBar {
         );
 
         let editor_selections_dropdown = selection_menu_enabled.then(|| {
-            IconButton::new("toggle_editor_selections_icon", IconName::TextCursor)
-                .shape(IconButtonShape::Square)
-                .icon_size(IconSize::Small)
-                .style(ButtonStyle::Subtle)
-                .selected(self.toggle_selections_menu.is_some())
-                .on_click({
-                    let focus = editor.focus_handle(cx);
-                    cx.listener(move |quick_action_bar, _, cx| {
-                        let focus = focus.clone();
-                        let menu = ContextMenu::build(cx, move |menu, _| {
-                            menu.context(focus.clone())
-                                .action("Select All", Box::new(SelectAll))
-                                .action(
-                                    "Select Next Occurrence",
-                                    Box::new(SelectNext {
-                                        replace_newest: false,
-                                    }),
-                                )
-                                .action("Expand Selection", Box::new(SelectLargerSyntaxNode))
-                                .action("Shrink Selection", Box::new(SelectSmallerSyntaxNode))
-                                .action("Add Cursor Above", Box::new(AddSelectionAbove))
-                                .action("Add Cursor Below", Box::new(AddSelectionBelow))
-                                .separator()
-                                .action("Go to Symbol", Box::new(ToggleOutline))
-                                .action("Go to Line/Column", Box::new(ToggleGoToLine))
-                                .separator()
-                                .action("Next Problem", Box::new(GoToDiagnostic))
-                                .action("Previous Problem", Box::new(GoToPrevDiagnostic))
-                                .separator()
-                                .action("Next Hunk", Box::new(GoToHunk))
-                                .action("Previous Hunk", Box::new(GoToPrevHunk))
-                                .separator()
-                                .action("Move Line Up", Box::new(MoveLineUp))
-                                .action("Move Line Down", Box::new(MoveLineDown))
-                                .action("Duplicate Selection", Box::new(DuplicateLineDown))
-                        });
-                        cx.subscribe(&menu, |quick_action_bar, _, _: &DismissEvent, _cx| {
-                            quick_action_bar.toggle_selections_menu = None;
-                        })
-                        .detach();
-                        quick_action_bar.toggle_selections_menu = Some(menu);
-                    })
-                })
-                .when(self.toggle_selections_menu.is_none(), |this| {
-                    this.tooltip(|cx| Tooltip::text("Selection Controls", cx))
+            let focus = editor.focus_handle(cx);
+            PopoverMenu::new("editor-selections-dropdown")
+                .trigger(
+                    IconButton::new("toggle_editor_selections_icon", IconName::TextCursor)
+                        .shape(IconButtonShape::Square)
+                        .icon_size(IconSize::Small)
+                        .style(ButtonStyle::Subtle)
+                        .selected(self.toggle_selections_handle.is_deployed())
+                        .when(!self.toggle_selections_handle.is_deployed(), |this| {
+                            this.tooltip(|cx| Tooltip::text("Selection Controls", cx))
+                        }),
+                )
+                .with_handle(self.toggle_selections_handle.clone())
+                .anchor(AnchorCorner::TopRight)
+                .menu(move |cx| {
+                    let focus = focus.clone();
+                    let menu = ContextMenu::build(cx, move |menu, _| {
+                        menu.context(focus.clone())
+                            .action("Select All", Box::new(SelectAll))
+                            .action(
+                                "Select Next Occurrence",
+                                Box::new(SelectNext {
+                                    replace_newest: false,
+                                }),
+                            )
+                            .action("Expand Selection", Box::new(SelectLargerSyntaxNode))
+                            .action("Shrink Selection", Box::new(SelectSmallerSyntaxNode))
+                            .action("Add Cursor Above", Box::new(AddSelectionAbove))
+                            .action("Add Cursor Below", Box::new(AddSelectionBelow))
+                            .separator()
+                            .action("Go to Symbol", Box::new(ToggleOutline))
+                            .action("Go to Line/Column", Box::new(ToggleGoToLine))
+                            .separator()
+                            .action("Next Problem", Box::new(GoToDiagnostic))
+                            .action("Previous Problem", Box::new(GoToPrevDiagnostic))
+                            .separator()
+                            .action("Next Hunk", Box::new(GoToHunk))
+                            .action("Previous Hunk", Box::new(GoToPrevHunk))
+                            .separator()
+                            .action("Move Line Up", Box::new(MoveLineUp))
+                            .action("Move Line Down", Box::new(MoveLineDown))
+                            .action("Duplicate Selection", Box::new(DuplicateLineDown))
+                    });
+                    Some(menu)
                 })
         });
 
-        let editor_settings_dropdown =
-            IconButton::new("toggle_editor_settings_icon", IconName::Sliders)
-                .shape(IconButtonShape::Square)
-                .icon_size(IconSize::Small)
-                .style(ButtonStyle::Subtle)
-                .selected(self.toggle_settings_menu.is_some())
-                .on_click({
-                    let editor = editor.clone();
-                    cx.listener(move |quick_action_bar, _, cx| {
-                        let menu = ContextMenu::build(cx, |mut menu, _| {
-                            if supports_inlay_hints {
-                                menu = menu.toggleable_entry(
-                                    "Inlay Hints",
-                                    inlay_hints_enabled,
-                                    IconPosition::Start,
-                                    Some(editor::actions::ToggleInlayHints.boxed_clone()),
-                                    {
-                                        let editor = editor.clone();
-                                        move |cx| {
-                                            editor.update(cx, |editor, cx| {
-                                                editor.toggle_inlay_hints(
-                                                    &editor::actions::ToggleInlayHints,
-                                                    cx,
-                                                );
-                                            });
-                                        }
-                                    },
-                                );
-                            }
-
-                            menu = menu.toggleable_entry(
-                                "Inline Git Blame",
-                                git_blame_inline_enabled,
-                                IconPosition::Start,
-                                Some(editor::actions::ToggleGitBlameInline.boxed_clone()),
-                                {
-                                    let editor = editor.clone();
-                                    move |cx| {
-                                        editor.update(cx, |editor, cx| {
-                                            editor.toggle_git_blame_inline(
-                                                &editor::actions::ToggleGitBlameInline,
+        let editor = editor.downgrade();
+        let editor_settings_dropdown = PopoverMenu::new("editor-settings")
+            .trigger(
+                IconButton::new("toggle_editor_settings_icon", IconName::Sliders)
+                    .shape(IconButtonShape::Square)
+                    .icon_size(IconSize::Small)
+                    .style(ButtonStyle::Subtle)
+                    .selected(self.toggle_settings_handle.is_deployed())
+                    .when(!self.toggle_settings_handle.is_deployed(), |this| {
+                        this.tooltip(|cx| Tooltip::text("Editor Controls", cx))
+                    }),
+            )
+            .anchor(AnchorCorner::TopRight)
+            .with_handle(self.toggle_settings_handle.clone())
+            .menu(move |cx| {
+                let menu = ContextMenu::build(cx, |mut menu, _| {
+                    if supports_inlay_hints {
+                        menu = menu.toggleable_entry(
+                            "Inlay Hints",
+                            inlay_hints_enabled,
+                            IconPosition::Start,
+                            Some(editor::actions::ToggleInlayHints.boxed_clone()),
+                            {
+                                let editor = editor.clone();
+                                move |cx| {
+                                    editor
+                                        .update(cx, |editor, cx| {
+                                            editor.toggle_inlay_hints(
+                                                &editor::actions::ToggleInlayHints,
                                                 cx,
-                                            )
-                                        });
-                                    }
-                                },
-                            );
+                                            );
+                                        })
+                                        .ok();
+                                }
+                            },
+                        );
+                    }
 
-                            menu = menu.toggleable_entry(
-                                "Selection Menu",
-                                selection_menu_enabled,
-                                IconPosition::Start,
-                                Some(editor::actions::ToggleSelectionMenu.boxed_clone()),
-                                {
-                                    let editor = editor.clone();
-                                    move |cx| {
-                                        editor.update(cx, |editor, cx| {
-                                            editor.toggle_selection_menu(
-                                                &editor::actions::ToggleSelectionMenu,
-                                                cx,
-                                            )
-                                        });
-                                    }
-                                },
-                            );
+                    menu = menu.toggleable_entry(
+                        "Inline Git Blame",
+                        git_blame_inline_enabled,
+                        IconPosition::Start,
+                        Some(editor::actions::ToggleGitBlameInline.boxed_clone()),
+                        {
+                            let editor = editor.clone();
+                            move |cx| {
+                                editor
+                                    .update(cx, |editor, cx| {
+                                        editor.toggle_git_blame_inline(
+                                            &editor::actions::ToggleGitBlameInline,
+                                            cx,
+                                        )
+                                    })
+                                    .ok();
+                            }
+                        },
+                    );
 
-                            menu = menu.toggleable_entry(
-                                "Auto Signature Help",
-                                auto_signature_help_enabled,
-                                IconPosition::Start,
-                                Some(editor::actions::ToggleAutoSignatureHelp.boxed_clone()),
-                                {
-                                    let editor = editor.clone();
-                                    move |cx| {
-                                        editor.update(cx, |editor, cx| {
-                                            editor.toggle_auto_signature_help_menu(
-                                                &editor::actions::ToggleAutoSignatureHelp,
-                                                cx,
-                                            );
-                                        });
-                                    }
-                                },
-                            );
+                    menu = menu.toggleable_entry(
+                        "Selection Menu",
+                        selection_menu_enabled,
+                        IconPosition::Start,
+                        Some(editor::actions::ToggleSelectionMenu.boxed_clone()),
+                        {
+                            let editor = editor.clone();
+                            move |cx| {
+                                editor
+                                    .update(cx, |editor, cx| {
+                                        editor.toggle_selection_menu(
+                                            &editor::actions::ToggleSelectionMenu,
+                                            cx,
+                                        )
+                                    })
+                                    .ok();
+                            }
+                        },
+                    );
 
-                            menu
-                        });
-                        cx.subscribe(&menu, |quick_action_bar, _, _: &DismissEvent, _cx| {
-                            quick_action_bar.toggle_settings_menu = None;
-                        })
-                        .detach();
-                        quick_action_bar.toggle_settings_menu = Some(menu);
-                    })
-                })
-                .when(self.toggle_settings_menu.is_none(), |this| {
-                    this.tooltip(|cx| Tooltip::text("Editor Controls", cx))
+                    menu = menu.toggleable_entry(
+                        "Auto Signature Help",
+                        auto_signature_help_enabled,
+                        IconPosition::Start,
+                        Some(editor::actions::ToggleAutoSignatureHelp.boxed_clone()),
+                        {
+                            let editor = editor.clone();
+                            move |cx| {
+                                editor
+                                    .update(cx, |editor, cx| {
+                                        editor.toggle_auto_signature_help_menu(
+                                            &editor::actions::ToggleAutoSignatureHelp,
+                                            cx,
+                                        );
+                                    })
+                                    .ok();
+                            }
+                        },
+                    );
+
+                    menu
                 });
+                Some(menu)
+            });
 
         h_flex()
             .id("quick action bar")
@@ -316,21 +309,6 @@ impl Render for QuickActionBar {
             )
             .children(editor_selections_dropdown)
             .child(editor_settings_dropdown)
-            .when_some(self.repl_menu.as_ref(), |el, repl_menu| {
-                el.child(Self::render_menu_overlay(repl_menu))
-            })
-            .when_some(
-                self.toggle_settings_menu.as_ref(),
-                |el, toggle_settings_menu| {
-                    el.child(Self::render_menu_overlay(toggle_settings_menu))
-                },
-            )
-            .when_some(
-                self.toggle_selections_menu.as_ref(),
-                |el, toggle_selections_menu| {
-                    el.child(Self::render_menu_overlay(toggle_selections_menu))
-                },
-            )
     }
 }
 

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -5,7 +5,7 @@ use collections::{HashMap, HashSet};
 use db::kvp::KEY_VALUE_STORE;
 use futures::future::join_all;
 use gpui::{
-    actions, Action, AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EventEmitter,
+    actions, Action, AnchorCorner, AnyView, AppContext, AsyncWindowContext, Entity, EventEmitter,
     ExternalPaths, FocusHandle, FocusableView, IntoElement, Model, ParentElement, Pixels, Render,
     Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
 };
@@ -20,7 +20,7 @@ use terminal::{
     Terminal,
 };
 use ui::{
-    h_flex, ButtonCommon, Clickable, ContextMenu, FluentBuilder, IconButton, IconSize, Selectable,
+    h_flex, ButtonCommon, Clickable, ContextMenu, IconButton, IconSize, PopoverMenu, Selectable,
     Tooltip,
 };
 use util::{ResultExt, TryFutureExt};
@@ -173,47 +173,42 @@ impl TerminalPanel {
         let additional_buttons = self.additional_tab_bar_buttons.clone();
         self.pane.update(cx, |pane, cx| {
             pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
-                if !pane.has_focus(cx) {
+                if !pane.has_focus(cx) && !pane.context_menu_focused(cx) {
                     return (None, None);
                 }
+                let focus_handle = pane.focus_handle(cx);
                 let right_children = h_flex()
                     .gap_2()
                     .children(additional_buttons.clone())
                     .child(
-                        IconButton::new("plus", IconName::Plus)
-                            .icon_size(IconSize::Small)
-                            .on_click(cx.listener(|pane, _, cx| {
-                                let focus_handle = pane.focus_handle(cx);
+                        PopoverMenu::new("terminal-tab-bar-popover-menu")
+                            .trigger(
+                                IconButton::new("plus", IconName::Plus)
+                                    .icon_size(IconSize::Small)
+                                    .tooltip(|cx| Tooltip::text("New...", cx)),
+                            )
+                            .anchor(AnchorCorner::TopRight)
+                            .with_handle(pane.new_item_context_menu_handle.clone())
+                            .menu(move |cx| {
+                                let focus_handle = focus_handle.clone();
                                 let menu = ContextMenu::build(cx, |menu, _| {
-                                    menu.action(
-                                        "New Terminal",
-                                        workspace::NewTerminal.boxed_clone(),
-                                    )
-                                    .entry(
-                                        "Spawn task",
-                                        Some(tasks_ui::Spawn::modal().boxed_clone()),
-                                        move |cx| {
-                                            // We want the focus to go back to terminal panel once task modal is dismissed,
-                                            // hence we focus that first. Otherwise, we'd end up without a focused element, as
-                                            // context menu will be gone the moment we spawn the modal.
-                                            cx.focus(&focus_handle);
-                                            cx.dispatch_action(
-                                                tasks_ui::Spawn::modal().boxed_clone(),
-                                            );
-                                        },
-                                    )
+                                    menu.context(focus_handle.clone())
+                                        .action(
+                                            "New Terminal",
+                                            workspace::NewTerminal.boxed_clone(),
+                                        )
+                                        // We want the focus to go back to terminal panel once task modal is dismissed,
+                                        // hence we focus that first. Otherwise, we'd end up without a focused element, as
+                                        // context menu will be gone the moment we spawn the modal.
+                                        .action(
+                                            "Spawn task",
+                                            tasks_ui::Spawn::modal().boxed_clone(),
+                                        )
                                 });
-                                cx.subscribe(&menu, |pane, _, _: &DismissEvent, _| {
-                                    pane.new_item_menu = None;
-                                })
-                                .detach();
-                                pane.new_item_menu = Some(menu);
-                            }))
-                            .tooltip(|cx| Tooltip::text("New...", cx)),
+
+                                Some(menu)
+                            }),
                     )
-                    .when_some(pane.new_item_menu.as_ref(), |el, new_item_menu| {
-                        el.child(Pane::render_menu_overlay(new_item_menu))
-                    })
                     .child({
                         let zoomed = pane.is_zoomed();
                         IconButton::new("toggle_zoom", IconName::Maximize)

crates/ui/src/components/popover_menu.rs 🔗

@@ -56,6 +56,23 @@ impl<M: ManagedView> PopoverMenuHandle<M> {
             }
         }
     }
+
+    pub fn is_deployed(&self) -> bool {
+        self.0
+            .borrow()
+            .as_ref()
+            .map_or(false, |state| state.menu.borrow().as_ref().is_some())
+    }
+
+    pub fn is_focused(&self, cx: &mut WindowContext) -> bool {
+        self.0.borrow().as_ref().map_or(false, |state| {
+            state
+                .menu
+                .borrow()
+                .as_ref()
+                .map_or(false, |view| view.focus_handle(cx).is_focused(cx))
+        })
+    }
 }
 
 pub struct PopoverMenu<M: ManagedView> {
@@ -340,9 +357,12 @@ impl<M: ManagedView> Element for PopoverMenu<M> {
                 // want a click on the toggle to re-open it.
                 cx.on_mouse_event(move |_: &MouseDownEvent, phase, cx| {
                     if phase == DispatchPhase::Bubble && child_hitbox.is_hovered(cx) {
-                        menu_handle.borrow_mut().take();
+                        if let Some(menu) = menu_handle.borrow().as_ref() {
+                            menu.update(cx, |_, cx| {
+                                cx.emit(DismissEvent);
+                            });
+                        }
                         cx.stop_propagation();
-                        cx.refresh();
                     }
                 })
             }

crates/workspace/src/pane.rs 🔗

@@ -17,9 +17,9 @@ use collections::{BTreeSet, HashMap, HashSet, VecDeque};
 use futures::{stream::FuturesUnordered, StreamExt};
 use gpui::{
     actions, anchored, deferred, impl_actions, prelude::*, Action, AnchorCorner, AnyElement,
-    AppContext, AsyncWindowContext, ClickEvent, ClipboardItem, DismissEvent, Div, DragMoveEvent,
-    EntityId, EventEmitter, ExternalPaths, FocusHandle, FocusOutEvent, FocusableView, KeyContext,
-    Model, MouseButton, MouseDownEvent, NavigationDirection, Pixels, Point, PromptLevel, Render,
+    AppContext, AsyncWindowContext, ClickEvent, ClipboardItem, Div, DragMoveEvent, EntityId,
+    EventEmitter, ExternalPaths, FocusHandle, FocusOutEvent, FocusableView, KeyContext, Model,
+    MouseButton, MouseDownEvent, NavigationDirection, Pixels, Point, PromptLevel, Render,
     ScrollHandle, Subscription, Task, View, ViewContext, VisualContext, WeakFocusHandle, WeakView,
     WindowContext,
 };
@@ -43,7 +43,7 @@ use theme::ThemeSettings;
 
 use ui::{
     prelude::*, right_click_menu, ButtonSize, Color, IconButton, IconButtonShape, IconName,
-    IconSize, Indicator, Label, Tab, TabBar, TabPosition, Tooltip,
+    IconSize, Indicator, Label, PopoverMenu, PopoverMenuHandle, Tab, TabBar, TabPosition, Tooltip,
 };
 use ui::{v_flex, ContextMenu};
 use util::{debug_panic, maybe, truncate_and_remove_front, ResultExt};
@@ -250,8 +250,6 @@ pub struct Pane {
     last_focus_handle_by_item: HashMap<EntityId, WeakFocusHandle>,
     nav_history: NavHistory,
     toolbar: View<Toolbar>,
-    pub new_item_menu: Option<View<ContextMenu>>,
-    split_item_menu: Option<View<ContextMenu>>,
     pub(crate) workspace: WeakView<Workspace>,
     project: Model<Project>,
     drag_split_direction: Option<SplitDirection>,
@@ -269,6 +267,8 @@ pub struct Pane {
     display_nav_history_buttons: Option<bool>,
     double_click_dispatch_action: Box<dyn Action>,
     save_modals_spawned: HashSet<EntityId>,
+    pub new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
+    split_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
 }
 
 pub struct ActivationHistoryEntry {
@@ -369,8 +369,6 @@ impl Pane {
                 next_timestamp,
             }))),
             toolbar: cx.new_view(|_| Toolbar::new()),
-            new_item_menu: None,
-            split_item_menu: None,
             tab_bar_scroll_handle: ScrollHandle::new(),
             drag_split_direction: None,
             workspace,
@@ -380,7 +378,7 @@ impl Pane {
             can_split: true,
             should_display_tab_bar: Rc::new(|cx| TabBarSettings::get_global(cx).show),
             render_tab_bar_buttons: Rc::new(move |pane, cx| {
-                if !pane.has_focus(cx) {
+                if !pane.has_focus(cx) && !pane.context_menu_focused(cx) {
                     return (None, None);
                 }
                 // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
@@ -389,10 +387,16 @@ impl Pane {
                     // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
                     .gap(Spacing::Small.rems(cx))
                     .child(
-                        IconButton::new("plus", IconName::Plus)
-                            .icon_size(IconSize::Small)
-                            .on_click(cx.listener(|pane, _, cx| {
-                                let menu = ContextMenu::build(cx, |menu, _| {
+                        PopoverMenu::new("pane-tab-bar-popover-menu")
+                            .trigger(
+                                IconButton::new("plus", IconName::Plus)
+                                    .icon_size(IconSize::Small)
+                                    .tooltip(|cx| Tooltip::text("New...", cx)),
+                            )
+                            .anchor(AnchorCorner::TopRight)
+                            .with_handle(pane.new_item_context_menu_handle.clone())
+                            .menu(move |cx| {
+                                Some(ContextMenu::build(cx, |menu, _| {
                                     menu.action("New File", NewFile.boxed_clone())
                                         .action(
                                             "Open File",
@@ -412,37 +416,27 @@ impl Pane {
                                         )
                                         .separator()
                                         .action("New Terminal", NewTerminal.boxed_clone())
-                                });
-                                cx.subscribe(&menu, |pane, _, _: &DismissEvent, cx| {
-                                    pane.focus(cx);
-                                    pane.new_item_menu = None;
-                                })
-                                .detach();
-                                pane.new_item_menu = Some(menu);
-                            }))
-                            .tooltip(|cx| Tooltip::text("New...", cx)),
+                                }))
+                            }),
                     )
-                    .when_some(pane.new_item_menu.as_ref(), |el, new_item_menu| {
-                        el.child(Self::render_menu_overlay(new_item_menu))
-                    })
                     .child(
-                        IconButton::new("split", IconName::Split)
-                            .icon_size(IconSize::Small)
-                            .on_click(cx.listener(|pane, _, cx| {
-                                let menu = ContextMenu::build(cx, |menu, _| {
+                        PopoverMenu::new("pane-tab-bar-split")
+                            .trigger(
+                                IconButton::new("split", IconName::Split)
+                                    .icon_size(IconSize::Small)
+                                    .tooltip(|cx| Tooltip::text("Split Pane", cx)),
+                            )
+                            .anchor(AnchorCorner::TopRight)
+                            .with_handle(pane.split_item_context_menu_handle.clone())
+                            .menu(move |cx| {
+                                ContextMenu::build(cx, |menu, _| {
                                     menu.action("Split Right", SplitRight.boxed_clone())
                                         .action("Split Left", SplitLeft.boxed_clone())
                                         .action("Split Up", SplitUp.boxed_clone())
                                         .action("Split Down", SplitDown.boxed_clone())
-                                });
-                                cx.subscribe(&menu, |pane, _, _: &DismissEvent, cx| {
-                                    pane.focus(cx);
-                                    pane.split_item_menu = None;
                                 })
-                                .detach();
-                                pane.split_item_menu = Some(menu);
-                            }))
-                            .tooltip(|cx| Tooltip::text("Split Pane", cx)),
+                                .into()
+                            }),
                     )
                     .child({
                         let zoomed = pane.is_zoomed();
@@ -461,9 +455,6 @@ impl Pane {
                                 )
                             })
                     })
-                    .when_some(pane.split_item_menu.as_ref(), |el, split_item_menu| {
-                        el.child(Self::render_menu_overlay(split_item_menu))
-                    })
                     .into_any_element()
                     .into();
                 (None, right_children)
@@ -474,6 +465,8 @@ impl Pane {
             _subscriptions: subscriptions,
             double_click_dispatch_action,
             save_modals_spawned: HashSet::default(),
+            split_item_context_menu_handle: Default::default(),
+            new_item_context_menu_handle: Default::default(),
         }
     }
 
@@ -557,11 +550,9 @@ impl Pane {
         }
     }
 
-    fn context_menu_focused(&self, cx: &mut ViewContext<Self>) -> bool {
-        self.new_item_menu
-            .as_ref()
-            .or(self.split_item_menu.as_ref())
-            .map_or(false, |menu| menu.focus_handle(cx).is_focused(cx))
+    pub fn context_menu_focused(&self, cx: &mut ViewContext<Self>) -> bool {
+        self.new_item_context_menu_handle.is_focused(cx)
+            || self.split_item_context_menu_handle.is_focused(cx)
     }
 
     fn focus_out(&mut self, _event: FocusOutEvent, cx: &mut ViewContext<Self>) {