keybinding.rs

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