Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/desktop/src/app/DesktopLifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ describe("DesktopLifecycle", () => {
handleBackendNotReady: Effect.void,
flushMainWindowBounds: Effect.void,
dispatchMenuAction: () => Effect.void,
zoomMain: () => Effect.void,
syncAppearance: Effect.void,
});

Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/backend/DesktopBackendPool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ function makePoolLayer(
handleBackendNotReady: Effect.void,
flushMainWindowBounds: Effect.void,
dispatchMenuAction: () => Effect.die("unexpected menu action"),
zoomMain: () => Effect.die("unexpected zoom"),
syncAppearance: Effect.void,
} satisfies DesktopWindow.DesktopWindow["Service"]),
),
Expand Down
28 changes: 12 additions & 16 deletions apps/desktop/src/window/DesktopApplicationMenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,6 @@ const makeDesktopWindowLayer = (selectedAction: Deferred.Deferred<string>) =>
handleBackendNotReady: Effect.void,
flushMainWindowBounds: Effect.void,
dispatchMenuAction: (action) => Deferred.succeed(selectedAction, action).pipe(Effect.asVoid),
zoomMain: (direction) =>
Deferred.succeed(selectedAction, `zoom-${direction}`).pipe(Effect.asVoid),
syncAppearance: Effect.void,
} satisfies DesktopWindow.DesktopWindow["Service"]);

Expand Down Expand Up @@ -146,10 +144,7 @@ describe("DesktopApplicationMenu", () => {
}),
);

// Zoom must route through DesktopWindow.zoomMain instead of the Electron
// zoom roles: the roles zoom whichever webContents has focus, which breaks
// app zoom while an embedded preview WebContentsView holds focus.
it.effect("routes View menu zoom to the main window instead of zoom roles", () =>
it.effect("uses focused-page zoom roles in the View menu", () =>
Effect.gen(function* () {
const selectedAction = yield* Deferred.make<string>();
const applicationMenuTemplate =
Expand All @@ -164,19 +159,20 @@ describe("DesktopApplicationMenu", () => {
throw new Error("Expected View menu submenu to be an array.");
}

assert.isUndefined(
viewMenu.submenu.find((item) => item.role?.toLowerCase().includes("zoom")),
const resetZoom = viewMenu.submenu.find((item) => item.role === "resetZoom");
const zoomIn = viewMenu.submenu.find(
(item) => item.role === "zoomIn" && item.visible !== false,
);

const zoomIn = viewMenu.submenu.find((item) => item.label === "Zoom In");
const alternateZoomIn = viewMenu.submenu.find(
(item) => item.role === "zoomIn" && item.visible === false,
);
const zoomOut = viewMenu.submenu.find((item) => item.role === "zoomOut");
assert.isDefined(resetZoom);
assert.isDefined(zoomIn);
assert.isDefined(alternateZoomIn);
assert.isDefined(zoomOut);
assert.equal(zoomIn.accelerator, "CmdOrCtrl+=");
if (typeof zoomIn.click !== "function") {
throw new Error("Expected Zoom In menu item to have a click handler.");
}

zoomIn.click({} as Electron.MenuItem, {} as Electron.BrowserWindow, {} as KeyboardEvent);
assert.equal(yield* Deferred.await(selectedAction), "zoom-in");
assert.equal(alternateZoomIn.accelerator, "CmdOrCtrl+Plus");
}),
);
});
29 changes: 4 additions & 25 deletions apps/desktop/src/window/DesktopApplicationMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,6 @@ const dispatchMenuAction = Effect.fn("desktop.menu.dispatchMenuAction")(function
yield* desktopWindow.dispatchMenuAction(action);
});

const zoomMainWindow = Effect.fn("desktop.menu.zoomMainWindow")(function* (
direction: DesktopWindow.MainWindowZoomDirection,
): Effect.fn.Return<void, never, DesktopWindow.DesktopWindow> {
const desktopWindow = yield* DesktopWindow.DesktopWindow;
yield* desktopWindow.zoomMain(direction);
});

const checkForUpdatesFromMenu = Effect.gen(function* () {
const updates = yield* DesktopUpdates.DesktopUpdates;
const electronDialog = yield* ElectronDialog.ElectronDialog;
Expand Down Expand Up @@ -134,9 +127,6 @@ export const make = Effect.gen(function* () {
const settingsClick = () => {
runMenuEffect("open-settings", dispatchMenuAction("open-settings"));
};
const zoomClick = (direction: DesktopWindow.MainWindowZoomDirection) => () => {
runMenuEffect(`zoom-${direction}`, zoomMainWindow(direction));
};
const template: Electron.MenuItemConstructorOptions[] = [];

if (environment.platform === "darwin") {
Expand Down Expand Up @@ -191,21 +181,10 @@ export const make = Effect.gen(function* () {
{ role: "forceReload" },
{ role: "toggleDevTools" },
{ type: "separator" },
/*
Not the zoom roles: those act on the focused webContents, so with
an embedded preview WebContentsView focused they zoom the guest
page and the app UI appears stuck. These always zoom the main
window (see DesktopWindow.zoomMain).
*/
{ label: "Actual Size", accelerator: "CmdOrCtrl+0", click: zoomClick("reset") },
{ label: "Zoom In", accelerator: "CmdOrCtrl+=", click: zoomClick("in") },
{
label: "Zoom In",
accelerator: "CmdOrCtrl+Plus",
visible: false,
click: zoomClick("in"),
},
{ label: "Zoom Out", accelerator: "CmdOrCtrl+-", click: zoomClick("out") },
{ role: "resetZoom" },
{ role: "zoomIn", accelerator: "CmdOrCtrl+=" },
{ role: "zoomIn", accelerator: "CmdOrCtrl+Plus", visible: false },
{ role: "zoomOut" },
Comment thread
SunkenInTime marked this conversation as resolved.
Outdated
{ type: "separator" },
{ role: "togglefullscreen" },
],
Expand Down
29 changes: 29 additions & 0 deletions apps/desktop/src/window/DesktopWindow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,35 @@ describe("DesktopWindow", () => {
);
});

it.effect("starts preview webviews at 100% independently of the app zoom", () =>
Effect.gen(function* () {
const fakeWindow = makeFakeBrowserWindow();
const createCount = yield* Ref.make(0);
const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
const layer = makeTestLayer({
window: fakeWindow.window,
createCount,
mainWindow,
});

yield* Effect.gen(function* () {
const desktopWindow = yield* DesktopWindow.DesktopWindow;
yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));

const willAttachWebview = fakeWindow.webContentsListeners.get("will-attach-webview");
if (!willAttachWebview) {
return yield* Effect.die("will-attach-webview listener was not registered");
}
const webPreferences = { zoomFactor: 0.8 } as Electron.WebPreferences;
willAttachWebview({ preventDefault: vi.fn() }, webPreferences, {
partition: "persist:t3code-preview-test",
});

assert.equal(webPreferences.zoomFactor, 1);
}).pipe(Effect.provide(layer));
}),
);

it.effect("does not open a development window until the backend is ready", () =>
Effect.gen(function* () {
const fakeWindow = makeFakeBrowserWindow();
Expand Down
23 changes: 3 additions & 20 deletions apps/desktop/src/window/DesktopWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ export type DesktopWindowError =
| ElectronWindow.ElectronWindowCreateError
| PreviewManager.PreviewManagerError;

export type MainWindowZoomDirection = "in" | "out" | "reset";

export class DesktopWindow extends Context.Service<
DesktopWindow,
{
Expand All @@ -89,12 +87,6 @@ export class DesktopWindow extends Context.Service<
readonly handleBackendNotReady: Effect.Effect<void>;
readonly flushMainWindowBounds: Effect.Effect<void>;
readonly dispatchMenuAction: (action: string) => Effect.Effect<void, DesktopWindowError>;
// Zooms the main window's own webContents. The Electron `zoomIn`/`zoomOut`
// menu roles act on whichever webContents has keyboard focus, so with an
// embedded preview WebContentsView (or DevTools) focused they zoom the
// guest page instead of the app UI. The menu routes here to always target
// the main window.
readonly zoomMain: (direction: MainWindowZoomDirection) => Effect.Effect<void>;
readonly syncAppearance: Effect.Effect<void>;
}
>()("@t3tools/desktop/window/DesktopWindow") {}
Expand Down Expand Up @@ -457,6 +449,9 @@ export const make = Effect.gen(function* () {
webPreferences.nodeIntegration = false;
webPreferences.nodeIntegrationInSubFrames = false;
webPreferences.contextIsolation = false;
// Electron otherwise copies the app renderer's current zoom into each
// new guest, coupling preview zoom to the surrounding T3 UI.
webPreferences.zoomFactor = 1;
});

window.webContents.on("context-menu", (event, params) => {
Expand Down Expand Up @@ -844,18 +839,6 @@ export const make = Effect.gen(function* () {

send();
}),
zoomMain: Effect.fn("desktop.window.zoomMain")(function* (direction) {
yield* Effect.annotateCurrentSpan({ direction });
const window = yield* focusedMainWindow;
if (Option.isNone(window) || window.value.isDestroyed()) {
return;
}
const webContents = window.value.webContents;
// Same step size as the Electron zoomIn/zoomOut menu roles.
webContents.setZoomLevel(
direction === "reset" ? 0 : webContents.getZoomLevel() + (direction === "in" ? 0.5 : -0.5),
);
}),
syncAppearance: Effect.gen(function* () {
const shouldUseDarkColors = yield* electronTheme.shouldUseDarkColors;
yield* electronWindow.syncAllAppearance((window) =>
Expand Down
4 changes: 4 additions & 0 deletions docs/user/keybindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ characters and uses SQLite's ASCII case-insensitive matching.
The full command list and the current defaults are shown in **Settings** → **Keybindings**, which
always matches the build you are running. Use that rather than a copied list.

On desktop, app UI zoom and integrated-browser zoom are independent. The standard zoom shortcuts
target whichever surface has focus, and the preview's three-dot menu changes only that browser tab.
New browser previews start at 100% regardless of the app UI's current zoom.

Note that `chat.new` and `chat.newLocal` both create a thread through the same path. A new thread
inherits the project you were in, along with model and mode selections. Branch, worktree, and
environment mode always come from your configured defaults, not from the thread you were looking
Expand Down
Loading