Skip to main content

maud_ui/primitives/
menu.rs

1//! Menu component — dropdown menu for actions
2
3use maud::{html, Markup};
4
5/// A single menu item
6#[derive(Debug, Clone)]
7pub struct MenuItem {
8    pub label: String,
9    pub action: String,
10    pub disabled: bool,
11    pub destructive: bool,
12    /// Optional keyboard shortcut displayed on the right (e.g. "⌘Z")
13    pub shortcut: Option<String>,
14}
15
16/// Menu entry: item, separator, or label
17#[derive(Debug, Clone)]
18pub enum MenuEntry {
19    Item(MenuItem),
20    Separator,
21    Label(String),
22}
23
24/// Menu rendering properties
25#[derive(Debug, Clone)]
26pub struct Props {
27    /// Text displayed on the trigger button
28    pub trigger_label: String,
29    /// Unique identifier for the menu
30    pub id: String,
31    /// Menu entries (items, separators, and labels)
32    pub items: Vec<MenuEntry>,
33}
34
35/// Render a dropdown menu with the given properties
36pub fn render(props: Props) -> Markup {
37    html! {
38        div.mui-menu data-mui="menu" {
39            button.mui-menu__trigger.mui-btn.mui-btn--default.mui-btn--md
40                type="button"
41                aria-expanded="false"
42                aria-haspopup="menu"
43                aria-controls=(format!("{}-items", props.id))
44            {
45                (props.trigger_label)
46                span.mui-menu__chevron aria-hidden="true" { "▾" }
47            }
48            div.mui-menu__content id=(format!("{}-items", props.id)) role="menu" hidden {
49                @for entry in &props.items {
50                    (render_entry(entry))
51                }
52            }
53        }
54    }
55}
56
57/// Render a single menu entry (shared between menu and context menu)
58pub fn render_entry(entry: &MenuEntry) -> Markup {
59    html! {
60        @match entry {
61            MenuEntry::Item(item) => {
62                @let cls = if item.destructive { "mui-menu__item mui-menu__item--danger" } else { "mui-menu__item" };
63                button
64                    type="button"
65                    role="menuitem"
66                    class=(cls)
67                    data-action=(item.action)
68                    tabindex="-1"
69                    disabled[item.disabled]
70                {
71                    (item.label.clone())
72                    @if let Some(shortcut) = &item.shortcut {
73                        span.mui-menu__shortcut { (shortcut) }
74                    }
75                }
76            }
77            MenuEntry::Separator => {
78                div.mui-menu__separator role="separator" {}
79            }
80            MenuEntry::Label(text) => {
81                div.mui-menu__label { (text) }
82            }
83        }
84    }
85}
86
87/// Showcase menu component
88pub fn showcase() -> Markup {
89    html! {
90        div.mui-showcase__grid {
91            div {
92                p.mui-showcase__caption { "File menu" }
93                div.mui-showcase__row {
94                    (render(Props {
95                        trigger_label: "File".into(),
96                        id: "demo-menu-file".into(),
97                        items: vec![
98                            MenuEntry::Item(MenuItem {
99                                label: "New".into(),
100                                action: "new".into(),
101                                disabled: false,
102                                destructive: false,
103                                shortcut: Some("\u{2318}N".into()),
104                            }),
105                            MenuEntry::Item(MenuItem {
106                                label: "Open\u{2026}".into(),
107                                action: "open".into(),
108                                disabled: false,
109                                destructive: false,
110                                shortcut: Some("\u{2318}O".into()),
111                            }),
112                            MenuEntry::Separator,
113                            MenuEntry::Item(MenuItem {
114                                label: "Save".into(),
115                                action: "save".into(),
116                                disabled: false,
117                                destructive: false,
118                                shortcut: Some("\u{2318}S".into()),
119                            }),
120                            MenuEntry::Item(MenuItem {
121                                label: "Save As\u{2026}".into(),
122                                action: "save-as".into(),
123                                disabled: false,
124                                destructive: false,
125                                shortcut: Some("\u{21e7}\u{2318}S".into()),
126                            }),
127                            MenuEntry::Separator,
128                            MenuEntry::Item(MenuItem {
129                                label: "Print\u{2026}".into(),
130                                action: "print".into(),
131                                disabled: false,
132                                destructive: false,
133                                shortcut: Some("\u{2318}P".into()),
134                            }),
135                            MenuEntry::Separator,
136                            MenuEntry::Item(MenuItem {
137                                label: "Exit".into(),
138                                action: "exit".into(),
139                                disabled: false,
140                                destructive: false,
141                                shortcut: None,
142                            }),
143                        ],
144                    }))
145                }
146            }
147            div {
148                p.mui-showcase__caption { "User menu" }
149                div.mui-showcase__row {
150                    (render(Props {
151                        trigger_label: "My Account".into(),
152                        id: "demo-menu-user".into(),
153                        items: vec![
154                            MenuEntry::Label("Account".into()),
155                            MenuEntry::Item(MenuItem {
156                                label: "Profile".into(),
157                                action: "profile".into(),
158                                disabled: false,
159                                destructive: false,
160                                shortcut: None,
161                            }),
162                            MenuEntry::Item(MenuItem {
163                                label: "Settings".into(),
164                                action: "settings".into(),
165                                disabled: false,
166                                destructive: false,
167                                shortcut: None,
168                            }),
169                            MenuEntry::Separator,
170                            MenuEntry::Item(MenuItem {
171                                label: "Sign out".into(),
172                                action: "sign-out".into(),
173                                disabled: false,
174                                destructive: true,
175                                shortcut: None,
176                            }),
177                        ],
178                    }))
179                }
180            }
181        }
182    }
183}