feat(core): add a new function to set theme dynamically (#10210)

closes #5279
This commit is contained in:
Tony 2024-09-24 10:18:53 +08:00 committed by GitHub
parent 8d22c0c814
commit 11db7be6c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 281 additions and 8 deletions

4
Cargo.lock generated
View File

@ -7153,9 +7153,9 @@ dependencies = [
[[package]]
name = "tao"
version = "0.30.0"
version = "0.30.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a93f2c6b8fdaeb7f417bda89b5bc767999745c3052969664ae1fa65892deb7e"
checksum = "06e48d7c56b3f7425d061886e8ce3b6acfab1993682ed70bef50fd133d721ee6"
dependencies = [
"bitflags 2.6.0",
"cocoa 0.26.0",

View File

@ -23,7 +23,7 @@ wry = { version = "0.44.0", default-features = false, features = [
"os-webview",
"linux-body",
] }
tao = { version = "0.30", default-features = false, features = ["rwh_06"] }
tao = { version = "0.30.2", default-features = false, features = ["rwh_06"] }
tauri-runtime = { version = "2.0.0-rc.12", path = "../tauri-runtime" }
tauri-utils = { version = "2.0.0-rc.12", path = "../tauri-utils" }
raw-window-handle = "0.6"

View File

@ -1204,6 +1204,7 @@ pub enum WindowMessage {
SetIgnoreCursorEvents(bool),
SetProgressBar(ProgressBarState),
SetTitleBarStyle(tauri_utils::TitleBarStyle),
SetTheme(Option<Theme>),
DragWindow,
ResizeDragWindow(tauri_runtime::ResizeDirection),
RequestRedraw,
@ -2026,6 +2027,13 @@ impl<T: UserEvent> WindowDispatch<T> for WryWindowDispatcher<T> {
Message::Window(self.window_id, WindowMessage::SetTitleBarStyle(style)),
)
}
fn set_theme(&self, theme: Option<Theme>) -> Result<()> {
send_user_message(
&self.context,
Message::Window(self.window_id, WindowMessage::SetTheme(theme)),
)
}
}
#[derive(Clone)]
@ -2286,6 +2294,18 @@ impl<T: UserEvent> RuntimeHandle<T> for WryHandle<T> {
.map_err(|_| Error::FailedToGetCursorPosition)
}
fn set_theme(&self, theme: Option<Theme>) {
self
.context
.main_thread
.window_target
.set_theme(match theme {
Some(Theme::Light) => Some(TaoTheme::Light),
Some(Theme::Dark) => Some(TaoTheme::Dark),
_ => None,
});
}
#[cfg(target_os = "macos")]
fn show(&self) -> tauri_runtime::Result<()> {
send_user_message(
@ -2564,6 +2584,14 @@ impl<T: UserEvent> Runtime<T> for Wry<T> {
.map_err(|_| Error::FailedToGetCursorPosition)
}
fn set_theme(&self, theme: Option<Theme>) {
self.event_loop.set_theme(match theme {
Some(Theme::Light) => Some(TaoTheme::Light),
Some(Theme::Dark) => Some(TaoTheme::Dark),
_ => None,
});
}
#[cfg(target_os = "macos")]
fn set_activation_policy(&mut self, activation_policy: ActivationPolicy) {
self
@ -2996,6 +3024,13 @@ fn handle_user_message<T: UserEvent>(
}
};
}
WindowMessage::SetTheme(theme) => {
window.set_theme(match theme {
Some(Theme::Light) => Some(TaoTheme::Light),
Some(Theme::Dark) => Some(TaoTheme::Dark),
_ => None,
});
}
}
}
}

View File

@ -311,6 +311,8 @@ pub trait RuntimeHandle<T: UserEvent>: Debug + Clone + Send + Sync + Sized + 'st
fn cursor_position(&self) -> Result<PhysicalPosition<f64>>;
fn set_theme(&self, theme: Option<Theme>);
/// Shows the application, but does not automatically focus it.
#[cfg(target_os = "macos")]
#[cfg_attr(docsrs, doc(cfg(target_os = "macos")))]
@ -402,6 +404,8 @@ pub trait Runtime<T: UserEvent>: Debug + Sized + 'static {
fn cursor_position(&self) -> Result<PhysicalPosition<f64>>;
fn set_theme(&self, theme: Option<Theme>);
/// Sets the activation policy for the application.
#[cfg(target_os = "macos")]
#[cfg_attr(docsrs, doc(cfg(target_os = "macos")))]
@ -802,4 +806,12 @@ pub trait WindowDispatch<T: UserEvent>: Debug + Clone + Send + Sync + Sized + 's
///
/// - **Linux / Windows / iOS / Android:** Unsupported.
fn set_title_bar_style(&self, style: tauri_utils::TitleBarStyle) -> Result<()>;
/// Sets the theme for this window.
///
/// ## Platform-specific
///
/// - **Linux / macOS**: Theme is app-wide and not specific to this window.
/// - **iOS / Android:** Unsupported.
fn set_theme(&self, theme: Option<Theme>) -> Result<()>;
}

View File

@ -105,6 +105,7 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[
("set_progress_bar", false),
("set_icon", false),
("set_title_bar_style", false),
("set_theme", false),
("toggle_maximize", false),
// internal
("internal_toggle_maximize", true),
@ -141,6 +142,7 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[
("app_show", false),
("app_hide", false),
("default_window_icon", false),
("set_app_theme", false),
],
),
(

View File

@ -122,6 +122,32 @@ Denies the name command without any pre-configured scope.
<tr>
<td>
`core:app:allow-set-app-theme`
</td>
<td>
Enables the set_app_theme command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:deny-set-app-theme`
</td>
<td>
Denies the set_app_theme command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:app:allow-tauri-version`
</td>

View File

@ -1469,6 +1469,32 @@ Denies the set_skip_taskbar command without any pre-configured scope.
<tr>
<td>
`core:window:allow-set-theme`
</td>
<td>
Enables the set_theme command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:window:deny-set-theme`
</td>
<td>
Denies the set_theme command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`core:window:allow-set-title`
</td>

File diff suppressed because one or more lines are too long

View File

@ -685,6 +685,31 @@ macro_rules! shared_app_impl {
})
}
/// Set the app theme.
pub fn set_theme(&self, theme: Option<Theme>) {
#[cfg(windows)]
for window in self.manager.windows().values() {
if let (Some(menu), Ok(hwnd)) = (window.menu(), window.hwnd()) {
let raw_hwnd = hwnd.0 as isize;
let _ = self.run_on_main_thread(move || {
let _ = unsafe {
menu.inner().set_theme_for_hwnd(
raw_hwnd,
theme
.map(crate::menu::map_to_menu_theme)
.unwrap_or(muda::MenuTheme::Auto),
)
};
});
};
}
match self.runtime() {
RuntimeOrDispatch::Runtime(h) => h.set_theme(theme),
RuntimeOrDispatch::RuntimeHandle(h) => h.set_theme(theme),
_ => unreachable!(),
}
}
/// Returns the default window icon.
pub fn default_window_icon(&self) -> Option<&Image<'_>> {
self.manager.window.default_icon.as_ref()

View File

@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use tauri_utils::Theme;
use crate::{
command,
plugin::{Builder, TauriPlugin},
@ -50,6 +52,11 @@ pub fn default_window_icon<R: Runtime>(
})
}
#[command(root = "crate")]
pub async fn set_app_theme<R: Runtime>(app: AppHandle<R>, theme: Option<Theme>) {
app.set_theme(theme);
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("app")
.invoke_handler(crate::generate_handler![
@ -59,6 +66,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
app_show,
app_hide,
default_window_icon,
set_app_theme,
])
.build()
}

View File

@ -243,6 +243,10 @@ impl<T: UserEvent> RuntimeHandle<T> for MockRuntimeHandle {
unimplemented!()
}
fn set_theme(&self, theme: Option<Theme>) {
unimplemented!()
}
/// Shows the application, but does not automatically focus it.
#[cfg(target_os = "macos")]
fn show(&self) -> Result<()> {
@ -955,6 +959,10 @@ impl<T: UserEvent> WindowDispatch<T> for MockWindowDispatcher {
) -> Result<()> {
Ok(())
}
fn set_theme(&self, theme: Option<Theme>) -> Result<()> {
Ok(())
}
}
#[derive(Debug, Clone)]
@ -1096,6 +1104,10 @@ impl<T: UserEvent> Runtime<T> for MockRuntime {
unimplemented!()
}
fn set_theme(&self, theme: Option<Theme>) {
unimplemented!()
}
#[cfg(target_os = "macos")]
#[cfg_attr(docsrs, doc(cfg(target_os = "macos")))]
fn set_activation_policy(&mut self, activation_policy: tauri_runtime::ActivationPolicy) {}

View File

@ -27,7 +27,10 @@ use crate::{
},
};
use serde::Serialize;
use tauri_utils::config::{WebviewUrl, WindowConfig};
use tauri_utils::{
config::{WebviewUrl, WindowConfig},
Theme,
};
use url::Url;
use crate::{
@ -1582,6 +1585,11 @@ impl<R: Runtime> WebviewWindow<R> {
pub fn set_title_bar_style(&self, style: tauri_utils::TitleBarStyle) -> crate::Result<()> {
self.webview.window().set_title_bar_style(style)
}
/// Set the window theme.
pub fn set_theme(&self, theme: Option<Theme>) -> crate::Result<()> {
self.webview.window().set_theme(theme)
}
}
/// Desktop webview setters and actions.

View File

@ -1981,6 +1981,7 @@ tauri::Builder::default()
})
.map_err(Into::into)
}
/// Sets the title bar style. **macOS only**.
pub fn set_title_bar_style(&self, style: tauri_utils::TitleBarStyle) -> crate::Result<()> {
self
@ -1989,6 +1990,35 @@ tauri::Builder::default()
.set_title_bar_style(style)
.map_err(Into::into)
}
/// Sets the theme for this window.
///
/// ## Platform-specific
///
/// - **Linux / macOS**: Theme is app-wide and not specific to this window.
/// - **iOS / Android:** Unsupported.
pub fn set_theme(&self, theme: Option<Theme>) -> crate::Result<()> {
self
.window
.dispatcher
.set_theme(theme)
.map_err(Into::<crate::Error>::into)?;
#[cfg(windows)]
if let (Some(menu), Ok(hwnd)) = (self.menu(), self.hwnd()) {
let raw_hwnd = hwnd.0 as isize;
self.run_on_main_thread(move || {
let _ = unsafe {
menu.inner().set_theme_for_hwnd(
raw_hwnd,
theme
.map(crate::menu::map_to_menu_theme)
.unwrap_or(muda::MenuTheme::Auto),
)
};
})?;
};
Ok(())
}
}
/// Progress bar state.

View File

@ -138,6 +138,7 @@ mod desktop_commands {
setter!(set_visible_on_all_workspaces, bool);
setter!(set_title_bar_style, TitleBarStyle);
setter!(set_size_constraints, WindowSizeConstraints);
setter!(set_theme, Option<Theme>);
#[command(root = "crate")]
pub async fn set_icon<R: Runtime>(
@ -287,6 +288,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
desktop_commands::set_icon,
desktop_commands::set_visible_on_all_workspaces,
desktop_commands::set_title_bar_style,
desktop_commands::set_theme,
desktop_commands::toggle_maximize,
desktop_commands::internal_toggle_maximize,
]);

View File

@ -20,6 +20,8 @@
"core:default",
"core:app:allow-app-hide",
"core:app:allow-app-show",
"core:app:allow-set-app-theme",
"core:window:allow-set-theme",
"core:window:allow-center",
"core:window:allow-request-user-attention",
"core:window:allow-set-resizable",

View File

@ -3,6 +3,7 @@
import { writable } from 'svelte/store'
import { invoke } from '@tauri-apps/api/core'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { setTheme } from '@tauri-apps/api/app'
import Welcome from './views/Welcome.svelte'
import Communication from './views/Communication.svelte'
@ -83,6 +84,7 @@
function toggleDark() {
isDark = !isDark
applyTheme(isDark)
setTheme(isDark ? 'dark' : 'light')
}
// Console

View File

@ -1,7 +1,9 @@
<script>
import { show, hide } from '@tauri-apps/api/app'
import { show, hide, setTheme } from '@tauri-apps/api/app'
export let onMessage
/** @type {import('@tauri-apps/api/window').Theme | 'auto'} */
let theme = 'auto'
function showApp() {
hideApp()
@ -20,6 +22,21 @@
.then(() => onMessage('Hide app'))
.catch(onMessage)
}
async function switchTheme() {
switch (theme) {
case 'dark':
theme = 'light'
break
case 'light':
theme = 'auto'
break
case 'auto':
theme = 'dark'
break
}
setTheme(theme === 'auto' ? null : theme)
}
</script>
<div>
@ -30,4 +47,5 @@
on:click={showApp}>Show</button
>
<button class="btn" id="hide" on:click={hideApp}>Hide</button>
<button class="btn" id="hide" on:click={switchTheme}>Switch Theme ({theme})</button>
</div>

View File

@ -123,6 +123,9 @@
let cursorIgnoreEvents = false
let windowTitle = 'Awesome Tauri Example!'
/** @type {import('@tauri-apps/api/window').Theme | 'auto'} */
let theme = 'auto'
let effects = []
let selectedEffect
let effectState
@ -206,6 +209,21 @@
await webviewMap[selectedWebview].requestUserAttention(null)
}
async function switchTheme() {
switch (theme) {
case 'dark':
theme = 'light'
break
case 'light':
theme = 'auto'
break
case 'auto':
theme = 'dark'
break
}
await webviewMap[selectedWebview].setTheme(theme === 'auto' ? null : theme)
}
async function updateProgressBar() {
webviewMap[selectedWebview]?.setProgressBar({
status: selectedProgressBarStatus,
@ -379,6 +397,7 @@
title="Minimizes the window, requests attention for 3s and then resets it"
>Request attention</button
>
<button class="btn" on:click={switchTheme}>Switch Theme ({theme})</button>
</div>
<div class="grid cols-[repeat(auto-fill,minmax(180px,1fr))]">
<label>

View File

@ -19,7 +19,8 @@
"build:api": "pnpm run --filter \"@tauri-apps/api\" build",
"build:cli": "pnpm run --filter \"@tauri-apps/cli\" build",
"build:cli:debug": "pnpm run --filter \"@tauri-apps/cli\" build:debug",
"test": "pnpm run -r build"
"test": "pnpm run -r test",
"example:api:dev": "pnpm run --filter \"api\" tauri dev"
},
"devDependencies": {
"prettier": "^3.3.3"

View File

@ -4,6 +4,7 @@
import { invoke } from './core'
import { Image } from './image'
import { Theme } from './window'
/**
* Application metadata and related APIs.
@ -101,4 +102,31 @@ async function defaultWindowIcon(): Promise<Image | null> {
)
}
export { getName, getVersion, getTauriVersion, show, hide, defaultWindowIcon }
/**
* Set app's theme, pass in `null` or `undefined` to follow system theme
*
* @example
* ```typescript
* import { setTheme } from '@tauri-apps/api/app';
* await setTheme('dark');
* ```
*
* #### Platform-specific
*
* - **iOS / Android:** Unsupported.
*
* @since 2.0.0
*/
async function setTheme(theme?: Theme | null): Promise<void> {
return invoke('plugin:app|set_app_theme', { theme })
}
export {
getName,
getVersion,
getTauriVersion,
show,
hide,
defaultWindowIcon,
setTheme
}

View File

@ -1676,6 +1676,23 @@ class Window {
})
}
/**
* Set window theme, pass in `null` or `undefined` to follow system theme
*
* #### Platform-specific
*
* - **Linux / macOS**: Theme is app-wide and not specific to this window.
* - **iOS / Android:** Unsupported.
*
* @since 2.0.0
*/
async setTheme(theme?: Theme | null): Promise<void> {
return invoke('plugin:window|set_theme', {
label: this.label,
value: theme
})
}
// Listeners
/**