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