// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 use super::CustomEvent; use super::WinitWindowAdapter; use crate::SlintEvent; use core::pin::Pin; use i_slint_core::api::LogicalPosition; use i_slint_core::items::MenuEntry; use i_slint_core::menus::MenuVTable; use i_slint_core::properties::{PropertyDirtyHandler, PropertyTracker}; use muda::ContextMenu; use std::rc::Weak; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; use winit::event_loop::EventLoopProxy; use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle}; use winit::window::Window; pub struct MudaAdapter { entries: Vec, tracker: Option>>>, menu: muda::Menu, } #[derive(Clone, Copy, Debug, strum::EnumString, strum::Display)] pub enum MudaType { Menubar, Context, } static MUDA_SET_EVENT_HANDLER_INSTALLED: AtomicBool = AtomicBool::new(false); struct MudaPropertyTracker { window_adapter_weak: Weak, } impl PropertyDirtyHandler for MudaPropertyTracker { fn notify(self: Pin<&Self>) { let win = self.window_adapter_weak.clone(); i_slint_core::timers::Timer::single_shot(Default::default(), move || { if let Some(win) = win.upgrade() { win.rebuild_menubar(); } }) } } impl MudaAdapter { pub fn setup( menubar: &vtable::VRc, winit_window: &Window, proxy: EventLoopProxy, window_adapter_weak: Weak, ) -> Self { let menu = muda::Menu::new(); install_event_handler_if_necessary(proxy); #[cfg(target_os = "windows")] if let RawWindowHandle::Win32(handle) = winit_window.window_handle().unwrap().as_raw() { unsafe { menu.init_for_hwnd(handle.hwnd.get()).unwrap() }; } #[cfg(target_os = "macos")] { menu.init_for_nsapp(); } let tracker = Some(Box::pin(PropertyTracker::new_with_dirty_handler(MudaPropertyTracker { window_adapter_weak, }))); let mut s = Self { entries: Default::default(), tracker, menu }; s.rebuild_menu(winit_window, Some(menubar), MudaType::Menubar); s } pub fn show_context_menu( context_menu: &vtable::VRc, winit_window: &Window, position: LogicalPosition, proxy: EventLoopProxy, ) -> Option { if cfg!(target_os = "macos") { // TODO: Implement this on macOS (Note that rebuild_menu must not create the default app) return None; } let menu = muda::Menu::new(); install_event_handler_if_necessary(proxy); let mut s = Self { entries: Default::default(), tracker: None, menu }; s.rebuild_menu(winit_window, Some(context_menu), MudaType::Context); let position = i_slint_core::api::WindowPosition::Logical(position); let position = Some(crate::winitwindowadapter::position_to_winit(&position)); match winit_window.window_handle().ok()?.as_raw() { #[cfg(target_os = "windows")] RawWindowHandle::Win32(handle) => { unsafe { s.menu.show_context_menu_for_hwnd(handle.hwnd.get(), position); } Some(s) } #[cfg(target_os = "macos")] RawWindowHandle::AppKit(handle) => { unsafe { s.menu.show_context_menu_for_nsview(handle.ns_view.as_ptr(), position) }; Some(s) } _ => None, } } pub fn rebuild_menu( &mut self, winit_window: &Window, menubar: Option<&vtable::VRc>, muda_type: MudaType, ) { win32_set_window_redraw(winit_window, false); // clear the menu while self.menu.remove_at(0).is_some() {} self.entries.clear(); fn generate_menu_entry( menu: vtable::VRef<'_, MenuVTable>, entry: &MenuEntry, depth: usize, map: &mut Vec, window_id: &str, muda_type: MudaType, ) -> Box { let id = muda::MenuId(format!("{window_id}|{}|{}", map.len(), muda_type)); map.push(entry.clone()); if entry.is_separator { Box::new(muda::PredefinedMenuItem::separator()) } else if !entry.has_sub_menu { // the top level always has a sub menu regardless of entry.has_sub_menu if entry.checkable { Box::new(muda::CheckMenuItem::with_id( id.clone(), &entry.title, entry.enabled, entry.checked, None, )) } else if let Some(rgba) = entry.icon.to_rgba8() { let icon = muda::Icon::from_rgba( rgba.as_bytes().to_vec(), rgba.width(), rgba.height(), ) .ok(); Box::new(muda::IconMenuItem::with_id( id.clone(), &entry.title, entry.enabled, icon, None, )) } else { Box::new(muda::MenuItem::with_id(id.clone(), &entry.title, entry.enabled, None)) } } else { let sub_menu = muda::Submenu::with_id(id.clone(), &entry.title, entry.enabled); if depth < 15 { let mut sub_entries = Default::default(); menu.sub_menu(Some(entry), &mut sub_entries); for e in sub_entries { sub_menu .append(&*generate_menu_entry( menu, &e, depth + 1, map, window_id, muda_type, )) .unwrap(); } } else { // infinite menu depth is possible, but we want to limit the amount of item passed to muda sub_menu .append(&muda::MenuItem::new( "", false, None, )) .unwrap(); } Box::new(sub_menu) } } // Until we have menu roles, always create an app menu on macOS. #[cfg(target_os = "macos")] create_default_app_menu(&self.menu).unwrap(); if let Some(menubar) = menubar.as_deref() { let mut build_menu = || { let mut menu_entries = Default::default(); vtable::VRc::borrow(&menubar).sub_menu(None, &mut menu_entries); let window_id = u64::from(winit_window.id()).to_string(); for e in menu_entries { self.menu .append(&*generate_menu_entry( vtable::VRc::borrow(&menubar), &e, 0, &mut self.entries, &window_id, muda_type, )) .unwrap(); } }; if let Some(tracker) = self.tracker.as_ref() { tracker.as_ref().evaluate(build_menu); } else { build_menu() } } win32_set_window_redraw(winit_window, true); } pub fn invoke(&self, menubar: &vtable::VRc, entry_id: usize) { let Some(entry) = &self.entries.get(entry_id) else { return }; vtable::VRc::borrow(&menubar).activate(entry); } #[cfg(target_os = "macos")] pub fn setup_default_menu_bar() -> Result { let menu_bar = muda::Menu::new(); create_default_app_menu(&menu_bar)?; menu_bar.init_for_nsapp(); Ok(Self { entries: vec![], menu: menu_bar, tracker: None }) } #[cfg(target_os = "macos")] pub fn window_activation_changed(&self, is_active: bool) { if is_active { self.menu.init_for_nsapp(); } else { self.menu.remove_for_nsapp(); } } } fn install_event_handler_if_necessary(proxy: EventLoopProxy) { // `MenuEvent::set_event_handler()` in `muda` seems to use `OnceCell`, which is an // can only be set a single time. Therefore, we need to take care to only call this // a single time // // Arguably, `set_event_handler()` is unsafe if !MUDA_SET_EVENT_HANDLER_INSTALLED.load(Ordering::Relaxed) { muda::MenuEvent::set_event_handler(Some(move |e| { let _ = proxy.send_event(SlintEvent(CustomEvent::Muda(e))); })); MUDA_SET_EVENT_HANDLER_INSTALLED.store(true, Ordering::Relaxed); } } #[cfg(target_os = "macos")] fn create_default_app_menu(menu_bar: &muda::Menu) -> Result<(), i_slint_core::api::PlatformError> { let app_menu = muda::Submenu::new("App", true); menu_bar .append(&app_menu) .and_then(|_| { app_menu.append_items(&[ &muda::PredefinedMenuItem::about(None, None), &muda::PredefinedMenuItem::separator(), &muda::PredefinedMenuItem::services(None), &muda::PredefinedMenuItem::separator(), &muda::PredefinedMenuItem::hide(None), &muda::PredefinedMenuItem::hide_others(None), &muda::PredefinedMenuItem::show_all(None), &muda::PredefinedMenuItem::separator(), &muda::PredefinedMenuItem::quit(None), ]) }) .map_err(|menu_bar_err| { i_slint_core::api::PlatformError::Other(menu_bar_err.to_string()) })?; Ok(()) } /// On Windows, we need to disable window redraw while rebuilding the menu, otherwise /// we might see flickering #[allow(unused_variables)] fn win32_set_window_redraw(winit_window: &Window, redraw: bool) { #[cfg(target_os = "windows")] if let RawWindowHandle::Win32(handle) = winit_window.window_handle().unwrap().as_raw() { use std::os::raw::c_void; use windows::Win32::Foundation::HWND; use windows::Win32::Foundation::WPARAM; use windows::Win32::UI::WindowsAndMessaging::DrawMenuBar; use windows::Win32::UI::WindowsAndMessaging::SendMessageW; use windows::Win32::UI::WindowsAndMessaging::WM_SETREDRAW; let hwnd = HWND(handle.hwnd.get() as *mut c_void); unsafe { SendMessageW(hwnd, WM_SETREDRAW, Some(WPARAM(redraw as usize)), None); } if redraw { unsafe { let _ = DrawMenuBar(hwnd); } } } }