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
13#include "absl/strings/str_format.h"
14#include "absl/types/span.h"
25#include "app/gui/core/icons.h"
26#include "app/gui/core/input.h"
29#include "app/gui/core/style.h"
36#include "imgui/imgui.h"
37#include "imgui/imgui_internal.h"
38#include "util/json.h"
39#include "util/log.h"
40#include "util/platform_paths.h"
41
42namespace yaze {
43namespace editor {
44
45namespace {
46
47std::string ResolveAgentChatHistoryPath() {
48 auto agent_dir = util::PlatformPaths::GetAppDataSubdirectory("agent");
49 if (agent_dir.ok()) {
50 return (*agent_dir / "agent_chat_history.json").string();
51 }
53 if (temp_dir.ok()) {
54 return (*temp_dir / "agent_chat_history.json").string();
55 }
56 return (std::filesystem::current_path() / "agent_chat_history.json").string();
57}
58
59std::string JsonValueToDisplayString(const Json& value) {
60 if (value.is_string()) {
61 return value.get<std::string>();
62 }
63 if (value.is_boolean()) {
64 return value.get<bool>() ? "true" : "false";
65 }
66 if (value.is_number_integer()) {
67 return std::to_string(value.get<long long>());
68 }
69 if (value.is_number_unsigned()) {
70 return std::to_string(value.get<unsigned long long>());
71 }
72 if (value.is_number_float()) {
73 return absl::StrFormat("%.3f", value.get<double>());
74 }
75 if (value.is_null()) {
76 return "null";
77 }
78 return value.dump();
79}
80
81std::string SanitizeToolOutputIdFragment(const std::string& value) {
82 std::string sanitized;
83 sanitized.reserve(value.size());
84 for (unsigned char ch : value) {
85 if (std::isalnum(ch)) {
86 sanitized.push_back(static_cast<char>(ch));
87 } else {
88 sanitized.push_back('_');
89 }
90 }
91 return sanitized.empty() ? std::string("entry") : sanitized;
92}
93
94bool TryParseToolOutputJson(const std::string& text, Json* out) {
95 if (!out || text.empty()) {
96 return false;
97 }
98 try {
99 *out = Json::parse(text);
100 return out->is_object();
101 } catch (...) {
102 return false;
103 }
104}
105
106std::optional<uint32_t> ParseToolOutputAddress(const Json& value) {
107 if (value.is_number_unsigned()) {
108 return static_cast<uint32_t>(value.get<uint64_t>());
109 }
110 if (value.is_number_integer()) {
111 return static_cast<uint32_t>(value.get<int64_t>());
112 }
113 if (!value.is_string()) {
114 return std::nullopt;
115 }
116
117 std::string token = value.get<std::string>();
118 if (token.empty()) {
119 return std::nullopt;
120 }
121 if (token[0] == '$') {
122 token = token.substr(1);
123 } else if (token.size() > 2 && token[0] == '0' &&
124 (token[1] == 'x' || token[1] == 'X')) {
125 token = token.substr(2);
126 }
127 try {
128 return static_cast<uint32_t>(std::stoul(token, nullptr, 16));
129 } catch (...) {
130 return std::nullopt;
131 }
132}
133
134std::optional<uint32_t> ExtractToolOutputAddress(const Json& object) {
135 if (!object.is_object()) {
136 return std::nullopt;
137 }
138 for (const char* key : {"address", "entry_address"}) {
139 if (object.contains(key)) {
140 auto parsed = ParseToolOutputAddress(object[key]);
141 if (parsed.has_value()) {
142 return parsed;
143 }
144 }
145 }
146 return std::nullopt;
147}
148
149std::string ExtractToolOutputReference(const Json& object) {
150 if (!object.is_object()) {
151 return {};
152 }
153 if (object.contains("source") && object["source"].is_string()) {
154 return object["source"].get<std::string>();
155 }
156 if (object.contains("file") && object["file"].is_string()) {
157 std::string reference = object["file"].get<std::string>();
158 if (object.contains("line") && object["line"].is_number_integer()) {
159 reference = absl::StrCat(reference, ":", object["line"].get<int>());
160 }
161 return reference;
162 }
163 return {};
164}
165
166std::string BuildToolOutputEntryTitle(const Json& object) {
167 if (!object.is_object()) {
168 return {};
169 }
170
171 const std::string address = object.contains("address")
172 ? JsonValueToDisplayString(object["address"])
173 : "";
174 const std::string bank =
175 object.contains("bank") ? JsonValueToDisplayString(object["bank"]) : "";
176 const std::string name =
177 object.contains("name") ? JsonValueToDisplayString(object["name"]) : "";
178 const std::string source = ExtractToolOutputReference(object);
179
180 if (!source.empty() && !address.empty()) {
181 return absl::StrFormat("%s (%s)", source.c_str(), address.c_str());
182 }
183 if (!name.empty() && !address.empty()) {
184 return absl::StrFormat("%s %s", name.c_str(), address.c_str());
185 }
186 if (!name.empty() && !bank.empty()) {
187 return absl::StrFormat("%s %s", name.c_str(), bank.c_str());
188 }
189 if (!source.empty()) {
190 return source;
191 }
192 if (!address.empty()) {
193 return address;
194 }
195 if (!bank.empty()) {
196 return bank;
197 }
198 if (!name.empty()) {
199 return name;
200 }
201 return {};
202}
203
204std::string BuildToolOutputActionLabel(const char* visible_label,
205 const char* action_key,
206 const Json& object) {
207 const auto address = ExtractToolOutputAddress(object);
208 if (address.has_value()) {
209 return absl::StrFormat("%s##tool_output_%s_%06X", visible_label, action_key,
210 *address);
211 }
212
213 const std::string reference = ExtractToolOutputReference(object);
214 if (!reference.empty()) {
215 return absl::StrFormat("%s##tool_output_%s_%s", visible_label, action_key,
216 SanitizeToolOutputIdFragment(reference).c_str());
217 }
218
219 return absl::StrFormat("%s##tool_output_%s_entry", visible_label, action_key);
220}
221
223 const Json& object, const RightDrawerManager::ToolOutputActions& actions) {
224 if (!object.is_object()) {
225 return;
226 }
227
228 const std::string reference = ExtractToolOutputReference(object);
229 const auto address = ExtractToolOutputAddress(object);
230 bool drew_any = false;
231
232 if (!reference.empty() && actions.on_open_reference) {
233 const std::string label =
234 BuildToolOutputActionLabel("Open", "open", object);
235 if (ImGui::SmallButton(label.c_str())) {
236 actions.on_open_reference(reference);
237 }
238 drew_any = true;
239 }
240 if (address.has_value() && actions.on_open_address) {
241 if (drew_any) {
242 ImGui::SameLine();
243 }
244 const std::string label =
245 BuildToolOutputActionLabel("Addr", "addr", object);
246 if (ImGui::SmallButton(label.c_str())) {
247 actions.on_open_address(*address);
248 }
249 drew_any = true;
250 }
251 if (address.has_value() && actions.on_open_lookup) {
252 if (drew_any) {
253 ImGui::SameLine();
254 }
255 const std::string label =
256 BuildToolOutputActionLabel("Lookup", "lookup", object);
257 if (ImGui::SmallButton(label.c_str())) {
258 actions.on_open_lookup(*address);
259 }
260 }
261}
262
263void DrawJsonObjectFields(const Json& object) {
264 for (auto it = object.begin(); it != object.end(); ++it) {
265 if (it.value().is_array() || it.value().is_object()) {
266 continue;
267 }
268 ImGui::BulletText("%s: %s", it.key().c_str(),
269 JsonValueToDisplayString(it.value()).c_str());
270 }
271}
272
274 const char* label, const Json& array,
276 if (!array.is_array() || array.empty()) {
277 return;
278 }
279 if (!ImGui::CollapsingHeader(label, ImGuiTreeNodeFlags_DefaultOpen)) {
280 return;
281 }
282 ImGui::PushID(label);
283 for (size_t i = 0; i < array.size(); ++i) {
284 const auto& entry = array[i];
285 ImGui::PushID(static_cast<int>(i));
286 if (entry.is_object()) {
287 const std::string title = BuildToolOutputEntryTitle(entry);
288 if (!title.empty()) {
289 gui::ColoredText(title.c_str(), gui::GetOnSurfaceVec4());
290 }
291 DrawToolOutputEntryActions(entry, actions);
292 if (!title.empty() || !entry.empty()) {
293 ImGui::Spacing();
294 }
296 } else {
297 ImGui::BulletText("%s", JsonValueToDisplayString(entry).c_str());
298 }
299 if (i + 1 < array.size()) {
300 ImGui::Separator();
301 }
302 ImGui::PopID();
303 }
304 ImGui::PopID();
305}
306
307std::string BuildSelectionContextSummary(const SelectionContext& selection) {
308 if (selection.type == SelectionType::kNone) {
309 return "";
310 }
311 std::string context =
312 absl::StrFormat("Selection: %s", GetSelectionTypeName(selection.type));
313 if (!selection.display_name.empty()) {
314 context += absl::StrFormat("\nName: %s", selection.display_name);
315 }
316 if (selection.id >= 0) {
317 context += absl::StrFormat("\nID: 0x%X", selection.id);
318 }
319 if (selection.secondary_id >= 0) {
320 context += absl::StrFormat("\nSecondary: 0x%X", selection.secondary_id);
321 }
322 if (selection.read_only) {
323 context += "\nRead Only: true";
324 }
325 return context;
326}
327
329 const char* title, const char* fallback_icon,
330 const ProjectWorkflowStatus& status,
331 const std::function<void()>& cancel_callback = {}) {
332 if (!status.visible) {
333 return;
334 }
335
337 workflow::WorkflowIcon(status, fallback_icon), title);
338 ImGui::TextWrapped("%s", status.summary.empty() ? status.label.c_str()
339 : status.summary.c_str());
340 if (!status.detail.empty()) {
341 ImGui::TextWrapped("%s", status.detail.c_str());
342 }
343 if (!status.output_tail.empty()) {
344 ImGui::TextWrapped("%s", status.output_tail.c_str());
345 }
346 if (status.can_cancel && cancel_callback) {
347 if (ImGui::SmallButton(ICON_MD_CANCEL " Cancel Build")) {
348 cancel_callback();
349 }
350 }
351}
352
354 const ProjectWorkflowHistoryEntry& entry,
355 const workflow::WorkflowActionCallbacks& callbacks) {
358 entry.status, entry.kind == "Run" ? ICON_MD_PLAY_ARROW
359 : ICON_MD_BUILD),
360 entry.kind.c_str());
361 ImGui::SameLine();
362 ImGui::TextDisabled("%s",
364 ImGui::TextWrapped("%s", entry.status.summary.empty()
365 ? entry.status.label.c_str()
366 : entry.status.summary.c_str());
367 if (!entry.status.output_tail.empty()) {
368 ImGui::TextWrapped("%s", entry.status.output_tail.c_str());
369 }
371 entry, callbacks, {.show_open_output = true, .show_copy_log = true});
372}
373
374} // namespace
375
376// Shared drawer catalog (header switcher, menu-bar overflow, View > Drawers).
377const std::array<DrawerCatalogEntry, 7> kDrawerCatalog = {{
379 "View: Toggle Project Panel"},
381 "View: Toggle Properties Panel"},
383 "View: Toggle AI Agent Panel"},
385 ICON_MD_DESCRIPTION, "View: Toggle Proposals Panel"},
387 ICON_MD_NOTIFICATIONS, "View: Toggle Notifications Panel"},
389 "View: Toggle Help Panel"},
391 "View: Toggle Settings Panel"},
392}};
393
394namespace {
395
397 for (size_t i = 0; i < kDrawerCatalog.size(); ++i) {
398 if (kDrawerCatalog[i].type == type) {
399 return static_cast<int>(i);
400 }
401 }
402 return -1;
403}
404
406 RightDrawerManager::PanelType current, int direction) {
407 if (kDrawerCatalog.empty()) {
409 }
410 int index = FindRightPanelIndex(current);
411 if (index < 0) {
412 index = 0;
413 }
414 const int size = static_cast<int>(kDrawerCatalog.size());
415 const int next = (index + direction + size) % size;
416 return kDrawerCatalog[static_cast<size_t>(next)].type;
417}
418
419} // namespace
420
421absl::Span<const DrawerCatalogEntry> GetDrawerCatalog() {
422 return absl::MakeSpan(kDrawerCatalog);
423}
424
426 switch (type) {
428 return "View: Toggle Project Panel";
430 return "View: Toggle AI Agent Panel";
432 return "View: Toggle Proposals Panel";
434 return "View: Toggle Settings Panel";
436 return "View: Toggle Help Panel";
438 return "View: Toggle Notifications Panel";
440 return "View: Toggle Properties Panel";
442 default:
443 return "";
444 }
445}
446
448 switch (type) {
450 return "None";
452 return "AI Agent";
454 return "Proposals";
456 return "Settings";
458 return "Help";
460 return "Notifications";
462 return "Properties";
464 return "Project";
466 return "Tool Output";
467 default:
468 return "Unknown";
469 }
470}
471
496
498 switch (type) {
500 return "agent_chat";
502 return "proposals";
504 return "settings";
505 case PanelType::kHelp:
506 return "help";
508 return "notifications";
510 return "properties";
512 return "project";
514 return "tool_output";
515 case PanelType::kNone:
516 default:
517 return "none";
518 }
519}
520
522 if (active_panel_ == type) {
523 CloseDrawer();
524 } else {
525 // Opens the requested panel (also handles re-opening during close animation)
526 OpenDrawer(type);
527 }
528}
529
530void RightDrawerManager::SetToolOutput(std::string title, std::string query,
531 std::string content,
532 ToolOutputActions actions) {
533 tool_output_title_ = std::move(title);
534 tool_output_query_ = std::move(query);
535 tool_output_content_ = std::move(content);
536 tool_output_actions_ = std::move(actions);
537}
538
542
544 // If we were closing, cancel the close animation
545 closing_ = false;
547
548 active_panel_ = type;
549 animating_ = true;
550 animation_target_ = 1.0f;
551
552 // Check if animations are enabled
553 if (!gui::GetAnimator().IsEnabled()) {
554 panel_animation_ = 1.0f;
555 animating_ = false;
556 }
557 // Otherwise keep current panel_animation_ for smooth transition
558}
559
561 if (!gui::GetAnimator().IsEnabled()) {
562 // Instant close
564 closing_ = false;
566 panel_animation_ = 0.0f;
567 animating_ = false;
568 return;
569 }
570
571 // Start close animation — keep the panel type so we can still draw it
572 closing_ = true;
575 animating_ = true;
576 animation_target_ = 0.0f;
577}
578
580 if (direction == 0) {
581 return;
582 }
583
584 const PanelType current_panel =
586 if (current_panel == PanelType::kNone) {
587 return;
588 }
589
590 const int step = direction > 0 ? 1 : -1;
591 OpenDrawer(StepRightPanel(current_panel, step));
592}
593
595 // Snap transition state to a stable endpoint. This avoids stale intermediate
596 // frames being composited when the OS moves the app across spaces.
597 (void)visible;
598 closing_ = false;
600 animating_ = false;
601
604}
605
607 // Determine which panel to measure: active panel, or the one being closed
608 PanelType effective_panel = active_panel_;
609 if (effective_panel == PanelType::kNone && closing_) {
610 effective_panel = closing_panel_;
611 }
612 if (effective_panel == PanelType::kNone) {
613 return 0.0f;
614 }
615
616 ImGuiContext* context = ImGui::GetCurrentContext();
617 if (!context) {
618 return GetConfiguredPanelWidth(effective_panel) * panel_animation_;
619 }
620
621 const ImGuiViewport* viewport = ImGui::GetMainViewport();
622 if (!viewport) {
623 return GetConfiguredPanelWidth(effective_panel) * panel_animation_;
624 }
625
626 const float vp_width = viewport->WorkSize.x;
627 const float width = GetClampedPanelWidth(effective_panel, vp_width);
628
629 // Scale by animation progress for smooth docking space adjustment
630 return width * panel_animation_;
631}
632
634 if (type == PanelType::kNone) {
635 return;
636 }
637 float viewport_width = 0.0f;
638 if (const ImGuiViewport* viewport = ImGui::GetMainViewport()) {
639 viewport_width = viewport->WorkSize.x;
640 }
641 if (viewport_width <= 0.0f && ImGui::GetCurrentContext()) {
642 viewport_width = ImGui::GetIO().DisplaySize.x;
643 }
644 const auto limits = GetPanelSizeLimits(type);
645 float clamped = std::max(limits.min_width, width);
646 if (viewport_width > 0.0f) {
647 const float ratio = viewport_width < 768.0f
648 ? std::max(0.88f, limits.max_width_ratio)
649 : limits.max_width_ratio;
650 const float max_width = std::max(limits.min_width, viewport_width * ratio);
651 clamped = std::clamp(clamped, limits.min_width, max_width);
652 }
653
654 float* target = nullptr;
655 switch (type) {
657 target = &agent_chat_width_;
658 break;
660 target = &proposals_width_;
661 break;
663 target = &settings_width_;
664 break;
665 case PanelType::kHelp:
666 target = &help_width_;
667 break;
669 target = &notifications_width_;
670 break;
672 target = &properties_width_;
673 break;
675 target = &project_width_;
676 break;
678 target = &tool_output_width_;
679 break;
680 default:
681 break;
682 }
683 if (!target) {
684 return;
685 }
686 if (std::abs(*target - clamped) < 0.5f) {
687 return;
688 }
689 *target = clamped;
690#if !defined(NDEBUG)
691 LOG_INFO("RightDrawerManager",
692 "SetDrawerWidth type=%d requested=%.1f clamped=%.1f",
693 static_cast<int>(type), width, clamped);
694#endif
695 NotifyPanelWidthChanged(type, *target);
696}
697
723
725 EditorType editor) {
726 switch (type) {
728 return std::max(gui::UIConfig::kPanelWidthAgentChat, 480.0f);
730 return std::max(gui::UIConfig::kPanelWidthProposals, 440.0f);
732 return std::max(gui::UIConfig::kPanelWidthSettings, 380.0f);
733 case PanelType::kHelp:
734 return std::max(gui::UIConfig::kPanelWidthHelp, 380.0f);
736 return std::max(gui::UIConfig::kPanelWidthNotifications, 380.0f);
738 // Property panel can be wider in certain editors.
739 if (editor == EditorType::kDungeon) {
740 return 440.0f;
741 }
742 return std::max(gui::UIConfig::kPanelWidthProperties, 400.0f);
744 return std::max(gui::UIConfig::kPanelWidthProject, 420.0f);
746 return 460.0f;
747 default:
748 return std::max(gui::UIConfig::kPanelWidthMedium, 380.0f);
749 }
750}
751
753 const PanelSizeLimits& limits) {
754 if (type == PanelType::kNone) {
755 return;
756 }
757 PanelSizeLimits normalized = limits;
758 normalized.min_width =
760 normalized.max_width_ratio =
761 std::clamp(normalized.max_width_ratio, 0.25f, 0.95f);
762 panel_size_limits_[PanelTypeKey(type)] = normalized;
763}
764
766 PanelType type) const {
767 auto it = panel_size_limits_.find(PanelTypeKey(type));
768 if (it != panel_size_limits_.end()) {
769 return it->second;
770 }
771
772 PanelSizeLimits defaults;
773 switch (type) {
776 defaults.max_width_ratio = 0.90f;
777 break;
780 defaults.max_width_ratio = 0.86f;
781 break;
784 defaults.max_width_ratio = 0.80f;
785 break;
786 case PanelType::kHelp:
788 defaults.max_width_ratio = 0.80f;
789 break;
792 defaults.max_width_ratio = 0.82f;
793 break;
796 defaults.max_width_ratio = 0.90f;
797 break;
800 defaults.max_width_ratio = 0.86f;
801 break;
803 defaults.min_width = 360.0f;
804 defaults.max_width_ratio = 0.88f;
805 break;
806 case PanelType::kNone:
807 default:
808 break;
809 }
810 return defaults;
811}
812
814 switch (type) {
816 return agent_chat_width_;
818 return proposals_width_;
820 return settings_width_;
821 case PanelType::kHelp:
822 return help_width_;
826 return properties_width_;
828 return project_width_;
830 return tool_output_width_;
831 case PanelType::kNone:
832 default:
833 return 0.0f;
834 }
835}
836
838 float viewport_width) const {
839 float width = GetConfiguredPanelWidth(type);
840 if (width <= 0.0f) {
841 return width;
842 }
843 const auto limits = GetPanelSizeLimits(type);
844 const float ratio = viewport_width < 768.0f
845 ? std::max(0.88f, limits.max_width_ratio)
846 : limits.max_width_ratio;
847 const float max_width = std::max(limits.min_width, viewport_width * ratio);
848 return std::clamp(width, limits.min_width, max_width);
849}
850
853 on_panel_width_changed_(type, width);
854 }
855}
856
857std::unordered_map<std::string, float>
870
872 const std::unordered_map<std::string, float>& widths) {
873#if !defined(NDEBUG)
874 LOG_INFO("RightDrawerManager",
875 "RestoreDrawerWidths: %zu entries from settings", widths.size());
876#endif
877 auto apply = [&](PanelType type) {
878 auto it = widths.find(PanelTypeKey(type));
879 if (it != widths.end()) {
880 SetDrawerWidth(type, it->second);
881 }
882 };
886 apply(PanelType::kHelp);
889 apply(PanelType::kProject);
891}
892
894 // Nothing to draw if no panel is active and no close animation running
896 return;
897 }
898
899 // Handle Escape key to close panel
901 ImGui::IsKeyPressed(ImGuiKey_Escape)) {
902 CloseDrawer();
903 // Don't return — we need to start drawing the close animation this frame
904 if (!closing_)
905 return;
906 }
907
908 const bool animations_enabled = gui::GetAnimator().IsEnabled();
909 if (!animations_enabled && animating_) {
911 animating_ = false;
912 if (closing_ && animation_target_ == 0.0f) {
913 closing_ = false;
915 return;
916 }
917 }
918
919 // Advance animation
920 if (animating_ && animations_enabled) {
921 // Clamp dt to avoid giant interpolation jumps after focus/space changes.
922 float delta_time = std::clamp(ImGui::GetIO().DeltaTime, 0.0f, 1.0f / 20.0f);
923 float speed = gui::UIConfig::kAnimationSpeed;
924 switch (gui::GetAnimator().motion_profile()) {
926 speed *= 1.20f;
927 break;
929 speed *= 0.75f;
930 break;
932 default:
933 break;
934 }
935 float diff = animation_target_ - panel_animation_;
936 panel_animation_ += diff * std::min(1.0f, delta_time * speed);
937
938 // Snap to target when close enough
939 if (std::abs(animation_target_ - panel_animation_) <
942 animating_ = false;
943
944 // Close animation finished — fully clean up
945 if (closing_ && animation_target_ == 0.0f) {
946 closing_ = false;
948 return;
949 }
950 }
951 }
952
953 // Determine which panel type to draw content for
954 PanelType draw_panel = active_panel_;
955 if (draw_panel == PanelType::kNone && closing_) {
956 draw_panel = closing_panel_;
957 }
958
959 const ImGuiViewport* viewport = ImGui::GetMainViewport();
960 const float viewport_width = viewport->WorkSize.x;
961 const float top_inset = gui::LayoutHelpers::GetTopInset();
962 const float bottom_safe = gui::LayoutHelpers::GetSafeAreaInsets().bottom;
963 const float viewport_height =
964 std::max(0.0f, viewport->WorkSize.y - top_inset - bottom_safe);
965
966 // Keep full-width state explicit so drag-resize and animation remain stable.
967 const float full_width =
968 (draw_panel == PanelType::kNone)
969 ? 0.0f
970 : GetClampedPanelWidth(draw_panel, viewport_width);
971 const float animated_width = full_width * panel_animation_;
972
973 // Use SurfaceContainer for slightly elevated panel background
974 ImVec4 panel_bg = gui::GetSurfaceContainerVec4();
975 ImVec4 panel_border = gui::GetOutlineVec4();
976
977 ImGuiWindowFlags panel_flags =
978 ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove |
979 ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDocking |
980 ImGuiWindowFlags_NoNavFocus;
981
982 // Position panel: slides from right edge. At animation=1.0, fully visible.
983 // At animation=0.0, fully off-screen to the right.
984 float panel_x = viewport->WorkPos.x + viewport_width - animated_width;
985 ImGui::SetNextWindowPos(ImVec2(panel_x, viewport->WorkPos.y + top_inset));
986 ImGui::SetNextWindowSize(ImVec2(full_width, viewport_height));
987
988 gui::StyledWindow panel("##RightPanel",
989 {.bg = panel_bg,
990 .border = panel_border,
991 .padding = ImVec2(0.0f, 0.0f),
992 .border_size = 1.0f},
993 nullptr, panel_flags);
994 if (panel) {
995 const char* panel_title = GetPanelTypeName(draw_panel);
996 const char* panel_icon = GetPanelTypeIcon(draw_panel);
997 if (draw_panel == PanelType::kToolOutput && !tool_output_title_.empty()) {
998 panel_title = tool_output_title_.c_str();
999 }
1000 // Draw enhanced panel header and navigation strip
1001 DrawPanelHeader(draw_panel, panel_title, panel_icon);
1002 DrawDrawerNavStrip(draw_panel);
1003
1004 // Content area with padding and minimum height so content never collapses
1005 gui::StyleVarGuard content_padding(
1006 ImGuiStyleVar_WindowPadding,
1009 const bool panel_content_open = gui::LayoutHelpers::BeginContentChild(
1010 "##PanelContent", ImVec2(0.0f, gui::UIConfig::kContentMinHeightList),
1011 ImGuiChildFlags_AlwaysUseWindowPadding);
1012 if (panel_content_open) {
1013 switch (draw_panel) {
1016 break;
1019 break;
1022 break;
1023 case PanelType::kHelp:
1024 DrawHelpPanel();
1025 break;
1028 break;
1031 break;
1034 break;
1037 break;
1038 default:
1039 break;
1040 }
1041 }
1043
1044 // VSCode-style splitter: drag from the left edge to resize.
1046 const float handle_width = gui::UIConfig::kSplitterWidth;
1047 const ImVec2 win_pos = ImGui::GetWindowPos();
1048 const float win_height = ImGui::GetWindowHeight();
1049 ImGui::SetCursorScreenPos(
1050 ImVec2(win_pos.x - handle_width * 0.5f, win_pos.y));
1051 ImGui::InvisibleButton("##RightPanelResizeHandle",
1052 ImVec2(handle_width, win_height));
1053 const bool handle_hovered = ImGui::IsItemHovered();
1054 const bool handle_active = ImGui::IsItemActive();
1055 if (handle_hovered || handle_active) {
1056 ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW);
1057 }
1058 if (handle_hovered &&
1059 ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
1062 }
1063 if (handle_active) {
1064 const float new_width = GetConfiguredPanelWidth(active_panel_) -
1065 ImGui::GetIO().MouseDelta.x;
1066 SetDrawerWidth(active_panel_, new_width);
1067 ImGui::SetTooltip(tr("Width: %.0f px"),
1069 }
1070
1071 ImVec4 handle_color = gui::GetOutlineVec4();
1072 handle_color.w = handle_active ? 0.95f : (handle_hovered ? 0.72f : 0.35f);
1073 ImGui::GetWindowDrawList()->AddLine(
1074 ImVec2(win_pos.x, win_pos.y),
1075 ImVec2(win_pos.x, win_pos.y + win_height),
1076 ImGui::GetColorU32(handle_color), handle_active ? 2.0f : 1.0f);
1077 }
1078 }
1079}
1080
1082 switch (type) {
1083 case PanelType::kAgentChat: {
1084#ifdef YAZE_BUILD_AGENT_UI
1085 if (agent_chat_) {
1087 if (ImGui::IsItemHovered()) {
1088 ImGui::SetTooltip("%s", tr("Agent Ready"));
1089 }
1090 }
1091#endif
1092 break;
1093 }
1095 if (toast_manager_) {
1096 const size_t unread = toast_manager_->GetUnreadCount();
1097 if (unread > 0) {
1098 const std::string badge = absl::StrFormat("%zu", unread);
1099 const ImVec2 badge_size = ImGui::CalcTextSize(badge.c_str());
1100 const float pad_x = 5.0f;
1101 const float badge_w = badge_size.x + pad_x * 2.0f;
1102 const float badge_h = ImGui::GetTextLineHeight() + 2.0f;
1103 const ImVec2 p = ImGui::GetCursorScreenPos();
1104 ImDrawList* dl = ImGui::GetWindowDrawList();
1105 dl->AddRectFilled(p, ImVec2(p.x + badge_w, p.y + badge_h),
1106 ImGui::GetColorU32(gui::GetPrimaryVec4()), 4.0f);
1107 dl->AddText(ImVec2(p.x + pad_x, p.y + 1.0f),
1108 ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 1.0f, 1.0f)),
1109 badge.c_str());
1110 ImGui::Dummy(ImVec2(badge_w, badge_h));
1111 if (ImGui::IsItemHovered()) {
1112 ImGui::SetTooltip(tr("%zu unread notifications"), unread);
1113 }
1114 }
1115 }
1116 break;
1117 }
1119 if (properties_locked_) {
1121 if (ImGui::IsItemHovered()) {
1122 ImGui::SetTooltip("%s", tr("Selection Locked"));
1123 }
1124 }
1125 break;
1126 }
1127 case PanelType::kHelp: {
1128 const char* editor_name = nullptr;
1129 switch (active_editor_type_) {
1131 editor_name = "Overworld";
1132 break;
1134 editor_name = "Dungeon";
1135 break;
1137 editor_name = "Palette";
1138 break;
1140 editor_name = "Graphics";
1141 break;
1143 editor_name = "Assembly";
1144 break;
1145 case EditorType::kMusic:
1146 editor_name = "Music";
1147 break;
1149 editor_name = "Messages";
1150 break;
1151 default:
1152 break;
1153 }
1154 if (editor_name) {
1155 const ImVec4 tag_bg = gui::GetSurfaceContainerHighestVec4();
1156 const ImVec2 text_sz = ImGui::CalcTextSize(editor_name);
1157 const float pad = 5.0f;
1158 const ImVec2 p = ImGui::GetCursorScreenPos();
1159 ImDrawList* dl = ImGui::GetWindowDrawList();
1160 dl->AddRectFilled(
1161 p, ImVec2(p.x + text_sz.x + pad * 2.0f, p.y + text_sz.y + 2.0f),
1162 ImGui::GetColorU32(tag_bg), 4.0f);
1163 dl->AddText(ImVec2(p.x + pad, p.y + 1.0f),
1164 ImGui::GetColorU32(gui::GetPrimaryVec4()), editor_name);
1165 ImGui::Dummy(ImVec2(text_sz.x + pad * 2.0f, text_sz.y + 2.0f));
1166 }
1167 break;
1168 }
1169 default:
1170 break;
1171 }
1172}
1173
1175 // Panel-specific actions are handled inline within DrawPanelHeader.
1176}
1177
1179 const char* icon) {
1180 const float header_height = gui::UIConfig::kPanelHeaderHeight;
1181 const float padding = gui::UIConfig::kPanelPaddingLarge;
1182
1183 // Header background - slightly elevated surface
1184 ImVec2 header_min = ImGui::GetCursorScreenPos();
1185 ImVec2 header_max = ImVec2(header_min.x + ImGui::GetWindowWidth(),
1186 header_min.y + header_height);
1187
1188 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1189 draw_list->AddRectFilled(
1190 header_min, header_max,
1191 ImGui::GetColorU32(gui::GetSurfaceContainerHighVec4()));
1192 draw_list->AddLine(ImVec2(header_min.x, header_max.y),
1193 ImVec2(header_max.x, header_max.y),
1194 ImGui::GetColorU32(gui::GetOutlineVec4()), 1.0f);
1195
1196 // Icon chip with semi-transparent primary background
1197 const float icon_chip_size = 24.0f;
1198 const float chip_y = header_min.y + (header_height - icon_chip_size) * 0.5f;
1199 ImVec2 chip_min(header_min.x + padding, chip_y);
1200 ImVec2 chip_max(chip_min.x + icon_chip_size, chip_min.y + icon_chip_size);
1201 ImVec4 chip_bg = gui::GetPrimaryVec4();
1202 chip_bg.w = 0.16f;
1203 draw_list->AddRectFilled(chip_min, chip_max, ImGui::GetColorU32(chip_bg),
1204 4.0f);
1205 const ImVec2 icon_sz = ImGui::CalcTextSize(icon);
1206 draw_list->AddText(ImVec2(chip_min.x + (icon_chip_size - icon_sz.x) * 0.5f,
1207 chip_min.y + (icon_chip_size - icon_sz.y) * 0.5f),
1208 ImGui::GetColorU32(gui::GetPrimaryVec4()), icon);
1209
1210 // Reserve fixed chrome width so title truncation stays stable as badges /
1211 // panel actions appear. Layout (right → left): close, switcher, up to three
1212 // panel-specific buttons.
1213 const ImVec2 chrome_btn_size(24.0f, 24.0f);
1214 const float chrome_gap = 4.0f;
1215 int chrome_button_count = 2; // close + switcher
1216 if (type == PanelType::kProperties) {
1217 chrome_button_count += 1;
1218 } else if (type == PanelType::kAgentChat) {
1219#ifdef YAZE_BUILD_AGENT_UI
1220 // Must match the draw path below (buttons only appear when agent_chat_).
1221 if (agent_chat_) {
1222 chrome_button_count += 2;
1223 if (proposal_drawer_) {
1224 chrome_button_count += 1;
1225 }
1226 }
1227#endif
1228 } else if (type == PanelType::kNotifications) {
1229 if (toast_manager_) {
1230 chrome_button_count += 2;
1231 }
1232 } else if (type == PanelType::kToolOutput) {
1233 if (!tool_output_content_.empty()) {
1234 chrome_button_count += 1;
1235 }
1236 } else if (type == PanelType::kHelp) {
1237 chrome_button_count += 1;
1238 }
1239 const float chrome_reserve =
1240 chrome_button_count * chrome_btn_size.x +
1241 std::max(0, chrome_button_count - 1) * chrome_gap + padding;
1242
1243 // Title: truncate into the remaining space so drawer width no longer breathes
1244 // when chrome buttons appear/disappear.
1245 const float title_x = padding + icon_chip_size + 6.0f;
1246 const float title_y = ImGui::GetCursorPosY() +
1247 (header_height - ImGui::GetTextLineHeight()) * 0.5f;
1248 ImGui::SetCursorPos(ImVec2(title_x, title_y));
1249 const float title_max_x =
1250 std::max(title_x, ImGui::GetWindowWidth() - chrome_reserve - 8.0f);
1251 const float title_avail = std::max(0.0f, title_max_x - title_x);
1252 ImVec2 title_size = ImGui::CalcTextSize(title);
1253 const ImVec2 text_min = ImGui::GetCursorScreenPos();
1254 const ImVec2 text_max =
1255 ImVec2(text_min.x + title_avail, text_min.y + ImGui::GetTextLineHeight());
1256 ImGui::RenderTextEllipsis(draw_list, text_min, text_max, text_max.x, title,
1257 nullptr, &title_size);
1258 const float drawn_title_w = std::min(title_size.x, title_avail);
1259 ImGui::Dummy(ImVec2(drawn_title_w, ImGui::GetTextLineHeight()));
1260 if (title_size.x > title_avail && ImGui::IsItemHovered()) {
1261 ImGui::SetTooltip("%s", title);
1262 }
1263
1264 // Contextual badge next to title when space remains.
1265 if (ImGui::GetCursorPosX() + 20.0f < title_max_x) {
1266 ImGui::SameLine(0.0f, 6.0f);
1268 }
1269
1270 // Right-aligned chrome buttons (right → left)
1271 const float btn_y = header_min.y + (header_height - chrome_btn_size.y) * 0.5f;
1272 float current_x = ImGui::GetWindowWidth() - chrome_btn_size.x - padding;
1273
1274 // 1. Close Button
1275 ImGui::SetCursorScreenPos(ImVec2(header_min.x + current_x, btn_y));
1277 ICON_MD_CANCEL, chrome_btn_size, "Close Drawer (Esc)", false,
1278 ImVec4(0, 0, 0, 0), "right_sidebar", "close_panel")) {
1279 CloseDrawer();
1280 }
1281
1282 // 2. Switcher popup button
1283 current_x -= (chrome_btn_size.x + 4.0f);
1284 ImGui::SetCursorScreenPos(ImVec2(header_min.x + current_x, btn_y));
1286 ICON_MD_SWAP_HORIZ, chrome_btn_size, "Switch Sidebar Drawer", false,
1287 gui::GetTextSecondaryVec4(), "right_sidebar", "switch_panel_menu")) {
1288 ImGui::OpenPopup("##RightPanelSwitcher");
1289 }
1290 if (ImGui::BeginPopup("##RightPanelSwitcher")) {
1291 for (const DrawerCatalogEntry& entry : GetDrawerCatalog()) {
1292 std::string label = absl::StrFormat("%s %s", entry.icon, entry.name);
1293 std::string shortcut;
1294 if (entry.shortcut_action && entry.shortcut_action[0] != '\0') {
1295 shortcut = GetShortcutLabel(entry.shortcut_action, "");
1296 if (shortcut == "Unassigned") {
1297 shortcut.clear();
1298 }
1299 }
1300 if (ImGui::MenuItem(label.c_str(),
1301 shortcut.empty() ? nullptr : shortcut.c_str(),
1302 type == entry.type)) {
1303 OpenDrawer(entry.type);
1304 }
1305 }
1306 ImGui::EndPopup();
1307 }
1308
1309 // 3. Panel-specific quick actions (right of switcher)
1310 if (type == PanelType::kProperties) {
1311 current_x -= (chrome_btn_size.x + 4.0f);
1312 ImGui::SetCursorScreenPos(ImVec2(header_min.x + current_x, btn_y));
1315 chrome_btn_size,
1316 properties_locked_ ? "Unlock Selection" : "Lock Selection",
1317 properties_locked_, ImVec4(0, 0, 0, 0), "right_sidebar",
1318 "lock_selection")) {
1320 }
1321 } else if (type == PanelType::kAgentChat) {
1322#ifdef YAZE_BUILD_AGENT_UI
1323 if (agent_chat_) {
1324 current_x -= (chrome_btn_size.x + 4.0f);
1325 ImGui::SetCursorScreenPos(ImVec2(header_min.x + current_x, btn_y));
1327 ICON_MD_DELETE_SWEEP, chrome_btn_size, "Clear Chat History",
1328 false, ImVec4(0, 0, 0, 0), "right_sidebar", "agent_clear_chat")) {
1330 }
1331 current_x -= (chrome_btn_size.x + 4.0f);
1332 ImGui::SetCursorScreenPos(ImVec2(header_min.x + current_x, btn_y));
1334 ICON_MD_SAVE_ALT, chrome_btn_size, "Save Chat History", false,
1335 ImVec4(0, 0, 0, 0), "right_sidebar", "agent_save_chat")) {
1336 agent_chat_->SaveHistory(ResolveAgentChatHistoryPath());
1337 }
1338 if (proposal_drawer_) {
1339 current_x -= (chrome_btn_size.x + 4.0f);
1340 ImGui::SetCursorScreenPos(ImVec2(header_min.x + current_x, btn_y));
1342 ICON_MD_DESCRIPTION, chrome_btn_size, "Open Proposals", false,
1343 ImVec4(0, 0, 0, 0), "right_sidebar", "agent_open_proposals")) {
1345 }
1346 }
1347 }
1348#endif
1349 } else if (type == PanelType::kNotifications) {
1350 if (toast_manager_) {
1351 current_x -= (chrome_btn_size.x + 4.0f);
1352 ImGui::SetCursorScreenPos(ImVec2(header_min.x + current_x, btn_y));
1354 ICON_MD_DELETE_SWEEP, chrome_btn_size, "Clear All Notifications",
1355 false, ImVec4(0, 0, 0, 0), "right_sidebar", "notif_clear_all")) {
1357 }
1358 current_x -= (chrome_btn_size.x + 4.0f);
1359 ImGui::SetCursorScreenPos(ImVec2(header_min.x + current_x, btn_y));
1360 if (gui::TransparentIconButton(ICON_MD_DONE_ALL, chrome_btn_size,
1361 "Mark All Read", false, ImVec4(0, 0, 0, 0),
1362 "right_sidebar", "notif_mark_read")) {
1364 }
1365 }
1366 } else if (type == PanelType::kToolOutput) {
1367 if (!tool_output_content_.empty()) {
1368 current_x -= (chrome_btn_size.x + 4.0f);
1369 ImGui::SetCursorScreenPos(ImVec2(header_min.x + current_x, btn_y));
1371 "Copy Output", false, ImVec4(0, 0, 0, 0),
1372 "right_sidebar", "tool_copy_out")) {
1373 ImGui::SetClipboardText(tool_output_content_.c_str());
1374 }
1375 }
1376 } else if (type == PanelType::kHelp) {
1377 current_x -= (chrome_btn_size.x + 4.0f);
1378 ImGui::SetCursorScreenPos(ImVec2(header_min.x + current_x, btn_y));
1380 ICON_MD_OPEN_IN_NEW, chrome_btn_size, "Open Online Documentation",
1381 false, ImVec4(0, 0, 0, 0), "right_sidebar", "help_open_docs")) {
1382 gui::OpenUrl("https://github.com/scawful/yaze/wiki");
1383 }
1384 }
1385
1386 ImGui::SetCursorPosY(header_height);
1387}
1388
1390 const float nav_height = 32.0f;
1391 const float padding = 6.0f;
1392 const float gap = 3.0f;
1393
1394 const ImVec2 nav_min = ImGui::GetCursorScreenPos();
1395 const ImVec2 nav_max =
1396 ImVec2(nav_min.x + ImGui::GetWindowWidth(), nav_min.y + nav_height);
1397
1398 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1399 draw_list->AddRectFilled(nav_min, nav_max,
1400 ImGui::GetColorU32(gui::GetSurfaceContainerVec4()));
1401 draw_list->AddLine(ImVec2(nav_min.x, nav_max.y), ImVec2(nav_max.x, nav_max.y),
1402 ImGui::GetColorU32(gui::GetOutlineVec4()), 1.0f);
1403
1404 const auto catalog = GetDrawerCatalog();
1405 const size_t count = catalog.size();
1406 if (count == 0) {
1407 ImGui::SetCursorPosY(ImGui::GetCursorPosY() + nav_height + 4.0f);
1408 return;
1409 }
1410
1411 const float avail_w = ImGui::GetWindowWidth() - padding * 2.0f;
1412 const float tab_w =
1413 std::max(24.0f, std::floor((avail_w - (count - 1) * gap) / count));
1414 const float tab_h = 24.0f;
1415 const float tab_y = nav_min.y + (nav_height - tab_h) * 0.5f;
1416
1417 for (size_t i = 0; i < count; ++i) {
1418 const DrawerCatalogEntry& entry = catalog[i];
1419 const bool is_active = (current_panel == entry.type);
1420 const float tab_x = nav_min.x + padding + i * (tab_w + gap);
1421
1422 const ImVec2 tab_rect_min(tab_x, tab_y);
1423 const ImVec2 tab_rect_max(tab_x + tab_w, tab_y + tab_h);
1424
1425 ImGui::SetCursorScreenPos(tab_rect_min);
1426 const std::string tab_id = absl::StrFormat("##drawer_nav_%zu_%s", i,
1427 entry.name ? entry.name : "x");
1428 if (ImGui::InvisibleButton(tab_id.c_str(), ImVec2(tab_w, tab_h))) {
1429 if (is_active) {
1430 CloseDrawer();
1431 } else {
1432 OpenDrawer(entry.type);
1433 }
1434 }
1435 const bool hovered = ImGui::IsItemHovered();
1436
1437 if (is_active) {
1438 draw_list->AddRectFilled(
1439 tab_rect_min, tab_rect_max,
1440 ImGui::GetColorU32(gui::GetSurfaceContainerHighestVec4()), 4.0f);
1441 draw_list->AddLine(ImVec2(tab_rect_min.x + 3.0f, tab_rect_max.y - 1.0f),
1442 ImVec2(tab_rect_max.x - 3.0f, tab_rect_max.y - 1.0f),
1443 ImGui::GetColorU32(gui::GetPrimaryVec4()), 2.0f);
1444 } else if (hovered) {
1445 draw_list->AddRectFilled(
1446 tab_rect_min, tab_rect_max,
1447 ImGui::GetColorU32(gui::GetSurfaceContainerHighVec4()), 4.0f);
1448 }
1449
1450 const ImVec2 icon_sz = ImGui::CalcTextSize(entry.icon);
1451 const ImVec2 icon_pos(tab_rect_min.x + (tab_w - icon_sz.x) * 0.5f,
1452 tab_rect_min.y + (tab_h - icon_sz.y) * 0.5f);
1453 const ImVec4 icon_col = is_active ? gui::GetPrimaryVec4()
1454 : (hovered ? gui::GetTextPrimaryVec4()
1456 draw_list->AddText(icon_pos, ImGui::GetColorU32(icon_col), entry.icon);
1457
1458 // Unread badge dot on Notifications tab
1461 draw_list->AddCircleFilled(
1462 ImVec2(tab_rect_max.x - 4.0f, tab_rect_min.y + 4.0f), 3.0f,
1463 ImGui::GetColorU32(gui::GetPrimaryVec4()));
1464 }
1465
1466 if (hovered) {
1467 std::string tip = entry.name ? entry.name : "";
1468 if (entry.shortcut_action && entry.shortcut_action[0] != '\0') {
1469 const std::string sc = GetShortcutLabel(entry.shortcut_action, "");
1470 if (!sc.empty() && sc != "Unassigned") {
1471 tip = absl::StrFormat("%s (%s)", entry.name, sc.c_str());
1472 }
1473 }
1474 ImGui::SetTooltip("%s", tip.c_str());
1475 }
1476 }
1477
1478 // Advance cursor past the nav strip + small gap
1479 ImGui::SetCursorPosY(gui::UIConfig::kPanelHeaderHeight + nav_height + 4.0f);
1480}
1481
1482// =============================================================================
1483// Panel Styling Helpers
1484// =============================================================================
1485
1486bool RightDrawerManager::BeginPanelSection(const char* label, const char* icon,
1487 bool default_open) {
1488 gui::StyleColorGuard section_colors({
1489 {ImGuiCol_Header, gui::GetSurfaceContainerHighVec4()},
1490 {ImGuiCol_HeaderHovered, gui::GetSurfaceContainerHighestVec4()},
1491 {ImGuiCol_HeaderActive, gui::GetSurfaceContainerHighestVec4()},
1492 });
1493 gui::StyleVarGuard section_vars({
1494 {ImGuiStyleVar_FramePadding, ImVec2(8.0f, 6.0f)},
1495 {ImGuiStyleVar_FrameRounding, 4.0f},
1496 });
1497
1498 // Build header text with icon if provided
1499 std::string header_text;
1500 if (icon) {
1501 header_text = std::string(icon) + " " + label;
1502 } else {
1503 header_text = label;
1504 }
1505
1506 ImGuiTreeNodeFlags flags =
1507 ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_SpanAvailWidth |
1508 ImGuiTreeNodeFlags_AllowOverlap | ImGuiTreeNodeFlags_FramePadding;
1509 if (default_open) {
1510 flags |= ImGuiTreeNodeFlags_DefaultOpen;
1511 }
1512
1513 bool is_open = ImGui::TreeNodeEx(header_text.c_str(), flags);
1514
1515 if (is_open) {
1516 ImGui::Spacing();
1517 ImGui::Indent(4.0f);
1518 }
1519
1520 return is_open;
1521}
1522
1524 ImGui::Unindent(4.0f);
1525 ImGui::TreePop();
1526 ImGui::Spacing();
1527}
1528
1530 ImGui::Spacing();
1531 {
1532 gui::StyleColorGuard sep_color(ImGuiCol_Separator, gui::GetOutlineVec4());
1533 ImGui::Separator();
1534 }
1535 ImGui::Spacing();
1536}
1537
1541
1542void RightDrawerManager::DrawPanelValue(const char* label, const char* value) {
1544 ImGui::SameLine();
1545 ImGui::TextUnformatted(value);
1546}
1547
1549 gui::StyleColorGuard desc_color(ImGuiCol_Text, gui::GetTextDisabledVec4());
1550 ImGui::PushTextWrapPos(ImGui::GetContentRegionAvail().x);
1551 ImGui::TextWrapped("%s", text);
1552 ImGui::PopTextWrapPos();
1553}
1554
1556 const std::string& action, const std::string& fallback) const {
1557 if (!shortcut_manager_) {
1558 return fallback;
1559 }
1560
1561 const Shortcut* shortcut = shortcut_manager_->FindShortcut(action);
1562 if (!shortcut) {
1563 return fallback;
1564 }
1565 if (shortcut->keys.empty()) {
1566 return "Unassigned";
1567 }
1568
1569 return PrintShortcut(shortcut->keys);
1570}
1571
1572void RightDrawerManager::DrawShortcutRow(const std::string& action,
1573 const char* description,
1574 const std::string& fallback) {
1575 std::string label = GetShortcutLabel(action, fallback);
1576 DrawPanelValue(label.c_str(), description);
1577}
1578
1579// =============================================================================
1580// Panel Content Drawing
1581// =============================================================================
1582
1584#ifdef YAZE_BUILD_AGENT_UI
1585 if (!agent_chat_) {
1586 gui::ColoredText(ICON_MD_SMART_TOY " AI Agent Not Available",
1588 ImGui::Spacing();
1590 "The AI Agent is not initialized. "
1591 "Open the AI Agent from View menu or use Ctrl+Shift+A.");
1592 return;
1593 }
1594
1595 agent_chat_->set_active(true);
1596
1597 // Actions (clear/save/proposals) have moved to the panel header.
1598 if (ImGui::BeginChild("AgentChatBody", ImVec2(0, 0), false)) {
1599 agent_chat_->Draw(0.0f);
1600 }
1601 ImGui::EndChild();
1602#else
1603 gui::ColoredText(ICON_MD_SMART_TOY " AI Agent Not Available",
1605
1606 ImGui::Spacing();
1608 "The AI Agent requires agent UI support. "
1609 "Build with YAZE_BUILD_AGENT_UI=ON to enable.");
1610#endif
1611}
1612
1614#ifdef YAZE_BUILD_AGENT_UI
1615 if (!agent_chat_) {
1616 return false;
1617 }
1618 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
1619 const ImVec4 accent = gui::GetPrimaryVec4();
1620
1621 std::string selection_context;
1623 selection_context =
1624 BuildSelectionContextSummary(properties_panel_->GetSelection());
1625 }
1626
1627 struct QuickAction {
1628 const char* label;
1629 std::string prompt;
1630 };
1631
1632 std::vector<QuickAction> actions;
1633 if (!selection_context.empty()) {
1634 actions.push_back({"Explain selection",
1635 "Explain this selection and how to edit it safely.\n\n" +
1636 selection_context});
1637 actions.push_back(
1638 {"Suggest fixes",
1639 "Suggest improvements or checks for this selection.\n\n" +
1640 selection_context});
1641 }
1642
1643 switch (active_editor_type_) {
1645 actions.push_back({"Summarize map",
1646 "Summarize the current overworld map and its key "
1647 "features. Use overworld tools if available."});
1648 actions.push_back({"List sprites/items",
1649 "List notable sprites or items on the current "
1650 "overworld map."});
1651 break;
1653 actions.push_back({"Audit room",
1654 "Summarize the current dungeon room layout, doors, "
1655 "and object density."});
1656 actions.push_back({"List sprites",
1657 "List sprites in the current dungeon room and any "
1658 "potential conflicts."});
1659 break;
1661 actions.push_back({"Review tiles",
1662 "Review the current tileset usage and point out any "
1663 "obvious issues."});
1664 actions.push_back({"Palette check",
1665 "Check palette usage for contrast/readability "
1666 "problems."});
1667 break;
1669 actions.push_back({"Palette audit",
1670 "Audit the active palette for hue/contrast balance "
1671 "and note risks."});
1672 actions.push_back({"Theme ideas",
1673 "Suggest a palette variation that fits the current "
1674 "scene style."});
1675 break;
1677 actions.push_back({"Sprite review",
1678 "Review the selected sprite properties and suggest "
1679 "tuning."});
1680 break;
1682 actions.push_back({"Copy edit",
1683 "Review the current message text for clarity and "
1684 "style improvements."});
1685 break;
1687 actions.push_back({"ASM review",
1688 "Review the current ASM changes for risks and style "
1689 "issues."});
1690 break;
1691 case EditorType::kHex:
1692 actions.push_back({"Hex context",
1693 "Explain what the current hex selection likely "
1694 "represents."});
1695 break;
1697 actions.push_back({"Test suggestion",
1698 "Propose a short emulator test to validate the "
1699 "current feature."});
1700 break;
1701 case EditorType::kAgent:
1702 actions.push_back({"Agent config review",
1703 "Review current agent configuration for practical "
1704 "improvements."});
1705 break;
1706 default:
1707 actions.push_back({"Agent overview",
1708 "Suggest the next best agent-assisted action for the "
1709 "current editor context."});
1710 break;
1711 }
1712
1713 if (actions.empty()) {
1714 return false;
1715 }
1716
1717 ImGui::TextColored(accent, tr("%s Editor Actions"), ICON_MD_BOLT);
1718 gui::ColoredText("Send a context-aware prompt to the agent.",
1720
1721 int columns = ImGui::GetContentRegionAvail().x > 420.0f ? 2 : 1;
1722 if (ImGui::BeginTable("AgentQuickActionsTable", columns,
1723 ImGuiTableFlags_SizingStretchSame)) {
1724 for (const auto& action : actions) {
1725 ImGui::TableNextColumn();
1726 if (ImGui::Button(action.label, ImVec2(-1, 0))) {
1727 agent_chat_->SendMessage(action.prompt);
1728 }
1729 }
1730 ImGui::EndTable();
1731 }
1732 return true;
1733#else
1734 return false;
1735#endif
1736}
1737
1739 if (proposal_drawer_) {
1740 // Set ROM and draw content inside the panel (not a separate window)
1741 if (rom_) {
1743 }
1745 } else {
1746 gui::ColoredText(ICON_MD_DESCRIPTION " Proposals Not Available",
1748
1749 ImGui::Spacing();
1751 "The proposal system is not initialized. "
1752 "Proposals will appear here when the AI Agent creates them.");
1753 }
1754}
1755
1757 if (settings_panel_) {
1758 // Draw settings inline (no card windows)
1760 } else {
1761 gui::ColoredText(ICON_MD_SETTINGS " Settings Not Available",
1763
1764 ImGui::Spacing();
1766 "Settings will be available once initialized. "
1767 "This panel provides quick access to application settings.");
1768 }
1769}
1770
1772 // Context-aware editor header
1774
1775 // Keyboard Shortcuts section (default open)
1776 if (BeginPanelSection("Keyboard Shortcuts", ICON_MD_KEYBOARD, true)) {
1780 }
1781
1782 // Editor-specific help (default open)
1783 if (BeginPanelSection("Editor Guide", ICON_MD_HELP, true)) {
1786 }
1787
1788 // Quick Actions (collapsed by default)
1789 if (BeginPanelSection("Quick Actions", ICON_MD_BOLT, false)) {
1792 }
1793
1794 // About section (collapsed by default)
1795 if (BeginPanelSection("About", ICON_MD_INFO, false)) {
1798 }
1799}
1800
1802 const char* editor_name = "No Editor Selected";
1803 const char* editor_icon = ICON_MD_HELP;
1804
1805 switch (active_editor_type_) {
1807 editor_name = "Overworld Editor";
1808 editor_icon = ICON_MD_LANDSCAPE;
1809 break;
1811 editor_name = "Dungeon Editor";
1812 editor_icon = ICON_MD_CASTLE;
1813 break;
1815 editor_name = "Graphics Editor";
1816 editor_icon = ICON_MD_IMAGE;
1817 break;
1819 editor_name = "Palette Editor";
1820 editor_icon = ICON_MD_PALETTE;
1821 break;
1822 case EditorType::kMusic:
1823 editor_name = "Music Editor";
1824 editor_icon = ICON_MD_MUSIC_NOTE;
1825 break;
1827 editor_name = "Screen Editor";
1828 editor_icon = ICON_MD_TV;
1829 break;
1831 editor_name = "Sprite Editor";
1832 editor_icon = ICON_MD_SMART_TOY;
1833 break;
1835 editor_name = "Message Editor";
1836 editor_icon = ICON_MD_CHAT;
1837 break;
1839 editor_name = "Emulator";
1840 editor_icon = ICON_MD_VIDEOGAME_ASSET;
1841 break;
1842 default:
1843 break;
1844 }
1845
1846 // Draw context header with editor info
1847 gui::ColoredTextF(gui::GetPrimaryVec4(), "%s %s Help", editor_icon,
1848 editor_name);
1849
1851}
1852
1854 const char* ctrl = gui::GetCtrlDisplayName();
1855 DrawPanelLabel("Global");
1856 ImGui::Indent(8.0f);
1857 DrawShortcutRow("Open", "Open ROM", absl::StrFormat("%s+O", ctrl));
1858 DrawShortcutRow("Save", "Save ROM", absl::StrFormat("%s+S", ctrl));
1859 DrawShortcutRow("Save As", "Save ROM As",
1860 absl::StrFormat("%s+Shift+S", ctrl));
1861 DrawShortcutRow("Undo", "Undo", absl::StrFormat("%s+Z", ctrl));
1862 DrawShortcutRow("Redo", "Redo", absl::StrFormat("%s+Shift+Z", ctrl));
1863 DrawShortcutRow("Command Palette", "Command Palette",
1864 absl::StrFormat("%s+Shift+P", ctrl));
1865 DrawShortcutRow("Global Search", "Global Search",
1866 absl::StrFormat("%s+Shift+K", ctrl));
1867 DrawShortcutRow("view.toggle_activity_bar", "Toggle Sidebar",
1868 absl::StrFormat("%s+B", ctrl));
1869 DrawShortcutRow("Show About", "About / Help", "F1");
1870 DrawPanelValue("Esc", "Close Drawer");
1871 ImGui::Unindent(8.0f);
1872 ImGui::Spacing();
1873}
1874
1876 const char* ctrl = gui::GetCtrlDisplayName();
1877 switch (active_editor_type_) {
1879 DrawPanelLabel("Overworld");
1880 ImGui::Indent(8.0f);
1881 DrawPanelValue("1-3", "Switch World (LW/DW/SP)");
1882 DrawPanelValue("Arrow Keys", "Navigate Maps");
1883 DrawPanelValue("E", "Entity Mode");
1884 DrawPanelValue("T", "Tile Mode");
1885 DrawShortcutRow("overworld.brush_toggle", "Toggle brush", "B");
1886 DrawShortcutRow("overworld.fill", "Fill tool", "F");
1887 DrawShortcutRow("overworld.next_tile", "Next tile", "]");
1888 DrawShortcutRow("overworld.prev_tile", "Previous tile", "[");
1889 DrawPanelValue("Right Click", "Pick Tile");
1890 ImGui::Unindent(8.0f);
1891 break;
1892
1894 DrawPanelLabel("Dungeon");
1895 ImGui::Indent(8.0f);
1896 DrawShortcutRow("dungeon.object.select_tool", "Select tool", "S");
1897 DrawShortcutRow("dungeon.object.place_tool", "Place tool", "P");
1898 DrawShortcutRow("dungeon.object.delete_tool", "Delete tool", "D");
1899 DrawShortcutRow("dungeon.object.copy", "Copy selection",
1900 absl::StrFormat("%s+C", ctrl));
1901 DrawShortcutRow("dungeon.object.paste", "Paste selection",
1902 absl::StrFormat("%s+V", ctrl));
1903 DrawShortcutRow("dungeon.object.delete", "Delete selection", "Delete");
1904 DrawPanelValue("Arrow Keys", "Move Object");
1905 DrawPanelValue("G", "Toggle Grid");
1906 DrawPanelValue("L", "Cycle Layers");
1907 ImGui::Unindent(8.0f);
1908 break;
1909
1911 DrawPanelLabel("Graphics");
1912 ImGui::Indent(8.0f);
1913 DrawShortcutRow("graphics.prev_sheet", "Previous sheet", "PageUp");
1914 DrawShortcutRow("graphics.next_sheet", "Next sheet", "PageDown");
1915 DrawShortcutRow("graphics.tool.pencil", "Pencil tool", "B");
1916 DrawShortcutRow("graphics.tool.fill", "Fill tool", "G");
1917 DrawShortcutRow("graphics.zoom_in", "Zoom in", "+");
1918 DrawShortcutRow("graphics.zoom_out", "Zoom out", "-");
1919 DrawShortcutRow("graphics.toggle_grid", "Toggle grid",
1920 absl::StrFormat("%s+G", ctrl));
1921 ImGui::Unindent(8.0f);
1922 break;
1923
1925 DrawPanelLabel("Palette");
1926 ImGui::Indent(8.0f);
1927 DrawPanelValue("Click", "Select Color");
1928 DrawPanelValue("Double Click", "Edit Color");
1929 DrawPanelValue("Drag", "Copy Color");
1930 ImGui::Unindent(8.0f);
1931 break;
1932
1933 case EditorType::kMusic:
1934 DrawPanelLabel("Music");
1935 ImGui::Indent(8.0f);
1936 DrawShortcutRow("music.play_pause", "Play/Pause", "Space");
1937 DrawShortcutRow("music.stop", "Stop", "Esc");
1938 DrawShortcutRow("music.speed_up", "Speed up", "+");
1939 DrawShortcutRow("music.speed_down", "Slow down", "-");
1940 DrawPanelValue("Left/Right", "Seek");
1941 ImGui::Unindent(8.0f);
1942 break;
1943
1945 DrawPanelLabel("Message");
1946 ImGui::Indent(8.0f);
1947 DrawPanelValue(absl::StrFormat("%s+Enter", ctrl).c_str(),
1948 "Insert Line Break");
1949 DrawPanelValue("Up/Down", "Navigate Messages");
1950 ImGui::Unindent(8.0f);
1951 break;
1952
1953 default:
1954 DrawPanelLabel("Editor Shortcuts");
1955 ImGui::Indent(8.0f);
1956 {
1957 gui::StyleColorGuard text_color(ImGuiCol_Text,
1959 ImGui::TextWrapped(tr("Select an editor to see specific shortcuts."));
1960 }
1961 ImGui::Unindent(8.0f);
1962 break;
1963 }
1964}
1965
1967 switch (active_editor_type_) {
1969 gui::StyleColorGuard text_color(ImGuiCol_Text,
1970 ImGui::GetStyleColorVec4(ImGuiCol_Text));
1971 ImGui::Bullet();
1972 ImGui::TextWrapped(tr("Paint tiles by selecting from Tile16 Selector"));
1973 ImGui::Bullet();
1974 ImGui::TextWrapped(
1975 tr("Switch between Light World, Dark World, and Special Areas"));
1976 ImGui::Bullet();
1977 ImGui::TextWrapped(
1978 tr("Use Entity Mode to place entrances, exits, items, and sprites"));
1979 ImGui::Bullet();
1980 ImGui::TextWrapped(
1981 tr("Right-click on the map to pick a tile for painting"));
1982 } break;
1983
1984 case EditorType::kDungeon: {
1985 gui::StyleColorGuard text_color(ImGuiCol_Text,
1986 ImGui::GetStyleColorVec4(ImGuiCol_Text));
1987 ImGui::Bullet();
1988 ImGui::TextWrapped(
1989 tr("Select rooms from the Room Selector or Room Matrix"));
1990 ImGui::Bullet();
1991 ImGui::TextWrapped(tr("Place objects using the Object Editor panel"));
1992 ImGui::Bullet();
1993 ImGui::TextWrapped(
1994 tr("Edit room headers for palette, GFX, and floor settings"));
1995 ImGui::Bullet();
1996 ImGui::TextWrapped(tr("Multiple rooms can be opened in separate tabs"));
1997 } break;
1998
1999 case EditorType::kGraphics: {
2000 gui::StyleColorGuard text_color(ImGuiCol_Text,
2001 ImGui::GetStyleColorVec4(ImGuiCol_Text));
2002 ImGui::Bullet();
2003 ImGui::TextWrapped(tr("Browse graphics sheets using the Sheet Browser"));
2004 ImGui::Bullet();
2005 ImGui::TextWrapped(tr("Edit pixels directly with the Pixel Editor"));
2006 ImGui::Bullet();
2007 ImGui::TextWrapped(tr("Choose palettes from Palette Controls"));
2008 ImGui::Bullet();
2009 ImGui::TextWrapped(tr("View 3D objects like rupees and crystals"));
2010 } break;
2011
2012 case EditorType::kPalette: {
2013 gui::StyleColorGuard text_color(ImGuiCol_Text,
2014 ImGui::GetStyleColorVec4(ImGuiCol_Text));
2015 ImGui::Bullet();
2016 ImGui::TextWrapped(tr("Edit overworld, dungeon, and sprite palettes"));
2017 ImGui::Bullet();
2018 ImGui::TextWrapped(tr("Use Quick Access for color harmony tools"));
2019 ImGui::Bullet();
2020 ImGui::TextWrapped(tr("Changes update in real-time across all editors"));
2021 } break;
2022
2023 case EditorType::kMusic: {
2024 gui::StyleColorGuard text_color(ImGuiCol_Text,
2025 ImGui::GetStyleColorVec4(ImGuiCol_Text));
2026 ImGui::Bullet();
2027 ImGui::TextWrapped(tr("Browse songs in the Song Browser"));
2028 ImGui::Bullet();
2029 ImGui::TextWrapped(tr("Use the tracker for playback control"));
2030 ImGui::Bullet();
2031 ImGui::TextWrapped(tr("Edit instruments and BRR samples"));
2032 } break;
2033
2034 case EditorType::kMessage: {
2035 gui::StyleColorGuard text_color(ImGuiCol_Text,
2036 ImGui::GetStyleColorVec4(ImGuiCol_Text));
2037 ImGui::Bullet();
2038 ImGui::TextWrapped(tr("Edit all in-game dialog messages"));
2039 ImGui::Bullet();
2040 ImGui::TextWrapped(tr("Preview text rendering with the font atlas"));
2041 ImGui::Bullet();
2042 ImGui::TextWrapped(tr("Manage the compression dictionary"));
2043 } break;
2044
2045 default:
2046 ImGui::Bullet();
2047 ImGui::TextWrapped(tr("Open a ROM file via File > Open ROM"));
2048 ImGui::Bullet();
2049 ImGui::TextWrapped(tr("Select an editor from the sidebar"));
2050 ImGui::Bullet();
2051 ImGui::TextWrapped(tr("Use panels to access tools and settings"));
2052 ImGui::Bullet();
2053 ImGui::TextWrapped(tr("Save your work via File > Save ROM"));
2054 break;
2055 }
2056}
2057
2059 const float button_width = ImGui::GetContentRegionAvail().x;
2060
2061 gui::StyleVarGuard button_vars({
2062 {ImGuiStyleVar_FramePadding, ImVec2(10.0f, 7.0f)},
2063 {ImGuiStyleVar_FrameRounding, 6.0f},
2064 {ImGuiStyleVar_FrameBorderSize, 1.0f},
2065 });
2066
2067 auto draw_link_button = [&](const char* icon, const char* label,
2068 const char* url) {
2069 gui::StyleColorGuard btn_colors({
2070 {ImGuiCol_Button, gui::GetSurfaceContainerHighVec4()},
2071 {ImGuiCol_ButtonHovered, gui::GetSurfaceContainerHighestVec4()},
2072 {ImGuiCol_Border, gui::GetOutlineVec4()},
2073 });
2074 const std::string text =
2075 absl::StrFormat("%s %s " ICON_MD_OPEN_IN_NEW, icon, label);
2076 if (ImGui::Button(text.c_str(), ImVec2(button_width, 0))) {
2077 gui::OpenUrl(url);
2078 }
2079 };
2080
2081 draw_link_button(ICON_MD_DESCRIPTION, "Open Documentation",
2082 "https://github.com/scawful/yaze/wiki");
2083 ImGui::Spacing();
2084 draw_link_button(ICON_MD_BUG_REPORT, "Report Issue",
2085 "https://github.com/scawful/yaze/issues/new");
2086 ImGui::Spacing();
2087 draw_link_button(ICON_MD_FORUM, "Join Discord",
2088 "https://discord.gg/zU5qDm8MZg");
2089}
2090
2092 gui::ColoredText("YAZE - Yet Another Zelda3 Editor", gui::GetPrimaryVec4());
2093
2094 ImGui::Spacing();
2096 "A comprehensive editor for The Legend of Zelda: "
2097 "A Link to the Past ROM files.");
2098
2100
2101 DrawPanelLabel("Credits");
2102 ImGui::Spacing();
2103 ImGui::Text(tr("Written by: scawful"));
2104 ImGui::Text(tr("Special Thanks: Zarby89, JaredBrian"));
2105
2107
2108 DrawPanelLabel("Links");
2109 ImGui::Spacing();
2110 gui::ColoredText(ICON_MD_LINK " github.com/scawful/yaze",
2112}
2113
2115 if (!toast_manager_) {
2116 gui::ColoredText(ICON_MD_NOTIFICATIONS_OFF " Notifications Unavailable",
2118 return;
2119 }
2120
2121 // Header actions
2122 float avail = ImGui::GetContentRegionAvail().x;
2123
2124 // Mark all read / Clear all buttons
2125 {
2126 gui::StyleColorGuard btn_colors({
2127 {ImGuiCol_Button, gui::GetSurfaceContainerHighVec4()},
2128 {ImGuiCol_ButtonHovered, gui::GetSurfaceContainerHighestVec4()},
2129 });
2130
2131 if (ImGui::Button(ICON_MD_DONE_ALL " Mark All Read",
2132 ImVec2(avail * 0.5f - 4.0f, 0))) {
2134 }
2135 ImGui::SameLine();
2136 if (ImGui::Button(ICON_MD_DELETE_SWEEP " Clear All",
2137 ImVec2(avail * 0.5f - 4.0f, 0))) {
2139 }
2140 }
2141
2143
2144 const auto build_status = ContentRegistry::Context::build_workflow_status();
2145 const auto run_status = ContentRegistry::Context::run_workflow_status();
2146 const auto workflow_history = ContentRegistry::Context::workflow_history();
2147 workflow::WorkflowActionCallbacks workflow_callbacks;
2148 workflow_callbacks.start_build =
2150 workflow_callbacks.run_project =
2152 workflow_callbacks.show_output =
2154 const auto cancel_build =
2156
2157 if (build_status.visible || run_status.visible || !workflow_history.empty()) {
2158 DrawPanelLabel("Workflow Activity");
2159 if (build_status.visible) {
2160 DrawWorkflowSummaryCard("Build", ICON_MD_BUILD, build_status,
2161 cancel_build);
2162 ImGui::Spacing();
2163 }
2164 if (run_status.visible) {
2165 DrawWorkflowSummaryCard("Run", ICON_MD_PLAY_ARROW, run_status);
2166 ImGui::Spacing();
2167 }
2168 if (!workflow_history.empty()) {
2169 DrawPanelLabel("Recent Workflow History");
2170 const auto preview_entries =
2171 workflow::SelectWorkflowPreviewEntries(workflow_history, 3);
2172 for (size_t i = 0; i < preview_entries.size(); ++i) {
2173 ImGui::PushID(static_cast<int>(i));
2174 DrawWorkflowPreviewEntry(preview_entries[i], workflow_callbacks);
2175 ImGui::PopID();
2176 if (i + 1 < preview_entries.size()) {
2177 ImGui::Separator();
2178 }
2179 }
2180 if (workflow_history.size() > preview_entries.size()) {
2181 ImGui::Spacing();
2182 ImGui::TextDisabled(
2183 tr("+%zu more entries available in Workflow Output"),
2184 workflow_history.size() - preview_entries.size());
2185 if (workflow_callbacks.show_output) {
2186 if (ImGui::SmallButton(ICON_MD_OPEN_IN_NEW
2187 " View Full History##workflow_view_full")) {
2188 workflow_callbacks.show_output();
2189 }
2190 }
2191 }
2192 }
2194 }
2195
2196 // Notification history
2197 const auto& history = toast_manager_->GetHistory();
2198
2199 if (history.empty()) {
2200 ImGui::Spacing();
2201 gui::ColoredText(ICON_MD_INBOX " No notifications",
2203 ImGui::Spacing();
2205 "Notifications will appear here when actions complete.");
2206 return;
2207 }
2208
2209 // Stats
2210 size_t unread_count = toast_manager_->GetUnreadCount();
2211 if (unread_count > 0) {
2212 gui::ColoredTextF(gui::GetPrimaryVec4(), "%zu unread", unread_count);
2213 } else {
2214 gui::ColoredText("All caught up", gui::GetTextSecondaryVec4());
2215 }
2216
2217 ImGui::Spacing();
2218
2219 // Scrollable notification list (minimum height so list never collapses)
2220 const bool notification_list_open = gui::LayoutHelpers::BeginContentChild(
2221 "##NotificationList", ImVec2(0.0f, gui::UIConfig::kContentMinHeightList),
2222 ImGuiChildFlags_None, ImGuiWindowFlags_AlwaysVerticalScrollbar);
2223 if (notification_list_open) {
2224 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
2225 auto now = std::chrono::system_clock::now();
2226
2227 // Group by time (Today, Yesterday, Older)
2228 bool shown_today = false;
2229 bool shown_yesterday = false;
2230 bool shown_older = false;
2231
2232 for (const auto& entry : history) {
2233 auto diff =
2234 std::chrono::duration_cast<std::chrono::hours>(now - entry.timestamp)
2235 .count();
2236
2237 // Time grouping headers
2238 if (diff < 24 && !shown_today) {
2239 DrawPanelLabel("Today");
2240 shown_today = true;
2241 } else if (diff >= 24 && diff < 48 && !shown_yesterday) {
2242 ImGui::Spacing();
2243 DrawPanelLabel("Yesterday");
2244 shown_yesterday = true;
2245 } else if (diff >= 48 && !shown_older) {
2246 ImGui::Spacing();
2247 DrawPanelLabel("Older");
2248 shown_older = true;
2249 }
2250
2251 // Notification item
2252 ImGui::PushID(&entry);
2253
2254 // Icon and color based on type
2255 const char* icon;
2256 ImVec4 color;
2257 switch (entry.type) {
2259 icon = ICON_MD_CHECK_CIRCLE;
2260 color = gui::ConvertColorToImVec4(theme.success);
2261 break;
2263 icon = ICON_MD_WARNING;
2264 color = gui::ConvertColorToImVec4(theme.warning);
2265 break;
2266 case ToastType::kError:
2267 icon = ICON_MD_ERROR;
2268 color = gui::ConvertColorToImVec4(theme.error);
2269 break;
2270 default:
2271 icon = ICON_MD_INFO;
2272 color = gui::ConvertColorToImVec4(theme.info);
2273 break;
2274 }
2275
2276 // Unread indicator
2277 if (!entry.read) {
2279 ImGui::SameLine();
2280 }
2281
2282 // Icon
2283 gui::ColoredTextF(color, "%s", icon);
2284 ImGui::SameLine();
2285
2286 // Message
2287 ImGui::TextWrapped("%s", entry.message.c_str());
2288
2289 // Timestamp
2290 auto diff_sec = std::chrono::duration_cast<std::chrono::seconds>(
2291 now - entry.timestamp)
2292 .count();
2293 std::string time_str;
2294 if (diff_sec < 60) {
2295 time_str = "just now";
2296 } else if (diff_sec < 3600) {
2297 time_str = absl::StrFormat("%dm ago", diff_sec / 60);
2298 } else if (diff_sec < 86400) {
2299 time_str = absl::StrFormat("%dh ago", diff_sec / 3600);
2300 } else {
2301 time_str = absl::StrFormat("%dd ago", diff_sec / 86400);
2302 }
2303
2304 gui::ColoredTextF(gui::GetTextDisabledVec4(), " %s", time_str.c_str());
2305
2306 ImGui::PopID();
2307 ImGui::Spacing();
2308 }
2309 }
2311}
2312
2314 if (properties_panel_) {
2316 } else {
2317 gui::DrawEmptyState(gui::EmptyNoSelection(/*compact=*/true));
2318 }
2319}
2320
2322 if (project_panel_) {
2324 } else {
2325 gui::DrawEmptyState(gui::EmptyNoProject(/*compact=*/true));
2326 }
2327}
2328
2330 if (!tool_output_query_.empty()) {
2332 ImGui::SameLine();
2333 if (ImGui::SmallButton(ICON_MD_CONTENT_COPY " Copy")) {
2334 ImGui::SetClipboardText(tool_output_query_.c_str());
2335 }
2336 ImGui::TextWrapped("%s", tool_output_query_.c_str());
2338 }
2339
2340 if (tool_output_content_.empty()) {
2342 opts.icon = ICON_MD_TERMINAL;
2343 opts.title = "No tool output";
2344 opts.detail =
2345 "Run a project-graph query from the editor to inspect its output here.";
2346 opts.compact = true;
2347 gui::DrawEmptyState(opts);
2348 return;
2349 }
2350
2351 Json parsed;
2352 const bool has_json = TryParseToolOutputJson(tool_output_content_, &parsed);
2353
2355 if (ImGui::SmallButton(ICON_MD_CONTENT_COPY " Copy Result")) {
2356 ImGui::SetClipboardText(tool_output_content_.c_str());
2357 }
2358
2359 if (has_json) {
2360 if (BeginPanelSection("Summary", ICON_MD_INFO, true)) {
2361 ImGui::PushID("summary");
2362 DrawToolOutputEntryActions(parsed, tool_output_actions_);
2363 if (!parsed.empty()) {
2364 ImGui::Spacing();
2365 }
2366 DrawJsonObjectFields(parsed);
2367 ImGui::PopID();
2369 }
2370 if (parsed.contains("source") && parsed["source"].is_object() &&
2371 BeginPanelSection("Resolved Source", ICON_MD_CODE, true)) {
2372 ImGui::PushID("resolved_source");
2373 DrawToolOutputEntryActions(parsed["source"], tool_output_actions_);
2374 if (!parsed["source"].empty()) {
2375 ImGui::Spacing();
2376 }
2377 DrawJsonObjectFields(parsed["source"]);
2378 ImGui::PopID();
2380 }
2381 DrawJsonObjectArraySection("Matching Symbols", parsed["matching_symbols"],
2383 DrawJsonObjectArraySection("Sources", parsed["sources"],
2385 DrawJsonObjectArraySection("Hooks", parsed["hooks"], tool_output_actions_);
2386 DrawJsonObjectArraySection("Writes", parsed["writes"],
2388 DrawJsonObjectArraySection("Banks", parsed["banks"], tool_output_actions_);
2389 DrawJsonObjectArraySection("Symbols", parsed["symbols"],
2391 }
2392
2393 if (ImGui::CollapsingHeader(tr("Raw Output"),
2394 has_json ? 0 : ImGuiTreeNodeFlags_DefaultOpen)) {
2395 if (ImGui::BeginChild("##tool_output_result", ImVec2(0.0f, 220.0f), true)) {
2396 ImGui::TextUnformatted(tool_output_content_.c_str());
2397 }
2398 ImGui::EndChild();
2399 }
2400}
2401
2403 bool interacted = false;
2404
2405 // Single overflow control — same SmallButton metrics as session/bell.
2406 const bool any_drawer_open = active_panel_ != PanelType::kNone;
2407 gui::StyleColorGuard button_colors({
2408 {ImGuiCol_Button, ImVec4(0, 0, 0, 0)},
2409 {ImGuiCol_ButtonHovered, gui::GetSurfaceContainerHighVec4()},
2410 {ImGuiCol_ButtonActive, gui::GetSurfaceContainerHighestVec4()},
2411 {ImGuiCol_Text,
2412 any_drawer_open ? gui::GetPrimaryVec4() : gui::GetTextSecondaryVec4()},
2413 });
2414
2415 if (ImGui::SmallButton(ICON_MD_VERTICAL_SPLIT "##DrawersOverflow")) {
2416 ImGui::OpenPopup("##DrawersOverflowMenu");
2417 interacted = true;
2418 }
2419 if (ImGui::IsItemHovered()) {
2420 ImGui::SetTooltip("%s", tr("Drawers"));
2421 }
2422
2423 if (ImGui::BeginPopup("##DrawersOverflowMenu")) {
2424 for (const DrawerCatalogEntry& entry : GetDrawerCatalog()) {
2425 std::string label = absl::StrFormat("%s %s", entry.icon, entry.name);
2426 std::string shortcut;
2427 if (entry.shortcut_action && entry.shortcut_action[0] != '\0') {
2428 shortcut = GetShortcutLabel(entry.shortcut_action, "");
2429 if (shortcut == "Unassigned") {
2430 shortcut.clear();
2431 }
2432 }
2433 if (ImGui::MenuItem(label.c_str(),
2434 shortcut.empty() ? nullptr : shortcut.c_str(),
2435 IsDrawerActive(entry.type))) {
2436 ToggleDrawer(entry.type);
2437 interacted = true;
2438 }
2439 }
2440 ImGui::EndPopup();
2441 }
2442
2443 return interacted;
2444}
2445
2447 const float frame_padding = ImGui::GetStyle().FramePadding.x;
2448 // Match SmallButton("##DrawersOverflow") footprint used above.
2449 const float icon_width = ImGui::CalcTextSize(ICON_MD_VERTICAL_SPLIT).x;
2450 return icon_width + frame_padding * 2.0f;
2451}
2452
2453} // namespace editor
2454} // 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.
bool DrawDrawerToggleButtons()
Draw the single Drawers overflow control for the status cluster.
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 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.
static float GetDrawerToggleClusterWidth()
Menu-bar width reserved for the Drawers overflow control.
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
std::unordered_map< std::string, PanelSizeLimits > panel_size_limits_
bool IsDrawerActive(DrawerType type) const
Check if a specific drawer is active.
bool IsDrawerExpanded() const
Check if any drawer is currently expanded (or animating closed)
float GetDrawerWidth() const
Get the width of the drawer when expanded.
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
Persist/restore per-drawer widths for user settings.
ProjectManagementPanel * project_panel_
void DrawDrawerNavStrip(PanelType current_panel)
PanelSizeLimits GetPanelSizeLimits(PanelType type) const
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()
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_SWAP_HORIZ
Definition icons.h:1896
#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_VERTICAL_SPLIT
Definition icons.h:2063
#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:106
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:134
void ColoredText(const char *text, const ImVec4 &color)
ImVec4 GetSurfaceContainerHighestVec4()
ImVec4 GetPrimaryVec4()
ImVec4 GetTextPrimaryVec4()
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