From 4a542fd6c8f45bd1ce786fda89cb440fd04bb7db Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Sat, 7 Mar 2026 22:41:25 +0300 Subject: [PATCH] Added multi-touch, added on press down --- resources/config/ui.json | 4 +- src/Game.cpp | 78 +++++++++++++++++++++++---------------- src/Game.h | 7 ++-- src/MenuManager.cpp | 8 ++-- src/UiManager.cpp | 79 +++++++++++++++++++++++++--------------- src/UiManager.h | 36 ++++++++++++++---- src/main.cpp | 4 ++ 7 files changed, 137 insertions(+), 79 deletions(-) diff --git a/resources/config/ui.json b/resources/config/ui.json index 06e895a..b7971a7 100644 --- a/resources/config/ui.json +++ b/resources/config/ui.json @@ -31,7 +31,7 @@ "vertical_gravity": "bottom", "textures": { "normal": "resources/fire.png", - "hover": "resources/fire2.png", + "hover": "resources/fire.png", "pressed": "resources/fire2.png", "disabled": "resources/fire_disabled.png" } @@ -47,7 +47,7 @@ "vertical_gravity": "bottom", "textures": { "normal": "resources/fire.png", - "hover": "resources/fire2.png", + "hover": "resources/fire.png", "pressed": "resources/fire2.png", "disabled": "resources/fire_disabled.png" } diff --git a/src/Game.cpp b/src/Game.cpp index a7fa628..9a1e24f 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -369,33 +369,56 @@ namespace ZL if (event.type == SDL_FINGERDOWN) { int mx = static_cast(event.tfinger.x * Environment::projectionWidth); int my = static_cast(event.tfinger.y * Environment::projectionHeight); - handleDown(mx, my); + handleDown(static_cast(event.tfinger.fingerId), mx, my); } else if (event.type == SDL_FINGERUP) { int mx = static_cast(event.tfinger.x * Environment::projectionWidth); int my = static_cast(event.tfinger.y * Environment::projectionHeight); - handleUp(mx, my); + handleUp(static_cast(event.tfinger.fingerId), mx, my); } else if (event.type == SDL_FINGERMOTION) { int mx = static_cast(event.tfinger.x * Environment::projectionWidth); int my = static_cast(event.tfinger.y * Environment::projectionHeight); - handleMotion(mx, my); + handleMotion(static_cast(event.tfinger.fingerId), mx, my); } #else - + // Emscripten on mobile browser: handle real touch events with per-finger IDs. + // SDL_HINT_TOUCH_MOUSE_EVENTS="0" is set in main.cpp so these don't + // also fire SDL_MOUSEBUTTONDOWN, preventing double-processing. +#ifdef EMSCRIPTEN + if (event.type == SDL_FINGERDOWN) { + int mx = static_cast(event.tfinger.x * Environment::projectionWidth); + int my = static_cast(event.tfinger.y * Environment::projectionHeight); + handleDown(static_cast(event.tfinger.fingerId), mx, my); + //std::cout << "Finger down: id=" << event.tfinger.fingerId << " x=" << mx << " y=" << my << std::endl; + } + else if (event.type == SDL_FINGERUP) { + int mx = static_cast(event.tfinger.x * Environment::projectionWidth); + int my = static_cast(event.tfinger.y * Environment::projectionHeight); + handleUp(static_cast(event.tfinger.fingerId), mx, my); + //std::cout << "Finger up: id=" << event.tfinger.fingerId << " x=" << mx << " y=" << my << std::endl; + } + else if (event.type == SDL_FINGERMOTION) { + int mx = static_cast(event.tfinger.x * Environment::projectionWidth); + int my = static_cast(event.tfinger.y * Environment::projectionHeight); + handleMotion(static_cast(event.tfinger.fingerId), mx, my); + //std::cout << "Finger motion: id=" << event.tfinger.fingerId << " x=" << mx << " y=" << my << std::endl; + } +#endif if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP) { // Преобразуем экранные пиксели в проекционные единицы int mx = static_cast((float)event.button.x / Environment::width * Environment::projectionWidth); int my = static_cast((float)event.button.y / Environment::height * Environment::projectionHeight); - if (event.type == SDL_MOUSEBUTTONDOWN) handleDown(mx, my); - else handleUp(mx, my); + if (event.type == SDL_MOUSEBUTTONDOWN) handleDown(ZL::UiManager::MOUSE_FINGER_ID, mx, my); + else handleUp(ZL::UiManager::MOUSE_FINGER_ID, mx, my); + //std::cout << "Mouse button " << (event.type == SDL_MOUSEBUTTONDOWN ? "down" : "up") << ": x=" << mx << " y=" << my << std::endl; } else if (event.type == SDL_MOUSEMOTION) { int mx = static_cast((float)event.motion.x / Environment::width * Environment::projectionWidth); int my = static_cast((float)event.motion.y / Environment::height * Environment::projectionHeight); - handleMotion(mx, my); + handleMotion(ZL::UiManager::MOUSE_FINGER_ID, mx, my); } /*if (event.type == SDL_MOUSEBUTTONDOWN) { @@ -507,62 +530,53 @@ namespace ZL } } - void Game::handleDown(int mx, int my) + void Game::handleDown(int64_t fingerId, int mx, int my) { int uiX = mx; int uiY = Environment::projectionHeight - my; - menuManager.uiManager.onMouseDown(uiX, uiY); + menuManager.uiManager.onTouchDown(fingerId, uiX, uiY); - bool uiHandled = false; - - for (const auto& button : menuManager.uiManager.findButton("") ? std::vector>{} : std::vector>{}) { - (void)button; - } - - auto pressedSlider = [&]() -> std::shared_ptr { - for (const auto& slider : menuManager.uiManager.findSlider("") ? std::vector>{} : std::vector>{}) { - (void)slider; - } - return nullptr; - }(); - - if (!menuManager.uiManager.isUiInteraction()) { + if (!menuManager.uiManager.isUiInteractionForFinger(fingerId)) { if (menuManager.shouldRenderSpace()) { space.handleDown(mx, my); } } - } - void Game::handleUp(int mx, int my) + void Game::handleUp(int64_t fingerId, int mx, int my) { int uiX = mx; int uiY = Environment::projectionHeight - my; - menuManager.uiManager.onMouseUp(uiX, uiY); + // Check BEFORE onTouchUp erases the finger from the map. + // If this finger started on a UI element, don't notify space — + // otherwise space would think the ship-control finger was released. + bool wasUiInteraction = menuManager.uiManager.isUiInteractionForFinger(fingerId); + menuManager.uiManager.onTouchUp(fingerId, uiX, uiY); - if (!menuManager.uiManager.isUiInteraction()) { + if (!wasUiInteraction) { if (menuManager.shouldRenderSpace()) { space.handleUp(mx, my); } } - } - void Game::handleMotion(int mx, int my) + void Game::handleMotion(int64_t fingerId, int mx, int my) { int uiX = mx; int uiY = Environment::projectionHeight - my; - menuManager.uiManager.onMouseMove(uiX, uiY); + // Check before onTouchMove so the "started on UI" state is preserved + // regardless of what onTouchMove does internally. + bool wasUiInteraction = menuManager.uiManager.isUiInteractionForFinger(fingerId); + menuManager.uiManager.onTouchMove(fingerId, uiX, uiY); - if (!menuManager.uiManager.isUiInteraction()) { + if (!wasUiInteraction) { if (menuManager.shouldRenderSpace()) { space.handleMotion(mx, my); } } - } diff --git a/src/Game.h b/src/Game.h index 083f007..7fd6d36 100644 --- a/src/Game.h +++ b/src/Game.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include "MenuManager.h" #include "Space.h" @@ -50,9 +51,9 @@ namespace ZL { void drawUI(); void drawUnderMainMenu(); void drawLoading(); - void handleDown(int mx, int my); - void handleUp(int mx, int my); - void handleMotion(int mx, int my); + void handleDown(int64_t fingerId, int mx, int my); + void handleUp(int64_t fingerId, int mx, int my); + void handleMotion(int64_t fingerId, int mx, int my); #ifdef EMSCRIPTEN static Game* s_instance; diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index 8b63165..e862e49 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -143,18 +143,18 @@ namespace ZL { uiManager.startAnimationOnNode("backgroundNode", "bgScroll"); - uiManager.setButtonCallback("shootButton", [this](const std::string&) { + uiManager.setButtonPressCallback("shootButton", [this](const std::string&) { if (onFirePressed) onFirePressed(); }); - uiManager.setButtonCallback("shootButton2", [this](const std::string&) { + uiManager.setButtonPressCallback("shootButton2", [this](const std::string&) { if (onFirePressed) onFirePressed(); }); - uiManager.setButtonCallback("plusButton", [this](const std::string&) { + uiManager.setButtonPressCallback("plusButton", [this](const std::string&) { int newVel = Environment::shipState.selectedVelocity + 1; if (newVel > 4) newVel = 4; if (onVelocityChanged) onVelocityChanged(newVel); }); - uiManager.setButtonCallback("minusButton", [this](const std::string&) { + uiManager.setButtonPressCallback("minusButton", [this](const std::string&) { int newVel = Environment::shipState.selectedVelocity - 1; if (newVel < 0) newVel = 0; if (onVelocityChanged) onVelocityChanged(newVel); diff --git a/src/UiManager.cpp b/src/UiManager.cpp index 4471248..b04a861 100644 --- a/src/UiManager.cpp +++ b/src/UiManager.cpp @@ -769,6 +769,16 @@ namespace ZL { return true; } + bool UiManager::setButtonPressCallback(const std::string& name, std::function cb) { + auto b = findButton(name); + if (!b) { + std::cerr << "UiManager: setButtonPressCallback failed, button not found: " << name << std::endl; + return false; + } + b->onPress = std::move(cb); + return true; + } + bool UiManager::addSlider(const std::string& name, const UiRect& rect, Renderer& renderer, const std::string& zipFile, const std::string& trackPath, const std::string& knobPath, float initialValue, bool vertical) { @@ -855,8 +865,8 @@ namespace ZL { prev.sliders = sliders; prev.textFields = textFields; prev.staticImages = staticImages; - prev.pressedButton = pressedButton; - prev.pressedSlider = pressedSlider; + prev.pressedButtons = pressedButtons; + prev.pressedSliders = pressedSliders; prev.focusedTextField = focusedTextField; prev.path = ""; @@ -906,8 +916,8 @@ namespace ZL { sliders = s.sliders; textFields = s.textFields; staticImages = s.staticImages; - pressedButton = s.pressedButton; - pressedSlider = s.pressedSlider; + pressedButtons = s.pressedButtons; + pressedSliders = s.pressedSliders; focusedTextField = s.focusedTextField; animCallbacks = s.animCallbacks; @@ -1122,21 +1132,25 @@ namespace ZL { } } - void UiManager::onMouseMove(int x, int y) { - for (auto& b : buttons) { - if (b->state != ButtonState::Disabled) - { - if (b->rect.containsConsideringBorder((float)x, (float)y, b->border)) { - if (b->state != ButtonState::Pressed) b->state = ButtonState::Hover; - } - else { - if (b->state != ButtonState::Pressed) b->state = ButtonState::Normal; + void UiManager::onTouchMove(int64_t fingerId, int x, int y) { + // Hover state updates only make sense for mouse (single pointer) + if (fingerId == MOUSE_FINGER_ID) { + for (auto& b : buttons) { + if (b->state != ButtonState::Disabled) + { + if (b->rect.containsConsideringBorder((float)x, (float)y, b->border)) { + if (b->state != ButtonState::Pressed) b->state = ButtonState::Hover; + } + else { + if (b->state != ButtonState::Pressed) b->state = ButtonState::Normal; + } } } } - if (pressedSlider) { - auto s = pressedSlider; + auto it = pressedSliders.find(fingerId); + if (it != pressedSliders.end()) { + auto s = it->second; float t; if (s->vertical) { t = (y - s->rect.y) / s->rect.h; @@ -1153,20 +1167,22 @@ namespace ZL { } - void UiManager::onMouseDown(int x, int y) { + void UiManager::onTouchDown(int64_t fingerId, int x, int y) { for (auto& b : buttons) { if (b->state != ButtonState::Disabled) { if (b->rect.containsConsideringBorder((float)x, (float)y, b->border)) { b->state = ButtonState::Pressed; - pressedButton = b; + pressedButtons[fingerId] = b; + if (b->onPress) b->onPress(b->name); + break; // a single finger can only press one button } } } for (auto& s : sliders) { if (s->rect.contains((float)x, (float)y)) { - pressedSlider = s; + pressedSliders[fingerId] = s; float t; if (s->vertical) { t = (y - s->rect.y) / s->rect.h; @@ -1194,29 +1210,32 @@ namespace ZL { } } - void UiManager::onMouseUp(int x, int y) { + void UiManager::onTouchUp(int64_t fingerId, int x, int y) { std::vector> clicked; - for (auto& b : buttons) { - if (!b) continue; - bool contains = b->rect.contains((float)x, (float)y); - - if (b->state == ButtonState::Pressed) { - if (contains && pressedButton == b) { - clicked.push_back(b); + auto btnIt = pressedButtons.find(fingerId); + if (btnIt != pressedButtons.end()) { + auto b = btnIt->second; + if (b) { + bool contains = b->rect.contains((float)x, (float)y); + if (b->state == ButtonState::Pressed) { + if (contains) { + clicked.push_back(b); + } + // On mouse: leave Hover if still over button. On touch: always Normal. + b->state = (contains && fingerId == MOUSE_FINGER_ID) ? ButtonState::Hover : ButtonState::Normal; } - b->state = contains ? ButtonState::Hover : ButtonState::Normal; } + pressedButtons.erase(btnIt); } + pressedSliders.erase(fingerId); + for (auto& b : clicked) { if (b->onClick) { b->onClick(b->name); } } - - pressedButton.reset(); - if (pressedSlider) pressedSlider.reset(); } void UiManager::onKeyPress(unsigned char key) { diff --git a/src/UiManager.h b/src/UiManager.h index ccc2986..e8f8c69 100644 --- a/src/UiManager.h +++ b/src/UiManager.h @@ -10,6 +10,7 @@ #include #include #include +#include namespace ZL { @@ -93,6 +94,7 @@ namespace ZL { VertexRenderStruct mesh; std::function onClick; + std::function onPress; // fires on touch/mouse down // animation runtime float animOffsetX = 0.0f; @@ -224,19 +226,35 @@ namespace ZL { public: UiManager() = default; + // Sentinel finger ID used for mouse events on desktop/web + static constexpr int64_t MOUSE_FINGER_ID = -1LL; + void replaceRoot(std::shared_ptr newRoot); void loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = ""); void draw(Renderer& renderer); - void onMouseMove(int x, int y); - void onMouseDown(int x, int y); - void onMouseUp(int x, int y); + // Multi-touch methods (used directly for touch events with per-finger IDs) + void onTouchDown(int64_t fingerId, int x, int y); + void onTouchUp(int64_t fingerId, int x, int y); + void onTouchMove(int64_t fingerId, int x, int y); + + // Mouse convenience wrappers (delegate to touch with MOUSE_FINGER_ID) + void onMouseMove(int x, int y) { onTouchMove(MOUSE_FINGER_ID, x, y); } + void onMouseDown(int x, int y) { onTouchDown(MOUSE_FINGER_ID, x, y); } + void onMouseUp(int x, int y) { onTouchUp(MOUSE_FINGER_ID, x, y); } + void onKeyPress(unsigned char key); void onKeyBackspace(); + // Returns true if any finger is currently interacting with UI bool isUiInteraction() const { - return pressedButton != nullptr || pressedSlider != nullptr || focusedTextField != nullptr; + return !pressedButtons.empty() || !pressedSliders.empty() || focusedTextField != nullptr; + } + + // Returns true if this specific finger is currently interacting with UI + bool isUiInteractionForFinger(int64_t fingerId) const { + return pressedButtons.count(fingerId) > 0 || pressedSliders.count(fingerId) > 0 || focusedTextField != nullptr; } void stopAllAnimations() { @@ -255,6 +273,7 @@ namespace ZL { std::shared_ptr findButton(const std::string& name); bool setButtonCallback(const std::string& name, std::function cb); + bool setButtonPressCallback(const std::string& name, std::function cb); bool addSlider(const std::string& name, const UiRect& rect, Renderer& renderer, const std::string& zipFile, const std::string& trackPath, const std::string& knobPath, float initialValue = 0.0f, bool vertical = true); @@ -322,8 +341,9 @@ namespace ZL { std::map, std::vector> nodeActiveAnims; std::map, std::function> animCallbacks; // key: (nodeName, animName) - std::shared_ptr pressedButton; - std::shared_ptr pressedSlider; + // Per-finger tracking for multi-touch support + std::map> pressedButtons; + std::map> pressedSliders; std::shared_ptr focusedTextField; struct MenuState { @@ -332,8 +352,8 @@ namespace ZL { std::vector> sliders; std::vector> textFields; std::vector> staticImages; - std::shared_ptr pressedButton; - std::shared_ptr pressedSlider; + std::map> pressedButtons; + std::map> pressedSliders; std::shared_ptr focusedTextField; std::string path; std::map, std::function> animCallbacks; diff --git a/src/main.cpp b/src/main.cpp index 94809bb..1258e80 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -127,7 +127,11 @@ int main(int argc, char* argv[]) { // канваса и отправит SDL_WINDOWEVENT_RESIZED для настройки проекции. applyResize(canvasW, canvasH); + // Prevent mouse clicks from generating fake SDL_FINGERDOWN events (desktop browser) SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0"); + // Prevent touch events from generating fake SDL_MOUSEBUTTONDOWN events (mobile browser), + // since we now handle SDL_FINGERDOWN directly for multi-touch support. + SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0"); emscripten_set_main_loop(MainLoop, 0, 1);