diff --git a/.changes/set-theme.md b/.changes/set-theme.md new file mode 100644 index 000000000..99d4ee513 --- /dev/null +++ b/.changes/set-theme.md @@ -0,0 +1,5 @@ +--- +"tao": "patch" +--- + +Add a function `Window::set_theme` and `EventLoopWindowTarget::set_them`to set theme after window or event loop creation. diff --git a/examples/theme.rs b/examples/theme.rs index fafff8b52..b92ecd008 100644 --- a/examples/theme.rs +++ b/examples/theme.rs @@ -2,12 +2,13 @@ // Copyright 2021-2023 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 -#[allow(clippy::single_match)] +use tao::{event::KeyEvent, keyboard::KeyCode}; + fn main() { use tao::{ event::{Event, WindowEvent}, event_loop::{ControlFlow, EventLoop}, - window::WindowBuilder, + window::{Theme, WindowBuilder}, }; env_logger::init(); @@ -20,22 +21,30 @@ fn main() { .unwrap(); println!("Initial theme: {:?}", window.theme()); + println!("Press D for Dark Mode"); + println!("Press L for Light Mode"); + println!("Press A for Auto Mode"); event_loop.run(move |event, _, control_flow| { *control_flow = ControlFlow::Wait; match event { - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - Event::WindowEvent { - event: WindowEvent::ThemeChanged(theme), - window_id, - .. - } if window_id == window.id() => { - println!("Theme is changed: {:?}", theme) - } + Event::WindowEvent { event, .. } => match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::KeyboardInput { + event: KeyEvent { physical_key, .. }, + .. + } => match physical_key { + KeyCode::KeyD => window.set_theme(Some(Theme::Dark)), + KeyCode::KeyL => window.set_theme(Some(Theme::Light)), + KeyCode::KeyA => window.set_theme(None), + _ => {} + }, + WindowEvent::ThemeChanged(theme) => { + println!("Theme is changed: {theme:?}") + } + _ => (), + }, _ => (), } }); diff --git a/src/event_loop.rs b/src/event_loop.rs index dbc2146cc..6565dde06 100644 --- a/src/event_loop.rs +++ b/src/event_loop.rs @@ -17,8 +17,12 @@ use instant::Instant; use std::{error, fmt, marker::PhantomData, ops::Deref}; use crate::{ - dpi::PhysicalPosition, error::ExternalError, event::Event, monitor::MonitorHandle, platform_impl, - window::ProgressBarState, + dpi::PhysicalPosition, + error::ExternalError, + event::Event, + monitor::MonitorHandle, + platform_impl, + window::{ProgressBarState, Theme}, }; /// Provides a way to retrieve events from the system and from the windows that were registered to @@ -296,6 +300,25 @@ impl EventLoopWindowTarget { #[cfg(any(target_os = "linux", target_os = "macos"))] self.p.set_progress_bar(_progress) } + + /// Sets the theme for the application. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_theme(&self, theme: Option) { + #[cfg(any( + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + target_os = "macos", + ))] + self.p.set_theme(theme) + } } #[cfg(feature = "rwh_05")] diff --git a/src/platform_impl/linux/event_loop.rs b/src/platform_impl/linux/event_loop.rs index 33ed9f3d9..6d84587b5 100644 --- a/src/platform_impl/linux/event_loop.rs +++ b/src/platform_impl/linux/event_loop.rs @@ -21,6 +21,7 @@ use gtk::{ cairo, gdk, gio, glib::{self}, prelude::*, + Settings, }; use crate::{ @@ -33,7 +34,9 @@ use crate::{ keyboard::ModifiersState, monitor::MonitorHandle as RootMonitorHandle, platform_impl::platform::{device, DEVICE_ID}, - window::{CursorIcon, Fullscreen, ProgressBarState, ResizeDirection, WindowId as RootWindowId}, + window::{ + CursorIcon, Fullscreen, ProgressBarState, ResizeDirection, Theme, WindowId as RootWindowId, + }, }; use super::{ @@ -153,7 +156,17 @@ impl EventLoopWindowTarget { .window_requests_tx .send((WindowId::dummy(), WindowRequest::ProgressBarState(progress))) { - log::warn!("Fail to send update progress bar request: {}", e); + log::warn!("Fail to send update progress bar request: {e}"); + } + } + + #[inline] + pub fn set_theme(&self, theme: Option) { + if let Err(e) = self + .window_requests_tx + .send((WindowId::dummy(), WindowRequest::SetTheme(theme))) + { + log::warn!("Fail to send update theme request: {e}"); } } } @@ -392,6 +405,7 @@ impl EventLoop { }; } WindowRequest::ProgressBarState(_) => unreachable!(), + WindowRequest::SetTheme(_) => unreachable!(), WindowRequest::WireUpEvents { transparent, fullscreen, @@ -857,6 +871,14 @@ impl EventLoop { WindowRequest::ProgressBarState(state) => { taskbar.update(state); } + WindowRequest::SetTheme(theme) => { + if let Some(settings) = Settings::default() { + match theme { + Some(Theme::Dark) => settings.set_gtk_application_prefer_dark_theme(true), + Some(Theme::Light) | None => settings.set_gtk_application_prefer_dark_theme(false), + } + } + } _ => unreachable!(), } } diff --git a/src/platform_impl/linux/window.rs b/src/platform_impl/linux/window.rs index 63e027363..07a1ca9bc 100644 --- a/src/platform_impl/linux/window.rs +++ b/src/platform_impl/linux/window.rs @@ -65,7 +65,7 @@ pub struct Window { inner_size_constraints: RefCell, /// Draw event Sender draw_tx: crossbeam_channel::Sender, - preferred_theme: Option, + preferred_theme: RefCell>, } impl Window { @@ -306,7 +306,7 @@ impl Window { minimized, fullscreen: RefCell::new(attributes.fullscreen), inner_size_constraints: RefCell::new(attributes.inner_size_constraints), - preferred_theme, + preferred_theme: RefCell::new(preferred_theme), }; win.set_skip_taskbar(pl_attribs.skip_taskbar); @@ -385,7 +385,7 @@ impl Window { minimized, fullscreen: RefCell::new(None), inner_size_constraints: RefCell::new(WindowSizeConstraints::default()), - preferred_theme: None, + preferred_theme: RefCell::new(None), }; Ok(win) @@ -941,7 +941,7 @@ impl Window { } pub fn theme(&self) -> Theme { - if let Some(theme) = self.preferred_theme { + if let Some(theme) = *self.preferred_theme.borrow() { return theme; } @@ -954,6 +954,16 @@ impl Window { Theme::Light } + + pub fn set_theme(&self, theme: Option) { + *self.preferred_theme.borrow_mut() = theme; + if let Err(e) = self + .window_requests_tx + .send((WindowId::dummy(), WindowRequest::SetTheme(theme))) + { + log::warn!("Fail to send set theme request: {e}"); + } + } } // We need GtkWindow to initialize WebView, so we have to keep it in the field. @@ -992,6 +1002,7 @@ pub enum WindowRequest { }, SetVisibleOnAllWorkspaces(bool), ProgressBarState(ProgressBarState), + SetTheme(Option), } impl Drop for Window { diff --git a/src/platform_impl/macos/event_loop.rs b/src/platform_impl/macos/event_loop.rs index 790d743a1..5fd89b86a 100644 --- a/src/platform_impl/macos/event_loop.rs +++ b/src/platform_impl/macos/event_loop.rs @@ -37,9 +37,11 @@ use crate::{ util::{self, IdRef}, }, platform_impl::set_progress_indicator, - window::ProgressBarState, + window::{ProgressBarState, Theme}, }; +use super::window::set_ns_theme; + #[derive(Default)] pub struct PanicInfo { inner: Cell>>, @@ -120,6 +122,11 @@ impl EventLoopWindowTarget { pub fn set_progress_bar(&self, progress: ProgressBarState) { set_progress_indicator(progress); } + + #[inline] + pub fn set_theme(&self, theme: Option) { + set_ns_theme(theme) + } } pub struct EventLoop { diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index c59f71808..49d420ea5 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -337,17 +337,20 @@ pub(super) fn get_ns_theme() -> Theme { } } -pub(super) fn set_ns_theme(theme: Theme) { - let name = match theme { - Theme::Dark => "NSAppearanceNameDarkAqua", - Theme::Light => "NSAppearanceNameAqua", - }; +pub(super) fn set_ns_theme(theme: Option) { unsafe { let app_class = class!(NSApplication); let app: id = msg_send![app_class, sharedApplication]; let has_theme: BOOL = msg_send![app, respondsToSelector: sel!(effectiveAppearance)]; if has_theme == YES { - let name = NSString::alloc(nil).init_str(name); + let name = if let Some(theme) = theme { + NSString::alloc(nil).init_str(match theme { + Theme::Dark => "NSAppearanceNameDarkAqua", + Theme::Light => "NSAppearanceNameAqua", + }) + } else { + nil + }; let appearance: id = msg_send![class!(NSAppearance), appearanceNamed: name]; let _: () = msg_send![app, setAppearance: appearance]; } @@ -547,7 +550,7 @@ impl UnownedWindow { match cloned_preferred_theme { Some(theme) => { - set_ns_theme(theme); + set_ns_theme(Some(theme)); let mut state = window.shared_state.lock().unwrap(); state.current_theme = theme.clone(); } @@ -1417,6 +1420,12 @@ impl UnownedWindow { state.current_theme } + pub fn set_theme(&self, theme: Option) { + set_ns_theme(theme); + let mut state = self.shared_state.lock().unwrap(); + state.current_theme = theme.unwrap_or_else(get_ns_theme); + } + pub fn set_content_protection(&self, enabled: bool) { unsafe { let _: () = msg_send![*self.ns_window, setSharingType: !enabled as i32]; diff --git a/src/platform_impl/windows/dark_mode.rs b/src/platform_impl/windows/dark_mode.rs index 34f977642..b12f98b4b 100644 --- a/src/platform_impl/windows/dark_mode.rs +++ b/src/platform_impl/windows/dark_mode.rs @@ -9,7 +9,8 @@ use once_cell::sync::Lazy; use windows::{ core::{s, w, PCSTR, PSTR}, Win32::{ - Foundation::{BOOL, HANDLE, HMODULE, HWND}, + Foundation::{BOOL, HANDLE, HMODULE, HWND, WPARAM}, + Graphics::Dwm::{DwmSetWindowAttribute, DWMWINDOWATTRIBUTE}, System::LibraryLoader::*, UI::{Accessibility::*, WindowsAndMessaging::*}, }, @@ -160,26 +161,10 @@ pub fn allow_dark_mode_for_window(hwnd: HWND, is_dark_mode: bool) { } } -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, is_dark_mode: bool) { - // SetWindowCompositionAttribute needs a bigbool (i32), not bool. - let mut is_dark_mode_bigbool: i32 = is_dark_mode.into(); - if let Some(ver) = *WIN10_BUILD_VERSION { - if ver < 18362 { + if ver < 17763 { + let mut is_dark_mode_bigbool: i32 = is_dark_mode.into(); unsafe { let _ = SetPropW( hwnd, @@ -187,13 +172,24 @@ fn refresh_titlebar_theme_color(hwnd: HWND, is_dark_mode: bool) { 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 _, + } else { + // https://github.com/MicrosoftDocs/sdk-api/pull/966/files + let dwmwa_use_immersive_dark_mode = if ver > 18985 { + DWMWINDOWATTRIBUTE(20) + } else { + DWMWINDOWATTRIBUTE(19) }; - let _ = unsafe { set_window_composition_attribute(hwnd, &mut data as *mut _) }; + let dark_mode = BOOL::from(is_dark_mode); + unsafe { + let _ = DwmSetWindowAttribute( + hwnd, + dwmwa_use_immersive_dark_mode, + &dark_mode as *const BOOL as *const c_void, + std::mem::size_of::() as u32, + ); + } + unsafe { DefWindowProcW(hwnd, WM_NCACTIVATE, None, None) }; + unsafe { DefWindowProcW(hwnd, WM_NCACTIVATE, WPARAM(true.into()), None) }; } } } diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index a64ac864a..7007ac89f 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -106,7 +106,7 @@ pub(crate) struct SubclassInput { pub _file_drop_handler: Option, pub subclass_removed: Cell, pub recurse_depth: Cell, - pub event_loop_preferred_theme: Option, + pub event_loop_preferred_theme: Arc>>, } impl SubclassInput { @@ -161,7 +161,7 @@ impl Default for PlatformSpecificEventLoopAttributes { pub struct EventLoopWindowTarget { thread_id: u32, thread_msg_target: HWND, - pub(crate) preferred_theme: Option, + pub(crate) preferred_theme: Arc>>, pub(crate) runner_shared: EventLoopRunnerShared, } @@ -202,7 +202,7 @@ impl EventLoop { thread_id, thread_msg_target, runner_shared, - preferred_theme: attributes.preferred_theme, + preferred_theme: Arc::new(Mutex::new(attributes.preferred_theme)), }, _marker: PhantomData, }, @@ -330,6 +330,14 @@ impl EventLoopWindowTarget { pub fn cursor_position(&self) -> Result, ExternalError> { util::cursor_position().map_err(Into::into) } + + #[inline] + pub fn set_theme(&self, theme: Option) { + *self.preferred_theme.lock() = theme; + self.runner_shared.owned_windows(|window| { + let _ = unsafe { SendMessageW(window, WM_SETTINGCHANGE, WPARAM(0), LPARAM(0)) }; + }); + } } fn main_thread_id() -> u32 { @@ -1193,7 +1201,7 @@ unsafe fn public_window_callback_inner( let windowpos = lparam.0 as *const WINDOWPOS; if (*windowpos).flags & SWP_NOMOVE != SWP_NOMOVE { - let physical_position = PhysicalPosition::new((*windowpos).x as i32, (*windowpos).y as i32); + let physical_position = PhysicalPosition::new((*windowpos).x, (*windowpos).y); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window.0 as _)), event: Moved(physical_position), @@ -1418,7 +1426,7 @@ unsafe fn public_window_callback_inner( win32wm::WM_RBUTTONDOWN => { use crate::event::{ElementState::Pressed, MouseButton::Right, WindowEvent::MouseInput}; - capture_mouse(window, &mut *subclass_input.window_state.lock()); + capture_mouse(window, &mut subclass_input.window_state.lock()); let modifiers = update_modifiers(window, subclass_input); @@ -2063,26 +2071,23 @@ unsafe fn public_window_callback_inner( result = ProcResult::Value(LRESULT(0)); } - win32wm::WM_WININICHANGE => { + win32wm::WM_SETTINGCHANGE => { use crate::event::WindowEvent::ThemeChanged; let preferred_theme = subclass_input.window_state.lock().preferred_theme; + let new_theme = try_window_theme( + window, + preferred_theme.or(*subclass_input.event_loop_preferred_theme.lock()), + ); + let mut window_state = subclass_input.window_state.lock(); - if preferred_theme.is_none() { - let new_theme = try_window_theme( - window, - preferred_theme.or(subclass_input.event_loop_preferred_theme), - ); - let mut window_state = subclass_input.window_state.lock(); - - if window_state.current_theme != new_theme { - window_state.current_theme = new_theme; - mem::drop(window_state); - subclass_input.send_event(Event::WindowEvent { - window_id: RootWindowId(WindowId(window.0 as _)), - event: ThemeChanged(new_theme), - }); - } + if window_state.current_theme != new_theme { + window_state.current_theme = new_theme; + mem::drop(window_state); + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: ThemeChanged(new_theme), + }); } } diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index e61c5cc33..a3a1978c6 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -133,7 +133,7 @@ impl Window { _file_drop_handler: file_drop_handler, subclass_removed: Cell::new(false), recurse_depth: Cell::new(0), - event_loop_preferred_theme: event_loop.preferred_theme, + event_loop_preferred_theme: event_loop.preferred_theme.clone(), }; event_loop::subclass_window(win.window.0, subclass_input); @@ -196,7 +196,7 @@ impl Window { #[inline] pub fn outer_position(&self) -> Result, NotSupportedError> { unsafe { util::get_window_rect(self.window.0) } - .map(|rect| Ok(PhysicalPosition::new(rect.left as i32, rect.top as i32))) + .map(|rect| Ok(PhysicalPosition::new(rect.left, rect.top))) .expect("Unexpected GetWindowRect failure") } @@ -206,7 +206,7 @@ impl Window { if !unsafe { ClientToScreen(self.window.0, &mut position) }.as_bool() { panic!("Unexpected ClientToScreen failure") } - Ok(PhysicalPosition::new(position.x as i32, position.y as i32)) + Ok(PhysicalPosition::new(position.x, position.y)) } #[inline] @@ -225,8 +225,8 @@ impl Window { let _ = SetWindowPos( self.window.0, HWND::default(), - x as i32, - y as i32, + x, + y, 0, 0, SWP_ASYNCWINDOWPOS | SWP_NOZORDER | SWP_NOSIZE | SWP_NOACTIVATE, @@ -924,6 +924,17 @@ impl Window { self.window_state.lock().current_theme } + pub fn set_theme(&self, theme: Option) { + { + let mut window_state = self.window_state.lock(); + if window_state.preferred_theme == theme { + return; + } + window_state.preferred_theme = theme; + } + unsafe { SendMessageW(self.hwnd(), WM_SETTINGCHANGE, WPARAM(0), LPARAM(0)) }; + } + #[inline] pub fn reset_dead_keys(&self) { // `ToUnicode` consumes the dead-key by default, so we are constructing a fake (but valid) @@ -1141,7 +1152,9 @@ unsafe fn init( // window for the first time). let current_theme = try_window_theme( real_window.0, - attributes.preferred_theme.or(event_loop.preferred_theme), + attributes + .preferred_theme + .or(*event_loop.preferred_theme.lock()), ); let window_state = { diff --git a/src/window.rs b/src/window.rs index 1a59e5c76..3b9d57c60 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1106,6 +1106,26 @@ impl Window { self.window.theme() } + /// Sets the theme for this window. + /// + /// ## Platform-specific + /// + /// - **Linux / macOS**: Theme is app-wide and not specific to this window. + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_theme(&self, #[allow(unused)] theme: Option) { + #[cfg(any( + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + target_os = "macos", + ))] + self.window.set_theme(theme) + } + /// Prevents the window contents from being captured by other apps. /// /// ## Platform-specific @@ -1401,18 +1421,13 @@ pub enum Fullscreen { } #[non_exhaustive] -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Default, Clone, Copy, Debug, PartialEq)] pub enum Theme { + #[default] Light, Dark, } -impl Default for Theme { - fn default() -> Self { - Theme::Light - } -} - #[non_exhaustive] #[derive(Debug, Clone, Copy, PartialEq)] pub enum UserAttentionType {