// Gamepad Mapping Settings Content
// Purpose: Defines the Gamepad Mapping Settings Content module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSettingsNamespace } from '../../settings/index.js';
import { GAMEPAD_PROFILE_DEFAULT, GAMEPAD_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
import {
computeGamepadOutputs,
createProfileForPad,
} from '../../controls/inputs/gamepadBindings.js';
import { useGamepadHubState } from '../../controls/inputs/gamepadHub.js';
import CardFrame from '../CardFrame/index.jsx';
import SliderField from './SliderField.jsx';
import { ACTIONS, NUMBER_FORMAT } from './constants.js';
import {
formatSource,
groupActions,
pickActivePad,
snapshotBaseline,
buildDescriptorFromCapture,
} from './helpers.js';
function SettingsGroupLabel({ children }) {
// Section labels stay visually modest so this panel matches the rest of the app, while white
// text keeps them readable without uppercase or oversized type.
return
{children}
;
}
function MappingRow({
action,
source,
isCapturing,
onClear,
onCapture,
onInvert,
}) {
// Mapping rows are constrained to a readable width so the source text and buttons remain
// visually connected. Buttons wrap on very narrow panes instead of forcing tiny text.
return (
{action.label}
{formatSource(source)}
{/* Axis-pair controls expose independent inversion because stick X/Y directions often
differ between browser mappings. Keeping those buttons beside the source avoids
making users hunt across a wide row while testing a binding. */}
{action.kind === 'axisPair' && (
<>
>
)}
{/* Single-axis mappings only have one inversion flag, so they render the smaller control
set and keep button clutter down for trigger-like bindings. */}
{action.kind === 'axis' && (
)}
{/* Clear and Capture are always present because they are the primary row actions. They
wrap with the inversion controls on narrow panes instead of shrinking text. */}
Calibration
{/* Calibration controls stay in one stacked column because range inputs become harder to
tune when squeezed into multiple narrow columns. */}
updateCalibration({ driveDeadzone: value })}
/>
updateCalibration({ cameraDeadzone: value })}
/>
updateCalibration({ auxDeadzone: value })}
/>
updateCalibration({ auxSideScale: value })}
/>
updateCalibration({ cameraSensitivity: value })}
/>
{actions.map((action) => {
const binding = activeProfile.bindings?.[action.id];
const source = binding?.sources?.[0] ?? null;
// The binding data is unchanged; MappingRow only changes presentation so the
// existing capture, clear, and invert handlers continue to own behavior.
return (
);
})}