Refine keymap UI design (#34437)
Release Notes: - N/A --------- Co-authored-by: Ben Kunkle <Ben.kunkle@gmail.com>
This commit is contained in:
parent
a65c0b2bff
commit
858e176a1c
6 changed files with 397 additions and 211 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -14684,6 +14684,7 @@ dependencies = [
|
||||||
"language",
|
"language",
|
||||||
"log",
|
"log",
|
||||||
"menu",
|
"menu",
|
||||||
|
"notifications",
|
||||||
"paths",
|
"paths",
|
||||||
"project",
|
"project",
|
||||||
"schemars",
|
"schemars",
|
||||||
|
@ -14695,6 +14696,7 @@ dependencies = [
|
||||||
"tree-sitter-json",
|
"tree-sitter-json",
|
||||||
"tree-sitter-rust",
|
"tree-sitter-rust",
|
||||||
"ui",
|
"ui",
|
||||||
|
"ui_input",
|
||||||
"util",
|
"util",
|
||||||
"workspace",
|
"workspace",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
|
|
3
assets/icons/play_filled.svg
Normal file
3
assets/icons/play_filled.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M5 4L10 7L5 10V4Z" fill="black" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 227 B |
|
@ -191,6 +191,7 @@ pub enum IconName {
|
||||||
Play,
|
Play,
|
||||||
PlayAlt,
|
PlayAlt,
|
||||||
PlayBug,
|
PlayBug,
|
||||||
|
PlayFilled,
|
||||||
Plus,
|
Plus,
|
||||||
PocketKnife,
|
PocketKnife,
|
||||||
Power,
|
Power,
|
||||||
|
|
|
@ -26,6 +26,7 @@ gpui.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
menu.workspace = true
|
menu.workspace = true
|
||||||
|
notifications.workspace = true
|
||||||
paths.workspace = true
|
paths.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
|
@ -37,6 +38,7 @@ theme.workspace = true
|
||||||
tree-sitter-json.workspace = true
|
tree-sitter-json.workspace = true
|
||||||
tree-sitter-rust.workspace = true
|
tree-sitter-rust.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
|
ui_input.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
|
|
|
@ -10,20 +10,22 @@ use feature_flags::FeatureFlagViewExt;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, AnimationExt, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, Entity,
|
Action, Animation, AnimationExt, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent,
|
||||||
EventEmitter, FocusHandle, Focusable, Global, IsZero, KeyContext, KeyDownEvent, Keystroke,
|
Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, IsZero, KeyContext,
|
||||||
ModifiersChangedEvent, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, StyledText,
|
KeyDownEvent, Keystroke, ModifiersChangedEvent, MouseButton, Point, ScrollStrategy,
|
||||||
Subscription, WeakEntity, actions, anchored, deferred, div,
|
ScrollWheelEvent, StyledText, Subscription, WeakEntity, actions, anchored, deferred, div,
|
||||||
};
|
};
|
||||||
use language::{Language, LanguageConfig, ToOffset as _};
|
use language::{Language, LanguageConfig, ToOffset as _};
|
||||||
|
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||||
use settings::{BaseKeymap, KeybindSource, KeymapFile, SettingsAssets};
|
use settings::{BaseKeymap, KeybindSource, KeymapFile, SettingsAssets};
|
||||||
|
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
use ui::{
|
use ui::{
|
||||||
ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, ParentElement as _, Render,
|
ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, Modal, ModalFooter, ModalHeader,
|
||||||
SharedString, Styled as _, Tooltip, Window, prelude::*,
|
ParentElement as _, Render, Section, SharedString, Styled as _, Tooltip, Window, prelude::*,
|
||||||
};
|
};
|
||||||
|
use ui_input::SingleLineInput;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
Item, ModalView, SerializableItem, Workspace, notifications::NotifyTaskExt as _,
|
Item, ModalView, SerializableItem, Workspace, notifications::NotifyTaskExt as _,
|
||||||
register_serializable_item,
|
register_serializable_item,
|
||||||
|
@ -637,7 +639,8 @@ impl KeymapEditor {
|
||||||
"Delete",
|
"Delete",
|
||||||
Box::new(DeleteBinding),
|
Box::new(DeleteBinding),
|
||||||
)
|
)
|
||||||
.action("Copy action", Box::new(CopyAction))
|
.separator()
|
||||||
|
.action("Copy Action", Box::new(CopyAction))
|
||||||
.action_disabled_when(
|
.action_disabled_when(
|
||||||
selected_binding_has_no_context,
|
selected_binding_has_no_context,
|
||||||
"Copy Context",
|
"Copy Context",
|
||||||
|
@ -845,9 +848,15 @@ impl KeymapEditor {
|
||||||
self.search_mode = self.search_mode.invert();
|
self.search_mode = self.search_mode.invert();
|
||||||
self.update_matches(cx);
|
self.update_matches(cx);
|
||||||
|
|
||||||
|
// Update the keystroke editor to turn the `search` bool on
|
||||||
|
self.keystroke_editor.update(cx, |keystroke_editor, cx| {
|
||||||
|
keystroke_editor.set_search_mode(self.search_mode == SearchMode::KeyStroke);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
|
||||||
match self.search_mode {
|
match self.search_mode {
|
||||||
SearchMode::KeyStroke => {
|
SearchMode::KeyStroke => {
|
||||||
window.focus(&self.keystroke_editor.focus_handle(cx));
|
window.focus(&self.keystroke_editor.read(cx).recording_focus_handle(cx));
|
||||||
}
|
}
|
||||||
SearchMode::Normal => {}
|
SearchMode::Normal => {}
|
||||||
}
|
}
|
||||||
|
@ -964,41 +973,60 @@ impl Render for KeymapEditor {
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.bg(theme.colors().editor_background)
|
.bg(theme.colors().editor_background)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
v_flex()
|
||||||
.p_2()
|
.p_2()
|
||||||
.gap_1()
|
.gap_2()
|
||||||
.key_context({
|
|
||||||
let mut context = KeyContext::new_with_defaults();
|
|
||||||
context.add("BufferSearchBar");
|
|
||||||
context
|
|
||||||
})
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
|
||||||
.size_full()
|
|
||||||
.h_8()
|
|
||||||
.pl_2()
|
|
||||||
.pr_1()
|
|
||||||
.py_1()
|
|
||||||
.border_1()
|
|
||||||
.border_color(theme.colors().border)
|
|
||||||
.rounded_lg()
|
|
||||||
.child(self.filter_editor.clone()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
// TODO: Ask Mikyala if there's a way to get have items be aligned by horizontally
|
|
||||||
// without embedding a h_flex in another h_flex
|
|
||||||
h_flex()
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.key_context({
|
||||||
|
let mut context = KeyContext::new_with_defaults();
|
||||||
|
context.add("BufferSearchBar");
|
||||||
|
context
|
||||||
|
})
|
||||||
|
.size_full()
|
||||||
|
.h_8()
|
||||||
|
.pl_2()
|
||||||
|
.pr_1()
|
||||||
|
.py_1()
|
||||||
|
.border_1()
|
||||||
|
.border_color(theme.colors().border)
|
||||||
|
.rounded_lg()
|
||||||
|
.child(self.filter_editor.clone()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
IconButton::new(
|
||||||
|
"KeymapEditorToggleFiltersIcon",
|
||||||
|
IconName::Keyboard,
|
||||||
|
)
|
||||||
|
.shape(ui::IconButtonShape::Square)
|
||||||
|
.tooltip(|window, cx| {
|
||||||
|
Tooltip::for_action(
|
||||||
|
"Search by Keystroke",
|
||||||
|
&ToggleKeystrokeSearch,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.toggle_state(matches!(self.search_mode, SearchMode::KeyStroke))
|
||||||
|
.on_click(|_, window, cx| {
|
||||||
|
window.dispatch_action(ToggleKeystrokeSearch.boxed_clone(), cx);
|
||||||
|
}),
|
||||||
|
)
|
||||||
.when(self.keybinding_conflict_state.any_conflicts(), |this| {
|
.when(self.keybinding_conflict_state.any_conflicts(), |this| {
|
||||||
this.child(
|
this.child(
|
||||||
IconButton::new("KeymapEditorConflictIcon", IconName::Warning)
|
IconButton::new("KeymapEditorConflictIcon", IconName::Warning)
|
||||||
|
.shape(ui::IconButtonShape::Square)
|
||||||
.tooltip({
|
.tooltip({
|
||||||
let filter_state = self.filter_state;
|
let filter_state = self.filter_state;
|
||||||
|
|
||||||
move |window, cx| {
|
move |window, cx| {
|
||||||
Tooltip::for_action(
|
Tooltip::for_action(
|
||||||
match filter_state {
|
match filter_state {
|
||||||
FilterState::All => "Show conflicts",
|
FilterState::All => "Show Conflicts",
|
||||||
FilterState::Conflicts => "Hide conflicts",
|
FilterState::Conflicts => "Hide Conflicts",
|
||||||
},
|
},
|
||||||
&ToggleConflictFilter,
|
&ToggleConflictFilter,
|
||||||
window,
|
window,
|
||||||
|
@ -1006,7 +1034,7 @@ impl Render for KeymapEditor {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.selected_icon_color(Color::Error)
|
.selected_icon_color(Color::Warning)
|
||||||
.toggle_state(matches!(
|
.toggle_state(matches!(
|
||||||
self.filter_state,
|
self.filter_state,
|
||||||
FilterState::Conflicts
|
FilterState::Conflicts
|
||||||
|
@ -1018,36 +1046,22 @@ impl Render for KeymapEditor {
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
}),
|
||||||
.child(
|
)
|
||||||
IconButton::new("KeymapEditorToggleFiltersIcon", IconName::Filter)
|
.when(matches!(self.search_mode, SearchMode::KeyStroke), |this| {
|
||||||
.tooltip(|window, cx| {
|
this.child(
|
||||||
Tooltip::for_action(
|
div()
|
||||||
"Toggle Keystroke Search",
|
.map(|this| {
|
||||||
&ToggleKeystrokeSearch,
|
if self.keybinding_conflict_state.any_conflicts() {
|
||||||
window,
|
this.pr(rems_from_px(54.))
|
||||||
cx,
|
} else {
|
||||||
)
|
this.pr_7()
|
||||||
})
|
}
|
||||||
.toggle_state(matches!(self.search_mode, SearchMode::KeyStroke))
|
})
|
||||||
.on_click(|_, window, cx| {
|
.child(self.keystroke_editor.clone()),
|
||||||
window.dispatch_action(
|
)
|
||||||
ToggleKeystrokeSearch.boxed_clone(),
|
}),
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.when(matches!(self.search_mode, SearchMode::KeyStroke), |this| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.child(self.keystroke_editor.clone())
|
|
||||||
.border_1()
|
|
||||||
.border_color(theme.colors().border)
|
|
||||||
.rounded_lg(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(
|
.child(
|
||||||
Table::new()
|
Table::new()
|
||||||
.interactable(&self.table_interaction_state)
|
.interactable(&self.table_interaction_state)
|
||||||
|
@ -1063,20 +1077,17 @@ impl Render for KeymapEditor {
|
||||||
.filter_map(|index| {
|
.filter_map(|index| {
|
||||||
let candidate_id = this.matches.get(index)?.candidate_id;
|
let candidate_id = this.matches.get(index)?.candidate_id;
|
||||||
let binding = &this.keybindings[candidate_id];
|
let binding = &this.keybindings[candidate_id];
|
||||||
|
let action_name = binding.action_name.clone();
|
||||||
|
|
||||||
let action = div()
|
let action = div()
|
||||||
.child(binding.action_name.clone())
|
|
||||||
.id(("keymap action", index))
|
.id(("keymap action", index))
|
||||||
|
.child(command_palette::humanize_action_name(&action_name))
|
||||||
.when(!context_menu_deployed, |this| {
|
.when(!context_menu_deployed, |this| {
|
||||||
this.tooltip({
|
this.tooltip({
|
||||||
let action_name = binding.action_name.clone();
|
let action_name = binding.action_name.clone();
|
||||||
let action_docs = binding.action_docs;
|
let action_docs = binding.action_docs;
|
||||||
move |_, cx| {
|
move |_, cx| {
|
||||||
let action_tooltip = Tooltip::new(
|
let action_tooltip = Tooltip::new(&action_name);
|
||||||
command_palette::humanize_action_name(
|
|
||||||
&action_name,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
let action_tooltip = match action_docs {
|
let action_tooltip = match action_docs {
|
||||||
Some(docs) => action_tooltip.meta(docs),
|
Some(docs) => action_tooltip.meta(docs),
|
||||||
None => action_tooltip,
|
None => action_tooltip,
|
||||||
|
@ -1285,11 +1296,12 @@ struct KeybindingEditorModal {
|
||||||
editing_keybind: ProcessedKeybinding,
|
editing_keybind: ProcessedKeybinding,
|
||||||
editing_keybind_idx: usize,
|
editing_keybind_idx: usize,
|
||||||
keybind_editor: Entity<KeystrokeInput>,
|
keybind_editor: Entity<KeystrokeInput>,
|
||||||
context_editor: Entity<Editor>,
|
context_editor: Entity<SingleLineInput>,
|
||||||
input_editor: Option<Entity<Editor>>,
|
input_editor: Option<Entity<Editor>>,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
error: Option<InputError>,
|
error: Option<InputError>,
|
||||||
keymap_editor: Entity<KeymapEditor>,
|
keymap_editor: Entity<KeymapEditor>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModalView for KeybindingEditorModal {}
|
impl ModalView for KeybindingEditorModal {}
|
||||||
|
@ -1316,25 +1328,28 @@ impl KeybindingEditorModal {
|
||||||
let keybind_editor = cx
|
let keybind_editor = cx
|
||||||
.new(|cx| KeystrokeInput::new(editing_keybind.keystrokes().map(Vec::from), window, cx));
|
.new(|cx| KeystrokeInput::new(editing_keybind.keystrokes().map(Vec::from), window, cx));
|
||||||
|
|
||||||
let context_editor = cx.new(|cx| {
|
let context_editor: Entity<SingleLineInput> = cx.new(|cx| {
|
||||||
let mut editor = Editor::single_line(window, cx);
|
let input = SingleLineInput::new(window, cx, "Keybinding Context")
|
||||||
|
.label("Edit Context")
|
||||||
|
.label_size(LabelSize::Default);
|
||||||
|
|
||||||
if let Some(context) = editing_keybind
|
if let Some(context) = editing_keybind
|
||||||
.context
|
.context
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(KeybindContextString::local)
|
.and_then(KeybindContextString::local)
|
||||||
{
|
{
|
||||||
editor.set_text(context.clone(), window, cx);
|
input.editor().update(cx, |editor, cx| {
|
||||||
} else {
|
editor.set_text(context.clone(), window, cx);
|
||||||
editor.set_placeholder_text("Keybinding context", cx);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.spawn(async |editor, cx| {
|
let editor_entity = input.editor().clone();
|
||||||
|
cx.spawn(async move |_input_handle, cx| {
|
||||||
let contexts = cx
|
let contexts = cx
|
||||||
.background_spawn(async { collect_contexts_from_assets() })
|
.background_spawn(async { collect_contexts_from_assets() })
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
editor
|
editor_entity
|
||||||
.update(cx, |editor, _cx| {
|
.update(cx, |editor, _cx| {
|
||||||
editor.set_completion_provider(Some(std::rc::Rc::new(
|
editor.set_completion_provider(Some(std::rc::Rc::new(
|
||||||
KeyContextCompletionProvider { contexts },
|
KeyContextCompletionProvider { contexts },
|
||||||
|
@ -1344,17 +1359,19 @@ impl KeybindingEditorModal {
|
||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
|
|
||||||
editor
|
input
|
||||||
});
|
});
|
||||||
|
|
||||||
let input_editor = editing_keybind.action_schema.clone().map(|_schema| {
|
let input_editor = editing_keybind.action_schema.clone().map(|_schema| {
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
let mut editor = Editor::auto_height_unbounded(1, window, cx);
|
let mut editor = Editor::auto_height_unbounded(1, window, cx);
|
||||||
|
let workspace = workspace.clone();
|
||||||
|
|
||||||
if let Some(input) = editing_keybind.action_input.clone() {
|
if let Some(input) = editing_keybind.action_input.clone() {
|
||||||
editor.set_text(input.text, window, cx);
|
editor.set_text(input.text, window, cx);
|
||||||
} else {
|
} else {
|
||||||
// TODO: default value from schema?
|
// TODO: default value from schema?
|
||||||
editor.set_placeholder_text("Action input", cx);
|
editor.set_placeholder_text("Action Input", cx);
|
||||||
}
|
}
|
||||||
cx.spawn(async |editor, cx| {
|
cx.spawn(async |editor, cx| {
|
||||||
let json_language = load_json_language(workspace, cx).await;
|
let json_language = load_json_language(workspace, cx).await;
|
||||||
|
@ -1383,6 +1400,7 @@ impl KeybindingEditorModal {
|
||||||
input_editor,
|
input_editor,
|
||||||
error: None,
|
error: None,
|
||||||
keymap_editor,
|
keymap_editor,
|
||||||
|
workspace,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1431,7 +1449,7 @@ impl KeybindingEditorModal {
|
||||||
let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
|
let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
|
||||||
let new_context = self
|
let new_context = self
|
||||||
.context_editor
|
.context_editor
|
||||||
.read_with(cx, |editor, cx| editor.text(cx));
|
.read_with(cx, |input, cx| input.editor().read(cx).text(cx));
|
||||||
let new_context = new_context.is_empty().not().then_some(new_context);
|
let new_context = new_context.is_empty().not().then_some(new_context);
|
||||||
let new_context_err = new_context.as_deref().and_then(|context| {
|
let new_context_err = new_context.as_deref().and_then(|context| {
|
||||||
gpui::KeyBindingContextPredicate::parse(context)
|
gpui::KeyBindingContextPredicate::parse(context)
|
||||||
|
@ -1503,6 +1521,25 @@ impl KeybindingEditorModal {
|
||||||
|
|
||||||
let create = self.creating;
|
let create = self.creating;
|
||||||
|
|
||||||
|
let status_toast = StatusToast::new(
|
||||||
|
format!(
|
||||||
|
"Saved edits to the {} action.",
|
||||||
|
command_palette::humanize_action_name(&self.editing_keybind.action_name)
|
||||||
|
),
|
||||||
|
cx,
|
||||||
|
move |this, _cx| {
|
||||||
|
this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
|
||||||
|
.dismiss_button(true)
|
||||||
|
// .action("Undo", f) todo: wire the undo functionality
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
self.workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
workspace.toggle_status_toast(status_toast, cx);
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
if let Err(err) = save_keybinding_update(
|
if let Err(err) = save_keybinding_update(
|
||||||
create,
|
create,
|
||||||
|
@ -1533,90 +1570,96 @@ impl KeybindingEditorModal {
|
||||||
impl Render for KeybindingEditorModal {
|
impl Render for KeybindingEditorModal {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let theme = cx.theme().colors();
|
let theme = cx.theme().colors();
|
||||||
let input_base = || {
|
let action_name =
|
||||||
div()
|
command_palette::humanize_action_name(&self.editing_keybind.action_name).to_string();
|
||||||
.w_full()
|
|
||||||
.py_2()
|
|
||||||
.px_3()
|
|
||||||
.min_h_8()
|
|
||||||
.rounded_md()
|
|
||||||
.bg(theme.editor_background)
|
|
||||||
.border_1()
|
|
||||||
.border_color(theme.border_variant)
|
|
||||||
};
|
|
||||||
|
|
||||||
v_flex()
|
v_flex().w(rems(34.)).elevation_3(cx).child(
|
||||||
.w(rems(34.))
|
Modal::new("keybinding_editor_modal", None)
|
||||||
.elevation_3(cx)
|
.header(
|
||||||
.child(
|
ModalHeader::new().child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.p_3()
|
.pb_1p5()
|
||||||
.child(Label::new("Edit Keystroke"))
|
.mb_1()
|
||||||
.child(
|
.gap_0p5()
|
||||||
Label::new("Input the desired keystroke for the selected action.")
|
.border_b_1()
|
||||||
.color(Color::Muted)
|
.border_color(theme.border_variant)
|
||||||
.mb_2(),
|
.child(Label::new(action_name))
|
||||||
)
|
.when_some(self.editing_keybind.action_docs, |this, docs| {
|
||||||
.child(self.keybind_editor.clone()),
|
this.child(
|
||||||
)
|
Label::new(docs).size(LabelSize::Small).color(Color::Muted),
|
||||||
.when_some(self.input_editor.clone(), |this, editor| {
|
)
|
||||||
this.child(
|
}),
|
||||||
v_flex()
|
),
|
||||||
.p_3()
|
|
||||||
.pt_0()
|
|
||||||
.child(Label::new("Edit Input"))
|
|
||||||
.child(
|
|
||||||
Label::new("Input the desired input to the binding.")
|
|
||||||
.color(Color::Muted)
|
|
||||||
.mb_2(),
|
|
||||||
)
|
|
||||||
.child(input_base().child(editor)),
|
|
||||||
)
|
)
|
||||||
})
|
.section(
|
||||||
.child(
|
Section::new().child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.p_3()
|
.gap_2()
|
||||||
.pt_0()
|
.child(
|
||||||
.child(Label::new("Edit Context"))
|
v_flex()
|
||||||
.child(
|
.child(Label::new("Edit Keystroke"))
|
||||||
Label::new("Input the desired context for the binding.")
|
.gap_1()
|
||||||
.color(Color::Muted)
|
.child(self.keybind_editor.clone()),
|
||||||
.mb_2(),
|
)
|
||||||
)
|
.when_some(self.input_editor.clone(), |this, editor| {
|
||||||
.child(input_base().child(self.context_editor.clone())),
|
this.child(
|
||||||
)
|
v_flex()
|
||||||
.when_some(self.error.as_ref(), |this, error| {
|
.mt_1p5()
|
||||||
this.child(
|
.gap_1()
|
||||||
div().p_2().child(
|
.child(Label::new("Edit Arguments"))
|
||||||
Banner::new()
|
.child(
|
||||||
.map(|banner| match error {
|
div()
|
||||||
InputError::Error(_) => banner.severity(ui::Severity::Error),
|
.w_full()
|
||||||
InputError::Warning(_) => banner.severity(ui::Severity::Warning),
|
.py_1()
|
||||||
|
.px_1p5()
|
||||||
|
.rounded_lg()
|
||||||
|
.bg(theme.editor_background)
|
||||||
|
.border_1()
|
||||||
|
.border_color(theme.border_variant)
|
||||||
|
.child(editor),
|
||||||
|
),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
// For some reason, the div overflows its container to the
|
.child(self.context_editor.clone())
|
||||||
// right. The padding accounts for that.
|
.when_some(self.error.as_ref(), |this, error| {
|
||||||
.child(div().size_full().pr_2().child(Label::new(error.content()))),
|
this.child(
|
||||||
|
Banner::new()
|
||||||
|
.map(|banner| match error {
|
||||||
|
InputError::Error(_) => {
|
||||||
|
banner.severity(ui::Severity::Error)
|
||||||
|
}
|
||||||
|
InputError::Warning(_) => {
|
||||||
|
banner.severity(ui::Severity::Warning)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// For some reason, the div overflows its container to the
|
||||||
|
//right. The padding accounts for that.
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.pr_2()
|
||||||
|
.child(Label::new(error.content())),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
.footer(
|
||||||
.child(
|
ModalFooter::new().end_slot(
|
||||||
h_flex()
|
h_flex()
|
||||||
.p_2()
|
.gap_1()
|
||||||
.w_full()
|
.child(
|
||||||
.gap_1()
|
Button::new("cancel", "Cancel")
|
||||||
.justify_end()
|
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
|
||||||
.border_t_1()
|
)
|
||||||
.border_color(theme.border_variant)
|
.child(Button::new("save-btn", "Save").on_click(cx.listener(
|
||||||
.child(
|
|this, _event, _window, cx| {
|
||||||
Button::new("cancel", "Cancel")
|
this.save(cx);
|
||||||
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
|
},
|
||||||
)
|
))),
|
||||||
.child(
|
|
||||||
Button::new("save-btn", "Save").on_click(
|
|
||||||
cx.listener(|this, _event, _window, cx| Self::save(this, cx)),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1848,6 +1891,7 @@ struct KeystrokeInput {
|
||||||
inner_focus_handle: FocusHandle,
|
inner_focus_handle: FocusHandle,
|
||||||
intercept_subscription: Option<Subscription>,
|
intercept_subscription: Option<Subscription>,
|
||||||
_focus_subscriptions: [Subscription; 2],
|
_focus_subscriptions: [Subscription; 2],
|
||||||
|
search: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeystrokeInput {
|
impl KeystrokeInput {
|
||||||
|
@ -1870,6 +1914,7 @@ impl KeystrokeInput {
|
||||||
outer_focus_handle,
|
outer_focus_handle,
|
||||||
intercept_subscription: None,
|
intercept_subscription: None,
|
||||||
_focus_subscriptions,
|
_focus_subscriptions,
|
||||||
|
search: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1987,6 +2032,14 @@ impl KeystrokeInput {
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn recording_focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||||
|
self.inner_focus_handle.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_search_mode(&mut self, search: bool) {
|
||||||
|
self.search = search;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<()> for KeystrokeInput {}
|
impl EventEmitter<()> for KeystrokeInput {}
|
||||||
|
@ -2000,7 +2053,84 @@ impl Focusable for KeystrokeInput {
|
||||||
impl Render for KeystrokeInput {
|
impl Render for KeystrokeInput {
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let colors = cx.theme().colors();
|
let colors = cx.theme().colors();
|
||||||
let is_inner_focused = self.inner_focus_handle.is_focused(window);
|
let is_focused = self.outer_focus_handle.contains_focused(window, cx);
|
||||||
|
let is_recording = self.inner_focus_handle.is_focused(window);
|
||||||
|
|
||||||
|
let horizontal_padding = rems_from_px(64.);
|
||||||
|
|
||||||
|
let recording_bg_color = colors
|
||||||
|
.editor_background
|
||||||
|
.blend(colors.text_accent.opacity(0.1));
|
||||||
|
|
||||||
|
let recording_indicator = h_flex()
|
||||||
|
.h_4()
|
||||||
|
.pr_1()
|
||||||
|
.gap_0p5()
|
||||||
|
.border_1()
|
||||||
|
.border_color(colors.border)
|
||||||
|
.bg(colors
|
||||||
|
.editor_background
|
||||||
|
.blend(colors.text_accent.opacity(0.1)))
|
||||||
|
.rounded_sm()
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::Circle)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Error)
|
||||||
|
.with_animation(
|
||||||
|
"recording-pulse",
|
||||||
|
Animation::new(std::time::Duration::from_secs(2))
|
||||||
|
.repeat()
|
||||||
|
.with_easing(gpui::pulsating_between(0.4, 0.8)),
|
||||||
|
{
|
||||||
|
let color = Color::Error.color(cx);
|
||||||
|
move |this, delta| this.color(Color::Custom(color.opacity(delta)))
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Label::new("REC")
|
||||||
|
.size(LabelSize::XSmall)
|
||||||
|
.weight(FontWeight::SEMIBOLD)
|
||||||
|
.color(Color::Error),
|
||||||
|
);
|
||||||
|
|
||||||
|
let search_indicator = h_flex()
|
||||||
|
.h_4()
|
||||||
|
.pr_1()
|
||||||
|
.gap_0p5()
|
||||||
|
.border_1()
|
||||||
|
.border_color(colors.border)
|
||||||
|
.bg(colors
|
||||||
|
.editor_background
|
||||||
|
.blend(colors.text_accent.opacity(0.1)))
|
||||||
|
.rounded_sm()
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::Circle)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Accent)
|
||||||
|
.with_animation(
|
||||||
|
"recording-pulse",
|
||||||
|
Animation::new(std::time::Duration::from_secs(2))
|
||||||
|
.repeat()
|
||||||
|
.with_easing(gpui::pulsating_between(0.4, 0.8)),
|
||||||
|
{
|
||||||
|
let color = Color::Accent.color(cx);
|
||||||
|
move |this, delta| this.color(Color::Custom(color.opacity(delta)))
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Label::new("SEARCH")
|
||||||
|
.size(LabelSize::XSmall)
|
||||||
|
.weight(FontWeight::SEMIBOLD)
|
||||||
|
.color(Color::Accent),
|
||||||
|
);
|
||||||
|
|
||||||
|
let record_icon = if self.search {
|
||||||
|
IconName::MagnifyingGlass
|
||||||
|
} else {
|
||||||
|
IconName::PlayFilled
|
||||||
|
};
|
||||||
|
|
||||||
return h_flex()
|
return h_flex()
|
||||||
.id("keystroke-input")
|
.id("keystroke-input")
|
||||||
|
@ -2008,18 +2138,23 @@ impl Render for KeystrokeInput {
|
||||||
.py_2()
|
.py_2()
|
||||||
.px_3()
|
.px_3()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.min_h_8()
|
.min_h_10()
|
||||||
.w_full()
|
.w_full()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.rounded_md()
|
.rounded_lg()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.bg(colors.editor_background)
|
.map(|this| {
|
||||||
.border_2()
|
if is_recording {
|
||||||
|
this.bg(recording_bg_color)
|
||||||
|
} else {
|
||||||
|
this.bg(colors.editor_background)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.border_1()
|
||||||
.border_color(colors.border_variant)
|
.border_color(colors.border_variant)
|
||||||
.focus(|mut s| {
|
.when(is_focused, |parent| {
|
||||||
s.border_color = Some(colors.border_focused);
|
parent.border_color(colors.border_focused)
|
||||||
s
|
|
||||||
})
|
})
|
||||||
.on_key_down(cx.listener(|this, event: &KeyDownEvent, window, cx| {
|
.on_key_down(cx.listener(|this, event: &KeyDownEvent, window, cx| {
|
||||||
// TODO: replace with action
|
// TODO: replace with action
|
||||||
|
@ -2028,19 +2163,29 @@ impl Render for KeystrokeInput {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.w(horizontal_padding)
|
||||||
|
.gap_0p5()
|
||||||
|
.justify_start()
|
||||||
|
.flex_none()
|
||||||
|
.when(is_recording, |this| {
|
||||||
|
this.map(|this| {
|
||||||
|
if self.search {
|
||||||
|
this.child(search_indicator)
|
||||||
|
} else {
|
||||||
|
this.child(recording_indicator)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.id("keystroke-input-inner")
|
.id("keystroke-input-inner")
|
||||||
.track_focus(&self.inner_focus_handle)
|
.track_focus(&self.inner_focus_handle)
|
||||||
.on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
|
.on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
|
||||||
.on_key_up(cx.listener(Self::on_key_up))
|
.on_key_up(cx.listener(Self::on_key_up))
|
||||||
.when(self.highlight_on_focus, |this| {
|
.size_full()
|
||||||
this.focus(|mut style| {
|
|
||||||
style.border_color = Some(colors.border_focused);
|
|
||||||
style
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.w_full()
|
|
||||||
.min_w_0()
|
.min_w_0()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.flex_wrap()
|
.flex_wrap()
|
||||||
|
@ -2049,40 +2194,52 @@ impl Render for KeystrokeInput {
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
.w(horizontal_padding)
|
||||||
.gap_0p5()
|
.gap_0p5()
|
||||||
|
.justify_end()
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.when(is_inner_focused, |this| {
|
.map(|this| {
|
||||||
this.child(
|
if is_recording {
|
||||||
Icon::new(IconName::Circle)
|
this.child(
|
||||||
.color(Color::Error)
|
IconButton::new("stop-record-btn", IconName::StopFilled)
|
||||||
.with_animation(
|
.shape(ui::IconButtonShape::Square)
|
||||||
"recording-pulse",
|
.map(|this| {
|
||||||
gpui::Animation::new(std::time::Duration::from_secs(1))
|
if self.search {
|
||||||
.repeat()
|
this.tooltip(Tooltip::text("Stop Searching"))
|
||||||
.with_easing(gpui::pulsating_between(0.8, 1.0)),
|
} else {
|
||||||
{
|
this.tooltip(Tooltip::text("Stop Recording"))
|
||||||
let color = Color::Error.color(cx);
|
|
||||||
move |this, delta| {
|
|
||||||
this.color(Color::Custom(color.opacity(delta)))
|
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
),
|
.icon_color(Color::Error)
|
||||||
)
|
.on_click(cx.listener(|this, _event, window, _cx| {
|
||||||
|
this.outer_focus_handle.focus(window);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.child(
|
||||||
|
IconButton::new("record-btn", record_icon)
|
||||||
|
.shape(ui::IconButtonShape::Square)
|
||||||
|
.map(|this| {
|
||||||
|
if self.search {
|
||||||
|
this.tooltip(Tooltip::text("Start Searching"))
|
||||||
|
} else {
|
||||||
|
this.tooltip(Tooltip::text("Start Recording"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.when(!is_focused, |this| this.icon_color(Color::Muted))
|
||||||
|
.on_click(cx.listener(|this, _event, window, _cx| {
|
||||||
|
this.inner_focus_handle.focus(window);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
IconButton::new("backspace-btn", IconName::Delete)
|
IconButton::new("clear-btn", IconName::Delete)
|
||||||
.tooltip(Tooltip::text("Delete Keystroke"))
|
.shape(ui::IconButtonShape::Square)
|
||||||
.when(!is_inner_focused, |this| this.icon_color(Color::Muted))
|
|
||||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
|
||||||
this.keystrokes.pop();
|
|
||||||
cx.emit(());
|
|
||||||
cx.notify();
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
IconButton::new("clear-btn", IconName::Eraser)
|
|
||||||
.tooltip(Tooltip::text("Clear Keystrokes"))
|
.tooltip(Tooltip::text("Clear Keystrokes"))
|
||||||
.when(!is_inner_focused, |this| this.icon_color(Color::Muted))
|
.when(!is_recording || !is_focused, |this| {
|
||||||
|
this.icon_color(Color::Muted)
|
||||||
|
})
|
||||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||||
this.keystrokes.clear();
|
this.keystrokes.clear();
|
||||||
cx.emit(());
|
cx.emit(());
|
||||||
|
|
|
@ -27,6 +27,8 @@ pub struct SingleLineInput {
|
||||||
///
|
///
|
||||||
/// Its position is determined by the [`FieldLabelLayout`].
|
/// Its position is determined by the [`FieldLabelLayout`].
|
||||||
label: Option<SharedString>,
|
label: Option<SharedString>,
|
||||||
|
/// The size of the label text.
|
||||||
|
label_size: LabelSize,
|
||||||
/// The placeholder text for the text field.
|
/// The placeholder text for the text field.
|
||||||
placeholder: SharedString,
|
placeholder: SharedString,
|
||||||
/// Exposes the underlying [`Entity<Editor>`] to allow for customizing the editor beyond the provided API.
|
/// Exposes the underlying [`Entity<Editor>`] to allow for customizing the editor beyond the provided API.
|
||||||
|
@ -59,6 +61,7 @@ impl SingleLineInput {
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
label: None,
|
label: None,
|
||||||
|
label_size: LabelSize::Small,
|
||||||
placeholder: placeholder_text,
|
placeholder: placeholder_text,
|
||||||
editor,
|
editor,
|
||||||
start_icon: None,
|
start_icon: None,
|
||||||
|
@ -76,6 +79,11 @@ impl SingleLineInput {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn label_size(mut self, size: LabelSize) -> Self {
|
||||||
|
self.label_size = size;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
|
pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
|
||||||
self.disabled = disabled;
|
self.disabled = disabled;
|
||||||
self.editor
|
self.editor
|
||||||
|
@ -138,7 +146,7 @@ impl Render for SingleLineInput {
|
||||||
.when_some(self.label.clone(), |this, label| {
|
.when_some(self.label.clone(), |this, label| {
|
||||||
this.child(
|
this.child(
|
||||||
Label::new(label)
|
Label::new(label)
|
||||||
.size(LabelSize::Small)
|
.size(self.label_size)
|
||||||
.color(if self.disabled {
|
.color(if self.disabled {
|
||||||
Color::Disabled
|
Color::Disabled
|
||||||
} else {
|
} else {
|
||||||
|
@ -148,16 +156,17 @@ impl Render for SingleLineInput {
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
.min_w_48()
|
||||||
|
.min_h_8()
|
||||||
|
.w_full()
|
||||||
.px_2()
|
.px_2()
|
||||||
.py_1p5()
|
.py_1p5()
|
||||||
.bg(style.background_color)
|
.flex_grow()
|
||||||
.text_color(style.text_color)
|
.text_color(style.text_color)
|
||||||
.rounded_md()
|
.rounded_lg()
|
||||||
|
.bg(style.background_color)
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(style.border_color)
|
.border_color(style.border_color)
|
||||||
.min_w_48()
|
|
||||||
.w_full()
|
|
||||||
.flex_grow()
|
|
||||||
.when_some(self.start_icon, |this, icon| {
|
.when_some(self.start_icon, |this, icon| {
|
||||||
this.gap_1()
|
this.gap_1()
|
||||||
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
|
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
|
||||||
|
@ -173,16 +182,28 @@ impl Component for SingleLineInput {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||||
let input_1 =
|
let input_small =
|
||||||
cx.new(|cx| SingleLineInput::new(window, cx, "placeholder").label("Some Label"));
|
cx.new(|cx| SingleLineInput::new(window, cx, "placeholder").label("Small Label"));
|
||||||
|
|
||||||
|
let input_regular = cx.new(|cx| {
|
||||||
|
SingleLineInput::new(window, cx, "placeholder")
|
||||||
|
.label("Regular Label")
|
||||||
|
.label_size(LabelSize::Default)
|
||||||
|
});
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_6()
|
.gap_6()
|
||||||
.children(vec![example_group(vec![single_example(
|
.children(vec![example_group(vec![
|
||||||
"Default",
|
single_example(
|
||||||
div().child(input_1.clone()).into_any_element(),
|
"Small Label (Default)",
|
||||||
)])])
|
div().child(input_small.clone()).into_any_element(),
|
||||||
|
),
|
||||||
|
single_example(
|
||||||
|
"Regular Label",
|
||||||
|
div().child(input_regular.clone()).into_any_element(),
|
||||||
|
),
|
||||||
|
])])
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue