Files
yaze/docs/internal/agents/archive/wasm-planning-2025/wasm-widget-tracking-implementation.md

345 lines
11 KiB
Markdown

# WASM Widget Tracking Implementation
**Date**: 2025-11-24
**Author**: Claude (AI Agent)
**Status**: Implemented
**Related Files**:
- `/Users/scawful/Code/yaze/src/app/platform/wasm/wasm_control_api.cc`
- `/Users/scawful/Code/yaze/src/app/gui/automation/widget_id_registry.h`
- `/Users/scawful/Code/yaze/src/app/gui/automation/widget_measurement.h`
- `/Users/scawful/Code/yaze/src/app/controller.cc`
## Overview
This document describes the implementation of actual ImGui widget bounds tracking for GUI automation in the YAZE WASM build. The system replaces placeholder hardcoded bounds with real-time widget position data from the `WidgetIdRegistry`.
## Problem Statement
The original `WasmControlApi::GetUIElementTree()` and `GetUIElementBounds()` implementations returned hardcoded placeholder bounds:
```cpp
// OLD: Hardcoded placeholder
elem["bounds"] = {{"x", 0}, {"y", 0}, {"width", 100}, {"height", 30}};
```
This prevented accurate GUI automation, as agents and test frameworks couldn't reliably click on or query widget positions.
## Solution Architecture
### 1. Existing Infrastructure (Already in Place)
YAZE already had a comprehensive widget tracking system:
- **`WidgetIdRegistry`** (`src/app/gui/automation/widget_id_registry.h`): Centralized registry that tracks all ImGui widgets with their bounds, visibility, and state
- **`WidgetMeasurement`** (`src/app/gui/automation/widget_measurement.h`): Measures widget dimensions using `ImGui::GetItemRectMin()` and `ImGui::GetItemRectMax()`
- **Frame lifecycle hooks**: `BeginFrame()` and `EndFrame()` calls already integrated in `Controller::OnLoad()` (lines 96-98)
### 2. Integration with WASM Control API
The implementation connects the WASM API to the existing widget registry:
#### Updated `GetUIElementTree()`
**File**: `/Users/scawful/Code/yaze/src/app/platform/wasm/wasm_control_api.cc` (lines 1386-1433)
```cpp
std::string WasmControlApi::GetUIElementTree() {
nlohmann::json result;
if (!IsReady()) {
result["error"] = "Control API not initialized";
result["elements"] = nlohmann::json::array();
return result.dump();
}
// Query the WidgetIdRegistry for all registered widgets
auto& registry = gui::WidgetIdRegistry::Instance();
const auto& all_widgets = registry.GetAllWidgets();
nlohmann::json elements = nlohmann::json::array();
// Convert WidgetInfo to JSON elements
for (const auto& [path, info] : all_widgets) {
nlohmann::json elem;
elem["id"] = info.full_path;
elem["type"] = info.type;
elem["label"] = info.label;
elem["enabled"] = info.enabled;
elem["visible"] = info.visible;
elem["window"] = info.window_name;
// Add bounds if available
if (info.bounds.valid) {
elem["bounds"] = {
{"x", info.bounds.min_x},
{"y", info.bounds.min_y},
{"width", info.bounds.max_x - info.bounds.min_x},
{"height", info.bounds.max_y - info.bounds.min_y}
};
} else {
elem["bounds"] = {
{"x", 0}, {"y", 0}, {"width", 0}, {"height", 0}
};
}
// Add metadata
if (!info.description.empty()) {
elem["description"] = info.description;
}
elem["imgui_id"] = static_cast<uint32_t>(info.imgui_id);
elem["last_seen_frame"] = info.last_seen_frame;
elements.push_back(elem);
}
result["elements"] = elements;
result["count"] = elements.size();
result["source"] = "WidgetIdRegistry";
return result.dump();
}
```
**Changes**:
- Removed hardcoded editor-specific element generation
- Queries `WidgetIdRegistry::GetAllWidgets()` for real widget data
- Returns actual bounds from `info.bounds` if valid
- Includes metadata: `imgui_id`, `last_seen_frame`, `description`
- Adds `source: "WidgetIdRegistry"` to JSON for debugging
#### Updated `GetUIElementBounds()`
**File**: `/Users/scawful/Code/yaze/src/app/platform/wasm/wasm_control_api.cc` (lines ~1435+)
```cpp
std::string WasmControlApi::GetUIElementBounds(const std::string& element_id) {
nlohmann::json result;
if (!IsReady()) {
result["error"] = "Control API not initialized";
return result.dump();
}
// Query the WidgetIdRegistry for the specific widget
auto& registry = gui::WidgetIdRegistry::Instance();
const auto* widget_info = registry.GetWidgetInfo(element_id);
result["id"] = element_id;
if (widget_info == nullptr) {
result["found"] = false;
result["error"] = "Element not found: " + element_id;
return result.dump();
}
result["found"] = true;
result["visible"] = widget_info->visible;
result["enabled"] = widget_info->enabled;
result["type"] = widget_info->type;
result["label"] = widget_info->label;
result["window"] = widget_info->window_name;
// Add bounds if available
if (widget_info->bounds.valid) {
result["x"] = widget_info->bounds.min_x;
result["y"] = widget_info->bounds.min_y;
result["width"] = widget_info->bounds.max_x - widget_info->bounds.min_x;
result["height"] = widget_info->bounds.max_y - widget_info->bounds.min_y;
result["bounds_valid"] = true;
} else {
result["x"] = 0;
result["y"] = 0;
result["width"] = 0;
result["height"] = 0;
result["bounds_valid"] = false;
}
// Add metadata
result["imgui_id"] = static_cast<uint32_t>(widget_info->imgui_id);
result["last_seen_frame"] = widget_info->last_seen_frame;
if (!widget_info->description.empty()) {
result["description"] = widget_info->description;
}
return result.dump();
}
```
**Changes**:
- Removed hardcoded element ID pattern matching
- Queries `WidgetIdRegistry::GetWidgetInfo(element_id)` for specific widget
- Returns `found: false` if widget doesn't exist
- Returns actual bounds with `bounds_valid` flag
- Includes full widget metadata
### 3. Frame Lifecycle Integration
**File**: `/Users/scawful/Code/yaze/src/app/controller.cc` (lines 96-98)
The widget registry is already integrated into the main render loop:
```cpp
absl::Status Controller::OnLoad() {
// ... ImGui::NewFrame() setup ...
gui::WidgetIdRegistry::Instance().BeginFrame();
absl::Status update_status = editor_manager_.Update();
gui::WidgetIdRegistry::Instance().EndFrame();
RETURN_IF_ERROR(update_status);
return absl::OkStatus();
}
```
**Frame Lifecycle**:
1. `BeginFrame()`: Resets `seen_in_current_frame` flag for all widgets
2. Widget rendering: Editors register widgets during `editor_manager_.Update()`
3. `EndFrame()`: Marks unseen widgets as invisible, prunes stale entries
### 4. Widget Registration (Future Work)
**Current State**: Widget registration infrastructure exists but **editors are not yet registering widgets**.
**Registration Pattern** (to be implemented in editors):
```cpp
// Example: Dungeon Editor registering a card
{
gui::WidgetIdScope scope("DungeonEditor");
if (ImGui::Begin("Room Selector##dungeon")) {
// Widget now has full path: "DungeonEditor/Room Selector"
if (ImGui::Button("Load Room")) {
// After rendering button, register it
gui::WidgetIdRegistry::Instance().RegisterWidget(
scope.GetWidgetPath("button", "Load Room"),
"button",
ImGui::GetItemID(),
"Loads the selected room into the editor"
);
}
}
ImGui::End();
}
```
**Macros Available**:
- `YAZE_WIDGET_SCOPE(name)`: RAII scope for hierarchical widget paths
- `YAZE_REGISTER_WIDGET(type, name)`: Register widget after rendering
- `YAZE_REGISTER_CURRENT_WIDGET(type)`: Auto-extract widget name from ImGui
## API Usage
### JavaScript API
**Get All UI Elements**:
```javascript
const elements = window.yaze.control.getUIElementTree();
console.log(elements);
// Output:
// {
// "elements": [
// {
// "id": "DungeonEditor/RoomSelector/button:LoadRoom",
// "type": "button",
// "label": "Load Room",
// "visible": true,
// "enabled": true,
// "window": "DungeonEditor",
// "bounds": {"x": 150, "y": 200, "width": 100, "height": 30},
// "imgui_id": 12345,
// "last_seen_frame": 4567
// }
// ],
// "count": 1,
// "source": "WidgetIdRegistry"
// }
```
**Get Specific Widget Bounds**:
```javascript
const bounds = window.yaze.control.getUIElementBounds("DungeonEditor/RoomSelector/button:LoadRoom");
console.log(bounds);
// Output:
// {
// "id": "DungeonEditor/RoomSelector/button:LoadRoom",
// "found": true,
// "visible": true,
// "enabled": true,
// "type": "button",
// "label": "Load Room",
// "window": "DungeonEditor",
// "x": 150,
// "y": 200,
// "width": 100,
// "height": 30,
// "bounds_valid": true,
// "imgui_id": 12345,
// "last_seen_frame": 4567
// }
```
## Performance Considerations
1. **Memory**: `WidgetIdRegistry` stores widget metadata in `std::unordered_map`, which grows with UI complexity. Stale widgets are pruned after 600 frames of inactivity.
2. **CPU Overhead**:
- `BeginFrame()`: O(n) iteration to reset flags (n = number of widgets)
- Widget registration: O(1) hash map lookup/insert
- `EndFrame()`: O(n) iteration for pruning stale entries
3. **Optimization**: Widget measurement can be disabled globally:
```cpp
gui::WidgetMeasurement::Instance().SetEnabled(false);
```
## Testing
**Manual Test (WASM Build)**:
```bash
# Build WASM
./scripts/build-wasm.sh
# Serve locally
cd build-wasm
python3 -m http.server 8080
# Open browser console
window.yaze.control.getUIElementTree();
```
**Expected Behavior**:
- Initially, `elements` array will be empty (no widgets registered yet)
- After editors implement registration, widgets will appear with real bounds
- `bounds_valid: false` for widgets not yet rendered in current frame
## Next Steps
1. **Add widget registration to editors**:
- `DungeonEditorV2`: Register room tabs, cards, buttons
- `OverworldEditor`: Register canvas, tile selectors, property panels
- `GraphicsEditor`: Register graphics sheets, palette pickers
2. **Add registration helpers**:
- Create `AgentUI::RegisterButton()`, `AgentUI::RegisterCard()` wrappers
- Auto-register common widget patterns (cards with visibility flags)
3. **Extend API**:
- `FindWidgetsByPattern(pattern)`: Search widgets by regex
- `ClickWidget(element_id)`: Simulate click via automation API
## References
- Widget ID Registry: `/Users/scawful/Code/yaze/src/app/gui/automation/widget_id_registry.h`
- Widget Measurement: `/Users/scawful/Code/yaze/src/app/gui/automation/widget_measurement.h`
- WASM Control API: `/Users/scawful/Code/yaze/src/app/platform/wasm/wasm_control_api.h`
- Controller Integration: `/Users/scawful/Code/yaze/src/app/controller.cc` (lines 96-98)
## Revision History
| Date | Author | Changes |
|------------|--------|--------------------------------------------|
| 2025-11-24 | Claude | Initial implementation and documentation |