From 58ff3e93d04dbce5091ff2b9984147086d9a4ed7 Mon Sep 17 00:00:00 2001 From: arookieofc <2128194521@qq.com> Date: Mon, 2 Nov 2026 19:04:25 +0800 Subject: [PATCH] fix(linux): paint a hairline edge on the frameless settings window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Linux compositors draw no shadow for undecorated windows (Wayland has no server-side decorations at all), so the frameless settings window is indistinguishable from the desktop there — #595's follow-up after the Windows DWM-shadow fix. Paint a 1px --border-window inset ring around the window root: the webview draws the same hairline on every windowing system. --- .../settings/__tests__/dialog.test.tsx | 20 +++++++++++++++++++ .../settings/dialog/SettingsDialog.module.css | 5 +++++ .../settings/window/SettingsWindow.tsx | 6 ++++-- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/features/settings/__tests__/dialog.test.tsx b/src/features/settings/__tests__/dialog.test.tsx index a72f4752..a187bf41 100644 --- a/src/features/settings/__tests__/dialog.test.tsx +++ b/src/features/settings/__tests__/dialog.test.tsx @@ -22,6 +22,7 @@ vi.mock("@/platform/ipc", async (importOriginal) => { }); import { registerAction, executeAction } from "@/platform/actions"; +import { usePlatform } from "@/platform/platformInfo"; import { useProject } from "@/platform/project"; import { useKeymap } from "@/platform/keymap"; import { ContextMenuHost } from "@/ui"; @@ -29,6 +30,8 @@ import { PageContext, useSetting } from "../components/context"; import { DEFAULT_SETTINGS, setAt } from "../model/schema"; import { openSettings, stage, useSettingsDialog } from "../model/staging"; import { SettingsDialog } from "../dialog/SettingsDialog"; +import { SettingsWindow } from "../window/SettingsWindow"; +import dialogStyles from "../dialog/SettingsDialog.module.css"; import { activate } from ".."; import { MessagesHost } from "@/workbench/messages"; @@ -471,6 +474,23 @@ describe("Settings dialog", () => { }); }); +describe("native Settings window root", () => { + it("marks the window edge on Linux, whose frameless windows get no compositor shadow", () => { + const saved = usePlatform.getState(); + usePlatform.setState({ os: "linux" }); + let view = render(); + let root = document.querySelector(`.${dialogStyles.window}`); + expect(root).toHaveClass(dialogStyles.windowEdge!); + view.unmount(); + usePlatform.setState({ os: "windows" }); + view = render(); + root = document.querySelector(`.${dialogStyles.window}`); + expect(root).not.toHaveClass(dialogStyles.windowEdge!); + view.unmount(); + usePlatform.setState(saved); + }); +}); + describe("native Settings window background", () => { it.each([ ["a hex frame", "#26282c", "", "#26282c"], diff --git a/src/features/settings/dialog/SettingsDialog.module.css b/src/features/settings/dialog/SettingsDialog.module.css index 24fdea93..0a1105ab 100644 --- a/src/features/settings/dialog/SettingsDialog.module.css +++ b/src/features/settings/dialog/SettingsDialog.module.css @@ -203,6 +203,11 @@ background: var(--bg-dialog, var(--bg-elev)); color: var(--fg); } +/* Linux draws no shadow for a frameless window (Wayland/KDE give undecorated + windows no CSD), so mark its edge in CSS instead. */ +.windowEdge { + box-shadow: inset 0 0 0 1px var(--border-window); +} .windowBody { flex: 1; min-height: 0; diff --git a/src/features/settings/window/SettingsWindow.tsx b/src/features/settings/window/SettingsWindow.tsx index e5509490..231a6064 100644 --- a/src/features/settings/window/SettingsWindow.tsx +++ b/src/features/settings/window/SettingsWindow.tsx @@ -1,4 +1,5 @@ /** Content of the native Settings window: title bar, page tree + page, buttons. */ +import clsx from "clsx"; import { useEffect, useRef } from "react"; import { OverlayHost } from "@/workbench/OverlayHost"; import { usePlatform } from "@/platform/platformInfo"; @@ -10,6 +11,7 @@ import styles from "../dialog/SettingsDialog.module.css"; export function SettingsWindow() { useApplyTheme(); + const os = usePlatform((s) => s.os); const ownTitleBar = usePlatform((s) => s.overlayTitleBar || s.framelessTitleBar); const open = useSettingsDialog((s) => s.open); const title = useSettingsTitle(); @@ -20,8 +22,8 @@ export function SettingsWindow() { }, [open]); return ( -
- {/* macOS overlays traffic lights on it; Windows is frameless. Linux keeps the OS title bar. */} +
+ {/* macOS overlays traffic lights on it; Windows and Linux are frameless (tauri..conf.json). */} {ownTitleBar && } {open && ( <>