diff --git a/jsonrpc.go b/jsonrpc.go
index 298a810..64935e1 100644
--- a/jsonrpc.go
+++ b/jsonrpc.go
@@ -799,6 +799,7 @@ var rpcHandlers = map[string]RPCHandler{
"getCloudState": {Func: rpcGetCloudState},
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
+ "relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},
"wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}},
"getVideoState": {Func: rpcGetVideoState},
"getUSBState": {Func: rpcGetUSBState},
diff --git a/ui/src/components/InfoBar.tsx b/ui/src/components/InfoBar.tsx
index be94043..7c002f1 100644
--- a/ui/src/components/InfoBar.tsx
+++ b/ui/src/components/InfoBar.tsx
@@ -14,6 +14,7 @@ export default function InfoBar() {
const activeModifiers = useHidStore(state => state.activeModifiers);
const mouseX = useMouseStore(state => state.mouseX);
const mouseY = useMouseStore(state => state.mouseY);
+ const mouseMove = useMouseStore(state => state.mouseMove);
const videoClientSize = useVideoStore(
state => `${Math.round(state.clientWidth)}x${Math.round(state.clientHeight)}`,
@@ -62,7 +63,7 @@ export default function InfoBar() {
) : null}
- {settings.debugMode ? (
+ {(settings.debugMode && settings.mouseMode == "absolute") ? (
USB State:
diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx
index 1587d29..29c72d1 100644
--- a/ui/src/components/WebRTCVideo.tsx
+++ b/ui/src/components/WebRTCVideo.tsx
@@ -29,6 +29,7 @@ export default function WebRTCVideo() {
const settings = useSettingsStore();
const { sendKeyboardEvent, resetKeyboardState } = useKeyboard();
const setMousePosition = useMouseStore(state => state.setMousePosition);
+ const setMouseMove = useMouseStore(state => state.setMouseMove);
const {
setClientSize: setVideoClientSize,
setSize: setVideoSize,
@@ -93,19 +94,44 @@ export default function WebRTCVideo() {
);
// Mouse-related
- const sendMouseMovement = useCallback(
+ const calcDelta = (pos: number) => Math.abs(pos) < 10 ? pos * 2 : pos;
+ const sendRelMouseMovement = useCallback(
(x: number, y: number, buttons: number) => {
- send("absMouseReport", { x, y, buttons });
+ if (settings.mouseMode !== "relative") return;
+ // if we ignore the event, double-click will not work
+ // if (x === 0 && y === 0 && buttons === 0) return;
+ send("relMouseReport", { dx: calcDelta(x), dy: calcDelta(y), buttons });
+ setMouseMove({ x, y, buttons });
+ },
+ [send, setMouseMove, settings.mouseMode],
+ );
+ const relMouseMoveHandler = useCallback(
+ (e: MouseEvent) => {
+ if (settings.mouseMode !== "relative") return;
+
+ // Send mouse movement
+ const { buttons } = e;
+ sendRelMouseMovement(e.movementX, e.movementY, buttons);
+ },
+ [sendRelMouseMovement, settings.mouseMode],
+ );
+
+ const sendAbsMouseMovement = useCallback(
+ (x: number, y: number, buttons: number) => {
+ if (settings.mouseMode !== "absolute") return;
+ send("absMouseReport", { x, y, buttons });
// We set that for the debug info bar
setMousePosition(x, y);
},
- [send, setMousePosition],
+ [send, setMousePosition, settings.mouseMode],
);
- const mouseMoveHandler = useCallback(
+ const absMouseMoveHandler = useCallback(
(e: MouseEvent) => {
if (!videoClientWidth || !videoClientHeight) return;
+ if (settings.mouseMode !== "absolute") return;
+
// Get the aspect ratios of the video element and the video stream
const videoElementAspectRatio = videoClientWidth / videoClientHeight;
const videoStreamAspectRatio = videoWidth / videoHeight;
@@ -140,9 +166,9 @@ export default function WebRTCVideo() {
// Send mouse movement
const { buttons } = e;
- sendMouseMovement(x, y, buttons);
+ sendAbsMouseMovement(x, y, buttons);
},
- [sendMouseMovement, videoClientHeight, videoClientWidth, videoWidth, videoHeight],
+ [sendAbsMouseMovement, videoClientHeight, videoClientWidth, videoWidth, videoHeight, settings.mouseMode],
);
const trackpadSensitivity = useDeviceSettingsStore(state => state.trackpadSensitivity);
@@ -193,8 +219,8 @@ export default function WebRTCVideo() {
);
const resetMousePosition = useCallback(() => {
- sendMouseMovement(0, 0, 0);
- }, [sendMouseMovement]);
+ sendAbsMouseMovement(0, 0, 0);
+ }, [sendAbsMouseMovement]);
// Keyboard-related
const handleModifierKeys = useCallback(
@@ -371,9 +397,9 @@ export default function WebRTCVideo() {
const abortController = new AbortController();
const signal = abortController.signal;
- videoElmRefValue.addEventListener("mousemove", mouseMoveHandler, { signal });
- videoElmRefValue.addEventListener("pointerdown", mouseMoveHandler, { signal });
- videoElmRefValue.addEventListener("pointerup", mouseMoveHandler, { signal });
+ videoElmRefValue.addEventListener("mousemove", absMouseMoveHandler, { signal });
+ videoElmRefValue.addEventListener("pointerdown", absMouseMoveHandler, { signal });
+ videoElmRefValue.addEventListener("pointerup", absMouseMoveHandler, { signal });
videoElmRefValue.addEventListener("keyup", videoKeyUpHandler, { signal });
videoElmRefValue.addEventListener("wheel", mouseWheelHandler, {
signal,
@@ -395,7 +421,7 @@ export default function WebRTCVideo() {
};
},
[
- mouseMoveHandler,
+ absMouseMoveHandler,
resetMousePosition,
onVideoPlaying,
mouseWheelHandler,
@@ -403,6 +429,31 @@ export default function WebRTCVideo() {
],
);
+ useEffect(
+ function setupRelativeMouseEventListeners() {
+ if (settings.mouseMode !== "relative") return;
+
+ const abortController = new AbortController();
+ const signal = abortController.signal;
+
+ // bind to body to capture all mouse events
+ const body = document.querySelector("body");
+ if (!body) return;
+
+ body.addEventListener("mousemove", relMouseMoveHandler, { signal });
+ body.addEventListener("pointerdown", relMouseMoveHandler, { signal });
+ body.addEventListener("pointerup", relMouseMoveHandler, { signal });
+
+ return () => {
+ abortController.abort();
+
+ body.removeEventListener("mousemove", relMouseMoveHandler);
+ body.removeEventListener("pointerdown", relMouseMoveHandler);
+ body.removeEventListener("pointerup", relMouseMoveHandler);
+ };
+ }, [settings.mouseMode, relMouseMoveHandler],
+ )
+
useEffect(
function updateVideoStream() {
if (!mediaStream) return;
diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts
index ac8ad7d..f30c28c 100644
--- a/ui/src/hooks/stores.ts
+++ b/ui/src/hooks/stores.ts
@@ -197,15 +197,23 @@ export const useRTCStore = create
(set => ({
setTerminalChannel: channel => set({ terminalChannel: channel }),
}));
+interface MouseMove {
+ x: number;
+ y: number;
+ buttons: number;
+}
interface MouseState {
mouseX: number;
mouseY: number;
+ mouseMove?: MouseMove;
+ setMouseMove: (move?: MouseMove) => void;
setMousePosition: (x: number, y: number) => void;
}
export const useMouseStore = create(set => ({
mouseX: 0,
mouseY: 0,
+ setMouseMove: (move?: MouseMove) => set({ mouseMove: move }),
setMousePosition: (x, y) => set({ mouseX: x, mouseY: y }),
}));
@@ -543,12 +551,12 @@ export interface UpdateState {
setOtaState: (state: UpdateState["otaState"]) => void;
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => void;
modalView:
- | "loading"
- | "updating"
- | "upToDate"
- | "updateAvailable"
- | "updateCompleted"
- | "error";
+ | "loading"
+ | "updating"
+ | "upToDate"
+ | "updateAvailable"
+ | "updateCompleted"
+ | "error";
setModalView: (view: UpdateState["modalView"]) => void;
setUpdateErrorMessage: (errorMessage: string) => void;
updateErrorMessage: string | null;
@@ -612,12 +620,12 @@ export const useUsbConfigModalStore = create(set => ({
interface LocalAuthModalState {
modalView:
- | "createPassword"
- | "deletePassword"
- | "updatePassword"
- | "creationSuccess"
- | "deleteSuccess"
- | "updateSuccess";
+ | "createPassword"
+ | "deletePassword"
+ | "updatePassword"
+ | "creationSuccess"
+ | "deleteSuccess"
+ | "updateSuccess";
setModalView: (view: LocalAuthModalState["modalView"]) => void;
}
diff --git a/ui/src/routes/devices.$id.settings.mouse.tsx b/ui/src/routes/devices.$id.settings.mouse.tsx
index c8c351a..1d3a6cd 100644
--- a/ui/src/routes/devices.$id.settings.mouse.tsx
+++ b/ui/src/routes/devices.$id.settings.mouse.tsx
@@ -1,23 +1,27 @@
-import { SettingsPageHeader } from "@components/SettingsPageheader";
-import { SettingsItem } from "./devices.$id.settings";
-import { Checkbox } from "@/components/Checkbox";
-import { GridCard } from "@/components/Card";
+import MouseIcon from "@/assets/mouse-icon.svg";
import PointingFinger from "@/assets/pointing-finger.svg";
-import { CheckCircleIcon } from "@heroicons/react/16/solid";
+import { GridCard } from "@/components/Card";
+import { Checkbox } from "@/components/Checkbox";
import { useDeviceSettingsStore, useSettingsStore } from "@/hooks/stores";
-import notifications from "@/notifications";
-import { useCallback, useEffect, useState } from "react";
import { useJsonRpc } from "@/hooks/useJsonRpc";
-import { cx } from "../cva.config";
+import notifications from "@/notifications";
+import { SettingsPageHeader } from "@components/SettingsPageheader";
+import { CheckCircleIcon } from "@heroicons/react/16/solid";
+import { useCallback, useEffect, useState } from "react";
+import { FeatureFlag } from "../components/FeatureFlag";
import { SelectMenuBasic } from "../components/SelectMenuBasic";
import { useFeatureFlag } from "../hooks/useFeatureFlag";
-import { FeatureFlag } from "../components/FeatureFlag";
+import { SettingsItem } from "./devices.$id.settings";
type ScrollSensitivity = "low" | "default" | "high";
export default function SettingsKeyboardMouseRoute() {
const hideCursor = useSettingsStore(state => state.isCursorHidden);
const setHideCursor = useSettingsStore(state => state.setCursorVisibility);
+
+ const mouseMode = useSettingsStore(state => state.mouseMode);
+ const setMouseMode = useSettingsStore(state => state.setMouseMode);
+
const scrollSensitivity = useDeviceSettingsStore(state => state.scrollSensitivity);
const setScrollSensitivity = useDeviceSettingsStore(
state => state.setScrollSensitivity,
@@ -122,19 +126,19 @@ export default function SettingsKeyboardMouseRoute() {
-
+