* refactor(desktop): extract titleBarOverlayOptions into a tested helper getTitleBarOverlayOptions in main.ts branched inline on mac/windows/wsl. Move the decision into titlebar-overlay-width.ts so the WSLg → false rule (renderer paints its own controls there) is unit-tested next to the width reservation it pairs with. main.ts keeps a thin caller. Co-authored-by: null-runner <nicholas.mariani@hotmail.it> * fix(desktop): renderer-drawn window controls on WSLg Under WSLg the frameless window (titleBarStyle: 'hidden') had no minimize/maximize/close. getTitleBarOverlayOptions returned false on the premise that the RDP host paints replacement controls; it does not for a frameless window. Electron's native overlay is not the fix either: its cluster's hit-region drifts from the rendered buttons under the RAIL compositor. The renderer now paints Windows-style min/max/close (wslg-window-controls) routed over a hermes:window-control IPC channel (window-controls.ts → preload → main). getWindowState reports customWindowControls/isMaximized so the renderer knows when to mount them and which glyph to show. The cluster is pinned to TITLEBAR_HEIGHT px (the contrib shell zeroes --titlebar-height for content subtrees) with 46px native-width caption buttons. Co-authored-by: Austin Pickett <pickett.austin@gmail.com> * fix(desktop): wire WSLg window-control IPC and snap maximize onto the work area Register hermes:window-control in main, report windowControlState from getWindowState, and re-send window state on maximize/unmaximize so the renderer's maximize/restore glyph tracks the window. maximizedBoundsCorrection snaps a WSLg-maximized frameless window back onto the display work area (RAIL can settle it offset — reported on WSLg 1.0.65). It is a no-op wherever native maximize already fills the work area, so healthy compositors are never fought and setBounds cannot loop. Co-authored-by: null-runner <nicholas.mariani@hotmail.it> * fix(desktop): call WSLg window-control bridge without the click event contextBridge structured-clones every argument, and a React SyntheticEvent is not cloneable, so onClick={controls.minimize} threw "An object could not be cloned" before ipcRenderer.send ran. The buttons rendered but did nothing. Wrap the handlers so the bridge is called with no arguments, and pin that in the test. * fix(desktop): remove recursive WSLg maximize bounds correction * fix(desktop): select native Wayland before Electron initialization on WSLg * fix(desktop): preserve ready pipe and debugger across WSLg launch * fix(desktop): keep WSLg caption controls scoped to every window * style(desktop): order window chrome event import --------- Co-authored-by: null-runner <nicholas.mariani@hotmail.it>
102 lines
3.2 KiB
TypeScript
102 lines
3.2 KiB
TypeScript
export type WindowControlAction = 'close' | 'minimize' | 'toggle-maximize'
|
|
|
|
interface CustomWindowControlsOptions {
|
|
env?: NodeJS.ProcessEnv
|
|
kernelRelease?: string | null
|
|
platform?: NodeJS.Platform
|
|
}
|
|
|
|
// WSL detection kept fs-free on purpose: this module is bundled into the
|
|
// sandboxed preload (sandbox: true), where importing node:fs — even
|
|
// transitively via bootstrap-platform — throws when the preload module loads
|
|
// and tears down the whole `window.hermesDesktop` bridge. The preload only
|
|
// needs the env-var signal (WSLg always sets WSL_INTEROP/WSL_DISTRO_NAME), and
|
|
// the authoritative flag still reaches the renderer through the main process's
|
|
// getWindowState (IS_WSL, which keeps the /proc kernel-release fallback). The
|
|
// kernelRelease branch mirrors bootstrap-platform's isWslEnvironment so tests
|
|
// and the main process agree; window-controls.test.ts guards that parity.
|
|
export function customWindowControlsEnabled(options: CustomWindowControlsOptions = {}): boolean {
|
|
const platform = options.platform ?? process.platform
|
|
|
|
if (platform !== 'linux') {
|
|
return false
|
|
}
|
|
|
|
const env = options.env ?? process.env
|
|
|
|
if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) {
|
|
return true
|
|
}
|
|
|
|
return options.kernelRelease ? /microsoft|wsl/i.test(options.kernelRelease) : false
|
|
}
|
|
|
|
interface ControllableWindow {
|
|
close?: () => void
|
|
focus?: () => void
|
|
isDestroyed?: () => boolean
|
|
isMaximized?: () => boolean
|
|
maximize?: () => void
|
|
minimize?: () => void
|
|
unmaximize?: () => void
|
|
}
|
|
|
|
export function performWindowControl(win: ControllableWindow | null | undefined, action: string): boolean {
|
|
if (!win || win.isDestroyed?.()) {
|
|
return false
|
|
}
|
|
|
|
if (action === 'minimize' && win.minimize) {
|
|
win.minimize()
|
|
|
|
return true
|
|
}
|
|
|
|
if (action === 'toggle-maximize' && win.maximize && win.unmaximize) {
|
|
if (win.isMaximized?.()) {
|
|
win.unmaximize()
|
|
} else {
|
|
win.maximize()
|
|
}
|
|
|
|
// WSLg's RAIL host can keep pointer activation while dropping the keyboard
|
|
// focus after a renderer-owned maximize/restore button invokes Electron.
|
|
// Reassert the BrowserWindow immediately so typing and Ctrl+V continue to
|
|
// reach the existing focused editor instead of requiring an app restart.
|
|
win.focus?.()
|
|
|
|
return true
|
|
}
|
|
|
|
if (action === 'close' && win.close) {
|
|
win.close()
|
|
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
export function windowControlState(win: ControllableWindow | null | undefined, customWindowControls: boolean) {
|
|
return {
|
|
customWindowControls,
|
|
isMaximized: Boolean(win && !win.isDestroyed?.() && win.isMaximized?.())
|
|
}
|
|
}
|
|
|
|
interface WindowControlIpc {
|
|
on(channel: string, listener: (event: { sender: Electron.WebContents }, action: unknown) => void): unknown
|
|
}
|
|
|
|
// Registers the renderer → main channel that the WSLg window-control buttons
|
|
// send on. Kept here (not inline in main.ts) so the registration + dispatch is
|
|
// unit-testable without importing the electron entry module.
|
|
export function registerWindowControlIpc(
|
|
ipcMain: WindowControlIpc,
|
|
resolveWindow: (sender: Electron.WebContents) => ControllableWindow | null | undefined
|
|
): void {
|
|
ipcMain.on('hermes:window-control', (event, action) => {
|
|
performWindowControl(resolveWindow(event.sender), typeof action === 'string' ? action : '')
|
|
})
|
|
}
|