Skip to content

Commit

Permalink
Support Windows
Browse files Browse the repository at this point in the history
  • Loading branch information
wyhaya committed Sep 13, 2024
1 parent af04cc2 commit 8b8c43c
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 56 deletions.
8 changes: 6 additions & 2 deletions tauri-v2/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "tauri-plugin-theme"
links = "tauri-plugin-theme"
version = "2.0.0"
version = "2.1.0"
edition = "2021"
license = "MIT"
repository = "https://github.com/wyhaya/tauri-plugin-theme"
Expand All @@ -26,4 +26,8 @@ futures-lite = "2.3"
tokio = { version = "1", features = ["macros"] }

[target."cfg(target_os = \"windows\")".dependencies]
dirs-next = "2.0"
webview2-com = "0.33"
windows = "0.58"
windows-core = "0.58"
windows-version = "0.1"

66 changes: 14 additions & 52 deletions tauri-v2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,59 +4,26 @@ use platform::set_theme;
use serde::{Deserialize, Serialize};
use std::fs;
use tauri::plugin::{Builder, TauriPlugin};
use tauri::{command, generate_handler, AppHandle, Config, Manager, Runtime};
use tauri::{command, generate_handler, AppHandle, Manager, RunEvent, Runtime};

const PLUGIN_NAME: &str = "theme";
const CONFIG_FILENAME: &str = "tauri-plugin-theme";
const ERROR_MESSAGE: &str = "Get app config dir failed";

#[cfg(target_os = "macos")]
pub fn init<R: Runtime>(_config: &mut Config) -> TauriPlugin<R> {
Builder::new(PLUGIN_NAME)
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("theme")
.invoke_handler(generate_handler![get_theme, set_theme])
.on_event(|app, e| {
if let tauri::RunEvent::Ready = e {
if let RunEvent::Ready = e {
let theme = saved_theme_value(&app);
let _ = set_theme(app.clone(), theme);
if let Err(err) = set_theme(app.clone(), theme) {
eprintln!("Failed to set theme: {}", err);
}
}
})
.build()
}

#[cfg(target_os = "linux")]
pub fn init<R: Runtime>(_config: &mut Config) -> TauriPlugin<R> {
Builder::new(PLUGIN_NAME)
.invoke_handler(generate_handler![get_theme, set_theme])
.setup(|app, _| {
let theme = saved_theme_value(&app);
let _ = set_theme(app.clone(), theme);
Ok(())
})
.build()
}

#[cfg(target_os = "windows")]
pub fn init<R: Runtime>(config: &mut Config) -> TauriPlugin<R> {
let theme = saved_theme_value_from_config(&config);
for window in &mut config.app.windows {
match theme {
Theme::Auto => window.theme = None,
Theme::Light => window.theme = Some(tauri::Theme::Light),
Theme::Dark => window.theme = Some(tauri::Theme::Dark),
}
}
Builder::new(PLUGIN_NAME)
.invoke_handler(generate_handler![get_theme, set_theme])
.build()
}

#[command]
fn get_theme<R: Runtime>(app: AppHandle<R>) -> Result<Theme, ()> {
let theme = saved_theme_value(&app);
Ok(theme)
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Theme {
Auto,
Expand Down Expand Up @@ -84,26 +51,21 @@ impl ToString for Theme {
}
}

#[cfg(target_os = "windows")]
fn saved_theme_value_from_config(config: &Config) -> Theme {
if let Some(dir) = dirs_next::config_dir() {
let p = dir.join(&config.identifier).join(CONFIG_FILENAME);
return fs::read_to_string(p)
.map(Theme::from)
.unwrap_or(Theme::Auto);
}
Theme::Auto
#[command]
fn get_theme<R: Runtime>(app: AppHandle<R>) -> Result<Theme, ()> {
let theme = saved_theme_value(&app);
Ok(theme)
}

pub(crate) fn saved_theme_value<R: Runtime>(app: &AppHandle<R>) -> Theme {
fn saved_theme_value<R: Runtime>(app: &AppHandle<R>) -> Theme {
let config_dir = app.path().app_config_dir().expect(ERROR_MESSAGE);
let p = config_dir.join(CONFIG_FILENAME);
fs::read_to_string(p)
.map(Theme::from)
.unwrap_or(Theme::Auto)
}

pub(crate) fn save_theme_value<R: Runtime>(app: &AppHandle<R>, theme: Theme) {
fn save_theme_value<R: Runtime>(app: &AppHandle<R>, theme: Theme) {
let config_dir = app.path().app_config_dir().expect(ERROR_MESSAGE);
if !config_dir.exists() {
let _ = std::fs::create_dir_all(&config_dir);
Expand Down
190 changes: 188 additions & 2 deletions tauri-v2/src/platform/windows.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,195 @@
use crate::{save_theme_value, Theme};
use tauri::{command, AppHandle, Runtime};
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::Arc;
use tauri::{command, AppHandle, Manager, Runtime};
use webview2_com::Microsoft::Web::WebView2::Win32::*;
use windows_core::Interface;

#[command]
pub fn set_theme<R: Runtime>(app: AppHandle<R>, theme: Theme) -> Result<(), &'static str> {
let err = Arc::new(AtomicU8::new(0));
for (_, window) in app.webview_windows() {
// Window
let hwnd = window.hwnd().map_err(|_| "Invalid window handle")?;
darkmode::try_window_theme(hwnd, theme, false);

// Webview
let (err, err2) = (err.clone(), err.clone());
let rst = window.with_webview(move |view| unsafe {
let controller = view.controller();
let coreview = match controller.CoreWebView2() {
Ok(coreview) => coreview,
Err(_) => {
err.store(1, Ordering::SeqCst);
return;
}
};
let webview = match coreview.cast::<ICoreWebView2_13>() {
Ok(webview) => webview,
Err(_) => {
err.store(2, Ordering::SeqCst);
return;
}
};
let profile = match webview.Profile() {
Ok(profile) => profile,
Err(_) => {
err.store(3, Ordering::SeqCst);
return;
}
};
let theme = match theme {
Theme::Dark => COREWEBVIEW2_PREFERRED_COLOR_SCHEME_DARK,
Theme::Light => COREWEBVIEW2_PREFERRED_COLOR_SCHEME_LIGHT,
Theme::Auto => COREWEBVIEW2_PREFERRED_COLOR_SCHEME_AUTO,
};
if let Err(_) = profile.SetPreferredColorScheme(theme) {
err.store(4, Ordering::SeqCst);
return;
}
});
if let Err(_) = rst {
return Err("Get webview error");
}
match err2.load(Ordering::SeqCst) {
1 => return Err("Get CoreWebView2 error"),
2 => return Err("Get ICoreWebView2_13 error"),
3 => return Err("Get webview.Profile error"),
4 => return Err("Set SetPreferredColorScheme error"),
_ => {}
}
}
save_theme_value(&app, theme);
app.restart();
Ok(())
}

// From: https://github.com/tauri-apps/tao/blob/2c6de758ac656c0621d20da7c1649adfb8847066/src/platform_impl/windows/dark_mode.rs
mod darkmode {
// Copyright 2014-2021 The winit contributors
// Copyright 2021-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0

use crate::Theme;
use std::ffi::c_void;
use std::sync::LazyLock;
/// 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, w, PCSTR, PSTR},
Win32::{
Foundation::{BOOL, HANDLE, HMODULE, HWND, WPARAM},
Graphics::Dwm::{DwmSetWindowAttribute, DWMWA_USE_IMMERSIVE_DARK_MODE},
System::LibraryLoader::*,
UI::{Accessibility::*, WindowsAndMessaging::*},
},
};

static HUXTHEME: LazyLock<isize> =
LazyLock::new(|| unsafe { LoadLibraryA(s!("uxtheme.dll")).unwrap_or_default().0 as _ });

static WIN10_BUILD_VERSION: LazyLock<Option<u32>> = LazyLock::new(|| {
let version = windows_version::OsVersion::current();
if version.major == 10 && version.minor == 0 {
Some(version.build)
} else {
None
}
});

static DARK_MODE_SUPPORTED: LazyLock<bool> = LazyLock::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,
}
});

/// Attempt to set a theme on a window, if necessary.
/// Returns the theme that was picked
pub fn try_window_theme(hwnd: HWND, preferred_theme: Theme, redraw_title_bar: bool) {
if *DARK_MODE_SUPPORTED {
let is_dark_mode = match preferred_theme {
Theme::Auto => should_use_dark_mode(),
Theme::Dark => true,
Theme::Light => false,
};
refresh_titlebar_theme_color(hwnd, is_dark_mode, redraw_title_bar);
}
}

fn refresh_titlebar_theme_color(hwnd: HWND, is_dark_mode: bool, redraw_title_bar: bool) {
if let Some(ver) = *WIN10_BUILD_VERSION {
if ver < 18362 {
let mut is_dark_mode_bigbool: i32 = is_dark_mode.into();
unsafe {
let _ = SetPropW(
hwnd,
w!("UseImmersiveDarkModeColors"),
HANDLE(&mut is_dark_mode_bigbool as *mut _ as _),
);
}
} else {
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::<BOOL>() as u32,
);
}
if redraw_title_bar {
unsafe { DefWindowProcW(hwnd, WM_NCACTIVATE, None, None) };
unsafe { DefWindowProcW(hwnd, WM_NCACTIVATE, WPARAM(true.into()), None) };
}
}
}
}

fn should_use_dark_mode() -> bool {
should_apps_use_dark_mode() && !is_high_contrast()
}

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: LazyLock<Option<ShouldAppsUseDarkMode>> =
LazyLock::new(|| unsafe {
if HMODULE(*HUXTHEME as _).is_invalid() {
return None;
}

GetProcAddress(
HMODULE(*HUXTHEME as _),
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)
}

fn is_high_contrast() -> bool {
const HCF_HIGHCONTRASTON: u32 = 1;

let mut hc = HIGHCONTRASTA {
cbSize: 0,
dwFlags: Default::default(),
lpszDefaultScheme: PSTR::null(),
};

let ok = unsafe {
SystemParametersInfoA(
SPI_GETHIGHCONTRAST,
std::mem::size_of_val(&hc) as _,
Some(&mut hc as *mut _ as _),
Default::default(),
)
};

ok.is_ok() && (HCF_HIGHCONTRASTON & hc.dwFlags.0) != 0
}
}

0 comments on commit 8b8c43c

Please sign in to comment.