mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-07-29 22:41:45 +08:00
fix: 修复移动端浏览器的触屏鼠标映射
This commit is contained in:
@@ -148,6 +148,34 @@ const isPointerLocked = ref(false)
|
|||||||
|
|
||||||
const localCrosshairPos = ref<{ x: number; y: number } | null>(null)
|
const localCrosshairPos = ref<{ x: number; y: number } | null>(null)
|
||||||
|
|
||||||
|
interface TouchPointerState {
|
||||||
|
pointerId: number
|
||||||
|
mode: 'absolute' | 'relative'
|
||||||
|
startX: number
|
||||||
|
startY: number
|
||||||
|
lastX: number
|
||||||
|
lastY: number
|
||||||
|
maxDistanceSquared: number
|
||||||
|
longPressTimer: ReturnType<typeof setTimeout> | null
|
||||||
|
leftButtonDown: boolean
|
||||||
|
doubleTapCandidate: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PendingTouchTap {
|
||||||
|
mode: 'absolute' | 'relative'
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
timestamp: number
|
||||||
|
timer: ReturnType<typeof setTimeout> | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOUCH_TAP_MOVE_THRESHOLD_PX = 8
|
||||||
|
const TOUCH_LONG_PRESS_MS = 450
|
||||||
|
const TOUCH_DOUBLE_TAP_MS = 300
|
||||||
|
const TOUCH_DOUBLE_TAP_DISTANCE_PX = 24
|
||||||
|
let touchPointer: TouchPointerState | null = null
|
||||||
|
let pendingTouchTap: PendingTouchTap | null = null
|
||||||
|
|
||||||
const DEFAULT_MOUSE_MOVE_SEND_INTERVAL_MS = 16
|
const DEFAULT_MOUSE_MOVE_SEND_INTERVAL_MS = 16
|
||||||
let mouseMoveSendIntervalMs = DEFAULT_MOUSE_MOVE_SEND_INTERVAL_MS
|
let mouseMoveSendIntervalMs = DEFAULT_MOUSE_MOVE_SEND_INTERVAL_MS
|
||||||
let mouseFlushTimer: ReturnType<typeof setTimeout> | null = null
|
let mouseFlushTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
@@ -164,6 +192,7 @@ function syncMouseModeFromConfig() {
|
|||||||
if (typeof mouseAbsolute !== 'boolean') return
|
if (typeof mouseAbsolute !== 'boolean') return
|
||||||
const nextMode: 'absolute' | 'relative' = mouseAbsolute ? 'absolute' : 'relative'
|
const nextMode: 'absolute' | 'relative' = mouseAbsolute ? 'absolute' : 'relative'
|
||||||
if (mouseMode.value !== nextMode) {
|
if (mouseMode.value !== nextMode) {
|
||||||
|
resetTouchInput()
|
||||||
mouseMode.value = nextMode
|
mouseMode.value = nextMode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2231,13 +2260,7 @@ function updateLocalCrosshairFromEvent(e: MouseEvent) {
|
|||||||
if (rect.width <= 0 || rect.height <= 0) return
|
if (rect.width <= 0 || rect.height <= 0) return
|
||||||
|
|
||||||
if (mouseMode.value === 'relative' && isPointerLocked.value) {
|
if (mouseMode.value === 'relative' && isPointerLocked.value) {
|
||||||
const cur = localCrosshairPos.value
|
updateLocalCrosshairByDelta(e.movementX, e.movementY)
|
||||||
const nx = cur ? cur.x + e.movementX : rect.width / 2
|
|
||||||
const ny = cur ? cur.y + e.movementY : rect.height / 2
|
|
||||||
localCrosshairPos.value = {
|
|
||||||
x: Math.max(0, Math.min(rect.width, nx)),
|
|
||||||
y: Math.max(0, Math.min(rect.height, ny)),
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2247,12 +2270,283 @@ function updateLocalCrosshairFromEvent(e: MouseEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateLocalCrosshairByDelta(dx: number, dy: number) {
|
||||||
|
if (!cursorVisible.value) {
|
||||||
|
localCrosshairPos.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = videoContainerRef.value
|
||||||
|
if (!container) return
|
||||||
|
|
||||||
|
const rect = container.getBoundingClientRect()
|
||||||
|
if (rect.width <= 0 || rect.height <= 0) return
|
||||||
|
|
||||||
|
const current = localCrosshairPos.value ?? { x: rect.width / 2, y: rect.height / 2 }
|
||||||
|
localCrosshairPos.value = {
|
||||||
|
x: Math.max(0, Math.min(rect.width, current.x + dx)),
|
||||||
|
y: Math.max(0, Math.min(rect.height, current.y + dy)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleMouseLeaveVideo() {
|
function handleMouseLeaveVideo() {
|
||||||
if (!isPointerLocked.value) {
|
if (!isPointerLocked.value) {
|
||||||
localCrosshairPos.value = null
|
localCrosshairPos.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sendTouchClick(button: 'left' | 'right') {
|
||||||
|
flushPendingMouseMovesImmediately()
|
||||||
|
sendMouseEvent({ type: 'down', button })
|
||||||
|
sendMouseEvent({ type: 'up', button })
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPendingTouchTap(commitRelativeClick = false) {
|
||||||
|
const tap = pendingTouchTap
|
||||||
|
pendingTouchTap = null
|
||||||
|
|
||||||
|
if (!tap) return
|
||||||
|
if (tap.timer !== null) {
|
||||||
|
clearTimeout(tap.timer)
|
||||||
|
}
|
||||||
|
if (commitRelativeClick && tap.mode === 'relative') {
|
||||||
|
sendTouchClick('left')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function schedulePendingTouchTap(mode: 'absolute' | 'relative', x: number, y: number) {
|
||||||
|
const tap: PendingTouchTap = {
|
||||||
|
mode,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
timer: null,
|
||||||
|
}
|
||||||
|
tap.timer = setTimeout(() => {
|
||||||
|
if (pendingTouchTap === tap) {
|
||||||
|
clearPendingTouchTap(true)
|
||||||
|
}
|
||||||
|
}, TOUCH_DOUBLE_TAP_MS)
|
||||||
|
pendingTouchTap = tap
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearTouchPointer() {
|
||||||
|
const activePointer = touchPointer
|
||||||
|
touchPointer = null
|
||||||
|
|
||||||
|
if (!activePointer) return
|
||||||
|
if (activePointer.longPressTimer !== null) {
|
||||||
|
clearTimeout(activePointer.longPressTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = videoContainerRef.value
|
||||||
|
if (container) {
|
||||||
|
try {
|
||||||
|
if (container.hasPointerCapture(activePointer.pointerId)) {
|
||||||
|
container.releasePointerCapture(activePointer.pointerId)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Pointer capture may already have been released by the browser.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activePointer.leftButtonDown) {
|
||||||
|
flushPendingMouseMovesImmediately()
|
||||||
|
sendMouseEvent({ type: 'up', button: 'left' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetTouchInput() {
|
||||||
|
clearTouchPointer()
|
||||||
|
clearPendingTouchTap(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendAbsoluteTouchPosition(e: PointerEvent, immediate = false) {
|
||||||
|
const absolutePosition = getAbsoluteMousePosition(e)
|
||||||
|
if (!absolutePosition) return
|
||||||
|
|
||||||
|
mousePosition.value = absolutePosition
|
||||||
|
updateLocalCrosshairFromEvent(e)
|
||||||
|
|
||||||
|
if (immediate) {
|
||||||
|
sendMouseEvent({ type: 'move_abs', ...absolutePosition })
|
||||||
|
pendingMouseMove = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingMouseMove = { type: 'move_abs', ...absolutePosition }
|
||||||
|
requestMouseMoveFlush()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTouchPointerDown(e: PointerEvent) {
|
||||||
|
if (e.pointerType !== 'touch') return
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const container = videoContainerRef.value
|
||||||
|
if (!container || touchPointer !== null) return
|
||||||
|
|
||||||
|
if (document.activeElement !== container) {
|
||||||
|
container.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousTap = pendingTouchTap
|
||||||
|
const now = Date.now()
|
||||||
|
const doubleTapDistanceX = previousTap ? e.clientX - previousTap.x : 0
|
||||||
|
const doubleTapDistanceY = previousTap ? e.clientY - previousTap.y : 0
|
||||||
|
const doubleTapCandidate = !!previousTap
|
||||||
|
&& previousTap.mode === mouseMode.value
|
||||||
|
&& now - previousTap.timestamp <= TOUCH_DOUBLE_TAP_MS
|
||||||
|
&& doubleTapDistanceX * doubleTapDistanceX + doubleTapDistanceY * doubleTapDistanceY
|
||||||
|
<= TOUCH_DOUBLE_TAP_DISTANCE_PX ** 2
|
||||||
|
|
||||||
|
clearPendingTouchTap(!doubleTapCandidate)
|
||||||
|
|
||||||
|
const activePointer: TouchPointerState = {
|
||||||
|
pointerId: e.pointerId,
|
||||||
|
mode: mouseMode.value,
|
||||||
|
startX: e.clientX,
|
||||||
|
startY: e.clientY,
|
||||||
|
lastX: e.clientX,
|
||||||
|
lastY: e.clientY,
|
||||||
|
maxDistanceSquared: 0,
|
||||||
|
longPressTimer: null,
|
||||||
|
leftButtonDown: false,
|
||||||
|
doubleTapCandidate,
|
||||||
|
}
|
||||||
|
touchPointer = activePointer
|
||||||
|
|
||||||
|
if (activePointer.mode === 'absolute') {
|
||||||
|
sendAbsoluteTouchPosition(e, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
activePointer.longPressTimer = setTimeout(() => {
|
||||||
|
if (
|
||||||
|
touchPointer !== activePointer
|
||||||
|
|| mouseMode.value !== activePointer.mode
|
||||||
|
|| activePointer.maxDistanceSquared > TOUCH_TAP_MOVE_THRESHOLD_PX ** 2
|
||||||
|
) return
|
||||||
|
|
||||||
|
if (activePointer.doubleTapCandidate) {
|
||||||
|
activePointer.doubleTapCandidate = false
|
||||||
|
if (activePointer.mode === 'relative') {
|
||||||
|
sendTouchClick('left')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flushPendingMouseMovesImmediately()
|
||||||
|
activePointer.leftButtonDown = true
|
||||||
|
sendMouseEvent({ type: 'down', button: 'left' })
|
||||||
|
}, TOUCH_LONG_PRESS_MS)
|
||||||
|
|
||||||
|
try {
|
||||||
|
container.setPointerCapture(e.pointerId)
|
||||||
|
} catch {
|
||||||
|
// The gesture can still be handled while it remains inside the video area.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTouchPointerMove(e: PointerEvent) {
|
||||||
|
const activePointer = touchPointer
|
||||||
|
if (
|
||||||
|
e.pointerType !== 'touch'
|
||||||
|
|| !activePointer
|
||||||
|
|| activePointer.pointerId !== e.pointerId
|
||||||
|
|| mouseMode.value !== activePointer.mode
|
||||||
|
) return
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const distanceX = e.clientX - activePointer.startX
|
||||||
|
const distanceY = e.clientY - activePointer.startY
|
||||||
|
const previousMaxDistanceSquared = activePointer.maxDistanceSquared
|
||||||
|
activePointer.maxDistanceSquared = Math.max(
|
||||||
|
activePointer.maxDistanceSquared,
|
||||||
|
distanceX * distanceX + distanceY * distanceY,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
previousMaxDistanceSquared <= TOUCH_TAP_MOVE_THRESHOLD_PX ** 2
|
||||||
|
&& activePointer.maxDistanceSquared > TOUCH_TAP_MOVE_THRESHOLD_PX ** 2
|
||||||
|
) {
|
||||||
|
if (activePointer.longPressTimer !== null) {
|
||||||
|
clearTimeout(activePointer.longPressTimer)
|
||||||
|
activePointer.longPressTimer = null
|
||||||
|
}
|
||||||
|
if (activePointer.doubleTapCandidate) {
|
||||||
|
activePointer.doubleTapCandidate = false
|
||||||
|
if (activePointer.mode === 'relative') {
|
||||||
|
sendTouchClick('left')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activePointer.mode === 'absolute') {
|
||||||
|
sendAbsoluteTouchPosition(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep sub-pixel movement as a remainder instead of losing it on every event.
|
||||||
|
const dx = Math.trunc(e.clientX - activePointer.lastX)
|
||||||
|
const dy = Math.trunc(e.clientY - activePointer.lastY)
|
||||||
|
if (dx === 0 && dy === 0) return
|
||||||
|
|
||||||
|
activePointer.lastX += dx
|
||||||
|
activePointer.lastY += dy
|
||||||
|
accumulatedDelta.x += dx
|
||||||
|
accumulatedDelta.y += dy
|
||||||
|
mousePosition.value = {
|
||||||
|
x: mousePosition.value.x + dx,
|
||||||
|
y: mousePosition.value.y + dy,
|
||||||
|
}
|
||||||
|
updateLocalCrosshairByDelta(dx, dy)
|
||||||
|
requestMouseMoveFlush()
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerTouchTap(activePointer: TouchPointerState) {
|
||||||
|
if (activePointer.doubleTapCandidate) {
|
||||||
|
sendTouchClick('right')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
schedulePendingTouchTap(activePointer.mode, activePointer.startX, activePointer.startY)
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishTouchPointer(e: PointerEvent, allowTap: boolean) {
|
||||||
|
const activePointer = touchPointer
|
||||||
|
if (e.pointerType !== 'touch' || !activePointer || activePointer.pointerId !== e.pointerId) return
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (allowTap && mouseMode.value === activePointer.mode) {
|
||||||
|
handleTouchPointerMove(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTap = allowTap
|
||||||
|
&& mouseMode.value === activePointer.mode
|
||||||
|
&& !activePointer.leftButtonDown
|
||||||
|
&& activePointer.maxDistanceSquared <= TOUCH_TAP_MOVE_THRESHOLD_PX ** 2
|
||||||
|
const wasDragging = activePointer.leftButtonDown
|
||||||
|
|
||||||
|
if (!allowTap && activePointer.doubleTapCandidate && activePointer.mode === 'relative') {
|
||||||
|
activePointer.doubleTapCandidate = false
|
||||||
|
sendTouchClick('left')
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTouchPointer()
|
||||||
|
|
||||||
|
if (!wasDragging && isTap) {
|
||||||
|
registerTouchTap(activePointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTouchPointerUp(e: PointerEvent) {
|
||||||
|
finishTouchPointer(e, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTouchPointerCancel(e: PointerEvent) {
|
||||||
|
finishTouchPointer(e, false)
|
||||||
|
}
|
||||||
|
|
||||||
function handleMouseMove(e: MouseEvent) {
|
function handleMouseMove(e: MouseEvent) {
|
||||||
updateLocalCrosshairFromEvent(e)
|
updateLocalCrosshairFromEvent(e)
|
||||||
|
|
||||||
@@ -2360,6 +2654,21 @@ function requestMouseMoveFlush() {
|
|||||||
scheduleMouseMoveFlush()
|
scheduleMouseMoveFlush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function flushPendingMouseMovesImmediately() {
|
||||||
|
if (mouseFlushTimer !== null) {
|
||||||
|
clearTimeout(mouseFlushTimer)
|
||||||
|
mouseFlushTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
let sent = false
|
||||||
|
while (flushMouseMoveOnce()) {
|
||||||
|
sent = true
|
||||||
|
}
|
||||||
|
if (sent) {
|
||||||
|
lastMouseMoveSendTime = Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const pressedMouseButton = ref<'left' | 'right' | 'middle' | null>(null)
|
const pressedMouseButton = ref<'left' | 'right' | 'middle' | null>(null)
|
||||||
|
|
||||||
function handleMouseDown(e: MouseEvent) {
|
function handleMouseDown(e: MouseEvent) {
|
||||||
@@ -2469,6 +2778,7 @@ function handleFullscreenChange() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleBlur() {
|
function handleBlur() {
|
||||||
|
resetTouchInput()
|
||||||
pressedKeys.value = []
|
pressedKeys.value = []
|
||||||
activeModifierMask.value = 0
|
activeModifierMask.value = 0
|
||||||
if (pressedMouseButton.value !== null) {
|
if (pressedMouseButton.value !== null) {
|
||||||
@@ -2609,6 +2919,8 @@ function handleVirtualKeyUp(key: CanonicalKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleToggleMouseMode() {
|
function handleToggleMouseMode() {
|
||||||
|
resetTouchInput()
|
||||||
|
|
||||||
if (mouseMode.value === 'relative' && isPointerLocked.value) {
|
if (mouseMode.value === 'relative' && isPointerLocked.value) {
|
||||||
exitPointerLock()
|
exitPointerLock()
|
||||||
}
|
}
|
||||||
@@ -2855,13 +3167,17 @@ onUnmounted(() => {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref="videoContainerRef"
|
ref="videoContainerRef"
|
||||||
class="relative bg-black overflow-hidden flex items-center justify-center"
|
class="relative bg-black overflow-hidden flex items-center justify-center touch-none"
|
||||||
:style="videoContainerStyle"
|
:style="videoContainerStyle"
|
||||||
:class="{
|
:class="{
|
||||||
'cursor-none': true,
|
'cursor-none': true,
|
||||||
}"
|
}"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@mouseleave="handleMouseLeaveVideo"
|
@mouseleave="handleMouseLeaveVideo"
|
||||||
|
@pointerdown="handleTouchPointerDown"
|
||||||
|
@pointermove="handleTouchPointerMove"
|
||||||
|
@pointerup="handleTouchPointerUp"
|
||||||
|
@pointercancel="handleTouchPointerCancel"
|
||||||
@mousemove="handleMouseMove"
|
@mousemove="handleMouseMove"
|
||||||
@mousedown="handleMouseDown"
|
@mousedown="handleMouseDown"
|
||||||
@mouseup="handleMouseUp"
|
@mouseup="handleMouseUp"
|
||||||
|
|||||||
Reference in New Issue
Block a user