mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-13 13:17:40 +02:00
[eric] settings: mouse wheel can scroll instead of zoom on the canvas
This commit is contained in:
@@ -43,6 +43,11 @@ class AppSettings(BaseModel):
|
||||
default_max_turns: Optional[int] = None
|
||||
default_thinking_level: Literal["off", "low", "medium", "high", "auto"] = "auto"
|
||||
zoom_sensitivity: float = 50.0
|
||||
# What a plain MOUSE wheel does on the canvas. "zoom" is the Google-Maps model we ship; "scroll"
|
||||
# suits people who expect a wheel to move the page, and swaps the pair so cmd/ctrl+wheel zooms
|
||||
# instead. A trackpad two-finger scroll pans either way, since that gesture is already a pan
|
||||
# everywhere else on the machine.
|
||||
mouse_wheel_action: Literal["zoom", "scroll"] = "zoom"
|
||||
# Root font-size multiplier (0.9/1/1.1/1.2 from Settings > Interface); the whole rem type scale rides it.
|
||||
ui_font_scale: float = 1.0
|
||||
theme: str = "light"
|
||||
|
||||
@@ -50,7 +50,12 @@ export interface ContentBounds {
|
||||
maxY: number;
|
||||
}
|
||||
|
||||
export function useCanvasControls(zoomSensitivity: number = 50, contentBounds?: ContentBounds, enabled: boolean = true) {
|
||||
export function useCanvasControls(
|
||||
zoomSensitivity: number = 50,
|
||||
contentBounds?: ContentBounds,
|
||||
enabled: boolean = true,
|
||||
wheelAction: 'zoom' | 'scroll' = 'zoom',
|
||||
) {
|
||||
const viewportRef = useRef<HTMLDivElement>(null);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const gridRef = useRef<HTMLDivElement>(null);
|
||||
@@ -67,6 +72,11 @@ export function useCanvasControls(zoomSensitivity: number = 50, contentBounds?:
|
||||
const spaceRef = useRef(false);
|
||||
const sensitivityRef = useRef(zoomSensitivity);
|
||||
sensitivityRef.current = zoomSensitivity;
|
||||
// Read through a ref, like sensitivity: the wheel listener is bound once per mount, so a plain
|
||||
// closure over the prop would keep the value the canvas had when it mounted and the setting
|
||||
// would appear to do nothing until you switched dashboards.
|
||||
const wheelActionRef = useRef(wheelAction);
|
||||
wheelActionRef.current = wheelAction;
|
||||
const contentBoundsRef = useRef(contentBounds);
|
||||
contentBoundsRef.current = contentBounds;
|
||||
const animFrameRef = useRef<number | null>(null);
|
||||
@@ -311,6 +321,12 @@ export function useCanvasControls(zoomSensitivity: number = 50, contentBounds?:
|
||||
if (selectFullscreenCardId(store.getState())) return;
|
||||
// ctrl/cmd wheel is the zoom gesture on every surface: a physically held key or a trackpad pinch (which also sets ctrlKey). It bypasses scrollable children so zoom is always reachable, even over a chat you're typing in.
|
||||
const isModifierWheel = e.ctrlKey || e.metaKey;
|
||||
// The setting swaps which of the two a bare mouse notch does. A PINCH must keep zooming
|
||||
// whatever the setting says: it sets ctrlKey but there is no key held, and nobody pinches to
|
||||
// scroll. So only a real held key counts as the swap trigger, and `e.ctrlKey && !isPinch`
|
||||
// cannot be used here because Chromium reports a pinch identically to ctrl+wheel; the
|
||||
// trackpad classifier is what tells them apart.
|
||||
const wheelZooms = wheelActionRef.current !== 'scroll';
|
||||
|
||||
// Let scrollable children handle the event when appropriate, but fall through to canvas pan if the child is at its scroll boundary.
|
||||
const dy = e.deltaMode === 1 ? e.deltaY * 40 : e.deltaY;
|
||||
@@ -392,12 +408,18 @@ export function useCanvasControls(zoomSensitivity: number = 50, contentBounds?:
|
||||
// Horizontal-dominant mouse scroll (tilt wheel) → pan X. Dominant-axis, so the vertical jitter in a sideways swipe doesn't also zoom.
|
||||
pendingPanDx += dx;
|
||||
scheduleWheelFlush();
|
||||
} else {
|
||||
} else if (wheelZooms) {
|
||||
// Mouse-wheel vertical notch → zoom at the cursor (same anchor as pinch) so the point under the pointer grows toward you, not away. Clamp the per-event delta so a discrete notch is a small step, not a lurch.
|
||||
const rect = el.getBoundingClientRect();
|
||||
pendingZoomDy += clamp(dy, -WHEEL_ZOOM_DELTA_CAP, WHEEL_ZOOM_DELTA_CAP);
|
||||
pendingZoomCenter = { cx: e.clientX - rect.left, cy: e.clientY - rect.top };
|
||||
scheduleWheelFlush();
|
||||
} else {
|
||||
// Setting says a wheel scrolls: pan vertically instead. Zoom is still reachable on
|
||||
// cmd/ctrl+wheel, which the isModifierWheel branch above already handles, so the two
|
||||
// gestures simply trade places rather than one of them going missing.
|
||||
pendingPanDy += dy;
|
||||
scheduleWheelFlush();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
|
||||
workflowCards, workflowItems, workflowOpenCards, workflowsHub,
|
||||
pendingFocusWorkflowId, pendingFocusWorkflowsHub,
|
||||
layoutInitialized, persistedExpandedSessionIds,
|
||||
zoomSensitivity, newAgentShortcut, browserHomepage, expandNewChats,
|
||||
zoomSensitivity, mouseWheelAction, newAgentShortcut, browserHomepage, expandNewChats,
|
||||
autoRevealSubAgents, outputs, outputsLoaded, glowingAgentCards, glowingBrowserCards,
|
||||
} = useDashboardSelectors(dashboardId);
|
||||
// sessions is the top-level dict; useMemo on its identity so sessionList is stable when sessions hasn't actually changed (RTK only swaps the dict ref when one of its values changes, so this is the right granularity).
|
||||
@@ -68,7 +68,7 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
|
||||
[cards, viewCards, browserCards, workflowCards, workflowsHub],
|
||||
);
|
||||
|
||||
const canvas = useCanvasControls(zoomSensitivity, contentBounds, isActive);
|
||||
const canvas = useCanvasControls(zoomSensitivity, contentBounds, isActive, mouseWheelAction);
|
||||
const selection = useDashboardSelection(
|
||||
{ panX: canvas.panX, panY: canvas.panY, zoom: canvas.zoom, viewportRef: canvas.viewportRef },
|
||||
cards,
|
||||
|
||||
@@ -36,6 +36,7 @@ export function useDashboardSelectors(dashboardId: string) {
|
||||
const layoutInitialized = useAppSelector((state) => state.dashboardLayout.initialized);
|
||||
const persistedExpandedSessionIds = useAppSelector((state) => state.dashboardLayout.persistedExpandedSessionIds);
|
||||
const zoomSensitivity = useAppSelector((state) => state.settings.data.zoom_sensitivity);
|
||||
const mouseWheelAction = useAppSelector((state) => state.settings.data.mouse_wheel_action);
|
||||
const newAgentShortcut = useAppSelector((state) => state.settings.data.new_agent_shortcut);
|
||||
const browserHomepage = useAppSelector((state) => state.settings.data.browser_homepage);
|
||||
const expandNewChats = useAppSelector((state) => state.settings.data.expand_new_chats_in_dashboard);
|
||||
@@ -62,6 +63,7 @@ export function useDashboardSelectors(dashboardId: string) {
|
||||
layoutInitialized,
|
||||
persistedExpandedSessionIds,
|
||||
zoomSensitivity,
|
||||
mouseWheelAction,
|
||||
newAgentShortcut,
|
||||
browserHomepage,
|
||||
expandNewChats,
|
||||
|
||||
@@ -43,6 +43,8 @@ export interface AppSettings {
|
||||
default_max_turns: number | null;
|
||||
default_thinking_level: 'off' | 'low' | 'medium' | 'high' | 'auto';
|
||||
zoom_sensitivity: number;
|
||||
/** What a plain mouse wheel does on the canvas; trackpad two-finger always pans. */
|
||||
mouse_wheel_action: 'zoom' | 'scroll';
|
||||
theme: 'light' | 'dark';
|
||||
new_agent_shortcut: string;
|
||||
dictation_shortcut?: string | null;
|
||||
@@ -163,6 +165,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
||||
default_max_turns: null,
|
||||
default_thinking_level: 'auto',
|
||||
zoom_sensitivity: 50,
|
||||
mouse_wheel_action: 'zoom',
|
||||
ui_font_scale: 1,
|
||||
voice_hold_to_talk: true,
|
||||
theme: 'light',
|
||||
|
||||
Reference in New Issue
Block a user