From 260e3e04e0164cbada960f5c96d66bc37417eb2b Mon Sep 17 00:00:00 2001 From: filtered <176114999+webfiltered@users.noreply.github.com> Date: Mon, 30 Dec 2024 05:44:46 +1100 Subject: [PATCH] Add custom window / native window controls (#457) * Allow preload types to be used in frontend Fix enum imported via meta file * Hide default titlebar on win32 / linux * Add interop for theme change handling Limitation: Cannot theme hover background of OS-controlled buttons - using a white background in dark mode appears to have no hover * Export overlay options * Add windowStyle setting * Allow disabling of modern window style * Fix height change ignored Removes initial test height * Prevent zero overlay height It resets the height of the buttons, but the CSS is set explicitly to zero, breaking anything relying on it. * Remove coupling requirement from initial feature - Removes on-by-default behaviour, leaving no changes to default behaviour - Allows linked commits to happen async * Add TS types, nits * Add API - setWindowStyle --- src/constants.ts | 2 ++ src/handlers/appInfoHandlers.ts | 7 +++++ src/main-process/appWindow.ts | 36 ++++++++++++++++++++++- src/main-process/comfyDesktopApp.ts | 5 +++- src/main_types.ts | 1 + src/preload.ts | 44 +++++++++++++++++++++++------ src/store/desktopSettings.ts | 6 ++++ 7 files changed, 90 insertions(+), 11 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index fcea650d..b8e7e727 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -30,9 +30,11 @@ export const IPC_CHANNELS = { VALIDATE_COMFYUI_SOURCE: 'validate-comfyui-source', SHOW_DIRECTORY_PICKER: 'show-directory-picker', INSTALL_COMFYUI: 'install-comfyui', + CHANGE_THEME: 'change-theme', SHOW_CONTEXT_MENU: 'show-context-menu', RESTART_CORE: 'restart-core', GET_GPU: 'get-gpu', + SET_WINDOW_STYLE: 'set-window-style', } as const; export enum ProgressStatus { diff --git a/src/handlers/appInfoHandlers.ts b/src/handlers/appInfoHandlers.ts index 4af71bf6..392d5149 100644 --- a/src/handlers/appInfoHandlers.ts +++ b/src/handlers/appInfoHandlers.ts @@ -2,6 +2,7 @@ import { app, ipcMain } from 'electron'; import { IPC_CHANNELS } from '../constants'; import { useDesktopConfig } from '../store/desktopConfig'; import type { TorchDeviceType } from '../preload'; +import type { DesktopSettings } from '../store/desktopSettings'; /** * Handles information about the app and current state in IPC channels. @@ -20,5 +21,11 @@ export class AppInfoHandlers { ipcMain.handle(IPC_CHANNELS.GET_GPU, async (): Promise => { return await useDesktopConfig().getAsync('detectedGpu'); }); + ipcMain.handle( + IPC_CHANNELS.SET_WINDOW_STYLE, + async (_event: Electron.IpcMainInvokeEvent, style: DesktopSettings['windowStyle']): Promise => { + await useDesktopConfig().setAsync('windowStyle', style); + } + ); } } diff --git a/src/main-process/appWindow.ts b/src/main-process/appWindow.ts index 6823fdab..508520b2 100644 --- a/src/main-process/appWindow.ts +++ b/src/main-process/appWindow.ts @@ -1,4 +1,16 @@ -import { BrowserWindow, screen, app, shell, ipcMain, Tray, Menu, dialog, MenuItem } from 'electron'; +import { + BrowserWindow, + screen, + app, + shell, + ipcMain, + Tray, + Menu, + dialog, + MenuItem, + nativeTheme, + type TitleBarOverlayOptions, +} from 'electron'; import path from 'node:path'; import Store from 'electron-store'; import { AppWindowSettings } from '../store/AppWindowSettings'; @@ -18,6 +30,10 @@ export class AppWindow { private store: Store; private messageQueue: Array<{ channel: string; data: unknown }> = []; private rendererReady: boolean = false; + /** Default dark mode config for system window overlay (min/max/close window). */ + private darkOverlay = { color: '#00000000', symbolColor: '#ddd' }; + /** Default light mode config for system window overlay (min/max/close window). */ + private lightOverlay = { ...this.darkOverlay, symbolColor: '#333' }; /** The application menu. */ private menu: Electron.Menu | null; /** The "edit" menu - cut/copy/paste etc. */ @@ -36,6 +52,15 @@ export class AppWindow { const storedX = store.get('windowX'); const storedY = store.get('windowY'); + // macOS requires different handling to linux / win32 + const customChrome: Pick = + process.platform !== 'darwin' && useDesktopConfig().get('windowStyle') === 'custom' + ? { + titleBarStyle: 'hidden', + titleBarOverlay: nativeTheme.shouldUseDarkColors ? this.darkOverlay : this.lightOverlay, + } + : {}; + this.window = new BrowserWindow({ title: 'ComfyUI', width: storedWidth, @@ -53,6 +78,7 @@ export class AppWindow { devTools: true, }, autoHideMenuBar: true, + ...customChrome, }); if (!installed && storedX === undefined) this.window.center(); @@ -245,6 +271,14 @@ export class AppWindow { }); } + changeTheme(options: TitleBarOverlayOptions): void { + if (process.platform === 'darwin' || useDesktopConfig().get('windowStyle') !== 'custom') return; + + if (options.height) options.height = Math.round(options.height); + if (!options.height) delete options.height; + this.window.setTitleBarOverlay(options); + } + showSystemContextMenu(options?: ElectronContextMenuOptions): void { if (options?.type === 'text') { this.editMenu?.popup(options.pos); diff --git a/src/main-process/comfyDesktopApp.ts b/src/main-process/comfyDesktopApp.ts index 0a01db42..d6675537 100644 --- a/src/main-process/comfyDesktopApp.ts +++ b/src/main-process/comfyDesktopApp.ts @@ -1,4 +1,4 @@ -import { app, dialog, ipcMain, Notification } from 'electron'; +import { app, dialog, ipcMain, Notification, type TitleBarOverlayOptions } from 'electron'; import log from 'electron-log/main'; import * as Sentry from '@sentry/electron/main'; import { graphics } from 'systeminformation'; @@ -91,6 +91,9 @@ export class ComfyDesktopApp { } registerIPCHandlers(): void { + ipcMain.on(IPC_CHANNELS.CHANGE_THEME, (_event, options: TitleBarOverlayOptions) => { + this.appWindow.changeTheme(options); + }); ipcMain.on(IPC_CHANNELS.SHOW_CONTEXT_MENU, (_event, options?: ElectronContextMenuOptions) => { this.appWindow.showSystemContextMenu(options); }); diff --git a/src/main_types.ts b/src/main_types.ts index de50f8c0..4d7bab16 100644 --- a/src/main_types.ts +++ b/src/main_types.ts @@ -9,4 +9,5 @@ export type { PathValidationResult, SystemPaths, DownloadProgressUpdate, + ElectronOverlayOptions, } from './preload'; diff --git a/src/preload.ts b/src/preload.ts index 2f39b895..d2892ffe 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -2,6 +2,7 @@ import { contextBridge, ipcRenderer } from 'electron'; import { IPC_CHANNELS, ELECTRON_BRIDGE_API, ProgressStatus, DownloadStatus } from './constants'; import type { DownloadState } from './models/DownloadManager'; import path from 'node:path'; +import type { DesktopSettings } from './store/desktopSettings'; /** * Open a folder in the system's default file explorer. @@ -41,6 +42,22 @@ export interface DownloadProgressUpdate { message?: string; } +/** @todo Type inference chain broken by comfyui-electron-types. This is duplication. */ +export interface ElectronOverlayOptions { + /** + * The CSS color of the Window Controls Overlay when enabled. + */ + color?: string; + /** + * The CSS color of the symbols on the Window Controls Overlay when enabled. + */ + symbolColor?: string; + /** + * The height of the title bar and Window Controls Overlay in pixels. + */ + height?: number; +} + export interface ElectronContextMenuOptions { type: 'system' | 'text' | 'image'; pos?: Electron.Point; @@ -86,14 +103,15 @@ const electronAPI = { console.log('Sending ready event to main process'); ipcRenderer.send(IPC_CHANNELS.RENDERER_READY); }, - isPackaged: () => { + /** Emulates app.ispackaged in renderer */ + isPackaged: (): Promise => { return ipcRenderer.invoke(IPC_CHANNELS.IS_PACKAGED); - }, //Emulates app.ispackaged in renderer + }, restartApp: (customMessage?: string, delay?: number): void => { console.log('Sending restarting app message to main process with custom message:', customMessage); ipcRenderer.send(IPC_CHANNELS.RESTART_APP, { customMessage, delay }); }, - reinstall: () => { + reinstall: (): Promise => { return ipcRenderer.invoke(IPC_CHANNELS.REINSTALL); }, openDialog: (options: Electron.OpenDialogOptions) => { @@ -161,9 +179,8 @@ const electronAPI = { getElectronVersion: () => { return ipcRenderer.invoke(IPC_CHANNELS.GET_ELECTRON_VERSION); }, - getComfyUIVersion: () => { - return __COMFYUI_VERSION__; - }, + /** The ComfyUI core version (as defined in package.json) */ + getComfyUIVersion: () => __COMFYUI_VERSION__, /** * Send an error message to Sentry * @param error The error object or message to send @@ -243,6 +260,12 @@ const electronAPI = { installComfyUI: (installOptions: InstallOptions) => { ipcRenderer.send(IPC_CHANNELS.INSTALL_COMFYUI, installOptions); }, + /** + * Update the Window Controls Overlay theme overrides + * @param theme The theme settings to apply + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Window_Controls_Overlay_API} + */ + changeTheme: (theme: ElectronOverlayOptions): void => ipcRenderer.send(IPC_CHANNELS.CHANGE_THEME, theme), /** * Opens native context menus. * @@ -261,15 +284,18 @@ const electronAPI = { // eslint-disable-next-line @typescript-eslint/no-unsafe-return return await ipcRenderer.invoke(IPC_CHANNELS.GET_GPU); }, + /** Sets the window style */ + setWindowStyle: (style: DesktopSettings['windowStyle']): Promise => { + return ipcRenderer.invoke(IPC_CHANNELS.SET_WINDOW_STYLE, style); + }, }, /** Restart the python server without restarting desktop. */ restartCore: async (): Promise => { console.log('Restarting core process'); await ipcRenderer.invoke(IPC_CHANNELS.RESTART_APP); }, - getPlatform: (): NodeJS.Platform => { - return process.platform; - }, + /** Gets the platform reported by node.js */ + getPlatform: () => process.platform, } as const; export type ElectronAPI = typeof electronAPI; diff --git a/src/store/desktopSettings.ts b/src/store/desktopSettings.ts index b374a0b8..75edeb0a 100644 --- a/src/store/desktopSettings.ts +++ b/src/store/desktopSettings.ts @@ -22,4 +22,10 @@ export type DesktopSettings = { /** The pytorch device that the user selected during installation. */ selectedDevice?: TorchDeviceType; 'Comfy-Desktop.RestoredCustomNodes': boolean; + /** + * Controls whether to use a custom window on linux/win32 + * - `custom`: Modern, theme-reactive, feels like an integral part of the UI + * - `default`: Impersonal, static, plain - default window title bar + */ + windowStyle?: 'custom' | 'default'; };