Jelajahi Sumber

feat: background color APIs

closes #10519
closes #1564
amrbashir 9 bulan lalu
induk
melakukan
46a861bfa2

+ 12 - 5
Cargo.lock

@@ -8758,9 +8758,7 @@ dependencies = [
 
 [[package]]
 name = "tao"
-version = "0.30.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06e48d7c56b3f7425d061886e8ce3b6acfab1993682ed70bef50fd133d721ee6"
+version = "0.30.3"
 dependencies = [
  "bitflags 2.6.0",
  "cocoa 0.26.0",
@@ -8786,7 +8784,7 @@ dependencies = [
  "parking_lot",
  "raw-window-handle",
  "scopeguard",
- "tao-macros",
+ "tao-macros 0.1.3",
  "unicode-segmentation",
  "url",
  "windows",
@@ -8795,6 +8793,15 @@ dependencies = [
  "x11-dl",
 ]
 
+[[package]]
+name = "tao-macros"
+version = "0.1.3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.85",
+]
+
 [[package]]
 name = "tao-macros"
 version = "0.1.3"
@@ -11100,7 +11107,7 @@ dependencies = [
  "raw-window-handle",
  "sha2",
  "soup3",
- "tao-macros",
+ "tao-macros 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "thiserror",
  "tracing",
  "webkit2gtk",

+ 95 - 18
crates/tauri-cli/config.schema.json

@@ -486,6 +486,17 @@
           "description": "Whether browser extensions can be installed for the webview process\n\n ## Platform-specific:\n\n - **Windows**: Enables the WebView2 environment's [`AreBrowserExtensionsEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2environmentoptions?view=webview2-winrt-1.0.2739.15#arebrowserextensionsenabled)\n - **MacOS / Linux / iOS / Android** - Unsupported.",
           "default": false,
           "type": "boolean"
+        },
+        "backgroundColor": {
+          "description": "Set the window and webview background color.\n\n ## Platform-specific:\n\n - **Windows**: alpha channel is ignored for the window layer.\n - **Windows**: On Windows 7, alpha channel is ignored for the webview layer.\n - **Windows**: On Windows 8 and newer, if alpha channel is not `0`, it will be ignored for the webview layer.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Color"
+            },
+            {
+              "type": "null"
+            }
+          ]
         }
       },
       "additionalProperties": false
@@ -827,32 +838,98 @@
       ]
     },
     "Color": {
-      "description": "a tuple struct of RGBA colors. Each value has minimum of 0 and maximum of 255.",
-      "type": "array",
-      "items": [
+      "$ref": "#/definitions/InnerColor"
+    },
+    "InnerColor": {
+      "anyOf": [
         {
-          "type": "integer",
-          "format": "uint8",
-          "minimum": 0.0
+          "description": "A tuple of RGB colors. Each value has minimum of 0 and maximum of 255.",
+          "type": "array",
+          "items": [
+            {
+              "type": "integer",
+              "format": "uint8",
+              "minimum": 0.0
+            },
+            {
+              "type": "integer",
+              "format": "uint8",
+              "minimum": 0.0
+            },
+            {
+              "type": "integer",
+              "format": "uint8",
+              "minimum": 0.0
+            }
+          ],
+          "maxItems": 3,
+          "minItems": 3
         },
         {
-          "type": "integer",
-          "format": "uint8",
-          "minimum": 0.0
+          "description": "A tuple of RGBA colors. Each value has minimum of 0 and maximum of 255.",
+          "type": "array",
+          "items": [
+            {
+              "type": "integer",
+              "format": "uint8",
+              "minimum": 0.0
+            },
+            {
+              "type": "integer",
+              "format": "uint8",
+              "minimum": 0.0
+            },
+            {
+              "type": "integer",
+              "format": "uint8",
+              "minimum": 0.0
+            },
+            {
+              "type": "integer",
+              "format": "uint8",
+              "minimum": 0.0
+            }
+          ],
+          "maxItems": 4,
+          "minItems": 4
         },
         {
-          "type": "integer",
-          "format": "uint8",
-          "minimum": 0.0
+          "description": "An object of red, green, blue, alpha color values. Each value has minimum of 0 and maximum of 255.",
+          "type": "object",
+          "required": [
+            "blue",
+            "green",
+            "red"
+          ],
+          "properties": {
+            "red": {
+              "type": "integer",
+              "format": "uint8",
+              "minimum": 0.0
+            },
+            "green": {
+              "type": "integer",
+              "format": "uint8",
+              "minimum": 0.0
+            },
+            "blue": {
+              "type": "integer",
+              "format": "uint8",
+              "minimum": 0.0
+            },
+            "alpha": {
+              "default": 255,
+              "type": "integer",
+              "format": "uint8",
+              "minimum": 0.0
+            }
+          }
         },
         {
-          "type": "integer",
-          "format": "uint8",
-          "minimum": 0.0
+          "description": "A color hex string, for example: #fff, #ffffff, or #ffffffff.",
+          "type": "string"
         }
-      ],
-      "maxItems": 4,
-      "minItems": 4
+      ]
     },
     "SecurityConfig": {
       "description": "Security configuration.\n\n See more: <https://v2.tauri.app/reference/config/#securityconfig>",

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

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

+ 60 - 4
crates/tauri-runtime-wry/src/lib.rs

@@ -65,7 +65,10 @@ use tao::{
 };
 #[cfg(target_os = "macos")]
 use tauri_utils::TitleBarStyle;
-use tauri_utils::{config::WindowConfig, Theme};
+use tauri_utils::{
+  config::{Color, WindowConfig},
+  Theme,
+};
 use url::Url;
 use wry::{
   DragDropEvent as WryDragDropEvent, ProxyConfig, ProxyEndpoint, WebContext as WryWebContext,
@@ -819,6 +822,9 @@ impl WindowBuilder for WindowBuilderWrapper {
       if let Some(max_height) = config.max_height {
         constraints.max_height = Some(tao::dpi::LogicalUnit::new(max_height).into());
       }
+      if let Some(color) = config.background_color {
+        window = window.background_color(color);
+      }
       window = window.inner_size_constraints(constraints);
 
       if let (Some(x), Some(y)) = (config.x, config.y) {
@@ -1053,6 +1059,11 @@ impl WindowBuilder for WindowBuilderWrapper {
     Ok(self)
   }
 
+  fn background_color(mut self, color: Color) -> Self {
+    self.inner = self.inner.with_background_color(color.into());
+    self
+  }
+
   #[cfg(any(windows, target_os = "linux"))]
   fn skip_taskbar(mut self, skip: bool) -> Self {
     self.inner = self.inner.with_skip_taskbar(skip);
@@ -1218,6 +1229,7 @@ pub enum WindowMessage {
   SetProgressBar(ProgressBarState),
   SetTitleBarStyle(tauri_utils::TitleBarStyle),
   SetTheme(Option<Theme>),
+  SetBackgroundColor(Option<Color>),
   DragWindow,
   ResizeDragWindow(tauri_runtime::ResizeDirection),
   RequestRedraw,
@@ -1259,6 +1271,7 @@ pub enum WebviewMessage {
   Reparent(WindowId, Sender<Result<()>>),
   SetAutoResize(bool),
   SetZoom(f64),
+  SetBackgroundColor(Option<Color>),
   ClearAllBrowsingData,
   // Getters
   Url(Sender<Result<String>>),
@@ -1575,6 +1588,17 @@ impl<T: UserEvent> WebviewDispatch<T> for WryWebviewDispatcher<T> {
       ),
     )
   }
+
+  fn set_background_color(&self, color: Option<Color>) -> Result<()> {
+    send_user_message(
+      &self.context,
+      Message::Webview(
+        *self.window_id.lock().unwrap(),
+        self.webview_id,
+        WebviewMessage::SetBackgroundColor(color),
+      ),
+    )
+  }
 }
 
 /// The Tauri [`WindowDispatch`] for [`Wry`].
@@ -2087,6 +2111,13 @@ impl<T: UserEvent> WindowDispatch<T> for WryWindowDispatcher<T> {
       Message::Window(self.window_id, WindowMessage::SetTheme(theme)),
     )
   }
+
+  fn set_background_color(&self, color: Option<Color>) -> Result<()> {
+    send_user_message(
+      &self.context,
+      Message::Window(self.window_id, WindowMessage::SetBackgroundColor(color)),
+    )
+  }
 }
 
 #[derive(Clone)]
@@ -2135,6 +2166,8 @@ pub struct WindowWrapper {
   webviews: Vec<WebviewWrapper>,
   window_event_listeners: WindowEventListeners,
   #[cfg(windows)]
+  background_color: Option<tao::window::RGBA>,
+  #[cfg(windows)]
   is_window_transparent: bool,
   #[cfg(windows)]
   surface: Option<softbuffer::Surface<Arc<Window>, Arc<Window>>>,
@@ -3048,6 +3081,9 @@ fn handle_user_message<T: UserEvent>(
               _ => None,
             });
           }
+          WindowMessage::SetBackgroundColor(color) => {
+            window.set_background_color(color.map(Into::into))
+          }
         }
       }
     }
@@ -3243,6 +3279,13 @@ fn handle_user_message<T: UserEvent>(
               log::error!("failed to set webview zoom: {e}");
             }
           }
+          WebviewMessage::SetBackgroundColor(color) => {
+            if let Err(e) =
+              webview.set_background_color(color.map(Into::into).unwrap_or((255, 255, 255, 255)))
+            {
+              log::error!("failed to set webview background color: {e}");
+            }
+          }
           WebviewMessage::ClearAllBrowsingData => {
             if let Err(e) = webview.clear_all_browsing_data() {
               log::error!("failed to clear webview browsing data: {e}");
@@ -3411,6 +3454,8 @@ fn handle_user_message<T: UserEvent>(
     Message::CreateRawWindow(window_id, handler, sender) => {
       let (label, builder) = handler();
 
+      #[cfg(windows)]
+      let background_color = builder.window.background_color;
       #[cfg(windows)]
       let is_window_transparent = builder.window.transparent;
 
@@ -3423,7 +3468,7 @@ fn handle_user_message<T: UserEvent>(
         let surface = if is_window_transparent {
           if let Ok(context) = softbuffer::Context::new(window.clone()) {
             if let Ok(mut surface) = softbuffer::Surface::new(&context, window.clone()) {
-              window.clear_surface(&mut surface);
+              window.draw_surface(&mut surface, background_color);
               Some(surface)
             } else {
               None
@@ -3444,6 +3489,8 @@ fn handle_user_message<T: UserEvent>(
             window_event_listeners: Default::default(),
             webviews: Vec::new(),
             #[cfg(windows)]
+            background_color,
+            #[cfg(windows)]
             is_window_transparent,
             #[cfg(windows)]
             surface,
@@ -3507,9 +3554,10 @@ fn handle_event_loop<T: UserEvent>(
         let mut windows_ref = windows.0.borrow_mut();
         if let Some(window) = windows_ref.get_mut(&window_id) {
           if window.is_window_transparent {
+            let background_color = window.background_color;
             if let Some(surface) = &mut window.surface {
               if let Some(window) = &window.inner {
-                window.clear_surface(surface);
+                window.draw_surface(surface, background_color);
               }
             }
           }
@@ -3793,6 +3841,8 @@ fn create_window<T: UserEvent, F: Fn(RawWindow) + Send + 'static>(
 
   let window_event_listeners = WindowEventListeners::default();
 
+  #[cfg(windows)]
+  let background_color = window_builder.inner.window.background_color;
   #[cfg(windows)]
   let is_window_transparent = window_builder.inner.window.transparent;
 
@@ -3924,7 +3974,7 @@ fn create_window<T: UserEvent, F: Fn(RawWindow) + Send + 'static>(
   let surface = if is_window_transparent {
     if let Ok(context) = softbuffer::Context::new(window.clone()) {
       if let Ok(mut surface) = softbuffer::Surface::new(&context, window.clone()) {
-        window.clear_surface(&mut surface);
+        window.draw_surface(&mut surface, background_color);
         Some(surface)
       } else {
         None
@@ -3943,6 +3993,8 @@ fn create_window<T: UserEvent, F: Fn(RawWindow) + Send + 'static>(
     webviews,
     window_event_listeners,
     #[cfg(windows)]
+    background_color,
+    #[cfg(windows)]
     is_window_transparent,
     #[cfg(windows)]
     surface,
@@ -4025,6 +4077,10 @@ fn create_webview<T: UserEvent>(
     .with_clipboard(webview_attributes.clipboard)
     .with_hotkeys_zoom(webview_attributes.zoom_hotkeys_enabled);
 
+  if let Some(color) = webview_attributes.background_color {
+    webview_builder = webview_builder.with_background_color(color.into());
+  }
+
   if webview_attributes.drag_drop_handler_enabled {
     let proxy = context.proxy.clone();
     let window_id_ = window_id.clone();

+ 2 - 1
crates/tauri-runtime-wry/src/window/mod.rs

@@ -39,12 +39,13 @@ pub trait WindowExt {
 
   /// Clears the window sufrace. i.e make it it transparent.
   #[cfg(windows)]
-  fn clear_surface(
+  fn draw_surface(
     &self,
     surface: &mut softbuffer::Surface<
       std::sync::Arc<tao::window::Window>,
       std::sync::Arc<tao::window::Window>,
     >,
+    background_color: Option<tao::window::RGBA>,
   );
 }
 

+ 6 - 2
crates/tauri-runtime-wry/src/window/windows.rs

@@ -43,12 +43,13 @@ impl super::WindowExt for tao::window::Window {
     }
   }
 
-  fn clear_surface(
+  fn draw_surface(
     &self,
     surface: &mut softbuffer::Surface<
       std::sync::Arc<tao::window::Window>,
       std::sync::Arc<tao::window::Window>,
     >,
+    background_color: Option<tao::window::RGBA>,
   ) {
     let size = self.inner_size();
     if let (Some(width), Some(height)) = (
@@ -57,7 +58,10 @@ impl super::WindowExt for tao::window::Window {
     ) {
       surface.resize(width, height).unwrap();
       let mut buffer = surface.buffer_mut().unwrap();
-      buffer.fill(0);
+      let color = background_color
+        .map(|(r, g, b, _)| ((b as u32) << 0) | ((g as u32) << 8) | ((r as u32) << 16))
+        .unwrap_or(0);
+      buffer.fill(color);
       let _ = buffer.present();
     }
   }

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

@@ -18,6 +18,7 @@
 use raw_window_handle::DisplayHandle;
 use serde::{Deserialize, Serialize};
 use std::{borrow::Cow, fmt::Debug, sync::mpsc::Sender};
+use tauri_utils::config::Color;
 use tauri_utils::Theme;
 use url::Url;
 use webview::{DetachedWebview, PendingWebview};
@@ -523,6 +524,9 @@ pub trait WebviewDispatch<T: UserEvent>: Debug + Clone + Send + Sync + Sized + '
   /// Set the webview zoom level
   fn set_zoom(&self, scale_factor: f64) -> Result<()>;
 
+  /// Set the webview background.
+  fn set_background_color(&self, color: Option<Color>) -> Result<()>;
+
   /// Clear all browsing data for this webview.
   fn clear_all_browsing_data(&self) -> Result<()>;
 }
@@ -753,6 +757,9 @@ pub trait WindowDispatch<T: UserEvent>: Debug + Clone + Send + Sync + Sized + 's
   /// Updates the window visibleOnAllWorkspaces flag.
   fn set_visible_on_all_workspaces(&self, visible_on_all_workspaces: bool) -> Result<()>;
 
+  /// Set the window background.
+  fn set_background_color(&self, color: Option<Color>) -> Result<()>;
+
   /// Prevents the window contents from being captured by other apps.
   fn set_content_protected(&self, protected: bool) -> Result<()>;
 

+ 17 - 1
crates/tauri-runtime/src/webview.rs

@@ -7,7 +7,7 @@
 use crate::{window::is_label_valid, Rect, Runtime, UserEvent};
 
 use http::Request;
-use tauri_utils::config::{WebviewUrl, WindowConfig, WindowEffectsConfig};
+use tauri_utils::config::{Color, WebviewUrl, WindowConfig, WindowEffectsConfig};
 use url::Url;
 
 use std::{
@@ -209,6 +209,7 @@ pub struct WebviewAttributes {
   pub proxy_url: Option<Url>,
   pub zoom_hotkeys_enabled: bool,
   pub browser_extensions_enabled: bool,
+  pub background_color: Option<Color>,
 }
 
 impl From<&WindowConfig> for WebviewAttributes {
@@ -235,6 +236,9 @@ impl From<&WindowConfig> for WebviewAttributes {
     if let Some(url) = &config.proxy_url {
       builder = builder.proxy_url(url.to_owned());
     }
+    if let Some(color) = config.background_color {
+      builder = builder.background_color(color);
+    }
     builder = builder.zoom_hotkeys_enabled(config.zoom_hotkeys_enabled);
     builder = builder.browser_extensions_enabled(config.browser_extensions_enabled);
     builder
@@ -261,6 +265,7 @@ impl WebviewAttributes {
       proxy_url: None,
       zoom_hotkeys_enabled: false,
       browser_extensions_enabled: false,
+      background_color: None,
     }
   }
 
@@ -378,6 +383,17 @@ impl WebviewAttributes {
     self.browser_extensions_enabled = enabled;
     self
   }
+
+  /// Set the window and webview background color.
+  /// ## Platform-specific:
+  ///
+  /// - **Windows**: On Windows 7, alpha channel is ignored for the webview layer.
+  /// - **Windows**: On Windows 8 and newer, if alpha channel is not `0`, it will be ignored.
+  #[must_use]
+  pub fn background_color(mut self, color: Color) -> Self {
+    self.background_color = Some(color);
+    self
+  }
 }
 
 /// IPC handler.

+ 8 - 1
crates/tauri-runtime/src/window.rs

@@ -11,7 +11,10 @@ use crate::{
 
 use dpi::PixelUnit;
 use serde::{Deserialize, Deserializer, Serialize};
-use tauri_utils::{config::WindowConfig, Theme};
+use tauri_utils::{
+  config::{Color, WindowConfig},
+  Theme,
+};
 #[cfg(windows)]
 use windows::Win32::Foundation::HWND;
 
@@ -354,6 +357,10 @@ pub trait WindowBuilder: WindowBuilderBase {
   #[must_use]
   fn skip_taskbar(self, skip: bool) -> Self;
 
+  /// Set the window background color.
+  #[must_use]
+  fn background_color(self, color: Color) -> Self;
+
   /// Sets whether or not the window has shadow.
   ///
   /// ## Platform-specific

+ 95 - 18
crates/tauri-schema-generator/schemas/config.schema.json

@@ -486,6 +486,17 @@
           "description": "Whether browser extensions can be installed for the webview process\n\n ## Platform-specific:\n\n - **Windows**: Enables the WebView2 environment's [`AreBrowserExtensionsEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2environmentoptions?view=webview2-winrt-1.0.2739.15#arebrowserextensionsenabled)\n - **MacOS / Linux / iOS / Android** - Unsupported.",
           "default": false,
           "type": "boolean"
+        },
+        "backgroundColor": {
+          "description": "Set the window and webview background color.\n\n ## Platform-specific:\n\n - **Windows**: alpha channel is ignored for the window layer.\n - **Windows**: On Windows 7, alpha channel is ignored for the webview layer.\n - **Windows**: On Windows 8 and newer, if alpha channel is not `0`, it will be ignored for the webview layer.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Color"
+            },
+            {
+              "type": "null"
+            }
+          ]
         }
       },
       "additionalProperties": false
@@ -827,32 +838,98 @@
       ]
     },
     "Color": {
-      "description": "a tuple struct of RGBA colors. Each value has minimum of 0 and maximum of 255.",
-      "type": "array",
-      "items": [
+      "$ref": "#/definitions/InnerColor"
+    },
+    "InnerColor": {
+      "anyOf": [
         {
-          "type": "integer",
-          "format": "uint8",
-          "minimum": 0.0
+          "description": "A tuple of RGB colors. Each value has minimum of 0 and maximum of 255.",
+          "type": "array",
+          "items": [
+            {
+              "type": "integer",
+              "format": "uint8",
+              "minimum": 0.0
+            },
+            {
+              "type": "integer",
+              "format": "uint8",
+              "minimum": 0.0
+            },
+            {
+              "type": "integer",
+              "format": "uint8",
+              "minimum": 0.0
+            }
+          ],
+          "maxItems": 3,
+          "minItems": 3
         },
         {
-          "type": "integer",
-          "format": "uint8",
-          "minimum": 0.0
+          "description": "A tuple of RGBA colors. Each value has minimum of 0 and maximum of 255.",
+          "type": "array",
+          "items": [
+            {
+              "type": "integer",
+              "format": "uint8",
+              "minimum": 0.0
+            },
+            {
+              "type": "integer",
+              "format": "uint8",
+              "minimum": 0.0
+            },
+            {
+              "type": "integer",
+              "format": "uint8",
+              "minimum": 0.0
+            },
+            {
+              "type": "integer",
+              "format": "uint8",
+              "minimum": 0.0
+            }
+          ],
+          "maxItems": 4,
+          "minItems": 4
         },
         {
-          "type": "integer",
-          "format": "uint8",
-          "minimum": 0.0
+          "description": "An object of red, green, blue, alpha color values. Each value has minimum of 0 and maximum of 255.",
+          "type": "object",
+          "required": [
+            "blue",
+            "green",
+            "red"
+          ],
+          "properties": {
+            "red": {
+              "type": "integer",
+              "format": "uint8",
+              "minimum": 0.0
+            },
+            "green": {
+              "type": "integer",
+              "format": "uint8",
+              "minimum": 0.0
+            },
+            "blue": {
+              "type": "integer",
+              "format": "uint8",
+              "minimum": 0.0
+            },
+            "alpha": {
+              "default": 255,
+              "type": "integer",
+              "format": "uint8",
+              "minimum": 0.0
+            }
+          }
         },
         {
-          "type": "integer",
-          "format": "uint8",
-          "minimum": 0.0
+          "description": "A color hex string, for example: #fff, #ffffff, or #ffffffff.",
+          "type": "string"
         }
-      ],
-      "maxItems": 4,
-      "minItems": 4
+      ]
     },
     "SecurityConfig": {
       "description": "Security configuration.\n\n See more: <https://v2.tauri.app/reference/config/#securityconfig>",

+ 153 - 6
crates/tauri-utils/src/config.rs

@@ -1242,9 +1242,8 @@ pub struct BundleConfig {
   pub android: AndroidConfig,
 }
 
-/// a tuple struct of RGBA colors. Each value has minimum of 0 and maximum of 255.
-#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, Default)]
-#[cfg_attr(feature = "schema", derive(JsonSchema))]
+/// A tuple struct of RGBA colors. Each value has minimum of 0 and maximum of 255.
+#[derive(Debug, PartialEq, Eq, Serialize, Default, Clone, Copy)]
 #[serde(rename_all = "camelCase", deny_unknown_fields)]
 pub struct Color(pub u8, pub u8, pub u8, pub u8);
 
@@ -1254,6 +1253,129 @@ impl From<Color> for (u8, u8, u8, u8) {
   }
 }
 
+impl From<Color> for (u8, u8, u8) {
+  fn from(value: Color) -> Self {
+    (value.0, value.1, value.2)
+  }
+}
+
+impl From<(u8, u8, u8, u8)> for Color {
+  fn from(value: (u8, u8, u8, u8)) -> Self {
+    Color(value.0, value.1, value.2, value.3)
+  }
+}
+
+impl From<(u8, u8, u8)> for Color {
+  fn from(value: (u8, u8, u8)) -> Self {
+    Color(value.0, value.1, value.2, 255)
+  }
+}
+
+impl From<Color> for [u8; 4] {
+  fn from(value: Color) -> Self {
+    [value.0, value.1, value.2, value.3]
+  }
+}
+
+impl From<Color> for [u8; 3] {
+  fn from(value: Color) -> Self {
+    [value.0, value.1, value.2]
+  }
+}
+
+impl From<[u8; 4]> for Color {
+  fn from(value: [u8; 4]) -> Self {
+    Color(value[0], value[1], value[2], value[3])
+  }
+}
+
+impl From<[u8; 3]> for Color {
+  fn from(value: [u8; 3]) -> Self {
+    Color(value[0], value[1], value[2], 255)
+  }
+}
+
+impl FromStr for Color {
+  type Err = String;
+  fn from_str(mut color: &str) -> Result<Self, Self::Err> {
+    color = color.trim().strip_prefix('#').unwrap_or(color);
+    let color = match color.len() {
+      // TODO: use repeat_n once our MSRV is bumped to 1.82
+      3 => color.chars()
+            .flat_map(|c| std::iter::repeat(c).take(2))
+            .chain(std::iter::repeat('f').take(2))
+            .collect(),
+      6 => format!("{color}FF"),
+      8 => color.to_string(),
+      _ => return Err("Invalid hex color length, must be either 3, 6 or 8, for example: #fff, #ffffff, or #ffffffff".into()),
+    };
+
+    let r = u8::from_str_radix(&color[0..2], 16).map_err(|e| e.to_string())?;
+    let g = u8::from_str_radix(&color[2..4], 16).map_err(|e| e.to_string())?;
+    let b = u8::from_str_radix(&color[4..6], 16).map_err(|e| e.to_string())?;
+    let a = u8::from_str_radix(&color[6..8], 16).map_err(|e| e.to_string())?;
+
+    Ok(Color(r, g, b, a))
+  }
+}
+
+fn default_alpha() -> u8 {
+  255
+}
+
+#[derive(Deserialize)]
+#[cfg_attr(feature = "schema", derive(JsonSchema))]
+#[serde(untagged)]
+enum InnerColor {
+  /// A tuple of RGB colors. Each value has minimum of 0 and maximum of 255.
+  Rgb((u8, u8, u8)),
+  /// A tuple of RGBA colors. Each value has minimum of 0 and maximum of 255.
+  Rgba((u8, u8, u8, u8)),
+  /// An object of red, green, blue, alpha color values. Each value has minimum of 0 and maximum of 255.
+  RgbaObject {
+    red: u8,
+    green: u8,
+    blue: u8,
+    #[serde(default = "default_alpha")]
+    alpha: u8,
+  },
+  /// A color hex string, for example: #fff, #ffffff, or #ffffffff.
+  String(String),
+}
+
+impl<'de> Deserialize<'de> for Color {
+  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+  where
+    D: Deserializer<'de>,
+  {
+    let color = InnerColor::deserialize(deserializer)?;
+    let color = match color {
+      InnerColor::Rgb(rgb) => Color(rgb.0, rgb.1, rgb.2, 255),
+      InnerColor::Rgba(rgb) => rgb.into(),
+      InnerColor::RgbaObject {
+        red,
+        green,
+        blue,
+        alpha,
+      } => Color(red, green, blue, alpha),
+      InnerColor::String(string) => string.parse().map_err(serde::de::Error::custom)?,
+    };
+
+    Ok(color)
+  }
+}
+
+#[cfg(feature = "schema")]
+impl schemars::JsonSchema for Color {
+  fn schema_name() -> String {
+    "Color".to_string()
+  }
+
+  fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
+    gen.subschema_for::<InnerColor>()
+  }
+}
+
 /// The window effects configuration object
 #[skip_serializing_none]
 #[derive(Debug, PartialEq, Clone, Deserialize, Serialize, Default)]
@@ -1466,6 +1588,7 @@ pub struct WindowConfig {
   /// ## Platform-specific
   ///
   /// - **macOS**: Requires the `macos-proxy` feature flag and only compiles for macOS 14+.
+  #[serde(alias = "proxy-url")]
   pub proxy_url: Option<Url>,
   /// Whether page zooming by hotkeys is enabled
   ///
@@ -1476,7 +1599,7 @@ pub struct WindowConfig {
   /// 20% in each step, ranging from 20% to 1000%. Requires `webview:allow-set-webview-zoom` permission
   ///
   /// - **Android / iOS**: Unsupported.
-  #[serde(default)]
+  #[serde(default, alias = "zoom-hotkeys-enabled")]
   pub zoom_hotkeys_enabled: bool,
   /// Whether browser extensions can be installed for the webview process
   ///
@@ -1484,8 +1607,18 @@ pub struct WindowConfig {
   ///
   /// - **Windows**: Enables the WebView2 environment's [`AreBrowserExtensionsEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2environmentoptions?view=webview2-winrt-1.0.2739.15#arebrowserextensionsenabled)
   /// - **MacOS / Linux / iOS / Android** - Unsupported.
-  #[serde(default)]
+  #[serde(default, alias = "browser-extensions-enabled")]
   pub browser_extensions_enabled: bool,
+
+  /// Set the window and webview background color.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Windows**: alpha channel is ignored for the window layer.
+  /// - **Windows**: On Windows 7, alpha channel is ignored for the webview layer.
+  /// - **Windows**: On Windows 8 and newer, if alpha channel is not `0`, it will be ignored for the webview layer.
+  #[serde(alias = "background-color")]
+  pub background_color: Option<Color>,
 }
 
 impl Default for WindowConfig {
@@ -1534,6 +1667,7 @@ impl Default for WindowConfig {
       proxy_url: None,
       zoom_hotkeys_enabled: false,
       browser_extensions_enabled: false,
+      background_color: None,
     }
   }
 }
@@ -2505,6 +2639,7 @@ mod build {
       let parent = opt_str_lit(self.parent.as_ref());
       let zoom_hotkeys_enabled = self.zoom_hotkeys_enabled;
       let browser_extensions_enabled = self.browser_extensions_enabled;
+      let background_color = opt_lit(self.background_color.as_ref());
 
       literal_struct!(
         tokens,
@@ -2551,7 +2686,8 @@ mod build {
         incognito,
         parent,
         zoom_hotkeys_enabled,
-        browser_extensions_enabled
+        browser_extensions_enabled,
+        background_color
       );
     }
   }
@@ -2986,4 +3122,15 @@ mod test {
     assert_eq!(d_bundle, bundle);
     assert_eq!(d_windows, app.windows);
   }
+
+  #[test]
+  fn parse_hex_color() {
+    use super::Color;
+
+    assert_eq!(Color(255, 255, 255, 255), "fff".parse().unwrap());
+    assert_eq!(Color(255, 255, 255, 255), "#fff".parse().unwrap());
+    assert_eq!(Color(0, 0, 0, 255), "#000000".parse().unwrap());
+    assert_eq!(Color(0, 0, 0, 255), "#000000ff".parse().unwrap());
+    assert_eq!(Color(0, 255, 0, 255), "#00ff00ff".parse().unwrap());
+  }
 }

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

@@ -477,6 +477,10 @@ impl WindowBuilder for MockWindowBuilder {
   fn get_theme(&self) -> Option<Theme> {
     None
   }
+
+  fn background_color(self, _color: tauri_utils::config::Color) -> Self {
+    self
+  }
 }
 
 impl<T: UserEvent> WebviewDispatch<T> for MockWebviewDispatcher {
@@ -588,6 +592,10 @@ impl<T: UserEvent> WebviewDispatch<T> for MockWebviewDispatcher {
   fn show(&self) -> Result<()> {
     Ok(())
   }
+
+  fn set_background_color(&self, color: Option<tauri_utils::config::Color>) -> Result<()> {
+    Ok(())
+  }
 }
 
 impl<T: UserEvent> WindowDispatch<T> for MockWindowDispatcher {
@@ -979,6 +987,10 @@ impl<T: UserEvent> WindowDispatch<T> for MockWindowDispatcher {
   fn is_enabled(&self) -> Result<bool> {
     Ok(true)
   }
+
+  fn set_background_color(&self, color: Option<tauri_utils::config::Color>) -> Result<()> {
+    Ok(())
+  }
 }
 
 #[derive(Debug, Clone)]

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

@@ -22,6 +22,7 @@ use tauri_runtime::{
   webview::{DetachedWebview, PendingWebview, WebviewAttributes},
   WebviewDispatch,
 };
+pub use tauri_utils::config::Color;
 use tauri_utils::config::{WebviewUrl, WindowConfig};
 pub use url::Url;
 
@@ -787,6 +788,19 @@ fn main() {
     self.webview_attributes.browser_extensions_enabled = enabled;
     self
   }
+
+  /// Set the webview background color.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **macOS / iOS**: Not implemented.
+  /// - **Windows**: On Windows 7, alpha channel is ignored.
+  /// - **Windows**: On Windows 8 and newer, if alpha channel is not `0`, it will be ignored.
+  #[must_use]
+  pub fn background_color(mut self, color: Color) -> Self {
+    self.webview_attributes.background_color = Some(color);
+    self
+  }
 }
 
 /// Webview.
@@ -1561,6 +1575,22 @@ tauri::Builder::default()
       .map_err(Into::into)
   }
 
+  /// Specify the webview background color.
+  ///
+  /// ## Platfrom-specific:
+  ///
+  /// - **macOS / iOS**: Not implemented.
+  /// - **Windows**:
+  ///   - On Windows 7, transparency is not supported and the alpha value will be ignored.
+  ///   - On Windows higher than 7: translucent colors are not supported so any alpha value other than `0` will be replaced by `255`
+  pub fn set_background_color(&self, color: Option<Color>) -> crate::Result<()> {
+    self
+      .webview
+      .dispatcher
+      .set_background_color(color)
+      .map_err(Into::into)
+  }
+
   /// Clear all browsing data for this webview.
   pub fn clear_all_browsing_data(&self) -> crate::Result<()> {
     self

+ 8 - 2
crates/tauri/src/webview/plugin.rs

@@ -18,8 +18,8 @@ mod desktop_commands {
 
   use super::*;
   use crate::{
-    command, sealed::ManagerBase, utils::config::WindowEffectsConfig, AppHandle, Webview,
-    WebviewWindowBuilder,
+    command, sealed::ManagerBase, utils::config::WindowEffectsConfig, webview::Color, AppHandle,
+    Webview, WebviewWindowBuilder,
   };
 
   #[derive(Debug, PartialEq, Clone, Deserialize)]
@@ -179,6 +179,11 @@ mod desktop_commands {
   setter!(webview_hide, hide);
   setter!(webview_show, show);
   setter!(set_webview_zoom, set_zoom, f64);
+  setter!(
+    set_webview_background_color,
+    set_background_color,
+    Option<Color>
+  );
   setter!(clear_all_browsing_data, clear_all_browsing_data);
 
   #[command(root = "crate")]
@@ -262,6 +267,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
             desktop_commands::set_webview_size,
             desktop_commands::set_webview_position,
             desktop_commands::set_webview_focus,
+            desktop_commands::set_webview_background_color,
             desktop_commands::set_webview_zoom,
             desktop_commands::webview_hide,
             desktop_commands::webview_show,

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

@@ -29,7 +29,7 @@ use crate::{
 };
 use serde::Serialize;
 use tauri_utils::{
-  config::{WebviewUrl, WindowConfig},
+  config::{Color, WebviewUrl, WindowConfig},
   Theme,
 };
 use url::Url;
@@ -896,6 +896,23 @@ impl<'a, R: Runtime, M: Manager<R>> WebviewWindowBuilder<'a, R, M> {
     self.webview_builder = self.webview_builder.browser_extensions_enabled(enabled);
     self
   }
+
+  /// Set the window and webview background color.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Android / iOS:** Unsupported for the window layer.
+  /// - **macOS / iOS**: Not implemented for the webview layer.
+  /// - **Windows**:
+  ///   - alpha channel is ignored for the window layer.
+  ///   - On Windows 7, alpha channel is ignored for the webview layer.
+  ///   - On Windows 8 and newer, if alpha channel is not `0`, it will be ignored.
+  #[must_use]
+  pub fn background_color(mut self, color: Color) -> Self {
+    self.window_builder = self.window_builder.background_color(color);
+    self.webview_builder = self.webview_builder.background_color(color);
+    self
+  }
 }
 
 /// A type that wraps a [`Window`] together with a [`Webview`].
@@ -1578,6 +1595,21 @@ impl<R: Runtime> WebviewWindow<R> {
     self.window.set_icon(icon)
   }
 
+  /// Sets the window background color.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **iOS / Android:** Unsupported.
+  /// - **macOS**: Not implemented for the webview layer..
+  /// - **Windows**:
+  ///   - alpha channel is ignored for the window layer.
+  ///   - On Windows 7, transparency is not supported and the alpha value will be ignored for the webview layer..
+  ///   - On Windows 8 and newer: translucent colors are not supported so any alpha value other than `0` will be replaced by `255` for the webview layer.
+  pub fn set_background_color(&self, color: Option<Color>) -> crate::Result<()> {
+    self.window.set_background_color(color)?;
+    self.webview.set_background_color(color)
+  }
+
   /// Whether to hide the window icon from the taskbar or not.
   ///
   /// ## Platform-specific

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

@@ -656,6 +656,17 @@ impl<'a, R: Runtime, M: Manager<R>> WindowBuilder<'a, R, M> {
     self
   }
 
+  /// Set the window and webview background color.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Windows**: alpha channel is ignored.
+  #[must_use]
+  pub fn background_color(mut self, color: Color) -> Self {
+    self.window_builder = self.window_builder.background_color(color);
+    self
+  }
+
   /// Sets a parent to the window to be created.
   ///
   /// ## Platform-specific
@@ -1806,6 +1817,20 @@ tauri::Builder::default()
       .map_err(Into::into)
   }
 
+  /// Sets the window background color.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Windows:** alpha channel is ignored.
+  /// - **iOS / Android:** Unsupported.
+  pub fn set_background_color(&self, color: Option<Color>) -> crate::Result<()> {
+    self
+      .window
+      .dispatcher
+      .set_background_color(color)
+      .map_err(Into::into)
+  }
+
   /// Prevents the window contents from being captured by other apps.
   pub fn set_content_protected(&self, protected: bool) -> crate::Result<()> {
     self

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

@@ -19,6 +19,7 @@ mod desktop_commands {
     command,
     sealed::ManagerBase,
     utils::config::{WindowConfig, WindowEffectsConfig},
+    window::Color,
     window::{ProgressBarState, WindowBuilder},
     AppHandle, CursorIcon, Manager, Monitor, PhysicalPosition, PhysicalSize, Position, Size, Theme,
     UserAttentionType, Webview, Window,
@@ -130,6 +131,7 @@ mod desktop_commands {
   setter!(set_skip_taskbar, bool);
   setter!(set_cursor_grab, bool);
   setter!(set_cursor_visible, bool);
+  setter!(set_background_color, Option<Color>);
   setter!(set_cursor_icon, CursorIcon);
   setter!(set_cursor_position, Position);
   setter!(set_ignore_cursor_events, bool);
@@ -291,6 +293,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
             desktop_commands::set_progress_bar,
             desktop_commands::set_icon,
             desktop_commands::set_visible_on_all_workspaces,
+            desktop_commands::set_background_color,
             desktop_commands::set_title_bar_style,
             desktop_commands::set_theme,
             desktop_commands::toggle_maximize,

+ 33 - 2
packages/api/src/webview.ts

@@ -29,7 +29,7 @@ import {
   once
 } from './event'
 import { invoke } from './core'
-import { Window, getCurrentWindow } from './window'
+import { Color, Window, getCurrentWindow } from './window'
 import { WebviewWindow } from './webviewWindow'
 
 /** The drag and drop event types. */
@@ -563,6 +563,24 @@ class Webview {
     return invoke('plugin:webview|clear_all_browsing_data')
   }
 
+  /**
+   * Specify the webview background color.
+   *
+   * #### Platfrom-specific:
+   *
+   * - **macOS / iOS**: Not implemented.
+   * - **Windows**:
+   *   - On Windows 7, transparency is not supported and the alpha value will be ignored.
+   *   - On Windows higher than 7: translucent colors are not supported so any alpha value other than `0` will be replaced by `255`
+   *
+   * @returns A promise indicating the success or failure of the operation.
+   *
+   * @since 2.1.0
+   */
+  async setBackgroundColor(color: Color | null): Promise<void> {
+    return invoke('plugin:webview|set_webview_background_color', { color })
+  }
+
   // Listeners
 
   /**
@@ -728,8 +746,21 @@ interface WebviewOptions {
    * - **Android / iOS**: Unsupported.
    */
   zoomHotkeysEnabled?: boolean
+  /**
+   * Set the window and webview background color.
+   *
+   * #### Platform-specific:
+   *
+   * - **macOS / iOS**: Not implemented.
+   * - **Windows**:
+   *   - On Windows 7, alpha channel is ignored.
+   *   - On Windows 8 and newer, if alpha channel is not `0`, it will be ignored.
+   *
+   * @since 2.1.0
+   */
+  backgroundColor?: Color
 }
 
 export { Webview, getCurrentWebview, getAllWebviews }
 
-export type { DragDropEvent, WebviewOptions }
+export type { DragDropEvent, WebviewOptions, Color }

+ 24 - 2
packages/api/src/webviewWindow.ts

@@ -13,7 +13,7 @@ import { Window } from './window'
 import { listen, once } from './event'
 import type { EventName, EventCallback, UnlistenFn } from './event'
 import { invoke } from './core'
-import type { DragDropEvent } from './webview'
+import type { Color, DragDropEvent } from './webview'
 
 /**
  * Get an instance of `Webview` for the current webview window.
@@ -202,6 +202,28 @@ class WebviewWindow {
       target: { kind: 'WebviewWindow', label: this.label }
     })
   }
+
+  /**
+   * Set the window and webview background color.
+   *
+   * #### Platform-specific:
+   *
+   * - **Android / iOS:** Unsupported for the window layer.
+   * - **macOS / iOS**: Not implemented for the webview layer.
+   * - **Windows**:
+   *   - alpha channel is ignored for the window layer.
+   *   - On Windows 7, alpha channel is ignored for the webview layer.
+   *   - On Windows 8 and newer, if alpha channel is not `0`, it will be ignored.
+   *
+   * @returns A promise indicating the success or failure of the operation.
+   *
+   * @since 2.1.0
+   */
+  async setBackgroundColor(color: Color): Promise<void> {
+    return invoke('plugin:window|set_background_color', { color }).then(() => {
+      return invoke('plugin:webview|set_webview_background_color', { color })
+    })
+  }
 }
 
 // Order matters, we use window APIs by default
@@ -235,4 +257,4 @@ function applyMixins(
 }
 
 export { WebviewWindow, getCurrentWebviewWindow, getAllWebviewWindows }
-export type { DragDropEvent }
+export type { DragDropEvent, Color }

+ 35 - 2
packages/api/src/window.ts

@@ -1572,6 +1572,22 @@ class Window {
     })
   }
 
+  /**
+   * Sets the window background color.
+   *
+   * #### Platform-specific:
+   *
+   * - **Windows:** alpha channel is ignored.
+   * - **iOS / Android:** Unsupported.
+   *
+   * @returns A promise indicating the success or failure of the operation.
+   *
+   * @since 2.1.0
+   */
+  async setBackgroundColor(color: Color): Promise<void> {
+    return invoke('plugin:window|set_background_color', { color })
+  }
+
   /**
    * Changes the position of the cursor in window coordinates.
    * @example
@@ -1990,11 +2006,17 @@ class Window {
 }
 
 /**
- * an array RGBA colors. Each value has minimum of 0 and maximum of 255.
+ * An RGBA color. Each value has minimum of 0 and maximum of 255.
+ *
+ * It can be either a string `#ffffff`, an array of 3 or 4 elements or an object.
  *
  * @since 2.0.0
  */
-type Color = [number, number, number, number]
+type Color =
+  | [number, number, number]
+  | [number, number, number, number]
+  | { red: number; green: number; blue: number; alpha: number }
+  | string
 
 /**
  * Platform-specific window effects
@@ -2291,6 +2313,17 @@ interface WindowOptions {
    * @since 2.0.0
    */
   visibleOnAllWorkspaces?: boolean
+  /**
+   * Set the window background color.
+   *
+   * #### Platform-specific:
+   *
+   * - **Android / iOS:** Unsupported.
+   * - **Windows**: alpha channel is ignored.
+   *
+   * @since 2.1.0
+   */
+  backgroundColor?: Color
 }
 
 function mapMonitor(m: Monitor | null): Monitor | null {