From 4254d835ff4ce703ea0aacd48411f6fb6bd8bf0c Mon Sep 17 00:00:00 2001 From: aeoform <2790848120@qq.com> Date: Mon, 25 May 2026 23:38:46 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20Linux=20Wayland=20=E6=94=B9=E7=94=A8?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E7=BA=A7=E8=87=AA=E5=AE=9A=E4=B9=89=E6=A0=87?= =?UTF-8?q?=E9=A2=98=E6=A0=8F=EF=BC=8C=E7=BB=95=E8=BF=87=20GTK=20CSD=20?= =?UTF-8?q?=E7=82=B9=E5=87=BB=E6=97=A0=E5=93=8D=E5=BA=94=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KDE/Wayland 下 GTK Client-Side Decorations 的 minimize/maximize/close 按钮无法响应鼠标事件(tao#1218, tauri#9394)。此前尝试的 WebKit 环境变量 + async nudge resize 均不能可靠修复,最终方案参考 cc-switch,完全放弃系统装饰: - decorations: false → 应用级 WindowChrome 绘制 36px 拖拽栏 + 三键窗口控制 - transparent: true → 配合 CSS 磨砂玻璃背景 - WEBKIT_DISABLE_COMPOSITING_MODE + DMABUF_RENDERER 兜底 WebKit 合成异常 - async nudge(set_focus + ±1px resize)保留为辅助修复,防止 show() 后 webview surface 输入区域未初始化 Co-Authored-By: Claude Opus 4.7 --- openless-all/app/src-tauri/src/lib.rs | 46 +++++- openless-all/app/src-tauri/src/main.rs | 15 ++ .../app/src-tauri/tauri.linux.conf.json | 4 +- .../app/src/components/WindowChrome.tsx | 145 ++++++++++++++++-- 4 files changed, 197 insertions(+), 13 deletions(-) diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs index 33bb590d..852bbb24 100644 --- a/openless-all/app/src-tauri/src/lib.rs +++ b/openless-all/app/src-tauri/src/lib.rs @@ -204,8 +204,50 @@ pub fn run() { let suppress_show = !force_show && coordinator.prefs().get().start_minimized; if suppress_show { log::info!("[main] start_minimized=true → 跳过初始 show,等用户点托盘"); - } else if let Err(e) = main.show() { - log::warn!("[main] initial show failed: {e}"); + } else { + #[cfg(target_os = "linux")] + { + // Workaround for Linux Wayland WebKitGTK compositing bugs: + // `visible:false` → `show()` can leave the webview surface + // without a valid input region, making the entire window + // (including CSD titlebar buttons) unresponsive to clicks. + // + // The fix: after show(), async-delay for webview realize, + // then set_focus() + ±1px pseudo-resize to force GTK's + // size-allocate → input surface reattach. + // Ref: tauri#9394, cc-switch linux_fix.rs + let main_clone = main.clone(); + let _ = main_clone.set_focus(); + tauri::async_runtime::spawn(async move { + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + let _ = main_clone.set_focus(); + if let Ok(orig) = main_clone.inner_size() { + let bumped = tauri::PhysicalSize::new( + orig.width.saturating_add(1), + orig.height, + ); + let _ = main_clone.set_size(bumped); + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + let _ = main_clone.set_size(orig); + log::info!("[main] Linux nudge: focus + surface reactivation done"); + // Reconcile: compositor may have coalesced the two + // set_size calls, leaving the window at width+1. + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + if let Ok(after) = main_clone.inner_size() { + if after.width != orig.width || after.height != orig.height { + log::info!( + "[main] Linux nudge drift detected: {}x{} → {}x{}, correcting", + orig.width, orig.height, after.width, after.height + ); + let _ = main_clone.set_size(orig); + } + } + } + }); + } + if let Err(e) = main.show() { + log::warn!("[main] initial show failed: {e}"); + } } } diff --git a/openless-all/app/src-tauri/src/main.rs b/openless-all/app/src-tauri/src/main.rs index cb1e6ae9..dc101365 100644 --- a/openless-all/app/src-tauri/src/main.rs +++ b/openless-all/app/src-tauri/src/main.rs @@ -2,5 +2,20 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { + // Work around WebKitGTK compositing bugs on Linux Wayland: + // - WEBKIT_DISABLE_COMPOSITING_MODE=1 fixes "whole window unresponsive + // to clicks until maximize/restore" (tauri#9394) + // - WEBKIT_DISABLE_DMABUF_RENDERER=1 fixes white/black screen on some + // GPU/driver combos (e.g. Nvidia + Debian) + #[cfg(target_os = "linux")] + { + if std::env::var("WEBKIT_DISABLE_DMABUF_RENDERER").is_err() { + std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); + } + if std::env::var("WEBKIT_DISABLE_COMPOSITING_MODE").is_err() { + std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1"); + } + } + openless_lib::run(); } diff --git a/openless-all/app/src-tauri/tauri.linux.conf.json b/openless-all/app/src-tauri/tauri.linux.conf.json index 4016955d..c77074e2 100644 --- a/openless-all/app/src-tauri/tauri.linux.conf.json +++ b/openless-all/app/src-tauri/tauri.linux.conf.json @@ -10,8 +10,8 @@ "minWidth": 980, "minHeight": 640, "resizable": true, - "decorations": true, - "transparent": false, + "decorations": false, + "transparent": true, "shadow": true, "visible": false, "acceptFirstMouse": true diff --git a/openless-all/app/src/components/WindowChrome.tsx b/openless-all/app/src/components/WindowChrome.tsx index fdd3d76f..3566bc50 100644 --- a/openless-all/app/src/components/WindowChrome.tsx +++ b/openless-all/app/src/components/WindowChrome.tsx @@ -1,4 +1,4 @@ -import { type CSSProperties, type ReactNode } from 'react'; +import { type CSSProperties, type ReactNode, useCallback, useEffect, useRef, useState } from 'react'; export type OS = 'mac' | 'win' | 'linux'; @@ -16,6 +16,7 @@ export function detectOS(): OS { const MAC_TITLEBAR_HEIGHT = 28; const MAC_SYSTEM_CONTROLS_RESERVED_WIDTH = 76; +const LINUX_TITLEBAR_HEIGHT = 36; const WIN_CONSOLE_RADIUS = 10; interface WindowChromeProps { @@ -30,17 +31,10 @@ export function WindowChrome({ children, height = 800, }: WindowChromeProps) { - // Windows 下交还原生外壳(decorations:true):外层不画圆角 / 边框 / 阴影 / 标题栏, - // 避免与原生窗口的角和关闭按钮重叠。内层卡片保留 10px 圆角,跟整体设计对齐。 const shellRadius = os === 'mac' ? 0 : os === 'win' ? 0 : 14; const consoleRadius = os === 'mac' ? 20 : os === 'win' ? WIN_CONSOLE_RADIUS : 14; - const titlebarHeight = os === 'mac' ? MAC_TITLEBAR_HEIGHT : 0; + const titlebarHeight = os === 'mac' ? MAC_TITLEBAR_HEIGHT : os === 'linux' ? LINUX_TITLEBAR_HEIGHT : 0; - // 两个平台用同一份半透明玻璃 background + backdropFilter,让 sidebar 透明地坐在 - // 磨砂底板上时有可见的玻璃感。 - // Windows: Tauri transparent:true + lib.rs apply_mica 提供 Win11 Mica 透出来; - // macOS: NSVisualEffectView 提供材质。 - // alpha 0.92:和原生 Win11 caption(lib.rs 设为 rgb(245,245,247))色差最小,玻璃感仍可见。 const background = ` radial-gradient(120% 80% at 0% 0%, rgba(255,255,255,0.55) 0%, rgba(255,255,255,0) 60%), radial-gradient(100% 70% at 100% 100%, rgba(37,99,235,0.07) 0%, rgba(37,99,235,0) 55%), @@ -83,9 +77,142 @@ export function WindowChrome({ }} /> )} + {os === 'linux' && }
{children}
); } + +// ── Linux custom titlebar — mirrors cc-switch's approach ── + +function LinuxTitlebar() { + const [maximized, setMaximized] = useState(false); + const winRef = useRef(null); + + useEffect(() => { + let cancelled = false; + import('@tauri-apps/api/window').then(({ getCurrentWindow }) => { + if (cancelled) return; + const w = getCurrentWindow(); + winRef.current = w; + w.isMaximized().then((m: boolean) => { + if (!cancelled) setMaximized(m); + }).catch(() => {}); + }).catch(() => {}); + return () => { cancelled = true; }; + }, []); + + const onMinimize = useCallback(() => { + winRef.current?.minimize().catch(() => {}); + }, []); + + const onToggleMaximize = useCallback(() => { + winRef.current?.toggleMaximize().catch(() => {}); + setMaximized((m) => !m); + }, []); + + const onClose = useCallback(() => { + winRef.current?.close().catch(() => {}); + }, []); + + return ( +
+ OpenLess +
e.stopPropagation()} + > + + + +
+ +
+ ); +} + +// ── inline SVG icons (no lucide-react dep) ── + +const svgWrap: CSSProperties = { width: 12, height: 12, display: 'block' }; +const ctrlBtn: CSSProperties = { + width: 30, height: 24, + display: 'inline-flex', alignItems: 'center', justifyContent: 'center', + borderRadius: 5, border: 0, padding: 0, + background: 'transparent', color: 'var(--ol-ink-3)', + fontFamily: 'inherit', cursor: 'default', + transition: 'background 0.12s, color 0.12s', +}; +const closeBtn: CSSProperties = { + ...ctrlBtn, +}; + +function MinimizeSvg() { + return ( + + + + ); +} + +function MaximizeSvg() { + return ( + + + + ); +} + +function RestoreSvg() { + return ( + + + + + ); +} + +function CloseSvg() { + return ( + + + + ); +} From a4705fa2869d9b1f2fde0ca6f58c7638081639c8 Mon Sep 17 00:00:00 2001 From: aeoform <2790848120@qq.com> Date: Mon, 25 May 2026 23:55:45 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20PR=20review=20?= =?UTF-8?q?=E5=8F=91=E7=8E=B0=E7=9A=84=E4=B8=A4=E4=B8=AA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 最大化状态同步:mount 后监听 tauri://resize 事件,每次 resize 都重新查询 isMaximized(),确保双击拖拽栏/键盘快捷键触发最大化时图标不脱节 - 按钮点击后 300ms 再次查询 isMaximized(),防止窗口管理器拒绝 toggle 时 状态错误 - 启动 nudge drift check:只修正 ±1px 的 nudge 产物,不再覆盖用户/合成器 的实质性窗口大小变化 - 还原误删的中文注释,移除冗余 closeBtn 常量, + )}
{children}
@@ -87,21 +95,36 @@ export function WindowChrome({ // ── Linux custom titlebar — mirrors cc-switch's approach ── +type TauriWindow = import('@tauri-apps/api/window').Window; + function LinuxTitlebar() { const [maximized, setMaximized] = useState(false); - const winRef = useRef(null); + const winRef = useRef(null); useEffect(() => { let cancelled = false; + let unlisten: (() => void) | undefined; import('@tauri-apps/api/window').then(({ getCurrentWindow }) => { if (cancelled) return; const w = getCurrentWindow(); winRef.current = w; - w.isMaximized().then((m: boolean) => { + w.isMaximized().then((m) => { if (!cancelled) setMaximized(m); }).catch(() => {}); + // Keep icon in sync when user maximizes via double-click / keyboard shortcut + w.listen('tauri://resize', () => { + if (cancelled) return; + w.isMaximized().then((m) => { + if (!cancelled) setMaximized(m); + }).catch(() => {}); + }).then((fn) => { + if (!cancelled) unlisten = fn; + }).catch(() => {}); }).catch(() => {}); - return () => { cancelled = true; }; + return () => { + cancelled = true; + unlisten?.(); + }; }, []); const onMinimize = useCallback(() => { @@ -109,8 +132,13 @@ function LinuxTitlebar() { }, []); const onToggleMaximize = useCallback(() => { - winRef.current?.toggleMaximize().catch(() => {}); - setMaximized((m) => !m); + const w = winRef.current; + if (!w) return; + w.toggleMaximize().catch(() => {}); + // Re-query after window manager processes the toggle, in case WM rejects it + setTimeout(() => { + w.isMaximized().then(setMaximized).catch(() => {}); + }, 300); }, []); const onClose = useCallback(() => { @@ -154,17 +182,11 @@ function LinuxTitlebar() { onClick={onClose} aria-label="Close" className="ol-linux-close-btn" - style={closeBtn} + style={ctrlBtn} > - ); } @@ -180,9 +202,6 @@ const ctrlBtn: CSSProperties = { fontFamily: 'inherit', cursor: 'default', transition: 'background 0.12s, color 0.12s', }; -const closeBtn: CSSProperties = { - ...ctrlBtn, -}; function MinimizeSvg() { return (