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