diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 2a7ab6fe..86a2c6d6 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 27d1db86..e02edd32 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 674b3bd3..866f1e8f 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 234d4326..bfd7d841 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 cefba943..d8172459 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 f7c0d1c1..0716a345 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 b6bb8048..ac0c5ef2 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"