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 absl::MutexLock lock(&mutex_);
66 ImGuiContext* ctx = ImGui::GetCurrentContext();
67 if (ctx) {
68 current_frame_ = ctx->FrameCount;
69 } else if (current_frame_ >= 0) {
70 ++current_frame_;
71 } else {
72 current_frame_ = 0;
73 }
74 frame_time_ = absl::Now();
75 for (auto& [_, info] : widgets_) {
76 info.seen_in_current_frame = false;
77 }
78}
79
81 absl::MutexLock lock(&mutex_);
82 for (auto& [_, info] : widgets_) {
83 if (!info.seen_in_current_frame) {
84 info.visible = false;
85 info.enabled = false;
86 info.bounds.valid = false;
87 info.stale_frame_count += 1;
88 } else {
89 info.seen_in_current_frame = false;
90 info.stale_frame_count = 0;
91 }
92 }
94}
95
96namespace {
97
98std::string ExtractWindowFromPath(absl::string_view path) {
99 size_t slash = path.find('/');
100 if (slash == absl::string_view::npos) {
101 return std::string(path);
102 }
103 return std::string(path.substr(0, slash));
104}
105
106std::string ExtractLabelFromPath(absl::string_view path) {
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);
112 }
113 return std::string(path.substr(slash + 1));
114 }
115 return std::string(path.substr(colon + 1));
116}
117
118std::string FormatTimestampUTC(const absl::Time& timestamp) {
119 if (timestamp == absl::Time()) {
120 return "";
121 }
122
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);
126
127 std::tm tm_buffer;
128#if defined(_WIN32)
129 if (gmtime_s(&tm_buffer, &time_value) != 0) {
130 return "";
131 }
132#else
133 if (gmtime_r(&time_value, &tm_buffer) == nullptr) {
134 return "";
135 }
136#endif
137
138 char buffer[32];
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) {
143 return "";
144 }
145
146 return std::string(buffer);
147}
148
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;
155 bounds.valid = true;
156 return bounds;
157}
158
159} // namespace
160
161void WidgetIdRegistry::RegisterWidget(const std::string& full_path,
162 const std::string& type, ImGuiID imgui_id,
163 const std::string& description,
164 const WidgetMetadata& metadata) {
165 absl::MutexLock lock(&mutex_);
166 WidgetInfo& info = widgets_[full_path];
167 info.full_path = full_path;
168 info.type = type;
169 info.imgui_id = imgui_id;
170 info.description = description;
171
172 if (metadata.label.has_value()) {
173 info.label = NormalizeLabel(*metadata.label);
174 } else {
175 info.label = NormalizeLabel(ExtractLabelFromPath(full_path));
176 }
177 if (info.label.empty()) {
178 info.label = ExtractLabelFromPath(full_path);
179 }
180
181 if (metadata.window_name.has_value()) {
182 info.window_name = NormalizeLabel(*metadata.window_name);
183 } else {
184 info.window_name = NormalizePathSegment(ExtractWindowFromPath(full_path));
185 }
186 if (info.window_name.empty()) {
187 info.window_name = ExtractWindowFromPath(full_path);
188 }
189
190 ImGuiContext* ctx = ImGui::GetCurrentContext();
191 absl::Time observed_at = absl::Now();
192
193 if (ctx) {
194 const ImGuiLastItemData& last = ctx->LastItemData;
195 if (metadata.visible.has_value()) {
196 info.visible = *metadata.visible;
197 } else {
198 info.visible = (last.StatusFlags & ImGuiItemStatusFlags_Visible) != 0;
199 }
200
201 if (metadata.enabled.has_value()) {
202 info.enabled = *metadata.enabled;
203 } else {
204 info.enabled = (last.ItemFlags & ImGuiItemFlags_Disabled) == 0;
205 }
206
207 if (metadata.bounds.has_value()) {
208 info.bounds = *metadata.bounds;
209 } else {
210 info.bounds = BoundsFromImGui(last.Rect);
211 }
212
213 info.last_seen_frame = ctx->FrameCount;
214 } else {
215 info.visible = metadata.visible.value_or(true);
216 info.enabled = metadata.enabled.value_or(true);
217 if (metadata.bounds.has_value()) {
218 info.bounds = *metadata.bounds;
219 } else {
220 info.bounds.valid = false;
221 }
222 if (current_frame_ >= 0) {
223 info.last_seen_frame = current_frame_;
224 }
225 }
226
227 info.last_seen_time = observed_at;
228 info.seen_in_current_frame = true;
229 info.stale_frame_count = 0;
230}
231
232std::vector<std::string> WidgetIdRegistry::FindWidgets(
233 const std::string& pattern) const {
234 absl::MutexLock lock(&mutex_);
235 std::vector<std::string> matches;
236
237 // Simple glob-style pattern matching
238 // Supports: "*" (any), "?" (single char), exact matches
239 for (const auto& [path, info] : widgets_) {
240 bool match = false;
241
242 if (pattern == "*") {
243 match = true;
244 } else if (pattern.find('*') != std::string::npos) {
245 // Wildcard pattern - convert to simple substring match for now
246 std::string search = pattern;
247 search.erase(std::remove(search.begin(), search.end(), '*'),
248 search.end());
249 if (!search.empty() && path.find(search) != std::string::npos) {
250 match = true;
251 }
252 } else {
253 // Exact match
254 if (path == pattern) {
255 match = true;
256 }
257 }
258
259 if (match) {
260 matches.push_back(path);
261 }
262 }
263
264 // Sort for consistent ordering
265 std::sort(matches.begin(), matches.end());
266 return matches;
267}
268
269ImGuiID WidgetIdRegistry::GetWidgetId(const std::string& full_path) const {
270 absl::MutexLock lock(&mutex_);
271 auto it = widgets_.find(full_path);
272 if (it != widgets_.end()) {
273 return it->second.imgui_id;
274 }
275 return 0;
276}
277
278std::optional<WidgetIdRegistry::WidgetInfo> WidgetIdRegistry::GetWidgetInfo(
279 const std::string& full_path) const {
280 absl::MutexLock lock(&mutex_);
281 auto it = widgets_.find(full_path);
282 if (it != widgets_.end()) {
283 return it->second;
284 }
285 return std::nullopt;
286}
287
288std::unordered_map<std::string, WidgetIdRegistry::WidgetInfo>
290 absl::MutexLock lock(&mutex_);
291 return widgets_;
292}
293
295 absl::MutexLock lock(&mutex_);
296 widgets_.clear();
297 current_frame_ = -1;
298}
299
300std::string WidgetIdRegistry::ExportCatalog(const std::string& format) const {
301 absl::MutexLock lock(&mutex_);
302 std::ostringstream ss;
303
304 if (format == "json") {
305 ss << "{\n";
306 ss << " \"widgets\": [\n";
307
308 bool first = true;
309 for (const auto& [path, info] : widgets_) {
310 if (!first)
311 ss << ",\n";
312 first = false;
313
314 ss << " {\n";
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, "
327 "%0.1f]},\n",
328 info.bounds.min_x, info.bounds.min_y, info.bounds.max_x,
329 info.bounds.max_y);
330 } else {
331 ss << " \"bounds\": null,\n";
332 }
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()) {
340 ss << ",\n";
341 ss << absl::StrFormat(" \"description\": \"%s\"\n",
342 info.description);
343 } else {
344 ss << "\n";
345 }
346 ss << " }";
347 }
348
349 ss << "\n ]\n";
350 ss << "}\n";
351 } else {
352 // YAML format (default)
353 ss << "widgets:\n";
354
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) {
366 ss << " bounds:\n";
367 ss << absl::StrFormat(" min: [%0.1f, %0.1f]\n", info.bounds.min_x,
368 info.bounds.min_y);
369 ss << absl::StrFormat(" max: [%0.1f, %0.1f]\n", info.bounds.max_x,
370 info.bounds.max_y);
371 }
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");
377
378 // Parse hierarchical context from path
379 std::vector<std::string> segments = absl::StrSplit(path, '/');
380 if (!segments.empty()) {
381 ss << " context:\n";
382 if (segments.size() > 0) {
383 ss << absl::StrFormat(" editor: %s\n", segments[0]);
384 }
385 if (segments.size() > 1) {
386 ss << absl::StrFormat(" tab: %s\n", segments[1]);
387 }
388 if (segments.size() > 2) {
389 ss << absl::StrFormat(" section: %s\n", segments[2]);
390 }
391 }
392
393 if (!info.description.empty()) {
394 ss << absl::StrFormat(" description: %s\n", info.description);
395 }
396
397 // Add suggested actions based on widget type
398 ss << " actions: [";
399 if (info.type == "button") {
400 ss << "click";
401 } else if (info.type == "input") {
402 ss << "type, clear";
403 } else if (info.type == "canvas") {
404 ss << "click, drag, scroll";
405 } else if (info.type == "checkbox") {
406 ss << "toggle";
407 } else if (info.type == "slider") {
408 ss << "drag, set";
409 } else {
410 ss << "interact";
411 }
412 ss << "]\n";
413 }
414 }
415
416 return ss.str();
417}
418
419void WidgetIdRegistry::ExportCatalogToFile(const std::string& output_file,
420 const std::string& format) const {
421 std::string content = ExportCatalog(format);
422 std::ofstream file(output_file);
423 if (file.is_open()) {
424 file << content;
425 file.close();
426 }
427}
428
429std::string WidgetIdRegistry::NormalizeLabel(absl::string_view label) {
430 size_t pos = label.find("##");
431 if (pos != absl::string_view::npos) {
432 label = label.substr(0, pos);
433 }
434 std::string sanitized = std::string(absl::StripAsciiWhitespace(label));
435 return sanitized;
436}
437
438std::string WidgetIdRegistry::NormalizePathSegment(absl::string_view segment) {
439 return NormalizeLabel(segment);
440}
441
443 auto it = widgets_.begin();
444 while (it != widgets_.end()) {
445 if (ShouldPrune(it->second)) {
446 it = widgets_.erase(it);
447 } else {
448 ++it;
449 }
450 }
451}
452
454 if (info.last_seen_frame < 0 || stale_frame_limit_ <= 0) {
455 return false;
456 }
457 if (current_frame_ < 0) {
458 return false;
459 }
460 return (current_frame_ - info.last_seen_frame) > stale_frame_limit_;
461}
462
463} // namespace gui
464} // namespace yaze
Centralized registry for discoverable GUI widgets.
std::optional< WidgetInfo > GetWidgetInfo(const std::string &full_path) const
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
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())
std::unordered_map< std::string, WidgetInfo > GetAllWidgets() const
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)