Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions src/hooks/useTrackpadGesture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@ const getTouchDistance = (a: TrackedTouch, b: TrackedTouch): number => {
export const useTrackpadGesture = (
send: (msg: any) => void,
scrollMode: boolean,
sensitivity: number = 1.5
) => {
const [isTracking, setIsTracking] = useState(false);

// Refs for tracking state (avoids re-renders during rapid movement)
const ongoingTouches = useRef<TrackedTouch[]>([]);
const moved = useRef(false);
Expand Down Expand Up @@ -137,17 +136,17 @@ export const useTrackpadGesture = (
if (pinching.current || Math.abs(delta) > PINCH_THRESHOLD) {
pinching.current = true;
lastPinchDist.current = dist;
send({ type: 'zoom', delta: delta * sensitivity });
send({ type: 'zoom', delta: delta });
} else {
lastPinchDist.current = dist;
send({ type: 'scroll', dx: -sumX * sensitivity, dy: -sumY * sensitivity });
send({ type: 'scroll', dx: -sumX, dy: -sumY });
}
} else if (scrollMode) {
// Scroll mode: single finger scrolls, or two-finger scroll in cursor mode
send({ type: 'scroll', dx: -sumX * sensitivity, dy: -sumY * sensitivity });
send({ type: 'scroll', dx: -sumX, dy: -sumY });
} else if (ongoingTouches.current.length === 1 || dragging.current) {
// Cursor movement (only in cursor mode with 1 finger, or when dragging)
send({ type: 'move', dx: sumX * sensitivity, dy: sumY * sensitivity });
send({ type: 'move', dx: sumX, dy: sumY });
}
}
};
Expand Down
44 changes: 23 additions & 21 deletions src/routes/trackpad.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createFileRoute } from '@tanstack/react-router'
import { useState, useRef } from 'react'
import { useState, useRef, useCallback } from 'react'
import { useRemoteConnection } from '../hooks/useRemoteConnection';
import { useTrackpadGesture } from '../hooks/useTrackpadGesture';
import { ControlBar } from '../components/Trackpad/ControlBar';
Expand All @@ -10,6 +10,9 @@ export const Route = createFileRoute('/trackpad')({
component: TrackpadPage,
})

const CLICK_RELEASE_DELAY_MS = 50;
const INPUT_REFOCUS_DELAY_MS = 10;

function TrackpadPage() {
const [scrollMode, setScrollMode] = useState(false);
const hiddenInputRef = useRef<HTMLInputElement>(null);
Expand All @@ -18,59 +21,58 @@ function TrackpadPage() {
const { status, send } = useRemoteConnection();
const { isTracking, handlers } = useTrackpadGesture(send, scrollMode);

const focusInput = () => {
const focusInput = useCallback(() => {
hiddenInputRef.current?.focus();
};
}, []);

const handleClick = (button: 'left' | 'right') => {
const handleClick = useCallback((button: 'left' | 'right') => {
send({ type: 'click', button, press: true });
// Release after short delay to simulate click
setTimeout(() => send({ type: 'click', button, press: false }), 50);
};
setTimeout(() => send({ type: 'click', button, press: false }), CLICK_RELEASE_DELAY_MS);
}, [send]);

const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
const key = e.key.toLowerCase();
if (key === 'backspace') send({ type: 'key', key: 'backspace' });
else if (key === 'enter') send({ type: 'key', key: 'enter' });
else if (key !== 'unidentified' && key.length > 1) {
send({ type: 'key', key });
}
};
}, [send]);

const sendText = (val: string) => {
const sendText = useCallback((val: string) => {
if (!val) return;
const toSend = val.length > 1 ? `${val} ` : val;
send({ type: 'text', text: toSend });
};
send({ type: 'text', text: val });
}, [send]);

const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleInput = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
if (isComposingRef.current) return;
const val = e.target.value;
if (val) {
sendText(val);
e.target.value = '';
}
};
}, [sendText]);

const handleCompositionStart = () => {
const handleCompositionStart = useCallback(() => {
isComposingRef.current = true;
};
}, []);

const handleCompositionEnd = (e: React.CompositionEvent<HTMLInputElement>) => {
const handleCompositionEnd = useCallback((e: React.CompositionEvent<HTMLInputElement>) => {
isComposingRef.current = false;
const val = (e.target as HTMLInputElement).value;
if (val) {
sendText(val);
(e.target as HTMLInputElement).value = '';
}
};
}, [sendText]);

const handleContainerClick = (e: React.MouseEvent) => {
const handleContainerClick = useCallback((e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
e.preventDefault();
focusInput();
}
};
}, [focusInput]);

return (
<div
Expand Down Expand Up @@ -109,7 +111,7 @@ function TrackpadPage() {
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onBlur={() => {
setTimeout(() => hiddenInputRef.current?.focus(), 10);
setTimeout(() => hiddenInputRef.current?.focus(), INPUT_REFOCUS_DELAY_MS);
}}
autoComplete="off"
autoCorrect="off"
Expand Down
34 changes: 20 additions & 14 deletions src/server/InputHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ export class InputHandler {
const currentPos = await mouse.getPosition();
// Apply sensitivity multiplier
const sensitivity = CONFIG.MOUSE_SENSITIVITY ?? 1.0;

await mouse.setPosition(new Point(
currentPos.x + (msg.dx * sensitivity),
currentPos.x + (msg.dx * sensitivity),
currentPos.y + (msg.dy * sensitivity)
));
}
Expand All @@ -44,27 +44,33 @@ export class InputHandler {
}
break;

case 'scroll':
case 'scroll': {
const invertMultiplier = (CONFIG.MOUSE_INVERT ?? false) ? -1 : 1;
if (msg.dy !== undefined && msg.dy !== 0) await mouse.scrollDown(msg.dy * invertMultiplier);
if (msg.dx !== undefined && msg.dx !== 0) await mouse.scrollRight(msg.dx * -1 * invertMultiplier);
const scrollSensitivity = CONFIG.MOUSE_SENSITIVITY ?? 1.0;
if (msg.dy !== undefined && msg.dy !== 0) await mouse.scrollDown(Math.round(msg.dy * invertMultiplier * scrollSensitivity));
if (msg.dx !== undefined && msg.dx !== 0) await mouse.scrollRight(Math.round(msg.dx * -1 * invertMultiplier * scrollSensitivity));
break;
}

case 'zoom':
case 'zoom': {
if (msg.delta !== undefined && msg.delta !== 0) {
const invertMultiplier = (CONFIG.MOUSE_INVERT ?? false) ? -1 : 1;
const zoomSensitivity = CONFIG.MOUSE_SENSITIVITY ?? 1.0;
const sensitivityFactor = 0.5; // Adjust scaling
const MAX_ZOOM_STEP = 5;
const scaledDelta = Math.sign(msg.delta) * Math.min(Math.abs(msg.delta) * sensitivityFactor, MAX_ZOOM_STEP);
const amount = -scaledDelta * invertMultiplier;

await keyboard.pressKey(Key.LeftControl);
try {
await mouse.scrollDown(amount);
} finally {
await keyboard.releaseKey(Key.LeftControl);
const scaledDelta = Math.sign(msg.delta) * Math.min(Math.abs(msg.delta) * sensitivityFactor * zoomSensitivity, MAX_ZOOM_STEP);
const amount = Math.round(-scaledDelta * invertMultiplier);
if (amount !== 0) {

await keyboard.pressKey(Key.LeftControl);
try {
await mouse.scrollDown(amount);
} finally {
await keyboard.releaseKey(Key.LeftControl);
}
}
}
}
break;

case 'key':
Expand Down