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:
parent
faca128304
commit
79f3cb1225
12 changed files with 308 additions and 204 deletions
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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")),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -4311,6 +4311,7 @@ mod tests {
|
|||
"icon_theme_selector",
|
||||
"jj",
|
||||
"journal",
|
||||
"keymap_editor",
|
||||
"language_selector",
|
||||
"lsp_tool",
|
||||
"markdown",
|
||||
|
|
|
@ -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))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue