From d54d52bff08e7d94d07fcb1e2ba23b84530b30ef Mon Sep 17 00:00:00 2001 From: Pihkaal Date: Fri, 14 Nov 2025 23:21:52 +0100 Subject: [PATCH] feat: generic button navigation --- app/components/Common/ButtonSelector.vue | 77 +++++++++++++++ app/composables/useButtonNavigation.ts | 121 +++++++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 app/components/Common/ButtonSelector.vue create mode 100644 app/composables/useButtonNavigation.ts diff --git a/app/components/Common/ButtonSelector.vue b/app/components/Common/ButtonSelector.vue new file mode 100644 index 0000000..a94f522 --- /dev/null +++ b/app/components/Common/ButtonSelector.vue @@ -0,0 +1,77 @@ + + + diff --git a/app/composables/useButtonNavigation.ts b/app/composables/useButtonNavigation.ts new file mode 100644 index 0000000..308dfb2 --- /dev/null +++ b/app/composables/useButtonNavigation.ts @@ -0,0 +1,121 @@ +export type ButtonConfig = [x: number, y: number, w: number, h: number]; + +export const useButtonNavigation = >({ + buttons, + initialButton, + onButtonClick, + navigation, +}: { + buttons: T; + initialButton: keyof T; + onButtonClick?: (buttonName: keyof T) => void; + navigation?: Record< + keyof T, + { + up?: keyof T; + down?: keyof T | "last"; + left?: keyof T; + right?: keyof T; + horizontalMode?: "navigate" | "preview"; + } + >; +}) => { + const selectedButton = ref(initialButton); + const selectorPosition = computed(() => buttons[selectedButton.value]!); + + const nextButton = ref(); + + useScreenClick((x: number, y: number) => { + for (const [buttonName, config] of Object.entries(buttons) as [ + keyof T, + ButtonConfig, + ][]) { + const [sx, sy, sw, sh] = config; + if (x >= sx && x <= sx + sw && y >= sy && y <= sy + sh) { + if (selectedButton.value === buttonName) { + onButtonClick?.(buttonName); + } else { + selectedButton.value = buttonName; + } + break; + } + } + }); + + if (navigation) { + const handleKeyPress = (event: KeyboardEvent) => { + const currentButton = selectedButton.value as keyof T; + const currentNav = navigation[currentButton]; + + if (!currentNav) return; + + switch (event.key) { + case "ArrowUp": + if (!currentNav.up) return; + + if (currentNav.up === "last") { + selectedButton.value = nextButton.value; + } else { + nextButton.value = selectedButton.value as keyof T; + selectedButton.value = currentNav.up; + } + + break; + + case "ArrowDown": + if (!currentNav.down) return; + + if (currentNav.down === "last") { + if (nextButton.value) { + selectedButton.value = nextButton.value; + } else { + selectedButton.value = currentNav.left ?? currentNav.right; + } + } else { + selectedButton.value = currentNav.down; + } + break; + + case "ArrowLeft": + if (!currentNav.left) return; + + if (currentNav.horizontalMode === "preview") { + nextButton.value = currentNav.left; + } else { + selectedButton.value = currentNav.left; + } + break; + + case "ArrowRight": + if (!currentNav.right) return; + + if (currentNav.horizontalMode === "preview") { + nextButton.value = currentNav.right; + } else { + selectedButton.value = currentNav.right; + } + break; + + case "Enter": + case " ": + onButtonClick?.(selectedButton.value); + break; + } + + event.preventDefault(); + }; + + onMounted(() => { + window.addEventListener("keydown", handleKeyPress); + }); + + onUnmounted(() => { + window.removeEventListener("keydown", handleKeyPress); + }); + } + + return { + selectedButton, + selectorPosition, + }; +};