keymap_ui: Dual-phase focus for keystroke input (#34312)
Closes #ISSUE An idea I and @MrSubidubi came up with, to improve UX around the keystroke input. Currently, there's a hard tradeoff with what to focus first in the edit keybind modal, if we focus the keystroke input, it makes keybind modification very easy, however, if you don't want to edit a keybind, you must use the mouse to escape the keystroke input before editing something else - breaking keyboard navigation. The idea in this PR is to have a dual-phased focus system for the keystroke input. There is an outer focus that has some sort of visual indicator to communicate it is focused (currently a border). While the outer focus region is focused, keystrokes are not intercepted. Then there is a keybind (currently hardcoded to `enter`) to enter the inner focus where keystrokes are intercepted, and which must be exited using the mouse. When the inner focus region is focused, there is a visual indicator for the fact it is "recording" (currently a hacked together red pulsing recording icon) <details><summary>Video</summary> https://github.com/user-attachments/assets/490538d0-f092-4df1-a53a-a47d7efe157b </details> Release Notes: - N/A *or* Added/Fixed/Improved ...
This commit is contained in:
parent
206cce6783
commit
67c765a99a
1 changed files with 62 additions and 26 deletions
|
@ -10,9 +10,10 @@ use feature_flags::FeatureFlagViewExt;
|
|||
use fs::Fs;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
Action, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, Global, KeyContext, Keystroke, ModifiersChangedEvent, MouseButton,
|
||||
Point, ScrollStrategy, StyledText, Subscription, WeakEntity, actions, anchored, deferred, div,
|
||||
Action, AnimationExt, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, Global, KeyContext, KeyDownEvent, Keystroke,
|
||||
ModifiersChangedEvent, MouseButton, Point, ScrollStrategy, StyledText, Subscription,
|
||||
WeakEntity, actions, anchored, deferred, div,
|
||||
};
|
||||
use language::{Language, LanguageConfig, ToOffset as _};
|
||||
use settings::{BaseKeymap, KeybindSource, KeymapFile, SettingsAssets};
|
||||
|
@ -1839,7 +1840,8 @@ struct KeystrokeInput {
|
|||
keystrokes: Vec<Keystroke>,
|
||||
placeholder_keystrokes: Option<Vec<Keystroke>>,
|
||||
highlight_on_focus: bool,
|
||||
focus_handle: FocusHandle,
|
||||
outer_focus_handle: FocusHandle,
|
||||
inner_focus_handle: FocusHandle,
|
||||
intercept_subscription: Option<Subscription>,
|
||||
_focus_subscriptions: [Subscription; 2],
|
||||
}
|
||||
|
@ -1850,16 +1852,18 @@ impl KeystrokeInput {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let outer_focus_handle = cx.focus_handle();
|
||||
let inner_focus_handle = cx.focus_handle();
|
||||
let _focus_subscriptions = [
|
||||
cx.on_focus_in(&focus_handle, window, Self::on_focus_in),
|
||||
cx.on_focus_out(&focus_handle, window, Self::on_focus_out),
|
||||
cx.on_focus_in(&inner_focus_handle, window, Self::on_inner_focus_in),
|
||||
cx.on_focus_out(&inner_focus_handle, window, Self::on_inner_focus_out),
|
||||
];
|
||||
Self {
|
||||
keystrokes: Vec::new(),
|
||||
placeholder_keystrokes,
|
||||
highlight_on_focus: true,
|
||||
focus_handle,
|
||||
inner_focus_handle,
|
||||
outer_focus_handle,
|
||||
intercept_subscription: None,
|
||||
_focus_subscriptions,
|
||||
}
|
||||
|
@ -1926,7 +1930,7 @@ impl KeystrokeInput {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
fn on_focus_in(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn on_inner_focus_in(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.intercept_subscription.is_none() {
|
||||
let listener = cx.listener(|this, event: &gpui::KeystrokeEvent, _window, cx| {
|
||||
this.handle_keystroke(&event.keystroke, cx);
|
||||
|
@ -1935,13 +1939,14 @@ impl KeystrokeInput {
|
|||
}
|
||||
}
|
||||
|
||||
fn on_focus_out(
|
||||
fn on_inner_focus_out(
|
||||
&mut self,
|
||||
_event: gpui::FocusOutEvent,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.intercept_subscription.take();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn keystrokes(&self) -> &[Keystroke] {
|
||||
|
@ -1984,26 +1989,18 @@ impl EventEmitter<()> for KeystrokeInput {}
|
|||
|
||||
impl Focusable for KeystrokeInput {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
self.outer_focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for KeystrokeInput {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let colors = cx.theme().colors();
|
||||
let is_focused = self.focus_handle.is_focused(window);
|
||||
let is_inner_focused = self.inner_focus_handle.is_focused(window);
|
||||
|
||||
return h_flex()
|
||||
.id("keybinding_input")
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
|
||||
.on_key_up(cx.listener(Self::on_key_up))
|
||||
.when(self.highlight_on_focus, |this| {
|
||||
this.focus(|mut style| {
|
||||
style.border_color = Some(colors.border_focused);
|
||||
style
|
||||
})
|
||||
})
|
||||
.id("keystroke-input")
|
||||
.track_focus(&self.outer_focus_handle)
|
||||
.py_2()
|
||||
.px_3()
|
||||
.gap_2()
|
||||
|
@ -2014,10 +2011,31 @@ impl Render for KeystrokeInput {
|
|||
.rounded_md()
|
||||
.overflow_hidden()
|
||||
.bg(colors.editor_background)
|
||||
.border_1()
|
||||
.border_2()
|
||||
.border_color(colors.border_variant)
|
||||
.focus(|mut s| {
|
||||
s.border_color = Some(colors.border_focused);
|
||||
s
|
||||
})
|
||||
.on_key_down(cx.listener(|this, event: &KeyDownEvent, window, cx| {
|
||||
// TODO: replace with action
|
||||
if !event.keystroke.modifiers.modified() && event.keystroke.key == "enter" {
|
||||
window.focus(&this.inner_focus_handle);
|
||||
cx.notify();
|
||||
}
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.id("keystroke-input-inner")
|
||||
.track_focus(&self.inner_focus_handle)
|
||||
.on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
|
||||
.on_key_up(cx.listener(Self::on_key_up))
|
||||
.when(self.highlight_on_focus, |this| {
|
||||
this.focus(|mut style| {
|
||||
style.border_color = Some(colors.border_focused);
|
||||
style
|
||||
})
|
||||
})
|
||||
.w_full()
|
||||
.min_w_0()
|
||||
.justify_center()
|
||||
|
@ -2029,10 +2047,28 @@ impl Render for KeystrokeInput {
|
|||
h_flex()
|
||||
.gap_0p5()
|
||||
.flex_none()
|
||||
.when(is_inner_focused, |this| {
|
||||
this.child(
|
||||
Icon::new(IconName::Circle)
|
||||
.color(Color::Error)
|
||||
.with_animation(
|
||||
"recording-pulse",
|
||||
gpui::Animation::new(std::time::Duration::from_secs(1))
|
||||
.repeat()
|
||||
.with_easing(gpui::pulsating_between(0.8, 1.0)),
|
||||
{
|
||||
let color = Color::Error.color(cx);
|
||||
move |this, delta| {
|
||||
this.color(Color::Custom(color.opacity(delta)))
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
IconButton::new("backspace-btn", IconName::Delete)
|
||||
.tooltip(Tooltip::text("Delete Keystroke"))
|
||||
.when(!is_focused, |this| this.icon_color(Color::Muted))
|
||||
.when(!is_inner_focused, |this| this.icon_color(Color::Muted))
|
||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||
this.keystrokes.pop();
|
||||
cx.emit(());
|
||||
|
@ -2042,7 +2078,7 @@ impl Render for KeystrokeInput {
|
|||
.child(
|
||||
IconButton::new("clear-btn", IconName::Eraser)
|
||||
.tooltip(Tooltip::text("Clear Keystrokes"))
|
||||
.when(!is_focused, |this| this.icon_color(Color::Muted))
|
||||
.when(!is_inner_focused, |this| this.icon_color(Color::Muted))
|
||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||
this.keystrokes.clear();
|
||||
cx.emit(());
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue