diff --git a/packages/app/src/home/route.tsx b/packages/app/src/home/route.tsx index 188210be169..6626e90f8f7 100644 --- a/packages/app/src/home/route.tsx +++ b/packages/app/src/home/route.tsx @@ -37,7 +37,6 @@ export function Home() { thumbHoverTarget={scroll.viewport.hoverTarget()} viewportRef={scroll.viewport.setViewport} onScroll={(event) => scroll.viewport.update(event.currentTarget.scrollTop)} - onWheel={scroll.viewport.containOuterWheel} >
) }, update, containWheel, - containOuterWheel: (event: WheelEvent) => { - if (!viewport) return - - if (event.target instanceof Node && viewport.contains(event.target)) return - containWheel(event) - }, }, header: { setContent: (element: HTMLDivElement) => { diff --git a/packages/app/src/session/screen.tsx b/packages/app/src/session/screen.tsx index d5916634f61..4e0993781c5 100644 --- a/packages/app/src/session/screen.tsx +++ b/packages/app/src/session/screen.tsx @@ -355,7 +355,7 @@ function SessionScreenContent(props: { data-scrollbar-hidden={store.timelineScrollbarHidden || store.sideWidthMotion} onPointerMove={revealTimelineScrollbar} onPointerDown={revealTimelineScrollbar} - onWheel={revealTimelineScrollbar} + on:wheel={{ passive: true, handleEvent: revealTimelineScrollbar }} onKeyDown={revealTimelineScrollbar} onTransitionRun={trackSideWidthMotion} onTransitionEnd={trackSideWidthMotion} diff --git a/packages/desktop/src/main/lifecycle/configure.ts b/packages/desktop/src/main/lifecycle/configure.ts index 878d184a4f0..488eca0fe3a 100644 --- a/packages/desktop/src/main/lifecycle/configure.ts +++ b/packages/desktop/src/main/lifecycle/configure.ts @@ -26,6 +26,9 @@ export function configureApplication() { const features = app.commandLine.getSwitchValue("enable-features") app.commandLine.appendSwitch("enable-features", features ? `${jsCallStackFeature},${features}` : jsCallStackFeature) + // Chromium animates each mouse-wheel notch on Linux; native Linux apps scroll by the full delta immediately. + if (process.platform === "linux") app.commandLine.appendSwitch("disable-smooth-scrolling") + if (!app.isPackaged) app.commandLine.appendSwitch("remote-debugging-port", process.env.OPENCODE_DESKTOP_REMOTE_DEBUGGING_PORT ?? "9222") diff --git a/packages/desktop/src/renderer/window/zoom.ts b/packages/desktop/src/renderer/window/zoom.ts index 1a21863d820..28f1a2b7623 100644 --- a/packages/desktop/src/renderer/window/zoom.ts +++ b/packages/desktop/src/renderer/window/zoom.ts @@ -70,16 +70,19 @@ api.onZoomFactorChanged((factor) => { void api.getPinchZoomEnabled().then((enabled) => { pinchZoomEnabled = enabled + syncWheelPinchListener() }) api.onPinchZoomEnabledChanged((enabled) => { pinchZoomEnabled = enabled resetWheelPinch() + syncWheelPinchListener() }) const setPinchZoomEnabled = (enabled: boolean) => { pinchZoomEnabled = enabled resetWheelPinch() + syncWheelPinchListener() return api.setPinchZoomEnabled(enabled) } @@ -129,18 +132,24 @@ const updateWheelPinch = (event: WheelEvent) => { applyZoom(clamp(wheelPinch.startZoom - (wheelPinch.totalDelta / WHEEL_PINCH_THRESHOLD) * WHEEL_PINCH_STEP)) } -window.addEventListener( - "wheel", - (event) => { - if (!pinchZoomEnabled) return +const onWheelPinch = (event: WheelEvent) => { + if (!event.ctrlKey) return - if (!event.ctrlKey) return + event.preventDefault() + updateWheelPinch(event) +} - event.preventDefault() - updateWheelPinch(event) - }, - { passive: false }, -) +// A cancelable window wheel listener makes every scroll gesture wait for the main thread, so it +// is only attached while pinch zoom is on. Main's zoom-changed handler covers Ctrl+wheel otherwise. +const syncWheelPinchListener = () => { + if (pinchZoomEnabled) { + window.addEventListener("wheel", onWheelPinch, { passive: false }) + + return + } + + window.removeEventListener("wheel", onWheelPinch) +} window.addEventListener("keydown", (event) => { if (!(OS_NAME === "macos" ? event.metaKey : event.ctrlKey)) return diff --git a/packages/ui/src/components/scroll-view.tsx b/packages/ui/src/components/scroll-view.tsx index 8d92074637a..e46cec7af82 100644 --- a/packages/ui/src/components/scroll-view.tsx +++ b/packages/ui/src/components/scroll-view.tsx @@ -397,8 +397,7 @@ export function ScrollView(props: ScrollViewProps) { // Keybinds implementation // We ensure the viewport has a tabindex so it can receive focus - // We can also explicitly catch PageUp/Down if we want smooth scroll or specific behavior, - // but native usually handles this perfectly. Let's explicitly ensure it behaves well. + // Keyboard scrolling jumps immediately, like the wheel, instead of animating. const onKeyDown = (e: KeyboardEvent) => { // If user is focused on an input inside the scroll view, don't hijack keys if (document.activeElement && ["INPUT", "TEXTAREA", "SELECT"].includes(document.activeElement.tagName)) { @@ -434,27 +433,27 @@ export function ScrollView(props: ScrollViewProps) { switch (next) { case "page-down": e.preventDefault() - viewportRef.scrollBy({ top: scrollAmount, behavior: "smooth" }) + viewportRef.scrollBy({ top: scrollAmount }) break case "page-up": e.preventDefault() - viewportRef.scrollBy({ top: -scrollAmount, behavior: "smooth" }) + viewportRef.scrollBy({ top: -scrollAmount }) break case "home": e.preventDefault() - viewportRef.scrollTo({ top: 0, behavior: "smooth" }) + viewportRef.scrollTo({ top: 0 }) break case "end": e.preventDefault() - viewportRef.scrollTo({ top: viewportRef.scrollHeight, behavior: "smooth" }) + viewportRef.scrollTo({ top: viewportRef.scrollHeight }) break case "up": e.preventDefault() - viewportRef.scrollBy({ top: -lineAmount, behavior: "smooth" }) + viewportRef.scrollBy({ top: -lineAmount }) break case "down": e.preventDefault() - viewportRef.scrollBy({ top: lineAmount, behavior: "smooth" }) + viewportRef.scrollBy({ top: lineAmount }) break } } @@ -484,13 +483,17 @@ export function ScrollView(props: ScrollViewProps) { if (typeof events.onScroll === "function") events.onScroll(e as any) }} - onWheel={(e) => { - markScrolling() - const handler = events.onWheel + // Passive so the compositor never waits on the main thread to scroll; onWheel cannot cancel. + on:wheel={{ + passive: true, + handleEvent: (e) => { + markScrolling() + const handler = events.onWheel - if (typeof handler === "function") handler(e as any) + if (typeof handler === "function") handler(e as any) - if (Array.isArray(handler)) handler[0](handler[1], e as any) + if (Array.isArray(handler)) handler[0](handler[1], e as any) + }, }} onTouchStart={events.onTouchStart as any} onTouchMove={events.onTouchMove as any}