refactor: Restructure file dialog handling and introduce utility classes
- Updated file dialog references across the application to utilize a new `util::FileDialogWrapper` for consistent file handling. - Refactored existing code to replace direct calls to `core::FileDialogWrapper` with the new utility class, enhancing modularity and maintainability. - Introduced `util::PlatformPaths` for cross-platform directory management, ensuring consistent access to user directories and application data paths. - Added new utility functions for file operations, improving the overall file handling capabilities within the application. - Updated CMake configurations to include new utility source files, streamlining the build process.
This commit is contained in:
87
src/app/gui/widgets/widget_auto_register.cc
Normal file
87
src/app/gui/widgets/widget_auto_register.cc
Normal file
@@ -0,0 +1,87 @@
|
||||
#include "app/gui/widgets/widget_auto_register.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "imgui/imgui_internal.h"
|
||||
#include "absl/strings/str_join.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace gui {
|
||||
|
||||
// Thread-local storage for the current auto-registration scope
|
||||
thread_local std::vector<std::string> g_auto_scope_stack_;
|
||||
|
||||
AutoWidgetScope::AutoWidgetScope(const std::string& name)
|
||||
: scope_(name), name_(name) {
|
||||
g_auto_scope_stack_.push_back(name);
|
||||
}
|
||||
|
||||
AutoWidgetScope::~AutoWidgetScope() {
|
||||
if (!g_auto_scope_stack_.empty()) {
|
||||
g_auto_scope_stack_.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
void AutoRegisterLastItem(const std::string& widget_type,
|
||||
const std::string& explicit_label,
|
||||
const std::string& description) {
|
||||
ImGuiContext* ctx = ImGui::GetCurrentContext();
|
||||
if (!ctx || !ctx->CurrentWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the last item's ID
|
||||
ImGuiID imgui_id = ctx->LastItemData.ID;
|
||||
if (imgui_id == 0) {
|
||||
return; // No valid item to register
|
||||
}
|
||||
|
||||
// Extract label
|
||||
std::string label = explicit_label;
|
||||
if (label.empty()) {
|
||||
label = absl::StrCat(widget_type, "_", imgui_id);
|
||||
}
|
||||
|
||||
// Build full hierarchical path
|
||||
std::string full_path;
|
||||
if (!g_auto_scope_stack_.empty()) {
|
||||
full_path = absl::StrJoin(g_auto_scope_stack_, "/");
|
||||
full_path += "/";
|
||||
}
|
||||
|
||||
// Add widget type and normalized label
|
||||
std::string normalized_label = WidgetIdRegistry::NormalizeLabel(label);
|
||||
full_path += absl::StrCat(widget_type, ":", normalized_label);
|
||||
|
||||
// Capture metadata from ImGui's last item
|
||||
WidgetIdRegistry::WidgetMetadata metadata;
|
||||
metadata.label = label;
|
||||
|
||||
// Get window name
|
||||
if (ctx->CurrentWindow) {
|
||||
metadata.window_name = std::string(ctx->CurrentWindow->Name);
|
||||
}
|
||||
|
||||
// Capture visibility and enabled state
|
||||
const ImGuiLastItemData& last = ctx->LastItemData;
|
||||
metadata.visible = (last.StatusFlags & ImGuiItemStatusFlags_Visible) != 0;
|
||||
metadata.enabled = (last.ItemFlags & ImGuiItemFlags_Disabled) == 0;
|
||||
|
||||
// Capture bounding rectangle
|
||||
WidgetIdRegistry::WidgetBounds bounds;
|
||||
bounds.min_x = last.Rect.Min.x;
|
||||
bounds.min_y = last.Rect.Min.y;
|
||||
bounds.max_x = last.Rect.Max.x;
|
||||
bounds.max_y = last.Rect.Max.y;
|
||||
bounds.valid = true;
|
||||
metadata.bounds = bounds;
|
||||
|
||||
// Register with the global registry
|
||||
WidgetIdRegistry::Instance().RegisterWidget(
|
||||
full_path, widget_type, imgui_id, description, metadata);
|
||||
}
|
||||
|
||||
} // namespace gui
|
||||
} // namespace yaze
|
||||
|
||||
264
src/app/gui/widgets/widget_auto_register.h
Normal file
264
src/app/gui/widgets/widget_auto_register.h
Normal file
@@ -0,0 +1,264 @@
|
||||
#ifndef YAZE_APP_GUI_WIDGETS_WIDGET_AUTO_REGISTER_H_
|
||||
#define YAZE_APP_GUI_WIDGETS_WIDGET_AUTO_REGISTER_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "imgui/imgui.h"
|
||||
#include "app/gui/widgets/widget_id_registry.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
|
||||
/**
|
||||
* @file widget_auto_register.h
|
||||
* @brief Automatic widget registration helpers for ImGui Test Engine integration
|
||||
*
|
||||
* This file provides inline wrappers and RAII helpers that automatically
|
||||
* register ImGui widgets with the WidgetIdRegistry for test automation.
|
||||
*
|
||||
* Usage:
|
||||
* {
|
||||
* gui::AutoWidgetScope scope("Dungeon/Canvas");
|
||||
* if (gui::AutoButton("Save##DungeonSave")) {
|
||||
* // Button clicked
|
||||
* }
|
||||
* gui::AutoInputText("RoomName", buffer, sizeof(buffer));
|
||||
* }
|
||||
*
|
||||
* All widgets created within this scope will be automatically registered
|
||||
* with their full hierarchical paths for test harness discovery.
|
||||
*/
|
||||
|
||||
namespace yaze {
|
||||
namespace gui {
|
||||
|
||||
/**
|
||||
* @class AutoWidgetScope
|
||||
* @brief RAII scope that enables automatic widget registration
|
||||
*
|
||||
* Creates a widget ID scope and enables auto-registration for all widgets
|
||||
* created within this scope. Combines WidgetIdScope with automatic metadata
|
||||
* capture.
|
||||
*/
|
||||
class AutoWidgetScope {
|
||||
public:
|
||||
explicit AutoWidgetScope(const std::string& name);
|
||||
~AutoWidgetScope();
|
||||
|
||||
// Get current scope path
|
||||
std::string GetPath() const { return scope_.GetFullPath(); }
|
||||
|
||||
private:
|
||||
WidgetIdScope scope_;
|
||||
std::string name_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Automatically register the last ImGui item
|
||||
*
|
||||
* Call this after any ImGui widget creation to automatically register it.
|
||||
* Captures widget type, bounds, visibility, and enabled state.
|
||||
*
|
||||
* @param widget_type Type of widget ("button", "input", "checkbox", etc.)
|
||||
* @param explicit_label Optional explicit label (uses ImGui::GetItemLabel() if empty)
|
||||
* @param description Optional description for the test harness
|
||||
*/
|
||||
void AutoRegisterLastItem(const std::string& widget_type,
|
||||
const std::string& explicit_label = "",
|
||||
const std::string& description = "");
|
||||
|
||||
// ============================================================================
|
||||
// Automatic Registration Wrappers for Common ImGui Widgets
|
||||
// ============================================================================
|
||||
// These wrappers call the standard ImGui functions and automatically register
|
||||
// the widget with the WidgetIdRegistry for test automation.
|
||||
//
|
||||
// They preserve the exact same API and return values as ImGui, so they can be
|
||||
// drop-in replacements in existing code.
|
||||
|
||||
inline bool AutoButton(const char* label, const ImVec2& size = ImVec2(0, 0)) {
|
||||
bool clicked = ImGui::Button(label, size);
|
||||
AutoRegisterLastItem("button", label);
|
||||
return clicked;
|
||||
}
|
||||
|
||||
inline bool AutoSmallButton(const char* label) {
|
||||
bool clicked = ImGui::SmallButton(label);
|
||||
AutoRegisterLastItem("button", label, "Small button");
|
||||
return clicked;
|
||||
}
|
||||
|
||||
inline bool AutoCheckbox(const char* label, bool* v) {
|
||||
bool changed = ImGui::Checkbox(label, v);
|
||||
AutoRegisterLastItem("checkbox", label);
|
||||
return changed;
|
||||
}
|
||||
|
||||
inline bool AutoRadioButton(const char* label, bool active) {
|
||||
bool clicked = ImGui::RadioButton(label, active);
|
||||
AutoRegisterLastItem("radio", label);
|
||||
return clicked;
|
||||
}
|
||||
|
||||
inline bool AutoRadioButton(const char* label, int* v, int v_button) {
|
||||
bool clicked = ImGui::RadioButton(label, v, v_button);
|
||||
AutoRegisterLastItem("radio", label);
|
||||
return clicked;
|
||||
}
|
||||
|
||||
inline bool AutoInputText(const char* label, char* buf, size_t buf_size,
|
||||
ImGuiInputTextFlags flags = 0,
|
||||
ImGuiInputTextCallback callback = nullptr,
|
||||
void* user_data = nullptr) {
|
||||
bool changed = ImGui::InputText(label, buf, buf_size, flags, callback, user_data);
|
||||
AutoRegisterLastItem("input", label);
|
||||
return changed;
|
||||
}
|
||||
|
||||
inline bool AutoInputTextMultiline(const char* label, char* buf, size_t buf_size,
|
||||
const ImVec2& size = ImVec2(0, 0),
|
||||
ImGuiInputTextFlags flags = 0,
|
||||
ImGuiInputTextCallback callback = nullptr,
|
||||
void* user_data = nullptr) {
|
||||
bool changed = ImGui::InputTextMultiline(label, buf, buf_size, size, flags, callback, user_data);
|
||||
AutoRegisterLastItem("textarea", label);
|
||||
return changed;
|
||||
}
|
||||
|
||||
inline bool AutoInputInt(const char* label, int* v, int step = 1, int step_fast = 100,
|
||||
ImGuiInputTextFlags flags = 0) {
|
||||
bool changed = ImGui::InputInt(label, v, step, step_fast, flags);
|
||||
AutoRegisterLastItem("input_int", label);
|
||||
return changed;
|
||||
}
|
||||
|
||||
inline bool AutoInputFloat(const char* label, float* v, float step = 0.0f,
|
||||
float step_fast = 0.0f, const char* format = "%.3f",
|
||||
ImGuiInputTextFlags flags = 0) {
|
||||
bool changed = ImGui::InputFloat(label, v, step, step_fast, format, flags);
|
||||
AutoRegisterLastItem("input_float", label);
|
||||
return changed;
|
||||
}
|
||||
|
||||
inline bool AutoSliderInt(const char* label, int* v, int v_min, int v_max,
|
||||
const char* format = "%d", ImGuiSliderFlags flags = 0) {
|
||||
bool changed = ImGui::SliderInt(label, v, v_min, v_max, format, flags);
|
||||
AutoRegisterLastItem("slider", label);
|
||||
return changed;
|
||||
}
|
||||
|
||||
inline bool AutoSliderFloat(const char* label, float* v, float v_min, float v_max,
|
||||
const char* format = "%.3f", ImGuiSliderFlags flags = 0) {
|
||||
bool changed = ImGui::SliderFloat(label, v, v_min, v_max, format, flags);
|
||||
AutoRegisterLastItem("slider", label);
|
||||
return changed;
|
||||
}
|
||||
|
||||
inline bool AutoCombo(const char* label, int* current_item, const char* const items[],
|
||||
int items_count, int popup_max_height_in_items = -1) {
|
||||
bool changed = ImGui::Combo(label, current_item, items, items_count, popup_max_height_in_items);
|
||||
AutoRegisterLastItem("combo", label);
|
||||
return changed;
|
||||
}
|
||||
|
||||
inline bool AutoSelectable(const char* label, bool selected = false,
|
||||
ImGuiSelectableFlags flags = 0,
|
||||
const ImVec2& size = ImVec2(0, 0)) {
|
||||
bool clicked = ImGui::Selectable(label, selected, flags, size);
|
||||
AutoRegisterLastItem("selectable", label);
|
||||
return clicked;
|
||||
}
|
||||
|
||||
inline bool AutoSelectable(const char* label, bool* p_selected,
|
||||
ImGuiSelectableFlags flags = 0,
|
||||
const ImVec2& size = ImVec2(0, 0)) {
|
||||
bool clicked = ImGui::Selectable(label, p_selected, flags, size);
|
||||
AutoRegisterLastItem("selectable", label);
|
||||
return clicked;
|
||||
}
|
||||
|
||||
inline bool AutoMenuItem(const char* label, const char* shortcut = nullptr,
|
||||
bool selected = false, bool enabled = true) {
|
||||
bool activated = ImGui::MenuItem(label, shortcut, selected, enabled);
|
||||
AutoRegisterLastItem("menuitem", label);
|
||||
return activated;
|
||||
}
|
||||
|
||||
inline bool AutoMenuItem(const char* label, const char* shortcut, bool* p_selected,
|
||||
bool enabled = true) {
|
||||
bool activated = ImGui::MenuItem(label, shortcut, p_selected, enabled);
|
||||
AutoRegisterLastItem("menuitem", label);
|
||||
return activated;
|
||||
}
|
||||
|
||||
inline bool AutoBeginMenu(const char* label, bool enabled = true) {
|
||||
bool opened = ImGui::BeginMenu(label, enabled);
|
||||
if (opened) {
|
||||
AutoRegisterLastItem("menu", label, "Submenu");
|
||||
}
|
||||
return opened;
|
||||
}
|
||||
|
||||
inline bool AutoBeginTabItem(const char* label, bool* p_open = nullptr,
|
||||
ImGuiTabItemFlags flags = 0) {
|
||||
bool selected = ImGui::BeginTabItem(label, p_open, flags);
|
||||
if (selected) {
|
||||
AutoRegisterLastItem("tab", label);
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
inline bool AutoTreeNode(const char* label) {
|
||||
bool opened = ImGui::TreeNode(label);
|
||||
if (opened) {
|
||||
AutoRegisterLastItem("treenode", label);
|
||||
}
|
||||
return opened;
|
||||
}
|
||||
|
||||
inline bool AutoTreeNodeEx(const char* label, ImGuiTreeNodeFlags flags = 0) {
|
||||
bool opened = ImGui::TreeNodeEx(label, flags);
|
||||
if (opened) {
|
||||
AutoRegisterLastItem("treenode", label);
|
||||
}
|
||||
return opened;
|
||||
}
|
||||
|
||||
inline bool AutoCollapsingHeader(const char* label, ImGuiTreeNodeFlags flags = 0) {
|
||||
bool opened = ImGui::CollapsingHeader(label, flags);
|
||||
if (opened) {
|
||||
AutoRegisterLastItem("collapsing", label);
|
||||
}
|
||||
return opened;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Canvas-specific registration helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief Register a canvas widget after BeginChild or similar
|
||||
*
|
||||
* Canvases typically use BeginChild which doesn't have a return value,
|
||||
* so we provide a separate registration helper.
|
||||
*
|
||||
* @param canvas_name Name of the canvas (should match BeginChild name)
|
||||
* @param description Optional description of the canvas purpose
|
||||
*/
|
||||
inline void RegisterCanvas(const char* canvas_name, const std::string& description = "") {
|
||||
AutoRegisterLastItem("canvas", canvas_name, description);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Register a table after BeginTable
|
||||
*
|
||||
* @param table_name Name of the table (should match BeginTable name)
|
||||
* @param description Optional description
|
||||
*/
|
||||
inline void RegisterTable(const char* table_name, const std::string& description = "") {
|
||||
AutoRegisterLastItem("table", table_name, description);
|
||||
}
|
||||
|
||||
} // namespace gui
|
||||
} // namespace yaze
|
||||
|
||||
#endif // YAZE_APP_GUI_WIDGETS_WIDGET_AUTO_REGISTER_H_
|
||||
|
||||
446
src/app/gui/widgets/widget_id_registry.cc
Normal file
446
src/app/gui/widgets/widget_id_registry.cc
Normal file
@@ -0,0 +1,446 @@
|
||||
#include "app/gui/widgets/widget_id_registry.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
#include "absl/strings/ascii.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/strings/str_join.h"
|
||||
#include "absl/strings/str_split.h"
|
||||
#include "absl/strings/strip.h"
|
||||
#include "absl/time/clock.h"
|
||||
#include "imgui/imgui_internal.h" // For ImGuiContext internals
|
||||
|
||||
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) {
|
||||
// Only push ID if we're in an active ImGui frame with a valid window
|
||||
// This prevents crashes during editor initialization before ImGui begins its frame
|
||||
ImGuiContext* ctx = ImGui::GetCurrentContext();
|
||||
if (ctx && ctx->CurrentWindow && !ctx->Windows.empty()) {
|
||||
ImGui::PushID(name.c_str());
|
||||
id_stack_.push_back(name);
|
||||
}
|
||||
}
|
||||
|
||||
WidgetIdScope::~WidgetIdScope() {
|
||||
// Only pop if we successfully pushed
|
||||
if (!id_stack_.empty() && id_stack_.back() == name_) {
|
||||
ImGui::PopID();
|
||||
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::BeginFrame() {
|
||||
ImGuiContext* ctx = ImGui::GetCurrentContext();
|
||||
if (ctx) {
|
||||
current_frame_ = ctx->FrameCount;
|
||||
} else if (current_frame_ >= 0) {
|
||||
++current_frame_;
|
||||
} else {
|
||||
current_frame_ = 0;
|
||||
}
|
||||
frame_time_ = absl::Now();
|
||||
for (auto& [_, info] : widgets_) {
|
||||
info.seen_in_current_frame = false;
|
||||
}
|
||||
}
|
||||
|
||||
void WidgetIdRegistry::EndFrame() {
|
||||
for (auto& [_, info] : widgets_) {
|
||||
if (!info.seen_in_current_frame) {
|
||||
info.visible = false;
|
||||
info.enabled = false;
|
||||
info.bounds.valid = false;
|
||||
info.stale_frame_count += 1;
|
||||
} else {
|
||||
info.seen_in_current_frame = false;
|
||||
info.stale_frame_count = 0;
|
||||
}
|
||||
}
|
||||
TrimStaleEntries();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::string ExtractWindowFromPath(absl::string_view path) {
|
||||
size_t slash = path.find('/');
|
||||
if (slash == absl::string_view::npos) {
|
||||
return std::string(path);
|
||||
}
|
||||
return std::string(path.substr(0, slash));
|
||||
}
|
||||
|
||||
std::string ExtractLabelFromPath(absl::string_view path) {
|
||||
size_t colon = path.rfind(':');
|
||||
if (colon == absl::string_view::npos) {
|
||||
size_t slash = path.rfind('/');
|
||||
if (slash == absl::string_view::npos) {
|
||||
return std::string(path);
|
||||
}
|
||||
return std::string(path.substr(slash + 1));
|
||||
}
|
||||
return std::string(path.substr(colon + 1));
|
||||
}
|
||||
|
||||
std::string FormatTimestampUTC(const absl::Time& timestamp) {
|
||||
if (timestamp == absl::Time()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
std::chrono::system_clock::time_point chrono_time =
|
||||
absl::ToChronoTime(timestamp);
|
||||
std::time_t time_value = std::chrono::system_clock::to_time_t(chrono_time);
|
||||
|
||||
std::tm tm_buffer;
|
||||
#if defined(_WIN32)
|
||||
if (gmtime_s(&tm_buffer, &time_value) != 0) {
|
||||
return "";
|
||||
}
|
||||
#else
|
||||
if (gmtime_r(&time_value, &tm_buffer) == nullptr) {
|
||||
return "";
|
||||
}
|
||||
#endif
|
||||
|
||||
char buffer[32];
|
||||
if (std::snprintf(buffer, sizeof(buffer), "%04d-%02d-%02dT%02d:%02d:%02dZ",
|
||||
tm_buffer.tm_year + 1900, tm_buffer.tm_mon + 1,
|
||||
tm_buffer.tm_mday, tm_buffer.tm_hour, tm_buffer.tm_min,
|
||||
tm_buffer.tm_sec) <= 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return std::string(buffer);
|
||||
}
|
||||
|
||||
WidgetIdRegistry::WidgetBounds BoundsFromImGui(const ImRect& rect) {
|
||||
WidgetIdRegistry::WidgetBounds bounds;
|
||||
bounds.min_x = rect.Min.x;
|
||||
bounds.min_y = rect.Min.y;
|
||||
bounds.max_x = rect.Max.x;
|
||||
bounds.max_y = rect.Max.y;
|
||||
bounds.valid = true;
|
||||
return bounds;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void WidgetIdRegistry::RegisterWidget(const std::string& full_path,
|
||||
const std::string& type,
|
||||
ImGuiID imgui_id,
|
||||
const std::string& description,
|
||||
const WidgetMetadata& metadata) {
|
||||
WidgetInfo& info = widgets_[full_path];
|
||||
info.full_path = full_path;
|
||||
info.type = type;
|
||||
info.imgui_id = imgui_id;
|
||||
info.description = description;
|
||||
|
||||
if (metadata.label.has_value()) {
|
||||
info.label = NormalizeLabel(*metadata.label);
|
||||
} else {
|
||||
info.label = NormalizeLabel(ExtractLabelFromPath(full_path));
|
||||
}
|
||||
if (info.label.empty()) {
|
||||
info.label = ExtractLabelFromPath(full_path);
|
||||
}
|
||||
|
||||
if (metadata.window_name.has_value()) {
|
||||
info.window_name = NormalizeLabel(*metadata.window_name);
|
||||
} else {
|
||||
info.window_name = NormalizePathSegment(ExtractWindowFromPath(full_path));
|
||||
}
|
||||
if (info.window_name.empty()) {
|
||||
info.window_name = ExtractWindowFromPath(full_path);
|
||||
}
|
||||
|
||||
ImGuiContext* ctx = ImGui::GetCurrentContext();
|
||||
absl::Time observed_at = absl::Now();
|
||||
|
||||
if (ctx) {
|
||||
const ImGuiLastItemData& last = ctx->LastItemData;
|
||||
if (metadata.visible.has_value()) {
|
||||
info.visible = *metadata.visible;
|
||||
} else {
|
||||
info.visible = (last.StatusFlags & ImGuiItemStatusFlags_Visible) != 0;
|
||||
}
|
||||
|
||||
if (metadata.enabled.has_value()) {
|
||||
info.enabled = *metadata.enabled;
|
||||
} else {
|
||||
info.enabled = (last.ItemFlags & ImGuiItemFlags_Disabled) == 0;
|
||||
}
|
||||
|
||||
if (metadata.bounds.has_value()) {
|
||||
info.bounds = *metadata.bounds;
|
||||
} else {
|
||||
info.bounds = BoundsFromImGui(last.Rect);
|
||||
}
|
||||
|
||||
info.last_seen_frame = ctx->FrameCount;
|
||||
} else {
|
||||
info.visible = metadata.visible.value_or(true);
|
||||
info.enabled = metadata.enabled.value_or(true);
|
||||
if (metadata.bounds.has_value()) {
|
||||
info.bounds = *metadata.bounds;
|
||||
} else {
|
||||
info.bounds.valid = false;
|
||||
}
|
||||
if (current_frame_ >= 0) {
|
||||
info.last_seen_frame = current_frame_;
|
||||
}
|
||||
}
|
||||
|
||||
info.last_seen_time = observed_at;
|
||||
info.seen_in_current_frame = true;
|
||||
info.stale_frame_count = 0;
|
||||
}
|
||||
|
||||
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();
|
||||
current_frame_ = -1;
|
||||
}
|
||||
|
||||
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,\n", info.imgui_id);
|
||||
ss << absl::StrFormat(" \"label\": \"%s\",\n", info.label);
|
||||
ss << absl::StrFormat(" \"window\": \"%s\",\n", info.window_name);
|
||||
ss << absl::StrFormat(" \"visible\": %s,\n",
|
||||
info.visible ? "true" : "false");
|
||||
ss << absl::StrFormat(" \"enabled\": %s,\n",
|
||||
info.enabled ? "true" : "false");
|
||||
if (info.bounds.valid) {
|
||||
ss << absl::StrFormat(
|
||||
" \"bounds\": {\"min\": [%0.1f, %0.1f], \"max\": [%0.1f, %0.1f]},\n",
|
||||
info.bounds.min_x, info.bounds.min_y, info.bounds.max_x,
|
||||
info.bounds.max_y);
|
||||
} else {
|
||||
ss << " \"bounds\": null,\n";
|
||||
}
|
||||
ss << absl::StrFormat(" \"last_seen_frame\": %d,\n",
|
||||
info.last_seen_frame);
|
||||
std::string iso_timestamp = FormatTimestampUTC(info.last_seen_time);
|
||||
ss << absl::StrFormat(" \"last_seen_at\": \"%s\",\n",
|
||||
iso_timestamp);
|
||||
ss << absl::StrFormat(" \"stale\": %s",
|
||||
info.stale_frame_count > 0 ? "true" : "false");
|
||||
if (!info.description.empty()) {
|
||||
ss << ",\n";
|
||||
ss << absl::StrFormat(" \"description\": \"%s\"\n",
|
||||
info.description);
|
||||
} else {
|
||||
ss << "\n";
|
||||
}
|
||||
ss << " }";
|
||||
}
|
||||
|
||||
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);
|
||||
ss << absl::StrFormat(" label: \"%s\"\n", info.label);
|
||||
ss << absl::StrFormat(" window: \"%s\"\n", info.window_name);
|
||||
ss << absl::StrFormat(" visible: %s\n", info.visible ? "true" : "false");
|
||||
ss << absl::StrFormat(" enabled: %s\n", info.enabled ? "true" : "false");
|
||||
if (info.bounds.valid) {
|
||||
ss << " bounds:\n";
|
||||
ss << absl::StrFormat(" min: [%0.1f, %0.1f]\n", info.bounds.min_x,
|
||||
info.bounds.min_y);
|
||||
ss << absl::StrFormat(" max: [%0.1f, %0.1f]\n", info.bounds.max_x,
|
||||
info.bounds.max_y);
|
||||
}
|
||||
ss << absl::StrFormat(" last_seen_frame: %d\n", info.last_seen_frame);
|
||||
std::string iso_timestamp = FormatTimestampUTC(info.last_seen_time);
|
||||
ss << absl::StrFormat(" last_seen_at: %s\n", iso_timestamp);
|
||||
ss << absl::StrFormat(" stale: %s\n",
|
||||
info.stale_frame_count > 0 ? "true" : "false");
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
std::string WidgetIdRegistry::NormalizeLabel(absl::string_view label) {
|
||||
size_t pos = label.find("##");
|
||||
if (pos != absl::string_view::npos) {
|
||||
label = label.substr(0, pos);
|
||||
}
|
||||
std::string sanitized = std::string(absl::StripAsciiWhitespace(label));
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
std::string WidgetIdRegistry::NormalizePathSegment(absl::string_view segment) {
|
||||
return NormalizeLabel(segment);
|
||||
}
|
||||
|
||||
void WidgetIdRegistry::TrimStaleEntries() {
|
||||
auto it = widgets_.begin();
|
||||
while (it != widgets_.end()) {
|
||||
if (ShouldPrune(it->second)) {
|
||||
it = widgets_.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool WidgetIdRegistry::ShouldPrune(const WidgetInfo& info) const {
|
||||
if (info.last_seen_frame < 0 || stale_frame_limit_ <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (current_frame_ < 0) {
|
||||
return false;
|
||||
}
|
||||
return (current_frame_ - info.last_seen_frame) > stale_frame_limit_;
|
||||
}
|
||||
|
||||
} // namespace gui
|
||||
} // namespace yaze
|
||||
176
src/app/gui/widgets/widget_id_registry.h
Normal file
176
src/app/gui/widgets/widget_id_registry.h
Normal file
@@ -0,0 +1,176 @@
|
||||
#ifndef YAZE_APP_GUI_WIDGETS_WIDGET_ID_REGISTRY_H_
|
||||
#define YAZE_APP_GUI_WIDGETS_WIDGET_ID_REGISTRY_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/time/time.h"
|
||||
#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 WidgetBounds {
|
||||
float min_x = 0.0f;
|
||||
float min_y = 0.0f;
|
||||
float max_x = 0.0f;
|
||||
float max_y = 0.0f;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
struct WidgetMetadata {
|
||||
std::optional<std::string> label;
|
||||
std::optional<std::string> window_name;
|
||||
std::optional<bool> visible;
|
||||
std::optional<bool> enabled;
|
||||
std::optional<WidgetBounds> bounds;
|
||||
};
|
||||
|
||||
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
|
||||
std::string label; // Sanitized display label (without IDs/icons)
|
||||
std::string window_name; // Window this widget was last seen in
|
||||
bool visible = true; // Visibility in the most recent frame
|
||||
bool enabled = true; // Enabled state in the most recent frame
|
||||
WidgetBounds bounds; // Bounding box in screen space
|
||||
int last_seen_frame = -1;
|
||||
absl::Time last_seen_time;
|
||||
bool seen_in_current_frame = false;
|
||||
int stale_frame_count = 0;
|
||||
};
|
||||
|
||||
static WidgetIdRegistry& Instance();
|
||||
|
||||
// Frame lifecycle - call once per ImGui frame
|
||||
void BeginFrame();
|
||||
void EndFrame();
|
||||
|
||||
// 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 = "",
|
||||
const WidgetMetadata& metadata = WidgetMetadata());
|
||||
|
||||
// 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;
|
||||
|
||||
// Helper utilities for consistent naming
|
||||
static std::string NormalizeLabel(absl::string_view label);
|
||||
static std::string NormalizePathSegment(absl::string_view segment);
|
||||
|
||||
private:
|
||||
WidgetIdRegistry() = default;
|
||||
void TrimStaleEntries();
|
||||
bool ShouldPrune(const WidgetInfo& info) const;
|
||||
|
||||
std::unordered_map<std::string, WidgetInfo> widgets_;
|
||||
int current_frame_ = -1;
|
||||
absl::Time frame_time_;
|
||||
int stale_frame_limit_ = 600; // frames before pruning a widget
|
||||
};
|
||||
|
||||
// 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_WIDGETS_WIDGET_ID_REGISTRY_H_
|
||||
Reference in New Issue
Block a user