/* RetroArch - A frontend for libretro. * Copyright (C) 2010-2018 - Michael Lelli * Copyright (C) 2011-2017 - Daniel De Matteis * * RetroArch is free software: you can redistribute it and/or modify it under the terms * of the GNU General Public License as published by the Free Software Found- * ation, either version 3 of the License, or (at your option) any later version. * * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with RetroArch. * If not, see . */ #include #include #include #include #include #include #include #include #include #include "../input_driver.h" #include "../input_keymaps.h" #include "../../tasks/tasks_internal.h" #include "../../configuration.h" #include "../../verbosity.h" /* https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button */ #define RWEBINPUT_MOUSE_BTNL 0 #define RWEBINPUT_MOUSE_BTNM 1 #define RWEBINPUT_MOUSE_BTNR 2 #define RWEBINPUT_MOUSE_BTN4 3 #define RWEBINPUT_MOUSE_BTN5 4 typedef struct rwebinput_key_to_code_map_entry { const char *key; enum retro_key rk; } rwebinput_key_to_code_map_entry_t; typedef struct rwebinput_keyboard_event { int type; EmscriptenKeyboardEvent event; } rwebinput_keyboard_event_t; typedef struct rwebinput_keyboard_event_queue { size_t count; size_t max_size; rwebinput_keyboard_event_t *events; } rwebinput_keyboard_event_queue_t; typedef struct rwebinput_mouse_states { signed x; signed y; signed delta_x; signed delta_y; uint8_t buttons; double scroll_x; double scroll_y; } rwebinput_mouse_state_t; typedef struct rwebinput_input { bool keys[RETROK_LAST]; rwebinput_mouse_state_t mouse; const input_device_driver_t *joypad; bool blocked; } rwebinput_input_t; /* KeyboardEvent.keyCode has been deprecated for a while and doesn't have * separate left/right modifer codes, so we have to map string labels from * KeyboardEvent.code to retro keys */ static const rwebinput_key_to_code_map_entry_t rwebinput_key_to_code_map[] = { { "KeyA", RETROK_a }, { "KeyB", RETROK_b }, { "KeyC", RETROK_c }, { "KeyD", RETROK_d }, { "KeyE", RETROK_e }, { "KeyF", RETROK_f }, { "KeyG", RETROK_g }, { "KeyH", RETROK_h }, { "KeyI", RETROK_i }, { "KeyJ", RETROK_j }, { "KeyK", RETROK_k }, { "KeyL", RETROK_l }, { "KeyM", RETROK_m }, { "KeyN", RETROK_n }, { "KeyO", RETROK_o }, { "KeyP", RETROK_p }, { "KeyQ", RETROK_q }, { "KeyR", RETROK_r }, { "KeyS", RETROK_s }, { "KeyT", RETROK_t }, { "KeyU", RETROK_u }, { "KeyV", RETROK_v }, { "KeyW", RETROK_w }, { "KeyX", RETROK_x }, { "KeyY", RETROK_y }, { "KeyZ", RETROK_z }, { "ArrowLeft", RETROK_LEFT }, { "ArrowRight", RETROK_RIGHT }, { "ArrowUp", RETROK_UP }, { "ArrowDown", RETROK_DOWN }, { "Enter", RETROK_RETURN }, { "NumpadEnter", RETROK_KP_ENTER }, { "Tab", RETROK_TAB }, { "Insert", RETROK_INSERT }, { "Delete", RETROK_DELETE }, { "End", RETROK_END }, { "Home", RETROK_HOME }, { "ShiftRight", RETROK_RSHIFT }, { "ShiftLeft", RETROK_LSHIFT }, { "ControlLeft", RETROK_LCTRL }, { "AltLeft", RETROK_LALT }, { "Space", RETROK_SPACE }, { "Escape", RETROK_ESCAPE }, { "NumpadAdd", RETROK_KP_PLUS }, { "NumpadSubtract", RETROK_KP_MINUS }, { "F1", RETROK_F1 }, { "F2", RETROK_F2 }, { "F3", RETROK_F3 }, { "F4", RETROK_F4 }, { "F5", RETROK_F5 }, { "F6", RETROK_F6 }, { "F7", RETROK_F7 }, { "F8", RETROK_F8 }, { "F9", RETROK_F9 }, { "F10", RETROK_F10 }, { "F11", RETROK_F11 }, { "F12", RETROK_F12 }, { "Digit0", RETROK_0 }, { "Digit1", RETROK_1 }, { "Digit2", RETROK_2 }, { "Digit3", RETROK_3 }, { "Digit4", RETROK_4 }, { "Digit5", RETROK_5 }, { "Digit6", RETROK_6 }, { "Digit7", RETROK_7 }, { "Digit8", RETROK_8 }, { "Digit9", RETROK_9 }, { "PageUp", RETROK_PAGEUP }, { "PageDown", RETROK_PAGEDOWN }, { "Numpad0", RETROK_KP0 }, { "Numpad1", RETROK_KP1 }, { "Numpad2", RETROK_KP2 }, { "Numpad3", RETROK_KP3 }, { "Numpad4", RETROK_KP4 }, { "Numpad5", RETROK_KP5 }, { "Numpad6", RETROK_KP6 }, { "Numpad7", RETROK_KP7 }, { "Numpad8", RETROK_KP8 }, { "Numpad9", RETROK_KP9 }, { "Period", RETROK_PERIOD }, { "CapsLock", RETROK_CAPSLOCK }, { "NumLock", RETROK_NUMLOCK }, { "Backspace", RETROK_BACKSPACE }, { "NumpadMultiply", RETROK_KP_MULTIPLY }, { "NumpadDivide", RETROK_KP_DIVIDE }, { "PrintScreen", RETROK_PRINT }, { "ScrollLock", RETROK_SCROLLOCK }, { "Backquote", RETROK_BACKQUOTE }, { "Pause", RETROK_PAUSE }, { "Quote", RETROK_QUOTE }, { "Comma", RETROK_COMMA }, { "Minus", RETROK_MINUS }, { "Slash", RETROK_SLASH }, { "Semicolon", RETROK_SEMICOLON }, { "Equal", RETROK_EQUALS }, { "BracketLeft", RETROK_LEFTBRACKET }, { "Backslash", RETROK_BACKSLASH }, { "BracketRight", RETROK_RIGHTBRACKET }, { "NumpadDecimal", RETROK_KP_PERIOD }, { "NumpadEqual", RETROK_KP_EQUALS }, { "ControlRight", RETROK_RCTRL }, { "AltRight", RETROK_RALT }, { "F13", RETROK_F13 }, { "F14", RETROK_F14 }, { "F15", RETROK_F15 }, { "MetaRight", RETROK_RMETA }, { "MetaLeft", RETROK_LMETA }, { "Help", RETROK_HELP }, { "ContextMenu", RETROK_MENU }, { "Power", RETROK_POWER }, }; static bool g_rwebinput_initialized; static rwebinput_keyboard_event_queue_t *g_rwebinput_keyboard; static rwebinput_mouse_state_t *g_rwebinput_mouse; /* to make the string labels for codes from JavaScript work, we convert them * to CRC32 hashes for the LUT */ static void rwebinput_generate_lut(void) { int i; struct rarch_key_map *key_map; retro_assert(ARRAY_SIZE(rarch_key_map_rwebinput) == ARRAY_SIZE(rwebinput_key_to_code_map) + 1); for (i = 0; i < ARRAY_SIZE(rwebinput_key_to_code_map); i++) { int j; uint32_t crc; const rwebinput_key_to_code_map_entry_t *key_to_code = &rwebinput_key_to_code_map[i]; key_map = &rarch_key_map_rwebinput[i]; crc = encoding_crc32(0, (const uint8_t *)key_to_code->key, strlen(key_to_code->key)); /* sanity check: make sure there's no collisions */ for (j = 0; j < i; j++) { retro_assert(rarch_key_map_rwebinput[j].sym != crc); } key_map->rk = key_to_code->rk; key_map->sym = crc; } /* set terminating entry */ key_map = &rarch_key_map_rwebinput[ARRAY_SIZE(rarch_key_map_rwebinput) - 1]; key_map->rk = RETROK_UNKNOWN; key_map->sym = 0; } static EM_BOOL rwebinput_keyboard_cb(int event_type, const EmscriptenKeyboardEvent *key_event, void *user_data) { if (event_type == EMSCRIPTEN_EVENT_KEYPRESS) return EM_TRUE; if (g_rwebinput_keyboard->count >= g_rwebinput_keyboard->max_size) { size_t new_max = MAX(1, g_rwebinput_keyboard->max_size << 1); g_rwebinput_keyboard->events = realloc(g_rwebinput_keyboard->events, new_max * sizeof(g_rwebinput_keyboard->events[0])); retro_assert(g_rwebinput_keyboard->events != NULL); g_rwebinput_keyboard->max_size = new_max; } g_rwebinput_keyboard->events[g_rwebinput_keyboard->count].type = event_type; memcpy(&g_rwebinput_keyboard->events[g_rwebinput_keyboard->count].event, key_event, sizeof(*key_event)); g_rwebinput_keyboard->count++; return EM_TRUE; } static EM_BOOL rwebinput_mouse_cb(int event_type, const EmscriptenMouseEvent *mouse_event, void *user_data) { (void)user_data; uint8_t mask = 1 << mouse_event->button; g_rwebinput_mouse->x = mouse_event->canvasX; g_rwebinput_mouse->y = mouse_event->canvasY; g_rwebinput_mouse->delta_x += mouse_event->movementX; g_rwebinput_mouse->delta_y += mouse_event->movementY; if (event_type == EMSCRIPTEN_EVENT_MOUSEDOWN) { g_rwebinput_mouse->buttons |= mask; } else if (event_type == EMSCRIPTEN_EVENT_MOUSEUP) { g_rwebinput_mouse->buttons &= ~mask; } return EM_TRUE; } static EM_BOOL rwebinput_wheel_cb(int event_type, const EmscriptenWheelEvent *wheel_event, void *user_data) { (void)event_type; (void)user_data; g_rwebinput_mouse->scroll_x += wheel_event->deltaX; g_rwebinput_mouse->scroll_y += wheel_event->deltaY; return EM_TRUE; } static void *rwebinput_input_init(const char *joypad_driver) { rwebinput_input_t *rwebinput = (rwebinput_input_t*)calloc(1, sizeof(*rwebinput)); g_rwebinput_keyboard = (rwebinput_keyboard_event_queue_t*) calloc(1, sizeof(rwebinput_keyboard_event_queue_t)); g_rwebinput_mouse = (rwebinput_mouse_state_t*) calloc(1, sizeof(rwebinput_mouse_state_t)); if (!rwebinput || !g_rwebinput_keyboard || !g_rwebinput_mouse) goto error; if (!g_rwebinput_initialized) { EMSCRIPTEN_RESULT r; g_rwebinput_initialized = true; rwebinput_generate_lut(); /* emscripten currently doesn't have an API to remove handlers, so make * once and reuse it */ r = emscripten_set_keydown_callback("#document", NULL, false, rwebinput_keyboard_cb); if (r != EMSCRIPTEN_RESULT_SUCCESS) { RARCH_ERR( "[EMSCRIPTEN/INPUT] failed to create keydown callback: %d\n", r); } r = emscripten_set_keyup_callback("#document", NULL, false, rwebinput_keyboard_cb); if (r != EMSCRIPTEN_RESULT_SUCCESS) { RARCH_ERR( "[EMSCRIPTEN/INPUT] failed to create keydown callback: %d\n", r); } r = emscripten_set_keypress_callback("#document", NULL, false, rwebinput_keyboard_cb); if (r != EMSCRIPTEN_RESULT_SUCCESS) { RARCH_ERR( "[EMSCRIPTEN/INPUT] failed to create keypress callback: %d\n", r); } r = emscripten_set_mousedown_callback("#canvas", NULL, false, rwebinput_mouse_cb); if (r != EMSCRIPTEN_RESULT_SUCCESS) { RARCH_ERR( "[EMSCRIPTEN/INPUT] failed to create mousedown callback: %d\n", r); } r = emscripten_set_mouseup_callback("#canvas", NULL, false, rwebinput_mouse_cb); if (r != EMSCRIPTEN_RESULT_SUCCESS) { RARCH_ERR( "[EMSCRIPTEN/INPUT] failed to create mouseup callback: %d\n", r); } r = emscripten_set_mousemove_callback("#canvas", NULL, false, rwebinput_mouse_cb); if (r != EMSCRIPTEN_RESULT_SUCCESS) { RARCH_ERR( "[EMSCRIPTEN/INPUT] failed to create mousemove callback: %d\n", r); } r = emscripten_set_wheel_callback("#document", NULL, false, rwebinput_wheel_cb); if (r != EMSCRIPTEN_RESULT_SUCCESS) { RARCH_ERR( "[EMSCRIPTEN/INPUT] failed to create wheel callback: %d\n", r); } } input_keymaps_init_keyboard_lut(rarch_key_map_rwebinput); rwebinput->joypad = input_joypad_init_driver(joypad_driver, rwebinput); return rwebinput; error: free(g_rwebinput_keyboard); free(g_rwebinput_mouse); free(rwebinput); return NULL; } static bool rwebinput_key_pressed(rwebinput_input_t *rwebinput, int key) { if (key >= RETROK_LAST) return false; return rwebinput->keys[key]; } static int16_t rwebinput_pointer_device_state(rwebinput_mouse_state_t *mouse, unsigned id, bool screen) { struct video_viewport vp; bool inside = false; int16_t res_x = 0; int16_t res_y = 0; int16_t res_screen_x = 0; int16_t res_screen_y = 0; vp.x = 0; vp.y = 0; vp.width = 0; vp.height = 0; vp.full_width = 0; vp.full_height = 0; if (!(video_driver_translate_coord_viewport_wrap(&vp, mouse->x, mouse->x, &res_x, &res_y, &res_screen_x, &res_screen_y))) return 0; if (screen) { res_x = res_screen_x; res_y = res_screen_y; } inside = (res_x >= -0x7fff) && (res_y >= -0x7fff); if (!inside) return 0; switch (id) { case RETRO_DEVICE_ID_POINTER_X: return res_x; case RETRO_DEVICE_ID_POINTER_Y: return res_y; case RETRO_DEVICE_ID_POINTER_PRESSED: return !!(mouse->buttons & (1 << RWEBINPUT_MOUSE_BTNL)); } return 0; } static int16_t rwebinput_mouse_state(rwebinput_mouse_state_t *mouse, unsigned id, bool screen) { switch (id) { case RETRO_DEVICE_ID_MOUSE_X: return (int16_t)(screen ? mouse->x : mouse->delta_x); case RETRO_DEVICE_ID_MOUSE_Y: return (int16_t)(screen ? mouse->y : mouse->delta_y); case RETRO_DEVICE_ID_MOUSE_LEFT: return !!(mouse->buttons & (1 << RWEBINPUT_MOUSE_BTNL)); case RETRO_DEVICE_ID_MOUSE_RIGHT: return !!(mouse->buttons & (1 << RWEBINPUT_MOUSE_BTNR)); case RETRO_DEVICE_ID_MOUSE_MIDDLE: return !!(mouse->buttons & (1 << RWEBINPUT_MOUSE_BTNM)); case RETRO_DEVICE_ID_MOUSE_BUTTON_4: return !!(mouse->buttons & (1 << RWEBINPUT_MOUSE_BTN4)); case RETRO_DEVICE_ID_MOUSE_BUTTON_5: return !!(mouse->buttons & (1 << RWEBINPUT_MOUSE_BTN5)); case RETRO_DEVICE_ID_MOUSE_WHEELUP: return mouse->scroll_y < 0.0; case RETRO_DEVICE_ID_MOUSE_WHEELDOWN: return mouse->scroll_y > 0.0; case RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELUP: return mouse->scroll_x < 0.0; case RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELDOWN: return mouse->scroll_x > 0.0; } return 0; } static bool rwebinput_is_pressed(rwebinput_input_t *rwebinput, rarch_joypad_info_t joypad_info, const struct retro_keybind *binds, unsigned port, unsigned id) { if (id < RARCH_BIND_LIST_END) { const struct retro_keybind *bind = &binds[id]; int key = bind->key; if ((key < RETROK_LAST) && rwebinput_key_pressed(rwebinput, key)) if ((id == RARCH_GAME_FOCUS_TOGGLE) || !rwebinput->blocked) return true; if (bind->valid) { if (port == 0 && !!rwebinput_mouse_state(&rwebinput->mouse, bind->mbutton, false)) return true; if (input_joypad_pressed(rwebinput->joypad, joypad_info, port, binds, id)) return true; } } return false; } static int16_t rwebinput_analog_pressed(rwebinput_input_t *rwebinput, rarch_joypad_info_t joypad_info, const struct retro_keybind *binds, unsigned idx, unsigned id) { int16_t pressed_minus = 0, pressed_plus = 0; unsigned id_minus = 0; unsigned id_plus = 0; input_conv_analog_id_to_bind_id(idx, id, &id_minus, &id_plus); if (rwebinput_is_pressed(rwebinput, joypad_info, binds, idx, id_minus)) pressed_minus = -0x7fff; if (rwebinput_is_pressed(rwebinput, joypad_info, binds, idx, id_plus)) pressed_plus = 0x7fff; return pressed_plus + pressed_minus; } static int16_t rwebinput_input_state(void *data, rarch_joypad_info_t joypad_info, const struct retro_keybind **binds, unsigned port, unsigned device, unsigned idx, unsigned id) { int16_t ret; rwebinput_input_t *rwebinput = (rwebinput_input_t*)data; switch (device) { case RETRO_DEVICE_JOYPAD: if (id < RARCH_BIND_LIST_END) return rwebinput_is_pressed(rwebinput, joypad_info, binds[port], port, id); break; case RETRO_DEVICE_ANALOG: ret = rwebinput_analog_pressed(rwebinput, joypad_info, binds[port], idx, id); if (!ret && binds[port]) ret = input_joypad_analog(rwebinput->joypad, joypad_info, port, idx, id, binds[port]); return ret; case RETRO_DEVICE_KEYBOARD: return rwebinput_key_pressed(rwebinput, id); case RETRO_DEVICE_MOUSE: return rwebinput_mouse_state(&rwebinput->mouse, id, false); case RARCH_DEVICE_MOUSE_SCREEN: return rwebinput_mouse_state(&rwebinput->mouse, id, true); case RETRO_DEVICE_POINTER: return rwebinput_pointer_device_state(&rwebinput->mouse, id, false); case RARCH_DEVICE_POINTER_SCREEN: return rwebinput_pointer_device_state(&rwebinput->mouse, id, true); } return 0; } static void rwebinput_input_free(void *data) { rwebinput_input_t *rwebinput = (rwebinput_input_t*)data; if (g_rwebinput_keyboard) free(g_rwebinput_keyboard->events); free(g_rwebinput_keyboard); g_rwebinput_keyboard = NULL; free(g_rwebinput_mouse); g_rwebinput_mouse = NULL; free(rwebinput); } static void rwebinput_process_keyboard_events(rwebinput_input_t *rwebinput, rwebinput_keyboard_event_t *event) { uint32_t keycode; unsigned translated_keycode; uint32_t character = 0; uint16_t mod = 0; const EmscriptenKeyboardEvent *key_event = &event->event; bool keydown = event->type == EMSCRIPTEN_EVENT_KEYDOWN; /* a printable key: populate character field */ if (utf8len(key_event->key) == 1) { const char *key_ptr = &key_event->key[0]; character = utf8_walk(&key_ptr); } if (key_event->ctrlKey) mod |= RETROKMOD_CTRL; if (key_event->altKey) mod |= RETROKMOD_ALT; if (key_event->shiftKey) mod |= RETROKMOD_SHIFT; if (key_event->metaKey) mod |= RETROKMOD_META; keycode = encoding_crc32(0, (const uint8_t *)key_event->code, strnlen(key_event->code, sizeof(key_event->code))); translated_keycode = input_keymaps_translate_keysym_to_rk(keycode); if (translated_keycode == RETROK_BACKSPACE) character = '\b'; else if (translated_keycode == RETROK_RETURN || translated_keycode == RETROK_KP_ENTER) character = '\n'; else if (translated_keycode == RETROK_TAB) character = '\t'; input_keyboard_event(keydown, translated_keycode, character, mod, RETRO_DEVICE_KEYBOARD); if (translated_keycode < RETROK_LAST) { rwebinput->keys[translated_keycode] = keydown; } } static void rwebinput_input_poll(void *data) { size_t i; rwebinput_input_t *rwebinput = (rwebinput_input_t*)data; for (i = 0; i < g_rwebinput_keyboard->count; i++) rwebinput_process_keyboard_events(rwebinput, &g_rwebinput_keyboard->events[i]); g_rwebinput_keyboard->count = 0; memcpy(&rwebinput->mouse, g_rwebinput_mouse, sizeof(*g_rwebinput_mouse)); g_rwebinput_mouse->delta_x = g_rwebinput_mouse->delta_y = 0; g_rwebinput_mouse->scroll_x = g_rwebinput_mouse->scroll_y = 0.0; if (rwebinput->joypad) rwebinput->joypad->poll(); } static void rwebinput_grab_mouse(void *data, bool state) { (void)data; if (state) emscripten_request_pointerlock("#canvas", EM_TRUE); else emscripten_exit_pointerlock(); } static bool rwebinput_set_rumble(void *data, unsigned port, enum retro_rumble_effect effect, uint16_t strength) { (void)data; (void)port; (void)effect; (void)strength; return false; } static const input_device_driver_t *rwebinput_get_joypad_driver(void *data) { rwebinput_input_t *rwebinput = (rwebinput_input_t*)data; if (!rwebinput) return NULL; return rwebinput->joypad; } static uint64_t rwebinput_get_capabilities(void *data) { uint64_t caps = 0; caps |= (1 << RETRO_DEVICE_JOYPAD); caps |= (1 << RETRO_DEVICE_ANALOG); caps |= (1 << RETRO_DEVICE_KEYBOARD); caps |= (1 << RETRO_DEVICE_MOUSE); caps |= (1 << RETRO_DEVICE_POINTER); return caps; } static bool rwebinput_keyboard_mapping_is_blocked(void *data) { rwebinput_input_t *rwebinput = (rwebinput_input_t*)data; if (!rwebinput) return false; return rwebinput->blocked; } static void rwebinput_keyboard_mapping_set_block(void *data, bool value) { rwebinput_input_t *rwebinput = (rwebinput_input_t*)data; if (!rwebinput) return; rwebinput->blocked = value; } input_driver_t input_rwebinput = { rwebinput_input_init, rwebinput_input_poll, rwebinput_input_state, rwebinput_input_free, NULL, NULL, rwebinput_get_capabilities, "rwebinput", rwebinput_grab_mouse, NULL, rwebinput_set_rumble, rwebinput_get_joypad_driver, NULL, rwebinput_keyboard_mapping_is_blocked, rwebinput_keyboard_mapping_set_block, };