fix(desktop): stabilize Windows titlebar zoom (#25813)
This commit is contained in:
@@ -35,6 +35,9 @@ type TauriApi = {
|
|||||||
const tauriApi = () => (window as unknown as { __TAURI__?: TauriApi }).__TAURI__
|
const tauriApi = () => (window as unknown as { __TAURI__?: TauriApi }).__TAURI__
|
||||||
const currentDesktopWindow = () => tauriApi()?.window?.getCurrentWindow?.()
|
const currentDesktopWindow = () => tauriApi()?.window?.getCurrentWindow?.()
|
||||||
const currentThemeWindow = () => tauriApi()?.webviewWindow?.getCurrentWebviewWindow?.()
|
const currentThemeWindow = () => tauriApi()?.webviewWindow?.getCurrentWebviewWindow?.()
|
||||||
|
const titlebarHeight = 40
|
||||||
|
const minTitlebarZoom = 0.25
|
||||||
|
const windowsControlsBaseWidth = 138 // 3 native Windows caption buttons at 46px each.
|
||||||
|
|
||||||
export function Titlebar() {
|
export function Titlebar() {
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
@@ -51,7 +54,14 @@ export function Titlebar() {
|
|||||||
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
|
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
|
||||||
const web = createMemo(() => platform.platform === "web")
|
const web = createMemo(() => platform.platform === "web")
|
||||||
const zoom = () => platform.webviewZoom?.() ?? 1
|
const zoom = () => platform.webviewZoom?.() ?? 1
|
||||||
const minHeight = () => (mac() ? `${40 / zoom()}px` : undefined)
|
const titlebarZoom = () => (windows() ? Math.max(zoom(), minTitlebarZoom) : zoom())
|
||||||
|
const counterZoom = () => (windows() && titlebarZoom() < 1 ? 1 / titlebarZoom() : 1)
|
||||||
|
const minHeight = () => {
|
||||||
|
if (mac()) return `${titlebarHeight / zoom()}px`
|
||||||
|
if (windows()) return `${titlebarHeight / Math.min(titlebarZoom(), 1)}px`
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const windowsControlsWidth = () => `${windowsControlsBaseWidth / Math.max(titlebarZoom(), 1)}px`
|
||||||
|
|
||||||
const [history, setHistory] = createStore({
|
const [history, setHistory] = createStore({
|
||||||
stack: [] as string[],
|
stack: [] as string[],
|
||||||
@@ -165,12 +175,16 @@ export function Titlebar() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
class="h-10 shrink-0 bg-background-base relative grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center"
|
class="h-10 shrink-0 bg-background-base relative overflow-hidden"
|
||||||
style={{ "min-height": minHeight() }}
|
style={{ "min-height": minHeight() }}
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
onMouseDown={drag}
|
onMouseDown={drag}
|
||||||
onDblClick={maximize}
|
onDblClick={maximize}
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
class="grid h-full min-h-full w-full grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center"
|
||||||
|
style={{ zoom: counterZoom() }}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"flex items-center min-w-0": true,
|
"flex items-center min-w-0": true,
|
||||||
@@ -312,10 +326,11 @@ export function Titlebar() {
|
|||||||
>
|
>
|
||||||
<div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
|
<div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
|
||||||
<Show when={windows()}>
|
<Show when={windows()}>
|
||||||
{!tauriApi() && <div class="w-36 shrink-0" />}
|
{!tauriApi() && <div class="shrink-0" style={{ width: windowsControlsWidth() }} />}
|
||||||
<div data-tauri-decorum-tb class="flex flex-row" />
|
<div data-tauri-decorum-tb class="flex flex-row" />
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type {
|
|||||||
WslConfig,
|
WslConfig,
|
||||||
} from "../preload/types"
|
} from "../preload/types"
|
||||||
import { getStore } from "./store"
|
import { getStore } from "./store"
|
||||||
import { setTitlebar } from "./windows"
|
import { setTitlebar, updateTitlebar } from "./windows"
|
||||||
|
|
||||||
const pickerFilters = (ext?: string[]) => {
|
const pickerFilters = (ext?: string[]) => {
|
||||||
if (!ext || ext.length === 0) return undefined
|
if (!ext || ext.length === 0) return undefined
|
||||||
@@ -183,7 +183,12 @@ export function registerIpcHandlers(deps: Deps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle("get-zoom-factor", (event: IpcMainInvokeEvent) => event.sender.getZoomFactor())
|
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-zoom-factor", (event: IpcMainInvokeEvent, factor: number) => {
|
||||||
|
event.sender.setZoomFactor(factor)
|
||||||
|
const win = BrowserWindow.fromWebContents(event.sender)
|
||||||
|
if (!win) return
|
||||||
|
updateTitlebar(win)
|
||||||
|
})
|
||||||
ipcMain.handle("set-titlebar", (event: IpcMainInvokeEvent, theme: TitlebarTheme) => {
|
ipcMain.handle("set-titlebar", (event: IpcMainInvokeEvent, theme: TitlebarTheme) => {
|
||||||
const win = BrowserWindow.fromWebContents(event.sender)
|
const win = BrowserWindow.fromWebContents(event.sender)
|
||||||
if (!win) return
|
if (!win) return
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ protocol.registerSchemesAsPrivileged([
|
|||||||
])
|
])
|
||||||
|
|
||||||
let backgroundColor: string | undefined
|
let backgroundColor: string | undefined
|
||||||
|
const titlebarThemes = new WeakMap<BrowserWindow, Partial<TitlebarTheme>>()
|
||||||
|
const titlebarHeight = 40
|
||||||
|
|
||||||
export function setBackgroundColor(color: string) {
|
export function setBackgroundColor(color: string) {
|
||||||
backgroundColor = color
|
backgroundColor = color
|
||||||
@@ -43,18 +45,23 @@ function tone() {
|
|||||||
return nativeTheme.shouldUseDarkColors ? "dark" : "light"
|
return nativeTheme.shouldUseDarkColors ? "dark" : "light"
|
||||||
}
|
}
|
||||||
|
|
||||||
function overlay(theme: Partial<TitlebarTheme> = {}) {
|
function overlay(theme: Partial<TitlebarTheme> = {}, zoom = 1) {
|
||||||
const mode = theme.mode ?? tone()
|
const mode = theme.mode ?? tone()
|
||||||
return {
|
return {
|
||||||
color: "#00000000",
|
color: "#00000000",
|
||||||
symbolColor: mode === "dark" ? "white" : "black",
|
symbolColor: mode === "dark" ? "white" : "black",
|
||||||
height: 40,
|
height: Math.max(titlebarHeight, Math.round(titlebarHeight * zoom)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setTitlebar(win: BrowserWindow, theme: Partial<TitlebarTheme> = {}) {
|
export function setTitlebar(win: BrowserWindow, theme: Partial<TitlebarTheme> = {}) {
|
||||||
|
titlebarThemes.set(win, theme)
|
||||||
|
updateTitlebar(win)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateTitlebar(win: BrowserWindow) {
|
||||||
if (process.platform !== "win32") return
|
if (process.platform !== "win32") return
|
||||||
win.setTitleBarOverlay(overlay(theme))
|
win.setTitleBarOverlay(overlay(titlebarThemes.get(win), win.webContents.getZoomFactor()))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setDockIcon() {
|
export function setDockIcon() {
|
||||||
@@ -188,6 +195,7 @@ function wireZoom(win: BrowserWindow) {
|
|||||||
win.webContents.setZoomFactor(1)
|
win.webContents.setZoomFactor(1)
|
||||||
win.webContents.on("zoom-changed", () => {
|
win.webContents.on("zoom-changed", () => {
|
||||||
win.webContents.setZoomFactor(1)
|
win.webContents.setZoomFactor(1)
|
||||||
|
updateTitlebar(win)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const OS_NAME = (() => {
|
|||||||
})()
|
})()
|
||||||
|
|
||||||
const [webviewZoom, setWebviewZoom] = createSignal(1)
|
const [webviewZoom, setWebviewZoom] = createSignal(1)
|
||||||
|
let requestedZoom = 1
|
||||||
|
|
||||||
const MAX_ZOOM_LEVEL = 10
|
const MAX_ZOOM_LEVEL = 10
|
||||||
const MIN_ZOOM_LEVEL = 0.2
|
const MIN_ZOOM_LEVEL = 0.2
|
||||||
@@ -19,8 +20,14 @@ const MIN_ZOOM_LEVEL = 0.2
|
|||||||
const clamp = (value: number) => Math.min(Math.max(value, MIN_ZOOM_LEVEL), MAX_ZOOM_LEVEL)
|
const clamp = (value: number) => Math.min(Math.max(value, MIN_ZOOM_LEVEL), MAX_ZOOM_LEVEL)
|
||||||
|
|
||||||
const applyZoom = (next: number) => {
|
const applyZoom = (next: number) => {
|
||||||
setWebviewZoom(next)
|
requestedZoom = next
|
||||||
void window.api.setZoomFactor(next)
|
void window.api.setZoomFactor(next).then(() => {
|
||||||
|
if (requestedZoom !== next) return
|
||||||
|
setWebviewZoom(next)
|
||||||
|
}).catch(() => {
|
||||||
|
if (requestedZoom !== next) return
|
||||||
|
requestedZoom = webviewZoom()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("keydown", (event) => {
|
window.addEventListener("keydown", (event) => {
|
||||||
@@ -28,12 +35,12 @@ window.addEventListener("keydown", (event) => {
|
|||||||
|
|
||||||
if (event.key === "-") {
|
if (event.key === "-") {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
applyZoom(clamp(webviewZoom() - 0.2))
|
applyZoom(clamp(requestedZoom - 0.2))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.key === "=" || event.key === "+") {
|
if (event.key === "=" || event.key === "+") {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
applyZoom(clamp(webviewZoom() + 0.2))
|
applyZoom(clamp(requestedZoom + 0.2))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.key === "0") {
|
if (event.key === "0") {
|
||||||
|
|||||||
Reference in New Issue
Block a user