| author | |
| committer | |
| log | c1b2210411da79350a3f9f5a65a4a64971e4acad |
| tree | 4a9b21f1d20b80f776b79a781c319a5b93280cf5 |
| parent | ae53072af0560c0606202b87b5fdb7e52d48ac5c |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: T3 Code Test <t3code-test@example.com>5 files changed, 78 insertions(+), 6 deletions(-)
apps/desktop/src/preload.ts+14-1| ... | ... | @@ -6,7 +6,7 @@ import type { |
| 6 | 6 | DesktopSnapShotEvent, |
| 7 | 7 | } from "@t3tools/contracts"; |
| 8 | 8 | import { exposeClerkBridge } from "@clerk/electron/preload"; |
| 9 | import { contextBridge, ipcRenderer } from "electron"; | |
| 9 | import { contextBridge, ipcRenderer, webFrame } from "electron"; | |
| 10 | 10 | |
| 11 | 11 | import * as IpcChannels from "./ipc/channels.ts"; |
| 12 | 12 | |
| ... | ... | @@ -32,6 +32,19 @@ exposeClerkBridge({ passkeys: true }); |
| 32 | 32 | // oxlint-disable-next-line t3code/no-global-process-runtime -- Electron exposes the client platform in its sandboxed preload process. |
| 33 | 33 | const clientPlatform = process.platform; |
| 34 | 34 | |
| 35 | if (clientPlatform === "darwin") { | |
| 36 | // Native window buttons do not scale with Chromium zoom. Keep their reserved | |
| 37 | // space in native points, including when a zoomed page is reloaded. | |
| 38 | const syncWindowControlInset = () => { | |
| 39 | document.documentElement.style.setProperty( | |
| 40 | "--desktop-window-controls-inset", | |
| 41 | `${90 / webFrame.getZoomFactor()}px`, | |
| 42 | ); | |
| 43 | }; | |
| 44 | window.addEventListener("DOMContentLoaded", syncWindowControlInset, { once: true }); | |
| 45 | window.addEventListener("resize", syncWindowControlInset); | |
| 46 | } | |
| 47 | ||
| 35 | 48 | function unwrapEnsureSshEnvironmentResult(result: unknown) { |
| 36 | 49 | if ( |
| 37 | 50 | typeof result === "object" && |
apps/desktop/src/window/DesktopWindow.test.ts+36| ... | ... | @@ -77,6 +77,7 @@ function makeFakeBrowserWindow() { |
| 77 | 77 | isDestroyed: vi.fn(() => false), |
| 78 | 78 | getURL: vi.fn(() => "t3code-dev://app/"), |
| 79 | 79 | getZoomLevel: vi.fn(() => zoomLevel), |
| 80 | getZoomFactor: vi.fn(() => 1.2 ** zoomLevel), | |
| 80 | 81 | setZoomLevel: vi.fn((level: number) => { |
| 81 | 82 | zoomLevel = level; |
| 82 | 83 | }), |
| ... | ... | @@ -118,6 +119,7 @@ function makeFakeBrowserWindow() { |
| 118 | 119 | setOpacity: vi.fn(), |
| 119 | 120 | setTitle: vi.fn(), |
| 120 | 121 | setTitleBarOverlay: vi.fn(), |
| 122 | setWindowButtonPosition: vi.fn(), | |
| 121 | 123 | show: vi.fn(), |
| 122 | 124 | webContents, |
| 123 | 125 | }; |
| ... | ... | @@ -136,6 +138,7 @@ function makeFakeBrowserWindow() { |
| 136 | 138 | reload: webContents.reload, |
| 137 | 139 | send: webContents.send, |
| 138 | 140 | setZoomLevel: webContents.setZoomLevel, |
| 141 | setWindowButtonPosition: window.setWindowButtonPosition, | |
| 139 | 142 | setBackgroundThrottling: webContents.setBackgroundThrottling, |
| 140 | 143 | setAutoHideCursor: window.setAutoHideCursor, |
| 141 | 144 | setFullScreen: window.setFullScreen, |
| ... | ... | @@ -742,6 +745,39 @@ describe("DesktopWindow", () => { |
| 742 | 745 | }), |
| 743 | 746 | ); |
| 744 | 747 | |
| 748 | it.effect("keeps macOS window buttons centered when zooming and leaving fullscreen", () => | |
| 749 | Effect.gen(function* () { | |
| 750 | const fakeWindow = makeFakeBrowserWindow(); | |
| 751 | const createCount = yield* Ref.make(0); | |
| 752 | const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none()); | |
| 753 | const layer = makeTestLayer({ window: fakeWindow.window, createCount, mainWindow }); | |
| 754 | ||
| 755 | yield* Effect.gen(function* () { | |
| 756 | const desktopWindow = yield* DesktopWindow.DesktopWindow; | |
| 757 | yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); | |
| 758 | ||
| 759 | for (const direction of ["in", "in", "out", "reset", "out"] as const) { | |
| 760 | yield* desktopWindow.zoomMain(direction); | |
| 761 | const position = fakeWindow.setWindowButtonPosition.mock.lastCall?.[0]; | |
| 762 | assert.isDefined(position); | |
| 763 | // The 14-point native buttons should share the zoomed 52px header's center. | |
| 764 | const headerCenter = 26 * fakeWindow.window.webContents.getZoomFactor(); | |
| 765 | assert.isAtMost(Math.abs(position.y + 7 - headerCenter), 0.5); | |
| 766 | assert.equal(position.x, 16); | |
| 767 | } | |
| 768 | ||
| 769 | fakeWindow.isFullScreen.mockReturnValue(true); | |
| 770 | fakeWindow.setWindowButtonPosition.mockClear(); | |
| 771 | yield* desktopWindow.zoomMain("reset"); | |
| 772 | assert.equal(fakeWindow.setWindowButtonPosition.mock.calls.length, 0); | |
| 773 | ||
| 774 | fakeWindow.isFullScreen.mockReturnValue(false); | |
| 775 | fakeWindow.windowListeners.get("leave-full-screen")?.(); | |
| 776 | assert.deepEqual(fakeWindow.setWindowButtonPosition.mock.lastCall, [{ x: 16, y: 19 }]); | |
| 777 | }).pipe(Effect.provide(layer)); | |
| 778 | }), | |
| 779 | ); | |
| 780 | ||
| 745 | 781 | it.effect("uses the persisted main window bounds when opening the window", () => |
| 746 | 782 | Effect.gen(function* () { |
| 747 | 783 | const fakeWindow = makeFakeBrowserWindow(); |
apps/desktop/src/window/DesktopWindow.ts+23-1| ... | ... | @@ -31,6 +31,22 @@ import * as ElectronApp from "../electron/ElectronApp.ts"; |
| 31 | 31 | import { makeQuitShortcutHandler } from "./QuitHold.ts"; |
| 32 | 32 | |
| 33 | 33 | const TITLEBAR_HEIGHT = 40; |
| 34 | // Matches --workspace-topbar-height in apps/web/src/index.css. Native macOS | |
| 35 | // buttons are 14 points tall and do not scale with the renderer's zoom. | |
| 36 | const MACOS_WORKSPACE_TOPBAR_HEIGHT = 52; | |
| 37 | const MACOS_WINDOW_BUTTON_RADIUS = 7; | |
| 38 | ||
| 39 | function syncMacosWindowButtons(window: Electron.BrowserWindow): void { | |
| 40 | if (window.isDestroyed() || window.isFullScreen()) return; | |
| 41 | window.setWindowButtonPosition({ | |
| 42 | x: 16, | |
| 43 | y: Math.round( | |
| 44 | (MACOS_WORKSPACE_TOPBAR_HEIGHT * window.webContents.getZoomFactor()) / 2 - | |
| 45 | MACOS_WINDOW_BUTTON_RADIUS, | |
| 46 | ), | |
| 47 | }); | |
| 48 | } | |
| 49 | ||
| 34 | 50 | const TITLEBAR_COLOR = "#01000000"; // #00000000 does not work correctly on Linux |
| 35 | 51 | const TITLEBAR_LIGHT_SYMBOL_COLOR = "#1f2937"; |
| 36 | 52 | const TITLEBAR_DARK_SYMBOL_COLOR = "#f8fafc"; |
| ... | ... | @@ -241,7 +257,10 @@ function getWindowTitleBarOptions( |
| 241 | 257 | if (platform === "darwin") { |
| 242 | 258 | return { |
| 243 | 259 | titleBarStyle: "hiddenInset", |
| 244 | trafficLightPosition: { x: 16, y: 18 }, | |
| 260 | trafficLightPosition: { | |
| 261 | x: 16, | |
| 262 | y: MACOS_WORKSPACE_TOPBAR_HEIGHT / 2 - MACOS_WINDOW_BUTTON_RADIUS, | |
| 263 | }, | |
| 245 | 264 | }; |
| 246 | 265 | } |
| 247 | 266 | |
| ... | ... | @@ -660,6 +679,7 @@ export const make = Effect.gen(function* () { |
| 660 | 679 | window.webContents.send(WINDOW_FULLSCREEN_STATE_CHANNEL, true); |
| 661 | 680 | }); |
| 662 | 681 | window.on("leave-full-screen", () => { |
| 682 | syncMacosWindowButtons(window); | |
| 663 | 683 | window.webContents.send(WINDOW_FULLSCREEN_STATE_CHANNEL, false); |
| 664 | 684 | }); |
| 665 | 685 | } |
| ... | ... | @@ -720,6 +740,7 @@ export const make = Effect.gen(function* () { |
| 720 | 740 | clearDevelopmentLoadRetry(); |
| 721 | 741 | developmentLoadRetryIndex = 0; |
| 722 | 742 | window.setTitle(environment.displayName); |
| 743 | if (environment.platform === "darwin") syncMacosWindowButtons(window); | |
| 723 | 744 | }); |
| 724 | 745 | window.webContents.on( |
| 725 | 746 | "did-fail-load", |
| ... | ... | @@ -990,6 +1011,7 @@ export const make = Effect.gen(function* () { |
| 990 | 1011 | webContents.setZoomLevel( |
| 991 | 1012 | direction === "reset" ? 0 : webContents.getZoomLevel() + (direction === "in" ? 0.5 : -0.5), |
| 992 | 1013 | ); |
| 1014 | if (environment.platform === "darwin") syncMacosWindowButtons(window.value); | |
| 993 | 1015 | // Chromium pushes the new level down to embedded guests, which would zoom |
| 994 | 1016 | // the previewed page along with the app UI. The preview browser keeps its |
| 995 | 1017 | // own zoom, so put each guest back where the preview left it. |
apps/web/src/components/AppSidebarLayout.tsx+1-1| ... | ... | @@ -46,7 +46,7 @@ import { |
| 46 | 46 | } from "./ui/sidebar"; |
| 47 | 47 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; |
| 48 | 48 | |
| 49 | const MACOS_TRAFFIC_LIGHTS_LEFT_INSET = "90px"; | |
| 49 | const MACOS_TRAFFIC_LIGHTS_LEFT_INSET = "var(--desktop-window-controls-inset, 90px)"; | |
| 50 | 50 | |
| 51 | 51 | function subscribeToViewportWidth(onChange: () => void): () => void { |
| 52 | 52 | window.addEventListener("resize", onChange); |
apps/web/src/components/sidebar/SidebarChrome.tsx+4-3| ... | ... | @@ -91,11 +91,12 @@ function SidebarBrand({ onBackdrop }: { onBackdrop: boolean }) { |
| 91 | 91 | )} |
| 92 | 92 | to="/" |
| 93 | 93 | > |
| 94 | <span className="inline-flex min-w-0 items-baseline gap-1"> | |
| 95 | <T3Wordmark aria-label="T3" className="h-2.5 w-auto shrink-0" /> | |
| 94 | {/* Center the visible capitals, without the font's ascender/descender space. */} | |
| 95 | <span className="inline-flex min-w-0 items-baseline gap-1 text-sm font-medium tracking-tight"> | |
| 96 | <T3Wordmark aria-label="T3" className="h-[1cap] w-auto shrink-0" /> | |
| 96 | 97 | <span |
| 97 | 98 | className={cn( |
| 98 | "truncate text-sm font-medium tracking-tight", | |
| 99 | "truncate [text-box:trim-both_cap_alphabetic]", | |
| 99 | 100 | onBackdrop ? "text-white/70" : "text-muted-foreground", |
| 100 | 101 | )} |
| 101 | 102 | > |