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 ...
This commit is contained in:
Ben Kunkle 2025-07-01 22:06:45 -05:00 committed by GitHub
parent faca128304
commit 79f3cb1225
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 308 additions and 204 deletions

View file

@ -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,

View file

@ -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(),
)
})
}

View file

@ -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(

View file

@ -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::<SettingsUiFeatureFlag, _>(
@ -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(|| "<global>".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<Self>,
) {
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<Self>) {
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<Self>) {
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<Self>,
) {
fn edit_selected_keybinding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
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>) {
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<Self>,
) {
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<Self>,
) {
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<ui::KeyBinding>,
action: SharedString,
action_input: Option<TextWithSyntaxHighlighting>,
context: SharedString,
context: Option<KeybindContextString>,
source: Option<(KeybindSource, SharedString)>,
}
#[derive(Clone, Debug, IntoElement)]
enum KeybindContextString {
Global,
Local(SharedString),
}
impl KeybindContextString {
const GLOBAL: SharedString = SharedString::new_static("<global>");
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<dyn Fs>,
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<KeymapEditor>,
window: &mut Window,
cx: &mut App,
) -> Entity<ContextMenu> {
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"

View file

@ -155,8 +155,6 @@ impl TableInteractionState {
self.vertical_scrollbar.hide(window, cx);
}
// fn listener(this: Entity<Self>, fn: F) ->
pub fn listener<E: ?Sized>(
this: &Entity<Self>,
f: impl Fn(&mut Self, &E, &mut Window, &mut Context<Self>) + 'static,
@ -353,9 +351,8 @@ pub struct Table<const COLS: usize = 3> {
headers: Option<[AnyElement; COLS]>,
rows: TableContents<COLS>,
interaction_state: Option<WeakEntity<TableInteractionState>>,
selected_item_index: Option<usize>,
column_widths: Option<[Length; COLS]>,
on_click_row: Option<Rc<dyn Fn(usize, &mut Window, &mut App)>>,
map_row: Option<Rc<dyn Fn((usize, Div), &mut Window, &mut App) -> AnyElement>>,
}
impl<const COLS: usize> Table<COLS> {
@ -367,9 +364,8 @@ impl<const COLS: usize> Table<COLS> {
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<const COLS: usize> Table<COLS> {
self
}
pub fn selected_item_index(mut self, selected_item_index: Option<usize>) -> 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<const COLS: usize> Table<COLS> {
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<const COLS: usize>(
row_index: usize,
items: [impl IntoElement; COLS],
table_context: TableRenderContext<COLS>,
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<const COLS: usize>(
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<const COLS: usize>(
pub struct TableRenderContext<const COLS: usize> {
pub striped: bool,
pub total_row_count: usize,
pub selected_item_index: Option<usize>,
pub column_widths: Option<[Length; COLS]>,
pub on_click_row: Option<Rc<dyn Fn(usize, &mut Window, &mut App)>>,
pub map_row: Option<Rc<dyn Fn((usize, Div), &mut Window, &mut App) -> AnyElement>>,
}
impl<const COLS: usize> TableRenderContext<COLS> {
@ -558,14 +539,13 @@ impl<const COLS: usize> TableRenderContext<COLS> {
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<const COLS: usize> RenderOnce for Table<COLS> {
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<const COLS: usize> RenderOnce for Table<COLS> {
.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<const COLS: usize> RenderOnce for Table<COLS> {
row_index,
row,
table_context.clone(),
window,
cx,
)
})

View file

@ -503,8 +503,9 @@ impl ContextMenu {
self
}
pub fn disabled_action(
pub fn action_disabled_when(
mut self,
disabled: bool,
label: impl Into<SharedString>,
action: Box<dyn Action>,
) -> 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,

View file

@ -9,7 +9,7 @@ use gpui::{
pub struct RightClickMenu<M: ManagedView> {
id: ElementId,
child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement + 'static>>,
child_builder: Option<Box<dyn FnOnce(bool, &mut Window, &mut App) -> AnyElement + 'static>>,
menu_builder: Option<Rc<dyn Fn(&mut Window, &mut App) -> Entity<M> + 'static>>,
anchor: Option<Corner>,
attach: Option<Corner>,
@ -23,11 +23,11 @@ impl<M: ManagedView> RightClickMenu<M> {
pub fn trigger<F, E>(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<M: ManagedView> Element for RightClickMenu<M> {
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()

View file

@ -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")),

View file

@ -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)

View file

@ -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();

View file

@ -4311,6 +4311,7 @@ mod tests {
"icon_theme_selector",
"jj",
"journal",
"keymap_editor",
"language_selector",
"lsp_tool",
"markdown",

View file

@ -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))