diff --git a/.changes/apply-dark-mode-to-menus.md b/.changes/apply-dark-mode-to-menus.md new file mode 100644 index 000000000..dd087f095 --- /dev/null +++ b/.changes/apply-dark-mode-to-menus.md @@ -0,0 +1,5 @@ +--- +"tao": patch +--- + +On Windows, apply dark mode app-wide to some controls like context menus. diff --git a/.changes/windows-app-wide-theme.md b/.changes/windows-app-wide-theme.md new file mode 100644 index 000000000..2fa0a2f55 --- /dev/null +++ b/.changes/windows-app-wide-theme.md @@ -0,0 +1,5 @@ +--- +"tao": "patch" +--- + +On Windows, add `EventLoopBuilderExtWindows::with_theme` to control the app-wide theme. diff --git a/Cargo.toml b/Cargo.toml index c5ba931f3..2c35996b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,12 +51,14 @@ url = "2" image = "0.24" env_logger = "0.10" +[target."cfg(any(target_os = \"android\", target_os = \"windows\"))".dependencies] +once_cell = "1" + [target."cfg(target_os = \"android\")".dependencies] jni = "0.21" ndk = "0.7" ndk-sys = "0.4" ndk-context = "0.1" -once_cell = "1" tao-macros = { version = "0.1.0", path = "./tao-macros" } [target."cfg(any(target_os = \"ios\", target_os = \"macos\"))".dependencies] @@ -76,9 +78,9 @@ unicode-segmentation = "1.10" image = { version = "0.24", default-features = false } windows-implement = "0.48.0" - [target."cfg(target_os = \"windows\")".dependencies.windows] - version = "0.48.0" - features = [ +[target."cfg(target_os = \"windows\")".dependencies.windows] +version = "0.48.0" +features = [ "implement", "Win32_Devices_HumanInterfaceDevice", "Win32_Foundation", @@ -95,6 +97,7 @@ windows-implement = "0.48.0" "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming", + "Win32_System_SystemInformation", "Win32_UI_Accessibility", "Win32_UI_Controls", "Win32_UI_HiDpi", @@ -104,7 +107,7 @@ windows-implement = "0.48.0" "Win32_UI_Input_Touch", "Win32_UI_Shell", "Win32_UI_TextServices", - "Win32_UI_WindowsAndMessaging" + "Win32_UI_WindowsAndMessaging", ] [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] diff --git a/src/platform/windows.rs b/src/platform/windows.rs index eede87536..820a40dec 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -87,6 +87,19 @@ pub trait EventLoopBuilderExtWindows { fn with_msg_hook(&mut self, callback: F) -> &mut Self where F: FnMut(*const std::ffi::c_void) -> bool + 'static; + + /// Forces a theme or uses the system settings if `None` was provided. + /// + /// This will only affect some controls like context menus. + /// + /// ## Note + /// + /// Since this setting is app-wide, using [`WindowBuilder::with_theme`] + /// will not change the affected controls for that specific window, + /// so it is recommended to always use the same theme used for this app-wide setting + /// or use `None` so it automatically uses the theme of this method + /// or falls back to the system preference. + fn with_theme(&mut self, theme: Option) -> &mut Self; } impl EventLoopBuilderExtWindows for EventLoopBuilder { @@ -110,6 +123,13 @@ impl EventLoopBuilderExtWindows for EventLoopBuilder { self.platform_specific.msg_hook = Some(Box::new(callback)); self } + + #[inline] + + fn with_theme(&mut self, theme: Option) -> &mut Self { + self.platform_specific.preferred_theme = theme; + self + } } /// Additional methods on `Window` that are specific to Windows. diff --git a/src/platform_impl/windows/dark_mode.rs b/src/platform_impl/windows/dark_mode.rs index 2d9435d4e..562f7bb8e 100644 --- a/src/platform_impl/windows/dark_mode.rs +++ b/src/platform_impl/windows/dark_mode.rs @@ -1,81 +1,160 @@ // Copyright 2014-2021 The winit contributors // Copyright 2021-2023 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 +#![allow(non_snake_case)] +use once_cell::sync::Lazy; /// This is a simple implementation of support for Windows Dark Mode, /// which is inspired by the solution in https://github.com/ysc3839/win32-darkmode use windows::{ - core::{s, PCSTR, PCWSTR, PSTR}, + core::{s, PCSTR, PSTR}, + w, Win32::{ - Foundation::{BOOL, HWND}, - System::LibraryLoader::*, - UI::{Accessibility::*, Controls::*, WindowsAndMessaging::*}, + Foundation::{BOOL, HANDLE, HMODULE, HWND}, + System::{LibraryLoader::*, SystemInformation::OSVERSIONINFOW}, + UI::{Accessibility::*, WindowsAndMessaging::*}, }, }; use std::ffi::c_void; -use crate::{platform_impl::platform::util, window::Theme}; - -lazy_static! { - static ref WIN10_BUILD_VERSION: Option = { - // FIXME: RtlGetVersion is a documented windows API, - // should be part of win32metadata! - - #[allow(non_snake_case)] - #[repr(C)] - struct OSVERSIONINFOW { - dwOSVersionInfoSize: u32, - dwMajorVersion: u32, - dwMinorVersion: u32, - dwBuildNumber: u32, - dwPlatformId: u32, - szCSDVersion: [u16; 128], - } - - type RtlGetVersion = unsafe extern "system" fn (*mut OSVERSIONINFOW) -> i32; - let handle = get_function!("ntdll.dll", RtlGetVersion); - - if let Some(rtl_get_version) = handle { - unsafe { - let mut vi = OSVERSIONINFOW { - dwOSVersionInfoSize: 0, - dwMajorVersion: 0, - dwMinorVersion: 0, - dwBuildNumber: 0, - dwPlatformId: 0, - szCSDVersion: [0; 128], - }; - - let status = (rtl_get_version)(&mut vi as _); - - if status >= 0 && vi.dwMajorVersion == 10 && vi.dwMinorVersion == 0 { - Some(vi.dwBuildNumber) - } else { - None - } - } - } else { - None - } - }; +use crate::window::Theme; + +static HUXTHEME: Lazy = + Lazy::new(|| unsafe { LoadLibraryA(s!("uxtheme.dll")).unwrap_or_default() }); + +static WIN10_BUILD_VERSION: Lazy> = Lazy::new(|| { + type RtlGetVersion = unsafe extern "system" fn(*mut OSVERSIONINFOW) -> i32; + + let handle = get_function!("ntdll.dll", RtlGetVersion); + + let mut vi = OSVERSIONINFOW { + dwOSVersionInfoSize: 0, + dwMajorVersion: 0, + dwMinorVersion: 0, + dwBuildNumber: 0, + dwPlatformId: 0, + szCSDVersion: [0; 128], + }; + + if let Some(rtl_get_version) = handle { + let status = unsafe { (rtl_get_version)(&mut vi as _) }; + + if status >= 0 && vi.dwMajorVersion == 10 && vi.dwMinorVersion == 0 { + Some(vi.dwBuildNumber) + } else { + None + } + } else { + None + } +}); + +static DARK_MODE_SUPPORTED: Lazy = Lazy::new(|| { + // We won't try to do anything for windows versions < 17763 + // (Windows 10 October 2018 update) + match *WIN10_BUILD_VERSION { + Some(v) => v >= 17763, + None => false, + } +}); - static ref DARK_MODE_SUPPORTED: bool = { - // We won't try to do anything for windows versions < 17763 - // (Windows 10 October 2018 update) - match *WIN10_BUILD_VERSION { - Some(v) => v >= 17763, - None => false - } +/// Attempts to set dark mode for the app +pub fn try_app_theme(preferred_theme: Option) -> Theme { + if *DARK_MODE_SUPPORTED { + let is_dark_mode = match preferred_theme { + Some(theme) => theme == Theme::Dark, + None => should_use_dark_mode(), }; - static ref DARK_THEME_NAME: Vec = util::encode_wide("DarkMode_Explorer"); - static ref LIGHT_THEME_NAME: Vec = util::encode_wide(""); + allow_dark_mode_for_app(is_dark_mode); + refresh_immersive_color_policy_state(); + match is_dark_mode { + true => Theme::Dark, + false => Theme::Light, + } + } else { + Theme::Light + } +} + +fn allow_dark_mode_for_app(is_dark_mode: bool) { + const UXTHEME_ALLOWDARKMODEFORAPP_ORDINAL: u16 = 135; + type AllowDarkModeForApp = unsafe extern "system" fn(bool) -> bool; + static ALLOW_DARK_MODE_FOR_APP: Lazy> = Lazy::new(|| unsafe { + if HUXTHEME.is_invalid() { + return None; + } + + GetProcAddress( + *HUXTHEME, + PCSTR::from_raw(UXTHEME_ALLOWDARKMODEFORAPP_ORDINAL as usize as *mut _), + ) + .map(|handle| std::mem::transmute(handle)) + }); + + #[repr(C)] + enum PreferredAppMode { + Default, + AllowDark, + // ForceDark, + // ForceLight, + // Max, + } + const UXTHEME_SETPREFERREDAPPMODE_ORDINAL: u16 = 135; + type SetPreferredAppMode = unsafe extern "system" fn(PreferredAppMode) -> PreferredAppMode; + static SET_PREFERRED_APP_MODE: Lazy> = Lazy::new(|| unsafe { + if HUXTHEME.is_invalid() { + return None; + } + + GetProcAddress( + *HUXTHEME, + PCSTR::from_raw(UXTHEME_SETPREFERREDAPPMODE_ORDINAL as usize as *mut _), + ) + .map(|handle| std::mem::transmute(handle)) + }); + + if let Some(ver) = *WIN10_BUILD_VERSION { + if ver < 18362 { + if let Some(_allow_dark_mode_for_app) = *ALLOW_DARK_MODE_FOR_APP { + unsafe { _allow_dark_mode_for_app(is_dark_mode) }; + } + } else if let Some(_set_preferred_app_mode) = *SET_PREFERRED_APP_MODE { + let mode = if is_dark_mode { + PreferredAppMode::AllowDark + } else { + PreferredAppMode::Default + }; + unsafe { _set_preferred_app_mode(mode) }; + } + } +} + +fn refresh_immersive_color_policy_state() { + const UXTHEME_REFRESHIMMERSIVECOLORPOLICYSTATE_ORDINAL: u16 = 104; + type RefreshImmersiveColorPolicyState = unsafe extern "system" fn(); + static REFRESH_IMMERSIVE_COLOR_POLICY_STATE: Lazy> = + Lazy::new(|| unsafe { + if HUXTHEME.is_invalid() { + return None; + } + + GetProcAddress( + *HUXTHEME, + PCSTR::from_raw(UXTHEME_REFRESHIMMERSIVECOLORPOLICYSTATE_ORDINAL as usize as *mut _), + ) + .map(|handle| std::mem::transmute(handle)) + }); + + if let Some(_refresh_immersive_color_policy_state) = *REFRESH_IMMERSIVE_COLOR_POLICY_STATE { + unsafe { _refresh_immersive_color_policy_state() } + } } /// Attempt to set a theme on a window, if necessary. /// Returns the theme that was picked -pub fn try_theme(hwnd: HWND, preferred_theme: Option) -> Theme { +pub fn try_window_theme(hwnd: HWND, preferred_theme: Option) -> Theme { if *DARK_MODE_SUPPORTED { let is_dark_mode = match preferred_theme { Some(theme) => theme == Theme::Dark, @@ -87,65 +166,96 @@ pub fn try_theme(hwnd: HWND, preferred_theme: Option) -> Theme { } else { Theme::Light }; - let theme_name = PCWSTR::from_raw( - match theme { - Theme::Dark => DARK_THEME_NAME.clone(), - Theme::Light => LIGHT_THEME_NAME.clone(), - } - .as_ptr(), - ); - let status = unsafe { SetWindowTheme(hwnd, theme_name, PCWSTR::null()) }; + allow_dark_mode_for_window(hwnd, is_dark_mode); + refresh_titlebar_theme_color(hwnd); - if status.is_ok() && set_dark_mode_for_window(hwnd, is_dark_mode) { - return theme; - } + theme + } else { + Theme::Light } - - Theme::Light } -fn set_dark_mode_for_window(hwnd: HWND, is_dark_mode: bool) -> bool { - // Uses Windows undocumented API SetWindowCompositionAttribute, - // as seen in win32-darkmode example linked at top of file. - - type SetWindowCompositionAttribute = - unsafe extern "system" fn(HWND, *mut WINDOWCOMPOSITIONATTRIBDATA) -> BOOL; +fn allow_dark_mode_for_window(hwnd: HWND, is_dark_mode: bool) { + const UXTHEME_ALLOWDARKMODEFORWINDOW_ORDINAL: u16 = 133; + type AllowDarkModeForWindow = unsafe extern "system" fn(HWND, bool) -> bool; + static ALLOW_DARK_MODE_FOR_WINDOW: Lazy> = Lazy::new(|| unsafe { + if HUXTHEME.is_invalid() { + return None; + } - #[allow(non_snake_case)] - type WINDOWCOMPOSITIONATTRIB = u32; - const WCA_USEDARKMODECOLORS: WINDOWCOMPOSITIONATTRIB = 26; + GetProcAddress( + *HUXTHEME, + PCSTR::from_raw(UXTHEME_ALLOWDARKMODEFORWINDOW_ORDINAL as usize as *mut _), + ) + .map(|handle| std::mem::transmute(handle)) + }); - #[allow(non_snake_case)] - #[repr(C)] - struct WINDOWCOMPOSITIONATTRIBDATA { - Attrib: WINDOWCOMPOSITIONATTRIB, - pvData: *mut c_void, - cbData: usize, + if *DARK_MODE_SUPPORTED { + if let Some(_allow_dark_mode_for_window) = *ALLOW_DARK_MODE_FOR_WINDOW { + unsafe { _allow_dark_mode_for_window(hwnd, is_dark_mode) }; + } } +} + +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.is_invalid() { + return None; + } + + GetProcAddress( + *HUXTHEME, + PCSTR::from_raw(UXTHEME_ISDARKMODEALLOWEDFORWINDOW_ORDINAL as usize as *mut _), + ) + .map(|handle| std::mem::transmute(handle)) + }); - lazy_static! { - static ref SET_WINDOW_COMPOSITION_ATTRIBUTE: Option = - get_function!("user32.dll", SetWindowCompositionAttribute); + 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 } +} - if let Some(set_window_composition_attribute) = *SET_WINDOW_COMPOSITION_ATTRIBUTE { - unsafe { - // SetWindowCompositionAttribute needs a bigbool (i32), not bool. - let mut is_dark_mode_bigbool: BOOL = is_dark_mode.into(); +type SetWindowCompositionAttribute = + unsafe extern "system" fn(HWND, *mut WINDOWCOMPOSITIONATTRIBDATA) -> BOOL; +static SET_WINDOW_COMPOSITION_ATTRIBUTE: Lazy> = + Lazy::new(|| get_function!("user32.dll", SetWindowCompositionAttribute)); + +type WINDOWCOMPOSITIONATTRIB = u32; +const WCA_USEDARKMODECOLORS: WINDOWCOMPOSITIONATTRIB = 26; +#[repr(C)] +struct WINDOWCOMPOSITIONATTRIBDATA { + Attrib: WINDOWCOMPOSITIONATTRIB, + pvData: *mut c_void, + cbData: usize, +} + +fn refresh_titlebar_theme_color(hwnd: HWND) { + let dark = should_use_dark_mode() && is_dark_mode_allowed_for_window(hwnd); + let mut is_dark_mode_bigbool: BOOL = dark.into(); + if let Some(ver) = *WIN10_BUILD_VERSION { + if ver < 18362 { + unsafe { + SetPropW( + hwnd, + w!("UseImmersiveDarkModeColors"), + HANDLE(&mut is_dark_mode_bigbool as *mut _ as _), + ) + }; + } else if let Some(set_window_composition_attribute) = *SET_WINDOW_COMPOSITION_ATTRIBUTE { let mut data = WINDOWCOMPOSITIONATTRIBDATA { Attrib: WCA_USEDARKMODECOLORS, pvData: &mut is_dark_mode_bigbool as *mut _ as _, cbData: std::mem::size_of_val(&is_dark_mode_bigbool) as _, }; - - let status = set_window_composition_attribute(hwnd, &mut data as *mut _); - - status.as_bool() + unsafe { set_window_composition_attribute(hwnd, &mut data as *mut _) }; } - } else { - false } } @@ -154,36 +264,28 @@ fn should_use_dark_mode() -> bool { } fn should_apps_use_dark_mode() -> bool { + const UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL: u16 = 132; type ShouldAppsUseDarkMode = unsafe extern "system" fn() -> bool; - lazy_static! { - static ref SHOULD_APPS_USE_DARK_MODE: Option = { - unsafe { - const UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL: u16 = 132; - - let module = LoadLibraryA(s!("uxtheme.dll")).unwrap_or_default(); - - if module.is_invalid() { - return None; - } - - let handle = GetProcAddress( - module, - PCSTR::from_raw(UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL as usize as *mut _), - ); + static SHOULD_APPS_USE_DARK_MODE: Lazy> = Lazy::new(|| unsafe { + if HUXTHEME.is_invalid() { + return None; + } - handle.map(|handle| std::mem::transmute(handle)) - } - }; - } + GetProcAddress( + *HUXTHEME, + PCSTR::from_raw(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) } -const HCF_HIGHCONTRASTON: u32 = 1; - fn is_high_contrast() -> bool { + const HCF_HIGHCONTRASTON: u32 = 1; + let mut hc = HIGHCONTRASTA { cbSize: 0, dwFlags: Default::default(), diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index 369edef70..f0eb19008 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -51,7 +51,7 @@ use crate::{ keyboard::{KeyCode, ModifiersState}, monitor::MonitorHandle as RootMonitorHandle, platform_impl::platform::{ - dark_mode::try_theme, + dark_mode::{try_app_theme, try_window_theme}, dpi::{become_dpi_aware, dpi_to_scale_factor, enable_non_client_dpi_scaling}, keyboard::is_msg_keyboard_related, keyboard_layout::LAYOUT_CACHE, @@ -62,7 +62,7 @@ use crate::{ window_state::{CursorFlags, WindowFlags, WindowState}, wrap_device_id, WindowId, DEVICE_ID, }, - window::{Fullscreen, WindowId as RootWindowId}, + window::{Fullscreen, Theme, WindowId as RootWindowId}, }; use runner::{EventLoopRunner, EventLoopRunnerShared}; @@ -142,6 +142,7 @@ pub(crate) struct PlatformSpecificEventLoopAttributes { pub(crate) any_thread: bool, pub(crate) dpi_aware: bool, pub(crate) msg_hook: Option bool + 'static>>, + pub(crate) preferred_theme: Option, } impl Default for PlatformSpecificEventLoopAttributes { @@ -150,6 +151,7 @@ impl Default for PlatformSpecificEventLoopAttributes { any_thread: false, dpi_aware: true, msg_hook: None, + preferred_theme: None, } } } @@ -158,6 +160,7 @@ impl Default for PlatformSpecificEventLoopAttributes { pub struct EventLoopWindowTarget { thread_id: u32, thread_msg_target: HWND, + pub(crate) theme: Theme, pub(crate) runner_shared: EventLoopRunnerShared, } @@ -180,6 +183,8 @@ impl EventLoop { let thread_msg_target = create_event_target_window(); + let theme = try_app_theme(attributes.preferred_theme); + let send_thread_msg_target = thread_msg_target; thread::spawn(move || wait_thread(thread_id, send_thread_msg_target)); let wait_thread_id = get_wait_thread_id(); @@ -196,6 +201,7 @@ impl EventLoop { thread_id, thread_msg_target, runner_shared, + theme, }, _marker: PhantomData, }, @@ -2039,7 +2045,7 @@ unsafe fn public_window_callback_inner( let preferred_theme = subclass_input.window_state.lock().preferred_theme; if preferred_theme.is_none() { - let new_theme = try_theme(window, preferred_theme); + let new_theme = try_window_theme(window, preferred_theme); let mut window_state = subclass_input.window_state.lock(); if window_state.current_theme != new_theme { diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index fc90570c9..428353dc4 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -42,7 +42,7 @@ use crate::{ icon::Icon, monitor::MonitorHandle as RootMonitorHandle, platform_impl::platform::{ - dark_mode::try_theme, + dark_mode::try_window_theme, dpi::{dpi_to_scale_factor, hwnd_dpi}, drop_handler::FileDropHandler, event_loop::{self, EventLoopWindowTarget, DESTROY_MSG_ID}, @@ -1064,7 +1064,10 @@ unsafe fn init( // If the system theme is dark, we need to set the window theme now // before we update the window flags (and possibly show the // window for the first time). - let current_theme = try_theme(real_window.0, attributes.preferred_theme); + let current_theme = try_window_theme( + real_window.0, + attributes.preferred_theme.or(Some(event_loop.theme)), + ); let window_state = { let window_state = WindowState::new( diff --git a/src/window.rs b/src/window.rs index 8fe6ec8c0..eb84c57be 100644 --- a/src/window.rs +++ b/src/window.rs @@ -497,6 +497,17 @@ impl WindowBuilder { } /// Forces a theme or uses the system settings if `None` was provided. + /// + /// ## Platform-specific: + /// + /// - **Windows**: It is recommended to always use the same theme used + /// in [`EventLoopBuilderExtWindows::with_theme`] for this method also + /// or use `None` so it automatically uses the theme used in [`EventLoopBuilderExtWindows::with_theme`] + /// or falls back to the system preference, because [`EventLoopBuilderExtWindows::with_theme`] changes + /// the theme for some controls like context menus which is app-wide and can't be changed by this method. + /// + /// [`EventLoopBuilderExtWindows::with_theme`]: crate::platform::windows::EventLoopBuilderExtWindows::with_theme + #[allow(rustdoc::broken_intra_doc_links)] #[inline] pub fn with_theme(mut self, theme: Option) -> WindowBuilder { self.window.preferred_theme = theme;