t2-mapper/src/components/InspectorControls.tsx

238 lines
7.1 KiB
TypeScript
Raw Normal View History

import {
useControls,
useDebug,
useSettings,
type TouchMode,
} from "./SettingsProvider";
2025-12-02 16:58:35 -08:00
import { MissionSelect } from "./MissionSelect";
import { useEffect, useState, useRef, RefObject } from "react";
import { CopyCoordinatesButton } from "./CopyCoordinatesButton";
2026-02-20 15:48:15 -08:00
import { LoadDemoButton } from "./LoadDemoButton";
2026-02-28 17:58:09 -08:00
import { useDemoRecording } from "./DemoProvider";
2026-02-19 05:51:55 -08:00
import { FiInfo, FiSettings } from "react-icons/fi";
import { Camera } from "three";
2026-03-01 09:40:17 -08:00
import styles from "./InspectorControls.module.css";
2025-11-13 22:55:58 -08:00
export function InspectorControls({
missionName,
2025-12-14 11:06:57 -08:00
missionType,
2025-11-13 22:55:58 -08:00
onChangeMission,
2026-02-19 05:51:55 -08:00
onOpenMapInfo,
isTouch,
cameraRef,
2025-11-13 22:55:58 -08:00
}: {
missionName: string;
2025-12-14 11:06:57 -08:00
missionType: string;
onChangeMission: ({
missionName,
missionType,
}: {
missionName: string;
missionType: string;
}) => void;
2026-02-19 05:51:55 -08:00
onOpenMapInfo: () => void;
isTouch: boolean | null;
cameraRef: RefObject<Camera>;
2025-11-13 22:55:58 -08:00
}) {
const {
fogEnabled,
setFogEnabled,
fov,
setFov,
2025-11-15 16:33:18 -08:00
audioEnabled,
setAudioEnabled,
2025-12-01 22:33:12 -08:00
animationEnabled,
setAnimationEnabled,
} = useSettings();
const { speedMultiplier, setSpeedMultiplier, touchMode, setTouchMode } =
useControls();
2025-11-25 23:44:37 -08:00
const { debugMode, setDebugMode } = useDebug();
2026-02-28 17:58:09 -08:00
const demoRecording = useDemoRecording();
const isDemoLoaded = demoRecording != null;
const [settingsOpen, setSettingsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const focusAreaRef = useRef<HTMLDivElement>(null);
// Focus the panel when it opens.
useEffect(() => {
if (settingsOpen) {
dropdownRef.current?.focus();
}
}, [settingsOpen]);
const handleDropdownBlur = (e: React.FocusEvent) => {
const relatedTarget = e.relatedTarget as Node | null;
if (relatedTarget && focusAreaRef.current?.contains(relatedTarget)) {
return;
}
setSettingsOpen(false);
};
// Close on Escape and return focus to the gear button.
const handlePanelKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Escape") {
setSettingsOpen(false);
buttonRef.current?.focus();
}
};
2025-11-13 22:55:58 -08:00
return (
2025-11-14 00:15:28 -08:00
<div
id="controls"
2026-03-01 09:40:17 -08:00
className={styles.Controls}
2025-11-14 22:46:58 -08:00
onKeyDown={(e) => e.stopPropagation()}
2025-11-14 00:15:28 -08:00
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
>
2026-03-01 09:40:17 -08:00
<div className={styles.MissionSelectWrapper}>
<MissionSelect
value={missionName}
missionType={missionType}
onChange={onChangeMission}
disabled={isDemoLoaded}
/>
</div>
<div ref={focusAreaRef}>
<button
ref={buttonRef}
2026-03-01 09:40:17 -08:00
className={styles.Toggle}
onClick={() => {
setSettingsOpen((isOpen) => !isOpen);
}}
aria-expanded={settingsOpen}
aria-controls="settingsPanel"
aria-label="Settings"
>
<FiSettings />
</button>
<div
2026-03-01 09:40:17 -08:00
className={styles.Dropdown}
ref={dropdownRef}
id="settingsPanel"
tabIndex={-1}
onKeyDown={handlePanelKeyDown}
onBlur={handleDropdownBlur}
data-open={settingsOpen}
>
2026-03-01 09:40:17 -08:00
<div className={styles.Group}>
2026-02-19 05:51:55 -08:00
<CopyCoordinatesButton
missionName={missionName}
missionType={missionType}
cameraRef={cameraRef}
2026-02-19 05:51:55 -08:00
/>
2026-02-20 15:48:15 -08:00
<LoadDemoButton />
2026-02-19 05:51:55 -08:00
<button
type="button"
2026-03-01 09:40:17 -08:00
className={styles.MapInfoButton}
2026-02-19 05:51:55 -08:00
aria-label="Show map info"
onClick={onOpenMapInfo}
>
<FiInfo />
2026-03-01 09:40:17 -08:00
<span className={styles.ButtonLabel}>Show map info</span>
2026-02-19 05:51:55 -08:00
</button>
</div>
2026-03-01 09:40:17 -08:00
<div className={styles.Group}>
<div className={styles.CheckboxField}>
2026-02-19 05:51:55 -08:00
<input
id="fogInput"
type="checkbox"
checked={fogEnabled}
onChange={(event) => {
setFogEnabled(event.target.checked);
}}
/>
<label htmlFor="fogInput">Fog?</label>
</div>
2026-03-01 09:40:17 -08:00
<div className={styles.CheckboxField}>
2026-02-19 05:51:55 -08:00
<input
id="audioInput"
type="checkbox"
checked={audioEnabled}
onChange={(event) => {
setAudioEnabled(event.target.checked);
}}
/>
<label htmlFor="audioInput">Audio?</label>
</div>
</div>
2026-03-01 09:40:17 -08:00
<div className={styles.Group}>
<div className={styles.CheckboxField}>
2026-02-19 05:51:55 -08:00
<input
id="animationInput"
type="checkbox"
checked={animationEnabled}
onChange={(event) => {
setAnimationEnabled(event.target.checked);
}}
/>
<label htmlFor="animationInput">Animation?</label>
</div>
2026-03-01 09:40:17 -08:00
<div className={styles.CheckboxField}>
2026-02-19 05:51:55 -08:00
<input
id="debugInput"
type="checkbox"
checked={debugMode}
onChange={(event) => {
setDebugMode(event.target.checked);
}}
/>
<label htmlFor="debugInput">Debug?</label>
</div>
</div>
2026-03-01 09:40:17 -08:00
<div className={styles.Group}>
{isDemoLoaded ? null : (
<div className={styles.Field}>
<label htmlFor="fovInput">FOV</label>
<input
id="fovInput"
type="range"
min={75}
max={120}
step={5}
value={fov}
disabled={isDemoLoaded}
onChange={(event) => setFov(parseInt(event.target.value))}
/>
<output htmlFor="fovInput">{fov}</output>
</div>
)}
{isDemoLoaded ? null : (
<div className={styles.Field}>
<label htmlFor="speedInput">Speed</label>
<input
id="speedInput"
type="range"
min={0.1}
max={5}
step={0.05}
value={speedMultiplier}
disabled={isDemoLoaded}
onChange={(event) =>
setSpeedMultiplier(parseFloat(event.target.value))
}
/>
</div>
)}
2026-02-19 05:51:55 -08:00
</div>
{isTouch && (
2026-03-01 09:40:17 -08:00
<div className={styles.Group}>
<div className={styles.Field}>
2026-02-19 05:51:55 -08:00
<label htmlFor="touchModeInput">Joystick:</label>{" "}
<select
id="touchModeInput"
value={touchMode}
onChange={(e) => setTouchMode(e.target.value as TouchMode)}
>
<option value="dualStick">Dual Stick</option>
<option value="moveLookStick">Single Stick</option>
</select>
</div>
</div>
)}
</div>
</div>
2025-11-13 22:55:58 -08:00
</div>
);
}