From c6611119fdddb2e2db6617a1135d221b0b589f17 Mon Sep 17 00:00:00 2001 From: hiroTamada Date: Wed, 1 Apr 2026 10:07:11 -0400 Subject: [PATCH 1/7] feat: XI2.1 smooth scrolling via xf86-input-neko scroll valuators Replace the XTest button-event scroll path (fixed ~100px quantum per click) with XInput2.1 scroll valuators on the xf86-input-neko driver. This gives pixel-precise 1:1 mapping between client trackpad deltas and browser scroll pixels, eliminating the jumpiness of discrete scroll notches. Changes: - neko.c: add vertical/horizontal scroll valuator axes (3, 4) with SetScrollValuator; handle NEKO_SCROLL messages via xf86PostMotionEventM instead of button events; change device type from XI_TOUCHSCREEN to XI_MOUSE so Chromium respects valuators - video.vue: remove PIXELS_PER_TICK quantization and 100ms throttle; send raw pixel deltas batched per rAF; use document-level wheel listener with passive:false for reliable preventDefault - base.ts: extend wheel binary message to include controlKey byte (PayloadScrollWithCtrl, length=5) - neko.yaml: enable xinput driver with socket path - Dockerfile: use neko base image with XI2 scroll support Requires: kernel/neko branch hiro/xi2-scroll Made-with: Cursor --- images/chromium-headful/Dockerfile | 4 +- .../client/src/components/video.vue | 63 ++++++++------- .../chromium-headful/client/src/neko/base.ts | 8 +- images/chromium-headful/neko.yaml | 3 + .../xorg-deps/xf86-input-neko/src/neko.c | 81 ++++++++++++------- 5 files changed, 96 insertions(+), 63 deletions(-) diff --git a/images/chromium-headful/Dockerfile b/images/chromium-headful/Dockerfile index f800fa0b..b6eb5e57 100644 --- a/images/chromium-headful/Dockerfile +++ b/images/chromium-headful/Dockerfile @@ -146,8 +146,8 @@ RUN --mount=type=cache,target=/tmp/cache/ffmpeg,sharing=locked,id=$CACHEIDPREFIX rm -rf /tmp/ffmpeg* EOT -FROM ghcr.io/kernel/neko/base:3.0.8-v1.4.0 AS neko -# ^--- now has event.SYSTEM_PONG with legacy support to keepalive +FROM ghcr.io/kernel/neko/base:xi2-scroll AS neko +# ^--- XI2 smooth scroll via xf86-input-neko scroll valuators FROM node:22-bullseye-slim AS node-22 FROM docker.io/ubuntu:22.04 diff --git a/images/chromium-headful/client/src/components/video.vue b/images/chromium-headful/client/src/components/video.vue index fdb1d375..9f9a7b88 100644 --- a/images/chromium-headful/client/src/components/video.vue +++ b/images/chromium-headful/client/src/components/video.vue @@ -17,7 +17,6 @@ :style="{ pointerEvents: hosting ? 'auto' : 'none' }" @click.stop.prevent @contextmenu.stop.prevent - @wheel.stop.prevent="onWheel" @mousemove.stop.prevent="onMouseMove" @mousedown.stop.prevent="onMouseDown" @mouseup.stop.prevent="onMouseUp" @@ -527,6 +526,12 @@ this.$nextTick(() => { this.isVideoSyncing = false }) }) + document.addEventListener('wheel', (e: WheelEvent) => { + if (!this.hosting || this.locked) return + e.preventDefault() + this.onWheel(e) + }, { passive: false, capture: true }) + /* Initialize Guacamole Keyboard */ this.keyboard.onkeydown = (key: number) => { if (!this.hosting || this.locked) { @@ -708,46 +713,42 @@ }) } - wheelThrottle = false - onWheel(e: WheelEvent) { - if (!this.hosting || this.locked) { - return - } + private _scrollAccX = 0 + private _scrollAccY = 0 + private _scrollCtrl = false + private _scrollRaf: number | null = null + onWheel(e: WheelEvent) { let x = e.deltaX let y = e.deltaY - // Normalize to pixel units. deltaMode 1 = lines, 2 = pages; convert - // both to approximate pixel values so the divisor below works uniformly. if (e.deltaMode !== 0) { x *= WHEEL_LINE_HEIGHT y *= WHEEL_LINE_HEIGHT } if (this.scroll_invert) { - x = x * -1 - y = y * -1 - } - - // The server sends one XTestFakeButtonEvent per unit we pass here, - // and each event scrolls Chromium by ~120 px. Raw pixel deltas from - // trackpads are already in pixels (~120 per notch), so dividing by - // PIXELS_PER_TICK converts them to discrete scroll "ticks". The - // result is clamped to [-scroll, scroll] (the user-facing sensitivity - // setting) so fast swipes don't over-scroll. - const PIXELS_PER_TICK = 120 - x = x === 0 ? 0 : Math.min(Math.max(Math.round(x / PIXELS_PER_TICK) || Math.sign(x), -this.scroll), this.scroll) - y = y === 0 ? 0 : Math.min(Math.max(Math.round(y / PIXELS_PER_TICK) || Math.sign(y), -this.scroll), this.scroll) - - this.sendMousePos(e) - - if (!this.wheelThrottle) { - this.wheelThrottle = true - this.$client.sendData('wheel', { x, y }) - - window.setTimeout(() => { - this.wheelThrottle = false - }, 100) + x *= -1 + y *= -1 + } + + this._scrollAccX += x + this._scrollAccY += y + if (e.ctrlKey || e.metaKey) this._scrollCtrl = true + + if (this._scrollRaf === null) { + this._scrollRaf = requestAnimationFrame(() => { + this._scrollRaf = null + const dx = Math.max(-32767, Math.min(32767, Math.round(this._scrollAccX))) + const dy = Math.max(-32767, Math.min(32767, Math.round(this._scrollAccY))) + const ctrl = this._scrollCtrl + this._scrollAccX = 0 + this._scrollAccY = 0 + this._scrollCtrl = false + if (dx !== 0 || dy !== 0) { + this.$client.sendData('wheel', { x: dx, y: dy, controlKey: ctrl }) + } + }) } } diff --git a/images/chromium-headful/client/src/neko/base.ts b/images/chromium-headful/client/src/neko/base.ts index 501a8fd8..d72fc069 100644 --- a/images/chromium-headful/client/src/neko/base.ts +++ b/images/chromium-headful/client/src/neko/base.ts @@ -136,7 +136,8 @@ export abstract class BaseClient extends EventEmitter { this._id = '' } - public sendData(event: 'wheel' | 'mousemove', data: { x: number; y: number }): void + public sendData(event: 'wheel', data: { x: number; y: number; controlKey?: boolean }): void + public sendData(event: 'mousemove', data: { x: number; y: number }): void public sendData(event: 'mousedown' | 'mouseup' | 'keydown' | 'keyup', data: { key: number }): void public sendData(event: string, data: any) { if (!this.connected) { @@ -156,12 +157,13 @@ export abstract class BaseClient extends EventEmitter { payload.setUint16(5, data.y, true) break case 'wheel': - buffer = new ArrayBuffer(7) + buffer = new ArrayBuffer(8) payload = new DataView(buffer) payload.setUint8(0, OPCODE.SCROLL) - payload.setUint16(1, 4, true) + payload.setUint16(1, 5, true) payload.setInt16(3, data.x, true) payload.setInt16(5, data.y, true) + payload.setUint8(7, data.controlKey ? 1 : 0) break case 'keydown': case 'mousedown': diff --git a/images/chromium-headful/neko.yaml b/images/chromium-headful/neko.yaml index dc9619ec..2e747ea1 100644 --- a/images/chromium-headful/neko.yaml +++ b/images/chromium-headful/neko.yaml @@ -3,6 +3,9 @@ desktop: screen: "1920x1080@25" + input: + enabled: true + socket: "/tmp/xf86-input-neko.sock" member: provider: multiuser diff --git a/images/chromium-headful/xorg-deps/xf86-input-neko/src/neko.c b/images/chromium-headful/xorg-deps/xf86-input-neko/src/neko.c index 3d8f88d6..1ea52e4e 100644 --- a/images/chromium-headful/xorg-deps/xf86-input-neko/src/neko.c +++ b/images/chromium-headful/xorg-deps/xf86-input-neko/src/neko.c @@ -54,10 +54,15 @@ #include #include #include +#include #include -#define MAX_USED_VALUATORS 3 /* x, y, pressure */ -#define TOUCH_MAX_SLOTS 10 /* max number of simultaneous touches */ +#define MAX_USED_VALUATORS 5 /* x, y, pressure, v-scroll, h-scroll */ +#define TOUCH_VALUATORS 3 /* touch only uses x, y, pressure */ +#define TOUCH_MAX_SLOTS 10 /* max number of simultaneous touches */ + +#define NEKO_SCROLL 0x80 +#define SCROLL_INCREMENT 120.0 struct neko_message { @@ -77,6 +82,9 @@ struct neko_priv int pmax; ValuatorMask *valuators; uint16_t slots; + /* scroll state (absolute, accumulated) */ + double scroll_x; + double scroll_y; /* socket */ struct sockaddr_un addr; int listen_socket; @@ -149,16 +157,25 @@ ReadInput(InputInfoPtr pInfo) ValuatorMask *m = priv->valuators; valuator_mask_zero(m); - // do not send valuators if x and y are -1 - if (msg.x != -1 && msg.y != -1) + if (msg.type == NEKO_SCROLL) { - valuator_mask_set_double(m, 0, msg.x); - valuator_mask_set_double(m, 1, msg.y); - valuator_mask_set_double(m, 2, msg.pressure); + if (msg.y != 0) + valuator_mask_set_double(m, 3, (double)msg.y); + if (msg.x != 0) + valuator_mask_set_double(m, 4, (double)msg.x); + xf86PostMotionEventM(pInfo->dev, FALSE, m); + } + else + { + // do not send valuators if x and y are -1 + if (msg.x != -1 && msg.y != -1) + { + valuator_mask_set_double(m, 0, msg.x); + valuator_mask_set_double(m, 1, msg.y); + valuator_mask_set_double(m, 2, msg.pressure); + } + xf86PostTouchEvent(pInfo->dev, msg.touchId, msg.type, 0, m); } - - // TODO: extend to other types, such as keyboard and mouse - xf86PostTouchEvent(pInfo->dev, msg.touchId, msg.type, 0, m); } /* Close socket. */ @@ -181,11 +198,11 @@ InitTouch(InputInfoPtr pInfo) struct neko_priv *priv = pInfo->private; const int nbtns = 11; - const int naxes = 3; + const int naxes = MAX_USED_VALUATORS; /* x, y, pressure, v-scroll, h-scroll */ unsigned char map[nbtns + 1]; Atom btn_labels[nbtns]; - Atom axis_labels[naxes]; + Atom axis_labels[MAX_USED_VALUATORS]; // init button map memset(map, 0, sizeof(map)); @@ -209,10 +226,12 @@ InitTouch(InputInfoPtr pInfo) btn_labels[10] = XIGetKnownProperty(BTN_LABEL_PROP_BTN_BACK); // init axis labels - memset(axis_labels, 0, ARRAY_SIZE(axis_labels) * sizeof(Atom)); + memset(axis_labels, 0, sizeof(axis_labels)); axis_labels[0] = XIGetKnownProperty(AXIS_LABEL_PROP_ABS_MT_POSITION_X); axis_labels[1] = XIGetKnownProperty(AXIS_LABEL_PROP_ABS_MT_POSITION_Y); axis_labels[2] = XIGetKnownProperty(AXIS_LABEL_PROP_ABS_MT_PRESSURE); + axis_labels[3] = XIGetKnownProperty(AXIS_LABEL_PROP_REL_VSCROLL); + axis_labels[4] = XIGetKnownProperty(AXIS_LABEL_PROP_REL_HSCROLL); /* initialize mouse emulation valuators */ if (InitPointerDeviceStruct((DevicePtr)pInfo->dev, @@ -274,22 +293,28 @@ InitTouch(InputInfoPtr pInfo) priv->pmax + 1, /* max_res */ Absolute); - /* - The mode field is either XIDirectTouch for direct−input touch devices - such as touchscreens or XIDependentTouch for indirect input devices such - as touchpads. For XIDirectTouch devices, touch events are sent to window - at the position the touch occured. For XIDependentTouch devices, touch - events are sent to the window at the position of the device's sprite. - - The num_touches field defines the maximum number of simultaneous touches - the device supports. A num_touches of 0 means the maximum number of - simultaneous touches is undefined or unspecified. This field should be - used as a guide only, devices will lie about their capabilities. - */ + /* scroll valuator axes — relative, so min=max=0 */ + xf86InitValuatorAxisStruct(pInfo->dev, 3, + axis_labels[3], + NO_AXIS_LIMITS, NO_AXIS_LIMITS, /* no limits for scroll */ + 0, 0, 0, + Relative); + SetScrollValuator(pInfo->dev, 3, SCROLL_TYPE_VERTICAL, + SCROLL_INCREMENT, SCROLL_FLAG_PREFERRED); + + xf86InitValuatorAxisStruct(pInfo->dev, 4, + axis_labels[4], + NO_AXIS_LIMITS, NO_AXIS_LIMITS, + 0, 0, 0, + Relative); + SetScrollValuator(pInfo->dev, 4, SCROLL_TYPE_HORIZONTAL, + SCROLL_INCREMENT, SCROLL_FLAG_PREFERRED); + + /* Touch class only uses the first 3 axes (x, y, pressure). */ if (InitTouchClassDeviceStruct(pInfo->dev, priv->slots, XIDirectTouch, - naxes) == FALSE) + TOUCH_VALUATORS) == FALSE) { xf86IDrvMsg(pInfo, X_ERROR, "unable to allocate TouchClassDeviceStruct\n"); @@ -354,7 +379,7 @@ PreInit(__attribute__ ((unused)) InputDriverPtr drv, return BadValue; } - pInfo->type_name = (char*)XI_TOUCHSCREEN; + pInfo->type_name = (char*)XI_MOUSE; pInfo->device_control = DeviceControl; pInfo->read_input = NULL; pInfo->control_proc = NULL; @@ -429,6 +454,8 @@ PreInit(__attribute__ ((unused)) InputDriverPtr drv, priv->width = 0xffff; priv->height = 0xffff; priv->pmax = 255; + priv->scroll_x = 0.0; + priv->scroll_y = 0.0; priv->thread = 0; /* Return the configured device */ From 2734a81e71f407c07a59e3e2b6335e94b738aef2 Mon Sep 17 00:00:00 2001 From: hiroTamada Date: Wed, 1 Apr 2026 14:57:31 -0400 Subject: [PATCH 2/7] fix: address review feedback on scroll implementation - Scope wheel listener to overlay element instead of document to avoid blocking scrolling in chat/emoji/files panels - Store handler reference and remove it in beforeDestroy to prevent leaks - Re-integrate scroll sensitivity setting (1-100 range, 50 = 1x) - Remove unused scroll_x/scroll_y fields from xf86-input-neko driver - Revert Dockerfile FROM to existing neko base tag for CI (pending neko#12) Made-with: Cursor --- images/chromium-headful/Dockerfile | 4 ++-- .../client/src/components/video.vue | 16 ++++++++++++---- .../xorg-deps/xf86-input-neko/src/neko.c | 5 ----- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/images/chromium-headful/Dockerfile b/images/chromium-headful/Dockerfile index b6eb5e57..170b2337 100644 --- a/images/chromium-headful/Dockerfile +++ b/images/chromium-headful/Dockerfile @@ -146,8 +146,8 @@ RUN --mount=type=cache,target=/tmp/cache/ffmpeg,sharing=locked,id=$CACHEIDPREFIX rm -rf /tmp/ffmpeg* EOT -FROM ghcr.io/kernel/neko/base:xi2-scroll AS neko -# ^--- XI2 smooth scroll via xf86-input-neko scroll valuators +FROM ghcr.io/kernel/neko/base:3.0.8-v1.4.0 AS neko +# TODO: update to xi2-scroll tag once kernel/neko#12 is merged and tagged FROM node:22-bullseye-slim AS node-22 FROM docker.io/ubuntu:22.04 diff --git a/images/chromium-headful/client/src/components/video.vue b/images/chromium-headful/client/src/components/video.vue index 9f9a7b88..5152d5fb 100644 --- a/images/chromium-headful/client/src/components/video.vue +++ b/images/chromium-headful/client/src/components/video.vue @@ -247,6 +247,8 @@ @Ref('player') readonly _player!: HTMLElement @Ref('video') readonly _video!: HTMLVideoElement @Ref('resolution') readonly _resolution!: Resolution + + private _wheelHandler: ((e: WheelEvent) => void) | null = null @Ref('clipboard') readonly _clipboard!: Clipboard // all controls are hidden (e.g. for cast mode) @@ -526,11 +528,12 @@ this.$nextTick(() => { this.isVideoSyncing = false }) }) - document.addEventListener('wheel', (e: WheelEvent) => { + this._wheelHandler = (e: WheelEvent) => { if (!this.hosting || this.locked) return e.preventDefault() this.onWheel(e) - }, { passive: false, capture: true }) + } + this._overlay.addEventListener('wheel', this._wheelHandler, { passive: false }) /* Initialize Guacamole Keyboard */ this.keyboard.onkeydown = (key: number) => { @@ -557,6 +560,10 @@ } beforeDestroy() { + if (this._wheelHandler) { + this._overlay.removeEventListener('wheel', this._wheelHandler) + this._wheelHandler = null + } this.observer.disconnect() this.$accessor.video.setPlayable(false) /* Guacamole Keyboard does not provide destroy functions */ @@ -732,8 +739,9 @@ y *= -1 } - this._scrollAccX += x - this._scrollAccY += y + const sensitivity = this.scroll / 50 + this._scrollAccX += x * sensitivity + this._scrollAccY += y * sensitivity if (e.ctrlKey || e.metaKey) this._scrollCtrl = true if (this._scrollRaf === null) { diff --git a/images/chromium-headful/xorg-deps/xf86-input-neko/src/neko.c b/images/chromium-headful/xorg-deps/xf86-input-neko/src/neko.c index 1ea52e4e..21bf9d61 100644 --- a/images/chromium-headful/xorg-deps/xf86-input-neko/src/neko.c +++ b/images/chromium-headful/xorg-deps/xf86-input-neko/src/neko.c @@ -82,9 +82,6 @@ struct neko_priv int pmax; ValuatorMask *valuators; uint16_t slots; - /* scroll state (absolute, accumulated) */ - double scroll_x; - double scroll_y; /* socket */ struct sockaddr_un addr; int listen_socket; @@ -454,8 +451,6 @@ PreInit(__attribute__ ((unused)) InputDriverPtr drv, priv->width = 0xffff; priv->height = 0xffff; priv->pmax = 255; - priv->scroll_x = 0.0; - priv->scroll_y = 0.0; priv->thread = 0; /* Return the configured device */ From 4578efa3fc8a2be1d618118f0ce3e7e02b360a47 Mon Sep 17 00:00:00 2001 From: hiroTamada Date: Wed, 1 Apr 2026 15:19:37 -0400 Subject: [PATCH 3/7] fix: cancel pending scroll RAF on destroy and sync mouse position - Cancel any pending requestAnimationFrame in beforeDestroy to prevent accessing torn-down Vue instance - Restore sendMousePos call in onWheel so cursor position is synced before scroll events (trackpad scrolling doesn't fire mousemove) Made-with: Cursor --- images/chromium-headful/client/src/components/video.vue | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/images/chromium-headful/client/src/components/video.vue b/images/chromium-headful/client/src/components/video.vue index 5152d5fb..79b9c395 100644 --- a/images/chromium-headful/client/src/components/video.vue +++ b/images/chromium-headful/client/src/components/video.vue @@ -564,6 +564,10 @@ this._overlay.removeEventListener('wheel', this._wheelHandler) this._wheelHandler = null } + if (this._scrollRaf !== null) { + cancelAnimationFrame(this._scrollRaf) + this._scrollRaf = null + } this.observer.disconnect() this.$accessor.video.setPlayable(false) /* Guacamole Keyboard does not provide destroy functions */ @@ -726,6 +730,8 @@ private _scrollRaf: number | null = null onWheel(e: WheelEvent) { + this.sendMousePos(e) + let x = e.deltaX let y = e.deltaY From de6b0cb0d88a051f0938d10b3944541fe9a27378 Mon Sep 17 00:00:00 2001 From: hiroTamada Date: Wed, 1 Apr 2026 15:37:26 -0400 Subject: [PATCH 4/7] fix: use correct divisor for scroll sensitivity default The default scroll setting is 10, so the divisor should be 10 (not 50) to produce 1x sensitivity at the default value. Range: 1=0.1x to 100=10x. Made-with: Cursor --- images/chromium-headful/client/src/components/video.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/chromium-headful/client/src/components/video.vue b/images/chromium-headful/client/src/components/video.vue index 79b9c395..0d80f970 100644 --- a/images/chromium-headful/client/src/components/video.vue +++ b/images/chromium-headful/client/src/components/video.vue @@ -745,7 +745,7 @@ y *= -1 } - const sensitivity = this.scroll / 50 + const sensitivity = this.scroll / 10 this._scrollAccX += x * sensitivity this._scrollAccY += y * sensitivity if (e.ctrlKey || e.metaKey) this._scrollCtrl = true From 1978a7f01c4800a47140e7510a8a85527fd27fa0 Mon Sep 17 00:00:00 2001 From: hiroTamada Date: Wed, 1 Apr 2026 15:52:53 -0400 Subject: [PATCH 5/7] fix: always prevent default on wheel events when hosting Call preventDefault() before the locked check so the page doesn't scroll underneath the video overlay when hosting but locked. Made-with: Cursor --- images/chromium-headful/client/src/components/video.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/images/chromium-headful/client/src/components/video.vue b/images/chromium-headful/client/src/components/video.vue index 0d80f970..c47db3ac 100644 --- a/images/chromium-headful/client/src/components/video.vue +++ b/images/chromium-headful/client/src/components/video.vue @@ -529,8 +529,9 @@ }) this._wheelHandler = (e: WheelEvent) => { - if (!this.hosting || this.locked) return + if (!this.hosting) return e.preventDefault() + if (this.locked) return this.onWheel(e) } this._overlay.addEventListener('wheel', this._wheelHandler, { passive: false }) From 11c1ba18a91654cc984cb9d9ee2a5673586cb6db Mon Sep 17 00:00:00 2001 From: hiroTamada Date: Wed, 1 Apr 2026 16:10:10 -0400 Subject: [PATCH 6/7] fix: preserve fractional scroll remainder and fix ctrl latch - Subtract sent dx/dy from accumulators instead of resetting to zero, preserving fractional remainders for accurate sensitivity scaling - Use assignment for ctrl flag to reflect latest event state, avoiding false positives from transient ctrl events within a RAF batch Made-with: Cursor --- images/chromium-headful/client/src/components/video.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/images/chromium-headful/client/src/components/video.vue b/images/chromium-headful/client/src/components/video.vue index c47db3ac..06b85085 100644 --- a/images/chromium-headful/client/src/components/video.vue +++ b/images/chromium-headful/client/src/components/video.vue @@ -749,7 +749,7 @@ const sensitivity = this.scroll / 10 this._scrollAccX += x * sensitivity this._scrollAccY += y * sensitivity - if (e.ctrlKey || e.metaKey) this._scrollCtrl = true + this._scrollCtrl = e.ctrlKey || e.metaKey if (this._scrollRaf === null) { this._scrollRaf = requestAnimationFrame(() => { @@ -757,8 +757,8 @@ const dx = Math.max(-32767, Math.min(32767, Math.round(this._scrollAccX))) const dy = Math.max(-32767, Math.min(32767, Math.round(this._scrollAccY))) const ctrl = this._scrollCtrl - this._scrollAccX = 0 - this._scrollAccY = 0 + this._scrollAccX -= dx + this._scrollAccY -= dy this._scrollCtrl = false if (dx !== 0 || dy !== 0) { this.$client.sendData('wheel', { x: dx, y: dy, controlKey: ctrl }) From c2fea7d4f93838a3293a06f09eb2293f68e882fa Mon Sep 17 00:00:00 2001 From: hiroTamada Date: Thu, 2 Apr 2026 16:27:15 -0400 Subject: [PATCH 7/7] feat: update neko base to v1.5.0, fix scroll defaults and Vue 2 reactivity - Bump neko base image to 3.0.8-v1.5.0 (XI2.1 smooth scrolling support) - Default scroll_invert to false (works with --disable-features=ScrollLatching) - Add useDefineForClassFields: false to tsconfig for Vue 2 compatibility - Use document-level wheel listener with capture: true for reliable interception - Send scroll deltas directly without rAF batching or accumulation Made-with: Cursor --- images/chromium-headful/Dockerfile | 3 +- .../client/src/components/video.vue | 35 ++++--------------- .../client/src/store/settings.ts | 2 +- images/chromium-headful/client/tsconfig.json | 1 + 4 files changed, 10 insertions(+), 31 deletions(-) diff --git a/images/chromium-headful/Dockerfile b/images/chromium-headful/Dockerfile index 170b2337..84ecfd9d 100644 --- a/images/chromium-headful/Dockerfile +++ b/images/chromium-headful/Dockerfile @@ -146,8 +146,7 @@ RUN --mount=type=cache,target=/tmp/cache/ffmpeg,sharing=locked,id=$CACHEIDPREFIX rm -rf /tmp/ffmpeg* EOT -FROM ghcr.io/kernel/neko/base:3.0.8-v1.4.0 AS neko -# TODO: update to xi2-scroll tag once kernel/neko#12 is merged and tagged +FROM ghcr.io/kernel/neko/base:3.0.8-v1.5.0 AS neko FROM node:22-bullseye-slim AS node-22 FROM docker.io/ubuntu:22.04 diff --git a/images/chromium-headful/client/src/components/video.vue b/images/chromium-headful/client/src/components/video.vue index 06b85085..a1928079 100644 --- a/images/chromium-headful/client/src/components/video.vue +++ b/images/chromium-headful/client/src/components/video.vue @@ -534,7 +534,7 @@ if (this.locked) return this.onWheel(e) } - this._overlay.addEventListener('wheel', this._wheelHandler, { passive: false }) + document.addEventListener('wheel', this._wheelHandler, { passive: false, capture: true }) /* Initialize Guacamole Keyboard */ this.keyboard.onkeydown = (key: number) => { @@ -562,13 +562,9 @@ beforeDestroy() { if (this._wheelHandler) { - this._overlay.removeEventListener('wheel', this._wheelHandler) + document.removeEventListener('wheel', this._wheelHandler, { capture: true }) this._wheelHandler = null } - if (this._scrollRaf !== null) { - cancelAnimationFrame(this._scrollRaf) - this._scrollRaf = null - } this.observer.disconnect() this.$accessor.video.setPlayable(false) /* Guacamole Keyboard does not provide destroy functions */ @@ -725,11 +721,6 @@ }) } - private _scrollAccX = 0 - private _scrollAccY = 0 - private _scrollCtrl = false - private _scrollRaf: number | null = null - onWheel(e: WheelEvent) { this.sendMousePos(e) @@ -747,23 +738,11 @@ } const sensitivity = this.scroll / 10 - this._scrollAccX += x * sensitivity - this._scrollAccY += y * sensitivity - this._scrollCtrl = e.ctrlKey || e.metaKey - - if (this._scrollRaf === null) { - this._scrollRaf = requestAnimationFrame(() => { - this._scrollRaf = null - const dx = Math.max(-32767, Math.min(32767, Math.round(this._scrollAccX))) - const dy = Math.max(-32767, Math.min(32767, Math.round(this._scrollAccY))) - const ctrl = this._scrollCtrl - this._scrollAccX -= dx - this._scrollAccY -= dy - this._scrollCtrl = false - if (dx !== 0 || dy !== 0) { - this.$client.sendData('wheel', { x: dx, y: dy, controlKey: ctrl }) - } - }) + const dx = Math.max(-32767, Math.min(32767, Math.round(x * sensitivity))) + const dy = Math.max(-32767, Math.min(32767, Math.round(y * sensitivity))) + + if (dx !== 0 || dy !== 0) { + this.$client.sendData('wheel', { x: dx, y: dy, controlKey: e.ctrlKey || e.metaKey }) } } diff --git a/images/chromium-headful/client/src/store/settings.ts b/images/chromium-headful/client/src/store/settings.ts index 28707345..88b0d64d 100644 --- a/images/chromium-headful/client/src/store/settings.ts +++ b/images/chromium-headful/client/src/store/settings.ts @@ -12,7 +12,7 @@ interface KeyboardLayouts { export const state = () => { return { scroll: get('scroll', 10), - scroll_invert: get('scroll_invert', true), + scroll_invert: get('scroll_invert', false), autoplay: get('autoplay', true), ignore_emotes: get('ignore_emotes', false), chat_sound: get('chat_sound', true), diff --git a/images/chromium-headful/client/tsconfig.json b/images/chromium-headful/client/tsconfig.json index fc4f3a3f..8537f56a 100644 --- a/images/chromium-headful/client/tsconfig.json +++ b/images/chromium-headful/client/tsconfig.json @@ -7,6 +7,7 @@ "importHelpers": true, "moduleResolution": "node", "experimentalDecorators": true, + "useDefineForClassFields": false, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "sourceMap": true,