From b214c9e4a812f01d8d2d624ecd4910804fd32beb Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 5 May 2025 16:05:47 -0700 Subject: [PATCH] Fix profile menu hover flickering due to documentation asides (#29958) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes https://github.com/zed-industries/zed/issues/29909 🍐'd with @nathansobo Release Notes: - N/A --------- Co-authored-by: Nathan --- crates/agent/src/assistant_panel.rs | 25 +++-- crates/agent/src/message_editor.rs | 30 ++++- crates/agent/src/profile_selector.rs | 23 +++- .../src/inline_completion_button.rs | 14 +-- crates/ui/src/components/context_menu.rs | 103 ++++++++++-------- crates/zed/src/zed/quick_action_bar.rs | 6 +- 6 files changed, 131 insertions(+), 70 deletions(-) diff --git a/crates/agent/src/assistant_panel.rs b/crates/agent/src/assistant_panel.rs index c16b6a67d0..76d75ccad4 100644 --- a/crates/agent/src/assistant_panel.rs +++ b/crates/agent/src/assistant_panel.rs @@ -466,6 +466,7 @@ impl AssistantPanel { thread_store.downgrade(), context_store.downgrade(), thread.clone(), + agent_panel_dock_position(cx), window, cx, ) @@ -795,6 +796,7 @@ impl AssistantPanel { self.thread_store.downgrade(), self.context_store.downgrade(), thread, + agent_panel_dock_position(cx), window, cx, ) @@ -1003,6 +1005,7 @@ impl AssistantPanel { self.thread_store.downgrade(), self.context_store.downgrade(), thread, + agent_panel_dock_position(cx), window, cx, ) @@ -1330,6 +1333,14 @@ impl Focusable for AssistantPanel { } } +fn agent_panel_dock_position(cx: &App) -> DockPosition { + match AssistantSettings::get_global(cx).dock { + AssistantDockPosition::Left => DockPosition::Left, + AssistantDockPosition::Bottom => DockPosition::Bottom, + AssistantDockPosition::Right => DockPosition::Right, + } +} + impl EventEmitter for AssistantPanel {} impl Panel for AssistantPanel { @@ -1338,18 +1349,18 @@ impl Panel for AssistantPanel { } fn position(&self, _window: &Window, cx: &App) -> DockPosition { - match AssistantSettings::get_global(cx).dock { - AssistantDockPosition::Left => DockPosition::Left, - AssistantDockPosition::Bottom => DockPosition::Bottom, - AssistantDockPosition::Right => DockPosition::Right, - } + agent_panel_dock_position(cx) } - fn position_is_valid(&self, _: DockPosition) -> bool { - true + fn position_is_valid(&self, position: DockPosition) -> bool { + position != DockPosition::Bottom } fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context) { + self.message_editor.update(cx, |message_editor, cx| { + message_editor.set_dock_position(position, cx); + }); + settings::update_settings_file::( self.fs.clone(), cx, diff --git a/crates/agent/src/message_editor.rs b/crates/agent/src/message_editor.rs index 007dd5b11e..8b084ea513 100644 --- a/crates/agent/src/message_editor.rs +++ b/crates/agent/src/message_editor.rs @@ -35,8 +35,9 @@ use proto::Plan; use settings::Settings; use std::time::Duration; use theme::ThemeSettings; -use ui::{Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*}; +use ui::{Disclosure, DocumentationSide, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*}; use util::{ResultExt as _, maybe}; +use workspace::dock::DockPosition; use workspace::{CollaboratorId, Workspace}; use zed_llm_client::CompletionMode; @@ -130,6 +131,14 @@ pub(crate) fn create_editor( editor } +fn documentation_side(position: DockPosition) -> DocumentationSide { + match position { + DockPosition::Left => DocumentationSide::Right, + DockPosition::Bottom => DocumentationSide::Left, + DockPosition::Right => DocumentationSide::Left, + } +} + impl MessageEditor { pub fn new( fs: Arc, @@ -140,6 +149,7 @@ impl MessageEditor { thread_store: WeakEntity, text_thread_store: WeakEntity, thread: Entity, + dock_position: DockPosition, window: &mut Window, cx: &mut Context, ) -> Self { @@ -213,8 +223,15 @@ impl MessageEditor { model_selector, edits_expanded: false, editor_is_expanded: false, - profile_selector: cx - .new(|cx| ProfileSelector::new(fs, thread_store, editor.focus_handle(cx), cx)), + profile_selector: cx.new(|cx| { + ProfileSelector::new( + fs, + thread_store, + editor.focus_handle(cx), + documentation_side(dock_position), + cx, + ) + }), last_estimated_token_count: None, update_token_count_task: None, _subscriptions: subscriptions, @@ -1253,6 +1270,12 @@ impl MessageEditor { .ok(); })); } + + pub fn set_dock_position(&mut self, position: DockPosition, cx: &mut Context) { + self.profile_selector.update(cx, |profile_selector, cx| { + profile_selector.set_documentation_side(documentation_side(position), cx) + }); + } } pub fn extract_message_creases( @@ -1426,6 +1449,7 @@ impl AgentPreview for MessageEditor { thread_store.downgrade(), text_thread_store.downgrade(), thread, + DockPosition::Left, window, cx, ) diff --git a/crates/agent/src/profile_selector.rs b/crates/agent/src/profile_selector.rs index b458f90f5a..9fed2cdd29 100644 --- a/crates/agent/src/profile_selector.rs +++ b/crates/agent/src/profile_selector.rs @@ -7,7 +7,10 @@ use fs::Fs; use gpui::{Action, Entity, FocusHandle, Subscription, WeakEntity, prelude::*}; use language_model::LanguageModelRegistry; use settings::{Settings as _, SettingsStore, update_settings_file}; -use ui::{ContextMenu, ContextMenuEntry, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*}; +use ui::{ + ContextMenu, ContextMenuEntry, DocumentationSide, PopoverMenu, PopoverMenuHandle, Tooltip, + prelude::*, +}; use util::ResultExt as _; use crate::{ManageProfiles, ThreadStore, ToggleProfileSelector}; @@ -19,6 +22,7 @@ pub struct ProfileSelector { menu_handle: PopoverMenuHandle, focus_handle: FocusHandle, _subscriptions: Vec, + documentation_side: DocumentationSide, } impl ProfileSelector { @@ -26,6 +30,7 @@ impl ProfileSelector { fs: Arc, thread_store: WeakEntity, focus_handle: FocusHandle, + documentation_side: DocumentationSide, cx: &mut Context, ) -> Self { let settings_subscription = cx.observe_global::(move |this, cx| { @@ -39,9 +44,15 @@ impl ProfileSelector { menu_handle: PopoverMenuHandle::default(), focus_handle, _subscriptions: vec![settings_subscription], + documentation_side, } } + pub fn set_documentation_side(&mut self, side: DocumentationSide, cx: &mut Context) { + self.documentation_side = side; + cx.notify(); + } + pub fn menu_handle(&self) -> PopoverMenuHandle { self.menu_handle.clone() } @@ -101,7 +112,9 @@ impl ProfileSelector { .toggleable(IconPosition::End, profile_id == settings.default_profile); let entry = if let Some(doc_text) = documentation { - entry.documentation_aside(move |_| Label::new(doc_text).into_any_element()) + entry.documentation_aside(self.documentation_side, move |_| { + Label::new(doc_text).into_any_element() + }) } else { entry }; @@ -175,7 +188,11 @@ impl Render for ProfileSelector { ) } }) - .anchor(gpui::Corner::BottomRight) + .anchor(if self.documentation_side == DocumentationSide::Left { + gpui::Corner::BottomRight + } else { + gpui::Corner::BottomLeft + }) .with_handle(self.menu_handle.clone()) .menu(move |window, cx| { Some(this.update(cx, |this, cx| this.build_context_menu(window, cx))) diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index 72e2782323..caed4cdc84 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -27,8 +27,8 @@ use std::{ }; use supermaven::{AccountStatus, Supermaven}; use ui::{ - Clickable, ContextMenu, ContextMenuEntry, IconButton, IconButtonShape, Indicator, PopoverMenu, - PopoverMenuHandle, ProgressBar, Tooltip, prelude::*, + Clickable, ContextMenu, ContextMenuEntry, DocumentationSide, IconButton, IconButtonShape, + Indicator, PopoverMenu, PopoverMenuHandle, ProgressBar, Tooltip, prelude::*, }; use util::maybe; use workspace::{ @@ -485,7 +485,7 @@ impl InlineCompletionButton { menu = menu.item( entry .disabled(true) - .documentation_aside(move |_cx| { + .documentation_aside(DocumentationSide::Left, move |_cx| { Label::new(format!("Edit predictions cannot be toggled for this buffer because they are disabled for {}", language.name())) .into_any_element() }) @@ -529,7 +529,7 @@ impl InlineCompletionButton { .item( ContextMenuEntry::new("Eager") .toggleable(IconPosition::Start, eager_mode) - .documentation_aside(move |_| { + .documentation_aside(DocumentationSide::Left, move |_| { Label::new("Display predictions inline when there are no language server completions available.").into_any_element() }) .handler({ @@ -542,7 +542,7 @@ impl InlineCompletionButton { .item( ContextMenuEntry::new("Subtle") .toggleable(IconPosition::Start, subtle_mode) - .documentation_aside(move |_| { + .documentation_aside(DocumentationSide::Left, move |_| { Label::new("Display predictions inline only when holding a modifier key (alt by default).").into_any_element() }) .handler({ @@ -573,7 +573,7 @@ impl InlineCompletionButton { .toggleable(IconPosition::Start, data_collection.is_enabled()) .icon(icon_name) .icon_color(icon_color) - .documentation_aside(move |cx| { + .documentation_aside(DocumentationSide::Left, move |cx| { let (msg, label_color, icon_name, icon_color) = match (is_open_source, is_collecting) { (true, true) => ( "Project identified as open source, and you're sharing data.", @@ -654,7 +654,7 @@ impl InlineCompletionButton { ContextMenuEntry::new("Configure Excluded Files") .icon(IconName::LockOutlined) .icon_color(Color::Muted) - .documentation_aside(|_| { + .documentation_aside(DocumentationSide::Left, |_| { Label::new(indoc!{" Open your settings to add sensitive paths for which Zed will never predict edits."}).into_any_element() }) diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 191e26ec73..aed9507b7b 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -50,7 +50,7 @@ pub struct ContextMenuEntry { handler: Rc, &mut Window, &mut App)>, action: Option>, disabled: bool, - documentation_aside: Option AnyElement>>, + documentation_aside: Option, end_slot_icon: Option, end_slot_title: Option, end_slot_handler: Option, &mut Window, &mut App)>>, @@ -124,9 +124,14 @@ impl ContextMenuEntry { pub fn documentation_aside( mut self, - element: impl Fn(&mut App) -> AnyElement + 'static, + side: DocumentationSide, + render: impl Fn(&mut App) -> AnyElement + 'static, ) -> Self { - self.documentation_aside = Some(Rc::new(element)); + self.documentation_aside = Some(DocumentationAside { + side, + render: Rc::new(render), + }); + self } } @@ -150,10 +155,22 @@ pub struct ContextMenu { _on_blur_subscription: Subscription, keep_open_on_confirm: bool, eager: bool, - documentation_aside: Option<(usize, Rc AnyElement>)>, + documentation_aside: Option<(usize, DocumentationAside)>, fixed_width: Option, } +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum DocumentationSide { + Left, + Right, +} + +#[derive(Clone)] +pub struct DocumentationAside { + side: DocumentationSide, + render: Rc AnyElement>, +} + impl Focusable for ContextMenu { fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() @@ -933,27 +950,19 @@ impl ContextMenu { .into_any_element() }; - let documentation_aside_callback = documentation_aside.clone(); - div() .id(("context-menu-child", ix)) - .when_some( - documentation_aside_callback.clone(), - |this, documentation_aside_callback| { - this.occlude() - .on_hover(cx.listener(move |menu, hovered, _, cx| { - if *hovered { - menu.documentation_aside = - Some((ix, documentation_aside_callback.clone())); - cx.notify(); - } else if matches!(menu.documentation_aside, Some((id, _)) if id == ix) - { - menu.documentation_aside = None; - cx.notify(); - } - })) - }, - ) + .when_some(documentation_aside.clone(), |this, documentation_aside| { + this.occlude() + .on_hover(cx.listener(move |menu, hovered, _, cx| { + if *hovered { + menu.documentation_aside = Some((ix, documentation_aside.clone())); + } else if matches!(menu.documentation_aside, Some((id, _)) if id == ix) { + menu.documentation_aside = None; + } + cx.notify(); + })) + }) .child( ListItem::new(ix) .group_name("label_container") @@ -992,21 +1001,18 @@ impl ContextMenu { }) .map(|binding| { div().ml_4().child(binding.disabled(*disabled)).when( - *disabled && documentation_aside_callback.is_some(), + *disabled && documentation_aside.is_some(), |parent| parent.invisible(), ) }) })) - .when( - *disabled && documentation_aside_callback.is_some(), - |parent| { - parent.child( - Icon::new(IconName::Info) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - }, - ), + .when(*disabled && documentation_aside.is_some(), |parent| { + parent.child( + Icon::new(IconName::Info) + .size(IconSize::XSmall) + .color(Color::Muted), + ) + }), ) .when_some( end_slot_icon @@ -1108,10 +1114,17 @@ impl Render for ContextMenu { let rem_size = window.rem_size(); let is_wide_window = window_size.width / rem_size > rems_from_px(800.).0; - let aside = self - .documentation_aside - .as_ref() - .map(|(_, callback)| callback.clone()); + let aside = self.documentation_aside.clone(); + let render_aside = |aside: DocumentationAside, cx: &mut Context| { + WithRemSize::new(ui_font_size) + .occlude() + .elevation_2(cx) + .p_2() + .overflow_hidden() + .when(is_wide_window, |this| this.max_w_96()) + .when(!is_wide_window, |this| this.max_w_48()) + .child((aside.render)(cx)) + }; h_flex() .when(is_wide_window, |this| this.flex_row()) @@ -1119,15 +1132,8 @@ impl Render for ContextMenu { .w_full() .items_start() .gap_1() - .child(div().children(aside.map(|aside| { - WithRemSize::new(ui_font_size) - .occlude() - .elevation_2(cx) - .p_2() - .overflow_hidden() - .when(is_wide_window, |this| this.max_w_96()) - .when(!is_wide_window, |this| this.max_w_48()) - .child(aside(cx)) + .child(div().children(aside.clone().and_then(|(_, aside)| { + (aside.side == DocumentationSide::Left).then(|| render_aside(aside, cx)) }))) .child( WithRemSize::new(ui_font_size) @@ -1185,5 +1191,8 @@ impl Render for ContextMenu { ), ), ) + .child(div().children(aside.and_then(|(_, aside)| { + (aside.side == DocumentationSide::Right).then(|| render_aside(aside, cx)) + }))) } } diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index 9ceceabce9..cf3dedcbf8 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -15,8 +15,8 @@ use gpui::{ use search::{BufferSearchBar, buffer_search}; use settings::{Settings, SettingsStore}; use ui::{ - ButtonStyle, ContextMenu, ContextMenuEntry, IconButton, IconName, IconSize, PopoverMenu, - PopoverMenuHandle, Tooltip, prelude::*, + ButtonStyle, ContextMenu, ContextMenuEntry, DocumentationSide, IconButton, IconName, IconSize, + PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*, }; use vim_mode_setting::VimModeSetting; use workspace::{ @@ -291,7 +291,7 @@ impl Render for QuickActionBar { } }); if !edit_predictions_enabled_at_cursor { - inline_completion_entry = inline_completion_entry.documentation_aside(|_| { + inline_completion_entry = inline_completion_entry.documentation_aside(DocumentationSide::Left, |_| { Label::new("You can't toggle edit predictions for this file as it is within the excluded files list.").into_any_element() }); }