yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
layout_manager.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <filesystem>
5#include <fstream>
6#include <string>
7#include <unordered_set>
8#include <utility>
9
10#include "absl/strings/str_cat.h"
16#include "imgui/imgui.h"
17#include "imgui/imgui_internal.h"
18
19#if defined(__APPLE__)
20#include <TargetConditionals.h>
21#endif
22#include "util/json.h"
23#include "util/log.h"
24#include "util/platform_paths.h"
25
26namespace yaze {
27namespace editor {
28
29namespace {
30
31constexpr char kLegacyPanelsKey[] = "panels";
32constexpr char kWindowsKey[] = "windows";
33
34// Helper function to show default windows from LayoutPresets
36 EditorType type) {
37 if (!registry)
38 return;
39
40 auto default_windows = LayoutPresets::GetDefaultWindows(type);
41 for (const auto& window_id : default_windows) {
42 registry->OpenWindow(window_id);
43 }
44
45 LOG_INFO("LayoutManager", "Showing %zu default windows for editor type %d",
46 default_windows.size(), static_cast<int>(type));
47}
48
49yaze::Json BoolMapToJson(const std::unordered_map<std::string, bool>& map) {
51 for (const auto& [key, value] : map) {
52 obj[key] = value;
53 }
54 return obj;
55}
56
57void JsonToBoolMap(const yaze::Json& obj,
58 std::unordered_map<std::string, bool>* map) {
59 if (!map || !obj.is_object()) {
60 return;
61 }
62 map->clear();
63 for (const auto& [key, value] : obj.items()) {
64 if (value.is_boolean()) {
65 (*map)[key] = value.get<bool>();
66 }
67 }
68}
69
70void JsonToWindowMap(const yaze::Json& entry,
71 std::unordered_map<std::string, bool>* windows) {
72 if (!windows || !entry.is_object()) {
73 return;
74 }
75 if (entry.contains(kWindowsKey)) {
76 JsonToBoolMap(entry[kWindowsKey], windows);
77 return;
78 }
79 if (entry.contains(kLegacyPanelsKey)) {
80 JsonToBoolMap(entry[kLegacyPanelsKey], windows);
81 return;
82 }
83 windows->clear();
84}
85
86std::filesystem::path GetLayoutsFilePath(LayoutScope scope,
87 const std::string& project_key) {
88 auto layouts_dir = util::PlatformPaths::GetAppDataSubdirectory("layouts");
89 if (!layouts_dir.ok()) {
90 return {};
91 }
92
93 if (scope == LayoutScope::kProject && !project_key.empty()) {
94 std::filesystem::path projects_dir = *layouts_dir / "projects";
96 return projects_dir / (project_key + ".json");
97 }
98
99 if (scope == LayoutScope::kProject) {
100 return {};
101 }
102
103 return *layouts_dir / "layouts.json";
104}
105
106bool TryGetNamedPreset(const std::string& preset_name,
107 PanelLayoutPreset* preset_out) {
108 if (!preset_out) {
109 return false;
110 }
111
112 if (preset_name == "Minimal") {
113 *preset_out = LayoutPresets::GetMinimalPreset();
114 return true;
115 }
116 if (preset_name == "Developer") {
117 *preset_out = LayoutPresets::GetDeveloperPreset();
118 return true;
119 }
120 if (preset_name == "Designer") {
121 *preset_out = LayoutPresets::GetDesignerPreset();
122 return true;
123 }
124 if (preset_name == "Modder") {
125 *preset_out = LayoutPresets::GetModderPreset();
126 return true;
127 }
128 if (preset_name == "Overworld Expert") {
130 return true;
131 }
132 if (preset_name == "Dungeon Expert") {
134 return true;
135 }
136 if (preset_name == "Testing") {
138 return true;
139 }
140 if (preset_name == "Audio") {
142 return true;
143 }
144
145 return false;
146}
147
148std::string ResolveProfilePresetName(const std::string& profile_id,
149 EditorType editor_type) {
150 if (profile_id == "mapping") {
151 if (editor_type == EditorType::kDungeon) {
152 return "Dungeon Expert";
153 }
154 return "Overworld Expert";
155 }
156 if (profile_id == "code") {
157 return "Minimal";
158 }
159 if (profile_id == "debug") {
160 return "Developer";
161 }
162 if (profile_id == "chat") {
163 return "Modder";
164 }
165 return "";
166}
167
168} // namespace
169
171 ImGuiID dockspace_id) {
172 // Phase 8.2 review (2026-04-25): one-shot protection set by
173 // MaybeReapplyStartupLayout. The very first lazy preset init after a
174 // successful startup-reapply must NOT rebuild the dockspace —
175 // doing so would silently overwrite the user's saved layout on
176 // their first editor activation. Consume the flag, mark the type
177 // initialized so subsequent activations behave normally, and bail.
180 last_dockspace_id_ = dockspace_id;
183 LOG_INFO("LayoutManager",
184 "Suppressed lazy preset init for editor type %d to preserve "
185 "startup-reapplied custom layout",
186 static_cast<int>(type));
187 return;
188 }
189
190 // Don't reinitialize if already set up
191 if (IsLayoutInitialized(type)) {
192 LOG_INFO("LayoutManager",
193 "Layout for editor type %d already initialized, skipping",
194 static_cast<int>(type));
195 return;
196 }
197
198 // Store dockspace ID and current editor type for potential rebuilds
199 last_dockspace_id_ = dockspace_id;
201
202 LOG_INFO("LayoutManager", "Initializing layout for editor type %d",
203 static_cast<int>(type));
204
205 // Clear existing layout for this dockspace
206 ImGui::DockBuilderRemoveNode(dockspace_id);
207 ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
208
209 ImVec2 dockspace_size = ImVec2(1280, 720); // Safe default
210 if (auto* viewport = ImGui::GetMainViewport()) {
211 dockspace_size = viewport->WorkSize;
212 }
213
214 const ImVec2 last_size = gui::DockSpaceRenderer::GetLastDockspaceSize();
215 if (last_size.x > 0.0f && last_size.y > 0.0f) {
216 dockspace_size = last_size;
217 }
218 ImGui::DockBuilderSetNodeSize(dockspace_id, dockspace_size);
219
220 // Build layout based on editor type using generic builder
221 BuildLayoutFromPreset(type, dockspace_id);
222
223 // Show default windows from LayoutPresets (single source of truth)
224 ShowDefaultWindowsForEditor(window_manager_, type);
225
226 // Finalize the layout
227 ImGui::DockBuilderFinish(dockspace_id);
228
229 // Mark as initialized
231}
232
233void LayoutManager::RebuildLayout(EditorType type, ImGuiID dockspace_id) {
234 // Validate dockspace exists
235 ImGuiDockNode* node = ImGui::DockBuilderGetNode(dockspace_id);
236 if (!node) {
237 LOG_ERROR("LayoutManager",
238 "Cannot rebuild layout: dockspace ID %u not found", dockspace_id);
239 return;
240 }
241
242 LOG_INFO("LayoutManager", "Forcing rebuild of layout for editor type %d",
243 static_cast<int>(type));
244
245 // Store dockspace ID and current editor type
246 last_dockspace_id_ = dockspace_id;
248
249 // Clear the layout initialization flag to force rebuild
250 layouts_initialized_[type] = false;
251
252 // Clear existing layout for this dockspace
253 ImGui::DockBuilderRemoveNode(dockspace_id);
254 ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
255 ImVec2 dockspace_size = ImGui::GetMainViewport()->WorkSize;
256 const ImVec2 last_size = gui::DockSpaceRenderer::GetLastDockspaceSize();
257 if (last_size.x > 0.0f && last_size.y > 0.0f) {
258 dockspace_size = last_size;
259 }
260 ImGui::DockBuilderSetNodeSize(dockspace_id, dockspace_size);
261
262 // Build layout based on editor type using generic builder
263 BuildLayoutFromPreset(type, dockspace_id);
264
265 // Show default cards from LayoutPresets (single source of truth)
266 ShowDefaultWindowsForEditor(window_manager_, type);
267
268 // Finalize the layout
269 ImGui::DockBuilderFinish(dockspace_id);
270
271 // Mark as initialized
273
274 LOG_INFO("LayoutManager", "Layout rebuild complete for editor type %d",
275 static_cast<int>(type));
276}
277
278namespace {
279
281 float left = 0.17f;
282 float right = 0.24f;
283 float bottom = 0.22f;
284 float top = 0.12f;
285 float vertical_split = 0.52f;
286
287 // Per-editor type configuration
289 DockSplitConfig cfg;
290 switch (type) {
291 case EditorType::kDungeon:
292 // Dungeon: reserve more right-side space for object/property workflows.
293 cfg.left = 0.16f;
294 cfg.right = 0.26f;
295 cfg.bottom = 0.20f;
296 cfg.vertical_split = 0.50f;
297 break;
298 case EditorType::kOverworld:
299 cfg.left = 0.22f;
300 cfg.right = 0.26f;
301 cfg.bottom = 0.23f;
302 cfg.vertical_split = 0.42f;
303 break;
304 case EditorType::kGraphics:
305 cfg.left = 0.16f;
306 cfg.right = 0.24f;
307 cfg.bottom = 0.20f;
308 break;
309 case EditorType::kPalette:
310 cfg.left = 0.16f;
311 cfg.right = 0.22f;
312 cfg.bottom = 0.20f;
313 break;
314 case EditorType::kSprite:
315 cfg.left = 0.18f;
316 cfg.right = 0.24f;
317 cfg.bottom = 0.20f;
318 break;
319 case EditorType::kScreen:
320 cfg.left = 0.16f;
321 cfg.right = 0.22f;
322 cfg.bottom = 0.20f;
323 break;
324 case EditorType::kMessage:
325 cfg.left = 0.20f;
326 cfg.right = 0.26f;
327 cfg.bottom = 0.18f;
328 break;
329 case EditorType::kAssembly:
330 cfg.left = 0.24f;
331 cfg.right = 0.16f;
332 cfg.bottom = 0.20f;
333 break;
334 case EditorType::kEmulator:
335 cfg.left = 0.14f;
336 cfg.right = 0.28f;
337 cfg.bottom = 0.22f;
338 break;
339 case EditorType::kAgent:
340 cfg.left = 0.16f;
341 cfg.right = 0.30f;
342 cfg.bottom = 0.24f;
343 break;
344 default:
345 // Use defaults
346 break;
347 }
348 return cfg;
349 }
350};
351
353 ImGuiID center = 0;
354 ImGuiID left = 0;
355 ImGuiID right = 0;
356 ImGuiID bottom = 0;
357 ImGuiID top = 0;
358 ImGuiID left_top = 0;
359 ImGuiID left_bottom = 0;
360 ImGuiID right_top = 0;
361 ImGuiID right_bottom = 0;
362};
363
365 bool left = false;
366 bool right = false;
367 bool bottom = false;
368 bool top = false;
369 bool left_top = false;
370 bool left_bottom = false;
371 bool right_top = false;
372 bool right_bottom = false;
373};
374
376 const std::string& panel_id) {
378 return true;
379 }
380 return std::find(preset.default_visible_panels.begin(),
381 preset.default_visible_panels.end(),
382 panel_id) != preset.default_visible_panels.end();
383}
384
385std::vector<std::pair<std::string, DockPosition>> CollectDockedPanels(
386 const PanelLayoutPreset& preset) {
387 std::vector<std::pair<std::string, DockPosition>> docked_panels;
388 docked_panels.reserve(preset.panel_positions.size());
389
390 std::unordered_set<std::string> seen_panels;
391 seen_panels.reserve(preset.panel_positions.size());
392
393 auto append_ordered = [&](const std::vector<std::string>& ordered_ids) {
394 for (const auto& panel_id : ordered_ids) {
395 if (seen_panels.contains(panel_id) ||
396 !ShouldDockPanelInDefaultLayout(preset, panel_id)) {
397 continue;
398 }
399 auto it = preset.panel_positions.find(panel_id);
400 if (it == preset.panel_positions.end()) {
401 continue;
402 }
403 docked_panels.emplace_back(it->first, it->second);
404 seen_panels.insert(panel_id);
405 }
406 };
407
408 append_ordered(preset.default_visible_panels);
409 append_ordered(preset.optional_panels);
410
411 std::vector<std::pair<std::string, DockPosition>> remaining_panels;
412 remaining_panels.reserve(preset.panel_positions.size());
413 for (const auto& [panel_id, position] : preset.panel_positions) {
414 if (seen_panels.contains(panel_id) ||
415 !ShouldDockPanelInDefaultLayout(preset, panel_id)) {
416 continue;
417 }
418 remaining_panels.emplace_back(panel_id, position);
419 }
420
421 std::sort(remaining_panels.begin(), remaining_panels.end(),
422 [](const auto& lhs, const auto& rhs) {
423 if (lhs.second != rhs.second) {
424 return static_cast<int>(lhs.second) <
425 static_cast<int>(rhs.second);
426 }
427 return lhs.first < rhs.first;
428 });
429 docked_panels.insert(docked_panels.end(), remaining_panels.begin(),
430 remaining_panels.end());
431
432 return docked_panels;
433}
434
436 const std::vector<std::pair<std::string, DockPosition>>& docked_panels) {
437 DockSplitNeeds needs{};
438 for (const auto& [_, pos] : docked_panels) {
439 switch (pos) {
441 needs.left = true;
442 break;
444 needs.right = true;
445 break;
447 needs.bottom = true;
448 break;
450 needs.top = true;
451 break;
453 needs.left = true;
454 needs.left_top = true;
455 break;
457 needs.left = true;
458 needs.left_bottom = true;
459 break;
461 needs.right = true;
462 needs.right_top = true;
463 break;
465 needs.right = true;
466 needs.right_bottom = true;
467 break;
469 default:
470 break;
471 }
472 }
473 return needs;
474}
475
480
482 return pos == DockPosition::Left || pos == DockPosition::LeftTop ||
484}
485
487 WorkspaceWindowManager* window_manager,
488 const std::vector<std::pair<std::string, DockPosition>>& docked_panels,
489 bool (*matches_region)(DockPosition)) {
490 if (!window_manager) {
491 return 0.0f;
492 }
493
494 float preferred_width = 0.0f;
495 for (const auto& [panel_id, position] : docked_panels) {
496 if (!matches_region(position)) {
497 continue;
498 }
499 if (WindowContent* panel = window_manager->GetWindowContent(panel_id)) {
500 preferred_width = std::max(preferred_width, panel->GetPreferredWidth());
501 }
502 }
503 return preferred_width;
504}
505
507 DockSplitConfig* cfg, const DockSplitNeeds& needs, float viewport_width,
508 WorkspaceWindowManager* window_manager,
509 const std::vector<std::pair<std::string, DockPosition>>& docked_panels) {
510 if (!cfg || !window_manager || viewport_width <= 0.0f) {
511 return;
512 }
513
514 if (needs.left) {
515 const float preferred_left = ResolvePreferredRegionWidth(
516 window_manager, docked_panels, IsLeftDockPosition);
517 if (preferred_left > 0.0f) {
518 cfg->left = std::clamp(preferred_left / viewport_width, 0.14f, 0.36f);
519 }
520 }
521
522 if (needs.right) {
523 const float preferred_right = ResolvePreferredRegionWidth(
524 window_manager, docked_panels, IsRightDockPosition);
525 if (preferred_right > 0.0f) {
526 cfg->right = std::clamp(preferred_right / viewport_width, 0.18f, 0.42f);
527 }
528 }
529}
530
531DockNodeIds BuildDockTree(ImGuiID dockspace_id, const DockSplitNeeds& needs,
532 const DockSplitConfig& cfg) {
533 DockNodeIds ids{};
534 ids.center = dockspace_id;
535
536 // Split major regions
537 if (needs.left) {
538 ids.left = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Left, cfg.left,
539 nullptr, &ids.center);
540 }
541 if (needs.right) {
542 ids.right = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Right,
543 cfg.right, nullptr, &ids.center);
544 }
545 if (needs.bottom) {
546 ids.bottom = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Down,
547 cfg.bottom, nullptr, &ids.center);
548 }
549 if (needs.top) {
550 ids.top = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Up, cfg.top,
551 nullptr, &ids.center);
552 }
553
554 // Sub-split Left region
555 if (ids.left && (needs.left_top || needs.left_bottom)) {
556 // If we only need one, the split still happens but we use the result accordingly
557 ids.left_bottom = ImGui::DockBuilderSplitNode(
558 ids.left, ImGuiDir_Down, cfg.vertical_split, nullptr, &ids.left_top);
559
560 // If one isn't needed, we technically don't have to split, but for a stable tree,
561 // we do it and get_dock_id will map to the leaf.
562 }
563
564 // Sub-split Right region
565 if (ids.right && (needs.right_top || needs.right_bottom)) {
566 ids.right_bottom = ImGui::DockBuilderSplitNode(
567 ids.right, ImGuiDir_Down, cfg.vertical_split, nullptr, &ids.right_top);
568 }
569
570 return ids;
571}
572
573} // namespace
574
575void LayoutManager::BuildLayoutFromPreset(EditorType type,
576 ImGuiID dockspace_id) {
577 auto preset = LayoutPresets::GetDefaultPreset(type);
578
579 if (!window_manager_) {
580 LOG_WARN("LayoutManager",
581 "WorkspaceWindowManager not available, skipping dock layout for "
582 "type %d",
583 static_cast<int>(type));
584 return;
585 }
586
587 const size_t session_id =
588 window_manager_ ? window_manager_->GetActiveSessionId() : 0;
589 const auto docked_panels = CollectDockedPanels(preset);
590
591 // On compact/touch layouts, collapse all panels into center tabs instead of
592 // splitting into left/right/bottom regions. This gives each panel full
593 // screen width and makes tab-switching more natural on touch.
594 const ImGuiViewport* viewport = ImGui::GetMainViewport();
595 const float viewport_width = viewport ? viewport->WorkSize.x : 0.0f;
596 const bool is_compact =
597#if defined(__APPLE__) && TARGET_OS_IOS == 1
598 [&]() {
599 static bool compact_mode = true;
600 constexpr float kEnterCompactWidth = 900.0f;
601 constexpr float kExitCompactWidth = 940.0f;
602 if (viewport_width <= 0.0f) {
603 return compact_mode;
604 }
605 compact_mode = compact_mode ? (viewport_width < kExitCompactWidth)
606 : (viewport_width < kEnterCompactWidth);
607 return compact_mode;
608 }();
609#else
610 (viewport_width > 0.0f && viewport_width < 900.0f);
611#endif
612
613 DockSplitNeeds needs{};
614 DockSplitConfig cfg{};
615 if (!is_compact) {
616 needs = ComputeSplitNeeds(docked_panels);
617 cfg = DockSplitConfig::ForEditor(type);
618 ApplyPreferredSplitWidths(&cfg, needs, viewport_width, window_manager_,
619 docked_panels);
620 }
621 // When compact, needs is all-false → BuildDockTree produces center-only.
622 DockNodeIds ids = BuildDockTree(dockspace_id, needs, cfg);
623
624 auto get_dock_id = [&](DockPosition pos) -> ImGuiID {
625 switch (pos) {
626 case DockPosition::Left:
627 if (ids.left_top || ids.left_bottom) {
628 // If sub-nodes exist, default "Left" to Top to avoid stacking in parent
629 return ids.left_top ? ids.left_top : ids.left_bottom;
630 }
631 return ids.left ? ids.left : ids.center;
632 case DockPosition::Right:
633 if (ids.right_top || ids.right_bottom) {
634 return ids.right_top ? ids.right_top : ids.right_bottom;
635 }
636 return ids.right ? ids.right : ids.center;
637 case DockPosition::Bottom:
638 return ids.bottom ? ids.bottom : ids.center;
639 case DockPosition::Top:
640 return ids.top ? ids.top : ids.center;
641 case DockPosition::LeftTop:
642 return ids.left_top ? ids.left_top : (ids.left ? ids.left : ids.center);
643 case DockPosition::LeftBottom:
644 return ids.left_bottom ? ids.left_bottom
645 : (ids.left ? ids.left : ids.center);
646 case DockPosition::RightTop:
647 return ids.right_top ? ids.right_top
648 : (ids.right ? ids.right : ids.center);
649 case DockPosition::RightBottom:
650 return ids.right_bottom ? ids.right_bottom
651 : (ids.right ? ids.right : ids.center);
652 case DockPosition::Center:
653 default:
654 return ids.center;
655 }
656 };
657
658 std::vector<ImGuiID> auto_hide_nodes;
659
660 // Iterate through positioned windows and dock them
661 for (const auto& [panel_id, position] : docked_panels) {
662 const WindowDescriptor* desc =
663 window_manager_
664 ? window_manager_->GetWindowDescriptor(session_id, panel_id)
665 : nullptr;
666 if (!desc) {
667 LOG_WARN("LayoutManager",
668 "Preset references window '%s' that is not registered (session "
669 "%zu)",
670 panel_id.c_str(), session_id);
671 continue;
672 }
673
674 std::string window_title = window_manager_->GetWorkspaceWindowName(*desc);
675 if (window_title.empty()) {
676 LOG_WARN("LayoutManager",
677 "Cannot dock window '%s': missing window name (session %zu)",
678 panel_id.c_str(), session_id);
679 continue;
680 }
681
682 const ImGuiID dock_id = get_dock_id(position);
683 ImGui::DockBuilderDockWindow(window_title.c_str(), dock_id);
684
685 if (WindowContent* panel = window_manager_->GetWindowContent(panel_id);
686 panel && panel->PreferAutoHideTabBar() &&
687 std::find(auto_hide_nodes.begin(), auto_hide_nodes.end(), dock_id) ==
688 auto_hide_nodes.end()) {
689 if (ImGuiDockNode* node = ImGui::DockBuilderGetNode(dock_id)) {
690 node->LocalFlags |= ImGuiDockNodeFlags_AutoHideTabBar;
691 auto_hide_nodes.push_back(dock_id);
692 }
693 }
694 }
695}
696
697// Deprecated individual build methods - redirected to generic or kept empty
698void LayoutManager::BuildOverworldLayout(ImGuiID dockspace_id) {
699 BuildLayoutFromPreset(EditorType::kOverworld, dockspace_id);
700}
701void LayoutManager::BuildDungeonLayout(ImGuiID dockspace_id) {
702 BuildLayoutFromPreset(EditorType::kDungeon, dockspace_id);
703}
704void LayoutManager::BuildGraphicsLayout(ImGuiID dockspace_id) {
705 BuildLayoutFromPreset(EditorType::kGraphics, dockspace_id);
706}
707void LayoutManager::BuildPaletteLayout(ImGuiID dockspace_id) {
708 BuildLayoutFromPreset(EditorType::kPalette, dockspace_id);
709}
710void LayoutManager::BuildScreenLayout(ImGuiID dockspace_id) {
711 BuildLayoutFromPreset(EditorType::kScreen, dockspace_id);
712}
713void LayoutManager::BuildMusicLayout(ImGuiID dockspace_id) {
714 BuildLayoutFromPreset(EditorType::kMusic, dockspace_id);
715}
716void LayoutManager::BuildSpriteLayout(ImGuiID dockspace_id) {
717 BuildLayoutFromPreset(EditorType::kSprite, dockspace_id);
718}
719void LayoutManager::BuildMessageLayout(ImGuiID dockspace_id) {
720 BuildLayoutFromPreset(EditorType::kMessage, dockspace_id);
721}
722void LayoutManager::BuildAssemblyLayout(ImGuiID dockspace_id) {
723 BuildLayoutFromPreset(EditorType::kAssembly, dockspace_id);
724}
725void LayoutManager::BuildSettingsLayout(ImGuiID dockspace_id) {
726 BuildLayoutFromPreset(EditorType::kSettings, dockspace_id);
727}
728void LayoutManager::BuildEmulatorLayout(ImGuiID dockspace_id) {
729 BuildLayoutFromPreset(EditorType::kEmulator, dockspace_id);
730}
731
732void LayoutManager::SaveCurrentLayout(const std::string& name, bool persist) {
733 if (!window_manager_) {
734 LOG_WARN("LayoutManager",
735 "Cannot save layout '%s': WorkspaceWindowManager not available",
736 name.c_str());
737 return;
738 }
739
740 const LayoutScope scope = GetActiveScope();
741
742 // Serialize current window visibility state
743 size_t session_id = window_manager_->GetActiveSessionId();
744 auto visibility_state = window_manager_->SerializeVisibilityState(session_id);
745
746 // Store in saved_layouts_ for later persistence
747 saved_layouts_[name] = visibility_state;
748 saved_pinned_layouts_[name] = window_manager_->SerializePinnedState();
749 layout_scopes_[name] = scope;
750
751 // Also save ImGui docking layout to memory
752 size_t ini_size = 0;
753 const char* ini_data = ImGui::SaveIniSettingsToMemory(&ini_size);
754 if (ini_data && ini_size > 0) {
755 saved_imgui_layouts_[name] = std::string(ini_data, ini_size);
756 }
757
758 if (persist) {
759 SaveLayoutsToDisk(scope);
760 }
761
762 LOG_INFO("LayoutManager", "Saved layout '%s' with %zu window states%s",
763 name.c_str(), visibility_state.size(),
764 persist ? "" : " (session-only)");
765}
766
767void LayoutManager::LoadLayout(const std::string& name) {
768 if (!window_manager_) {
769 LOG_WARN("LayoutManager",
770 "Cannot load layout '%s': WorkspaceWindowManager not available",
771 name.c_str());
772 return;
773 }
774
775 // Find saved layout
776 auto layout_it = saved_layouts_.find(name);
777 if (layout_it == saved_layouts_.end()) {
778 LOG_WARN("LayoutManager", "Layout '%s' not found", name.c_str());
779 return;
780 }
781
782 // Restore window visibility
783 size_t session_id = window_manager_->GetActiveSessionId();
784 window_manager_->RestoreVisibilityState(session_id, layout_it->second,
785 /*publish_events=*/true);
786
787 auto pinned_it = saved_pinned_layouts_.find(name);
788 if (pinned_it != saved_pinned_layouts_.end()) {
789 window_manager_->RestorePinnedState(pinned_it->second);
790 }
791
792 // Restore ImGui docking layout if available
793 auto imgui_it = saved_imgui_layouts_.find(name);
794 if (imgui_it != saved_imgui_layouts_.end() && !imgui_it->second.empty()) {
795 ImGui::LoadIniSettingsFromMemory(imgui_it->second.c_str(),
796 imgui_it->second.size());
797 }
798
799 LOG_INFO("LayoutManager", "Loaded layout '%s'", name.c_str());
800}
801
802void LayoutManager::CaptureTemporarySessionLayout(size_t session_id) {
803 if (!window_manager_) {
804 return;
805 }
806
807 temp_session_id_ = session_id;
808 temp_session_visibility_ =
809 window_manager_->SerializeVisibilityState(session_id);
810 temp_session_pinned_ = window_manager_->SerializePinnedState();
811
812 size_t ini_size = 0;
813 const char* ini_data = ImGui::SaveIniSettingsToMemory(&ini_size);
814 if (ini_data && ini_size > 0) {
815 temp_session_imgui_layout_ = std::string(ini_data, ini_size);
816 } else {
817 temp_session_imgui_layout_.clear();
818 }
819
820 has_temp_session_layout_ = true;
821 LOG_INFO(
822 "LayoutManager",
823 "Captured temporary session layout for session %zu (%zu panel states)",
824 session_id, temp_session_visibility_.size());
825}
826
827bool LayoutManager::RestoreTemporarySessionLayout(size_t session_id,
828 bool clear_after_restore) {
829 if (!window_manager_ || !has_temp_session_layout_) {
830 return false;
831 }
832
833 if (session_id != temp_session_id_) {
834 LOG_WARN("LayoutManager",
835 "Session layout snapshot belongs to session %zu, requested %zu",
836 temp_session_id_, session_id);
837 return false;
838 }
839
840 window_manager_->RestoreVisibilityState(session_id, temp_session_visibility_,
841 /*publish_events=*/true);
842 window_manager_->RestorePinnedState(temp_session_pinned_);
843
844 if (!temp_session_imgui_layout_.empty()) {
845 ImGui::LoadIniSettingsFromMemory(temp_session_imgui_layout_.c_str(),
846 temp_session_imgui_layout_.size());
847 }
848
849 if (clear_after_restore) {
850 ClearTemporarySessionLayout();
851 }
852
853 LOG_INFO("LayoutManager", "Restored temporary session layout for session %zu",
854 session_id);
855 return true;
856}
857
858void LayoutManager::ClearTemporarySessionLayout() {
859 has_temp_session_layout_ = false;
860 temp_session_id_ = 0;
861 temp_session_visibility_.clear();
862 temp_session_pinned_.clear();
863 temp_session_imgui_layout_.clear();
864}
865
866bool LayoutManager::SaveNamedSnapshot(const std::string& name,
867 size_t session_id) {
868 if (!window_manager_ || name.empty()) {
869 return false;
870 }
871 SessionSnapshot snapshot;
872 snapshot.session_id = session_id;
873 snapshot.visibility = window_manager_->SerializeVisibilityState(session_id);
874 snapshot.pinned = window_manager_->SerializePinnedState();
875 size_t ini_size = 0;
876 const char* ini_data = ImGui::SaveIniSettingsToMemory(&ini_size);
877 if (ini_data && ini_size > 0) {
878 snapshot.imgui_layout.assign(ini_data, ini_size);
879 }
880 named_snapshots_[name] = std::move(snapshot);
881 LOG_INFO("LayoutManager", "Saved named snapshot '%s' for session %zu",
882 name.c_str(), session_id);
883 return true;
884}
885
886bool LayoutManager::RestoreNamedSnapshot(const std::string& name,
887 size_t session_id,
888 bool remove_after_restore) {
889 if (!window_manager_) {
890 return false;
891 }
892 auto it = named_snapshots_.find(name);
893 if (it == named_snapshots_.end()) {
894 return false;
895 }
896 const SessionSnapshot& snapshot = it->second;
897 if (snapshot.session_id != session_id) {
898 return false;
899 }
900 window_manager_->RestoreVisibilityState(session_id, snapshot.visibility,
901 /*publish_events=*/true);
902 window_manager_->RestorePinnedState(snapshot.pinned);
903 if (!snapshot.imgui_layout.empty()) {
904 ImGui::LoadIniSettingsFromMemory(snapshot.imgui_layout.c_str(),
905 snapshot.imgui_layout.size());
906 }
907 if (remove_after_restore) {
908 named_snapshots_.erase(it);
909 }
910 return true;
911}
912
913bool LayoutManager::DeleteNamedSnapshot(const std::string& name) {
914 auto it = named_snapshots_.find(name);
915 if (it == named_snapshots_.end()) {
916 return false;
917 }
918 named_snapshots_.erase(it);
919 return true;
920}
921
922std::vector<std::string> LayoutManager::ListNamedSnapshots(
923 size_t session_id) const {
924 std::vector<std::string> names;
925 names.reserve(named_snapshots_.size());
926 for (const auto& [name, snapshot] : named_snapshots_) {
927 if (snapshot.session_id == session_id) {
928 names.push_back(name);
929 }
930 }
931 std::sort(names.begin(), names.end());
932 return names;
933}
934
935bool LayoutManager::HasNamedSnapshot(const std::string& name) const {
936 return named_snapshots_.find(name) != named_snapshots_.end();
937}
938
939bool LayoutManager::DeleteLayout(const std::string& name) {
940 auto layout_it = saved_layouts_.find(name);
941 if (layout_it == saved_layouts_.end()) {
942 LOG_WARN("LayoutManager", "Cannot delete layout '%s': not found",
943 name.c_str());
944 return false;
945 }
946
947 LayoutScope scope = GetActiveScope();
948 auto scope_it = layout_scopes_.find(name);
949 if (scope_it != layout_scopes_.end()) {
950 scope = scope_it->second;
951 }
952
953 saved_layouts_.erase(layout_it);
954 saved_imgui_layouts_.erase(name);
955 saved_pinned_layouts_.erase(name);
956 layout_scopes_.erase(name);
957
958 SaveLayoutsToDisk(scope);
959
960 LOG_INFO("LayoutManager", "Deleted layout '%s'", name.c_str());
961 return true;
962}
963
964std::vector<LayoutProfile> LayoutManager::GetBuiltInProfiles() {
965 return {
966 {.id = "code",
967 .label = "Code",
968 .description = "Focused editing workspace with minimal panel noise",
969 .preset_name = "Minimal",
970 .open_agent_chat = false},
971 {.id = "debug",
972 .label = "Debug",
973 .description = "Debugger-first workspace for tracing and memory tools",
974 .preset_name = "Developer",
975 .open_agent_chat = false},
976 {.id = "mapping",
977 .label = "Mapping",
978 .description = "Map-centric layout for overworld or dungeon workflows",
979 .preset_name = "Overworld Expert",
980 .open_agent_chat = false},
981 {.id = "chat",
982 .label = "Chat",
983 .description = "Collaboration-heavy layout with agent-centric tooling",
984 .preset_name = "Modder",
985 .open_agent_chat = true},
986 };
987}
988
989bool LayoutManager::ApplyBuiltInProfile(const std::string& profile_id,
990 size_t session_id,
991 EditorType editor_type,
992 LayoutProfile* out_profile) {
993 if (!window_manager_) {
994 LOG_WARN("LayoutManager",
995 "Cannot apply profile '%s': WorkspaceWindowManager not available",
996 profile_id.c_str());
997 return false;
998 }
999
1000 LayoutProfile matched_profile;
1001 bool found = false;
1002 for (const auto& profile : GetBuiltInProfiles()) {
1003 if (profile.id == profile_id) {
1004 matched_profile = profile;
1005 found = true;
1006 break;
1007 }
1008 }
1009
1010 if (!found) {
1011 LOG_WARN("LayoutManager", "Unknown layout profile id: %s",
1012 profile_id.c_str());
1013 return false;
1014 }
1015
1016 const std::string resolved_preset =
1017 ResolveProfilePresetName(profile_id, editor_type);
1018 if (!resolved_preset.empty()) {
1019 matched_profile.preset_name = resolved_preset;
1020 }
1021
1022 PanelLayoutPreset preset;
1023 if (!TryGetNamedPreset(matched_profile.preset_name, &preset)) {
1024 LOG_WARN("LayoutManager", "Unable to resolve preset '%s' for profile '%s'",
1025 matched_profile.preset_name.c_str(), profile_id.c_str());
1026 return false;
1027 }
1028
1029 window_manager_->HideAllWindowsInSession(session_id);
1030 for (const auto& panel_id : preset.default_visible_panels) {
1031 window_manager_->OpenWindow(session_id, panel_id);
1032 }
1033
1034 RequestRebuild();
1035
1036 if (out_profile) {
1037 *out_profile = matched_profile;
1038 }
1039
1040 LOG_INFO("LayoutManager", "Applied profile '%s' via preset '%s'",
1041 profile_id.c_str(), matched_profile.preset_name.c_str());
1042 return true;
1043}
1044
1045std::vector<std::string> LayoutManager::GetSavedLayoutNames() const {
1046 std::vector<std::string> names;
1047 names.reserve(saved_layouts_.size());
1048 for (const auto& [name, _] : saved_layouts_) {
1049 names.push_back(name);
1050 }
1051 return names;
1052}
1053
1054bool LayoutManager::HasLayout(const std::string& name) const {
1055 return saved_layouts_.find(name) != saved_layouts_.end();
1056}
1057
1058void LayoutManager::LoadLayoutsFromDisk() {
1059 saved_layouts_.clear();
1060 saved_imgui_layouts_.clear();
1061 saved_pinned_layouts_.clear();
1062 layout_scopes_.clear();
1063
1064 LoadLayoutsFromDiskInternal(LayoutScope::kGlobal, /*merge=*/false);
1065 if (!project_layout_key_.empty()) {
1066 LoadLayoutsFromDiskInternal(LayoutScope::kProject, /*merge=*/true);
1067 }
1068}
1069
1070void LayoutManager::SetProjectLayoutKey(const std::string& key) {
1071 if (key.empty()) {
1072 UseGlobalLayouts();
1073 return;
1074 }
1075 project_layout_key_ = key;
1076 LoadLayoutsFromDisk();
1077}
1078
1079void LayoutManager::UseGlobalLayouts() {
1080 project_layout_key_.clear();
1081 LoadLayoutsFromDisk();
1082}
1083
1084LayoutScope LayoutManager::GetActiveScope() const {
1085 return project_layout_key_.empty() ? LayoutScope::kGlobal
1086 : LayoutScope::kProject;
1087}
1088
1089void LayoutManager::LoadLayoutsFromDiskInternal(LayoutScope scope, bool merge) {
1090 std::filesystem::path layout_path =
1091 GetLayoutsFilePath(scope, project_layout_key_);
1092 if (layout_path.empty()) {
1093 return;
1094 }
1095
1096 if (!std::filesystem::exists(layout_path)) {
1097 if (!merge) {
1098 LOG_INFO("LayoutManager", "No layouts file at %s",
1099 layout_path.string().c_str());
1100 }
1101 return;
1102 }
1103
1104 try {
1105 std::ifstream file(layout_path);
1106 if (!file.is_open()) {
1107 LOG_WARN("LayoutManager", "Failed to open layouts file: %s",
1108 layout_path.string().c_str());
1109 return;
1110 }
1111
1112 yaze::Json root;
1113 file >> root;
1114
1115 if (!root.contains("layouts") || !root["layouts"].is_object()) {
1116 LOG_WARN("LayoutManager", "Layouts file missing 'layouts' object: %s",
1117 layout_path.string().c_str());
1118 return;
1119 }
1120
1121 for (auto& [name, entry] : root["layouts"].items()) {
1122 if (!entry.is_object()) {
1123 continue;
1124 }
1125
1126 std::unordered_map<std::string, bool> windows;
1127 std::unordered_map<std::string, bool> pinned;
1128
1129 JsonToWindowMap(entry, &windows);
1130 if (entry.contains("pinned")) {
1131 JsonToBoolMap(entry["pinned"], &pinned);
1132 }
1133
1134 saved_layouts_[name] = std::move(windows);
1135 saved_pinned_layouts_[name] = std::move(pinned);
1136 layout_scopes_[name] = scope;
1137
1138 if (entry.contains("imgui_ini") && entry["imgui_ini"].is_string()) {
1139 saved_imgui_layouts_[name] = entry["imgui_ini"].get<std::string>();
1140 } else {
1141 saved_imgui_layouts_.erase(name);
1142 }
1143 }
1144
1145 LOG_INFO("LayoutManager", "Loaded layouts from %s",
1146 layout_path.string().c_str());
1147 } catch (const std::exception& e) {
1148 LOG_WARN("LayoutManager", "Failed to load layouts: %s", e.what());
1149 }
1150}
1151
1152void LayoutManager::SaveLayoutsToDisk(LayoutScope scope) const {
1153 std::filesystem::path layout_path =
1154 GetLayoutsFilePath(scope, project_layout_key_);
1155 if (layout_path.empty()) {
1156 LOG_WARN("LayoutManager", "No layout path resolved for scope");
1157 return;
1158 }
1159
1160 auto status =
1161 util::PlatformPaths::EnsureDirectoryExists(layout_path.parent_path());
1162 if (!status.ok()) {
1163 LOG_WARN("LayoutManager", "Failed to create layout directory: %s",
1164 status.ToString().c_str());
1165 return;
1166 }
1167
1168 try {
1169 yaze::Json root;
1170 root["version"] = 2;
1171 root["layouts"] = yaze::Json::object();
1172
1173 for (const auto& [name, windows] : saved_layouts_) {
1174 auto scope_it = layout_scopes_.find(name);
1175 if (scope_it != layout_scopes_.end() && scope_it->second != scope) {
1176 continue;
1177 }
1178
1179 yaze::Json entry;
1180 entry[kWindowsKey] = BoolMapToJson(windows);
1181
1182 auto pinned_it = saved_pinned_layouts_.find(name);
1183 if (pinned_it != saved_pinned_layouts_.end()) {
1184 entry["pinned"] = BoolMapToJson(pinned_it->second);
1185 }
1186
1187 auto imgui_it = saved_imgui_layouts_.find(name);
1188 if (imgui_it != saved_imgui_layouts_.end()) {
1189 entry["imgui_ini"] = imgui_it->second;
1190 }
1191
1192 root["layouts"][name] = entry;
1193 }
1194
1195 std::ofstream file(layout_path);
1196 if (!file.is_open()) {
1197 LOG_WARN("LayoutManager", "Failed to open layouts file for write: %s",
1198 layout_path.string().c_str());
1199 return;
1200 }
1201 file << root.dump(2);
1202 file.close();
1203 } catch (const std::exception& e) {
1204 LOG_WARN("LayoutManager", "Failed to save layouts: %s", e.what());
1205 }
1206}
1207
1208void LayoutManager::ResetToDefaultLayout(EditorType type) {
1209 layouts_initialized_[type] = false;
1210 LOG_INFO("LayoutManager", "Reset layout for editor type %d",
1211 static_cast<int>(type));
1212}
1213
1214bool LayoutManager::IsLayoutInitialized(EditorType type) const {
1215 auto it = layouts_initialized_.find(type);
1216 return it != layouts_initialized_.end() && it->second;
1217}
1218
1219void LayoutManager::MarkLayoutInitialized(EditorType type) {
1220 layouts_initialized_[type] = true;
1221 LOG_INFO("LayoutManager", "Marked layout for editor type %d as initialized",
1222 static_cast<int>(type));
1223}
1224
1225void LayoutManager::ClearInitializationFlags() {
1226 layouts_initialized_.clear();
1227 LOG_INFO("LayoutManager", "Cleared all layout initialization flags");
1228}
1229
1230std::string LayoutManager::GetWindowTitle(const std::string& card_id) const {
1231 if (!window_manager_) {
1232 return "";
1233 }
1234
1235 const size_t session_id = window_manager_->GetActiveSessionId();
1236 return window_manager_->GetWorkspaceWindowName(session_id, card_id);
1237}
1238
1239namespace {
1240
1243 switch (d) {
1244 case SD::kLeft:
1245 return ImGuiDir_Left;
1246 case SD::kRight:
1247 return ImGuiDir_Right;
1248 case SD::kUp:
1249 return ImGuiDir_Up;
1250 case SD::kDown:
1251 return ImGuiDir_Down;
1252 }
1253 return ImGuiDir_Left;
1254}
1255
1257 size_t session_id,
1258 const layout_designer::PanelEntry& panel) {
1259 if (wm) {
1260 if (const auto* desc =
1261 wm->GetWindowDescriptor(session_id, panel.panel_id)) {
1262 return wm->GetWorkspaceWindowName(*desc);
1263 }
1264 }
1265 // Fallback: reconstruct the ImGui window name using the same formula as
1266 // WindowDescriptor::GetImGuiWindowName so layouts referencing panels
1267 // that register late still find their windows when they come up.
1268 std::string label;
1269 if (!panel.icon.empty() && !panel.display_name.empty()) {
1270 label = panel.icon + " " + panel.display_name;
1271 } else if (!panel.display_name.empty()) {
1272 label = panel.display_name;
1273 } else if (!panel.icon.empty()) {
1274 label = panel.icon;
1275 }
1276 if (panel.panel_id.empty()) {
1277 return label;
1278 }
1279 return label.empty() ? panel.panel_id : (label + "##" + panel.panel_id);
1280}
1281
1283 const layout_designer::DockNode& node,
1284 ImGuiID target_id) {
1285 using NodeType = layout_designer::DockNode::Type;
1286 if (node.type == NodeType::kLeaf) {
1287 for (const auto& panel : node.panels) {
1288 const std::string title = ResolveDockWindowTitle(wm, session_id, panel);
1289 if (!title.empty()) {
1290 ImGui::DockBuilderDockWindow(title.c_str(), target_id);
1291 }
1292 }
1293 return;
1294 }
1295
1296 // Split node.
1297 const ImGuiDir dir = SplitDirectionToImGuiDir(node.split_direction);
1298 const float ratio = std::clamp(node.split_ratio, 0.05f, 0.95f);
1299 ImGuiID id_other = 0;
1300 const ImGuiID id_at_dir =
1301 ImGui::DockBuilderSplitNode(target_id, dir, ratio, nullptr, &id_other);
1302 if (node.child_a) {
1303 ApplyDockNodeRecursive(wm, session_id, *node.child_a, id_at_dir);
1304 }
1305 if (node.child_b) {
1306 ApplyDockNodeRecursive(wm, session_id, *node.child_b, id_other);
1307 }
1308}
1309
1311 std::string panel_id;
1312 std::string display_name;
1313 std::string icon;
1314};
1315
1316std::unique_ptr<layout_designer::DockNode> CaptureDockNodeRecursive(
1317 const ImGuiDockNode* node,
1318 const std::unordered_map<std::string, PanelLookupEntry>& by_window_name) {
1319 if (!node) {
1320 return layout_designer::DockNode::MakeLeaf({});
1321 }
1322
1323 if (node->IsSplitNode()) {
1324 auto child_a =
1325 CaptureDockNodeRecursive(node->ChildNodes[0], by_window_name);
1326 auto child_b =
1327 CaptureDockNodeRecursive(node->ChildNodes[1], by_window_name);
1328
1329 const bool vertical = node->SplitAxis == ImGuiAxis_Y;
1331 vertical ? layout_designer::SplitDirection::kUp
1332 : layout_designer::SplitDirection::kLeft;
1333
1334 float ratio = 0.5f;
1335 if (node->ChildNodes[0] && node->ChildNodes[1]) {
1336 if (vertical && node->Size.y > 0.0f) {
1337 ratio = node->ChildNodes[0]->Size.y / node->Size.y;
1338 } else if (!vertical && node->Size.x > 0.0f) {
1339 ratio = node->ChildNodes[0]->Size.x / node->Size.x;
1340 }
1341 }
1342 ratio = std::clamp(ratio, 0.05f, 0.95f);
1343 return layout_designer::DockNode::MakeSplit(dir, ratio, std::move(child_a),
1344 std::move(child_b));
1345 }
1346
1347 // Leaf.
1348 std::vector<layout_designer::PanelEntry> panels;
1349 panels.reserve(static_cast<size_t>(node->Windows.Size));
1350 int selected_index = -1;
1351 for (int i = 0; i < node->Windows.Size; ++i) {
1352 const ImGuiWindow* w = node->Windows[i];
1353 if (!w || !w->Name)
1354 continue;
1355 auto it = by_window_name.find(std::string(w->Name));
1356 if (it == by_window_name.end())
1357 continue;
1358 if (node->SelectedTabId != 0 && w->ID == node->SelectedTabId) {
1359 selected_index = static_cast<int>(panels.size());
1360 }
1361 panels.push_back(
1362 {it->second.panel_id, it->second.display_name, it->second.icon});
1363 }
1364 auto leaf = layout_designer::DockNode::MakeLeaf(std::move(panels));
1365 if (selected_index >= 0 &&
1366 selected_index < static_cast<int>(leaf->panels.size())) {
1367 leaf->active_tab_index = selected_index;
1368 }
1369 return leaf;
1370}
1371
1372// Recursively walk a DockTree and append every leaf's panel_ids into
1373// `out`. Used by ApplyDockTree to drive the visibility-open pass.
1375 std::vector<std::string>* out) {
1376 if (node.type == layout_designer::DockNode::Type::kLeaf) {
1377 for (const auto& p : node.panels) {
1378 out->push_back(p.panel_id);
1379 }
1380 return;
1381 }
1382 if (node.child_a)
1384 if (node.child_b)
1386}
1387
1388} // namespace
1389
1390absl::Status LayoutManager::ApplyDockTree(const layout_designer::DockTree& tree,
1391 ImGuiID dockspace_id) {
1392 if (!window_manager_) {
1393 return absl::FailedPreconditionError(
1394 "LayoutManager::ApplyDockTree: WorkspaceWindowManager not bound");
1395 }
1396 std::string validation_error;
1397 if (!tree.Validate(&validation_error)) {
1398 return absl::InvalidArgumentError("LayoutManager::ApplyDockTree: " +
1399 validation_error);
1400 }
1401
1402 // Visibility pass (Phase 8 review 2026-04-24, refined 2026-04-25):
1403 // - Open every panel referenced in the tree so users see what they
1404 // docked rather than empty slots.
1405 // - Close every non-pinned panel that is NOT in the tree, so a saved
1406 // subset doesn't leave previously-visible panels lingering as
1407 // floating ghosts after apply. Pinned panels are intentionally
1408 // persistent (rev-7 / rev-17 force-pinned panels like
1409 // `agent.oracle_ram`, `workflow.output`, `layout.designer`) and
1410 // must survive — closing them would silently hide cross-editor
1411 // functionality the user can't easily put back.
1412 std::vector<std::string> panel_ids;
1413 if (tree.root) {
1414 CollectPanelIdsInSubtree(*tree.root, &panel_ids);
1415 }
1416 const std::unordered_set<std::string> tree_id_set(panel_ids.begin(),
1417 panel_ids.end());
1418 const size_t session_id = window_manager_->GetActiveSessionId();
1419 for (const auto& id : panel_ids) {
1420 // Phase 9 review (2026-04-25): skip already-open panels. Mirrors
1421 // the close-pass guard below — OpenWindowImpl publishes
1422 // WindowVisibilityChanged(true) and runs `on_show` regardless of
1423 // prior state, so a no-op Re-apply (or a startup reapply where
1424 // every panel is already visible) would otherwise dirty settings
1425 // and fire a burst of redundant show events.
1426 if (window_manager_->IsWindowOpen(session_id, id))
1427 continue;
1428 window_manager_->OpenWindow(session_id, id);
1429 }
1430 for (const std::string& id :
1431 window_manager_->GetWindowsInSession(session_id)) {
1432 if (tree_id_set.count(id) > 0)
1433 continue;
1434 if (window_manager_->IsWindowPinned(session_id, id))
1435 continue;
1436 // Phase 8.2 review 3 (2026-04-25): skip already-closed panels.
1437 // CloseWindowImpl fires on_hide and publishes
1438 // WindowVisibilityChanged(false) regardless of prior state, so
1439 // blindly closing every non-tree panel produces a burst of
1440 // redundant hide events for unrelated already-hidden windows.
1441 if (!window_manager_->IsWindowOpen(session_id, id))
1442 continue;
1443 window_manager_->CloseWindow(id);
1444 }
1445
1446 ImGui::DockBuilderRemoveNode(dockspace_id);
1447 ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
1448
1449 ImVec2 size(1280.0f, 720.0f);
1450 if (const ImGuiViewport* vp = ImGui::GetMainViewport()) {
1451 if (vp->WorkSize.x > 0.0f && vp->WorkSize.y > 0.0f) {
1452 size = vp->WorkSize;
1453 }
1454 }
1455 ImGui::DockBuilderSetNodeSize(dockspace_id, size);
1456
1457 ApplyDockNodeRecursive(window_manager_, session_id, *tree.root, dockspace_id);
1458 ImGui::DockBuilderFinish(dockspace_id);
1459
1460 last_dockspace_id_ = dockspace_id;
1461
1462 // Init-tracking pass (Phase 8.2 review 2026-04-25; refined 8.2 review 3):
1463 // - When the user is in an editor (current_editor_type_ != kUnknown),
1464 // mark only that editor type initialized. Other editors keep their
1465 // lazy first-run init — their preset fires on activation as before.
1466 // - When no editor is active yet (kUnknown — startup-reapply OR
1467 // manual apply from the dashboard/settings shell BEFORE any editor
1468 // activation), the per-editor mark wouldn't protect anything, so
1469 // arm the one-shot `startup_reapply_pending_protection_` flag that
1470 // the next InitializeEditorLayout call consumes. Round-3 Codex
1471 // noted that without this branch, applying from a no-editor
1472 // context still left the original clobber bug intact.
1473 if (current_editor_type_ != EditorType::kUnknown) {
1474 MarkLayoutInitialized(current_editor_type_);
1475 } else {
1476 startup_reapply_pending_protection_ = true;
1477 }
1478
1479 return absl::OkStatus();
1480}
1481
1482absl::Status LayoutManager::MaybeReapplyStartupLayout(UserSettings* settings) {
1483 if (startup_layout_consumed_) {
1484 return absl::OkStatus();
1485 }
1486 if (settings == nullptr) {
1487 startup_layout_consumed_ = true;
1488 return absl::OkStatus();
1489 }
1490
1491 const std::string& name = settings->prefs().last_applied_layout_name;
1492 if (name.empty()) {
1493 // Nothing to reapply. Mark consumed so we stop checking each frame.
1494 startup_layout_consumed_ = true;
1495 return absl::OkStatus();
1496 }
1497
1498 if (main_dockspace_id_ == 0) {
1499 // The controller hasn't bound the main dockspace yet — try again
1500 // next frame. Leave the flag clear.
1501 return absl::OkStatus();
1502 }
1503
1504 const auto& named_layouts = settings->prefs().named_layouts;
1505 const auto it = named_layouts.find(name);
1506 if (it == named_layouts.end()) {
1507 startup_layout_consumed_ = true;
1508 util::logf(
1509 "LayoutManager: startup layout '%s' missing from "
1510 "named_layouts; falling through to default.",
1511 name.c_str());
1512 return absl::NotFoundError(absl::StrCat("startup layout \"", name,
1513 "\" not found in named_layouts"));
1514 }
1515
1516 nlohmann::json parsed;
1517 try {
1518 parsed = nlohmann::json::parse(it->second);
1519 } catch (const nlohmann::json::parse_error& e) {
1520 startup_layout_consumed_ = true;
1521 util::logf("LayoutManager: startup layout '%s' JSON parse error: %s",
1522 name.c_str(), e.what());
1523 return absl::InvalidArgumentError(
1524 absl::StrCat("startup layout \"", name, "\" parse error: ", e.what()));
1525 }
1526
1527 auto tree_or = layout_designer::DockTreeFromJson(parsed);
1528 if (!tree_or.ok()) {
1529 startup_layout_consumed_ = true;
1530 util::logf("LayoutManager: startup layout '%s' failed to parse: %s",
1531 name.c_str(), std::string(tree_or.status().message()).c_str());
1532 return tree_or.status();
1533 }
1534
1535 std::string validation_error;
1536 if (!tree_or->Validate(&validation_error)) {
1537 startup_layout_consumed_ = true;
1538 util::logf("LayoutManager: startup layout '%s' failed validation: %s",
1539 name.c_str(), validation_error.c_str());
1540 return absl::InvalidArgumentError(absl::StrCat(
1541 "startup layout \"", name, "\" validation failed: ", validation_error));
1542 }
1543
1544 absl::Status apply_status = ApplyDockTree(*tree_or, main_dockspace_id_);
1545 startup_layout_consumed_ = true;
1546 if (!apply_status.ok()) {
1547 util::logf("LayoutManager: startup layout '%s' ApplyDockTree failed: %s",
1548 name.c_str(), std::string(apply_status.message()).c_str());
1549 }
1550 // ApplyDockTree itself arms `startup_reapply_pending_protection_`
1551 // because `current_editor_type_` is still `kUnknown` at this point
1552 // in the boot — no need to set it again here. Same arming covers
1553 // dashboard-time manual apply, which round-3 Codex flagged.
1554 return apply_status;
1555}
1556
1557absl::StatusOr<layout_designer::DockTree> LayoutManager::CaptureDockTree(
1558 ImGuiID dockspace_id) const {
1559 if (!window_manager_) {
1560 return absl::FailedPreconditionError(
1561 "LayoutManager::CaptureDockTree: WorkspaceWindowManager not bound");
1562 }
1563 ImGuiDockNode* root_node = ImGui::DockBuilderGetNode(dockspace_id);
1564 if (!root_node) {
1565 return absl::NotFoundError(
1566 "LayoutManager::CaptureDockTree: no dock node at given id");
1567 }
1568
1569 std::unordered_map<std::string, PanelLookupEntry> by_window_name;
1570 for (const auto& [panel_id, desc] :
1571 window_manager_->GetAllWindowDescriptors()) {
1572 const std::string title = window_manager_->GetWorkspaceWindowName(desc);
1573 if (title.empty())
1574 continue;
1575 by_window_name.emplace(
1576 title, PanelLookupEntry{panel_id, desc.display_name, desc.icon});
1577 }
1578
1580 tree.root = CaptureDockNodeRecursive(root_node, by_window_name);
1581 if (!tree.root) {
1582 tree.root = layout_designer::DockNode::MakeLeaf({});
1583 }
1584 return tree;
1585}
1586
1587} // namespace editor
1588} // namespace yaze
bool is_object() const
Definition json.h:57
static Json object()
Definition json.h:34
items_view items()
Definition json.h:88
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
WorkspaceWindowManager * window_manager_
void RebuildLayout(EditorType type, ImGuiID dockspace_id)
Force rebuild of layout for a specific editor type.
std::unordered_map< EditorType, bool > layouts_initialized_
bool IsLayoutInitialized(EditorType type) const
Check if a layout has been initialized for an editor.
void MarkLayoutInitialized(EditorType type)
Mark a layout as initialized.
void InitializeEditorLayout(EditorType type, ImGuiID dockspace_id)
Initialize the default layout for a specific editor type.
void BuildLayoutFromPreset(EditorType type, ImGuiID dockspace_id)
static PanelLayoutPreset GetLogicDebuggerPreset()
Get the "logic debugger" workspace preset (QA and debug focused)
static PanelLayoutPreset GetDungeonMasterPreset()
Get the "dungeon master" workspace preset.
static PanelLayoutPreset GetAudioEngineerPreset()
Get the "audio engineer" workspace preset (music focused)
static PanelLayoutPreset GetDesignerPreset()
Get the "designer" workspace preset (visual-focused)
static std::vector< std::string > GetDefaultWindows(EditorType type)
static PanelLayoutPreset GetOverworldArtistPreset()
Get the "overworld artist" workspace preset.
static PanelLayoutPreset GetModderPreset()
Get the "modder" workspace preset (full-featured)
static PanelLayoutPreset GetMinimalPreset()
Get the "minimal" workspace preset (minimal cards)
static PanelLayoutPreset GetDeveloperPreset()
Get the "developer" workspace preset (debug-focused)
Manages user preferences and settings persistence.
Base interface for all logical window content components.
Central registry for all editor cards with session awareness and dependency injection.
std::string GetWorkspaceWindowName(size_t session_id, const std::string &base_window_id) const
Resolve the exact ImGui window name for a panel by base ID.
const WindowDescriptor * GetWindowDescriptor(size_t session_id, const std::string &base_window_id) const
bool OpenWindow(size_t session_id, const std::string &base_window_id)
WindowContent * GetWindowContent(const std::string &window_id)
Get a WindowContent instance by ID.
static absl::StatusOr< std::filesystem::path > GetAppDataSubdirectory(const std::string &subdir)
Get a subdirectory within the app data folder.
static absl::Status EnsureDirectoryExists(const std::filesystem::path &path)
Ensure a directory exists, creating it if necessary.
@ SD
Definition zelda.h:44
#define LOG_ERROR(category, format,...)
Definition log.h:109
#define LOG_WARN(category, format,...)
Definition log.h:107
#define LOG_INFO(category, format,...)
Definition log.h:105
std::vector< std::pair< std::string, DockPosition > > CollectDockedPanels(const PanelLayoutPreset &preset)
float ResolvePreferredRegionWidth(WorkspaceWindowManager *window_manager, const std::vector< std::pair< std::string, DockPosition > > &docked_panels, bool(*matches_region)(DockPosition))
std::string ResolveProfilePresetName(const std::string &profile_id, EditorType editor_type)
void CollectPanelIdsInSubtree(const layout_designer::DockNode &node, std::vector< std::string > *out)
bool TryGetNamedPreset(const std::string &preset_name, PanelLayoutPreset *preset_out)
yaze::Json BoolMapToJson(const std::unordered_map< std::string, bool > &map)
void ApplyDockNodeRecursive(WorkspaceWindowManager *wm, size_t session_id, const layout_designer::DockNode &node, ImGuiID target_id)
void ApplyPreferredSplitWidths(DockSplitConfig *cfg, const DockSplitNeeds &needs, float viewport_width, WorkspaceWindowManager *window_manager, const std::vector< std::pair< std::string, DockPosition > > &docked_panels)
ImGuiDir SplitDirectionToImGuiDir(layout_designer::SplitDirection d)
void JsonToWindowMap(const yaze::Json &entry, std::unordered_map< std::string, bool > *windows)
std::filesystem::path GetLayoutsFilePath(LayoutScope scope, const std::string &project_key)
void JsonToBoolMap(const yaze::Json &obj, std::unordered_map< std::string, bool > *map)
bool ShouldDockPanelInDefaultLayout(const PanelLayoutPreset &preset, const std::string &panel_id)
std::unique_ptr< layout_designer::DockNode > CaptureDockNodeRecursive(const ImGuiDockNode *node, const std::unordered_map< std::string, PanelLookupEntry > &by_window_name)
std::string ResolveDockWindowTitle(const WorkspaceWindowManager *wm, size_t session_id, const layout_designer::PanelEntry &panel)
DockSplitNeeds ComputeSplitNeeds(const std::vector< std::pair< std::string, DockPosition > > &docked_panels)
void ShowDefaultWindowsForEditor(WorkspaceWindowManager *registry, EditorType type)
DockNodeIds BuildDockTree(ImGuiID dockspace_id, const DockSplitNeeds &needs, const DockSplitConfig &cfg)
LayoutScope
Storage scope for saved layouts.
DockPosition
Preferred dock position for a card in a layout.
void logf(const absl::FormatSpec< Args... > &format, Args &&... args)
Definition log.h:115
std::unordered_map< std::string, bool > visibility
std::unordered_map< std::string, bool > pinned
Built-in workflow-oriented layout profiles.
Defines default panel visibility for an editor type.
std::vector< std::string > optional_panels
std::unordered_map< std::string, DockPosition > panel_positions
std::vector< std::string > default_visible_panels
std::unordered_map< std::string, std::string > named_layouts
Metadata for a dockable editor window (formerly PanelInfo)
Represents a dock node in the layout tree.
Definition dock_tree.h:61
std::unique_ptr< DockNode > child_a
Definition dock_tree.h:78
std::unique_ptr< DockNode > child_b
Definition dock_tree.h:79
std::vector< PanelEntry > panels
Definition dock_tree.h:72
std::unique_ptr< DockNode > root
Definition dock_tree.h:115
bool Validate(std::string *error) const
Definition dock_tree.cc:222