1use std::collections::HashMap;
10
11use serde::{Deserialize, Serialize};
12use tauri::menu::{Menu, MenuItem, MenuItemKind, PredefinedMenuItem, Submenu, SubmenuBuilder};
13use tauri::{AppHandle, Wry};
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(tag = "kind", rename_all = "kebab-case")]
17pub enum MenuEntry {
18 Item {
19 id: String,
20 label: String,
21 #[serde(default)]
22 accelerator: Option<String>,
23 #[serde(default = "yes")]
24 enabled: bool,
25 },
26 Separator,
27}
28
29fn yes() -> bool {
30 true
31}
32
33impl MenuEntry {
34 pub fn item(id: &str, label: &str) -> Self {
35 MenuEntry::Item {
36 id: id.into(),
37 label: label.into(),
38 accelerator: None,
39 enabled: true,
40 }
41 }
42 pub fn accel(id: &str, label: &str, accelerator: &str) -> Self {
43 MenuEntry::Item {
44 id: id.into(),
45 label: label.into(),
46 accelerator: Some(accelerator.into()),
47 enabled: true,
48 }
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53pub struct MenuGroup {
54 pub label: String,
55 pub entries: Vec<MenuEntry>,
56}
57
58#[derive(Debug, Clone, Default)]
59pub struct MenuSpec {
60 pub app_name: String,
61 pub groups: Vec<MenuGroup>,
63 pub help: Vec<MenuEntry>,
65 pub settings: Option<(String, String)>,
67}
68
69pub struct BuiltMenu {
70 pub menu: Menu<Wry>,
71 pub items: HashMap<String, MenuItem<Wry>>,
72}
73
74fn add_entries(
75 app: &AppHandle<Wry>,
76 sub: &Submenu<Wry>,
77 entries: &[MenuEntry],
78 items: &mut HashMap<String, MenuItem<Wry>>,
79) -> Result<(), String> {
80 for e in entries {
81 match e {
82 MenuEntry::Separator => sub
83 .append(&PredefinedMenuItem::separator(app).map_err(s)?)
84 .map_err(s)?,
85 MenuEntry::Item {
86 id,
87 label,
88 accelerator,
89 enabled,
90 } => {
91 let mi = MenuItem::with_id(app, id, label, *enabled, accelerator.as_deref())
92 .map_err(s)?;
93 sub.append(&mi).map_err(s)?;
94 items.insert(id.clone(), mi);
95 }
96 }
97 }
98 Ok(())
99}
100
101fn s<E: std::fmt::Display>(e: E) -> String {
102 e.to_string()
103}
104
105pub fn build(app: &AppHandle<Wry>, spec: &MenuSpec) -> Result<BuiltMenu, String> {
106 let mut items = HashMap::new();
107 let menu = Menu::new(app).map_err(s)?;
108
109 #[cfg(target_os = "macos")]
110 {
111 let app_menu = SubmenuBuilder::new(app, &spec.app_name)
112 .about(None)
113 .build()
114 .map_err(s)?;
115 if let Some((id, accel)) = &spec.settings {
116 app_menu
117 .append(&PredefinedMenuItem::separator(app).map_err(s)?)
118 .map_err(s)?;
119 add_entries(
120 app,
121 &app_menu,
122 &[MenuEntry::accel(id, "Settings\u{2026}", accel)],
123 &mut items,
124 )?;
125 }
126 app_menu
127 .append(&PredefinedMenuItem::separator(app).map_err(s)?)
128 .map_err(s)?;
129 app_menu
130 .append(&PredefinedMenuItem::services(app, None).map_err(s)?)
131 .map_err(s)?;
132 app_menu
133 .append(&PredefinedMenuItem::separator(app).map_err(s)?)
134 .map_err(s)?;
135 app_menu
136 .append(&PredefinedMenuItem::hide(app, None).map_err(s)?)
137 .map_err(s)?;
138 app_menu
139 .append(&PredefinedMenuItem::hide_others(app, None).map_err(s)?)
140 .map_err(s)?;
141 app_menu
142 .append(&PredefinedMenuItem::show_all(app, None).map_err(s)?)
143 .map_err(s)?;
144 app_menu
145 .append(&PredefinedMenuItem::separator(app).map_err(s)?)
146 .map_err(s)?;
147 app_menu
148 .append(
149 &PredefinedMenuItem::quit(app, Some(&format!("Quit {}", spec.app_name)))
150 .map_err(s)?,
151 )
152 .map_err(s)?;
153 menu.append(&app_menu).map_err(s)?;
154 }
155 #[cfg(not(target_os = "macos"))]
156 {
157 let file = SubmenuBuilder::new(app, "File").build().map_err(s)?;
158 if let Some((id, accel)) = &spec.settings {
159 add_entries(
160 app,
161 &file,
162 &[
163 MenuEntry::accel(id, "Settings\u{2026}", accel),
164 MenuEntry::Separator,
165 ],
166 &mut items,
167 )?;
168 }
169 file.append(&PredefinedMenuItem::close_window(app, None).map_err(s)?)
170 .map_err(s)?;
171 file.append(
172 &PredefinedMenuItem::quit(app, Some(&format!("Exit {}", spec.app_name))).map_err(s)?,
173 )
174 .map_err(s)?;
175 menu.append(&file).map_err(s)?;
176 }
177
178 let edit = SubmenuBuilder::new(app, "Edit")
179 .undo()
180 .redo()
181 .separator()
182 .cut()
183 .copy()
184 .paste()
185 .select_all()
186 .build()
187 .map_err(s)?;
188 menu.append(&edit).map_err(s)?;
189
190 #[cfg(target_os = "macos")]
191 {
192 let view = SubmenuBuilder::new(app, "View")
194 .fullscreen()
195 .build()
196 .map_err(s)?;
197 menu.append(&view).map_err(s)?;
198 }
199
200 for g in &spec.groups {
201 let sub = SubmenuBuilder::new(app, &g.label).build().map_err(s)?;
202 add_entries(app, &sub, &g.entries, &mut items)?;
203 menu.append(&sub).map_err(s)?;
204 }
205
206 let window = SubmenuBuilder::new(app, "Window")
207 .minimize()
208 .maximize()
209 .separator()
210 .close_window()
211 .build()
212 .map_err(s)?;
213 menu.append(&window).map_err(s)?;
214
215 let help = SubmenuBuilder::new(app, "Help").build().map_err(s)?;
216 add_entries(app, &help, &spec.help, &mut items)?;
217 menu.append(&help).map_err(s)?;
218
219 Ok(BuiltMenu { menu, items })
220}
221
222pub fn describe(menu: &Menu<Wry>) -> Vec<(String, String)> {
224 fn walk(kind: &MenuItemKind<Wry>, prefix: &str, out: &mut Vec<(String, String)>) {
225 match kind {
226 MenuItemKind::Submenu(sm) => {
227 let title = sm.text().unwrap_or_default();
228 let path = format!("{prefix}{title}/");
229 out.push((path.clone(), title));
230 for child in sm.items().unwrap_or_default() {
231 walk(&child, &path, out);
232 }
233 }
234 MenuItemKind::MenuItem(mi) => out.push((
235 format!("{prefix}{}", mi.id().as_ref()),
236 mi.text().unwrap_or_default(),
237 )),
238 MenuItemKind::Predefined(p) => out.push((
239 format!("{prefix}(predefined)"),
240 p.text().unwrap_or_default(),
241 )),
242 MenuItemKind::Check(c) => out.push((
243 format!("{prefix}{}", c.id().as_ref()),
244 c.text().unwrap_or_default(),
245 )),
246 MenuItemKind::Icon(i) => out.push((
247 format!("{prefix}{}", i.id().as_ref()),
248 i.text().unwrap_or_default(),
249 )),
250 }
251 }
252 let mut out = Vec::new();
253 for item in menu.items().unwrap_or_default() {
254 walk(&item, "", &mut out);
255 }
256 out
257}