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

Closes #ISSUE

Release Notes:

- N/A
This commit is contained in:
Piotr Osiewicz 2024-08-22 18:05:23 +02:00 committed by GitHub
parent 72b5cda356
commit 182b7af299
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 326 additions and 358 deletions

View File

@ -3,10 +3,9 @@ use editor::Editor;
use extension::ExtensionStore; use extension::ExtensionStore;
use futures::StreamExt; use futures::StreamExt;
use gpui::{ use gpui::{
actions, anchored, deferred, percentage, Animation, AnimationExt as _, AppContext, CursorStyle, actions, percentage, Animation, AnimationExt as _, AppContext, CursorStyle, EventEmitter,
DismissEvent, EventEmitter, InteractiveElement as _, Model, ParentElement as _, Render, InteractiveElement as _, Model, ParentElement as _, Render, SharedString,
SharedString, StatefulInteractiveElement, Styled, Transformation, View, ViewContext, StatefulInteractiveElement, Styled, Transformation, View, ViewContext, VisualContext as _,
VisualContext as _,
}; };
use language::{ use language::{
LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId, LanguageServerName, LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId, LanguageServerName,
@ -14,7 +13,7 @@ use language::{
use project::{LanguageServerProgress, Project}; use project::{LanguageServerProgress, Project};
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration}; 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}; use workspace::{item::ItemHandle, StatusItemView, Workspace};
actions!(activity_indicator, [ShowErrorMessage]); actions!(activity_indicator, [ShowErrorMessage]);
@ -27,7 +26,7 @@ pub struct ActivityIndicator {
statuses: Vec<LspStatus>, statuses: Vec<LspStatus>,
project: Model<Project>, project: Model<Project>,
auto_updater: Option<Model<AutoUpdater>>, auto_updater: Option<Model<AutoUpdater>>,
context_menu: Option<View<ContextMenu>>, context_menu_handle: PopoverMenuHandle<ContextMenu>,
} }
struct LspStatus { struct LspStatus {
@ -79,7 +78,7 @@ impl ActivityIndicator {
statuses: Default::default(), statuses: Default::default(),
project: project.clone(), project: project.clone(),
auto_updater, 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>) { fn toggle_language_server_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
if self.context_menu.take().is_some() { self.context_menu_handle.toggle(cx);
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();
}
} }
} }
@ -455,19 +389,72 @@ impl Render for ActivityIndicator {
on_click(this, cx); on_click(this, cx);
})) }))
} }
let this = cx.view().downgrade();
result result.gap_2().child(
.gap_2() PopoverMenu::new("activity-indicator-popover")
.children(content.icon) .trigger(
.child(Label::new(SharedString::from(content.message)).size(LabelSize::Small)) ButtonLike::new("activity-indicator-trigger").child(
.children(self.context_menu.as_ref().map(|menu| { h_flex()
deferred( .gap_2()
anchored() .children(content.icon)
.anchor(gpui::AnchorCorner::BottomLeft) .child(Label::new(content.message).size(LabelSize::Small)),
.child(menu.clone()), ),
) )
.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()
}),
)
} }
} }

View File

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

View File

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

View File

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

View File

@ -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> { 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. // want a click on the toggle to re-open it.
cx.on_mouse_event(move |_: &MouseDownEvent, phase, cx| { cx.on_mouse_event(move |_: &MouseDownEvent, phase, cx| {
if phase == DispatchPhase::Bubble && child_hitbox.is_hovered(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.stop_propagation();
cx.refresh();
} }
}) })
} }

View File

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