From 182b7af2991bafed8258bbe014d6f4befb208477 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 22 Aug 2024 18:05:23 +0200 Subject: [PATCH] ui: Use popover menus for tab bar in panes (#16497) Closes #ISSUE Release Notes: - N/A --- .../src/activity_indicator.rs | 157 ++++----- crates/assistant/src/assistant_panel.rs | 37 +- .../quick_action_bar/src/quick_action_bar.rs | 322 ++++++++---------- 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, 326 insertions(+), 358 deletions(-) diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index d39cb4af47..3f318866c3 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/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, project: Model, auto_updater: Option>, - context_menu: Option>, + context_menu_handle: PopoverMenuHandle, } 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) { - 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) { - 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() + }), + ) } } diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index a6c22eae95..52f4838f89 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/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::().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(); diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index 5ac49189a9..4d031eb60c 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/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, active_item: Option>, buffer_search_bar: View, - repl_menu: Option>, show: bool, - toggle_selections_menu: Option>, - toggle_settings_menu: Option>, + toggle_selections_handle: PopoverMenuHandle, + toggle_settings_handle: PopoverMenuHandle, workspace: WeakView, } @@ -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) -> 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, - 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, - ) - }); - } - }, - ); - - 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, + 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 - }); - 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( + "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( + "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 = 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)) - }, - ) } } diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index e4d40e561c..e2725183b5 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/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) diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs index 8707bee0a7..17cdf7e34c 100644 --- a/crates/ui/src/components/popover_menu.rs +++ b/crates/ui/src/components/popover_menu.rs @@ -56,6 +56,23 @@ impl PopoverMenuHandle { } } } + + 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 { @@ -340,9 +357,12 @@ impl Element for PopoverMenu { // 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(); } }) } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 741962b43d..cc00ab9f61 100644 --- a/crates/workspace/src/pane.rs +++ b/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, nav_history: NavHistory, toolbar: View, - pub new_item_menu: Option>, - split_item_menu: Option>, pub(crate) workspace: WeakView, project: Model, drag_split_direction: Option, @@ -269,6 +267,8 @@ pub struct Pane { display_nav_history_buttons: Option, double_click_dispatch_action: Box, save_modals_spawned: HashSet, + pub new_item_context_menu_handle: PopoverMenuHandle, + split_item_context_menu_handle: PopoverMenuHandle, } 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) -> 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) -> 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) {