mirror of
https://github.com/nimbold/Firelink.git
synced 2026-09-11 14:00:31 +00:00
fix(window): refine cross-platform window frames and eliminate Windows shadow bleed
- Set shadow: false on Windows main and Properties window builders to prevent rectangular DWM shadow bleed behind rounded corners. - Refine active and inactive window frame tokens across themes for subtle, non-glaring boundaries. - Apply a 0.5px hairline window border rule specifically on macOS to match native Retina AppKit window strokes. - Synchronize document platform dataset attribute on app boot across main and Properties windows. - Update and extend window configuration and platform synchronization test suites.
This commit is contained in:
@@ -455,12 +455,14 @@ pub async fn open_download_properties_window(
|
||||
// A hidden WebView2 must not request focus during construction. The
|
||||
// native reveal path focuses it after the window is visible.
|
||||
.focused(false);
|
||||
// Native elevation follows the window shape on macOS. On Windows, Tauri
|
||||
// gives an undecorated window the system contour and Windows 11 corners as
|
||||
// well as its shadow. Linux does not implement this API, so its guaranteed
|
||||
// boundary remains the renderer's theme-aware contour and non-transparent frame.
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
// Native elevation follows the window shape on macOS. On Windows, Tao enables
|
||||
// an undecorated shadow that leaves an opaque native frame outside the rounded
|
||||
// renderer surface at the corners, so shadow is disabled. Linux does not implement
|
||||
// this API, so its boundary remains the renderer's theme-aware contour.
|
||||
#[cfg(target_os = "macos")]
|
||||
let builder = builder.transparent(true).shadow(true);
|
||||
#[cfg(target_os = "windows")]
|
||||
let builder = builder.transparent(true).shadow(false);
|
||||
#[cfg(target_os = "linux")]
|
||||
let builder = builder.transparent(false).shadow(false);
|
||||
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"minHeight": 640,
|
||||
"transparent": true,
|
||||
"decorations": false,
|
||||
"shadow": true
|
||||
"shadow": false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
+1
-1
@@ -1222,7 +1222,7 @@ function App() {
|
||||
}, [autoAddClipboardLinks, coreReady, showKeychainModal]);
|
||||
|
||||
return (
|
||||
<div data-window-active={isWindowActive ? 'true' : 'false'} className={`app-shell flex h-screen w-screen overflow-hidden text-text-primary ${
|
||||
<div data-window-active={isWindowActive ? 'true' : 'false'} className={`app-shell app-shell--style-${windowControlStyle} flex h-screen w-screen overflow-hidden text-text-primary ${
|
||||
isSidebarOnRight ? 'app-shell--sidebar-right' : 'app-shell--sidebar-left'
|
||||
} ${
|
||||
hasWindowChrome ? 'app-shell--window-chrome' : ''
|
||||
|
||||
+14
-10
@@ -27,8 +27,8 @@
|
||||
/* Keep this token alpha-free because some consumers apply their own /alpha. */
|
||||
--surface-overlay: 0 0% 100%;
|
||||
--shadow-color: 220 10% 20% / 0.1;
|
||||
--window-frame-active: 220 12% 30% / 0.60;
|
||||
--window-frame-inactive: 220 10% 30% / 0.18;
|
||||
--window-frame-active: 220 12% 30% / 0.22;
|
||||
--window-frame-inactive: 220 10% 30% / 0.08;
|
||||
--sidebar-shell-bg: 0 0% 92%;
|
||||
--sidebar-panel-bg: 0 0% 96%;
|
||||
--workspace-bg: 0 0% 98%;
|
||||
@@ -75,8 +75,8 @@
|
||||
--properties-header-surface: hsl(var(--bg-modal));
|
||||
--surface-overlay: 0 0% 100%;
|
||||
--shadow-color: 220 10% 20% / 0.1;
|
||||
--window-frame-active: 220 12% 30% / 0.60;
|
||||
--window-frame-inactive: 220 10% 30% / 0.18;
|
||||
--window-frame-active: 220 12% 30% / 0.22;
|
||||
--window-frame-inactive: 220 10% 30% / 0.08;
|
||||
--sidebar-shell-bg: 0 0% 92%;
|
||||
--sidebar-panel-bg: 0 0% 96%;
|
||||
--workspace-bg: 0 0% 98%;
|
||||
@@ -107,8 +107,8 @@
|
||||
--bg-input: 0 0% 16%;
|
||||
--properties-header-surface: hsl(0 0% 10%);
|
||||
--shadow-color: 0 0% 0% / 0.30;
|
||||
--window-frame-active: 0 0% 100% / 0.35;
|
||||
--window-frame-inactive: 0 0% 100% / 0.14;
|
||||
--window-frame-active: 0 0% 100% / 0.14;
|
||||
--window-frame-inactive: 0 0% 100% / 0.06;
|
||||
--status-completed: 136 62% 48%;
|
||||
--status-paused: 0 0% 56%;
|
||||
--status-downloading: 211 100% 56%;
|
||||
@@ -156,8 +156,8 @@
|
||||
--bg-input: 231 15% 20%;
|
||||
--properties-header-surface: hsl(231 15% 17%);
|
||||
--shadow-color: 231 20% 8% / 0.35;
|
||||
--window-frame-active: 228 14% 84% / 0.45;
|
||||
--window-frame-inactive: 228 14% 84% / 0.18;
|
||||
--window-frame-active: 228 14% 84% / 0.18;
|
||||
--window-frame-inactive: 228 14% 84% / 0.08;
|
||||
--status-completed: 135 94% 65%;
|
||||
--status-paused: 65 92% 76%;
|
||||
--status-downloading: 191 97% 77%;
|
||||
@@ -205,8 +205,8 @@
|
||||
--bg-input: 220 16% 24%;
|
||||
--properties-header-surface: hsl(220 16% 19%);
|
||||
--shadow-color: 220 25% 10% / 0.34;
|
||||
--window-frame-active: 218 27% 88% / 0.45;
|
||||
--window-frame-inactive: 218 27% 88% / 0.18;
|
||||
--window-frame-active: 218 27% 88% / 0.18;
|
||||
--window-frame-inactive: 218 27% 88% / 0.08;
|
||||
--status-completed: 92 28% 65%;
|
||||
--status-paused: 40 71% 73%;
|
||||
--status-downloading: 193 43% 67%;
|
||||
@@ -2369,6 +2369,10 @@ html[data-list-density="relaxed"] {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
html[data-platform="macos"] :is(.app-shell, .properties-window-shell) {
|
||||
border-width: 0.5px;
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
.app-shell,
|
||||
.properties-window-shell {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { error as logError, warn as logWarn, initLogger } from "./utils/logger";
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
import { invokeCommand as invoke } from './ipc';
|
||||
import { useWindowFocusState } from './utils/windowFocus';
|
||||
import './utils/platform';
|
||||
|
||||
const isPropertiesWindow = getCurrentWindow().label.startsWith('properties-');
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { shouldUseCustomWindowControls } from './platform';
|
||||
import { shouldUseCustomWindowControls, syncPlatformDataset } from './platform';
|
||||
|
||||
describe('shouldUseCustomWindowControls', () => {
|
||||
it('keeps custom controls present while Windows/Linux detection is unresolved', () => {
|
||||
@@ -20,3 +20,32 @@ describe('shouldUseCustomWindowControls', () => {
|
||||
expect(shouldUseCustomWindowControls('unknown', 'Mozilla/5.0 (Linux; Android 14; Mobile)')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('syncPlatformDataset', () => {
|
||||
it('synchronizes known desktop platforms to document dataset', () => {
|
||||
const mockDocument = { documentElement: { dataset: {} as Record<string, string | undefined> } };
|
||||
|
||||
syncPlatformDataset('macos', mockDocument);
|
||||
expect(mockDocument.documentElement.dataset.platform).toBe('macos');
|
||||
|
||||
syncPlatformDataset('windows', mockDocument);
|
||||
expect(mockDocument.documentElement.dataset.platform).toBe('windows');
|
||||
|
||||
syncPlatformDataset('linux', mockDocument);
|
||||
expect(mockDocument.documentElement.dataset.platform).toBe('linux');
|
||||
});
|
||||
|
||||
it('ignores unknown or unsupported platforms', () => {
|
||||
const mockDocument = { documentElement: { dataset: { platform: 'macos' } } };
|
||||
|
||||
syncPlatformDataset('unknown', mockDocument);
|
||||
expect(mockDocument.documentElement.dataset.platform).toBe('macos');
|
||||
|
||||
syncPlatformDataset('android', mockDocument);
|
||||
expect(mockDocument.documentElement.dataset.platform).toBe('macos');
|
||||
});
|
||||
|
||||
it('safely handles missing document in headless environments', () => {
|
||||
expect(() => syncPlatformDataset('macos', undefined)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,12 +22,39 @@ export const shouldUseCustomWindowControls = (os: string, userAgent: string): bo
|
||||
let cached: PlatformInfo | null = null;
|
||||
let pending: Promise<PlatformInfo> | null = null;
|
||||
|
||||
type TargetDocument = {
|
||||
documentElement: {
|
||||
dataset: Record<string, string | undefined>;
|
||||
};
|
||||
};
|
||||
|
||||
export const syncPlatformDataset = (
|
||||
os: string,
|
||||
targetDocument: TargetDocument | undefined = typeof document !== 'undefined' ? document : undefined,
|
||||
): void => {
|
||||
if (!targetDocument) return;
|
||||
if (os === 'macos' || os === 'windows' || os === 'linux') {
|
||||
targetDocument.documentElement.dataset.platform = os;
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof document !== 'undefined' && typeof navigator !== 'undefined') {
|
||||
if (/Macintosh|Mac OS X/i.test(navigator.userAgent)) {
|
||||
syncPlatformDataset('macos');
|
||||
} else if (/Windows/i.test(navigator.userAgent)) {
|
||||
syncPlatformDataset('windows');
|
||||
} else if (/Linux/i.test(navigator.userAgent)) {
|
||||
syncPlatformDataset('linux');
|
||||
}
|
||||
}
|
||||
|
||||
export const getPlatformInfo = (): Promise<PlatformInfo> => {
|
||||
if (cached) return Promise.resolve(cached);
|
||||
if (!pending) {
|
||||
pending = invoke('get_platform_info')
|
||||
.then(info => {
|
||||
cached = info;
|
||||
syncPlatformDataset(info.os);
|
||||
return info;
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
@@ -24,22 +24,25 @@ describe('main window configuration', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('uses native elevation where Tauri supports undecorated window shadows', () => {
|
||||
for (const [platform, config] of [
|
||||
['macOS', macosConfiguration],
|
||||
['Windows', windowsConfiguration],
|
||||
] as const) {
|
||||
expect(config.app.windows[0], platform).toMatchObject({
|
||||
transparent: true,
|
||||
decorations: false,
|
||||
shadow: true,
|
||||
});
|
||||
}
|
||||
it('uses native elevation on macOS where undecorated window shadows conform to the surface', () => {
|
||||
expect(macosConfiguration.app.windows[0], 'macOS').toMatchObject({
|
||||
transparent: true,
|
||||
decorations: false,
|
||||
shadow: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps Windows transparent without native shadow bleed outside rounded corners', () => {
|
||||
expect(windowsConfiguration.app.windows[0], 'Windows').toMatchObject({
|
||||
transparent: true,
|
||||
decorations: false,
|
||||
shadow: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps Linux on the renderer contour without claiming native shadow support', () => {
|
||||
const mainWindow = linuxConfiguration.app.windows[0];
|
||||
expect(mainWindow).toMatchObject({
|
||||
expect(mainWindow, 'Linux').toMatchObject({
|
||||
transparent: false,
|
||||
decorations: false,
|
||||
shadow: false,
|
||||
|
||||
Reference in New Issue
Block a user