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

@@ -7,6 +7,7 @@
#include "app/editor/editor_manager.h"
#include "app/gui/background_renderer.h"
#include "app/gui/theme_manager.h"
#include "app/gui/widget_id_registry.h"
#include "imgui/backends/imgui_impl_sdl2.h"
#include "imgui/backends/imgui_impl_sdlrenderer2.h"
#include "imgui/imgui.h"
@@ -65,7 +66,10 @@ absl::Status Controller::OnLoad() {
ImGui::End();
#endif
RETURN_IF_ERROR(editor_manager_.Update());
gui::WidgetIdRegistry::Instance().BeginFrame();
absl::Status update_status = editor_manager_.Update();
gui::WidgetIdRegistry::Instance().EndFrame();
RETURN_IF_ERROR(update_status);
return absl::OkStatus();
}

View File

@@ -290,6 +290,9 @@ message DiscoveredWidget {
bool enabled = 7; // Currently enabled for interaction
WidgetBounds bounds = 8; // Bounding rectangle in screen coordinates
uint32 widget_id = 9; // ImGui ID (debugging / direct access)
int64 last_seen_frame = 10; // Frame number when widget was last observed
int64 last_seen_at_ms = 11; // Wall-clock timestamp of last observation
bool stale = 12; // True if widget not seen in the current frame
}
message DiscoveredWindow {

View File

@@ -1,6 +1,7 @@
#include "app/core/service/widget_discovery_service.h"
#include <algorithm>
#include <cstdint>
#include <map>
#include <string>
#include <utility>
@@ -17,7 +18,7 @@ namespace {
struct WindowEntry {
int index = -1;
bool visible = true;
bool visible = false;
};
} // namespace
@@ -57,39 +58,17 @@ void WidgetDiscoveryService::CollectWidgets(
continue;
}
const std::string window_name = ExtractWindowName(path);
const std::string window_name =
info.window_name.empty() ? ExtractWindowName(path) : info.window_name;
if (!MatchesWindow(window_name, window_filter_lower)) {
continue;
}
auto [it, inserted] = window_lookup.emplace(window_name, WindowEntry{});
WindowEntry& entry = it->second;
const std::string label =
info.label.empty() ? ExtractLabel(path) : info.label;
if (inserted) {
#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
if (ctx) {
ImGuiTestItemInfo window_info =
ctx->WindowInfo(window_name.c_str(), ImGuiTestOpFlags_NoError);
entry.visible = (window_info.ID != 0);
}
#endif
}
if (!include_invisible && !entry.visible) {
continue;
}
if (entry.index == -1) {
DiscoveredWindow* window_proto = response->add_windows();
entry.index = response->windows_size() - 1;
window_proto->set_name(window_name);
window_proto->set_visible(entry.visible);
}
const std::string label = ExtractLabel(path);
bool widget_enabled = true;
bool widget_visible = entry.visible;
bool widget_enabled = info.enabled;
bool widget_visible = info.visible;
#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
bool has_item_info = false;
@@ -103,8 +82,18 @@ void WidgetDiscoveryService::CollectWidgets(
widget_enabled = (item_info.ItemFlags & ImGuiItemFlags_Disabled) == 0;
}
}
#else
(void)ctx;
#endif
auto [it, inserted] = window_lookup.emplace(window_name, WindowEntry{});
WindowEntry& entry = it->second;
if (inserted) {
entry.visible = widget_visible;
} else {
entry.visible = entry.visible || widget_visible;
}
if (!include_invisible && !widget_visible) {
continue;
}
@@ -112,7 +101,16 @@ void WidgetDiscoveryService::CollectWidgets(
continue;
}
if (entry.index == -1) {
DiscoveredWindow* window_proto = response->add_windows();
entry.index = response->windows_size() - 1;
window_proto->set_name(window_name);
window_proto->set_visible(entry.visible);
}
auto* window_proto = response->mutable_windows(entry.index);
window_proto->set_visible(entry.visible);
auto* widget_proto = window_proto->add_widgets();
widget_proto->set_path(path);
widget_proto->set_label(label);
@@ -126,19 +124,34 @@ void WidgetDiscoveryService::CollectWidgets(
widget_proto->set_description(info.description);
}
WidgetBounds* bounds = widget_proto->mutable_bounds();
if (info.bounds.valid) {
WidgetBounds* bounds = widget_proto->mutable_bounds();
bounds->set_min_x(info.bounds.min_x);
bounds->set_min_y(info.bounds.min_y);
bounds->set_max_x(info.bounds.max_x);
bounds->set_max_y(info.bounds.max_y);
#if defined(YAZE_ENABLE_IMGUI_TEST_ENGINE) && YAZE_ENABLE_IMGUI_TEST_ENGINE
if (ctx && has_item_info) {
} else if (ctx && has_item_info) {
WidgetBounds* bounds = widget_proto->mutable_bounds();
bounds->set_min_x(item_info.RectFull.Min.x);
bounds->set_min_y(item_info.RectFull.Min.y);
bounds->set_max_x(item_info.RectFull.Max.x);
bounds->set_max_y(item_info.RectFull.Max.y);
} else {
(void)ctx;
}
#else
(void)ctx;
} else {
(void)ctx;
#endif
}
widget_proto->set_last_seen_frame(info.last_seen_frame);
int64_t last_seen_ms = 0;
if (info.last_seen_time != absl::Time()) {
last_seen_ms = absl::ToUnixMillis(info.last_seen_time);
}
widget_proto->set_last_seen_at_ms(last_seen_ms);
widget_proto->set_stale(info.stale_frame_count > 0);
++total_widgets;
}

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