Skip to main content

armas_basic/components/
kbd.rs

1//! Kbd Component (shadcn/ui style)
2//!
3//! Keyboard shortcut display element.
4
5use crate::ext::ArmasContextExt;
6use crate::theme::Theme;
7use egui::{Response, Ui, Vec2};
8
9/// Keyboard shortcut display component
10///
11/// # Example
12///
13/// ```rust,no_run
14/// # use egui::Ui;
15/// # fn example(ui: &mut Ui) {
16/// use armas_basic::Kbd;
17///
18/// // Single key
19/// Kbd::new("K").show(ui);
20///
21/// // Key combination (auto-splits on +)
22/// Kbd::new("Ctrl+K").show(ui);
23/// # }
24/// ```
25pub struct Kbd {
26    text: String,
27}
28
29impl Kbd {
30    /// Create a new Kbd with the given key text
31    pub fn new(text: impl Into<String>) -> Self {
32        Self { text: text.into() }
33    }
34
35    /// Show the keyboard shortcut
36    pub fn show(self, ui: &mut Ui) -> Response {
37        let theme = ui.ctx().armas_theme();
38        // Check if this is a key combination
39        let parts: Vec<&str> = self.text.split('+').map(str::trim).collect();
40
41        if parts.len() > 1 {
42            // Multiple keys - render as group with a small gap, no '+' separator.
43            let response = ui.horizontal(|ui| {
44                ui.spacing_mut().item_spacing.x = 2.0;
45                for part in &parts {
46                    render_key(ui, part, &theme);
47                }
48            });
49            response.response
50        } else {
51            // Single key
52            render_key(ui, &self.text, &theme)
53        }
54    }
55}
56
57/// Map common key names to compact glyphs (Mac-style modifier symbols, arrow
58/// glyphs, etc.). Keeps single-char and unknown labels as-is.
59fn key_glyph(text: &str) -> &str {
60    match text {
61        "Cmd" | "Command" | "Meta" | "Super" | "Win" => "⌘",
62        "Shift" => "⇧",
63        "Alt" | "Option" | "Opt" => "⌥",
64        "Ctrl" | "Control" => "⌃",
65        "Enter" | "Return" => "⏎",
66        "Backspace" => "⌫",
67        "Delete" | "Del" => "⌦",
68        "Escape" | "Esc" => "⎋",
69        "Tab" => "⇥",
70        "CapsLock" => "⇪",
71        "Up" | "ArrowUp" => "↑",
72        "Down" | "ArrowDown" => "↓",
73        "Left" | "ArrowLeft" => "←",
74        "Right" | "ArrowRight" => "→",
75        "PageUp" => "⇞",
76        "PageDown" => "⇟",
77        "Home" => "↖",
78        "End" => "↘",
79        "Space" => "␣",
80        other => other,
81    }
82}
83
84fn render_key(ui: &mut Ui, text: &str, theme: &Theme) -> Response {
85    let display = key_glyph(text);
86    let font_size = theme.typography.sm;
87    let font_id = egui::FontId::proportional(font_size);
88    let text_color = theme.muted_foreground();
89    let bg_color = theme.muted();
90
91    // Calculate text size
92    let galley = ui
93        .painter()
94        .layout_no_wrap(display.to_string(), font_id, text_color);
95
96    let text_size = galley.size();
97    let padding_x = 6.0;
98    let padding_y = 3.0;
99    let min_width = 20.0;
100    let height = font_size + padding_y * 2.0;
101
102    let size = Vec2::new((text_size.x + padding_x * 2.0).max(min_width), height);
103
104    let (rect, response) = ui.allocate_exact_size(size, egui::Sense::hover());
105
106    if ui.is_rect_visible(rect) {
107        let rounding = 4.0;
108
109        // Background
110        ui.painter().rect_filled(rect, rounding, bg_color);
111
112        // Border for depth
113        ui.painter().rect_stroke(
114            rect,
115            rounding,
116            egui::Stroke::new(1.0, theme.border()),
117            egui::StrokeKind::Inside,
118        );
119
120        // Text centered
121        ui.painter()
122            .galley(rect.center() - text_size / 2.0, galley, text_color);
123    }
124
125    response
126}