yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
right_drawer_manager.cc
Go to the documentation of this file.
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <array>
6#include <cctype>
7#include <chrono>
8#include <cmath>
9#include <ctime>
10#include <filesystem>
11#include <optional>
12#include <vector>
13
14#include "absl/strings/str_format.h"
15#include "absl/types/span.h"
26#include "app/gui/core/icons.h"
27#include "app/gui/core/input.h"
30#include "app/gui/core/style.h"
37#include "imgui/imgui.h"
38#include "imgui/imgui_internal.h"
39#include "util/json.h"
40#include "util/log.h"
41#include "util/platform_paths.h"
42
43namespace yaze {
44namespace editor {
45
46namespace {
47
48std::string ResolveAgentChatHistoryPath() {
49 auto agent_dir = util::PlatformPaths::GetAppDataSubdirectory("agent");
50 if (agent_dir.ok()) {
51 return (*agent_dir / "agent_chat_history.json").string();
52 }
54 if (temp_dir.ok()) {
55 return (*temp_dir / "agent_chat_history.json").string();
56 }
57 return (std::filesystem::current_path() / "agent_chat_history.json").string();
58}
59
60std::string JsonValueToDisplayString(const Json& value) {
61 if (value.is_string()) {
62 return value.get<std::string>();
63 }
64 if (value.is_boolean()) {
65 return value.get<bool>() ? "true" : "false";
66 }
67 if (value.is_number_integer()) {
68 return std::to_string(value.get<long long>());
69 }
70 if (value.is_number_unsigned()) {
71 return std::to_string(value.get<unsigned long long>());
72 }
73 if (value.is_number_float()) {
74 return absl::StrFormat("%.3f", value.get<double>());
75 }
76 if (value.is_null()) {
77 return "null";
78 }
79 return value.dump();
80}
81
82std::string SanitizeToolOutputIdFragment(const std::string& value) {
83 std::string sanitized;
84 sanitized.reserve(value.size());
85 for (unsigned char ch : value) {
86 if (std::isalnum(ch)) {
87 sanitized.push_back(static_cast<char>(ch));
88 } else {
89 sanitized.push_back('_');
90 }
91 }
92 return sanitized.empty() ? std::string("entry") : sanitized;
93}
94
95bool TryParseToolOutputJson(const std::string& text, Json* out) {
96 if (!out || text.empty()) {
97 return false;
98 }
99 try {
100 *out = Json::parse(text);
101 return out->is_object();
102 } catch (...) {
103 return false;
104 }
105}
106
107std::optional<uint32_t> ParseToolOutputAddress(const Json& value) {
108 if (value.is_number_unsigned()) {
109 return static_cast<uint32_t>(value.get<uint64_t>());
110 }
111 if (value.is_number_integer()) {
112 return static_cast<uint32_t>(value.get<int64_t>());
113 }
114 if (!value.is_string()) {
115 return std::nullopt;
116 }
117
118 std::string token = value.get<std::string>();
119 if (token.empty()) {
120 return std::nullopt;
121 }
122 if (token[0] == '$') {
123 token = token.substr(1);
124 } else if (token.size() > 2 && token[0] == '0' &&
125 (token[1] == 'x' || token[1] == 'X')) {
126 token = token.substr(2);
127 }
128 try {
129 return static_cast<uint32_t>(std::stoul(token, nullptr, 16));
130 } catch (...) {
131 return std::nullopt;
132 }
133}
134
135std::optional<uint32_t> ExtractToolOutputAddress(const Json& object) {
136 if (!object.is_object()) {
137 return std::nullopt;
138 }
139 for (const char* key : {"address", "entry_address"}) {
140 if (object.contains(key)) {
141 auto parsed = ParseToolOutputAddress(object[key]);
142 if (parsed.has_value()) {
143 return parsed;
144 }
145 }
146 }
147 return std::nullopt;
148}
149
150std::string ExtractToolOutputReference(const Json& object) {
151 if (!object.is_object()) {
152 return {};
153 }
154 if (object.contains("source") && object["source"].is_string()) {
155 return object["source"].get<std::string>();
156 }
157 if (object.contains("file") && object["file"].is_string()) {
158 std::string reference = object["file"].get<std::string>();
159 if (object.contains("line") && object["line"].is_number_integer()) {
160 reference = absl::StrCat(reference, ":", object["line"].get<int>());
161 }
162 return reference;
163 }
164 return {};
165}
166
167std::string BuildToolOutputEntryTitle(const Json& object) {
168 if (!object.is_object()) {
169 return {};
170 }
171
172 const std::string address = object.contains("address")
173 ? JsonValueToDisplayString(object["address"])
174 : "";
175 const std::string bank =
176 object.contains("bank") ? JsonValueToDisplayString(object["bank"]) : "";
177 const std::string name =
178 object.contains("name") ? JsonValueToDisplayString(object["name"]) : "";
179 const std::string source = ExtractToolOutputReference(object);
180
181 if (!source.empty() && !address.empty()) {
182 return absl::StrFormat("%s (%s)", source.c_str(), address.c_str());
183 }
184 if (!name.empty() && !address.empty()) {
185 return absl::StrFormat("%s %s", name.c_str(), address.c_str());
186 }
187 if (!name.empty() && !bank.empty()) {
188 return absl::StrFormat("%s %s", name.c_str(), bank.c_str());
189 }
190 if (!source.empty()) {
191 return source;
192 }
193 if (!address.empty()) {
194 return address;
195 }
196 if (!bank.empty()) {
197 return bank;
198 }
199 if (!name.empty()) {
200 return name;
201 }
202 return {};
203}
204
205std::string BuildToolOutputActionLabel(const char* visible_label,
206 const char* action_key,
207 const Json& object) {
208 const auto address = ExtractToolOutputAddress(object);
209 if (address.has_value()) {
210 return absl::StrFormat("%s##tool_output_%s_%06X", visible_label, action_key,
211 *address);
212 }
213
214 const std::string reference = ExtractToolOutputReference(object);
215 if (!reference.empty()) {
216 return absl::StrFormat("%s##tool_output_%s_%s", visible_label, action_key,
217 SanitizeToolOutputIdFragment(reference).c_str());
218 }
219
220 return absl::StrFormat("%s##tool_output_%s_entry", visible_label, action_key);
221}
222
224 const Json& object, const RightDrawerManager::ToolOutputActions& actions) {
225 if (!object.is_object()) {
226 return;
227 }
228
229 const std::string reference = ExtractToolOutputReference(object);
230 const auto address = ExtractToolOutputAddress(object);
231 bool drew_any = false;
232
233 if (!reference.empty() && actions.on_open_reference) {
234 const std::string label =
235 BuildToolOutputActionLabel("Open", "open", object);
236 if (ImGui::SmallButton(label.c_str())) {
237 actions.on_open_reference(reference);
238 }
239 drew_any = true;
240 }
241 if (address.has_value() && actions.on_open_address) {
242 if (drew_any) {
243 ImGui::SameLine();
244 }
245 const std::string label =
246 BuildToolOutputActionLabel("Addr", "addr", object);
247 if (ImGui::SmallButton(label.c_str())) {
248 actions.on_open_address(*address);
249 }
250 drew_any = true;
251 }
252 if (address.has_value() && actions.on_open_lookup) {
253 if (drew_any) {
254 ImGui::SameLine();
255 }
256 const std::string label =
257 BuildToolOutputActionLabel("Lookup", "lookup", object);
258 if (ImGui::SmallButton(label.c_str())) {
259 actions.on_open_lookup(*address);
260 }
261 }
262}
263
264void DrawJsonObjectFields(const Json& object) {
265 for (auto it = object.begin(); it != object.end(); ++it) {
266 if (it.value().is_array() || it.value().is_object()) {
267 continue;
268 }
269 ImGui::BulletText("%s: %s", it.key().c_str(),
270 JsonValueToDisplayString(it.value()).c_str());
271 }
272}
273
275 const char* label, const Json& array,
277 if (!array.is_array() || array.empty()) {
278 return;
279 }
280 if (!ImGui::CollapsingHeader(label, ImGuiTreeNodeFlags_DefaultOpen)) {
281 return;
282 }
283 ImGui::PushID(label);
284 for (size_t i = 0; i < array.size(); ++i) {
285 const auto& entry = array[i];
286 ImGui::PushID(static_cast<int>(i));
287 if (entry.is_object()) {
288 const std::string title = BuildToolOutputEntryTitle(entry);
289 if (!title.empty()) {
290 gui::ColoredText(title.c_str(), gui::GetOnSurfaceVec4());
291 }
292 DrawToolOutputEntryActions(entry, actions);
293 if (!title.empty() || !entry.empty()) {
294 ImGui::Spacing();
295 }
297 } else {
298 ImGui::BulletText("%s", JsonValueToDisplayString(entry).c_str());
299 }
300 if (i + 1 < array.size()) {
301 ImGui::Separator();
302 }
303 ImGui::PopID();
304 }
305 ImGui::PopID();
306}
307
308std::string BuildSelectionContextSummary(const SelectionContext& selection) {
309 if (selection.type == SelectionType::kNone) {
310 return "";
311 }
312 std::string context =
313 absl::StrFormat("Selection: %s", GetSelectionTypeName(selection.type));
314 if (!selection.display_name.empty()) {
315 context += absl::StrFormat("\nName: %s", selection.display_name);
316 }
317 if (selection.id >= 0) {
318 context += absl::StrFormat("\nID: 0x%X", selection.id);
319 }
320 if (selection.secondary_id >= 0) {
321 context += absl::StrFormat("\nSecondary: 0x%X", selection.secondary_id);
322 }
323 if (selection.read_only) {
324 context += "\nRead Only: true";
325 }
326 return context;
327}
328
330 const char* title, const char* fallback_icon,
331 const ProjectWorkflowStatus& status,
332 const std::function<void()>& cancel_callback = {}) {
333 if (!status.visible) {
334 return;
335 }
336
338 workflow::WorkflowIcon(status, fallback_icon), title);
339 ImGui::TextWrapped("%s", status.summary.empty() ? status.label.c_str()
340 : status.summary.c_str());
341 if (!status.detail.empty()) {
342 ImGui::TextWrapped("%s", status.detail.c_str());
343 }
344 if (!status.output_tail.empty()) {
345 ImGui::TextWrapped("%s", status.output_tail.c_str());
346 }
347 if (status.can_cancel && cancel_callback) {
348 if (ImGui::SmallButton(ICON_MD_CANCEL " Cancel Build")) {
349 cancel_callback();
350 }
351 }
352}
353
355 const ProjectWorkflowHistoryEntry& entry,
356 const workflow::WorkflowActionCallbacks& callbacks) {
359 entry.status, entry.kind == "Run" ? ICON_MD_PLAY_ARROW
360 : ICON_MD_BUILD),
361 entry.kind.c_str());
362 ImGui::SameLine();
363 ImGui::TextDisabled("%s",
365 ImGui::TextWrapped("%s", entry.status.summary.empty()
366 ? entry.status.label.c_str()
367 : entry.status.summary.c_str());
368 if (!entry.status.output_tail.empty()) {
369 ImGui::TextWrapped("%s", entry.status.output_tail.c_str());
370 }
372 entry, callbacks, {.show_open_output = true, .show_copy_log = true});
373}
374
375} // namespace
376
377// Shared drawer catalog (persistent tabs, shortcuts, and command palette).
378const std::array<DrawerCatalogEntry, 7> kDrawerCatalog = {{
380 "View: Toggle Project Panel"},
382 "View: Toggle Properties Panel"},
384 "View: Toggle AI Agent Panel"},
386 ICON_MD_DESCRIPTION, "View: Toggle Proposals Panel"},
388 ICON_MD_NOTIFICATIONS, "View: Toggle Notifications Panel"},
390 "View: Toggle Help Panel"},
392 "View: Toggle Settings Panel"},
393}};
394
395namespace {
396
398 for (size_t i = 0; i < kDrawerCatalog.size(); ++i) {
399 if (kDrawerCatalog[i].type == type) {
400 return static_cast<int>(i);
401 }
402 }
403 return -1;
404}
405
407 RightDrawerManager::PanelType current, int direction) {
408 if (kDrawerCatalog.empty()) {
410 }
411 int index = FindRightPanelIndex(current);
412 if (index < 0) {
413 index = 0;
414 }
415 const int size = static_cast<int>(kDrawerCatalog.size());
416 const int next = (index + direction + size) % size;
417 return kDrawerCatalog[static_cast<size_t>(next)].type;
418}
419
420} // namespace
421
422absl::Span<const DrawerCatalogEntry> GetDrawerCatalog() {
423 return absl::MakeSpan(kDrawerCatalog);
424}
425
427 switch (type) {
429 return "View: Toggle Project Panel";
431 return "View: Toggle AI Agent Panel";
433 return "View: Toggle Proposals Panel";
435 return "View: Toggle Settings Panel";
437 return "View: Toggle Help Panel";
439 return "View: Toggle Notifications Panel";
441 return "View: Toggle Properties Panel";
443 default:
444 return "";
445 }
446}
447
449 switch (type) {
451 return "None";
453 return "AI Agent";
455 return "Proposals";
457 return "Settings";
459 return "Help";
461 return "Notifications";
463 return "Properties";
465 return "Project";
467 return "Tool Output";
468 default:
469 return "Unknown";
470 }
471}
472
497
499 switch (type) {
501 return "agent_chat";
503 return "proposals";
505 return "settings";
506 case PanelType::kHelp:
507 return "help";
509 return "notifications";
511 return "properties";
513 return "project";
515 return "tool_output";
516 case PanelType::kNone:
517 default:
518 return "none";
519 }
520}
521
523 if (active_panel_ == type) {
524 CloseDrawer();
525 } else {
526 // Opens the requested panel (also handles re-opening during close animation)
527 OpenDrawer(type);
528 }
529}
530
539
540void RightDrawerManager::SetToolOutput(std::string title, std::string query,
541 std::string content,
542 ToolOutputActions actions) {
543 tool_output_title_ = std::move(title);
544 tool_output_query_ = std::move(query);
545 tool_output_content_ = std::move(content);
546 tool_output_actions_ = std::move(actions);
547}
548
552
554 if (type == PanelType::kNone) {
555 CloseDrawer();
556 return;
557 }
558
559 // If we were closing, cancel the close animation
560 closing_ = false;
562
563 active_panel_ = type;
564 if (FindRightPanelIndex(type) >= 0) {
565 last_active_panel_ = type;
566 }
567 animating_ = true;
568 animation_target_ = 1.0f;
569
570 // Check if animations are enabled
571 if (!gui::GetAnimator().IsEnabled()) {
572 panel_animation_ = 1.0f;
573 animating_ = false;
574 }
575 // Otherwise keep current panel_animation_ for smooth transition
576}
577
579 if (!gui::GetAnimator().IsEnabled()) {
580 // Instant close
582 closing_ = false;
584 panel_animation_ = 0.0f;
585 animating_ = false;
586 return;
587 }
588
589 // Start close animation — keep the panel type so we can still draw it
590 closing_ = true;
593 animating_ = true;
594 animation_target_ = 0.0f;
595}
596
598 if (direction == 0) {
599 return;
600 }
601
602 const PanelType current_panel =
604 if (current_panel == PanelType::kNone) {
605 return;
606 }
607
608 const int step = direction > 0 ? 1 : -1;
609 OpenDrawer(StepRightPanel(current_panel, step));
610}
611
613 // Snap transition state to a stable endpoint. This avoids stale intermediate
614 // frames being composited when the OS moves the app across spaces.
615 (void)visible;
616 closing_ = false;
618 animating_ = false;
619
622}
623
627
629 // Determine which panel to measure: active panel, or the one being closed
630 PanelType effective_panel = active_panel_;
631 if (effective_panel == PanelType::kNone && closing_) {
632 effective_panel = closing_panel_;
633 }
634 if (effective_panel == PanelType::kNone) {
635 return 0.0f;
636 }
637
638 ImGuiContext* context = ImGui::GetCurrentContext();
639 if (!context) {
640 return GetConfiguredPanelWidth(effective_panel);
641 }
642
643 const ImGuiViewport* viewport = ImGui::GetMainViewport();
644 if (!viewport) {
645 return GetConfiguredPanelWidth(effective_panel);
646 }
647
648 return GetClampedPanelWidth(effective_panel, viewport->WorkSize.x);
649}
650
652 if (type == PanelType::kNone) {
653 return;
654 }
655 float viewport_width = 0.0f;
656 if (const ImGuiViewport* viewport = ImGui::GetMainViewport()) {
657 viewport_width = viewport->WorkSize.x;
658 }
659 if (viewport_width <= 0.0f && ImGui::GetCurrentContext()) {
660 viewport_width = ImGui::GetIO().DisplaySize.x;
661 }
662 const auto limits = GetSharedPanelSizeLimits();
663 float clamped = std::max(limits.min_width, width);
664 if (viewport_width > 0.0f) {
665 const float ratio = viewport_width < 768.0f
666 ? std::max(0.88f, limits.max_width_ratio)
667 : limits.max_width_ratio;
668 const float max_width = std::max(limits.min_width, viewport_width * ratio);
669 clamped = std::clamp(clamped, limits.min_width, max_width);
670 }
671
672 if (std::abs(shared_width_ - clamped) < 0.5f) {
673 return;
674 }
675 shared_width_ = clamped;
676#if !defined(NDEBUG)
677 LOG_INFO("RightDrawerManager",
678 "SetDrawerWidth type=%d requested=%.1f clamped=%.1f",
679 static_cast<int>(type), width, clamped);
680#endif
682}
683
685 float default_width =
687 for (const DrawerCatalogEntry& entry : GetDrawerCatalog()) {
688 default_width = std::max(
689 default_width, GetDefaultDrawerWidth(entry.type, active_editor_type_));
690 }
691 SetDrawerWidth(PanelType::kProject, default_width);
692}
693
695 EditorType editor) {
696 switch (type) {
698 return std::max(gui::UIConfig::kPanelWidthAgentChat, 480.0f);
700 return std::max(gui::UIConfig::kPanelWidthProposals, 440.0f);
702 return std::max(gui::UIConfig::kPanelWidthSettings, 380.0f);
703 case PanelType::kHelp:
704 return std::max(gui::UIConfig::kPanelWidthHelp, 380.0f);
706 return std::max(gui::UIConfig::kPanelWidthNotifications, 380.0f);
708 // Property panel can be wider in certain editors.
709 if (editor == EditorType::kDungeon) {
710 return 440.0f;
711 }
712 return std::max(gui::UIConfig::kPanelWidthProperties, 400.0f);
714 return std::max(gui::UIConfig::kPanelWidthProject, 420.0f);
716 return 460.0f;
717 default:
718 return std::max(gui::UIConfig::kPanelWidthMedium, 380.0f);
719 }
720}
721
723 const PanelSizeLimits& limits) {
724 if (type == PanelType::kNone) {
725 return;
726 }
727 PanelSizeLimits normalized = limits;
728 normalized.min_width =
730 normalized.max_width_ratio =
731 std::clamp(normalized.max_width_ratio, 0.25f, 0.95f);
732 panel_size_limits_[PanelTypeKey(type)] = normalized;
733}
734
736 PanelType type) const {
737 auto it = panel_size_limits_.find(PanelTypeKey(type));
738 if (it != panel_size_limits_.end()) {
739 return it->second;
740 }
741
742 PanelSizeLimits defaults;
743 switch (type) {
746 defaults.max_width_ratio = 0.90f;
747 break;
750 defaults.max_width_ratio = 0.86f;
751 break;
754 defaults.max_width_ratio = 0.80f;
755 break;
756 case PanelType::kHelp:
758 defaults.max_width_ratio = 0.80f;
759 break;
762 defaults.max_width_ratio = 0.82f;
763 break;
766 defaults.max_width_ratio = 0.90f;
767 break;
770 defaults.max_width_ratio = 0.86f;
771 break;
773 defaults.min_width = 360.0f;
774 defaults.max_width_ratio = 0.88f;
775 break;
776 case PanelType::kNone:
777 default:
778 break;
779 }
780 return defaults;
781}
782
785 PanelSizeLimits shared{
787 .max_width_ratio = 0.95f,
788 };
789 const auto fold = [this, &shared](PanelType type) {
790 const PanelSizeLimits limits = GetPanelSizeLimits(type);
791 shared.min_width = std::max(shared.min_width, limits.min_width);
792 shared.max_width_ratio =
793 std::min(shared.max_width_ratio, limits.max_width_ratio);
794 };
795 for (const DrawerCatalogEntry& entry : GetDrawerCatalog()) {
796 fold(entry.type);
797 }
799 return shared;
800}
801
803 return type == PanelType::kNone ? 0.0f : shared_width_;
804}
805
807 float viewport_width) const {
808 float width = GetConfiguredPanelWidth(type);
809 if (width <= 0.0f) {
810 return width;
811 }
812 const auto limits = GetSharedPanelSizeLimits();
813 const float ratio = viewport_width < 768.0f
814 ? std::max(0.88f, limits.max_width_ratio)
815 : limits.max_width_ratio;
816 const float max_width = std::max(limits.min_width, viewport_width * ratio);
817 return std::clamp(width, limits.min_width, max_width);
818}
819
822 on_panel_width_changed_(type, width);
823 }
824}
825
826std::unordered_map<std::string, float>
828 return {{"right_sidebar.shared", shared_width_}};
829}
830
832 const std::unordered_map<std::string, float>& widths) {
833#if !defined(NDEBUG)
834 LOG_INFO("RightDrawerManager",
835 "RestoreDrawerWidths: %zu entries from settings", widths.size());
836#endif
837 if (const auto shared = widths.find("right_sidebar.shared");
838 shared != widths.end()) {
839 SetDrawerWidth(PanelType::kProject, shared->second);
840 return;
841 }
842
843 // One-way migration from #260's per-drawer map. Start at the widest
844 // context-aware default, then retain any wider user value.
845 float migrated =
847 for (const DrawerCatalogEntry& entry : GetDrawerCatalog()) {
848 migrated = std::max(migrated,
850 }
851 for (const DrawerCatalogEntry& entry : GetDrawerCatalog()) {
852 if (const auto saved = widths.find(PanelTypeKey(entry.type));
853 saved != widths.end()) {
854 migrated = std::max(migrated, saved->second);
855 }
856 }
857 if (const auto tool_output =
859 tool_output != widths.end()) {
860 migrated = std::max(migrated, tool_output->second);
861 }
863}
864
866 // Nothing to draw if no panel is active and no close animation running
868 return;
869 }
870
871 // Handle Escape key to close panel
873 ImGui::IsKeyPressed(ImGuiKey_Escape)) {
874 CloseDrawer();
875 // Don't return — we need to start drawing the close animation this frame
876 if (!closing_)
877 return;
878 }
879
880 const bool animations_enabled = gui::GetAnimator().IsEnabled();
881 if (!animations_enabled && animating_) {
883 animating_ = false;
884 if (closing_ && animation_target_ == 0.0f) {
885 closing_ = false;
887 return;
888 }
889 }
890
891 // Advance animation
892 if (animating_ && animations_enabled) {
893 // Clamp dt to avoid giant interpolation jumps after focus/space changes.
894 float delta_time = std::clamp(ImGui::GetIO().DeltaTime, 0.0f, 1.0f / 20.0f);
895 float speed = gui::UIConfig::kAnimationSpeed;
896 switch (gui::GetAnimator().motion_profile()) {
898 speed *= 1.20f;
899 break;
901 speed *= 0.75f;
902 break;
904 default:
905 break;
906 }
907 float diff = animation_target_ - panel_animation_;
908 panel_animation_ += diff * std::min(1.0f, delta_time * speed);
909
910 // Snap to target when close enough
911 if (std::abs(animation_target_ - panel_animation_) <
914 animating_ = false;
915
916 // Close animation finished — fully clean up
917 if (closing_ && animation_target_ == 0.0f) {
918 closing_ = false;
920 return;
921 }
922 }
923 }
924
925 // Determine which panel type to draw content for
926 PanelType draw_panel = active_panel_;
927 if (draw_panel == PanelType::kNone && closing_) {
928 draw_panel = closing_panel_;
929 }
930
931 const ImGuiViewport* viewport = ImGui::GetMainViewport();
932 const float viewport_width = viewport->WorkSize.x;
933 const float top_inset = gui::LayoutHelpers::GetTopInset();
934 const float bottom_safe = gui::LayoutHelpers::GetSafeAreaInsets().bottom;
935 const float viewport_height =
936 std::max(0.0f, viewport->WorkSize.y - top_inset - bottom_safe);
937
938 // Keep full-width state explicit so drag-resize and animation remain stable.
939 const float full_width =
940 (draw_panel == PanelType::kNone)
941 ? 0.0f
942 : GetClampedPanelWidth(draw_panel, viewport_width);
943 const float animated_width = full_width * panel_animation_;
944
945 // Use SurfaceContainer for slightly elevated panel background
946 ImVec4 panel_bg = gui::GetSurfaceContainerVec4();
947 ImVec4 panel_border = gui::GetOutlineVec4();
948
949 ImGuiWindowFlags panel_flags =
950 ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove |
951 ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDocking |
952 ImGuiWindowFlags_NoNavFocus;
953
954 // Position panel: slides from right edge. At animation=1.0, fully visible.
955 // At animation=0.0, fully off-screen to the right.
956 float panel_x = viewport->WorkPos.x + viewport_width - animated_width;
957 ImGui::SetNextWindowPos(ImVec2(panel_x, viewport->WorkPos.y + top_inset));
958 ImGui::SetNextWindowSize(ImVec2(full_width, viewport_height));
959
960 gui::StyledWindow panel("##RightPanel",
961 {.bg = panel_bg,
962 .border = panel_border,
963 .padding = ImVec2(0.0f, 0.0f),
964 .border_size = 0.0f},
965 nullptr, panel_flags);
966 if (panel) {
967 const char* panel_title = GetPanelTypeName(draw_panel);
968 const char* panel_icon = GetPanelTypeIcon(draw_panel);
969 if (draw_panel == PanelType::kToolOutput && !tool_output_title_.empty()) {
970 panel_title = tool_output_title_.c_str();
971 }
972 // Draw enhanced panel header and navigation strip
973 DrawPanelHeader(draw_panel, panel_title, panel_icon);
974 DrawDrawerNavStrip(draw_panel);
975
976 // Content area with padding and minimum height so content never collapses
977 gui::StyleVarGuard content_padding(
978 ImGuiStyleVar_WindowPadding,
981 const bool panel_content_open = gui::LayoutHelpers::BeginContentChild(
982 "##PanelContent", ImVec2(0.0f, gui::UIConfig::kContentMinHeightList),
983 ImGuiChildFlags_AlwaysUseWindowPadding);
984 if (panel_content_open) {
985 switch (draw_panel) {
988 break;
991 break;
994 break;
995 case PanelType::kHelp:
997 break;
1000 break;
1003 break;
1006 break;
1009 break;
1010 default:
1011 break;
1012 }
1013 }
1015
1016 // VSCode-style splitter: drag from the left edge to resize.
1018 const float handle_width = gui::UIConfig::kSplitterWidth;
1019 const ImVec2 win_pos = ImGui::GetWindowPos();
1020 const float win_height = ImGui::GetWindowHeight();
1021 ImGui::SetCursorScreenPos(
1022 ImVec2(win_pos.x - handle_width * 0.5f, win_pos.y));
1023 ImGui::InvisibleButton("##RightPanelResizeHandle",
1024 ImVec2(handle_width, win_height));
1025 const bool handle_hovered = ImGui::IsItemHovered();
1026 const bool handle_active = ImGui::IsItemActive();
1027 if (handle_hovered || handle_active) {
1028 ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW);
1029 }
1030 if (handle_hovered &&
1031 ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
1033 }
1034 if (handle_active) {
1035 const float new_width = GetConfiguredPanelWidth(active_panel_) -
1036 ImGui::GetIO().MouseDelta.x;
1037 SetDrawerWidth(active_panel_, new_width);
1038 ImGui::SetTooltip(tr("Width: %.0f px"),
1040 }
1041
1042 ImVec4 handle_color = gui::GetOutlineVec4();
1043 handle_color.w = handle_active ? 0.95f : (handle_hovered ? 0.72f : 0.35f);
1044 ImGui::GetWindowDrawList()->AddLine(
1045 ImVec2(win_pos.x, win_pos.y),
1046 ImVec2(win_pos.x, win_pos.y + win_height),
1047 ImGui::GetColorU32(handle_color), handle_active ? 2.0f : 1.0f);
1048 }
1049 }
1050}
1051
1053 float available_width) {
1054 switch (type) {
1055 case PanelType::kAgentChat: {
1056#ifdef YAZE_BUILD_AGENT_UI
1057 if (agent_chat_ &&
1058 ImGui::CalcTextSize(ICON_MD_CIRCLE).x <= available_width) {
1060 if (ImGui::IsItemHovered()) {
1061 ImGui::SetTooltip("%s", tr("Agent Ready"));
1062 }
1063 }
1064#endif
1065 break;
1066 }
1068 if (toast_manager_) {
1069 const size_t unread = toast_manager_->GetUnreadCount();
1070 if (unread > 0) {
1071 const std::string badge = absl::StrFormat("%zu", unread);
1072 const ImVec2 badge_size = ImGui::CalcTextSize(badge.c_str());
1073 const float pad_x = 5.0f;
1074 const float badge_w = badge_size.x + pad_x * 2.0f;
1075 if (badge_w > available_width) {
1076 break;
1077 }
1078 const float badge_h = ImGui::GetTextLineHeight() + 2.0f;
1079 const ImVec2 p = ImGui::GetCursorScreenPos();
1080 ImDrawList* dl = ImGui::GetWindowDrawList();
1081 dl->AddRectFilled(p, ImVec2(p.x + badge_w, p.y + badge_h),
1082 ImGui::GetColorU32(gui::GetPrimaryVec4()), 4.0f);
1083 dl->AddText(ImVec2(p.x + pad_x, p.y + 1.0f),
1084 ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 1.0f, 1.0f)),
1085 badge.c_str());
1086 ImGui::Dummy(ImVec2(badge_w, badge_h));
1087 if (ImGui::IsItemHovered()) {
1088 ImGui::SetTooltip(tr("%zu unread notifications"), unread);
1089 }
1090 }
1091 }
1092 break;
1093 }
1095 if (properties_locked_ &&
1096 ImGui::CalcTextSize(ICON_MD_LOCK).x <= available_width) {
1098 if (ImGui::IsItemHovered()) {
1099 ImGui::SetTooltip("%s", tr("Selection Locked"));
1100 }
1101 }
1102 break;
1103 }
1104 case PanelType::kHelp: {
1105 const char* editor_name = nullptr;
1106 switch (active_editor_type_) {
1108 editor_name = "Overworld";
1109 break;
1111 editor_name = "Dungeon";
1112 break;
1114 editor_name = "Palette";
1115 break;
1117 editor_name = "Graphics";
1118 break;
1120 editor_name = "Assembly";
1121 break;
1122 case EditorType::kMusic:
1123 editor_name = "Music";
1124 break;
1126 editor_name = "Messages";
1127 break;
1128 default:
1129 break;
1130 }
1131 if (editor_name) {
1132 const ImVec4 tag_bg = gui::GetSurfaceContainerHighestVec4();
1133 const ImVec2 text_sz = ImGui::CalcTextSize(editor_name);
1134 const float pad = 5.0f;
1135 if (text_sz.x + pad * 2.0f > available_width) {
1136 break;
1137 }
1138 const ImVec2 p = ImGui::GetCursorScreenPos();
1139 ImDrawList* dl = ImGui::GetWindowDrawList();
1140 dl->AddRectFilled(
1141 p, ImVec2(p.x + text_sz.x + pad * 2.0f, p.y + text_sz.y + 2.0f),
1142 ImGui::GetColorU32(tag_bg), 4.0f);
1143 dl->AddText(ImVec2(p.x + pad, p.y + 1.0f),
1144 ImGui::GetColorU32(gui::GetPrimaryVec4()), editor_name);
1145 ImGui::Dummy(ImVec2(text_sz.x + pad * 2.0f, text_sz.y + 2.0f));
1146 }
1147 break;
1148 }
1149 default:
1150 break;
1151 }
1152}
1153
1155 // Panel-specific actions are handled inline within DrawPanelHeader.
1156}
1157
1159 const char* icon) {
1160 const float header_height =
1162 const float padding =
1164
1165 // Keep the header on the drawer surface; the navigation divider supplies
1166 // separation without another outlined region around the title.
1167 ImVec2 header_min = ImGui::GetCursorScreenPos();
1168
1169 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1170 // Match the title's text color rather than introducing a separate accent.
1171 const float icon_chip_size = gui::ScaledSize(24.0f, 0.0f).x;
1172 const float chip_y = header_min.y + (header_height - icon_chip_size) * 0.5f;
1173 ImVec2 chip_min(header_min.x + padding, chip_y);
1174 const ImVec2 icon_sz = ImGui::CalcTextSize(icon);
1175 draw_list->AddText(ImVec2(chip_min.x + (icon_chip_size - icon_sz.x) * 0.5f,
1176 chip_min.y + (icon_chip_size - icon_sz.y) * 0.5f),
1177 ImGui::GetColorU32(gui::GetTextPrimaryVec4()), icon);
1178
1179 // Reserve fixed chrome width so title truncation stays stable as badges /
1180 // panel actions appear. Layout (right → left): close, then panel actions.
1181 const ImVec2 chrome_btn_size = gui::ScaledSize(24.0f, 24.0f);
1182 const float chrome_gap = gui::ScaledSize(4.0f, 0.0f).x;
1183 int chrome_button_count = 1; // close
1184 if (type == PanelType::kProperties) {
1185 chrome_button_count += 1;
1186 } else if (type == PanelType::kAgentChat) {
1187#ifdef YAZE_BUILD_AGENT_UI
1188 // Must match the draw path below (buttons only appear when agent_chat_).
1189 if (agent_chat_) {
1190 chrome_button_count += 2;
1191 if (proposal_drawer_) {
1192 chrome_button_count += 1;
1193 }
1194 }
1195#endif
1196 } else if (type == PanelType::kNotifications) {
1197 if (toast_manager_) {
1198 chrome_button_count += 2;
1199 }
1200 } else if (type == PanelType::kToolOutput) {
1201 if (!tool_output_content_.empty()) {
1202 chrome_button_count += 1;
1203 }
1204 } else if (type == PanelType::kHelp) {
1205 chrome_button_count += 1;
1206 }
1207 const float chrome_reserve =
1208 chrome_button_count * chrome_btn_size.x +
1209 std::max(0, chrome_button_count - 1) * chrome_gap + padding;
1210
1211 // Title: truncate into the remaining space so drawer width no longer breathes
1212 // when chrome buttons appear/disappear.
1213 const float title_x = padding + icon_chip_size + 6.0f;
1214 const float title_y = ImGui::GetCursorPosY() +
1215 (header_height - ImGui::GetTextLineHeight()) * 0.5f;
1216 ImGui::SetCursorPos(ImVec2(title_x, title_y));
1217 const float title_max_x =
1218 std::max(title_x, ImGui::GetWindowWidth() - chrome_reserve - 8.0f);
1219 const float title_avail = std::max(0.0f, title_max_x - title_x);
1220 ImVec2 title_size = ImGui::CalcTextSize(title);
1221 const ImVec2 text_min = ImGui::GetCursorScreenPos();
1222 const ImVec2 text_max =
1223 ImVec2(text_min.x + title_avail, text_min.y + ImGui::GetTextLineHeight());
1224 ImGui::RenderTextEllipsis(draw_list, text_min, text_max, text_max.x, title,
1225 nullptr, &title_size);
1226 const float drawn_title_w = std::min(title_size.x, title_avail);
1227 ImGui::Dummy(ImVec2(drawn_title_w, ImGui::GetTextLineHeight()));
1228 if (title_size.x > title_avail && ImGui::IsItemHovered()) {
1229 ImGui::SetTooltip("%s", title);
1230 }
1231
1232 // Dummy() advances to the next line and resets cursor X. Measure from the
1233 // title's right edge instead, then require the complete badge to fit.
1234 const float badge_x = title_x + drawn_title_w + 6.0f;
1235 if (badge_x < title_max_x) {
1236 ImGui::SameLine(0.0f, 6.0f);
1237 DrawHeaderContextBadge(type, title_max_x - badge_x);
1238 }
1239
1240 // Right-aligned chrome buttons (right → left)
1241 const float btn_y = header_min.y + (header_height - chrome_btn_size.y) * 0.5f;
1242 float current_x = ImGui::GetWindowWidth() - chrome_btn_size.x - padding;
1243
1244 // 1. Close Button
1245 ImGui::SetCursorScreenPos(ImVec2(header_min.x + current_x, btn_y));
1247 ICON_MD_CANCEL, chrome_btn_size, "Close Drawer (Esc)", false,
1248 ImVec4(0, 0, 0, 0), "right_sidebar", "close_panel")) {
1249 CloseDrawer();
1250 }
1251
1252 // Panel-specific quick actions
1253 if (type == PanelType::kProperties) {
1254 current_x -= (chrome_btn_size.x + chrome_gap);
1255 ImGui::SetCursorScreenPos(ImVec2(header_min.x + current_x, btn_y));
1258 chrome_btn_size,
1259 properties_locked_ ? "Unlock Selection" : "Lock Selection",
1260 properties_locked_, ImVec4(0, 0, 0, 0), "right_sidebar",
1261 "lock_selection")) {
1263 }
1264 } else if (type == PanelType::kAgentChat) {
1265#ifdef YAZE_BUILD_AGENT_UI
1266 if (agent_chat_) {
1267 current_x -= (chrome_btn_size.x + chrome_gap);
1268 ImGui::SetCursorScreenPos(ImVec2(header_min.x + current_x, btn_y));
1270 ICON_MD_DELETE_SWEEP, chrome_btn_size, "Clear Chat History",
1271 false, ImVec4(0, 0, 0, 0), "right_sidebar", "agent_clear_chat")) {
1273 }
1274 current_x -= (chrome_btn_size.x + chrome_gap);
1275 ImGui::SetCursorScreenPos(ImVec2(header_min.x + current_x, btn_y));
1277 ICON_MD_SAVE_ALT, chrome_btn_size, "Save Chat History", false,
1278 ImVec4(0, 0, 0, 0), "right_sidebar", "agent_save_chat")) {
1279 agent_chat_->SaveHistory(ResolveAgentChatHistoryPath());
1280 }
1281 if (proposal_drawer_) {
1282 current_x -= (chrome_btn_size.x + chrome_gap);
1283 ImGui::SetCursorScreenPos(ImVec2(header_min.x + current_x, btn_y));
1285 ICON_MD_DESCRIPTION, chrome_btn_size, "Open Proposals", false,
1286 ImVec4(0, 0, 0, 0), "right_sidebar", "agent_open_proposals")) {
1288 }
1289 }
1290 }
1291#endif
1292 } else if (type == PanelType::kNotifications) {
1293 if (toast_manager_) {
1294 current_x -= (chrome_btn_size.x + chrome_gap);
1295 ImGui::SetCursorScreenPos(ImVec2(header_min.x + current_x, btn_y));
1297 ICON_MD_DELETE_SWEEP, chrome_btn_size, "Clear All Notifications",
1298 false, ImVec4(0, 0, 0, 0), "right_sidebar", "notif_clear_all")) {
1300 }
1301 current_x -= (chrome_btn_size.x + chrome_gap);
1302 ImGui::SetCursorScreenPos(ImVec2(header_min.x + current_x, btn_y));
1303 if (gui::TransparentIconButton(ICON_MD_DONE_ALL, chrome_btn_size,
1304 "Mark All Read", false, ImVec4(0, 0, 0, 0),
1305 "right_sidebar", "notif_mark_read")) {
1307 }
1308 }
1309 } else if (type == PanelType::kToolOutput) {
1310 if (!tool_output_content_.empty()) {
1311 current_x -= (chrome_btn_size.x + chrome_gap);
1312 ImGui::SetCursorScreenPos(ImVec2(header_min.x + current_x, btn_y));
1314 "Copy Output", false, ImVec4(0, 0, 0, 0),
1315 "right_sidebar", "tool_copy_out")) {
1316 ImGui::SetClipboardText(tool_output_content_.c_str());
1317 }
1318 }
1319 } else if (type == PanelType::kHelp) {
1320 current_x -= (chrome_btn_size.x + chrome_gap);
1321 ImGui::SetCursorScreenPos(ImVec2(header_min.x + current_x, btn_y));
1323 ICON_MD_OPEN_IN_NEW, chrome_btn_size, "Open Online Documentation",
1324 false, ImVec4(0, 0, 0, 0), "right_sidebar", "help_open_docs")) {
1325 gui::OpenUrl("https://github.com/scawful/yaze/wiki");
1326 }
1327 }
1328
1329 ImGui::SetCursorPosY(header_height);
1330}
1331
1333 const float standard_height =
1335 const float padding =
1336 std::clamp(gui::LayoutHelpers::GetStandardSpacing(), 4.0f, 8.0f);
1337 const float gap = std::max(2.0f, padding * 0.5f);
1338 const float tab_h = standard_height;
1339 const float nav_height = tab_h + padding * 2.0f;
1340
1341 const ImVec2 nav_min = ImGui::GetCursorScreenPos();
1342 const ImVec2 nav_max =
1343 ImVec2(nav_min.x + ImGui::GetWindowWidth(), nav_min.y + nav_height);
1344
1345 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1346 draw_list->AddRectFilled(nav_min, nav_max,
1347 ImGui::GetColorU32(gui::GetSurfaceContainerVec4()));
1348 draw_list->AddLine(ImVec2(nav_min.x, nav_max.y), ImVec2(nav_max.x, nav_max.y),
1349 ImGui::GetColorU32(gui::GetOutlineVec4()), 1.0f);
1350
1351 const auto catalog = GetDrawerCatalog();
1352 const size_t count = catalog.size();
1353 if (count == 0) {
1354 ImGui::SetCursorPosY(ImGui::GetCursorPosY() + nav_height + 4.0f);
1355 return;
1356 }
1357
1358 const float avail_w = ImGui::GetWindowWidth() - padding * 2.0f;
1359 const float tab_y = nav_min.y + (nav_height - tab_h) * 0.5f;
1360 std::vector<float> labeled_widths;
1361 labeled_widths.reserve(count);
1362 float labeled_total = gap * static_cast<float>(count - 1);
1363 for (const DrawerCatalogEntry& entry : catalog) {
1364 const std::string label = absl::StrFormat("%s %s", entry.icon, entry.name);
1365 const float width =
1366 std::max(tab_h, ImGui::CalcTextSize(label.c_str()).x + padding * 2.0f);
1367 labeled_widths.push_back(width);
1368 labeled_total += width;
1369 }
1370 const bool show_labels = labeled_total <= avail_w;
1371 const float extra_per_tab =
1372 show_labels ? std::max(0.0f, (avail_w - labeled_total) / count) : 0.0f;
1373 const float compact_tab_w =
1374 std::max(tab_h, std::floor((avail_w - (count - 1) * gap) / count));
1375 float tab_x = nav_min.x + padding;
1376
1377 for (size_t i = 0; i < count; ++i) {
1378 const DrawerCatalogEntry& entry = catalog[i];
1379 const bool is_active = (current_panel == entry.type);
1380 const float tab_w =
1381 show_labels ? labeled_widths[i] + extra_per_tab : compact_tab_w;
1382
1383 const ImVec2 tab_rect_min(tab_x, tab_y);
1384 const ImVec2 tab_rect_max(tab_x + tab_w, tab_y + tab_h);
1385
1386 ImGui::SetCursorScreenPos(tab_rect_min);
1387 const std::string tab_id = absl::StrFormat("##drawer_nav_%zu_%s", i,
1388 entry.name ? entry.name : "x");
1389 if (ImGui::InvisibleButton(tab_id.c_str(), ImVec2(tab_w, tab_h))) {
1390 if (!is_active) {
1391 OpenDrawer(entry.type);
1392 }
1393 }
1394 const bool hovered = ImGui::IsItemHovered();
1395
1396 if (hovered) {
1397 draw_list->AddRectFilled(
1398 tab_rect_min, tab_rect_max,
1399 ImGui::GetColorU32(gui::GetSurfaceContainerHighVec4()), 4.0f);
1400 }
1401 if (is_active) {
1402 draw_list->AddLine(ImVec2(tab_rect_min.x + 3.0f, tab_rect_max.y - 1.0f),
1403 ImVec2(tab_rect_max.x - 3.0f, tab_rect_max.y - 1.0f),
1404 ImGui::GetColorU32(gui::GetPrimaryVec4()), 2.0f);
1405 }
1406
1407 const std::string tab_label =
1408 show_labels ? absl::StrFormat("%s %s", entry.icon, entry.name)
1409 : std::string(entry.icon);
1410 const ImVec2 label_size = ImGui::CalcTextSize(tab_label.c_str());
1411 const ImVec2 label_pos(tab_rect_min.x + (tab_w - label_size.x) * 0.5f,
1412 tab_rect_min.y + (tab_h - label_size.y) * 0.5f);
1413 const ImVec4 icon_col = (is_active || hovered)
1416 draw_list->AddText(label_pos, ImGui::GetColorU32(icon_col),
1417 tab_label.c_str());
1418
1419 // Unread badge dot on Notifications tab
1422 draw_list->AddCircleFilled(
1423 ImVec2(tab_rect_max.x - 4.0f, tab_rect_min.y + 4.0f), 3.0f,
1424 ImGui::GetColorU32(gui::GetPrimaryVec4()));
1425 }
1426
1427 if (hovered) {
1428 std::string tip = entry.name ? entry.name : "";
1429 if (entry.shortcut_action && entry.shortcut_action[0] != '\0') {
1430 const std::string sc = GetShortcutLabel(entry.shortcut_action, "");
1431 if (!sc.empty() && sc != "Unassigned") {
1432 tip = absl::StrFormat("%s (%s)", entry.name, sc.c_str());
1433 }
1434 }
1435 ImGui::SetTooltip("%s", tip.c_str());
1436 }
1437 tab_x += tab_w + gap;
1438 }
1439
1440 // Advance cursor past the nav strip + small gap
1441 const float header_height =
1443 ImGui::SetCursorPosY(header_height + nav_height +
1444 gui::ScaledSize(0.0f, 4.0f).y);
1445}
1446
1447// =============================================================================
1448// Panel Styling Helpers
1449// =============================================================================
1450
1451bool RightDrawerManager::BeginPanelSection(const char* label, const char* icon,
1452 bool default_open) {
1453 gui::StyleColorGuard section_colors({
1454 {ImGuiCol_Header, gui::GetSurfaceContainerHighVec4()},
1455 {ImGuiCol_HeaderHovered, gui::GetSurfaceContainerHighestVec4()},
1456 {ImGuiCol_HeaderActive, gui::GetSurfaceContainerHighestVec4()},
1457 });
1458 gui::StyleVarGuard section_vars({
1459 {ImGuiStyleVar_FramePadding, ImVec2(8.0f, 6.0f)},
1460 {ImGuiStyleVar_FrameRounding, 4.0f},
1461 });
1462
1463 // Build header text with icon if provided
1464 std::string header_text;
1465 if (icon) {
1466 header_text = std::string(icon) + " " + label;
1467 } else {
1468 header_text = label;
1469 }
1470
1471 ImGuiTreeNodeFlags flags =
1472 ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_SpanAvailWidth |
1473 ImGuiTreeNodeFlags_AllowOverlap | ImGuiTreeNodeFlags_FramePadding;
1474 if (default_open) {
1475 flags |= ImGuiTreeNodeFlags_DefaultOpen;
1476 }
1477
1478 bool is_open = ImGui::TreeNodeEx(header_text.c_str(), flags);
1479
1480 if (is_open) {
1481 ImGui::Spacing();
1482 ImGui::Indent(4.0f);
1483 }
1484
1485 return is_open;
1486}
1487
1489 ImGui::Unindent(4.0f);
1490 ImGui::TreePop();
1491 ImGui::Spacing();
1492}
1493
1495 ImGui::Spacing();
1496 {
1497 gui::StyleColorGuard sep_color(ImGuiCol_Separator, gui::GetOutlineVec4());
1498 ImGui::Separator();
1499 }
1500 ImGui::Spacing();
1501}
1502
1506
1507void RightDrawerManager::DrawPanelValue(const char* label, const char* value) {
1509 ImGui::SameLine();
1510 ImGui::TextUnformatted(value);
1511}
1512
1514 gui::StyleColorGuard desc_color(ImGuiCol_Text, gui::GetTextDisabledVec4());
1515 ImGui::PushTextWrapPos(ImGui::GetContentRegionAvail().x);
1516 ImGui::TextWrapped("%s", text);
1517 ImGui::PopTextWrapPos();
1518}
1519
1521 const std::string& action, const std::string& fallback) const {
1522 if (!shortcut_manager_) {
1523 return fallback;
1524 }
1525
1526 const Shortcut* shortcut = shortcut_manager_->FindShortcut(action);
1527 if (!shortcut) {
1528 return fallback;
1529 }
1530 if (shortcut->keys.empty()) {
1531 return "Unassigned";
1532 }
1533
1534 return PrintShortcut(shortcut->keys);
1535}
1536
1537void RightDrawerManager::DrawShortcutRow(const std::string& action,
1538 const char* description,
1539 const std::string& fallback) {
1540 std::string label = GetShortcutLabel(action, fallback);
1541 DrawPanelValue(label.c_str(), description);
1542}
1543
1544// =============================================================================
1545// Panel Content Drawing
1546// =============================================================================
1547
1549#ifdef YAZE_BUILD_AGENT_UI
1550 if (!agent_chat_) {
1551 gui::ColoredText(ICON_MD_SMART_TOY " AI Agent Not Available",
1553 ImGui::Spacing();
1555 "The AI Agent is not initialized. "
1556 "Open the AI Agent from View menu or use Ctrl+Shift+A.");
1557 return;
1558 }
1559
1560 agent_chat_->set_active(true);
1561
1562 // Actions (clear/save/proposals) have moved to the panel header.
1563 if (ImGui::BeginChild("AgentChatBody", ImVec2(0, 0), false)) {
1564 agent_chat_->Draw(0.0f);
1565 }
1566 ImGui::EndChild();
1567#else
1568 gui::ColoredText(ICON_MD_SMART_TOY " AI Agent Not Available",
1570
1571 ImGui::Spacing();
1573 "The AI Agent requires agent UI support. "
1574 "Build with YAZE_BUILD_AGENT_UI=ON to enable.");
1575#endif
1576}
1577
1579#ifdef YAZE_BUILD_AGENT_UI
1580 if (!agent_chat_) {
1581 return false;
1582 }
1583 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
1584 const ImVec4 accent = gui::GetPrimaryVec4();
1585
1586 std::string selection_context;
1588 selection_context =
1589 BuildSelectionContextSummary(properties_panel_->GetSelection());
1590 }
1591
1592 struct QuickAction {
1593 const char* label;
1594 std::string prompt;
1595 };
1596
1597 std::vector<QuickAction> actions;
1598 if (!selection_context.empty()) {
1599 actions.push_back({"Explain selection",
1600 "Explain this selection and how to edit it safely.\n\n" +
1601 selection_context});
1602 actions.push_back(
1603 {"Suggest fixes",
1604 "Suggest improvements or checks for this selection.\n\n" +
1605 selection_context});
1606 }
1607
1608 switch (active_editor_type_) {
1610 actions.push_back({"Summarize map",
1611 "Summarize the current overworld map and its key "
1612 "features. Use overworld tools if available."});
1613 actions.push_back({"List sprites/items",
1614 "List notable sprites or items on the current "
1615 "overworld map."});
1616 break;
1618 actions.push_back({"Audit room",
1619 "Summarize the current dungeon room layout, doors, "
1620 "and object density."});
1621 actions.push_back({"List sprites",
1622 "List sprites in the current dungeon room and any "
1623 "potential conflicts."});
1624 break;
1626 actions.push_back({"Review tiles",
1627 "Review the current tileset usage and point out any "
1628 "obvious issues."});
1629 actions.push_back({"Palette check",
1630 "Check palette usage for contrast/readability "
1631 "problems."});
1632 break;
1634 actions.push_back({"Palette audit",
1635 "Audit the active palette for hue/contrast balance "
1636 "and note risks."});
1637 actions.push_back({"Theme ideas",
1638 "Suggest a palette variation that fits the current "
1639 "scene style."});
1640 break;
1642 actions.push_back({"Sprite review",
1643 "Review the selected sprite properties and suggest "
1644 "tuning."});
1645 break;
1647 actions.push_back({"Copy edit",
1648 "Review the current message text for clarity and "
1649 "style improvements."});
1650 break;
1652 actions.push_back({"ASM review",
1653 "Review the current ASM changes for risks and style "
1654 "issues."});
1655 break;
1656 case EditorType::kHex:
1657 actions.push_back({"Hex context",
1658 "Explain what the current hex selection likely "
1659 "represents."});
1660 break;
1662 actions.push_back({"Test suggestion",
1663 "Propose a short emulator test to validate the "
1664 "current feature."});
1665 break;
1666 case EditorType::kAgent:
1667 actions.push_back({"Agent config review",
1668 "Review current agent configuration for practical "
1669 "improvements."});
1670 break;
1671 default:
1672 actions.push_back({"Agent overview",
1673 "Suggest the next best agent-assisted action for the "
1674 "current editor context."});
1675 break;
1676 }
1677
1678 if (actions.empty()) {
1679 return false;
1680 }
1681
1682 ImGui::TextColored(accent, tr("%s Editor Actions"), ICON_MD_BOLT);
1683 gui::ColoredText("Send a context-aware prompt to the agent.",
1685
1686 int columns = ImGui::GetContentRegionAvail().x > 420.0f ? 2 : 1;
1687 if (ImGui::BeginTable("AgentQuickActionsTable", columns,
1688 ImGuiTableFlags_SizingStretchSame)) {
1689 for (const auto& action : actions) {
1690 ImGui::TableNextColumn();
1691 if (ImGui::Button(action.label, ImVec2(-1, 0))) {
1692 agent_chat_->SendMessage(action.prompt);
1693 }
1694 }
1695 ImGui::EndTable();
1696 }
1697 return true;
1698#else
1699 return false;
1700#endif
1701}
1702
1704 if (proposal_drawer_) {
1705 // Set ROM and draw content inside the panel (not a separate window)
1706 if (rom_) {
1708 }
1710 } else {
1711 gui::ColoredText(ICON_MD_DESCRIPTION " Proposals Not Available",
1713
1714 ImGui::Spacing();
1716 "The proposal system is not initialized. "
1717 "Proposals will appear here when the AI Agent creates them.");
1718 }
1719}
1720
1724 }
1725 if (settings_panel_) {
1726 // Draw settings inline (no card windows)
1728 } else {
1729 gui::ColoredText(ICON_MD_SETTINGS " Settings Not Available",
1731
1732 ImGui::Spacing();
1734 "Settings will be available once initialized. "
1735 "This panel provides quick access to application settings.");
1736 }
1737}
1738
1740 // Context-aware editor header
1742
1743 // Keyboard Shortcuts section (default open)
1744 if (BeginPanelSection("Keyboard Shortcuts", ICON_MD_KEYBOARD, true)) {
1748 }
1749
1750 // Editor-specific help (default open)
1751 if (BeginPanelSection("Editor Guide", ICON_MD_HELP, true)) {
1754 }
1755
1756 // Quick Actions (collapsed by default)
1757 if (BeginPanelSection("Quick Actions", ICON_MD_BOLT, false)) {
1760 }
1761
1762 // About section (collapsed by default)
1763 if (BeginPanelSection("About", ICON_MD_INFO, false)) {
1766 }
1767}
1768
1770 const char* editor_name = "No Editor Selected";
1771 const char* editor_icon = ICON_MD_HELP;
1772
1773 switch (active_editor_type_) {
1775 editor_name = "Overworld Editor";
1776 editor_icon = ICON_MD_LANDSCAPE;
1777 break;
1779 editor_name = "Dungeon Editor";
1780 editor_icon = ICON_MD_CASTLE;
1781 break;
1783 editor_name = "Graphics Editor";
1784 editor_icon = ICON_MD_IMAGE;
1785 break;
1787 editor_name = "Palette Editor";
1788 editor_icon = ICON_MD_PALETTE;
1789 break;
1790 case EditorType::kMusic:
1791 editor_name = "Music Editor";
1792 editor_icon = ICON_MD_MUSIC_NOTE;
1793 break;
1795 editor_name = "Screen Editor";
1796 editor_icon = ICON_MD_TV;
1797 break;
1799 editor_name = "Sprite Editor";
1800 editor_icon = ICON_MD_SMART_TOY;
1801 break;
1803 editor_name = "Message Editor";
1804 editor_icon = ICON_MD_CHAT;
1805 break;
1807 editor_name = "Emulator";
1808 editor_icon = ICON_MD_VIDEOGAME_ASSET;
1809 break;
1810 default:
1811 break;
1812 }
1813
1814 // Draw context header with editor info
1815 gui::ColoredTextF(gui::GetPrimaryVec4(), "%s %s Help", editor_icon,
1816 editor_name);
1817
1819}
1820
1822 const char* ctrl = gui::GetCtrlDisplayName();
1823 DrawPanelLabel("Global");
1824 ImGui::Indent(8.0f);
1825 DrawShortcutRow("Open", "Open ROM", absl::StrFormat("%s+O", ctrl));
1826 DrawShortcutRow("Save", "Save ROM", absl::StrFormat("%s+S", ctrl));
1827 DrawShortcutRow("Save As", "Save ROM As",
1828 absl::StrFormat("%s+Shift+S", ctrl));
1829 DrawShortcutRow("Undo", "Undo", absl::StrFormat("%s+Z", ctrl));
1830 DrawShortcutRow("Redo", "Redo", absl::StrFormat("%s+Shift+Z", ctrl));
1831 DrawShortcutRow("Command Palette", "Command Palette",
1832 absl::StrFormat("%s+Shift+P", ctrl));
1833 DrawShortcutRow("Global Search", "Global Search",
1834 absl::StrFormat("%s+Shift+K", ctrl));
1835 DrawShortcutRow("view.toggle_activity_bar", "Toggle Sidebar",
1836 absl::StrFormat("%s+B", ctrl));
1837 DrawShortcutRow("Show About", "About / Help", "F1");
1838 DrawPanelValue("Esc", "Close Drawer");
1839 ImGui::Unindent(8.0f);
1840 ImGui::Spacing();
1841}
1842
1844 const char* ctrl = gui::GetCtrlDisplayName();
1845 switch (active_editor_type_) {
1847 DrawPanelLabel("Overworld");
1848 ImGui::Indent(8.0f);
1849 DrawPanelValue("1 / 2 / 3", "Select / Paint / Entrances");
1850 DrawPanelValue("Alt+1 / Alt+2 / Alt+3", "Switch World (LW/DW/SP)");
1851 DrawPanelValue("Arrow Keys", "Navigate Maps");
1852 DrawPanelValue("E", "Entity Mode");
1853 DrawPanelValue("T", "Tile Mode");
1854 DrawShortcutRow("overworld.brush_toggle", "Toggle brush", "B");
1855 DrawShortcutRow("overworld.fill", "Fill tool", "F");
1856 DrawShortcutRow("overworld.next_tile", "Next tile", "]");
1857 DrawShortcutRow("overworld.prev_tile", "Previous tile", "[");
1858 DrawPanelValue("Right Click", "Pick Tile");
1859 ImGui::Unindent(8.0f);
1860 break;
1861
1863 DrawPanelLabel("Dungeon");
1864 ImGui::Indent(8.0f);
1865 DrawShortcutRow("dungeon.object.select_tool", "Select tool", "S");
1866 DrawShortcutRow("dungeon.object.place_tool", "Place tool", "P");
1867 DrawShortcutRow("dungeon.object.delete_tool", "Delete tool", "D");
1868 DrawShortcutRow("dungeon.object.copy", "Copy selection",
1869 absl::StrFormat("%s+C", ctrl));
1870 DrawShortcutRow("dungeon.object.paste", "Paste selection",
1871 absl::StrFormat("%s+V", ctrl));
1872 DrawShortcutRow("dungeon.object.delete", "Delete selection", "Delete");
1873 DrawPanelValue("Arrow Keys", "Move Object");
1874 DrawPanelValue("G", "Toggle Grid");
1875 DrawPanelValue("L", "Cycle Layers");
1876 ImGui::Unindent(8.0f);
1877 break;
1878
1880 DrawPanelLabel("Graphics");
1881 ImGui::Indent(8.0f);
1882 DrawShortcutRow("graphics.prev_sheet", "Previous sheet", "PageUp");
1883 DrawShortcutRow("graphics.next_sheet", "Next sheet", "PageDown");
1884 DrawShortcutRow("graphics.tool.pencil", "Pencil tool", "B");
1885 DrawShortcutRow("graphics.tool.fill", "Fill tool", "G");
1886 DrawShortcutRow("graphics.zoom_in", "Zoom in", "+");
1887 DrawShortcutRow("graphics.zoom_out", "Zoom out", "-");
1888 DrawShortcutRow("graphics.toggle_grid", "Toggle grid",
1889 absl::StrFormat("%s+G", ctrl));
1890 ImGui::Unindent(8.0f);
1891 break;
1892
1894 DrawPanelLabel("Palette");
1895 ImGui::Indent(8.0f);
1896 DrawPanelValue("Click", "Select Color");
1897 DrawPanelValue("Double Click", "Edit Color");
1898 DrawPanelValue("Drag", "Copy Color");
1899 ImGui::Unindent(8.0f);
1900 break;
1901
1902 case EditorType::kMusic:
1903 DrawPanelLabel("Music");
1904 ImGui::Indent(8.0f);
1905 DrawShortcutRow("music.play_pause", "Play/Pause", "Space");
1906 DrawShortcutRow("music.stop", "Stop", "Esc");
1907 DrawShortcutRow("music.speed_up", "Speed up", "+");
1908 DrawShortcutRow("music.speed_down", "Slow down", "-");
1909 DrawPanelValue("Left/Right", "Seek");
1910 ImGui::Unindent(8.0f);
1911 break;
1912
1914 DrawPanelLabel("Message");
1915 ImGui::Indent(8.0f);
1916 DrawPanelValue(absl::StrFormat("%s+Enter", ctrl).c_str(),
1917 "Insert Line Break");
1918 DrawPanelValue("Up/Down", "Navigate Messages");
1919 ImGui::Unindent(8.0f);
1920 break;
1921
1922 default:
1923 DrawPanelLabel("Editor Shortcuts");
1924 ImGui::Indent(8.0f);
1925 {
1926 gui::StyleColorGuard text_color(ImGuiCol_Text,
1928 ImGui::TextWrapped(tr("Select an editor to see specific shortcuts."));
1929 }
1930 ImGui::Unindent(8.0f);
1931 break;
1932 }
1933}
1934
1936 switch (active_editor_type_) {
1938 gui::StyleColorGuard text_color(ImGuiCol_Text,
1939 ImGui::GetStyleColorVec4(ImGuiCol_Text));
1940 ImGui::Bullet();
1941 ImGui::TextWrapped(tr("Paint tiles by selecting from Tile16 Selector"));
1942 ImGui::Bullet();
1943 ImGui::TextWrapped(
1944 tr("Switch between Light World, Dark World, and Special Areas"));
1945 ImGui::Bullet();
1946 ImGui::TextWrapped(
1947 tr("Use Entity Mode to place entrances, exits, items, and sprites"));
1948 ImGui::Bullet();
1949 ImGui::TextWrapped(
1950 tr("Right-click on the map to pick a tile for painting"));
1951 } break;
1952
1953 case EditorType::kDungeon: {
1954 gui::StyleColorGuard text_color(ImGuiCol_Text,
1955 ImGui::GetStyleColorVec4(ImGuiCol_Text));
1956 ImGui::Bullet();
1957 ImGui::TextWrapped(
1958 tr("Select rooms from the Room Selector or Room Matrix"));
1959 ImGui::Bullet();
1960 ImGui::TextWrapped(tr("Place objects using the Object Editor panel"));
1961 ImGui::Bullet();
1962 ImGui::TextWrapped(
1963 tr("Edit room headers for palette, GFX, and floor settings"));
1964 ImGui::Bullet();
1965 ImGui::TextWrapped(tr("Multiple rooms can be opened in separate tabs"));
1966 } break;
1967
1968 case EditorType::kGraphics: {
1969 gui::StyleColorGuard text_color(ImGuiCol_Text,
1970 ImGui::GetStyleColorVec4(ImGuiCol_Text));
1971 ImGui::Bullet();
1972 ImGui::TextWrapped(tr("Browse graphics sheets using the Sheet Browser"));
1973 ImGui::Bullet();
1974 ImGui::TextWrapped(tr("Edit pixels directly with the Pixel Editor"));
1975 ImGui::Bullet();
1976 ImGui::TextWrapped(tr("Choose palettes from Palette Controls"));
1977 ImGui::Bullet();
1978 ImGui::TextWrapped(tr("View 3D objects like rupees and crystals"));
1979 } break;
1980
1981 case EditorType::kPalette: {
1982 gui::StyleColorGuard text_color(ImGuiCol_Text,
1983 ImGui::GetStyleColorVec4(ImGuiCol_Text));
1984 ImGui::Bullet();
1985 ImGui::TextWrapped(tr("Edit overworld, dungeon, and sprite palettes"));
1986 ImGui::Bullet();
1987 ImGui::TextWrapped(tr("Use Quick Access for color harmony tools"));
1988 ImGui::Bullet();
1989 ImGui::TextWrapped(tr("Changes update in real-time across all editors"));
1990 } break;
1991
1992 case EditorType::kMusic: {
1993 gui::StyleColorGuard text_color(ImGuiCol_Text,
1994 ImGui::GetStyleColorVec4(ImGuiCol_Text));
1995 ImGui::Bullet();
1996 ImGui::TextWrapped(tr("Browse songs in the Song Browser"));
1997 ImGui::Bullet();
1998 ImGui::TextWrapped(tr("Use the tracker for playback control"));
1999 ImGui::Bullet();
2000 ImGui::TextWrapped(tr("Edit instruments and BRR samples"));
2001 } break;
2002
2003 case EditorType::kMessage: {
2004 gui::StyleColorGuard text_color(ImGuiCol_Text,
2005 ImGui::GetStyleColorVec4(ImGuiCol_Text));
2006 ImGui::Bullet();
2007 ImGui::TextWrapped(tr("Edit all in-game dialog messages"));
2008 ImGui::Bullet();
2009 ImGui::TextWrapped(tr("Preview text rendering with the font atlas"));
2010 ImGui::Bullet();
2011 ImGui::TextWrapped(tr("Manage the compression dictionary"));
2012 } break;
2013
2014 default:
2015 ImGui::Bullet();
2016 ImGui::TextWrapped(tr("Open a ROM file via File > Open ROM"));
2017 ImGui::Bullet();
2018 ImGui::TextWrapped(tr("Select an editor from the sidebar"));
2019 ImGui::Bullet();
2020 ImGui::TextWrapped(tr("Use panels to access tools and settings"));
2021 ImGui::Bullet();
2022 ImGui::TextWrapped(tr("Save your work via File > Save ROM"));
2023 break;
2024 }
2025}
2026
2028 const float button_width = ImGui::GetContentRegionAvail().x;
2029
2030 gui::StyleVarGuard button_vars({
2031 {ImGuiStyleVar_FramePadding, ImVec2(10.0f, 7.0f)},
2032 {ImGuiStyleVar_FrameRounding, 6.0f},
2033 {ImGuiStyleVar_FrameBorderSize, 1.0f},
2034 });
2035
2036 auto draw_link_button = [&](const char* icon, const char* label,
2037 const char* url) {
2038 gui::StyleColorGuard btn_colors({
2039 {ImGuiCol_Button, gui::GetSurfaceContainerHighVec4()},
2040 {ImGuiCol_ButtonHovered, gui::GetSurfaceContainerHighestVec4()},
2041 {ImGuiCol_Border, gui::GetOutlineVec4()},
2042 });
2043 const std::string text =
2044 absl::StrFormat("%s %s " ICON_MD_OPEN_IN_NEW, icon, label);
2045 if (ImGui::Button(text.c_str(), ImVec2(button_width, 0))) {
2046 gui::OpenUrl(url);
2047 }
2048 };
2049
2050 draw_link_button(ICON_MD_DESCRIPTION, "Open Documentation",
2051 "https://github.com/scawful/yaze/wiki");
2052 ImGui::Spacing();
2053 draw_link_button(ICON_MD_BUG_REPORT, "Report Issue",
2054 "https://github.com/scawful/yaze/issues/new");
2055 ImGui::Spacing();
2056 draw_link_button(ICON_MD_FORUM, "Join Discord",
2057 "https://discord.gg/zU5qDm8MZg");
2058}
2059
2061 gui::ColoredText("YAZE - Yet Another Zelda3 Editor", gui::GetPrimaryVec4());
2062
2063 ImGui::Spacing();
2065 "A comprehensive editor for The Legend of Zelda: "
2066 "A Link to the Past ROM files.");
2067
2069
2070 DrawPanelLabel("Credits");
2071 ImGui::Spacing();
2072 ImGui::Text(tr("Written by: scawful"));
2073 ImGui::Text(tr("Special Thanks: Zarby89, JaredBrian"));
2074
2076
2077 DrawPanelLabel("Links");
2078 ImGui::Spacing();
2079 gui::ColoredText(ICON_MD_LINK " github.com/scawful/yaze",
2081}
2082
2084 if (!toast_manager_) {
2085 gui::ColoredText(ICON_MD_NOTIFICATIONS_OFF " Notifications Unavailable",
2087 return;
2088 }
2089
2090 // Header actions
2091 float avail = ImGui::GetContentRegionAvail().x;
2092
2093 // Mark all read / Clear all buttons
2094 {
2095 gui::StyleColorGuard btn_colors({
2096 {ImGuiCol_Button, gui::GetSurfaceContainerHighVec4()},
2097 {ImGuiCol_ButtonHovered, gui::GetSurfaceContainerHighestVec4()},
2098 });
2099
2100 if (ImGui::Button(ICON_MD_DONE_ALL " Mark All Read",
2101 ImVec2(avail * 0.5f - 4.0f, 0))) {
2103 }
2104 ImGui::SameLine();
2105 if (ImGui::Button(ICON_MD_DELETE_SWEEP " Clear All",
2106 ImVec2(avail * 0.5f - 4.0f, 0))) {
2108 }
2109 }
2110
2112
2113 const auto build_status = ContentRegistry::Context::build_workflow_status();
2114 const auto run_status = ContentRegistry::Context::run_workflow_status();
2115 const auto workflow_history = ContentRegistry::Context::workflow_history();
2116 workflow::WorkflowActionCallbacks workflow_callbacks;
2117 workflow_callbacks.start_build =
2119 workflow_callbacks.run_project =
2121 workflow_callbacks.show_output =
2123 const auto cancel_build =
2125
2126 if (build_status.visible || run_status.visible || !workflow_history.empty()) {
2127 DrawPanelLabel("Workflow Activity");
2128 if (build_status.visible) {
2129 DrawWorkflowSummaryCard("Build", ICON_MD_BUILD, build_status,
2130 cancel_build);
2131 ImGui::Spacing();
2132 }
2133 if (run_status.visible) {
2134 DrawWorkflowSummaryCard("Run", ICON_MD_PLAY_ARROW, run_status);
2135 ImGui::Spacing();
2136 }
2137 if (!workflow_history.empty()) {
2138 DrawPanelLabel("Recent Workflow History");
2139 const auto preview_entries =
2140 workflow::SelectWorkflowPreviewEntries(workflow_history, 3);
2141 for (size_t i = 0; i < preview_entries.size(); ++i) {
2142 ImGui::PushID(static_cast<int>(i));
2143 DrawWorkflowPreviewEntry(preview_entries[i], workflow_callbacks);
2144 ImGui::PopID();
2145 if (i + 1 < preview_entries.size()) {
2146 ImGui::Separator();
2147 }
2148 }
2149 if (workflow_history.size() > preview_entries.size()) {
2150 ImGui::Spacing();
2151 ImGui::TextDisabled(
2152 tr("+%zu more entries available in Workflow Output"),
2153 workflow_history.size() - preview_entries.size());
2154 if (workflow_callbacks.show_output) {
2155 if (ImGui::SmallButton(ICON_MD_OPEN_IN_NEW
2156 " View Full History##workflow_view_full")) {
2157 workflow_callbacks.show_output();
2158 }
2159 }
2160 }
2161 }
2163 }
2164
2165 // Notification history
2166 const auto& history = toast_manager_->GetHistory();
2167
2168 if (history.empty()) {
2169 ImGui::Spacing();
2170 gui::ColoredText(ICON_MD_INBOX " No notifications",
2172 ImGui::Spacing();
2174 "Notifications will appear here when actions complete.");
2175 return;
2176 }
2177
2178 // Stats
2179 size_t unread_count = toast_manager_->GetUnreadCount();
2180 if (unread_count > 0) {
2181 gui::ColoredTextF(gui::GetPrimaryVec4(), "%zu unread", unread_count);
2182 } else {
2183 gui::ColoredText("All caught up", gui::GetTextSecondaryVec4());
2184 }
2185
2186 ImGui::Spacing();
2187
2188 // Scrollable notification list (minimum height so list never collapses)
2189 const bool notification_list_open = gui::LayoutHelpers::BeginContentChild(
2190 "##NotificationList", ImVec2(0.0f, gui::UIConfig::kContentMinHeightList),
2191 ImGuiChildFlags_None, ImGuiWindowFlags_AlwaysVerticalScrollbar);
2192 if (notification_list_open) {
2193 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
2194 auto now = std::chrono::system_clock::now();
2195
2196 // Group by time (Today, Yesterday, Older)
2197 bool shown_today = false;
2198 bool shown_yesterday = false;
2199 bool shown_older = false;
2200
2201 for (const auto& entry : history) {
2202 auto diff =
2203 std::chrono::duration_cast<std::chrono::hours>(now - entry.timestamp)
2204 .count();
2205
2206 // Time grouping headers
2207 if (diff < 24 && !shown_today) {
2208 DrawPanelLabel("Today");
2209 shown_today = true;
2210 } else if (diff >= 24 && diff < 48 && !shown_yesterday) {
2211 ImGui::Spacing();
2212 DrawPanelLabel("Yesterday");
2213 shown_yesterday = true;
2214 } else if (diff >= 48 && !shown_older) {
2215 ImGui::Spacing();
2216 DrawPanelLabel("Older");
2217 shown_older = true;
2218 }
2219
2220 // Notification item
2221 ImGui::PushID(&entry);
2222
2223 // Icon and color based on type
2224 const char* icon;
2225 ImVec4 color;
2226 switch (entry.type) {
2228 icon = ICON_MD_CHECK_CIRCLE;
2229 color = gui::ConvertColorToImVec4(theme.success);
2230 break;
2232 icon = ICON_MD_WARNING;
2233 color = gui::ConvertColorToImVec4(theme.warning);
2234 break;
2235 case ToastType::kError:
2236 icon = ICON_MD_ERROR;
2237 color = gui::ConvertColorToImVec4(theme.error);
2238 break;
2239 default:
2240 icon = ICON_MD_INFO;
2241 color = gui::ConvertColorToImVec4(theme.info);
2242 break;
2243 }
2244
2245 // Unread indicator
2246 if (!entry.read) {
2248 ImGui::SameLine();
2249 }
2250
2251 // Icon
2252 gui::ColoredTextF(color, "%s", icon);
2253 ImGui::SameLine();
2254
2255 // Message
2256 ImGui::TextWrapped("%s", entry.message.c_str());
2257
2258 // Timestamp
2259 auto diff_sec = std::chrono::duration_cast<std::chrono::seconds>(
2260 now - entry.timestamp)
2261 .count();
2262 std::string time_str;
2263 if (diff_sec < 60) {
2264 time_str = "just now";
2265 } else if (diff_sec < 3600) {
2266 time_str = absl::StrFormat("%dm ago", diff_sec / 60);
2267 } else if (diff_sec < 86400) {
2268 time_str = absl::StrFormat("%dh ago", diff_sec / 3600);
2269 } else {
2270 time_str = absl::StrFormat("%dd ago", diff_sec / 86400);
2271 }
2272
2273 gui::ColoredTextF(gui::GetTextDisabledVec4(), " %s", time_str.c_str());
2274
2275 ImGui::PopID();
2276 ImGui::Spacing();
2277 }
2278 }
2280}
2281
2283 if (properties_panel_) {
2285 } else {
2286 gui::DrawEmptyState(gui::EmptyNoSelection(/*compact=*/true));
2287 }
2288}
2289
2291 if (project_panel_) {
2293 } else {
2294 gui::DrawEmptyState(gui::EmptyNoProject(/*compact=*/true));
2295 }
2296}
2297
2299 if (!tool_output_query_.empty()) {
2301 ImGui::SameLine();
2302 if (ImGui::SmallButton(ICON_MD_CONTENT_COPY " Copy")) {
2303 ImGui::SetClipboardText(tool_output_query_.c_str());
2304 }
2305 ImGui::TextWrapped("%s", tool_output_query_.c_str());
2307 }
2308
2309 if (tool_output_content_.empty()) {
2311 opts.icon = ICON_MD_TERMINAL;
2312 opts.title = "No tool output";
2313 opts.detail =
2314 "Run a project-graph query from the editor to inspect its output here.";
2315 opts.compact = true;
2316 gui::DrawEmptyState(opts);
2317 return;
2318 }
2319
2320 Json parsed;
2321 const bool has_json = TryParseToolOutputJson(tool_output_content_, &parsed);
2322
2324 if (ImGui::SmallButton(ICON_MD_CONTENT_COPY " Copy Result")) {
2325 ImGui::SetClipboardText(tool_output_content_.c_str());
2326 }
2327
2328 if (has_json) {
2329 if (BeginPanelSection("Summary", ICON_MD_INFO, true)) {
2330 ImGui::PushID("summary");
2331 DrawToolOutputEntryActions(parsed, tool_output_actions_);
2332 if (!parsed.empty()) {
2333 ImGui::Spacing();
2334 }
2335 DrawJsonObjectFields(parsed);
2336 ImGui::PopID();
2338 }
2339 if (parsed.contains("source") && parsed["source"].is_object() &&
2340 BeginPanelSection("Resolved Source", ICON_MD_CODE, true)) {
2341 ImGui::PushID("resolved_source");
2342 DrawToolOutputEntryActions(parsed["source"], tool_output_actions_);
2343 if (!parsed["source"].empty()) {
2344 ImGui::Spacing();
2345 }
2346 DrawJsonObjectFields(parsed["source"]);
2347 ImGui::PopID();
2349 }
2350 DrawJsonObjectArraySection("Matching Symbols", parsed["matching_symbols"],
2352 DrawJsonObjectArraySection("Sources", parsed["sources"],
2354 DrawJsonObjectArraySection("Hooks", parsed["hooks"], tool_output_actions_);
2355 DrawJsonObjectArraySection("Writes", parsed["writes"],
2357 DrawJsonObjectArraySection("Banks", parsed["banks"], tool_output_actions_);
2358 DrawJsonObjectArraySection("Symbols", parsed["symbols"],
2360 }
2361
2362 if (ImGui::CollapsingHeader(tr("Raw Output"),
2363 has_json ? 0 : ImGuiTreeNodeFlags_DefaultOpen)) {
2364 if (ImGui::BeginChild("##tool_output_result", ImVec2(0.0f, 220.0f), true)) {
2365 ImGui::TextUnformatted(tool_output_content_.c_str());
2366 }
2367 ImGui::EndChild();
2368 }
2369}
2370
2372 const bool sidebar_open = active_panel_ != PanelType::kNone;
2373 gui::StyleColorGuard button_colors({
2374 {ImGuiCol_Button, ImVec4(0, 0, 0, 0)},
2375 {ImGuiCol_ButtonHovered, gui::GetSurfaceContainerHighVec4()},
2376 {ImGuiCol_ButtonActive, gui::GetSurfaceContainerHighestVec4()},
2377 {ImGuiCol_Text,
2378 sidebar_open ? gui::GetPrimaryVec4() : gui::GetTextSecondaryVec4()},
2379 });
2380
2381 bool interacted = false;
2382 if (ImGui::SmallButton(ICON_MD_VIEW_SIDEBAR "##RightSidebarToggle")) {
2384 interacted = true;
2385 }
2386 if (ImGui::IsItemHovered()) {
2387 const std::string tooltip =
2388 sidebar_open ? tr("Close Right Sidebar")
2389 : absl::StrFormat("Open Right Sidebar (%s)",
2391 ImGui::SetTooltip("%s", tooltip.c_str());
2392 }
2393
2394 return interacted;
2395}
2396
2398 const float frame_padding = ImGui::GetStyle().FramePadding.x;
2399 const float icon_width = ImGui::CalcTextSize(ICON_MD_VIEW_SIDEBAR).x;
2400 return icon_width + frame_padding * 2.0f;
2401}
2402
2403} // namespace editor
2404} // namespace yaze
bool is_object() const
Definition json.h:57
bool is_boolean() const
Definition json.h:55
static Json parse(const std::string &)
Definition json.h:36
bool is_array() const
Definition json.h:58
bool is_null() const
Definition json.h:54
bool is_string() const
Definition json.h:59
size_t size() const
Definition json.h:61
T get() const
Definition json.h:49
bool empty() const
Definition json.h:62
std::string dump(int=-1, char=' ', bool=false, int=0) const
Definition json.h:91
bool contains(const std::string &) const
Definition json.h:53
void SendMessage(const std::string &message)
void Draw(float available_height=0.0f)
void set_active(bool active)
Definition agent_chat.h:71
absl::Status SaveHistory(const std::string &filepath)
void CycleDrawer(int direction)
Cycle to the next/previous right drawer in header order.
static std::string PanelTypeKey(PanelType type)
void NotifyPanelWidthChanged(PanelType type, float width)
void DrawPanelHeader(PanelType type, const char *title, const char *icon)
void DrawShortcutRow(const std::string &action, const char *description, const std::string &fallback)
std::string GetShortcutLabel(const std::string &action, const std::string &fallback) const
float GetClampedPanelWidth(PanelType type, float viewport_width) const
void OpenDrawer(DrawerType type)
Open a specific drawer.
void SetDrawerWidth(DrawerType type, float width)
Set drawer width for a specific drawer type.
void SetToolOutput(std::string title, std::string query, std::string content, ToolOutputActions actions={})
void ToggleLastDrawer()
Toggle the right sidebar using its last selected persistent tab.
void DrawPanelValue(const char *label, const char *value)
void Draw()
Draw the drawer and its contents.
std::function< void(PanelType, float)> on_panel_width_changed_
void ResetDrawerWidths()
Reset all drawer widths to their defaults.
SelectionPropertiesPanel * properties_panel_
bool BeginPanelSection(const char *label, const char *icon=nullptr, bool default_open=true)
void ToggleDrawer(DrawerType type)
Toggle a specific drawer on/off.
static float GetDefaultDrawerWidth(DrawerType type, EditorType editor=EditorType::kUnknown)
Get the default width for a specific drawer type.
float GetConfiguredPanelWidth(PanelType type) const
PanelSizeLimits GetSharedPanelSizeLimits() const
std::unordered_map< std::string, PanelSizeLimits > panel_size_limits_
void DrawHeaderContextBadge(PanelType type, float available_width)
bool IsDrawerExpanded() const
Check if any drawer is currently expanded (or animating closed)
void OnHostVisibilityChanged(bool visible)
Snap transient animations when host visibility changes.
void CloseDrawer()
Close the currently active drawer.
void RestoreDrawerWidths(const std::unordered_map< std::string, float > &widths)
std::unordered_map< std::string, float > SerializeDrawerWidths() const
ProjectManagementPanel * project_panel_
void DrawDrawerNavStrip(PanelType current_panel)
PanelSizeLimits GetPanelSizeLimits(PanelType type) const
std::function< SettingsPanel *()> settings_panel_provider_
void SetPanelSizeLimits(PanelType type, const PanelSizeLimits &limits)
Set sizing constraints for an individual right panel.
const SelectionContext & GetSelection() const
Get the current selection context.
bool HasSelection() const
Check if there's an active selection.
void Draw()
Draw the properties panel content.
const Shortcut * FindShortcut(const std::string &name) const
const std::deque< NotificationEntry > & GetHistory() const
bool IsEnabled() const
Definition animator.cc:264
static bool BeginContentChild(const char *id, const ImVec2 &min_size, bool border=false, ImGuiWindowFlags flags=0)
static void EndContentChild()
static SafeAreaInsets GetSafeAreaInsets()
static float GetStandardWidgetHeight()
static float GetStandardSpacing()
RAII guard for ImGui style colors.
Definition style_guard.h:27
RAII guard for ImGui style vars.
Definition style_guard.h:68
RAII compound guard for window-level style setup.
const Theme & GetCurrentTheme() const
static ThemeManager & Get()
static absl::StatusOr< std::filesystem::path > GetTempDirectory()
Get a temporary directory for the application.
static absl::StatusOr< std::filesystem::path > GetAppDataSubdirectory(const std::string &subdir)
Get a subdirectory within the app data folder.
#define ICON_MD_NOTIFICATIONS
Definition icons.h:1335
#define ICON_MD_SAVE_ALT
Definition icons.h:1645
#define ICON_MD_ACCOUNT_TREE
Definition icons.h:83
#define ICON_MD_SETTINGS
Definition icons.h:1699
#define ICON_MD_LINK
Definition icons.h:1090
#define ICON_MD_INFO
Definition icons.h:993
#define ICON_MD_CHAT
Definition icons.h:394
#define ICON_MD_CANCEL
Definition icons.h:364
#define ICON_MD_LANDSCAPE
Definition icons.h:1059
#define ICON_MD_WARNING
Definition icons.h:2123
#define ICON_MD_LOCK_OPEN
Definition icons.h:1142
#define ICON_MD_DONE_ALL
Definition icons.h:608
#define ICON_MD_FOLDER_SPECIAL
Definition icons.h:815
#define ICON_MD_LOCK
Definition icons.h:1140
#define ICON_MD_FORUM
Definition icons.h:851
#define ICON_MD_PLAY_ARROW
Definition icons.h:1479
#define ICON_MD_CIRCLE
Definition icons.h:411
#define ICON_MD_CODE
Definition icons.h:434
#define ICON_MD_LIST_ALT
Definition icons.h:1095
#define ICON_MD_VIDEOGAME_ASSET
Definition icons.h:2076
#define ICON_MD_BUG_REPORT
Definition icons.h:327
#define ICON_MD_CASTLE
Definition icons.h:380
#define ICON_MD_ERROR
Definition icons.h:686
#define ICON_MD_MUSIC_NOTE
Definition icons.h:1264
#define ICON_MD_INBOX
Definition icons.h:990
#define ICON_MD_KEYBOARD
Definition icons.h:1028
#define ICON_MD_BOLT
Definition icons.h:282
#define ICON_MD_IMAGE
Definition icons.h:982
#define ICON_MD_CHECK_CIRCLE
Definition icons.h:400
#define ICON_MD_TERMINAL
Definition icons.h:1951
#define ICON_MD_DESCRIPTION
Definition icons.h:539
#define ICON_MD_BUILD
Definition icons.h:328
#define ICON_MD_NOTIFICATIONS_OFF
Definition icons.h:1338
#define ICON_MD_PALETTE
Definition icons.h:1370
#define ICON_MD_OPEN_IN_NEW
Definition icons.h:1354
#define ICON_MD_CONTENT_COPY
Definition icons.h:465
#define ICON_MD_TV
Definition icons.h:2032
#define ICON_MD_HELP
Definition icons.h:933
#define ICON_MD_VIEW_SIDEBAR
Definition icons.h:2095
#define ICON_MD_FIBER_MANUAL_RECORD
Definition icons.h:739
#define ICON_MD_SMART_TOY
Definition icons.h:1781
#define ICON_MD_DELETE_SWEEP
Definition icons.h:533
#define LOG_INFO(category, format,...)
Definition log.h:136
std::vector< ProjectWorkflowHistoryEntry > workflow_history()
std::function< void()> cancel_build_workflow_callback()
ProjectWorkflowStatus build_workflow_status()
std::function< void()> run_project_workflow_callback()
std::function< void()> show_workflow_output_callback()
std::function< void()> start_build_workflow_callback()
void DrawWorkflowPreviewEntry(const ProjectWorkflowHistoryEntry &entry, const workflow::WorkflowActionCallbacks &callbacks)
void DrawWorkflowSummaryCard(const char *title, const char *fallback_icon, const ProjectWorkflowStatus &status, const std::function< void()> &cancel_callback={})
bool TryParseToolOutputJson(const std::string &text, Json *out)
std::string BuildSelectionContextSummary(const SelectionContext &selection)
std::string SanitizeToolOutputIdFragment(const std::string &value)
void DrawJsonObjectArraySection(const char *label, const Json &array, const RightDrawerManager::ToolOutputActions &actions)
RightDrawerManager::PanelType StepRightPanel(RightDrawerManager::PanelType current, int direction)
std::optional< uint32_t > ParseToolOutputAddress(const Json &value)
std::optional< uint32_t > ExtractToolOutputAddress(const Json &object)
void DrawToolOutputEntryActions(const Json &object, const RightDrawerManager::ToolOutputActions &actions)
std::string BuildToolOutputActionLabel(const char *visible_label, const char *action_key, const Json &object)
std::string FormatHistoryTime(std::chrono::system_clock::time_point timestamp)
const char * WorkflowIcon(const ProjectWorkflowStatus &status, const char *fallback_icon)
WorkflowActionRowResult DrawHistoryActionRow(const ProjectWorkflowHistoryEntry &entry, const WorkflowActionCallbacks &callbacks, const WorkflowActionRowOptions &options)
std::vector< ProjectWorkflowHistoryEntry > SelectWorkflowPreviewEntries(const std::vector< ProjectWorkflowHistoryEntry > &history, size_t max_entries)
ImVec4 WorkflowColor(ProjectWorkflowState state)
const std::array< DrawerCatalogEntry, 7 > kDrawerCatalog
absl::Span< const DrawerCatalogEntry > GetDrawerCatalog()
Switchable drawers in header-cycle order (excludes Tool Output).
const char * GetPanelTypeName(RightDrawerManager::PanelType type)
Get the name of a panel type.
const char * GetPanelShortcutAction(RightDrawerManager::PanelType type)
Shortcut action id used by ShortcutManager for a drawer toggle.
const char * GetSelectionTypeName(SelectionType type)
Get a human-readable name for a selection type.
std::string PrintShortcut(const std::vector< ImGuiKey > &keys)
const char * GetPanelTypeIcon(RightDrawerManager::PanelType type)
Get the icon for a panel type.
bool TransparentIconButton(const char *icon, const ImVec2 &size, const char *tooltip, bool is_active, const ImVec4 &active_color, const char *panel_id, const char *anim_id)
Draw a transparent icon button (hover effect only).
const char * GetCtrlDisplayName()
Get the display name for the primary modifier key.
ImVec4 ConvertColorToImVec4(const Color &color)
Definition color.h:140
void ColoredText(const char *text, const ImVec4 &color)
ImVec4 GetSurfaceContainerHighestVec4()
ImVec4 GetPrimaryVec4()
ImVec4 GetTextPrimaryVec4()
ImVec2 ScaledSize(float base_width, float base_height)
Returns a DPI-scaled version of a fixed pixel size.
Definition ui_config.h:148
bool OpenUrl(const std::string &url)
Definition input.cc:798
ImVec4 GetTextDisabledVec4()
EmptyStateOptions EmptyNoProject(bool compact)
ImVec4 GetTextSecondaryVec4()
void ColoredTextF(const ImVec4 &color, const char *fmt,...)
bool DrawEmptyState(const EmptyStateOptions &options)
Draw a centered empty-state block.
Animator & GetAnimator()
Definition animator.cc:318
ImVec4 GetSurfaceContainerHighVec4()
EmptyStateOptions EmptyNoSelection(bool compact)
ImVec4 GetSuccessVec4()
ImVec4 GetOutlineVec4()
ImVec4 GetOnSurfaceVec4()
ImVec4 GetSurfaceContainerVec4()
ImVec4 GetWarningVec4()
One entry in the shared right-drawer catalog (header / overflow / View).
RightDrawerManager::DrawerType type
std::chrono::system_clock::time_point timestamp
std::function< void(const std::string &) on_open_reference)
Holds information about the current selection.
std::vector< ImGuiKey > keys
Shared empty / disabled panel presentation.
Definition empty_state.h:15
bool compact
Tighter vertical rhythm for drawers / inspectors.
Definition empty_state.h:22
static constexpr float kPanelWidthSettings
Definition ui_config.h:31
static constexpr float kPanelWidthHelp
Definition ui_config.h:32
static constexpr float kPanelMinWidthProject
Definition ui_config.h:50
static constexpr float kPanelMinWidthHelp
Definition ui_config.h:47
static constexpr float kPanelWidthNotifications
Definition ui_config.h:33
static constexpr float kPanelWidthMedium
Definition ui_config.h:25
static constexpr float kAnimationSnapThreshold
Definition ui_config.h:82
static constexpr float kPanelMinWidthNotifications
Definition ui_config.h:48
static constexpr float kPanelMinWidthAgentChat
Definition ui_config.h:44
static constexpr float kPanelPaddingLarge
Definition ui_config.h:73
static constexpr float kAnimationSpeed
Definition ui_config.h:81
static constexpr float kPanelMinWidthSettings
Definition ui_config.h:46
static constexpr float kPanelPaddingMedium
Definition ui_config.h:72
static constexpr float kPanelWidthProject
Definition ui_config.h:35
static constexpr float kSplitterWidth
Definition ui_config.h:76
static constexpr float kPanelMinWidthProposals
Definition ui_config.h:45
static constexpr float kPanelHeaderHeight
Definition ui_config.h:38
static constexpr float kPanelMinWidthAbsolute
Definition ui_config.h:43
static constexpr float kContentMinHeightList
Definition ui_config.h:54
static constexpr float kPanelMinWidthProperties
Definition ui_config.h:49
static constexpr float kPanelWidthProposals
Definition ui_config.h:30
static constexpr float kPanelWidthProperties
Definition ui_config.h:34
static constexpr float kPanelWidthAgentChat
Definition ui_config.h:29