ferroui_native/
ferro_native_menu_exporter.rs1use crate::ferro_native_platform_extensions::MacOSPlatformOptions;
5use crate::frn_menu::{FrnMenu, IMenuFactory};
6use crate::helpers::ComResultExt;
7use crate::interop::*;
8use crate::mac_os_native_menu_commands::MacOSNativeMenuCommands;
9use ferroui_base::input::{Key, KeyGesture, KeyModifiers};
10use ferroui_base::reactive::{Disposable, IDisposable};
11use ferroui_base::threading::{Dispatcher, DispatcherPriority};
12use ferroui_base::{FerroLocator, LocatorExtensions, Ref};
13use ferroui_controls::platform::{INativeMenuExporter, ITopLevelNativeMenuExporter};
14use ferroui_controls::{Application, NativeDock, NativeMenu, NativeMenuItem, NativeMenuItemSeparator, WindowBase};
15use ferroui_dialogs::AboutFerroDialog;
16use ferroui_microcom::ComPtr;
17use std::cell::{Cell, RefCell};
18use std::rc::{Rc, Weak};
19
20pub(crate) enum MenuTarget {
22 Application,
23 Window(ComPtr<IFrnWindow>),
24 TrayIcon(ComPtr<IFrnTrayIcon>),
25 Dock,
26}
27
28pub struct FerroNativeMenuExporter {
30 weak_self: Weak<FerroNativeMenuExporter>,
31 factory: Rc<dyn IMenuFactory>,
32 target: MenuTarget,
33 reset_queued: Cell<bool>,
34 exported: Cell<bool>,
35 menu: RefCell<Option<Ref<NativeMenu>>>,
36 native_menu: RefCell<Option<Rc<FrnMenu>>>,
37 application_commands: Option<ComPtr<IFrnApplicationCommands>>,
38 dock_menu_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
40}
41
42fn mac_options() -> MacOSPlatformOptions {
43 FerroLocator::current().get_service::<MacOSPlatformOptions>().as_deref().cloned().unwrap_or_default()
44}
45
46impl FerroNativeMenuExporter {
47 fn new(
48 factory: Rc<dyn IMenuFactory>,
49 target: MenuTarget,
50 reset_queued: bool,
51 application_commands: Option<ComPtr<IFrnApplicationCommands>>,
52 ) -> Rc<FerroNativeMenuExporter> {
53 Rc::new_cyclic(|weak_self| FerroNativeMenuExporter {
54 weak_self: weak_self.clone(),
55 factory,
56 target,
57 reset_queued: Cell::new(reset_queued),
58 exported: Cell::new(false),
59 menu: RefCell::new(None),
60 native_menu: RefCell::new(None),
61 application_commands,
62 dock_menu_subscription: RefCell::new(None),
63 })
64 }
65
66 fn with_commands(factory: &ComPtr<IFerroNativeFactory>, target: MenuTarget) -> Rc<FerroNativeMenuExporter> {
67 let application_commands = factory.create_application_commands().check();
68 let this = Self::new(Rc::new(factory.clone()), target, true, application_commands);
69
70 this.do_layout_reset(false);
71 this
72 }
73
74 pub(crate) fn for_window(
76 native_window: ComPtr<IFrnWindow>,
77 factory: &ComPtr<IFerroNativeFactory>,
78 ) -> Rc<FerroNativeMenuExporter> {
79 Self::with_commands(factory, MenuTarget::Window(native_window))
80 }
81
82 pub(crate) fn for_application(factory: &ComPtr<IFerroNativeFactory>) -> Rc<FerroNativeMenuExporter> {
84 Self::with_commands(factory, MenuTarget::Application)
85 }
86
87 pub(crate) fn for_tray_icon(
89 tray_icon: ComPtr<IFrnTrayIcon>,
90 factory: &ComPtr<IFerroNativeFactory>,
91 ) -> Rc<FerroNativeMenuExporter> {
92 Self::with_commands(factory, MenuTarget::TrayIcon(tray_icon))
93 }
94
95 pub(crate) fn for_dock(factory: &ComPtr<IFerroNativeFactory>) -> Rc<FerroNativeMenuExporter> {
98 Self::for_dock_with(Rc::new(factory.clone()))
99 }
100
101 pub(crate) fn for_dock_with(factory: Rc<dyn IMenuFactory>) -> Rc<FerroNativeMenuExporter> {
102 let this = Self::new(factory, MenuTarget::Dock, false, None);
103
104 if mac_options().disable_native_menus {
105 return this;
106 }
107
108 let weak = Rc::downgrade(&this);
109 let subscription = NativeDock::menu_property().changed().subscribe(move |args| {
110 if args.sender().is::<Application>() {
111 if let Some(this) = weak.upgrade() {
112 this.set_native_menu(args.get_new_value::<Option<Ref<NativeMenu>>>());
113 }
114 }
115 });
116 *this.dock_menu_subscription.borrow_mut() = Some(subscription);
117
118 if let Some(app) = Application::current() {
119 if let Some(dock_menu) = NativeDock::get_menu(&app) {
120 this.set_native_menu(Some(dock_menu));
121 }
122 }
123
124 this
125 }
126
127 pub(crate) fn update_if_needed(&self) {
128 if self.reset_queued.get() {
129 self.do_layout_reset(false);
130 }
131 }
132
133 fn create_default_app_menu() -> Ref<NativeMenu> {
134 let result = NativeMenu::new();
135
136 let about_item = NativeMenuItem::with_header("About FerroUI");
137
138 let _ = about_item.click(|_| {
140 Dispatcher::ui_thread().invoke_async_task_local(|| async {
141 let dialog = AboutFerroDialog::new();
142
143 let main_window = Application::current()
144 .and_then(|app| app.application_lifetime())
145 .and_then(|lifetime| lifetime.as_classic_desktop_style_application_lifetime().and_then(|d| d.main_window()))
146 .filter(|main_window| main_window.is_visible());
147 match main_window {
148 Some(main_window) => {
149 let _ = dialog.show_dialog(&main_window).await;
150 }
151 None => WindowBase::show(&dialog),
152 }
153 });
154 });
155
156 result.add(about_item);
157
158 result
159 }
160
161 fn populate_standard_osx_menu_items(&self, app_menu: &Ref<NativeMenu>) {
162 app_menu.add(NativeMenuItemSeparator::new());
163
164 let services_menu = NativeMenuItem::with_header("Services");
165 let services_sub_menu = NativeMenu::new();
166 services_sub_menu.set_value(MacOSNativeMenuCommands::is_services_submenu_property(), true);
167 services_menu.set_menu(Some(services_sub_menu));
168
169 app_menu.add(services_menu);
170
171 app_menu.add(NativeMenuItemSeparator::new());
172
173 let application_name =
174 Application::current().and_then(|app| app.name()).unwrap_or_else(|| "Application".to_string());
175 let hide_item = NativeMenuItem::with_header(&format!("Hide {application_name}"));
176 hide_item.set_gesture(Some(KeyGesture::new(Key::H, KeyModifiers::META)));
177
178 let commands = self.application_commands.clone();
179 let _ = hide_item.click(move |_| {
180 if let Some(commands) = &commands {
181 commands.hide_app().check();
182 }
183 });
184
185 app_menu.add(hide_item);
186
187 let hide_others_item = NativeMenuItem::with_header("Hide Others");
188 hide_others_item.set_gesture(Some(KeyGesture::new(Key::Q, KeyModifiers::META | KeyModifiers::ALT)));
189 let commands = self.application_commands.clone();
190 let _ = hide_others_item.click(move |_| {
191 if let Some(commands) = &commands {
192 commands.hide_others().check();
193 }
194 });
195 app_menu.add(hide_others_item);
196
197 let show_all_item = NativeMenuItem::with_header("Show All");
198 let commands = self.application_commands.clone();
199 let _ = show_all_item.click(move |_| {
200 if let Some(commands) = &commands {
201 commands.show_all().check();
202 }
203 });
204
205 app_menu.add(show_all_item);
206
207 app_menu.add(NativeMenuItemSeparator::new());
208
209 let quit_item = NativeMenuItem::with_header("Quit");
210 quit_item.set_gesture(Some(KeyGesture::new(Key::Q, KeyModifiers::META)));
211 let _ = quit_item.click(|_| {
212 let lifetime = Application::current().and_then(|app| app.application_lifetime());
213 let Some(lifetime) = lifetime else {
214 return;
215 };
216 if let Some(lifetime) = lifetime.as_classic_desktop_style_application_lifetime() {
217 lifetime.try_shutdown(0);
218 } else if let Some(controlled_lifetime) = lifetime.as_controlled_application_lifetime() {
219 controlled_lifetime.shutdown(0);
220 }
221 });
222
223 app_menu.add(quit_item);
224 }
225
226 fn do_layout_reset(&self, force_update: bool) {
227 if mac_options().disable_native_menus {
228 return;
229 }
230
231 if self.reset_queued.get() || force_update {
232 self.reset_queued.set(false);
233
234 let menu = self.menu.borrow().clone();
235 match &self.target {
236 MenuTarget::Application => {
237 let app = Application::current();
238 let app_menu = app.as_ref().and_then(|app| NativeMenu::get_menu(app));
239
240 let app_menu = match app_menu {
241 Some(app_menu) => app_menu,
242 None => {
243 let app_menu = Self::create_default_app_menu();
244
245 if let Some(app) = &app {
246 NativeMenu::set_menu(app, Some(app_menu.clone()));
247 }
248 app_menu
249 }
250 };
251
252 self.set_app_menu(&app_menu);
253 }
254
255 MenuTarget::Window(native_window) => {
256 if let Some(menu) = menu {
257 if self.ensure_native_menu(&menu) {
258 let native_menu = self.native_menu.borrow().clone().expect("just created");
259 native_window.set_main_menu(Some(native_menu.native())).check();
260 }
261 }
262 }
263
264 MenuTarget::TrayIcon(tray_icon) => {
265 if let Some(menu) = menu {
266 if self.ensure_native_menu(&menu) {
267 let native_menu = self.native_menu.borrow().clone().expect("just created");
268 tray_icon.set_menu(Some(native_menu.native())).check();
269 }
270 }
271 }
272
273 MenuTarget::Dock => {
274 if let Some(menu) = menu {
275 if self.ensure_native_menu(&menu) {
276 let native_menu = self.native_menu.borrow().clone().expect("just created");
277 self.factory.set_dock_menu(native_menu.native());
278 }
279 }
280 }
281 }
282
283 self.exported.set(true);
284 }
285 }
286
287 pub(crate) fn queue_reset(&self) {
288 if self.reset_queued.get() {
289 return;
290 }
291 self.reset_queued.set(true);
292 let this = self.weak_self.clone();
293 Dispatcher::ui_thread().post_local(
294 move || {
295 if let Some(this) = this.upgrade() {
296 this.do_layout_reset(false);
297 }
298 },
299 DispatcherPriority::BACKGROUND,
300 );
301 }
302
303 fn this(&self) -> Rc<FerroNativeMenuExporter> {
304 self.weak_self.upgrade().expect("the exporter is alive while it is borrowed")
305 }
306
307 fn ensure_native_menu(&self, menu: &Ref<NativeMenu>) -> bool {
311 let mut set_menu = false;
312
313 let native_menu = self.native_menu.borrow().clone();
314 let native_menu = match native_menu {
315 Some(native_menu) => native_menu,
316 None => {
317 let native_menu = FrnMenu::create(&*self.factory);
318
319 native_menu.initialize(&self.this(), menu, Some(""));
320 *self.native_menu.borrow_mut() = Some(native_menu.clone());
321
322 set_menu = true;
323 native_menu
324 }
325 };
326
327 native_menu.update(&*self.factory, menu);
328
329 set_menu
330 }
331
332 fn set_app_menu(&self, menu: &Ref<NativeMenu>) {
335 let menu_item = menu.parent();
336
337 let app_menu_holder = menu_item.as_ref().and_then(|menu_item| menu_item.parent());
338
339 let menu_item = menu_item.unwrap_or_else(NativeMenuItem::new);
340
341 let app_menu_holder = app_menu_holder.unwrap_or_else(|| {
342 let app_menu_holder = NativeMenu::new();
343
344 app_menu_holder.add(menu_item.clone());
345 app_menu_holder
346 });
347
348 menu_item.set_menu(Some(menu.clone()));
349
350 let mut set_menu = false;
351
352 let native_menu = self.native_menu.borrow().clone();
353 let native_menu = match native_menu {
354 Some(native_menu) => native_menu,
355 None => {
356 let native_menu = FrnMenu::create(&*self.factory);
357
358 native_menu.initialize(&self.this(), &app_menu_holder, Some(""));
359 *self.native_menu.borrow_mut() = Some(native_menu.clone());
360
361 if !mac_options().disable_default_application_menu_items {
362 self.populate_standard_osx_menu_items(menu);
363 }
364
365 set_menu = true;
366 native_menu
367 }
368 };
369
370 native_menu.update(&*self.factory, &app_menu_holder);
371
372 if set_menu {
373 self.factory.set_app_menu(native_menu.native());
374 }
375 }
376
377 #[cfg(test)]
378 pub(crate) fn native_menu(&self) -> Option<Rc<FrnMenu>> {
379 self.native_menu.borrow().clone()
380 }
381
382 #[cfg(test)]
383 pub(crate) fn for_application_with(factory: Rc<dyn IMenuFactory>) -> Rc<FerroNativeMenuExporter> {
384 let this = Self::new(factory, MenuTarget::Application, true, None);
385 this.do_layout_reset(false);
386 this
387 }
388}
389
390impl INativeMenuExporter for FerroNativeMenuExporter {
391 fn set_native_menu(&self, menu: Option<Ref<NativeMenu>>) {
392 if matches!(self.target, MenuTarget::Dock) {
393 let has_menu = menu.is_some();
394 *self.menu.borrow_mut() = menu;
395
396 if has_menu {
397 self.do_layout_reset(true);
398 }
399 } else {
400 *self.menu.borrow_mut() = Some(menu.unwrap_or_else(NativeMenu::new));
401 self.do_layout_reset(true);
402 }
403 }
404}
405
406impl ITopLevelNativeMenuExporter for FerroNativeMenuExporter {
407 fn is_native_menu_exported(&self) -> bool {
408 self.exported.get()
409 }
410
411 fn on_is_native_menu_exported_changed(&self, _handler: Rc<dyn Fn()>) -> Rc<dyn IDisposable> {
413 Disposable::empty()
414 }
415}
416
417impl Drop for FerroNativeMenuExporter {
418 fn drop(&mut self) {
419 if let Some(subscription) = self.dock_menu_subscription.borrow_mut().take() {
420 subscription.dispose();
421 }
422 }
423}