# 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(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(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 |