From 1c7195a74806b5adff60cbeb98637262b7578f8c Mon Sep 17 00:00:00 2001 From: John McCardle Date: Sun, 6 Jul 2025 12:11:13 -0400 Subject: [PATCH] fix: improve click handling with proper z-order and coordinate transforms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UIFrame: Fix coordinate transformation (subtract parent pos, not add) - UIFrame: Check children in reverse order (highest z-index first) - UIFrame: Skip invisible elements entirely - PyScene: Sort elements by z-index before checking clicks - PyScene: Stop at first element that handles the click - UIGrid: Implement entity click detection with grid coordinate transform - UIGrid: Check entities in reverse order, return sprite as target Click events now correctly respect z-order (top elements get priority), handle coordinate transforms for nested frames, and support clicking on grid entities. Elements without click handlers are transparent to clicks, allowing elements below to receive them. Note: Click testing requires non-headless mode due to PyScene limitation. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/PyScene.cpp | 31 ++++++++++---------------- src/UIFrame.cpp | 33 ++++++++++++++++++--------- src/UIGrid.cpp | 59 +++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 90 insertions(+), 33 deletions(-) diff --git a/src/PyScene.cpp b/src/PyScene.cpp index c5ae5d6..a05a395 100644 --- a/src/PyScene.cpp +++ b/src/PyScene.cpp @@ -29,26 +29,19 @@ void PyScene::do_mouse_input(std::string button, std::string type) auto unscaledmousepos = sf::Mouse::getPosition(game->getWindow()); auto mousepos = game->getWindow().mapPixelToCoords(unscaledmousepos); - UIDrawable* target; - for (auto d: *ui_elements) - { - target = d->click_at(sf::Vector2f(mousepos)); - if (target) - { - /* - PyObject* args = Py_BuildValue("(iiss)", (int)mousepos.x, (int)mousepos.y, button.c_str(), type.c_str()); - PyObject* retval = PyObject_Call(target->click_callable, args, NULL); - if (!retval) - { - std::cout << "click_callable has raised an exception. It's going to STDERR and being dropped:" << std::endl; - PyErr_Print(); - PyErr_Clear(); - } else if (retval != Py_None) - { - std::cout << "click_callable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl; - } - */ + + // Create a sorted copy by z-index (highest first) + std::vector> sorted_elements(*ui_elements); + std::sort(sorted_elements.begin(), sorted_elements.end(), + [](const auto& a, const auto& b) { return a->z_index > b->z_index; }); + + // Check elements in z-order (top to bottom) + for (const auto& element : sorted_elements) { + if (!element->visible) continue; + + if (auto target = element->click_at(sf::Vector2f(mousepos))) { target->click_callable->call(mousepos, button, type); + return; // Stop after first handler } } } diff --git a/src/UIFrame.cpp b/src/UIFrame.cpp index a46e31b..7a3c842 100644 --- a/src/UIFrame.cpp +++ b/src/UIFrame.cpp @@ -11,18 +11,31 @@ UIDrawable* UIFrame::click_at(sf::Vector2f point) { - for (auto e: *children) - { - auto p = e->click_at(point + box.getPosition()); - if (p) - return p; + // Check bounds first (optimization) + float x = box.getPosition().x, y = box.getPosition().y, w = box.getSize().x, h = box.getSize().y; + if (point.x < x || point.y < y || point.x >= x+w || point.y >= y+h) { + return nullptr; } - if (click_callable) - { - float x = box.getPosition().x, y = box.getPosition().y, w = box.getSize().x, h = box.getSize().y; - if (point.x > x && point.y > y && point.x < x+w && point.y < y+h) return this; + + // Transform to local coordinates for children + sf::Vector2f localPoint = point - box.getPosition(); + + // Check children in reverse order (top to bottom, highest z-index first) + for (auto it = children->rbegin(); it != children->rend(); ++it) { + auto& child = *it; + if (!child->visible) continue; + + if (auto target = child->click_at(localPoint)) { + return target; + } } - return NULL; + + // No child handled it, check if we have a handler + if (click_callable) { + return this; + } + + return nullptr; } UIFrame::UIFrame() diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index 1cdad6c..7a592e2 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -265,11 +265,62 @@ std::shared_ptr UIGrid::getTexture() UIDrawable* UIGrid::click_at(sf::Vector2f point) { - if (click_callable) - { - if(box.getGlobalBounds().contains(point)) return this; + // Check grid bounds first + if (!box.getGlobalBounds().contains(point)) { + return nullptr; } - return NULL; + + // Transform to local coordinates + sf::Vector2f localPoint = point - box.getPosition(); + + // Get cell dimensions + int cell_width = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH; + int cell_height = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT; + + // Calculate visible area parameters (from render function) + float center_x_sq = center_x / cell_width; + float center_y_sq = center_y / cell_height; + float width_sq = box.getSize().x / (cell_width * zoom); + float height_sq = box.getSize().y / (cell_height * zoom); + + int left_spritepixels = center_x - (box.getSize().x / 2.0 / zoom); + int top_spritepixels = center_y - (box.getSize().y / 2.0 / zoom); + + // Convert click position to grid coordinates + float grid_x = (localPoint.x / zoom + left_spritepixels) / cell_width; + float grid_y = (localPoint.y / zoom + top_spritepixels) / cell_height; + + // Check entities in reverse order (assuming they should be checked top to bottom) + // Note: entities list is not sorted by z-index currently, but we iterate in reverse + // to match the render order assumption + if (entities) { + for (auto it = entities->rbegin(); it != entities->rend(); ++it) { + auto& entity = *it; + if (!entity || !entity->sprite.visible) continue; + + // Check if click is within entity's grid cell + // Entities occupy a 1x1 grid cell centered on their position + float dx = grid_x - entity->position.x; + float dy = grid_y - entity->position.y; + + if (dx >= -0.5f && dx < 0.5f && dy >= -0.5f && dy < 0.5f) { + // Click is within the entity's cell + // Check if entity sprite has a click handler + // For now, we return the entity's sprite as the click target + // Note: UIEntity doesn't derive from UIDrawable, so we check its sprite + if (entity->sprite.click_callable) { + return &entity->sprite; + } + } + } + } + + // No entity handled it, check if grid itself has handler + if (click_callable) { + return this; + } + + return nullptr; }