Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
24 changes: 16 additions & 8 deletions apps/desktop/src/preview/Manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ type TestDisplayMediaHandler = (

interface TestHostWebContents {
readonly id: number;
readonly getZoomFactor: () => number;
readonly mainFrame: { readonly frameTreeNodeId: number };
readonly executeJavaScript: ReturnType<typeof vi.fn>;
readonly isDestroyed: () => boolean;
Expand All @@ -312,6 +313,7 @@ const makeTestHostWebContents = (): TestHostWebContents => {
let handler: TestDisplayMediaHandler | undefined;
return {
id: 7,
getZoomFactor: () => 1,
mainFrame: { frameTreeNodeId: 7 },
executeJavaScript: vi.fn(async () => true),
isDestroyed: () => false,
Expand Down Expand Up @@ -560,7 +562,7 @@ describe("PreviewManager", () => {
Effect.gen(function* () {
const preview = makeFaviconWebContents();
const sendInputEvent = vi.fn();
const hostWebContents = { sendInputEvent };
const hostWebContents = { sendInputEvent, getZoomFactor: () => 1 };
Object.assign(preview.webContents, { hostWebContents });
fromId.mockReturnValue(preview.webContents);
getFocusedWebContents.mockReturnValue(preview.webContents as never);
Expand Down Expand Up @@ -1387,13 +1389,15 @@ describe("PreviewManager", () => {
effectIt.effect("keeps the tab's own zoom instead of the guest's reported zoom", () =>
withManager((manager) =>
Effect.gen(function* () {
const hostWebContents = { getZoomFactor: () => 1.44 };
let effectiveZoom = 0.9;
let zoomReadable = true;
let url = "https://example.com";
const listeners = new Map<string, (...args: unknown[]) => void>();
const setZoomFactor = vi.fn();
fromId.mockReturnValue({
id: 42,
hostWebContents,
isDestroyed: () => false,
getType: () => "webview",
getURL: () => url,
Expand Down Expand Up @@ -1434,7 +1438,7 @@ describe("PreviewManager", () => {
yield* manager.registerWebview("tab_zoom", 42);

expect(states.at(-1)?.zoomFactor).toBe(1);
expect(setZoomFactor).toHaveBeenCalledWith(1);
expect(setZoomFactor).toHaveBeenCalledWith(1.44);

// An app zoom leaves the guest reporting the inherited level. Navigating
// must not adopt it as the preview's zoom.
Expand All @@ -1452,7 +1456,7 @@ describe("PreviewManager", () => {

// Only the preview's own zoom controls move it.
yield* manager.zoomIn("tab_zoom");
expect(setZoomFactor).toHaveBeenCalledWith(1.1);
expect(setZoomFactor).toHaveBeenCalledWith(1.1 * 1.44);
expect(states.at(-1)?.zoomFactor).toBe(1.1);

zoomReadable = false;
Expand All @@ -1464,6 +1468,7 @@ describe("PreviewManager", () => {
const replacementSetZoomFactor = vi.fn();
fromId.mockReturnValue({
id: 43,
hostWebContents,
isDestroyed: () => false,
getType: () => "webview",
getURL: () => url,
Expand Down Expand Up @@ -1491,7 +1496,7 @@ describe("PreviewManager", () => {

yield* manager.registerWebview("tab_zoom", 43);

expect(replacementSetZoomFactor).toHaveBeenCalledWith(1.1);
expect(replacementSetZoomFactor).toHaveBeenCalledWith(1.1 * 1.44);
expect(states.at(-1)?.zoomFactor).toBe(1.1);
}),
),
Expand All @@ -1502,9 +1507,11 @@ describe("PreviewManager", () => {
effectIt.effect("re-applies each tab's own zoom when the app window zooms", () =>
withManager((manager) =>
Effect.gen(function* () {
let hostZoom = 1;
const setZoomFactor = vi.fn();
fromId.mockReturnValue({
id: 42,
hostWebContents: { getZoomFactor: () => hostZoom },
isDestroyed: () => false,
getType: () => "webview",
getURL: () => "https://example.com",
Expand Down Expand Up @@ -1535,10 +1542,11 @@ describe("PreviewManager", () => {
yield* manager.zoomIn("tab_reapply");
setZoomFactor.mockClear();

yield* manager.reapplyZoom();

expect(setZoomFactor).toHaveBeenCalledTimes(1);
expect(setZoomFactor).toHaveBeenCalledWith(1.1);
for (const zoom of [1.44, 0.8, 1]) {
hostZoom = zoom;
yield* manager.reapplyZoom();
expect(setZoomFactor).toHaveBeenLastCalledWith(1.1 * hostZoom);
}
}),
),
);
Expand Down
21 changes: 8 additions & 13 deletions apps/desktop/src/preview/Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1020,7 +1020,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
const wc = webContents.fromId(tab.webContentsId);
if (!wc || wc.isDestroyed()) return;
yield* attempt({ operation: "assertTabZoom", tabId, webContentsId: wc.id }, () =>
wc.setZoomFactor(tab.zoomFactor),
wc.setZoomFactor(tab.zoomFactor * (wc.hostWebContents?.getZoomFactor() ?? 1)),
).pipe(Effect.ignore);
});

Expand Down Expand Up @@ -2282,12 +2282,10 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
) {
return yield* new PreviewTabNotFoundError({ tabId });
}
// Always assert the tab's own zoom rather than reading the guest's: a guest
// attaching while the app UI is zoomed starts at the embedder's inherited
// zoom level, which is not the preview's zoom. Done before the guest is
// published so it never paints a frame at the inherited zoom.
// The webview's box is in host CSS pixels. Include host zoom so the guest's
// CSS viewport matches the requested size, even when the app UI is zoomed.
yield* attempt({ operation: "registerWebview.restoreZoomFactor", tabId, webContentsId }, () =>
wc.setZoomFactor(currentTab.zoomFactor),
wc.setZoomFactor(currentTab.zoomFactor * (wc.hostWebContents?.getZoomFactor() ?? 1)),
);
// A replacement guest attaches unmuted, so reassert the tab's mute before it
// is published rather than letting it emit audio the user already silenced.
Expand Down Expand Up @@ -2704,10 +2702,8 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
});

/**
* Chromium hands every guest `<webview>` the embedder's zoom level, so zooming
* the app UI drags the previewed page along with it. The preview browser owns
* its own zoom factor, so re-assert it on each attached guest whenever the main
* window's zoom changes (see DesktopWindow.zoomMain).
* Re-assert each tab's zoom relative to the host after app zoom changes
* (see DesktopWindow.zoomMain), keeping its CSS viewport stable.
*/
const reapplyZoom = Effect.fn("PreviewManager.reapplyZoom")(function* () {
const tabIds = Array.from((yield* SynchronizedRef.get(tabsRef)).keys());
Expand All @@ -2726,7 +2722,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
const wc = webContents.fromId(tab.webContentsId);
if (wc && !wc.isDestroyed()) {
yield* attempt({ operation: "applyZoom", tabId, webContentsId: wc.id }, () =>
wc.setZoomFactor(next),
wc.setZoomFactor(next * (wc.hostWebContents?.getZoomFactor() ?? 1)),
);
}
}
Expand Down Expand Up @@ -4971,8 +4967,7 @@ export class PreviewManager extends Context.Service<
readonly zoomIn: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
readonly zoomOut: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
readonly resetZoom: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
// Re-applies every attached guest's own zoom factor, undoing the zoom level
// Chromium inherits from the embedder when the app UI zooms.
// Re-applies each tab's zoom relative to its host after app UI zoom changes.
readonly reapplyZoom: () => Effect.Effect<void>;
readonly hardReload: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
readonly setColorScheme: (
Expand Down
Loading