import { useCallback, useEffect, useRef, useState } from "react"; import Keyboard from "react-simple-keyboard"; import { ChevronDownIcon } from "@heroicons/react/16/solid"; import { motion, AnimatePresence } from "framer-motion"; 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 { useHidStore, useUiStore, useKeyboardMappingsStore } from "@/hooks/stores"; import { cx } from "@/cva.config"; import { keyDisplayMap } from "@/keyboardMappings/KeyboardLayouts"; import useKeyboard from "@/hooks/useKeyboard"; import DetachIconRaw from "@/assets/detach-icon.svg"; import AttachIconRaw from "@/assets/attach-icon.svg"; export const DetachIcon = ({ className }: { className?: string }) => { return ; }; const AttachIcon = ({ className }: { className?: string }) => { return ; }; function KeyboardWrapper() { const [keys, setKeys] = useState(useKeyboardMappingsStore.keys); const [chars, setChars] = useState(useKeyboardMappingsStore.chars); const [modifiers, setModifiers] = useState(useKeyboardMappingsStore.modifiers); useEffect(() => { const unsubscribeKeyboardStore = useKeyboardMappingsStore.subscribe(() => { setKeys(useKeyboardMappingsStore.keys); setChars(useKeyboardMappingsStore.chars); setModifiers(useKeyboardMappingsStore.modifiers); setMappingsEnabled(useKeyboardMappingsStore.getMappingState()); }); return unsubscribeKeyboardStore; // Cleanup on unmount }, []); const [layoutName, setLayoutName] = useState("default"); const [mappingsEnabled, setMappingsEnabled] = useState(useKeyboardMappingsStore.getMappingState()); useEffect(() => { if (mappingsEnabled) { if (layoutName == "default" ) { setLayoutName("mappedLower") } if (layoutName == "shift") { setLayoutName("mappedUpper") } } else { if (layoutName == "mappedLower") { setLayoutName("default") } if (layoutName == "mappedUpper") { setLayoutName("shift") } } }, [mappingsEnabled, layoutName]); const keyboardRef = useRef(null); const showAttachedVirtualKeyboard = useUiStore( state => state.isAttachedVirtualKeyboardVisible, ); const setShowAttachedVirtualKeyboard = useUiStore( state => state.setAttachedVirtualKeyboardVisibility, ); const { sendKeyboardEvent, resetKeyboardState } = useKeyboard(); const [isDragging, setIsDragging] = useState(false); const [position, setPosition] = useState({ x: 0, y: 0 }); const [newPosition, setNewPosition] = useState({ x: 0, y: 0 }); const isCapsLockActive = useHidStore(state => state.isCapsLockActive); const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive); 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]); // TODO implement meta key and meta key modifer // TODO implement hold functionality for key combos. (add a hold button, add all keys to an array, when released send as one) const onKeyDown = useCallback( (key: string) => { const cleanKey = key.replace(/[()]/g, ""); // Mappings const { key: mappedKey, shift, altLeft, altRight } = chars[cleanKey] ?? {}; const isKeyShift = key === "{shift}" || key === "ShiftLeft" || key === "ShiftRight"; const isKeyCaps = key === "CapsLock"; const keyHasShiftModifier = (key.includes("(") && key !== "(") || shift; // Handle toggle of layout for shift or caps lock const toggleLayout = () => { if (mappingsEnabled) { setLayoutName(prevLayout => (prevLayout === "mappedLower" ? "mappedUpper" : "mappedLower")); } else { setLayoutName(prevLayout => (prevLayout === "default" ? "shift" : "default")); } }; if (key === "CtrlAltDelete") { sendKeyboardEvent( [keys["Delete"]], [modifiers["ControlLeft"], modifiers["AltLeft"]], ); setTimeout(resetKeyboardState, 100); return; } if (key === "AltMetaEscape") { sendKeyboardEvent( [keys["Escape"]], [modifiers["MetaLeft"], modifiers["AltLeft"]], ); setTimeout(resetKeyboardState, 100); return; } if (isKeyShift || (!(layoutName == "shift" || layoutName == "mappedUpper") && isCapsLockActive)) { toggleLayout(); } if (layoutName == "shift" || layoutName == "mappedUpper") { if (!isCapsLockActive) { toggleLayout(); } if (isKeyCaps && isCapsLockActive) { toggleLayout(); setIsCapsLockActive(false); sendKeyboardEvent([keys["CapsLock"]], []); return; } } // Handle caps lock state change if (isKeyCaps) { toggleLayout(); setIsCapsLockActive(!isCapsLockActive); } // Collect new active keys and modifiers const newKeys = keys[mappedKey ?? cleanKey] ? [keys[mappedKey ?? cleanKey]] : []; const newModifiers = [ ((shift || isKeyShift)? modifiers['ShiftLeft'] : 0), (altLeft? modifiers['AltLeft'] : 0), (altRight? modifiers['AltRight'] : 0), ].filter(Boolean); // Update current keys and modifiers sendKeyboardEvent(newKeys, [...new Set(newModifiers)]); // If shift was used as a modifier and caps lock is not active, revert to default layout if (keyHasShiftModifier && !isCapsLockActive) { setLayoutName(mappingsEnabled ? "mappedLower" : "default"); } setTimeout(resetKeyboardState, 100); }, [isCapsLockActive, sendKeyboardEvent, resetKeyboardState, setIsCapsLockActive, mappingsEnabled, chars, keys, modifiers, layoutName], ); const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled); const setVirtualKeyboard = useHidStore(state => state.setVirtualKeyboardEnabled); return ( {virtualKeyboard && ( {showAttachedVirtualKeyboard ? ( setShowAttachedVirtualKeyboard(false)} /> ) : ( setShowAttachedVirtualKeyboard(true)} /> )} Virtual Keyboard setVirtualKeyboard(false)} /> ? ShiftRight", "ControlLeft AltLeft MetaLeft Space MetaRight AltRight" ], }} disableButtonHold={true} mergeDisplay={true} debug={false} /> )} ); } export default KeyboardWrapper;