use gpui::{
Div, ElementId, Hsla, InteractiveElement, IntoElement, KeyDownEvent, Keystroke,
ModifiersChangedEvent, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement,
Styled, div, prelude::FluentBuilder, px,
};
use crate::{
component::{compute_input_style, format_keybinding_ui, shortcut_hint},
i18n::{I18nContext, defaults::DefaultPlaceholders},
theme::ActiveTheme,
};
pub fn keybinding_input(id: impl Into<ElementId>) -> KeybindingInput {
KeybindingInput::new().id(id)
}
type ChangeFn = std::sync::Arc<dyn Fn(SharedString, &mut gpui::Window, &mut gpui::App)>;
#[derive(IntoElement)]
pub struct KeybindingInput {
element_id: ElementId,
base: Div,
value: Option<SharedString>,
placeholder: SharedString,
waiting_hint: SharedString,
localized: bool,
disabled: bool,
bg: Option<Hsla>,
border: Option<Hsla>,
focus_border: Option<Hsla>,
text_color: Option<Hsla>,
height: Option<gpui::AbsoluteLength>,
on_change: Option<ChangeFn>,
}
impl Default for KeybindingInput {
fn default() -> Self {
Self::new()
}
}
impl KeybindingInput {
pub fn new() -> Self {
Self {
element_id: "ui:keybinding-input".into(),
base: div(),
value: None,
placeholder: "Press keys…".into(),
waiting_hint: "Waiting for keys…".into(),
localized: false,
disabled: false,
bg: None,
border: None,
focus_border: None,
text_color: None,
height: None,
on_change: None,
}
}
pub fn localized(mut self) -> Self {
self.localized = true;
self
}
pub fn id(mut self, id: impl Into<ElementId>) -> Self {
self.element_id = id.into();
self
}
pub fn key(self, key: impl Into<ElementId>) -> Self {
self.id(key)
}
pub fn value(mut self, value: impl Into<SharedString>) -> Self {
self.value = Some(value.into());
self
}
pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
self.placeholder = placeholder.into();
self
}
pub fn waiting_hint(mut self, hint: impl Into<SharedString>) -> Self {
self.waiting_hint = hint.into();
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn bg(mut self, color: impl Into<Hsla>) -> Self {
self.bg = Some(color.into());
self
}
pub fn border(mut self, color: impl Into<Hsla>) -> Self {
self.border = Some(color.into());
self
}
pub fn focus_border(mut self, color: impl Into<Hsla>) -> Self {
self.focus_border = Some(color.into());
self
}
pub fn text_color(mut self, color: impl Into<Hsla>) -> Self {
self.text_color = Some(color.into());
self
}
pub fn height(mut self, height: gpui::AbsoluteLength) -> Self {
self.height = Some(height);
self
}
pub fn on_change<F>(mut self, handler: F) -> Self
where
F: 'static + Fn(SharedString, &mut gpui::Window, &mut gpui::App),
{
self.on_change = Some(std::sync::Arc::new(handler));
self
}
pub fn child_id(&self, suffix: &str) -> ElementId {
(self.element_id.clone(), suffix.to_string()).into()
}
}
impl ParentElement for KeybindingInput {
fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
self.base.extend(elements);
}
}
impl Styled for KeybindingInput {
fn style(&mut self) -> &mut gpui::StyleRefinement {
self.base.style()
}
}
impl InteractiveElement for KeybindingInput {
fn interactivity(&mut self) -> &mut gpui::Interactivity {
self.base.interactivity()
}
}
impl StatefulInteractiveElement for KeybindingInput {}
impl RenderOnce for KeybindingInput {
fn render(self, window: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement {
let id = self.element_id.clone();
let localized = self.localized;
let placeholder = if localized {
DefaultPlaceholders::keybinding_press_keys(cx.i18n().locale()).into()
} else {
self.placeholder
};
let waiting_hint = if localized {
DefaultPlaceholders::keybinding_waiting(cx.i18n().locale()).into()
} else {
self.waiting_hint
};
let disabled = self.disabled;
let theme = cx.theme().clone();
let bg = self.bg;
let border = self.border;
let focus_border = self.focus_border;
let text_color = self.text_color;
let input_style =
compute_input_style(&theme, disabled, bg, border, focus_border, text_color);
let height = self.height.unwrap_or_else(|| px(36.).into());
let on_change = self.on_change;
let use_internal_value = on_change.is_none();
let initial_value = self
.value
.clone()
.unwrap_or_else(|| SharedString::new_static(""));
let internal_value = if use_internal_value {
Some(
window.use_keyed_state((id.clone(), "ui:keybinding:value"), cx, |_, _| {
initial_value
}),
)
} else {
None
};
let capture_active =
window.use_keyed_state((id.clone(), "ui:keybinding:active"), cx, |_, _| false);
let current_modifiers =
window.use_keyed_state((id.clone(), "ui:keybinding:modifiers"), cx, |_, _| {
gpui::Modifiers::default()
});
let focus_handle =
window.use_keyed_state((id.clone(), "ui:keybinding:focus"), cx, |_, cx| {
cx.focus_handle()
});
let on_change_state =
window.use_keyed_state((id.clone(), "ui:keybinding:on-change"), cx, |_, _| {
None::<ChangeFn>
});
on_change_state.update(cx, |state, _| *state = on_change.clone());
let disabled_state =
window.use_keyed_state((id.clone(), "ui:keybinding:disabled"), cx, |_, _| disabled);
disabled_state.update(cx, |state, _| *state = disabled);
let value = if use_internal_value {
internal_value
.as_ref()
.expect("internal value should exist")
.read(cx)
.clone()
} else {
self.value
.clone()
.unwrap_or_else(|| SharedString::new_static(""))
};
let is_active = *capture_active.read(cx);
let showing_placeholder = value.is_empty() && !is_active;
let _interceptor_subscription =
window.use_keyed_state((id.clone(), "ui:keybinding:interceptor"), cx, {
let capture_active = capture_active.clone();
let focus_handle = focus_handle.clone();
let current_modifiers = current_modifiers.clone();
let internal_value = internal_value.clone();
let on_change_state = on_change_state.clone();
let disabled_state = disabled_state.clone();
move |_, cx| {
Some(cx.intercept_keystrokes(move |event, window, cx| {
if *disabled_state.read(cx) {
return;
}
if !*capture_active.read(cx) {
return;
}
if !focus_handle.read(cx).is_focused(window) {
return;
}
current_modifiers.update(cx, |state, _| *state = event.keystroke.modifiers);
let key = event.keystroke.key.to_ascii_lowercase();
let is_modifier_key = matches!(
key.as_str(),
"shift"
| "control"
| "ctrl"
| "alt"
| "option"
| "cmd"
| "meta"
| "super"
| "fn"
);
if event.keystroke.key.is_empty() || is_modifier_key {
return;
}
cx.stop_propagation();
let text = format_keybinding_ui(&event.keystroke);
if let Some(internal_value) = &internal_value {
internal_value.update(cx, |state, cx| {
*state = text.clone();
cx.notify();
});
}
if let Some(handler) = on_change_state.read(cx).as_ref().cloned() {
handler(text.clone(), window, cx);
}
capture_active.update(cx, |state, _| *state = false);
window.refresh();
}))
}
});
let mods_for_display = *current_modifiers.read(cx);
self.base
.id(id.clone())
.h(height)
.w_full()
.flex()
.items_center()
.justify_between()
.gap_2()
.px_3()
.rounded_md()
.bg(input_style.bg)
.border_1()
.border_color(input_style.border)
.focusable()
.focus_visible(|style| style.border_2().border_color(input_style.focus_border))
.track_focus(focus_handle.read(cx))
.when(disabled, |this| this.opacity(0.6).cursor_not_allowed())
.when(!disabled, |this| this.cursor_pointer())
.on_click({
let capture_active = capture_active.clone();
let focus_handle = focus_handle.clone();
move |_ev, window, cx| {
if disabled {
return;
}
let handle = focus_handle.read(cx).clone();
window.focus(&handle);
capture_active.update(cx, |state, _| *state = true);
window.refresh();
}
})
.on_modifiers_changed({
let capture_active = capture_active.clone();
let current_modifiers = current_modifiers.clone();
move |event: &ModifiersChangedEvent, _window, cx| {
if disabled || !*capture_active.read(cx) {
return;
}
current_modifiers.update(cx, |state, cx| {
*state = event.modifiers;
cx.notify();
});
}
})
.capture_key_down({
let capture_active = capture_active.clone();
let current_modifiers = current_modifiers.clone();
let internal_value = internal_value.clone();
let on_change = on_change_state.clone();
move |event: &KeyDownEvent, window, cx| {
if disabled || !*capture_active.read(cx) {
return;
}
cx.stop_propagation();
let mut keystroke = event.keystroke.clone();
keystroke.modifiers = *current_modifiers.read(cx);
if keystroke.key.is_empty() {
return;
}
let text = format_keybinding_ui(&keystroke);
if let Some(internal_value) = &internal_value {
internal_value.update(cx, |state, cx| {
*state = text.clone();
cx.notify();
});
}
if let Some(handler) = on_change.read(cx).as_ref().cloned() {
handler(text.clone(), window, cx);
}
capture_active.update(cx, |state, _| *state = false);
}
})
.child(
div().flex().items_center().gap_2().child(
(if is_active {
let ks = Keystroke {
modifiers: mods_for_display,
..Default::default()
};
let hint = if ks.modifiers.modified() {
format_keybinding_ui(&ks)
} else {
waiting_hint.clone()
};
div()
.text_color(theme.content.secondary)
.child(hint)
.into_any_element()
} else if showing_placeholder {
div()
.text_color(theme.content.tertiary)
.child(placeholder)
.into_any_element()
} else {
div()
.font_family("monospace")
.text_color(input_style.text_color)
.child(value)
.into_any_element()
})
.into_any_element(),
),
)
.child(
div()
.id((id.clone(), "hint"))
.child(shortcut_hint("Press keys")),
)
}
}