import { useMemo, useState } from "react"; import { BUTTON_CAPABILITIES } from "@/constants/controls"; import { Button } from "@/components/common/Button"; import { Panel } from "@/components/common/Panel"; import { Select } from "@/components/inputs/Select"; import { TextField } from "@/components/inputs/TextField"; import { UnsavedChangesBar } from "@/components/feedback/UnsavedChangesBar"; import { PageShell } from "@/components/layout/PageShell"; import { ControllerDiagram, type ControllerView } from "@/components/controller/ControllerDiagram"; import { useControllerStore } from "@/stores/controllerStore"; import { useProfileStore } from "@/stores/profileStore"; import { useUIStore } from "@/stores/uiStore"; import type { ActionMode, ButtonMapping as ButtonMappingType, MappingTarget } from "@/types/profile"; /** Buttons that must always stay functional so the user can never lose control of the app or controller. */ const PROTECTED_CONTROL_IDS = new Set(["guide"]); const MODE_OPTIONS: { value: ActionMode; label: string }[] = [ { value: "normal", label: "Normal" }, { value: "turbo", label: "Turbo" }, { value: "toggle", label: "Toggle" }, { value: "hold", label: "Hold" }, { value: "longPress", label: "Long Press" }, { value: "doubleTap", label: "Double Tap" }, ]; type TargetKind = MappingTarget["kind"]; const TARGET_KIND_OPTIONS: { value: TargetKind; label: string }[] = [ { value: "button", label: "Button" }, { value: "key", label: "Keyboard Key" }, { value: "mouseButton", label: "Mouse Button" }, { value: "disabled", label: "Disabled" }, ]; function defaultMappingFor(controlId: string): ButtonMappingType { return { target: { kind: "button", button: controlId }, mode: "normal" }; } function isRemapped(controlId: string, mapping: ButtonMappingType | undefined): boolean { if (!mapping) return false; return !(mapping.target.kind === "button" && mapping.target.button === controlId) || mapping.mode !== "normal"; } export function ButtonMapping() { const [view, setView] = useState("front"); const [selectedId, setSelectedId] = useState(null); const liveState = useControllerStore((s) => s.liveState); const draftProfile = useProfileStore((s) => s.draftProfile); const updateDraft = useProfileStore((s) => s.updateDraft); const pushToast = useUIStore((s) => s.pushToast); const remappedIds = useMemo(() => { if (!draftProfile) return new Set(); const ids = Object.entries(draftProfile.buttonMappings) .filter(([id, mapping]) => isRemapped(id, mapping)) .map(([id]) => id); return new Set(ids); }, [draftProfile]); if (!draftProfile) { return (
); } const selectedButton = selectedId ? BUTTON_CAPABILITIES.find((b) => b.id === selectedId) : null; const mapping = selectedId ? draftProfile.buttonMappings[selectedId] ?? defaultMappingFor(selectedId) : null; function updateMapping(controlId: string, next: ButtonMappingType) { if (PROTECTED_CONTROL_IDS.has(controlId) && next.target.kind === "disabled") { pushToast("The Guide button can't be disabled — it's kept available so you can't lock yourself out.", "warning"); return; } updateDraft((draft) => ({ ...draft, buttonMappings: { ...draft.buttonMappings, [controlId]: next }, })); } return (
} >
{!selectedId || !mapping ? (

Click a button, D-pad direction, shoulder button, stick click, or paddle on the diagram to configure it.

) : (
({ value: b.id, label: b.label }))} onChange={(button) => updateMapping(selectedId, { ...mapping, target: { kind: "button", button } })} /> )} {mapping.target.kind === "key" && ( updateMapping(selectedId, { ...mapping, target: { kind: "key", key: e.currentTarget.value } }) } /> )} {mapping.target.kind === "mouseButton" && ( updateMapping(selectedId, { ...mapping, mode })} /> {mapping.mode === "turbo" && ( updateMapping(selectedId, { ...mapping, turboRateHz: Number(e.currentTarget.value) })} /> )} {mapping.mode === "longPress" && ( updateMapping(selectedId, { ...mapping, longPressMs: Number(e.currentTarget.value) })} /> )} {mapping.mode === "doubleTap" && ( updateMapping(selectedId, { ...mapping, doubleTapWindowMs: Number(e.currentTarget.value) }) } /> )}
)}
); }