Skip to content

Commit d481f64

Browse files
HonaBrendonovich
andauthored
fix(electron): theme Windows titlebar overlay (anomalyco#16843)
Co-authored-by: Brendan Allan <brendonovich@outlook.com>
1 parent 54e7baa commit d481f64

7 files changed

Lines changed: 56 additions & 20 deletions

File tree

packages/app/src/app.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ declare global {
6262
deepLinks?: string[]
6363
wsl?: boolean
6464
}
65+
api?: {
66+
setTitlebar?: (theme: { mode: "light" | "dark" }) => Promise<void>
67+
}
6568
}
6669
}
6770

@@ -115,7 +118,11 @@ export function AppBaseProviders(props: ParentProps) {
115118
return (
116119
<MetaProvider>
117120
<Font />
118-
<ThemeProvider>
121+
<ThemeProvider
122+
onThemeApplied={(_, mode) => {
123+
void window.api?.setTitlebar?.({ mode })
124+
}}
125+
>
119126
<LanguageProvider>
120127
<UiI18nBridge>
121128
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>

packages/app/src/components/titlebar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createEffect, createMemo, Show, untrack } from "solid-js"
1+
import { createEffect, createMemo, onCleanup, Show, untrack } from "solid-js"
22
import { createStore } from "solid-js/store"
33
import { useLocation, useNavigate, useParams } from "@solidjs/router"
44
import { IconButton } from "@opencode-ai/ui/icon-button"
@@ -282,7 +282,7 @@ export function Titlebar() {
282282
>
283283
<div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
284284
<Show when={windows()}>
285-
<div class="w-6 shrink-0" />
285+
{!tauriApi() && <div class="w-36 shrink-0" />}
286286
<div data-tauri-decorum-tb class="flex flex-row" />
287287
</Show>
288288
</div>

packages/desktop-electron/src/main/ipc.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { execFile } from "node:child_process"
22
import { BrowserWindow, Notification, app, clipboard, dialog, ipcMain, shell } from "electron"
33
import type { IpcMainEvent, IpcMainInvokeEvent } from "electron"
44

5-
import type { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types"
5+
import type { InitStep, ServerReadyData, SqliteMigrationProgress, TitlebarTheme, WslConfig } from "../preload/types"
66
import { getStore } from "./store"
7+
import { setTitlebar } from "./windows"
78

89
type Deps = {
910
killSidecar: () => void
@@ -161,6 +162,11 @@ export function registerIpcHandlers(deps: Deps) {
161162

162163
ipcMain.handle("get-zoom-factor", (event: IpcMainInvokeEvent) => event.sender.getZoomFactor())
163164
ipcMain.handle("set-zoom-factor", (event: IpcMainInvokeEvent, factor: number) => event.sender.setZoomFactor(factor))
165+
ipcMain.handle("set-titlebar", (event: IpcMainInvokeEvent, theme: TitlebarTheme) => {
166+
const win = BrowserWindow.fromWebContents(event.sender)
167+
if (!win) return
168+
setTitlebar(win, theme)
169+
})
164170
}
165171

166172
export function sendSqliteMigrationProgress(win: BrowserWindow, progress: SqliteMigrationProgress) {

packages/desktop-electron/src/main/windows.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import windowState from "electron-window-state"
2-
import { app, BrowserWindow, nativeImage } from "electron"
2+
import { app, BrowserWindow, nativeImage, nativeTheme } from "electron"
33
import { dirname, join } from "node:path"
44
import { fileURLToPath } from "node:url"
5+
import type { TitlebarTheme } from "../preload/types"
56

67
type Globals = {
78
updaterEnabled: boolean
@@ -20,6 +21,24 @@ function iconPath() {
2021
return join(iconsDir(), `icon.${ext}`)
2122
}
2223

24+
function tone() {
25+
return nativeTheme.shouldUseDarkColors ? "dark" : "light"
26+
}
27+
28+
function overlay(theme: Partial<TitlebarTheme> = {}) {
29+
const mode = theme.mode ?? tone()
30+
return {
31+
color: "#00000000",
32+
symbolColor: mode === "dark" ? "white" : "black",
33+
height: 40,
34+
}
35+
}
36+
37+
export function setTitlebar(win: BrowserWindow, theme: Partial<TitlebarTheme> = {}) {
38+
if (process.platform !== "win32") return
39+
win.setTitleBarOverlay(overlay(theme))
40+
}
41+
2342
export function setDockIcon() {
2443
if (process.platform !== "darwin") return
2544
app.dock?.setIcon(nativeImage.createFromPath(join(iconsDir(), "128x128@2x.png")))
@@ -31,6 +50,7 @@ export function createMainWindow(globals: Globals) {
3150
defaultHeight: 800,
3251
})
3352

53+
const mode = tone()
3454
const win = new BrowserWindow({
3555
x: state.x,
3656
y: state.y,
@@ -49,11 +69,7 @@ export function createMainWindow(globals: Globals) {
4969
? {
5070
frame: false,
5171
titleBarStyle: "hidden" as const,
52-
titleBarOverlay: {
53-
color: "transparent",
54-
symbolColor: "#999",
55-
height: 40,
56-
},
72+
titleBarOverlay: overlay({ mode }),
5773
}
5874
: {}),
5975
webPreferences: {
@@ -71,6 +87,7 @@ export function createMainWindow(globals: Globals) {
7187
}
7288

7389
export function createLoadingWindow(globals: Globals) {
90+
const mode = tone()
7491
const win = new BrowserWindow({
7592
width: 640,
7693
height: 480,
@@ -83,11 +100,7 @@ export function createLoadingWindow(globals: Globals) {
83100
? {
84101
frame: false,
85102
titleBarStyle: "hidden" as const,
86-
titleBarOverlay: {
87-
color: "transparent",
88-
symbolColor: "#999",
89-
height: 40,
90-
},
103+
titleBarOverlay: overlay({ mode }),
91104
}
92105
: {}),
93106
webPreferences: {

packages/desktop-electron/src/preload/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const api: ElectronAPI = {
5757
relaunch: () => ipcRenderer.send("relaunch"),
5858
getZoomFactor: () => ipcRenderer.invoke("get-zoom-factor"),
5959
setZoomFactor: (factor) => ipcRenderer.invoke("set-zoom-factor", factor),
60+
setTitlebar: (theme) => ipcRenderer.invoke("set-titlebar", theme),
6061
loadingWindowComplete: () => ipcRenderer.send("loading-window-complete"),
6162
runUpdater: (alertOnFail) => ipcRenderer.invoke("run-updater", alertOnFail),
6263
checkUpdate: () => ipcRenderer.invoke("check-update"),

packages/desktop-electron/src/preload/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ export type SqliteMigrationProgress = { type: "InProgress"; value: number } | {
1010
export type WslConfig = { enabled: boolean }
1111

1212
export type LinuxDisplayBackend = "wayland" | "auto"
13+
export type TitlebarTheme = {
14+
mode: "light" | "dark"
15+
}
1316

1417
export type ElectronAPI = {
1518
killSidecar: () => Promise<void>
@@ -57,6 +60,7 @@ export type ElectronAPI = {
5760
relaunch: () => void
5861
getZoomFactor: () => Promise<number>
5962
setZoomFactor: (factor: number) => Promise<void>
63+
setTitlebar: (theme: TitlebarTheme) => Promise<void>
6064
loadingWindowComplete: () => void
6165
runUpdater: (alertOnFail: boolean) => Promise<void>
6266
checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>

packages/ui/src/theme/context.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ function cacheThemeVariants(theme: DesktopTheme, themeId: string) {
7777

7878
export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
7979
name: "Theme",
80-
init: (props: { defaultTheme?: string }) => {
80+
init: (props: { defaultTheme?: string; onThemeApplied?: (theme: DesktopTheme, mode: "light" | "dark") => void }) => {
8181
const [store, setStore] = createStore({
8282
themes: DEFAULT_THEMES as Record<string, DesktopTheme>,
8383
themeId: normalize(props.defaultTheme) ?? "oc-2",
@@ -119,10 +119,15 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
119119
}
120120
})
121121

122+
const applyTheme = (theme: DesktopTheme, themeId: string, mode: "light" | "dark") => {
123+
applyThemeCss(theme, themeId, mode)
124+
props.onThemeApplied?.(theme, mode)
125+
}
126+
122127
createEffect(() => {
123128
const theme = store.themes[store.themeId]
124129
if (theme) {
125-
applyThemeCss(theme, store.themeId, store.mode)
130+
applyTheme(theme, store.themeId, store.mode)
126131
}
127132
})
128133

@@ -171,15 +176,15 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
171176
? getSystemMode()
172177
: store.previewScheme
173178
: store.mode
174-
applyThemeCss(theme, next, previewMode)
179+
applyTheme(theme, next, previewMode)
175180
},
176181
previewColorScheme: (scheme: ColorScheme) => {
177182
setStore("previewScheme", scheme)
178183
const previewMode = scheme === "system" ? getSystemMode() : scheme
179184
const id = store.previewThemeId ?? store.themeId
180185
const theme = store.themes[id]
181186
if (theme) {
182-
applyThemeCss(theme, id, previewMode)
187+
applyTheme(theme, id, previewMode)
183188
}
184189
},
185190
commitPreview: () => {
@@ -197,7 +202,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
197202
setStore("previewScheme", null)
198203
const theme = store.themes[store.themeId]
199204
if (theme) {
200-
applyThemeCss(theme, store.themeId, store.mode)
205+
applyTheme(theme, store.themeId, store.mode)
201206
}
202207
},
203208
}

0 commit comments

Comments
 (0)