Add ImGui WidgetIdRegistry
This commit is contained in:
197
src/app/gui/widget_id_registry.cc
Normal file
197
src/app/gui/widget_id_registry.cc
Normal file
@@ -0,0 +1,197 @@
|
||||
#include "widget_id_registry.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/strings/str_join.h"
|
||||
#include "absl/strings/str_split.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace gui {
|
||||
|
||||
// Thread-local storage for ID stack
|
||||
thread_local std::vector<std::string> WidgetIdScope::id_stack_;
|
||||
|
||||
WidgetIdScope::WidgetIdScope(const std::string& name) : name_(name) {
|
||||
ImGui::PushID(name.c_str());
|
||||
id_stack_.push_back(name);
|
||||
}
|
||||
|
||||
WidgetIdScope::~WidgetIdScope() {
|
||||
ImGui::PopID();
|
||||
if (!id_stack_.empty()) {
|
||||
id_stack_.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
std::string WidgetIdScope::GetFullPath() const {
|
||||
return absl::StrJoin(id_stack_, "/");
|
||||
}
|
||||
|
||||
std::string WidgetIdScope::GetWidgetPath(const std::string& widget_type,
|
||||
const std::string& widget_name) const {
|
||||
std::string path = GetFullPath();
|
||||
if (!path.empty()) {
|
||||
path += "/";
|
||||
}
|
||||
return absl::StrCat(path, widget_type, ":", widget_name);
|
||||
}
|
||||
|
||||
// WidgetIdRegistry implementation
|
||||
|
||||
WidgetIdRegistry& WidgetIdRegistry::Instance() {
|
||||
static WidgetIdRegistry instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void WidgetIdRegistry::RegisterWidget(const std::string& full_path,
|
||||
const std::string& type, ImGuiID imgui_id,
|
||||
const std::string& description) {
|
||||
WidgetInfo info{full_path, type, imgui_id, description};
|
||||
widgets_[full_path] = info;
|
||||
}
|
||||
|
||||
std::vector<std::string> WidgetIdRegistry::FindWidgets(
|
||||
const std::string& pattern) const {
|
||||
std::vector<std::string> matches;
|
||||
|
||||
// Simple glob-style pattern matching
|
||||
// Supports: "*" (any), "?" (single char), exact matches
|
||||
for (const auto& [path, info] : widgets_) {
|
||||
bool match = false;
|
||||
|
||||
if (pattern == "*") {
|
||||
match = true;
|
||||
} else if (pattern.find('*') != std::string::npos) {
|
||||
// Wildcard pattern - convert to simple substring match for now
|
||||
std::string search = pattern;
|
||||
search.erase(std::remove(search.begin(), search.end(), '*'), search.end());
|
||||
if (!search.empty() && path.find(search) != std::string::npos) {
|
||||
match = true;
|
||||
}
|
||||
} else {
|
||||
// Exact match
|
||||
if (path == pattern) {
|
||||
match = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (match) {
|
||||
matches.push_back(path);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort for consistent ordering
|
||||
std::sort(matches.begin(), matches.end());
|
||||
return matches;
|
||||
}
|
||||
|
||||
ImGuiID WidgetIdRegistry::GetWidgetId(const std::string& full_path) const {
|
||||
auto it = widgets_.find(full_path);
|
||||
if (it != widgets_.end()) {
|
||||
return it->second.imgui_id;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const WidgetIdRegistry::WidgetInfo* WidgetIdRegistry::GetWidgetInfo(
|
||||
const std::string& full_path) const {
|
||||
auto it = widgets_.find(full_path);
|
||||
if (it != widgets_.end()) {
|
||||
return &it->second;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void WidgetIdRegistry::Clear() { widgets_.clear(); }
|
||||
|
||||
std::string WidgetIdRegistry::ExportCatalog(const std::string& format) const {
|
||||
std::ostringstream ss;
|
||||
|
||||
if (format == "json") {
|
||||
ss << "{\n";
|
||||
ss << " \"widgets\": [\n";
|
||||
|
||||
bool first = true;
|
||||
for (const auto& [path, info] : widgets_) {
|
||||
if (!first) ss << ",\n";
|
||||
first = false;
|
||||
|
||||
ss << " {\n";
|
||||
ss << absl::StrFormat(" \"path\": \"%s\",\n", path);
|
||||
ss << absl::StrFormat(" \"type\": \"%s\",\n", info.type);
|
||||
ss << absl::StrFormat(" \"imgui_id\": %u", info.imgui_id);
|
||||
if (!info.description.empty()) {
|
||||
ss << ",\n";
|
||||
ss << absl::StrFormat(" \"description\": \"%s\"", info.description);
|
||||
}
|
||||
ss << "\n }";
|
||||
}
|
||||
|
||||
ss << "\n ]\n";
|
||||
ss << "}\n";
|
||||
} else {
|
||||
// YAML format (default)
|
||||
ss << "widgets:\n";
|
||||
|
||||
for (const auto& [path, info] : widgets_) {
|
||||
ss << absl::StrFormat(" - path: \"%s\"\n", path);
|
||||
ss << absl::StrFormat(" type: %s\n", info.type);
|
||||
ss << absl::StrFormat(" imgui_id: %u\n", info.imgui_id);
|
||||
|
||||
// Parse hierarchical context from path
|
||||
std::vector<std::string> segments = absl::StrSplit(path, '/');
|
||||
if (!segments.empty()) {
|
||||
ss << " context:\n";
|
||||
if (segments.size() > 0) {
|
||||
ss << absl::StrFormat(" editor: %s\n", segments[0]);
|
||||
}
|
||||
if (segments.size() > 1) {
|
||||
ss << absl::StrFormat(" tab: %s\n", segments[1]);
|
||||
}
|
||||
if (segments.size() > 2) {
|
||||
ss << absl::StrFormat(" section: %s\n", segments[2]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!info.description.empty()) {
|
||||
ss << absl::StrFormat(" description: %s\n", info.description);
|
||||
}
|
||||
|
||||
// Add suggested actions based on widget type
|
||||
ss << " actions: [";
|
||||
if (info.type == "button") {
|
||||
ss << "click";
|
||||
} else if (info.type == "input") {
|
||||
ss << "type, clear";
|
||||
} else if (info.type == "canvas") {
|
||||
ss << "click, drag, scroll";
|
||||
} else if (info.type == "checkbox") {
|
||||
ss << "toggle";
|
||||
} else if (info.type == "slider") {
|
||||
ss << "drag, set";
|
||||
} else {
|
||||
ss << "interact";
|
||||
}
|
||||
ss << "]\n";
|
||||
}
|
||||
}
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
void WidgetIdRegistry::ExportCatalogToFile(const std::string& output_file,
|
||||
const std::string& format) const {
|
||||
std::string content = ExportCatalog(format);
|
||||
std::ofstream file(output_file);
|
||||
if (file.is_open()) {
|
||||
file << content;
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace gui
|
||||
} // namespace yaze
|
||||
132
src/app/gui/widget_id_registry.h
Normal file
132
src/app/gui/widget_id_registry.h
Normal file
@@ -0,0 +1,132 @@
|
||||
#ifndef YAZE_APP_GUI_WIDGET_ID_REGISTRY_H_
|
||||
#define YAZE_APP_GUI_WIDGET_ID_REGISTRY_H_
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui/imgui.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace gui {
|
||||
|
||||
/**
|
||||
* @class WidgetIdScope
|
||||
* @brief RAII helper for managing hierarchical ImGui ID scopes
|
||||
*
|
||||
* This class automatically pushes/pops ImGui IDs and maintains a thread-local
|
||||
* stack for building hierarchical widget paths. Use this to create predictable,
|
||||
* stable widget IDs for test automation.
|
||||
*
|
||||
* Example:
|
||||
* {
|
||||
* WidgetIdScope editor("Overworld");
|
||||
* // Widgets here have "Overworld/" prefix
|
||||
* {
|
||||
* WidgetIdScope section("Canvas");
|
||||
* // Widgets here have "Overworld/Canvas/" prefix
|
||||
* ImGui::BeginChild("Map", ...); // Full path: Overworld/Canvas/Map
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
class WidgetIdScope {
|
||||
public:
|
||||
explicit WidgetIdScope(const std::string& name);
|
||||
~WidgetIdScope();
|
||||
|
||||
// Get the full hierarchical path at this scope level
|
||||
std::string GetFullPath() const;
|
||||
|
||||
// Get the full path with a widget suffix
|
||||
std::string GetWidgetPath(const std::string& widget_type,
|
||||
const std::string& widget_name) const;
|
||||
|
||||
private:
|
||||
std::string name_;
|
||||
static thread_local std::vector<std::string> id_stack_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @class WidgetIdRegistry
|
||||
* @brief Centralized registry for discoverable GUI widgets
|
||||
*
|
||||
* This singleton maintains a catalog of all registered widgets in the
|
||||
* application, enabling test automation and AI-driven GUI interaction.
|
||||
* Widgets are identified by hierarchical paths like:
|
||||
* "Overworld/Main/Toolset/button:DrawTile"
|
||||
*
|
||||
* The registry provides:
|
||||
* - Widget discovery by pattern matching
|
||||
* - Mapping widget paths to ImGui IDs
|
||||
* - Export to machine-readable formats for z3ed agent
|
||||
*/
|
||||
class WidgetIdRegistry {
|
||||
public:
|
||||
struct WidgetInfo {
|
||||
std::string full_path; // e.g. "Overworld/Canvas/canvas:Map"
|
||||
std::string type; // e.g. "button", "input", "canvas", "table"
|
||||
ImGuiID imgui_id; // ImGui's internal ID
|
||||
std::string description; // Optional human-readable description
|
||||
};
|
||||
|
||||
static WidgetIdRegistry& Instance();
|
||||
|
||||
// Register a widget for discovery
|
||||
// Should be called after widget is created (when ImGui::GetItemID() is valid)
|
||||
void RegisterWidget(const std::string& full_path, const std::string& type,
|
||||
ImGuiID imgui_id,
|
||||
const std::string& description = "");
|
||||
|
||||
// Query widgets for test automation
|
||||
std::vector<std::string> FindWidgets(const std::string& pattern) const;
|
||||
ImGuiID GetWidgetId(const std::string& full_path) const;
|
||||
const WidgetInfo* GetWidgetInfo(const std::string& full_path) const;
|
||||
|
||||
// Get all registered widgets
|
||||
const std::unordered_map<std::string, WidgetInfo>& GetAllWidgets() const {
|
||||
return widgets_;
|
||||
}
|
||||
|
||||
// Clear all registered widgets (useful between frames for dynamic UIs)
|
||||
void Clear();
|
||||
|
||||
// Export catalog for z3ed agent describe
|
||||
// Format: "yaml" or "json"
|
||||
std::string ExportCatalog(const std::string& format = "yaml") const;
|
||||
void ExportCatalogToFile(const std::string& output_file,
|
||||
const std::string& format = "yaml") const;
|
||||
|
||||
private:
|
||||
WidgetIdRegistry() = default;
|
||||
std::unordered_map<std::string, WidgetInfo> widgets_;
|
||||
};
|
||||
|
||||
// RAII helper macros for convenient scoping
|
||||
#define YAZE_WIDGET_SCOPE(name) yaze::gui::WidgetIdScope _yaze_scope_##__LINE__(name)
|
||||
|
||||
// Register a widget after creation (when GetItemID() is valid)
|
||||
#define YAZE_REGISTER_WIDGET(widget_type, widget_name) \
|
||||
do { \
|
||||
if (ImGui::GetItemID() != 0) { \
|
||||
yaze::gui::WidgetIdRegistry::Instance().RegisterWidget( \
|
||||
_yaze_scope_##__LINE__.GetWidgetPath(#widget_type, widget_name), \
|
||||
#widget_type, ImGui::GetItemID()); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
// Convenience macro for registering with automatic name extraction
|
||||
// Usage: YAZE_REGISTER_CURRENT_WIDGET("button")
|
||||
#define YAZE_REGISTER_CURRENT_WIDGET(widget_type) \
|
||||
do { \
|
||||
if (ImGui::GetItemID() != 0) { \
|
||||
yaze::gui::WidgetIdRegistry::Instance().RegisterWidget( \
|
||||
_yaze_scope_##__LINE__.GetWidgetPath(widget_type, \
|
||||
ImGui::GetLastItemLabel()), \
|
||||
widget_type, ImGui::GetItemID()); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
} // namespace gui
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_GUI_WIDGET_ID_REGISTRY_H_
|
||||
Reference in New Issue
Block a user