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",
"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"
}

View File

@ -369,33 +369,56 @@ namespace ZL
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(mx, my);
handleDown(static_cast<int64_t>(event.tfinger.fingerId), mx, my);
}
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(mx, my);
handleUp(static_cast<int64_t>(event.tfinger.fingerId), mx, my);
}
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(mx, my);
handleMotion(static_cast<int64_t>(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<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) {
// Преобразуем экранные пиксели в проекционные единицы
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);
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<int>((float)event.motion.x / Environment::width * Environment::projectionWidth);
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) {
@ -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::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.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);
}
}
}

View File

@ -13,6 +13,7 @@
#include <vector>
#include <string>
#include <memory>
#include <cstdint>
#include <render/TextRenderer.h>
#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;

View File

@ -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);

View File

@ -769,6 +769,16 @@ namespace ZL {
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,
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<std::shared_ptr<UiButton>> 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) {

View File

@ -10,6 +10,7 @@
#include <memory>
#include <functional>
#include <map>
#include <cstdint>
namespace ZL {
@ -93,6 +94,7 @@ namespace ZL {
VertexRenderStruct mesh;
std::function<void(const std::string&)> onClick;
std::function<void(const std::string&)> 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<UiNode> 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<UiButton> findButton(const std::string& name);
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,
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::pair<std::string, std::string>, std::function<void()>> animCallbacks; // key: (nodeName, animName)
std::shared_ptr<UiButton> pressedButton;
std::shared_ptr<UiSlider> pressedSlider;
// Per-finger tracking for multi-touch support
std::map<int64_t, std::shared_ptr<UiButton>> pressedButtons;
std::map<int64_t, std::shared_ptr<UiSlider>> pressedSliders;
std::shared_ptr<UiTextField> focusedTextField;
struct MenuState {
@ -332,8 +352,8 @@ namespace ZL {
std::vector<std::shared_ptr<UiSlider>> sliders;
std::vector<std::shared_ptr<UiTextField>> textFields;
std::vector<std::shared_ptr<UiStaticImage>> staticImages;
std::shared_ptr<UiButton> pressedButton;
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::string path;
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 для настройки проекции.
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);