fix(windows): 收紧主窗口 shell 外观契约
This commit is contained in:
1 parent
946d842023
commit
802ee653c9
4 files changed
+54
-32
No files matched your search
@@ -15,18 +15,39 @@ function assertMatch(source, pattern, name) {
|
||||
const raw = await readFile(new URL('../src-tauri/tauri.conf.json', import.meta.url), 'utf-8');
|
||||
const config = JSON.parse(raw);
|
||||
const capsuleWindow = config.app.windows.find((window) => window.label === 'capsule');
|
||||
const mainWindow = config.app.windows.find((window) => window.label === 'main');
|
||||
const libRs = await readFile(new URL('../src-tauri/src/lib.rs', import.meta.url), 'utf-8');
|
||||
const coordinatorRs = await readFile(new URL('../src-tauri/src/coordinator.rs', import.meta.url), 'utf-8');
|
||||
const capsuleTsx = await readFile(new URL('../src/components/Capsule.tsx', import.meta.url), 'utf-8');
|
||||
const capsuleLayoutTs = await readFile(new URL('../src/lib/capsuleLayout.ts', import.meta.url), 'utf-8');
|
||||
const windowChromeTsx = await readFile(new URL('../src/components/WindowChrome.tsx', import.meta.url), 'utf-8');
|
||||
const floatingShellTsx = await readFile(new URL('../src/components/FloatingShell.tsx', import.meta.url), 'utf-8');
|
||||
|
||||
if (!capsuleWindow) {
|
||||
throw new Error('capsule window config missing');
|
||||
}
|
||||
if (!mainWindow) {
|
||||
throw new Error('main window config missing');
|
||||
}
|
||||
assertEqual(capsuleWindow.width, 220, 'windows capsule config keeps translation-capable width baseline');
|
||||
assertEqual(capsuleWindow.height, 110, 'windows capsule config keeps translation-capable height baseline');
|
||||
assertEqual(capsuleWindow.transparent, true, 'capsule window should keep transparent visuals');
|
||||
assertEqual(capsuleWindow.alwaysOnTop, true, 'capsule window should stay above the focused app while recording');
|
||||
assertEqual(mainWindow.decorations, false, 'windows main window should use only custom titlebar');
|
||||
assertEqual(mainWindow.visible, false, 'windows main window should stay hidden until the intended first show point');
|
||||
|
||||
if (!/function WindowsResizeHandles\(\)/.test(windowChromeTsx)) {
|
||||
throw new Error('windows frameless shell should expose explicit resize handles');
|
||||
}
|
||||
|
||||
if (!/startResizeDragging\(direction\)/.test(windowChromeTsx)) {
|
||||
throw new Error('windows resize handles should delegate edge dragging to Tauri');
|
||||
}
|
||||
|
||||
if (!/borderRadius:\s*'var\(--ol-window-console-radius\)'/.test(floatingShellTsx)) {
|
||||
throw new Error('floating shell should consume the shared window-console radius');
|
||||
}
|
||||
|
||||
assertMatch(
|
||||
coordinatorRs,
|
||||
/let visible = !matches!\(state,\s*CapsuleState::Idle\);/,
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"minWidth": 980,
|
||||
"minHeight": 640,
|
||||
"resizable": true,
|
||||
"decorations": true,
|
||||
"decorations": false,
|
||||
"transparent": true,
|
||||
"shadow": true,
|
||||
"hiddenTitle": true,
|
||||
|
||||
@@ -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 : 10,
|
||||
borderRadius: 'var(--ol-window-console-radius)',
|
||||
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',
|
||||
|
||||
@@ -1,16 +1,3 @@
|
||||
// WindowChrome.tsx — frosted outer frame + raised inner console pattern.
|
||||
// The OUTER frame is a translucent shell with a tinted backdrop showing through.
|
||||
// The INNER content lives in a single raised card that floats above it.
|
||||
//
|
||||
// Layout per window:
|
||||
// ┌─ frosted outer ───────────────────────────────┐
|
||||
// │ [titlebar] │
|
||||
// │ ┌─ raised console (white, shadow) ─┐ │
|
||||
// │ │ sidebar │ main │ │
|
||||
// │ └──────────────────────────────────┘ │
|
||||
// │ [icon footer] │
|
||||
// └───────────────────────────────────────────────┘
|
||||
|
||||
import { useState, type CSSProperties, type ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
@@ -31,8 +18,9 @@ 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;
|
||||
export const WIN_TITLEBAR_HEIGHT = 36;
|
||||
export const WIN_WINDOW_RADIUS = 10;
|
||||
export const WIN_CONSOLE_RADIUS = 10;
|
||||
const WIN_RESIZE_EDGE = 6;
|
||||
const WIN_RESIZE_CORNER = 14;
|
||||
|
||||
@@ -53,19 +41,32 @@ interface WindowChromeProps {
|
||||
height?: number | string;
|
||||
}
|
||||
|
||||
export function WindowChrome({ os = 'mac', title = 'OpenLess', children, height = 800 }: WindowChromeProps) {
|
||||
export function WindowChrome({
|
||||
os = 'mac',
|
||||
title = 'OpenLess',
|
||||
children,
|
||||
height = 800,
|
||||
}: WindowChromeProps) {
|
||||
const shellRadius = os === 'mac' ? 20 : os === 'win' ? WIN_WINDOW_RADIUS : 14;
|
||||
const consoleRadius = os === 'mac' ? 20 : os === 'win' ? WIN_CONSOLE_RADIUS : 14;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
'--ol-window-shell-radius': `${shellRadius}px`,
|
||||
'--ol-window-console-radius': `${consoleRadius}px`,
|
||||
'--ol-window-titlebar-height': `${os === 'mac' ? MAC_TITLEBAR_HEIGHT : WIN_TITLEBAR_HEIGHT}px`,
|
||||
width: '100%',
|
||||
height,
|
||||
position: 'relative',
|
||||
borderRadius: os === 'mac' ? MAC_WINDOW_RADIUS : WIN_WINDOW_RADIUS,
|
||||
boxShadow: 'var(--ol-shadow-xl)',
|
||||
borderRadius: 'var(--ol-window-shell-radius)',
|
||||
boxShadow: os === 'win'
|
||||
? '0 22px 54px -28px rgba(15, 17, 22, 0.38), 0 10px 30px -20px rgba(15, 17, 22, 0.22), 0 0 0 0.5px rgba(0, 0, 0, 0.10)'
|
||||
: 'var(--ol-shadow-xl)',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
border: '0.5px solid rgba(0,0,0,.10)',
|
||||
border: os === 'win' ? '1px solid rgba(255,255,255,0.18)' : '0.5px solid rgba(0,0,0,.10)',
|
||||
background: `
|
||||
radial-gradient(120% 80% at 0% 0%, rgba(255,255,255,0.7) 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%),
|
||||
@@ -73,12 +74,10 @@ export function WindowChrome({ os = 'mac', title = 'OpenLess', children, height
|
||||
`,
|
||||
backdropFilter: 'blur(40px) saturate(180%)',
|
||||
WebkitBackdropFilter: 'blur(40px) saturate(180%)',
|
||||
}}
|
||||
} as CSSProperties}
|
||||
>
|
||||
{os === 'win' && <WinTitleBar title={title} />}
|
||||
{os === 'win' && <WindowsResizeHandles />}
|
||||
{/* macOS:三色窗口按钮交给系统绘制和定位。这里只保留顶部拖动区,
|
||||
并避开系统按钮热区,防止拖动层吞掉 close/minimize/zoom 点击。 */}
|
||||
{os === 'mac' && (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
@@ -108,12 +107,14 @@ function WinTitleBar({ title }: WinTitleBarProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: 36,
|
||||
height: WIN_TITLEBAR_HEIGHT,
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'stretch',
|
||||
position: 'relative',
|
||||
zIndex: 70,
|
||||
borderBottom: '0.5px solid rgba(0, 0, 0, 0.08)',
|
||||
background: 'linear-gradient(180deg, rgba(255,255,255,0.20) 0%, rgba(255,255,255,0.08) 100%)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -163,7 +164,7 @@ function WindowsResizeHandles() {
|
||||
if (event.button !== 0) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
void startWindowsResize(handle.direction);
|
||||
void startResizeDragging(handle.direction);
|
||||
}}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
@@ -180,17 +181,17 @@ function WindowsResizeHandles() {
|
||||
interface WinTitleButtonProps {
|
||||
title: string;
|
||||
action: 'minimize' | 'toggleMaximize' | 'close';
|
||||
children: ReactNode;
|
||||
tone?: 'default' | 'danger';
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function WinTitleButton({ title, action, children, tone = 'default' }: WinTitleButtonProps) {
|
||||
function WinTitleButton({ title, action, tone = 'default', children }: WinTitleButtonProps) {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [pressed, setPressed] = useState(false);
|
||||
const danger = tone === 'danger';
|
||||
const background = pressed
|
||||
? danger ? '#c42b1c' : 'rgba(0,0,0,0.12)'
|
||||
: hovered ? danger ? '#e81123' : 'rgba(0,0,0,0.08)'
|
||||
? danger ? '#c42b1c' : 'rgba(0, 0, 0, 0.12)'
|
||||
: hovered ? danger ? '#e81123' : 'rgba(0, 0, 0, 0.08)'
|
||||
: 'transparent';
|
||||
const color = danger && (hovered || pressed) ? '#fff' : 'var(--ol-ink-3)';
|
||||
|
||||
@@ -198,6 +199,7 @@ function WinTitleButton({ title, action, children, tone = 'default' }: WinTitleB
|
||||
<button
|
||||
style={{ ...winBtnStyle, background, color }}
|
||||
title={title}
|
||||
onClick={() => runWindowsWindowAction(action)}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => {
|
||||
setHovered(false);
|
||||
@@ -205,14 +207,13 @@ function WinTitleButton({ title, action, children, tone = 'default' }: WinTitleB
|
||||
}}
|
||||
onMouseDown={() => setPressed(true)}
|
||||
onMouseUp={() => setPressed(false)}
|
||||
onClick={() => runWindowsWindowAction(action)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
async function startWindowsResize(direction: ResizeDirection) {
|
||||
async function startResizeDragging(direction: ResizeDirection) {
|
||||
try {
|
||||
await getCurrentWindow().startResizeDragging(direction);
|
||||
} catch (error) {
|
||||
|
||||
Reference in new issue
Block a user