## 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>
132 lines
4.5 KiB
Markdown
132 lines
4.5 KiB
Markdown
# UIModule - IIO Topics Reference
|
|
|
|
Complete reference of all IIO topics consumed and published by UIModule.
|
|
|
|
## Topics Consumed
|
|
|
|
### From InputModule
|
|
|
|
| Topic | Payload | Description |
|
|
|-------|---------|-------------|
|
|
| `input:mouse:move` | `{x, y}` | Mouse position |
|
|
| `input:mouse:button` | `{button, pressed, x, y}` | Mouse click |
|
|
| `input:mouse:wheel` | `{delta}` | Mouse wheel |
|
|
| `input:keyboard` | `{keyCode, pressed, char}` | Keyboard event |
|
|
|
|
### UI Control Commands
|
|
|
|
| Topic | Payload | Description |
|
|
|-------|---------|-------------|
|
|
| `ui:set_text` | `{id, text}` | Update label text dynamically |
|
|
| `ui:set_visible` | `{id, visible}` | Show/hide widget |
|
|
| `ui:set_value` | `{id, value}` | Set slider/progressbar value |
|
|
| `ui:load` | `{layoutPath}` | Load new UI layout from file |
|
|
|
|
## Topics Published
|
|
|
|
### UI Events
|
|
|
|
| Topic | Payload | Description |
|
|
|-------|---------|-------------|
|
|
| `ui:click` | `{widgetId, x, y}` | Widget clicked |
|
|
| `ui:action` | `{widgetId, action}` | Button action triggered |
|
|
| `ui:value_changed` | `{widgetId, value}` | Slider/checkbox/input changed |
|
|
| `ui:text_submitted` | `{widgetId, text}` | Text input submitted (Enter) |
|
|
| `ui:hover` | `{widgetId, enter}` | Mouse entered/left widget |
|
|
| `ui:scroll` | `{widgetId, scrollX, scrollY}` | Scroll panel scrolled |
|
|
|
|
### Rendering (Retained Mode)
|
|
|
|
| Topic | Payload | Description |
|
|
|-------|---------|-------------|
|
|
| `render:sprite:add` | `{renderId, x, y, scaleX, scaleY, color, textureId, layer}` | Register new sprite |
|
|
| `render:sprite:update` | `{renderId, x, y, scaleX, scaleY, color, textureId, layer}` | Update existing sprite |
|
|
| `render:sprite:remove` | `{renderId}` | Unregister sprite |
|
|
| `render:text:add` | `{renderId, x, y, text, fontSize, color, layer}` | Register new text |
|
|
| `render:text:update` | `{renderId, x, y, text, fontSize, color, layer}` | Update existing text |
|
|
| `render:text:remove` | `{renderId}` | Unregister text |
|
|
|
|
### Rendering (Immediate Mode - Legacy)
|
|
|
|
| Topic | Payload | Description |
|
|
|-------|---------|-------------|
|
|
| `render:sprite` | `{x, y, w, h, color, layer, ...}` | Ephemeral sprite (1 frame) |
|
|
| `render:text` | `{x, y, text, fontSize, color, layer}` | Ephemeral text (1 frame) |
|
|
|
|
See [UI Rendering Documentation](UI_RENDERING.md) for details on retained vs immediate mode.
|
|
|
|
## Usage Examples
|
|
|
|
### Handling UI Events with Callbacks
|
|
|
|
```cpp
|
|
// Subscribe to UI events with callback handlers (in setConfiguration)
|
|
gameIO->subscribe("ui:action", [this](const grove::Message& msg) {
|
|
std::string action = msg.data->getString("action", "");
|
|
if (action == "start_game") {
|
|
startGame();
|
|
}
|
|
});
|
|
|
|
gameIO->subscribe("ui:value_changed", [this](const grove::Message& msg) {
|
|
std::string widgetId = msg.data->getString("widgetId", "");
|
|
if (widgetId == "volume_slider") {
|
|
double value = msg.data->getDouble("value", 50.0);
|
|
setVolume(value);
|
|
}
|
|
});
|
|
|
|
// In game loop (process method)
|
|
while (m_io->hasMessages() > 0) {
|
|
m_io->pullAndDispatch(); // Callbacks invoked automatically
|
|
}
|
|
```
|
|
|
|
### Updating UI Dynamically
|
|
|
|
```cpp
|
|
// Update label text
|
|
auto msg = std::make_unique<JsonDataNode>("set_text");
|
|
msg->setString("id", "score_label");
|
|
msg->setString("text", "Score: " + std::to_string(score));
|
|
m_io->publish("ui:set_text", std::move(msg));
|
|
|
|
// Hide/show widget
|
|
auto msg = std::make_unique<JsonDataNode>("set_visible");
|
|
msg->setString("id", "loading_panel");
|
|
msg->setBool("visible", false);
|
|
m_io->publish("ui:set_visible", std::move(msg));
|
|
|
|
// Update progress bar
|
|
auto msg = std::make_unique<JsonDataNode>("set_value");
|
|
msg->setString("id", "health_bar");
|
|
msg->setDouble("value", 0.75); // 75%
|
|
m_io->publish("ui:set_value", std::move(msg));
|
|
```
|
|
|
|
### Slider + Label Pattern
|
|
|
|
Common pattern: update a label when a slider changes.
|
|
|
|
```cpp
|
|
// Subscribe to slider value changes (in setConfiguration)
|
|
gameIO->subscribe("ui:value_changed", [this](const grove::Message& msg) {
|
|
std::string widgetId = msg.data->getString("widgetId", "");
|
|
if (widgetId == "volume_slider") {
|
|
double value = msg.data->getDouble("value", 50.0);
|
|
setVolume(value);
|
|
|
|
// Update label to show current value
|
|
auto updateMsg = std::make_unique<JsonDataNode>("set_text");
|
|
updateMsg->setString("id", "volume_label");
|
|
updateMsg->setString("text", "Volume: " + std::to_string((int)value) + "%");
|
|
m_io->publish("ui:set_text", std::move(updateMsg));
|
|
}
|
|
});
|
|
|
|
// In process()
|
|
while (gameIO->hasMessages() > 0) {
|
|
gameIO->pullAndDispatch(); // Callback invoked automatically
|
|
}
|
|
```
|