From 0cc118707e4ed2c17bdf85c59b7ce0f948ee3a48 Mon Sep 17 00:00:00 2001 From: Nakshatra Sharma Date: Thu, 12 Feb 2026 02:56:17 +0530 Subject: [PATCH] feat(trackpad): 6x14 grid layout with virtual keyboard handling --- src/components/Trackpad/ControlBar.tsx | 38 ++---- src/components/Trackpad/ExtraKeys.tsx | 96 +++---------- src/components/Trackpad/FnKeys.tsx | 25 ++++ src/components/Trackpad/KeyGrid.tsx | 35 +++++ src/components/Trackpad/MediaKeys.tsx | 55 ++++++++ src/routes/trackpad.tsx | 178 +++++++++++++++---------- 6 files changed, 258 insertions(+), 169 deletions(-) create mode 100644 src/components/Trackpad/FnKeys.tsx create mode 100644 src/components/Trackpad/KeyGrid.tsx create mode 100644 src/components/Trackpad/MediaKeys.tsx diff --git a/src/components/Trackpad/ControlBar.tsx b/src/components/Trackpad/ControlBar.tsx index bc4785a..7f34792 100644 --- a/src/components/Trackpad/ControlBar.tsx +++ b/src/components/Trackpad/ControlBar.tsx @@ -3,6 +3,7 @@ import React from "react"; interface ControlBarProps { scrollMode: boolean; + showKeyboard: boolean; modifier: ModifierState; buffer: string; onToggleScroll: () => void; @@ -14,6 +15,7 @@ interface ControlBarProps { export const ControlBar: React.FC = ({ scrollMode, + showKeyboard, modifier, buffer, onToggleScroll, @@ -30,7 +32,7 @@ export const ControlBar: React.FC = ({ const getModifierButtonClass = () => { switch (modifier) { case "Active": - if (buffer.length > 0) return "btn-success" + if (buffer.length > 0) return "btn-success"; else return "btn-warning"; case "Hold": return "btn-warning"; @@ -43,7 +45,7 @@ export const ControlBar: React.FC = ({ const getModifierLabel = () => { switch (modifier) { case "Active": - if (buffer.length > 0) return "Press" + if (buffer.length > 0) return "Press"; else return "Release"; case "Hold": return "Release"; @@ -53,48 +55,36 @@ export const ControlBar: React.FC = ({ }; return ( -
+
- - {/* - - */}
); diff --git a/src/components/Trackpad/ExtraKeys.tsx b/src/components/Trackpad/ExtraKeys.tsx index 50308b6..005c1ce 100644 --- a/src/components/Trackpad/ExtraKeys.tsx +++ b/src/components/Trackpad/ExtraKeys.tsx @@ -1,90 +1,30 @@ -import React, { useState } from "react"; +import React from "react"; +import { KeyGrid } from "./KeyGrid"; -interface ExtraKeysProps { - sendKey: (key: string) => void; - onInputFocus?: () => void; -} - -/** All extra keys in one row (must match KeyMap.ts). Play/Pause is a single toggle. */ -const EXTRA_KEYS: { label: string; key: string }[] = [ +const KEYS = [ { label: "Esc", key: "esc" }, { label: "Tab", key: "tab" }, { label: "Ctrl", key: "ctrl" }, { label: "Alt", key: "alt" }, + { label: "↑", key: "arrowup" }, + { label: "PrtSc", key: "printscreen" }, { label: "Shift", key: "shift" }, { label: "Meta", key: "meta" }, - { label: "Home", key: "home" }, - { label: "End", key: "end" }, - { label: "PgUp", key: "pgup" }, - { label: "PgDn", key: "pgdn" }, - { label: "Ins", key: "insert" }, - { label: "Del", key: "del" }, - { label: "↑", key: "arrowup" }, - { label: "↓", key: "arrowdown" }, + { label: "Del", key: "delete" }, { label: "←", key: "arrowleft" }, + { label: "↓", key: "arrowdown" }, { label: "→", key: "arrowright" }, - { label: "F1", key: "f1" }, - { label: "F2", key: "f2" }, - { label: "F3", key: "f3" }, - { label: "F4", key: "f4" }, - { label: "F5", key: "f5" }, - { label: "F6", key: "f6" }, - { label: "F7", key: "f7" }, - { label: "F8", key: "f8" }, - { label: "F9", key: "f9" }, - { label: "F10", key: "f10" }, - { label: "F11", key: "f11" }, - { label: "F12", key: "f12" }, - { label: "Mute", key: "audiomute" }, - { label: "Vol−", key: "audiovoldown" }, - { label: "Vol+", key: "audiovolup" }, - { label: "Prev", key: "audioprev" }, - { label: "Next", key: "audionext" }, ]; -export const ExtraKeys: React.FC = ({ sendKey, onInputFocus: _onInputFocus }) => { - const [isPlaying, setIsPlaying] = useState(false); - - const handleInteract = (e: React.PointerEvent, key: string) => { - e.preventDefault(); - sendKey(key); - }; - - const handlePlayPause = (e: React.PointerEvent) => { - e.preventDefault(); - if (isPlaying) { - sendKey("audiopause"); - } else { - sendKey("audioplay"); - } - setIsPlaying((prev) => !prev); - }; +interface ExtraKeysProps { + sendKey: (key: string) => void; +} - return ( -
-
- {EXTRA_KEYS.map(({ label, key }) => ( - - ))} - -
-
- ); -}; +export const ExtraKeys: React.FC = ({ sendKey }) => ( + +); diff --git a/src/components/Trackpad/FnKeys.tsx b/src/components/Trackpad/FnKeys.tsx new file mode 100644 index 0000000..607dfed --- /dev/null +++ b/src/components/Trackpad/FnKeys.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { KeyGrid } from "./KeyGrid"; + +const FN_KEYS = [ + { label: "F1", key: "f1" }, + { label: "F2", key: "f2" }, + { label: "F3", key: "f3" }, + { label: "F4", key: "f4" }, + { label: "F5", key: "f5" }, + { label: "F6", key: "f6" }, + { label: "F7", key: "f7" }, + { label: "F8", key: "f8" }, + { label: "F9", key: "f9" }, + { label: "F10", key: "f10" }, + { label: "F11", key: "f11" }, + { label: "F12", key: "f12" }, +]; + +interface FnKeysProps { + sendKey: (key: string) => void; +} + +export const FnKeys: React.FC = ({ sendKey }) => ( + +); diff --git a/src/components/Trackpad/KeyGrid.tsx b/src/components/Trackpad/KeyGrid.tsx new file mode 100644 index 0000000..3facd06 --- /dev/null +++ b/src/components/Trackpad/KeyGrid.tsx @@ -0,0 +1,35 @@ +import React from "react"; + +interface KeyDef { + label: string; + key: string; +} + +interface KeyGridProps { + keys: KeyDef[]; + sendKey: (key: string) => void; + className?: string; + buttonClass?: string; +} + +export const KeyGrid: React.FC = ({ keys, sendKey, className = "", buttonClass = "btn min-w-0 h-full" }) => { + const handleInteract = (e: React.PointerEvent, key: string) => { + e.preventDefault(); + sendKey(key); + }; + + return ( +
+ {keys.map(({ label, key }) => ( + + ))} +
+ ); +}; diff --git a/src/components/Trackpad/MediaKeys.tsx b/src/components/Trackpad/MediaKeys.tsx new file mode 100644 index 0000000..9d7cc4a --- /dev/null +++ b/src/components/Trackpad/MediaKeys.tsx @@ -0,0 +1,55 @@ +import React, { useState } from "react"; +import { VolumeX, Volume1, Volume2, SkipBack, Play, Pause, SkipForward } from "lucide-react"; + +interface MediaKeysProps { + sendKey: (key: string) => void; +} + +const MEDIA_KEYS = [ + { label: "Mute", key: "audiomute", icon: VolumeX }, + { label: "Vol−", key: "audiovoldown", icon: Volume1 }, + { label: "Vol+", key: "audiovolup", icon: Volume2 }, + { label: "Prev", key: "audioprev", icon: SkipBack }, + { label: "Next", key: "audionext", icon: SkipForward }, +]; + +export const MediaKeys: React.FC = ({ sendKey }) => { + const [isPlaying, setIsPlaying] = useState(false); + + const handleInteract = (e: React.PointerEvent, key: string) => { + e.preventDefault(); + sendKey(key); + }; + + const handlePlayPause = (e: React.PointerEvent) => { + e.preventDefault(); + sendKey(isPlaying ? "audiopause" : "audioplay"); + setIsPlaying(prev => !prev); + }; + + return ( +
+ {MEDIA_KEYS.map(({ label, key, icon: Icon }) => ( + + ))} + +
+ ); +}; diff --git a/src/routes/trackpad.tsx b/src/routes/trackpad.tsx index cf21e0b..fc6e7f2 100644 --- a/src/routes/trackpad.tsx +++ b/src/routes/trackpad.tsx @@ -1,9 +1,11 @@ import { createFileRoute } from '@tanstack/react-router' -import { useState, useRef } from 'react' +import { useState, useRef, useEffect } from 'react' import { useRemoteConnection } from '../hooks/useRemoteConnection'; import { useTrackpadGesture } from '../hooks/useTrackpadGesture'; import { ControlBar } from '../components/Trackpad/ControlBar'; import { ExtraKeys } from '../components/Trackpad/ExtraKeys'; +import { FnKeys } from '../components/Trackpad/FnKeys'; +import { MediaKeys } from '../components/Trackpad/MediaKeys'; import { TouchArea } from '../components/Trackpad/TouchArea'; import { BufferBar } from '@/components/Trackpad/Buffer'; import { ModifierState } from '@/types'; @@ -14,19 +16,22 @@ export const Route = createFileRoute('/trackpad')({ function TrackpadPage() { const [scrollMode, setScrollMode] = useState(false); + const [showKeyboard, setShowKeyboard] = useState(false); const [modifier, setModifier] = useState("Release"); const [buffer, setBuffer] = useState([]); + const [vh, setVh] = useState(null); + const [keyboardHeight, setKeyboardHeight] = useState(0); const bufferText = buffer.join(" + "); + const hiddenInputRef = useRef(null); const isComposingRef = useRef(false); - - // Load Client Settings + const [sensitivity] = useState(() => { if (typeof window === 'undefined') return 1.0; const s = localStorage.getItem('rein_sensitivity'); return s ? parseFloat(s) : 1.0; }); - + const [invertScroll] = useState(() => { if (typeof window === 'undefined') return false; const s = localStorage.getItem('rein_invert'); @@ -34,22 +39,55 @@ function TrackpadPage() { }); const { status, send, sendCombo } = useRemoteConnection(); - // Pass sensitivity and invertScroll to the gesture hook const { isTracking, handlers } = useTrackpadGesture(send, scrollMode, sensitivity, invertScroll); + useEffect(() => { + setVh(window.innerHeight); + let lastWidth = window.innerWidth; + const handleResize = () => { + if (Math.abs(window.innerWidth - lastWidth) > 50) { + lastWidth = window.innerWidth; + setVh(window.innerHeight); + } + }; + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + useEffect(() => { + if (!window.visualViewport) return; + const fullHeight = window.innerHeight; + const handleViewport = () => { + const vv = window.visualViewport!; + const diff = fullHeight - vv.height; + setKeyboardHeight(diff > 100 ? diff : 0); + }; + window.visualViewport.addEventListener('resize', handleViewport); + return () => window.visualViewport?.removeEventListener('resize', handleViewport); + }, []); + const focusInput = () => { - hiddenInputRef.current?.focus(); + hiddenInputRef.current?.focus({ preventScroll: true }); + }; + + const handleToggleKeyboard = () => { + if (showKeyboard) { + hiddenInputRef.current?.blur(); + setShowKeyboard(false); + } else { + focusInput(); + setShowKeyboard(true); + } }; const handleClick = (button: 'left' | 'right') => { send({ type: 'click', button, press: true }); - // Release after short delay to simulate click setTimeout(() => send({ type: 'click', button, press: false }), 50); }; const handleKeyDown = (e: React.KeyboardEvent) => { const key = e.key.toLowerCase(); - + if (modifier !== "Release") { if (key === 'backspace') { e.preventDefault(); @@ -76,7 +114,7 @@ function TrackpadPage() { }; const handleModifierState = () => { - switch(modifier){ + switch (modifier) { case "Active": if (buffer.length > 0) { setModifier("Hold"); @@ -96,11 +134,8 @@ function TrackpadPage() { }; const handleModifier = (key: string) => { - console.log(`handleModifier called with key: ${key}, current modifier: ${modifier}, buffer:`, buffer); - if (modifier === "Hold") { const comboKeys = [...buffer, key]; - console.log(`Sending combo:`, comboKeys); sendCombo(comboKeys); return; } else if (modifier === "Active") { @@ -136,74 +171,83 @@ function TrackpadPage() { isComposingRef.current = false; const val = (e.target as HTMLInputElement).value; if (val) { - // Don't send text during modifier mode if (modifier !== "Release") { handleModifier(val); - }else{ + } else { sendText(val); } (e.target as HTMLInputElement).value = ''; } }; - const handleContainerClick = (e: React.MouseEvent) => { - if (e.target === e.currentTarget) { - e.preventDefault(); - focusInput(); - } + const sendKey = (k: string) => { + if (modifier !== "Release") handleModifier(k); + else send({ type: 'key', key: k }); }; return (
- {/* Touch Surface */} - - {bufferText !== "" && } - - {/* Controls */} - setScrollMode(!scrollMode)} - onLeftClick={() => handleClick('left')} - onRightClick={() => handleClick('right')} - onKeyboardToggle={focusInput} - onModifierToggle={handleModifierState} - /> - - {/* Extra Keys */} - { - if (modifier !== "Release") handleModifier(k); - else send({ type: 'key', key: k }); - }} - onInputFocus={focusInput} - /> - - {/* Hidden Input for Mobile Keyboard */} - { - setTimeout(() => hiddenInputRef.current?.focus(), 10); - }} - autoComplete="off" - autoCorrect="off" - autoCapitalize="off" - autoFocus // Attempt autofocus on mount - /> +
+ +
+ +
+ {bufferText !== "" && } +
+ +
+
0 ? { + position: 'fixed', + bottom: `${keyboardHeight}px`, + left: 0, + right: 0, + zIndex: 50, + height: vh ? `${vh / 14}px` : '7.14vh' + } : { height: '100%' }}> + setScrollMode(!scrollMode)} + onLeftClick={() => handleClick('left')} + onRightClick={() => handleClick('right')} + onKeyboardToggle={handleToggleKeyboard} + onModifierToggle={handleModifierState} + /> +
+
+ +
+ +
+ +
+ +
+ +
+ + +
) }