Added multi-touch, added on press down

This commit is contained in:
Vladislav Khorev 2026-03-07 22:41:25 +03:00
parent a06cb77a66
commit 4a542fd6c8
7 changed files with 137 additions and 79 deletions

View File

@ -31,7 +31,7 @@
"vertical_gravity": "bottom", "vertical_gravity": "bottom",
"textures": { "textures": {
"normal": "resources/fire.png", "normal": "resources/fire.png",
"hover": "resources/fire2.png", "hover": "resources/fire.png",
"pressed": "resources/fire2.png", "pressed": "resources/fire2.png",
"disabled": "resources/fire_disabled.png" "disabled": "resources/fire_disabled.png"
} }
@ -47,7 +47,7 @@
"vertical_gravity": "bottom", "vertical_gravity": "bottom",
"textures": { "textures": {
"normal": "resources/fire.png", "normal": "resources/fire.png",
"hover": "resources/fire2.png", "hover": "resources/fire.png",
"pressed": "resources/fire2.png", "pressed": "resources/fire2.png",
"disabled": "resources/fire_disabled.png" "disabled": "resources/fire_disabled.png"
} }

View File

@ -369,33 +369,56 @@ namespace ZL
if (event.type == SDL_FINGERDOWN) { if (event.type == SDL_FINGERDOWN) {
int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth); int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight); int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
handleDown(mx, my); handleDown(static_cast<int64_t>(event.tfinger.fingerId), mx, my);
} }
else if (event.type == SDL_FINGERUP) { else if (event.type == SDL_FINGERUP) {
int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth); int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight); int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
handleUp(mx, my); handleUp(static_cast<int64_t>(event.tfinger.fingerId), mx, my);
} }
else if (event.type == SDL_FINGERMOTION) { else if (event.type == SDL_FINGERMOTION) {
int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth); int mx = static_cast<int>(event.tfinger.x * Environment::projectionWidth);
int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight); int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
handleMotion(mx, my); handleMotion(static_cast<int64_t>(event.tfinger.fingerId), mx, my);
} }
#else #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<int>(event.tfinger.x * Environment::projectionWidth);
int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
handleDown(static_cast<int64_t>(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<int>(event.tfinger.x * Environment::projectionWidth);
int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
handleUp(static_cast<int64_t>(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<int>(event.tfinger.x * Environment::projectionWidth);
int my = static_cast<int>(event.tfinger.y * Environment::projectionHeight);
handleMotion(static_cast<int64_t>(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) { if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP) {
// Преобразуем экранные пиксели в проекционные единицы // Преобразуем экранные пиксели в проекционные единицы
int mx = static_cast<int>((float)event.button.x / Environment::width * Environment::projectionWidth); int mx = static_cast<int>((float)event.button.x / Environment::width * Environment::projectionWidth);
int my = static_cast<int>((float)event.button.y / Environment::height * Environment::projectionHeight); int my = static_cast<int>((float)event.button.y / Environment::height * Environment::projectionHeight);
if (event.type == SDL_MOUSEBUTTONDOWN) handleDown(mx, my); if (event.type == SDL_MOUSEBUTTONDOWN) handleDown(ZL::UiManager::MOUSE_FINGER_ID, mx, my);
else handleUp(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) { else if (event.type == SDL_MOUSEMOTION) {
int mx = static_cast<int>((float)event.motion.x / Environment::width * Environment::projectionWidth); int mx = static_cast<int>((float)event.motion.x / Environment::width * Environment::projectionWidth);
int my = static_cast<int>((float)event.motion.y / Environment::height * Environment::projectionHeight); int my = static_cast<int>((float)event.motion.y / Environment::height * Environment::projectionHeight);
handleMotion(mx, my); handleMotion(ZL::UiManager::MOUSE_FINGER_ID, mx, my);
} }
/*if (event.type == SDL_MOUSEBUTTONDOWN) { /*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 uiX = mx;
int uiY = Environment::projectionHeight - my; int uiY = Environment::projectionHeight - my;
menuManager.uiManager.onMouseDown(uiX, uiY); menuManager.uiManager.onTouchDown(fingerId, uiX, uiY);
bool uiHandled = false; if (!menuManager.uiManager.isUiInteractionForFinger(fingerId)) {
for (const auto& button : menuManager.uiManager.findButton("") ? std::vector<std::shared_ptr<UiButton>>{} : std::vector<std::shared_ptr<UiButton>>{}) {
(void)button;
}
auto pressedSlider = [&]() -> std::shared_ptr<UiSlider> {
for (const auto& slider : menuManager.uiManager.findSlider("") ? std::vector<std::shared_ptr<UiSlider>>{} : std::vector<std::shared_ptr<UiSlider>>{}) {
(void)slider;
}
return nullptr;
}();
if (!menuManager.uiManager.isUiInteraction()) {
if (menuManager.shouldRenderSpace()) { if (menuManager.shouldRenderSpace()) {
space.handleDown(mx, my); 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 uiX = mx;
int uiY = Environment::projectionHeight - my; 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()) { if (menuManager.shouldRenderSpace()) {
space.handleUp(mx, my); 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 uiX = mx;
int uiY = Environment::projectionHeight - my; 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()) { if (menuManager.shouldRenderSpace()) {
space.handleMotion(mx, my); space.handleMotion(mx, my);
} }
} }
} }

View File

@ -13,6 +13,7 @@
#include <vector> #include <vector>
#include <string> #include <string>
#include <memory> #include <memory>
#include <cstdint>
#include <render/TextRenderer.h> #include <render/TextRenderer.h>
#include "MenuManager.h" #include "MenuManager.h"
#include "Space.h" #include "Space.h"
@ -50,9 +51,9 @@ namespace ZL {
void drawUI(); void drawUI();
void drawUnderMainMenu(); void drawUnderMainMenu();
void drawLoading(); void drawLoading();
void handleDown(int mx, int my); void handleDown(int64_t fingerId, int mx, int my);
void handleUp(int mx, int my); void handleUp(int64_t fingerId, int mx, int my);
void handleMotion(int mx, int my); void handleMotion(int64_t fingerId, int mx, int my);
#ifdef EMSCRIPTEN #ifdef EMSCRIPTEN
static Game* s_instance; static Game* s_instance;

View File

@ -143,18 +143,18 @@ namespace ZL {
uiManager.startAnimationOnNode("backgroundNode", "bgScroll"); uiManager.startAnimationOnNode("backgroundNode", "bgScroll");
uiManager.setButtonCallback("shootButton", [this](const std::string&) { uiManager.setButtonPressCallback("shootButton", [this](const std::string&) {
if (onFirePressed) onFirePressed(); if (onFirePressed) onFirePressed();
}); });
uiManager.setButtonCallback("shootButton2", [this](const std::string&) { uiManager.setButtonPressCallback("shootButton2", [this](const std::string&) {
if (onFirePressed) onFirePressed(); if (onFirePressed) onFirePressed();
}); });
uiManager.setButtonCallback("plusButton", [this](const std::string&) { uiManager.setButtonPressCallback("plusButton", [this](const std::string&) {
int newVel = Environment::shipState.selectedVelocity + 1; int newVel = Environment::shipState.selectedVelocity + 1;
if (newVel > 4) newVel = 4; if (newVel > 4) newVel = 4;
if (onVelocityChanged) onVelocityChanged(newVel); if (onVelocityChanged) onVelocityChanged(newVel);
}); });
uiManager.setButtonCallback("minusButton", [this](const std::string&) { uiManager.setButtonPressCallback("minusButton", [this](const std::string&) {
int newVel = Environment::shipState.selectedVelocity - 1; int newVel = Environment::shipState.selectedVelocity - 1;
if (newVel < 0) newVel = 0; if (newVel < 0) newVel = 0;
if (onVelocityChanged) onVelocityChanged(newVel); if (onVelocityChanged) onVelocityChanged(newVel);

View File

@ -769,6 +769,16 @@ namespace ZL {
return true; return true;
} }
bool UiManager::setButtonPressCallback(const std::string& name, std::function<void(const std::string&)> 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, 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) { const std::string& trackPath, const std::string& knobPath, float initialValue, bool vertical) {
@ -855,8 +865,8 @@ namespace ZL {
prev.sliders = sliders; prev.sliders = sliders;
prev.textFields = textFields; prev.textFields = textFields;
prev.staticImages = staticImages; prev.staticImages = staticImages;
prev.pressedButton = pressedButton; prev.pressedButtons = pressedButtons;
prev.pressedSlider = pressedSlider; prev.pressedSliders = pressedSliders;
prev.focusedTextField = focusedTextField; prev.focusedTextField = focusedTextField;
prev.path = ""; prev.path = "";
@ -906,8 +916,8 @@ namespace ZL {
sliders = s.sliders; sliders = s.sliders;
textFields = s.textFields; textFields = s.textFields;
staticImages = s.staticImages; staticImages = s.staticImages;
pressedButton = s.pressedButton; pressedButtons = s.pressedButtons;
pressedSlider = s.pressedSlider; pressedSliders = s.pressedSliders;
focusedTextField = s.focusedTextField; focusedTextField = s.focusedTextField;
animCallbacks = s.animCallbacks; animCallbacks = s.animCallbacks;
@ -1122,21 +1132,25 @@ namespace ZL {
} }
} }
void UiManager::onMouseMove(int x, int y) { void UiManager::onTouchMove(int64_t fingerId, int x, int y) {
for (auto& b : buttons) { // Hover state updates only make sense for mouse (single pointer)
if (b->state != ButtonState::Disabled) if (fingerId == MOUSE_FINGER_ID) {
{ for (auto& b : buttons) {
if (b->rect.containsConsideringBorder((float)x, (float)y, b->border)) { if (b->state != ButtonState::Disabled)
if (b->state != ButtonState::Pressed) b->state = ButtonState::Hover; {
} if (b->rect.containsConsideringBorder((float)x, (float)y, b->border)) {
else { if (b->state != ButtonState::Pressed) b->state = ButtonState::Hover;
if (b->state != ButtonState::Pressed) b->state = ButtonState::Normal; }
else {
if (b->state != ButtonState::Pressed) b->state = ButtonState::Normal;
}
} }
} }
} }
if (pressedSlider) { auto it = pressedSliders.find(fingerId);
auto s = pressedSlider; if (it != pressedSliders.end()) {
auto s = it->second;
float t; float t;
if (s->vertical) { if (s->vertical) {
t = (y - s->rect.y) / s->rect.h; 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) { for (auto& b : buttons) {
if (b->state != ButtonState::Disabled) if (b->state != ButtonState::Disabled)
{ {
if (b->rect.containsConsideringBorder((float)x, (float)y, b->border)) { if (b->rect.containsConsideringBorder((float)x, (float)y, b->border)) {
b->state = ButtonState::Pressed; 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) { for (auto& s : sliders) {
if (s->rect.contains((float)x, (float)y)) { if (s->rect.contains((float)x, (float)y)) {
pressedSlider = s; pressedSliders[fingerId] = s;
float t; float t;
if (s->vertical) { if (s->vertical) {
t = (y - s->rect.y) / s->rect.h; 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<std::shared_ptr<UiButton>> clicked; std::vector<std::shared_ptr<UiButton>> clicked;
for (auto& b : buttons) { auto btnIt = pressedButtons.find(fingerId);
if (!b) continue; if (btnIt != pressedButtons.end()) {
bool contains = b->rect.contains((float)x, (float)y); auto b = btnIt->second;
if (b) {
if (b->state == ButtonState::Pressed) { bool contains = b->rect.contains((float)x, (float)y);
if (contains && pressedButton == b) { if (b->state == ButtonState::Pressed) {
clicked.push_back(b); 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) { for (auto& b : clicked) {
if (b->onClick) { if (b->onClick) {
b->onClick(b->name); b->onClick(b->name);
} }
} }
pressedButton.reset();
if (pressedSlider) pressedSlider.reset();
} }
void UiManager::onKeyPress(unsigned char key) { void UiManager::onKeyPress(unsigned char key) {

View File

@ -10,6 +10,7 @@
#include <memory> #include <memory>
#include <functional> #include <functional>
#include <map> #include <map>
#include <cstdint>
namespace ZL { namespace ZL {
@ -93,6 +94,7 @@ namespace ZL {
VertexRenderStruct mesh; VertexRenderStruct mesh;
std::function<void(const std::string&)> onClick; std::function<void(const std::string&)> onClick;
std::function<void(const std::string&)> onPress; // fires on touch/mouse down
// animation runtime // animation runtime
float animOffsetX = 0.0f; float animOffsetX = 0.0f;
@ -224,19 +226,35 @@ namespace ZL {
public: public:
UiManager() = default; 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<UiNode> newRoot); void replaceRoot(std::shared_ptr<UiNode> newRoot);
void loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = ""); void loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = "");
void draw(Renderer& renderer); void draw(Renderer& renderer);
void onMouseMove(int x, int y); // Multi-touch methods (used directly for touch events with per-finger IDs)
void onMouseDown(int x, int y); void onTouchDown(int64_t fingerId, int x, int y);
void onMouseUp(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 onKeyPress(unsigned char key);
void onKeyBackspace(); void onKeyBackspace();
// Returns true if any finger is currently interacting with UI
bool isUiInteraction() const { 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() { void stopAllAnimations() {
@ -255,6 +273,7 @@ namespace ZL {
std::shared_ptr<UiButton> findButton(const std::string& name); std::shared_ptr<UiButton> findButton(const std::string& name);
bool setButtonCallback(const std::string& name, std::function<void(const std::string&)> cb); bool setButtonCallback(const std::string& name, std::function<void(const std::string&)> cb);
bool setButtonPressCallback(const std::string& name, std::function<void(const std::string&)> cb);
bool addSlider(const std::string& name, const UiRect& rect, Renderer& renderer, const std::string& zipFile, 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); const std::string& trackPath, const std::string& knobPath, float initialValue = 0.0f, bool vertical = true);
@ -322,8 +341,9 @@ namespace ZL {
std::map<std::shared_ptr<UiNode>, std::vector<ActiveAnim>> nodeActiveAnims; std::map<std::shared_ptr<UiNode>, std::vector<ActiveAnim>> nodeActiveAnims;
std::map<std::pair<std::string, std::string>, std::function<void()>> animCallbacks; // key: (nodeName, animName) std::map<std::pair<std::string, std::string>, std::function<void()>> animCallbacks; // key: (nodeName, animName)
std::shared_ptr<UiButton> pressedButton; // Per-finger tracking for multi-touch support
std::shared_ptr<UiSlider> pressedSlider; std::map<int64_t, std::shared_ptr<UiButton>> pressedButtons;
std::map<int64_t, std::shared_ptr<UiSlider>> pressedSliders;
std::shared_ptr<UiTextField> focusedTextField; std::shared_ptr<UiTextField> focusedTextField;
struct MenuState { struct MenuState {
@ -332,8 +352,8 @@ namespace ZL {
std::vector<std::shared_ptr<UiSlider>> sliders; std::vector<std::shared_ptr<UiSlider>> sliders;
std::vector<std::shared_ptr<UiTextField>> textFields; std::vector<std::shared_ptr<UiTextField>> textFields;
std::vector<std::shared_ptr<UiStaticImage>> staticImages; std::vector<std::shared_ptr<UiStaticImage>> staticImages;
std::shared_ptr<UiButton> pressedButton; std::map<int64_t, std::shared_ptr<UiButton>> pressedButtons;
std::shared_ptr<UiSlider> pressedSlider; std::map<int64_t, std::shared_ptr<UiSlider>> pressedSliders;
std::shared_ptr<UiTextField> focusedTextField; std::shared_ptr<UiTextField> focusedTextField;
std::string path; std::string path;
std::map<std::pair<std::string, std::string>, std::function<void()>> animCallbacks; std::map<std::pair<std::string, std::string>, std::function<void()>> animCallbacks;

View File

@ -127,7 +127,11 @@ int main(int argc, char* argv[]) {
// канваса и отправит SDL_WINDOWEVENT_RESIZED для настройки проекции. // канваса и отправит SDL_WINDOWEVENT_RESIZED для настройки проекции.
applyResize(canvasW, canvasH); applyResize(canvasW, canvasH);
// Prevent mouse clicks from generating fake SDL_FINGERDOWN events (desktop browser)
SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0"); 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); emscripten_set_main_loop(MainLoop, 0, 1);