-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: navigate with Shoulder buttons (#34)
Previously I've mapped for my specific case, with an Arcade Stick, now shoulder buttons should work for most cases.
- Loading branch information
Showing
11 changed files
with
240 additions
and
120 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import { tabNext, tabPrev } from "@app/dom"; | ||
|
||
/** | ||
* Buttons as defined by Controller.js | ||
* https://github.com/samiare/Controller.js/blob/347fc1416b7d5889983933a916cd7d1eeb3ee166/source/lib/GC_Layouts.js#L62-L81 | ||
*/ | ||
export type Button = | ||
| "FACE_1" | ||
| "FACE_2" | ||
| "FACE_3" | ||
| "FACE_4" | ||
| "LEFT_SHOULDER" | ||
| "RIGHT_SHOULDER" | ||
| "LEFT_SHOULDER_BOTTOM" | ||
| "RIGHT_SHOULDER_BOTTOM" | ||
| "SELECT" | ||
| "START" | ||
| "LEFT_ANALOG_BUTTON" | ||
| "RIGHT_ANALOG_BUTTON" | ||
| "DPAD_UP" | ||
| "DPAD_DOWN" | ||
| "DPAD_LEFT" | ||
| "DPAD_RIGHT" | ||
| "HOME"; | ||
|
||
export type NavigationControls = | ||
| "Up" | ||
| "Down" | ||
| "Right" | ||
| "Left" | ||
| "Confirm" | ||
| "Return" | ||
| "TabPrev" | ||
| "TabNext"; | ||
|
||
/* | ||
* given a layout and a Controllerjs button | ||
* return the mapping | ||
* The reason is that the mapping is defined as a record where keys | ||
* are NavigationControls so that the domain is smaller | ||
* but when figuring out the mapping, we need the reverse operation | ||
*/ | ||
export function convertToNavigationControl( | ||
mappings: Layout["Mappings"], | ||
button: Button | ||
): NavigationControls | undefined { | ||
const reverse = Object.fromEntries( | ||
Object.entries(mappings).map(([key, value]) => [value, key]) | ||
) as Record<Button, NavigationControls>; | ||
return reverse[button]; | ||
} | ||
|
||
export function toKeyboardEvent( | ||
mappings: Layout["Mappings"], | ||
button: Button, | ||
activeElement: Element | null | ||
) { | ||
const nc = convertToNavigationControl(mappings, button); | ||
switch (nc) { | ||
case "TabPrev": { | ||
return tabPrev; | ||
} | ||
case "TabNext": { | ||
return tabNext; | ||
} | ||
|
||
case undefined: { | ||
return () => {}; | ||
} | ||
|
||
default: { | ||
return () => { | ||
activeElement?.dispatchEvent( | ||
new KeyboardEvent("keydown", { key: toKey(nc), bubbles: true }) | ||
); | ||
}; | ||
} | ||
} | ||
} | ||
|
||
function toKey(nc: Exclude<NavigationControls, "TabPrev" | "TabNext">) { | ||
switch (nc) { | ||
case "Up": | ||
case "Right": | ||
case "Down": | ||
case "Left": { | ||
return `Arrow${nc}`; | ||
} | ||
case "Confirm": { | ||
return "Enter"; | ||
} | ||
case "Return": { | ||
return "Escape"; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { Button } from "./buttons"; | ||
|
||
// Source: https://github.com/microsoft/TypeScript/issues/28357#issuecomment-748550734 | ||
declare global { | ||
interface GlobalEventHandlersEventMap { | ||
"gc.controller.found": ControllerConnectEvent; | ||
"gc.controller.lost": ControllerDisconnectEvent; | ||
"gc.button.press": ControllerButtonPressed; | ||
} | ||
} | ||
|
||
type ControllerConnectEvent = CustomEvent<{ | ||
index: number; | ||
controller: { | ||
index: number; | ||
name: string; | ||
}; | ||
}>; | ||
type ControllerDisconnectEvent = CustomEvent<{ index: number }>; | ||
type ControllerButtonPressed = CustomEvent<{ | ||
controllerIndex: number; | ||
name: Button; | ||
value: number; | ||
pressed: true; | ||
time: number; | ||
}>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { Controller } from "@controllerjs"; | ||
import { notify } from "@app/notify"; | ||
import { toKeyboardEvent } from "@app/gamepad/buttons"; | ||
import { identifyLayout } from "@app/gamepad/layouts/identify"; | ||
import { findFirstFocusableChild } from "@app/dom"; | ||
|
||
type ControllerInfo = { | ||
name: string; | ||
layout: Layout; | ||
}; | ||
type ControllerIndex = number; | ||
const controllers: Record<ControllerIndex, ControllerInfo> = []; | ||
|
||
export function initGamepad() { | ||
Controller.globalSettings.useAnalogAsDpad = "left"; | ||
Controller.search(); | ||
|
||
window.addEventListener( | ||
"gc.controller.found", | ||
function (event) { | ||
const ci: ControllerInfo = { | ||
layout: identifyLayout(event.detail.controller.name), | ||
name: event.detail.controller.name, | ||
}; | ||
|
||
controllers[event.detail.controller.index] = ci; | ||
|
||
const controller = event.detail.controller; | ||
notify( | ||
`[CONNECTED]: Controller ${controller.name} recognized at index ${controller.index} with layout ${ci.layout.Name}` | ||
); | ||
}, | ||
false | ||
); | ||
window.addEventListener( | ||
"gc.controller.lost", | ||
function (event) { | ||
notify(`[DISCONNECTED]: Controller at index ${event.detail.index}`); | ||
}, | ||
false | ||
); | ||
|
||
window.addEventListener("gc.button.press", function (event) { | ||
const activeElement = document.activeElement; | ||
|
||
// If nothing is focused, navigation won't work | ||
if (!activeElement || activeElement === document.body) { | ||
const firstFocusable = findFirstFocusableChild(document.body); | ||
firstFocusable?.focus(); | ||
} | ||
|
||
// It may be possible that this event is dispatched before the controller initialization | ||
const controller = controllers[event.detail.controllerIndex]; | ||
if (!controller) { | ||
return; | ||
} | ||
|
||
const ev = toKeyboardEvent( | ||
controller.layout.Mappings, | ||
event.detail.name, | ||
document.activeElement | ||
); | ||
|
||
if (ev) { | ||
ev(); | ||
} | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
export const EightBitDoArcadeStick: Layout = { | ||
Name: "8BitDo Arcade Stick", | ||
Mappings: { | ||
Up: "DPAD_UP", | ||
Down: "DPAD_DOWN", | ||
Left: "DPAD_LEFT", | ||
Right: "DPAD_RIGHT", | ||
Confirm: "FACE_1", | ||
Return: "FACE_2", | ||
TabPrev: "RIGHT_SHOULDER_BOTTOM", | ||
TabNext: "LEFT_SHOULDER_BOTTOM", | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
export const GenericLayout: Layout = { | ||
Name: "Generic", | ||
Mappings: { | ||
Up: "DPAD_UP", | ||
Down: "DPAD_DOWN", | ||
Left: "DPAD_LEFT", | ||
Right: "DPAD_RIGHT", | ||
Confirm: "FACE_1", | ||
Return: "FACE_2", | ||
TabPrev: "LEFT_SHOULDER", | ||
TabNext: "RIGHT_SHOULDER", | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { GenericLayout } from "@app/gamepad/layouts"; | ||
import { EightBitDoArcadeStick } from "./8bitdo_arcade_stick"; | ||
|
||
export function identifyLayout(name: string): Layout { | ||
switch (name) { | ||
case EightBitDoArcadeStick.Name: { | ||
return EightBitDoArcadeStick; | ||
} | ||
|
||
default: | ||
return GenericLayout; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from "./8bitdo_arcade_stick"; | ||
export * from "./generic"; | ||
export * from "./identify"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
type Layout = { | ||
/** name of the controller as provided by the driver. */ | ||
Name: string; | ||
Mappings: Record<NavigationControls, Button>; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters