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