#include "InputManager.hpp" #include "CustomEvents.hpp" #include #include #include #include #include #include #include #include #ifdef _WIN32 #include #include #include "../deps/clip/clip.h" #endif #ifdef __EMSCRIPTEN__ #include extern "C" { int isAcceptingInputEmscripten = 0; EMSCRIPTEN_KEEPALIVE inline int is_text_input_happening() { return isAcceptingInputEmscripten; } } #endif #include "MainProgram.hpp" InputManager::InputManager(MainProgram& initMain): main(initMain) { frame_reset({0, 0}); defaultKeyAssignments[{0, SDLK_W}] = KEY_CAMERA_ROTATE_COUNTERCLOCKWISE; defaultKeyAssignments[{0, SDLK_Q}] = KEY_CAMERA_ROTATE_CLOCKWISE; defaultKeyAssignments[{0, SDLK_DELETE}] = KEY_DRAW_DELETE; defaultKeyAssignments[{CTRL_MOD, SDLK_Z}] = KEY_UNDO; defaultKeyAssignments[{CTRL_MOD, SDLK_R}] = KEY_REDO; defaultKeyAssignments[{0, SDLK_TAB}] = KEY_NOGUI; defaultKeyAssignments[{0, SDLK_F11}] = KEY_FULLSCREEN; defaultKeyAssignments[{CTRL_MOD, SDLK_S}] = KEY_SAVE; defaultKeyAssignments[{CTRL_MOD | SDL_KMOD_SHIFT, SDLK_S}] = KEY_SAVE_AS; defaultKeyAssignments[{CTRL_MOD, SDLK_C}] = KEY_COPY; defaultKeyAssignments[{CTRL_MOD, SDLK_X}] = KEY_CUT; defaultKeyAssignments[{CTRL_MOD, SDLK_V}] = KEY_PASTE; defaultKeyAssignments[{CTRL_MOD | SDL_KMOD_SHIFT, SDLK_V}] = KEY_PASTE_IMAGE; defaultKeyAssignments[{0, SDLK_B}] = KEY_DRAW_TOOL_BRUSH; defaultKeyAssignments[{0, SDLK_E}] = KEY_DRAW_TOOL_ERASER; defaultKeyAssignments[{0, SDLK_U}] = KEY_DRAW_TOOL_ZOOM; defaultKeyAssignments[{0, SDLK_L}] = KEY_DRAW_TOOL_LASSOSELECT; defaultKeyAssignments[{0, SDLK_H}] = KEY_DRAW_TOOL_PAN; defaultKeyAssignments[{0, SDLK_T}] = KEY_DRAW_TOOL_TEXTBOX; defaultKeyAssignments[{0, SDLK_R}] = KEY_DRAW_TOOL_RECTANGLE; defaultKeyAssignments[{0, SDLK_C}] = KEY_DRAW_TOOL_ELLIPSE; defaultKeyAssignments[{0, SDLK_X}] = KEY_DRAW_TOOL_RECTSELECT; defaultKeyAssignments[{0, SDLK_I}] = KEY_DRAW_TOOL_EYEDROPPER; defaultKeyAssignments[{0, SDLK_P}] = KEY_DRAW_TOOL_SCREENSHOT; defaultKeyAssignments[{0, SDLK_N}] = KEY_DRAW_TOOL_LINE; defaultKeyAssignments[{0, SDLK_F1}] = KEY_OPEN_CHAT; defaultKeyAssignments[{0, SDLK_F3}] = KEY_SHOW_METRICS; defaultKeyAssignments[{0, SDLK_F2}] = KEY_SHOW_PLAYER_LIST; defaultKeyAssignments[{0, SDLK_SPACE}] = KEY_HOLD_TO_PAN; defaultKeyAssignments[{0, SDLK_Z}] = KEY_HOLD_TO_ZOOM; defaultKeyAssignments[{0, SDLK_ESCAPE}] = KEY_DESELECT_AND_EDIT_TOOL; #ifdef __EMSCRIPTEN__ // Without this, SDL eats the CTRL-V event that initiates the paste event // https://github.com/pthom/hello_imgui/issues/3#issuecomment-1564536870 EM_ASM({ window.addEventListener('keydown', function(event) { if((event.ctrlKey || event.metaKey) && (event.key == 'v' || event.code == 'KeyV')) { if(Module["ccall"]('is_text_input_happening', 'number', [], []) === 1) event.stopImmediatePropagation(); } }, true); }); emscripten_browser_clipboard::paste_event([](std::string&& pasteData, void* callbackData){ InputManager* inMan = (InputManager*)callbackData; inMan->process_text_paste(pasteData, true); }, this); #endif keyAssignments = defaultKeyAssignments; } unsigned InputManager::Text::get_id() { static unsigned nextID = 0; while(std::find_if(textBoxes.begin(), textBoxes.end(), [&](const TextBoxInfo& info) { return info.id == nextID; }) != textBoxes.end()) ++nextID; return nextID; } unsigned InputManager::set_text_box_front(const std::optional>& textboxRect, const TextInputProperties& inputProps) { Text::TextBoxInfo textBoxInfo; textBoxInfo.rect = textboxRect; textBoxInfo.textInputProperties = inputProps; textBoxInfo.id = text.get_id(); text.textBoxes.emplace_front(textBoxInfo); text.update_accepting_input(main.window.sdlWindow); return textBoxInfo.id; } unsigned InputManager::set_text_box_back(const std::optional>& textboxRect, const TextInputProperties& inputProps) { Text::TextBoxInfo textBoxInfo; textBoxInfo.rect = textboxRect; textBoxInfo.textInputProperties = inputProps; textBoxInfo.id = text.get_id(); text.textBoxes.emplace_back(textBoxInfo); text.update_accepting_input(main.window.sdlWindow); return textBoxInfo.id; } void InputManager::remove_text_box(unsigned id) { std::erase_if(text.textBoxes, [id](Text::TextBoxInfo& info) { return (info.id == id); }); text.update_accepting_input(main.window.sdlWindow); } bool InputManager::Text::is_accepting_input() { return !textBoxes.empty(); } std::optional InputManager::Text::current_textbox_editing_id() { if(textBoxes.empty()) return std::nullopt; return textBoxes.front().id; } void InputManager::Text::update_accepting_input(SDL_Window* window) { if(!textBoxes.empty()) { auto& textbox = textBoxes.front(); if(!propID.has_value()) propID = SDL_CreateProperties(); SDL_PropertiesID propIDVal = propID.value(); // There is a bug where decimal places disappear when textinput type is set to number on android. We'll just set those textinputs back to text type SDL_SetNumberProperty(propIDVal, SDL_PROP_TEXTINPUT_TYPE_NUMBER, (textbox.textInputProperties.inputType == SDL_TEXTINPUT_TYPE_NUMBER) ? SDL_TEXTINPUT_TYPE_TEXT : textbox.textInputProperties.inputType); SDL_SetNumberProperty(propIDVal, SDL_PROP_TEXTINPUT_CAPITALIZATION_NUMBER, textbox.textInputProperties.capitalization); SDL_SetNumberProperty(propIDVal, SDL_PROP_TEXTINPUT_AUTOCORRECT_BOOLEAN, textbox.textInputProperties.autocorrect); SDL_SetNumberProperty(propIDVal, SDL_PROP_TEXTINPUT_MULTILINE_BOOLEAN, textbox.textInputProperties.multiline); SDL_SetNumberProperty(propIDVal, SDL_PROP_TEXTINPUT_ANDROID_INPUTTYPE_NUMBER, (textbox.textInputProperties.androidInputType & ANDROIDTEXT_TYPE_CLASS_NUMBER) ? ANDROIDTEXT_TYPE_CLASS_TEXT : textbox.textInputProperties.androidInputType); if(!textBoxes.front().rect) SDL_SetTextInputArea(window, nullptr, 0); else { SDL_Rect r = textBoxes.front().rect->get_sdl_rect(); SDL_SetTextInputArea(window, &r, 0); } SDL_StartTextInputWithProperties(window, propIDVal); #ifdef __EMSCRIPTEN__ isAcceptingInputEmscripten = 1; #endif } else { SDL_StopTextInput(window); SDL_SetTextInputArea(window, nullptr, 0); #ifdef __EMSCRIPTEN__ isAcceptingInputEmscripten = 0; #endif } } std::string InputManager::get_clipboard_str_SDL() { char* data = SDL_GetClipboardText(); std::string toRet(data); SDL_free(data); return toRet; } #ifdef _WIN32 // From clip library example, useful for debugging void print_clip_image_spec(const clip::image_spec& spec) { std::cout << "Image in clipboard " << spec.width << "x" << spec.height << " (" << spec.bits_per_pixel << "bpp)\n" << "Format:" << "\n" << std::hex << " Red mask: " << spec.red_mask << "\n" << " Green mask: " << spec.green_mask << "\n" << " Blue mask: " << spec.blue_mask << "\n" << " Alpha mask: " << spec.alpha_mask << "\n" << std::dec << " Red shift: " << spec.red_shift << "\n" << " Green shift: " << spec.green_shift << "\n" << " Blue shift: " << spec.blue_shift << "\n" << " Alpha shift: " << spec.alpha_shift << "\n"; } #endif void InputManager::get_clipboard_image_data_SDL(const std::function& callback) { #ifndef __EMSCRIPTEN__ static std::unordered_set validMimetypes; if(validMimetypes.empty()) { validMimetypes.emplace("image/png"); validMimetypes.emplace("image/gif"); validMimetypes.emplace("image/jpeg"); validMimetypes.emplace("image/svg+xml"); validMimetypes.emplace("image/webp"); #ifdef _WIN32 validMimetypes.emplace("image/bmp"); #endif } size_t mimeTypeSize; char** mimeTypes = SDL_GetClipboardMimeTypes(&mimeTypeSize); if(mimeTypes) { for(size_t i = 0; i < mimeTypeSize; i++) { if(validMimetypes.contains(mimeTypes[i])) { if(std::string(mimeTypes[i]) == "image/bmp") { #ifdef _WIN32 std::vector result; try { if(!clip::has(clip::image_format())) throw std::runtime_error("Clipboard doesn't contain an image"); clip::image img; if(!clip::get_image(img)) throw std::runtime_error("Error getting image from clipboard"); clip::image_spec spec = img.spec(); SDL_PixelFormat pFormat = SDL_GetPixelFormatForMasks(spec.bits_per_pixel, spec.red_mask, spec.green_mask, spec.blue_mask, spec.alpha_mask); if(pFormat != SDL_PIXELFORMAT_UNKNOWN) { SDL_Surface* surfOriginal = SDL_CreateSurfaceFrom(spec.width, spec.height, pFormat, img.data(), spec.bytes_per_row); if(surfOriginal) { SDL_LockSurface(surfOriginal); SDL_Surface* cSurf = SDL_ConvertSurface(surfOriginal, SDL_PIXELFORMAT_RGBA32); SDL_UnlockSurface(surfOriginal); SDL_DestroySurface(surfOriginal); if(cSurf) { SDL_LockSurface(cSurf); SkDynamicMemoryWStream out; if(SkPngEncoder::Encode(&out, SkPixmap(SkImageInfo::Make(cSurf->w, cSurf->h, kRGBA_8888_SkColorType, kUnpremul_SkAlphaType), cSurf->pixels, cSurf->pitch), {})) { out.flush(); SDL_UnlockSurface(cSurf); SDL_DestroySurface(cSurf); result = out.detachAsVector(); } else { SDL_UnlockSurface(cSurf); SDL_DestroySurface(cSurf); throw std::runtime_error("Could not encode image data to PNG"); } } else throw std::runtime_error("Bitmap could not be converted to new format"); } else throw std::runtime_error("Bitmap could not be read into surface"); } else throw std::runtime_error("Bitmap format is unsupported"); } catch(const std::runtime_error& e) { Logger::get().log("INFO", std::string("[InputManager::get_clipboard_image_data_SDL] Error thrown: ") + e.what()); } catch(...) { Logger::get().log("INFO", "[InputManager::get_clipboard_image_data_SDL] Unknown error thrown"); } if(!result.empty()) callback(std::string_view((char*)(result.data()), result.size())); // This part of the code should throw errors #endif } else { size_t clipboardDataSize; void* clipboardData = SDL_GetClipboardData(mimeTypes[i], &clipboardDataSize); callback(std::string_view(static_cast(clipboardData), clipboardDataSize)); SDL_free(clipboardData); } return; } } } #endif } void InputManager::set_clipboard_str(std::string_view s) { #ifdef __EMSCRIPTEN__ emscripten_browser_clipboard::copy(std::string(s)); lastCopiedRichText = std::nullopt; #else SDL_SetClipboardText(s.data()); #endif } void InputManager::set_clipboard_plain_and_richtext_pair(const std::pair& plainAndRichtextPair) { set_clipboard_str(plainAndRichtextPair.first); lastCopiedRichText = plainAndRichtextPair.second; } std::string InputManager::key_assignment_to_str(const Vector2ui32& k) const { std::string toRet; if(k.x() & SDL_KMOD_CTRL) toRet += "CTRL+"; if(k.x() & SDL_KMOD_ALT) toRet += "ALT+"; if(k.x() & SDL_KMOD_SHIFT) toRet += "SHIFT+"; if(k.x() & SDL_KMOD_GUI) toRet += "META+"; toRet += SDL_GetKeyName(k.y()); return toRet; } Vector2ui32 InputManager::key_assignment_from_str(const std::string& s) const { Vector2ui32 toRet = {0, 0}; if(s.find("CTRL+") != std::string::npos) toRet.x() |= SDL_KMOD_CTRL; if(s.find("ALT+") != std::string::npos) toRet.x() |= SDL_KMOD_ALT; if(s.find("SHIFT+") != std::string::npos) toRet.x() |= SDL_KMOD_SHIFT; if(s.find("META+") != std::string::npos) toRet.x() |= SDL_KMOD_GUI; size_t startInd = s.find_last_of("+"); if(startInd == std::string::npos) startInd = 0; else startInd++; toRet.y() = SDL_GetKeyFromName(s.substr(startInd).c_str()); if(toRet.y() == 0) toRet.x() = 0; return toRet; } const InputManager::KeyData& InputManager::key(KeyCode kCode) { return keys[kCode]; } void InputManager::set_key_down(const SDL_KeyboardEvent& e, KeyCode kCode, bool acceptingTextInput) { auto& k = keys[kCode]; k.held = true; if(acceptingTextInput) main.input_text_key_callback(KeyCallbackArgs{.key = kCode, .down = e.down, .repeat = e.repeat}); else main.input_key_callback(KeyCallbackArgs{.key = kCode, .down = e.down, .repeat = e.repeat}); } void InputManager::set_key_up(const SDL_KeyboardEvent& e, KeyCode kCode, bool acceptingTextInput) { keys[kCode].held = false; if(acceptingTextInput) main.input_text_key_callback(KeyCallbackArgs{.key = kCode, .down = e.down, .repeat = e.repeat}); // Send key up even if text input is happening main.input_key_callback(KeyCallbackArgs{.key = kCode, .down = e.down, .repeat = e.repeat}); } uint32_t InputManager::make_generic_key_mod(SDL_Keymod m) { uint32_t toRet = 0; if(m & SDL_KMOD_CTRL) toRet |= SDL_KMOD_CTRL; if(m & SDL_KMOD_ALT) toRet |= SDL_KMOD_ALT; if(m & SDL_KMOD_GUI) toRet |= SDL_KMOD_GUI; if(m & SDL_KMOD_SHIFT) toRet |= SDL_KMOD_SHIFT; return toRet; } void InputManager::backend_input_text_event(const std::string& str) { main.input_text_callback(TextCallbackArgs{ .str = str }); } void InputManager::backend_drop_file_event(const SDL_DropEvent& e) { Vector2f mousePos = {e.x * main.window.density, e.y * main.window.density}; mouse.set_pos(mousePos); main.input_drop_file_callback({ .pos = mousePos, .source = e.source, .data = e.data }); } void InputManager::backend_drop_text_event(const SDL_DropEvent& e) { Vector2f mousePos = {e.x * main.window.density, e.y * main.window.density}; mouse.set_pos(mousePos); main.input_drop_text_callback({ .pos = mousePos, .source = e.source, .data = e.data }); } void InputManager::backend_mouse_button_up_update(const SDL_MouseButtonEvent& e) { Vector2f mousePos = {e.x * main.window.density, e.y * main.window.density}; mouse.set_pos(mousePos); if(e.button == 1) mouse.leftDown = false; else if(e.button == 2) mouse.middleDown = false; else if(e.button == 3) mouse.rightDown = false; MouseButtonCallbackArgs args{ .deviceType = MouseDeviceType::MOUSE, .button = static_cast(e.button), .down = e.down, .clicks = e.clicks, .pos = mousePos }; main.input_mouse_button_callback(args); isTouchDevice = false; } void InputManager::backend_mouse_button_down_update(const SDL_MouseButtonEvent& e) { Vector2f mousePos = {e.x * main.window.density, e.y * main.window.density}; mouse.set_pos(mousePos); if(e.button == 1) mouse.leftDown = true; else if(e.button == 2) mouse.middleDown = true; else if(e.button == 3) mouse.rightDown = true; MouseButtonCallbackArgs args{ .deviceType = MouseDeviceType::MOUSE, .button = static_cast(e.button), .down = e.down, .clicks = e.clicks, .pos = mousePos }; main.input_mouse_button_callback(args); isTouchDevice = false; } void InputManager::backend_mouse_motion_update(const SDL_MouseMotionEvent& e) { Vector2f mouseNewPos = {e.x * main.window.density, e.y * main.window.density}; mouse.set_pos(mouseNewPos); Vector2f mouseRel = {e.xrel * main.window.density, e.yrel * main.window.density}; main.input_mouse_motion_callback({ .deviceType = MouseDeviceType::MOUSE, .pos = mouseNewPos, .move = mouseRel }); isTouchDevice = false; } void InputManager::backend_mouse_wheel_update(const SDL_MouseWheelEvent& e) { Vector2f mouseNewPos = {e.mouse_x * main.window.density, e.mouse_y * main.window.density}; mouse.set_pos(mouseNewPos); main.input_mouse_wheel_callback({ .mousePos = mouseNewPos, .amount = {e.x, e.y}, .tickAmount = {e.integer_x, e.integer_y} }); isTouchDevice = false; } void InputManager::backend_pen_button_down_update(const SDL_PenButtonEvent& e) { Vector2f mouseNewPos = {e.x * main.window.density, e.y * main.window.density}; mouse.set_pos(mouseNewPos); pen.previousPos = mouseNewPos; pen.buttons[e.button].held = true; pen.isEraser = e.pen_state & SDL_PEN_INPUT_ERASER_TIP; if(e.button == main.conf.tabletOptions.middleClickButton) { main.input_mouse_button_callback({ .deviceType = MouseDeviceType::PEN, .button = MouseButton::MIDDLE, .down = e.down, .clicks = 1, .pos = mouseNewPos }); } else if(e.button == main.conf.tabletOptions.rightClickButton) { main.input_mouse_button_callback({ .deviceType = MouseDeviceType::PEN, .button = MouseButton::RIGHT, .down = e.down, .clicks = 1, .pos = mouseNewPos }); } main.input_pen_button_callback({ .button = e.button, .down = e.down, .pos = mouseNewPos }); isTouchDevice = false; } void InputManager::backend_pen_button_up_update(const SDL_PenButtonEvent& e) { Vector2f mouseNewPos = {e.x * main.window.density, e.y * main.window.density}; mouse.set_pos(mouseNewPos); pen.previousPos = mouseNewPos; pen.buttons[e.button].held = false; pen.isEraser = e.pen_state & SDL_PEN_INPUT_ERASER_TIP; if(e.button == main.conf.tabletOptions.middleClickButton) { main.input_mouse_button_callback({ .deviceType = MouseDeviceType::PEN, .button = MouseButton::MIDDLE, .down = e.down, .clicks = 1, .pos = mouseNewPos }); } else if(e.button == main.conf.tabletOptions.rightClickButton) { main.input_mouse_button_callback({ .deviceType = MouseDeviceType::PEN, .button = MouseButton::RIGHT, .down = e.down, .clicks = 1, .pos = mouseNewPos }); } main.input_pen_button_callback({ .button = e.button, .down = e.down, .pos = mouseNewPos }); isTouchDevice = false; } void InputManager::backend_pen_touch_down_update(const SDL_PenTouchEvent& e) { Vector2f mouseNewPos = {e.x * main.window.density, e.y * main.window.density}; mouse.set_pos(mouseNewPos); pen.previousPos = mouseNewPos; pen.isDown = true; mouse.leftDown = true; if((std::chrono::steady_clock::now() - pen.lastPenLeftClickTime) > std::chrono::milliseconds(300)) pen.leftClicksSaved = 0; pen.leftClicksSaved++; pen.lastPenLeftClickTime = std::chrono::steady_clock::now(); pen.isEraser = e.eraser; main.input_mouse_button_callback({ .deviceType = MouseDeviceType::PEN, .button = MouseButton::LEFT, .down = e.down, .clicks = pen.leftClicksSaved, .pos = mouseNewPos }); main.input_pen_touch_callback({ .down = e.down, .eraser = e.eraser, .pos = mouseNewPos }); isTouchDevice = false; } void InputManager::backend_pen_touch_up_update(const SDL_PenTouchEvent& e) { Vector2f mouseNewPos = {e.x * main.window.density, e.y * main.window.density}; mouse.set_pos(mouseNewPos); pen.previousPos = mouseNewPos; pen.isDown = false; mouse.leftDown = false; pen.isEraser = e.eraser; pen.pressure = 0.0; main.input_mouse_button_callback({ .deviceType = MouseDeviceType::PEN, .button = MouseButton::LEFT, .down = e.down, .clicks = 0, .pos = mouseNewPos }); main.input_pen_touch_callback({ .down = e.down, .eraser = e.eraser, .pos = mouseNewPos }); isTouchDevice = false; } void InputManager::backend_pen_motion_update(const SDL_PenMotionEvent& e) { Vector2f mouseNewPos = {e.x * main.window.density, e.y * main.window.density}; mouse.set_pos(mouseNewPos); Vector2f mouseRel = mouseNewPos - pen.previousPos; pen.previousPos = mouseNewPos; pen.isEraser = e.pen_state & SDL_PEN_INPUT_ERASER_TIP; main.input_mouse_motion_callback({ .deviceType = MouseDeviceType::PEN, .pos = mouseNewPos, .move = mouseRel }); main.input_pen_motion_callback({ .pos = mouseNewPos, .move = mouseRel }); isTouchDevice = false; } void InputManager::backend_pen_axis_update(const SDL_PenAxisEvent& e) { Vector2f mouseNewPos = {e.x * main.window.density, e.y * main.window.density}; mouse.set_pos(mouseNewPos); pen.previousPos = mouseNewPos; pen.isEraser = e.pen_state & SDL_PEN_INPUT_ERASER_TIP; // For now, only pass pressure axis if(e.axis == SDL_PEN_AXIS_PRESSURE) { pen.pressure = std::clamp(e.value, 0.0f, 1.0f); main.input_pen_axis_callback({ .pos = mouseNewPos, .axis = e.axis, .value = pen.pressure }); } } std::vector InputManager::get_multiple_finger_positions() { std::vector pos; for(const SDL_TouchFingerEvent& fingerEvent : touch.fingers) pos.emplace_back(fingerEvent.x * main.window.size.x() * main.window.density, fingerEvent.y * main.window.size.y() * main.window.density); return pos; } std::vector InputManager::get_multiple_finger_motions() { std::vector pos; for(const SDL_TouchFingerEvent& fingerEvent : touch.fingers) pos.emplace_back(fingerEvent.dx * main.window.size.x() * main.window.density, fingerEvent.dy * main.window.size.y() * main.window.density); return pos; } void InputManager::touch_finger_do_mouse_down() { auto& prevTouch = touch.fingers[0]; Vector2f mouseNewPos = {prevTouch.x * main.window.size.x() * main.window.density, prevTouch.y * main.window.size.y() * main.window.density}; mouse.set_pos(mouseNewPos); mouse.leftDown = true; if((std::chrono::steady_clock::now() - touch.lastLeftClickTime) > std::chrono::milliseconds(500)) touch.leftClicksSaved = 0; touch.leftClicksSaved++; touch.lastLeftClickTime = std::chrono::steady_clock::now(); main.input_mouse_button_callback({ .deviceType = MouseDeviceType::TOUCH, .button = MouseButton::LEFT, .down = true, .clicks = touch.leftClicksSaved, .pos = mouseNewPos }); } void InputManager::touch_finger_do_mouse_up(const SDL_TouchFingerEvent& e) { Vector2f mouseNewPos = {e.x * main.window.size.x() * main.window.density, e.y * main.window.size.y() * main.window.density}; mouse.set_pos(mouseNewPos); mouse.leftDown = false; main.input_mouse_button_callback({ .deviceType = MouseDeviceType::TOUCH, .button = MouseButton::LEFT, .down = false, .clicks = 0, .pos = mouseNewPos }); } void InputManager::touch_finger_do_mouse_motion(const SDL_TouchFingerEvent& e) { Vector2f mouseNewPos = {e.x * main.window.size.x() * main.window.density, e.y * main.window.size.y() * main.window.density}; mouse.set_pos(mouseNewPos); Vector2f mouseRel = {e.dx * main.window.size.x() * main.window.density, e.dy * main.window.size.y() * main.window.density}; main.input_mouse_motion_callback({ .deviceType = MouseDeviceType::TOUCH, .pos = mouseNewPos, .move = mouseRel }); } void InputManager::backend_touch_finger_down_update(const SDL_TouchFingerEvent& e) { touch.fingers.emplace_back(e); auto fingerPosVec = get_multiple_finger_positions(); switch(touch.touchEventType) { case Touch::NO_TOUCH_EVENT: { if(touch.fingers.size() == 2) { touch.touchEventType = Touch::TWO_FINGER_EVENT; main.input_multi_finger_touch_callback({ .down = true, .pos = fingerPosVec }); } break; } case Touch::ONE_FINGER_EVENT: { if(touch.fingers.size() == 2) { touch_finger_do_mouse_up(e); touch.touchEventType = Touch::TWO_FINGER_EVENT; main.input_multi_finger_touch_callback({ .down = true, .pos = fingerPosVec }); } break; } case Touch::TWO_FINGER_EVENT: { break; } case Touch::EVENT_DONE: { if(touch.fingers.size() == 1) touch.touchEventType = Touch::NO_TOUCH_EVENT; break; } } if((std::chrono::steady_clock::now() - touch.lastFingerTapTime) > std::chrono::milliseconds(500)) touch.fingerTapsSaved = 0; touch.fingerTapsSaved++; touch.lastFingerTapTime = std::chrono::steady_clock::now(); main.input_finger_touch_callback({ .fingerID = e.fingerID, .down = true, .pos = fingerPosVec.back(), .fingerDownCount = touch.fingers.size(), .fingerTapCount = touch.fingerTapsSaved }); isTouchDevice = true; } void InputManager::backend_touch_finger_up_update(const SDL_TouchFingerEvent& e) { switch(touch.touchEventType) { case Touch::NO_TOUCH_EVENT: { touch_finger_do_mouse_down(); touch_finger_do_mouse_up(e); touch.touchEventType = Touch::EVENT_DONE; break; } case Touch::ONE_FINGER_EVENT: { touch_finger_do_mouse_up(e); touch.touchEventType = Touch::EVENT_DONE; break; } case Touch::TWO_FINGER_EVENT: { main.input_multi_finger_touch_callback({ .down = false, .pos = get_multiple_finger_positions() }); touch.touchEventType = Touch::EVENT_DONE; break; } case Touch::EVENT_DONE: { break; } } main.input_finger_touch_callback({ .fingerID = e.fingerID, .down = false, .pos = {e.x * main.window.size.x() * main.window.density, e.y * main.window.size.y() * main.window.density}, .fingerDownCount = touch.fingers.size(), .fingerTapCount = 0 }); std::erase_if(touch.fingers, [&e](auto& tE) { return e.fingerID == tE.fingerID; }); isTouchDevice = true; } void InputManager::backend_touch_finger_motion_update(const SDL_TouchFingerEvent& e) { switch(touch.touchEventType) { case Touch::NO_TOUCH_EVENT: { touch_finger_do_mouse_down(); touch_finger_do_mouse_motion(e); touch.touchEventType = Touch::ONE_FINGER_EVENT; break; } case Touch::ONE_FINGER_EVENT: { touch_finger_do_mouse_motion(e); break; } case Touch::TWO_FINGER_EVENT: { main.input_multi_finger_motion_callback({ .pos = get_multiple_finger_positions(), .move = get_multiple_finger_motions() }); break; } case Touch::EVENT_DONE: { break; } } for(auto& tE : touch.fingers) { if(tE.fingerID == e.fingerID) { tE = e; break; } } main.input_finger_motion_callback({ .fingerID = e.fingerID, .pos = {e.x * main.window.size.x() * main.window.density, e.y * main.window.size.y() * main.window.density}, .move = {e.dx * main.window.size.x() * main.window.density, e.dy * main.window.size.y() * main.window.density}, .fingerDownCount = touch.fingers.size() }); isTouchDevice = true; } void InputManager::update_safe_area() { SDL_Rect safeArea; if(SDL_GetWindowSafeArea(main.window.sdlWindow, &safeArea)) main.window.safeArea = SCollision::AABB({safeArea.x, safeArea.y}, {safeArea.x + safeArea.w, safeArea.y + safeArea.h}); else main.window.safeArea = SCollision::AABB({0, 0}, {main.window.size.x(), main.window.size.y()}); } void InputManager::backend_window_resize_update() { update_safe_area(); WindowResizeCallbackArgs w; w.density = main.window.density; w.size = main.window.size; w.safeArea = main.window.safeArea; main.input_window_resize_callback(w); } void InputManager::backend_window_scale_update(const SDL_WindowEvent& e) { WindowScaleCallbackArgs w; w.scale = main.window.scale; main.input_window_scale_callback(w); } void InputManager::backend_key_down_update(const SDL_KeyboardEvent& e) { auto kPress = e.key; auto kMod = e.mod; #ifdef __ANDROID__ if(kPress == SDLK_AC_BACK) { main.input_global_back_button_callback(); return; } #endif if(kPress != SDLK_LSHIFT && kPress != SDLK_RSHIFT && kPress != SDLK_LCTRL && kPress != SDLK_RCTRL && kPress != SDLK_LGUI && kPress != SDLK_RGUI && kPress != SDLK_LALT && kPress != SDLK_RALT) { if(main.input_keybind_callback({make_generic_key_mod(kMod), kPress})) return; } bool acceptingTextInput = text.is_accepting_input(); switch(kPress) { case SDLK_UP: set_key_down(e, KEY_GENERIC_UP, acceptingTextInput); break; case SDLK_DOWN: set_key_down(e, KEY_GENERIC_DOWN, acceptingTextInput); break; case SDLK_LEFT: set_key_down(e, KEY_GENERIC_LEFT, acceptingTextInput); break; case SDLK_RIGHT: set_key_down(e, KEY_GENERIC_RIGHT, acceptingTextInput); break; case SDLK_BACKSPACE: set_key_down(e, KEY_TEXT_BACKSPACE, acceptingTextInput); break; case SDLK_DELETE: set_key_down(e, KEY_TEXT_DELETE, acceptingTextInput); break; case SDLK_HOME: set_key_down(e, KEY_TEXT_HOME, acceptingTextInput); break; case SDLK_END: set_key_down(e, KEY_TEXT_END, acceptingTextInput); break; case SDLK_LSHIFT: set_key_down(e, KEY_GENERIC_LSHIFT, acceptingTextInput); break; case SDLK_LALT: set_key_down(e, KEY_GENERIC_LALT, acceptingTextInput); break; case SDLK_LCTRL: set_key_down(e, KEY_GENERIC_LCTRL, acceptingTextInput); break; case SDLK_LMETA: set_key_down(e, KEY_GENERIC_LMETA, acceptingTextInput); break; case SDLK_C: // Use either Ctrl or Meta (command for Mac) keys. We do either instead of checking, since checking can get complicated on Emscripten if((kMod & SDL_KMOD_GUI) || (kMod & SDL_KMOD_CTRL)) set_key_down(e, KEY_TEXT_COPY, acceptingTextInput); break; case SDLK_X: if((kMod & SDL_KMOD_GUI) || (kMod & SDL_KMOD_CTRL)) set_key_down(e, KEY_TEXT_CUT, acceptingTextInput); break; case SDLK_V: if((kMod & SDL_KMOD_GUI) || (kMod & SDL_KMOD_CTRL)) set_key_down(e, KEY_TEXT_PASTE, acceptingTextInput); break; case SDLK_A: if((kMod & SDL_KMOD_GUI) || (kMod & SDL_KMOD_CTRL)) set_key_down(e, KEY_TEXT_SELECTALL, acceptingTextInput); break; case SDLK_Z: if((kMod & SDL_KMOD_GUI) || (kMod & SDL_KMOD_CTRL)) set_key_down(e, KEY_TEXT_UNDO, acceptingTextInput); break; case SDLK_R: if((kMod & SDL_KMOD_GUI) || (kMod & SDL_KMOD_CTRL)) set_key_down(e, KEY_TEXT_REDO, acceptingTextInput); break; case SDLK_RETURN: set_key_down(e, KEY_GENERIC_ENTER, acceptingTextInput); break; case SDLK_TAB: set_key_down(e, KEY_TEXT_TAB, acceptingTextInput); break; case SDLK_ESCAPE: set_key_down(e, KEY_GENERIC_ESCAPE, acceptingTextInput); break; default: break; } auto f = keyAssignments.find({make_generic_key_mod(kMod), kPress}); if(f != keyAssignments.end()) set_key_down(e, f->second, acceptingTextInput); } void InputManager::call_paste(CustomEvents::PasteEvent::DataType type, const InputManagerCallPasteInfo& info) { // Workaround for not being able to copy richtext to system clipboard, this should at least work within the application itself #ifdef __EMSCRIPTEN__ switch(type) { case CustomEvents::PasteEvent::DataType::TEXT: { struct PasteData { bool allowRichText; InputManager* t; }; static PasteData pasteData; pasteData.allowRichText = info.allowRichText; pasteData.t = this; emscripten_browser_clipboard::paste_async([](std::string&& pasteData, void* callbackData){ PasteData* p = (PasteData*)callbackData; p->t->process_text_paste(pasteData, p->allowRichText); }, &pasteData); break; } case CustomEvents::PasteEvent::DataType::IMAGE: { struct PasteData { std::optional pos; InputManager* t; }; static PasteData pasteData; pasteData.pos = info.pastePosition; pasteData.t = this; emscripten_browser_clipboard::paste_async_image([](std::string_view pasteData, void* callbackData){ PasteData* p = (PasteData*)callbackData; p->t->process_image_paste(pasteData, p->pos); }, &pasteData); break; } } #else switch(type) { case CustomEvents::PasteEvent::DataType::TEXT: process_text_paste(get_clipboard_str_SDL(), info.allowRichText); break; case CustomEvents::PasteEvent::DataType::IMAGE: get_clipboard_image_data_SDL([&](std::string_view pasteData) { process_image_paste(pasteData, info.pastePosition); }); break; } #endif } void InputManager::process_text_paste(const std::string& pasteStr, bool allowRichText) { CustomEvents::emit_event({ .type = CustomEvents::PasteEvent::DataType::TEXT, .data = pasteStr, .richText = allowRichText && lastCopiedRichText.has_value() && lastCopiedRichText.value().get_plain_text() == remove_carriage_returns_from_str(pasteStr) ? lastCopiedRichText : std::nullopt }); } void InputManager::process_image_paste(std::string_view pasteData, const std::optional& pastePos) { CustomEvents::emit_event({ .type = CustomEvents::PasteEvent::DataType::IMAGE, .data = std::string(pasteData), .mousePos = pastePos }); } bool InputManager::ctrl_or_meta_held() { return key(KEY_GENERIC_LCTRL).held || key(KEY_GENERIC_LMETA).held; } void InputManager::backend_key_up_update(const SDL_KeyboardEvent& e) { auto kPress = e.key; bool acceptingTextInput = text.is_accepting_input(); switch(kPress) { case SDLK_UP: set_key_up(e, KEY_GENERIC_UP, acceptingTextInput); break; case SDLK_DOWN: set_key_up(e, KEY_GENERIC_DOWN, acceptingTextInput); break; case SDLK_LEFT: set_key_up(e, KEY_GENERIC_LEFT, acceptingTextInput); break; case SDLK_RIGHT: set_key_up(e, KEY_GENERIC_RIGHT, acceptingTextInput); break; case SDLK_BACKSPACE: set_key_up(e, KEY_TEXT_BACKSPACE, acceptingTextInput); break; case SDLK_DELETE: set_key_up(e, KEY_TEXT_DELETE, acceptingTextInput); break; case SDLK_HOME: set_key_up(e, KEY_TEXT_HOME, acceptingTextInput); break; case SDLK_END: set_key_up(e, KEY_TEXT_END, acceptingTextInput); break; case SDLK_LSHIFT: set_key_up(e, KEY_GENERIC_LSHIFT, acceptingTextInput); break; case SDLK_LALT: set_key_up(e, KEY_GENERIC_LALT, acceptingTextInput); break; case SDLK_LCTRL: set_key_up(e, KEY_GENERIC_LCTRL, acceptingTextInput); break; case SDLK_C: set_key_up(e, KEY_TEXT_COPY, acceptingTextInput); break; case SDLK_X: set_key_up(e, KEY_TEXT_CUT, acceptingTextInput); break; case SDLK_V: set_key_up(e, KEY_TEXT_PASTE, acceptingTextInput); break; case SDLK_A: set_key_up(e, KEY_TEXT_SELECTALL, acceptingTextInput); break; case SDLK_Z: set_key_up(e, KEY_TEXT_UNDO, acceptingTextInput); break; case SDLK_R: set_key_up(e, KEY_TEXT_REDO, acceptingTextInput); break; case SDLK_RETURN: set_key_up(e, KEY_GENERIC_ENTER, acceptingTextInput); break; case SDLK_TAB: set_key_up(e, KEY_TEXT_TAB, acceptingTextInput); break; case SDLK_ESCAPE: set_key_up(e, KEY_GENERIC_ESCAPE, acceptingTextInput); break; default: break; } for(auto& p : keyAssignments) if(p.first.y() == kPress) // Dont try matching modifiers when setting key to go up set_key_up(e, p.second, acceptingTextInput); } void InputManager::Mouse::set_pos(const Vector2f& newPos) { pos = newPos; } void InputManager::update() { if(touch.touchEventType == Touch::NO_TOUCH_EVENT && (SDL_GetTicksNS() - touch.fingers[0].timestamp) > (200 * 1000000)) { // 200ms to determine whether touch is with a single finger touch_finger_do_mouse_down(); touch.touchEventType = Touch::ONE_FINGER_EVENT; } if(!text.textBoxes.empty()) { //auto& textbox = text.textBoxes.front(); //if(textbox.rect) { // SDL_Rect r = textbox.rect->get_sdl_rect(); // //SDL_SetTextInputArea(main.window.sdlWindow, &r, textbox.cursor->pos.fTextByteIndex); //} //else // //SDL_SetTextInputArea(main.window.sdlWindow, nullptr, textbox.cursor->pos.fTextByteIndex); } } void InputManager::frame_reset(const Vector2i& windowSize) { cursorIcon = SystemCursorType::DEFAULT; hideCursor = false; }