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]] [[package]]
name = "tao" name = "tao"
version = "0.30.0" version = "0.30.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a93f2c6b8fdaeb7f417bda89b5bc767999745c3052969664ae1fa65892deb7e" checksum = "06e48d7c56b3f7425d061886e8ce3b6acfab1993682ed70bef50fd133d721ee6"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"cocoa 0.26.0", "cocoa 0.26.0",

View File

@ -23,7 +23,7 @@ wry = { version = "0.44.0", default-features = false, features = [
"os-webview", "os-webview",
"linux-body", "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-runtime = { version = "2.0.0-rc.12", path = "../tauri-runtime" }
tauri-utils = { version = "2.0.0-rc.12", path = "../tauri-utils" } tauri-utils = { version = "2.0.0-rc.12", path = "../tauri-utils" }
raw-window-handle = "0.6" raw-window-handle = "0.6"

View File

@ -1204,6 +1204,7 @@ pub enum WindowMessage {
SetIgnoreCursorEvents(bool), SetIgnoreCursorEvents(bool),
SetProgressBar(ProgressBarState), SetProgressBar(ProgressBarState),
SetTitleBarStyle(tauri_utils::TitleBarStyle), SetTitleBarStyle(tauri_utils::TitleBarStyle),
SetTheme(Option<Theme>),
DragWindow, DragWindow,
ResizeDragWindow(tauri_runtime::ResizeDirection), ResizeDragWindow(tauri_runtime::ResizeDirection),
RequestRedraw, RequestRedraw,
@ -2026,6 +2027,13 @@ impl<T: UserEvent> WindowDispatch<T> for WryWindowDispatcher<T> {
Message::Window(self.window_id, WindowMessage::SetTitleBarStyle(style)), 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)] #[derive(Clone)]
@ -2286,6 +2294,18 @@ impl<T: UserEvent> RuntimeHandle<T> for WryHandle<T> {
.map_err(|_| Error::FailedToGetCursorPosition) .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")] #[cfg(target_os = "macos")]
fn show(&self) -> tauri_runtime::Result<()> { fn show(&self) -> tauri_runtime::Result<()> {
send_user_message( send_user_message(
@ -2564,6 +2584,14 @@ impl<T: UserEvent> Runtime<T> for Wry<T> {
.map_err(|_| Error::FailedToGetCursorPosition) .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")] #[cfg(target_os = "macos")]
fn set_activation_policy(&mut self, activation_policy: ActivationPolicy) { fn set_activation_policy(&mut self, activation_policy: ActivationPolicy) {
self 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 cursor_position(&self) -> Result<PhysicalPosition<f64>>;
fn set_theme(&self, theme: Option<Theme>);
/// Shows the application, but does not automatically focus it. /// Shows the application, but does not automatically focus it.
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
#[cfg_attr(docsrs, doc(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 cursor_position(&self) -> Result<PhysicalPosition<f64>>;
fn set_theme(&self, theme: Option<Theme>);
/// Sets the activation policy for the application. /// Sets the activation policy for the application.
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
#[cfg_attr(docsrs, doc(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. /// - **Linux / Windows / iOS / Android:** Unsupported.
fn set_title_bar_style(&self, style: tauri_utils::TitleBarStyle) -> Result<()>; 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_progress_bar", false),
("set_icon", false), ("set_icon", false),
("set_title_bar_style", false), ("set_title_bar_style", false),
("set_theme", false),
("toggle_maximize", false), ("toggle_maximize", false),
// internal // internal
("internal_toggle_maximize", true), ("internal_toggle_maximize", true),
@ -141,6 +142,7 @@ const PLUGINS: &[(&str, &[(&str, bool)])] = &[
("app_show", false), ("app_show", false),
("app_hide", false), ("app_hide", false),
("default_window_icon", 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> <tr>
<td> <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` `core:app:allow-tauri-version`
</td> </td>

View File

@ -1469,6 +1469,32 @@ Denies the set_skip_taskbar command without any pre-configured scope.
<tr> <tr>
<td> <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` `core:window:allow-set-title`
</td> </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. /// Returns the default window icon.
pub fn default_window_icon(&self) -> Option<&Image<'_>> { pub fn default_window_icon(&self) -> Option<&Image<'_>> {
self.manager.window.default_icon.as_ref() self.manager.window.default_icon.as_ref()

View File

@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use tauri_utils::Theme;
use crate::{ use crate::{
command, command,
plugin::{Builder, TauriPlugin}, 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> { pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("app") Builder::new("app")
.invoke_handler(crate::generate_handler![ .invoke_handler(crate::generate_handler![
@ -59,6 +66,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
app_show, app_show,
app_hide, app_hide,
default_window_icon, default_window_icon,
set_app_theme,
]) ])
.build() .build()
} }

View File

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

View File

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

View File

@ -1981,6 +1981,7 @@ tauri::Builder::default()
}) })
.map_err(Into::into) .map_err(Into::into)
} }
/// Sets the title bar style. **macOS only**. /// Sets the title bar style. **macOS only**.
pub fn set_title_bar_style(&self, style: tauri_utils::TitleBarStyle) -> crate::Result<()> { pub fn set_title_bar_style(&self, style: tauri_utils::TitleBarStyle) -> crate::Result<()> {
self self
@ -1989,6 +1990,35 @@ tauri::Builder::default()
.set_title_bar_style(style) .set_title_bar_style(style)
.map_err(Into::into) .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. /// Progress bar state.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,7 +19,8 @@
"build:api": "pnpm run --filter \"@tauri-apps/api\" build", "build:api": "pnpm run --filter \"@tauri-apps/api\" build",
"build:cli": "pnpm run --filter \"@tauri-apps/cli\" build", "build:cli": "pnpm run --filter \"@tauri-apps/cli\" build",
"build:cli:debug": "pnpm run --filter \"@tauri-apps/cli\" build:debug", "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": { "devDependencies": {
"prettier": "^3.3.3" "prettier": "^3.3.3"

View File

@ -4,6 +4,7 @@
import { invoke } from './core' import { invoke } from './core'
import { Image } from './image' import { Image } from './image'
import { Theme } from './window'
/** /**
* Application metadata and related APIs. * 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 // Listeners
/** /**