import { useShallow } from "zustand/react/shallow"; 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, useSettingsStore, useUiStore } from "@/hooks/stores"; import useKeyboard from "@/hooks/useKeyboard"; import { keyDisplayMap, keys, modifiers } from "@/keyboardMappings"; export const DetachIcon = ({ className }: { className?: string }) => { return Detach Icon; }; const AttachIcon = ({ className }: { className?: string }) => { return Attach Icon; }; function KeyboardWrapper() { const [layoutName, setLayoutName] = useState("default"); const [depressedButtons, setDepressedButtons] = useState(""); 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(useShallow(state => state.keyboardLedState?.caps_lock)); // HID related states const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable); const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync); const isKeyboardLedManagedByHost = useMemo(() => keyboardLedSync !== "browser" && keyboardLedStateSyncAvailable, [keyboardLedSync, keyboardLedStateSyncAvailable], ); const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive); const isShiftActive = useHidStore(state => state.isShiftActive); const setIsShiftActive = useHidStore(state => state.setIsShiftActive); const isCtrlActive = useHidStore(state => state.isCtrlActive); const setIsCtrlActive = useHidStore(state => state.setIsCtrlActive); const isAltActive = useHidStore(state => state.isAltActive); const setIsAltActive = useHidStore(state => state.setIsAltActive); const isMetaActive = useHidStore(state => state.isMetaActive); const setIsMetaActive = useHidStore(state => state.setIsMetaActive); const isAltGrActive = useHidStore(state => state.isAltGrActive); const setIsAltGrActive = useHidStore(state => state.setIsAltGrActive); 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]); useEffect(() => { // if you have the CapsLock "down", then the shift state is inverted const effectiveShift = isCapsLockActive ? false === isShiftActive : isShiftActive; setLayoutName(effectiveShift ? "shift" : "default"); }, [setLayoutName, isCapsLockActive, isShiftActive] ); // this causes the buttons to look depressed/clicked depending on the sticky state useEffect(() => { let buttons = "None "; // make sure we name at least one (fake) button if (isCapsLockActive) buttons += "CapsLock "; if (isShiftActive) buttons += "ShiftLeft ShiftRight "; if (isCtrlActive) buttons += "ControlLeft ControlRight "; if (isAltActive) buttons += "AltLeft AltRight "; if (isMetaActive) buttons += "MetaLeft MetaRight "; setDepressedButtons(buttons.trimEnd()); }, [setDepressedButtons, isCapsLockActive, isShiftActive, isCtrlActive, isAltActive, isMetaActive, isAltGrActive] ); const onKeyPress = useCallback((key: string) => { // handle the fake combo keys first 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 (key === "CtrlAltBackspace") { sendKeyboardEvent( [keys["Backspace"]], [modifiers["ControlLeft"], modifiers["AltLeft"]], ); setTimeout(resetKeyboardState, 100); return; } // strip away the parens for shifted characters const cleanKey = key.replace(/[()]/g, ""); const passthrough = ["PrintScreen", "SystemRequest", "Pause", "Break", "ScrollLock", "Enter", "Space"].find((value) => value === cleanKey); if (passthrough) { emitkeycode(cleanKey); return; } // adjust the sticky state of the Shift/Ctrl/Alt/Meta/AltGr if (key === "CapsLock" && !isKeyboardLedManagedByHost) setIsCapsLockActive(!isCapsLockActive); else if (key === "ShiftLeft" || key === "ShiftRight") setIsShiftActive(!isShiftActive); else if (key === "ControlLeft" || key === "ControlRight") setIsCtrlActive(!isCtrlActive); else if (key === "AltLeft" || key === "AltRight") setIsAltActive(!isAltActive); else if (key === "MetaLeft" || key === "MetaRight") setIsMetaActive(!isMetaActive); else if (key === "AltGr") setIsAltGrActive(!isAltGrActive); emitkeycode(cleanKey); function emitkeycode(key: string) { const effectiveMods: number[] = []; if (isShiftActive) effectiveMods.push(modifiers["ShiftLeft"]); if (isCtrlActive) effectiveMods.push(modifiers["ControlLeft"]); if (isAltActive) effectiveMods.push(modifiers["AltLeft"]); if (isMetaActive) effectiveMods.push(modifiers["MetaLeft"]); if (isAltGrActive) { effectiveMods.push(modifiers["MetaRight"]); effectiveMods.push(modifiers["CtrlLeft"]); } const keycode = keys[key]; if (keycode) { // send the keycode with modifiers sendKeyboardEvent([keycode], effectiveMods); } // release the key (if one pressed), but retain the modifiers setTimeout(() => sendKeyboardEvent([], effectiveMods), 50); } }, [isKeyboardLedManagedByHost, setIsCapsLockActive, isCapsLockActive, setIsShiftActive, isShiftActive, setIsCtrlActive, isCtrlActive, setIsAltActive, isAltActive, setIsMetaActive, isMetaActive, setIsAltGrActive, isAltGrActive, sendKeyboardEvent, resetKeyboardState ], ); const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled); const setVirtualKeyboard = useHidStore(state => state.setVirtualKeyboardEnabled); return (
{virtualKeyboard && (
{showAttachedVirtualKeyboard ? (

Virtual Keyboard

)}
); } export default KeyboardWrapper;