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