/** * Frameless windows on Windows/Linux. Electron 0.1.0 used `titleBarStyle: hidden` with a * native `titleBarOverlay` (min/max/close drawn by Chromium in the top-right 40 px). * Tauri has no overlay, so the same area gets three equivalent buttons here, and the * existing drag regions keep working through a Core command. */ import { windowControls } from "../core/window"; import { call, subscribe } from "../transport"; import { bootInfo } from "./boot"; const CSS = ` chj-window-controls { position: fixed !important; top: 0 !important; right: 0 !important; height: 40px !important; z-index: 2147483647 !important; display: flex !important; } chj-window-controls button { width: 46px; height: 40px; border: 0; margin: 0; padding: 0; display: grid; place-items: center; color: #d9eaff; background: transparent; cursor: default; -webkit-app-region: no-drag; } chj-window-controls button:hover { background: rgba(217, 234, 255, .12); } chj-window-controls button.close:hover { background: #c42b1c; color: #fff; } chj-window-controls svg { width: 10px; height: 10px; stroke: currentColor; fill: none; stroke-width: 1.1; shape-rendering: crispEdges; } `; const ICONS: Record = { minimize: '', toggleMaximize: '', close: '' }; function isDragTarget(target: EventTarget | null, selectors: string[]): boolean { if (!(target instanceof Element)) return false; if (target.closest("button, a, input, select, textarea, [contenteditable], chj-window-controls")) return false; return selectors.some((selector) => Boolean(target.closest(selector))); } export function installWindowChrome(platform: string, dragSelectors: string[], dragRoots: () => Array): void { // This handshake runs on WKWebView, WebView2 and WebKitGTK, including native macOS chrome. window.addEventListener("chj:query-dirty", (event) => { let dirty = true; try { const page = window as Window & { __CHJ_HAS_UNSAVED_CHANGES__?: () => boolean; isDirty?: () => boolean }; // NGINX uses a classic script with a global isDirty(), including disconnected drafts. const query = page.__CHJ_HAS_UNSAVED_CHANGES__ || (bootInfo().moduleId === "chj.nginx-manager" ? page.isDirty : undefined); dirty = query ? Boolean(query()) : false; } catch { /* A failed dirty query preserves the draft. */ } void call("window_control", "window:control", { action: "closeQueryReply", queryId: (event as CustomEvent).detail, dirty }).catch(() => {}); }); if (platform !== "win32" && platform !== "linux") return; const mount = () => { if (document.querySelector("chj-window-controls")) return; const host = document.createElement("chj-window-controls"); const shadow = host.attachShadow({ mode: "closed" }); const style = document.createElement("style"); style.textContent = CSS.replace(/chj-window-controls /g, ":host ").replace(/chj-window-controls \{/g, ":host {"); shadow.append(style); let language = document.documentElement.lang || "en"; let maximized = false; const buttons = new Map(); const translate = () => { const catalogs = globalThis.__CHJ_I18N__?.catalogs?.chrome || {}; for (const [action, button] of buttons) { const key = `chrome.${action === "toggleMaximize" ? (maximized ? "restore" : "maximize") : action}`; const label = catalogs[language]?.[key] || catalogs[language.split("-")[0]]?.[key] || catalogs.en?.[key] || key; button.setAttribute("aria-label", label); button.title = label; if (action === "toggleMaximize") button.setAttribute("aria-pressed", String(maximized)); } }; const updateMaximized = () => { void windowControls.isMaximized().then((value) => { maximized = value; translate(); }).catch(() => {}); }; subscribe("module:ui:languageChanged", (value) => { language = value; translate(); }); new MutationObserver(() => { language = document.documentElement.lang || language; translate(); }).observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] }); window.addEventListener("resize", updateMaximized); for (const action of ["minimize", "toggleMaximize", "close"] as const) { const button = document.createElement("button"); button.type = "button"; button.className = action === "close" ? "close" : action; buttons.set(action, button); button.innerHTML = ICONS[action]; button.addEventListener("click", () => { void windowControls.perform(action).then(updateMaximized).catch(() => {}); }); shadow.append(button); } translate(); updateMaximized(); const outer = document.createElement("style"); outer.textContent = "chj-window-controls { position: fixed; top: 0; right: 0; height: 40px; z-index: 2147483647; display: flex; }"; document.head.append(outer); document.body.append(host); const handler = (event: Event) => { const mouse = event as MouseEvent; if (mouse.button !== 0) return; const path = mouse.composedPath(); const target = (path[0] as EventTarget) || mouse.target; if (!isDragTarget(target, dragSelectors)) return; if (mouse.detail === 2) void windowControls.perform("toggleMaximize").catch(() => {}); else void windowControls.perform("startDrag").catch(() => {}); }; for (const root of dragRoots()) root.addEventListener("mousedown", handler); }; if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", mount, { once: true }); else mount(); }