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