GroveEngine/tests/visual/test_single_button.cpp
StillHammer 5cef0e25b0 fix: UIModule button interaction + JsonDataNode array children support
- Fix JsonDataNode::getChildReadOnly() to handle JSON array access by numeric index
- Fix test_ui_showcase to use JSON array for children (matching test_single_button pattern)
- Add visual test files: test_single_button, test_ui_showcase, test_sprite_debug
- Clean up debug logging from SpritePass, SceneCollector, UIButton, BgfxDevice

The root cause was that UITree couldn't access array children in JSON layouts.
UIButton hover/click now works correctly in both test files.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-05 18:23:16 +07:00

222 lines
7.9 KiB
C++

/**
* Test: Single UIButton with interaction
* - Bouton rouge
* - Feedback visuel hover (orange)
* - Feedback visuel pressed (rouge foncé)
* - Logs des événements
*/
#include <SDL.h>
#include <SDL_syswm.h>
#include <iostream>
#include <memory>
#include "BgfxRendererModule.h"
#include "UIModule/UIModule.h"
#include <grove/JsonDataNode.h>
#include <grove/IntraIOManager.h>
#include <grove/IntraIO.h>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
using namespace grove;
int main(int argc, char* argv[]) {
spdlog::set_level(spdlog::level::info);
auto logger = spdlog::stdout_color_mt("ButtonTest");
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
std::cerr << "SDL_Init failed" << std::endl;
return 1;
}
SDL_Window* window = SDL_CreateWindow(
"Single Button Test",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
800, 600, SDL_WINDOW_SHOWN
);
SDL_SysWMinfo wmi;
SDL_VERSION(&wmi.version);
SDL_GetWindowWMInfo(window, &wmi);
// Create IIO instances
auto rendererIO = IntraIOManager::getInstance().createInstance("renderer");
auto uiIO = IntraIOManager::getInstance().createInstance("ui");
auto gameIO = IntraIOManager::getInstance().createInstance("game");
// Subscribe to UI events for logging
gameIO->subscribe("ui:hover");
gameIO->subscribe("ui:click");
gameIO->subscribe("ui:action");
// Initialize BgfxRenderer
auto renderer = std::make_unique<BgfxRendererModule>();
{
JsonDataNode config("config");
config.setDouble("nativeWindowHandle",
static_cast<double>(reinterpret_cast<uintptr_t>(wmi.info.win.window)));
config.setInt("windowWidth", 800);
config.setInt("windowHeight", 600);
renderer->setConfiguration(config, rendererIO.get(), nullptr);
}
// Initialize UIModule with ONE button - proper style structure
auto ui = std::make_unique<UIModule>();
{
JsonDataNode config("config");
config.setInt("windowWidth", 800);
config.setInt("windowHeight", 600);
/*
* COMMENT CA MARCHE:
*
* 1. UIModule charge le layout JSON et crée les widgets
* 2. Chaque frame:
* - UIModule reçoit les events input (mouse, keyboard) via IIO
* - UIModule update les widgets (hover detection, click handling)
* - UIModule appelle render() sur chaque widget
* - UIButton::render() publie un sprite via IIO topic "render:sprite"
* - BgfxRenderer reçoit le sprite et le dessine
*
* 3. Les styles du bouton:
* - normal: état par défaut
* - hover: quand la souris est dessus
* - pressed: quand on clique
* - disabled: quand enabled=false
*
* 4. Les événements publiés par UIModule:
* - ui:hover - quand on entre/sort d'un widget
* - ui:click - quand on clique
* - ui:action - quand un bouton avec onClick est cliqué
*/
nlohmann::json layoutJson = {
{"id", "root"},
{"type", "panel"},
{"x", 0}, {"y", 0},
{"width", 800}, {"height", 600},
{"style", {
{"bgColor", "0x1A1A2EFF"} // Fond sombre
}},
{"children", {
{
{"id", "btn_test"},
{"type", "button"},
{"x", 250},
{"y", 250},
{"width", 300},
{"height", 100},
{"text", "CLICK ME!"},
{"onClick", "test_action"},
{"style", {
{"fontSize", 24.0},
{"normal", {
{"bgColor", "0xE74C3CFF"}, // Rouge
{"textColor", "0xFFFFFFFF"}, // Blanc
{"borderRadius", 8.0}
}},
{"hover", {
{"bgColor", "0xF39C12FF"}, // Orange (hover)
{"textColor", "0xFFFFFFFF"},
{"borderRadius", 8.0}
}},
{"pressed", {
{"bgColor", "0xC0392BFF"}, // Rouge foncé (pressed)
{"textColor", "0xFFFFFFFF"},
{"borderRadius", 8.0}
}}
}}
}
}}
};
auto layoutNode = std::make_unique<JsonDataNode>("layout", layoutJson);
config.setChild("layout", std::move(layoutNode));
ui->setConfiguration(config, uiIO.get(), nullptr);
}
logger->info("=== SINGLE BUTTON TEST ===");
logger->info("- Bouton ROUGE au centre");
logger->info("- Hover = ORANGE");
logger->info("- Click = ROUGE FONCE");
logger->info("- Les events sont loggés ci-dessous");
logger->info("Press ESC to exit\n");
bool running = true;
while (running) {
SDL_Event e;
while (SDL_PollEvent(&e)) {
if (e.type == SDL_QUIT ||
(e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE)) {
running = false;
}
// Forward mouse events to UI via IIO
// IMPORTANT: Publish from gameIO (not uiIO) because IIO doesn't deliver to self
if (e.type == SDL_MOUSEMOTION) {
auto mouseMsg = std::make_unique<JsonDataNode>("mouse");
mouseMsg->setDouble("x", static_cast<double>(e.motion.x));
mouseMsg->setDouble("y", static_cast<double>(e.motion.y));
gameIO->publish("input:mouse:move", std::move(mouseMsg));
}
else if (e.type == SDL_MOUSEBUTTONDOWN || e.type == SDL_MOUSEBUTTONUP) {
auto mouseMsg = std::make_unique<JsonDataNode>("mouse");
mouseMsg->setInt("button", e.button.button);
mouseMsg->setBool("pressed", e.type == SDL_MOUSEBUTTONDOWN);
mouseMsg->setDouble("x", static_cast<double>(e.button.x));
mouseMsg->setDouble("y", static_cast<double>(e.button.y));
gameIO->publish("input:mouse:button", std::move(mouseMsg));
// Log press/release
logger->info("[INPUT] Mouse {} at ({}, {})",
e.type == SDL_MOUSEBUTTONDOWN ? "PRESSED" : "RELEASED",
e.button.x, e.button.y);
}
}
// Check for UI events
while (gameIO->hasMessages() > 0) {
auto msg = gameIO->pullMessage();
if (msg.topic == "ui:hover") {
std::string widgetId = msg.data->getString("widgetId", "");
bool enter = msg.data->getBool("enter", false);
logger->info("[UI EVENT] HOVER {} widget '{}'",
enter ? "ENTER" : "LEAVE", widgetId);
}
else if (msg.topic == "ui:click") {
std::string widgetId = msg.data->getString("widgetId", "");
logger->info("[UI EVENT] CLICK on widget '{}'", widgetId);
}
else if (msg.topic == "ui:action") {
std::string action = msg.data->getString("action", "");
std::string widgetId = msg.data->getString("widgetId", "");
logger->info("[UI EVENT] ACTION '{}' from widget '{}'", action, widgetId);
}
}
JsonDataNode input("input");
input.setDouble("deltaTime", 0.016);
// Process UI (publishes sprites)
ui->process(input);
// Render
renderer->process(input);
SDL_Delay(16);
}
ui->shutdown();
renderer->shutdown();
IntraIOManager::getInstance().removeInstance("renderer");
IntraIOManager::getInstance().removeInstance("ui");
IntraIOManager::getInstance().removeInstance("game");
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}