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(
101            if keystroke.key.len() > 1 {
102                keystroke.key.clone()
103            } else {
104                keystroke.key.to_uppercase()
105            },
106            color,
107        )
108        .into_any_element(),
109    }
110}
111
112fn icon_for_key(keystroke: &Keystroke, platform_style: PlatformStyle) -> Option<IconName> {
113    match keystroke.key.as_str() {
114        "left" => Some(IconName::ArrowLeft),
115        "right" => Some(IconName::ArrowRight),
116        "up" => Some(IconName::ArrowUp),
117        "down" => Some(IconName::ArrowDown),
118        "backspace" => Some(IconName::Backspace),
119        "delete" => Some(IconName::Delete),
120        "return" => Some(IconName::Return),
121        "enter" => Some(IconName::Return),
122        // "tab" => Some(IconName::Tab),
123        "space" => Some(IconName::Space),
124        "escape" => Some(IconName::Escape),
125        "pagedown" => Some(IconName::PageDown),
126        "pageup" => Some(IconName::PageUp),
127        "shift" if platform_style == PlatformStyle::Mac => Some(IconName::Shift),
128        "control" if platform_style == PlatformStyle::Mac => Some(IconName::Control),
129        "platform" if platform_style == PlatformStyle::Mac => Some(IconName::Command),
130        "function" if platform_style == PlatformStyle::Mac => Some(IconName::Control),
131        "alt" if platform_style == PlatformStyle::Mac => Some(IconName::Option),
132        _ => None,
133    }
134}
135
136pub fn render_modifiers(
137    modifiers: &Modifiers,
138    platform_style: PlatformStyle,
139    color: Option<Color>,
140) -> impl Iterator<Item = AnyElement> {
141    enum KeyOrIcon {
142        Key(&'static str),
143        Icon(IconName),
144    }
145
146    struct Modifier {
147        enabled: bool,
148        mac: KeyOrIcon,
149        linux: KeyOrIcon,
150        windows: KeyOrIcon,
151    }
152
153    let table = {
154        use KeyOrIcon::*;
155
156        [
157            Modifier {
158                enabled: modifiers.function,
159                mac: Icon(IconName::Control),
160                linux: Key("Fn"),
161                windows: Key("Fn"),
162            },
163            Modifier {
164                enabled: modifiers.control,
165                mac: Icon(IconName::Control),
166                linux: Key("Ctrl"),
167                windows: Key("Ctrl"),
168            },
169            Modifier {
170                enabled: modifiers.alt,
171                mac: Icon(IconName::Option),
172                linux: Key("Alt"),
173                windows: Key("Alt"),
174            },
175            Modifier {
176                enabled: modifiers.platform,
177                mac: Icon(IconName::Command),
178                linux: Key("Super"),
179                windows: Key("Win"),
180            },
181            Modifier {
182                enabled: modifiers.shift,
183                mac: Icon(IconName::Shift),
184                linux: Key("Shift"),
185                windows: Key("Shift"),
186            },
187        ]
188    };
189
190    table
191        .into_iter()
192        .flat_map(move |modifier| {
193            if modifier.enabled {
194                match platform_style {
195                    PlatformStyle::Mac => Some(modifier.mac),
196                    PlatformStyle::Linux => Some(modifier.linux)
197                        .into_iter()
198                        .chain(Some(KeyOrIcon::Key("+")))
199                        .next(),
200                    PlatformStyle::Windows => Some(modifier.windows)
201                        .into_iter()
202                        .chain(Some(KeyOrIcon::Key("+")))
203                        .next(),
204                }
205            } else {
206                None
207            }
208        })
209        .map(move |key_or_icon| match key_or_icon {
210            KeyOrIcon::Key(key) => Key::new(key, color).into_any_element(),
211            KeyOrIcon::Icon(icon) => KeyIcon::new(icon, color).into_any_element(),
212        })
213}
214
215#[derive(IntoElement)]
216pub struct Key {
217    key: SharedString,
218    color: Option<Color>,
219}
220
221impl RenderOnce for Key {
222    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
223        let single_char = self.key.len() == 1;
224
225        div()
226            .py_0()
227            .map(|this| {
228                if single_char {
229                    this.w(rems_from_px(14.))
230                        .flex()
231                        .flex_none()
232                        .justify_center()
233                } else {
234                    this.px_0p5()
235                }
236            })
237            .h(rems_from_px(14.))
238            .text_ui(cx)
239            .line_height(relative(1.))
240            .text_color(self.color.unwrap_or(Color::Muted).color(cx))
241            .child(self.key.clone())
242    }
243}
244
245impl Key {
246    pub fn new(key: impl Into<SharedString>, color: Option<Color>) -> Self {
247        Self {
248            key: key.into(),
249            color,
250        }
251    }
252}
253
254#[derive(IntoElement)]
255pub struct KeyIcon {
256    icon: IconName,
257    color: Option<Color>,
258}
259
260impl RenderOnce for KeyIcon {
261    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
262        Icon::new(self.icon)
263            .size(IconSize::XSmall)
264            .color(self.color.unwrap_or(Color::Muted))
265    }
266}
267
268impl KeyIcon {
269    pub fn new(icon: IconName, color: Option<Color>) -> Self {
270        Self { icon, color }
271    }
272}
273
274/// Returns a textual representation of the key binding for the given [`Action`].
275pub fn text_for_action(action: &dyn Action, window: &Window) -> Option<String> {
276    let bindings = window.bindings_for_action(action);
277    let key_binding = bindings.last()?;
278    Some(text_for_key_binding(key_binding, PlatformStyle::platform()))
279}
280
281/// Returns a textual representation of the key binding for the given [`Action`]
282/// as if the provided [`FocusHandle`] was focused.
283pub fn text_for_action_in(
284    action: &dyn Action,
285    focus: &FocusHandle,
286    window: &mut Window,
287) -> Option<String> {
288    let bindings = window.bindings_for_action_in(action, focus);
289    let key_binding = bindings.last()?;
290    Some(text_for_key_binding(key_binding, PlatformStyle::platform()))
291}
292
293/// Returns a textual representation of the given key binding for the specified platform.
294pub fn text_for_key_binding(
295    key_binding: &gpui::KeyBinding,
296    platform_style: PlatformStyle,
297) -> String {
298    key_binding
299        .keystrokes()
300        .iter()
301        .map(|keystroke| text_for_keystroke(keystroke, platform_style))
302        .collect::<Vec<_>>()
303        .join(" ")
304}
305
306/// Returns a textual representation of the given [`Keystroke`].
307pub fn text_for_keystroke(keystroke: &Keystroke, platform_style: PlatformStyle) -> String {
308    let mut text = String::new();
309
310    let delimiter = match platform_style {
311        PlatformStyle::Mac => '-',
312        PlatformStyle::Linux | PlatformStyle::Windows => '+',
313    };
314
315    if keystroke.modifiers.function {
316        match platform_style {
317            PlatformStyle::Mac => text.push_str("fn"),
318            PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Fn"),
319        }
320
321        text.push(delimiter);
322    }
323
324    if keystroke.modifiers.control {
325        match platform_style {
326            PlatformStyle::Mac => text.push_str("Control"),
327            PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Ctrl"),
328        }
329
330        text.push(delimiter);
331    }
332
333    if keystroke.modifiers.alt {
334        match platform_style {
335            PlatformStyle::Mac => text.push_str("Option"),
336            PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Alt"),
337        }
338
339        text.push(delimiter);
340    }
341
342    if keystroke.modifiers.platform {
343        match platform_style {
344            PlatformStyle::Mac => text.push_str("Command"),
345            PlatformStyle::Linux => text.push_str("Super"),
346            PlatformStyle::Windows => text.push_str("Win"),
347        }
348
349        text.push(delimiter);
350    }
351
352    if keystroke.modifiers.shift {
353        match platform_style {
354            PlatformStyle::Mac | PlatformStyle::Linux | PlatformStyle::Windows => {
355                text.push_str("Shift")
356            }
357        }
358
359        text.push(delimiter);
360    }
361
362    fn capitalize(str: &str) -> String {
363        let mut chars = str.chars();
364        match chars.next() {
365            None => String::new(),
366            Some(first_char) => first_char.to_uppercase().collect::<String>() + chars.as_str(),
367        }
368    }
369
370    let key = match keystroke.key.as_str() {
371        "pageup" => "PageUp",
372        "pagedown" => "PageDown",
373        key => &capitalize(key),
374    };
375
376    text.push_str(key);
377
378    text
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384
385    #[test]
386    fn test_text_for_keystroke() {
387        assert_eq!(
388            text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Mac),
389            "Command-C".to_string()
390        );
391        assert_eq!(
392            text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Linux),
393            "Super+C".to_string()
394        );
395        assert_eq!(
396            text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Windows),
397            "Win+C".to_string()
398        );
399
400        assert_eq!(
401            text_for_keystroke(
402                &Keystroke::parse("ctrl-alt-delete").unwrap(),
403                PlatformStyle::Mac
404            ),
405            "Control-Option-Delete".to_string()
406        );
407        assert_eq!(
408            text_for_keystroke(
409                &Keystroke::parse("ctrl-alt-delete").unwrap(),
410                PlatformStyle::Linux
411            ),
412            "Ctrl+Alt+Delete".to_string()
413        );
414        assert_eq!(
415            text_for_keystroke(
416                &Keystroke::parse("ctrl-alt-delete").unwrap(),
417                PlatformStyle::Windows
418            ),
419            "Ctrl+Alt+Delete".to_string()
420        );
421
422        assert_eq!(
423            text_for_keystroke(
424                &Keystroke::parse("shift-pageup").unwrap(),
425                PlatformStyle::Mac
426            ),
427            "Shift-PageUp".to_string()
428        );
429        assert_eq!(
430            text_for_keystroke(
431                &Keystroke::parse("shift-pageup").unwrap(),
432                PlatformStyle::Linux
433            ),
434            "Shift+PageUp".to_string()
435        );
436        assert_eq!(
437            text_for_keystroke(
438                &Keystroke::parse("shift-pageup").unwrap(),
439                PlatformStyle::Windows
440            ),
441            "Shift+PageUp".to_string()
442        );
443    }
444}