Explorar el Código

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

closes #5279
Tony hace 10 meses
padre
commit
11db7be6c2

+ 2 - 2
Cargo.lock

@@ -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",

+ 1 - 1
crates/tauri-runtime-wry/Cargo.toml

@@ -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"

+ 35 - 0
crates/tauri-runtime-wry/src/lib.rs

@@ -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,
+            });
+          }
         }
       }
     }

+ 12 - 0
crates/tauri-runtime/src/lib.rs

@@ -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<()>;
 }

+ 2 - 0
crates/tauri/build.rs

@@ -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),
     ],
   ),
   (

+ 26 - 0
crates/tauri/permissions/app/autogenerated/reference.md

@@ -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>

+ 26 - 0
crates/tauri/permissions/window/autogenerated/reference.md

@@ -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>

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
crates/tauri/scripts/bundle.global.js


+ 25 - 0
crates/tauri/src/app.rs

@@ -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()

+ 8 - 0
crates/tauri/src/app/plugin.rs

@@ -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()
 }

+ 12 - 0
crates/tauri/src/test/mock_runtime.rs

@@ -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) {}

+ 9 - 1
crates/tauri/src/webview/webview_window.rs

@@ -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.

+ 30 - 0
crates/tauri/src/window/mod.rs

@@ -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.

+ 2 - 0
crates/tauri/src/window/plugin.rs

@@ -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,
           ]);

+ 2 - 0
examples/api/src-tauri/capabilities/run-app.json

@@ -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",

+ 2 - 0
examples/api/src/App.svelte

@@ -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

+ 19 - 1
examples/api/src/views/App.svelte

@@ -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>

+ 19 - 0
examples/api/src/views/Window.svelte

@@ -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>

+ 2 - 1
package.json

@@ -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"

+ 29 - 1
packages/api/src/app.ts

@@ -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
+}

+ 17 - 0
packages/api/src/window.ts

@@ -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
 
   /**

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio