From e6003c310e9e64ca587d8e9d67c96abd41da3e64 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Mon, 19 Aug 2024 14:16:09 -0700 Subject: [PATCH] Set background color for window controls on Linux (#247) The Window Controls Overlay API applies a transparent overlay on Windows, but not on Linux. This PR addresses this by capturing the area underneath the overlay, averaging the color of the area, and setting this as the overlay background color. It will also detect whether to make the control symbols white or black, depending on how dark the background color is. On Linux, this will set both the background color and the symbol color, on Windows it will just set the symbol color. image --- electron.vite.config.ts | 1 + emain/emain.ts | 22 ++++++++++++++ emain/preload.ts | 1 + frontend/app/app.tsx | 35 ++++++++++++++++++++-- frontend/types/custom.d.ts | 8 ++++++ package.json | 1 + yarn.lock | 59 +++++++++++++++++++++++++++++++++++++- 7 files changed, 124 insertions(+), 3 deletions(-) diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 2a7ab6fe0..86a2c6d61 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -17,6 +17,7 @@ export default defineConfig({ input: { index: "emain/emain.ts", }, + external: ["sharp"], }, outDir: "dist/main", }, diff --git a/emain/emain.ts b/emain/emain.ts index 27d1db865..e02edd32b 100644 --- a/emain/emain.ts +++ b/emain/emain.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import * as electron from "electron"; +import { getAverageColor } from "fast-average-color-node"; import fs from "fs"; import * as child_process from "node:child_process"; import os from "os"; @@ -584,6 +585,27 @@ electron.ipcMain.on("getEnv", (event, varName) => { event.returnValue = process.env[varName] ?? null; }); +electron.ipcMain.on("update-window-controls-overlay", async (event, rect: Dimensions) => { + if (unamePlatform !== "darwin") { + const zoomFactor = event.sender.getZoomFactor(); + const electronRect: Electron.Rectangle = { + x: rect.left * zoomFactor, + y: rect.top * zoomFactor, + height: rect.height * zoomFactor, + width: rect.width * zoomFactor, + }; + const overlay = await event.sender.capturePage(electronRect); + const overlayBuffer = overlay.toPNG(); + + const color = await getAverageColor(overlayBuffer); + const window = electron.BrowserWindow.fromWebContents(event.sender); + window.setTitleBarOverlay({ + color: unamePlatform === "linux" ? color.rgba : "#00000000", // Windows supports a true transparent overlay, so we don't need to set a background color. + symbolColor: color.isDark ? "white" : "black", + }); + } +}); + async function createNewWaveWindow() { const clientData = await services.ClientService.GetClientData(); const newWindow = await services.ClientService.MakeWindow(); diff --git a/emain/preload.ts b/emain/preload.ts index 674b3bd32..866f1e8f7 100644 --- a/emain/preload.ts +++ b/emain/preload.ts @@ -24,6 +24,7 @@ contextBridge.exposeInMainWorld("api", { onUpdaterStatusChange: (callback) => ipcRenderer.on("app-update-status", (_event, status) => callback(status)), getUpdaterStatus: () => ipcRenderer.sendSync("get-app-update-status"), installAppUpdate: () => ipcRenderer.send("install-app-update"), + updateWindowControlsOverlay: (rect) => ipcRenderer.send("update-window-controls-overlay", rect), }); // Custom event for "new-window" diff --git a/frontend/app/app.tsx b/frontend/app/app.tsx index 234d43261..bfd7d841f 100644 --- a/frontend/app/app.tsx +++ b/frontend/app/app.tsx @@ -5,11 +5,12 @@ import { useWaveObjectValue } from "@/app/store/wos"; import { Workspace } from "@/app/workspace/workspace"; import { deleteLayoutModelForTab, getLayoutModelForTab } from "@/layout/index"; import { ContextMenuModel } from "@/store/contextmenu"; -import { PLATFORM, WOS, atoms, globalStore, setBlockFocus } from "@/store/global"; +import { PLATFORM, WOS, atoms, getApi, globalStore, setBlockFocus } from "@/store/global"; import * as services from "@/store/services"; import { getWebServerEndpoint } from "@/util/endpoints"; import * as keyutil from "@/util/keyutil"; import * as util from "@/util/util"; +import useResizeObserver from "@react-hook/resize-observer"; import clsx from "clsx"; import Color from "color"; import * as csstree from "css-tree"; @@ -18,6 +19,7 @@ import "overlayscrollbars/overlayscrollbars.css"; import * as React from "react"; import { DndProvider } from "react-dnd"; import { HTML5Backend } from "react-dnd-html5-backend"; +import { debounce } from "throttle-debounce"; import "./app.less"; import { CenteredDiv } from "./element/quickelems"; @@ -291,7 +293,9 @@ function processBackgroundUrls(cssText: string): string { const backgroundAttr = "url(/Users/mike/Downloads/wave-logo_appicon.png) repeat-x fixed"; function AppBackground() { + const bgRef = React.useRef(null); const tabId = jotai.useAtomValue(atoms.activeTabId); + const windowOpacity = jotai.useAtomValue(atoms.settingsConfigAtom).window.opacity; const [tabData] = useWaveObjectValue(WOS.makeORef("tab", tabId)); const bgAttr = tabData?.meta?.bg; const style: React.CSSProperties = {}; @@ -311,7 +315,34 @@ function AppBackground() { console.error("error processing background", e); } } - return
; + const getAvgColor = React.useCallback( + debounce(10, () => { + if ( + bgRef.current && + PLATFORM !== "darwin" && + bgRef.current && + "windowControlsOverlay" in window.navigator + ) { + const titlebarRect: Dimensions = (window.navigator.windowControlsOverlay as any).getTitlebarAreaRect(); + const bgRect = bgRef.current.getBoundingClientRect(); + if (titlebarRect && bgRect) { + const windowControlsLeft = titlebarRect.width - titlebarRect.height; + const windowControlsRect: Dimensions = { + top: titlebarRect.top, + left: windowControlsLeft, + height: titlebarRect.height, + width: bgRect.width - bgRect.left - windowControlsLeft, + }; + getApi().updateWindowControlsOverlay(windowControlsRect); + } + } + }), + [bgRef, style] + ); + React.useEffect(getAvgColor, [getAvgColor]); + useResizeObserver(bgRef, getAvgColor); + + return
; } function genericClose(tabId: string) { diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index cefba9438..d8172459b 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -68,6 +68,7 @@ declare global { onUpdaterStatusChange: (callback: (status: UpdaterStatus) => void) => void; getUpdaterStatus: () => UpdaterStatus; installAppUpdate: () => void; + updateWindowControlsOverlay: (rect: Dimensions) => void; }; type ElectronContextMenuItem = { @@ -206,6 +207,13 @@ declare global { // jotai doesn't export this type :/ type Loadable = { state: "loading" } | { state: "hasData"; data: T } | { state: "hasError"; error: unknown }; + + interface Dimensions { + width: number; + height: number; + left: number; + top: number; + } } export {}; diff --git a/package.json b/package.json index f7c0d1c12..0716a3456 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "css-tree": "^2.3.1", "dayjs": "^1.11.12", "electron-updater": "6.3.3", + "fast-average-color-node": "^3.0.0", "htl": "^0.3.1", "html-to-image": "^1.11.11", "immer": "^10.1.1", diff --git a/yarn.lock b/yarn.lock index b6bb8048a..ac0c5ef28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7559,6 +7559,24 @@ __metadata: languageName: node linkType: hard +"fast-average-color-node@npm:^3.0.0": + version: 3.0.0 + resolution: "fast-average-color-node@npm:3.0.0" + dependencies: + fast-average-color: "npm:^9.4.0" + node-fetch: "npm:^2.6.7" + sharp: "npm:^0.33.2" + checksum: 10c0/92b9dd3f19a7ef64542e47d512ca25b93d38da5b4a3c47ee1e9e2e8df3f3a24cac1f2a21fd028b443c92dae3b608702195d596aaa0d6877d9fb9c990361ae69c + languageName: node + linkType: hard + +"fast-average-color@npm:^9.4.0": + version: 9.4.0 + resolution: "fast-average-color@npm:9.4.0" + checksum: 10c0/9031181113356abe240c52f78e908607e3b47dc0121cec3077b3735823951e40f8d6e14eca50d9941e30bcea60e0ed52e36410a8ded0972a89253c3dbefc966d + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -10450,6 +10468,20 @@ __metadata: languageName: node linkType: hard +"node-fetch@npm:^2.6.7": + version: 2.7.0 + resolution: "node-fetch@npm:2.7.0" + dependencies: + whatwg-url: "npm:^5.0.0" + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: 10c0/b55786b6028208e6fbe594ccccc213cab67a72899c9234eb59dba51062a299ea853210fcf526998eaa2867b0963ad72338824450905679ff0fa304b8c5093ae8 + languageName: node + linkType: hard + "node-gyp@npm:latest": version: 10.1.0 resolution: "node-gyp@npm:10.1.0" @@ -12008,7 +12040,7 @@ __metadata: languageName: node linkType: hard -"sharp@npm:^0.33.5": +"sharp@npm:^0.33.2, sharp@npm:^0.33.5": version: 0.33.5 resolution: "sharp@npm:0.33.5" dependencies: @@ -12640,6 +12672,7 @@ __metadata: electron-vite: "npm:^2.3.0" eslint: "npm:^9.9.0" eslint-config-prettier: "npm:^9.1.0" + fast-average-color-node: "npm:^3.0.0" htl: "npm:^0.3.1" html-to-image: "npm:^1.11.11" immer: "npm:^10.1.1" @@ -12787,6 +12820,13 @@ __metadata: languageName: node linkType: hard +"tr46@npm:~0.0.3": + version: 0.0.3 + resolution: "tr46@npm:0.0.3" + checksum: 10c0/047cb209a6b60c742f05c9d3ace8fa510bff609995c129a37ace03476a9b12db4dbf975e74600830ef0796e18882b2381fb5fb1f6b4f96b832c374de3ab91a11 + languageName: node + linkType: hard + "trim-lines@npm:^3.0.0": version: 3.0.1 resolution: "trim-lines@npm:3.0.1" @@ -13610,6 +13650,13 @@ __metadata: languageName: node linkType: hard +"webidl-conversions@npm:^3.0.0": + version: 3.0.1 + resolution: "webidl-conversions@npm:3.0.1" + checksum: 10c0/5612d5f3e54760a797052eb4927f0ddc01383550f542ccd33d5238cfd65aeed392a45ad38364970d0a0f4fea32e1f4d231b3d8dac4a3bdd385e5cf802ae097db + languageName: node + linkType: hard + "webpack-sources@npm:^3.2.3": version: 3.2.3 resolution: "webpack-sources@npm:3.2.3" @@ -13624,6 +13671,16 @@ __metadata: languageName: node linkType: hard +"whatwg-url@npm:^5.0.0": + version: 5.0.0 + resolution: "whatwg-url@npm:5.0.0" + dependencies: + tr46: "npm:~0.0.3" + webidl-conversions: "npm:^3.0.0" + checksum: 10c0/1588bed84d10b72d5eec1d0faa0722ba1962f1821e7539c535558fb5398d223b0c50d8acab950b8c488b4ba69043fd833cc2697056b167d8ad46fac3995a55d5 + languageName: node + linkType: hard + "which-typed-array@npm:^1.1.14, which-typed-array@npm:^1.1.2": version: 1.1.15 resolution: "which-typed-array@npm:1.1.15"