mirror of
https://github.com/tauri-apps/tauri.git
synced 2024-12-24 19:25:12 +03:00
feat(core): add a new function to set theme dynamically (#10210)
closes #5279
This commit is contained in:
parent
8d22c0c814
commit
11db7be6c2
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<()>;
|
||||
}
|
||||
|
@ -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),
|
||||
],
|
||||
),
|
||||
(
|
||||
|
@ -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>
|
||||
|
@ -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
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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) {}
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
]);
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user