From fdb5a466c29f9e149dc696842f33024528e990ab Mon Sep 17 00:00:00 2001 From: raymond <13162938362@163.com> Date: Mon, 10 Aug 2026 18:57:02 +0800 Subject: [PATCH 1/2] fix(windows): version-aware window chrome for Win10/Win11 Win10 frameless transparent windows showed a visible 1px DWM shadow border, and acrylic caused per-frame recomposition lag while dragging. Win11 was supposed to keep acrylic, but the opaque CSS fallback and a fully-opaque tint made it invisible. Make the Rust side the single source of truth for the Windows chrome policy (windows-version build check, >= 22000 = Win11): - Win11: DWM rounded corners + semi-transparent acrylic + shadow - Win10: no acrylic, no DWM shadow, opaque native background set at runtime (config backgroundColor stays fully transparent) - browser windows (decorated) get rounded corners only The frontend queries the policy via main_window_chrome_is_acrylic and mirrors it as ; index.scss keeps an opaque fail-safe background until acrylic is confirmed, then relaxes to the themed translucent tint (--windows-native-chrome-opacity). Pre-commit hook ran. Total eslint: 0, total circular: 0 --- src-tauri/Cargo.lock | 2 + src-tauri/crates/app-window/Cargo.toml | 5 +- src-tauri/crates/app-window/src/commands.rs | 17 ++++ src-tauri/crates/app-window/src/lib.rs | 20 ++++- .../src/tests/windows_corner_tests.rs | 32 ++++++- .../crates/app-window/src/windows_corner.rs | 89 ++++++++++++++++++- src-tauri/crates/browser/src/windows.rs | 2 +- src-tauri/src/commands/handler_list.inc | 1 + src-tauri/tauri.windows.conf.json | 5 +- src/config/windowChromeRadius.ts | 26 ++++++ src/index.scss | 25 +++++- src/index.tsx | 7 +- 12 files changed, 214 insertions(+), 17 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index dd3028fd1f..4870792752 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -271,7 +271,9 @@ dependencies = [ "tauri", "tauri-plugin-liquid-glass", "tracing", + "window-vibrancy", "windows 0.61.3", + "windows-version", ] [[package]] diff --git a/src-tauri/crates/app-window/Cargo.toml b/src-tauri/crates/app-window/Cargo.toml index c4646c11a4..e04e6e7b31 100644 --- a/src-tauri/crates/app-window/Cargo.toml +++ b/src-tauri/crates/app-window/Cargo.toml @@ -42,6 +42,9 @@ dispatch2 = { workspace = true } # the frontend passes when toggling vibrancy off (so we can paint a static # bitmap behind external content like Stripe Checkout). base64 = { workspace = true } -# Windows-specific DWM rounded-corner attribute (`set_rounded_corners`). +# Windows-specific DWM rounded corners + acrylic backdrop + OS version +# detection (Win10 vs Win11 chrome policy in `windows_corner.rs`). [target.'cfg(windows)'.dependencies] windows = { version = "0.61", features = ["Win32_Foundation", "Win32_Graphics_Dwm"] } +windows-version = "0.1" +window-vibrancy = "0.6" diff --git a/src-tauri/crates/app-window/src/commands.rs b/src-tauri/crates/app-window/src/commands.rs index 2aed97ef25..7aa683d243 100644 --- a/src-tauri/crates/app-window/src/commands.rs +++ b/src-tauri/crates/app-window/src/commands.rs @@ -228,3 +228,20 @@ pub async fn remove_window_background(app: AppHandle) -> Result<(), String> { Ok(()) } + +/// Whether the main window has a translucent native backdrop (Windows 11 +/// acrylic). The frontend mirrors this as `` so +/// CSS can relax its opaque fail-safe background. Always `false` on +/// Windows 10 (acrylic disabled — drag lag) and non-Windows hosts (macOS +/// vibrancy uses its own `data-host-desktop="macos"` CSS path). +#[tauri::command] +pub fn main_window_chrome_is_acrylic() -> bool { + #[cfg(windows)] + { + super::windows_corner::current_policy().acrylic + } + #[cfg(not(windows))] + { + false + } +} diff --git a/src-tauri/crates/app-window/src/lib.rs b/src-tauri/crates/app-window/src/lib.rs index 5cfcafb4bf..ecb626e027 100644 --- a/src-tauri/crates/app-window/src/lib.rs +++ b/src-tauri/crates/app-window/src/lib.rs @@ -221,16 +221,30 @@ fn is_main_thread() -> bool { } } -/// Host-native window chrome so the OS frame matches frontend corner radii. +/// Host-native chrome for the frameless, transparent main window. /// -/// - **Windows 11+:** `DWMWCP_ROUND` via DWM (pairs with `--border-radius-window` in the web layer). +/// - **Windows 11+:** DWM rounded corners + translucent acrylic backdrop. +/// - **Windows 10:** opaque background, no acrylic (drag lag), no DWM shadow +/// (renders as a 1px border artifact on transparent frameless windows). /// - **macOS:** Applied separately through [`apply_macos_window_material`]. /// - **Linux / others:** No-op. pub fn apply_host_desktop_window_chrome( #[cfg_attr(not(windows), allow(unused_variables))] window: &tauri::WebviewWindow, ) { #[cfg(windows)] - windows_corner::apply_dwm_rounded_corner_preference(window); + windows_corner::apply_frameless_window_chrome(window); +} + +/// Rounded corners only, for decorated secondary windows (e.g. browser). +/// Decorated windows keep their native frame, shadow, and opaque backdrop. +/// +/// - **Windows 11+:** `DWMWCP_ROUND` via DWM. +/// - **Windows 10 / macOS / Linux:** No-op. +pub fn apply_host_desktop_decorated_window_corners( + #[cfg_attr(not(windows), allow(unused_variables))] window: &tauri::WebviewWindow, +) { + #[cfg(windows)] + windows_corner::apply_rounded_corners(window); } /// Apply the native macOS AbuttedSidebar material underneath the transparent webview. diff --git a/src-tauri/crates/app-window/src/tests/windows_corner_tests.rs b/src-tauri/crates/app-window/src/tests/windows_corner_tests.rs index daa3b5a80a..1a432c4d31 100644 --- a/src-tauri/crates/app-window/src/tests/windows_corner_tests.rs +++ b/src-tauri/crates/app-window/src/tests/windows_corner_tests.rs @@ -1,10 +1,40 @@ -//! Sanity checks for DWM corner preference enum values (Win32 docs). +//! Windows chrome policy tests + DWM corner preference enum sanity checks. use windows::Win32::Graphics::Dwm::{DWMWCP_DONOTROUND, DWMWCP_ROUND, DWMWCP_ROUNDSMALL}; +use super::policy_for_build; + #[test] fn dwm_corner_preference_enum_matches_win32_docs() { assert_eq!(DWMWCP_ROUND.0, 2); assert_eq!(DWMWCP_ROUNDSMALL.0, 3); assert_eq!(DWMWCP_DONOTROUND.0, 1); } + +#[test] +fn win10_builds_get_conservative_chrome() { + // 19045 = Win10 22H2 final build. + let policy = policy_for_build(19045); + assert!(!policy.acrylic); + assert!(!policy.rounded_corners); + assert!(!policy.shadow); +} + +#[test] +fn win11_builds_get_full_chrome() { + // 22000 = first Win11 build; 26100 = Win11 24H2. + for build in [22000, 26100] { + let policy = policy_for_build(build); + assert!(policy.acrylic); + assert!(policy.rounded_corners); + assert!(policy.shadow); + } +} + +#[test] +fn failed_version_lookup_falls_back_to_win10_chrome() { + let policy = policy_for_build(0); + assert!(!policy.acrylic); + assert!(!policy.rounded_corners); + assert!(!policy.shadow); +} diff --git a/src-tauri/crates/app-window/src/windows_corner.rs b/src-tauri/crates/app-window/src/windows_corner.rs index cf285dbb07..cc85dd60f4 100644 --- a/src-tauri/crates/app-window/src/windows_corner.rs +++ b/src-tauri/crates/app-window/src/windows_corner.rs @@ -1,7 +1,13 @@ -//! Windows 11+ DWM window corner preference for decorated windows. +//! Windows native chrome: version-aware acrylic, corner, and shadow policy. //! -//! Sets `DWMWCP_ROUND` (8 px at 100 % DPI) so the native frame matches the -//! frontend's `--border-radius-window: 8px` on Windows (see `windowChromeRadius.ts`). +//! Win10's DWM cannot round frameless windows, draws the window shadow as a +//! visible 1px border around transparent windows, and recomposits acrylic on +//! every frame while dragging (visible lag). Win11 (build 22000+) supports +//! all three natively. The policy is decided once from the OS build number +//! and applied from Rust only — the frontend reads the resulting policy via +//! the `main_window_chrome_is_acrylic` command and mirrors it as +//! `` so CSS can relax its opaque +//! fail-safe background (see `src/index.scss`). use std::ffi::c_void; @@ -11,7 +17,82 @@ use windows::Win32::Graphics::Dwm::{ DwmSetWindowAttribute, DWMWA_WINDOW_CORNER_PREFERENCE, DWMWCP_ROUND, }; -pub(super) fn apply_dwm_rounded_corner_preference(window: &WebviewWindow) { +/// Native chrome capabilities for a given Windows build. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) struct WindowsChromePolicy { + /// Translucent acrylic backdrop behind the frameless main window. + pub acrylic: bool, + /// DWM rounded corners (`DWMWCP_ROUND`, 8 px at 100 % DPI — pairs with + /// the frontend's `--border-radius-window`, see `windowChromeRadius.ts`). + pub rounded_corners: bool, + /// DWM window shadow. On Win10 it renders as a 1px border artifact + /// around transparent frameless windows, so it is disabled there. + pub shadow: bool, +} + +const WINDOWS_11_FIRST_BUILD: u32 = 22000; + +/// Pure build-number → policy mapping. Build 0 (version lookup failed) +/// deliberately falls into the conservative Win10 branch. +const fn policy_for_build(build: u32) -> WindowsChromePolicy { + let win11 = build >= WINDOWS_11_FIRST_BUILD; + WindowsChromePolicy { + acrylic: win11, + rounded_corners: win11, + shadow: win11, + } +} + +/// Policy for the Windows version this process is running on. +pub(super) fn current_policy() -> WindowsChromePolicy { + policy_for_build(windows_version::OsVersion::current().build) +} + +/// Rounded corners only — for decorated secondary windows (e.g. browser). +/// Decorated windows keep their native frame, shadow, and opaque backdrop, +/// so the acrylic/shadow parts of the policy do not apply to them. +pub(super) fn apply_rounded_corners(window: &WebviewWindow) { + if current_policy().rounded_corners { + set_dwm_rounded_corners(window); + } +} + +/// Full chrome for the frameless, transparent main window. +/// +/// **Win11+:** DWM rounded corners + translucent acrylic backdrop. +/// **Win10:** no acrylic (drag lag), no DWM shadow (1px border artifact), +/// opaque native background so `transparent: true` does not punch a hole +/// through to the desktop. +pub(super) fn apply_frameless_window_chrome(window: &WebviewWindow) { + let policy = current_policy(); + + if policy.rounded_corners { + set_dwm_rounded_corners(window); + } + + if policy.acrylic { + // Semi-transparent dark tint over the system backdrop; an alpha of + // 255 would make the acrylic fully opaque and thus invisible. + if let Err(err) = window_vibrancy::apply_acrylic(window, Some((13, 13, 13, 125))) { + warn!( + target: "app_lib::window", + "apply_acrylic failed (non-fatal, continuing without acrylic): {}", + err + ); + } + } else { + // Clear any acrylic left over from a previous run/config layer, and + // paint an opaque native background as the pre-CSS fallback. + let _ = window_vibrancy::clear_acrylic(window); + let _ = window.set_background_color(Some(tauri::window::Color(13, 13, 13, 255))); + } + + if !policy.shadow { + let _ = window.set_shadow(false); + } +} + +fn set_dwm_rounded_corners(window: &WebviewWindow) { let hwnd = match window.hwnd() { Ok(handle) => handle, Err(err) => { diff --git a/src-tauri/crates/browser/src/windows.rs b/src-tauri/crates/browser/src/windows.rs index 9ef14d010b..975e8e0e02 100644 --- a/src-tauri/crates/browser/src/windows.rs +++ b/src-tauri/crates/browser/src/windows.rs @@ -109,7 +109,7 @@ pub async fn open_browser_window( set_traffic_light_position(&window, TRAFFIC_LIGHT_X, TRAFFIC_LIGHT_Y); } - app_window::apply_host_desktop_window_chrome(&window); + app_window::apply_host_desktop_decorated_window_corners(&window); #[cfg(all(not(target_os = "macos"), not(windows)))] let _ = window; diff --git a/src-tauri/src/commands/handler_list.inc b/src-tauri/src/commands/handler_list.inc index a99333dbee..ce59c08f11 100644 --- a/src-tauri/src/commands/handler_list.inc +++ b/src-tauri/src/commands/handler_list.inc @@ -15,6 +15,7 @@ infrastructure::dev_bundled_auth::debug_import_bundled_org2_cloud_auth, app_window::commands::set_window_vibrancy, app_window::commands::set_main_webview_zoom, app_window::commands::remove_window_background, +app_window::commands::main_window_chrome_is_acrylic, // Optional sidecar commands - lazy install after first paint crate::setup::sidecar_setup::sidecar_list_status, crate::setup::sidecar_setup::sidecar_install, diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json index e339a1b7dd..62b52846b2 100644 --- a/src-tauri/tauri.windows.conf.json +++ b/src-tauri/tauri.windows.conf.json @@ -14,10 +14,7 @@ "decorations": false, "hiddenTitle": true, "backgroundColor": "#00000000", - "transparent": true, - "windowEffects": { - "effects": ["acrylic"] - } + "transparent": true } ] }, diff --git a/src/config/windowChromeRadius.ts b/src/config/windowChromeRadius.ts index 1cdcf0e14d..df400e37d1 100644 --- a/src/config/windowChromeRadius.ts +++ b/src/config/windowChromeRadius.ts @@ -59,3 +59,29 @@ export function applyHostDesktopWindowChromeRadius(): void { document.body.style.setProperty("--radius-page", pageVal); } } + +/** + * Mirrors the Rust-side Windows chrome policy as + * `` so index.scss can relax the opaque + * Windows fail-safe background when a translucent native backdrop (Win11 + * acrylic) actually exists behind the webview. Until this resolves — and on + * Windows 10, where acrylic is disabled — the attribute stays absent and the + * opaque fallback applies. Requires initializeTauriAPIs() to have completed. + */ +export async function applyWindowsNativeChromeAttribute(): Promise { + if (typeof document === "undefined") { + return; + } + if (resolveHostDesktop() !== HOST_DESKTOP.WINDOWS) { + return; + } + try { + const { invokeTauri } = await import("@src/util/platform/tauri/init"); + const acrylic = await invokeTauri("main_window_chrome_is_acrylic"); + if (acrylic) { + document.documentElement.dataset.windowsChrome = "acrylic"; + } + } catch { + // Keep the opaque fail-safe background if the policy can't be read. + } +} diff --git a/src/index.scss b/src/index.scss index 63c5531044..c8966c0dc6 100644 --- a/src/index.scss +++ b/src/index.scss @@ -536,12 +536,35 @@ html[data-host-desktop="macos"] .sidebar-base { background: transparent; } +// Windows fail-safe: opaque until the Rust chrome policy confirms a +// translucent native backdrop exists. Win10 never gets one (acrylic causes +// drag lag; DWM shadow draws a 1px border on transparent frameless windows), +// and even on Win11 the window must not be see-through before the policy +// query resolves. See applyWindowsNativeChromeAttribute() in +// windowChromeRadius.ts. html[data-host-desktop="windows"] body, html[data-host-desktop="windows"] #root { - background: transparent; + background: var(--color-bg-2); } html[data-host-desktop="windows"] .sidebar-base { + background: var(--color-bg-2); +} + +// Win11 acrylic confirmed: let the native backdrop show through, tinted per +// theme (same opacity variable the top bar and sidebar chrome use). Must +// include itself — the global rule above paints it opaque. +html[data-windows-chrome="acrylic"], +html[data-windows-chrome="acrylic"] body, +html[data-windows-chrome="acrylic"] #root { + background: color-mix( + in srgb, + var(--color-bg-2) var(--windows-native-chrome-opacity, 30%), + transparent + ); +} + +html[data-windows-chrome="acrylic"] .sidebar-base { background: transparent; } diff --git a/src/index.tsx b/src/index.tsx index 172b6007e0..9676a638c1 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,7 +2,10 @@ import { createRoot } from "react-dom/client"; import { initializeSharedServiceAuthStorage } from "@src/api/http/auth/sharedAuthStorage"; import { configureIdeServerForIdentifier } from "@src/config/ideServer"; -import { applyHostDesktopWindowChromeRadius } from "@src/config/windowChromeRadius"; +import { + applyHostDesktopWindowChromeRadius, + applyWindowsNativeChromeAttribute, +} from "@src/config/windowChromeRadius"; import { configureCloudAuthCallbackForIdentifier } from "@src/features/Org2Cloud/config"; import { installGlobalTauriSelectAllShortcut } from "@src/hooks/keyboard/useTauriSelectAllShortcut"; import { createLogger, initializeLogging } from "@src/hooks/logger/useLogger"; @@ -212,7 +215,7 @@ async function initializeApp() { // locale bundles are still loading. const initPromise = Promise.all([ initTheme(), - initializeTauriAPIs(), + initializeTauriAPIs().then(() => applyWindowsNativeChromeAttribute()), initBackgroundImage(), appModulePromise, ]); From c5b375aaaf5d2aeed6c9d7e2f5972be5cd5a6249 Mon Sep 17 00:00:00 2001 From: raymond <13162938362@163.com> Date: Tue, 11 Aug 2026 16:44:05 +0800 Subject: [PATCH 2/2] fix(windows): defer main window show until first paint to remove startup border artifact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows 10 a transparent frameless window shows thin black lines around its border during the first ~1s of startup, then settles once the webview paints. Root cause: the main window was created visible (visible default true) over a transparent surface, so DWM/WebView2 edge artifacts are exposed until the webview's first composited frame. The prior attempt (apply chrome before show) did not help because set_background_color is a visual no-op when transparent:true — WebView2 composites directly over the transparent surface. Fix the startup timing so the window's first visible frame is the painted splash, not a transparent surface: - tauri.windows.conf.json: set visible:false on the main window so it starts hidden. - src/lib.rs: on non-macOS, do not show() eagerly in setup; instead listen for the "orgii:main-window-ready" event from the frontend and show+focus then. A 3 s tokio timeout is a safety fallback so a bundle crash or IPC failure can never strand the user on a hidden window. - crates/app-window/src/lib.rs (recreate_main_window): apply chrome then show() explicitly, since the window config now starts hidden. - src/index.tsx: emit "orgii:main-window-ready" at the very start of initializeApp() (fire-and-forget), before any other init — by then the splash HTML has loaded and painted. Verified: release exe builds and links (thin LTO); runtime startup artifacts to be confirmed on Win10/Win11 by the user. Pre-commit hook ran. Total eslint: 0, total circular: 0 --- src-tauri/crates/app-window/src/lib.rs | 5 +++ src-tauri/src/lib.rs | 44 ++++++++++++++++++++++---- src-tauri/tauri.windows.conf.json | 3 +- src/index.tsx | 10 ++++++ 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/src-tauri/crates/app-window/src/lib.rs b/src-tauri/crates/app-window/src/lib.rs index ecb626e027..927a33450d 100644 --- a/src-tauri/crates/app-window/src/lib.rs +++ b/src-tauri/crates/app-window/src/lib.rs @@ -321,6 +321,11 @@ pub fn recreate_main_window(app: &AppHandle) -> Result<(), String> { apply_host_desktop_window_chrome(&window); + // The main window starts hidden (visible:false in the platform config) + // so chrome can be applied before first paint; show it now that the + // opaque background + shadow policy are in place. + let _ = window.show(); + let _ = window.set_focus(); println!("✅ [Window] Main window recreated"); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 544b863989..432f500dbe 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -439,12 +439,10 @@ pub fn run() { { use tauri::Manager; + if let Some(main_window) = app.handle().get_webview_window("main") { - #[cfg(not(target_os = "macos"))] - { - let _ = main_window.show(); - let _ = main_window.set_focus(); - } + // Apply chrome while the window is still hidden. + app_window::apply_host_desktop_window_chrome(&main_window); #[cfg(target_os = "macos")] { @@ -455,9 +453,43 @@ pub fn run() { app_window::TRAFFIC_LIGHT_Y, ); app_window::apply_macos_window_material(&main_window); + let _ = main_window.show(); + let _ = main_window.set_focus(); } + } - app_window::apply_host_desktop_window_chrome(&main_window); + // On Windows the main window starts hidden (visible:false in the + // platform config). With transparent:true, set_background_color + // is a visual no-op — WebView2 composites directly over the + // transparent surface, so showing the window before the webview + // has painted exposes DWM/WebView2 edge artifacts (thin black + // lines around the border on Win10). We defer show() until the + // frontend emits "orgii:main-window-ready", which fires once + // the splash HTML has loaded and painted. + #[cfg(not(target_os = "macos"))] + { + let show_handle = app.handle().clone(); + app.handle().listen( + "orgii:main-window-ready", + move |_| { + if let Some(w) = show_handle.get_webview_window("main") { + let _ = w.show(); + let _ = w.set_focus(); + } + }, + ); + + // Safety fallback: if the frontend event never arrives + // (bundle crash, IPC failure), show after 3 s so the user + // is never stranded on a hidden window. + let timeout_handle = app.handle().clone(); + tauri::async_runtime::spawn(async move { + tokio::time::sleep(std::time::Duration::from_secs(3)).await; + if let Some(w) = timeout_handle.get_webview_window("main") { + let _ = w.show(); + let _ = w.set_focus(); + } + }); } } diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json index 62b52846b2..9ea73fd4c7 100644 --- a/src-tauri/tauri.windows.conf.json +++ b/src-tauri/tauri.windows.conf.json @@ -14,7 +14,8 @@ "decorations": false, "hiddenTitle": true, "backgroundColor": "#00000000", - "transparent": true + "transparent": true, + "visible": false } ] }, diff --git a/src/index.tsx b/src/index.tsx index 9676a638c1..dba4c27afe 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -151,6 +151,16 @@ async function initializeRuntimeInstanceIdentity(): Promise { // PERFORMANCE: Initialize all critical services in parallel before render async function initializeApp() { + // Signal the Rust backend that the webview bundle has loaded and the + // splash HTML is painted. On Windows the main window starts hidden + // (visible:false) to avoid DWM/WebView2 edge artifacts on transparent + // frameless windows; this event triggers show() so the first visible + // frame is the painted splash, not a transparent artifact. + // Fire-and-forget: a 3 s safety timeout on the Rust side covers failures. + import("@tauri-apps/api/event") + .then(({ emit }) => emit("orgii:main-window-ready")) + .catch(() => {}); + // Runtime identity must be known before loading App: several API modules // derive local HTTP/WebSocket constants at module evaluation time. await initializeRuntimeInstanceIdentity();