GroveEngine/docs/UI_TOPICS.md
StillHammer 1b7703f07b feat(IIO)!: BREAKING CHANGE - Callback-based message dispatch
## 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>
2026-01-19 14:19:27 +07:00

4.5 KiB

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 for details on retained vs immediate mode.

Usage Examples

Handling UI Events with Callbacks

// 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

// 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.

// 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
}