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