mirror of
https://github.com/luckfox-eng29/kvm.git
synced 2026-05-28 09:01:22 +02:00
feat(mobile): adapt touch mouse behavior
Signed-off-by: luckfox-eng29 <eng29@luckfox.com>
This commit is contained in:
@@ -13,9 +13,10 @@ export const VideoElement = forwardRef<HTMLVideoElement, VideoElementProps>(
|
|||||||
({ onPlaying, style, className }, ref) => {
|
({ onPlaying, style, className }, ref) => {
|
||||||
const setVirtualKeyboardEnabled = useHidStore(state => state.setVirtualKeyboardEnabled);
|
const setVirtualKeyboardEnabled = useHidStore(state => state.setVirtualKeyboardEnabled);
|
||||||
const isVirtualKeyboardEnabled = useHidStore(state => state.isVirtualKeyboardEnabled);
|
const isVirtualKeyboardEnabled = useHidStore(state => state.isVirtualKeyboardEnabled);
|
||||||
|
const allowTapToOpenVirtualKeyboard = useHidStore(state => state.allowTapToOpenVirtualKeyboard);
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (isMobile && !isVirtualKeyboardEnabled) {
|
if (isMobile && allowTapToOpenVirtualKeyboard && !isVirtualKeyboardEnabled) {
|
||||||
setVirtualKeyboardEnabled(true);
|
setVirtualKeyboardEnabled(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -46,4 +47,4 @@ export const VideoElement = forwardRef<HTMLVideoElement, VideoElementProps>(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
VideoElement.displayName = "VideoElement";
|
VideoElement.displayName = "VideoElement";
|
||||||
|
|||||||
@@ -278,9 +278,10 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
|
|||||||
|
|
||||||
const setVirtualKeyboardEnabled = useHidStore(state => state.setVirtualKeyboardEnabled);
|
const setVirtualKeyboardEnabled = useHidStore(state => state.setVirtualKeyboardEnabled);
|
||||||
const isVirtualKeyboardEnabled = useHidStore(state => state.isVirtualKeyboardEnabled);
|
const isVirtualKeyboardEnabled = useHidStore(state => state.isVirtualKeyboardEnabled);
|
||||||
|
const allowTapToOpenVirtualKeyboard = useHidStore(state => state.allowTapToOpenVirtualKeyboard);
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (isMobile && !isVirtualKeyboardEnabled) {
|
if (isMobile && allowTapToOpenVirtualKeyboard && !isVirtualKeyboardEnabled) {
|
||||||
setVirtualKeyboardEnabled(true);
|
setVirtualKeyboardEnabled(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -600,6 +600,8 @@ export interface HidState {
|
|||||||
|
|
||||||
isVirtualKeyboardEnabled: boolean;
|
isVirtualKeyboardEnabled: boolean;
|
||||||
setVirtualKeyboardEnabled: (enabled: boolean) => void;
|
setVirtualKeyboardEnabled: (enabled: boolean) => void;
|
||||||
|
allowTapToOpenVirtualKeyboard: boolean;
|
||||||
|
setAllowTapToOpenVirtualKeyboard: (enabled: boolean) => void;
|
||||||
|
|
||||||
isPasteModeEnabled: boolean;
|
isPasteModeEnabled: boolean;
|
||||||
setPasteModeEnabled: (enabled: boolean) => void;
|
setPasteModeEnabled: (enabled: boolean) => void;
|
||||||
@@ -665,6 +667,8 @@ export const useHidStore = create<HidState>((set, get) => ({
|
|||||||
|
|
||||||
isVirtualKeyboardEnabled: false,
|
isVirtualKeyboardEnabled: false,
|
||||||
setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }),
|
setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }),
|
||||||
|
allowTapToOpenVirtualKeyboard: true,
|
||||||
|
setAllowTapToOpenVirtualKeyboard: enabled => set({ allowTapToOpenVirtualKeyboard: enabled }),
|
||||||
|
|
||||||
isPasteModeEnabled: false,
|
isPasteModeEnabled: false,
|
||||||
setPasteModeEnabled: enabled => set({ isPasteModeEnabled: enabled }),
|
setPasteModeEnabled: enabled => set({ isPasteModeEnabled: enabled }),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { BsMouseFill, BsLockFill, BsUnlockFill } from "react-icons/bs";
|
import { BsKeyboardFill, BsLockFill, BsUnlockFill } from "react-icons/bs";
|
||||||
import { useReactAt } from "i18n-auto-extractor/react";
|
import { useReactAt } from "i18n-auto-extractor/react";
|
||||||
|
|
||||||
import VirtualKeyboard from "@components/VirtualKeyboard";
|
import VirtualKeyboard from "@components/VirtualKeyboard";
|
||||||
@@ -41,7 +41,47 @@ import VirtualMediaSource from "@/layout/components_side/VirtualMediaSource";
|
|||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import OcrOverlay from "@components/OcrOverlay";
|
import OcrOverlay from "@components/OcrOverlay";
|
||||||
|
|
||||||
|
const GestureIcon = ({ className = "h-4 w-4" }: { className?: string }) => (
|
||||||
|
<svg viewBox="0 0 24 24" className={className} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||||
|
<circle cx="10" cy="10" r="6.5" />
|
||||||
|
<path d="M14.8 14.8L21 21" />
|
||||||
|
<path d="M10 7.5V12.5" />
|
||||||
|
<path d="M7.5 10H12.5" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ResetViewIcon = ({ className = "h-4 w-4" }: { className?: string }) => (
|
||||||
|
<svg viewBox="0 0 24 24" className={className} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||||
|
<path d="M19 12A7 7 0 1 1 12 5" />
|
||||||
|
<path d="M12 2L12 6L16 6" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const MouseStickIcon = ({ className = "h-4 w-4" }: { className?: string }) => (
|
||||||
|
<svg viewBox="0 0 24 24" className={className} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||||
|
<rect x="7" y="3" width="10" height="18" rx="5" />
|
||||||
|
<path d="M12 3V8" />
|
||||||
|
<circle cx="12" cy="13" r="1.5" fill="currentColor" stroke="none" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const FourWayMoveIcon = ({ className = "h-5 w-5" }: { className?: string }) => (
|
||||||
|
<svg viewBox="0 0 24 24" className={className} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||||
|
<path d="M12 3V21" />
|
||||||
|
<path d="M3 12H21" />
|
||||||
|
<path d="M12 3L9 6" />
|
||||||
|
<path d="M12 3L15 6" />
|
||||||
|
<path d="M12 21L9 18" />
|
||||||
|
<path d="M12 21L15 18" />
|
||||||
|
<path d="M3 12L6 9" />
|
||||||
|
<path d="M3 12L6 15" />
|
||||||
|
<path d="M21 12L18 9" />
|
||||||
|
<path d="M21 12L18 15" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
export default function MobileDesktop({ isFullscreen }: { isFullscreen?: number }) {
|
export default function MobileDesktop({ isFullscreen }: { isFullscreen?: number }) {
|
||||||
|
const joystickSpeedLevels = [1.4, 1.05, 0.7];
|
||||||
const { $at } = useReactAt();
|
const { $at } = useReactAt();
|
||||||
const { isDark } = useTheme();
|
const { isDark } = useTheme();
|
||||||
const videoElm = useRef<HTMLVideoElement>(null);
|
const videoElm = useRef<HTMLVideoElement>(null);
|
||||||
@@ -57,50 +97,119 @@ export default function MobileDesktop({ isFullscreen }: { isFullscreen?: number
|
|||||||
const videoStream = useVideoStream(videoElm as React.RefObject<HTMLVideoElement>, audioElm as React.RefObject<HTMLAudioElement>);
|
const videoStream = useVideoStream(videoElm as React.RefObject<HTMLVideoElement>, audioElm as React.RefObject<HTMLAudioElement>);
|
||||||
const pointerLock = usePointerLock(videoElm as React.RefObject<HTMLVideoElement>);
|
const pointerLock = usePointerLock(videoElm as React.RefObject<HTMLVideoElement>);
|
||||||
useFullscreen(videoElm as React.RefObject<HTMLVideoElement>, pointerLock, isFullscreen);
|
useFullscreen(videoElm as React.RefObject<HTMLVideoElement>, pointerLock, isFullscreen);
|
||||||
const touchZoom = useTouchZoom(zoomContainerRef as React.RefObject<HTMLDivElement>);
|
const [isTouchGestureEnabled, setIsTouchGestureEnabled] = useState(true);
|
||||||
|
const touchZoom = useTouchZoom(zoomContainerRef as React.RefObject<HTMLDivElement>, isTouchGestureEnabled);
|
||||||
const { handleGlobalPaste } = usePasteHandler(pasteCaptureRef as React.RefObject<HTMLTextAreaElement>);
|
const { handleGlobalPaste } = usePasteHandler(pasteCaptureRef as React.RefObject<HTMLTextAreaElement>);
|
||||||
const keyboardEvents = useKeyboardEvents(pasteCaptureRef as React.RefObject<HTMLTextAreaElement>, isReinitializingGadget);
|
const keyboardEvents = useKeyboardEvents(pasteCaptureRef as React.RefObject<HTMLTextAreaElement>, isReinitializingGadget);
|
||||||
const [showVirtualMouseButtons, setShowVirtualMouseButtons] = useState(false);
|
const [showVirtualMouseButtons, setShowVirtualMouseButtons] = useState(false);
|
||||||
|
const [showVirtualJoystick, setShowVirtualJoystick] = useState(false);
|
||||||
|
const [joystickVector, setJoystickVector] = useState({ x: 0, y: 0 });
|
||||||
|
const [joystickSensitivity, setJoystickSensitivity] = useState(1);
|
||||||
|
const [joystickPos, setJoystickPos] = useState({ x: 16, y: 24 });
|
||||||
const [lockedButtons, setLockedButtons] = useState(0);
|
const [lockedButtons, setLockedButtons] = useState(0);
|
||||||
const mouseEvents = useMouseEvents(videoElm as React.RefObject<HTMLVideoElement>, pointerLock, touchZoom, showVirtualMouseButtons, lockedButtons);
|
const mouseEvents = useMouseEvents(videoElm as React.RefObject<HTMLVideoElement>, pointerLock, touchZoom, showVirtualMouseButtons, lockedButtons);
|
||||||
const overlays = useVideoOverlays(videoStream, pointerLock, videoEffects);
|
const overlays = useVideoOverlays(videoStream, pointerLock, videoEffects);
|
||||||
|
|
||||||
const forceHttp = useSettingsStore(state => state.forceHttp);
|
const forceHttp = useSettingsStore(state => state.forceHttp);
|
||||||
|
const mouseMode = useSettingsStore(state => state.mouseMode);
|
||||||
const mouseX = useMouseStore(state => state.mouseX);
|
const mouseX = useMouseStore(state => state.mouseX);
|
||||||
const mouseY = useMouseStore(state => state.mouseY);
|
const mouseY = useMouseStore(state => state.mouseY);
|
||||||
|
const allowTapToOpenVirtualKeyboard = useHidStore(state => state.allowTapToOpenVirtualKeyboard);
|
||||||
|
const setAllowTapToOpenVirtualKeyboard = useHidStore(state => state.setAllowTapToOpenVirtualKeyboard);
|
||||||
const [send] = useJsonRpc();
|
const [send] = useJsonRpc();
|
||||||
const [leftBtnPos, setLeftBtnPos] = useState({ x: 40, y: 40 });
|
const [leftBtnPos, setLeftBtnPos] = useState({ x: 40, y: 40 });
|
||||||
const [rightBtnPos, setRightBtnPos] = useState({ x: 120, y: 40 });
|
const [rightBtnPos, setRightBtnPos] = useState({ x: 120, y: 40 });
|
||||||
const [leftLockPos, setLeftLockPos] = useState({ x: 40, y: 110 });
|
const [wheelPos, setWheelPos] = useState({ x: 184, y: 140 });
|
||||||
const [rightLockPos, setRightLockPos] = useState({ x: 120, y: 110 });
|
|
||||||
|
|
||||||
const [draggingBtn, setDraggingBtn] = useState<"left" | "right" | "leftLock" | "rightLock" | null>(null);
|
const [draggingBtn, setDraggingBtn] = useState<"left" | "right" | "wheel" | null>(null);
|
||||||
const dragOffset = useRef({ x: 0, y: 0 });
|
const dragOffset = useRef({ x: 0, y: 0 });
|
||||||
|
const joystickAreaRef = useRef<HTMLDivElement>(null);
|
||||||
|
const joystickPointerIdRef = useRef<number | null>(null);
|
||||||
|
const joystickVectorRef = useRef({ x: 0, y: 0 });
|
||||||
|
const joystickFrameRef = useRef<number | null>(null);
|
||||||
|
const joystickLastTsRef = useRef<number | null>(null);
|
||||||
|
const joystickMovePointerIdRef = useRef<number | null>(null);
|
||||||
|
const joystickMoveHoldTimerRef = useRef<number | null>(null);
|
||||||
|
const joystickMoveEnabledRef = useRef(false);
|
||||||
|
|
||||||
const activeButtonsRef = useRef(0);
|
const activeButtonsRef = useRef(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isFullscreen) {
|
if (isFullscreen) {
|
||||||
setShowVirtualMouseButtons(false);
|
setShowVirtualMouseButtons(false);
|
||||||
|
setShowVirtualJoystick(false);
|
||||||
}
|
}
|
||||||
}, [isFullscreen]);
|
}, [isFullscreen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
joystickVectorRef.current = joystickVector;
|
||||||
|
}, [joystickVector]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showVirtualJoystick) {
|
||||||
|
joystickPointerIdRef.current = null;
|
||||||
|
joystickMovePointerIdRef.current = null;
|
||||||
|
if (joystickMoveHoldTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(joystickMoveHoldTimerRef.current);
|
||||||
|
joystickMoveHoldTimerRef.current = null;
|
||||||
|
}
|
||||||
|
joystickMoveEnabledRef.current = false;
|
||||||
|
joystickLastTsRef.current = null;
|
||||||
|
setJoystickVector({ x: 0, y: 0 });
|
||||||
|
if (joystickFrameRef.current !== null) {
|
||||||
|
cancelAnimationFrame(joystickFrameRef.current);
|
||||||
|
joystickFrameRef.current = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tick = (timestamp: number) => {
|
||||||
|
const prevTs = joystickLastTsRef.current ?? timestamp;
|
||||||
|
joystickLastTsRef.current = timestamp;
|
||||||
|
const frameScale = Math.min(2, Math.max(0.5, (timestamp - prevTs) / 16.67));
|
||||||
|
const vector = joystickVectorRef.current;
|
||||||
|
const container = containerRef.current;
|
||||||
|
const containerWidth = container?.clientWidth ?? 1280;
|
||||||
|
// Reduce sensitivity on small screens to avoid over-shooting.
|
||||||
|
const resolutionFactor = Math.max(0.35, Math.min(1, containerWidth / 1280));
|
||||||
|
const speed = 12 * resolutionFactor * joystickSensitivity;
|
||||||
|
const dx = Math.round(vector.x * speed * frameScale);
|
||||||
|
const dy = Math.round(vector.y * speed * frameScale);
|
||||||
|
if (dx !== 0 || dy !== 0) {
|
||||||
|
mouseEvents.sendVirtualRelativeMovement(dx, dy, 0);
|
||||||
|
}
|
||||||
|
joystickFrameRef.current = requestAnimationFrame(tick);
|
||||||
|
};
|
||||||
|
|
||||||
|
joystickFrameRef.current = requestAnimationFrame(tick);
|
||||||
|
return () => {
|
||||||
|
if (joystickFrameRef.current !== null) {
|
||||||
|
cancelAnimationFrame(joystickFrameRef.current);
|
||||||
|
joystickFrameRef.current = null;
|
||||||
|
}
|
||||||
|
joystickLastTsRef.current = null;
|
||||||
|
};
|
||||||
|
}, [showVirtualJoystick, mouseEvents, joystickSensitivity]);
|
||||||
|
|
||||||
const updateButtons = (mask: number, isDown: boolean) => {
|
const updateButtons = (mask: number, isDown: boolean) => {
|
||||||
if (isReinitializingGadget) return;
|
if (isReinitializingGadget) return;
|
||||||
|
|
||||||
let newButtons = activeButtonsRef.current;
|
let newButtons = activeButtonsRef.current;
|
||||||
if (isDown) {
|
if (isDown) {
|
||||||
newButtons |= mask;
|
newButtons |= mask;
|
||||||
|
} else if (lockedButtons & mask) {
|
||||||
|
// Keep pressed while lock is enabled.
|
||||||
|
newButtons |= mask;
|
||||||
} else {
|
} else {
|
||||||
newButtons &= ~mask;
|
newButtons &= ~mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDown && (lockedButtons & mask)) {
|
|
||||||
setLockedButtons(prev => prev & ~mask);
|
|
||||||
}
|
|
||||||
|
|
||||||
activeButtonsRef.current = newButtons;
|
activeButtonsRef.current = newButtons;
|
||||||
send("absMouseReport", { x: mouseX, y: mouseY, buttons: newButtons });
|
if (mouseMode === "relative") {
|
||||||
|
mouseEvents.sendVirtualRelativeMovement(0, 0, newButtons);
|
||||||
|
} else {
|
||||||
|
send("absMouseReport", { x: mouseX, y: mouseY, buttons: newButtons });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleLock = (mask: number) => {
|
const toggleLock = (mask: number) => {
|
||||||
@@ -117,10 +226,14 @@ export default function MobileDesktop({ isFullscreen }: { isFullscreen?: number
|
|||||||
}
|
}
|
||||||
|
|
||||||
activeButtonsRef.current = newButtons;
|
activeButtonsRef.current = newButtons;
|
||||||
send("absMouseReport", { x: mouseX, y: mouseY, buttons: newButtons });
|
if (mouseMode === "relative") {
|
||||||
|
mouseEvents.sendVirtualRelativeMovement(0, 0, newButtons);
|
||||||
|
} else {
|
||||||
|
send("absMouseReport", { x: mouseX, y: mouseY, buttons: newButtons });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>, type: "left" | "right" | "leftLock" | "rightLock") => {
|
const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>, type: "left" | "right" | "wheel") => {
|
||||||
const target = e.currentTarget;
|
const target = e.currentTarget;
|
||||||
const rect = target.getBoundingClientRect();
|
const rect = target.getBoundingClientRect();
|
||||||
dragOffset.current = {
|
dragOffset.current = {
|
||||||
@@ -138,26 +251,121 @@ export default function MobileDesktop({ isFullscreen }: { isFullscreen?: number
|
|||||||
const containerRect = container.getBoundingClientRect();
|
const containerRect = container.getBoundingClientRect();
|
||||||
const x = e.clientX - containerRect.left - dragOffset.current.x;
|
const x = e.clientX - containerRect.left - dragOffset.current.x;
|
||||||
const y = e.clientY - containerRect.top - dragOffset.current.y;
|
const y = e.clientY - containerRect.top - dragOffset.current.y;
|
||||||
const clampedX = Math.max(0, Math.min(containerRect.width - 56, x));
|
const dragWidth = draggingBtn === "wheel" ? 32 : 56;
|
||||||
const clampedY = Math.max(0, Math.min(containerRect.height - 56, y));
|
const dragHeight = draggingBtn === "wheel" ? 68 : 56;
|
||||||
|
const clampedX = Math.max(0, Math.min(containerRect.width - dragWidth, x));
|
||||||
|
const clampedY = Math.max(0, Math.min(containerRect.height - dragHeight, y));
|
||||||
if (draggingBtn === "left") {
|
if (draggingBtn === "left") {
|
||||||
setLeftBtnPos({ x: clampedX, y: clampedY });
|
setLeftBtnPos({ x: clampedX, y: clampedY });
|
||||||
} else if (draggingBtn === "right") {
|
} else if (draggingBtn === "right") {
|
||||||
setRightBtnPos({ x: clampedX, y: clampedY });
|
setRightBtnPos({ x: clampedX, y: clampedY });
|
||||||
} else if (draggingBtn === "leftLock") {
|
} else if (draggingBtn === "wheel") {
|
||||||
setLeftLockPos({ x: clampedX, y: clampedY });
|
setWheelPos({ x: clampedX, y: clampedY });
|
||||||
} else if (draggingBtn === "rightLock") {
|
|
||||||
setRightLockPos({ x: clampedX, y: clampedY });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePointerUp = (e: React.PointerEvent<HTMLDivElement>, type: "left" | "right" | "leftLock" | "rightLock") => {
|
const handlePointerUp = (e: React.PointerEvent<HTMLDivElement>, type: "left" | "right" | "wheel") => {
|
||||||
const wasDragging = draggingBtn === type;
|
const wasDragging = draggingBtn === type;
|
||||||
setDraggingBtn(null);
|
setDraggingBtn(null);
|
||||||
(e.currentTarget as HTMLDivElement).releasePointerCapture(e.pointerId);
|
(e.currentTarget as HTMLDivElement).releasePointerCapture(e.pointerId);
|
||||||
if (!wasDragging) return;
|
if (!wasDragging) return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateJoystickVector = (clientX: number, clientY: number) => {
|
||||||
|
const joystickElm = joystickAreaRef.current;
|
||||||
|
if (!joystickElm) return;
|
||||||
|
const rect = joystickElm.getBoundingClientRect();
|
||||||
|
const centerX = rect.left + rect.width / 2;
|
||||||
|
const centerY = rect.top + rect.height / 2;
|
||||||
|
const rawX = clientX - centerX;
|
||||||
|
const rawY = clientY - centerY;
|
||||||
|
const maxRadius = 32;
|
||||||
|
const length = Math.hypot(rawX, rawY);
|
||||||
|
if (!length || length <= maxRadius) {
|
||||||
|
setJoystickVector({ x: rawX / maxRadius, y: rawY / maxRadius });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const scale = maxRadius / length;
|
||||||
|
setJoystickVector({ x: (rawX * scale) / maxRadius, y: (rawY * scale) / maxRadius });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleJoystickPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||||
|
if (joystickMovePointerIdRef.current !== null) return;
|
||||||
|
e.preventDefault();
|
||||||
|
joystickPointerIdRef.current = e.pointerId;
|
||||||
|
e.currentTarget.setPointerCapture(e.pointerId);
|
||||||
|
updateJoystickVector(e.clientX, e.clientY);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleJoystickPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||||
|
if (joystickPointerIdRef.current !== e.pointerId) return;
|
||||||
|
e.preventDefault();
|
||||||
|
updateJoystickVector(e.clientX, e.clientY);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleJoystickPointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||||
|
if (joystickPointerIdRef.current !== e.pointerId) return;
|
||||||
|
joystickPointerIdRef.current = null;
|
||||||
|
e.currentTarget.releasePointerCapture(e.pointerId);
|
||||||
|
setJoystickVector({ x: 0, y: 0 });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleJoystickMoveStart = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||||
|
joystickMovePointerIdRef.current = e.pointerId;
|
||||||
|
joystickMoveEnabledRef.current = false;
|
||||||
|
if (joystickMoveHoldTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(joystickMoveHoldTimerRef.current);
|
||||||
|
}
|
||||||
|
joystickMoveHoldTimerRef.current = window.setTimeout(() => {
|
||||||
|
joystickMoveEnabledRef.current = true;
|
||||||
|
}, 350);
|
||||||
|
e.currentTarget.setPointerCapture(e.pointerId);
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleJoystickMove = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||||
|
if (joystickMovePointerIdRef.current !== e.pointerId) return;
|
||||||
|
if (!joystickMoveEnabledRef.current) return;
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
const containerRect = container.getBoundingClientRect();
|
||||||
|
const joystickSize = 80;
|
||||||
|
const nextLeft = e.clientX - containerRect.left - joystickSize / 2;
|
||||||
|
const nextTop = e.clientY - containerRect.top - joystickSize / 2;
|
||||||
|
const clampedLeft = Math.max(0, Math.min(containerRect.width - joystickSize, nextLeft));
|
||||||
|
const clampedTop = Math.max(0, Math.min(containerRect.height - joystickSize, nextTop));
|
||||||
|
const nextBottom = containerRect.height - joystickSize - clampedTop;
|
||||||
|
setJoystickPos({ x: clampedLeft, y: nextBottom });
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleJoystickMoveEnd = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||||
|
if (joystickMovePointerIdRef.current !== e.pointerId) return;
|
||||||
|
if (joystickMoveHoldTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(joystickMoveHoldTimerRef.current);
|
||||||
|
joystickMoveHoldTimerRef.current = null;
|
||||||
|
}
|
||||||
|
joystickMoveEnabledRef.current = false;
|
||||||
|
joystickMovePointerIdRef.current = null;
|
||||||
|
e.currentTarget.releasePointerCapture(e.pointerId);
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMouseControlEnabled = showVirtualJoystick || showVirtualMouseButtons;
|
||||||
|
const joystickSpeedIndex = joystickSpeedLevels.reduce((bestIndex, value, index, arr) => {
|
||||||
|
const bestDistance = Math.abs(arr[bestIndex] - joystickSensitivity);
|
||||||
|
const currentDistance = Math.abs(value - joystickSensitivity);
|
||||||
|
return currentDistance < bestDistance ? index : bestIndex;
|
||||||
|
}, 0);
|
||||||
|
const toggleMouseControl = () => {
|
||||||
|
const nextEnabled = !isMouseControlEnabled;
|
||||||
|
setShowVirtualJoystick(nextEnabled);
|
||||||
|
setShowVirtualMouseButtons(nextEnabled);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const keyboardCleanup = keyboardEvents.setupKeyboardEvents();
|
const keyboardCleanup = keyboardEvents.setupKeyboardEvents();
|
||||||
const videoCleanup = videoStream.setupVideoEventListeners();
|
const videoCleanup = videoStream.setupVideoEventListeners();
|
||||||
@@ -303,107 +511,292 @@ export default function MobileDesktop({ isFullscreen }: { isFullscreen?: number
|
|||||||
className="pointer-events-none absolute inset-0"
|
className="pointer-events-none absolute inset-0"
|
||||||
onPointerMove={handlePointerMove}
|
onPointerMove={handlePointerMove}
|
||||||
>
|
>
|
||||||
<div
|
<div className="pointer-events-auto absolute right-3 top-3 grid grid-cols-[auto_auto_auto] grid-rows-2 gap-1.5">
|
||||||
className={cx(
|
<div
|
||||||
"pointer-events-auto absolute right-3 top-3 flex h-8 w-8 items-center justify-center rounded-full text-white text-xs",
|
className={cx(
|
||||||
isDark ? "bg-gray-500/70" : "bg-black/30",
|
"flex h-8 w-8 items-center justify-center rounded-full text-white",
|
||||||
)}
|
isTouchGestureEnabled
|
||||||
style={{
|
? "bg-green-600/80"
|
||||||
touchAction: "none",
|
: "bg-gray-500/70",
|
||||||
}}
|
)}
|
||||||
onClick={() => setShowVirtualMouseButtons(prev => !prev)}
|
style={{
|
||||||
>
|
touchAction: "none",
|
||||||
<BsMouseFill className="h-4 w-4" />
|
}}
|
||||||
|
onClick={() => setIsTouchGestureEnabled(prev => !prev)}
|
||||||
|
>
|
||||||
|
<GestureIcon />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"flex h-8 w-8 items-center justify-center rounded-full text-white",
|
||||||
|
isMouseControlEnabled
|
||||||
|
? "bg-green-600/80"
|
||||||
|
: "bg-gray-500/70",
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
touchAction: "none",
|
||||||
|
}}
|
||||||
|
onClick={toggleMouseControl}
|
||||||
|
>
|
||||||
|
<MouseStickIcon />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"flex h-8 w-8 items-center justify-center rounded-full text-white",
|
||||||
|
allowTapToOpenVirtualKeyboard
|
||||||
|
? "bg-green-600/80"
|
||||||
|
: "bg-gray-500/70",
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
touchAction: "none",
|
||||||
|
}}
|
||||||
|
onClick={() => setAllowTapToOpenVirtualKeyboard(!allowTapToOpenVirtualKeyboard)}
|
||||||
|
>
|
||||||
|
<BsKeyboardFill className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"col-span-3 flex h-8 items-center justify-center rounded-full text-white",
|
||||||
|
isDark ? "bg-gray-500/70" : "bg-black/30",
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
touchAction: "none",
|
||||||
|
}}
|
||||||
|
onClick={() => touchZoom.resetTransform()}
|
||||||
|
>
|
||||||
|
<ResetViewIcon />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{showVirtualMouseButtons && (
|
{showVirtualMouseButtons && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
"pointer-events-auto absolute flex h-14 w-14 items-center justify-center rounded-full text-white text-xs active:scale-90 transition-transform duration-100",
|
"pointer-events-auto absolute flex h-14 w-14 items-center justify-center rounded-full text-white text-xs active:scale-90 transition-transform duration-100 relative",
|
||||||
isDark ? "bg-gray-500/70" : "bg-black/30",
|
(lockedButtons & 1) ? "bg-green-600/80" : (isDark ? "bg-gray-500/70" : "bg-black/30"),
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
left: leftBtnPos.x,
|
left: leftBtnPos.x,
|
||||||
top: leftBtnPos.y,
|
top: leftBtnPos.y,
|
||||||
touchAction: "none",
|
touchAction: "none",
|
||||||
}}
|
}}
|
||||||
onPointerDown={e => {
|
onPointerDown={() => {
|
||||||
handlePointerDown(e, "left");
|
|
||||||
updateButtons(1, true);
|
updateButtons(1, true);
|
||||||
}}
|
}}
|
||||||
onPointerUp={e => {
|
onPointerUp={() => {
|
||||||
handlePointerUp(e, "left");
|
|
||||||
updateButtons(1, false);
|
updateButtons(1, false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
L
|
L
|
||||||
|
<div
|
||||||
|
className="absolute -left-1 -top-1 flex h-5 w-5 items-center justify-center rounded-full bg-black/45 text-white"
|
||||||
|
onPointerDown={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePointerDown(e, "left");
|
||||||
|
}}
|
||||||
|
onPointerMove={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePointerMove(e);
|
||||||
|
}}
|
||||||
|
onPointerUp={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePointerUp(e, "left");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FourWayMoveIcon className="h-3 w-3" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"absolute -right-1 -top-1 flex h-5 w-5 items-center justify-center rounded-full text-white",
|
||||||
|
(lockedButtons & 1) ? "bg-green-600/90" : "bg-black/45",
|
||||||
|
)}
|
||||||
|
onPointerDown={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleLock(1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(lockedButtons & 1) ? <BsLockFill className="h-3 w-3" /> : <BsUnlockFill className="h-3 w-3" />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
"pointer-events-auto absolute flex h-14 w-14 items-center justify-center rounded-full text-white text-xs active:scale-90 transition-transform duration-100",
|
"pointer-events-auto absolute flex h-14 w-14 items-center justify-center rounded-full text-white text-xs active:scale-90 transition-transform duration-100 relative",
|
||||||
(lockedButtons & 1) ? "bg-green-600/80" : (isDark ? "bg-gray-500/70" : "bg-black/30"),
|
(lockedButtons & 2) ? "bg-green-600/80" : (isDark ? "bg-gray-500/70" : "bg-black/30"),
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
left: leftLockPos.x,
|
|
||||||
top: leftLockPos.y,
|
|
||||||
touchAction: "none",
|
|
||||||
}}
|
|
||||||
onPointerDown={e => {
|
|
||||||
handlePointerDown(e, "leftLock");
|
|
||||||
}}
|
|
||||||
onPointerUp={e => {
|
|
||||||
handlePointerUp(e, "leftLock");
|
|
||||||
}}
|
|
||||||
onClick={() => toggleLock(1)}
|
|
||||||
>
|
|
||||||
{(lockedButtons & 1) ? <BsLockFill /> : <BsUnlockFill />} L
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={cx(
|
|
||||||
"pointer-events-auto absolute flex h-14 w-14 items-center justify-center rounded-full text-white text-xs active:scale-90 transition-transform duration-100",
|
|
||||||
isDark ? "bg-gray-500/70" : "bg-black/30",
|
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
left: rightBtnPos.x,
|
left: rightBtnPos.x,
|
||||||
top: rightBtnPos.y,
|
top: rightBtnPos.y,
|
||||||
touchAction: "none",
|
touchAction: "none",
|
||||||
}}
|
}}
|
||||||
onPointerDown={e => {
|
onPointerDown={() => {
|
||||||
handlePointerDown(e, "right");
|
|
||||||
updateButtons(2, true);
|
updateButtons(2, true);
|
||||||
}}
|
}}
|
||||||
onPointerUp={e => {
|
onPointerUp={() => {
|
||||||
handlePointerUp(e, "right");
|
|
||||||
updateButtons(2, false);
|
updateButtons(2, false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
R
|
R
|
||||||
|
<div
|
||||||
|
className="absolute -left-1 -top-1 flex h-5 w-5 items-center justify-center rounded-full bg-black/45 text-white"
|
||||||
|
onPointerDown={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePointerDown(e, "right");
|
||||||
|
}}
|
||||||
|
onPointerMove={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePointerMove(e);
|
||||||
|
}}
|
||||||
|
onPointerUp={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePointerUp(e, "right");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FourWayMoveIcon className="h-3 w-3" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"absolute -right-1 -top-1 flex h-5 w-5 items-center justify-center rounded-full text-white",
|
||||||
|
(lockedButtons & 2) ? "bg-green-600/90" : "bg-black/45",
|
||||||
|
)}
|
||||||
|
onPointerDown={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleLock(2);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(lockedButtons & 2) ? <BsLockFill className="h-3 w-3" /> : <BsUnlockFill className="h-3 w-3" />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className="pointer-events-auto absolute"
|
||||||
"pointer-events-auto absolute flex h-14 w-14 items-center justify-center rounded-full text-white text-xs active:scale-90 transition-transform duration-100",
|
|
||||||
(lockedButtons & 2) ? "bg-green-600/80" : (isDark ? "bg-gray-500/70" : "bg-black/30"),
|
|
||||||
)}
|
|
||||||
style={{
|
style={{
|
||||||
left: rightLockPos.x,
|
left: wheelPos.x,
|
||||||
top: rightLockPos.y,
|
top: wheelPos.y,
|
||||||
touchAction: "none",
|
touchAction: "none",
|
||||||
}}
|
}}
|
||||||
onPointerDown={e => {
|
|
||||||
handlePointerDown(e, "rightLock");
|
|
||||||
}}
|
|
||||||
onPointerUp={e => {
|
|
||||||
handlePointerUp(e, "rightLock");
|
|
||||||
}}
|
|
||||||
onClick={() => toggleLock(2)}
|
|
||||||
>
|
>
|
||||||
{(lockedButtons & 2) ? <BsLockFill /> : <BsUnlockFill />} R
|
<div
|
||||||
|
className={cx(
|
||||||
|
"flex h-8 w-8 items-center justify-center rounded-full text-white text-xs active:scale-90 transition-transform duration-100",
|
||||||
|
isDark ? "bg-gray-500/70" : "bg-black/30",
|
||||||
|
)}
|
||||||
|
onClick={() => send("wheelReport", { wheelY: 1 })}
|
||||||
|
>
|
||||||
|
▲
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="mt-1 flex h-5 w-8 items-center justify-center rounded-full bg-black/45 text-white"
|
||||||
|
onPointerDown={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePointerDown(e, "wheel");
|
||||||
|
}}
|
||||||
|
onPointerMove={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePointerMove(e);
|
||||||
|
}}
|
||||||
|
onPointerUp={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePointerUp(e, "wheel");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FourWayMoveIcon className="h-3 w-3" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"mt-1 flex h-8 w-8 items-center justify-center rounded-full text-white text-xs active:scale-90 transition-transform duration-100",
|
||||||
|
isDark ? "bg-gray-500/70" : "bg-black/30",
|
||||||
|
)}
|
||||||
|
onClick={() => send("wheelReport", { wheelY: -1 })}
|
||||||
|
>
|
||||||
|
▼
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{showVirtualJoystick && (
|
||||||
|
<div
|
||||||
|
className="pointer-events-auto absolute"
|
||||||
|
style={{ left: joystickPos.x, bottom: joystickPos.y }}
|
||||||
|
>
|
||||||
|
<div className="absolute -top-7 left-0 flex items-center gap-1">
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"flex h-6 w-6 items-center justify-center rounded-full text-white transition-transform duration-100 active:scale-95",
|
||||||
|
isDark ? "bg-gray-500/70" : "bg-black/30",
|
||||||
|
)}
|
||||||
|
style={{ touchAction: "none" }}
|
||||||
|
onPointerDown={handleJoystickMoveStart}
|
||||||
|
onPointerMove={handleJoystickMove}
|
||||||
|
onPointerUp={handleJoystickMoveEnd}
|
||||||
|
onPointerCancel={handleJoystickMoveEnd}
|
||||||
|
>
|
||||||
|
<FourWayMoveIcon className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="absolute right-[-36px] top-0 flex h-20 items-center">
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"relative flex h-16 w-3 flex-col justify-between rounded-full py-1",
|
||||||
|
isDark ? "bg-white/25" : "bg-black/20",
|
||||||
|
)}
|
||||||
|
style={{ touchAction: "none" }}
|
||||||
|
>
|
||||||
|
{joystickSpeedLevels.map((level, index) => (
|
||||||
|
<button
|
||||||
|
key={level}
|
||||||
|
type="button"
|
||||||
|
className={cx(
|
||||||
|
"relative z-10 h-3 w-3 rounded-full border",
|
||||||
|
joystickSpeedIndex === index
|
||||||
|
? "border-blue-300 bg-blue-400"
|
||||||
|
: (isDark ? "border-white/60 bg-white/40" : "border-black/40 bg-black/20"),
|
||||||
|
)}
|
||||||
|
onClick={() => setJoystickSensitivity(level)}
|
||||||
|
aria-label={`Set joystick speed ${level.toFixed(2)}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute left-full top-1/2 ml-[1px] -translate-y-1/2 border-y-[4px] border-l-[6px] border-y-transparent border-l-blue-400"
|
||||||
|
style={{
|
||||||
|
top: `${(joystickSpeedIndex / (joystickSpeedLevels.length - 1)) * 100}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="ml-1 flex h-16 flex-col justify-between text-[8px] text-white/80">
|
||||||
|
<span>Fast</span>
|
||||||
|
<span>Slow</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={joystickAreaRef}
|
||||||
|
className={cx(
|
||||||
|
"flex h-20 w-20 items-center justify-center rounded-full border",
|
||||||
|
isDark ? "border-white/40 bg-black/20" : "border-black/30 bg-white/20",
|
||||||
|
)}
|
||||||
|
style={{ touchAction: "none" }}
|
||||||
|
onPointerDown={handleJoystickPointerDown}
|
||||||
|
onPointerMove={handleJoystickPointerMove}
|
||||||
|
onPointerUp={handleJoystickPointerUp}
|
||||||
|
onPointerCancel={handleJoystickPointerUp}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"h-9 w-9 rounded-full",
|
||||||
|
isDark ? "bg-white/70" : "bg-black/50",
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
transform: `translate(${joystickVector.x * 24}px, ${joystickVector.y * 24}px)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<VirtualKeyboard />
|
<VirtualKeyboard />
|
||||||
|
|||||||
@@ -25,12 +25,13 @@ export const useMouseEvents = (
|
|||||||
const { setMousePosition, setMouseMove } = useMouseStore();
|
const { setMousePosition, setMouseMove } = useMouseStore();
|
||||||
const { width: videoWidth, height: videoHeight } = useVideoStore();
|
const { width: videoWidth, height: videoHeight } = useVideoStore();
|
||||||
const isReinitializingGadget = useHidStore(state => state.isReinitializingGadget);
|
const isReinitializingGadget = useHidStore(state => state.isReinitializingGadget);
|
||||||
|
const touchDragActiveRef = useRef(false);
|
||||||
|
|
||||||
const calcDelta = (pos: number) => (Math.abs(pos) < 10 ? pos * 2 : pos);
|
const calcDelta = (pos: number) => (Math.abs(pos) < 10 ? pos * 2 : pos);
|
||||||
|
|
||||||
const sendRelMouseMovement = useCallback(
|
const sendRelMouseMovement = useCallback(
|
||||||
(x: number, y: number, buttons: number) => {
|
(x: number, y: number, buttons: number, force = false) => {
|
||||||
if (settings.mouseMode !== "relative") return;
|
if (!force && settings.mouseMode !== "relative") return;
|
||||||
// Don't send mouse events while reinitializing gadget
|
// Don't send mouse events while reinitializing gadget
|
||||||
if (isReinitializingGadget) return;
|
if (isReinitializingGadget) return;
|
||||||
send("relMouseReport", { dx: calcDelta(x), dy: calcDelta(y), buttons });
|
send("relMouseReport", { dx: calcDelta(x), dy: calcDelta(y), buttons });
|
||||||
@@ -50,6 +51,13 @@ export const useMouseEvents = (
|
|||||||
[send, setMousePosition, settings.mouseMode, isReinitializingGadget],
|
[send, setMousePosition, settings.mouseMode, isReinitializingGadget],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sendVirtualRelativeMovement = useCallback(
|
||||||
|
(x: number, y: number, buttons = 0) => {
|
||||||
|
sendRelMouseMovement(x, y, buttons, true);
|
||||||
|
},
|
||||||
|
[sendRelMouseMovement],
|
||||||
|
);
|
||||||
|
|
||||||
const relMouseMoveHandler = useCallback(
|
const relMouseMoveHandler = useCallback(
|
||||||
(e: MouseEvent) => {
|
(e: MouseEvent) => {
|
||||||
const pt = (e as unknown as PointerEvent).pointerType as unknown as string;
|
const pt = (e as unknown as PointerEvent).pointerType as unknown as string;
|
||||||
@@ -76,11 +84,17 @@ export const useMouseEvents = (
|
|||||||
const absMouseMoveHandler = useCallback(
|
const absMouseMoveHandler = useCallback(
|
||||||
(e: MouseEvent) => {
|
(e: MouseEvent) => {
|
||||||
const pt = (e as unknown as PointerEvent).pointerType as unknown as string;
|
const pt = (e as unknown as PointerEvent).pointerType as unknown as string;
|
||||||
|
const pointerEvent = e as unknown as PointerEvent;
|
||||||
|
const eventType = pointerEvent.type;
|
||||||
if (pt === "touch") {
|
if (pt === "touch") {
|
||||||
if (touchZoom) {
|
if (touchZoom) {
|
||||||
const touchCount = touchZoom.activeTouchPointers.current.size;
|
const touchCount = touchZoom.activeTouchPointers.current.size;
|
||||||
const eventType = (e as unknown as PointerEvent).type;
|
if (touchCount >= 2) {
|
||||||
if (touchCount >= 2 && eventType !== "pointerup") return;
|
if (eventType === "pointerup" || eventType === "pointercancel") {
|
||||||
|
touchDragActiveRef.current = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,28 +149,20 @@ export const useMouseEvents = (
|
|||||||
let buttons = e.buttons;
|
let buttons = e.buttons;
|
||||||
|
|
||||||
if (pt === "touch") {
|
if (pt === "touch") {
|
||||||
const touchCount = touchZoom ? touchZoom.activeTouchPointers.current.size : 1;
|
if (eventType === "pointerdown") {
|
||||||
const pointerEvent = e as unknown as PointerEvent;
|
touchDragActiveRef.current = !disableTouchClick;
|
||||||
const eventType = pointerEvent.type;
|
}
|
||||||
|
if (eventType === "pointerup" || eventType === "pointercancel") {
|
||||||
if (eventType === "pointerup") {
|
|
||||||
if (touchCount >= 2 || disableTouchClick) {
|
|
||||||
buttons = 0;
|
|
||||||
} else {
|
|
||||||
buttons = 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
buttons = 0;
|
buttons = 0;
|
||||||
|
touchDragActiveRef.current = false;
|
||||||
|
} else {
|
||||||
|
buttons = touchDragActiveRef.current ? 1 : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buttons |= externalButtons;
|
buttons |= externalButtons;
|
||||||
|
|
||||||
sendAbsMouseMovement(x, y, buttons);
|
sendAbsMouseMovement(x, y, buttons);
|
||||||
|
|
||||||
if (pt === "touch" && buttons !== externalButtons && (e as unknown as PointerEvent).type === "pointerup") {
|
|
||||||
sendAbsMouseMovement(x, y, externalButtons);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[settings.mouseMode, videoElm, videoWidth, videoHeight, sendAbsMouseMovement, touchZoom, disableTouchClick, externalButtons],
|
[settings.mouseMode, videoElm, videoWidth, videoHeight, sendAbsMouseMovement, touchZoom, disableTouchClick, externalButtons],
|
||||||
);
|
);
|
||||||
@@ -216,8 +222,10 @@ export const useMouseEvents = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
videoElmRefValue.addEventListener("mousemove", eventHandler, { signal });
|
videoElmRefValue.addEventListener("mousemove", eventHandler, { signal });
|
||||||
|
videoElmRefValue.addEventListener("pointermove", eventHandler, { signal });
|
||||||
videoElmRefValue.addEventListener("pointerdown", eventHandler, { signal });
|
videoElmRefValue.addEventListener("pointerdown", eventHandler, { signal });
|
||||||
videoElmRefValue.addEventListener("pointerup", eventHandler, { signal });
|
videoElmRefValue.addEventListener("pointerup", eventHandler, { signal });
|
||||||
|
videoElmRefValue.addEventListener("pointercancel", eventHandler, { signal });
|
||||||
videoElmRefValue.addEventListener("wheel", mouseWheelHandler, {
|
videoElmRefValue.addEventListener("wheel", mouseWheelHandler, {
|
||||||
signal,
|
signal,
|
||||||
passive: true,
|
passive: true,
|
||||||
@@ -255,5 +263,6 @@ export const useMouseEvents = (
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
setupMouseEvents,
|
setupMouseEvents,
|
||||||
|
sendVirtualRelativeMovement,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
export const useTouchZoom = (
|
export const useTouchZoom = (
|
||||||
containerRef: React.RefObject<HTMLDivElement>
|
containerRef: React.RefObject<HTMLDivElement>,
|
||||||
|
gestureEnabled = true,
|
||||||
) => {
|
) => {
|
||||||
const [mobileScale, setMobileScale] = useState(1);
|
const [mobileScale, setMobileScale] = useState(1);
|
||||||
const [mobileTx, setMobileTx] = useState(0);
|
const [mobileTx, setMobileTx] = useState(0);
|
||||||
@@ -17,67 +18,83 @@ export const useTouchZoom = (
|
|||||||
if (!el) return;
|
if (!el) return;
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
const signal = abortController.signal;
|
const signal = abortController.signal;
|
||||||
|
const isPointInVideo = (x: number, y: number) => {
|
||||||
|
const video = el.querySelector("video") as HTMLVideoElement | null;
|
||||||
|
if (!video) return false;
|
||||||
|
const vRect = video.getBoundingClientRect();
|
||||||
|
return x >= vRect.left && x <= vRect.right && y >= vRect.top && y <= vRect.bottom;
|
||||||
|
};
|
||||||
|
|
||||||
const onPointerDown = (e: PointerEvent) => {
|
const onPointerDown = (e: PointerEvent) => {
|
||||||
if (e.pointerType !== "touch") return;
|
if (e.pointerType !== "touch") return;
|
||||||
activeTouchPointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
activeTouchPointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
||||||
|
if (!gestureEnabled) return;
|
||||||
|
let shouldHandleLocalGesture = false;
|
||||||
|
|
||||||
if (activeTouchPointers.current.size === 1) {
|
if (activeTouchPointers.current.size === 1) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
let isInVideo = false;
|
const isInVideo = isPointInVideo(e.clientX, e.clientY);
|
||||||
const video = el.querySelector("video") as HTMLVideoElement | null;
|
|
||||||
if (video) {
|
|
||||||
const vRect = video.getBoundingClientRect();
|
|
||||||
if (
|
|
||||||
e.clientX >= vRect.left &&
|
|
||||||
e.clientX <= vRect.right &&
|
|
||||||
e.clientY >= vRect.top &&
|
|
||||||
e.clientY <= vRect.bottom
|
|
||||||
) {
|
|
||||||
isInVideo = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!isInVideo) {
|
if (!isInVideo) {
|
||||||
if (now - lastTapAt.current < 300) {
|
if (now - lastTapAt.current < 300) {
|
||||||
setMobileScale(1);
|
setMobileScale(1);
|
||||||
setMobileTx(0);
|
setMobileTx(0);
|
||||||
setMobileTy(0);
|
setMobileTy(0);
|
||||||
}
|
}
|
||||||
|
shouldHandleLocalGesture = true;
|
||||||
}
|
}
|
||||||
lastTapAt.current = now;
|
lastTapAt.current = now;
|
||||||
lastPanPoint.current = { x: e.clientX, y: e.clientY };
|
if (mobileScale > 1) {
|
||||||
|
lastPanPoint.current = { x: e.clientX, y: e.clientY };
|
||||||
|
shouldHandleLocalGesture = true;
|
||||||
|
} else {
|
||||||
|
lastPanPoint.current = null;
|
||||||
|
}
|
||||||
} else if (activeTouchPointers.current.size === 2) {
|
} else if (activeTouchPointers.current.size === 2) {
|
||||||
const pts = Array.from(activeTouchPointers.current.values());
|
const pts = Array.from(activeTouchPointers.current.values());
|
||||||
const d = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);
|
const d = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);
|
||||||
initialPinchDistance.current = d;
|
initialPinchDistance.current = d;
|
||||||
initialPinchScale.current = mobileScale;
|
initialPinchScale.current = mobileScale;
|
||||||
|
shouldHandleLocalGesture = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldHandleLocalGesture) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPointerMove = (e: PointerEvent) => {
|
const onPointerMove = (e: PointerEvent) => {
|
||||||
if (e.pointerType !== "touch") return;
|
if (e.pointerType !== "touch") return;
|
||||||
const prev = activeTouchPointers.current.get(e.pointerId);
|
const prev = activeTouchPointers.current.get(e.pointerId);
|
||||||
activeTouchPointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
activeTouchPointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
||||||
|
if (!gestureEnabled) return;
|
||||||
const pts = Array.from(activeTouchPointers.current.values());
|
const pts = Array.from(activeTouchPointers.current.values());
|
||||||
|
let shouldHandleLocalGesture = false;
|
||||||
|
|
||||||
if (pts.length === 2 && initialPinchDistance.current) {
|
if (pts.length === 2 && initialPinchDistance.current) {
|
||||||
const d = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);
|
const d = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);
|
||||||
const factor = d / initialPinchDistance.current;
|
const factor = d / initialPinchDistance.current;
|
||||||
const next = Math.max(1, Math.min(4, initialPinchScale.current * factor));
|
const next = Math.max(1, Math.min(4, initialPinchScale.current * factor));
|
||||||
setMobileScale(next);
|
setMobileScale(next);
|
||||||
} else if (pts.length === 1 && lastPanPoint.current && prev) {
|
shouldHandleLocalGesture = true;
|
||||||
|
} else if (pts.length === 1 && mobileScale > 1 && lastPanPoint.current && prev) {
|
||||||
const dx = e.clientX - lastPanPoint.current.x;
|
const dx = e.clientX - lastPanPoint.current.x;
|
||||||
const dy = e.clientY - lastPanPoint.current.y;
|
const dy = e.clientY - lastPanPoint.current.y;
|
||||||
lastPanPoint.current = { x: e.clientX, y: e.clientY };
|
lastPanPoint.current = { x: e.clientX, y: e.clientY };
|
||||||
setMobileTx(v => v + dx);
|
setMobileTx(v => v + dx);
|
||||||
setMobileTy(v => v + dy);
|
setMobileTy(v => v + dy);
|
||||||
|
shouldHandleLocalGesture = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldHandleLocalGesture) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPointerUp = (e: PointerEvent) => {
|
const onPointerUp = (e: PointerEvent) => {
|
||||||
if (e.pointerType !== "touch") return;
|
if (e.pointerType !== "touch") return;
|
||||||
|
const wasHandlingLocalGesture = gestureEnabled && (initialPinchDistance.current !== null || mobileScale > 1);
|
||||||
activeTouchPointers.current.delete(e.pointerId);
|
activeTouchPointers.current.delete(e.pointerId);
|
||||||
if (activeTouchPointers.current.size < 2) {
|
if (activeTouchPointers.current.size < 2) {
|
||||||
initialPinchDistance.current = null;
|
initialPinchDistance.current = null;
|
||||||
@@ -85,8 +102,11 @@ export const useTouchZoom = (
|
|||||||
if (activeTouchPointers.current.size === 0) {
|
if (activeTouchPointers.current.size === 0) {
|
||||||
lastPanPoint.current = null;
|
lastPanPoint.current = null;
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
if (wasHandlingLocalGesture) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
el.addEventListener("pointerdown", onPointerDown, { signal });
|
el.addEventListener("pointerdown", onPointerDown, { signal });
|
||||||
@@ -95,7 +115,15 @@ export const useTouchZoom = (
|
|||||||
el.addEventListener("pointercancel", onPointerUp, { signal });
|
el.addEventListener("pointercancel", onPointerUp, { signal });
|
||||||
|
|
||||||
return () => abortController.abort();
|
return () => abortController.abort();
|
||||||
}, [mobileScale, containerRef]);
|
}, [mobileScale, containerRef, gestureEnabled]);
|
||||||
|
|
||||||
|
const resetTransform = () => {
|
||||||
|
initialPinchDistance.current = null;
|
||||||
|
lastPanPoint.current = null;
|
||||||
|
setMobileScale(1);
|
||||||
|
setMobileTx(0);
|
||||||
|
setMobileTy(0);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
@@ -115,5 +143,6 @@ export const useTouchZoom = (
|
|||||||
mobileTy,
|
mobileTy,
|
||||||
activeTouchPointers,
|
activeTouchPointers,
|
||||||
lastPanPoint,
|
lastPanPoint,
|
||||||
|
resetTransform,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user