fix(desktop): align macOS traffic lights with the vertical tabs header (#57258)

This commit is contained in:
Brendan Allan
2026-10-20 10:14:47 +08:00
committed by GitHub
Unverified
parent f3f1fc7511
commit 15be9f1939
10 changed files with 64 additions and 7 deletions
@@ -86,6 +86,9 @@ type PlatformBase = {
/** Whether the native desktop window is fullscreen */
windowFullscreen?: Accessor<boolean>
/** Move the native macOS traffic lights' top-left corner, in window points (desktop only) */
setTrafficLightsPosition?(position: { x: number; y: number }): Promise<void>
/** Get whether native pinch/Ctrl-scroll zoom gestures are enabled (desktop only) */
getPinchZoomEnabled?(): Promise<boolean> | boolean
+16 -4
View File
@@ -9,11 +9,20 @@ import { useLanguage } from "@/runtime/i18n/language"
import { usePlatform } from "@/runtime/platform/platform"
import { useSettings } from "@/settings/model"
// The desktop window sets `trafficLightPosition: { x: 14, y: 14 }`. macOS 26 and later draw 14px buttons on a 23px
// pitch, so the zoom button ends 74px from the window edge and the buttons center 21px from the top. macOS 15 and earlier
// end at 68px and center at 22px.
// The desktop window sets `trafficLightPosition: { x: 14, y: 14 }`, and the titlebar moves the buttons down to share the
// header's center line in vertical tabs. macOS 26 and later draw 14px buttons on a 23px pitch, so the zoom button ends
// 74px from the window edge. macOS 15 and earlier end at 68px.
export const macTrafficLightsBaseWidth = 74
// Vertical tabs center the 14pt traffic lights on the session header's center line, 32px from the top, and inset them as
// far from the left edge as from the top. The center scales with zoom. Returns window points; `width` is where the zoom
// button ends.
export function macVerticalTrafficLights(zoom: number) {
const inset = 32 * zoom - 7
return { x: inset, y: inset, width: inset + macTrafficLightsBaseWidth - 14 }
}
// Matches the horizontal titlebar, whose first control starts 16px past the traffic lights.
const macTrafficLightsGap = 16
@@ -73,7 +82,10 @@ function useCollapsedSlot() {
// Lays the toggle out as if the traffic lights were the header's first control.
width: () =>
mac()
? macTrafficLightsBaseWidth / (platform.webviewZoom?.() ?? 1) + macTrafficLightsGap - contentStart + slotWidth
? macVerticalTrafficLights(platform.webviewZoom?.() ?? 1).width / (platform.webviewZoom?.() ?? 1) +
macTrafficLightsGap -
contentStart +
slotWidth
: slotWidth,
}
}
+16 -2
View File
@@ -16,7 +16,7 @@ import { useSettings } from "@/settings/model"
import { WindowsAppMenu } from "./windows-menu"
import { applyPath, backPath, forwardPath, type HistoryLocation } from "./history"
import { TitlebarTabStrip } from "@/shell/titlebar/tab-strip"
import { macTrafficLightsBaseWidth, SidebarToggle } from "@/shell/sidebar-toggle"
import { macTrafficLightsBaseWidth, macVerticalTrafficLights, SidebarToggle } from "@/shell/sidebar-toggle"
import { SidebarInbox } from "@/shell/titlebar/inbox"
import { makeEventListener } from "@solid-primitives/event-listener"
import { createMediaQuery } from "@solid-primitives/media"
@@ -46,6 +46,9 @@ const windowsControlsBaseWidth = 138 // 3 native Windows caption buttons at 46px
const macTrafficLightsTopClearance = 28
// Matches the desktop window's `trafficLightPosition`.
const macTrafficLightsDefaultPosition = { x: 14, y: 14 }
// iOS blurs page content just below the status bar in Home Screen web apps, so with a top safe area the phone
// titlebar row stays 16px clear of it, as ChatGPT's phone header does. Without one, 8px matches the content gap below.
const mobileTopClearance = "max(8px, min(16px, env(safe-area-inset-top, 0px) * 1000))"
@@ -81,6 +84,15 @@ export function Titlebar(props: { verticalTabs?: { mount?: HTMLElement } }) {
const windowsControlsWidth = () => `${windowsControlsBaseWidth / Math.max(titlebarZoom(), 1)}px`
// The vertical sidebar header's center line scales with zoom, so the 14pt traffic lights follow it. The horizontal
// titlebar stays at the window's default position.
createEffect(() => {
if (!mac()) return
void platform.setTrafficLightsPosition?.(
macVerticalTabs() ? macVerticalTrafficLights(zoom()) : macTrafficLightsDefaultPosition,
)
})
const [history, setHistory] = createStore<{
stack: HistoryLocation[]
index: number
@@ -790,7 +802,9 @@ export function Titlebar(props: { verticalTabs?: { mount?: HTMLElement } }) {
{/* The sidebar's 10px start padding already covers part of the traffic lights. */}
<div
class="shrink-0"
style={{ width: `calc(${macTrafficLightsBaseWidth / zoom()}px - 10px)` }}
style={{
width: `calc(${macVerticalTrafficLights(zoom()).width / zoom()}px - 10px)`,
}}
aria-hidden="true"
/>
</Show>
@@ -2,7 +2,14 @@ import { app, BrowserWindow } from "electron"
import { Effect } from "effect"
import { WindowRpcs } from "../../shared/ipc-rpc"
import { IpcPortHandoff } from "../ipc-transport"
import { getPinchZoomEnabled, setPinchZoomEnabled, setTitlebar, setWindowThemeReady, updateTitlebar } from "../windows"
import {
getPinchZoomEnabled,
setPinchZoomEnabled,
setTitlebar,
setTrafficLightsPosition,
setWindowThemeReady,
updateTitlebar,
} from "../windows"
import { focusWindow, showWindow } from "../windows/focus"
import { sender } from "./context"
@@ -43,6 +50,12 @@ export const windowHandlers = WindowRpcs.toLayer(
if (win) setTitlebar(win, theme)
}),
WindowSetTrafficLightsPosition: (position, context) =>
Effect.sync(() => {
const win = BrowserWindow.fromWebContents(sender(handoff, context))
if (win) setTrafficLightsPosition(win, position)
}),
})
}),
)
@@ -83,6 +83,11 @@ export function setTitlebar(win: BrowserWindow, theme: Partial<TitlebarTheme> =
updateTitlebar(win)
}
export function setTrafficLightsPosition(win: BrowserWindow, position: { x: number; y: number }) {
if (process.platform !== "darwin") return
win.setWindowButtonPosition({ x: Math.round(position.x), y: Math.round(position.y) })
}
export function updateTitlebar(win: BrowserWindow) {
if (process.platform !== "win32") return
win.setTitleBarOverlay(overlay(titlebarThemes.get(win), win.webContents.getZoomFactor()))
@@ -16,6 +16,7 @@ import {
setDockIcon,
setPinchZoomEnabled,
setTitlebar,
setTrafficLightsPosition,
setZoomFactor,
updateTitlebar,
windowAppearance,
@@ -60,6 +61,7 @@ export {
setDockIcon,
setPinchZoomEnabled,
setTitlebar,
setTrafficLightsPosition,
setZoomFactor,
updateTitlebar,
}
@@ -62,6 +62,7 @@ export type ElectronAPI = {
onPinchZoomEnabledChanged(cb: (enabled: boolean) => void): () => void
onZoomFactorChanged(cb: (factor: number) => void): () => void
setTitlebar(theme: TitlebarTheme): Promise<void>
setTrafficLightsPosition(position: { x: number; y: number }): Promise<void>
runDesktopMenuAction(action: DesktopMenuAction): Promise<void>
setBackgroundColor(color: string): Promise<void>
exportDebugLogs(): Promise<string>
+1
View File
@@ -85,6 +85,7 @@ export const api: ElectronAPI = {
onPinchZoomEnabledChanged: (cb) => listen("WindowPinchZoomChanged", (event) => cb(event.enabled)),
onZoomFactorChanged: (cb) => listen("WindowZoomChanged", (event) => cb(event.factor)),
setTitlebar: (theme) => invoke("WindowSetTitlebar", { theme }),
setTrafficLightsPosition: (position) => invoke("WindowSetTrafficLightsPosition", position),
runDesktopMenuAction: (action) => invoke("MenuRunAction", { action }),
setBackgroundColor: (color) => invoke("AppSetBackgroundColor", { color }),
exportDebugLogs: () => invoke("AppExportDebugLogs"),
@@ -36,6 +36,7 @@ export function createDesktopPlatform(api: ElectronAPI, windowState: DesktopWind
},
webviewZoom,
windowFullscreen,
setTrafficLightsPosition: (position) => api.setTrafficLightsPosition(position),
getPinchZoomEnabled: () => api.getPinchZoomEnabled(),
setPinchZoomEnabled,
onDragCancel: (callback) => {
@@ -34,6 +34,10 @@ export const WindowSetTitlebar = Rpc.make("WindowSetTitlebar", {
},
})
export const WindowSetTrafficLightsPosition = Rpc.make("WindowSetTrafficLightsPosition", {
payload: { x: Schema.Number, y: Schema.Number },
})
export const WindowRpcs = RpcGroup.make(
WindowThemeReady,
WindowGetFocused,
@@ -45,4 +49,5 @@ export const WindowRpcs = RpcGroup.make(
WindowGetPinchZoomEnabled,
WindowSetPinchZoomEnabled,
WindowSetTitlebar,
WindowSetTrafficLightsPosition,
)