diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index aa2f220e49..41d134f0f4 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -5,7 +5,6 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { Icon } from "@opencode-ai/ui/icon" import { Button } from "@opencode-ai/ui/button" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" -import { useTheme } from "@opencode-ai/ui/theme/context" import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" @@ -30,31 +29,11 @@ import "./titlebar.css" import { newTabTooltipKeybind } from "./command-tooltip-keybind" import { normalizeSessionInfo } from "@/utils/session" -type TauriDesktopWindow = { - startDragging?: () => Promise - toggleMaximize?: () => Promise -} - -type TauriThemeWindow = { - setTheme?: (theme?: "light" | "dark" | null) => Promise -} - -type TauriApi = { - window?: { - getCurrentWindow?: () => TauriDesktopWindow - } - webviewWindow?: { - getCurrentWebviewWindow?: () => TauriThemeWindow - } -} - -const tauriApi = () => (window as unknown as { __TAURI__?: TauriApi }).__TAURI__ -const currentDesktopWindow = () => tauriApi()?.window?.getCurrentWindow?.() -const currentThemeWindow = () => tauriApi()?.webviewWindow?.getCurrentWebviewWindow?.() const legacyTitlebarHeight = 40 const v2TitlebarHeight = 36 const minTitlebarZoom = 0.25 const windowsControlsBaseWidth = 138 // 3 native Windows caption buttons at 46px each. +const macTrafficLightsBaseWidth = 84 export type TitlebarUpdate = { version: () => string | undefined @@ -74,7 +53,6 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl const command = useCommand() const language = useLanguage() const settings = useSettings() - const theme = useTheme() const server = useServer() const navigate = useNavigate() const location = useLocation() @@ -85,9 +63,9 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos") const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows") - const electronWindows = createMemo(() => windows() && !tauriApi()) const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux") const web = createMemo(() => platform.platform === "web") + const macTrafficLights = createMemo(() => mac() && !platform.windowFullscreen?.()) const zoom = () => platform.webviewZoom?.() ?? 1 const titlebarZoom = () => (windows() ? Math.max(zoom(), minTitlebarZoom) : zoom()) const counterZoom = () => (windows() && titlebarZoom() < 1 ? 1 / titlebarZoom() : 1) @@ -176,56 +154,6 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl }, ]) - const getWin = () => { - if (platform.platform !== "desktop") return - return currentDesktopWindow() - } - - createEffect(() => { - if (platform.platform !== "desktop") return - - const scheme = theme.colorScheme() - const value = scheme === "system" ? null : scheme - - const win = currentThemeWindow() - if (!win?.setTheme) return - - void win.setTheme(value).catch(() => undefined) - }) - - const interactive = (target: EventTarget | null) => { - if (!(target instanceof Element)) return false - - const selector = - "button, a, input, textarea, select, option, [role='button'], [role='menuitem'], [contenteditable='true'], [contenteditable='']" - - return !!target.closest(selector) - } - - const drag = (e: MouseEvent) => { - if (platform.platform !== "desktop") return - if (e.buttons !== 1) return - if (interactive(e.target)) return - - const win = getWin() - if (!win?.startDragging) return - - e.preventDefault() - void win.startDragging().catch(() => undefined) - } - - const maximize = (e: MouseEvent) => { - if (platform.platform !== "desktop") return - if (interactive(e.target)) return - if (e.target instanceof Element && e.target.closest("[data-tauri-decorum-tb]")) return - - const win = getWin() - if (!win?.toggleMaximize) return - - e.preventDefault() - void win.toggleMaximize().catch(() => undefined) - } - return (
@@ -459,8 +383,8 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl classList={{ "pt-2": !bottom(), "pb-2": bottom(), - "md:pl-2": mac(), - "md:pl-4": !mac(), + "md:pl-2": macTrafficLights(), + "md:pl-4": !macTrafficLights(), }} > @@ -528,9 +452,6 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
- -
-
) }} @@ -543,14 +464,13 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
- {/*
*/}
- {!tauriApi() &&
} -
+
diff --git a/packages/app/src/context/platform.tsx b/packages/app/src/context/platform.tsx index cf343bfa7e..4c374961a3 100644 --- a/packages/app/src/context/platform.tsx +++ b/packages/app/src/context/platform.tsx @@ -97,6 +97,9 @@ type PlatformBase = { /** Webview zoom level (desktop only) */ webviewZoom?: Accessor + /** Whether the native desktop window is fullscreen */ + windowFullscreen?: Accessor + /** Get whether native pinch/Ctrl-scroll zoom gestures are enabled (desktop only) */ getPinchZoomEnabled?(): Promise | boolean diff --git a/packages/desktop/src/main/ipc.ts b/packages/desktop/src/main/ipc.ts index 4563b688ac..5ecd4cb8c4 100644 --- a/packages/desktop/src/main/ipc.ts +++ b/packages/desktop/src/main/ipc.ts @@ -227,6 +227,11 @@ export function registerIpcHandlers(deps: Deps) { return win?.isFocused() ?? false }) + ipcMain.handle("get-window-fullscreen", (event: IpcMainInvokeEvent) => { + const win = BrowserWindow.fromWebContents(event.sender) + return win?.isFullScreen() ?? false + }) + ipcMain.handle("set-window-focus", (event: IpcMainInvokeEvent) => { const win = BrowserWindow.fromWebContents(event.sender) win?.focus() diff --git a/packages/desktop/src/main/windows.ts b/packages/desktop/src/main/windows.ts index 7a594b5302..0b88032950 100644 --- a/packages/desktop/src/main/windows.ts +++ b/packages/desktop/src/main/windows.ts @@ -219,6 +219,7 @@ export function createMainWindow(id: string = randomUUID()) { state.manage(win) registerWindow(win, id) + wireFullscreen(win) loadWindow(win, "index.html") wireZoom(win) @@ -472,6 +473,16 @@ function wireZoom(win: BrowserWindow) { }) } +function wireFullscreen(win: BrowserWindow) { + const send = (fullscreen: boolean) => { + if (win.isDestroyed() || win.webContents.isDestroyed()) return + win.webContents.send("window-fullscreen-changed", fullscreen) + } + + win.on("enter-full-screen", () => send(true)) + win.on("leave-full-screen", () => send(false)) +} + function clampZoom(value: number) { return Math.min(Math.max(value, minZoomLevel), maxZoomLevel) } diff --git a/packages/desktop/src/preload/index.ts b/packages/desktop/src/preload/index.ts index 87eac1fa80..1a0056dfe7 100644 --- a/packages/desktop/src/preload/index.ts +++ b/packages/desktop/src/preload/index.ts @@ -100,6 +100,12 @@ const api: ElectronAPI = { readClipboardImage: () => ipcRenderer.invoke("read-clipboard-image"), showNotification: (title, body) => ipcRenderer.send("show-notification", title, body), getWindowFocused: () => ipcRenderer.invoke("get-window-focused"), + getWindowFullscreen: () => ipcRenderer.invoke("get-window-fullscreen"), + onWindowFullscreenChanged: (cb) => { + const handler = (_: unknown, fullscreen: boolean) => cb(fullscreen) + ipcRenderer.on("window-fullscreen-changed", handler) + return () => ipcRenderer.removeListener("window-fullscreen-changed", handler) + }, setWindowFocus: () => ipcRenderer.invoke("set-window-focus"), showWindow: () => ipcRenderer.invoke("show-window"), relaunch: () => ipcRenderer.send("relaunch"), diff --git a/packages/desktop/src/preload/types.ts b/packages/desktop/src/preload/types.ts index 5fbace72c7..ea201af63e 100644 --- a/packages/desktop/src/preload/types.ts +++ b/packages/desktop/src/preload/types.ts @@ -91,6 +91,8 @@ export type ElectronAPI = { readClipboardImage: () => Promise<{ buffer: ArrayBuffer; width: number; height: number } | null> showNotification: (title: string, body?: string) => void getWindowFocused: () => Promise + getWindowFullscreen: () => Promise + onWindowFullscreenChanged: (cb: (fullscreen: boolean) => void) => () => void setWindowFocus: () => Promise showWindow: () => Promise relaunch: () => void diff --git a/packages/desktop/src/renderer/index.tsx b/packages/desktop/src/renderer/index.tsx index b7d2d9a74d..ac5ded25f1 100644 --- a/packages/desktop/src/renderer/index.tsx +++ b/packages/desktop/src/renderer/index.tsx @@ -25,6 +25,7 @@ import { initI18n, t } from "./i18n" import { initializationData, initializationReady } from "./initialization" import { DesktopFirstLaunchOnboarding } from "./onboarding" import { resetZoom, setPinchZoomEnabled, webviewZoom, zoomIn, zoomOut } from "./webview-zoom" +import { windowFullscreen } from "./window-fullscreen" import { availableStartupServer, readyWslConnections } from "./wsl/connections" import "./styles.css" import { Splash } from "@opencode-ai/ui/logo" @@ -294,6 +295,8 @@ const createPlatform = (windowState: DesktopWindowState): Platform => { webviewZoom, + windowFullscreen, + getPinchZoomEnabled: () => window.api.getPinchZoomEnabled(), setPinchZoomEnabled, diff --git a/packages/desktop/src/renderer/window-fullscreen.ts b/packages/desktop/src/renderer/window-fullscreen.ts new file mode 100644 index 0000000000..95d91fa2e7 --- /dev/null +++ b/packages/desktop/src/renderer/window-fullscreen.ts @@ -0,0 +1,8 @@ +import { createSignal } from "solid-js" + +const [windowFullscreen, setWindowFullscreen] = createSignal(false) + +window.api.onWindowFullscreenChanged(setWindowFullscreen) +void window.api.getWindowFullscreen().then(setWindowFullscreen) + +export { windowFullscreen }