keybinding.rs

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