diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c6278bb..6ecc487 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -69,7 +69,7 @@ objc2-quartz-core = "0.3.1" block2 = "0.6.1" [target.'cfg(target_os = "windows")'.dependencies] -windows-sys = { version = "0.61", features = ["Win32_UI_WindowsAndMessaging"] } +windows-sys = { version = "0.61", features = ["Win32_Graphics_Gdi", "Win32_UI_WindowsAndMessaging"] } [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-updater = "2" diff --git a/src-tauri/src/app_bootstrap.rs b/src-tauri/src/app_bootstrap.rs index ea70be7..3369df0 100644 --- a/src-tauri/src/app_bootstrap.rs +++ b/src-tauri/src/app_bootstrap.rs @@ -27,6 +27,12 @@ pub(crate) fn setup(app: &mut tauri::App) -> Result<(), Box> { .get_webview_window("main") .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Failed to get main window"))?; + // Only the webview needs transparency for the UI overlay. + #[cfg(target_os = "windows")] + window + .as_ref() + .set_background_color(Some(tauri::utils::config::Color(0, 0, 0, 0)))?; + #[cfg(any(target_os = "windows", target_os = "linux"))] { let _ = window.set_decorations(false); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3e50cde..372fcc4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -502,6 +502,14 @@ fn show_main_window(app_handle: &tauri::AppHandle) { let _ = app_handle_for_thread.run_on_main_thread(move || { if let Some(window) = app_handle_for_show.get_webview_window(MAIN_WINDOW_LABEL) { let _ = window.show(); + #[cfg(target_os = "windows")] + if let Err(error) = crate::platform::windows::paint_native_window_background( + &window, + None, + window.theme().unwrap_or(tauri::Theme::Dark), + ) { + log::warn!("Failed to paint native main window background: {error}"); + } } }); } diff --git a/src-tauri/src/platform/default.rs b/src-tauri/src/platform/default.rs index d000e5a..9f39200 100644 --- a/src-tauri/src/platform/default.rs +++ b/src-tauri/src/platform/default.rs @@ -41,12 +41,26 @@ impl PlatformIntegration for DefaultPlatformIntegration { fn apply_window_appearance( &self, - _window: tauri::Window, + window: tauri::Window, _compact_mode: bool, _corner_radius: Option, - _theme: Option, + theme: Option, ) -> Result<(), String> { - Ok(()) + #[cfg(target_os = "windows")] + { + let system_theme = window.theme().unwrap_or(tauri::Theme::Dark); + super::windows::paint_native_window_background( + &window, + theme.as_deref(), + system_theme, + ) + } + + #[cfg(not(target_os = "windows"))] + { + let _ = (window, theme); + Ok(()) + } } fn set_window_vibrancy_visible( diff --git a/src-tauri/src/platform/windows.rs b/src-tauri/src/platform/windows.rs index 443c0e0..b293699 100644 --- a/src-tauri/src/platform/windows.rs +++ b/src-tauri/src/platform/windows.rs @@ -2,10 +2,17 @@ mod imp { use raw_window_handle::{HasWindowHandle, RawWindowHandle}; use std::sync::mpsc; + use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::{Mutex, OnceLock}; use std::time::{Duration, Instant}; use tauri::{Emitter, Manager}; use tokio::time::sleep; + use windows_sys::Win32::Graphics::Gdi::{ + CreateSolidBrush, FillRect, GetDC, InvalidateRect, ReleaseDC, UpdateWindow, + }; + use windows_sys::Win32::UI::WindowsAndMessaging::{ + GetClientRect, SetClassLongPtrW, GCLP_HBRBACKGROUND, + }; use windows_sys::Win32::UI::WindowsAndMessaging::IsZoomed; const MAIN_WINDOW_LABEL: &str = "main"; @@ -17,6 +24,87 @@ mod imp { const PIP_SCREEN_WIDTH_FACTOR: f64 = 0.30; const PIP_SCREEN_HEIGHT_FACTOR: f64 = 0.45; const PIP_MARGIN: i32 = 20; + const LIGHT_BACKGROUND_COLOR: u32 = 0x00f6f6f6; + const DARK_BACKGROUND_COLOR: u32 = 0x000f0f0f; + // Win32 COLORREF uses 0x00BBGGRR, so CSS #1e2227 becomes 0x0027221e. + const GRAPHITE_BACKGROUND_COLOR: u32 = 0x0027221e; + static CURRENT_BACKGROUND_COLOR: AtomicU32 = AtomicU32::new(0); + + pub(crate) fn paint_native_window_background( + window: &W, + theme: Option<&str>, + system_theme: tauri::Theme, + ) -> Result<(), String> { + let handle = window.window_handle().map_err(|error| error.to_string())?; + let hwnd = match handle.as_raw() { + RawWindowHandle::Win32(handle) => { + handle.hwnd.get() as usize as *mut std::ffi::c_void + } + _ => return Err("Main window does not expose a Win32 window handle".into()), + }; + + let color = match theme.map(|value| value.trim().to_ascii_lowercase()) { + Some(value) if value == "light" => LIGHT_BACKGROUND_COLOR, + Some(value) if value == "dark" => DARK_BACKGROUND_COLOR, + Some(value) if value == "graphite" => GRAPHITE_BACKGROUND_COLOR, + Some(_) => system_background_color(system_theme), + None => { + let current = CURRENT_BACKGROUND_COLOR.load(Ordering::Acquire); + if current == 0 { + system_background_color(system_theme) + } else { + current + } + } + }; + CURRENT_BACKGROUND_COLOR.store(color, Ordering::Release); + + let brush = background_brush(color); + if brush.is_null() { + return Err("Failed to create native window background brush".into()); + } + + let mut client_rect = windows_sys::Win32::Foundation::RECT::default(); + unsafe { + // The class owns this process-lifetime brush after assignment. + SetClassLongPtrW(hwnd, GCLP_HBRBACKGROUND, brush as isize); + if GetClientRect(hwnd, &mut client_rect) == 0 { + return Err("Failed to query main window client area".into()); + } + + let dc = GetDC(hwnd); + if dc.is_null() { + return Err("Failed to acquire main window device context".into()); + } + FillRect(dc, &client_rect, brush); + ReleaseDC(hwnd, dc); + + InvalidateRect(hwnd, std::ptr::null(), 1); + UpdateWindow(hwnd); + } + Ok(()) + } + + fn system_background_color(theme: tauri::Theme) -> u32 { + match theme { + tauri::Theme::Light => LIGHT_BACKGROUND_COLOR, + _ => DARK_BACKGROUND_COLOR, + } + } + + fn background_brush(color: u32) -> *mut std::ffi::c_void { + static LIGHT_BRUSH: OnceLock = OnceLock::new(); + static DARK_BRUSH: OnceLock = OnceLock::new(); + static GRAPHITE_BRUSH: OnceLock = OnceLock::new(); + + let slot = match color { + LIGHT_BACKGROUND_COLOR => &LIGHT_BRUSH, + GRAPHITE_BACKGROUND_COLOR => &GRAPHITE_BRUSH, + _ => &DARK_BRUSH, + }; + *slot.get_or_init(|| unsafe { CreateSolidBrush(color) as usize }) + as *mut std::ffi::c_void + } #[derive(Clone, Copy, Debug)] struct WindowSnapshot { @@ -383,6 +471,6 @@ mod imp { #[cfg(target_os = "windows")] pub(crate) use imp::{ - enforce_native_pip_aspect, is_native_pip_enabled, set_native_pip_enabled, - prepare_window_for_fullscreen, update_native_pip_state, + enforce_native_pip_aspect, is_native_pip_enabled, paint_native_window_background, + prepare_window_for_fullscreen, set_native_pip_enabled, update_native_pip_state, }; diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json index 8c15172..0619be1 100644 --- a/src-tauri/tauri.windows.conf.json +++ b/src-tauri/tauri.windows.conf.json @@ -1,4 +1,16 @@ { + "app": { + "windows": [ + { + "title": "Soia", + "width": 1280, + "height": 720, + "decorations": true, + "transparent": false, + "visible": false + } + ] + }, "bundle": { "fileAssociations": [ {