diff --git a/openless-all/app/src-tauri/Cargo.toml b/openless-all/app/src-tauri/Cargo.toml index 6a69b47a..0a8484df 100644 --- a/openless-all/app/src-tauri/Cargo.toml +++ b/openless-all/app/src-tauri/Cargo.toml @@ -53,8 +53,11 @@ objc2-foundation = "0.2" objc2-app-kit = "0.2" [target.'cfg(target_os = "windows")'.dependencies] +raw-window-handle = "0.6" windows = { version = "0.58", features = [ "Win32_Foundation", + "Win32_Graphics_Dwm", + "Win32_Graphics_Gdi", "Win32_UI_Shell", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_WindowsAndMessaging", diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs index 94684d77..b5454aac 100644 --- a/openless-all/app/src-tauri/src/lib.rs +++ b/openless-all/app/src-tauri/src/lib.rs @@ -113,6 +113,7 @@ pub fn run() { if let Err(e) = apply_mica(&main, None) { log::warn!("[main] mica failed: {e}"); } + apply_windows_rounded_frame(&main); } if let Err(e) = main.show() { log::warn!("[main] initial show failed: {e}"); @@ -219,10 +220,16 @@ pub fn run() { RunEvent::Reopen { .. } => show_main_window(app), RunEvent::WindowEvent { label, event, .. } => { if label == "main" { - if let tauri::WindowEvent::CloseRequested { api, .. } = event { + if let tauri::WindowEvent::CloseRequested { ref api, .. } = event { api.prevent_close(); hide_main_window(app); } + #[cfg(target_os = "windows")] + if matches!(event, tauri::WindowEvent::Resized(_) | tauri::WindowEvent::ScaleFactorChanged { .. }) { + if let Some(main) = app.get_webview_window("main") { + apply_windows_rounded_frame(&main); + } + } } } RunEvent::Exit => { @@ -234,6 +241,97 @@ pub fn run() { }); } +#[cfg(target_os = "windows")] +fn apply_windows_rounded_frame(window: &tauri::WebviewWindow) { + use raw_window_handle::{HasWindowHandle, RawWindowHandle}; + use windows::Win32::Foundation::{BOOL, HWND, RECT}; + use windows::Win32::Graphics::Dwm::{ + DwmSetWindowAttribute, DWMWA_BORDER_COLOR, DWMWA_WINDOW_CORNER_PREFERENCE, DWMWCP_ROUND, + }; + use windows::Win32::Graphics::Gdi::{CreateRoundRectRgn, SetWindowRgn, HRGN}; + use windows::Win32::UI::WindowsAndMessaging::{ + GetWindowLongW, GetWindowRect, SetWindowLongW, SetWindowPos, GWL_STYLE, SWP_FRAMECHANGED, + SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, WS_CAPTION, WS_THICKFRAME, + }; + + let handle = match window.window_handle().map(|h| h.as_raw()) { + Ok(RawWindowHandle::Win32(handle)) => handle, + Ok(other) => { + log::warn!("[main] unexpected raw window handle for DWM frame: {other:?}"); + return; + } + Err(e) => { + log::warn!("[main] read raw window handle failed: {e}"); + return; + } + }; + let hwnd = HWND(handle.hwnd.get() as *mut core::ffi::c_void); + + unsafe { + let style = GetWindowLongW(hwnd, GWL_STYLE); + let native_frame_bits = (WS_CAPTION.0 | WS_THICKFRAME.0) as i32; + if style & native_frame_bits != 0 { + SetWindowLongW(hwnd, GWL_STYLE, style & !native_frame_bits); + if let Err(e) = SetWindowPos( + hwnd, + HWND::default(), + 0, + 0, + 0, + 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED, + ) { + log::warn!("[main] refresh native frame after style update failed: {e}"); + } + } + + if window.is_maximized().unwrap_or(false) { + let _ = SetWindowRgn(hwnd, HRGN::default(), BOOL(1)); + return; + } + + let corner_preference = DWMWCP_ROUND; + if let Err(e) = DwmSetWindowAttribute( + hwnd, + DWMWA_WINDOW_CORNER_PREFERENCE, + &corner_preference as *const _ as *const core::ffi::c_void, + std::mem::size_of_val(&corner_preference) as u32, + ) { + log::warn!("[main] set DWM rounded corners failed: {e}"); + } + + // Remove DWM's fallback 1px light border; the React shell draws the visual stroke. + let border_color_none: u32 = 0xFFFFFFFE; + if let Err(e) = DwmSetWindowAttribute( + hwnd, + DWMWA_BORDER_COLOR, + &border_color_none as *const _ as *const core::ffi::c_void, + std::mem::size_of_val(&border_color_none) as u32, + ) { + log::warn!("[main] remove DWM border color failed: {e}"); + } + + let mut rect = RECT::default(); + if let Err(e) = GetWindowRect(hwnd, &mut rect) { + log::warn!("[main] read window rect for rounded region failed: {e}"); + return; + } + let width = rect.right - rect.left; + let height = rect.bottom - rect.top; + if width <= 0 || height <= 0 { + return; + } + let region = CreateRoundRectRgn(0, 0, width + 1, height + 1, 18, 18); + if region.is_invalid() { + log::warn!("[main] create rounded window region failed"); + return; + } + if SetWindowRgn(hwnd, region, BOOL(1)) == 0 { + log::warn!("[main] apply rounded window region failed"); + } + } +} + #[tauri::command] fn restart_app(app: AppHandle) { // macOS:自动更新会让新装的 .app 带 com.apple.quarantine(无论 Tauri updater diff --git a/openless-all/app/src/components/FloatingShell.tsx b/openless-all/app/src/components/FloatingShell.tsx index 3f93aa6c..08ab4932 100644 --- a/openless-all/app/src/components/FloatingShell.tsx +++ b/openless-all/app/src/components/FloatingShell.tsx @@ -255,7 +255,7 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia flex: 1, minWidth: 0, overflow: 'hidden', background: 'var(--ol-surface)', - borderRadius: os === 'mac' ? 20 : 14, + borderRadius: os === 'mac' ? 20 : 10, border: '0.5px solid rgba(0,0,0,0.06)', boxShadow: '0 1px 0 rgba(255,255,255,0.8) inset, 0 8px 24px -12px rgba(15,17,22,0.10), 0 2px 6px -2px rgba(15,17,22,0.06)', display: 'flex', diff --git a/openless-all/app/src/components/WindowChrome.tsx b/openless-all/app/src/components/WindowChrome.tsx index a572c5dc..81ff3b65 100644 --- a/openless-all/app/src/components/WindowChrome.tsx +++ b/openless-all/app/src/components/WindowChrome.tsx @@ -30,6 +30,8 @@ export function detectOS(): OS { const MAC_TITLEBAR_HEIGHT = 36; const MAC_SYSTEM_CONTROLS_RESERVED_WIDTH = 80; +const MAC_WINDOW_RADIUS = 20; +const WIN_WINDOW_RADIUS = 0; interface WindowChromeProps { os?: OS; @@ -45,7 +47,7 @@ export function WindowChrome({ os = 'mac', title = 'OpenLess', children, height width: '100%', height, position: 'relative', - borderRadius: os === 'mac' ? 20 : 14, + borderRadius: os === 'mac' ? MAC_WINDOW_RADIUS : WIN_WINDOW_RADIUS, boxShadow: 'var(--ol-shadow-xl)', overflow: 'hidden', display: 'flex', diff --git a/openless-all/app/src/styles/global.css b/openless-all/app/src/styles/global.css index 8ab6a8d5..cda62844 100644 --- a/openless-all/app/src/styles/global.css +++ b/openless-all/app/src/styles/global.css @@ -3,17 +3,19 @@ html, body, #root { width: 100%; margin: 0; padding: 0; + background: transparent; + overflow: hidden; } body { background: transparent; user-select: none; -webkit-user-select: none; - overflow: hidden; } #root { display: flex; + isolation: isolate; } button {