diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index c7cdc73f9b3d..acdb087a1661 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -186,7 +186,6 @@ declare global { deepLinks?: string[] } api?: { - setTitlebar?: (theme: { mode: "light" | "dark" }) => Promise exportDebugLogs?: () => Promise } } @@ -317,11 +316,7 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) { return ( - { - void window.api?.setTitlebar?.({ mode }) - }} - > + tauriApi()?.webviewWindow?.getCurrentWebviewWin const legacyTitlebarHeight = 40 const v2TitlebarHeight = 36 const minTitlebarZoom = 0.25 -const windowsControlsBaseWidth = 138 // 3 native Windows caption buttons at 46px each. export type TitlebarUpdate = { version: () => string | undefined @@ -89,8 +88,6 @@ export function Titlebar(props: { update?: TitlebarUpdate }) { if (windows()) return `${height / Math.min(titlebarZoom(), 1)}px` return undefined } - const windowsControlsWidth = () => `${windowsControlsBaseWidth / Math.max(titlebarZoom(), 1)}px` - const [history, setHistory] = createStore({ stack: [] as string[], index: 0, @@ -231,11 +228,6 @@ export function Titlebar(props: { update?: TitlebarUpdate }) { "min-height": minHeight(), // Keep native macOS traffic lights clear even when the desktop window is narrow. "padding-left": mac() ? `${84 / zoom()}px` : 0, - width: electronWindows() ? `env(titlebar-area-width, calc(100vw - ${windowsControlsWidth()}))` : undefined, - "max-width": electronWindows() - ? `env(titlebar-area-width, calc(100vw - ${windowsControlsWidth()}))` - : undefined, - "align-self": electronWindows() ? "flex-start" : undefined, }} data-tauri-drag-region onMouseDown={drag} @@ -498,6 +490,9 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
+ + +
@@ -653,8 +648,10 @@ export function Titlebar(props: { update?: TitlebarUpdate }) { onMouseDown={drag} >
- - {!tauriApi() &&
} + + + +
@@ -729,3 +726,59 @@ function ChannelIndicator() { ) } + +function WindowsCaptionButtons() { + const [maximized, setMaximized] = createSignal(false) + let unlisten: (() => void) | undefined + + onMount(() => { + window.api.isMaximized().then(setMaximized) + unlisten = window.api.onMaximizeChanged(setMaximized) + }) + + onCleanup(() => unlisten?.()) + + const btn = "oc-caption-btn" + const icon = "size-3" + + return ( +
+ + + + +
+ ) +} diff --git a/packages/desktop/src/main/desktop-menu-actions.ts b/packages/desktop/src/main/desktop-menu-actions.ts index aa15e05e277f..fcce89195857 100644 --- a/packages/desktop/src/main/desktop-menu-actions.ts +++ b/packages/desktop/src/main/desktop-menu-actions.ts @@ -1,6 +1,6 @@ import { BrowserWindow } from "electron" import type { DesktopMenuAction } from "@opencode-ai/app/desktop-menu" -import { createMainWindow, updateTitlebar } from "./windows" +import { createMainWindow } from "./windows" export type DesktopMenuActionHandlers = Partial<{ checkForUpdates: () => void @@ -80,5 +80,4 @@ export function runDesktopMenuAction( function setZoom(win: BrowserWindow | null, value: number) { if (!win) return win.webContents.setZoomFactor(Math.min(Math.max(value, 0.2), 10)) - updateTitlebar(win) } diff --git a/packages/desktop/src/main/ipc.ts b/packages/desktop/src/main/ipc.ts index 22560ff7da3a..1bed102b6738 100644 --- a/packages/desktop/src/main/ipc.ts +++ b/packages/desktop/src/main/ipc.ts @@ -5,11 +5,11 @@ import { app, BrowserWindow, Notification, clipboard, dialog, ipcMain, shell } f import type { IpcMainEvent, IpcMainInvokeEvent } from "electron" import type { DesktopMenuAction } from "@opencode-ai/app/desktop-menu" -import type { FatalRendererError, ServerReadyData, TitlebarTheme } from "../preload/types" +import type { FatalRendererError, ServerReadyData } from "../preload/types" import { runDesktopMenuAction } from "./desktop-menu-actions" import { assertAttachmentBudget, createPickedFileAuthorizations } from "./attachment-picker" import { getStore, removeStoreFileIfEmpty } from "./store" -import { getPinchZoomEnabled, getWindowID, setPinchZoomEnabled, setTitlebar, updateTitlebar } from "./windows" +import { getPinchZoomEnabled, getWindowID, setPinchZoomEnabled } from "./windows" import type { UpdaterController } from "./updater-controller" import { createUpdaterSubscriptions } from "./updater-subscriptions" @@ -222,19 +222,26 @@ export function registerIpcHandlers(deps: Deps) { ipcMain.handle("get-zoom-factor", (event: IpcMainInvokeEvent) => event.sender.getZoomFactor()) ipcMain.handle("set-zoom-factor", (event: IpcMainInvokeEvent, factor: number) => { event.sender.setZoomFactor(factor) - const win = BrowserWindow.fromWebContents(event.sender) - if (!win) return - updateTitlebar(win) }) ipcMain.handle("get-pinch-zoom-enabled", () => getPinchZoomEnabled()) ipcMain.handle("set-pinch-zoom-enabled", (_event: IpcMainInvokeEvent, enabled: boolean) => { setPinchZoomEnabled(enabled) }) - ipcMain.handle("set-titlebar", (event: IpcMainInvokeEvent, theme: TitlebarTheme) => { + ipcMain.handle("window-minimize", (event: IpcMainInvokeEvent) => + BrowserWindow.fromWebContents(event.sender)?.minimize(), + ) + ipcMain.handle("window-toggle-maximize", (event: IpcMainInvokeEvent) => { const win = BrowserWindow.fromWebContents(event.sender) if (!win) return - setTitlebar(win, theme) + if (win.isMaximized()) win.unmaximize() + else win.maximize() }) + ipcMain.handle("window-is-maximized", (event: IpcMainInvokeEvent) => + BrowserWindow.fromWebContents(event.sender)?.isMaximized() ?? false, + ) + ipcMain.handle("window-close", (event: IpcMainInvokeEvent) => + BrowserWindow.fromWebContents(event.sender)?.close(), + ) ipcMain.handle("run-desktop-menu-action", (event: IpcMainInvokeEvent, action: DesktopMenuAction) => { runDesktopMenuAction(BrowserWindow.fromWebContents(event.sender), action, { checkForUpdates: () => void deps.showUpdater(), diff --git a/packages/desktop/src/main/windows.ts b/packages/desktop/src/main/windows.ts index a2c7ca617b7b..f106fec14dd0 100644 --- a/packages/desktop/src/main/windows.ts +++ b/packages/desktop/src/main/windows.ts @@ -7,7 +7,6 @@ import { rmSync } from "node:fs" import { app, BrowserWindow, dialog, net, nativeImage, nativeTheme, protocol } from "electron" import { dirname, isAbsolute, join, relative, resolve } from "node:path" import { fileURLToPath, pathToFileURL } from "node:url" -import type { TitlebarTheme } from "../preload/types" import { exportDebugLogs, write as writeLog } from "./logging" import { getStore, removeStoreFile } from "./store" import { PINCH_ZOOM_ENABLED_KEY, WINDOW_IDS_KEY } from "./store-keys" @@ -46,7 +45,6 @@ let relaunchHandler = () => { app.relaunch() app.exit(0) } -const titlebarThemes = new WeakMap>() const pinchZoomEnabled = new WeakMap() const windowIDs = new WeakMap() const registry = createWindowRegistry({ @@ -57,7 +55,6 @@ const registry = createWindowRegistry({ removeStoreFile(windowDataFile(id)) }, }) -const titlebarHeight = 40 const maxZoomLevel = 10 const minZoomLevel = 0.2 @@ -95,25 +92,6 @@ function defaultBackgroundColor() { return oc2Background[tone()] } -function overlay(theme: Partial = {}, zoom = 1) { - const mode = theme.mode ?? tone() - return { - color: "#00000000", - symbolColor: mode === "dark" ? "white" : "black", - height: Math.max(titlebarHeight, Math.round(titlebarHeight * zoom)), - } -} - -export function setTitlebar(win: BrowserWindow, theme: Partial = {}) { - titlebarThemes.set(win, theme) - updateTitlebar(win) -} - -export function updateTitlebar(win: BrowserWindow) { - if (process.platform !== "win32") return - win.setTitleBarOverlay(overlay(titlebarThemes.get(win), win.webContents.getZoomFactor())) -} - export function setPinchZoomEnabled(enabled: boolean) { getStore().set(PINCH_ZOOM_ENABLED_KEY, enabled) for (const win of BrowserWindow.getAllWindows()) { @@ -179,7 +157,6 @@ export function createMainWindow(id: string = randomUUID()) { ? { frame: false, titleBarStyle: "hidden" as const, - titleBarOverlay: overlay({ mode }), } : {}), webPreferences: { @@ -226,6 +203,10 @@ function registerWindow(win: BrowserWindow, id: string) { // gets session-end before it closes; flag the quit so ids stay persisted. win.on("session-end", () => registry.setQuitting()) win.on("closed", () => registry.closed(id)) + if (process.platform === "win32") { + win.on("maximize", () => win.webContents.send("maximize-changed", true)) + win.on("unmaximize", () => win.webContents.send("maximize-changed", false)) + } } function windowStateFile(id: string) { @@ -467,7 +448,6 @@ function clampZoom(value: number) { } function updateZoom(win: BrowserWindow) { - updateTitlebar(win) win.webContents.send("zoom-factor-changed", win.webContents.getZoomFactor()) } diff --git a/packages/desktop/src/preload/index.ts b/packages/desktop/src/preload/index.ts index 2cb8a9ee073c..70249a724bc1 100644 --- a/packages/desktop/src/preload/index.ts +++ b/packages/desktop/src/preload/index.ts @@ -112,7 +112,15 @@ const api: ElectronAPI = { ipcRenderer.on("zoom-factor-changed", handler) return () => ipcRenderer.removeListener("zoom-factor-changed", handler) }, - setTitlebar: (theme) => ipcRenderer.invoke("set-titlebar", theme), + minimizeWindow: () => ipcRenderer.invoke("window-minimize"), + toggleMaximize: () => ipcRenderer.invoke("window-toggle-maximize"), + isMaximized: () => ipcRenderer.invoke("window-is-maximized"), + closeWindow: () => ipcRenderer.invoke("window-close"), + onMaximizeChanged: (cb) => { + const handler = (_: unknown, maximized: boolean) => cb(maximized) + ipcRenderer.on("maximize-changed", handler) + return () => ipcRenderer.removeListener("maximize-changed", handler) + }, runDesktopMenuAction: (action) => ipcRenderer.invoke("run-desktop-menu-action", action), setBackgroundColor: (color: string) => ipcRenderer.invoke("set-background-color", color), exportDebugLogs: () => ipcRenderer.invoke("export-debug-logs"), diff --git a/packages/desktop/src/preload/types.ts b/packages/desktop/src/preload/types.ts index 5401c2070d7a..b0aad681360e 100644 --- a/packages/desktop/src/preload/types.ts +++ b/packages/desktop/src/preload/types.ts @@ -29,9 +29,6 @@ export type UpdaterAPI = { } export type LinuxDisplayBackend = "wayland" | "auto" -export type TitlebarTheme = { - mode: "light" | "dark" -} export type FatalRendererError = { error: string url: string @@ -95,7 +92,11 @@ export type ElectronAPI = { setPinchZoomEnabled: (enabled: boolean) => Promise onPinchZoomEnabledChanged: (cb: (enabled: boolean) => void) => () => void onZoomFactorChanged: (cb: (factor: number) => void) => () => void - setTitlebar: (theme: TitlebarTheme) => Promise + minimizeWindow: () => Promise + toggleMaximize: () => Promise + isMaximized: () => Promise + closeWindow: () => Promise + onMaximizeChanged: (cb: (maximized: boolean) => void) => () => void runDesktopMenuAction: (action: DesktopMenuAction) => Promise setBackgroundColor: (color: string) => Promise exportDebugLogs: () => Promise