refactor: migrate to VS Code titlebar drag region pattern

This commit is contained in:
Mohamed Boudra
2026-04-01 16:00:33 +07:00
parent 953f7898e7
commit 58d72bea87
20 changed files with 963 additions and 610 deletions
+120 -68
View File
@@ -1,10 +1,4 @@
import { app, BrowserWindow, ipcMain, nativeTheme } from "electron";
import {
createWindowMoveState,
readWindowMovePayload,
resolveWindowMovePosition,
type WindowMoveState,
} from "./window-drag-coordinates.js";
export function readBadgeCount(input: unknown): number {
if (typeof input !== "number" || !Number.isSafeInteger(input) || input < 0) {
@@ -15,6 +9,17 @@ export function readBadgeCount(input: unknown): number {
}
export type WindowTheme = "light" | "dark";
export type WindowControlsOverlayUpdate = {
height?: number;
backgroundColor?: string;
foregroundColor?: string;
};
export type WindowControlsOverlayState = {
height: number;
backgroundColor?: string;
foregroundColor?: string;
};
export function readWindowTheme(input: unknown): WindowTheme | null {
if (input === "light" || input === "dark") {
@@ -28,71 +33,111 @@ export function resolveSystemWindowTheme(): WindowTheme {
return nativeTheme.shouldUseDarkColors ? "dark" : "light";
}
export function createWindowControlsOverlayState(theme: WindowTheme): WindowControlsOverlayState {
const overlay = getTitleBarOverlayOptions(theme);
return {
height: overlay.height ?? 29,
backgroundColor: overlay.color,
foregroundColor: overlay.symbolColor,
};
}
export function getTitleBarOverlayOptions(theme: WindowTheme): Electron.TitleBarOverlayOptions {
if (theme === "dark") {
return { color: "#18181c", symbolColor: "#e4e4e7", height: 48 };
return { color: "#18181c", symbolColor: "#e4e4e7", height: 29 };
}
return { color: "#ffffff", symbolColor: "#09090b", height: 48 };
return { color: "#ffffff", symbolColor: "#09090b", height: 29 };
}
export function getMainWindowChromeOptions(input: {
platform: NodeJS.Platform;
theme: WindowTheme;
}): Pick<
Electron.BrowserWindowConstructorOptions,
"titleBarStyle" | "trafficLightPosition" | "frame" | "titleBarOverlay" | "autoHideMenuBar"
> {
if (input.platform === "darwin") {
return {
titleBarStyle: "hidden",
titleBarOverlay: true,
trafficLightPosition: { x: 16, y: 14 },
};
}
return {
titleBarStyle: "hidden",
frame: false,
titleBarOverlay: getTitleBarOverlayOptions(input.theme),
autoHideMenuBar: true,
};
}
function readFiniteOverlayHeight(input: unknown): number | null {
if (typeof input !== "number" || !Number.isFinite(input)) {
return null;
}
const rounded = Math.round(input);
return rounded >= 1 ? rounded : null;
}
function readOverlayColor(input: unknown): string | null {
if (typeof input !== "string") {
return null;
}
return input;
}
export function readWindowControlsOverlayUpdate(input: unknown): WindowControlsOverlayUpdate | null {
if (!input || typeof input !== "object") {
return null;
}
const candidate = input as Record<string, unknown>;
const height = readFiniteOverlayHeight(candidate.height);
const backgroundColor = readOverlayColor(candidate.backgroundColor);
const foregroundColor = readOverlayColor(candidate.foregroundColor);
if (height === null && backgroundColor === null && foregroundColor === null) {
return null;
}
return {
...(height !== null ? { height } : {}),
...(backgroundColor !== null ? { backgroundColor } : {}),
...(foregroundColor !== null ? { foregroundColor } : {}),
};
}
export function resolveRuntimeTitleBarOverlayOptions(
state: WindowControlsOverlayState,
): Electron.TitleBarOverlayOptions {
return {
color: state.backgroundColor?.trim() === "" ? undefined : state.backgroundColor,
symbolColor: state.foregroundColor?.trim() === "" ? undefined : state.foregroundColor,
height: Math.max(0, state.height - 1),
};
}
export function applyWindowControlsOverlayUpdate(input: {
win: Pick<BrowserWindow, "setTitleBarOverlay">;
current: WindowControlsOverlayState;
update: WindowControlsOverlayUpdate;
}): WindowControlsOverlayState {
const next: WindowControlsOverlayState = {
height: input.update.height ?? input.current.height,
backgroundColor: input.update.backgroundColor ?? input.current.backgroundColor,
foregroundColor: input.update.foregroundColor ?? input.current.foregroundColor,
};
input.win.setTitleBarOverlay(resolveRuntimeTitleBarOverlayOptions(next));
return next;
}
export function registerWindowManager(): void {
// ---------------------------------------------------------------------------
// Manual window dragging (replaces flaky CSS -webkit-app-region: drag).
// State is keyed per window id to support multiple windows.
// ---------------------------------------------------------------------------
const moveStates = new Map<number, WindowMoveState>();
ipcMain.on("paseo:window:startMove", (event, payload: { screenX: number; screenY: number }) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (!win) return;
const movePayload = readWindowMovePayload(payload);
if (!movePayload) {
moveStates.delete(win.id);
return;
}
const [winX, winY] = win.getPosition();
const moveState = createWindowMoveState({
payload: movePayload,
windowX: winX,
windowY: winY,
});
if (!moveState) {
moveStates.delete(win.id);
return;
}
moveStates.set(win.id, moveState);
});
ipcMain.on("paseo:window:moving", (event, payload: { screenX: number; screenY: number }) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (!win) return;
const state = moveStates.get(win.id);
if (!state) return;
const movePayload = readWindowMovePayload(payload);
if (!movePayload) {
moveStates.delete(win.id);
return;
}
const nextPosition = resolveWindowMovePosition({
payload: movePayload,
state,
});
if (!nextPosition) {
moveStates.delete(win.id);
return;
}
win.setPosition(nextPosition.x, nextPosition.y);
});
ipcMain.on("paseo:window:endMove", (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) moveStates.delete(win.id);
});
app.on("browser-window-created", (_event, win) => {
win.on("closed", () => moveStates.delete(win.id));
});
const overlayStateByWindow = new WeakMap<BrowserWindow, WindowControlsOverlayState>();
ipcMain.handle("paseo:window:toggleMaximize", (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
@@ -124,18 +169,25 @@ export function registerWindowManager(): void {
}
});
ipcMain.handle("paseo:window:setTitleBarTheme", (event, theme?: unknown) => {
ipcMain.handle("paseo:window:updateWindowControls", (event, update?: unknown) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (!win || process.platform === "darwin") {
return;
}
const nextTheme = readWindowTheme(theme);
if (!nextTheme) {
const nextUpdate = readWindowControlsOverlayUpdate(update);
if (!nextUpdate) {
return;
}
win.setTitleBarOverlay(getTitleBarOverlayOptions(nextTheme));
const current =
overlayStateByWindow.get(win) ?? createWindowControlsOverlayState(resolveSystemWindowTheme());
const nextState = applyWindowControlsOverlayUpdate({
win,
current,
update: nextUpdate,
});
overlayStateByWindow.set(win, nextState);
});
}