import { ChevronDownIcon } from "@heroicons/react/16/solid"; import { AnimatePresence, motion } from "framer-motion"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import Keyboard from "react-simple-keyboard"; import Card from "@components/Card"; // eslint-disable-next-line import/order import { Button } from "@components/Button"; import "react-simple-keyboard/build/css/index.css"; import AttachIconRaw from "@/assets/attach-icon.svg"; import DetachIconRaw from "@/assets/detach-icon.svg"; import { cx } from "@/cva.config"; import { useHidStore, useUiStore } from "@/hooks/stores"; import useKeyboard from "@/hooks/useKeyboard"; import { useKeyboardLayout } from "@/hooks/useKeyboardLayout"; import { keys, modifiers, latchingKeys, decodeModifiers } from "@/keyboardMappings"; export const DetachIcon = ({ className }: { className?: string }) => { return ; }; const AttachIcon = ({ className }: { className?: string }) => { return ; }; function KeyboardWrapper() { const keyboardRef = useRef(null); const { isAttachedVirtualKeyboardVisible, setAttachedVirtualKeyboardVisibility } = useUiStore(); const { keysDownState, /* keyboardLedState,*/ isVirtualKeyboardEnabled, setVirtualKeyboardEnabled } = useHidStore(); const { handleKeyPress, executeMacro } = useKeyboard(); const [isDragging, setIsDragging] = useState(false); const [position, setPosition] = useState({ x: 0, y: 0 }); const [newPosition, setNewPosition] = useState({ x: 0, y: 0 }); const { keyboard } = useKeyboardLayout(); //const isCapsLockActive = useMemo(() => { // return (keyboardLedState.caps_lock); //}, [keyboardLedState]); const { isShiftActive, /*isControlActive, isAltActive, isMetaActive, isAltGrActive*/ } = useMemo(() => { return decodeModifiers(keysDownState.modifier); }, [keysDownState]); const mainLayoutName = useMemo(() => { const layoutName = isShiftActive ? "shift": "default"; return layoutName; }, [isShiftActive]); const keyNamesForDownKeys = useMemo(() => { const activeModifierMask = keysDownState.modifier || 0; const modifierNames = Object.entries(modifiers).filter(([_, mask]) => (activeModifierMask & mask) !== 0).map(([name, _]) => name); const keysDown = keysDownState.keys || []; const keyNames = Object.entries(keys).filter(([_, value]) => keysDown.includes(value)).map(([name, _]) => name); return [...modifierNames,...keyNames, ' ']; // we have to have at least one space to avoid keyboard whining }, [keysDownState]); const startDrag = useCallback((e: MouseEvent | TouchEvent) => { if (!keyboardRef.current) return; if (e instanceof TouchEvent && e.touches.length > 1) return; setIsDragging(true); const clientX = e instanceof TouchEvent ? e.touches[0].clientX : e.clientX; const clientY = e instanceof TouchEvent ? e.touches[0].clientY : e.clientY; const rect = keyboardRef.current.getBoundingClientRect(); setPosition({ x: clientX - rect.left, y: clientY - rect.top, }); }, []); const onDrag = useCallback( (e: MouseEvent | TouchEvent) => { if (!keyboardRef.current) return; if (isDragging) { const clientX = e instanceof TouchEvent ? e.touches[0].clientX : e.clientX; const clientY = e instanceof TouchEvent ? e.touches[0].clientY : e.clientY; const newX = clientX - position.x; const newY = clientY - position.y; const rect = keyboardRef.current.getBoundingClientRect(); const maxX = window.innerWidth - rect.width; const maxY = window.innerHeight - rect.height; setNewPosition({ x: Math.min(maxX, Math.max(0, newX)), y: Math.min(maxY, Math.max(0, newY)), }); } }, [isDragging, position.x, position.y], ); const endDrag = useCallback(() => { setIsDragging(false); }, []); useEffect(() => { const handle = keyboardRef.current; if (handle) { handle.addEventListener("touchstart", startDrag); handle.addEventListener("mousedown", startDrag); } document.addEventListener("mouseup", endDrag); document.addEventListener("touchend", endDrag); document.addEventListener("mousemove", onDrag); document.addEventListener("touchmove", onDrag); return () => { if (handle) { handle.removeEventListener("touchstart", startDrag); handle.removeEventListener("mousedown", startDrag); } document.removeEventListener("mouseup", endDrag); document.removeEventListener("touchend", endDrag); document.removeEventListener("mousemove", onDrag); document.removeEventListener("touchmove", onDrag); }; }, [endDrag, onDrag, startDrag]); const onKeyUp = useCallback( async (_: string, e: MouseEvent | undefined) => { e?.preventDefault(); e?.stopPropagation(); }, [] ); const onKeyDown = useCallback( async (key: string, e: MouseEvent | undefined) => { e?.preventDefault(); e?.stopPropagation(); // handle the fake key-macros we have defined for common combinations if (key === "CtrlAltDelete") { await executeMacro([ { keys: ["Delete"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 } ]); return; } if (key === "AltMetaEscape") { await executeMacro([ { keys: ["Escape"], modifiers: ["AltLeft", "MetaLeft"], delay: 100 } ]); return; } if (key === "CtrlAltBackspace") { await executeMacro([ { keys: ["Backspace"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 } ]); return; } // if they press any of the latching keys, we send a keypress down event and the release it automatically (on timer) if (latchingKeys.includes(key)) { console.debug(`Latching key pressed: ${key} sending down and delayed up pair`); handleKeyPress(keys[key], true) setTimeout(() => handleKeyPress(keys[key], false), 100); return; } // if they press any of the dynamic keys, we send a keypress down event but we don't release it until they click it again if (Object.keys(modifiers).includes(key)) { const currentlyDown = keyNamesForDownKeys.includes(key); console.debug(`Dynamic key pressed: ${key} was currently down: ${currentlyDown}, toggling state`); handleKeyPress(keys[key], !currentlyDown) return; } // otherwise, just treat it as a down+up pair const cleanKey = key.replace(/[()]/g, ""); console.debug(`Regular key pressed: ${cleanKey} sending down and up pair`); handleKeyPress(keys[cleanKey], true); setTimeout(() => handleKeyPress(keys[cleanKey], false), 50); }, [executeMacro, handleKeyPress, keyNamesForDownKeys], ); return ( {isVirtualKeyboardEnabled && ( {isAttachedVirtualKeyboardVisible ? ( setAttachedVirtualKeyboardVisibility(false)} /> ) : ( setAttachedVirtualKeyboardVisibility(true)} /> )} Virtual Keyboard setVirtualKeyboardEnabled(false)} /> { /* TODO add optional number pad */ } )} ); } export default KeyboardWrapper;