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}