fix(window): harden frame geometry across window states

- Remove rounded renderer corners from opaque Linux window surfaces.
- Track maximized state with race-safe native reads for main and Properties windows.
- Flatten maximized Windows frames without changing macOS AppKit zoom contours.
- Make early platform detection explicit, testable, and fail closed on unsupported targets.
This commit is contained in:
NimBold
2026-09-05 20:54:13 +03:30
parent 1ddfaec338
commit c3afc414a3
8 changed files with 252 additions and 17 deletions
+3 -1
View File
@@ -57,6 +57,7 @@ import {
} from './utils/schedulerControl';
import { createSerialTaskQueue } from './utils/serialTaskQueue';
import { useWindowFocusState } from './utils/windowFocus';
import { useWindowMaximizedState } from './utils/windowMaximized';
const loadSettingsView = () => import('./components/SettingsView');
const loadSchedulerView = () => import('./components/SchedulerView');
@@ -189,6 +190,7 @@ function App() {
const { i18n, t } = useTranslation();
const platform = usePlatformInfo();
const isWindowActive = useWindowFocusState();
const isWindowMaximized = useWindowMaximizedState();
const [filter, setFilter] = useState<SidebarFilter>('all');
const [downloadTableSummary, setDownloadTableSummary] = useState<DownloadTableStatusSummary | null>(null);
const [coreReady, setCoreReady] = useState(false);
@@ -1222,7 +1224,7 @@ function App() {
}, [autoAddClipboardLinks, coreReady, showKeychainModal]);
return (
<div data-window-active={isWindowActive ? 'true' : 'false'} className={`app-shell app-shell--style-${windowControlStyle} flex h-screen w-screen overflow-hidden text-text-primary ${
<div data-window-active={isWindowActive ? 'true' : 'false'} data-window-maximized={isWindowMaximized ? 'true' : 'false'} className={`app-shell flex h-screen w-screen overflow-hidden text-text-primary ${
isSidebarOnRight ? 'app-shell--sidebar-right' : 'app-shell--sidebar-left'
} ${
hasWindowChrome ? 'app-shell--window-chrome' : ''
+4
View File
@@ -57,6 +57,7 @@ import { isTorrentLiveStatus } from '../utils/propertiesTorrentLifecycle';
import { isTorrentWaitingForPeers } from '../utils/torrentPresentation';
import { copyTorrentFilePath } from '../utils/torrentFilePath';
import { useWindowFocusState } from '../utils/windowFocus';
import { useWindowMaximizedState } from '../utils/windowMaximized';
import { WindowControls } from './WindowControls';
import {
TORRENT_ENCRYPTION_POLICY_DISABLED,
@@ -204,6 +205,7 @@ const propertiesDiagnosticLifecycleKey = (snapshot: PropertiesSnapshot): string
export const PropertiesWindowApp = () => {
const { t } = useTranslation();
const isWindowActive = useWindowFocusState();
const isWindowMaximized = useWindowMaximizedState();
const translationRef = useRef(t);
translationRef.current = t;
const currentWindow = useMemo(() => getCurrentWindow(), []);
@@ -1138,6 +1140,7 @@ export const PropertiesWindowApp = () => {
className={windowShellClassName}
style={windowShellStyle}
data-window-active={isWindowActive ? 'true' : 'false'}
data-window-maximized={isWindowMaximized ? 'true' : 'false'}
aria-labelledby="properties-window-title"
>
<WindowControls side={windowChrome.side} controlStyle={windowChrome.controlStyle} />
@@ -1253,6 +1256,7 @@ export const PropertiesWindowApp = () => {
className={windowShellClassName}
style={windowShellStyle}
data-window-active={isWindowActive ? 'true' : 'false'}
data-window-maximized={isWindowMaximized ? 'true' : 'false'}
aria-labelledby="properties-window-title"
>
<WindowControls side={windowChrome.side} controlStyle={windowChrome.controlStyle} />
+13
View File
@@ -2373,6 +2373,19 @@ html[data-list-density="relaxed"] {
border-width: 0.5px;
}
/* Linux uses an opaque GTK/WebKit surface, so renderer-only curves would
expose square native backing pixels. Maximized Windows surfaces likewise
need to meet the work area instead of leaving transparent corner cutouts.
macOS zoomed windows retain their native rounded AppKit contour. */
html[data-platform="linux"] :is(.app-shell, .properties-window-shell),
html[data-platform="windows"] :is(.app-shell, .properties-window-shell)[data-window-maximized="true"] {
border-radius: 0;
}
html[data-platform="windows"] :is(.app-shell, .properties-window-shell)[data-window-maximized="true"] {
border-color: transparent;
}
@media (forced-colors: active) {
.app-shell,
.properties-window-shell {
+8 -3
View File
@@ -14,7 +14,10 @@ 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';
import { syncPlatformDatasetFromUserAgent } from './utils/platform';
import { useWindowMaximizedState } from './utils/windowMaximized';
syncPlatformDatasetFromUserAgent(navigator.userAgent);
const isPropertiesWindow = getCurrentWindow().label.startsWith('properties-');
@@ -86,8 +89,9 @@ const renderRoot = (RootComponent: ComponentType) => {
const PropertiesStartupFailure = () => {
const isWindowActive = useWindowFocusState();
const isWindowMaximized = useWindowMaximizedState();
return (
<main data-window-active={isWindowActive ? 'true' : 'false'} className="properties-window-shell flex h-screen min-h-0 flex-col items-center justify-center gap-4 bg-main-bg p-6 text-text-primary">
<main data-window-active={isWindowActive ? 'true' : 'false'} data-window-maximized={isWindowMaximized ? 'true' : 'false'} className="properties-window-shell flex h-screen min-h-0 flex-col items-center justify-center gap-4 bg-main-bg p-6 text-text-primary">
<p role="alert">Download Properties could not be loaded.</p>
<button
type="button"
@@ -106,8 +110,9 @@ const PropertiesStartupFailure = () => {
const MainStartupFailure = () => {
const isWindowActive = useWindowFocusState();
const isWindowMaximized = useWindowMaximizedState();
return (
<main data-window-active={isWindowActive ? 'true' : 'false'} className="app-shell flex h-screen min-h-0 flex-col items-center justify-center gap-4 bg-main-bg p-6 text-text-primary">
<main data-window-active={isWindowActive ? 'true' : 'false'} data-window-maximized={isWindowMaximized ? 'true' : 'false'} className="app-shell flex h-screen min-h-0 flex-col items-center justify-center gap-4 bg-main-bg p-6 text-text-primary">
<p role="alert">Firelink could not be loaded.</p>
<button
type="button"
+31 -4
View File
@@ -1,5 +1,10 @@
import { describe, expect, it } from 'vitest';
import { shouldUseCustomWindowControls, syncPlatformDataset } from './platform';
import {
inferDesktopPlatform,
shouldUseCustomWindowControls,
syncPlatformDataset,
syncPlatformDatasetFromUserAgent,
} from './platform';
describe('shouldUseCustomWindowControls', () => {
it('keeps custom controls present while Windows/Linux detection is unresolved', () => {
@@ -35,17 +40,39 @@ describe('syncPlatformDataset', () => {
expect(mockDocument.documentElement.dataset.platform).toBe('linux');
});
it('ignores unknown or unsupported platforms', () => {
it('clears a stale platform when authoritative detection is unknown or unsupported', () => {
const mockDocument = { documentElement: { dataset: { platform: 'macos' } } };
syncPlatformDataset('unknown', mockDocument);
expect(mockDocument.documentElement.dataset.platform).toBe('macos');
expect(mockDocument.documentElement.dataset.platform).toBeUndefined();
mockDocument.documentElement.dataset.platform = 'macos';
syncPlatformDataset('android', mockDocument);
expect(mockDocument.documentElement.dataset.platform).toBe('macos');
expect(mockDocument.documentElement.dataset.platform).toBeUndefined();
});
it('safely handles missing document in headless environments', () => {
expect(() => syncPlatformDataset('macos', undefined)).not.toThrow();
});
});
describe('inferDesktopPlatform', () => {
it('identifies supported desktop webviews', () => {
expect(inferDesktopPlatform('Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0)')).toBe('macos');
expect(inferDesktopPlatform('Mozilla/5.0 (Windows NT 10.0; Win64; x64)')).toBe('windows');
expect(inferDesktopPlatform('Mozilla/5.0 (X11; Linux x86_64)')).toBe('linux');
});
it('does not misclassify mobile user agents that contain desktop tokens', () => {
expect(inferDesktopPlatform('Mozilla/5.0 (Linux; Android 14; Mobile)')).toBe('unknown');
expect(inferDesktopPlatform('Mozilla/5.0 (Macintosh; iPad; Mobile)')).toBe('unknown');
});
it('synchronizes the inferred platform before the renderer mounts', () => {
const mockDocument = { documentElement: { dataset: {} as Record<string, string | undefined> } };
syncPlatformDatasetFromUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64)', mockDocument);
expect(mockDocument.documentElement.dataset.platform).toBe('windows');
});
});
+14 -9
View File
@@ -35,18 +35,23 @@ export const syncPlatformDataset = (
if (!targetDocument) return;
if (os === 'macos' || os === 'windows' || os === 'linux') {
targetDocument.documentElement.dataset.platform = os;
} else {
delete targetDocument.documentElement.dataset.platform;
}
};
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 inferDesktopPlatform = (userAgent: string): PlatformInfo['os'] => {
if (/Android|iPhone|iPad|iPod|Mobile/i.test(userAgent)) return 'unknown';
if (/Macintosh|Mac OS X/i.test(userAgent)) return 'macos';
if (/Windows/i.test(userAgent)) return 'windows';
if (/Linux/i.test(userAgent)) return 'linux';
return 'unknown';
};
export const syncPlatformDatasetFromUserAgent = (
userAgent: string,
targetDocument: TargetDocument | undefined = typeof document !== 'undefined' ? document : undefined,
): void => syncPlatformDataset(inferDesktopPlatform(userAgent), targetDocument);
export const getPlatformInfo = (): Promise<PlatformInfo> => {
if (cached) return Promise.resolve(cached);
+88
View File
@@ -0,0 +1,88 @@
import { describe, expect, it, vi } from 'vitest';
import { subscribeToWindowMaximized, type WindowMaximizedSource } from './windowMaximized';
type WindowStateDisposer = () => void | Promise<void>;
const deferred = <T,>() => {
let resolve!: (value: T) => void;
const promise = new Promise<T>(settle => { resolve = settle; });
return { promise, resolve };
};
describe('window maximized state', () => {
it('reads state immediately and again after listener registration closes the startup gap', async () => {
const onChange = vi.fn();
const source: WindowMaximizedSource = {
isMaximized: vi.fn()
.mockResolvedValueOnce(false)
.mockResolvedValueOnce(true),
listenResized: vi.fn(async () => vi.fn()),
};
const dispose = subscribeToWindowMaximized(source, onChange);
await vi.waitFor(() => expect(onChange).toHaveBeenLastCalledWith(true));
expect(source.isMaximized).toHaveBeenCalledTimes(2);
dispose();
});
it('keeps the newest resize state when native reads resolve out of order', async () => {
const initial = deferred<boolean>();
const maximized = deferred<boolean>();
const restored = deferred<boolean>();
let onResize: (() => void) | undefined;
const onChange = vi.fn();
const source: WindowMaximizedSource = {
isMaximized: vi.fn()
.mockReturnValueOnce(initial.promise)
.mockReturnValueOnce(maximized.promise)
.mockReturnValueOnce(restored.promise)
.mockResolvedValue(false),
listenResized: vi.fn(async listener => {
onResize = listener;
return vi.fn();
}),
};
const dispose = subscribeToWindowMaximized(source, onChange);
await vi.waitFor(() => expect(source.isMaximized).toHaveBeenCalledTimes(2));
onResize?.();
expect(source.isMaximized).toHaveBeenCalledTimes(3);
restored.resolve(false);
maximized.resolve(true);
initial.resolve(false);
await vi.waitFor(() => expect(onChange).toHaveBeenCalledWith(false));
expect(onChange).toHaveBeenCalledTimes(1);
dispose();
});
it('disposes a listener that finishes registering after unmount and ignores late state', async () => {
const registration = deferred<WindowStateDisposer>();
const state = deferred<boolean>();
const disposer = vi.fn();
const onChange = vi.fn();
const source: WindowMaximizedSource = {
isMaximized: () => state.promise,
listenResized: () => registration.promise,
};
const dispose = subscribeToWindowMaximized(source, onChange);
dispose();
registration.resolve(disposer);
state.resolve(true);
await vi.waitFor(() => expect(disposer).toHaveBeenCalledOnce());
expect(onChange).not.toHaveBeenCalled();
});
it('survives synchronous native state and listener failures', () => {
const source: WindowMaximizedSource = {
isMaximized: () => { throw new Error('state unavailable'); },
listenResized: () => { throw new Error('listener unavailable'); },
};
expect(() => subscribeToWindowMaximized(source, vi.fn())()).not.toThrow();
});
});
+91
View File
@@ -0,0 +1,91 @@
import { getCurrentWindow } from '@tauri-apps/api/window';
import { useEffect, useMemo, useState } from 'react';
type WindowStateDisposer = () => void | Promise<void>;
export type WindowMaximizedSource = {
isMaximized: () => boolean | Promise<boolean>;
listenResized: (listener: () => void) => Promise<WindowStateDisposer>;
};
const createCurrentWindowMaximizedSource = (): WindowMaximizedSource => {
const currentWindow = getCurrentWindow();
return {
isMaximized: () => currentWindow.isMaximized(),
listenResized: listener => currentWindow.onResized(listener),
};
};
const safelyDispose = (disposer: WindowStateDisposer | undefined) => {
if (typeof disposer !== 'function') return;
try {
void Promise.resolve(disposer()).catch(() => undefined);
} catch {
// Window teardown remains best-effort if the native listener is already gone.
}
};
export const subscribeToWindowMaximized = (
source: WindowMaximizedSource,
onChange: (maximized: boolean) => void,
): (() => void) => {
let disposed = false;
let readGeneration = 0;
let lastPublished: boolean | undefined;
let listenerDisposer: WindowStateDisposer | undefined;
const refresh = () => {
const generation = ++readGeneration;
let stateRead: boolean | Promise<boolean>;
try {
stateRead = source.isMaximized();
} catch {
return;
}
void Promise.resolve(stateRead)
.then(maximized => {
if (disposed || generation !== readGeneration || maximized === lastPublished) return;
lastPublished = maximized;
onChange(maximized);
})
.catch(() => undefined);
};
let registration: Promise<WindowStateDisposer>;
try {
registration = Promise.resolve(source.listenResized(refresh));
} catch {
registration = Promise.reject();
}
void registration
.then(disposer => {
if (disposed) safelyDispose(disposer);
else if (typeof disposer === 'function') listenerDisposer = disposer;
// Close the registration gap with an authoritative post-listener read.
if (!disposed) refresh();
})
.catch(() => undefined);
// Do not delay the first state read behind asynchronous listener registration.
refresh();
return () => {
if (disposed) return;
disposed = true;
readGeneration += 1;
safelyDispose(listenerDisposer);
listenerDisposer = undefined;
};
};
export const useWindowMaximizedState = (providedSource?: WindowMaximizedSource): boolean => {
const source = useMemo(
() => providedSource ?? createCurrentWindowMaximizedSource(),
[providedSource],
);
const [maximized, setMaximized] = useState(false);
useEffect(() => subscribeToWindowMaximized(source, setMaximized), [source]);
return maximized;
};