feat: Enhance widget discovery with telemetry data and improve output formatting

This commit is contained in:
scawful
2025-10-02 22:50:47 -04:00
parent 21074f6445
commit c202aa9fa4
11 changed files with 457 additions and 78 deletions

View File

@@ -1,13 +1,19 @@
#include "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 {
@@ -54,11 +60,170 @@ WidgetIdRegistry& 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) {
WidgetInfo info{full_path, type, imgui_id, description};
widgets_[full_path] = info;
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(
@@ -113,7 +278,10 @@ const WidgetIdRegistry::WidgetInfo* WidgetIdRegistry::GetWidgetInfo(
return nullptr;
}
void WidgetIdRegistry::Clear() { widgets_.clear(); }
void WidgetIdRegistry::Clear() {
widgets_.clear();
current_frame_ = -1;
}
std::string WidgetIdRegistry::ExportCatalog(const std::string& format) const {
std::ostringstream ss;
@@ -130,12 +298,36 @@ std::string WidgetIdRegistry::ExportCatalog(const std::string& format) const {
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);
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\"", info.description);
ss << absl::StrFormat(" \"description\": \"%s\"\n",
info.description);
} else {
ss << "\n";
}
ss << "\n }";
ss << " }";
}
ss << "\n ]\n";
@@ -148,6 +340,22 @@ std::string WidgetIdRegistry::ExportCatalog(const std::string& format) const {
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, '/');
@@ -200,5 +408,39 @@ void WidgetIdRegistry::ExportCatalogToFile(const std::string& output_file,
}
}
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

View File

@@ -1,10 +1,14 @@
#ifndef YAZE_APP_GUI_WIDGET_ID_REGISTRY_H_
#define YAZE_APP_GUI_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 {
@@ -62,20 +66,50 @@ class WidgetIdScope {
*/
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 std::string& description = "",
const WidgetMetadata& metadata = WidgetMetadata());
// Query widgets for test automation
std::vector<std::string> FindWidgets(const std::string& pattern) const;
@@ -96,9 +130,19 @@ class WidgetIdRegistry {
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