Pārlūkot izejas kodu

fix(electron): theme Windows titlebar overlay (#16843)

Co-authored-by: Brendan Allan <brendonovich@outlook.com>
Luke Parker 5 mēneši atpakaļ
vecāks
revīzija
d481f64bde

+ 8 - 1
packages/app/src/app.tsx

@@ -62,6 +62,9 @@ declare global {
       deepLinks?: string[]
       wsl?: boolean
     }
+    api?: {
+      setTitlebar?: (theme: { mode: "light" | "dark" }) => Promise<void>
+    }
   }
 }
 
@@ -115,7 +118,11 @@ export function AppBaseProviders(props: ParentProps) {
   return (
     <MetaProvider>
       <Font />
-      <ThemeProvider>
+      <ThemeProvider
+        onThemeApplied={(_, mode) => {
+          void window.api?.setTitlebar?.({ mode })
+        }}
+      >
         <LanguageProvider>
           <UiI18nBridge>
             <ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>

+ 2 - 2
packages/app/src/components/titlebar.tsx

@@ -1,4 +1,4 @@
-import { createEffect, createMemo, Show, untrack } from "solid-js"
+import { createEffect, createMemo, onCleanup, Show, untrack } from "solid-js"
 import { createStore } from "solid-js/store"
 import { useLocation, useNavigate, useParams } from "@solidjs/router"
 import { IconButton } from "@opencode-ai/ui/icon-button"
@@ -282,7 +282,7 @@ export function Titlebar() {
       >
         <div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
         <Show when={windows()}>
-          <div class="w-6 shrink-0" />
+          {!tauriApi() && <div class="w-36 shrink-0" />}
           <div data-tauri-decorum-tb class="flex flex-row" />
         </Show>
       </div>

+ 7 - 1
packages/desktop-electron/src/main/ipc.ts

@@ -2,8 +2,9 @@ import { execFile } from "node:child_process"
 import { BrowserWindow, Notification, app, clipboard, dialog, ipcMain, shell } from "electron"
 import type { IpcMainEvent, IpcMainInvokeEvent } from "electron"
 
-import type { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types"
+import type { InitStep, ServerReadyData, SqliteMigrationProgress, TitlebarTheme, WslConfig } from "../preload/types"
 import { getStore } from "./store"
+import { setTitlebar } from "./windows"
 
 type Deps = {
   killSidecar: () => void
@@ -161,6 +162,11 @@ 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))
+  ipcMain.handle("set-titlebar", (event: IpcMainInvokeEvent, theme: TitlebarTheme) => {
+    const win = BrowserWindow.fromWebContents(event.sender)
+    if (!win) return
+    setTitlebar(win, theme)
+  })
 }
 
 export function sendSqliteMigrationProgress(win: BrowserWindow, progress: SqliteMigrationProgress) {

+ 24 - 11
packages/desktop-electron/src/main/windows.ts

@@ -1,7 +1,8 @@
 import windowState from "electron-window-state"
-import { app, BrowserWindow, nativeImage } from "electron"
+import { app, BrowserWindow, nativeImage, nativeTheme } from "electron"
 import { dirname, join } from "node:path"
 import { fileURLToPath } from "node:url"
+import type { TitlebarTheme } from "../preload/types"
 
 type Globals = {
   updaterEnabled: boolean
@@ -20,6 +21,24 @@ function iconPath() {
   return join(iconsDir(), `icon.${ext}`)
 }
 
+function tone() {
+  return nativeTheme.shouldUseDarkColors ? "dark" : "light"
+}
+
+function overlay(theme: Partial<TitlebarTheme> = {}) {
+  const mode = theme.mode ?? tone()
+  return {
+    color: "#00000000",
+    symbolColor: mode === "dark" ? "white" : "black",
+    height: 40,
+  }
+}
+
+export function setTitlebar(win: BrowserWindow, theme: Partial<TitlebarTheme> = {}) {
+  if (process.platform !== "win32") return
+  win.setTitleBarOverlay(overlay(theme))
+}
+
 export function setDockIcon() {
   if (process.platform !== "darwin") return
   app.dock?.setIcon(nativeImage.createFromPath(join(iconsDir(), "128x128@2x.png")))
@@ -31,6 +50,7 @@ export function createMainWindow(globals: Globals) {
     defaultHeight: 800,
   })
 
+  const mode = tone()
   const win = new BrowserWindow({
     x: state.x,
     y: state.y,
@@ -49,11 +69,7 @@ export function createMainWindow(globals: Globals) {
       ? {
           frame: false,
           titleBarStyle: "hidden" as const,
-          titleBarOverlay: {
-            color: "transparent",
-            symbolColor: "#999",
-            height: 40,
-          },
+          titleBarOverlay: overlay({ mode }),
         }
       : {}),
     webPreferences: {
@@ -71,6 +87,7 @@ export function createMainWindow(globals: Globals) {
 }
 
 export function createLoadingWindow(globals: Globals) {
+  const mode = tone()
   const win = new BrowserWindow({
     width: 640,
     height: 480,
@@ -83,11 +100,7 @@ export function createLoadingWindow(globals: Globals) {
       ? {
           frame: false,
           titleBarStyle: "hidden" as const,
-          titleBarOverlay: {
-            color: "transparent",
-            symbolColor: "#999",
-            height: 40,
-          },
+          titleBarOverlay: overlay({ mode }),
         }
       : {}),
     webPreferences: {

+ 1 - 0
packages/desktop-electron/src/preload/index.ts

@@ -57,6 +57,7 @@ const api: ElectronAPI = {
   relaunch: () => ipcRenderer.send("relaunch"),
   getZoomFactor: () => ipcRenderer.invoke("get-zoom-factor"),
   setZoomFactor: (factor) => ipcRenderer.invoke("set-zoom-factor", factor),
+  setTitlebar: (theme) => ipcRenderer.invoke("set-titlebar", theme),
   loadingWindowComplete: () => ipcRenderer.send("loading-window-complete"),
   runUpdater: (alertOnFail) => ipcRenderer.invoke("run-updater", alertOnFail),
   checkUpdate: () => ipcRenderer.invoke("check-update"),

+ 4 - 0
packages/desktop-electron/src/preload/types.ts

@@ -10,6 +10,9 @@ export type SqliteMigrationProgress = { type: "InProgress"; value: number } | {
 export type WslConfig = { enabled: boolean }
 
 export type LinuxDisplayBackend = "wayland" | "auto"
+export type TitlebarTheme = {
+  mode: "light" | "dark"
+}
 
 export type ElectronAPI = {
   killSidecar: () => Promise<void>
@@ -57,6 +60,7 @@ export type ElectronAPI = {
   relaunch: () => void
   getZoomFactor: () => Promise<number>
   setZoomFactor: (factor: number) => Promise<void>
+  setTitlebar: (theme: TitlebarTheme) => Promise<void>
   loadingWindowComplete: () => void
   runUpdater: (alertOnFail: boolean) => Promise<void>
   checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>

+ 10 - 5
packages/ui/src/theme/context.tsx

@@ -77,7 +77,7 @@ function cacheThemeVariants(theme: DesktopTheme, themeId: string) {
 
 export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
   name: "Theme",
-  init: (props: { defaultTheme?: string }) => {
+  init: (props: { defaultTheme?: string; onThemeApplied?: (theme: DesktopTheme, mode: "light" | "dark") => void }) => {
     const [store, setStore] = createStore({
       themes: DEFAULT_THEMES as Record<string, DesktopTheme>,
       themeId: normalize(props.defaultTheme) ?? "oc-2",
@@ -119,10 +119,15 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
       }
     })
 
+    const applyTheme = (theme: DesktopTheme, themeId: string, mode: "light" | "dark") => {
+      applyThemeCss(theme, themeId, mode)
+      props.onThemeApplied?.(theme, mode)
+    }
+
     createEffect(() => {
       const theme = store.themes[store.themeId]
       if (theme) {
-        applyThemeCss(theme, store.themeId, store.mode)
+        applyTheme(theme, store.themeId, store.mode)
       }
     })
 
@@ -171,7 +176,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
             ? getSystemMode()
             : store.previewScheme
           : store.mode
-        applyThemeCss(theme, next, previewMode)
+        applyTheme(theme, next, previewMode)
       },
       previewColorScheme: (scheme: ColorScheme) => {
         setStore("previewScheme", scheme)
@@ -179,7 +184,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
         const id = store.previewThemeId ?? store.themeId
         const theme = store.themes[id]
         if (theme) {
-          applyThemeCss(theme, id, previewMode)
+          applyTheme(theme, id, previewMode)
         }
       },
       commitPreview: () => {
@@ -197,7 +202,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
         setStore("previewScheme", null)
         const theme = store.themes[store.themeId]
         if (theme) {
-          applyThemeCss(theme, store.themeId, store.mode)
+          applyTheme(theme, store.themeId, store.mode)
         }
       },
     }