diff --git a/src/apps/desktop/src/appearance.rs b/src/apps/desktop/src/appearance.rs index 443c2e2d7d..e090751248 100644 --- a/src/apps/desktop/src/appearance.rs +++ b/src/apps/desktop/src/appearance.rs @@ -318,7 +318,7 @@ impl AppearanceConfig { let startup_locale_json = serde_json::to_string(&startup_locale).unwrap_or_else(|_| "\"zh-CN\"".to_string()); let show_startup_window_controls = !cfg!(target_os = "macos"); - let native_sidebar_material = cfg!(any(target_os = "windows", target_os = "macos")); + let native_sidebar_material = native_sidebar_material_available(); let startup_trace_id_json = serde_json::to_string(startup_trace_id) .unwrap_or_else(|_| "\"desktop-unknown\"".to_string()); let bootstrap_log_level_json = serde_json::to_string(crate::logging::level_to_str( @@ -510,6 +510,28 @@ fn use_development_frontend() -> bool { } } +/// Whether the window-level sidebar material can be hosted by the compositor. +/// +/// macOS vibrancy and the Windows 11 Mica/acrylic backdrops are owned by the +/// compositor. Older Windows builds have to fall back to a live blur-behind, +/// which re-blurs everything behind the window on every move and makes dragging +/// the window stutter, so those builds keep the frontend fallback surfaces +/// (opaque theme colors plus a CSS backdrop) instead. +fn native_sidebar_material_available() -> bool { + #[cfg(target_os = "macos")] + { + true + } + #[cfg(target_os = "windows")] + { + crate::window_shell::window_backdrop_available() + } + #[cfg(not(any(target_os = "macos", target_os = "windows")))] + { + false + } +} + pub fn create_main_window( app_handle: &tauri::AppHandle, startup_trace_id: &str, @@ -619,19 +641,22 @@ pub fn create_main_window( // The webview must be transparent for the OS material to reach the sidebar. // Scene backgrounds and the startup tint remain owned by the frontend. + // Requesting the material where the compositor cannot host it would leave + // the window with a live blur-behind that drops frames while the window is + // dragged, so those builds keep the opaque fallback surfaces instead. #[cfg(any(target_os = "windows", target_os = "macos"))] { + let mut effects = tauri::window::EffectsBuilder::new(); + if native_sidebar_material_available() { + effects = effects.effects([ + tauri::window::Effect::Acrylic, + tauri::window::Effect::Sidebar, + ]); + } builder = builder .transparent(true) .background_color(tauri::window::Color(0, 0, 0, 0)) - .effects( - tauri::window::EffectsBuilder::new() - .effects([ - tauri::window::Effect::Acrylic, - tauri::window::Effect::Sidebar, - ]) - .build(), - ); + .effects(effects.build()); } #[cfg(debug_assertions)] @@ -672,6 +697,10 @@ pub fn create_main_window( if let Err(error) = crate::window_webview_geometry::install(&window) { error!("Failed to install main WebView geometry protection: {error}"); } + #[cfg(target_os = "windows")] + if let Err(error) = crate::window_shell::install_frame_handling(&window) { + error!("Failed to install main window frame handling: {error}"); + } let reapply_maximized = crate::restore_main_window_state(&window); crate::webview_recovery::install(&window); startup_trace.record_elapsed_step("native_window", "webview_build", build_started_at); diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index dbe27c0e63..3c8c96bda4 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -35,6 +35,8 @@ pub mod sleep_prevention; pub mod startup_trace; pub mod tray; mod webview_recovery; +#[cfg(target_os = "windows")] +mod window_shell; mod window_state_support; #[cfg(target_os = "windows")] mod window_webview_geometry; diff --git a/src/apps/desktop/src/window_shell.rs b/src/apps/desktop/src/window_shell.rs new file mode 100644 index 0000000000..7423dc0109 --- /dev/null +++ b/src/apps/desktop/src/window_shell.rs @@ -0,0 +1,99 @@ +//! Windows shell integration for the main window. +//! +//! Two window details that the native non-client area owns are settled here: +//! the frame strips the undecorated window leaves unpainted, and the backdrop +//! capability that decides whether the native sidebar material can be hosted +//! by the compositor at all. + +use windows::Win32::Foundation::{GetLastError, HWND, LPARAM, LRESULT, WPARAM}; +use windows::Win32::UI::Shell::{DefSubclassProc, SetWindowSubclass, SUBCLASSPROC}; +use windows::Win32::UI::WindowsAndMessaging::{ + IsZoomed, SetWindowPos, SWP_FRAMECHANGED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOOWNERZORDER, + SWP_NOSIZE, SWP_NOZORDER, WM_NCCALCSIZE, +}; + +/// Identifies the main window's client/frame subclass. +const FRAME_SUBCLASS_ID: usize = 0x4f42_4653; // "OBFS" + +/// Windows 11 22H2 is the first release that can host the window backdrop in +/// the compositor (`DWMWA_SYSTEMBACKDROP_TYPE`). +const WINDOWS_BACKDROP_MIN_BUILD: u32 = 22621; + +/// The undecorated main window keeps its resizable frame so that Windows still +/// provides the resize border, Aero Snap and the system move/size gestures. +/// tao sizes the WebView to the client rectangle and leaves the window class +/// without a background brush, so the strips between the client rectangle and +/// the window rectangle stay unpainted. Those strips sit on the left, right and +/// bottom edges (the top edge has none), and a transparent window composites +/// them as bare window backdrop, which reads as a drop shadow around the window. +/// +/// Widen the client rectangle over the strips instead. Resizing, snapping and +/// the move/size gestures are all driven by the window rectangle, so removing +/// the non-client area leaves every native interaction intact while the WebView +/// paints the window edge-to-edge. Maximized windows keep the default handling: +/// Windows inflates their window rectangle beyond the work area, which already +/// moves the strips off-screen. +pub(crate) fn install_frame_handling(window: &tauri::WebviewWindow) -> Result<(), String> { + let hwnd = window.hwnd().map_err(|error| error.to_string())?; + let subclass: SUBCLASSPROC = Some(frame_subclass_proc); + let installed = unsafe { SetWindowSubclass(hwnd, subclass, FRAME_SUBCLASS_ID, 0) }; + if !installed.as_bool() { + return Err(format!( + "SetWindowSubclass failed for the main window: 0x{:08X}", + unsafe { GetLastError() }.0 + )); + } + // The subclass applies from the next frame recalculation onwards. + unsafe { + SetWindowPos( + hwnd, + None, + 0, + 0, + 0, + 0, + SWP_FRAMECHANGED + | SWP_NOMOVE + | SWP_NOSIZE + | SWP_NOZORDER + | SWP_NOOWNERZORDER + | SWP_NOACTIVATE, + ) + .map_err(|error| error.to_string())?; + } + Ok(()) +} + +/// Whether the compositor can host the window backdrop on this build. +/// +/// Windows 10 has no `DWMWA_SYSTEMBACKDROP_TYPE`, so the window material falls +/// back to a live blur-behind that re-blurs everything behind the window on +/// every move. Dragging the window then drops frames, which is why the native +/// sidebar material is only requested where the backdrop is compositor-owned. +pub(crate) fn window_backdrop_available() -> bool { + use windows::Win32::System::SystemInformation::{GetVersionExW, OSVERSIONINFOW}; + + let mut version = OSVERSIONINFOW { + dwOSVersionInfoSize: std::mem::size_of::() as u32, + ..Default::default() + }; + unsafe { GetVersionExW(&mut version) }.is_ok() + && version.dwBuildNumber >= WINDOWS_BACKDROP_MIN_BUILD +} + +unsafe extern "system" fn frame_subclass_proc( + hwnd: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + _subclass_id: usize, + _ref_data: usize, +) -> LRESULT { + // A non-zero `wparam` marks the `NCCALCSIZE_PARAMS` form of the message, + // whose `rgrc[0]` already holds the proposed window rectangle. Returning + // zero adopts that rectangle unchanged, which is what removes the strips. + if msg == WM_NCCALCSIZE && wparam.0 != 0 && !IsZoomed(hwnd).as_bool() { + return LRESULT(0); + } + DefSubclassProc(hwnd, msg, wparam, lparam) +}