10#include "absl/strings/ascii.h"
11#include "absl/strings/str_cat.h"
12#include "absl/strings/str_format.h"
13#include "absl/strings/str_join.h"
14#include "absl/strings/str_split.h"
15#include "absl/strings/strip.h"
16#include "absl/time/clock.h"
17#include "imgui/imgui_internal.h"
29 ImGuiContext* ctx = ImGui::GetCurrentContext();
30 if (ctx && ctx->CurrentWindow && !ctx->Windows.empty()) {
31 ImGui::PushID(name.c_str());
49 const std::string& widget_name)
const {
54 return absl::StrCat(path, widget_type,
":", widget_name);
65 absl::MutexLock lock(&
mutex_);
66 ImGuiContext* ctx = ImGui::GetCurrentContext();
68 current_frame_ = ctx->FrameCount;
69 }
else if (current_frame_ >= 0) {
74 frame_time_ = absl::Now();
75 for (
auto& [_, info] : widgets_) {
76 info.seen_in_current_frame =
false;
81 absl::MutexLock lock(&
mutex_);
82 for (
auto& [_, info] : widgets_) {
83 if (!info.seen_in_current_frame) {
86 info.bounds.valid =
false;
87 info.stale_frame_count += 1;
89 info.seen_in_current_frame =
false;
90 info.stale_frame_count = 0;
99 size_t slash = path.find(
'/');
100 if (slash == absl::string_view::npos) {
101 return std::string(path);
103 return std::string(path.substr(0, slash));
107 size_t colon = path.rfind(
':');
108 if (colon == absl::string_view::npos) {
109 size_t slash = path.rfind(
'/');
110 if (slash == absl::string_view::npos) {
111 return std::string(path);
113 return std::string(path.substr(slash + 1));
115 return std::string(path.substr(colon + 1));
119 if (timestamp == absl::Time()) {
123 std::chrono::system_clock::time_point chrono_time =
124 absl::ToChronoTime(timestamp);
125 std::time_t time_value = std::chrono::system_clock::to_time_t(chrono_time);
129 if (gmtime_s(&tm_buffer, &time_value) != 0) {
133 if (gmtime_r(&time_value, &tm_buffer) ==
nullptr) {
139 if (std::snprintf(buffer,
sizeof(buffer),
"%04d-%02d-%02dT%02d:%02d:%02dZ",
140 tm_buffer.tm_year + 1900, tm_buffer.tm_mon + 1,
141 tm_buffer.tm_mday, tm_buffer.tm_hour, tm_buffer.tm_min,
142 tm_buffer.tm_sec) <= 0) {
146 return std::string(buffer);
151 bounds.
min_x = rect.Min.x;
152 bounds.
min_y = rect.Min.y;
153 bounds.
max_x = rect.Max.x;
154 bounds.
max_y = rect.Max.y;
162 const std::string& type, ImGuiID imgui_id,
163 const std::string& description,
165 absl::MutexLock lock(&
mutex_);
172 if (metadata.
label.has_value()) {
177 if (info.
label.empty()) {
178 info.
label = ExtractLabelFromPath(full_path);
187 info.
window_name = ExtractWindowFromPath(full_path);
190 ImGuiContext* ctx = ImGui::GetCurrentContext();
191 absl::Time observed_at = absl::Now();
194 const ImGuiLastItemData& last = ctx->LastItemData;
195 if (metadata.
visible.has_value()) {
198 info.
visible = (last.StatusFlags & ImGuiItemStatusFlags_Visible) != 0;
201 if (metadata.
enabled.has_value()) {
204 info.
enabled = (last.ItemFlags & ImGuiItemFlags_Disabled) == 0;
207 if (metadata.
bounds.has_value()) {
210 info.
bounds = BoundsFromImGui(last.Rect);
217 if (metadata.
bounds.has_value()) {
222 if (current_frame_ >= 0) {
233 const std::string& pattern)
const {
234 absl::MutexLock lock(&
mutex_);
235 std::vector<std::string> matches;
239 for (
const auto& [path, info] : widgets_) {
242 if (pattern ==
"*") {
244 }
else if (pattern.find(
'*') != std::string::npos) {
246 std::string search = pattern;
247 search.erase(std::remove(search.begin(), search.end(),
'*'),
249 if (!search.empty() && path.find(search) != std::string::npos) {
254 if (path == pattern) {
260 matches.push_back(path);
265 std::sort(matches.begin(), matches.end());
270 absl::MutexLock lock(&
mutex_);
271 auto it = widgets_.find(full_path);
272 if (it != widgets_.end()) {
273 return it->second.imgui_id;
279 const std::string& full_path)
const {
280 absl::MutexLock lock(&
mutex_);
281 auto it = widgets_.find(full_path);
282 if (it != widgets_.end()) {
288std::unordered_map<std::string, WidgetIdRegistry::WidgetInfo>
290 absl::MutexLock lock(&
mutex_);
295 absl::MutexLock lock(&
mutex_);
301 absl::MutexLock lock(&
mutex_);
302 std::ostringstream ss;
304 if (format ==
"json") {
306 ss <<
" \"widgets\": [\n";
309 for (
const auto& [path, info] : widgets_) {
315 ss << absl::StrFormat(
" \"path\": \"%s\",\n", path);
316 ss << absl::StrFormat(
" \"type\": \"%s\",\n", info.type);
317 ss << absl::StrFormat(
" \"imgui_id\": %u,\n", info.imgui_id);
318 ss << absl::StrFormat(
" \"label\": \"%s\",\n", info.label);
319 ss << absl::StrFormat(
" \"window\": \"%s\",\n", info.window_name);
320 ss << absl::StrFormat(
" \"visible\": %s,\n",
321 info.visible ?
"true" :
"false");
322 ss << absl::StrFormat(
" \"enabled\": %s,\n",
323 info.enabled ?
"true" :
"false");
324 if (info.bounds.valid) {
325 ss << absl::StrFormat(
326 " \"bounds\": {\"min\": [%0.1f, %0.1f], \"max\": [%0.1f, "
328 info.bounds.min_x, info.bounds.min_y, info.bounds.max_x,
331 ss <<
" \"bounds\": null,\n";
333 ss << absl::StrFormat(
" \"last_seen_frame\": %d,\n",
334 info.last_seen_frame);
335 std::string iso_timestamp = FormatTimestampUTC(info.last_seen_time);
336 ss << absl::StrFormat(
" \"last_seen_at\": \"%s\",\n", iso_timestamp);
337 ss << absl::StrFormat(
" \"stale\": %s",
338 info.stale_frame_count > 0 ?
"true" :
"false");
339 if (!info.description.empty()) {
341 ss << absl::StrFormat(
" \"description\": \"%s\"\n",
355 for (
const auto& [path, info] : widgets_) {
356 ss << absl::StrFormat(
" - path: \"%s\"\n", path);
357 ss << absl::StrFormat(
" type: %s\n", info.type);
358 ss << absl::StrFormat(
" imgui_id: %u\n", info.imgui_id);
359 ss << absl::StrFormat(
" label: \"%s\"\n", info.label);
360 ss << absl::StrFormat(
" window: \"%s\"\n", info.window_name);
361 ss << absl::StrFormat(
" visible: %s\n",
362 info.visible ?
"true" :
"false");
363 ss << absl::StrFormat(
" enabled: %s\n",
364 info.enabled ?
"true" :
"false");
365 if (info.bounds.valid) {
367 ss << absl::StrFormat(
" min: [%0.1f, %0.1f]\n", info.bounds.min_x,
369 ss << absl::StrFormat(
" max: [%0.1f, %0.1f]\n", info.bounds.max_x,
372 ss << absl::StrFormat(
" last_seen_frame: %d\n", info.last_seen_frame);
373 std::string iso_timestamp = FormatTimestampUTC(info.last_seen_time);
374 ss << absl::StrFormat(
" last_seen_at: %s\n", iso_timestamp);
375 ss << absl::StrFormat(
" stale: %s\n",
376 info.stale_frame_count > 0 ?
"true" :
"false");
379 std::vector<std::string> segments = absl::StrSplit(path,
'/');
380 if (!segments.empty()) {
382 if (segments.size() > 0) {
383 ss << absl::StrFormat(
" editor: %s\n", segments[0]);
385 if (segments.size() > 1) {
386 ss << absl::StrFormat(
" tab: %s\n", segments[1]);
388 if (segments.size() > 2) {
389 ss << absl::StrFormat(
" section: %s\n", segments[2]);
393 if (!info.description.empty()) {
394 ss << absl::StrFormat(
" description: %s\n", info.description);
399 if (info.type ==
"button") {
401 }
else if (info.type ==
"input") {
403 }
else if (info.type ==
"canvas") {
404 ss <<
"click, drag, scroll";
405 }
else if (info.type ==
"checkbox") {
407 }
else if (info.type ==
"slider") {
420 const std::string& format)
const {
422 std::ofstream file(output_file);
423 if (file.is_open()) {
430 size_t pos = label.find(
"##");
431 if (pos != absl::string_view::npos) {
432 label = label.substr(0, pos);
434 std::string sanitized = std::string(absl::StripAsciiWhitespace(label));
443 auto it = widgets_.begin();
444 while (it != widgets_.end()) {
446 it = widgets_.erase(it);
457 if (current_frame_ < 0) {