fix: Linux Wayland 改用应用级自定义标题栏,绕过 GTK CSD 点击无响应问题
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
b7c6217519
commit
4254d835ff
4 files changed
+197
-13
No files matched your search
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -10,8 +10,8 @@
|
||||
"minWidth": 980,
|
||||
"minHeight": 640,
|
||||
"resizable": true,
|
||||
"decorations": true,
|
||||
"transparent": false,
|
||||
"decorations": false,
|
||||
"transparent": true,
|
||||
"shadow": true,
|
||||
"visible": false,
|
||||
"acceptFirstMouse": true
|
||||
|
||||
@@ -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' && <LinuxTitlebar />}
|
||||
<div style={{ flex: 1, minHeight: 0, display: 'flex', position: 'relative' }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Linux custom titlebar — mirrors cc-switch's approach ──
|
||||
|
||||
function LinuxTitlebar() {
|
||||
const [maximized, setMaximized] = useState(false);
|
||||
const winRef = useRef<any>(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 (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
style={{
|
||||
height: LINUX_TITLEBAR_HEIGHT,
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '0 6px 0 14px',
|
||||
background: 'rgba(245,245,247,0.85)',
|
||||
backdropFilter: 'blur(12px) saturate(180%)',
|
||||
WebkitBackdropFilter: 'blur(12px) saturate(180%)',
|
||||
borderBottom: '0.5px solid rgba(0,0,0,0.08)',
|
||||
color: 'var(--ol-ink-3)',
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
cursor: 'default',
|
||||
userSelect: 'none',
|
||||
zIndex: 50,
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'var(--ol-ink-2)' }}>OpenLess</span>
|
||||
<div
|
||||
style={{ display: 'flex', gap: 4, pointerEvents: 'auto' }}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button onClick={onMinimize} aria-label="Minimize" style={ctrlBtn}>
|
||||
<MinimizeSvg />
|
||||
</button>
|
||||
<button onClick={onToggleMaximize} aria-label={maximized ? 'Restore' : 'Maximize'} style={ctrlBtn}>
|
||||
{maximized ? <RestoreSvg /> : <MaximizeSvg />}
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
className="ol-linux-close-btn"
|
||||
style={closeBtn}
|
||||
>
|
||||
<CloseSvg />
|
||||
</button>
|
||||
</div>
|
||||
<style>{`
|
||||
.ol-linux-close-btn:hover {
|
||||
background: rgba(220,38,38,0.12) !important;
|
||||
color: rgb(220,38,38) !important;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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 (
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" style={svgWrap}>
|
||||
<rect x="2" y="5.5" width="8" height="1" rx="0.5" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MaximizeSvg() {
|
||||
return (
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" style={svgWrap}>
|
||||
<rect x="2" y="2" width="8" height="8" rx="1.5" stroke="currentColor" strokeWidth="1.1" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function RestoreSvg() {
|
||||
return (
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" style={svgWrap}>
|
||||
<rect x="3.6" y="0.6" width="7.2" height="7.2" rx="1.3" stroke="currentColor" strokeWidth="1.1" />
|
||||
<rect x="0.6" y="3.6" width="7.2" height="7.2" rx="1.3" fill="var(--ol-surface, #fff)" stroke="currentColor" strokeWidth="1.1" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function CloseSvg() {
|
||||
return (
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" style={svgWrap}>
|
||||
<path d="M2.8 2.8l6.4 6.4M9.2 2.8l-6.4 6.4" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user