diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 234701b2..4430dac4 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -46,7 +46,7 @@ import { TitleBar } from "./components/TitleBar"; import { PlayerBar } from "./components/player/PlayerBar"; import { QueuePanel } from "./components/player/QueuePanel"; import { useQueuePanelCollapsed } from "./settings/queuePanel"; -import { useNativeWindowControls } from "./settings/windowControls"; +import { useNativeWindowControls, useWindowOutline } from "./settings/windowControls"; /** Wide enough for a 44px cover plus breathing room, matching the sidebar rail's feel. */ const COLLAPSED_QUEUE_WIDTH = 62; @@ -377,6 +377,7 @@ export default function App() { const [queuePanelWidth, setQueuePanelWidth] = useState(340); const isQueuePanelCollapsed = useQueuePanelCollapsed(); const nativeWindowControls = useNativeWindowControls(); + const windowOutline = useWindowOutline(); const [loadingScreenState, setLoadingScreenState] = useState<"visible" | "leaving" | "hidden">("visible"); const [onboardingComplete, setOnboardingComplete] = useState(() => readLocalOnboardingComplete() ? true : null @@ -1987,10 +1988,14 @@ useEffect(() => { Both drop out under OS native decorations: the WM already draws a real frame above the webview there, so this edge would just be a stray line under the OS title bar. + The "Window outline" setting can also turn it off for desktops where it reads as a + white border rather than an edge. */}
{/* {!paperPcMode && } diff --git a/src/ui/components/settingsSearchIndex.ts b/src/ui/components/settingsSearchIndex.ts index 3c4b9846..40476ea0 100644 --- a/src/ui/components/settingsSearchIndex.ts +++ b/src/ui/components/settingsSearchIndex.ts @@ -30,14 +30,15 @@ const ENTRIES: SettingsSearchEntry[] = [ { tab: "system", category: "Library", title: "Download quality", description: "Choose quality for offline music." }, { tab: "system", category: "Library", title: "Lyrics", description: "Translate lyrics, set text size, and choose a source." }, { tab: "system", category: "Library", title: "Launch at startup", description: "Start Zuno when your computer starts." }, - { tab: "system", category: "Library", title: "Minimize to tray", description: "Keep Zuno available from the system tray." }, { tab: "system", category: "Library", title: "Remember window size and location", description: "Restore the main window geometry." }, { tab: "system", category: "Library", title: "Troubleshooting", description: "Open logs or reset Zuno data." }, { tab: "window", category: "Window", title: "Mini player", description: "Show a compact player when the main window loses focus." }, { tab: "window", category: "Window", title: "Library sidebar", description: "Choose the playlist rail size and hover behavior." }, { tab: "window", category: "Window", title: "Window controls", description: "Choose macOS, Windows, or native title-bar buttons." }, + { tab: "window", category: "Window", title: "Minimize to tray", description: "Keep Zuno available from the system tray." }, { tab: "window", category: "Window", title: "Compact player bar", description: "Use a smaller playback bar." }, { tab: "window", category: "Window", title: "System media controls", description: "Show playback in system media controls." }, + { tab: "window", category: "Window", title: "Window outline", description: "Show or hide the thin edge around the window." }, { tab: "shortcuts", category: "Shortcuts", title: "Keyboard shortcuts", description: "View, record, or clear keyboard bindings." }, ]; diff --git a/src/ui/pages/SettingsPage.tsx b/src/ui/pages/SettingsPage.tsx index 10f3d6e3..e9fb5e8e 100644 --- a/src/ui/pages/SettingsPage.tsx +++ b/src/ui/pages/SettingsPage.tsx @@ -127,9 +127,11 @@ import { import { setForceWindowControls, setNativeWindowControls, + setWindowOutline, setWindowsStyleWindowControls, useForceWindowControls, useNativeWindowControls, + useWindowOutline, useWindowsStyleWindowControls, } from "../settings/windowControls"; import { @@ -647,6 +649,7 @@ export function SettingsPage({ const windowsStyleWindowControls = useWindowsStyleWindowControls(); const nativeWindowControls = useNativeWindowControls(); const forceWindowControls = useForceWindowControls(); + const windowOutline = useWindowOutline(); const tilingWindowManager = useSyncExternalStore( subscribeTilingWindowManager, isTilingWindowManager, @@ -1755,15 +1758,6 @@ export function SettingsPage({ {autostartError &&

{autostartError}

} - - - - + {/* Next to the window buttons, because it decides what the close button does. */} + + {/* Only reachable when it does something: hidden once native chrome takes over, and off tiling compositors the buttons already show without this. */} {isLinux && tilingWindowManager && windowControlStyle !== "native" && ( @@ -2022,6 +2024,16 @@ export function SettingsPage({ /> )} + {/* The OS frame replaces the outline, so the switch would do nothing there. */} + {windowControlStyle !== "native" && ( + + )} + {isLinux && ( { const win = getCurrentWindow(); + // Inner size, because that is what `setSize` restores. Saving the outer size grew the + // window by the height of the OS frame on every launch with native decorations on. const [position, size, isMaximized, isFullscreen] = await Promise.all([ win.outerPosition(), - win.outerSize(), + win.innerSize(), win.isMaximized(), win.isFullscreen(), ]); diff --git a/src/ui/settings/windowControls.ts b/src/ui/settings/windowControls.ts index 3521d93f..fe788dd1 100644 --- a/src/ui/settings/windowControls.ts +++ b/src/ui/settings/windowControls.ts @@ -10,6 +10,7 @@ import { const WINDOWS_STYLE_STORAGE_KEY = "windows-style-window-controls"; const NATIVE_CONTROLS_STORAGE_KEY = "native-window-controls"; const FORCE_CONTROLS_STORAGE_KEY = "force-window-controls-on-tiling-wm"; +const WINDOW_OUTLINE_STORAGE_KEY = "window-outline-visible"; const CHANGE_EVENT = "window-controls-change"; function readBooleanSetting(key: string) { @@ -48,6 +49,13 @@ function readForceWindowControls() { return readBooleanSetting(FORCE_CONTROLS_STORAGE_KEY); } +function readWindowOutline() { + // Default on, so the frameless window keeps the edge it has always had. Some desktops + // already separate windows well (shadows, a dark wallpaper), where the light hairline + // reads as a stray white border, so this lets it go. + return readLocalBooleanSetting(WINDOW_OUTLINE_STORAGE_KEY, true); +} + function emitWindowControlsChange() { window.dispatchEvent(new Event(CHANGE_EVENT)); } @@ -65,6 +73,10 @@ export function setForceWindowControls(enabled: boolean) { writeBooleanSetting(FORCE_CONTROLS_STORAGE_KEY, enabled); } +export function setWindowOutline(visible: boolean) { + writeBooleanSetting(WINDOW_OUTLINE_STORAGE_KEY, visible); +} + export async function applyNativeWindowControls(enabled = readNativeWindowControls()) { try { await getCurrentWindow().setDecorations(enabled); @@ -87,6 +99,7 @@ export async function hydrateWindowControlSettings() { () => applyNativeWindowControls(), ), hydrateLocalBooleanSetting(FORCE_CONTROLS_STORAGE_KEY, false, CHANGE_EVENT), + hydrateLocalBooleanSetting(WINDOW_OUTLINE_STORAGE_KEY, true, CHANGE_EVENT), ]); } @@ -101,3 +114,7 @@ export function useNativeWindowControls() { export function useForceWindowControls() { return useSyncExternalStore(subscribe, readForceWindowControls, () => false); } + +export function useWindowOutline() { + return useSyncExternalStore(subscribe, readWindowOutline, () => true); +}