From d59eec4633a1ff10a0c22cdde6725dd14e83536f Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Sun, 22 Dec 2019 19:04:09 +0000 Subject: [PATCH] Add support for Windows Dark Mode (#1217) * Add support for Windows Dark Mode * Add is_dark_mode() getter to WindowExtWindows * Add WindowEvent::DarkModeChanged * Add support for dark mode in Windows 10 builds > 18362 * Change strategy for querying windows 10 build version * Drop window state before sending event Co-Authored-By: daxpedda * Change implementation of windows dark mode support * Expand supported range of windows 10 versions with dark mode * Use get_function! macro where possible * Minor style fixes * Improve documentation for ThemeChanged * Use `as` conversion for `BOOL` * Correct CHANGELOG entry for dark mode Co-authored-by: daxpedda Co-authored-by: Osspial --- CHANGELOG.md | 2 + FEATURES.md | 1 + src/event.rs | 10 +- src/platform/windows.rs | 8 + src/platform_impl/windows/dark_mode.rs | 216 ++++++++++++++++++++++ src/platform_impl/windows/event_loop.rs | 23 +++ src/platform_impl/windows/mod.rs | 1 + src/platform_impl/windows/window.rs | 19 +- src/platform_impl/windows/window_state.rs | 3 + src/window.rs | 6 + 10 files changed, 287 insertions(+), 2 deletions(-) create mode 100644 src/platform_impl/windows/dark_mode.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 76f52907..ddb3d629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ - On macOS, fix application not to terminate on `run_return`. - On Wayland, fix cursor icon updates on window borders when using CSD. - On Wayland, under mutter(GNOME Wayland), fix CSD being behind the status bar, when starting window in maximized mode. +- On Windows, theme the title bar according to whether the system theme is "Light" or "Dark". +- Added `WindowEvent::ThemeChanged` variant to handle changes to the system theme. Currently only implemented on Windows. - Changes to the `RedrawRequested` event (#1041): - `RedrawRequested` has been moved from `WindowEvent` to `Event`. - `EventsCleared` has been renamed to `MainEventsCleared`. diff --git a/FEATURES.md b/FEATURES.md index 7a64fcb4..3d439588 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -117,6 +117,7 @@ If your PR makes notable changes to Winit's features, please update this section * Setting the taskbar icon * Setting the parent window * `WS_EX_NOREDIRECTIONBITMAP` support +* Theme the title bar according to Windows 10 Dark Mode setting ### macOS * Window activation policy diff --git a/src/event.rs b/src/event.rs index 9e3a838d..c2bb0eef 100644 --- a/src/event.rs +++ b/src/event.rs @@ -10,7 +10,7 @@ use std::path::PathBuf; use crate::{ dpi::{LogicalPosition, LogicalSize}, platform_impl, - window::WindowId, + window::{Theme, WindowId}, }; /// Describes a generic event. @@ -222,6 +222,14 @@ pub enum WindowEvent { /// /// For more information about DPI in general, see the [`dpi`](crate::dpi) module. HiDpiFactorChanged(f64), + + /// The system window theme has changed. + /// + /// Applications might wish to react to this to change the theme of the content of the window + /// when the system changes the window theme. + /// + /// At the moment this is only supported on Windows. + ThemeChanged(Theme), } /// Identifier of an input device. diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 3ab9b1dd..6e43734c 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -77,6 +77,9 @@ pub trait WindowExtWindows { /// This sets `ICON_BIG`. A good ceiling here is 256x256. fn set_taskbar_icon(&self, taskbar_icon: Option); + + /// Whether the system theme is currently Windows 10's "Dark Mode". + fn is_dark_mode(&self) -> bool; } impl WindowExtWindows for Window { @@ -94,6 +97,11 @@ impl WindowExtWindows for Window { fn set_taskbar_icon(&self, taskbar_icon: Option) { self.window.set_taskbar_icon(taskbar_icon) } + + #[inline] + fn is_dark_mode(&self) -> bool { + self.window.is_dark_mode() + } } /// Additional methods on `WindowBuilder` that are specific to Windows. diff --git a/src/platform_impl/windows/dark_mode.rs b/src/platform_impl/windows/dark_mode.rs new file mode 100644 index 00000000..c50c4f21 --- /dev/null +++ b/src/platform_impl/windows/dark_mode.rs @@ -0,0 +1,216 @@ +/// 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 std::ffi::OsStr; +use std::os::windows::ffi::OsStrExt; + +use winapi::{ + shared::{ + basetsd::SIZE_T, + minwindef::{BOOL, DWORD, UINT, ULONG, WORD}, + ntdef::{LPSTR, NTSTATUS, NT_SUCCESS, PVOID, WCHAR}, + windef::HWND, + }, + um::{libloaderapi, uxtheme, winuser}, +}; + +lazy_static! { + static ref WIN10_BUILD_VERSION: Option = { + // FIXME: RtlGetVersion is a documented windows API, + // should be part of winapi! + + #[allow(non_snake_case)] + #[repr(C)] + struct OSVERSIONINFOW { + dwOSVersionInfoSize: ULONG, + dwMajorVersion: ULONG, + dwMinorVersion: ULONG, + dwBuildNumber: ULONG, + dwPlatformId: ULONG, + szCSDVersion: [WCHAR; 128], + } + + type RtlGetVersion = unsafe extern "system" fn (*mut OSVERSIONINFOW) -> NTSTATUS; + 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 _); + assert!(NT_SUCCESS(status)); + + if vi.dwMajorVersion == 10 && vi.dwMinorVersion == 0 { + Some(vi.dwBuildNumber) + } else { + None + } + } + } else { + None + } + }; + + 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 + } + }; + + static ref DARK_THEME_NAME: Vec = widestring("DarkMode_Explorer"); + static ref LIGHT_THEME_NAME: Vec = widestring(""); +} + +/// Attempt to set dark mode on a window, if necessary. +/// Returns true if dark mode was set, false if not. +pub fn try_dark_mode(hwnd: HWND) -> bool { + if *DARK_MODE_SUPPORTED { + let is_dark_mode = should_use_dark_mode(); + + let theme_name = if is_dark_mode { + DARK_THEME_NAME.as_ptr() + } else { + LIGHT_THEME_NAME.as_ptr() + }; + + unsafe { + assert_eq!( + 0, + uxtheme::SetWindowTheme(hwnd, theme_name as _, std::ptr::null()) + ); + + set_dark_mode_for_window(hwnd, is_dark_mode) + } + + is_dark_mode + } else { + false + } +} + +fn set_dark_mode_for_window(hwnd: HWND, is_dark_mode: 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; + + #[allow(non_snake_case)] + type WINDOWCOMPOSITIONATTRIB = u32; + const WCA_USEDARKMODECOLORS: WINDOWCOMPOSITIONATTRIB = 26; + + #[allow(non_snake_case)] + #[repr(C)] + struct WINDOWCOMPOSITIONATTRIBDATA { + Attrib: WINDOWCOMPOSITIONATTRIB, + pvData: PVOID, + cbData: SIZE_T, + } + + lazy_static! { + static ref SET_WINDOW_COMPOSITION_ATTRIBUTE: Option = + get_function!("user32.dll", SetWindowCompositionAttribute); + } + + 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 = is_dark_mode as BOOL; + + 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 _, + }; + + assert_eq!( + 1, + set_window_composition_attribute(hwnd, &mut data as *mut _) + ); + } + } +} + +fn should_use_dark_mode() -> bool { + should_apps_use_dark_mode() && !is_high_contrast() +} + +fn should_apps_use_dark_mode() -> bool { + type ShouldAppsUseDarkMode = unsafe extern "system" fn() -> bool; + lazy_static! { + static ref SHOULD_APPS_USE_DARK_MODE: Option = { + unsafe { + const UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL: WORD = 132; + + let module = libloaderapi::LoadLibraryA("uxtheme.dll\0".as_ptr() as _); + + if module.is_null() { + return None; + } + + let handle = libloaderapi::GetProcAddress( + module, + winuser::MAKEINTRESOURCEA(UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL), + ); + + if handle.is_null() { + None + } else { + Some(std::mem::transmute(handle)) + } + } + }; + } + + SHOULD_APPS_USE_DARK_MODE + .map(|should_apps_use_dark_mode| unsafe { (should_apps_use_dark_mode)() }) + .unwrap_or(false) +} + +// FIXME: This definition was missing from winapi. Can remove from +// here and use winapi once the following PR is released: +// https://github.com/retep998/winapi-rs/pull/815 +#[repr(C)] +#[allow(non_snake_case)] +struct HIGHCONTRASTA { + cbSize: UINT, + dwFlags: DWORD, + lpszDefaultScheme: LPSTR, +} + +const HCF_HIGHCONTRASTON: DWORD = 1; + +fn is_high_contrast() -> bool { + let mut hc = HIGHCONTRASTA { + cbSize: 0, + dwFlags: 0, + lpszDefaultScheme: std::ptr::null_mut(), + }; + + let ok = unsafe { + winuser::SystemParametersInfoA( + winuser::SPI_GETHIGHCONTRAST, + std::mem::size_of_val(&hc) as _, + &mut hc as *mut _ as _, + 0, + ) + }; + + (ok > 0) && ((HCF_HIGHCONTRASTON & hc.dwFlags) == 1) +} + +fn widestring(src: &'static str) -> Vec { + OsStr::new(src) + .encode_wide() + .chain(Some(0).into_iter()) + .collect() +} diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index 08b74204..c1abdcc7 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -48,6 +48,7 @@ use crate::{ event::{DeviceEvent, Event, Force, KeyboardInput, Touch, TouchPhase, WindowEvent}, event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootELW}, platform_impl::platform::{ + dark_mode::try_dark_mode, dpi::{ become_dpi_aware, dpi_to_scale_factor, enable_non_client_dpi_scaling, hwnd_scale_factor, }, @@ -1540,6 +1541,28 @@ unsafe extern "system" fn public_window_callback( 0 } + winuser::WM_SETTINGCHANGE => { + use crate::event::WindowEvent::ThemeChanged; + + let is_dark_mode = try_dark_mode(window); + let mut window_state = subclass_input.window_state.lock(); + let changed = window_state.is_dark_mode != is_dark_mode; + + if changed { + use crate::window::Theme::*; + let theme = if is_dark_mode { Dark } else { Light }; + + window_state.is_dark_mode = is_dark_mode; + mem::drop(window_state); + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: ThemeChanged(theme), + }); + } + + commctrl::DefSubclassProc(window, msg, wparam, lparam) + } + _ => { if msg == *DESTROY_MSG_ID { winuser::DestroyWindow(window); diff --git a/src/platform_impl/windows/mod.rs b/src/platform_impl/windows/mod.rs index 34e9327d..47bf7fb5 100644 --- a/src/platform_impl/windows/mod.rs +++ b/src/platform_impl/windows/mod.rs @@ -69,6 +69,7 @@ impl WindowId { #[macro_use] mod util; +mod dark_mode; mod dpi; mod drop_handler; mod event; diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index 452eac17..08e884d4 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -34,6 +34,7 @@ use crate::{ error::{ExternalError, NotSupportedError, OsError as RootOsError}, monitor::MonitorHandle as RootMonitorHandle, platform_impl::platform::{ + dark_mode::try_dark_mode, dpi::{dpi_to_scale_factor, hwnd_dpi}, drop_handler::FileDropHandler, event_loop::{self, EventLoopWindowTarget, DESTROY_MSG_ID, INITIAL_DPI_MSG_ID}, @@ -696,6 +697,11 @@ impl Window { pub fn set_ime_position(&self, _logical_spot: LogicalPosition) { unimplemented!(); } + + #[inline] + pub fn is_dark_mode(&self) -> bool { + self.window_state.lock().is_dark_mode + } } impl Drop for Window { @@ -903,8 +909,19 @@ unsafe fn init( window_flags.set(WindowFlags::VISIBLE, attributes.visible); window_flags.set(WindowFlags::MAXIMIZED, attributes.maximized); + // 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 dark_mode = try_dark_mode(real_window.0); + let window_state = { - let window_state = WindowState::new(&attributes, window_icon, taskbar_icon, dpi_factor); + let window_state = WindowState::new( + &attributes, + window_icon, + taskbar_icon, + dpi_factor, + dark_mode, + ); let window_state = Arc::new(Mutex::new(window_state)); WindowState::set_window_flags(window_state.lock(), real_window.0, |f| *f = window_flags); window_state diff --git a/src/platform_impl/windows/window_state.rs b/src/platform_impl/windows/window_state.rs index cee80263..97874e59 100644 --- a/src/platform_impl/windows/window_state.rs +++ b/src/platform_impl/windows/window_state.rs @@ -32,6 +32,7 @@ pub struct WindowState { /// Used to supress duplicate redraw attempts when calling `request_redraw` multiple /// times in `EventsCleared`. pub queued_out_of_band_redraw: bool, + pub is_dark_mode: bool, pub high_surrogate: Option, window_flags: WindowFlags, } @@ -98,6 +99,7 @@ impl WindowState { window_icon: Option, taskbar_icon: Option, dpi_factor: f64, + is_dark_mode: bool, ) -> WindowState { WindowState { mouse: MouseProperties { @@ -117,6 +119,7 @@ impl WindowState { fullscreen: None, queued_out_of_band_redraw: false, + is_dark_mode, high_surrogate: None, window_flags: WindowFlags::empty(), } diff --git a/src/window.rs b/src/window.rs index a6b25ad3..43885240 100644 --- a/src/window.rs +++ b/src/window.rs @@ -857,3 +857,9 @@ pub enum Fullscreen { Exclusive(VideoMode), Borderless(MonitorHandle), } + +#[derive(Clone, Debug, PartialEq)] +pub enum Theme { + Light, + Dark, +}