Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/app/src/components/titlebar-padding.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { describe, expect, test } from "bun:test"
import { macTitlebarLeftPadding } from "./titlebar-padding"

describe("macOS titlebar padding", () => {
test("reserves traffic light space while windowed", () => {
expect(macTitlebarLeftPadding(1, false)).toBe("84px")
expect(macTitlebarLeftPadding(2, false)).toBe("42px")
})

test("does not reserve traffic light space in fullscreen", () => {
expect(macTitlebarLeftPadding(1, true)).toBe("0px")
})
})
6 changes: 6 additions & 0 deletions packages/app/src/components/titlebar-padding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const macTrafficLightsWidth = 84

export function macTitlebarLeftPadding(zoom: number, fullscreen: boolean) {
if (fullscreen) return "0px"
return `${macTrafficLightsWidth / zoom}px`
}
97 changes: 7 additions & 90 deletions packages/app/src/components/titlebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -29,28 +28,8 @@ import type { PromptSession } from "@/context/prompt"
import "./titlebar.css"
import { newTabTooltipKeybind } from "./command-tooltip-keybind"
import { normalizeSessionInfo } from "@/utils/session"
import { macTitlebarLeftPadding } from "./titlebar-padding"

type TauriDesktopWindow = {
startDragging?: () => Promise<void>
toggleMaximize?: () => Promise<void>
}

type TauriThemeWindow = {
setTheme?: (theme?: "light" | "dark" | null) => Promise<void>
}

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
Expand All @@ -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()
Expand All @@ -85,7 +63,6 @@ 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 zoom = () => platform.webviewZoom?.() ?? 1
Expand Down Expand Up @@ -176,56 +153,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 (
<header
data-slot={useV2Titlebar() ? "titlebar-v2" : undefined}
Expand All @@ -237,17 +164,13 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
}}
style={{
"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,
// Keep visible native macOS traffic lights clear even when the desktop window is narrow.
"padding-left": mac() ? macTitlebarLeftPadding(zoom(), platform.windowFullscreen?.() ?? false) : 0,
width: windows() ? `env(titlebar-area-width, calc(100vw - ${windowsControlsWidth()}))` : undefined,
"max-width": windows() ? `env(titlebar-area-width, calc(100vw - ${windowsControlsWidth()}))` : undefined,
"align-self": windows() ? "flex-start" : undefined,
}}
data-tauri-drag-region
onMouseDown={drag}
onDblClick={maximize}
>
<Switch>
<Match when={useV2Titlebar()}>
Expand Down Expand Up @@ -528,9 +451,6 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
</Show>
<div class="flex-1" />
<TitlebarV2Right state={v2RightState()} />
<Show when={windows() && !electronWindows()}>
<div data-tauri-decorum-tb class="flex flex-row" />
</Show>
</div>
)
}}
Expand All @@ -550,7 +470,6 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
<WindowsAppMenu command={command} platform={platform} />
</Show>
<Show when={mac()}>
{/*<div class="h-full shrink-0" style={{ width: `${72 / zoom()}px` }} />*/}
<div class="xl:hidden w-10 shrink-0 flex items-center justify-center">
<IconButton
icon="menu"
Expand Down Expand Up @@ -680,12 +599,10 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
"pr-2": !windows(),
}}
data-tauri-drag-region
onMouseDown={drag}
>
<div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
<Show when={windows()}>
{!tauriApi() && <div class="shrink-0" style={{ width: windowsControlsWidth() }} />}
<div data-tauri-decorum-tb class="flex flex-row" />
<div class="shrink-0" style={{ width: windowsControlsWidth() }} />
</Show>
</div>
</div>
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/context/platform.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ type PlatformBase = {
/** Webview zoom level (desktop only) */
webviewZoom?: Accessor<number>

/** Whether the native desktop window is fullscreen */
windowFullscreen?: Accessor<boolean>

/** Get whether native pinch/Ctrl-scroll zoom gestures are enabled (desktop only) */
getPinchZoomEnabled?(): Promise<boolean> | boolean

Expand Down
5 changes: 5 additions & 0 deletions packages/desktop/src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
6 changes: 6 additions & 0 deletions packages/desktop/src/main/windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ export function createMainWindow(id: string = randomUUID()) {

state.manage(win)
registerWindow(win, id)
wireFullscreen(win)
loadWindow(win, "index.html")
wireZoom(win)

Expand Down Expand Up @@ -472,6 +473,11 @@ function wireZoom(win: BrowserWindow) {
})
}

function wireFullscreen(win: BrowserWindow) {
win.on("enter-full-screen", () => win.webContents.send("window-fullscreen-changed", true))
win.on("leave-full-screen", () => win.webContents.send("window-fullscreen-changed", false))
}

function clampZoom(value: number) {
return Math.min(Math.max(value, minZoomLevel), maxZoomLevel)
}
Expand Down
6 changes: 6 additions & 0 deletions packages/desktop/src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
2 changes: 2 additions & 0 deletions packages/desktop/src/preload/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ export type ElectronAPI = {
readClipboardImage: () => Promise<{ buffer: ArrayBuffer; width: number; height: number } | null>
showNotification: (title: string, body?: string) => void
getWindowFocused: () => Promise<boolean>
getWindowFullscreen: () => Promise<boolean>
onWindowFullscreenChanged: (cb: (fullscreen: boolean) => void) => () => void
setWindowFocus: () => Promise<void>
showWindow: () => Promise<void>
relaunch: () => void
Expand Down
6 changes: 6 additions & 0 deletions packages/desktop/src/renderer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ import "./styles.css"
import { Splash } from "@opencode-ai/ui/logo"
import { useTheme } from "@opencode-ai/ui/theme/context"

const [windowFullscreen, setWindowFullscreen] = createSignal(false)
window.api.onWindowFullscreenChanged(setWindowFullscreen)
void window.api.getWindowFullscreen().then(setWindowFullscreen)

const root = document.getElementById("root")
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
throw new Error(t("error.dev.rootNotFound"))
Expand Down Expand Up @@ -294,6 +298,8 @@ const createPlatform = (windowState: DesktopWindowState): Platform => {

webviewZoom,

windowFullscreen,

getPinchZoomEnabled: () => window.api.getPinchZoomEnabled(),

setPinchZoomEnabled,
Expand Down
Loading