gpuicn Components

Component 26 of 60

Kbd

Shows a keyboard key or shortcut with platform modifier names.

site/src/previews/kbd.rs

Starting the preview. GPUI compiles to WebAssembly, so the first load takes a few seconds. This browser has neither WebGPU nor WebGL 2, so the live preview cannot run. The code on this page still works. The preview did not start.

Kbd, running in the browser. The second tab shows the preview's own source, which includes its layout and demo state. For the smallest working example, see Usage below. Download the whole showcase for its imports and helpers.

Installation

After the one-time setup, run the command from your app root. The CLI writes the modules and keeps src/ui/mod.rs in step.

gpuicn add kbd

If you prefer to copy the files by hand, declare them yourself:

// src/ui/mod.rs
pub mod theme;
pub mod kbd;

// src/main.rs
mod ui;

Usage

Put this in a view module and return example(cx) from its render method.

use crate::ui::kbd::*;
use gpui_kit::{App, IntoElement};

fn example(cx: &App) -> impl IntoElement {
    kbd_group([Kbd::shortcut("secondary-k"), Kbd::new("Esc")], cx)
}

API reference

pub fn new(keys: impl Into<SharedString>) -> Self

Shows `keys` as written, for example `"Esc"` or `"⌘K"`.

pub fn keystroke(keystroke: &Keystroke) -> Self

Shows a GPUI keystroke with the platform's modifier names, such as `⌘K` on macOS and `Ctrl+K` elsewhere.

pub fn shortcut(source: impl Into<SharedString>) -> Self

Shows a keystroke written like a key binding, such as `"secondary-k"`, for the platform the app runs on. Text that does not parse is shown as written.

pub fn for_action(action: &dyn Action, window: &Window) -> Option<Self>

Shows the first keystroke bound to `action` in the window, if any.

pub fn format_shortcut(source: &str, os: OperatingSystem) -> SharedString

Formats a key-binding string such as `"secondary-shift-p"` for `os`, or returns it unchanged when it does not parse.

pub fn format_keystroke(keystroke: &Keystroke, os: OperatingSystem) -> SharedString

Formats a keystroke the way `os` shows shortcuts in menus: modifier glyphs such as `⌃⌥⇧⌘K` on macOS, and `Ctrl+Alt+Shift+K` elsewhere.

pub fn kbd_group(keys: impl IntoIterator<Item = Kbd>, cx: &App) -> Div

Lays out several key caps in a row, for sequences such as `Ctrl` `B`.

pub fn key_label(keys: &str, size: gpui_kit::Pixels, color: gpui_kit::Hsla) -> Div

Draws formatted keys such as `"⇧⌘P"` inline, with key symbols as icons, for shortcut hints outside a key cap.

Installed source, 247 lines
//! Nova-styled keyboard shortcut hints.

use gpui_icons::{LucideIcon, lucide};
use gpui_kit::{
    Action, AnyElement, App, AsKeystroke as _, Div, FontWeight, IntoElement, Keystroke,
    OperatingSystem, ParentElement, RenderOnce, SharedString, Styled, Window, div, relative,
};

use super::theme::UiTheme;

/// A noninteractive key cap that shows one key or shortcut.
#[derive(IntoElement)]
pub struct Kbd {
    style: gpui_kit::StyleRefinement,
    keys: Keys,
}

enum Keys {
    Text(SharedString),
    Shortcut(SharedString),
    Keystroke(Keystroke),
}

impl Kbd {
    /// Shows `keys` as written, for example `"Esc"` or `"⌘K"`.
    pub fn new(keys: impl Into<SharedString>) -> Self {
        Self::with(Keys::Text(keys.into()))
    }

    /// Shows a GPUI keystroke with the platform's modifier names, such as `⌘K` on
    /// macOS and `Ctrl+K` elsewhere.
    pub fn keystroke(keystroke: &Keystroke) -> Self {
        Self::with(Keys::Keystroke(keystroke.clone()))
    }

    /// Shows a keystroke written like a key binding, such as `"secondary-k"`, for
    /// the platform the app runs on. Text that does not parse is shown as written.
    pub fn shortcut(source: impl Into<SharedString>) -> Self {
        Self::with(Keys::Shortcut(source.into()))
    }

    /// Shows the first keystroke bound to `action` in the window, if any.
    pub fn for_action(action: &dyn Action, window: &Window) -> Option<Self> {
        let binding = window.highest_precedence_binding_for_action(action)?;
        let keystroke = binding.keystrokes().first()?;
        Some(Self::keystroke(keystroke.as_keystroke()))
    }

    fn with(keys: Keys) -> Self {
        Self {
            style: gpui_kit::StyleRefinement::default(),
            keys,
        }
    }
}

/// Formats a key-binding string such as `"secondary-shift-p"` for `os`, or
/// returns it unchanged when it does not parse.
pub fn format_shortcut(source: &str, os: OperatingSystem) -> SharedString {
    match Keystroke::parse_with_os(source, os) {
        Ok(keystroke) => format_keystroke(&keystroke, os),
        Err(_) => SharedString::from(source.to_string()),
    }
}

/// Formats a keystroke the way `os` shows shortcuts in menus: modifier glyphs
/// such as `⌃⌥⇧⌘K` on macOS, and `Ctrl+Alt+Shift+K` elsewhere.
pub fn format_keystroke(keystroke: &Keystroke, os: OperatingSystem) -> SharedString {
    let mac = matches!(os, OperatingSystem::Mac | OperatingSystem::Ios);
    let modifiers = keystroke.modifiers;
    let mut parts: Vec<&str> = Vec::new();
    for (on, glyph, name) in [
        (modifiers.control, "⌃", "Ctrl"),
        (modifiers.alt, "⌥", "Alt"),
        (modifiers.shift, "⇧", "Shift"),
        (
            modifiers.platform,
            "⌘",
            if os == OperatingSystem::Windows {
                "Win"
            } else {
                "Super"
            },
        ),
    ] {
        if on {
            parts.push(if mac { glyph } else { name });
        }
    }
    let key = keystroke.key.as_str();
    let named = match (key, mac) {
        ("enter", true) => Some("↩"),
        ("escape", true) => Some("⎋"),
        ("backspace" | "delete", true) => Some("⌫"),
        ("tab", true) => Some("⇥"),
        ("up", true) => Some("↑"),
        ("down", true) => Some("↓"),
        ("left", true) => Some("←"),
        ("right", true) => Some("→"),
        ("escape", false) => Some("Esc"),
        ("pageup", _) => Some("Page Up"),
        ("pagedown", _) => Some("Page Down"),
        _ => None,
    };
    let key = match named {
        Some(named) => named.to_string(),
        None => {
            let mut chars = key.chars();
            chars
                .next()
                .map(|first| first.to_uppercase().chain(chars).collect())
                .unwrap_or_default()
        }
    };
    parts.push(&key);
    parts.join(if mac { "" } else { "+" }).into()
}

/// Lays out several key caps in a row, for sequences such as `Ctrl` `B`.
pub fn kbd_group(keys: impl IntoIterator<Item = Kbd>, cx: &App) -> Div {
    let theme = UiTheme::read(cx);
    div()
        .flex()
        .flex_shrink_0()
        .items_center()
        .gap(theme.space(1.))
        .children(keys)
}

impl RenderOnce for Kbd {
    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
        let theme = UiTheme::read(cx);
        let root = div()
            .flex()
            .flex_shrink_0()
            .items_center()
            .justify_center()
            .h(theme.space(5.))
            .min_w(theme.space(5.))
            .px(theme.space(1.))
            .rounded(theme.radius.sm)
            .bg(theme.colors.muted)
            .text_color(theme.colors.muted_foreground)
            .font_family(theme.fonts.body.clone())
            .font_weight(FontWeight::MEDIUM)
            .text_size(theme.text(12.))
            .line_height(relative(1.))
            .whitespace_nowrap()
            .gap(theme.space(0.5))
            .children(key_parts(
                &match self.keys {
                    Keys::Text(text) => text,
                    Keys::Shortcut(source) => format_shortcut(&source, cx.platform_info().os),
                    Keys::Keystroke(keystroke) => {
                        format_keystroke(&keystroke, cx.platform_info().os)
                    }
                },
                theme.text(12.),
                // Icons don't inherit text color, so follow a caller's override.
                self.style
                    .text
                    .color
                    .unwrap_or(theme.colors.muted_foreground.into()),
            ));
        super::theme::apply_style(root, &self.style).into_any_element()
    }
}

/// The icon drawn for a key symbol. UI fonts such as Geist have no glyphs for
/// most of these, and the browser build has no system font to fall back on.
fn key_icon(symbol: char) -> Option<LucideIcon> {
    Some(match symbol {
        '⌘' => LucideIcon::Command,
        '⌥' => LucideIcon::Option,
        '⇧' => LucideIcon::ArrowBigUp,
        '⌃' => LucideIcon::ChevronUp,
        '↩' => LucideIcon::CornerDownLeft,
        '⌫' => LucideIcon::Delete,
        '⇥' => LucideIcon::ArrowRightToLine,
        '↑' => LucideIcon::ArrowUp,
        '↓' => LucideIcon::ArrowDown,
        '←' => LucideIcon::ArrowLeft,
        '→' => LucideIcon::ArrowRight,
        _ => return None,
    })
}

/// Draws formatted keys such as `"⇧⌘P"` inline, with key symbols as icons, for
/// shortcut hints outside a key cap.
pub fn key_label(keys: &str, size: gpui_kit::Pixels, color: gpui_kit::Hsla) -> Div {
    div()
        .flex()
        .items_center()
        .gap(size / 6.)
        .text_color(color)
        .children(key_parts(keys, size, color))
}

/// Splits formatted keys into text runs and symbol icons.
fn key_parts(keys: &str, size: gpui_kit::Pixels, color: gpui_kit::Hsla) -> Vec<AnyElement> {
    let mut parts = Vec::new();
    let mut text = String::new();
    for symbol in keys.chars() {
        if let Some(icon) = key_icon(symbol) {
            if !text.is_empty() {
                parts.push(std::mem::take(&mut text).into_any_element());
            }
            parts.push(lucide(icon).size(size).text_color(color).into_any_element());
        } else if symbol == '⎋' {
            text.push_str("Esc");
        } else {
            text.push(symbol);
        }
    }
    if !text.is_empty() {
        parts.push(text.into_any_element());
    }
    parts
}

impl Styled for Kbd {
    fn style(&mut self) -> &mut gpui_kit::StyleRefinement {
        &mut self.style
    }
}

#[cfg(test)]
mod tests {
    use super::{OperatingSystem, format_shortcut};

    #[test]
    fn shortcuts_follow_platform_conventions() {
        let mac = OperatingSystem::Mac;
        let linux = OperatingSystem::Linux;
        assert_eq!(format_shortcut("secondary-k", mac).as_ref(), "⌘K");
        assert_eq!(format_shortcut("secondary-k", linux).as_ref(), "Ctrl+K");
        assert_eq!(format_shortcut("secondary-shift-p", mac).as_ref(), "⇧⌘P");
        assert_eq!(
            format_shortcut("ctrl-alt-shift-a", linux).as_ref(),
            "Ctrl+Alt+Shift+A"
        );
        assert_eq!(format_shortcut("escape", linux).as_ref(), "Esc");
        assert_eq!(format_shortcut("alt-up", mac).as_ref(), "⌥↑");
        assert_eq!(format_shortcut("secondary-,", linux).as_ref(), "Ctrl+,");
    }
}

Platform notes

Visual reference: shadcn/ui 4.19.0 kbd, Neutral Nova. Implementation: crates/ui/registry/kbd/kbd.rs, using GPUI Kit 0.8.1.

gpuicn draws the Nova key cap: a 20px muted surface with small radius and medium 12px text. kbd_group matches shadcn's KbdGroup spacing.

Unlike the web component, Kbd::shortcut, Kbd::keystroke and Kbd::for_action take GPUI keystrokes and format them for the operating system GPUI reports, including in the browser: modifier glyphs in menu order such as ⇧⌘P on macOS, and Ctrl+Shift+P on Windows and Linux. secondary- resolves to Command on macOS and Control elsewhere, as in key bindings. Kbd::for_action reads the window's keymap, so a hint follows the user's rebinding. Use Kbd::new for literal text.

Kbd is presentational. It takes no focus and has no role; put the action's name in nearby text or in the control the shortcut belongs to.