Initial release of 8BitDo Control Center
Provide a Linux desktop controller configuration app with evdev input, uinput remapping, profiles, calibration, and reconnect recovery. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
141
src/pages/InputTester.tsx
Normal file
141
src/pages/InputTester.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import { clsx } from "clsx";
|
||||
import { useState } from "react";
|
||||
|
||||
import { BUTTON_CAPABILITIES } from "@/constants/controls";
|
||||
import { Badge } from "@/components/common/Badge";
|
||||
import { Button } from "@/components/common/Button";
|
||||
import { Panel } from "@/components/common/Panel";
|
||||
import { ControllerDiagram, type ControllerView } from "@/components/controller/ControllerDiagram";
|
||||
import { PageShell } from "@/components/layout/PageShell";
|
||||
import { EventLog } from "@/components/visualizations/EventLog";
|
||||
import { mockControllerBackend } from "@/services/mockControllerBackend";
|
||||
import { useControllerStore } from "@/stores/controllerStore";
|
||||
import { useProfileStore } from "@/stores/profileStore";
|
||||
|
||||
export function InputTester() {
|
||||
const [view, setView] = useState<ControllerView>("front");
|
||||
|
||||
const activeController = useControllerStore((s) => s.activeController());
|
||||
const liveState = useControllerStore((s) => s.liveState);
|
||||
const eventLog = useControllerStore((s) => s.eventLog);
|
||||
const pollRateHz = useControllerStore((s) => s.pollRateHz);
|
||||
const latencyEstimateMs = useControllerStore((s) => s.latencyEstimateMs);
|
||||
const clearEventLog = useControllerStore((s) => s.clearEventLog);
|
||||
const activeProfile = useProfileStore((s) => s.activeProfile);
|
||||
|
||||
const buttons = liveState?.buttons ?? {};
|
||||
const axes = liveState?.axes ?? {};
|
||||
const hasGyro = activeController?.capabilities.hasGyro ?? false;
|
||||
|
||||
return (
|
||||
<PageShell title="Input Tester" description="Live view of every raw and processed input event.">
|
||||
<div className="grid grid-cols-3 gap-5">
|
||||
<Panel title="Session info">
|
||||
<dl className="grid grid-cols-2 gap-y-2 text-sm">
|
||||
<dt className="text-fg-muted">Active profile</dt>
|
||||
<dd className="text-right font-medium text-fg">{activeProfile?.name ?? "—"}</dd>
|
||||
<dt className="text-fg-muted">Poll rate</dt>
|
||||
<dd className="text-right font-mono text-fg">{pollRateHz ? `${pollRateHz} Hz` : "—"}</dd>
|
||||
<dt className="text-fg-muted">Latency estimate</dt>
|
||||
<dd className="text-right font-mono text-fg">{latencyEstimateMs ? `${latencyEstimateMs} ms` : "—"}</dd>
|
||||
<dt className="text-fg-muted">Controller</dt>
|
||||
<dd className="text-right text-fg">{activeController ? "Connected" : "None"}</dd>
|
||||
</dl>
|
||||
</Panel>
|
||||
|
||||
<Panel title="Analog sticks">
|
||||
<dl className="grid grid-cols-2 gap-y-1.5 font-mono text-xs">
|
||||
<dt className="text-fg-muted">left_stick_x</dt>
|
||||
<dd className="text-right text-fg">{(axes.left_stick_x ?? 0).toFixed(3)}</dd>
|
||||
<dt className="text-fg-muted">left_stick_y</dt>
|
||||
<dd className="text-right text-fg">{(axes.left_stick_y ?? 0).toFixed(3)}</dd>
|
||||
<dt className="text-fg-muted">right_stick_x</dt>
|
||||
<dd className="text-right text-fg">{(axes.right_stick_x ?? 0).toFixed(3)}</dd>
|
||||
<dt className="text-fg-muted">right_stick_y</dt>
|
||||
<dd className="text-right text-fg">{(axes.right_stick_y ?? 0).toFixed(3)}</dd>
|
||||
</dl>
|
||||
</Panel>
|
||||
|
||||
<Panel title="Triggers & gyroscope">
|
||||
<dl className="grid grid-cols-2 gap-y-1.5 font-mono text-xs">
|
||||
<dt className="text-fg-muted">left_trigger</dt>
|
||||
<dd className="text-right text-fg">{(axes.left_trigger ?? 0).toFixed(3)}</dd>
|
||||
<dt className="text-fg-muted">right_trigger</dt>
|
||||
<dd className="text-right text-fg">{(axes.right_trigger ?? 0).toFixed(3)}</dd>
|
||||
{hasGyro ? (
|
||||
<>
|
||||
<dt className="text-fg-muted">gyro_x</dt>
|
||||
<dd className="text-right text-fg">{(axes.gyro_x ?? 0).toFixed(3)}</dd>
|
||||
<dt className="text-fg-muted">gyro_y</dt>
|
||||
<dd className="text-right text-fg">{(axes.gyro_y ?? 0).toFixed(3)}</dd>
|
||||
<dt className="text-fg-muted">gyro_z</dt>
|
||||
<dd className="text-right text-fg">{(axes.gyro_z ?? 0).toFixed(3)}</dd>
|
||||
</>
|
||||
) : (
|
||||
<dd className="col-span-2 text-fg-subtle">No gyroscope detected</dd>
|
||||
)}
|
||||
</dl>
|
||||
</Panel>
|
||||
|
||||
<Panel
|
||||
title="Controller diagram"
|
||||
description={activeController?.isSimulated ? "Click a control to simulate a press." : undefined}
|
||||
className="col-span-2"
|
||||
actions={
|
||||
<div className="flex gap-1.5">
|
||||
<Button size="sm" variant={view === "front" ? "primary" : "secondary"} onClick={() => setView("front")}>
|
||||
Front
|
||||
</Button>
|
||||
<Button size="sm" variant={view === "rear" ? "primary" : "secondary"} onClick={() => setView("rear")}>
|
||||
Rear
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="mx-auto aspect-[480/280] max-w-lg">
|
||||
<ControllerDiagram
|
||||
view={view}
|
||||
buttons={buttons}
|
||||
axes={axes}
|
||||
onSelect={(id) => mockControllerBackend.pulseButton(id)}
|
||||
/>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel title="Buttons">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{BUTTON_CAPABILITIES.map((button) => {
|
||||
const pressed = Boolean(buttons[button.id]);
|
||||
return (
|
||||
<button
|
||||
key={button.id}
|
||||
onClick={() => mockControllerBackend.pulseButton(button.id)}
|
||||
className={clsx(
|
||||
"cursor-pointer rounded-lg border px-2 py-1.5 text-center text-xs font-medium transition-colors duration-100",
|
||||
pressed ? "border-accent bg-accent text-accent-fg" : "border-border bg-surface-2 text-fg-muted hover:border-border-strong",
|
||||
)}
|
||||
>
|
||||
{button.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel
|
||||
title="Event log"
|
||||
className="col-span-3"
|
||||
actions={
|
||||
<Badge tone="neutral">
|
||||
<Button size="sm" variant="ghost" onClick={clearEventLog}>
|
||||
Clear
|
||||
</Button>
|
||||
</Badge>
|
||||
}
|
||||
>
|
||||
<EventLog events={eventLog} />
|
||||
</Panel>
|
||||
</div>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user