ui: Extend `KeyBinding` with support for displaying keybindings for other platforms (#9192)

Marshall Bowers created

This PR extends the `KeyBinding` component with support for displaying
keybindings for platforms other than macOS.

<img width="824" alt="Screenshot 2024-03-11 at 2 41 59 PM"
src="https://github.com/zed-industries/zed/assets/1486634/7108b17d-dfc3-42ee-9bfd-c58b334d7374">

Release Notes:

- N/A

Change summary

crates/ui/src/components/keybinding.rs         | 122 ++++++++++++++-----
crates/ui/src/components/stories/keybinding.rs |  74 +++++++----
2 files changed, 133 insertions(+), 63 deletions(-)

Detailed changes

crates/ui/src/components/keybinding.rs 🔗

@@ -1,6 +1,30 @@
 use crate::{h_flex, prelude::*, Icon, IconName, IconSize};
 use gpui::{relative, rems, Action, FocusHandle, IntoElement, Keystroke};
 
+/// The way a [`KeyBinding`] should be displayed.
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub enum KeyBindingDisplay {
+    /// Display in macOS style.
+    Mac,
+    /// Display in Linux style.
+    Linux,
+    /// Display in Windows style.
+    Windows,
+}
+
+impl KeyBindingDisplay {
+    /// Returns the [`KeyBindingDisplay`] for the current platform.
+    pub const fn platform() -> Self {
+        if cfg!(target_os = "linux") {
+            KeyBindingDisplay::Linux
+        } else if cfg!(target_os = "windows") {
+            KeyBindingDisplay::Windows
+        } else {
+            KeyBindingDisplay::Mac
+        }
+    }
+}
+
 #[derive(IntoElement, Clone)]
 pub struct KeyBinding {
     /// A keybinding consists of a key and a set of modifier keys.
@@ -8,41 +32,9 @@ pub struct KeyBinding {
     ///
     /// This should always contain at least one element.
     key_binding: gpui::KeyBinding,
-}
-
-impl RenderOnce for KeyBinding {
-    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
-        h_flex()
-            .flex_none()
-            .gap_2()
-            .children(self.key_binding.keystrokes().iter().map(|keystroke| {
-                let key_icon = Self::icon_for_key(keystroke);
 
-                h_flex()
-                    .flex_none()
-                    .gap_0p5()
-                    .p_0p5()
-                    .rounded_sm()
-                    .text_color(cx.theme().colors().text_muted)
-                    .when(keystroke.modifiers.function, |el| el.child(Key::new("fn")))
-                    .when(keystroke.modifiers.control, |el| {
-                        el.child(KeyIcon::new(IconName::Control))
-                    })
-                    .when(keystroke.modifiers.alt, |el| {
-                        el.child(KeyIcon::new(IconName::Option))
-                    })
-                    .when(keystroke.modifiers.command, |el| {
-                        el.child(KeyIcon::new(IconName::Command))
-                    })
-                    .when(keystroke.modifiers.shift, |el| {
-                        el.child(KeyIcon::new(IconName::Shift))
-                    })
-                    .when_some(key_icon, |el, icon| el.child(KeyIcon::new(icon)))
-                    .when(key_icon.is_none(), |el| {
-                        el.child(Key::new(keystroke.key.to_uppercase().clone()))
-                    })
-            }))
-    }
+    /// How keybindings should be displayed.
+    display: KeyBindingDisplay,
 }
 
 impl KeyBinding {
@@ -82,7 +74,67 @@ impl KeyBinding {
     }
 
     pub fn new(key_binding: gpui::KeyBinding) -> Self {
-        Self { key_binding }
+        Self {
+            key_binding,
+            display: KeyBindingDisplay::platform(),
+        }
+    }
+
+    /// Sets how this [`KeyBinding`] should be displayed.
+    pub fn display(mut self, display: KeyBindingDisplay) -> Self {
+        self.display = display;
+        self
+    }
+}
+
+impl RenderOnce for KeyBinding {
+    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+        h_flex()
+            .flex_none()
+            .gap_2()
+            .children(self.key_binding.keystrokes().iter().map(|keystroke| {
+                let key_icon = Self::icon_for_key(keystroke);
+
+                h_flex()
+                    .flex_none()
+                    .gap_0p5()
+                    .p_0p5()
+                    .rounded_sm()
+                    .text_color(cx.theme().colors().text_muted)
+                    .when(keystroke.modifiers.function, |el| match self.display {
+                        KeyBindingDisplay::Mac => el.child(Key::new("fn")),
+                        KeyBindingDisplay::Linux | KeyBindingDisplay::Windows => {
+                            el.child(Key::new("Fn"))
+                        }
+                    })
+                    .when(keystroke.modifiers.control, |el| match self.display {
+                        KeyBindingDisplay::Mac => el.child(KeyIcon::new(IconName::Control)),
+                        KeyBindingDisplay::Linux | KeyBindingDisplay::Windows => {
+                            el.child(Key::new("Ctrl"))
+                        }
+                    })
+                    .when(keystroke.modifiers.alt, |el| match self.display {
+                        KeyBindingDisplay::Mac => el.child(KeyIcon::new(IconName::Option)),
+                        KeyBindingDisplay::Linux | KeyBindingDisplay::Windows => {
+                            el.child(Key::new("Alt"))
+                        }
+                    })
+                    .when(keystroke.modifiers.command, |el| match self.display {
+                        KeyBindingDisplay::Mac => el.child(KeyIcon::new(IconName::Command)),
+                        KeyBindingDisplay::Linux => el.child(Key::new("Super")),
+                        KeyBindingDisplay::Windows => el.child(Key::new("Win")),
+                    })
+                    .when(keystroke.modifiers.shift, |el| match self.display {
+                        KeyBindingDisplay::Mac => el.child(KeyIcon::new(IconName::Option)),
+                        KeyBindingDisplay::Linux | KeyBindingDisplay::Windows => {
+                            el.child(Key::new("Shift"))
+                        }
+                    })
+                    .map(|el| match key_icon {
+                        Some(icon) => el.child(KeyIcon::new(icon)),
+                        None => el.child(Key::new(keystroke.key.to_uppercase())),
+                    })
+            }))
     }
 }
 

crates/ui/src/components/stories/keybinding.rs 🔗

@@ -1,10 +1,9 @@
 use gpui::NoAction;
 use gpui::Render;
 use itertools::Itertools;
-use story::Story;
+use story::{Story, StoryContainer};
 
-use crate::prelude::*;
-use crate::KeyBinding;
+use crate::{prelude::*, KeyBinding, KeyBindingDisplay};
 
 pub struct KeybindingStory;
 
@@ -16,23 +15,28 @@ impl Render for KeybindingStory {
     fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
         let all_modifier_permutations = ["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2);
 
-        Story::container()
-            .child(Story::title_for::<KeyBinding>())
-            .child(Story::label("Single Key"))
-            .child(KeyBinding::new(binding("Z")))
-            .child(Story::label("Single Key with Modifier"))
-            .child(
-                div()
-                    .flex()
-                    .gap_3()
-                    .child(KeyBinding::new(binding("ctrl-c")))
-                    .child(KeyBinding::new(binding("alt-c")))
-                    .child(KeyBinding::new(binding("cmd-c")))
-                    .child(KeyBinding::new(binding("shift-c"))),
-            )
-            .child(Story::label("Single Key with Modifier (Permuted)"))
-            .child(
-                div().flex().flex_col().children(
+        StoryContainer::new(
+            "KeyBinding",
+            "crates/ui/src/components/stories/keybinding.rs",
+        )
+        .child(Story::label("Single Key"))
+        .child(KeyBinding::new(binding("Z")))
+        .child(Story::label("Single Key with Modifier"))
+        .child(
+            div()
+                .flex()
+                .gap_3()
+                .child(KeyBinding::new(binding("ctrl-c")))
+                .child(KeyBinding::new(binding("alt-c")))
+                .child(KeyBinding::new(binding("cmd-c")))
+                .child(KeyBinding::new(binding("shift-c"))),
+        )
+        .child(Story::label("Single Key with Modifier (Permuted)"))
+        .child(
+            div()
+                .flex()
+                .flex_col()
+                .children(
                     all_modifier_permutations
                         .chunks(4)
                         .into_iter()
@@ -46,13 +50,27 @@ impl Render for KeybindingStory {
                                 }))
                         }),
                 ),
-            )
-            .child(Story::label("Single Key with All Modifiers"))
-            .child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z")))
-            .child(Story::label("Chord"))
-            .child(KeyBinding::new(binding("a z")))
-            .child(Story::label("Chord with Modifier"))
-            .child(KeyBinding::new(binding("ctrl-a shift-z")))
-            .child(KeyBinding::new(binding("fn-s")))
+        )
+        .child(Story::label("Single Key with All Modifiers"))
+        .child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z")))
+        .child(Story::label("Chord"))
+        .child(KeyBinding::new(binding("a z")))
+        .child(Story::label("Chord with Modifier"))
+        .child(KeyBinding::new(binding("ctrl-a shift-z")))
+        .child(KeyBinding::new(binding("fn-s")))
+        .child(Story::label("Single Key with All Modifiers (Linux)"))
+        .child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z")).display(KeyBindingDisplay::Linux))
+        .child(Story::label("Chord (Linux)"))
+        .child(KeyBinding::new(binding("a z")).display(KeyBindingDisplay::Linux))
+        .child(Story::label("Chord with Modifier (Linux)"))
+        .child(KeyBinding::new(binding("ctrl-a shift-z")).display(KeyBindingDisplay::Linux))
+        .child(KeyBinding::new(binding("fn-s")).display(KeyBindingDisplay::Linux))
+        .child(Story::label("Single Key with All Modifiers (Windows)"))
+        .child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z")).display(KeyBindingDisplay::Windows))
+        .child(Story::label("Chord (Windows)"))
+        .child(KeyBinding::new(binding("a z")).display(KeyBindingDisplay::Windows))
+        .child(Story::label("Chord with Modifier (Windows)"))
+        .child(KeyBinding::new(binding("ctrl-a shift-z")).display(KeyBindingDisplay::Windows))
+        .child(KeyBinding::new(binding("fn-s")).display(KeyBindingDisplay::Windows))
     }
 }