mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-24 21:42:22 +02:00
[eric] dashboard: scroll zooms the canvas over any card (Google Maps); click into a card to scroll its content
This commit is contained in:
+43
-11
@@ -163,6 +163,15 @@ try {
|
||||
});
|
||||
} catch (_) {}
|
||||
|
||||
// Scroll-focus (Google Maps model): plain wheel zooms the canvas over this card UNLESS the user
|
||||
// has clicked INTO it, in which case plain wheel scrolls the page/app content. Host pushes the flag.
|
||||
let scrollFocused = false;
|
||||
try {
|
||||
ipcRenderer.on('openswarm:set-scroll-focus', (_event, payload) => {
|
||||
scrollFocused = !!(payload && payload.focused);
|
||||
});
|
||||
} catch (_) {}
|
||||
|
||||
// First in-guest mousedown tells the host to activate interact mode. Never
|
||||
// preventDefault so the click still reaches the app (Minecraft etc).
|
||||
const onMouseDownNotify = (e) => {
|
||||
@@ -225,20 +234,43 @@ try {
|
||||
} catch (_) {}
|
||||
return;
|
||||
}
|
||||
if (isInteractive) 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
|
||||
// it; otherwise forward to the host as a canvas pan.
|
||||
if (pageCanScrollX(e.target, e.deltaX)) return;
|
||||
// Focused (clicked-into) card, or an interactive app: wheel stays with the page/app content.
|
||||
if (isInteractive || scrollFocused) {
|
||||
// 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 it; otherwise forward as a canvas pan.
|
||||
if (pageCanScrollX(e.target, e.deltaX)) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
try {
|
||||
ipcRenderer.sendToHost('canvas-wheel-pan', {
|
||||
deltaX: e.deltaX,
|
||||
deltaY: e.deltaY,
|
||||
deltaMode: e.deltaMode,
|
||||
});
|
||||
} catch (_) {}
|
||||
return;
|
||||
}
|
||||
// Not focused: navigate the CANVAS while hovering the card (Google Maps). Horizontal-dominant swipe pans; vertical scroll zooms at the cursor.
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
try {
|
||||
ipcRenderer.sendToHost('canvas-wheel-pan', {
|
||||
deltaX: e.deltaX,
|
||||
deltaY: e.deltaY,
|
||||
deltaMode: e.deltaMode,
|
||||
});
|
||||
if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
|
||||
ipcRenderer.sendToHost('canvas-wheel-pan', {
|
||||
deltaX: e.deltaX,
|
||||
deltaY: e.deltaY,
|
||||
deltaMode: e.deltaMode,
|
||||
});
|
||||
} else {
|
||||
const iw2 = window.innerWidth || 1;
|
||||
const ih2 = window.innerHeight || 1;
|
||||
ipcRenderer.sendToHost('canvas-wheel-zoom', {
|
||||
deltaY: e.deltaY,
|
||||
deltaMode: e.deltaMode,
|
||||
fracX: Math.max(0, Math.min(1, e.clientX / iw2)),
|
||||
fracY: Math.max(0, Math.min(1, e.clientY / ih2)),
|
||||
});
|
||||
}
|
||||
} catch (_) {}
|
||||
};
|
||||
// Listen on both window and document in capture phase so we run before any
|
||||
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
type BrowserWebview,
|
||||
} from '@/shared/browserRegistry';
|
||||
import { setLastInteractedBrowser } from '@/shared/browserFocus';
|
||||
import { onScrollFocusChange, getScrollFocusedCard } from '@/shared/cardScrollFocus';
|
||||
import { registerCapsuleForRestore } from '@/shared/browserStateCapsule';
|
||||
import BrowserFindBar from './BrowserFindBar';
|
||||
import { useBrowserActivity } from '@/shared/useBrowserActivity';
|
||||
@@ -271,6 +272,19 @@ const BrowserCard: React.FC<Props> = ({
|
||||
setRegistryActiveTab(browserId, activeTabId);
|
||||
}, [browserId, activeTabId]);
|
||||
|
||||
// Tell the (out-of-process) guest whether plain wheel should scroll the page or zoom the canvas.
|
||||
// Focused (clicked-into) = scroll the page; otherwise scroll-to-zoom works while hovering it (Google Maps).
|
||||
useEffect(() => {
|
||||
const push = (focusedId: string | null) => {
|
||||
const focused = focusedId === browserId;
|
||||
for (const wv of webviewMap.current.values()) {
|
||||
try { wv.send?.('openswarm:set-scroll-focus', { focused }); } catch (_e) { /* guest not ready */ }
|
||||
}
|
||||
};
|
||||
push(getScrollFocusedCard());
|
||||
return onScrollFocusChange(push);
|
||||
}, [browserId, activeTabId]);
|
||||
|
||||
// Open the find bar when AppShell routes a Ctrl/Cmd+F to this browser; re-trigger re-focuses the input.
|
||||
useEffect(() => {
|
||||
const onFind = (e: Event) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useCallback, useRef, useEffect, useMemo, RefObject } from 'react';
|
||||
import { setCanvasInteractionActive } from '@/shared/canvasInteractionState';
|
||||
import { getLastInteractedBrowser } from '@/shared/browserFocus';
|
||||
import { getScrollFocusedCard } from '@/shared/cardScrollFocus';
|
||||
import { getWebview } from '@/shared/browserRegistry';
|
||||
import { applyBrowserZoom } from '@/shared/browserZoom';
|
||||
|
||||
@@ -264,6 +265,13 @@ export function useCanvasControls(zoomSensitivity: number = 50, contentBounds?:
|
||||
}
|
||||
|
||||
if (cls === 'scrollable' && !isModifierWheel) {
|
||||
// Google Maps model: plain scroll zooms the canvas over ANY card (chat, app, scheduled task) UNLESS you've clicked INTO that card to read it. So a scrollable child only eats the wheel when its card is the scroll-focused one; otherwise fall through to canvas zoom.
|
||||
const cardEl = target.closest('[data-select-id]');
|
||||
const cardId = cardEl?.getAttribute('data-select-id') ?? null;
|
||||
if (!cardId || cardId !== getScrollFocusedCard()) {
|
||||
target = target.parentElement;
|
||||
continue;
|
||||
}
|
||||
// Re-read scrollHeight/clientHeight; cached decision is structural, scroll position is dynamic.
|
||||
const canScrollY = target.scrollHeight > target.clientHeight;
|
||||
const canScrollX = target.scrollWidth > target.clientWidth;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useAppDispatch } from '@/shared/hooks';
|
||||
import { store } from '@/shared/state/store';
|
||||
import { collapseSession, expandSession } from '@/shared/state/agentsSlice';
|
||||
import { bringToFront } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { setScrollFocusedCard } from '@/shared/cardScrollFocus';
|
||||
import type { CardType, useDashboardSelection } from '../state/useDashboardSelection';
|
||||
import type { useCanvasControls } from './useCanvasControls';
|
||||
|
||||
@@ -52,6 +53,8 @@ export function useDashboardInteractions({
|
||||
|
||||
selection.selectCard(id, type, false);
|
||||
dispatch(bringToFront({ id, type }));
|
||||
// Clicking INTO a card focuses it for scrolling: plain wheel now reads its content instead of zooming the canvas (Google Maps model). Clicking blank canvas clears it (below).
|
||||
setScrollFocusedCard(id);
|
||||
|
||||
// The Workflows window is an app you click around inside, not a card you re-center every tap. Single-click only raises + selects it; double-click still zoom-to-fits (handleCardDoubleClick). Without this, clicking any button inside it yanked the canvas into a re-zoom.
|
||||
if (type === 'workflows-hub' || type === 'workflows-monitor') return;
|
||||
@@ -109,6 +112,8 @@ export function useDashboardInteractions({
|
||||
if (agentDriven) return;
|
||||
selection.selectCard(browserId, 'browser', false);
|
||||
dispatch(bringToFront({ id: browserId, type: 'browser' }));
|
||||
// Clicking inside a browser's page focuses it: plain wheel now scrolls the page instead of zooming the canvas.
|
||||
setScrollFocusedCard(browserId);
|
||||
};
|
||||
window.addEventListener('openswarm:browser-guest-select', onGuestSelect);
|
||||
return () => window.removeEventListener('openswarm:browser-guest-select', onGuestSelect);
|
||||
@@ -130,6 +135,9 @@ export function useDashboardInteractions({
|
||||
if (e.button !== 0) return;
|
||||
if (isCardTarget(e.target, e.currentTarget)) return;
|
||||
|
||||
// Clicking blank canvas leaves every card: plain scroll zooms the canvas again (Google Maps model).
|
||||
setScrollFocusedCard(null);
|
||||
|
||||
// Canvas click, drop any lingering input focus so arrow-key nav works immediately without the user having to press Escape first.
|
||||
const active = document.activeElement as HTMLElement | null;
|
||||
const activeTag = active?.tagName;
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface BrowserWebview extends HTMLElement {
|
||||
isLoading: () => boolean;
|
||||
// Optional: present on real Electron webviews; the iframe fallback lacks it, callers must ?.() it.
|
||||
isCurrentlyAudible?: () => boolean;
|
||||
send?: (channel: string, ...args: any[]) => void;
|
||||
capturePage: (rect?: { x: number; y: number; width: number; height: number }) => Promise<ElectronNativeImage>;
|
||||
executeJavaScript: (code: string) => Promise<any>;
|
||||
sendInputEvent: (event: any) => void;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// The card you've clicked INTO, so plain scroll reads its content (chat transcript, web page)
|
||||
// while scroll everywhere else zooms the canvas (Google Maps model). Imperative + read on the
|
||||
// wheel handler so no re-render; cleared when you click blank canvas.
|
||||
let scrollFocusedCardId: string | null = null;
|
||||
type Listener = (id: string | null) => void;
|
||||
const listeners = new Set<Listener>();
|
||||
|
||||
export function setScrollFocusedCard(id: string | null): void {
|
||||
if (scrollFocusedCardId === id) return;
|
||||
scrollFocusedCardId = id;
|
||||
for (const l of listeners) l(id);
|
||||
}
|
||||
|
||||
export function getScrollFocusedCard(): string | null {
|
||||
return scrollFocusedCardId;
|
||||
}
|
||||
|
||||
// Browser cards subscribe so they can tell their (out-of-process) guest whether plain wheel should scroll the page or zoom the canvas.
|
||||
export function onScrollFocusChange(l: Listener): () => void {
|
||||
listeners.add(l);
|
||||
return () => { listeners.delete(l); };
|
||||
}
|
||||
Reference in New Issue
Block a user