keybinding.rs

  1use crate::PlatformStyle;
  2use crate::{h_flex, prelude::*, Icon, IconName, IconSize};
  3use gpui::{
  4    relative, Action, AnyElement, App, FocusHandle, Global, IntoElement, Keystroke, Modifiers,
  5    Window,
  6};
  7use itertools::Itertools;
  8
  9#[derive(Debug, IntoElement, Clone)]
 10pub struct KeyBinding {
 11    /// A keybinding consists of a key and a set of modifier keys.
 12    /// More then one keybinding produces a chord.
 13    ///
 14    /// This should always contain at least one element.
 15    key_binding: gpui::KeyBinding,
 16
 17    /// The [`PlatformStyle`] to use when displaying this keybinding.
 18    platform_style: PlatformStyle,
 19    size: Option<AbsoluteLength>,
 20
 21    /// Determines whether the keybinding is meant for vim mode.
 22    vim_mode: bool,
 23}
 24
 25struct VimStyle(bool);
 26impl Global for VimStyle {}
 27
 28impl KeyBinding {
 29    /// Returns the highest precedence keybinding for an action. This is the last binding added to
 30    /// the keymap. User bindings are added after built-in bindings so that they take precedence.
 31    pub fn for_action(action: &dyn Action, window: &mut Window, cx: &App) -> Option<Self> {
 32        if let Some(focused) = window.focused(cx) {
 33            return Self::for_action_in(action, &focused, window, cx);
 34        }
 35        let key_binding =
 36            gpui::Keymap::binding_to_display_from_bindings(window.bindings_for_action(action))?;
 37        Some(Self::new(key_binding, cx))
 38    }
 39
 40    /// Like `for_action`, but lets you specify the context from which keybindings are matched.
 41    pub fn for_action_in(
 42        action: &dyn Action,
 43        focus: &FocusHandle,
 44        window: &mut Window,
 45        cx: &App,
 46    ) -> Option<Self> {
 47        let key_binding = gpui::Keymap::binding_to_display_from_bindings(
 48            window.bindings_for_action_in(action, focus),
 49        )?;
 50        Some(Self::new(key_binding, cx))
 51    }
 52
 53    pub fn set_vim_mode(cx: &mut App, enabled: bool) {
 54        cx.set_global(VimStyle(enabled));
 55    }
 56
 57    fn is_vim_mode(cx: &App) -> bool {
 58        cx.try_global::<VimStyle>().is_some_and(|g| g.0)
 59    }
 60
 61    pub fn new(key_binding: gpui::KeyBinding, cx: &App) -> Self {
 62        Self {
 63            key_binding,
 64            platform_style: PlatformStyle::platform(),
 65            size: None,
 66            vim_mode: KeyBinding::is_vim_mode(cx),
 67        }
 68    }
 69
 70    /// Sets the [`PlatformStyle`] for this [`KeyBinding`].
 71    pub fn platform_style(mut self, platform_style: PlatformStyle) -> Self {
 72        self.platform_style = platform_style;
 73        self
 74    }
 75
 76    /// Sets the size for this [`KeyBinding`].
 77    pub fn size(mut self, size: impl Into<AbsoluteLength>) -> Self {
 78        self.size = Some(size.into());
 79        self
 80    }
 81
 82    pub fn vim_mode(mut self, enabled: bool) -> Self {
 83        self.vim_mode = enabled;
 84        self
 85    }
 86
 87    fn render_key(&self, keystroke: &Keystroke, color: Option<Color>) -> AnyElement {
 88        let key_icon = icon_for_key(keystroke, self.platform_style);
 89        match key_icon {
 90            Some(icon) => KeyIcon::new(icon, color).size(self.size).into_any_element(),
 91            None => {
 92                let key = util::capitalize(&keystroke.key);
 93                Key::new(&key, color).size(self.size).into_any_element()
 94            }
 95        }
 96    }
 97}
 98
 99impl RenderOnce for KeyBinding {
100    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
101        let use_text = self.vim_mode
102            || matches!(
103                self.platform_style,
104                PlatformStyle::Linux | PlatformStyle::Windows
105            );
106        h_flex()
107            .debug_selector(|| {
108                format!(
109                    "KEY_BINDING-{}",
110                    self.key_binding
111                        .keystrokes()
112                        .iter()
113                        .map(|k| k.key.to_string())
114                        .collect::<Vec<_>>()
115                        .join(" ")
116                )
117            })
118            .gap(DynamicSpacing::Base04.rems(cx))
119            .flex_none()
120            .children(self.key_binding.keystrokes().iter().map(|keystroke| {
121                h_flex()
122                    .flex_none()
123                    .py_0p5()
124                    .rounded_xs()
125                    .text_color(cx.theme().colors().text_muted)
126                    .when(use_text, |el| {
127                        el.child(
128                            Key::new(
129                                keystroke_text(&keystroke, self.platform_style, self.vim_mode),
130                                None,
131                            )
132                            .size(self.size),
133                        )
134                    })
135                    .when(!use_text, |el| {
136                        el.children(render_modifiers(
137                            &keystroke.modifiers,
138                            self.platform_style,
139                            None,
140                            self.size,
141                            true,
142                        ))
143                        .map(|el| el.child(self.render_key(&keystroke, None)))
144                    })
145            }))
146    }
147}
148
149fn icon_for_key(keystroke: &Keystroke, platform_style: PlatformStyle) -> Option<IconName> {
150    match keystroke.key.as_str() {
151        "left" => Some(IconName::ArrowLeft),
152        "right" => Some(IconName::ArrowRight),
153        "up" => Some(IconName::ArrowUp),
154        "down" => Some(IconName::ArrowDown),
155        "backspace" => Some(IconName::Backspace),
156        "delete" => Some(IconName::Delete),
157        "return" => Some(IconName::Return),
158        "enter" => Some(IconName::Return),
159        "tab" => Some(IconName::Tab),
160        "space" => Some(IconName::Space),
161        "escape" => Some(IconName::Escape),
162        "pagedown" => Some(IconName::PageDown),
163        "pageup" => Some(IconName::PageUp),
164        "shift" if platform_style == PlatformStyle::Mac => Some(IconName::Shift),
165        "control" if platform_style == PlatformStyle::Mac => Some(IconName::Control),
166        "platform" if platform_style == PlatformStyle::Mac => Some(IconName::Command),
167        "function" if platform_style == PlatformStyle::Mac => Some(IconName::Control),
168        "alt" if platform_style == PlatformStyle::Mac => Some(IconName::Option),
169        _ => None,
170    }
171}
172
173pub fn render_modifiers(
174    modifiers: &Modifiers,
175    platform_style: PlatformStyle,
176    color: Option<Color>,
177    size: Option<AbsoluteLength>,
178    trailing_separator: bool,
179) -> impl Iterator<Item = AnyElement> {
180    #[derive(Clone)]
181    enum KeyOrIcon {
182        Key(&'static str),
183        Plus,
184        Icon(IconName),
185    }
186
187    struct Modifier {
188        enabled: bool,
189        mac: KeyOrIcon,
190        linux: KeyOrIcon,
191        windows: KeyOrIcon,
192    }
193
194    let table = {
195        use KeyOrIcon::*;
196
197        [
198            Modifier {
199                enabled: modifiers.function,
200                mac: Icon(IconName::Control),
201                linux: Key("Fn"),
202                windows: Key("Fn"),
203            },
204            Modifier {
205                enabled: modifiers.control,
206                mac: Icon(IconName::Control),
207                linux: Key("Ctrl"),
208                windows: Key("Ctrl"),
209            },
210            Modifier {
211                enabled: modifiers.alt,
212                mac: Icon(IconName::Option),
213                linux: Key("Alt"),
214                windows: Key("Alt"),
215            },
216            Modifier {
217                enabled: modifiers.platform,
218                mac: Icon(IconName::Command),
219                linux: Key("Super"),
220                windows: Key("Win"),
221            },
222            Modifier {
223                enabled: modifiers.shift,
224                mac: Icon(IconName::Shift),
225                linux: Key("Shift"),
226                windows: Key("Shift"),
227            },
228        ]
229    };
230
231    let filtered = table
232        .into_iter()
233        .filter(|modifier| modifier.enabled)
234        .collect::<Vec<_>>();
235
236    let platform_keys = filtered
237        .into_iter()
238        .map(move |modifier| match platform_style {
239            PlatformStyle::Mac => Some(modifier.mac),
240            PlatformStyle::Linux => Some(modifier.linux),
241            PlatformStyle::Windows => Some(modifier.windows),
242        });
243
244    let separator = match platform_style {
245        PlatformStyle::Mac => None,
246        PlatformStyle::Linux => Some(KeyOrIcon::Plus),
247        PlatformStyle::Windows => Some(KeyOrIcon::Plus),
248    };
249
250    let platform_keys = itertools::intersperse(platform_keys, separator.clone());
251
252    platform_keys
253        .chain(if modifiers.modified() && trailing_separator {
254            Some(separator)
255        } else {
256            None
257        })
258        .flatten()
259        .map(move |key_or_icon| match key_or_icon {
260            KeyOrIcon::Key(key) => Key::new(key, color).size(size).into_any_element(),
261            KeyOrIcon::Icon(icon) => KeyIcon::new(icon, color).size(size).into_any_element(),
262            KeyOrIcon::Plus => "+".into_any_element(),
263        })
264}
265
266#[derive(IntoElement)]
267pub struct Key {
268    key: SharedString,
269    color: Option<Color>,
270    size: Option<AbsoluteLength>,
271}
272
273impl RenderOnce for Key {
274    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
275        let single_char = self.key.len() == 1;
276        let size = self
277            .size
278            .unwrap_or_else(|| TextSize::default().rems(cx).into());
279
280        div()
281            .py_0()
282            .map(|this| {
283                if single_char {
284                    this.w(size).flex().flex_none().justify_center()
285                } else {
286                    this.px_0p5()
287                }
288            })
289            .h(size)
290            .text_size(size)
291            .line_height(relative(1.))
292            .text_color(self.color.unwrap_or(Color::Muted).color(cx))
293            .child(self.key.clone())
294    }
295}
296
297impl Key {
298    pub fn new(key: impl Into<SharedString>, color: Option<Color>) -> Self {
299        Self {
300            key: key.into(),
301            color,
302            size: None,
303        }
304    }
305
306    pub fn size(mut self, size: impl Into<Option<AbsoluteLength>>) -> Self {
307        self.size = size.into();
308        self
309    }
310}
311
312#[derive(IntoElement)]
313pub struct KeyIcon {
314    icon: IconName,
315    color: Option<Color>,
316    size: Option<AbsoluteLength>,
317}
318
319impl RenderOnce for KeyIcon {
320    fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
321        let size = self.size.unwrap_or(IconSize::Small.rems().into());
322
323        Icon::new(self.icon)
324            .size(IconSize::Custom(size.to_rems(window.rem_size())))
325            .color(self.color.unwrap_or(Color::Muted))
326    }
327}
328
329impl KeyIcon {
330    pub fn new(icon: IconName, color: Option<Color>) -> Self {
331        Self {
332            icon,
333            color,
334            size: None,
335        }
336    }
337
338    pub fn size(mut self, size: impl Into<Option<AbsoluteLength>>) -> Self {
339        self.size = size.into();
340        self
341    }
342}
343
344/// Returns a textual representation of the key binding for the given [`Action`].
345pub fn text_for_action(action: &dyn Action, window: &Window, cx: &App) -> Option<String> {
346    let bindings = window.bindings_for_action(action);
347    let key_binding = bindings.last()?;
348    Some(text_for_keystrokes(key_binding.keystrokes(), cx))
349}
350
351pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String {
352    let platform_style = PlatformStyle::platform();
353    let vim_enabled = cx.try_global::<VimStyle>().is_some();
354    keystrokes
355        .iter()
356        .map(|keystroke| keystroke_text(keystroke, platform_style, vim_enabled))
357        .join(" ")
358}
359
360pub fn text_for_keystroke(keystroke: &Keystroke, cx: &App) -> String {
361    let platform_style = PlatformStyle::platform();
362    let vim_enabled = cx.try_global::<VimStyle>().is_some();
363    keystroke_text(keystroke, platform_style, vim_enabled)
364}
365
366/// Returns a textual representation of the given [`Keystroke`].
367fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode: bool) -> String {
368    let mut text = String::new();
369
370    let delimiter = match (platform_style, vim_mode) {
371        (PlatformStyle::Mac, false) => '-',
372        (PlatformStyle::Linux | PlatformStyle::Windows, false) => '-',
373        (_, true) => '-',
374    };
375
376    if keystroke.modifiers.function {
377        match vim_mode {
378            false => text.push_str("Fn"),
379            true => text.push_str("fn"),
380        }
381
382        text.push(delimiter);
383    }
384
385    if keystroke.modifiers.control {
386        match (platform_style, vim_mode) {
387            (PlatformStyle::Mac, false) => text.push_str("Control"),
388            (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Ctrl"),
389            (_, true) => text.push_str("ctrl"),
390        }
391
392        text.push(delimiter);
393    }
394
395    if keystroke.modifiers.platform {
396        match (platform_style, vim_mode) {
397            (PlatformStyle::Mac, false) => text.push_str("Command"),
398            (PlatformStyle::Mac, true) => text.push_str("cmd"),
399            (PlatformStyle::Linux, false) => text.push_str("Super"),
400            (PlatformStyle::Linux, true) => text.push_str("super"),
401            (PlatformStyle::Windows, false) => text.push_str("Win"),
402            (PlatformStyle::Windows, true) => text.push_str("win"),
403        }
404
405        text.push(delimiter);
406    }
407
408    if keystroke.modifiers.alt {
409        match (platform_style, vim_mode) {
410            (PlatformStyle::Mac, false) => text.push_str("Option"),
411            (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Alt"),
412            (_, true) => text.push_str("alt"),
413        }
414
415        text.push(delimiter);
416    }
417
418    if keystroke.modifiers.shift {
419        match (platform_style, vim_mode) {
420            (_, false) => text.push_str("Shift"),
421            (_, true) => text.push_str("shift"),
422        }
423        text.push(delimiter);
424    }
425
426    if vim_mode {
427        text.push_str(&keystroke.key)
428    } else {
429        let key = match keystroke.key.as_str() {
430            "pageup" => "PageUp",
431            "pagedown" => "PageDown",
432            key => &util::capitalize(key),
433        };
434        text.push_str(key);
435    }
436
437    text
438}
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443
444    #[test]
445    fn test_text_for_keystroke() {
446        assert_eq!(
447            keystroke_text(
448                &Keystroke::parse("cmd-c").unwrap(),
449                PlatformStyle::Mac,
450                false
451            ),
452            "Command-C".to_string()
453        );
454        assert_eq!(
455            keystroke_text(
456                &Keystroke::parse("cmd-c").unwrap(),
457                PlatformStyle::Linux,
458                false
459            ),
460            "Super-C".to_string()
461        );
462        assert_eq!(
463            keystroke_text(
464                &Keystroke::parse("cmd-c").unwrap(),
465                PlatformStyle::Windows,
466                false
467            ),
468            "Win-C".to_string()
469        );
470
471        assert_eq!(
472            keystroke_text(
473                &Keystroke::parse("ctrl-alt-delete").unwrap(),
474                PlatformStyle::Mac,
475                false
476            ),
477            "Control-Option-Delete".to_string()
478        );
479        assert_eq!(
480            keystroke_text(
481                &Keystroke::parse("ctrl-alt-delete").unwrap(),
482                PlatformStyle::Linux,
483                false
484            ),
485            "Ctrl-Alt-Delete".to_string()
486        );
487        assert_eq!(
488            keystroke_text(
489                &Keystroke::parse("ctrl-alt-delete").unwrap(),
490                PlatformStyle::Windows,
491                false
492            ),
493            "Ctrl-Alt-Delete".to_string()
494        );
495
496        assert_eq!(
497            keystroke_text(
498                &Keystroke::parse("shift-pageup").unwrap(),
499                PlatformStyle::Mac,
500                false
501            ),
502            "Shift-PageUp".to_string()
503        );
504        assert_eq!(
505            keystroke_text(
506                &Keystroke::parse("shift-pageup").unwrap(),
507                PlatformStyle::Linux,
508                false,
509            ),
510            "Shift-PageUp".to_string()
511        );
512        assert_eq!(
513            keystroke_text(
514                &Keystroke::parse("shift-pageup").unwrap(),
515                PlatformStyle::Windows,
516                false
517            ),
518            "Shift-PageUp".to_string()
519        );
520    }
521}