From 1dd6be1e63e7b9795e281385dfed2cb19a20c50a Mon Sep 17 00:00:00 2001 From: rstein Date: Fri, 11 Aug 2023 18:04:04 +0200 Subject: [PATCH] refactored touch handling --- src/ui/CMakeLists.txt | 6 +- src/ui/cmake/Dependencies.cmake | 10 +- src/ui/dashboardpage.cpp | 6 +- src/ui/flowgraphitem.h | 6 + src/ui/imguiutils.cpp | 12 +- src/ui/imguiutils.h | 27 +- src/ui/main.cpp | 56 +--- src/ui/utils/CMakeLists.txt | 5 + src/ui/utils/TouchHandler.hpp | 510 ++++++++++++++++++++++++++++++++ 9 files changed, 566 insertions(+), 72 deletions(-) create mode 100644 src/ui/utils/CMakeLists.txt create mode 100644 src/ui/utils/TouchHandler.hpp diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index bb4e9c7a..fab22e26 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -29,6 +29,7 @@ set(sources set(GNURADIO_PREFIX "/usr/" CACHE FILEPATH "Prefix of the GNURadio installation") add_subdirectory(app_header) +add_subdirectory(utils) if (EMSCRIPTEN) message(STATUS "Detected emscripten webassembly build") @@ -90,6 +91,7 @@ if (EMSCRIPTEN) implot plf_colony app_header + utils ui_assets sample_dashboards fonts @@ -107,7 +109,8 @@ if (EMSCRIPTEN) else () # native build set(target_name "opendigitizer-ui") - add_executable(${target_name}) + add_executable(${target_name} + utils/TouchHandler.hpp) target_sources(${target_name} PRIVATE ${sources}) @@ -123,6 +126,7 @@ else () # native build core client app_header + utils ui_assets sample_dashboards fonts diff --git a/src/ui/cmake/Dependencies.cmake b/src/ui/cmake/Dependencies.cmake index a9652233..22723f67 100644 --- a/src/ui/cmake/Dependencies.cmake +++ b/src/ui/cmake/Dependencies.cmake @@ -54,11 +54,11 @@ FetchContent_Declare( if (EMSCRIPTEN) FetchContent_MakeAvailable(imgui implot imgui-node-editor yaml-cpp stb opencmw-cpp plf_colony function2) else () # native build - # FetchContent_Declare( - # sdl2 - # GIT_REPOSITORY "https://github.com/libsdl-org/SDL" - # GIT_TAG release-2.24.2 - # ) + FetchContent_Declare( + sdl2 + GIT_REPOSITORY "https://github.com/libsdl-org/SDL" + GIT_TAG release-2.28.2 + ) FetchContent_MakeAvailable(imgui implot imgui-node-editor yaml-cpp plf_colony stb opencmw-cpp function2) find_package(SDL2 REQUIRED) find_package(OpenGL REQUIRED COMPONENTS OpenGL) diff --git a/src/ui/dashboardpage.cpp b/src/ui/dashboardpage.cpp index c10358c4..1ad10c7d 100644 --- a/src/ui/dashboardpage.cpp +++ b/src/ui/dashboardpage.cpp @@ -8,6 +8,8 @@ #include "flowgraph/datasink.h" #include "imguiutils.h" +#include "utils/TouchHandler.hpp" + namespace DigitizerUi { namespace { @@ -470,7 +472,7 @@ void DashboardPage::drawPlots(App *app, DigitizerUi::DashboardPage::Mode mode, D ImPlot::PushStyleVar(ImPlotStyleVar_PlotPadding, ImVec2{ 0, 0 }); // TODO: make this perhaps a global style setting via ImPlot::GetStyle() ImPlot::PushStyleVar(ImPlotStyleVar_LabelPadding, ImVec2{ 3, 1 }); - if (ImPlot::BeginPlot(plot.name.c_str(), plotSize - ImVec2(2 * offset, 2 * offset), plotFlags)) { + if (fair::TouchHandler<>::BeginZoomablePlot(plot.name, plotSize - ImVec2(2 * offset, 2 * offset), plotFlags)) { drawPlot(plot); // allow the main plot area to be a DND target @@ -517,7 +519,7 @@ void DashboardPage::drawPlots(App *app, DigitizerUi::DashboardPage::Mode mode, D } } - ImPlot::EndPlot(); + fair::TouchHandler<>::EndZoomablePlot(); ImPlot::PopStyleVar(2); if (mode == Mode::Layout) { diff --git a/src/ui/flowgraphitem.h b/src/ui/flowgraphitem.h index 225b9bf6..3b2a5ae1 100644 --- a/src/ui/flowgraphitem.h +++ b/src/ui/flowgraphitem.h @@ -1,6 +1,12 @@ #pragma once +#ifndef IMPLOT_POINT_CLASS_EXTRA +#define IMGUI_DEFINE_MATH_OPERATORS true +#endif + #include +#include +#include #include #include diff --git a/src/ui/imguiutils.cpp b/src/ui/imguiutils.cpp index 21206856..d238aacf 100644 --- a/src/ui/imguiutils.cpp +++ b/src/ui/imguiutils.cpp @@ -1,3 +1,7 @@ +#ifndef IMPLOT_POINT_CLASS_EXTRA +#define IMGUI_DEFINE_MATH_OPERATORS true +#endif + #include #include #include @@ -211,7 +215,7 @@ class InputKeypad { public: template - requires std::integral || std::floating_point || std::same_as + requires std::integral || std::floating_point || std::same_as [[nodiscard]] static bool edit(const char *label, EdTy *value) { if (!label || !value) { return false; @@ -1182,13 +1186,13 @@ void drawBlockControlsPanel(BlockControlsPanel &ctx, const ImVec2 &pos, const Im auto listSize = verticalLayout ? ImVec2(size.x, 200) : ImVec2(200, size.y - ImGui::GetFrameHeightWithSpacing()); auto ret = filteredListBox( - "blocks", BlockType::registry().types(), [](auto &it) -> std::pair { + "blocks", BlockType::registry().types(), [](auto &it) -> std::pair { if (it.second->inputs.size() != 1 || it.second->outputs.size() != 1) { return {}; } return std::pair{ it.second.get(), it.first }; - }, - listSize); + }, + listSize); { DisabledGuard dg(!ret.has_value()); diff --git a/src/ui/imguiutils.h b/src/ui/imguiutils.h index 2e890725..9433e1d9 100644 --- a/src/ui/imguiutils.h +++ b/src/ui/imguiutils.h @@ -1,6 +1,10 @@ #ifndef IMGUIUTILS_H #define IMGUIUTILS_H +#ifndef IMPLOT_POINT_CLASS_EXTRA +#define IMGUI_DEFINE_MATH_OPERATORS true +#endif + #include #include #include @@ -8,24 +12,11 @@ #include #include +#include #include #include "flowgraph.h" -inline ImVec2 operator+(const ImVec2 a, const ImVec2 b) { - ImVec2 r = a; - r.x += b.x; - r.y += b.y; - return r; -} - -inline ImVec2 operator-(const ImVec2 a, const ImVec2 b) { - ImVec2 r = a; - r.x -= b.x; - r.y -= b.y; - return r; -} - namespace DigitizerUi { class Block; class Dashboard; @@ -198,7 +189,9 @@ std::optional filteredListBox(const char *id, const ImVec2 &size, Items &&ite } template -auto filteredListBox(const char *id, Items &&items, ItemGetter getItem, const ImVec2 &size = { 200, 200 }) requires std::is_invocable_v { +auto filteredListBox(const char *id, Items &&items, ItemGetter getItem, const ImVec2 &size = { 200, 200 }) + requires std::is_invocable_v +{ using T = decltype(getItem(*items.begin())); return filteredListBox(id, size, items, getItem, [](auto &&item, bool selected) { return ImGui::Selectable(item.second.data(), selected); @@ -206,7 +199,9 @@ auto filteredListBox(const char *id, Items &&items, ItemGetter getItem, const Im } template -auto filteredListBox(const char *id, Items &&items, ItemGetter getItem, ItemDrawer drawItem, const ImVec2 &size = { 200, 200 }) requires std::is_invocable_v { +auto filteredListBox(const char *id, Items &&items, ItemGetter getItem, ItemDrawer drawItem, const ImVec2 &size = { 200, 200 }) + requires std::is_invocable_v +{ using T = decltype(getItem(*items.begin())); return filteredListBox(id, size, items, getItem, drawItem); } diff --git a/src/ui/main.cpp b/src/ui/main.cpp index a5133655..91858e03 100644 --- a/src/ui/main.cpp +++ b/src/ui/main.cpp @@ -1,3 +1,7 @@ +#ifndef IMPLOT_POINT_CLASS_EXTRA +#define IMGUI_DEFINE_MATH_OPERATORS true +#endif + #ifdef __EMSCRIPTEN__ #include #endif @@ -25,6 +29,7 @@ #include "flowgraph/datasource.h" #include "flowgraph/fftblock.h" #include "flowgraphitem.h" +#include "utils/TouchHandler.hpp" CMRC_DECLARE(ui_assets); CMRC_DECLARE(fonts); @@ -183,7 +188,9 @@ int main(int argc, char **argv) { IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImPlot::CreateContext(); - ImGuiIO &io = ImGui::GetIO(); + ImGuiIO &io = ImGui::GetIO(); + ImPlot::GetInputMap().Select = ImGuiPopupFlags_MouseButtonLeft; + ImPlot::GetInputMap().Pan = ImGuiPopupFlags_MouseButtonMiddle; // For an Emscripten build we are disabling file-system access, so let's not // attempt to do a fopen() of the imgui.ini file. You may manually call @@ -287,8 +294,7 @@ static void main_loop(void *arg) { app->fireCallbacks(); // Poll and handle events (inputs, window resize, etc.) - SDL_Event event; - static std::size_t nFingers = 0; + SDL_Event event; while (SDL_PollEvent(&event)) { ImGui_ImplSDL2_ProcessEvent(&event); switch (event.type) { @@ -324,41 +330,11 @@ static void main_loop(void *arg) { break; } break; - case SDL_FINGERDOWN: - ++nFingers; - if (!app->touchDiagnostics) { - break; - } - fmt::print("touch: finger down: {} fingerID: {} p:{} @({},{})\n", // - nFingers, event.tfinger.fingerId, event.tfinger.pressure, event.tfinger.x, event.tfinger.y); - break; - case SDL_FINGERUP: - --nFingers; - if (!app->touchDiagnostics) { - break; - } - fmt::print("touch: finger up: {} fingerID: {} p:{} @({},{})\n", // - nFingers, event.tfinger.fingerId, event.tfinger.pressure, event.tfinger.x, event.tfinger.y); - break; - case SDL_FINGERMOTION: - if (!app->touchDiagnostics) { - break; - } - fmt::print("touch: finger motion: {} fingerID: {} p:{} @({},{}) motion (dx,dy): ({}, {})\n", // - nFingers, event.tfinger.fingerId, event.tfinger.pressure, event.tfinger.x, event.tfinger.y, event.tfinger.dx, event.tfinger.dy); - break; - case SDL_MULTIGESTURE: - switch (event.mgesture.type) { - case SDL_MULTIGESTURE: - const auto &gesture = event.mgesture; - fmt::print("detected multi-gesture event -- touchId:{} numFingers: {} @({},{}) dDist:{} dTheta:{}\n", // - gesture.touchId, gesture.numFingers, gesture.x, gesture.y, gesture.dDist, gesture.dTheta); - break; - } - break; } + fair::TouchHandler<>::processSDLEvent(event); // Capture events here, based on io.WantCaptureMouse and io.WantCaptureKeyboard } + fair::TouchHandler<>::updateLogic(); // Start the Dear ImGui frame ImGui_ImplOpenGL3_NewFrame(); @@ -369,14 +345,7 @@ static void main_loop(void *arg) { int width, height; SDL_GetWindowSize(app->sdlState->window, &width, &height); ImGui::SetNextWindowSize({ float(width), float(height) }); - if (nFingers >= 2) { - fmt::print("touch -- pressed two fingers emulating mouse button two\n"); - ImGui::GetIO().MouseDown[0] = false; - ImGui::GetIO().MouseDown[1] = true; - } else { - ImGui::GetIO().MouseDown[1] = false; - } - // TODO: add gesture pinch, stretch and rotate event mappings here + fair::TouchHandler<>::applyToImGui(); ImGui::Begin("Main Window", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus); @@ -387,7 +356,6 @@ static void main_loop(void *arg) { ImGui::BeginDisabled(); } - auto pos = ImGui::GetCursorPos(); ImGuiID viewId = 0; if (app->mainViewMode == "View" || app->mainViewMode == "") { viewId = ImGui::GetID(""); diff --git a/src/ui/utils/CMakeLists.txt b/src/ui/utils/CMakeLists.txt new file mode 100644 index 00000000..657db985 --- /dev/null +++ b/src/ui/utils/CMakeLists.txt @@ -0,0 +1,5 @@ +add_library( + utils INTERFACE TouchHandler.hpp +) +target_include_directories(utils INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(utils INTERFACE fmt) diff --git a/src/ui/utils/TouchHandler.hpp b/src/ui/utils/TouchHandler.hpp new file mode 100644 index 00000000..68cb63a6 --- /dev/null +++ b/src/ui/utils/TouchHandler.hpp @@ -0,0 +1,510 @@ +#ifndef OPENDIGITIZER_TOUCHHANDLER_HPP +#define OPENDIGITIZER_TOUCHHANDLER_HPP + +#ifndef IMPLOT_POINT_CLASS_EXTRA +#define IMGUI_DEFINE_MATH_OPERATORS true +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../app.h" + +namespace fair { + +template +struct TouchHandler { + using TimePoint = std::chrono::time_point; + constexpr static inline std::size_t N_MAX_FINGERS = 10; + + // fingerID to unique small int mapper + static inline std::unordered_map fingerIdToIndex; + static inline std::stack releasedIndices; + static inline std::size_t nextAvailableIndex = 0; + + // + static std::size_t getOrAssignIndex(SDL_FingerID fingerId) { + if (const auto it = fingerIdToIndex.find(fingerId); it != fingerIdToIndex.end()) { + return it->second; // fingerID already mapped return index + } + + if (!releasedIndices.empty()) { // reuse released indices + std::size_t index = releasedIndices.top(); + releasedIndices.pop(); + fingerIdToIndex.emplace(fingerId, index); + return index; + } + + // no released indices -> use nextAvailableIndex. + fingerIdToIndex[fingerId] = nextAvailableIndex; + assert(((nextAvailableIndex + 1) < N_MAX_FINGERS) && "more fingers than N_MAX_FINGERS detected"); + return nextAvailableIndex++; + } + + static void releaseIndex(SDL_FingerID fingerId) { + if (fingerIdToIndex.find(fingerId) != fingerIdToIndex.end()) { + releasedIndices.push(fingerIdToIndex[fingerId]); + fingerIdToIndex.erase(fingerId); + } + } + + static void releaseFingerIndex(std::size_t index) { + SDL_FingerID targetFingerId = -1; // Initialize with an invalid value + for (const auto &pair : fingerIdToIndex) { + if (pair.second == index) { + targetFingerId = pair.first; + break; + } + } + + if (targetFingerId != -1) { + fingerIdToIndex.erase(targetFingerId); + releasedIndices.push(index); + } + } + + // Static state variables + static inline std::array fingerPressed{}; // actual finger state + static inline std::array fingerLifted{}; // actual finger lifted + static inline std::array fingerPos{}; // actual finger position + static inline std::array fingerLastPos{}; // previous frame's finger position + static inline std::array fingerPosDiff{}; // actual finger position difference + static inline std::array fingerPosDown{}; // finger position when finger touch down + static inline std::array fingerPosUp{}; // finger position when finger is lifted + static inline std::array fingerTimeStamp{}; // time stamp when the last change occurred + static inline std::array fingerDownTimeStamp{}; // time stamp when the finger was pressed + static inline std::array fingerUpTimeStamp{}; // time stamp when the finger was lifted + static inline std::array fingerWindowID{}; // windowID where finger position when finger is lifted + // gesture state + static inline TimePoint gestureTimeStamp{}; // time stamp when the last change occurred + static inline TimePoint gestureDownTimeStamp{}; // time stamp when the finger was pressed + static inline TimePoint gestureUpTimeStamp{}; // time stamp when the finger was lifted + static inline ImVec2 gestureCentre{ -1.f, -1.f }; + static inline ImVec2 gestureCentreDiff{ 0.f, 0.f }; + static inline ImVec2 gestureLastCentre{ -1.f, -1.f }; + static inline ImVec2 gestureCentreDown{ -1.f, -1.f }; + static inline ImVec2 gestureCentreUp{ -1.f, -1.f }; + static inline ImVec2 gestureZoom{ 0.f, 0.f }; + static inline bool gestureActive = false; + static inline bool gestureDragActive = false; + static inline bool gestureZoomActive = false; + static inline float gestureRotationRad = 0.0f; + static inline float gestureRotationDeg = 0.0f; + + static inline std::size_t nFingers = 0; + static inline bool fingerDown = false; + static inline bool fingerUp = false; + static inline bool touchActive = false; + static inline bool singleFingerClicked = false; + + // some helper functions + static float getFingerMovementDistance(std::size_t fingerIndex) { + if (fingerIndex >= N_MAX_FINGERS) { + return -1.0f; + } + + return std::hypot(fingerPos[fingerIndex].x - fingerPosDown[fingerIndex].x, fingerPos[fingerIndex].y - fingerPosDown[fingerIndex].y); + } + + static auto getFingerPressedDuration(std::size_t fingerIndex) { + if (fingerIndex >= N_MAX_FINGERS) { + return std::chrono::microseconds(-1); + } + return std::chrono::duration_cast(fingerTimeStamp[fingerIndex] - fingerDownTimeStamp[fingerIndex]); + } + + static void drawFingerPositions() { + const float circleRadius = 10.0f; + const auto now = ClockSourceType::now(); + + ImDrawList *draw_list = ImGui::GetForegroundDrawList(); + for (std::size_t fingerIndex = 0; fingerIndex < N_MAX_FINGERS; ++fingerIndex) { + if (fingerPressed[fingerIndex] && fingerPosDown[fingerIndex].x != -1.f && fingerPosDown[fingerIndex].y != -1.f) { + draw_list->AddCircle(fingerPosDown[fingerIndex], circleRadius, IM_COL32(0, 255, 0, 255), 12, 3.0f); // green for pressed + } + + if (fingerPressed[fingerIndex] && fingerLastPos[fingerIndex].x != -1.f && fingerLastPos[fingerIndex].y != -1.f) { + draw_list->AddCircle(fingerLastPos[fingerIndex], 3.0f * circleRadius, IM_COL32(165, 165, 0, 255), 12, 3.0f); // yellow for last + } + + if (fingerPressed[fingerIndex] && fingerPos[fingerIndex].x != -1.f && fingerPos[fingerIndex].y != -1.f) { + draw_list->AddCircle(fingerPos[fingerIndex], 3.0f * circleRadius, IM_COL32(255, 165, 0, 255), 12, 3.0f); // orange for moving + } + + const auto timeSinceLifted = std::chrono::duration_cast(now - fingerTimeStamp[fingerIndex]); + if (fingerPosUp[fingerIndex].x != -1.f && fingerPosUp[fingerIndex].y != -1.f && (timeSinceLifted < std::chrono::seconds(3))) { + draw_list->AddNgon(fingerPosUp[fingerIndex], circleRadius, IM_COL32(255, 0, 0, 255), 3, 3.0f); // red for lifted + } + } + + // gesture diagnostics + const auto timeSinceLifted = std::chrono::duration_cast(now - gestureTimeStamp); + if (gestureDragActive && gestureCentreDown.x != -1.f && gestureCentreDown.y != -1.f) { + draw_list->AddNgon(gestureCentreDown, 1.0f * circleRadius, IM_COL32(0, 255, 0, 255), 5, 3.0f); // green for initial gesture centre + } + if (gestureDragActive && gestureLastCentre.x != -1.f && gestureLastCentre.y != -1.f) { + draw_list->AddNgon(gestureLastCentre, 3.0f * circleRadius, IM_COL32(165, 165, 0, 255), 5, 3.0f); // yellow pentagon for last gesture centre + } + if (gestureDragActive && gestureCentre.x != -1.f && gestureCentre.y != -1.f) { + draw_list->AddNgon(gestureCentre, 3.0f * circleRadius, IM_COL32(255, 165, 0, 255), 5, 3.0f); // orange pentagon for moving gesture centre + } + if (gestureCentreUp.x != -1.f && gestureCentreUp.y != -1.f && (timeSinceLifted < std::chrono::seconds(3))) { + draw_list->AddNgon(gestureCentreUp, circleRadius, IM_COL32(255, 0, 0, 255), 4, 3.0f); // red square for gesture centre lifted + } + } + + static void processSDLEvent(const SDL_Event &event) { + const auto &app = DigitizerUi::App::instance(); + const auto &displaySize = ImGui::GetIO().DisplaySize; + const auto now = ClockSourceType::now(); + + switch (event.type) { + case SDL_FINGERDOWN: { + const std::size_t fingerIndex = getOrAssignIndex(event.tfinger.fingerId); + touchActive = true; + fingerDown = true; + fingerTimeStamp[fingerIndex] = now; + fingerDownTimeStamp[fingerIndex] = fingerTimeStamp[fingerIndex]; + fingerUpTimeStamp[fingerIndex] = fingerTimeStamp[fingerIndex]; + fingerPressed[fingerIndex] = true; + fingerLifted[fingerIndex] = false; + fingerPos[fingerIndex] = { event.tfinger.x * displaySize.x, event.tfinger.y * displaySize.y }; + fingerLastPos[fingerIndex] = fingerPos[fingerIndex]; + fingerPosDiff[fingerIndex] = { 0.f, 0.f }; + fingerPosDown[fingerIndex] = fingerPos[fingerIndex]; + fingerPosUp[fingerIndex] = { -1.f, -1.f }; + fingerWindowID[fingerIndex] = event.tfinger.windowID; + ++nFingers; + + if (nFingers >= 2 && !gestureActive) { + if (singleFingerClicked) { + ImGui::GetIO().AddMouseButtonEvent(0, false); // release initial finger - not a simple click/drag + singleFingerClicked = false; + } + gestureActive = true; + gestureDownTimeStamp = now; + const ImVec2 centre = { 0.5f * fingerPos[0].x + 0.5f * fingerPos[1].x, + 0.5f * fingerPos[0].y + 0.5f * fingerPos[1].y }; + gestureLastCentre = (gestureCentre.x != -1.f && gestureCentre.y != -1.f) ? gestureCentre : centre; + gestureCentre = centre; + gestureCentreDown = gestureCentre; + } + if (!gestureActive && !gestureDragActive && !gestureZoomActive && nFingers == 1) { + ImGui::GetIO().AddMousePosEvent(fingerPos[fingerIndex].x, fingerPos[fingerIndex].y); + ImGui::GetIO().AddMouseButtonEvent(fingerIndex, true); + singleFingerClicked = true; + } + if (app.touchDiagnostics) { + fmt::print("touch: finger down: {} fingerID: {} p:{} @({},{})\n", nFingers, fingerIndex, event.tfinger.pressure, event.tfinger.x, event.tfinger.y); + } + } break; + case SDL_FINGERUP: { + const std::size_t fingerIndex = getOrAssignIndex(event.tfinger.fingerId); + touchActive = true; + fingerUp = true; + fingerTimeStamp[fingerIndex] = now; + fingerUpTimeStamp[fingerIndex] = fingerTimeStamp[fingerIndex]; + fingerPressed[fingerIndex] = false; + fingerLifted[fingerIndex] = true; + fingerLastPos[fingerIndex] = fingerPos[fingerIndex]; + fingerPos[fingerIndex] = { event.tfinger.x * displaySize.x, event.tfinger.y * displaySize.y }; + fingerPosDiff[fingerIndex] = fingerPos[fingerIndex] - fingerLastPos[fingerIndex]; + fingerPosUp[fingerIndex] = fingerPos[fingerIndex]; + fingerWindowID[fingerIndex] = event.tfinger.windowID; + assert(nFingers > 0); + --nFingers; + releaseIndex(event.tfinger.fingerId); + if (nFingers == 0 && !gestureActive && !gestureDragActive && !gestureZoomActive) { + if (getFingerPressedDuration(0) < std::chrono::milliseconds(500)) { // short click -> process as left click + ImGui::GetIO().AddMouseButtonEvent(ImGuiPopupFlags_MouseButtonLeft, true); + ImGui::GetIO().AddMouseButtonEvent(ImGuiPopupFlags_MouseButtonLeft, false); + } else { // long click -> process as right click + ImGui::GetIO().AddMouseButtonEvent(ImGuiPopupFlags_MouseButtonRight, true); + ImGui::GetIO().AddMouseButtonEvent(ImGuiPopupFlags_MouseButtonRight, false); + } + } + + if (!gestureDragActive && !gestureZoomActive && nFingers == 0) { // finish single-finger drag + ImGui::GetIO().AddMousePosEvent(fingerPos[fingerIndex].x, fingerPos[fingerIndex].y); + ImGui::GetIO().AddMouseButtonEvent(fingerIndex, false); + } + + if (app.touchDiagnostics) { + fmt::print("touch: finger up: {} fingerID: {} p:{} @({},{})\n", nFingers, fingerIndex, event.tfinger.pressure, event.tfinger.x, event.tfinger.y); + } + } break; + case SDL_FINGERMOTION: { + std::size_t fingerIndex = getOrAssignIndex(event.tfinger.fingerId); + touchActive = true; + fingerTimeStamp[fingerIndex] = now; + fingerPressed[fingerIndex] = true; + fingerLifted[fingerIndex] = false; + fingerLastPos[fingerIndex] = fingerPos[fingerIndex]; + fingerPos[fingerIndex] = { event.tfinger.x * displaySize.x, event.tfinger.y * displaySize.y }; + fingerPosDiff[fingerIndex] = fingerPos[fingerIndex] - fingerLastPos[fingerIndex]; + fingerWindowID[fingerIndex] = event.tfinger.windowID; + if (nFingers == 1) { + ImGui::GetIO().AddMousePosEvent(fingerPos[fingerIndex].x, fingerPos[fingerIndex].y); + } + if (app.touchDiagnostics) { + fmt::print("touch: finger motion: {} fingerID: {} p:{} @({},{}) motion (dx,dy): ({}, {})\n", + nFingers, fingerIndex, event.tfinger.pressure, event.tfinger.x, event.tfinger.y, + event.tfinger.dx, event.tfinger.dy); + } + } break; + case SDL_MULTIGESTURE: + if (app.touchDiagnostics) { + fmt::print( + "detected multi-gesture event -- touchId:{} numFingers: {} @({},{}) dDist:{} dTheta:{}\n", + event.mgesture.touchId, event.mgesture.numFingers, event.mgesture.x, event.mgesture.y, + event.mgesture.dDist, event.mgesture.dTheta); + } + break; + + // ... [add any other cases you'd like to handle] + } + } + + static void updateLogic() { + const auto now = ClockSourceType::now(); + const auto &app = DigitizerUi::App::instance(); + + // compute gesture centre, pinch, and rotation + if (nFingers >= 2 && fingerPressed[0] && fingerPressed[1]) { + gestureTimeStamp = now; + + const ImVec2 centre = { 0.5f * fingerPos[0].x + 0.5f * fingerPos[1].x, + 0.5f * fingerPos[0].y + 0.5f * fingerPos[1].y }; + gestureLastCentre = (gestureCentre.x != -1.f && gestureCentre.y != -1.f) ? gestureCentre : centre; + gestureCentre = centre; + + gestureCentreDiff = gestureCentre - gestureCentreDown; + + if (!gestureDragActive && std::hypot(gestureCentreDiff.x, gestureCentreDiff.y) > ImGui::GetIO().MouseDragThreshold) { + ImGui::GetIO().AddMouseButtonEvent(ImPlot::GetInputMap().Pan, true); + // ImGui::GetIO().AddMousePosEvent(gestureCentre.x, gestureCentre.y); + ImGui::GetIO().MousePos = gestureCentre; + ImGui::GetIO().MouseDelta = gestureCentre - gestureLastCentre; + gestureDragActive = true; + if (app.touchDiagnostics) { + fmt::print("gesture: start two finger drag - centre ({},{})\n", gestureCentreUp.x, gestureCentreUp.y); + } + } + } else if (nFingers == 0) { + if (gestureActive) { + gestureActive = false; + gestureUpTimeStamp = now; + gestureCentreUp = gestureCentre; + + gestureCentre = { -1.f, -1.f }; + gestureLastCentre = { -1.f, -1.f }; + gestureCentreDiff = { 0.f, 0.f }; + gestureRotationRad = 0.0f; + gestureRotationDeg = 0.0f; + } + if (gestureDragActive) { + ImGui::GetIO().AddMouseButtonEvent(ImPlot::GetInputMap().Pan, false); + gestureDragActive = false; + + if (app.touchDiagnostics) { + fmt::print("gesture: stop two finger drag - centre ({},{})\n", gestureCentreUp.x, gestureCentreUp.y); + } + } + if (gestureZoomActive) { + gestureZoomActive = false; + if (app.touchDiagnostics) { + fmt::print("gesture: stop two finger zoom - centre ({},{})\n", gestureCentreUp.x, + gestureCentreUp.y); + } + } + } + + if (nFingers >= 2) { // handle pinch/spread and rotation gestures + const float prevDist = std::hypot(fingerLastPos[0].x - fingerLastPos[1].x, + fingerLastPos[0].y - fingerLastPos[1].y); + const float currDist = std::hypot(fingerPos[0].x - fingerPos[1].x, fingerPos[0].y - fingerPos[1].y); + + float pinchFactor = currDist / prevDist; + + if constexpr (zoomViaMouseWheel) { + // zoom interaction via mouse wheel + ImGui::GetIO().AddMouseWheelEvent((pinchFactor - 1.f) * 2.f, (pinchFactor - 1.f) * 2.f); + } + ImVec2 prevDir = { fingerLastPos[1].x - fingerLastPos[0].x, fingerLastPos[1].y - fingerLastPos[0].y }; + ImVec2 currDir = { fingerPos[1].x - fingerPos[0].x, fingerPos[1].y - fingerPos[0].y }; + + float prevAngle = std::atan2(prevDir.y, prevDir.x); + float currAngle = std::atan2(currDir.y, currDir.x); + + const float rotationAngleRad = (currAngle - prevAngle); + const float rotationAngleDeg = rotationAngleRad * (180.f / std::numbers::pi); + + const float ROTATION_THRESHOLD = 0 * 2.0f; + if (std::fabs(rotationAngleDeg) > ROTATION_THRESHOLD) { + // apply rotation -> check w.r.t. ImPlot handling + // fmt::print("rotate by {} degree\n", rotationAngleDeg); + } + if (app.touchDiagnostics) { + fmt::print("multi-gesture event -- {}: numFingers: {} @({},{} delta {},{}) pinchFactor:{} dTheta:{}\n", + fingerTimeStamp[0], nFingers, fingerLastPos[0].x, fingerLastPos[0].y, fingerPosDiff[1].x, fingerPosDiff[1].y, pinchFactor, rotationAngleDeg); + } + } + + // auto-lift finger if it hasn't been active (moving/lifted) for more than 10 seconds -> usually happens when an IO event has been lost + for (std::size_t fingerIndex = 0UL; fingerIndex < N_MAX_FINGERS; fingerIndex++) { + const auto timeSinceLifted = std::chrono::duration_cast(now - fingerTimeStamp[fingerIndex]); + if (fingerPressed[fingerIndex] && timeSinceLifted > std::chrono::seconds(5)) { // more than 5 seconds of inaction, reset finger state + ImGui::GetIO().AddMouseButtonEvent(fingerIndex, false); + fingerPressed[fingerIndex] = false; + assert(nFingers > 0); + --nFingers; + touchActive = true; + fingerUp = true; + singleFingerClicked = false; + releaseFingerIndex(fingerIndex); + fmt::print("WARNING: probably lost SDL_FINGERUP event -> reset inactive fingerID {} out of {} - timeSinceLifted {}\n", + fingerIndex, nFingers, timeSinceLifted); + } + } + } + + static void resetState() { + touchActive = false; + fingerDown = false; + fingerUp = false; + singleFingerClicked = false; + } + + inline static std::map[ImAxis_COUNT]> plotLimits; + inline static ImGuiID zoomablePlotInit = 0UL; + + // + inline static bool BeginZoomablePlot(const std::string &plotName, const ImVec2 &size, ImPlotFlags flags) { + assert((zoomablePlotInit == 0) && "mismatched BeginZoomablePlot <-> EndZoomablePlot"); + const ImGuiID ID = ImHashStr(plotName.c_str(), plotName.length()); + zoomablePlotInit = ID; + + if (auto limits = plotLimits.find(ID); limits != plotLimits.end()) { + for (int axisID = 0; axisID < ImAxis_COUNT; ++axisID) { + auto &[apply, range] = limits->second[axisID]; + if (!apply) { + continue; + } + ImPlot::SetNextAxisLimits(axisID, range.Min, range.Max, ImGuiCond_Always); + apply = false; + } + } + + if (ImPlot::BeginPlot(plotName.c_str(), size, flags)) { + return true; + } + zoomablePlotInit = 0UL; + return false; + } + + static void EndZoomablePlot() { + struct scppe_guard { + ~scppe_guard() { + zoomablePlotInit = 0UL; + ImPlot::EndPlot(); + } + } guard; + + if (!gestureActive) { + return; + } + + ImPlotPlot plot = *ImPlot::GetCurrentContext()->CurrentPlot; + const auto isPointInRect = [&plot](const ImVec2 &point) -> bool { + const ImVec2 min = plot.PlotRect.Min; + const ImVec2 max = plot.PlotRect.Max; + return point.x >= min.x && point.x <= max.x && point.y >= min.y && point.y <= max.y; + }; + + if (!isPointInRect(gestureCentre)) { + return; + } + + const ImVec2 initialDist = fingerPosDown[0] - fingerPosDown[1]; + const ImVec2 currDist = fingerPosDiff[0] - fingerPosDiff[1]; + const ImVec2 zoomFactor = { 1.0f - currDist.x / initialDist.x, 1.0f - currDist.y / initialDist.y }; + if (std::abs(zoomFactor.x - 1.f) < 0.001) { + return; + } + const auto &app = DigitizerUi::App::instance(); + if (!gestureZoomActive) { + gestureZoomActive = true; + ImGui::GetIO().AddMouseButtonEvent(0, false); + if (app.touchDiagnostics) { + fmt::print("gesture: start two finger zoom - centre ({},{})\n", gestureCentreUp.x, gestureCentreUp.y); + } + } + + auto processAxis = [zoomFactor](const ImPlotAxis &axis, ImPlotRange &rangeToUpdate) { + // this assumes you're between ImPlot::BeginPlot() and ImPlot::EndPlot() + const ImPlotRect currLimits = ImPlot::GetPlotLimits(); + + const ImPlotRange currentRange = axis.Vertical ? currLimits.Y : currLimits.X; + const double minVal = (axis.Flags & ImPlotAxisFlags_LockMin) ? axis.Range.Min : currentRange.Min; + const double maxVal = (axis.Flags & ImPlotAxisFlags_LockMax) ? axis.Range.Max : currentRange.Max; + const double totalRange = maxVal - minVal; // initial range + const double minRange = totalRange * 0.001; // here, 0.001 ensures it's always 0.1% of the initial range + + const double centre = (maxVal + minVal) * 0.5; + double halfRange = (maxVal - minVal) * 0.5 * (axis.Vertical ? zoomFactor.y : zoomFactor.x); + + // ensure halfRange is not too small or too large + if (std::abs(halfRange) < minRange * 0.5) { + halfRange = (halfRange < 0 ? -1 : 1) * minRange * 0.5; + } else if (std::abs(halfRange) > totalRange * 0.5) { + halfRange = (halfRange < 0 ? -1 : 1) * totalRange * 0.5; + } + + rangeToUpdate.Min = ((axis.Flags & ImPlotAxisFlags_LockMin) || axis.FitThisFrame) ? minVal : (centre - halfRange); + rangeToUpdate.Max = ((axis.Flags & ImPlotAxisFlags_LockMax) || axis.FitThisFrame) ? maxVal : (centre + halfRange); + }; + + // retrieve or create the plot limit entry and apply the new limits + auto &limitsEntry = plotLimits[zoomablePlotInit]; + for (std::size_t axisID = 0UL; axisID < ImAxis_COUNT; ++axisID) { + auto &axis = plot.Axes[axisID]; + if (!axis.Enabled) { + limitsEntry[axisID].first = false; + continue; + } + ImPlotRange range; + processAxis(axis, range); + limitsEntry[axisID].first = true; + limitsEntry[axisID].second = range; + } + } + + static void applyToImGui() { + const auto &app = DigitizerUi::App::instance(); + const auto &io = ImGui::GetIO(); + + if (app.touchDiagnostics || true) { + drawFingerPositions(); + } + + if (!touchActive) { + return; + } + + resetState(); + } +}; + +} // namespace fair + +#endif // OPENDIGITIZER_TOUCHHANDLER_HPP