Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 38 additions & 9 deletions src/apps/desktop/src/appearance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/apps/desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
99 changes: 99 additions & 0 deletions src/apps/desktop/src/window_shell.rs
Original file line number Diff line number Diff line change
@@ -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::<OSVERSIONINFOW>() 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)
}