1use crate::PlatformStyle;
2use crate::{Icon, IconName, IconSize, h_flex, prelude::*};
3use gpui::{
4 Action, AnyElement, App, FocusHandle, Global, IntoElement, Keystroke, Modifiers, Window,
5 relative,
6};
7use itertools::Itertools;
8
9#[derive(Debug, IntoElement, Clone)]
10pub struct KeyBinding {
11 /// A keybinding consists of a key and a set of modifier keys.
12 /// More then one keybinding produces a chord.
13 ///
14 /// This should always contain at least one element.
15 key_binding: gpui::KeyBinding,
16
17 /// The [`PlatformStyle`] to use when displaying this keybinding.
18 platform_style: PlatformStyle,
19 size: Option<AbsoluteLength>,
20
21 /// Determines whether the keybinding is meant for vim mode.
22 vim_mode: bool,
23
24 /// Indicates whether the keybinding is currently disabled.
25 disabled: bool,
26}
27
28struct VimStyle(bool);
29impl Global for VimStyle {}
30
31impl KeyBinding {
32 /// Returns the highest precedence keybinding for an action. This is the last binding added to
33 /// the keymap. User bindings are added after built-in bindings so that they take precedence.
34 pub fn for_action(action: &dyn Action, window: &mut Window, cx: &App) -> Option<Self> {
35 if let Some(focused) = window.focused(cx) {
36 return Self::for_action_in(action, &focused, window, cx);
37 }
38 let key_binding =
39 gpui::Keymap::binding_to_display_from_bindings(window.bindings_for_action(action))?;
40 Some(Self::new(key_binding, cx))
41 }
42
43 /// Like `for_action`, but lets you specify the context from which keybindings are matched.
44 pub fn for_action_in(
45 action: &dyn Action,
46 focus: &FocusHandle,
47 window: &mut Window,
48 cx: &App,
49 ) -> Option<Self> {
50 let key_binding = gpui::Keymap::binding_to_display_from_bindings(
51 window.bindings_for_action_in(action, focus),
52 )?;
53 Some(Self::new(key_binding, cx))
54 }
55
56 pub fn set_vim_mode(cx: &mut App, enabled: bool) {
57 cx.set_global(VimStyle(enabled));
58 }
59
60 fn is_vim_mode(cx: &App) -> bool {
61 cx.try_global::<VimStyle>().is_some_and(|g| g.0)
62 }
63
64 pub fn new(key_binding: gpui::KeyBinding, cx: &App) -> Self {
65 Self {
66 key_binding,
67 platform_style: PlatformStyle::platform(),
68 size: None,
69 vim_mode: KeyBinding::is_vim_mode(cx),
70 disabled: false,
71 }
72 }
73
74 /// Sets the [`PlatformStyle`] for this [`KeyBinding`].
75 pub fn platform_style(mut self, platform_style: PlatformStyle) -> Self {
76 self.platform_style = platform_style;
77 self
78 }
79
80 /// Sets the size for this [`KeyBinding`].
81 pub fn size(mut self, size: impl Into<AbsoluteLength>) -> Self {
82 self.size = Some(size.into());
83 self
84 }
85
86 /// Sets whether this keybinding is currently disabled.
87 /// Disabled keybinds will be rendered in a dimmed state.
88 pub fn disabled(mut self, disabled: bool) -> Self {
89 self.disabled = disabled;
90 self
91 }
92
93 pub fn vim_mode(mut self, enabled: bool) -> Self {
94 self.vim_mode = enabled;
95 self
96 }
97
98 fn render_key(&self, keystroke: &Keystroke, color: Option<Color>) -> AnyElement {
99 let key_icon = icon_for_key(keystroke, self.platform_style);
100 match key_icon {
101 Some(icon) => KeyIcon::new(icon, color).size(self.size).into_any_element(),
102 None => {
103 let key = util::capitalize(&keystroke.key);
104 Key::new(&key, color).size(self.size).into_any_element()
105 }
106 }
107 }
108}
109
110impl RenderOnce for KeyBinding {
111 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
112 let color = self.disabled.then_some(Color::Disabled);
113 let use_text = self.vim_mode
114 || matches!(
115 self.platform_style,
116 PlatformStyle::Linux | PlatformStyle::Windows
117 );
118 h_flex()
119 .debug_selector(|| {
120 format!(
121 "KEY_BINDING-{}",
122 self.key_binding
123 .keystrokes()
124 .iter()
125 .map(|k| k.key.to_string())
126 .collect::<Vec<_>>()
127 .join(" ")
128 )
129 })
130 .gap(DynamicSpacing::Base04.rems(cx))
131 .flex_none()
132 .children(self.key_binding.keystrokes().iter().map(|keystroke| {
133 h_flex()
134 .flex_none()
135 .py_0p5()
136 .rounded_xs()
137 .text_color(cx.theme().colors().text_muted)
138 .when(use_text, |el| {
139 el.child(
140 Key::new(
141 keystroke_text(&keystroke, self.platform_style, self.vim_mode),
142 color,
143 )
144 .size(self.size),
145 )
146 })
147 .when(!use_text, |el| {
148 el.children(render_modifiers(
149 &keystroke.modifiers,
150 self.platform_style,
151 color,
152 self.size,
153 true,
154 ))
155 .map(|el| el.child(self.render_key(&keystroke, color)))
156 })
157 }))
158 }
159}
160
161fn icon_for_key(keystroke: &Keystroke, platform_style: PlatformStyle) -> Option<IconName> {
162 match keystroke.key.as_str() {
163 "left" => Some(IconName::ArrowLeft),
164 "right" => Some(IconName::ArrowRight),
165 "up" => Some(IconName::ArrowUp),
166 "down" => Some(IconName::ArrowDown),
167 "backspace" => Some(IconName::Backspace),
168 "delete" => Some(IconName::Delete),
169 "return" => Some(IconName::Return),
170 "enter" => Some(IconName::Return),
171 "tab" => Some(IconName::Tab),
172 "space" => Some(IconName::Space),
173 "escape" => Some(IconName::Escape),
174 "pagedown" => Some(IconName::PageDown),
175 "pageup" => Some(IconName::PageUp),
176 "shift" if platform_style == PlatformStyle::Mac => Some(IconName::Shift),
177 "control" if platform_style == PlatformStyle::Mac => Some(IconName::Control),
178 "platform" if platform_style == PlatformStyle::Mac => Some(IconName::Command),
179 "function" if platform_style == PlatformStyle::Mac => Some(IconName::Control),
180 "alt" if platform_style == PlatformStyle::Mac => Some(IconName::Option),
181 _ => None,
182 }
183}
184
185pub fn render_modifiers(
186 modifiers: &Modifiers,
187 platform_style: PlatformStyle,
188 color: Option<Color>,
189 size: Option<AbsoluteLength>,
190 trailing_separator: bool,
191) -> impl Iterator<Item = AnyElement> {
192 #[derive(Clone)]
193 enum KeyOrIcon {
194 Key(&'static str),
195 Plus,
196 Icon(IconName),
197 }
198
199 struct Modifier {
200 enabled: bool,
201 mac: KeyOrIcon,
202 linux: KeyOrIcon,
203 windows: KeyOrIcon,
204 }
205
206 let table = {
207 use KeyOrIcon::*;
208
209 [
210 Modifier {
211 enabled: modifiers.function,
212 mac: Icon(IconName::Control),
213 linux: Key("Fn"),
214 windows: Key("Fn"),
215 },
216 Modifier {
217 enabled: modifiers.control,
218 mac: Icon(IconName::Control),
219 linux: Key("Ctrl"),
220 windows: Key("Ctrl"),
221 },
222 Modifier {
223 enabled: modifiers.alt,
224 mac: Icon(IconName::Option),
225 linux: Key("Alt"),
226 windows: Key("Alt"),
227 },
228 Modifier {
229 enabled: modifiers.platform,
230 mac: Icon(IconName::Command),
231 linux: Key("Super"),
232 windows: Key("Win"),
233 },
234 Modifier {
235 enabled: modifiers.shift,
236 mac: Icon(IconName::Shift),
237 linux: Key("Shift"),
238 windows: Key("Shift"),
239 },
240 ]
241 };
242
243 let filtered = table
244 .into_iter()
245 .filter(|modifier| modifier.enabled)
246 .collect::<Vec<_>>();
247
248 let platform_keys = filtered
249 .into_iter()
250 .map(move |modifier| match platform_style {
251 PlatformStyle::Mac => Some(modifier.mac),
252 PlatformStyle::Linux => Some(modifier.linux),
253 PlatformStyle::Windows => Some(modifier.windows),
254 });
255
256 let separator = match platform_style {
257 PlatformStyle::Mac => None,
258 PlatformStyle::Linux => Some(KeyOrIcon::Plus),
259 PlatformStyle::Windows => Some(KeyOrIcon::Plus),
260 };
261
262 let platform_keys = itertools::intersperse(platform_keys, separator.clone());
263
264 platform_keys
265 .chain(if modifiers.modified() && trailing_separator {
266 Some(separator)
267 } else {
268 None
269 })
270 .flatten()
271 .map(move |key_or_icon| match key_or_icon {
272 KeyOrIcon::Key(key) => Key::new(key, color).size(size).into_any_element(),
273 KeyOrIcon::Icon(icon) => KeyIcon::new(icon, color).size(size).into_any_element(),
274 KeyOrIcon::Plus => "+".into_any_element(),
275 })
276}
277
278#[derive(IntoElement)]
279pub struct Key {
280 key: SharedString,
281 color: Option<Color>,
282 size: Option<AbsoluteLength>,
283}
284
285impl RenderOnce for Key {
286 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
287 let single_char = self.key.len() == 1;
288 let size = self
289 .size
290 .unwrap_or_else(|| TextSize::default().rems(cx).into());
291
292 div()
293 .py_0()
294 .map(|this| {
295 if single_char {
296 this.w(size).flex().flex_none().justify_center()
297 } else {
298 this.px_0p5()
299 }
300 })
301 .h(size)
302 .text_size(size)
303 .line_height(relative(1.))
304 .text_color(self.color.unwrap_or(Color::Muted).color(cx))
305 .child(self.key.clone())
306 }
307}
308
309impl Key {
310 pub fn new(key: impl Into<SharedString>, color: Option<Color>) -> Self {
311 Self {
312 key: key.into(),
313 color,
314 size: None,
315 }
316 }
317
318 pub fn size(mut self, size: impl Into<Option<AbsoluteLength>>) -> Self {
319 self.size = size.into();
320 self
321 }
322}
323
324#[derive(IntoElement)]
325pub struct KeyIcon {
326 icon: IconName,
327 color: Option<Color>,
328 size: Option<AbsoluteLength>,
329}
330
331impl RenderOnce for KeyIcon {
332 fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
333 let size = self.size.unwrap_or(IconSize::Small.rems().into());
334
335 Icon::new(self.icon)
336 .size(IconSize::Custom(size.to_rems(window.rem_size())))
337 .color(self.color.unwrap_or(Color::Muted))
338 }
339}
340
341impl KeyIcon {
342 pub fn new(icon: IconName, color: Option<Color>) -> Self {
343 Self {
344 icon,
345 color,
346 size: None,
347 }
348 }
349
350 pub fn size(mut self, size: impl Into<Option<AbsoluteLength>>) -> Self {
351 self.size = size.into();
352 self
353 }
354}
355
356/// Returns a textual representation of the key binding for the given [`Action`].
357pub fn text_for_action(action: &dyn Action, window: &Window, cx: &App) -> Option<String> {
358 let bindings = window.bindings_for_action(action);
359 let key_binding = bindings.last()?;
360 Some(text_for_keystrokes(key_binding.keystrokes(), cx))
361}
362
363pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String {
364 let platform_style = PlatformStyle::platform();
365 let vim_enabled = cx.try_global::<VimStyle>().is_some();
366 keystrokes
367 .iter()
368 .map(|keystroke| keystroke_text(keystroke, platform_style, vim_enabled))
369 .join(" ")
370}
371
372pub fn text_for_keystroke(keystroke: &Keystroke, cx: &App) -> String {
373 let platform_style = PlatformStyle::platform();
374 let vim_enabled = cx.try_global::<VimStyle>().is_some();
375 keystroke_text(keystroke, platform_style, vim_enabled)
376}
377
378/// Returns a textual representation of the given [`Keystroke`].
379fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode: bool) -> String {
380 let mut text = String::new();
381
382 let delimiter = match (platform_style, vim_mode) {
383 (PlatformStyle::Mac, false) => '-',
384 (PlatformStyle::Linux | PlatformStyle::Windows, false) => '-',
385 (_, true) => '-',
386 };
387
388 if keystroke.modifiers.function {
389 match vim_mode {
390 false => text.push_str("Fn"),
391 true => text.push_str("fn"),
392 }
393
394 text.push(delimiter);
395 }
396
397 if keystroke.modifiers.control {
398 match (platform_style, vim_mode) {
399 (PlatformStyle::Mac, false) => text.push_str("Control"),
400 (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Ctrl"),
401 (_, true) => text.push_str("ctrl"),
402 }
403
404 text.push(delimiter);
405 }
406
407 if keystroke.modifiers.platform {
408 match (platform_style, vim_mode) {
409 (PlatformStyle::Mac, false) => text.push_str("Command"),
410 (PlatformStyle::Mac, true) => text.push_str("cmd"),
411 (PlatformStyle::Linux, false) => text.push_str("Super"),
412 (PlatformStyle::Linux, true) => text.push_str("super"),
413 (PlatformStyle::Windows, false) => text.push_str("Win"),
414 (PlatformStyle::Windows, true) => text.push_str("win"),
415 }
416
417 text.push(delimiter);
418 }
419
420 if keystroke.modifiers.alt {
421 match (platform_style, vim_mode) {
422 (PlatformStyle::Mac, false) => text.push_str("Option"),
423 (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Alt"),
424 (_, true) => text.push_str("alt"),
425 }
426
427 text.push(delimiter);
428 }
429
430 if keystroke.modifiers.shift {
431 match (platform_style, vim_mode) {
432 (_, false) => text.push_str("Shift"),
433 (_, true) => text.push_str("shift"),
434 }
435 text.push(delimiter);
436 }
437
438 if vim_mode {
439 text.push_str(&keystroke.key)
440 } else {
441 let key = match keystroke.key.as_str() {
442 "pageup" => "PageUp",
443 "pagedown" => "PageDown",
444 key => &util::capitalize(key),
445 };
446 text.push_str(key);
447 }
448
449 text
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455
456 #[test]
457 fn test_text_for_keystroke() {
458 assert_eq!(
459 keystroke_text(
460 &Keystroke::parse("cmd-c").unwrap(),
461 PlatformStyle::Mac,
462 false
463 ),
464 "Command-C".to_string()
465 );
466 assert_eq!(
467 keystroke_text(
468 &Keystroke::parse("cmd-c").unwrap(),
469 PlatformStyle::Linux,
470 false
471 ),
472 "Super-C".to_string()
473 );
474 assert_eq!(
475 keystroke_text(
476 &Keystroke::parse("cmd-c").unwrap(),
477 PlatformStyle::Windows,
478 false
479 ),
480 "Win-C".to_string()
481 );
482
483 assert_eq!(
484 keystroke_text(
485 &Keystroke::parse("ctrl-alt-delete").unwrap(),
486 PlatformStyle::Mac,
487 false
488 ),
489 "Control-Option-Delete".to_string()
490 );
491 assert_eq!(
492 keystroke_text(
493 &Keystroke::parse("ctrl-alt-delete").unwrap(),
494 PlatformStyle::Linux,
495 false
496 ),
497 "Ctrl-Alt-Delete".to_string()
498 );
499 assert_eq!(
500 keystroke_text(
501 &Keystroke::parse("ctrl-alt-delete").unwrap(),
502 PlatformStyle::Windows,
503 false
504 ),
505 "Ctrl-Alt-Delete".to_string()
506 );
507
508 assert_eq!(
509 keystroke_text(
510 &Keystroke::parse("shift-pageup").unwrap(),
511 PlatformStyle::Mac,
512 false
513 ),
514 "Shift-PageUp".to_string()
515 );
516 assert_eq!(
517 keystroke_text(
518 &Keystroke::parse("shift-pageup").unwrap(),
519 PlatformStyle::Linux,
520 false,
521 ),
522 "Shift-PageUp".to_string()
523 );
524 assert_eq!(
525 keystroke_text(
526 &Keystroke::parse("shift-pageup").unwrap(),
527 PlatformStyle::Windows,
528 false
529 ),
530 "Shift-PageUp".to_string()
531 );
532 }
533}