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}