## Breaking Change
IIO API redesigned from manual pull+if-forest to callback dispatch.
All modules must update their subscribe() calls to pass handlers.
### Before (OLD API)
```cpp
io->subscribe("input:mouse");
void process(...) {
while (io->hasMessages()) {
auto msg = io->pullMessage();
if (msg.topic == "input:mouse") {
handleMouse(msg);
} else if (msg.topic == "input:keyboard") {
handleKeyboard(msg);
}
}
}
```
### After (NEW API)
```cpp
io->subscribe("input:mouse", [this](const Message& msg) {
handleMouse(msg);
});
void process(...) {
while (io->hasMessages()) {
io->pullAndDispatch(); // Callbacks invoked automatically
}
}
```
## Changes
**Core API (include/grove/IIO.h)**
- Added: `using MessageHandler = std::function<void(const Message&)>`
- Changed: `subscribe()` now requires `MessageHandler` callback parameter
- Changed: `subscribeLowFreq()` now requires `MessageHandler` callback
- Removed: `pullMessage()`
- Added: `pullAndDispatch()` - pulls and auto-dispatches to handlers
**Implementation (src/IntraIO.cpp)**
- Store callbacks in `Subscription.handler`
- `pullAndDispatch()` matches topic against ALL subscriptions (not just first)
- Fixed: Regex pattern compilation supports both wildcards (*) and regex (.*)
- Performance: ~1000 msg/s throughput (unchanged from before)
**Files Updated**
- 31 test/module files migrated to callback API (via parallel agents)
- 8 documentation files updated (DEVELOPER_GUIDE, USER_GUIDE, module READMEs)
## Bugs Fixed During Migration
1. **pullAndDispatch() early return bug**: Was only calling FIRST matching handler
- Fix: Loop through ALL subscriptions, invoke all matching handlers
2. **Regex pattern compilation bug**: Pattern "player:.*" failed to match
- Fix: Detect ".*" in pattern → use as regex, otherwise escape and convert wildcards
## Testing
✅ test_11_io_system: PASSED (IIO pub/sub, pattern matching, batching)
✅ test_threaded_module_system: 6/6 PASSED
✅ test_threaded_stress: 5/5 PASSED (50 modules, 100x reload, concurrent ops)
✅ test_12_datanode: PASSED
✅ 10 TopicTree scenarios: 10/10 PASSED
✅ benchmark_e2e: ~1000 msg/s throughput
Total: 23+ tests passing
## Performance Impact
No performance regression from callback dispatch:
- IIO throughput: ~1000 msg/s (same as before)
- ThreadedModuleSystem: Speedup ~1.0x (barrier pattern expected)
## Migration Guide
For all modules using IIO:
1. Update subscribe() calls to include handler lambda
2. Replace pullMessage() loops with pullAndDispatch()
3. Move topic-specific logic from if-forest into callbacks
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
218 lines
7.8 KiB
C++
218 lines
7.8 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 with callbacks
|
|
gameIO->subscribe("ui:hover", [&logger](const Message& msg) {
|
|
std::string widgetId = msg.data->getString("widgetId", "");
|
|
bool enter = msg.data->getBool("enter", false);
|
|
logger->info("[UI EVENT] HOVER {} widget '{}'",
|
|
enter ? "ENTER" : "LEAVE", widgetId);
|
|
});
|
|
gameIO->subscribe("ui:click", [&logger](const Message& msg) {
|
|
std::string widgetId = msg.data->getString("widgetId", "");
|
|
logger->info("[UI EVENT] CLICK on widget '{}'", widgetId);
|
|
});
|
|
gameIO->subscribe("ui:action", [&logger](const Message& msg) {
|
|
std::string action = msg.data->getString("action", "");
|
|
std::string widgetId = msg.data->getString("widgetId", "");
|
|
logger->info("[UI EVENT] ACTION '{}' from widget '{}'", action, widgetId);
|
|
});
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
// Dispatch UI events (callbacks handle logging)
|
|
while (gameIO->hasMessages() > 0) {
|
|
gameIO->pullAndDispatch();
|
|
}
|
|
|
|
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;
|
|
}
|