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