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 | 95 +++---- 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, 309 insertions(+), 205 deletions(-) diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index 4780f1f56582bf675d7cd7deb7b8f8effb98bfae..cbb6791a2f0c7bba9fa0da9774d71eedd78f2c55 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 51ef90fd38287a0b28debb90baf97c135f4ab9d4..86a67fcc59d6ea47395885c424561163411c975a 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 4db83bcf4c897d3a9bddf304ee96b3de600899bb..657cccf98a1007660e1563cc16585d7658e95b7c 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 73b5d06ba0b0be1b8bfa2399af9a1a650330809c..4adac417bf9e517f9146cca3cfb8707394d1faff 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 62f597e148b013ffaf59eca99f5105920701fcb5..bce131e48187185a7fe3ebb49d3f6295e703e8c8 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)), - ), - ); - - 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() + 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(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 91b2dc8fd414d9817580f5fa12a99c3318ec24f7..d7080f21f4f374777fab03104dad339d250f2d2a 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 3328644e8e192b1cc8d3456f009d0745a3bef669..85ef549bc017eb0708fe81c41693232a2d6deaa5 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 b34c65a89b75588163a6cb5887e0d6cb37257077..197964adc86ef25b52eacd0631e4e7989b49bec0 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 66336c7be64b6c076fd014ae209ad0aaefecb623..8fcd55b784fc4202a34a1a34f72590933bb0f3d1 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 5c04912d6b07e236652d04a220f00038287a76e6..cb2dd99f5e69842d64c62ff78911799aa323c14a 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 333282611befcc5f71cebf15cb29d5c6713bfbe8..944e6b26af3ed4295fb1f4b72763698153481103 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 85e28c6ae826b479731e35397d8d4195628a06d4..c998ac10755c91c4c1a7ea172eadf3f4d86fce57 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))