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