diff --git a/images/chromium-headful/Dockerfile b/images/chromium-headful/Dockerfile index f800fa0b..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 -# ^--- now has event.SYSTEM_PONG with legacy support to keepalive +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 fdb1d375..a1928079 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" @@ -248,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) @@ -527,6 +528,14 @@ this.$nextTick(() => { this.isVideoSyncing = false }) }) + this._wheelHandler = (e: WheelEvent) => { + if (!this.hosting) return + e.preventDefault() + if (this.locked) return + this.onWheel(e) + } + document.addEventListener('wheel', this._wheelHandler, { passive: false, capture: true }) + /* Initialize Guacamole Keyboard */ this.keyboard.onkeydown = (key: number) => { if (!this.hosting || this.locked) { @@ -552,6 +561,10 @@ } beforeDestroy() { + if (this._wheelHandler) { + document.removeEventListener('wheel', this._wheelHandler, { capture: true }) + this._wheelHandler = null + } this.observer.disconnect() this.$accessor.video.setPlayable(false) /* Guacamole Keyboard does not provide destroy functions */ @@ -708,46 +721,28 @@ }) } - wheelThrottle = false onWheel(e: WheelEvent) { - if (!this.hosting || this.locked) { - return - } + this.sendMousePos(e) 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 + x *= -1 + 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 }) + const sensitivity = this.scroll / 10 + const dx = Math.max(-32767, Math.min(32767, Math.round(x * sensitivity))) + const dy = Math.max(-32767, Math.min(32767, Math.round(y * sensitivity))) - window.setTimeout(() => { - this.wheelThrottle = false - }, 100) + 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/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/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, 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..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 @@ -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 { @@ -149,16 +154,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 +195,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 +223,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 +290,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 +376,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;