Fix hover tooltips appearing after related element is pressed (#24540)

Kirill Bulatov and Danilo Leal created

Closes https://github.com/zed-industries/zed/issues/23894

Reworks all trigger declarations from
`.trigger(element.tooltip(tooltip))` into
`.trigger_with_tooltip(element, tooltip)` , with new API disallowing
simultaneous trigger and tooltip display.

All existing `.trigger(` calls were replaced, except 2 not applicable
(in dock.rs and pane.rs), 15 left as ones without tooltips, and 2
unchanged places in `inline_completion_button.rs`, where


https://github.com/zed-industries/zed/blob/0f7bb2e9fd6dc1fe3f0127de19df372f75ad0c4f/crates/inline_completion_button/src/inline_completion_button.rs#L311-L319

`with_animation` does not allow us to simply use the same approach.

Release Notes:

- Fixed hover tooltips appearing after related element is pressed

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>

Change summary

crates/assistant/src/assistant_panel.rs                         |  6 
crates/assistant/src/inline_assistant.rs                        | 32 +-
crates/assistant/src/terminal_inline_assistant.rs               | 32 +-
crates/assistant2/src/assistant_model_selector.rs               | 18 
crates/assistant2/src/assistant_panel.rs                        | 12 
crates/assistant2/src/context_strip.rs                          | 28 +-
crates/assistant_context_editor/src/context_editor.rs           | 12 
crates/assistant_context_editor/src/slash_command_picker.rs     | 25 +
crates/editor/src/hunk_diff.rs                                  | 15 -
crates/git_ui/src/git_panel.rs                                  |  1 
crates/git_ui/src/repository_selector.rs                        | 27 +
crates/inline_completion_button/src/inline_completion_button.rs | 39 --
crates/language_model_selector/src/language_model_selector.rs   | 29 +
crates/repl/src/components/kernel_options.rs                    | 25 +
crates/terminal_view/src/terminal_panel.rs                      | 13 
crates/title_bar/src/application_menu.rs                        |  8 
crates/title_bar/src/title_bar.rs                               | 13 
crates/ui/src/components/popover_menu.rs                        | 26 +
crates/workspace/src/pane.rs                                    | 13 
crates/zed/src/zed/quick_action_bar.rs                          | 16 
crates/zed/src/zed/quick_action_bar/repl_menu.rs                |  8 
21 files changed, 218 insertions(+), 180 deletions(-)

Detailed changes

crates/assistant/src/assistant_panel.rs πŸ”—

@@ -250,10 +250,10 @@ impl AssistantPanel {
                     )
                     .child(
                         PopoverMenu::new("assistant-panel-popover-menu")
-                            .trigger(
+                            .trigger_with_tooltip(
                                 IconButton::new("menu", IconName::EllipsisVertical)
-                                    .icon_size(IconSize::Small)
-                                    .tooltip(Tooltip::text("Toggle Assistant Menu")),
+                                    .icon_size(IconSize::Small),
+                                Tooltip::text("Toggle Assistant Menu"),
                             )
                             .menu(move |window, cx| {
                                 let zoom_label = if _pane.read(cx).is_zoomed() {

crates/assistant/src/inline_assistant.rs πŸ”—

@@ -1595,22 +1595,22 @@ impl Render for PromptEditor {
                         IconButton::new("context", IconName::SettingsAlt)
                             .shape(IconButtonShape::Square)
                             .icon_size(IconSize::Small)
-                            .icon_color(Color::Muted)
-                            .tooltip(move |window, cx| {
-                                Tooltip::with_meta(
-                                    format!(
-                                        "Using {}",
-                                        LanguageModelRegistry::read_global(cx)
-                                            .active_model()
-                                            .map(|model| model.name().0)
-                                            .unwrap_or_else(|| "No model selected".into()),
-                                    ),
-                                    None,
-                                    "Change Model",
-                                    window,
-                                    cx,
-                                )
-                            }),
+                            .icon_color(Color::Muted),
+                        move |window, cx| {
+                            Tooltip::with_meta(
+                                format!(
+                                    "Using {}",
+                                    LanguageModelRegistry::read_global(cx)
+                                        .active_model()
+                                        .map(|model| model.name().0)
+                                        .unwrap_or_else(|| "No model selected".into()),
+                                ),
+                                None,
+                                "Change Model",
+                                window,
+                                cx,
+                            )
+                        },
                     ))
                     .map(|el| {
                         let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx) else {

crates/assistant/src/terminal_inline_assistant.rs πŸ”—

@@ -646,22 +646,22 @@ impl Render for PromptEditor {
                         IconButton::new("context", IconName::SettingsAlt)
                             .shape(IconButtonShape::Square)
                             .icon_size(IconSize::Small)
-                            .icon_color(Color::Muted)
-                            .tooltip(move |window, cx| {
-                                Tooltip::with_meta(
-                                    format!(
-                                        "Using {}",
-                                        LanguageModelRegistry::read_global(cx)
-                                            .active_model()
-                                            .map(|model| model.name().0)
-                                            .unwrap_or_else(|| "No model selected".into()),
-                                    ),
-                                    None,
-                                    "Change Model",
-                                    window,
-                                    cx,
-                                )
-                            }),
+                            .icon_color(Color::Muted),
+                        move |window, cx| {
+                            Tooltip::with_meta(
+                                format!(
+                                    "Using {}",
+                                    LanguageModelRegistry::read_global(cx)
+                                        .active_model()
+                                        .map(|model| model.name().0)
+                                        .unwrap_or_else(|| "No model selected".into()),
+                                ),
+                                None,
+                                "Change Model",
+                                window,
+                                cx,
+                            )
+                        },
                     ))
                     .children(
                         if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {

crates/assistant2/src/assistant_model_selector.rs πŸ”—

@@ -74,16 +74,16 @@ impl Render for AssistantModelSelector {
                                 .color(Color::Muted)
                                 .size(IconSize::XSmall),
                         ),
+                ),
+            move |window, cx| {
+                Tooltip::for_action_in(
+                    "Change Model",
+                    &ToggleModelSelector,
+                    &focus_handle,
+                    window,
+                    cx,
                 )
-                .tooltip(move |window, cx| {
-                    Tooltip::for_action_in(
-                        "Change Model",
-                        &ToggleModelSelector,
-                        &focus_handle,
-                        window,
-                        cx,
-                    )
-                }),
+            },
         )
         .with_handle(self.menu_handle.clone())
     }

crates/assistant2/src/assistant_panel.rs πŸ”—

@@ -660,11 +660,11 @@ impl AssistantPanel {
                     .gap(DynamicSpacing::Base02.rems(cx))
                     .child(
                         PopoverMenu::new("assistant-toolbar-new-popover-menu")
-                            .trigger(
+                            .trigger_with_tooltip(
                                 IconButton::new("new", IconName::Plus)
                                     .icon_size(IconSize::Small)
-                                    .style(ButtonStyle::Subtle)
-                                    .tooltip(Tooltip::text("New…")),
+                                    .style(ButtonStyle::Subtle),
+                                Tooltip::text("New…"),
                             )
                             .anchor(Corner::TopRight)
                             .with_handle(self.new_item_context_menu_handle.clone())
@@ -677,11 +677,11 @@ impl AssistantPanel {
                     )
                     .child(
                         PopoverMenu::new("assistant-toolbar-history-popover-menu")
-                            .trigger(
+                            .trigger_with_tooltip(
                                 IconButton::new("open-history", IconName::HistoryRerun)
                                     .icon_size(IconSize::Small)
-                                    .style(ButtonStyle::Subtle)
-                                    .tooltip(Tooltip::text("History…")),
+                                    .style(ButtonStyle::Subtle),
+                                Tooltip::text("History…"),
                             )
                             .anchor(Corner::TopRight)
                             .with_handle(self.open_history_context_menu_handle.clone())

crates/assistant2/src/context_strip.rs πŸ”—

@@ -411,22 +411,22 @@ impl Render for ContextStrip {
 
                         Some(context_picker.clone())
                     })
-                    .trigger(
+                    .trigger_with_tooltip(
                         IconButton::new("add-context", IconName::Plus)
                             .icon_size(IconSize::Small)
-                            .style(ui::ButtonStyle::Filled)
-                            .tooltip({
-                                let focus_handle = focus_handle.clone();
-                                move |window, cx| {
-                                    Tooltip::for_action_in(
-                                        "Add Context",
-                                        &ToggleContextPicker,
-                                        &focus_handle,
-                                        window,
-                                        cx,
-                                    )
-                                }
-                            }),
+                            .style(ui::ButtonStyle::Filled),
+                        {
+                            let focus_handle = focus_handle.clone();
+                            move |window, cx| {
+                                Tooltip::for_action_in(
+                                    "Add Context",
+                                    &ToggleContextPicker,
+                                    &focus_handle,
+                                    window,
+                                    cx,
+                                )
+                            }
+                        },
                     )
                     .attach(gpui::Corner::TopLeft)
                     .anchor(gpui::Corner::BottomLeft)

crates/assistant_context_editor/src/context_editor.rs πŸ”—

@@ -2359,8 +2359,8 @@ impl ContextEditor {
                 .icon(IconName::Plus)
                 .icon_size(IconSize::Small)
                 .icon_color(Color::Muted)
-                .icon_position(IconPosition::Start)
-                .tooltip(Tooltip::text("Type / to insert via keyboard")),
+                .icon_position(IconPosition::Start),
+            Tooltip::text("Type / to insert via keyboard"),
         )
     }
 
@@ -3323,10 +3323,10 @@ impl Render for ContextEditorToolbarItem {
                                         .color(Color::Muted)
                                         .size(IconSize::XSmall),
                                 ),
-                        )
-                        .tooltip(move |window, cx| {
-                            Tooltip::for_action("Change Model", &ToggleModelSelector, window, cx)
-                        }),
+                        ),
+                    move |window, cx| {
+                        Tooltip::for_action("Change Model", &ToggleModelSelector, window, cx)
+                    },
                 )
                 .with_handle(self.language_model_selector_menu_handle.clone()),
             )

crates/assistant_context_editor/src/slash_command_picker.rs πŸ”—

@@ -1,17 +1,22 @@
 use std::sync::Arc;
 
 use assistant_slash_command::SlashCommandWorkingSet;
-use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakEntity};
+use gpui::{AnyElement, AnyView, DismissEvent, SharedString, Task, WeakEntity};
 use picker::{Picker, PickerDelegate, PickerEditorPosition};
 use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
 
 use crate::context_editor::ContextEditor;
 
 #[derive(IntoElement)]
-pub(super) struct SlashCommandSelector<T: PopoverTrigger> {
+pub(super) struct SlashCommandSelector<T, TT>
+where
+    T: PopoverTrigger + ButtonCommon,
+    TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
     working_set: Arc<SlashCommandWorkingSet>,
     active_context_editor: WeakEntity<ContextEditor>,
     trigger: T,
+    tooltip: TT,
 }
 
 #[derive(Clone)]
@@ -48,16 +53,22 @@ pub(crate) struct SlashCommandDelegate {
     selected_index: usize,
 }
 
-impl<T: PopoverTrigger> SlashCommandSelector<T> {
+impl<T, TT> SlashCommandSelector<T, TT>
+where
+    T: PopoverTrigger + ButtonCommon,
+    TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
     pub(crate) fn new(
         working_set: Arc<SlashCommandWorkingSet>,
         active_context_editor: WeakEntity<ContextEditor>,
         trigger: T,
+        tooltip: TT,
     ) -> Self {
         SlashCommandSelector {
             working_set,
             active_context_editor,
             trigger,
+            tooltip,
         }
     }
 }
@@ -241,7 +252,11 @@ impl PickerDelegate for SlashCommandDelegate {
     }
 }
 
-impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
+impl<T, TT> RenderOnce for SlashCommandSelector<T, TT>
+where
+    T: PopoverTrigger + ButtonCommon,
+    TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
     fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
         let all_models = self
             .working_set
@@ -322,7 +337,7 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
             .ok();
         PopoverMenu::new("model-switcher")
             .menu(move |_window, _cx| Some(picker_view.clone()))
-            .trigger(self.trigger)
+            .trigger_with_tooltip(self.trigger, self.tooltip)
             .attach(gpui::Corner::TopLeft)
             .anchor(gpui::Corner::BottomLeft)
             .offset(gpui::Point {

crates/editor/src/hunk_diff.rs πŸ”—

@@ -763,7 +763,7 @@ impl Editor {
                                                 this.child({
                                                     let focus = editor.focus_handle(cx);
                                                     PopoverMenu::new("hunk-controls-dropdown")
-                                                        .trigger(
+                                                        .trigger_with_tooltip(
                                                             IconButton::new(
                                                                 "toggle_editor_selections_icon",
                                                                 IconName::EllipsisVertical,
@@ -774,19 +774,8 @@ impl Editor {
                                                             .toggle_state(
                                                                 hunk_controls_menu_handle
                                                                     .is_deployed(),
-                                                            )
-                                                            .when(
-                                                                !hunk_controls_menu_handle
-                                                                    .is_deployed(),
-                                                                |this| {
-                                                                    this.tooltip(|_, cx| {
-                                                                        Tooltip::simple(
-                                                                            "Hunk Controls",
-                                                                            cx,
-                                                                        )
-                                                                    })
-                                                                },
                                                             ),
+                                                            Tooltip::simple("Hunk Controls", cx),
                                                         )
                                                         .anchor(Corner::TopRight)
                                                         .with_handle(hunk_controls_menu_handle)

crates/git_ui/src/git_panel.rs πŸ”—

@@ -1161,6 +1161,7 @@ impl GitPanel {
             ButtonLike::new("active-repository")
                 .style(ButtonStyle::Subtle)
                 .child(Label::new(repository_display_name).size(LabelSize::Small)),
+            Tooltip::text("Select a repository"),
         )
     }
 

crates/git_ui/src/repository_selector.rs πŸ”—

@@ -1,6 +1,6 @@
 use gpui::{
-    AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription,
-    Task, WeakEntity,
+    AnyElement, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
+    Subscription, Task, WeakEntity,
 };
 use picker::{Picker, PickerDelegate};
 use project::{
@@ -79,20 +79,27 @@ impl Render for RepositorySelector {
 }
 
 #[derive(IntoElement)]
-pub struct RepositorySelectorPopoverMenu<T>
+pub struct RepositorySelectorPopoverMenu<T, TT>
 where
-    T: PopoverTrigger,
+    T: PopoverTrigger + ButtonCommon,
+    TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
 {
     repository_selector: Entity<RepositorySelector>,
     trigger: T,
+    tooltip: TT,
     handle: Option<PopoverMenuHandle<RepositorySelector>>,
 }
 
-impl<T: PopoverTrigger> RepositorySelectorPopoverMenu<T> {
-    pub fn new(repository_selector: Entity<RepositorySelector>, trigger: T) -> Self {
+impl<T, TT> RepositorySelectorPopoverMenu<T, TT>
+where
+    T: PopoverTrigger + ButtonCommon,
+    TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
+    pub fn new(repository_selector: Entity<RepositorySelector>, trigger: T, tooltip: TT) -> Self {
         Self {
             repository_selector,
             trigger,
+            tooltip,
             handle: None,
         }
     }
@@ -103,13 +110,17 @@ impl<T: PopoverTrigger> RepositorySelectorPopoverMenu<T> {
     }
 }
 
-impl<T: PopoverTrigger> RenderOnce for RepositorySelectorPopoverMenu<T> {
+impl<T, TT> RenderOnce for RepositorySelectorPopoverMenu<T, TT>
+where
+    T: PopoverTrigger + ButtonCommon,
+    TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
     fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
         let repository_selector = self.repository_selector.clone();
 
         PopoverMenu::new("repository-switcher")
             .menu(move |_window, _cx| Some(repository_selector.clone()))
-            .trigger(self.trigger)
+            .trigger_with_tooltip(self.trigger, self.tooltip)
             .attach(gpui::Corner::BottomLeft)
             .when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle))
     }

crates/inline_completion_button/src/inline_completion_button.rs πŸ”—

@@ -142,9 +142,12 @@ impl Render for InlineCompletionButton {
                             })
                         })
                         .anchor(Corner::BottomRight)
-                        .trigger(IconButton::new("copilot-icon", icon).tooltip(|window, cx| {
-                            Tooltip::for_action("GitHub Copilot", &ToggleMenu, window, cx)
-                        }))
+                        .trigger_with_tooltip(
+                            IconButton::new("copilot-icon", icon),
+                            |window, cx| {
+                                Tooltip::for_action("GitHub Copilot", &ToggleMenu, window, cx)
+                            },
+                        )
                         .with_handle(self.popover_menu_handle.clone()),
                 )
             }
@@ -211,7 +214,8 @@ impl Render for InlineCompletionButton {
                             _ => None,
                         })
                         .anchor(Corner::BottomRight)
-                        .trigger(IconButton::new("supermaven-icon", icon).tooltip(
+                        .trigger_with_tooltip(
+                            IconButton::new("supermaven-icon", icon),
                             move |window, cx| {
                                 if has_menu {
                                     Tooltip::for_action(
@@ -224,7 +228,7 @@ impl Render for InlineCompletionButton {
                                     Tooltip::text(tooltip_text.clone())(window, cx)
                                 }
                             },
-                        ))
+                        )
                         .with_handle(self.popover_menu_handle.clone()),
                 );
             }
@@ -287,31 +291,6 @@ impl Render for InlineCompletionButton {
                     .when(enabled && !show_editor_predictions, |this| {
                         this.indicator(Indicator::dot().color(Color::Muted))
                             .indicator_border_color(Some(cx.theme().colors().status_bar_background))
-                    })
-                    .when(!self.popover_menu_handle.is_deployed(), |element| {
-                        element.tooltip(move |window, cx| {
-                            if enabled {
-                                if show_editor_predictions {
-                                    Tooltip::for_action("Edit Prediction", &ToggleMenu, window, cx)
-                                } else {
-                                    Tooltip::with_meta(
-                                        "Edit Prediction",
-                                        Some(&ToggleMenu),
-                                        "Hidden For This File",
-                                        window,
-                                        cx,
-                                    )
-                                }
-                            } else {
-                                Tooltip::with_meta(
-                                    "Edit Prediction",
-                                    Some(&ToggleMenu),
-                                    "Disabled For This File",
-                                    window,
-                                    cx,
-                                )
-                            }
-                        })
                     });
 
                 let this = cx.entity().clone();

crates/language_model_selector/src/language_model_selector.rs πŸ”—

@@ -2,7 +2,7 @@ use std::sync::Arc;
 
 use feature_flags::ZedPro;
 use gpui::{
-    Action, AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
+    Action, AnyElement, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
     Subscription, Task, WeakEntity,
 };
 use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry};
@@ -115,20 +115,31 @@ impl Render for LanguageModelSelector {
 }
 
 #[derive(IntoElement)]
-pub struct LanguageModelSelectorPopoverMenu<T>
+pub struct LanguageModelSelectorPopoverMenu<T, TT>
 where
-    T: PopoverTrigger,
+    T: PopoverTrigger + ButtonCommon,
+    TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
 {
     language_model_selector: Entity<LanguageModelSelector>,
     trigger: T,
+    tooltip: TT,
     handle: Option<PopoverMenuHandle<LanguageModelSelector>>,
 }
 
-impl<T: PopoverTrigger> LanguageModelSelectorPopoverMenu<T> {
-    pub fn new(language_model_selector: Entity<LanguageModelSelector>, trigger: T) -> Self {
+impl<T, TT> LanguageModelSelectorPopoverMenu<T, TT>
+where
+    T: PopoverTrigger + ButtonCommon,
+    TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
+    pub fn new(
+        language_model_selector: Entity<LanguageModelSelector>,
+        trigger: T,
+        tooltip: TT,
+    ) -> Self {
         Self {
             language_model_selector,
             trigger,
+            tooltip,
             handle: None,
         }
     }
@@ -139,13 +150,17 @@ impl<T: PopoverTrigger> LanguageModelSelectorPopoverMenu<T> {
     }
 }
 
-impl<T: PopoverTrigger> RenderOnce for LanguageModelSelectorPopoverMenu<T> {
+impl<T, TT> RenderOnce for LanguageModelSelectorPopoverMenu<T, TT>
+where
+    T: PopoverTrigger + ButtonCommon,
+    TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
     fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
         let language_model_selector = self.language_model_selector.clone();
 
         PopoverMenu::new("model-switcher")
             .menu(move |_window, _cx| Some(language_model_selector.clone()))
-            .trigger(self.trigger)
+            .trigger_with_tooltip(self.trigger, self.tooltip)
             .anchor(gpui::Corner::BottomRight)
             .when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle))
             .offset(gpui::Point {

crates/repl/src/components/kernel_options.rs πŸ”—

@@ -2,6 +2,7 @@ use crate::kernels::KernelSpecification;
 use crate::repl_store::ReplStore;
 use crate::KERNEL_DOCS_URL;
 
+use gpui::AnyView;
 use gpui::DismissEvent;
 
 use gpui::FontWeight;
@@ -19,10 +20,15 @@ use ui::{prelude::*, ListItem, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
 type OnSelect = Box<dyn Fn(KernelSpecification, &mut Window, &mut App)>;
 
 #[derive(IntoElement)]
-pub struct KernelSelector<T: PopoverTrigger> {
+pub struct KernelSelector<T, TT>
+where
+    T: PopoverTrigger + ButtonCommon,
+    TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
     handle: Option<PopoverMenuHandle<Picker<KernelPickerDelegate>>>,
     on_select: OnSelect,
     trigger: T,
+    tooltip: TT,
     info_text: Option<SharedString>,
     worktree_id: WorktreeId,
 }
@@ -44,12 +50,17 @@ fn truncate_path(path: &SharedString, max_length: usize) -> SharedString {
     }
 }
 
-impl<T: PopoverTrigger> KernelSelector<T> {
-    pub fn new(on_select: OnSelect, worktree_id: WorktreeId, trigger: T) -> Self {
+impl<T, TT> KernelSelector<T, TT>
+where
+    T: PopoverTrigger + ButtonCommon,
+    TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
+    pub fn new(on_select: OnSelect, worktree_id: WorktreeId, trigger: T, tooltip: TT) -> Self {
         KernelSelector {
             on_select,
             handle: None,
             trigger,
+            tooltip,
             info_text: None,
             worktree_id,
         }
@@ -235,7 +246,11 @@ impl PickerDelegate for KernelPickerDelegate {
     }
 }
 
-impl<T: PopoverTrigger> RenderOnce for KernelSelector<T> {
+impl<T, TT> RenderOnce for KernelSelector<T, TT>
+where
+    T: PopoverTrigger + ButtonCommon,
+    TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
     fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
         let store = ReplStore::global(cx).read(cx);
 
@@ -262,7 +277,7 @@ impl<T: PopoverTrigger> RenderOnce for KernelSelector<T> {
 
         PopoverMenu::new("kernel-switcher")
             .menu(move |_window, _cx| Some(picker_view.clone()))
-            .trigger(self.trigger)
+            .trigger_with_tooltip(self.trigger, self.tooltip)
             .attach(gpui::Corner::BottomLeft)
             .when_some(self.handle, |menu, handle| menu.with_handle(handle))
     }

crates/terminal_view/src/terminal_panel.rs πŸ”—

@@ -139,10 +139,9 @@ impl TerminalPanel {
                     .gap(DynamicSpacing::Base02.rems(cx))
                     .child(
                         PopoverMenu::new("terminal-tab-bar-popover-menu")
-                            .trigger(
-                                IconButton::new("plus", IconName::Plus)
-                                    .icon_size(IconSize::Small)
-                                    .tooltip(Tooltip::text("New…")),
+                            .trigger_with_tooltip(
+                                IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
+                                Tooltip::text("New…"),
                             )
                             .anchor(Corner::TopRight)
                             .with_handle(pane.new_item_context_menu_handle.clone())
@@ -169,10 +168,10 @@ impl TerminalPanel {
                     .children(assistant_tab_bar_button.clone())
                     .child(
                         PopoverMenu::new("terminal-pane-tab-bar-split")
-                            .trigger(
+                            .trigger_with_tooltip(
                                 IconButton::new("terminal-pane-split", IconName::Split)
-                                    .icon_size(IconSize::Small)
-                                    .tooltip(Tooltip::text("Split Pane")),
+                                    .icon_size(IconSize::Small),
+                                Tooltip::text("Split Pane"),
                             )
                             .anchor(Corner::TopRight)
                             .with_handle(pane.split_item_context_menu_handle.clone())

crates/title_bar/src/application_menu.rs πŸ”—

@@ -133,16 +133,14 @@ impl ApplicationMenu {
                     .menu(move |window, cx| {
                         Self::build_menu_from_items(entry.clone(), window, cx).into()
                     })
-                    .trigger(
+                    .trigger_with_tooltip(
                         IconButton::new(
                             SharedString::from(format!("{}-menu-trigger", menu_name)),
                             ui::IconName::Menu,
                         )
                         .style(ButtonStyle::Subtle)
-                        .icon_size(IconSize::Small)
-                        .when(!handle.is_deployed(), |this| {
-                            this.tooltip(Tooltip::text("Open Application Menu"))
-                        }),
+                        .icon_size(IconSize::Small),
+                        Tooltip::text("Open Application Menu"),
                     )
                     .with_handle(handle),
             )

crates/title_bar/src/title_bar.rs πŸ”—

@@ -690,7 +690,7 @@ impl TitleBar {
                     })
                     .into()
                 })
-                .trigger(
+                .trigger_with_tooltip(
                     ButtonLike::new("user-menu")
                         .child(
                             h_flex()
@@ -706,8 +706,8 @@ impl TitleBar {
                                         .color(Color::Muted),
                                 ),
                         )
-                        .style(ButtonStyle::Subtle)
-                        .tooltip(Tooltip::text("Toggle User Menu")),
+                        .style(ButtonStyle::Subtle),
+                    Tooltip::text("Toggle User Menu"),
                 )
                 .anchor(gpui::Corner::TopRight)
         } else {
@@ -736,10 +736,9 @@ impl TitleBar {
                     })
                     .into()
                 })
-                .trigger(
-                    IconButton::new("user-menu", IconName::ChevronDown)
-                        .icon_size(IconSize::Small)
-                        .tooltip(Tooltip::text("Toggle User Menu")),
+                .trigger_with_tooltip(
+                    IconButton::new("user-menu", IconName::ChevronDown).icon_size(IconSize::Small),
+                    Tooltip::text("Toggle User Menu"),
                 )
         }
     }

crates/ui/src/components/popover_menu.rs πŸ”—

@@ -3,8 +3,8 @@
 use std::{cell::RefCell, rc::Rc};
 
 use gpui::{
-    anchored, deferred, div, point, prelude::FluentBuilder, px, size, AnyElement, App, Bounds,
-    Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, Focusable as _,
+    anchored, deferred, div, point, prelude::FluentBuilder, px, size, AnyElement, AnyView, App,
+    Bounds, Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, Focusable as _,
     GlobalElementId, HitboxId, InteractiveElement, IntoElement, LayoutId, Length, ManagedView,
     MouseDownEvent, ParentElement, Pixels, Point, Style, Window,
 };
@@ -178,6 +178,28 @@ impl<M: ManagedView> PopoverMenu<M> {
         self
     }
 
+    pub fn trigger_with_tooltip<T: PopoverTrigger + ButtonCommon>(
+        mut self,
+        t: T,
+        tooltip_builder: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
+    ) -> Self {
+        let on_open = self.on_open.clone();
+        self.child_builder = Some(Box::new(move |menu, builder| {
+            let open = menu.borrow().is_some();
+            t.toggle_state(open)
+                .when_some(builder, |el, builder| {
+                    el.on_click(move |_, window, cx| {
+                        show_menu(&builder, &menu, on_open.clone(), window, cx)
+                    })
+                    .when(!open, |t| {
+                        t.tooltip(move |window, cx| tooltip_builder(window, cx))
+                    })
+                })
+                .into_any_element()
+        }));
+        self
+    }
+
     /// anchor defines which corner of the menu to anchor to the attachment point
     /// (by default the cursor position, but see attach)
     pub fn anchor(mut self, anchor: Corner) -> Self {

crates/workspace/src/pane.rs πŸ”—

@@ -441,10 +441,9 @@ impl Pane {
                     .gap(DynamicSpacing::Base04.rems(cx))
                     .child(
                         PopoverMenu::new("pane-tab-bar-popover-menu")
-                            .trigger(
-                                IconButton::new("plus", IconName::Plus)
-                                    .icon_size(IconSize::Small)
-                                    .tooltip(Tooltip::text("New...")),
+                            .trigger_with_tooltip(
+                                IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
+                                Tooltip::text("New..."),
                             )
                             .anchor(Corner::TopRight)
                             .with_handle(pane.new_item_context_menu_handle.clone())
@@ -474,10 +473,10 @@ impl Pane {
                     )
                     .child(
                         PopoverMenu::new("pane-tab-bar-split")
-                            .trigger(
+                            .trigger_with_tooltip(
                                 IconButton::new("split", IconName::Split)
-                                    .icon_size(IconSize::Small)
-                                    .tooltip(Tooltip::text("Split Pane")),
+                                    .icon_size(IconSize::Small),
+                                Tooltip::text("Split Pane"),
                             )
                             .anchor(Corner::TopRight)
                             .with_handle(pane.split_item_context_menu_handle.clone())

crates/zed/src/zed/quick_action_bar.rs πŸ”—

@@ -168,15 +168,13 @@ impl Render for QuickActionBar {
             let focus = editor.focus_handle(cx);
 
             PopoverMenu::new("editor-selections-dropdown")
-                .trigger(
+                .trigger_with_tooltip(
                     IconButton::new("toggle_editor_selections_icon", IconName::CursorIBeam)
                         .shape(IconButtonShape::Square)
                         .icon_size(IconSize::Small)
                         .style(ButtonStyle::Subtle)
-                        .toggle_state(self.toggle_selections_handle.is_deployed())
-                        .when(!self.toggle_selections_handle.is_deployed(), |this| {
-                            this.tooltip(Tooltip::text("Selection Controls"))
-                        }),
+                        .toggle_state(self.toggle_selections_handle.is_deployed()),
+                    Tooltip::text("Selection Controls"),
                 )
                 .with_handle(self.toggle_selections_handle.clone())
                 .anchor(Corner::TopRight)
@@ -219,15 +217,13 @@ impl Render for QuickActionBar {
             let vim_mode_enabled = VimModeSetting::get_global(cx).0;
 
             PopoverMenu::new("editor-settings")
-                .trigger(
+                .trigger_with_tooltip(
                     IconButton::new("toggle_editor_settings_icon", IconName::Sliders)
                         .shape(IconButtonShape::Square)
                         .icon_size(IconSize::Small)
                         .style(ButtonStyle::Subtle)
-                        .toggle_state(self.toggle_settings_handle.is_deployed())
-                        .when(!self.toggle_settings_handle.is_deployed(), |this| {
-                            this.tooltip(Tooltip::text("Editor Controls"))
-                        }),
+                        .toggle_state(self.toggle_settings_handle.is_deployed()),
+                    Tooltip::text("Editor Controls"),
                 )
                 .anchor(Corner::TopRight)
                 .with_handle(self.toggle_settings_handle.clone())

crates/zed/src/zed/quick_action_bar/repl_menu.rs πŸ”—

@@ -209,16 +209,16 @@ impl QuickActionBar {
                 })
                 .into()
             })
-            .trigger(
+            .trigger_with_tooltip(
                 ButtonLike::new_rounded_right(element_id("dropdown"))
                     .child(
                         Icon::new(IconName::ChevronDownSmall)
                             .size(IconSize::XSmall)
                             .color(Color::Muted),
                     )
-                    .tooltip(Tooltip::text("REPL Menu"))
                     .width(rems(1.).into())
                     .disabled(menu_state.popover_disabled),
+                Tooltip::text("REPL Menu"),
             );
 
         let button = ButtonLike::new_rounded_left("toggle_repl_icon")
@@ -343,8 +343,8 @@ impl QuickActionBar {
                                 .color(Color::Muted)
                                 .size(IconSize::XSmall),
                         ),
-                )
-                .tooltip(Tooltip::text("Select Kernel")),
+                ),
+            Tooltip::text("Select Kernel"),
         )
         .with_handle(menu_handle.clone())
         .into_any_element()