Skip to content
Open
Show file tree
Hide file tree
Changes from all 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/app/src/app/lib/desktop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ export type BrowserStatePayload = {

declare global {
interface Window {
__OPENWORK_ZOOM_FACTOR__?: number;
__OPENWORK_ELECTRON__?: {
invokeDesktop?: (command: string, ...args: unknown[]) => Promise<unknown>;
shell?: {
Expand Down
38 changes: 14 additions & 24 deletions apps/app/src/react-app/domains/session/panel/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,20 @@ export function getElectronBrowser() {
return window.__OPENWORK_ELECTRON__?.browser ?? null;
}

// The renderer uses Electron's webContents.setZoomFactor, which scales the page
// so getBoundingClientRect() / innerWidth report CSS pixels DIVIDED by the zoom
// factor (e.g. at zoom 1.5 a 1180 DIP window measures ~786). WebContentsView
// bounds, however, are in window device-independent pixels. So renderer rects
// must be multiplied back by the zoom factor to land in the native coordinate
// space. At zoom = 1 this is the identity.
function getZoomFactor() {
const zoom = window.__OPENWORK_ZOOM_FACTOR__;
return typeof zoom === "number" && zoom > 0 ? zoom : 1;
}

// Bounds and points are sent to the main process in the page's CSS pixels.
// The main process converts them to window DIPs using the authoritative
// webContents.getZoomFactor() at apply time (see scaleBoundsForZoom in
// apps/desktop/electron/main.mjs). The renderer must NOT pre-scale: it has no
// reliable view of the zoom factor (native View menu zoom roles change it
// without notifying the page).
export function getNativeMenuPoint(
el: HTMLElement | null,
point?: { clientX: number; clientY: number },
) {
const zoom = getZoomFactor();

if (point) {
return {
x: Math.round(point.clientX * zoom),
y: Math.round(point.clientY * zoom),
x: Math.round(point.clientX),
y: Math.round(point.clientY),
};
}

Expand All @@ -39,24 +32,21 @@ export function getNativeMenuPoint(
const rect = el.getBoundingClientRect();

return {
x: Math.round((rect.left + 8) * zoom),
y: Math.round((rect.bottom + 4) * zoom),
x: Math.round(rect.left + 8),
y: Math.round(rect.bottom + 4),
};
}

export function computeBounds(el: HTMLElement) {
// Scale each edge to native DIP, then derive width/height from the rounded
// edges so the far edge has no sub-pixel seam at any zoom level.
const rect = el.getBoundingClientRect();
const zoom = getZoomFactor();
const x = Math.round(rect.x * zoom);
const y = Math.round(rect.y * zoom);
const x = Math.round(rect.x);
const y = Math.round(rect.y);

return {
x,
y,
width: Math.round(rect.right * zoom) - x,
height: Math.round(rect.bottom * zoom) - y,
width: Math.round(rect.right) - x,
height: Math.round(rect.bottom) - y,
};
}

Expand Down
4 changes: 0 additions & 4 deletions apps/app/src/react-app/shell/font-zoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ export function useDesktopFontZoomBehavior() {
const next = normalizeFontZoom(value);
persistFontZoom(window.localStorage, next);

// Keep the current desktop zoom available so native WebContentsView bounds
// can be converted from renderer CSS pixels to contentView coordinates.
window.__OPENWORK_ZOOM_FACTOR__ = next;

void setDesktopZoomFactor(next)
.then((applied) => {
if (applied) {
Expand Down
39 changes: 35 additions & 4 deletions apps/desktop/electron/main.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,37 @@ function isHttpUrl(url) {
}
}

// Renderer-supplied geometry is in the page's CSS pixels. WebContentsView
// bounds are in window DIPs: at zoom factor Z, DIP = cssPx * Z. The renderer
// cannot reliably know Z (the native View menu zoom roles change it without
// notifying the page, which is how stale-zoom distortion bugs crept in), so
// the main process owns this conversion and reads the factor at apply time.
function browserZoomFactor() {
const zoom = mainWindow?.webContents?.getZoomFactor?.();
return Number.isFinite(zoom) && zoom > 0 ? zoom : 1;
}

function scaleBoundsForZoom(bounds) {
const zoom = browserZoomFactor();
if (zoom === 1) return bounds;
// Scale each edge, then derive width/height from the rounded edges so the
// far edge has no sub-pixel seam at any zoom level.
const x = Math.round(bounds.x * zoom);
const y = Math.round(bounds.y * zoom);
return {
x,
y,
width: Math.round((bounds.x + bounds.width) * zoom) - x,
height: Math.round((bounds.y + bounds.height) * zoom) - y,
};
}

function scalePointForZoom(point) {
const zoom = browserZoomFactor();
if (zoom === 1 || !point || typeof point !== "object") return point;
return { x: Number(point.x) * zoom, y: Number(point.y) * zoom };
}

function normalizeMenuOverlayPoint(point) {
if (!point || typeof point !== "object") {
return { x: 0, y: 0 };
Expand Down Expand Up @@ -1051,7 +1082,7 @@ function tabMenuRequest(tab, point) {
source: "tab",
tabId: tab.tabId,
url,
bounds: menuOverlayBounds(normalizeMenuOverlayPoint(point)),
bounds: menuOverlayBounds(normalizeMenuOverlayPoint(scalePointForZoom(point))),
items: [
{ id: "copy-url", label: "Copy URL", iconName: "copy", disabled: !url },
{ id: "open-external", label: "Open in Browser", iconName: "external", disabled: !(url && isHttpUrl(url)) },
Expand Down Expand Up @@ -1198,7 +1229,7 @@ function attachActiveBrowserView() {
mainWindow.contentView.addChildView(view);
}
if (lastBrowserBounds && lastBrowserBounds.width > 0 && lastBrowserBounds.height > 0) {
view.setBounds(lastBrowserBounds);
view.setBounds(scaleBoundsForZoom(lastBrowserBounds));
}
}

Expand Down Expand Up @@ -1297,7 +1328,7 @@ function attachBrowserView(bounds, { preloadDefault = false, ensureTab = false }
const view = getActiveBrowserView();
attachActiveBrowserView();
if (bounds.width > 0 && bounds.height > 0) {
view?.setBounds(bounds);
view?.setBounds(scaleBoundsForZoom(bounds));
}
const url = view?.webContents.getURL();
if (preloadDefault && (!url || url === "about:blank")) {
Expand Down Expand Up @@ -3241,7 +3272,7 @@ ipcMain.handle("openwork:browser:bounds", (_event, bounds) => {
lastBrowserBounds = bounds;
const view = getActiveBrowserView();
if (view && browserViewVisible && bounds.width > 0 && bounds.height > 0) {
view.setBounds(bounds);
view.setBounds(scaleBoundsForZoom(bounds));
}
});
ipcMain.handle("openwork:browser:state", () => browserStatePayload());
Expand Down
Loading