Skip to content

Commit

Permalink
initial gamepad/keyboard navigation support
Browse files Browse the repository at this point in the history
Far from complete, but this does the bulk of the work, the rest is just
fixing places where the selection moves in weird ways after doing some
things, and overall improving the look and feel of it.
  • Loading branch information
0neGal committed Jun 14, 2024
1 parent 04b0e9f commit 1384647
Show file tree
Hide file tree
Showing 9 changed files with 1,024 additions and 9 deletions.
9 changes: 6 additions & 3 deletions src/app/css/launcher.css
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@
.contentMenu li:last-child {margin-right: 0px}
.contentMenu li:first-child {margin-left: 0px}

.contentMenu li:hover {opacity: 0.7}
.contentMenu li:hover,
.contentMenu li.active-selection {opacity: 0.7}

.contentMenu li[active] {
opacity: 1.0;
Expand Down Expand Up @@ -305,7 +306,8 @@ button:has(img):has(span) img {
transition: opacity 0.2s ease-in-out;
}

button:has(img):has(span):hover img {
button:has(img):has(span):hover img,
button:has(img):has(span).active-selection img {
opacity: 1.0;
}

Expand All @@ -315,7 +317,8 @@ button:has(img):has(span) span {
transition: right 0.2s ease-in-out;
}

button:has(img):has(span):hover span {
button:has(img):has(span):hover span,
button:has(img):has(span).active-selection span {
right: 0.1em;
}

Expand Down
19 changes: 19 additions & 0 deletions src/app/css/selection.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#selection {
z-index: 99999999999999;

transition: 0.15s ease-in-out;
transition-property:
opacity, border-radius,
transform, width, height, top, left;

position: fixed;
pointer-events: none;
transform: scale(1.0);
background: rgba(255, 255, 255, 0.3);
border-radius: calc(var(--padding) / 2);
}

#selection.keyboard-selecting,
#selection.controller-selecting {
transform: scale(1.1);
}
2 changes: 1 addition & 1 deletion src/app/css/theming.css
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ a.disabled:not("[onclick='kill('game')']") {
pointer-events: none;
}

a:hover {
a:hover, a.active-selection {
filter: brightness(80%) !important;
}

Expand Down
8 changes: 5 additions & 3 deletions src/app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<meta charset="utf-8">
</head>
<body>
<div id="selection"></div>

<div id="tooltip">Test</div>

<div id="bgHolder"></div>
Expand Down Expand Up @@ -33,7 +35,7 @@
<div id="overlay" onclick="popups.set_all(false)"></div>
<div class="popup" id="options">
<div class="misc">
<input class="search" placeholder="%%gui.search%%">
<input class="search default-selection" placeholder="%%gui.search%%">
<button id="apply" onclick="settings.popup.apply();settings.popup.toggle(false)">
<img src="icons/apply.png">
%%gui.settings.save%%
Expand Down Expand Up @@ -204,7 +206,7 @@ <h2>%%gui.settings.title.misc%%</h2>
</div>

<div class="misc">
<input class="search" placeholder="%%gui.search%%">
<input class="search default-selection" placeholder="%%gui.search%%">
<button id="filter" onclick="browser.filters.toggle()">
<img src="icons/filter.png">
</button>
Expand All @@ -217,7 +219,7 @@ <h2>%%gui.settings.title.misc%%</h2>
</div>
</div>
<div class="popup small blur" id="preview">
<div class="misc fixed vertical">
<div class="misc fixed vertical default-selection">
<button id="close" onclick="browser.preview.hide()">
<img src="icons/close.png">
</button>
Expand Down
301 changes: 301 additions & 0 deletions src/app/js/gamepad.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
const navigate = require("./navigate");

window.addEventListener("gamepadconnected", (e) => {
console.log("Gamepad connected:", e.gamepad.id);
}, false)

window.addEventListener("gamepaddisconnected", (e) => {
console.log("Gamepad disconnected:", e.gamepad.id);
}, false)

// this contains the names/directions of axes and IDs that have
// previously been pressed, if it is found that these were recently
// pressed in the next iteration of the `setInterval()` below than the
// iteration is skipped
//
// the value of each item is equivalent to the amount of iterations to
// wait, so `up: 3` will cause it to wait 3 iterations, before `up` can
// be pressed again
let delay_press = {};

setInterval(() => {
let gamepads = navigator.getGamepads();

// this has a list of all the directions that the `.axes[]` are
// pointing in, letting us navigate in that direction
let directions = {}

// keeps track of which buttons `delay_press` that have already been
// lowered, that way we can lower the ones that haven't been lowered
// through a button press
let lowered_delay = [];

// is the select/accept button being held
let selecting = false;

for (let i in gamepads) {
if (! gamepads[i]) {continue}
// every other `.axes[]` element is a different coordinate, each
// analog stick has 2 elements in `.axes[]`, the first one is
// the x coordinate, second is the y coordinate
//
// so we use this to keep track of which coordinate we're
// currently on, and thereby the direction of the float inside
// `.axes[i]`
let coord = "x";
let deadzone = 0.5;

for (let ii = 0; ii < gamepads[i].axes.length; ii++) {
let value = gamepads[i].axes[ii];

// check if we're beyond the deadzone in both the negative
// and positive direction, and then using `coord` add a
// direction to `directions`
if (value < -deadzone) {
if (coord == "y") {
directions.up = true;
} else {
directions.left = true;
}
} else if (value > deadzone) {
if (coord == "y") {
directions.down = true;
} else {
directions.right = true;
}
}

// flip `coord`
if (coord == "x") {
coord = "y";
} else {
coord = "x";
}
}

// only support "standard" button layouts/mappings
//
// TODO: for anybody reading this in the future, the support
// for other mappings is something that's on the table,
// however, due to not having all the hardware in the world,
// this will have to be up to someone else
if (gamepads[i].mapping != "standard") {
continue;
}

for (let ii = 0; ii < gamepads[i].buttons.length; ii++) {
if (! gamepads[i].buttons[ii].pressed) {
continue;
}

// a list of known combinations of buttons for the most
// common brands out there, more should possibly be added
let brands = {
"Xbox": {
accept: 0,
cancel: 1
},
"Nintendo": {
accept: 1,
cancel: 0
},
"PlayStation": {
accept: 0,
cancel: 1
}
}

// this is the most common setup, to my understanding, with
// the exception of third party Nintendo controller, may
// need to be adjusted in the future
let buttons = {
accept: 0,
cancel: 1
}

// set `cancel` and `accept` accordingly to the ID of the
// gamepad, if its a known brand
for (let brand in brands) {
// unknown brand
if (! gamepads[i].id.includes(brand)) {
continue;
}

// set buttons according to brand
buttons = brands[brand];
break;
}

// if the button that's being pressed is the "accept"
// button, then we set `selecting` to `true`, this is done
// before we check for the button delay so that holding the
// button keeps the selection in place, until the button is
// no longer pressed
if (ii == buttons.accept) {
selecting = true;
}

// if this button is still delayed, we lower the delay and
// then go to the next button
if (delay_press[ii]) {
delay_press[ii]--;
lowered_delay.push(ii);
continue;
}

// add delay to this button, so it doesn't get clicked
// immediately again after this
delay_press[ii] = 3;

// interpret `ii` as a specific button/action, using the
// standard IDs: https://w3c.github.io/gamepad/#remapping
switch(ii) {
case 4: // switch tab (prev)
case 5: // switch tab (next)

case 12: navigate.move("up"); break;
case 13: navigate.move("down"); break;
case 14: navigate.move("left"); break;
case 15: navigate.move("right"); break;

case buttons.accept: navigate.select(); break;

case buttons.cancel: popups.set_all(false); break;
}
}
}

for (let i in directions) {
if (directions[i] === true) {
// if this direction is still delayed, we lower the delay,
// and then go to the next direction
if (delay_press[i]) {
delay_press[i]--;
lowered_delay.push(i);
continue;
}

// move in the direction
navigate.move(i);

// add delay to this direction, to prevent it from being
// triggered immediately again
delay_press[i] = 3;
}
}

// run through buttons that have or have had a delay
for (let i in delay_press) {
// if a button has a delay, and it hasn't already been lowered,
// then we lower it
if (delay_press[i] && ! lowered_delay.includes(i)) {
delay_press[i]--;
}
}

let selection_el = document.getElementById("selection");

// add `.selecting` to `#selection` depending on whether
// `selecting`, is set or not
if (selecting) {
selection_el.classList.add("controller-selecting");
} else {
selection_el.classList.remove("controller-selecting");
}
}, 50)


let can_keyboard_navigate = (e) => {
// quite empty right now, might add more in the future, these are
// just element selectors where movement with the keyboard is off
let ignore_on_focus = [
"input",
"select"
]

// check for whether the active element is one that matches
// something in `ignore_on_focus`
for (let i = 0; i < ignore_on_focus.length; i++) {
if (! document.activeElement.matches(ignore_on_focus)) {
// active element does not match to `ignore_on_focus[i]`
continue;
}

// if the key that's being pressed is "Escape" then we unfocus
// to the currently focused active element, this lets you go
// into an input, and then exit it as well
if (e.key == "Escape") {
document.activeElement.blur();
}

return false;
}

// check if there's already an active selection
if (document.querySelector(".active-selection")) {
// this is a list of keys where this keyboard event will be
// cancelled on, this prevents key events from being sent to
// element, but still lets you type
let cancel_keys = [
"Space", "Enter",
"ArrowUp", "ArrowDown",
"ArrowLeft", "ArrowRight"
]

// cancel this keyboard event if `e.key` is inside `cancel_keys`
if (cancel_keys.includes(e.code)) {
e.preventDefault();
}
}

return true;
}

window.addEventListener("keydown", (e) => {
// do nothing if we cant navigate
if (! can_keyboard_navigate(e)) {
return;
}

let select = () => {
// do nothing if this is a repeat key press
if (e.repeat) {return}

// select `.active-selection`
navigate.select();

// add `.keyboard-selecting` to `#selection`
document.getElementById("selection")
.classList.add("keyboard-selecting");
}

// perform the relevant action for the key that was pressed
switch(e.code) {
// select
case "Space": return select();
case "Enter": return select();

// move selection
case "ArrowUp": return navigate.move("up")
case "ArrowDown": return navigate.move("down")
case "ArrowLeft": return navigate.move("left")
case "ArrowRight": return navigate.move("right")
}
})

window.addEventListener("keyup", (e) => {
if (! can_keyboard_navigate(e)) {
return;
}

let selection_el = document.getElementById("selection");

// perform the relevant action for the key that was pressed
switch(e.code) {
case "Space": return selection_el
.classList.remove("keyboard-selecting");

case "Enter": return selection_el
.classList.remove("keyboard-selecting");
}
})
Loading

0 comments on commit 1384647

Please sign in to comment.