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.

<img width="721" alt="image"
src="https://github.com/user-attachments/assets/e6f9f8f8-a49f-41b6-984e-09e7d52c631d">
This commit is contained in:
Evan Simkowitz 2024-08-19 14:16:09 -07:00 committed by GitHub
parent 3f37837394
commit e6003c310e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 124 additions and 3 deletions

View File

@ -17,6 +17,7 @@ export default defineConfig({
input: {
index: "emain/emain.ts",
},
external: ["sharp"],
},
outDir: "dist/main",
},

View File

@ -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();

View File

@ -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"

View File

@ -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<HTMLDivElement>(null);
const tabId = jotai.useAtomValue(atoms.activeTabId);
const windowOpacity = jotai.useAtomValue(atoms.settingsConfigAtom).window.opacity;
const [tabData] = useWaveObjectValue<Tab>(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 <div className="app-background" style={style} />;
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 <div ref={bgRef} className="app-background" style={style} />;
}
function genericClose(tabId: string) {

View File

@ -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<T> = { state: "loading" } | { state: "hasData"; data: T } | { state: "hasError"; error: unknown };
interface Dimensions {
width: number;
height: number;
left: number;
top: number;
}
}
export {};

View File

@ -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",

View File

@ -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"