export type ButtonRect = [x: number, y: number, w: number, h: number]; export interface ButtonNavigationConfig { up?: T | "last"; down?: T | "last"; left?: T; right?: T; horizontalMode?: "navigate" | "preview"; } export class ButtonNavigation { private selectedButton: T; private nextButton?: T; private buttons: Record; private navigation: Record>; private onButtonClick?: (buttonName: T) => void; private keydownHandler: (event: KeyboardEvent) => void; constructor(config: { buttons: Record; initialButton: T; navigation: Record>; onButtonClick?: (buttonName: T) => void; }) { this.buttons = config.buttons; this.selectedButton = config.initialButton; this.navigation = config.navigation; this.onButtonClick = config.onButtonClick; // TODO: this should be handled by the nds itself i think, and dispatched this.keydownHandler = this.handleKeyPress.bind(this); window.addEventListener("keydown", this.keydownHandler); } public getSelectedButton(): T { return this.selectedButton; } public getSelectorPosition(): ButtonRect { return this.buttons[this.selectedButton]; } public handleTouch(x: number, y: number): void { for (const [buttonName, config] of Object.entries(this.buttons) as [ T, ButtonRect, ][]) { const [sx, sy, sw, sh] = config; if (x >= sx && x <= sx + sw && y >= sy && y <= sy + sh) { if (this.selectedButton === buttonName) { this.onButtonClick?.(buttonName); } else { if ( (this.navigation[buttonName].down === "last" && this.navigation[this.selectedButton]!.up === buttonName) || (this.navigation[buttonName].up === "last" && this.navigation[this.selectedButton]!.down === buttonName) ) { this.nextButton = this.selectedButton; } this.selectedButton = buttonName; } break; } } } private handleKeyPress(event: KeyboardEvent): void { const currentButton = this.selectedButton; const currentNav = this.navigation[currentButton]; if (!currentNav) return; switch (event.key) { case "ArrowUp": if (!currentNav.up) return; if (currentNav.up === "last") { if (this.nextButton) { this.selectedButton = this.nextButton; } else { this.selectedButton = (currentNav.left ?? currentNav.right) as T; } } else { if (this.navigation[currentNav.up].down === "last") { this.nextButton = this.selectedButton; } this.selectedButton = currentNav.up; } break; case "ArrowDown": if (!currentNav.down) return; if (currentNav.down === "last") { if (this.nextButton) { this.selectedButton = this.nextButton; } else { this.selectedButton = (currentNav.left ?? currentNav.right) as T; } } else { if (this.navigation[currentNav.down].up === "last") { this.nextButton = this.selectedButton; } this.selectedButton = currentNav.down; } break; case "ArrowLeft": if (!currentNav.left) return; if (currentNav.horizontalMode === "preview") { this.nextButton = currentNav.left; } else { this.selectedButton = currentNav.left; } break; case "ArrowRight": if (!currentNav.right) return; if (currentNav.horizontalMode === "preview") { this.nextButton = currentNav.right; } else { this.selectedButton = currentNav.right; } break; case "Enter": case " ": this.onButtonClick?.(this.selectedButton); break; default: return; } event.preventDefault(); } public destroy(): void { window.removeEventListener("keydown", this.keydownHandler); } }