Allow passing a handler function to context menu items

This commit is contained in:
Antonio Scandurra 2023-04-27 11:32:12 +02:00
parent d3b976d044
commit 5521ff1b22
8 changed files with 137 additions and 78 deletions

View file

@ -301,13 +301,19 @@ impl CollabTitlebarItem {
.with_style(item_style.container)
.into_any()
})),
ContextMenuItem::item("Sign out", SignOut),
ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback),
ContextMenuItem::action("Sign out", SignOut),
ContextMenuItem::action(
"Send Feedback",
feedback::feedback_editor::GiveFeedback,
),
]
} else {
vec![
ContextMenuItem::item("Sign in", SignIn),
ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback),
ContextMenuItem::action("Sign in", SignIn),
ContextMenuItem::action(
"Send Feedback",
feedback::feedback_editor::GiveFeedback,
),
]
};

View file

@ -10,7 +10,7 @@ use gpui::{
};
use menu::*;
use settings::Settings;
use std::{any::TypeId, borrow::Cow, time::Duration};
use std::{any::TypeId, borrow::Cow, sync::Arc, time::Duration};
#[derive(Copy, Clone, PartialEq)]
struct Clicked;
@ -64,20 +64,44 @@ where
}
}
pub enum ContextMenuItemAction {
Action(Box<dyn Action>),
Handler(Arc<dyn Fn(&mut ViewContext<ContextMenu>)>),
}
impl Clone for ContextMenuItemAction {
fn clone(&self) -> Self {
match self {
Self::Action(action) => Self::Action(action.boxed_clone()),
Self::Handler(handler) => Self::Handler(handler.clone()),
}
}
}
pub enum ContextMenuItem {
Item {
label: ContextMenuItemLabel,
action: Box<dyn Action>,
action: ContextMenuItemAction,
},
Static(StaticItem),
Separator,
}
impl ContextMenuItem {
pub fn item(label: impl Into<ContextMenuItemLabel>, action: impl 'static + Action) -> Self {
pub fn action(label: impl Into<ContextMenuItemLabel>, action: impl 'static + Action) -> Self {
Self::Item {
label: label.into(),
action: Box::new(action),
action: ContextMenuItemAction::Action(Box::new(action)),
}
}
pub fn handler(
label: impl Into<ContextMenuItemLabel>,
handler: impl 'static + Fn(&mut ViewContext<ContextMenu>),
) -> Self {
Self::Item {
label: label.into(),
action: ContextMenuItemAction::Handler(Arc::new(handler)),
}
}
@ -91,7 +115,10 @@ impl ContextMenuItem {
fn action_id(&self) -> Option<TypeId> {
match self {
ContextMenuItem::Item { action, .. } => Some(action.id()),
ContextMenuItem::Item { action, .. } => match action {
ContextMenuItemAction::Action(action) => Some(action.id()),
ContextMenuItemAction::Handler(_) => None,
},
ContextMenuItem::Static(..) | ContextMenuItem::Separator => None,
}
}
@ -208,7 +235,17 @@ impl ContextMenu {
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.selected_index {
if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) {
cx.dispatch_any_action(action.boxed_clone());
match action {
ContextMenuItemAction::Action(action) => {
let window_id = cx.window_id();
cx.dispatch_any_action_at(
window_id,
self.parent_view_id,
action.boxed_clone(),
);
}
ContextMenuItemAction::Handler(handler) => handler(cx),
}
self.reset(cx);
}
}
@ -351,13 +388,16 @@ impl ContextMenu {
Some(ix) == self.selected_index,
);
KeystrokeLabel::new(
self.parent_view_id,
action.boxed_clone(),
style.keystroke.container,
style.keystroke.text.clone(),
)
.into_any()
match action {
ContextMenuItemAction::Action(action) => KeystrokeLabel::new(
self.parent_view_id,
action.boxed_clone(),
style.keystroke.container,
style.keystroke.text.clone(),
)
.into_any(),
ContextMenuItemAction::Handler(_) => Empty::new().into_any(),
}
}
ContextMenuItem::Static(_) => Empty::new().into_any(),
@ -389,11 +429,23 @@ impl ContextMenu {
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
match item {
ContextMenuItem::Item { label, action } => {
let action = action.boxed_clone();
let action = action.clone();
let view_id = self.parent_view_id;
MouseEventHandler::<MenuItem, ContextMenu>::new(ix, cx, |state, _| {
let style =
style.item.style_for(state, Some(ix) == self.selected_index);
let keystroke = match &action {
ContextMenuItemAction::Action(action) => Some(
KeystrokeLabel::new(
view_id,
action.boxed_clone(),
style.keystroke.container,
style.keystroke.text.clone(),
)
.flex_float(),
),
ContextMenuItemAction::Handler(_) => None,
};
Flex::row()
.with_child(match label {
@ -406,15 +458,7 @@ impl ContextMenu {
element(state, style)
}
})
.with_child({
KeystrokeLabel::new(
view_id,
action.boxed_clone(),
style.keystroke.container,
style.keystroke.text.clone(),
)
.flex_float()
})
.with_children(keystroke)
.contained()
.with_style(style.container)
})
@ -424,7 +468,16 @@ impl ContextMenu {
.on_click(MouseButton::Left, move |_, _, cx| {
let window_id = cx.window_id();
cx.dispatch_action(Clicked);
cx.dispatch_any_action_at(window_id, view_id, action.boxed_clone());
match &action {
ContextMenuItemAction::Action(action) => {
cx.dispatch_any_action_at(
window_id,
view_id,
action.boxed_clone(),
);
}
ContextMenuItemAction::Handler(handler) => handler(cx),
}
})
.on_drag(MouseButton::Left, |_, _, _| {})
.into_any()

View file

@ -271,8 +271,8 @@ impl CopilotButton {
) {
let mut menu_options = Vec::with_capacity(2);
menu_options.push(ContextMenuItem::item("Sign In", InitiateSignIn));
menu_options.push(ContextMenuItem::item("Disable Copilot", HideCopilot));
menu_options.push(ContextMenuItem::action("Sign In", InitiateSignIn));
menu_options.push(ContextMenuItem::action("Disable Copilot", HideCopilot));
self.popup_menu.update(cx, |menu, cx| {
menu.show(
@ -292,7 +292,7 @@ impl CopilotButton {
if let Some(language) = &self.language {
let language_enabled = settings.show_copilot_suggestions(Some(language.as_ref()));
menu_options.push(ContextMenuItem::item(
menu_options.push(ContextMenuItem::action(
format!(
"{} Suggestions for {}",
if language_enabled { "Hide" } else { "Show" },
@ -305,7 +305,7 @@ impl CopilotButton {
}
let globally_enabled = cx.global::<Settings>().show_copilot_suggestions(None);
menu_options.push(ContextMenuItem::item(
menu_options.push(ContextMenuItem::action(
if globally_enabled {
"Hide Suggestions for All Files"
} else {
@ -317,7 +317,7 @@ impl CopilotButton {
menu_options.push(ContextMenuItem::Separator);
let icon_style = settings.theme.copilot.out_link_icon.clone();
menu_options.push(ContextMenuItem::item(
menu_options.push(ContextMenuItem::action(
move |state: &mut MouseState, style: &theme::ContextMenuItem| {
Flex::row()
.with_child(Label::new("Copilot Settings", style.label.clone()))
@ -328,7 +328,7 @@ impl CopilotButton {
OsOpen::new(COPILOT_SETTINGS_URL),
));
menu_options.push(ContextMenuItem::item("Sign Out", SignOut));
menu_options.push(ContextMenuItem::action("Sign Out", SignOut));
self.popup_menu.update(cx, |menu, cx| {
menu.show(

View file

@ -51,18 +51,18 @@ pub fn deploy_context_menu(
position,
AnchorCorner::TopLeft,
vec![
ContextMenuItem::item("Rename Symbol", Rename),
ContextMenuItem::item("Go to Definition", GoToDefinition),
ContextMenuItem::item("Go to Type Definition", GoToTypeDefinition),
ContextMenuItem::item("Find All References", FindAllReferences),
ContextMenuItem::item(
ContextMenuItem::action("Rename Symbol", Rename),
ContextMenuItem::action("Go to Definition", GoToDefinition),
ContextMenuItem::action("Go to Type Definition", GoToTypeDefinition),
ContextMenuItem::action("Find All References", FindAllReferences),
ContextMenuItem::action(
"Code Actions",
ToggleCodeActions {
deployed_from_indicator: false,
},
),
ContextMenuItem::Separator,
ContextMenuItem::item("Reveal in Finder", RevealInFinder),
ContextMenuItem::action("Reveal in Finder", RevealInFinder),
],
cx,
);

View file

@ -296,38 +296,38 @@ impl ProjectPanel {
if let Some((worktree, entry)) = self.selected_entry(cx) {
let is_root = Some(entry) == worktree.root_entry();
if !project.is_remote() {
menu_entries.push(ContextMenuItem::item(
menu_entries.push(ContextMenuItem::action(
"Add Folder to Project",
workspace::AddFolderToProject,
));
if is_root {
menu_entries.push(ContextMenuItem::item(
menu_entries.push(ContextMenuItem::action(
"Remove from Project",
workspace::RemoveWorktreeFromProject(worktree_id),
));
}
}
menu_entries.push(ContextMenuItem::item("New File", NewFile));
menu_entries.push(ContextMenuItem::item("New Folder", NewDirectory));
menu_entries.push(ContextMenuItem::action("New File", NewFile));
menu_entries.push(ContextMenuItem::action("New Folder", NewDirectory));
menu_entries.push(ContextMenuItem::Separator);
menu_entries.push(ContextMenuItem::item("Cut", Cut));
menu_entries.push(ContextMenuItem::item("Copy", Copy));
menu_entries.push(ContextMenuItem::action("Cut", Cut));
menu_entries.push(ContextMenuItem::action("Copy", Copy));
menu_entries.push(ContextMenuItem::Separator);
menu_entries.push(ContextMenuItem::item("Copy Path", CopyPath));
menu_entries.push(ContextMenuItem::item(
menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath));
menu_entries.push(ContextMenuItem::action(
"Copy Relative Path",
CopyRelativePath,
));
menu_entries.push(ContextMenuItem::item("Reveal in Finder", RevealInFinder));
menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder));
if let Some(clipboard_entry) = self.clipboard_entry {
if clipboard_entry.worktree_id() == worktree.id() {
menu_entries.push(ContextMenuItem::item("Paste", Paste));
menu_entries.push(ContextMenuItem::action("Paste", Paste));
}
}
menu_entries.push(ContextMenuItem::Separator);
menu_entries.push(ContextMenuItem::item("Rename", Rename));
menu_entries.push(ContextMenuItem::action("Rename", Rename));
if !is_root {
menu_entries.push(ContextMenuItem::item("Delete", Delete));
menu_entries.push(ContextMenuItem::action("Delete", Delete));
}
}

View file

@ -134,7 +134,7 @@ impl TerminalButton {
_action: &DeployTerminalMenu,
cx: &mut ViewContext<Self>,
) {
let mut menu_options = vec![ContextMenuItem::item("New Terminal", NewTerminal)];
let mut menu_options = vec![ContextMenuItem::action("New Terminal", NewTerminal)];
if let Some(workspace) = self.workspace.upgrade(cx) {
let project = workspace.read(cx).project().read(cx);
@ -146,7 +146,7 @@ impl TerminalButton {
for local_terminal_handle in local_terminal_handles {
if let Some(terminal) = local_terminal_handle.upgrade(cx) {
menu_options.push(ContextMenuItem::item(
menu_options.push(ContextMenuItem::action(
terminal.read(cx).title(),
FocusTerminal {
terminal_handle: local_terminal_handle.clone(),

View file

@ -199,8 +199,8 @@ impl TerminalView {
pub fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext<Self>) {
let menu_entries = vec![
ContextMenuItem::item("Clear", Clear),
ContextMenuItem::item("Close", pane::CloseActiveItem),
ContextMenuItem::action("Clear", Clear),
ContextMenuItem::action("Close", pane::CloseActiveItem),
];
self.context_menu.update(cx, |menu, cx| {

View file

@ -1229,10 +1229,10 @@ impl Pane {
Default::default(),
AnchorCorner::TopRight,
vec![
ContextMenuItem::item("Split Right", SplitRight),
ContextMenuItem::item("Split Left", SplitLeft),
ContextMenuItem::item("Split Up", SplitUp),
ContextMenuItem::item("Split Down", SplitDown),
ContextMenuItem::action("Split Right", SplitRight),
ContextMenuItem::action("Split Left", SplitLeft),
ContextMenuItem::action("Split Up", SplitUp),
ContextMenuItem::action("Split Down", SplitDown),
],
cx,
);
@ -1247,9 +1247,9 @@ impl Pane {
Default::default(),
AnchorCorner::TopRight,
vec![
ContextMenuItem::item("Anchor Dock Right", AnchorDockRight),
ContextMenuItem::item("Anchor Dock Bottom", AnchorDockBottom),
ContextMenuItem::item("Expand Dock", ExpandDock),
ContextMenuItem::action("Anchor Dock Right", AnchorDockRight),
ContextMenuItem::action("Anchor Dock Bottom", AnchorDockBottom),
ContextMenuItem::action("Expand Dock", ExpandDock),
],
cx,
);
@ -1264,9 +1264,9 @@ impl Pane {
Default::default(),
AnchorCorner::TopRight,
vec![
ContextMenuItem::item("New File", NewFile),
ContextMenuItem::item("New Terminal", NewTerminal),
ContextMenuItem::item("New Search", NewSearch),
ContextMenuItem::action("New File", NewFile),
ContextMenuItem::action("New Terminal", NewTerminal),
ContextMenuItem::action("New Search", NewSearch),
],
cx,
);
@ -1293,40 +1293,40 @@ impl Pane {
AnchorCorner::TopLeft,
if is_active_item {
vec![
ContextMenuItem::item("Close Active Item", CloseActiveItem),
ContextMenuItem::item("Close Inactive Items", CloseInactiveItems),
ContextMenuItem::item("Close Clean Items", CloseCleanItems),
ContextMenuItem::item("Close Items To The Left", CloseItemsToTheLeft),
ContextMenuItem::item("Close Items To The Right", CloseItemsToTheRight),
ContextMenuItem::item("Close All Items", CloseAllItems),
ContextMenuItem::action("Close Active Item", CloseActiveItem),
ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
ContextMenuItem::action("Close Clean Items", CloseCleanItems),
ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft),
ContextMenuItem::action("Close Items To The Right", CloseItemsToTheRight),
ContextMenuItem::action("Close All Items", CloseAllItems),
]
} else {
// In the case of the user right clicking on a non-active tab, for some item-closing commands, we need to provide the id of the tab, for the others, we can reuse the existing command.
vec![
ContextMenuItem::item(
ContextMenuItem::action(
"Close Inactive Item",
CloseItemById {
item_id: target_item_id,
pane: target_pane.clone(),
},
),
ContextMenuItem::item("Close Inactive Items", CloseInactiveItems),
ContextMenuItem::item("Close Clean Items", CloseCleanItems),
ContextMenuItem::item(
ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
ContextMenuItem::action("Close Clean Items", CloseCleanItems),
ContextMenuItem::action(
"Close Items To The Left",
CloseItemsToTheLeftById {
item_id: target_item_id,
pane: target_pane.clone(),
},
),
ContextMenuItem::item(
ContextMenuItem::action(
"Close Items To The Right",
CloseItemsToTheRightById {
item_id: target_item_id,
pane: target_pane.clone(),
},
),
ContextMenuItem::item("Close All Items", CloseAllItems),
ContextMenuItem::action("Close All Items", CloseAllItems),
]
},
cx,