diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs index 33bb590d..e5096788 100644 --- a/openless-all/app/src-tauri/src/lib.rs +++ b/openless-all/app/src-tauri/src/lib.rs @@ -204,8 +204,47 @@ 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: + // `visible:false` → `show()` can leave the webview surface + // without a valid input region. The ±1px nudge forces + // GTK 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() { + // Only correct the ±1px nudge artifact — if the + // compositor or user resized the window significantly + // during this window, don't clobber that change. + let dw = if after.width > orig.width { after.width - orig.width } else { orig.width - after.width }; + let dh = if after.height > orig.height { after.height - orig.height } else { orig.height - after.height }; + if dw <= 1 && dh <= 1 && (dw > 0 || dh > 0) { + 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..0e669373 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,15 @@ export function WindowChrome({ children, height = 800, }: WindowChromeProps) { - // Windows 下交还原生外壳(decorations:true):外层不画圆角 / 边框 / 阴影 / 标题栏, - // 避免与原生窗口的角和关闭按钮重叠。内层卡片保留 10px 圆角,跟整体设计对齐。 + // Windows: decorations:true 时外层不画圆角/边框/阴影/标题栏,避免与原生窗口重叠。 + // Linux: decorations:false 时外层画 14px 圆角 + 自定义标题栏。 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))色差最小,玻璃感仍可见。 + // 三个平台共用半透明玻璃 background + backdropFilter。 + // macOS: NSVisualEffectView 提供材质;Windows: Tauri apply_mica 提供 Mica; + // Linux: decorations:false 后 CSS 磨砂玻璃自成背景。 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 +82,156 @@ export function WindowChrome({ }} /> )} + {os === 'linux' && } + {os === 'linux' && ( + + )}
{children}
); } + +// ── 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); + + 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) => { + 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; + unlisten?.(); + }; + }, []); + + const onMinimize = useCallback(() => { + winRef.current?.minimize().catch(() => {}); + }, []); + + const onToggleMaximize = useCallback(() => { + 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(() => { + 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', +}; + +function MinimizeSvg() { + return ( + + + + ); +} + +function MaximizeSvg() { + return ( + + + + ); +} + +function RestoreSvg() { + return ( + + + + + ); +} + +function CloseSvg() { + return ( + + + + ); +}