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