Skip to content

Commit 1a085ad

Browse files
feat: add a function to set theme dynamically (#937)
1 parent ad652e5 commit 1a085ad

File tree

11 files changed

+203
-88
lines changed

11 files changed

+203
-88
lines changed

.changes/set-theme.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tao": "patch"
3+
---
4+
5+
Add a function `Window::set_theme` and `EventLoopWindowTarget::set_them`to set theme after window or event loop creation.

examples/theme.rs

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
// Copyright 2021-2023 Tauri Programme within The Commons Conservancy
33
// SPDX-License-Identifier: Apache-2.0
44

5-
#[allow(clippy::single_match)]
5+
use tao::{event::KeyEvent, keyboard::KeyCode};
6+
67
fn main() {
78
use tao::{
89
event::{Event, WindowEvent},
910
event_loop::{ControlFlow, EventLoop},
10-
window::WindowBuilder,
11+
window::{Theme, WindowBuilder},
1112
};
1213

1314
env_logger::init();
@@ -20,22 +21,30 @@ fn main() {
2021
.unwrap();
2122

2223
println!("Initial theme: {:?}", window.theme());
24+
println!("Press D for Dark Mode");
25+
println!("Press L for Light Mode");
26+
println!("Press A for Auto Mode");
2327

2428
event_loop.run(move |event, _, control_flow| {
2529
*control_flow = ControlFlow::Wait;
2630

2731
match event {
28-
Event::WindowEvent {
29-
event: WindowEvent::CloseRequested,
30-
..
31-
} => *control_flow = ControlFlow::Exit,
32-
Event::WindowEvent {
33-
event: WindowEvent::ThemeChanged(theme),
34-
window_id,
35-
..
36-
} if window_id == window.id() => {
37-
println!("Theme is changed: {:?}", theme)
38-
}
32+
Event::WindowEvent { event, .. } => match event {
33+
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
34+
WindowEvent::KeyboardInput {
35+
event: KeyEvent { physical_key, .. },
36+
..
37+
} => match physical_key {
38+
KeyCode::KeyD => window.set_theme(Some(Theme::Dark)),
39+
KeyCode::KeyL => window.set_theme(Some(Theme::Light)),
40+
KeyCode::KeyA => window.set_theme(None),
41+
_ => {}
42+
},
43+
WindowEvent::ThemeChanged(theme) => {
44+
println!("Theme is changed: {theme:?}")
45+
}
46+
_ => (),
47+
},
3948
_ => (),
4049
}
4150
});

src/event_loop.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@ use instant::Instant;
1717
use std::{error, fmt, marker::PhantomData, ops::Deref};
1818

1919
use crate::{
20-
dpi::PhysicalPosition, error::ExternalError, event::Event, monitor::MonitorHandle, platform_impl,
21-
window::ProgressBarState,
20+
dpi::PhysicalPosition,
21+
error::ExternalError,
22+
event::Event,
23+
monitor::MonitorHandle,
24+
platform_impl,
25+
window::{ProgressBarState, Theme},
2226
};
2327

2428
/// Provides a way to retrieve events from the system and from the windows that were registered to
@@ -296,6 +300,25 @@ impl<T> EventLoopWindowTarget<T> {
296300
#[cfg(any(target_os = "linux", target_os = "macos"))]
297301
self.p.set_progress_bar(_progress)
298302
}
303+
304+
/// Sets the theme for the application.
305+
///
306+
/// ## Platform-specific
307+
///
308+
/// - **iOS / Android:** Unsupported.
309+
#[inline]
310+
pub fn set_theme(&self, theme: Option<Theme>) {
311+
#[cfg(any(
312+
windows,
313+
target_os = "linux",
314+
target_os = "dragonfly",
315+
target_os = "freebsd",
316+
target_os = "netbsd",
317+
target_os = "openbsd",
318+
target_os = "macos",
319+
))]
320+
self.p.set_theme(theme)
321+
}
299322
}
300323

301324
#[cfg(feature = "rwh_05")]

src/platform_impl/linux/event_loop.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use gtk::{
2121
cairo, gdk, gio,
2222
glib::{self},
2323
prelude::*,
24+
Settings,
2425
};
2526

2627
use crate::{
@@ -33,7 +34,9 @@ use crate::{
3334
keyboard::ModifiersState,
3435
monitor::MonitorHandle as RootMonitorHandle,
3536
platform_impl::platform::{device, DEVICE_ID},
36-
window::{CursorIcon, Fullscreen, ProgressBarState, ResizeDirection, WindowId as RootWindowId},
37+
window::{
38+
CursorIcon, Fullscreen, ProgressBarState, ResizeDirection, Theme, WindowId as RootWindowId,
39+
},
3740
};
3841

3942
use super::{
@@ -153,7 +156,17 @@ impl<T> EventLoopWindowTarget<T> {
153156
.window_requests_tx
154157
.send((WindowId::dummy(), WindowRequest::ProgressBarState(progress)))
155158
{
156-
log::warn!("Fail to send update progress bar request: {}", e);
159+
log::warn!("Fail to send update progress bar request: {e}");
160+
}
161+
}
162+
163+
#[inline]
164+
pub fn set_theme(&self, theme: Option<Theme>) {
165+
if let Err(e) = self
166+
.window_requests_tx
167+
.send((WindowId::dummy(), WindowRequest::SetTheme(theme)))
168+
{
169+
log::warn!("Fail to send update theme request: {e}");
157170
}
158171
}
159172
}
@@ -392,6 +405,7 @@ impl<T: 'static> EventLoop<T> {
392405
};
393406
}
394407
WindowRequest::ProgressBarState(_) => unreachable!(),
408+
WindowRequest::SetTheme(_) => unreachable!(),
395409
WindowRequest::WireUpEvents {
396410
transparent,
397411
fullscreen,
@@ -857,6 +871,14 @@ impl<T: 'static> EventLoop<T> {
857871
WindowRequest::ProgressBarState(state) => {
858872
taskbar.update(state);
859873
}
874+
WindowRequest::SetTheme(theme) => {
875+
if let Some(settings) = Settings::default() {
876+
match theme {
877+
Some(Theme::Dark) => settings.set_gtk_application_prefer_dark_theme(true),
878+
Some(Theme::Light) | None => settings.set_gtk_application_prefer_dark_theme(false),
879+
}
880+
}
881+
}
860882
_ => unreachable!(),
861883
}
862884
}

src/platform_impl/linux/window.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ pub struct Window {
6565
inner_size_constraints: RefCell<WindowSizeConstraints>,
6666
/// Draw event Sender
6767
draw_tx: crossbeam_channel::Sender<WindowId>,
68-
preferred_theme: Option<Theme>,
68+
preferred_theme: RefCell<Option<Theme>>,
6969
}
7070

7171
impl Window {
@@ -306,7 +306,7 @@ impl Window {
306306
minimized,
307307
fullscreen: RefCell::new(attributes.fullscreen),
308308
inner_size_constraints: RefCell::new(attributes.inner_size_constraints),
309-
preferred_theme,
309+
preferred_theme: RefCell::new(preferred_theme),
310310
};
311311

312312
win.set_skip_taskbar(pl_attribs.skip_taskbar);
@@ -385,7 +385,7 @@ impl Window {
385385
minimized,
386386
fullscreen: RefCell::new(None),
387387
inner_size_constraints: RefCell::new(WindowSizeConstraints::default()),
388-
preferred_theme: None,
388+
preferred_theme: RefCell::new(None),
389389
};
390390

391391
Ok(win)
@@ -941,7 +941,7 @@ impl Window {
941941
}
942942

943943
pub fn theme(&self) -> Theme {
944-
if let Some(theme) = self.preferred_theme {
944+
if let Some(theme) = *self.preferred_theme.borrow() {
945945
return theme;
946946
}
947947

@@ -954,6 +954,16 @@ impl Window {
954954

955955
Theme::Light
956956
}
957+
958+
pub fn set_theme(&self, theme: Option<Theme>) {
959+
*self.preferred_theme.borrow_mut() = theme;
960+
if let Err(e) = self
961+
.window_requests_tx
962+
.send((WindowId::dummy(), WindowRequest::SetTheme(theme)))
963+
{
964+
log::warn!("Fail to send set theme request: {e}");
965+
}
966+
}
957967
}
958968

959969
// We need GtkWindow to initialize WebView, so we have to keep it in the field.
@@ -992,6 +1002,7 @@ pub enum WindowRequest {
9921002
},
9931003
SetVisibleOnAllWorkspaces(bool),
9941004
ProgressBarState(ProgressBarState),
1005+
SetTheme(Option<Theme>),
9951006
}
9961007

9971008
impl Drop for Window {

src/platform_impl/macos/event_loop.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@ use crate::{
3737
util::{self, IdRef},
3838
},
3939
platform_impl::set_progress_indicator,
40-
window::ProgressBarState,
40+
window::{ProgressBarState, Theme},
4141
};
4242

43+
use super::window::set_ns_theme;
44+
4345
#[derive(Default)]
4446
pub struct PanicInfo {
4547
inner: Cell<Option<Box<dyn Any + Send + 'static>>>,
@@ -120,6 +122,11 @@ impl<T: 'static> EventLoopWindowTarget<T> {
120122
pub fn set_progress_bar(&self, progress: ProgressBarState) {
121123
set_progress_indicator(progress);
122124
}
125+
126+
#[inline]
127+
pub fn set_theme(&self, theme: Option<Theme>) {
128+
set_ns_theme(theme)
129+
}
123130
}
124131

125132
pub struct EventLoop<T: 'static> {

src/platform_impl/macos/window.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -337,17 +337,20 @@ pub(super) fn get_ns_theme() -> Theme {
337337
}
338338
}
339339

340-
pub(super) fn set_ns_theme(theme: Theme) {
341-
let name = match theme {
342-
Theme::Dark => "NSAppearanceNameDarkAqua",
343-
Theme::Light => "NSAppearanceNameAqua",
344-
};
340+
pub(super) fn set_ns_theme(theme: Option<Theme>) {
345341
unsafe {
346342
let app_class = class!(NSApplication);
347343
let app: id = msg_send![app_class, sharedApplication];
348344
let has_theme: BOOL = msg_send![app, respondsToSelector: sel!(effectiveAppearance)];
349345
if has_theme == YES {
350-
let name = NSString::alloc(nil).init_str(name);
346+
let name = if let Some(theme) = theme {
347+
NSString::alloc(nil).init_str(match theme {
348+
Theme::Dark => "NSAppearanceNameDarkAqua",
349+
Theme::Light => "NSAppearanceNameAqua",
350+
})
351+
} else {
352+
nil
353+
};
351354
let appearance: id = msg_send![class!(NSAppearance), appearanceNamed: name];
352355
let _: () = msg_send![app, setAppearance: appearance];
353356
}
@@ -547,7 +550,7 @@ impl UnownedWindow {
547550

548551
match cloned_preferred_theme {
549552
Some(theme) => {
550-
set_ns_theme(theme);
553+
set_ns_theme(Some(theme));
551554
let mut state = window.shared_state.lock().unwrap();
552555
state.current_theme = theme.clone();
553556
}
@@ -1417,6 +1420,12 @@ impl UnownedWindow {
14171420
state.current_theme
14181421
}
14191422

1423+
pub fn set_theme(&self, theme: Option<Theme>) {
1424+
set_ns_theme(theme);
1425+
let mut state = self.shared_state.lock().unwrap();
1426+
state.current_theme = theme.unwrap_or_else(get_ns_theme);
1427+
}
1428+
14201429
pub fn set_content_protection(&self, enabled: bool) {
14211430
unsafe {
14221431
let _: () = msg_send![*self.ns_window, setSharingType: !enabled as i32];

src/platform_impl/windows/dark_mode.rs

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ use once_cell::sync::Lazy;
99
use windows::{
1010
core::{s, w, PCSTR, PSTR},
1111
Win32::{
12-
Foundation::{BOOL, HANDLE, HMODULE, HWND},
12+
Foundation::{BOOL, HANDLE, HMODULE, HWND, WPARAM},
13+
Graphics::Dwm::{DwmSetWindowAttribute, DWMWINDOWATTRIBUTE},
1314
System::LibraryLoader::*,
1415
UI::{Accessibility::*, WindowsAndMessaging::*},
1516
},
@@ -160,40 +161,35 @@ pub fn allow_dark_mode_for_window(hwnd: HWND, is_dark_mode: bool) {
160161
}
161162
}
162163

163-
type SetWindowCompositionAttribute =
164-
unsafe extern "system" fn(HWND, *mut WINDOWCOMPOSITIONATTRIBDATA) -> BOOL;
165-
static SET_WINDOW_COMPOSITION_ATTRIBUTE: Lazy<Option<SetWindowCompositionAttribute>> =
166-
Lazy::new(|| get_function!("user32.dll", SetWindowCompositionAttribute));
167-
168-
type WINDOWCOMPOSITIONATTRIB = u32;
169-
const WCA_USEDARKMODECOLORS: WINDOWCOMPOSITIONATTRIB = 26;
170-
#[repr(C)]
171-
struct WINDOWCOMPOSITIONATTRIBDATA {
172-
Attrib: WINDOWCOMPOSITIONATTRIB,
173-
pvData: *mut c_void,
174-
cbData: usize,
175-
}
176-
177164
fn refresh_titlebar_theme_color(hwnd: HWND, is_dark_mode: bool) {
178-
// SetWindowCompositionAttribute needs a bigbool (i32), not bool.
179-
let mut is_dark_mode_bigbool: i32 = is_dark_mode.into();
180-
181165
if let Some(ver) = *WIN10_BUILD_VERSION {
182-
if ver < 18362 {
166+
if ver < 17763 {
167+
let mut is_dark_mode_bigbool: i32 = is_dark_mode.into();
183168
unsafe {
184169
let _ = SetPropW(
185170
hwnd,
186171
w!("UseImmersiveDarkModeColors"),
187172
HANDLE(&mut is_dark_mode_bigbool as *mut _ as _),
188173
);
189174
}
190-
} else if let Some(set_window_composition_attribute) = *SET_WINDOW_COMPOSITION_ATTRIBUTE {
191-
let mut data = WINDOWCOMPOSITIONATTRIBDATA {
192-
Attrib: WCA_USEDARKMODECOLORS,
193-
pvData: &mut is_dark_mode_bigbool as *mut _ as _,
194-
cbData: std::mem::size_of_val(&is_dark_mode_bigbool) as _,
175+
} else {
176+
// https://github.com/MicrosoftDocs/sdk-api/pull/966/files
177+
let dwmwa_use_immersive_dark_mode = if ver > 18985 {
178+
DWMWINDOWATTRIBUTE(20)
179+
} else {
180+
DWMWINDOWATTRIBUTE(19)
195181
};
196-
let _ = unsafe { set_window_composition_attribute(hwnd, &mut data as *mut _) };
182+
let dark_mode = BOOL::from(is_dark_mode);
183+
unsafe {
184+
let _ = DwmSetWindowAttribute(
185+
hwnd,
186+
dwmwa_use_immersive_dark_mode,
187+
&dark_mode as *const BOOL as *const c_void,
188+
std::mem::size_of::<BOOL>() as u32,
189+
);
190+
}
191+
unsafe { DefWindowProcW(hwnd, WM_NCACTIVATE, None, None) };
192+
unsafe { DefWindowProcW(hwnd, WM_NCACTIVATE, WPARAM(true.into()), None) };
197193
}
198194
}
199195
}

0 commit comments

Comments
 (0)