diff --git a/jsonrpc.go b/jsonrpc.go index 198d130..40be85c 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -762,6 +762,17 @@ func rpcSetCloudUrl(apiUrl string, appUrl string) error { return nil } +var currentScrollSensitivity string = "default" + +func rpcGetScrollSensitivity() (string, error) { + return currentScrollSensitivity, nil +} + +func rpcSetScrollSensitivity(sensitivity string) error { + currentScrollSensitivity = sensitivity + return nil +} + var rpcHandlers = map[string]RPCHandler{ "ping": {Func: rpcPing}, "getDeviceID": {Func: rpcGetDeviceID}, @@ -821,4 +832,6 @@ var rpcHandlers = map[string]RPCHandler{ "getSerialSettings": {Func: rpcGetSerialSettings}, "setSerialSettings": {Func: rpcSetSerialSettings, Params: []string{"settings"}}, "setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}}, + "getScrollSensitivity": {Func: rpcGetScrollSensitivity}, + "setScrollSensitivity": {Func: rpcSetScrollSensitivity, Params: []string{"sensitivity"}}, } diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index bb17e39..1587d29 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { + useDeviceSettingsStore, useHidStore, useMouseStore, useRTCStore, @@ -144,19 +145,28 @@ export default function WebRTCVideo() { [sendMouseMovement, videoClientHeight, videoClientWidth, videoWidth, videoHeight], ); + const trackpadSensitivity = useDeviceSettingsStore(state => state.trackpadSensitivity); + const mouseSensitivity = useDeviceSettingsStore(state => state.mouseSensitivity); + const clampMin = useDeviceSettingsStore(state => state.clampMin); + const clampMax = useDeviceSettingsStore(state => state.clampMax); + const blockDelay = useDeviceSettingsStore(state => state.blockDelay); + const trackpadThreshold = useDeviceSettingsStore(state => state.trackpadThreshold); + const mouseWheelHandler = useCallback( (e: WheelEvent) => { if (blockWheelEvent) return; - e.preventDefault(); - // Define a scaling factor to adjust scrolling sensitivity - const scrollSensitivity = 0.8; // Adjust this value to change scroll speed + // Determine if the wheel event is from a trackpad or a mouse wheel + const isTrackpad = Math.abs(e.deltaY) < trackpadThreshold; + + // Apply appropriate sensitivity based on input device + const scrollSensitivity = isTrackpad ? trackpadSensitivity : mouseSensitivity; // Calculate the scroll value const scroll = e.deltaY * scrollSensitivity; - // Clamp the scroll value to a reasonable range (e.g., -15 to 15) - const clampedScroll = Math.max(-4, Math.min(4, scroll)); + // Apply clamping + const clampedScroll = Math.max(clampMin, Math.min(clampMax, scroll)); // Round to the nearest integer const roundedScroll = Math.round(clampedScroll); @@ -164,13 +174,22 @@ export default function WebRTCVideo() { // Invert the scroll value to match expected behavior const invertedScroll = -roundedScroll; - console.log("wheelReport", { wheelY: invertedScroll }); send("wheelReport", { wheelY: invertedScroll }); + // Apply blocking delay setBlockWheelEvent(true); - setTimeout(() => setBlockWheelEvent(false), 50); + setTimeout(() => setBlockWheelEvent(false), blockDelay); }, - [blockWheelEvent, send], + [ + blockDelay, + blockWheelEvent, + clampMax, + clampMin, + mouseSensitivity, + send, + trackpadSensitivity, + trackpadThreshold, + ], ); const resetMousePosition = useCallback(() => { @@ -356,7 +375,10 @@ export default function WebRTCVideo() { videoElmRefValue.addEventListener("pointerdown", mouseMoveHandler, { signal }); videoElmRefValue.addEventListener("pointerup", mouseMoveHandler, { signal }); videoElmRefValue.addEventListener("keyup", videoKeyUpHandler, { signal }); - videoElmRefValue.addEventListener("wheel", mouseWheelHandler, { signal }); + videoElmRefValue.addEventListener("wheel", mouseWheelHandler, { + signal, + passive: true, + }); videoElmRefValue.addEventListener( "contextmenu", (e: MouseEvent) => e.preventDefault(), diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 3dfc96c..ac8ad7d 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -306,6 +306,78 @@ export const useSettingsStore = create( ), ); +export interface DeviceSettingsState { + trackpadSensitivity: number; + mouseSensitivity: number; + clampMin: number; + clampMax: number; + blockDelay: number; + trackpadThreshold: number; + scrollSensitivity: "low" | "default" | "high"; + setScrollSensitivity: (sensitivity: DeviceSettingsState["scrollSensitivity"]) => void; +} + +export const useDeviceSettingsStore = create(set => ({ + trackpadSensitivity: 3.0, + mouseSensitivity: 5.0, + clampMin: -8, + clampMax: 8, + blockDelay: 25, + trackpadThreshold: 10, + + scrollSensitivity: "default", + setScrollSensitivity: sensitivity => { + const wheelSettings: Record< + DeviceSettingsState["scrollSensitivity"], + { + trackpadSensitivity: DeviceSettingsState["trackpadSensitivity"]; + mouseSensitivity: DeviceSettingsState["mouseSensitivity"]; + clampMin: DeviceSettingsState["clampMin"]; + clampMax: DeviceSettingsState["clampMax"]; + blockDelay: DeviceSettingsState["blockDelay"]; + trackpadThreshold: DeviceSettingsState["trackpadThreshold"]; + } + > = { + low: { + trackpadSensitivity: 2.0, + mouseSensitivity: 3.0, + clampMin: -6, + clampMax: 6, + blockDelay: 30, + trackpadThreshold: 10, + }, + default: { + trackpadSensitivity: 3.0, + mouseSensitivity: 5.0, + clampMin: -8, + clampMax: 8, + blockDelay: 25, + trackpadThreshold: 10, + }, + high: { + trackpadSensitivity: 4.0, + mouseSensitivity: 6.0, + clampMin: -9, + clampMax: 9, + blockDelay: 20, + trackpadThreshold: 10, + }, + }; + + const settings = wheelSettings[sensitivity]; + + return set({ + trackpadSensitivity: settings.trackpadSensitivity, + trackpadThreshold: settings.trackpadThreshold, + mouseSensitivity: settings.mouseSensitivity, + clampMin: settings.clampMin, + clampMax: settings.clampMax, + blockDelay: settings.blockDelay, + scrollSensitivity: sensitivity, + }); + }, +})); + export interface RemoteVirtualMediaState { source: "WebRTC" | "HTTP" | "Storage" | null; mode: "CDROM" | "Disk" | null; diff --git a/ui/src/routes/devices.$id.settings.mouse.tsx b/ui/src/routes/devices.$id.settings.mouse.tsx index e7dc4b8..9956ecb 100644 --- a/ui/src/routes/devices.$id.settings.mouse.tsx +++ b/ui/src/routes/devices.$id.settings.mouse.tsx @@ -4,15 +4,22 @@ import { Checkbox } from "@/components/Checkbox"; import { GridCard } from "@/components/Card"; import PointingFinger from "@/assets/pointing-finger.svg"; import { CheckCircleIcon } from "@heroicons/react/16/solid"; -import { useSettingsStore } from "@/hooks/stores"; +import { useDeviceSettingsStore, useSettingsStore } from "@/hooks/stores"; import notifications from "@/notifications"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useJsonRpc } from "@/hooks/useJsonRpc"; import { cx } from "../cva.config"; +import { SelectMenuBasic } from "../components/SelectMenuBasic"; + +type ScrollSensitivity = "low" | "default" | "high"; export default function SettingsKeyboardMouseRoute() { const hideCursor = useSettingsStore(state => state.isCursorHidden); const setHideCursor = useSettingsStore(state => state.setCursorVisibility); + const scrollSensitivity = useDeviceSettingsStore(state => state.scrollSensitivity); + const setScrollSensitivity = useDeviceSettingsStore( + state => state.setScrollSensitivity, + ); const [jiggler, setJiggler] = useState(false); @@ -23,7 +30,12 @@ export default function SettingsKeyboardMouseRoute() { if ("error" in resp) return; setJiggler(resp.result as boolean); }); - }, [send]); + + send("getScrollSensitivity", {}, resp => { + if ("error" in resp) return; + setScrollSensitivity(resp.result as ScrollSensitivity); + }); + }, [send, setScrollSensitivity]); const handleJigglerChange = (enabled: boolean) => { send("setJigglerState", { enabled }, resp => { @@ -37,6 +49,22 @@ export default function SettingsKeyboardMouseRoute() { }); }; + const onScrollSensitivityChange = useCallback( + (e: React.ChangeEvent) => { + const sensitivity = e.target.value as ScrollSensitivity; + send("setScrollSensitivity", { sensitivity }, resp => { + if ("error" in resp) { + notifications.error( + `Failed to set scroll sensitivity: ${resp.error.data || "Unknown error"}`, + ); + } + notifications.success("Scroll sensitivity set successfully"); + setScrollSensitivity(sensitivity); + }); + }, + [send, setScrollSensitivity], + ); + return (
setHideCursor(e.target.checked)} /> + + + + { - kvmTerminal?.addEventListener("message", e => { - console.log(e.data); - }); - - return () => { - kvmTerminal?.removeEventListener("message", e => { - console.log(e.data); - }); - }; - }, [kvmTerminal]); - const outlet = useOutlet(); const location = useLocation(); const onModalClose = useCallback(() => { @@ -464,6 +454,21 @@ export default function KvmIdRoute() { }); }, [appVersion, send, setAppVersion, setSystemVersion]); + const setScrollSensitivity = useDeviceSettingsStore( + state => state.setScrollSensitivity, + ); + + // Initialize device settings + useEffect( + function initializeDeviceSettings() { + send("getScrollSensitivity", {}, resp => { + if ("error" in resp) return; + setScrollSensitivity(resp.result as DeviceSettingsState["scrollSensitivity"]); + }); + }, + [send, setScrollSensitivity], + ); + return ( {!outlet && otaState.updating && ( diff --git a/usb.go b/usb.go index 8f413c7..1318d09 100644 --- a/usb.go +++ b/usb.go @@ -332,12 +332,15 @@ func rpcWheelReport(wheelY int8) error { return errors.New("hid not initialized") } - // Accumulate the wheelY value - accumulatedWheelY += float64(wheelY) / 8.0 + // Accumulate the wheelY value with finer granularity + // Reduce divisor from 8.0 to a smaller value (e.g., 2.0 or 4.0) + accumulatedWheelY += float64(wheelY) / 4.0 - // Only send a report if the accumulated value is significant - if abs(accumulatedWheelY) >= 1.0 { - scaledWheelY := int8(accumulatedWheelY) + // Lower the threshold for sending a report (0.25 instead of 1.0) + if abs(accumulatedWheelY) >= 0.25 { + // Scale the wheel value appropriately for the HID report + // The descriptor uses an 8-bit signed value (-127 to 127) + scaledWheelY := int8(accumulatedWheelY * 0.5) // Scale down to prevent too much scrolling _, err := mouseHidFile.Write([]byte{ 2, // Report ID 2 @@ -345,7 +348,7 @@ func rpcWheelReport(wheelY int8) error { }) // Reset the accumulator, keeping any remainder - accumulatedWheelY -= float64(scaledWheelY) + accumulatedWheelY -= float64(scaledWheelY) / 0.5 // Adjust based on the scaling factor resetUserInputTime() return err