ui: Add functions for generating textual representations of key bindings (#15287)

Marshall Bowers created

This PR adds some helper functions in the `ui` crate that can be used to
get textural representations of keystrokes or key bindings.

Release Notes:

- N/A

Change summary

crates/gpui/src/platform/keystroke.rs                  |  39 +-
crates/quick_action_bar/src/quick_action_bar.rs        |   2 
crates/quick_action_bar/src/toggle_markdown_preview.rs |  16 
crates/ui/src/key_bindings.rs                          | 170 ++++++++++++
crates/ui/src/ui.rs                                    |   2 
5 files changed, 206 insertions(+), 23 deletions(-)

Detailed changes

crates/gpui/src/platform/keystroke.rs 🔗

@@ -237,14 +237,15 @@ pub struct Modifiers {
 }
 
 impl Modifiers {
-    /// Returns true if any modifier key is pressed
+    /// Returns whether any modifier key is pressed.
     pub fn modified(&self) -> bool {
         self.control || self.alt || self.shift || self.platform || self.function
     }
 
-    /// Whether the semantically 'secondary' modifier key is pressed
-    /// On macos, this is the command key
-    /// On windows and linux, this is the control key
+    /// Whether the semantically 'secondary' modifier key is pressed.
+    ///
+    /// On macOS, this is the command key.
+    /// On Linux and Windows, this is the control key.
     pub fn secondary(&self) -> bool {
         #[cfg(target_os = "macos")]
         {
@@ -257,7 +258,7 @@ impl Modifiers {
         }
     }
 
-    /// How many modifier keys are pressed
+    /// Returns how many modifier keys are pressed.
     pub fn number_of_modifiers(&self) -> u8 {
         self.control as u8
             + self.alt as u8
@@ -266,12 +267,12 @@ impl Modifiers {
             + self.function as u8
     }
 
-    /// helper method for Modifiers with no modifiers
+    /// Returns [`Modifiers`] with no modifiers.
     pub fn none() -> Modifiers {
         Default::default()
     }
 
-    /// helper method for Modifiers with just the command key
+    /// Returns [`Modifiers`] with just the command key.
     pub fn command() -> Modifiers {
         Modifiers {
             platform: true,
@@ -279,7 +280,7 @@ impl Modifiers {
         }
     }
 
-    /// A helper method for Modifiers with just the secondary key pressed
+    /// A Returns [`Modifiers`] with just the secondary key pressed.
     pub fn secondary_key() -> Modifiers {
         #[cfg(target_os = "macos")]
         {
@@ -298,7 +299,7 @@ impl Modifiers {
         }
     }
 
-    /// helper method for Modifiers with just the windows key
+    /// Returns [`Modifiers`] with just the windows key.
     pub fn windows() -> Modifiers {
         Modifiers {
             platform: true,
@@ -306,7 +307,7 @@ impl Modifiers {
         }
     }
 
-    /// helper method for Modifiers with just the super key
+    /// Returns [`Modifiers`] with just the super key.
     pub fn super_key() -> Modifiers {
         Modifiers {
             platform: true,
@@ -314,7 +315,7 @@ impl Modifiers {
         }
     }
 
-    /// helper method for Modifiers with just control
+    /// Returns [`Modifiers`] with just control.
     pub fn control() -> Modifiers {
         Modifiers {
             control: true,
@@ -322,7 +323,15 @@ impl Modifiers {
         }
     }
 
-    /// helper method for Modifiers with just shift
+    /// Returns [`Modifiers`] with just control.
+    pub fn alt() -> Modifiers {
+        Modifiers {
+            alt: true,
+            ..Default::default()
+        }
+    }
+
+    /// Returns [`Modifiers`] with just shift.
     pub fn shift() -> Modifiers {
         Modifiers {
             shift: true,
@@ -330,7 +339,7 @@ impl Modifiers {
         }
     }
 
-    /// helper method for Modifiers with command + shift
+    /// Returns [`Modifiers`] with command + shift.
     pub fn command_shift() -> Modifiers {
         Modifiers {
             shift: true,
@@ -339,7 +348,7 @@ impl Modifiers {
         }
     }
 
-    /// helper method for Modifiers with command + shift
+    /// Returns [`Modifiers`] with command + shift.
     pub fn control_shift() -> Modifiers {
         Modifiers {
             shift: true,
@@ -348,7 +357,7 @@ impl Modifiers {
         }
     }
 
-    /// Checks if this Modifiers is a subset of another Modifiers
+    /// Checks if this [`Modifiers`] is a subset of another [`Modifiers`].
     pub fn is_subset_of(&self, other: &Modifiers) -> bool {
         (other.control || !self.control)
             && (other.alt || !self.alt)

crates/quick_action_bar/src/quick_action_bar.rs 🔗

@@ -27,7 +27,6 @@ pub struct QuickActionBar {
     _inlay_hints_enabled_subscription: Option<Subscription>,
     active_item: Option<Box<dyn ItemHandle>>,
     buffer_search_bar: View<BufferSearchBar>,
-    platform_style: PlatformStyle,
     repl_menu: Option<View<ContextMenu>>,
     show: bool,
     toggle_selections_menu: Option<View<ContextMenu>>,
@@ -45,7 +44,6 @@ impl QuickActionBar {
             _inlay_hints_enabled_subscription: None,
             active_item: None,
             buffer_search_bar,
-            platform_style: PlatformStyle::platform(),
             repl_menu: None,
             show: true,
             toggle_selections_menu: None,

crates/quick_action_bar/src/toggle_markdown_preview.rs 🔗

@@ -1,8 +1,8 @@
-use gpui::{AnyElement, WeakView};
+use gpui::{AnyElement, Modifiers, WeakView};
 use markdown_preview::{
     markdown_preview_view::MarkdownPreviewView, OpenPreview, OpenPreviewToTheSide,
 };
-use ui::{prelude::*, IconButtonShape, Tooltip};
+use ui::{prelude::*, text_for_keystroke, IconButtonShape, Tooltip};
 use workspace::Workspace;
 
 use crate::QuickActionBar;
@@ -27,9 +27,10 @@ impl QuickActionBar {
             return None;
         }
 
-        let tooltip_meta = match self.platform_style {
-            PlatformStyle::Mac => "Option+Click to open in a split",
-            _ => "Alt+Click to open in a split",
+        let alt_click = gpui::Keystroke {
+            key: "click".into(),
+            modifiers: Modifiers::alt(),
+            ..Default::default()
         };
 
         let button = IconButton::new("toggle-markdown-preview", IconName::Eye)
@@ -40,7 +41,10 @@ impl QuickActionBar {
                 Tooltip::with_meta(
                     "Preview Markdown",
                     Some(&markdown_preview::OpenPreview),
-                    tooltip_meta,
+                    format!(
+                        "{} to open in a split",
+                        text_for_keystroke(&alt_click, PlatformStyle::platform())
+                    ),
                     cx,
                 )
             })

crates/ui/src/key_bindings.rs 🔗

@@ -0,0 +1,170 @@
+use gpui::{Action, FocusHandle, KeyBinding, Keystroke, WindowContext};
+
+use crate::PlatformStyle;
+
+/// Returns a textual representation of the key binding for the given [`Action`].
+pub fn text_for_action(action: &dyn Action, cx: &mut WindowContext) -> Option<String> {
+    let key_binding = cx.bindings_for_action(action).last().cloned()?;
+    Some(text_for_key_binding(key_binding, PlatformStyle::platform()))
+}
+
+/// Returns a textual representation of the key binding for the given [`Action`]
+/// as if the provided [`FocusHandle`] was focused.
+pub fn text_for_action_in(
+    action: &dyn Action,
+    focus: &FocusHandle,
+    cx: &mut WindowContext,
+) -> Option<String> {
+    let key_binding = cx.bindings_for_action_in(action, focus).last().cloned()?;
+    Some(text_for_key_binding(key_binding, PlatformStyle::platform()))
+}
+
+/// Returns a textual representation of the given key binding for the specified platform.
+pub fn text_for_key_binding(key_binding: KeyBinding, platform_style: PlatformStyle) -> String {
+    key_binding
+        .keystrokes()
+        .into_iter()
+        .map(|keystroke| text_for_keystroke(keystroke, platform_style))
+        .collect::<Vec<_>>()
+        .join(" ")
+}
+
+/// Returns a textual representation of the given [`Keystroke`].
+pub fn text_for_keystroke(keystroke: &Keystroke, platform_style: PlatformStyle) -> String {
+    let mut text = String::new();
+
+    let delimiter = match platform_style {
+        PlatformStyle::Mac => '-',
+        PlatformStyle::Linux | PlatformStyle::Windows => '+',
+    };
+
+    if keystroke.modifiers.function {
+        match platform_style {
+            PlatformStyle::Mac => text.push_str("fn"),
+            PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Fn"),
+        }
+
+        text.push(delimiter);
+    }
+
+    if keystroke.modifiers.control {
+        match platform_style {
+            PlatformStyle::Mac => text.push_str("Control"),
+            PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Ctrl"),
+        }
+
+        text.push(delimiter);
+    }
+
+    if keystroke.modifiers.alt {
+        match platform_style {
+            PlatformStyle::Mac => text.push_str("Option"),
+            PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Alt"),
+        }
+
+        text.push(delimiter);
+    }
+
+    if keystroke.modifiers.platform {
+        match platform_style {
+            PlatformStyle::Mac => text.push_str("Command"),
+            PlatformStyle::Linux => text.push_str("Super"),
+            PlatformStyle::Windows => text.push_str("Win"),
+        }
+
+        text.push(delimiter);
+    }
+
+    if keystroke.modifiers.shift {
+        match platform_style {
+            PlatformStyle::Mac | PlatformStyle::Linux | PlatformStyle::Windows => {
+                text.push_str("Shift")
+            }
+        }
+
+        text.push(delimiter);
+    }
+
+    fn capitalize(str: &str) -> String {
+        let mut chars = str.chars();
+        match chars.next() {
+            None => String::new(),
+            Some(first_char) => first_char.to_uppercase().collect::<String>() + chars.as_str(),
+        }
+    }
+
+    let key = match keystroke.key.as_str() {
+        "pageup" => "PageUp",
+        "pagedown" => "PageDown",
+        key => &capitalize(key),
+    };
+
+    text.push_str(key);
+
+    text
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_text_for_keystroke() {
+        assert_eq!(
+            text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Mac),
+            "Command-C".to_string()
+        );
+        assert_eq!(
+            text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Linux),
+            "Super+C".to_string()
+        );
+        assert_eq!(
+            text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Windows),
+            "Win+C".to_string()
+        );
+
+        assert_eq!(
+            text_for_keystroke(
+                &Keystroke::parse("ctrl-alt-delete").unwrap(),
+                PlatformStyle::Mac
+            ),
+            "Control-Option-Delete".to_string()
+        );
+        assert_eq!(
+            text_for_keystroke(
+                &Keystroke::parse("ctrl-alt-delete").unwrap(),
+                PlatformStyle::Linux
+            ),
+            "Ctrl+Alt+Delete".to_string()
+        );
+        assert_eq!(
+            text_for_keystroke(
+                &Keystroke::parse("ctrl-alt-delete").unwrap(),
+                PlatformStyle::Windows
+            ),
+            "Ctrl+Alt+Delete".to_string()
+        );
+
+        assert_eq!(
+            text_for_keystroke(
+                &Keystroke::parse("shift-pageup").unwrap(),
+                PlatformStyle::Mac
+            ),
+            "Shift-PageUp".to_string()
+        );
+        assert_eq!(
+            text_for_keystroke(
+                &Keystroke::parse("shift-pageup").unwrap(),
+                PlatformStyle::Linux
+            ),
+            "Shift+PageUp".to_string()
+        );
+        assert_eq!(
+            text_for_keystroke(
+                &Keystroke::parse("shift-pageup").unwrap(),
+                PlatformStyle::Windows
+            ),
+            "Shift+PageUp".to_string()
+        );
+    }
+}

crates/ui/src/ui.rs 🔗

@@ -7,6 +7,7 @@ mod clickable;
 mod components;
 mod disableable;
 mod fixed;
+mod key_bindings;
 pub mod prelude;
 mod selectable;
 mod styled_ext;
@@ -19,6 +20,7 @@ pub use clickable::*;
 pub use components::*;
 pub use disableable::*;
 pub use fixed::*;
+pub use key_bindings::*;
 pub use prelude::*;
 pub use styled_ext::*;
 pub use styles::*;