feat(windows): draw dark menubar if necessary (#98)

* feat(windows): draw dark menubar if necessary

closes #97

* Update Cargo.toml
This commit is contained in:
Amr Bashir 2023-08-15 16:50:14 +03:00 committed by GitHub
parent cfd4cb3fc5
commit 33168fa0a0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 365 additions and 75 deletions

5
.changes/dark_menubar.md Normal file
View file

@ -0,0 +1,5 @@
---
"muda": "patch"
---
On Windows, draw a dark menu bar if the Window supports and has dark-mode enabled.

View file

@ -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]

View file

@ -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},

View file

@ -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::<MENUBARINFO>() 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::<u16>::with_capacity(256);
let mut info: MENUITEMINFOW = unsafe { std::mem::zeroed() };
info.cbSize = std::mem::size_of::<MENUITEMINFOW>() 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<HMODULE> = 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<Option<ShouldAppsUseDarkMode>> = 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<Option<IsDarkModeAllowedForWindow>> =
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
}

View file

@ -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>;
(*menu).find_by_id(id)
} else {
let menu = dwrefdata as *mut Box<MenuChild>;
(*menu).find_by_id(id)
};
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>;
(*menu).find_by_id(id)
} else {
let menu = dwrefdata as *mut Box<MenuChild>;
(*menu).find_by_id(id)
};
if let Some(item) = item {
ret = 0;
if let Some(item) = item {
ret = Some(0);
let (mut dispatch, mut menu_id) = (true, None);
let (mut dispatch, mut menu_id) = (true, None);
{
let mut item = item.borrow_mut();
{
let mut item = item.borrow_mut();
if item.item_type() == MenuItemType::Predefined {
dispatch = false;
} else {
menu_id.replace(item.id.clone());
}
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)
}
}