keybinding.rs

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