fix(desktop): disable animated wheel scrolling on linux

Chromium animates each mouse-wheel notch on Linux, unlike native Linux apps.
Append disable-smooth-scrolling to the desktop app on Linux so wheel deltas
apply immediately.

Make ScrollView's viewport wheel listener and the session panel's
scrollbar-reveal wheel listener passive, so the compositor no longer waits on
the main thread before scrolling. Remove the home route's viewport wheel
handler, which could never cancel because its target was always inside the
viewport. Attach the desktop window wheel listener only while pinch zoom is
enabled; main's zoom-changed handler covers Ctrl+wheel otherwise.

ScrollView keyboard scrolling (PageUp/Down, Home/End, arrows, Space) now
jumps instantly instead of animating.
This commit is contained in:
Dax Raad
2026-10-18 23:23:59 -04:00
Unverified
parent 7aac8f71d9
commit 161cbad85b
6 changed files with 39 additions and 31 deletions
-1
View File
@@ -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}
>
<div
class={`
-6
View File
@@ -143,12 +143,6 @@ export function createHomeScrollController(groups: Accessor<HomeSessionGroup[]>)
},
update,
containWheel,
containOuterWheel: (event: WheelEvent) => {
if (!viewport) return
if (event.target instanceof Node && viewport.contains(event.target)) return
containWheel(event)
},
},
header: {
setContent: (element: HTMLDivElement) => {
+1 -1
View File
@@ -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}
@@ -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")
+19 -10
View File
@@ -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
+16 -13
View File
@@ -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}