Skip to main content

damascene_core/widgets/
command.rs

1//! Command/menu anatomy — familiar rows for command palettes and menus.
2//!
3//! This does not add new interaction state. It packages the row
4//! conventions that shadcn examples repeat constantly: icon slot, label,
5//! trailing shortcut, menu-item density, and centered inline content.
6
7// Lock in full per-item documentation for this module (issue #73).
8#![warn(missing_docs)]
9
10use std::panic::Location;
11
12use crate::cursor::Cursor;
13use crate::metrics::MetricsRole;
14use crate::style::StyleProfile;
15use crate::tokens;
16use crate::tree::*;
17use crate::widgets::text::{mono, text};
18use crate::{IntoIconSource, icon};
19
20/// Structural column of [`command_item`]s (shadcn's `CommandGroup`).
21#[track_caller]
22pub fn command_group<I, E>(children: I) -> El
23where
24    I: IntoIterator<Item = E>,
25    E: Into<El>,
26{
27    El::new(Kind::Custom("command_group"))
28        .at_loc(Location::caller())
29        .children(children)
30        .axis(Axis::Column)
31        .align(Align::Stretch)
32        .width(Size::Fill(1.0))
33        .height(Size::Hug)
34        .default_gap(0.0)
35}
36
37/// A focusable palette row (shadcn's `CommandItem`) — compose from
38/// [`command_icon`], [`command_label`], and [`command_shortcut`], or
39/// use [`command_row`]. Key it with the action it routes to; clicks
40/// emit `UiEventKind::Click` to that key.
41#[track_caller]
42pub fn command_item<I, E>(children: I) -> El
43where
44    I: IntoIterator<Item = E>,
45    E: Into<El>,
46{
47    El::new(Kind::Custom("command_item"))
48        .at_loc(Location::caller())
49        .style_profile(StyleProfile::Solid)
50        .metrics_role(MetricsRole::MenuItem)
51        .surface_role(SurfaceRole::Raised)
52        .focusable()
53        .cursor(Cursor::Pointer)
54        .children(children)
55        .axis(Axis::Row)
56        .align(Align::Center)
57        .justify(Justify::Start)
58        .fill(tokens::CARD)
59        .default_radius(tokens::RADIUS_SM)
60        .default_padding(Sides::xy(tokens::SPACE_2, 0.0))
61        .default_gap(tokens::SPACE_2)
62        .width(Size::Fill(1.0))
63        .default_height(Size::Fixed(tokens::CONTROL_HEIGHT))
64}
65
66/// Leading icon slot for a [`command_item`] — a small bordered,
67/// muted-fill tile around the icon.
68#[track_caller]
69pub fn command_icon(source: impl IntoIconSource) -> El {
70    El::new(Kind::Custom("command_icon"))
71        .at_loc(Location::caller())
72        .style_profile(StyleProfile::Surface)
73        .child(
74            icon(source)
75                .icon_size(tokens::ICON_XS)
76                .color(tokens::FOREGROUND),
77        )
78        .align(Align::Center)
79        .justify(Justify::Center)
80        .fill(tokens::MUTED)
81        .stroke(tokens::BORDER)
82        .default_radius(tokens::RADIUS_SM)
83        .width(Size::Fixed(24.0))
84        .height(Size::Fixed(24.0))
85}
86
87/// The label text inside a [`command_item`] — fills the row's
88/// remaining width and truncates with an ellipsis.
89#[track_caller]
90pub fn command_label(label: impl Into<String>) -> El {
91    text(label)
92        .at_loc(Location::caller())
93        .label()
94        .font_weight(FontWeight::Regular)
95        .ellipsis()
96        .width(Size::Fill(1.0))
97}
98
99/// Trailing keyboard-shortcut hint (shadcn's `CommandShortcut`) —
100/// muted monospace caption text. Display only; it does not register
101/// the shortcut.
102#[track_caller]
103pub fn command_shortcut(shortcut: impl Into<String>) -> El {
104    mono(shortcut)
105        .at_loc(Location::caller())
106        .caption()
107        .color(tokens::MUTED_FOREGROUND)
108        .width(Size::Hug)
109}
110
111/// Shorthand: a [`command_item`] with icon, label, and shortcut.
112#[track_caller]
113pub fn command_row(
114    source: impl IntoIconSource,
115    label: impl Into<String>,
116    shortcut: impl Into<String>,
117) -> El {
118    command_item([
119        command_icon(source),
120        command_label(label),
121        command_shortcut(shortcut),
122    ])
123    .at_loc(Location::caller())
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn command_item_uses_menu_density_and_centered_row() {
132        let item = command_item([command_label("New branch"), command_shortcut("Ctrl+B")]);
133
134        assert_eq!(item.kind, Kind::Custom("command_item"));
135        assert_eq!(item.metrics_role, Some(MetricsRole::MenuItem));
136        assert_eq!(item.axis, Axis::Row);
137        assert_eq!(item.align, Align::Center);
138        assert_eq!(item.gap, tokens::SPACE_2);
139        assert_eq!(item.width, Size::Fill(1.0));
140        assert!(item.focusable);
141    }
142
143    #[test]
144    fn command_row_builds_icon_label_and_shortcut() {
145        let row = command_row("git-branch", "New branch", "Ctrl+B");
146
147        assert_eq!(row.children.len(), 3);
148        assert_eq!(row.children[0].kind, Kind::Custom("command_icon"));
149        assert_eq!(row.children[0].width, Size::Fixed(24.0));
150        assert_eq!(row.children[1].text.as_deref(), Some("New branch"));
151        assert_eq!(row.children[1].width, Size::Fill(1.0));
152        assert_eq!(row.children[2].text.as_deref(), Some("Ctrl+B"));
153        assert_eq!(row.children[2].text_role, TextRole::Caption);
154    }
155}