1use muda::{
9 accelerator::{Accelerator, Code, Modifiers},
10 Menu, MenuItem, PredefinedMenuItem, Submenu,
11};
12use std::collections::HashMap;
13use std::sync::{Arc, Mutex};
14
15type HandlerMap = Arc<Mutex<HashMap<String, Box<dyn Fn() + Send + Sync>>>>;
16
17pub struct MenuBar {
19 pub(crate) inner: Menu,
20 pub(crate) handlers: HandlerMap,
21}
22
23impl MenuBar {
24 pub fn new() -> Self {
25 Self {
26 inner: Menu::new(),
27 handlers: Arc::new(Mutex::new(HashMap::new())),
28 }
29 }
30
31 pub fn submenu(self, label: &str, build: impl FnOnce(SubMenuBuilder) -> SubMenuBuilder) -> Self {
34 let handlers = Arc::clone(&self.handlers);
35 let builder = SubMenuBuilder::new(label, handlers);
36 let built = build(builder).finish();
37 let _ = self.inner.append(&built.submenu);
38 Self { inner: self.inner, handlers: built.handlers }
39 }
40}
41
42impl Default for MenuBar {
43 fn default() -> Self { Self::new() }
44}
45
46
47pub struct SubMenuBuilder {
48 label: String,
49 items: Vec<muda::MenuItemKind>,
50 handlers: HandlerMap,
51}
52
53pub(crate) struct SubMenuBuilt {
54 pub submenu: Submenu,
55 pub handlers: HandlerMap,
56}
57
58impl SubMenuBuilder {
59 fn new(label: &str, handlers: HandlerMap) -> Self {
60 Self { label: label.to_string(), items: vec![], handlers }
61 }
62
63 pub fn item(mut self, label: &str, shortcut: &str, on_click: impl Fn() + Send + Sync + 'static) -> Self {
66 let accel = parse_shortcut(shortcut);
67 let item = MenuItem::new(label, true, accel);
68 let id = item.id().0.to_string();
69 self.handlers.lock().unwrap().insert(id, Box::new(on_click));
70 self.items.push(muda::MenuItemKind::MenuItem(item));
71 self
72 }
73
74 pub fn separator(mut self) -> Self {
76 self.items.push(muda::MenuItemKind::Predefined(PredefinedMenuItem::separator()));
77 self
78 }
79
80 pub fn cut(mut self) -> Self { self.items.push(muda::MenuItemKind::Predefined(PredefinedMenuItem::cut(None))); self }
82 pub fn copy(mut self) -> Self { self.items.push(muda::MenuItemKind::Predefined(PredefinedMenuItem::copy(None))); self }
83 pub fn paste(mut self) -> Self { self.items.push(muda::MenuItemKind::Predefined(PredefinedMenuItem::paste(None))); self }
84 pub fn select_all(mut self) -> Self { self.items.push(muda::MenuItemKind::Predefined(PredefinedMenuItem::select_all(None))); self }
85 pub fn undo(mut self) -> Self { self.items.push(muda::MenuItemKind::Predefined(PredefinedMenuItem::undo(None))); self }
86 pub fn redo(mut self) -> Self { self.items.push(muda::MenuItemKind::Predefined(PredefinedMenuItem::redo(None))); self }
87 pub fn minimize(mut self) -> Self { self.items.push(muda::MenuItemKind::Predefined(PredefinedMenuItem::minimize(None))); self }
88 pub fn maximize(mut self) -> Self { self.items.push(muda::MenuItemKind::Predefined(PredefinedMenuItem::maximize(None))); self }
89 pub fn fullscreen(mut self) -> Self { self.items.push(muda::MenuItemKind::Predefined(PredefinedMenuItem::fullscreen(None))); self }
90 pub fn close_window(mut self) -> Self { self.items.push(muda::MenuItemKind::Predefined(PredefinedMenuItem::close_window(None))); self }
91 pub fn quit(mut self) -> Self { self.items.push(muda::MenuItemKind::Predefined(PredefinedMenuItem::quit(None))); self }
92
93 pub fn about(mut self, app_name: &str) -> Self {
94 self.items.push(muda::MenuItemKind::Predefined(PredefinedMenuItem::about(
95 None,
96 Some(muda::AboutMetadata { name: Some(app_name.to_string()), ..Default::default() }),
97 )));
98 self
99 }
100
101 pub fn submenu(mut self, label: &str, build: impl FnOnce(SubMenuBuilder) -> SubMenuBuilder) -> Self {
103 let handlers = Arc::clone(&self.handlers);
104 let builder = SubMenuBuilder::new(label, handlers);
105 let built = build(builder).finish();
106 self.items.push(muda::MenuItemKind::Submenu(built.submenu));
107 self.handlers = built.handlers;
108 self
109 }
110
111 pub(crate) fn finish(self) -> SubMenuBuilt {
112 let refs: Vec<&dyn muda::IsMenuItem> = self.items.iter().map(|k| -> &dyn muda::IsMenuItem {
113 match k {
114 muda::MenuItemKind::MenuItem(i) => i,
115 muda::MenuItemKind::Submenu(s) => s,
116 muda::MenuItemKind::Predefined(p) => p,
117 muda::MenuItemKind::Check(c) => c,
118 muda::MenuItemKind::Icon(i) => i,
119 }
120 }).collect();
121 let submenu = Submenu::with_items(&self.label, true, &refs).expect("submenu");
122 SubMenuBuilt { submenu, handlers: self.handlers }
123 }
124}
125
126
127fn parse_shortcut(s: &str) -> Option<Accelerator> {
128 if s.is_empty() { return None; }
129 let mut mods = Modifiers::empty();
130 let mut key_part = "";
131 for part in s.split('+') {
132 match part.trim() {
133 "CmdOrCtrl" | "Cmd" => {
134 #[cfg(target_os = "macos")]
135 { mods |= Modifiers::SUPER; }
136 #[cfg(not(target_os = "macos"))]
137 { mods |= Modifiers::CONTROL; }
138 }
139 "Ctrl" => { mods |= Modifiers::CONTROL; }
140 "Alt" | "Option" => { mods |= Modifiers::ALT; }
141 "Shift" => { mods |= Modifiers::SHIFT; }
142 "Meta" | "Super" => { mods |= Modifiers::SUPER; }
143 other => { key_part = other; }
144 }
145 }
146 let code = parse_code(key_part)?;
147 Some(Accelerator::new(Some(mods), code))
148}
149
150fn parse_code(s: &str) -> Option<Code> {
151 if s.len() == 1 {
152 return match s.chars().next().unwrap().to_ascii_uppercase() {
153 'A' => Some(Code::KeyA), 'B' => Some(Code::KeyB), 'C' => Some(Code::KeyC),
154 'D' => Some(Code::KeyD), 'E' => Some(Code::KeyE), 'F' => Some(Code::KeyF),
155 'G' => Some(Code::KeyG), 'H' => Some(Code::KeyH), 'I' => Some(Code::KeyI),
156 'J' => Some(Code::KeyJ), 'K' => Some(Code::KeyK), 'L' => Some(Code::KeyL),
157 'M' => Some(Code::KeyM), 'N' => Some(Code::KeyN), 'O' => Some(Code::KeyO),
158 'P' => Some(Code::KeyP), 'Q' => Some(Code::KeyQ), 'R' => Some(Code::KeyR),
159 'S' => Some(Code::KeyS), 'T' => Some(Code::KeyT), 'U' => Some(Code::KeyU),
160 'V' => Some(Code::KeyV), 'W' => Some(Code::KeyW), 'X' => Some(Code::KeyX),
161 'Y' => Some(Code::KeyY), 'Z' => Some(Code::KeyZ),
162 '0' => Some(Code::Digit0), '1' => Some(Code::Digit1), '2' => Some(Code::Digit2),
163 '3' => Some(Code::Digit3), '4' => Some(Code::Digit4), '5' => Some(Code::Digit5),
164 '6' => Some(Code::Digit6), '7' => Some(Code::Digit7), '8' => Some(Code::Digit8),
165 '9' => Some(Code::Digit9),
166 _ => None,
167 };
168 }
169 match s {
170 "F1" => Some(Code::F1), "F2" => Some(Code::F2), "F3" => Some(Code::F3),
171 "F4" => Some(Code::F4), "F5" => Some(Code::F5), "F6" => Some(Code::F6),
172 "F7" => Some(Code::F7), "F8" => Some(Code::F8), "F9" => Some(Code::F9),
173 "F10" => Some(Code::F10), "F11" => Some(Code::F11), "F12" => Some(Code::F12),
174 "Enter"|"Return" => Some(Code::Enter),
175 "Escape"|"Esc" => Some(Code::Escape),
176 "Backspace" => Some(Code::Backspace),
177 "Delete" => Some(Code::Delete),
178 "Tab" => Some(Code::Tab),
179 "Space" => Some(Code::Space),
180 "ArrowUp"|"Up" => Some(Code::ArrowUp),
181 "ArrowDown"|"Down"=> Some(Code::ArrowDown),
182 "ArrowLeft"|"Left"=> Some(Code::ArrowLeft),
183 "ArrowRight"|"Right"=>Some(Code::ArrowRight),
184 "Home" => Some(Code::Home),
185 "End" => Some(Code::End),
186 "PageUp" => Some(Code::PageUp),
187 "PageDown" => Some(Code::PageDown),
188 _ => None,
189 }
190}
191
192
193#[cfg(target_os = "macos")]
194pub fn install_menu_macos(menu: &MenuBar) {
195 menu.inner.init_for_nsapp();
196}
197#[cfg(not(target_os = "macos"))]
198pub fn install_menu_macos(_menu: &MenuBar) {}
199
200#[cfg(target_os = "windows")]
201pub fn install_menu_windows(menu: &MenuBar, hwnd: isize) {
202 unsafe { let _ = menu.inner.init_for_hwnd(hwnd); }
203}
204#[cfg(not(target_os = "windows"))]
205pub fn install_menu_windows(_menu: &MenuBar, _hwnd: isize) {}
206
207pub fn poll_menu_events(handlers: &HandlerMap) {
209 while let Ok(event) = muda::MenuEvent::receiver().try_recv() {
210 let id = event.id().0.to_string();
211 if let Some(f) = handlers.lock().unwrap().get(&id) {
212 f();
213 }
214 }
215}