From 79f3cb1225729ceaed3fd3ab0e1a23f4f7bf9f8f Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Tue, 1 Jul 2025 22:06:45 -0500 Subject: [PATCH] keymap_ui: Add context menu for table rows (#33747) Closes #ISSUE Adds a right click context menu to table rows, refactoring the table API to support more general row rendering in the process, and creating actions for the couple of operations available in the context menu. Additionally includes an only partially related change to the context menu API, which makes it easier to have actions that are disabled based on a boolean value. Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/editor/src/mouse_context_menu.rs | 38 ++- crates/git_ui/src/git_panel.rs | 51 ++-- crates/project_panel/src/project_panel.rs | 12 +- crates/settings_ui/src/keybindings.rs | 267 ++++++++++++++---- crates/settings_ui/src/ui_components/table.rs | 93 +++--- crates/ui/src/components/context_menu.rs | 5 +- crates/ui/src/components/right_click_menu.rs | 15 +- .../ui/src/components/stories/context_menu.rs | 8 +- crates/workspace/src/dock.rs | 2 +- crates/workspace/src/pane.rs | 2 +- crates/zed/src/zed.rs | 1 + crates/zed/src/zed/quick_action_bar.rs | 18 +- 12 files changed, 308 insertions(+), 204 deletions(-) diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index 4780f1f565..cbb6791a2f 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -233,31 +233,25 @@ pub fn deploy_context_menu( .action("Copy and Trim", Box::new(CopyAndTrim)) .action("Paste", Box::new(Paste)) .separator() - .map(|builder| { - let reveal_in_finder_label = if cfg!(target_os = "macos") { + .action_disabled_when( + !has_reveal_target, + if cfg!(target_os = "macos") { "Reveal in Finder" } else { "Reveal in File Manager" - }; - const OPEN_IN_TERMINAL_LABEL: &str = "Open in Terminal"; - if has_reveal_target { - builder - .action(reveal_in_finder_label, Box::new(RevealInFileManager)) - .action(OPEN_IN_TERMINAL_LABEL, Box::new(OpenInTerminal)) - } else { - builder - .disabled_action(reveal_in_finder_label, Box::new(RevealInFileManager)) - .disabled_action(OPEN_IN_TERMINAL_LABEL, Box::new(OpenInTerminal)) - } - }) - .map(|builder| { - const COPY_PERMALINK_LABEL: &str = "Copy Permalink"; - if has_git_repo { - builder.action(COPY_PERMALINK_LABEL, Box::new(CopyPermalinkToLine)) - } else { - builder.disabled_action(COPY_PERMALINK_LABEL, Box::new(CopyPermalinkToLine)) - } - }); + }, + Box::new(RevealInFileManager), + ) + .action_disabled_when( + !has_reveal_target, + "Open in Terminal", + Box::new(OpenInTerminal), + ) + .action_disabled_when( + !has_git_repo, + "Copy Permalink", + Box::new(CopyPermalinkToLine), + ); match focus { Some(focus) => builder.context(focus), None => builder, diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 51ef90fd38..86a67fcc59 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -122,40 +122,29 @@ fn git_panel_context_menu( ContextMenu::build(window, cx, move |context_menu, _, _| { context_menu .context(focus_handle) - .map(|menu| { - if state.has_unstaged_changes { - menu.action("Stage All", StageAll.boxed_clone()) - } else { - menu.disabled_action("Stage All", StageAll.boxed_clone()) - } - }) - .map(|menu| { - if state.has_staged_changes { - menu.action("Unstage All", UnstageAll.boxed_clone()) - } else { - menu.disabled_action("Unstage All", UnstageAll.boxed_clone()) - } - }) + .action_disabled_when( + !state.has_unstaged_changes, + "Stage All", + StageAll.boxed_clone(), + ) + .action_disabled_when( + !state.has_staged_changes, + "Unstage All", + UnstageAll.boxed_clone(), + ) .separator() .action("Open Diff", project_diff::Diff.boxed_clone()) .separator() - .map(|menu| { - if state.has_tracked_changes { - menu.action("Discard Tracked Changes", RestoreTrackedFiles.boxed_clone()) - } else { - menu.disabled_action( - "Discard Tracked Changes", - RestoreTrackedFiles.boxed_clone(), - ) - } - }) - .map(|menu| { - if state.has_new_changes { - menu.action("Trash Untracked Files", TrashUntrackedFiles.boxed_clone()) - } else { - menu.disabled_action("Trash Untracked Files", TrashUntrackedFiles.boxed_clone()) - } - }) + .action_disabled_when( + !state.has_tracked_changes, + "Discard Tracked Changes", + RestoreTrackedFiles.boxed_clone(), + ) + .action_disabled_when( + !state.has_new_changes, + "Trash Untracked Files", + TrashUntrackedFiles.boxed_clone(), + ) }) } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 4db83bcf4c..657cccf98a 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -820,13 +820,11 @@ impl ProjectPanel { .action("Copy", Box::new(Copy)) .action("Duplicate", Box::new(Duplicate)) // TODO: Paste should always be visible, cbut disabled when clipboard is empty - .map(|menu| { - if self.clipboard.as_ref().is_some() { - menu.action("Paste", Box::new(Paste)) - } else { - menu.disabled_action("Paste", Box::new(Paste)) - } - }) + .action_disabled_when( + self.clipboard.as_ref().is_none(), + "Paste", + Box::new(Paste), + ) .separator() .action("Copy Path", Box::new(zed_actions::workspace::CopyPath)) .action( diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 73b5d06ba0..4adac417bf 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -9,7 +9,7 @@ use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ AppContext as _, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, KeyContext, Keystroke, ModifiersChangedEvent, ScrollStrategy, StyledText, - Subscription, WeakEntity, actions, div, + Subscription, WeakEntity, actions, div, transparent_black, }; use language::{Language, LanguageConfig}; use settings::KeybindSource; @@ -17,8 +17,8 @@ use settings::KeybindSource; use util::ResultExt; use ui::{ - ActiveTheme as _, App, BorrowAppContext, ParentElement as _, Render, SharedString, Styled as _, - Window, prelude::*, + ActiveTheme as _, App, BorrowAppContext, ContextMenu, ParentElement as _, Render, SharedString, + Styled as _, Window, prelude::*, right_click_menu, }; use workspace::{Item, ModalView, SerializableItem, Workspace, register_serializable_item}; @@ -30,6 +30,9 @@ use crate::{ actions!(zed, [OpenKeymapEditor]); +const KEYMAP_EDITOR_NAMESPACE: &'static str = "keymap_editor"; +actions!(keymap_editor, [EditBinding, CopyAction, CopyContext]); + pub fn init(cx: &mut App) { let keymap_event_channel = KeymapEventChannel::new(); cx.set_global(keymap_event_channel); @@ -59,6 +62,7 @@ pub fn init(cx: &mut App) { command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _cx| { filter.hide_action_types(&keymap_ui_actions); + filter.hide_namespace(KEYMAP_EDITOR_NAMESPACE); }); cx.observe_flag::( @@ -69,6 +73,7 @@ pub fn init(cx: &mut App) { cx, |filter, _cx| { filter.show_action_types(keymap_ui_actions.iter()); + filter.show_namespace(KEYMAP_EDITOR_NAMESPACE); }, ); } else { @@ -76,6 +81,7 @@ pub fn init(cx: &mut App) { cx, |filter, _cx| { filter.hide_action_types(&keymap_ui_actions); + filter.hide_namespace(KEYMAP_EDITOR_NAMESPACE); }, ); } @@ -231,8 +237,8 @@ impl KeymapEditor { let context = key_binding .predicate() - .map(|predicate| predicate.to_string()) - .unwrap_or_else(|| "".to_string()); + .map(|predicate| KeybindContextString::Local(predicate.to_string().into())) + .unwrap_or(KeybindContextString::Global); let source = source.map(|source| (source, source.name().into())); @@ -249,7 +255,7 @@ impl KeymapEditor { ui_key_binding, action: action_name.into(), action_input, - context: context.into(), + context: Some(context), source, }); string_match_candidates.push(string_match_candidate); @@ -264,7 +270,7 @@ impl KeymapEditor { ui_key_binding: None, action: (*action_name).into(), action_input: None, - context: empty.clone(), + context: None, source: None, }); string_match_candidates.push(string_match_candidate); @@ -345,6 +351,33 @@ impl KeymapEditor { }); } + fn focus_search( + &mut self, + _: &search::FocusSearch, + window: &mut Window, + cx: &mut Context, + ) { + if !self + .filter_editor + .focus_handle(cx) + .contains_focused(window, cx) + { + window.focus(&self.filter_editor.focus_handle(cx)); + } else { + self.filter_editor.update(cx, |editor, cx| { + editor.select_all(&Default::default(), window, cx); + }); + } + self.selected_index.take(); + } + + fn selected_binding(&self) -> Option<&ProcessedKeybinding> { + self.selected_index + .and_then(|match_index| self.matches.get(match_index)) + .map(|r#match| r#match.candidate_id) + .and_then(|keybind_index| self.keybindings.get(keybind_index)) + } + fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context) { if let Some(selected) = self.selected_index { let selected = selected + 1; @@ -408,25 +441,18 @@ impl KeymapEditor { } fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { - let Some(index) = self.selected_index else { - return; - }; - let keybind = self.keybindings[self.matches[index].candidate_id].clone(); - - self.edit_keybinding(keybind, window, cx); + self.edit_selected_keybinding(window, cx); } - fn edit_keybinding( - &mut self, - keybind: ProcessedKeybinding, - window: &mut Window, - cx: &mut Context, - ) { + fn edit_selected_keybinding(&mut self, window: &mut Window, cx: &mut Context) { + let Some(keybind) = self.selected_binding() else { + return; + }; self.workspace .update(cx, |workspace, cx| { let fs = workspace.app_state().fs.clone(); workspace.toggle_modal(window, cx, |window, cx| { - let modal = KeybindingEditorModal::new(keybind, fs, window, cx); + let modal = KeybindingEditorModal::new(keybind.clone(), fs, window, cx); window.focus(&modal.focus_handle(cx)); modal }); @@ -434,24 +460,40 @@ impl KeymapEditor { .log_err(); } - fn focus_search( + fn edit_binding(&mut self, _: &EditBinding, window: &mut Window, cx: &mut Context) { + self.edit_selected_keybinding(window, cx); + } + + fn copy_context_to_clipboard( &mut self, - _: &search::FocusSearch, - window: &mut Window, + _: &CopyContext, + _window: &mut Window, cx: &mut Context, ) { - if !self - .filter_editor - .focus_handle(cx) - .contains_focused(window, cx) - { - window.focus(&self.filter_editor.focus_handle(cx)); - } else { - self.filter_editor.update(cx, |editor, cx| { - editor.select_all(&Default::default(), window, cx); - }); - } - self.selected_index.take(); + let context = self + .selected_binding() + .and_then(|binding| binding.context.as_ref()) + .and_then(KeybindContextString::local_str) + .map(|context| context.to_string()); + let Some(context) = context else { + return; + }; + cx.write_to_clipboard(gpui::ClipboardItem::new_string(context.clone())); + } + + fn copy_action_to_clipboard( + &mut self, + _: &CopyAction, + _window: &mut Window, + cx: &mut Context, + ) { + let action = self + .selected_binding() + .map(|binding| binding.action.to_string()); + let Some(action) = action else { + return; + }; + cx.write_to_clipboard(gpui::ClipboardItem::new_string(action.clone())); } } @@ -461,10 +503,43 @@ struct ProcessedKeybinding { ui_key_binding: Option, action: SharedString, action_input: Option, - context: SharedString, + context: Option, source: Option<(KeybindSource, SharedString)>, } +#[derive(Clone, Debug, IntoElement)] +enum KeybindContextString { + Global, + Local(SharedString), +} + +impl KeybindContextString { + const GLOBAL: SharedString = SharedString::new_static(""); + + pub fn local(&self) -> Option<&SharedString> { + match self { + KeybindContextString::Global => None, + KeybindContextString::Local(name) => Some(name), + } + } + + pub fn local_str(&self) -> Option<&str> { + match self { + KeybindContextString::Global => None, + KeybindContextString::Local(name) => Some(name), + } + } +} + +impl RenderOnce for KeybindContextString { + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + match self { + KeybindContextString::Global => KeybindContextString::GLOBAL.clone(), + KeybindContextString::Local(name) => name, + } + } +} + impl Item for KeymapEditor { type Event = (); @@ -486,6 +561,9 @@ impl Render for KeymapEditor { .on_action(cx.listener(Self::select_last)) .on_action(cx.listener(Self::focus_search)) .on_action(cx.listener(Self::confirm)) + .on_action(cx.listener(Self::edit_binding)) + .on_action(cx.listener(Self::copy_action_to_clipboard)) + .on_action(cx.listener(Self::copy_context_to_clipboard)) .size_full() .bg(theme.colors().editor_background) .id("keymap-editor") @@ -514,10 +592,6 @@ impl Render for KeymapEditor { .striped() .column_widths([rems(16.), rems(16.), rems(16.), rems(32.), rems(8.)]) .header(["Action", "Arguments", "Keystrokes", "Context", "Source"]) - .selected_item_index(self.selected_index) - .on_click_row(cx.processor(|this, row_index, _window, _cx| { - this.selected_index = Some(row_index); - })) .uniform_list( "keymap-editor-table", row_count, @@ -538,7 +612,12 @@ impl Render for KeymapEditor { .map_or(gpui::Empty.into_any_element(), |input| { input.into_any_element() }); - let context = binding.context.clone().into_any_element(); + let context = binding + .context + .clone() + .map_or(gpui::Empty.into_any_element(), |context| { + context.into_any_element() + }); let source = binding .source .clone() @@ -549,6 +628,43 @@ impl Render for KeymapEditor { }) .collect() }), + ) + .map_row( + cx.processor(|this, (row_index, row): (usize, Div), _window, cx| { + let is_selected = this.selected_index == Some(row_index); + let row = row + .id(("keymap-table-row", row_index)) + .on_click(cx.listener(move |this, _event, _window, _cx| { + this.selected_index = Some(row_index); + })) + .border_2() + .border_color(transparent_black()) + .when(is_selected, |row| { + row.border_color(cx.theme().colors().panel_focused_border) + }); + + right_click_menu(("keymap-table-row-menu", row_index)) + .trigger({ + let this = cx.weak_entity(); + move |is_menu_open: bool, _window, cx| { + if is_menu_open { + this.update(cx, |this, cx| { + if this.selected_index != Some(row_index) { + this.selected_index = Some(row_index); + cx.notify(); + } + }) + .ok(); + } + row + } + }) + .menu({ + let this = cx.weak_entity(); + move |window, cx| build_keybind_context_menu(&this, window, cx) + }) + .into_any_element() + }), ), ) } @@ -712,7 +828,7 @@ impl Render for KeybindingEditorModal { .await { this.update(cx, |this, cx| { - this.error = Some(err); + this.error = Some(err.to_string()); cx.notify(); }) .log_err(); @@ -741,54 +857,55 @@ async fn save_keybinding_update( new_keystrokes: &[Keystroke], fs: &Arc, tab_size: usize, -) -> Result<(), String> { +) -> anyhow::Result<()> { let keymap_contents = settings::KeymapFile::load_keymap_file(fs) .await - .map_err(|err| format!("Failed to load keymap file: {}", err))?; + .context("Failed to load keymap file")?; let existing_keystrokes = existing .ui_key_binding .as_ref() .map(|keybinding| keybinding.key_binding.keystrokes()) .unwrap_or_default(); + let context = existing + .context + .as_ref() + .and_then(KeybindContextString::local_str); + + let input = existing + .action_input + .as_ref() + .map(|input| input.text.as_ref()); + let operation = if existing.ui_key_binding.is_some() { settings::KeybindUpdateOperation::Replace { target: settings::KeybindUpdateTarget { - context: Some(existing.context.as_ref()).filter(|context| !context.is_empty()), + context, keystrokes: existing_keystrokes, action_name: &existing.action, use_key_equivalents: false, - input: existing - .action_input - .as_ref() - .map(|input| input.text.as_ref()), + input, }, target_source: existing .source .map(|(source, _name)| source) .unwrap_or(KeybindSource::User), source: settings::KeybindUpdateTarget { - context: Some(existing.context.as_ref()).filter(|context| !context.is_empty()), + context, keystrokes: new_keystrokes, action_name: &existing.action, use_key_equivalents: false, - input: existing - .action_input - .as_ref() - .map(|input| input.text.as_ref()), + input, }, } } else { - return Err( - "Not Implemented: Creating new bindings from unbound actions is not supported yet." - .to_string(), - ); + anyhow::bail!("Adding new bindings not implemented yet"); }; let updated_keymap_contents = settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) - .map_err(|err| format!("Failed to update keybinding: {}", err))?; + .context("Failed to update keybinding")?; fs.atomic_write(paths::keymap_file().clone(), updated_keymap_contents) .await - .map_err(|err| format!("Failed to write keymap file: {}", err))?; + .context("Failed to write keymap file")?; Ok(()) } @@ -903,6 +1020,36 @@ impl Render for KeybindInput { } } +fn build_keybind_context_menu( + this: &WeakEntity, + window: &mut Window, + cx: &mut App, +) -> Entity { + ContextMenu::build(window, cx, |menu, _window, cx| { + let Some(this) = this.upgrade() else { + return menu; + }; + let selected_binding = this.read_with(cx, |this, _cx| this.selected_binding().cloned()); + let Some(selected_binding) = selected_binding else { + return menu; + }; + + let selected_binding_has_context = selected_binding + .context + .as_ref() + .and_then(KeybindContextString::local) + .is_some(); + + menu.action("Edit Binding", Box::new(EditBinding)) + .action("Copy action", Box::new(CopyAction)) + .action_disabled_when( + !selected_binding_has_context, + "Copy Context", + Box::new(CopyContext), + ) + }) +} + impl SerializableItem for KeymapEditor { fn serialized_item_kind() -> &'static str { "KeymapEditor" diff --git a/crates/settings_ui/src/ui_components/table.rs b/crates/settings_ui/src/ui_components/table.rs index 62f597e148..bce131e481 100644 --- a/crates/settings_ui/src/ui_components/table.rs +++ b/crates/settings_ui/src/ui_components/table.rs @@ -155,8 +155,6 @@ impl TableInteractionState { self.vertical_scrollbar.hide(window, cx); } - // fn listener(this: Entity, fn: F) -> - pub fn listener( this: &Entity, f: impl Fn(&mut Self, &E, &mut Window, &mut Context) + 'static, @@ -353,9 +351,8 @@ pub struct Table { headers: Option<[AnyElement; COLS]>, rows: TableContents, interaction_state: Option>, - selected_item_index: Option, column_widths: Option<[Length; COLS]>, - on_click_row: Option>, + map_row: Option AnyElement>>, } impl Table { @@ -367,9 +364,8 @@ impl Table { headers: None, rows: TableContents::Vec(Vec::new()), interaction_state: None, - selected_item_index: None, column_widths: None, - on_click_row: None, + map_row: None, } } @@ -418,11 +414,6 @@ impl Table { self } - pub fn selected_item_index(mut self, selected_item_index: Option) -> Self { - self.selected_item_index = selected_item_index; - self - } - pub fn header(mut self, headers: [impl IntoElement; COLS]) -> Self { self.headers = Some(headers.map(IntoElement::into_any_element)); self @@ -440,11 +431,11 @@ impl Table { self } - pub fn on_click_row( + pub fn map_row( mut self, - callback: impl Fn(usize, &mut Window, &mut App) + 'static, + callback: impl Fn((usize, Div), &mut Window, &mut App) -> AnyElement + 'static, ) -> Self { - self.on_click_row = Some(Rc::new(callback)); + self.map_row = Some(Rc::new(callback)); self } } @@ -465,7 +456,8 @@ pub fn render_row( row_index: usize, items: [impl IntoElement; COLS], table_context: TableRenderContext, - cx: &App, + window: &mut Window, + cx: &mut App, ) -> AnyElement { let is_striped = table_context.striped; let is_last = row_index == table_context.total_row_count - 1; @@ -477,43 +469,33 @@ pub fn render_row( let column_widths = table_context .column_widths .map_or([None; COLS], |widths| widths.map(Some)); - let is_selected = table_context.selected_item_index == Some(row_index); - let row = div() - .w_full() - .border_2() - .border_color(transparent_black()) - .when(is_selected, |row| { - row.border_color(cx.theme().colors().panel_focused_border) - }) - .child( - div() - .w_full() - .flex() - .flex_row() - .items_center() - .justify_between() - .px_1p5() - .py_1() - .when_some(bg, |row, bg| row.bg(bg)) - .when(!is_striped, |row| { - row.border_b_1() - .border_color(transparent_black()) - .when(!is_last, |row| row.border_color(cx.theme().colors().border)) - }) - .children( - items - .map(IntoElement::into_any_element) - .into_iter() - .zip(column_widths) - .map(|(cell, width)| base_cell_style(width, cx).child(cell)), - ), - ); + let row = div().w_full().child( + div() + .w_full() + .flex() + .flex_row() + .items_center() + .justify_between() + .px_1p5() + .py_1() + .when_some(bg, |row, bg| row.bg(bg)) + .when(!is_striped, |row| { + row.border_b_1() + .border_color(transparent_black()) + .when(!is_last, |row| row.border_color(cx.theme().colors().border)) + }) + .children( + items + .map(IntoElement::into_any_element) + .into_iter() + .zip(column_widths) + .map(|(cell, width)| base_cell_style(width, cx).child(cell)), + ), + ); - if let Some(on_click) = table_context.on_click_row { - row.id(("table-row", row_index)) - .on_click(move |_, window, cx| on_click(row_index, window, cx)) - .into_any_element() + if let Some(map_row) = table_context.map_row { + map_row((row_index, row), window, cx) } else { row.into_any_element() } @@ -547,9 +529,8 @@ pub fn render_header( pub struct TableRenderContext { pub striped: bool, pub total_row_count: usize, - pub selected_item_index: Option, pub column_widths: Option<[Length; COLS]>, - pub on_click_row: Option>, + pub map_row: Option AnyElement>>, } impl TableRenderContext { @@ -558,14 +539,13 @@ impl TableRenderContext { striped: table.striped, total_row_count: table.rows.len(), column_widths: table.column_widths, - selected_item_index: table.selected_item_index, - on_click_row: table.on_click_row.clone(), + map_row: table.map_row.clone(), } } } impl RenderOnce for Table { - fn render(mut self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement { let table_context = TableRenderContext::new(&self); let interaction_state = self.interaction_state.and_then(|state| state.upgrade()); @@ -598,7 +578,7 @@ impl RenderOnce for Table { .map(|parent| match self.rows { TableContents::Vec(items) => { parent.children(items.into_iter().enumerate().map(|(index, row)| { - render_row(index, row, table_context.clone(), cx) + render_row(index, row, table_context.clone(), window, cx) })) } TableContents::UniformList(uniform_list_data) => parent.child( @@ -617,6 +597,7 @@ impl RenderOnce for Table { row_index, row, table_context.clone(), + window, cx, ) }) diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 91b2dc8fd4..d7080f21f4 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -503,8 +503,9 @@ impl ContextMenu { self } - pub fn disabled_action( + pub fn action_disabled_when( mut self, + disabled: bool, label: impl Into, action: Box, ) -> Self { @@ -522,7 +523,7 @@ impl ContextMenu { icon_size: IconSize::Small, icon_position: IconPosition::End, icon_color: None, - disabled: true, + disabled, documentation_aside: None, end_slot_icon: None, end_slot_title: None, diff --git a/crates/ui/src/components/right_click_menu.rs b/crates/ui/src/components/right_click_menu.rs index 3328644e8e..85ef549bc0 100644 --- a/crates/ui/src/components/right_click_menu.rs +++ b/crates/ui/src/components/right_click_menu.rs @@ -9,7 +9,7 @@ use gpui::{ pub struct RightClickMenu { id: ElementId, - child_builder: Option AnyElement + 'static>>, + child_builder: Option AnyElement + 'static>>, menu_builder: Option Entity + 'static>>, anchor: Option, attach: Option, @@ -23,11 +23,11 @@ impl RightClickMenu { pub fn trigger(mut self, e: F) -> Self where - F: FnOnce(bool) -> E + 'static, + F: FnOnce(bool, &mut Window, &mut App) -> E + 'static, E: IntoElement + 'static, { - self.child_builder = Some(Box::new(move |is_menu_active| { - e(is_menu_active).into_any_element() + self.child_builder = Some(Box::new(move |is_menu_active, window, cx| { + e(is_menu_active, window, cx).into_any_element() })); self } @@ -149,10 +149,9 @@ impl Element for RightClickMenu { element }); - let mut child_element = this - .child_builder - .take() - .map(|child_builder| (child_builder)(element_state.menu.borrow().is_some())); + let mut child_element = this.child_builder.take().map(|child_builder| { + (child_builder)(element_state.menu.borrow().is_some(), window, cx) + }); let child_layout_id = child_element .as_mut() diff --git a/crates/ui/src/components/stories/context_menu.rs b/crates/ui/src/components/stories/context_menu.rs index b34c65a89b..197964adc8 100644 --- a/crates/ui/src/components/stories/context_menu.rs +++ b/crates/ui/src/components/stories/context_menu.rs @@ -47,12 +47,12 @@ impl Render for ContextMenuStory { .justify_between() .child( right_click_menu("test2") - .trigger(|_| Label::new("TOP LEFT")) + .trigger(|_, _, _| Label::new("TOP LEFT")) .menu(move |window, cx| build_menu(window, cx, "top left")), ) .child( right_click_menu("test1") - .trigger(|_| Label::new("BOTTOM LEFT")) + .trigger(|_, _, _| Label::new("BOTTOM LEFT")) .anchor(Corner::BottomLeft) .attach(Corner::TopLeft) .menu(move |window, cx| build_menu(window, cx, "bottom left")), @@ -65,13 +65,13 @@ impl Render for ContextMenuStory { .justify_between() .child( right_click_menu("test3") - .trigger(|_| Label::new("TOP RIGHT")) + .trigger(|_, _, _| Label::new("TOP RIGHT")) .anchor(Corner::TopRight) .menu(move |window, cx| build_menu(window, cx, "top right")), ) .child( right_click_menu("test4") - .trigger(|_| Label::new("BOTTOM RIGHT")) + .trigger(|_, _, _| Label::new("BOTTOM RIGHT")) .anchor(Corner::BottomRight) .attach(Corner::TopRight) .menu(move |window, cx| build_menu(window, cx, "bottom right")), diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 66336c7be6..8fcd55b784 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -902,7 +902,7 @@ impl Render for PanelButtons { }) .anchor(menu_anchor) .attach(menu_attach) - .trigger(move |is_active| { + .trigger(move |is_active, _window, _cx| { IconButton::new(name, icon) .icon_size(IconSize::Small) .toggle_state(is_active_button) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 5c04912d6b..cb2dd99f5e 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2521,7 +2521,7 @@ impl Pane { let pane = cx.entity().downgrade(); let menu_context = item.item_focus_handle(cx); right_click_menu(ix) - .trigger(|_| tab) + .trigger(|_, _, _| tab) .menu(move |window, cx| { let pane = pane.clone(); let menu_context = menu_context.clone(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 333282611b..944e6b26af 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4311,6 +4311,7 @@ mod tests { "icon_theme_selector", "jj", "journal", + "keymap_editor", "language_selector", "lsp_tool", "markdown", diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index 85e28c6ae8..c998ac1075 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -258,18 +258,12 @@ impl Render for QuickActionBar { .action("Next Problem", Box::new(GoToDiagnostic)) .action("Previous Problem", Box::new(GoToPreviousDiagnostic)) .separator() - .map(|menu| { - if has_diff_hunks { - menu.action("Next Hunk", Box::new(GoToHunk)) - .action("Previous Hunk", Box::new(GoToPreviousHunk)) - } else { - menu.disabled_action("Next Hunk", Box::new(GoToHunk)) - .disabled_action( - "Previous Hunk", - Box::new(GoToPreviousHunk), - ) - } - }) + .action_disabled_when(!has_diff_hunks, "Next Hunk", Box::new(GoToHunk)) + .action_disabled_when( + !has_diff_hunks, + "Previous Hunk", + Box::new(GoToPreviousHunk), + ) .separator() .action("Move Line Up", Box::new(MoveLineUp)) .action("Move Line Down", Box::new(MoveLineDown))