mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-11-13 04:46:08 -08:00
Controller: layouts for the media timing review and duplicate card dialogs (#313)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
696462a82e
commit
6d0f37ac21
50 files changed
+4555
-46
No files matched your search
@@ -13,7 +13,7 @@ type ControllerButtonState = {
|
||||
touched?: boolean;
|
||||
};
|
||||
|
||||
type GamepadLike = {
|
||||
export type GamepadLike = {
|
||||
id: string;
|
||||
index: number;
|
||||
connected: boolean;
|
||||
@@ -22,6 +22,22 @@ type GamepadLike = {
|
||||
buttons: readonly ControllerButtonState[];
|
||||
};
|
||||
|
||||
/**
|
||||
* A screen that takes over every controller input while it is open (for example the
|
||||
* media timing review). `activate` runs on the first poll of a new session; with
|
||||
* `latchHeld` it ignores buttons already held (so the press that opened the screen does
|
||||
* not fire), otherwise it treats them as fresh presses. `poll` runs on every later frame.
|
||||
*/
|
||||
export type ControllerScreenLayout = {
|
||||
activate: (
|
||||
gamepad: GamepadLike,
|
||||
config: ResolvedControllerConfig,
|
||||
now: number,
|
||||
options: { latchHeld: boolean },
|
||||
) => void;
|
||||
poll: (gamepad: GamepadLike, config: ResolvedControllerConfig, now: number) => void;
|
||||
};
|
||||
|
||||
type GamepadControllerOptions = {
|
||||
getGamepads: () => Array<GamepadLike | null>;
|
||||
getConfig: () => ResolvedControllerConfig;
|
||||
@@ -44,9 +60,13 @@ type GamepadControllerOptions = {
|
||||
scrollPopup: (deltaPixels: number) => void;
|
||||
jumpPopup: (deltaPixels: number) => void;
|
||||
onState: (state: ControllerRuntimeSnapshot) => void;
|
||||
/** Screen layout that currently owns the controller in this window, if any. */
|
||||
getScreenLayout?: () => ControllerScreenLayout | null;
|
||||
/** True while a modal in another window owns the controller; this window stands down. */
|
||||
getSuspended?: () => boolean;
|
||||
};
|
||||
|
||||
type HoldState = {
|
||||
export type HoldState = {
|
||||
repeatStarted: boolean;
|
||||
direction: -1 | 1 | null;
|
||||
lastFireAt: number;
|
||||
@@ -65,7 +85,7 @@ const DPAD_AXIS_INDEX = {
|
||||
vertical: 7,
|
||||
} as const;
|
||||
|
||||
function normalizeRawButtonState(
|
||||
export function normalizeRawButtonState(
|
||||
button: ControllerButtonState | undefined,
|
||||
triggerDeadzone: number,
|
||||
): boolean {
|
||||
@@ -114,7 +134,7 @@ function resolveDpadAxisValue(gamepad: GamepadLike, axisIndex: number): number {
|
||||
return Math.sign(value);
|
||||
}
|
||||
|
||||
function resolveDpadHorizontalValue(gamepad: GamepadLike, triggerDeadzone: number): number {
|
||||
export function resolveDpadHorizontalValue(gamepad: GamepadLike, triggerDeadzone: number): number {
|
||||
const axisValue = resolveDpadAxisValue(gamepad, DPAD_AXIS_INDEX.horizontal);
|
||||
if (axisValue !== 0) {
|
||||
return axisValue;
|
||||
@@ -127,7 +147,7 @@ function resolveDpadHorizontalValue(gamepad: GamepadLike, triggerDeadzone: numbe
|
||||
);
|
||||
}
|
||||
|
||||
function resolveDpadVerticalValue(gamepad: GamepadLike, triggerDeadzone: number): number {
|
||||
export function resolveDpadVerticalValue(gamepad: GamepadLike, triggerDeadzone: number): number {
|
||||
const axisValue = resolveDpadAxisValue(gamepad, DPAD_AXIS_INDEX.vertical);
|
||||
if (axisValue !== 0) {
|
||||
return axisValue;
|
||||
@@ -141,7 +161,7 @@ function resolveConnectedGamepads(gamepads: Array<GamepadLike | null>): GamepadL
|
||||
.sort((left, right) => left.index - right.index);
|
||||
}
|
||||
|
||||
function createHoldState(): HoldState {
|
||||
export function createHoldState(): HoldState {
|
||||
return {
|
||||
repeatStarted: false,
|
||||
direction: null,
|
||||
@@ -150,7 +170,7 @@ function createHoldState(): HoldState {
|
||||
};
|
||||
}
|
||||
|
||||
function shouldFireHeldAction(
|
||||
export function shouldFireHeldAction(
|
||||
state: HoldState,
|
||||
now: number,
|
||||
repeatDelayMs: number,
|
||||
@@ -173,7 +193,7 @@ function shouldFireHeldAction(
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetHeldAction(state: HoldState): void {
|
||||
export function resetHeldAction(state: HoldState): void {
|
||||
state.repeatStarted = false;
|
||||
state.direction = null;
|
||||
state.lastFireAt = 0;
|
||||
@@ -198,7 +218,7 @@ function syncHeldActionBlocked(
|
||||
state.initialFired = true;
|
||||
}
|
||||
|
||||
function resolveDiscreteBindingPressed(
|
||||
export function resolveDiscreteBindingPressed(
|
||||
gamepad: GamepadLike,
|
||||
binding: ResolvedControllerDiscreteBinding,
|
||||
config: ResolvedControllerConfig,
|
||||
@@ -263,6 +283,8 @@ export function createGamepadController(options: GamepadControllerOptions) {
|
||||
let activeGamepadId: string | null = null;
|
||||
let lastPollAt: number | null = null;
|
||||
let secondarySubHeld = false;
|
||||
let activeScreenLayout: ControllerScreenLayout | null = null;
|
||||
let screenLayoutWaitingForPad = false;
|
||||
|
||||
function getConnectedGamepads(): GamepadLike[] {
|
||||
return resolveConnectedGamepads(options.getGamepads());
|
||||
@@ -423,6 +445,11 @@ export function createGamepadController(options: GamepadControllerOptions) {
|
||||
|
||||
if (!activeGamepad) {
|
||||
syncSecondarySubHold(false);
|
||||
activeScreenLayout = null;
|
||||
// Browsers hide a pad from a page until a button is pressed while it is open, and
|
||||
// each review opens in a fresh window. When a screen is already waiting for the pad,
|
||||
// the press that reveals it started after the screen opened, so it should count.
|
||||
screenLayoutWaitingForPad = config.enabled && (options.getScreenLayout?.() ?? null) !== null;
|
||||
previousActions = new Map();
|
||||
resetHeldAction(selectionHold);
|
||||
resetHeldAction(jumpHold);
|
||||
@@ -431,6 +458,7 @@ export function createGamepadController(options: GamepadControllerOptions) {
|
||||
}
|
||||
|
||||
if (activeGamepad.id !== previousActiveGamepadId) {
|
||||
activeScreenLayout = null;
|
||||
previousActions = new Map();
|
||||
resetHeldAction(selectionHold);
|
||||
resetHeldAction(jumpHold);
|
||||
@@ -438,6 +466,31 @@ export function createGamepadController(options: GamepadControllerOptions) {
|
||||
|
||||
const activeConfig = resolveControllerConfigForGamepad(config, activeGamepad.id);
|
||||
|
||||
// A screen layout or another window owning the controller pauses every normal action,
|
||||
// including the keyboard-mode toggle and the secondary subtitle hold. Their edge state
|
||||
// keeps tracking the pad, so a button still held when control returns does not fire.
|
||||
const screenLayout = activeConfig.enabled ? (options.getScreenLayout?.() ?? null) : null;
|
||||
const suspended = screenLayout === null && (options.getSuspended?.() ?? false);
|
||||
if (screenLayout !== null || suspended) {
|
||||
syncSecondarySubHold(false);
|
||||
syncBlockedInteractionState(activeGamepad, activeConfig, now);
|
||||
if (screenLayout === null) {
|
||||
activeScreenLayout = null;
|
||||
screenLayoutWaitingForPad = false;
|
||||
} else if (screenLayout !== activeScreenLayout) {
|
||||
activeScreenLayout = screenLayout;
|
||||
screenLayout.activate(activeGamepad, activeConfig, now, {
|
||||
latchHeld: !screenLayoutWaitingForPad,
|
||||
});
|
||||
screenLayoutWaitingForPad = false;
|
||||
} else {
|
||||
screenLayout.poll(activeGamepad, activeConfig, now);
|
||||
}
|
||||
return;
|
||||
}
|
||||
activeScreenLayout = null;
|
||||
screenLayoutWaitingForPad = false;
|
||||
|
||||
// Unlike most actions, this one does not require keyboard-only mode, so it can be
|
||||
// used while simply watching.
|
||||
syncSecondarySubHold(
|
||||
|
||||
Reference in new issue
Block a user