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<Pixels>,
 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: Pixels) -> Self {
 63        self.size = Some(size);
 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<Pixels>,
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<Pixels>,
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<Pixels>,
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.size.unwrap_or(px(14.));
234        let size_f32: f32 = size.into();
235
236        div()
237            .py_0()
238            .map(|this| {
239                if single_char {
240                    this.w(size).flex().flex_none().justify_center()
241                } else {
242                    this.px_0p5()
243                }
244            })
245            .h(rems_from_px(size_f32))
246            .text_size(size)
247            .line_height(relative(1.))
248            .text_color(self.color.unwrap_or(Color::Muted).color(cx))
249            .child(self.key.clone())
250    }
251}
252
253impl Key {
254    pub fn new(key: impl Into<SharedString>, color: Option<Color>) -> Self {
255        Self {
256            key: key.into(),
257            color,
258            size: None,
259        }
260    }
261
262    pub fn size(mut self, size: impl Into<Option<Pixels>>) -> Self {
263        self.size = size.into();
264        self
265    }
266}
267
268#[derive(IntoElement)]
269pub struct KeyIcon {
270    icon: IconName,
271    color: Option<Color>,
272    size: Option<Pixels>,
273}
274
275impl RenderOnce for KeyIcon {
276    fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
277        let size = self
278            .size
279            .unwrap_or(IconSize::Small.rems().to_pixels(window.rem_size()));
280
281        Icon::new(self.icon)
282            .size(IconSize::Custom(size))
283            .color(self.color.unwrap_or(Color::Muted))
284    }
285}
286
287impl KeyIcon {
288    pub fn new(icon: IconName, color: Option<Color>) -> Self {
289        Self {
290            icon,
291            color,
292            size: None,
293        }
294    }
295
296    pub fn size(mut self, size: impl Into<Option<Pixels>>) -> Self {
297        self.size = size.into();
298        self
299    }
300}
301
302/// Returns a textual representation of the key binding for the given [`Action`].
303pub fn text_for_action(action: &dyn Action, window: &Window) -> Option<String> {
304    let bindings = window.bindings_for_action(action);
305    let key_binding = bindings.last()?;
306    Some(text_for_key_binding(key_binding, PlatformStyle::platform()))
307}
308
309/// Returns a textual representation of the key binding for the given [`Action`]
310/// as if the provided [`FocusHandle`] was focused.
311pub fn text_for_action_in(
312    action: &dyn Action,
313    focus: &FocusHandle,
314    window: &mut Window,
315) -> Option<String> {
316    let bindings = window.bindings_for_action_in(action, focus);
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 given key binding for the specified platform.
322pub fn text_for_key_binding(
323    key_binding: &gpui::KeyBinding,
324    platform_style: PlatformStyle,
325) -> String {
326    key_binding
327        .keystrokes()
328        .iter()
329        .map(|keystroke| text_for_keystroke(keystroke, platform_style))
330        .collect::<Vec<_>>()
331        .join(" ")
332}
333
334/// Returns a textual representation of the given [`Keystroke`].
335pub fn text_for_keystroke(keystroke: &Keystroke, platform_style: PlatformStyle) -> String {
336    let mut text = String::new();
337
338    let delimiter = match platform_style {
339        PlatformStyle::Mac => '-',
340        PlatformStyle::Linux | PlatformStyle::Windows => '+',
341    };
342
343    if keystroke.modifiers.function {
344        match platform_style {
345            PlatformStyle::Mac => text.push_str("fn"),
346            PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Fn"),
347        }
348
349        text.push(delimiter);
350    }
351
352    if keystroke.modifiers.control {
353        match platform_style {
354            PlatformStyle::Mac => text.push_str("Control"),
355            PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Ctrl"),
356        }
357
358        text.push(delimiter);
359    }
360
361    if keystroke.modifiers.alt {
362        match platform_style {
363            PlatformStyle::Mac => text.push_str("Option"),
364            PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Alt"),
365        }
366
367        text.push(delimiter);
368    }
369
370    if keystroke.modifiers.platform {
371        match platform_style {
372            PlatformStyle::Mac => text.push_str("Command"),
373            PlatformStyle::Linux => text.push_str("Super"),
374            PlatformStyle::Windows => text.push_str("Win"),
375        }
376
377        text.push(delimiter);
378    }
379
380    if keystroke.modifiers.shift {
381        match platform_style {
382            PlatformStyle::Mac | PlatformStyle::Linux | PlatformStyle::Windows => {
383                text.push_str("Shift")
384            }
385        }
386
387        text.push(delimiter);
388    }
389
390    let key = match keystroke.key.as_str() {
391        "pageup" => "PageUp",
392        "pagedown" => "PageDown",
393        key => &capitalize(key),
394    };
395
396    text.push_str(key);
397
398    text
399}
400
401fn capitalize(str: &str) -> String {
402    let mut chars = str.chars();
403    match chars.next() {
404        None => String::new(),
405        Some(first_char) => first_char.to_uppercase().collect::<String>() + chars.as_str(),
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412
413    #[test]
414    fn test_text_for_keystroke() {
415        assert_eq!(
416            text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Mac),
417            "Command-C".to_string()
418        );
419        assert_eq!(
420            text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Linux),
421            "Super+C".to_string()
422        );
423        assert_eq!(
424            text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Windows),
425            "Win+C".to_string()
426        );
427
428        assert_eq!(
429            text_for_keystroke(
430                &Keystroke::parse("ctrl-alt-delete").unwrap(),
431                PlatformStyle::Mac
432            ),
433            "Control-Option-Delete".to_string()
434        );
435        assert_eq!(
436            text_for_keystroke(
437                &Keystroke::parse("ctrl-alt-delete").unwrap(),
438                PlatformStyle::Linux
439            ),
440            "Ctrl+Alt+Delete".to_string()
441        );
442        assert_eq!(
443            text_for_keystroke(
444                &Keystroke::parse("ctrl-alt-delete").unwrap(),
445                PlatformStyle::Windows
446            ),
447            "Ctrl+Alt+Delete".to_string()
448        );
449
450        assert_eq!(
451            text_for_keystroke(
452                &Keystroke::parse("shift-pageup").unwrap(),
453                PlatformStyle::Mac
454            ),
455            "Shift-PageUp".to_string()
456        );
457        assert_eq!(
458            text_for_keystroke(
459                &Keystroke::parse("shift-pageup").unwrap(),
460                PlatformStyle::Linux
461            ),
462            "Shift+PageUp".to_string()
463        );
464        assert_eq!(
465            text_for_keystroke(
466                &Keystroke::parse("shift-pageup").unwrap(),
467                PlatformStyle::Windows
468            ),
469            "Shift+PageUp".to_string()
470        );
471    }
472}