1use std::rc::Rc;
2
3use crate::PlatformStyle;
4use crate::utils::capitalize;
5use crate::{Icon, IconName, IconSize, h_flex, prelude::*};
6use gpui::{
7 Action, AnyElement, App, FocusHandle, Global, IntoElement, KeybindingKeystroke, Keystroke,
8 Modifiers, Window, relative,
9};
10use itertools::Itertools;
11use settings::KeybindSource;
12
13#[derive(Debug)]
14enum Source {
15 Action {
16 action: Box<dyn Action>,
17 focus_handle: Option<FocusHandle>,
18 },
19 Keystrokes {
20 /// A keybinding consists of a set of keystrokes,
21 /// where each keystroke is a key and a set of modifier keys.
22 /// More than one keystroke produces a chord.
23 ///
24 /// This should always contain at least one keystroke.
25 keystrokes: Rc<[KeybindingKeystroke]>,
26 },
27}
28
29impl Clone for Source {
30 fn clone(&self) -> Self {
31 match self {
32 Source::Action {
33 action,
34 focus_handle,
35 } => Source::Action {
36 action: action.boxed_clone(),
37 focus_handle: focus_handle.clone(),
38 },
39 Source::Keystrokes { keystrokes } => Source::Keystrokes {
40 keystrokes: keystrokes.clone(),
41 },
42 }
43 }
44}
45
46#[derive(Clone, Debug, IntoElement, RegisterComponent)]
47pub struct KeyBinding {
48 source: Source,
49 size: Option<AbsoluteLength>,
50 /// The [`PlatformStyle`] to use when displaying this keybinding.
51 platform_style: PlatformStyle,
52 /// Determines whether the keybinding is meant for vim mode.
53 vim_mode: bool,
54 /// Indicates whether the keybinding is currently disabled.
55 disabled: bool,
56}
57
58struct VimStyle(bool);
59impl Global for VimStyle {}
60
61impl KeyBinding {
62 /// Returns the highest precedence keybinding for an action. This is the last binding added to
63 /// the keymap. User bindings are added after built-in bindings so that they take precedence.
64 pub fn for_action(action: &dyn Action, cx: &App) -> Self {
65 Self::new(action, None, cx)
66 }
67
68 /// Like `for_action`, but lets you specify the context from which keybindings are matched.
69 pub fn for_action_in(action: &dyn Action, focus: &FocusHandle, cx: &App) -> Self {
70 Self::new(action, Some(focus.clone()), cx)
71 }
72 pub fn has_binding(&self, window: &Window) -> bool {
73 match &self.source {
74 Source::Action {
75 action,
76 focus_handle: Some(focus),
77 } => window
78 .highest_precedence_binding_for_action_in(action.as_ref(), focus)
79 .or_else(|| window.highest_precedence_binding_for_action(action.as_ref()))
80 .is_some(),
81 _ => false,
82 }
83 }
84
85 pub fn set_vim_mode(cx: &mut App, enabled: bool) {
86 cx.set_global(VimStyle(enabled));
87 }
88
89 fn is_vim_mode(cx: &App) -> bool {
90 cx.try_global::<VimStyle>().is_some_and(|g| g.0)
91 }
92
93 pub fn new(action: &dyn Action, focus_handle: Option<FocusHandle>, cx: &App) -> Self {
94 Self {
95 source: Source::Action {
96 action: action.boxed_clone(),
97 focus_handle,
98 },
99 size: None,
100 vim_mode: KeyBinding::is_vim_mode(cx),
101 platform_style: PlatformStyle::platform(),
102 disabled: false,
103 }
104 }
105
106 pub fn from_keystrokes(keystrokes: Rc<[KeybindingKeystroke]>, source: KeybindSource) -> Self {
107 Self {
108 source: Source::Keystrokes { keystrokes },
109 size: None,
110 vim_mode: source == KeybindSource::Vim,
111 platform_style: PlatformStyle::platform(),
112 disabled: false,
113 }
114 }
115
116 /// Sets the [`PlatformStyle`] for this [`KeyBinding`].
117 pub fn platform_style(mut self, platform_style: PlatformStyle) -> Self {
118 self.platform_style = platform_style;
119 self
120 }
121
122 /// Sets the size for this [`KeyBinding`].
123 pub fn size(mut self, size: impl Into<AbsoluteLength>) -> Self {
124 self.size = Some(size.into());
125 self
126 }
127
128 /// Sets whether this keybinding is currently disabled.
129 /// Disabled keybinds will be rendered in a dimmed state.
130 pub fn disabled(mut self, disabled: bool) -> Self {
131 self.disabled = disabled;
132 self
133 }
134}
135
136fn render_key(
137 key: &str,
138 color: Option<Color>,
139 platform_style: PlatformStyle,
140 size: impl Into<Option<AbsoluteLength>>,
141) -> AnyElement {
142 let key_icon = icon_for_key(key, platform_style);
143 match key_icon {
144 Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(),
145 None => {
146 let key = capitalize(key);
147 Key::new(&key, color).size(size).into_any_element()
148 }
149 }
150}
151
152impl RenderOnce for KeyBinding {
153 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
154 let render_keybinding = |keystrokes: &[KeybindingKeystroke]| {
155 let color = self.disabled.then_some(Color::Disabled);
156
157 h_flex()
158 .debug_selector(|| {
159 format!(
160 "KEY_BINDING-{}",
161 keystrokes
162 .iter()
163 .map(|k| k.key().to_string())
164 .collect::<Vec<_>>()
165 .join(" ")
166 )
167 })
168 .gap(DynamicSpacing::Base04.rems(cx))
169 .flex_none()
170 .children(keystrokes.iter().map(|keystroke| {
171 h_flex()
172 .flex_none()
173 .py_0p5()
174 .rounded_xs()
175 .text_color(cx.theme().colors().text_muted)
176 .children(render_keybinding_keystroke(
177 keystroke,
178 color,
179 self.size,
180 PlatformStyle::platform(),
181 self.vim_mode,
182 ))
183 }))
184 .into_any_element()
185 };
186
187 match self.source {
188 Source::Action {
189 action,
190 focus_handle,
191 } => focus_handle
192 .or_else(|| window.focused(cx))
193 .and_then(|focus| {
194 window.highest_precedence_binding_for_action_in(action.as_ref(), &focus)
195 })
196 .or_else(|| window.highest_precedence_binding_for_action(action.as_ref()))
197 .map(|binding| render_keybinding(binding.keystrokes())),
198 Source::Keystrokes { keystrokes } => Some(render_keybinding(keystrokes.as_ref())),
199 }
200 .unwrap_or_else(|| gpui::Empty.into_any_element())
201 }
202}
203
204pub fn render_keybinding_keystroke(
205 keystroke: &KeybindingKeystroke,
206 color: Option<Color>,
207 size: impl Into<Option<AbsoluteLength>>,
208 platform_style: PlatformStyle,
209 vim_mode: bool,
210) -> Vec<AnyElement> {
211 let use_text = vim_mode
212 || matches!(
213 platform_style,
214 PlatformStyle::Linux | PlatformStyle::Windows
215 );
216 let size = size.into();
217
218 if use_text {
219 let element = Key::new(
220 keystroke_text(
221 keystroke.modifiers(),
222 keystroke.key(),
223 platform_style,
224 vim_mode,
225 ),
226 color,
227 )
228 .size(size)
229 .into_any_element();
230 vec![element]
231 } else {
232 let mut elements = Vec::new();
233 elements.extend(render_modifiers(
234 keystroke.modifiers(),
235 platform_style,
236 color,
237 size,
238 true,
239 ));
240 elements.push(render_key(keystroke.key(), color, platform_style, size));
241 elements
242 }
243}
244
245fn icon_for_key(key: &str, platform_style: PlatformStyle) -> Option<IconName> {
246 match key {
247 "left" => Some(IconName::ArrowLeft),
248 "right" => Some(IconName::ArrowRight),
249 "up" => Some(IconName::ArrowUp),
250 "down" => Some(IconName::ArrowDown),
251 "backspace" => Some(IconName::Backspace),
252 "delete" => Some(IconName::Backspace),
253 "return" => Some(IconName::Return),
254 "enter" => Some(IconName::Return),
255 "tab" => Some(IconName::Tab),
256 "space" => Some(IconName::Space),
257 "escape" => Some(IconName::Escape),
258 "pagedown" => Some(IconName::PageDown),
259 "pageup" => Some(IconName::PageUp),
260 "shift" if platform_style == PlatformStyle::Mac => Some(IconName::Shift),
261 "control" if platform_style == PlatformStyle::Mac => Some(IconName::Control),
262 "platform" if platform_style == PlatformStyle::Mac => Some(IconName::Command),
263 "function" if platform_style == PlatformStyle::Mac => Some(IconName::Control),
264 "alt" if platform_style == PlatformStyle::Mac => Some(IconName::Option),
265 _ => None,
266 }
267}
268
269pub fn render_modifiers(
270 modifiers: &Modifiers,
271 platform_style: PlatformStyle,
272 color: Option<Color>,
273 size: Option<AbsoluteLength>,
274 trailing_separator: bool,
275) -> impl Iterator<Item = AnyElement> {
276 #[derive(Clone)]
277 enum KeyOrIcon {
278 Key(&'static str),
279 Plus,
280 Icon(IconName),
281 }
282
283 struct Modifier {
284 enabled: bool,
285 mac: KeyOrIcon,
286 linux: KeyOrIcon,
287 windows: KeyOrIcon,
288 }
289
290 let table = {
291 use KeyOrIcon::*;
292
293 [
294 Modifier {
295 enabled: modifiers.function,
296 mac: Icon(IconName::Control),
297 linux: Key("Fn"),
298 windows: Key("Fn"),
299 },
300 Modifier {
301 enabled: modifiers.control,
302 mac: Icon(IconName::Control),
303 linux: Key("Ctrl"),
304 windows: Key("Ctrl"),
305 },
306 Modifier {
307 enabled: modifiers.alt,
308 mac: Icon(IconName::Option),
309 linux: Key("Alt"),
310 windows: Key("Alt"),
311 },
312 Modifier {
313 enabled: modifiers.platform,
314 mac: Icon(IconName::Command),
315 linux: Key("Super"),
316 windows: Key("Win"),
317 },
318 Modifier {
319 enabled: modifiers.shift,
320 mac: Icon(IconName::Shift),
321 linux: Key("Shift"),
322 windows: Key("Shift"),
323 },
324 ]
325 };
326
327 let filtered = table
328 .into_iter()
329 .filter(|modifier| modifier.enabled)
330 .collect::<Vec<_>>();
331
332 let platform_keys = filtered
333 .into_iter()
334 .map(move |modifier| match platform_style {
335 PlatformStyle::Mac => Some(modifier.mac),
336 PlatformStyle::Linux => Some(modifier.linux),
337 PlatformStyle::Windows => Some(modifier.windows),
338 });
339
340 let separator = match platform_style {
341 PlatformStyle::Mac => None,
342 PlatformStyle::Linux => Some(KeyOrIcon::Plus),
343 PlatformStyle::Windows => Some(KeyOrIcon::Plus),
344 };
345
346 let platform_keys = itertools::intersperse(platform_keys, separator.clone());
347
348 platform_keys
349 .chain(if modifiers.modified() && trailing_separator {
350 Some(separator)
351 } else {
352 None
353 })
354 .flatten()
355 .map(move |key_or_icon| match key_or_icon {
356 KeyOrIcon::Key(key) => Key::new(key, color).size(size).into_any_element(),
357 KeyOrIcon::Icon(icon) => KeyIcon::new(icon, color).size(size).into_any_element(),
358 KeyOrIcon::Plus => "+".into_any_element(),
359 })
360}
361
362#[derive(IntoElement)]
363pub struct Key {
364 key: SharedString,
365 color: Option<Color>,
366 size: Option<AbsoluteLength>,
367}
368
369impl RenderOnce for Key {
370 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
371 let single_char = self.key.len() == 1;
372 let size = self
373 .size
374 .unwrap_or_else(|| TextSize::default().rems(cx).into());
375
376 div()
377 .py_0()
378 .map(|this| {
379 if single_char {
380 this.w(size).flex().flex_none().justify_center()
381 } else {
382 this.px_0p5()
383 }
384 })
385 .h(size)
386 .text_size(size)
387 .line_height(relative(1.))
388 .text_color(self.color.unwrap_or(Color::Muted).color(cx))
389 .child(self.key)
390 }
391}
392
393impl Key {
394 pub fn new(key: impl Into<SharedString>, color: Option<Color>) -> Self {
395 Self {
396 key: key.into(),
397 color,
398 size: None,
399 }
400 }
401
402 pub fn size(mut self, size: impl Into<Option<AbsoluteLength>>) -> Self {
403 self.size = size.into();
404 self
405 }
406}
407
408#[derive(IntoElement)]
409pub struct KeyIcon {
410 icon: IconName,
411 color: Option<Color>,
412 size: Option<AbsoluteLength>,
413}
414
415impl RenderOnce for KeyIcon {
416 fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
417 let size = self.size.unwrap_or(IconSize::Small.rems().into());
418
419 Icon::new(self.icon)
420 .size(IconSize::Custom(size.to_rems(window.rem_size())))
421 .color(self.color.unwrap_or(Color::Muted))
422 }
423}
424
425impl KeyIcon {
426 pub fn new(icon: IconName, color: Option<Color>) -> Self {
427 Self {
428 icon,
429 color,
430 size: None,
431 }
432 }
433
434 pub fn size(mut self, size: impl Into<Option<AbsoluteLength>>) -> Self {
435 self.size = size.into();
436 self
437 }
438}
439
440/// Returns a textual representation of the key binding for the given [`Action`].
441pub fn text_for_action(action: &dyn Action, window: &Window, cx: &App) -> Option<String> {
442 let key_binding = window.highest_precedence_binding_for_action(action)?;
443 Some(text_for_keybinding_keystrokes(key_binding.keystrokes(), cx))
444}
445
446pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String {
447 let platform_style = PlatformStyle::platform();
448 let vim_enabled = KeyBinding::is_vim_mode(cx);
449 keystrokes
450 .iter()
451 .map(|keystroke| {
452 keystroke_text(
453 &keystroke.modifiers,
454 &keystroke.key,
455 platform_style,
456 vim_enabled,
457 )
458 })
459 .join(" ")
460}
461
462pub fn text_for_keybinding_keystrokes(keystrokes: &[KeybindingKeystroke], cx: &App) -> String {
463 let platform_style = PlatformStyle::platform();
464 let vim_enabled = KeyBinding::is_vim_mode(cx);
465 keystrokes
466 .iter()
467 .map(|keystroke| {
468 keystroke_text(
469 keystroke.modifiers(),
470 keystroke.key(),
471 platform_style,
472 vim_enabled,
473 )
474 })
475 .join(" ")
476}
477
478pub fn text_for_keystroke(modifiers: &Modifiers, key: &str, cx: &App) -> String {
479 let platform_style = PlatformStyle::platform();
480 keystroke_text(modifiers, key, platform_style, KeyBinding::is_vim_mode(cx))
481}
482
483/// Returns a textual representation of the given [`Keystroke`].
484fn keystroke_text(
485 modifiers: &Modifiers,
486 key: &str,
487 platform_style: PlatformStyle,
488 vim_mode: bool,
489) -> String {
490 let mut text = String::new();
491 let delimiter = '-';
492
493 if modifiers.function {
494 match vim_mode {
495 false => text.push_str("Fn"),
496 true => text.push_str("fn"),
497 }
498
499 text.push(delimiter);
500 }
501
502 if modifiers.control {
503 match (platform_style, vim_mode) {
504 (PlatformStyle::Mac, false) => text.push_str("Control"),
505 (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Ctrl"),
506 (_, true) => text.push_str("ctrl"),
507 }
508
509 text.push(delimiter);
510 }
511
512 if modifiers.platform {
513 match (platform_style, vim_mode) {
514 (PlatformStyle::Mac, false) => text.push_str("Command"),
515 (PlatformStyle::Mac, true) => text.push_str("cmd"),
516 (PlatformStyle::Linux, false) => text.push_str("Super"),
517 (PlatformStyle::Linux, true) => text.push_str("super"),
518 (PlatformStyle::Windows, false) => text.push_str("Win"),
519 (PlatformStyle::Windows, true) => text.push_str("win"),
520 }
521
522 text.push(delimiter);
523 }
524
525 if modifiers.alt {
526 match (platform_style, vim_mode) {
527 (PlatformStyle::Mac, false) => text.push_str("Option"),
528 (PlatformStyle::Mac, true) => text.push_str("option"),
529 (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Alt"),
530 (_, true) => text.push_str("alt"),
531 }
532
533 text.push(delimiter);
534 }
535
536 if modifiers.shift {
537 match (platform_style, vim_mode) {
538 (_, false) => text.push_str("Shift"),
539 (_, true) => text.push_str("shift"),
540 }
541 text.push(delimiter);
542 }
543
544 if vim_mode {
545 text.push_str(key)
546 } else {
547 let key = match key {
548 "pageup" => "PageUp",
549 "pagedown" => "PageDown",
550 key => &capitalize(key),
551 };
552 text.push_str(key);
553 }
554
555 text
556}
557
558impl Component for KeyBinding {
559 fn scope() -> ComponentScope {
560 ComponentScope::Typography
561 }
562
563 fn name() -> &'static str {
564 "KeyBinding"
565 }
566
567 fn description() -> Option<&'static str> {
568 Some(
569 "A component that displays a key binding, supporting different platform styles and vim mode.",
570 )
571 }
572
573 // fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
574 // Some(
575 // v_flex()
576 // .gap_6()
577 // .children(vec![
578 // example_group_with_title(
579 // "Basic Usage",
580 // vec![
581 // single_example(
582 // "Default",
583 // KeyBinding::new_from_gpui(
584 // gpui::KeyBinding::new("ctrl-s", gpui::NoAction, None),
585 // cx,
586 // )
587 // .into_any_element(),
588 // ),
589 // single_example(
590 // "Mac Style",
591 // KeyBinding::new_from_gpui(
592 // gpui::KeyBinding::new("cmd-s", gpui::NoAction, None),
593 // cx,
594 // )
595 // .platform_style(PlatformStyle::Mac)
596 // .into_any_element(),
597 // ),
598 // single_example(
599 // "Windows Style",
600 // KeyBinding::new_from_gpui(
601 // gpui::KeyBinding::new("ctrl-s", gpui::NoAction, None),
602 // cx,
603 // )
604 // .platform_style(PlatformStyle::Windows)
605 // .into_any_element(),
606 // ),
607 // ],
608 // ),
609 // example_group_with_title(
610 // "Vim Mode",
611 // vec![single_example(
612 // "Vim Mode Enabled",
613 // KeyBinding::new_from_gpui(
614 // gpui::KeyBinding::new("dd", gpui::NoAction, None),
615 // cx,
616 // )
617 // .vim_mode(true)
618 // .into_any_element(),
619 // )],
620 // ),
621 // example_group_with_title(
622 // "Complex Bindings",
623 // vec![
624 // single_example(
625 // "Multiple Keys",
626 // KeyBinding::new_from_gpui(
627 // gpui::KeyBinding::new("ctrl-k ctrl-b", gpui::NoAction, None),
628 // cx,
629 // )
630 // .into_any_element(),
631 // ),
632 // single_example(
633 // "With Shift",
634 // KeyBinding::new_from_gpui(
635 // gpui::KeyBinding::new("shift-cmd-p", gpui::NoAction, None),
636 // cx,
637 // )
638 // .into_any_element(),
639 // ),
640 // ],
641 // ),
642 // ])
643 // .into_any_element(),
644 // )
645 // }
646}
647
648#[cfg(test)]
649mod tests {
650 use super::*;
651
652 #[test]
653 fn test_text_for_keystroke() {
654 let keystroke = Keystroke::parse("cmd-c").unwrap();
655 assert_eq!(
656 keystroke_text(
657 &keystroke.modifiers,
658 &keystroke.key,
659 PlatformStyle::Mac,
660 false
661 ),
662 "Command-C".to_string()
663 );
664 assert_eq!(
665 keystroke_text(
666 &keystroke.modifiers,
667 &keystroke.key,
668 PlatformStyle::Linux,
669 false
670 ),
671 "Super-C".to_string()
672 );
673 assert_eq!(
674 keystroke_text(
675 &keystroke.modifiers,
676 &keystroke.key,
677 PlatformStyle::Windows,
678 false
679 ),
680 "Win-C".to_string()
681 );
682
683 let keystroke = Keystroke::parse("ctrl-alt-delete").unwrap();
684 assert_eq!(
685 keystroke_text(
686 &keystroke.modifiers,
687 &keystroke.key,
688 PlatformStyle::Mac,
689 false
690 ),
691 "Control-Option-Delete".to_string()
692 );
693 assert_eq!(
694 keystroke_text(
695 &keystroke.modifiers,
696 &keystroke.key,
697 PlatformStyle::Linux,
698 false
699 ),
700 "Ctrl-Alt-Delete".to_string()
701 );
702 assert_eq!(
703 keystroke_text(
704 &keystroke.modifiers,
705 &keystroke.key,
706 PlatformStyle::Windows,
707 false
708 ),
709 "Ctrl-Alt-Delete".to_string()
710 );
711
712 let keystroke = Keystroke::parse("shift-pageup").unwrap();
713 assert_eq!(
714 keystroke_text(
715 &keystroke.modifiers,
716 &keystroke.key,
717 PlatformStyle::Mac,
718 false
719 ),
720 "Shift-PageUp".to_string()
721 );
722 assert_eq!(
723 keystroke_text(
724 &keystroke.modifiers,
725 &keystroke.key,
726 PlatformStyle::Linux,
727 false,
728 ),
729 "Shift-PageUp".to_string()
730 );
731 assert_eq!(
732 keystroke_text(
733 &keystroke.modifiers,
734 &keystroke.key,
735 PlatformStyle::Windows,
736 false
737 ),
738 "Shift-PageUp".to_string()
739 );
740 }
741}