diff --git a/.changes/dark_menubar.md b/.changes/dark_menubar.md new file mode 100644 index 00000000..e3f109f6 --- /dev/null +++ b/.changes/dark_menubar.md @@ -0,0 +1,5 @@ +--- +"muda": "patch" +--- + +On Windows, draw a dark menu bar if the Window supports and has dark-mode enabled. diff --git a/Cargo.toml b/Cargo.toml index ecd912a7..81ff8147 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ categories = [ "gui" ] [features] default = [ "libxdo" ] libxdo = [ "dep:libxdo" ] -common-controls-v6 = [ "windows-sys/Win32_UI_Controls" ] +common-controls-v6 = [ ] serde = [ "dep:serde" ] [dependencies] @@ -33,8 +33,10 @@ features = [ "Win32_Globalization", "Win32_UI_Input_KeyboardAndMouse", "Win32_System_SystemServices", + "Win32_UI_Accessibility", "Win32_UI_HiDpi", - "Win32_System_LibraryLoader" + "Win32_System_LibraryLoader", + "Win32_UI_Controls" ] [target."cfg(target_os = \"linux\")".dependencies] diff --git a/examples/wry.rs b/examples/wry.rs index c795df44..60081d10 100644 --- a/examples/wry.rs +++ b/examples/wry.rs @@ -9,12 +9,12 @@ use muda::{ PredefinedMenuItem, Submenu, }; #[cfg(target_os = "macos")] -use tao::platform::macos::WindowExtMacOS; +use wry::application::platform::macos::WindowExtMacOS; #[cfg(target_os = "linux")] -use tao::platform::unix::WindowExtUnix; +use wry::application::platform::unix::WindowExtUnix; #[cfg(target_os = "windows")] -use tao::platform::windows::{EventLoopBuilderExtWindows, WindowExtWindows}; -use tao::{ +use wry::application::platform::windows::{EventLoopBuilderExtWindows, WindowExtWindows}; +use wry::application::{ event::{ElementState, Event, MouseButton, WindowEvent}, event_loop::{ControlFlow, EventLoopBuilder}, window::{Window, WindowBuilder}, diff --git a/src/platform_impl/windows/dark_menu_bar.rs b/src/platform_impl/windows/dark_menu_bar.rs new file mode 100644 index 00000000..79ffa1e0 --- /dev/null +++ b/src/platform_impl/windows/dark_menu_bar.rs @@ -0,0 +1,271 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// this is a port of combination of https://github.com/hrydgard/ppsspp/blob/master/Windows/W32Util/UAHMenuBar.cpp and https://github.com/ysc3839/win32-darkmode/blob/master/win32-darkmode/DarkMode.h + +#![allow(non_snake_case)] + +use once_cell::sync::Lazy; +use windows_sys::{ + s, w, + Win32::{ + Foundation::{HMODULE, HWND, LPARAM, RECT, WPARAM}, + Graphics::Gdi::{OffsetRect, DT_CENTER, DT_HIDEPREFIX, DT_SINGLELINE, DT_VCENTER, HDC}, + System::LibraryLoader::{GetProcAddress, LoadLibraryA}, + UI::{ + Accessibility::HIGHCONTRASTA, + Controls::{ + CloseThemeData, DrawThemeBackground, DrawThemeText, OpenThemeData, DRAWITEMSTRUCT, + MENU_POPUPITEM, MPI_DISABLED, MPI_HOT, MPI_NORMAL, ODS_DEFAULT, ODS_DISABLED, + ODS_GRAYED, ODS_HOTLIGHT, ODS_INACTIVE, ODS_NOACCEL, ODS_SELECTED, + }, + WindowsAndMessaging::{ + GetMenuBarInfo, GetMenuItemInfoW, GetWindowRect, SystemParametersInfoA, HMENU, + MENUBARINFO, MENUITEMINFOW, MIIM_STRING, OBJID_MENU, SPI_GETHIGHCONTRAST, + }, + }, + }, +}; + +pub const WM_UAHDRAWMENU: u32 = 0x0091; +pub const WM_UAHDRAWMENUITEM: u32 = 0x0092; + +#[repr(C)] +struct UAHMENUITEMMETRICS0 { + cx: u32, + cy: u32, +} + +#[repr(C)] +struct UAHMENUITEMMETRICS { + rgsizeBar: [UAHMENUITEMMETRICS0; 2], + rgsizePopup: [UAHMENUITEMMETRICS0; 4], +} + +#[repr(C)] +struct UAHMENUPOPUPMETRICS { + rgcx: [u32; 4], + fUpdateMaxWidths: u32, +} + +#[repr(C)] +struct UAHMENU { + hmenu: HMENU, + hdc: HDC, + dwFlags: u32, +} +#[repr(C)] +struct UAHMENUITEM { + iPosition: u32, + umim: UAHMENUITEMMETRICS, + umpm: UAHMENUPOPUPMETRICS, +} +#[repr(C)] +struct UAHDRAWMENUITEM { + dis: DRAWITEMSTRUCT, + um: UAHMENU, + umi: UAHMENUITEM, +} + +/// Draws a dark menu bar if needed and returns whether it draws it or not +pub fn draw(hwnd: HWND, msg: u32, _wparam: WPARAM, lparam: LPARAM) -> bool { + if !should_use_dark_mode(hwnd) { + return false; + } + + match msg { + WM_UAHDRAWMENU => { + let pudm = lparam as *const UAHMENU; + + // get the menubar rect + let rc = { + let mut mbi = MENUBARINFO { + cbSize: std::mem::size_of::() as _, + ..unsafe { std::mem::zeroed() } + }; + unsafe { GetMenuBarInfo(hwnd, OBJID_MENU, 0, &mut mbi) }; + + let mut window_rc = RECT { + ..unsafe { std::mem::zeroed() } + }; + unsafe { GetWindowRect(hwnd, &mut window_rc) }; + + let mut rc = mbi.rcBar; + // the rcBar is offset by the window rect + unsafe { OffsetRect(&mut rc, -window_rc.left, -window_rc.top) }; + rc.top -= 1; + rc + }; + + unsafe { + let theme = OpenThemeData(hwnd, w!("Menu")); + + DrawThemeBackground( + theme, + (*pudm).hdc, + MENU_POPUPITEM, + MPI_NORMAL, + &rc, + std::ptr::null(), + ); + CloseThemeData(theme); + } + } + + WM_UAHDRAWMENUITEM => { + let pudmi = lparam as *const UAHDRAWMENUITEM; + + // get the menu item string + let (label, cch) = { + let mut label = Vec::::with_capacity(256); + let mut info: MENUITEMINFOW = unsafe { std::mem::zeroed() }; + info.cbSize = std::mem::size_of::() as _; + info.fMask = MIIM_STRING; + info.dwTypeData = label.as_mut_ptr(); + info.cch = (std::mem::size_of_val(&label) / 2 - 1) as _; + unsafe { + GetMenuItemInfoW( + (*pudmi).um.hmenu, + (*pudmi).umi.iPosition, + true.into(), + &mut info, + ) + }; + (label, info.cch) + }; + + // get the item state for drawing + let mut dw_flags = DT_CENTER | DT_SINGLELINE | DT_VCENTER; + let mut i_text_state_id = 0; + let mut i_background_state_id = 0; + + unsafe { + if (((*pudmi).dis.itemState & ODS_INACTIVE) + | ((*pudmi).dis.itemState & ODS_DEFAULT)) + != 0 + { + // normal display + i_text_state_id = MPI_NORMAL; + i_background_state_id = MPI_NORMAL; + } + if (*pudmi).dis.itemState & ODS_HOTLIGHT != 0 { + // hot tracking + i_text_state_id = MPI_HOT; + i_background_state_id = MPI_HOT; + } + if (*pudmi).dis.itemState & ODS_SELECTED != 0 { + // clicked -- MENU_POPUPITEM has no state for this, though MENU_BARITEM does + i_text_state_id = MPI_HOT; + i_background_state_id = MPI_HOT; + } + if ((*pudmi).dis.itemState & ODS_GRAYED) != 0 + || ((*pudmi).dis.itemState & ODS_DISABLED) != 0 + { + // disabled / grey text + i_text_state_id = MPI_DISABLED; + i_background_state_id = MPI_DISABLED; + } + if ((*pudmi).dis.itemState & ODS_NOACCEL) != 0 { + dw_flags |= DT_HIDEPREFIX; + } + + let theme = OpenThemeData(hwnd, w!("Menu")); + DrawThemeBackground( + theme, + (*pudmi).um.hdc, + MENU_POPUPITEM, + i_background_state_id, + &(*pudmi).dis.rcItem, + std::ptr::null(), + ); + DrawThemeText( + theme, + (*pudmi).um.hdc, + MENU_POPUPITEM, + i_text_state_id, + label.as_ptr(), + cch as _, + dw_flags, + 0, + &(*pudmi).dis.rcItem, + ); + CloseThemeData(theme); + } + } + + _ => return false, + }; + + true +} + +fn should_use_dark_mode(hwnd: HWND) -> bool { + should_apps_use_dark_mode() && !is_high_contrast() && is_dark_mode_allowed_for_window(hwnd) +} + +static HUXTHEME: Lazy = Lazy::new(|| unsafe { LoadLibraryA(s!("uxtheme.dll")) }); + +fn should_apps_use_dark_mode() -> bool { + const UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL: u16 = 132; + type ShouldAppsUseDarkMode = unsafe extern "system" fn() -> bool; + static SHOULD_APPS_USE_DARK_MODE: Lazy> = Lazy::new(|| unsafe { + if *HUXTHEME == 0 { + return None; + } + + GetProcAddress( + *HUXTHEME, + UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL as usize as *mut _, + ) + .map(|handle| std::mem::transmute(handle)) + }); + + SHOULD_APPS_USE_DARK_MODE + .map(|should_apps_use_dark_mode| unsafe { (should_apps_use_dark_mode)() }) + .unwrap_or(false) +} + +fn is_dark_mode_allowed_for_window(hwnd: HWND) -> bool { + const UXTHEME_ISDARKMODEALLOWEDFORWINDOW_ORDINAL: u16 = 137; + type IsDarkModeAllowedForWindow = unsafe extern "system" fn(HWND) -> bool; + static IS_DARK_MODE_ALLOWED_FOR_WINDOW: Lazy> = + Lazy::new(|| unsafe { + if *HUXTHEME == 0 { + return None; + } + + GetProcAddress( + *HUXTHEME, + UXTHEME_ISDARKMODEALLOWEDFORWINDOW_ORDINAL as usize as *mut _, + ) + .map(|handle| std::mem::transmute(handle)) + }); + + if let Some(_is_dark_mode_allowed_for_window) = *IS_DARK_MODE_ALLOWED_FOR_WINDOW { + unsafe { _is_dark_mode_allowed_for_window(hwnd) } + } else { + false + } +} + +fn is_high_contrast() -> bool { + const HCF_HIGHCONTRASTON: u32 = 1; + + let mut hc = HIGHCONTRASTA { + cbSize: 0, + dwFlags: Default::default(), + lpszDefaultScheme: std::ptr::null_mut(), + }; + + let ok = unsafe { + SystemParametersInfoA( + SPI_GETHIGHCONTRAST, + std::mem::size_of_val(&hc) as _, + &mut hc as *mut _ as _, + Default::default(), + ) + }; + + ok != 0 && (HCF_HIGHCONTRASTON & hc.dwFlags) != 0 +} diff --git a/src/platform_impl/windows/mod.rs b/src/platform_impl/windows/mod.rs index 4437bbef..43ba3289 100644 --- a/src/platform_impl/windows/mod.rs +++ b/src/platform_impl/windows/mod.rs @@ -3,9 +3,11 @@ // SPDX-License-Identifier: MIT mod accelerator; +mod dark_menu_bar; mod icon; mod util; +use self::dark_menu_bar::{WM_UAHDRAWMENU, WM_UAHDRAWMENUITEM}; pub(crate) use self::icon::WinIcon as PlatformIcon; use crate::{ @@ -1002,91 +1004,101 @@ unsafe extern "system" fn menu_subclass_proc( uidsubclass: usize, dwrefdata: usize, ) -> LRESULT { - let mut ret = -1; - if msg == WM_COMMAND { - let id = util::LOWORD(wparam as _) as u32; - let item = if uidsubclass == MENU_SUBCLASS_ID { - let menu = dwrefdata as *mut Box; - (*menu).find_by_id(id) - } else { - let menu = dwrefdata as *mut Box; - (*menu).find_by_id(id) - }; - - if let Some(item) = item { - ret = 0; + let mut ret = None; + match msg { + WM_COMMAND => { + let id = util::LOWORD(wparam as _) as u32; + let item = if uidsubclass == MENU_SUBCLASS_ID { + let menu = dwrefdata as *mut Box; + (*menu).find_by_id(id) + } else { + let menu = dwrefdata as *mut Box; + (*menu).find_by_id(id) + }; - let (mut dispatch, mut menu_id) = (true, None); + if let Some(item) = item { + ret = Some(0); - { - let mut item = item.borrow_mut(); + let (mut dispatch, mut menu_id) = (true, None); - if item.item_type() == MenuItemType::Predefined { - dispatch = false; - } else { - menu_id.replace(item.id.clone()); - } + { + let mut item = item.borrow_mut(); - match item.item_type() { - MenuItemType::Check => { - let checked = !item.checked; - item.set_checked(checked); + if item.item_type() == MenuItemType::Predefined { + dispatch = false; + } else { + menu_id.replace(item.id.clone()); } - MenuItemType::Predefined => { - if let Some(predefined_item_type) = &item.predefined_item_type { - match predefined_item_type { - PredefinedMenuItemType::Copy => { - execute_edit_command(EditCommand::Copy) - } - PredefinedMenuItemType::Cut => { - execute_edit_command(EditCommand::Cut) - } - PredefinedMenuItemType::Paste => { - execute_edit_command(EditCommand::Paste) - } - PredefinedMenuItemType::SelectAll => { - execute_edit_command(EditCommand::SelectAll) - } - PredefinedMenuItemType::Separator => {} - PredefinedMenuItemType::Minimize => { - ShowWindow(hwnd, SW_MINIMIZE); - } - PredefinedMenuItemType::Maximize => { - ShowWindow(hwnd, SW_MAXIMIZE); - } - PredefinedMenuItemType::Hide => { - ShowWindow(hwnd, SW_HIDE); - } - PredefinedMenuItemType::CloseWindow => { - SendMessageW(hwnd, WM_CLOSE, 0, 0); - } - PredefinedMenuItemType::Quit => { - PostQuitMessage(0); - } - PredefinedMenuItemType::About(Some(ref metadata)) => { - show_about_dialog(hwnd, metadata) - } - _ => {} + match item.item_type() { + MenuItemType::Check => { + let checked = !item.checked; + item.set_checked(checked); + } + MenuItemType::Predefined => { + if let Some(predefined_item_type) = &item.predefined_item_type { + match predefined_item_type { + PredefinedMenuItemType::Copy => { + execute_edit_command(EditCommand::Copy) + } + PredefinedMenuItemType::Cut => { + execute_edit_command(EditCommand::Cut) + } + PredefinedMenuItemType::Paste => { + execute_edit_command(EditCommand::Paste) + } + PredefinedMenuItemType::SelectAll => { + execute_edit_command(EditCommand::SelectAll) + } + PredefinedMenuItemType::Separator => {} + PredefinedMenuItemType::Minimize => { + ShowWindow(hwnd, SW_MINIMIZE); + } + PredefinedMenuItemType::Maximize => { + ShowWindow(hwnd, SW_MAXIMIZE); + } + PredefinedMenuItemType::Hide => { + ShowWindow(hwnd, SW_HIDE); + } + PredefinedMenuItemType::CloseWindow => { + SendMessageW(hwnd, WM_CLOSE, 0, 0); + } + PredefinedMenuItemType::Quit => { + PostQuitMessage(0); + } + PredefinedMenuItemType::About(Some(ref metadata)) => { + show_about_dialog(hwnd, metadata) + } + + _ => {} + } } } + _ => {} } - _ => {} + } + + if dispatch { + MenuEvent::send(MenuEvent { + id: menu_id.unwrap(), + }); } } + } - if dispatch { - MenuEvent::send(MenuEvent { - id: menu_id.unwrap(), - }); + WM_UAHDRAWMENUITEM | WM_UAHDRAWMENU => { + if dark_menu_bar::draw(hwnd, msg, wparam, lparam) { + ret = Some(0); } } - } - if ret == -1 { - DefSubclassProc(hwnd, msg, wparam, lparam) - } else { + _ => {} + }; + + if let Some(ret) = ret { ret + } else { + DefSubclassProc(hwnd, msg, wparam, lparam) } }