diff --git a/electron/webview-preload.js b/electron/webview-preload.js index 073a875c..6571d4e9 100644 --- a/electron/webview-preload.js +++ b/electron/webview-preload.js @@ -174,8 +174,21 @@ try { }; const onWheelCapture = (e) => { - // Pinch / ctrl+wheel stays with the page (chromium's in-page zoom). - if (e.ctrlKey || e.metaKey) return; + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + e.stopPropagation(); + const iw = window.innerWidth || 1; + const ih = window.innerHeight || 1; + try { + ipcRenderer.sendToHost('canvas-wheel-zoom', { + deltaY: e.deltaY, + deltaMode: e.deltaMode, + fracX: Math.max(0, Math.min(1, e.clientX / iw)), + fracY: Math.max(0, Math.min(1, e.clientY / ih)), + }); + } catch (_) {} + return; + } // Vertical-dominant scroll stays with the page. if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return; // Horizontal-dominant: defer to the page if anything inside can absorb diff --git a/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx b/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx index ee0b56b9..7fce9c06 100644 --- a/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx +++ b/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx @@ -303,6 +303,10 @@ const BrowserCard: React.FC = ({ // (the historical Windows mount segfault). Clear the crash-safety marker. if (isWindows) markWindowsWebviewSurvived(); wv.loadURL(targetUrl).catch(() => {}); + try { + (wv as any).setVisualZoomLevelLimits?.(1, 1); + (wv as any).setZoomFactor?.(1); + } catch (_) {} }; wv.addEventListener('dom-ready', doLoad, { once: true }); cleanups.push(() => wv.removeEventListener('dom-ready', doLoad)); @@ -323,6 +327,21 @@ const BrowserCard: React.FC = ({ setPasskeyDialogOpen(true); } else if (e?.channel === 'browser-dblclick') { onDoubleClickRef.current?.(browserId, 'browser'); + } else if (e?.channel === 'canvas-wheel-zoom') { + const payload = e.args?.[0] || {}; + const wvRect = wv.getBoundingClientRect(); + const fx = typeof payload.fracX === 'number' ? payload.fracX : 0.5; + const fy = typeof payload.fracY === 'number' ? payload.fracY : 0.5; + window.dispatchEvent( + new CustomEvent('openswarm:canvas-wheel-zoom', { + detail: { + deltaY: payload.deltaY ?? 0, + deltaMode: payload.deltaMode ?? 0, + clientX: wvRect.left + fx * wvRect.width, + clientY: wvRect.top + fy * wvRect.height, + }, + }), + ); } else if (e?.channel === 'canvas-wheel-pan') { // Plain wheel inside an unselected webview never bubbles out; the // preload forwards it here so the dashboard canvas can pan. diff --git a/frontend/src/app/pages/Dashboard/hooks/interaction/useCanvasControls.ts b/frontend/src/app/pages/Dashboard/hooks/interaction/useCanvasControls.ts index 66446189..6db17045 100644 --- a/frontend/src/app/pages/Dashboard/hooks/interaction/useCanvasControls.ts +++ b/frontend/src/app/pages/Dashboard/hooks/interaction/useCanvasControls.ts @@ -311,6 +311,23 @@ export function useCanvasControls(zoomSensitivity: number = 50, contentBounds?: el.addEventListener('wheel', onWheel, { passive: false }); + const onForwardedZoom = (e: Event) => { + const detail = (e as CustomEvent).detail || {}; + const dy = detail.deltaMode === 1 ? detail.deltaY * 40 : detail.deltaY; + const rect = el.getBoundingClientRect(); + if (inertiaFrameRef.current) { + cancelAnimationFrame(inertiaFrameRef.current); + inertiaFrameRef.current = null; + } + pendingZoomDy += dy; + pendingZoomCenter = { + cx: (detail.clientX ?? 0) - rect.left, + cy: (detail.clientY ?? 0) - rect.top, + }; + scheduleWheelFlush(); + }; + window.addEventListener('openswarm:canvas-wheel-zoom', onForwardedZoom); + // Plain wheel inside a webview can't bubble out either; the preload // forwards horizontal-dominant scrolls as a pan when the guest page // has nothing to scroll horizontally, plus middle-mouse drag deltas. @@ -330,6 +347,7 @@ export function useCanvasControls(zoomSensitivity: number = 50, contentBounds?: return () => { el.removeEventListener('wheel', onWheel); + window.removeEventListener('openswarm:canvas-wheel-zoom', onForwardedZoom); window.removeEventListener('openswarm:canvas-wheel-pan', onForwardedPan); if (wheelRafId != null) cancelAnimationFrame(wheelRafId); if (wheelIdleTimer != null) clearTimeout(wheelIdleTimer);