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