Detailed changes
@@ -8,8 +8,8 @@ use fs::Fs;
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, transparent_black,
+ Global, KeyContext, Keystroke, ModifiersChangedEvent, ScrollStrategy, StyledText, Subscription,
+ WeakEntity, actions, div, transparent_black,
};
use language::{Language, LanguageConfig};
use settings::KeybindSource;
@@ -18,7 +18,7 @@ use util::ResultExt;
use ui::{
ActiveTheme as _, App, BorrowAppContext, ContextMenu, ParentElement as _, Render, SharedString,
- Styled as _, Window, prelude::*, right_click_menu,
+ Styled as _, Tooltip, Window, prelude::*, right_click_menu,
};
use workspace::{Item, ModalView, SerializableItem, Workspace, register_serializable_item};
@@ -145,7 +145,7 @@ impl KeymapEditor {
let filter_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
- editor.set_placeholder_text("Filter action names...", cx);
+ editor.set_placeholder_text("Filter action namesβ¦", cx);
editor
});
@@ -248,7 +248,7 @@ impl KeymapEditor {
let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx);
let ui_key_binding = Some(
- ui::KeyBinding::new(key_binding.clone(), cx)
+ ui::KeyBinding::new_from_gpui(key_binding.clone(), cx)
.vim_mode(source == Some(settings::KeybindSource::Vim)),
);
@@ -571,7 +571,7 @@ impl Render for KeymapEditor {
let row_count = self.matches.len();
let theme = cx.theme();
- div()
+ v_flex()
.key_context(self.dispatch_context(window, cx))
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_previous))
@@ -586,9 +586,9 @@ impl Render for KeymapEditor {
.bg(theme.colors().editor_background)
.id("keymap-editor")
.track_focus(&self.focus_handle)
+ .pt_4()
.px_4()
- .v_flex()
- .pb_4()
+ .gap_4()
.child(
h_flex()
.key_context({
@@ -596,12 +596,13 @@ impl Render for KeymapEditor {
context.add("BufferSearchBar");
context
})
- .w_full()
- .h_12()
- .px_4()
- .my_4()
- .border_2()
+ .h_8()
+ .pl_2()
+ .pr_1()
+ .py_1()
+ .border_1()
.border_color(theme.colors().border)
+ .rounded_lg()
.child(self.filter_editor.clone()),
)
.child(
@@ -742,7 +743,7 @@ impl RenderOnce for SyntaxHighlightedText {
struct KeybindingEditorModal {
editing_keybind: ProcessedKeybinding,
- keybind_editor: Entity<KeybindInput>,
+ keybind_editor: Entity<KeystrokeInput>,
fs: Arc<dyn Fs>,
error: Option<String>,
}
@@ -764,7 +765,7 @@ impl KeybindingEditorModal {
_window: &mut Window,
cx: &mut App,
) -> Self {
- let keybind_editor = cx.new(KeybindInput::new);
+ let keybind_editor = cx.new(KeystrokeInput::new);
Self {
editing_keybind,
fs,
@@ -777,84 +778,65 @@ impl KeybindingEditorModal {
impl Render for KeybindingEditorModal {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme().colors();
+
return v_flex()
- .gap_4()
.w(rems(36.))
+ .elevation_3(cx)
.child(
v_flex()
- .items_center()
- .text_center()
- .bg(theme.background)
- .border_color(theme.border)
- .border_2()
+ .pt_2()
.px_4()
- .py_2()
+ .pb_4()
+ .gap_2()
+ .child(Label::new("Input desired keystroke, then hit save"))
+ .child(self.keybind_editor.clone()),
+ )
+ .child(
+ h_flex()
+ .p_2()
.w_full()
+ .gap_1()
+ .justify_end()
+ .border_t_1()
+ .border_color(cx.theme().colors().border_variant)
.child(
- div()
- .text_lg()
- .font_weight(FontWeight::BOLD)
- .child("Input desired keybinding, then hit save"),
+ Button::new("cancel", "Cancel")
+ .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
)
.child(
- h_flex()
- .w_full()
- .child(self.keybind_editor.clone())
- .child(
- IconButton::new("backspace-btn", ui::IconName::Backspace).on_click(
- cx.listener(|this, _event, _window, cx| {
- this.keybind_editor.update(cx, |editor, cx| {
- editor.keystrokes.pop();
+ Button::new("save-btn", "Save Keybinding").on_click(cx.listener(
+ |this, _event, _window, cx| {
+ let existing_keybind = this.editing_keybind.clone();
+ let fs = this.fs.clone();
+ let new_keystrokes = this
+ .keybind_editor
+ .read_with(cx, |editor, _| editor.keystrokes.clone());
+ if new_keystrokes.is_empty() {
+ this.error = Some("Keystrokes cannot be empty".to_string());
+ cx.notify();
+ return;
+ }
+ let tab_size =
+ cx.global::<settings::SettingsStore>().json_tab_size();
+ cx.spawn(async move |this, cx| {
+ if let Err(err) = save_keybinding_update(
+ existing_keybind,
+ &new_keystrokes,
+ &fs,
+ tab_size,
+ )
+ .await
+ {
+ this.update(cx, |this, cx| {
+ this.error = Some(err.to_string());
cx.notify();
})
- }),
- ),
- )
- .child(IconButton::new("clear-btn", ui::IconName::Eraser).on_click(
- cx.listener(|this, _event, _window, cx| {
- this.keybind_editor.update(cx, |editor, cx| {
- editor.keystrokes.clear();
- cx.notify();
- })
- }),
- )),
- )
- .child(
- h_flex().w_full().items_center().justify_center().child(
- Button::new("save-btn", "Save")
- .label_size(LabelSize::Large)
- .on_click(cx.listener(|this, _event, _window, cx| {
- let existing_keybind = this.editing_keybind.clone();
- let fs = this.fs.clone();
- let new_keystrokes = this
- .keybind_editor
- .read_with(cx, |editor, _| editor.keystrokes.clone());
- if new_keystrokes.is_empty() {
- this.error = Some("Keystrokes cannot be empty".to_string());
- cx.notify();
- return;
+ .log_err();
}
- let tab_size =
- cx.global::<settings::SettingsStore>().json_tab_size();
- cx.spawn(async move |this, cx| {
- if let Err(err) = save_keybinding_update(
- existing_keybind,
- &new_keystrokes,
- &fs,
- tab_size,
- )
- .await
- {
- this.update(cx, |this, cx| {
- this.error = Some(err.to_string());
- cx.notify();
- })
- .log_err();
- }
- })
- .detach();
- })),
- ),
+ })
+ .detach();
+ },
+ )),
),
)
.when_some(self.error.clone(), |this, error| {
@@ -879,11 +861,13 @@ async fn save_keybinding_update(
let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
.await
.context("Failed to load keymap file")?;
+
let existing_keystrokes = existing
.ui_key_binding
.as_ref()
- .map(|keybinding| keybinding.key_binding.keystrokes())
+ .map(|keybinding| keybinding.keystrokes.as_slice())
.unwrap_or_default();
+
let context = existing
.context
.as_ref()
@@ -927,12 +911,12 @@ async fn save_keybinding_update(
Ok(())
}
-struct KeybindInput {
+struct KeystrokeInput {
keystrokes: Vec<Keystroke>,
focus_handle: FocusHandle,
}
-impl KeybindInput {
+impl KeystrokeInput {
fn new(cx: &mut Context<Self>) -> Self {
let focus_handle = cx.focus_handle();
Self {
@@ -1007,16 +991,18 @@ impl KeybindInput {
}
}
-impl Focusable for KeybindInput {
+impl Focusable for KeystrokeInput {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
-impl Render for KeybindInput {
+impl Render for KeystrokeInput {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.theme().colors();
- return div()
+
+ return h_flex()
+ .id("keybinding_input")
.track_focus(&self.focus_handle)
.on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
.on_key_down(cx.listener(Self::on_key_down))
@@ -1025,16 +1011,55 @@ impl Render for KeybindInput {
style.border_color = Some(colors.border_focused);
style
})
- .h_12()
+ .py_2()
+ .px_3()
+ .gap_2()
+ .min_h_8()
.w_full()
+ .justify_between()
.bg(colors.editor_background)
- .border_2()
- .border_color(colors.border)
- .p_4()
- .flex_row()
- .text_center()
- .justify_center()
- .child(ui::text_for_keystrokes(&self.keystrokes, cx));
+ .border_1()
+ .rounded_md()
+ .flex_1()
+ .overflow_hidden()
+ .child(
+ h_flex()
+ .w_full()
+ .min_w_0()
+ .justify_center()
+ .flex_wrap()
+ .gap(ui::DynamicSpacing::Base04.rems(cx))
+ .children(self.keystrokes.iter().map(|keystroke| {
+ h_flex().children(ui::render_keystroke(
+ keystroke,
+ None,
+ Some(rems(0.875).into()),
+ ui::PlatformStyle::platform(),
+ false,
+ ))
+ })),
+ )
+ .child(
+ h_flex()
+ .gap_0p5()
+ .flex_none()
+ .child(
+ IconButton::new("backspace-btn", IconName::Delete)
+ .tooltip(Tooltip::text("Delete Keystroke"))
+ .on_click(cx.listener(|this, _event, _window, cx| {
+ this.keystrokes.pop();
+ cx.notify();
+ })),
+ )
+ .child(
+ IconButton::new("clear-btn", IconName::Eraser)
+ .tooltip(Tooltip::text("Clear Keystrokes"))
+ .on_click(cx.listener(|this, _event, _window, cx| {
+ this.keystrokes.clear();
+ cx.notify();
+ })),
+ ),
+ );
}
}
@@ -13,7 +13,7 @@ pub struct KeyBinding {
/// More than one keystroke produces a chord.
///
/// This should always contain at least one keystroke.
- pub key_binding: gpui::KeyBinding,
+ pub keystrokes: Vec<Keystroke>,
/// The [`PlatformStyle`] to use when displaying this keybinding.
platform_style: PlatformStyle,
@@ -37,7 +37,7 @@ impl KeyBinding {
return Self::for_action_in(action, &focused, window, cx);
}
let key_binding = window.highest_precedence_binding_for_action(action)?;
- Some(Self::new(key_binding, cx))
+ Some(Self::new_from_gpui(key_binding, cx))
}
/// Like `for_action`, but lets you specify the context from which keybindings are matched.
@@ -48,7 +48,7 @@ impl KeyBinding {
cx: &App,
) -> Option<Self> {
let key_binding = window.highest_precedence_binding_for_action_in(action, focus)?;
- Some(Self::new(key_binding, cx))
+ Some(Self::new_from_gpui(key_binding, cx))
}
pub fn set_vim_mode(cx: &mut App, enabled: bool) {
@@ -59,9 +59,9 @@ impl KeyBinding {
cx.try_global::<VimStyle>().is_some_and(|g| g.0)
}
- pub fn new(key_binding: gpui::KeyBinding, cx: &App) -> Self {
+ pub fn new(keystrokes: Vec<Keystroke>, cx: &App) -> Self {
Self {
- key_binding,
+ keystrokes,
platform_style: PlatformStyle::platform(),
size: None,
vim_mode: KeyBinding::is_vim_mode(cx),
@@ -69,6 +69,10 @@ impl KeyBinding {
}
}
+ pub fn new_from_gpui(key_binding: gpui::KeyBinding, cx: &App) -> Self {
+ Self::new(key_binding.keystrokes().to_vec(), cx)
+ }
+
/// Sets the [`PlatformStyle`] for this [`KeyBinding`].
pub fn platform_style(mut self, platform_style: PlatformStyle) -> Self {
self.platform_style = platform_style;
@@ -92,15 +96,20 @@ impl KeyBinding {
self.vim_mode = enabled;
self
}
+}
- fn render_key(&self, keystroke: &Keystroke, color: Option<Color>) -> AnyElement {
- let key_icon = icon_for_key(keystroke, self.platform_style);
- match key_icon {
- Some(icon) => KeyIcon::new(icon, color).size(self.size).into_any_element(),
- None => {
- let key = util::capitalize(&keystroke.key);
- Key::new(&key, color).size(self.size).into_any_element()
- }
+fn render_key(
+ keystroke: &Keystroke,
+ color: Option<Color>,
+ platform_style: PlatformStyle,
+ size: impl Into<Option<AbsoluteLength>>,
+) -> AnyElement {
+ let key_icon = icon_for_key(keystroke, platform_style);
+ match key_icon {
+ Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(),
+ None => {
+ let key = util::capitalize(&keystroke.key);
+ Key::new(&key, color).size(size).into_any_element()
}
}
}
@@ -108,17 +117,12 @@ impl KeyBinding {
impl RenderOnce for KeyBinding {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let color = self.disabled.then_some(Color::Disabled);
- let use_text = self.vim_mode
- || matches!(
- self.platform_style,
- PlatformStyle::Linux | PlatformStyle::Windows
- );
+
h_flex()
.debug_selector(|| {
format!(
"KEY_BINDING-{}",
- self.key_binding
- .keystrokes()
+ self.keystrokes
.iter()
.map(|k| k.key.to_string())
.collect::<Vec<_>>()
@@ -127,35 +131,56 @@ impl RenderOnce for KeyBinding {
})
.gap(DynamicSpacing::Base04.rems(cx))
.flex_none()
- .children(self.key_binding.keystrokes().iter().map(|keystroke| {
+ .children(self.keystrokes.iter().map(|keystroke| {
h_flex()
.flex_none()
.py_0p5()
.rounded_xs()
.text_color(cx.theme().colors().text_muted)
- .when(use_text, |el| {
- el.child(
- Key::new(
- keystroke_text(&keystroke, self.platform_style, self.vim_mode),
- color,
- )
- .size(self.size),
- )
- })
- .when(!use_text, |el| {
- el.children(render_modifiers(
- &keystroke.modifiers,
- self.platform_style,
- color,
- self.size,
- true,
- ))
- .map(|el| el.child(self.render_key(&keystroke, color)))
- })
+ .children(render_keystroke(
+ keystroke,
+ color,
+ self.size,
+ self.platform_style,
+ self.vim_mode,
+ ))
}))
}
}
+pub fn render_keystroke(
+ keystroke: &Keystroke,
+ color: Option<Color>,
+ size: impl Into<Option<AbsoluteLength>>,
+ platform_style: PlatformStyle,
+ vim_mode: bool,
+) -> Vec<AnyElement> {
+ let use_text = vim_mode
+ || matches!(
+ platform_style,
+ PlatformStyle::Linux | PlatformStyle::Windows
+ );
+ let size = size.into();
+
+ if use_text {
+ let element = Key::new(keystroke_text(&keystroke, platform_style, vim_mode), color)
+ .size(size)
+ .into_any_element();
+ vec![element]
+ } else {
+ let mut elements = Vec::new();
+ elements.extend(render_modifiers(
+ &keystroke.modifiers,
+ platform_style,
+ color,
+ size,
+ true,
+ ));
+ elements.push(render_key(&keystroke, color, platform_style, size));
+ elements
+ }
+}
+
fn icon_for_key(keystroke: &Keystroke, platform_style: PlatformStyle) -> Option<IconName> {
match keystroke.key.as_str() {
"left" => Some(IconName::ArrowLeft),
@@ -466,7 +491,7 @@ impl Component for KeyBinding {
vec![
single_example(
"Default",
- KeyBinding::new(
+ KeyBinding::new_from_gpui(
gpui::KeyBinding::new("ctrl-s", gpui::NoAction, None),
cx,
)
@@ -474,7 +499,7 @@ impl Component for KeyBinding {
),
single_example(
"Mac Style",
- KeyBinding::new(
+ KeyBinding::new_from_gpui(
gpui::KeyBinding::new("cmd-s", gpui::NoAction, None),
cx,
)
@@ -483,7 +508,7 @@ impl Component for KeyBinding {
),
single_example(
"Windows Style",
- KeyBinding::new(
+ KeyBinding::new_from_gpui(
gpui::KeyBinding::new("ctrl-s", gpui::NoAction, None),
cx,
)
@@ -496,9 +521,12 @@ impl Component for KeyBinding {
"Vim Mode",
vec![single_example(
"Vim Mode Enabled",
- KeyBinding::new(gpui::KeyBinding::new("dd", gpui::NoAction, None), cx)
- .vim_mode(true)
- .into_any_element(),
+ KeyBinding::new_from_gpui(
+ gpui::KeyBinding::new("dd", gpui::NoAction, None),
+ cx,
+ )
+ .vim_mode(true)
+ .into_any_element(),
)],
),
example_group_with_title(
@@ -506,7 +534,7 @@ impl Component for KeyBinding {
vec![
single_example(
"Multiple Keys",
- KeyBinding::new(
+ KeyBinding::new_from_gpui(
gpui::KeyBinding::new("ctrl-k ctrl-b", gpui::NoAction, None),
cx,
)
@@ -514,7 +542,7 @@ impl Component for KeyBinding {
),
single_example(
"With Shift",
- KeyBinding::new(
+ KeyBinding::new_from_gpui(
gpui::KeyBinding::new("shift-cmd-p", gpui::NoAction, None),
cx,
)
@@ -216,7 +216,7 @@ impl Component for KeybindingHint {
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let enter_fallback = gpui::KeyBinding::new("enter", menu::Confirm, None);
let enter = KeyBinding::for_action(&menu::Confirm, window, cx)
- .unwrap_or(KeyBinding::new(enter_fallback, cx));
+ .unwrap_or(KeyBinding::new_from_gpui(enter_fallback, cx));
let bg_color = cx.theme().colors().surface_background;
@@ -18,16 +18,16 @@ impl Render for KeybindingStory {
Story::container(cx)
.child(Story::title_for::<KeyBinding>(cx))
.child(Story::label("Single Key", cx))
- .child(KeyBinding::new(binding("Z"), cx))
+ .child(KeyBinding::new_from_gpui(binding("Z"), cx))
.child(Story::label("Single Key with Modifier", cx))
.child(
div()
.flex()
.gap_3()
- .child(KeyBinding::new(binding("ctrl-c"), cx))
- .child(KeyBinding::new(binding("alt-c"), cx))
- .child(KeyBinding::new(binding("cmd-c"), cx))
- .child(KeyBinding::new(binding("shift-c"), cx)),
+ .child(KeyBinding::new_from_gpui(binding("ctrl-c"), cx))
+ .child(KeyBinding::new_from_gpui(binding("alt-c"), cx))
+ .child(KeyBinding::new_from_gpui(binding("cmd-c"), cx))
+ .child(KeyBinding::new_from_gpui(binding("shift-c"), cx)),
)
.child(Story::label("Single Key with Modifier (Permuted)", cx))
.child(
@@ -41,42 +41,59 @@ impl Render for KeybindingStory {
.gap_4()
.py_3()
.children(chunk.map(|permutation| {
- KeyBinding::new(binding(&(permutation.join("-") + "-x")), cx)
+ KeyBinding::new_from_gpui(
+ binding(&(permutation.join("-") + "-x")),
+ cx,
+ )
}))
}),
),
)
.child(Story::label("Single Key with All Modifiers", cx))
- .child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z"), cx))
+ .child(KeyBinding::new_from_gpui(
+ binding("ctrl-alt-cmd-shift-z"),
+ cx,
+ ))
.child(Story::label("Chord", cx))
- .child(KeyBinding::new(binding("a z"), cx))
+ .child(KeyBinding::new_from_gpui(binding("a z"), cx))
.child(Story::label("Chord with Modifier", cx))
- .child(KeyBinding::new(binding("ctrl-a shift-z"), cx))
- .child(KeyBinding::new(binding("fn-s"), cx))
+ .child(KeyBinding::new_from_gpui(binding("ctrl-a shift-z"), cx))
+ .child(KeyBinding::new_from_gpui(binding("fn-s"), cx))
.child(Story::label("Single Key with All Modifiers (Linux)", cx))
.child(
- KeyBinding::new(binding("ctrl-alt-cmd-shift-z"), cx)
+ KeyBinding::new_from_gpui(binding("ctrl-alt-cmd-shift-z"), cx)
.platform_style(PlatformStyle::Linux),
)
.child(Story::label("Chord (Linux)", cx))
- .child(KeyBinding::new(binding("a z"), cx).platform_style(PlatformStyle::Linux))
+ .child(
+ KeyBinding::new_from_gpui(binding("a z"), cx).platform_style(PlatformStyle::Linux),
+ )
.child(Story::label("Chord with Modifier (Linux)", cx))
.child(
- KeyBinding::new(binding("ctrl-a shift-z"), cx).platform_style(PlatformStyle::Linux),
+ KeyBinding::new_from_gpui(binding("ctrl-a shift-z"), cx)
+ .platform_style(PlatformStyle::Linux),
+ )
+ .child(
+ KeyBinding::new_from_gpui(binding("fn-s"), cx).platform_style(PlatformStyle::Linux),
)
- .child(KeyBinding::new(binding("fn-s"), cx).platform_style(PlatformStyle::Linux))
.child(Story::label("Single Key with All Modifiers (Windows)", cx))
.child(
- KeyBinding::new(binding("ctrl-alt-cmd-shift-z"), cx)
+ KeyBinding::new_from_gpui(binding("ctrl-alt-cmd-shift-z"), cx)
.platform_style(PlatformStyle::Windows),
)
.child(Story::label("Chord (Windows)", cx))
- .child(KeyBinding::new(binding("a z"), cx).platform_style(PlatformStyle::Windows))
+ .child(
+ KeyBinding::new_from_gpui(binding("a z"), cx)
+ .platform_style(PlatformStyle::Windows),
+ )
.child(Story::label("Chord with Modifier (Windows)", cx))
.child(
- KeyBinding::new(binding("ctrl-a shift-z"), cx)
+ KeyBinding::new_from_gpui(binding("ctrl-a shift-z"), cx)
+ .platform_style(PlatformStyle::Windows),
+ )
+ .child(
+ KeyBinding::new_from_gpui(binding("fn-s"), cx)
.platform_style(PlatformStyle::Windows),
)
- .child(KeyBinding::new(binding("fn-s"), cx).platform_style(PlatformStyle::Windows))
}
}