yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
widget_id_registry.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <chrono>
5#include <cstdio>
6#include <ctime>
7#include <fstream>
8#include <sstream>
9
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" // For ImGuiContext internals
18
19namespace yaze {
20namespace gui {
21
22// Thread-local storage for ID stack
23thread_local std::vector<std::string> WidgetIdScope::id_stack_;
24
25WidgetIdScope::WidgetIdScope(const std::string& name) : name_(name) {
26 // Only push ID if we're in an active ImGui frame with a valid window
27 // This prevents crashes during editor initialization before ImGui begins its
28 // frame
29 ImGuiContext* ctx = ImGui::GetCurrentContext();
30 if (ctx && ctx->CurrentWindow && !ctx->Windows.empty()) {
31 ImGui::PushID(name.c_str());
32 id_stack_.push_back(name);
33 }
34}
35
37 // Only pop if we successfully pushed
38 if (!id_stack_.empty() && id_stack_.back() == name_) {
39 ImGui::PopID();
40 id_stack_.pop_back();
41 }
42}
43
44std::string WidgetIdScope::GetFullPath() const {
45 return absl::StrJoin(id_stack_, "/");
46}
47
48std::string WidgetIdScope::GetWidgetPath(const std::string& widget_type,
49 const std::string& widget_name) const {
50 std::string path = GetFullPath();
51 if (!path.empty()) {
52 path += "/";
53 }
54 return absl::StrCat(path, widget_type, ":", widget_name);
55}
56
57// WidgetIdRegistry implementation
58
60 static WidgetIdRegistry instance;
61 return instance;
62}
63
65 ImGuiContext* ctx = ImGui::GetCurrentContext();
66 if (ctx) {
67 current_frame_ = ctx->FrameCount;
68 } else if (current_frame_ >= 0) {
70 } else {
72 }
73 frame_time_ = absl::Now();
74 for (auto& [_, info] : widgets_) {
75 info.seen_in_current_frame = false;
76 }
77}
78
80 for (auto& [_, info] : widgets_) {
81 if (!info.seen_in_current_frame) {
82 info.visible = false;
83 info.enabled = false;
84 info.bounds.valid = false;
85 info.stale_frame_count += 1;
86 } else {
87 info.seen_in_current_frame = false;
88 info.stale_frame_count = 0;
89 }
90 }
92}
93
94namespace {
95
96std::string ExtractWindowFromPath(absl::string_view path) {
97 size_t slash = path.find('/');
98 if (slash == absl::string_view::npos) {
99 return std::string(path);
100 }
101 return std::string(path.substr(0, slash));
102}
103
104std::string ExtractLabelFromPath(absl::string_view path) {
105 size_t colon = path.rfind(':');
106 if (colon == absl::string_view::npos) {
107 size_t slash = path.rfind('/');
108 if (slash == absl::string_view::npos) {
109 return std::string(path);
110 }
111 return std::string(path.substr(slash + 1));
112 }
113 return std::string(path.substr(colon + 1));
114}
115
116std::string FormatTimestampUTC(const absl::Time& timestamp) {
117 if (timestamp == absl::Time()) {
118 return "";
119 }
120
121 std::chrono::system_clock::time_point chrono_time =
122 absl::ToChronoTime(timestamp);
123 std::time_t time_value = std::chrono::system_clock::to_time_t(chrono_time);
124
125 std::tm tm_buffer;
126#if defined(_WIN32)
127 if (gmtime_s(&tm_buffer, &time_value) != 0) {
128 return "";
129 }
130#else
131 if (gmtime_r(&time_value, &tm_buffer) == nullptr) {
132 return "";
133 }
134#endif
135
136 char buffer[32];
137 if (std::snprintf(buffer, sizeof(buffer), "%04d-%02d-%02dT%02d:%02d:%02dZ",
138 tm_buffer.tm_year + 1900, tm_buffer.tm_mon + 1,
139 tm_buffer.tm_mday, tm_buffer.tm_hour, tm_buffer.tm_min,
140 tm_buffer.tm_sec) <= 0) {
141 return "";
142 }
143
144 return std::string(buffer);
145}
146
149 bounds.min_x = rect.Min.x;
150 bounds.min_y = rect.Min.y;
151 bounds.max_x = rect.Max.x;
152 bounds.max_y = rect.Max.y;
153 bounds.valid = true;
154 return bounds;
155}
156
157} // namespace
158
159void WidgetIdRegistry::RegisterWidget(const std::string& full_path,
160 const std::string& type, ImGuiID imgui_id,
161 const std::string& description,
162 const WidgetMetadata& metadata) {
163 WidgetInfo& info = widgets_[full_path];
164 info.full_path = full_path;
165 info.type = type;
166 info.imgui_id = imgui_id;
167 info.description = description;
168
169 if (metadata.label.has_value()) {
170 info.label = NormalizeLabel(*metadata.label);
171 } else {
172 info.label = NormalizeLabel(ExtractLabelFromPath(full_path));
173 }
174 if (info.label.empty()) {
175 info.label = ExtractLabelFromPath(full_path);
176 }
177
178 if (metadata.window_name.has_value()) {
179 info.window_name = NormalizeLabel(*metadata.window_name);
180 } else {
181 info.window_name = NormalizePathSegment(ExtractWindowFromPath(full_path));
182 }
183 if (info.window_name.empty()) {
184 info.window_name = ExtractWindowFromPath(full_path);
185 }
186
187 ImGuiContext* ctx = ImGui::GetCurrentContext();
188 absl::Time observed_at = absl::Now();
189
190 if (ctx) {
191 const ImGuiLastItemData& last = ctx->LastItemData;
192 if (metadata.visible.has_value()) {
193 info.visible = *metadata.visible;
194 } else {
195 info.visible = (last.StatusFlags & ImGuiItemStatusFlags_Visible) != 0;
196 }
197
198 if (metadata.enabled.has_value()) {
199 info.enabled = *metadata.enabled;
200 } else {
201 info.enabled = (last.ItemFlags & ImGuiItemFlags_Disabled) == 0;
202 }
203
204 if (metadata.bounds.has_value()) {
205 info.bounds = *metadata.bounds;
206 } else {
207 info.bounds = BoundsFromImGui(last.Rect);
208 }
209
210 info.last_seen_frame = ctx->FrameCount;
211 } else {
212 info.visible = metadata.visible.value_or(true);
213 info.enabled = metadata.enabled.value_or(true);
214 if (metadata.bounds.has_value()) {
215 info.bounds = *metadata.bounds;
216 } else {
217 info.bounds.valid = false;
218 }
219 if (current_frame_ >= 0) {
221 }
222 }
223
224 info.last_seen_time = observed_at;
225 info.seen_in_current_frame = true;
226 info.stale_frame_count = 0;
227}
228
229std::vector<std::string> WidgetIdRegistry::FindWidgets(
230 const std::string& pattern) const {
231 std::vector<std::string> matches;
232
233 // Simple glob-style pattern matching
234 // Supports: "*" (any), "?" (single char), exact matches
235 for (const auto& [path, info] : widgets_) {
236 bool match = false;
237
238 if (pattern == "*") {
239 match = true;
240 } else if (pattern.find('*') != std::string::npos) {
241 // Wildcard pattern - convert to simple substring match for now
242 std::string search = pattern;
243 search.erase(std::remove(search.begin(), search.end(), '*'),
244 search.end());
245 if (!search.empty() && path.find(search) != std::string::npos) {
246 match = true;
247 }
248 } else {
249 // Exact match
250 if (path == pattern) {
251 match = true;
252 }
253 }
254
255 if (match) {
256 matches.push_back(path);
257 }
258 }
259
260 // Sort for consistent ordering
261 std::sort(matches.begin(), matches.end());
262 return matches;
263}
264
265ImGuiID WidgetIdRegistry::GetWidgetId(const std::string& full_path) const {
266 auto it = widgets_.find(full_path);
267 if (it != widgets_.end()) {
268 return it->second.imgui_id;
269 }
270 return 0;
271}
272
274 const std::string& full_path) const {
275 auto it = widgets_.find(full_path);
276 if (it != widgets_.end()) {
277 return &it->second;
278 }
279 return nullptr;
280}
281
283 widgets_.clear();
284 current_frame_ = -1;
285}
286
287std::string WidgetIdRegistry::ExportCatalog(const std::string& format) const {
288 std::ostringstream ss;
289
290 if (format == "json") {
291 ss << "{\n";
292 ss << " \"widgets\": [\n";
293
294 bool first = true;
295 for (const auto& [path, info] : widgets_) {
296 if (!first)
297 ss << ",\n";
298 first = false;
299
300 ss << " {\n";
301 ss << absl::StrFormat(" \"path\": \"%s\",\n", path);
302 ss << absl::StrFormat(" \"type\": \"%s\",\n", info.type);
303 ss << absl::StrFormat(" \"imgui_id\": %u,\n", info.imgui_id);
304 ss << absl::StrFormat(" \"label\": \"%s\",\n", info.label);
305 ss << absl::StrFormat(" \"window\": \"%s\",\n", info.window_name);
306 ss << absl::StrFormat(" \"visible\": %s,\n",
307 info.visible ? "true" : "false");
308 ss << absl::StrFormat(" \"enabled\": %s,\n",
309 info.enabled ? "true" : "false");
310 if (info.bounds.valid) {
311 ss << absl::StrFormat(
312 " \"bounds\": {\"min\": [%0.1f, %0.1f], \"max\": [%0.1f, "
313 "%0.1f]},\n",
314 info.bounds.min_x, info.bounds.min_y, info.bounds.max_x,
315 info.bounds.max_y);
316 } else {
317 ss << " \"bounds\": null,\n";
318 }
319 ss << absl::StrFormat(" \"last_seen_frame\": %d,\n",
320 info.last_seen_frame);
321 std::string iso_timestamp = FormatTimestampUTC(info.last_seen_time);
322 ss << absl::StrFormat(" \"last_seen_at\": \"%s\",\n", iso_timestamp);
323 ss << absl::StrFormat(" \"stale\": %s",
324 info.stale_frame_count > 0 ? "true" : "false");
325 if (!info.description.empty()) {
326 ss << ",\n";
327 ss << absl::StrFormat(" \"description\": \"%s\"\n",
328 info.description);
329 } else {
330 ss << "\n";
331 }
332 ss << " }";
333 }
334
335 ss << "\n ]\n";
336 ss << "}\n";
337 } else {
338 // YAML format (default)
339 ss << "widgets:\n";
340
341 for (const auto& [path, info] : widgets_) {
342 ss << absl::StrFormat(" - path: \"%s\"\n", path);
343 ss << absl::StrFormat(" type: %s\n", info.type);
344 ss << absl::StrFormat(" imgui_id: %u\n", info.imgui_id);
345 ss << absl::StrFormat(" label: \"%s\"\n", info.label);
346 ss << absl::StrFormat(" window: \"%s\"\n", info.window_name);
347 ss << absl::StrFormat(" visible: %s\n",
348 info.visible ? "true" : "false");
349 ss << absl::StrFormat(" enabled: %s\n",
350 info.enabled ? "true" : "false");
351 if (info.bounds.valid) {
352 ss << " bounds:\n";
353 ss << absl::StrFormat(" min: [%0.1f, %0.1f]\n", info.bounds.min_x,
354 info.bounds.min_y);
355 ss << absl::StrFormat(" max: [%0.1f, %0.1f]\n", info.bounds.max_x,
356 info.bounds.max_y);
357 }
358 ss << absl::StrFormat(" last_seen_frame: %d\n", info.last_seen_frame);
359 std::string iso_timestamp = FormatTimestampUTC(info.last_seen_time);
360 ss << absl::StrFormat(" last_seen_at: %s\n", iso_timestamp);
361 ss << absl::StrFormat(" stale: %s\n",
362 info.stale_frame_count > 0 ? "true" : "false");
363
364 // Parse hierarchical context from path
365 std::vector<std::string> segments = absl::StrSplit(path, '/');
366 if (!segments.empty()) {
367 ss << " context:\n";
368 if (segments.size() > 0) {
369 ss << absl::StrFormat(" editor: %s\n", segments[0]);
370 }
371 if (segments.size() > 1) {
372 ss << absl::StrFormat(" tab: %s\n", segments[1]);
373 }
374 if (segments.size() > 2) {
375 ss << absl::StrFormat(" section: %s\n", segments[2]);
376 }
377 }
378
379 if (!info.description.empty()) {
380 ss << absl::StrFormat(" description: %s\n", info.description);
381 }
382
383 // Add suggested actions based on widget type
384 ss << " actions: [";
385 if (info.type == "button") {
386 ss << "click";
387 } else if (info.type == "input") {
388 ss << "type, clear";
389 } else if (info.type == "canvas") {
390 ss << "click, drag, scroll";
391 } else if (info.type == "checkbox") {
392 ss << "toggle";
393 } else if (info.type == "slider") {
394 ss << "drag, set";
395 } else {
396 ss << "interact";
397 }
398 ss << "]\n";
399 }
400 }
401
402 return ss.str();
403}
404
405void WidgetIdRegistry::ExportCatalogToFile(const std::string& output_file,
406 const std::string& format) const {
407 std::string content = ExportCatalog(format);
408 std::ofstream file(output_file);
409 if (file.is_open()) {
410 file << content;
411 file.close();
412 }
413}
414
415std::string WidgetIdRegistry::NormalizeLabel(absl::string_view label) {
416 size_t pos = label.find("##");
417 if (pos != absl::string_view::npos) {
418 label = label.substr(0, pos);
419 }
420 std::string sanitized = std::string(absl::StripAsciiWhitespace(label));
421 return sanitized;
422}
423
424std::string WidgetIdRegistry::NormalizePathSegment(absl::string_view segment) {
425 return NormalizeLabel(segment);
426}
427
429 auto it = widgets_.begin();
430 while (it != widgets_.end()) {
431 if (ShouldPrune(it->second)) {
432 it = widgets_.erase(it);
433 } else {
434 ++it;
435 }
436 }
437}
438
440 if (info.last_seen_frame < 0 || stale_frame_limit_ <= 0) {
441 return false;
442 }
443 if (current_frame_ < 0) {
444 return false;
445 }
447}
448
449} // namespace gui
450} // namespace yaze
Centralized registry for discoverable GUI widgets.
std::unordered_map< std::string, WidgetInfo > widgets_
void ExportCatalogToFile(const std::string &output_file, const std::string &format="yaml") const
static std::string NormalizePathSegment(absl::string_view segment)
std::string ExportCatalog(const std::string &format="yaml") const
bool ShouldPrune(const WidgetInfo &info) const
const WidgetInfo * GetWidgetInfo(const std::string &full_path) const
ImGuiID GetWidgetId(const std::string &full_path) const
static std::string NormalizeLabel(absl::string_view label)
void RegisterWidget(const std::string &full_path, const std::string &type, ImGuiID imgui_id, const std::string &description="", const WidgetMetadata &metadata=WidgetMetadata())
static WidgetIdRegistry & Instance()
std::vector< std::string > FindWidgets(const std::string &pattern) const
WidgetIdScope(const std::string &name)
std::string GetFullPath() const
static thread_local std::vector< std::string > id_stack_
std::string GetWidgetPath(const std::string &widget_type, const std::string &widget_name) const
std::string ExtractWindowFromPath(absl::string_view path)
std::string ExtractLabelFromPath(absl::string_view path)
std::string FormatTimestampUTC(const absl::Time &timestamp)
WidgetIdRegistry::WidgetBounds BoundsFromImGui(const ImRect &rect)