diff --git a/src/components/Trackpad/ScreenMirror.tsx b/src/components/Trackpad/ScreenMirror.tsx index 4121e635..e88a2b62 100644 --- a/src/components/Trackpad/ScreenMirror.tsx +++ b/src/components/Trackpad/ScreenMirror.tsx @@ -4,11 +4,11 @@ import type React from "react" import { useRef } from "react" import { useConnection } from "../../contexts/ConnectionProvider" import { useMirrorStream } from "../../hooks/useMirrorStream" +import { useRemoteConnection } from "../../hooks/useRemoteConnection" interface ScreenMirrorProps { scrollMode: boolean isTracking: boolean - handlers: React.HTMLAttributes } const TEXTS = { @@ -16,23 +16,113 @@ const TEXTS = { AUTOMATIC: "Mirroring will start automatically", } -export const ScreenMirror = ({ - scrollMode, - isTracking, - handlers, -}: ScreenMirrorProps) => { +export const ScreenMirror = ({ scrollMode, isTracking }: ScreenMirrorProps) => { const { wsRef, status } = useConnection() const canvasRef = useRef(null) const { hasFrame } = useMirrorStream(wsRef, canvasRef, status) + const { send } = useRemoteConnection() + const lastPos = useRef<{ x: number; y: number } | null>(null) + const lastMoveTime = useRef(0) + + const getAbsoluteCoords = (clientX: number, clientY: number) => { + if (!canvasRef.current || !hasFrame) return null + const canvas = canvasRef.current + const rect = canvas.getBoundingClientRect() + + const cw = rect.width + const ch = rect.height + const iw = canvas.width + const ih = canvas.height + + if (iw === 0 || ih === 0) return null + + const scale = Math.min(cw / iw, ch / ih) + const dw = iw * scale + const dh = ih * scale + const offsetX = (cw - dw) / 2 + const offsetY = (ch - dh) / 2 + + const cx = clientX - rect.left + const cy = clientY - rect.top + + const x = (cx - offsetX) / scale + const y = (cy - offsetY) / scale + + if (x < 0 || x > iw || y < 0 || y > ih) { + return { + x: Math.max(0, Math.min(iw, x)), + y: Math.max(0, Math.min(ih, y)), + } + } + + return { x, y } + } + + const handlePointerDown = (e: React.PointerEvent) => { + const coords = getAbsoluteCoords(e.clientX, e.clientY) + if (!coords) return + + e.currentTarget.setPointerCapture(e.pointerId) + if (e.pointerType === "mouse" || !scrollMode) { + send({ type: "absolute", x: coords.x, y: coords.y }) + } + if (e.pointerType === "mouse") { + const button = + e.button === 0 ? "left" : e.button === 2 ? "right" : "middle" + send({ type: "click", button, press: true }) + } else { + if (!scrollMode) { + send({ type: "click", button: "left", press: true }) + } + } + lastPos.current = { x: e.clientX, y: e.clientY } + } + + const handlePointerMove = (e: React.PointerEvent) => { + const now = performance.now() + if (now - lastMoveTime.current < 16) return + lastMoveTime.current = now + + const coords = getAbsoluteCoords(e.clientX, e.clientY) + if (!coords) return + + if (e.pointerType === "mouse") { + send({ type: "absolute", x: coords.x, y: coords.y }) + } else { + if (scrollMode && lastPos.current) { + const dx = e.clientX - lastPos.current.x + const dy = e.clientY - lastPos.current.y + send({ type: "scroll", dx: -dx * 1.5, dy: -dy * 1.5 }) + lastPos.current = { x: e.clientX, y: e.clientY } + } else if (lastPos.current && !scrollMode) { + send({ type: "absolute", x: coords.x, y: coords.y }) + } + } + } + + const handlePointerUp = (e: React.PointerEvent) => { + if (e.currentTarget.hasPointerCapture(e.pointerId)) { + e.currentTarget.releasePointerCapture(e.pointerId) + } + if (e.pointerType === "mouse") { + const button = + e.button === 0 ? "left" : e.button === 2 ? "right" : "middle" + send({ type: "click", button, press: false }) + } else { + if (!scrollMode) { + send({ type: "click", button: "left", press: false }) + } + } + lastPos.current = null + } return (
{/* Mirror Canvas */} {/* Standby UI */} @@ -49,9 +139,13 @@ export const ScreenMirror = ({ {/* Transparent Gesture Overlay */}
e.preventDefault()} style={{ - cursor: scrollMode ? "ns-resize" : isTracking ? "none" : "default", + cursor: scrollMode ? "ns-resize" : "crosshair", }} />
diff --git a/src/routes/trackpad.tsx b/src/routes/trackpad.tsx index f2f31461..f7f7c305 100644 --- a/src/routes/trackpad.tsx +++ b/src/routes/trackpad.tsx @@ -224,11 +224,7 @@ function TrackpadPage() { scrollMode={scrollMode} handlers={handlers} /> - + {bufferText !== "" && }
diff --git a/src/server/InputHandler.ts b/src/server/InputHandler.ts index 63f9b26e..f37f68a1 100644 --- a/src/server/InputHandler.ts +++ b/src/server/InputHandler.ts @@ -14,8 +14,11 @@ export interface InputMessage { | "text" | "zoom" | "combo" + | "absolute" dx?: number dy?: number + x?: number + y?: number button?: "left" | "right" | "middle" press?: boolean key?: string @@ -58,6 +61,7 @@ export class InputHandler { } const MAX_COORD = 2000 + const MAX_ABS_COORD = 10000 if (this.isFiniteNumber(msg.dx)) { msg.dx = this.clamp(msg.dx, -MAX_COORD, MAX_COORD) } else { @@ -68,6 +72,16 @@ export class InputHandler { } else { msg.dy = 0 } + if (this.isFiniteNumber(msg.x)) { + msg.x = this.clamp(msg.x, -MAX_ABS_COORD, MAX_ABS_COORD) + } else { + msg.x = undefined + } + if (this.isFiniteNumber(msg.y)) { + msg.y = this.clamp(msg.y, -MAX_ABS_COORD, MAX_ABS_COORD) + } else { + msg.y = undefined + } if (this.isFiniteNumber(msg.delta)) { msg.delta = this.clamp(msg.delta, -MAX_COORD, MAX_COORD) } else { @@ -144,6 +158,29 @@ export class InputHandler { } break + case "absolute": + if ( + typeof msg.x === "number" && + typeof msg.y === "number" && + Number.isFinite(msg.x) && + Number.isFinite(msg.y) + ) { + try { + // Attempt ydotool absolute movement first + const success = await moveRelative(msg.x, msg.y, true) + + // Fallback to absolute positioning if ydotool is unavailable or fails + if (!success) { + await mouse.setPosition( + new Point(Math.round(msg.x), Math.round(msg.y)), + ) + } + } catch (err) { + console.error("Absolute move event failed:", err) + } + } + break + case "click": { const VALID_BUTTONS = ["left", "right", "middle"] if (msg.button && VALID_BUTTONS.includes(msg.button)) { diff --git a/src/server/websocket.ts b/src/server/websocket.ts index 0388a12a..f6db0346 100644 --- a/src/server/websocket.ts +++ b/src/server/websocket.ts @@ -345,6 +345,7 @@ export async function createWsServer( "combo", "copy", "paste", + "absolute", ] if (!msg.type || !VALID_INPUT_TYPES.includes(msg.type)) { logger.warn(`Unknown message type: ${msg.type}`) diff --git a/src/server/ydotool.ts b/src/server/ydotool.ts index b75704fe..fda17dbf 100644 --- a/src/server/ydotool.ts +++ b/src/server/ydotool.ts @@ -50,20 +50,23 @@ export async function checkYdotool(): Promise { * @param dy Y offset * @returns true if successful, false otherwise */ -export async function moveRelative(dx: number, dy: number): Promise { +export async function moveRelative( + dx: number, + dy: number, + absolute = false, +): Promise { if (!(await checkYdotool()) || !ydotoolPath) { return false } try { - // ydotool mousemove -x -y - await execFileAsync(ydotoolPath, [ - "mousemove", - "-x", - String(dx), - "-y", - String(dy), - ]) + const args = ["mousemove"] + if (absolute) { + args.push("--absolute") + } + args.push("-x", String(dx), "-y", String(dy)) + + await execFileAsync(ydotoolPath, args) return true } catch (err) { console.error("[ydotool] Error executing mousemove:", err)