Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 additions & 2 deletions openless-all/app/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
}
}
}

Expand Down
15 changes: 15 additions & 0 deletions openless-all/app/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
4 changes: 2 additions & 2 deletions openless-all/app/src-tauri/tauri.linux.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"minWidth": 980,
"minHeight": 640,
"resizable": true,
"decorations": true,
"transparent": false,
"decorations": false,
"transparent": true,
"shadow": true,
"visible": false,
"acceptFirstMouse": true
Expand Down
164 changes: 155 additions & 9 deletions openless-all/app/src/components/WindowChrome.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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 {
Expand All @@ -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%),
Expand Down Expand Up @@ -83,9 +82,156 @@ export function WindowChrome({
}}
/>
)}
{os === 'linux' && <LinuxTitlebar />}
{os === 'linux' && (
<style>{`.ol-linux-close-btn:hover{background:rgba(220,38,38,0.12)!important;color:rgb(220,38,38)!important}`}</style>
)}
<div style={{ flex: 1, minHeight: 0, display: 'flex', position: 'relative' }}>
{children}
</div>
</div>
);
}

// ── 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<TauriWindow | null>(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 (
<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={ctrlBtn}
>
<CloseSvg />
</button>
</div>
</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',
};

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>
);
}
Loading