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