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
173
175 ImGuiID dockspace_id) {
177 auto& state = lazy_default_dock_state_;
179 state.dockspace != dockspace_id ||
180 !ImGui::DockBuilderGetNode(state.dockspace) ||
181 !ImGui::DockBuilderGetNode(state.center)) {
182 state.enabled = false;
183 return;
184 }
185
186 const size_t session_id = window_manager_->GetActiveSessionId();
187 state.enabled = true;
188 state.editor_type = type;
189 state.session_id = session_id;
190 state.attempted_panels = lazy_default_dock_attempts_[session_id][type];
191}
192
193void LayoutManager::MarkLazyDefaultDockAttempted(const std::string& panel_id) {
194 auto& state = lazy_default_dock_state_;
195 state.attempted_panels.insert(panel_id);
196 lazy_default_dock_attempts_[state.session_id][state.editor_type].insert(
197 panel_id);
198}
199
201 if (!window_manager_) {
202 return;
203 }
204
205 const size_t session_id = window_manager_->GetActiveSessionId();
207 const auto dock_if_open = [&](const std::string& panel_id) {
208 if (window_manager_->IsWindowOpen(session_id, panel_id)) {
209 DockPresetPositionOnFirstOpen(session_id, panel_id,
210 /*include_default_visible=*/true);
211 }
212 };
213 for (const auto& panel_id : preset.default_visible_panels) {
214 dock_if_open(panel_id);
215 }
216 for (const auto& panel_id : preset.optional_panels) {
217 dock_if_open(panel_id);
218 }
219}
220
228
230 ImGuiID dockspace_id) {
231 // Phase 8.2 review (2026-04-25): one-shot protection set by
232 // MaybeReapplyStartupLayout. The very first lazy preset init after a
233 // successful startup-reapply must NOT rebuild the dockspace —
234 // doing so would silently overwrite the user's saved layout on
235 // their first editor activation. Consume the flag, mark the type
236 // initialized so subsequent activations behave normally, and bail.
240 last_dockspace_id_ = dockspace_id;
243 LOG_INFO("LayoutManager",
244 "Suppressed lazy preset init for editor type %d to preserve "
245 "startup-reapplied custom layout",
246 static_cast<int>(type));
247 return;
248 }
249
250 // Refresh the active editor/session even when its default dock tree was
251 // initialized earlier. Lazy optional-panel placement uses the live tree but
252 // keeps its one-shot history scoped to this ROM session and editor.
253 last_dockspace_id_ = dockspace_id;
255
256 // Don't reinitialize if already set up
257 if (IsLayoutInitialized(type)) {
258 RefreshLazyDefaultDockingContext(type, dockspace_id);
259 // Session visibility restoration intentionally does not publish panel-open
260 // events. Sweep the restored state after refreshing the session context so
261 // an already-visible optional panel still receives its one-shot default.
263 LOG_INFO("LayoutManager",
264 "Layout for editor type %d already initialized, skipping",
265 static_cast<int>(type));
266 return;
267 }
268
269 LOG_INFO("LayoutManager", "Initializing layout for editor type %d",
270 static_cast<int>(type));
271
272 // Clear existing layout for this dockspace
273 ImGui::DockBuilderRemoveNode(dockspace_id);
274 ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
275
276 ImVec2 dockspace_size = ImVec2(1280, 720); // Safe default
277 if (auto* viewport = ImGui::GetMainViewport()) {
278 dockspace_size = viewport->WorkSize;
279 }
280
281 const ImVec2 last_size = gui::DockSpaceRenderer::GetLastDockspaceSize();
282 if (last_size.x > 0.0f && last_size.y > 0.0f) {
283 dockspace_size = last_size;
284 }
285 ImGui::DockBuilderSetNodeSize(dockspace_id, dockspace_size);
286
287 // Build layout based on editor type using generic builder
288 BuildLayoutFromPreset(type, dockspace_id);
289
290 // Show default windows from LayoutPresets (single source of truth)
291 ShowDefaultWindowsForEditor(window_manager_, type);
292
293 // Finalize the layout
294 ImGui::DockBuilderFinish(dockspace_id);
295
296 // Visibility may have been restored before this editor's first layout
297 // build. Give already-open optional panels the same first-open placement.
299
300 // Mark as initialized
302}
303
304void LayoutManager::RebuildLayout(EditorType type, ImGuiID dockspace_id) {
305 // Validate dockspace exists
306 ImGuiDockNode* node = ImGui::DockBuilderGetNode(dockspace_id);
307 if (!node) {
308 LOG_ERROR("LayoutManager",
309 "Cannot rebuild layout: dockspace ID %u not found", dockspace_id);
310 return;
311 }
312
313 LOG_INFO("LayoutManager", "Forcing rebuild of layout for editor type %d",
314 static_cast<int>(type));
315
316 // Store dockspace ID and current editor type
317 last_dockspace_id_ = dockspace_id;
319
320 // Clear the layout initialization flag to force rebuild
321 layouts_initialized_[type] = false;
322
323 // Clear existing layout for this dockspace
324 ImGui::DockBuilderRemoveNode(dockspace_id);
325 ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
326 ImVec2 dockspace_size = ImGui::GetMainViewport()->WorkSize;
327 const ImVec2 last_size = gui::DockSpaceRenderer::GetLastDockspaceSize();
328 if (last_size.x > 0.0f && last_size.y > 0.0f) {
329 dockspace_size = last_size;
330 }
331 ImGui::DockBuilderSetNodeSize(dockspace_id, dockspace_size);
332
333 // Build layout based on editor type using generic builder
334 BuildLayoutFromPreset(type, dockspace_id);
335
336 // Show default cards from LayoutPresets (single source of truth)
337 ShowDefaultWindowsForEditor(window_manager_, type);
338
339 // Finalize the layout
340 ImGui::DockBuilderFinish(dockspace_id);
341
343
344 // Mark as initialized
346
347 LOG_INFO("LayoutManager", "Layout rebuild complete for editor type %d",
348 static_cast<int>(type));
349}
350
351namespace {
352
354 float left = 0.17f;
355 float right = 0.24f;
356 float bottom = 0.22f;
357 float top = 0.12f;
358 float vertical_split = 0.52f;
359
360 // Per-editor type configuration
362 DockSplitConfig cfg;
363 switch (type) {
364 case EditorType::kDungeon:
365 // Dungeon: reserve more right-side space for object/property workflows.
366 cfg.left = 0.16f;
367 cfg.right = 0.26f;
368 cfg.bottom = 0.20f;
369 cfg.vertical_split = 0.50f;
370 break;
371 case EditorType::kOverworld:
372 cfg.left = 0.22f;
373 cfg.right = 0.26f;
374 cfg.bottom = 0.23f;
375 cfg.vertical_split = 0.42f;
376 break;
377 case EditorType::kGraphics:
378 cfg.left = 0.16f;
379 cfg.right = 0.24f;
380 cfg.bottom = 0.20f;
381 break;
382 case EditorType::kPalette:
383 cfg.left = 0.16f;
384 cfg.right = 0.22f;
385 cfg.bottom = 0.20f;
386 break;
387 case EditorType::kSprite:
388 cfg.left = 0.18f;
389 cfg.right = 0.24f;
390 cfg.bottom = 0.20f;
391 break;
392 case EditorType::kScreen:
393 cfg.left = 0.16f;
394 cfg.right = 0.22f;
395 cfg.bottom = 0.20f;
396 break;
397 case EditorType::kMessage:
398 cfg.left = 0.20f;
399 cfg.right = 0.26f;
400 cfg.bottom = 0.18f;
401 break;
402 case EditorType::kAssembly:
403 cfg.left = 0.24f;
404 cfg.right = 0.16f;
405 cfg.bottom = 0.20f;
406 break;
407 case EditorType::kEmulator:
408 cfg.left = 0.14f;
409 cfg.right = 0.28f;
410 cfg.bottom = 0.22f;
411 break;
412 case EditorType::kAgent:
413 cfg.left = 0.16f;
414 cfg.right = 0.30f;
415 cfg.bottom = 0.24f;
416 break;
417 default:
418 // Use defaults
419 break;
420 }
421 return cfg;
422 }
423};
424
426 ImGuiID center = 0;
427 ImGuiID left = 0;
428 ImGuiID right = 0;
429 ImGuiID bottom = 0;
430 ImGuiID top = 0;
431 ImGuiID left_top = 0;
432 ImGuiID left_bottom = 0;
433 ImGuiID right_top = 0;
434 ImGuiID right_bottom = 0;
435};
436
438 bool left = false;
439 bool right = false;
440 bool bottom = false;
441 bool top = false;
442 bool left_top = false;
443 bool left_bottom = false;
444 bool right_top = false;
445 bool right_bottom = false;
446};
447
449 const std::string& panel_id) {
451 return true;
452 }
453 return std::find(preset.default_visible_panels.begin(),
454 preset.default_visible_panels.end(),
455 panel_id) != preset.default_visible_panels.end();
456}
457
458std::vector<std::pair<std::string, DockPosition>> CollectDockedPanels(
459 const PanelLayoutPreset& preset) {
460 std::vector<std::pair<std::string, DockPosition>> docked_panels;
461 docked_panels.reserve(preset.panel_positions.size());
462
463 std::unordered_set<std::string> seen_panels;
464 seen_panels.reserve(preset.panel_positions.size());
465
466 auto append_ordered = [&](const std::vector<std::string>& ordered_ids) {
467 for (const auto& panel_id : ordered_ids) {
468 if (seen_panels.contains(panel_id) ||
469 !ShouldDockPanelInDefaultLayout(preset, panel_id)) {
470 continue;
471 }
472 auto it = preset.panel_positions.find(panel_id);
473 if (it == preset.panel_positions.end()) {
474 continue;
475 }
476 docked_panels.emplace_back(it->first, it->second);
477 seen_panels.insert(panel_id);
478 }
479 };
480
481 append_ordered(preset.default_visible_panels);
482 append_ordered(preset.optional_panels);
483
484 std::vector<std::pair<std::string, DockPosition>> remaining_panels;
485 remaining_panels.reserve(preset.panel_positions.size());
486 for (const auto& [panel_id, position] : preset.panel_positions) {
487 if (seen_panels.contains(panel_id) ||
488 !ShouldDockPanelInDefaultLayout(preset, panel_id)) {
489 continue;
490 }
491 remaining_panels.emplace_back(panel_id, position);
492 }
493
494 std::sort(remaining_panels.begin(), remaining_panels.end(),
495 [](const auto& lhs, const auto& rhs) {
496 if (lhs.second != rhs.second) {
497 return static_cast<int>(lhs.second) <
498 static_cast<int>(rhs.second);
499 }
500 return lhs.first < rhs.first;
501 });
502 docked_panels.insert(docked_panels.end(), remaining_panels.begin(),
503 remaining_panels.end());
504
505 return docked_panels;
506}
507
509 const std::vector<std::pair<std::string, DockPosition>>& docked_panels) {
510 DockSplitNeeds needs{};
511 for (const auto& [_, pos] : docked_panels) {
512 switch (pos) {
514 needs.left = true;
515 break;
517 needs.right = true;
518 break;
520 needs.bottom = true;
521 break;
523 needs.top = true;
524 break;
526 needs.left = true;
527 needs.left_top = true;
528 break;
530 needs.left = true;
531 needs.left_bottom = true;
532 break;
534 needs.right = true;
535 needs.right_top = true;
536 break;
538 needs.right = true;
539 needs.right_bottom = true;
540 break;
542 default:
543 break;
544 }
545 }
546 return needs;
547}
548
553
555 return pos == DockPosition::Left || pos == DockPosition::LeftTop ||
557}
558
560 WorkspaceWindowManager* window_manager,
561 const std::vector<std::pair<std::string, DockPosition>>& docked_panels,
562 bool (*matches_region)(DockPosition)) {
563 if (!window_manager) {
564 return 0.0f;
565 }
566
567 float preferred_width = 0.0f;
568 for (const auto& [panel_id, position] : docked_panels) {
569 if (!matches_region(position)) {
570 continue;
571 }
572 if (WindowContent* panel = window_manager->GetWindowContent(panel_id)) {
573 preferred_width = std::max(preferred_width, panel->GetPreferredWidth());
574 }
575 }
576 return preferred_width;
577}
578
580 DockSplitConfig* cfg, const DockSplitNeeds& needs, float viewport_width,
581 WorkspaceWindowManager* window_manager,
582 const std::vector<std::pair<std::string, DockPosition>>& docked_panels) {
583 if (!cfg || !window_manager || viewport_width <= 0.0f) {
584 return;
585 }
586
587 if (needs.left) {
588 const float preferred_left = ResolvePreferredRegionWidth(
589 window_manager, docked_panels, IsLeftDockPosition);
590 if (preferred_left > 0.0f) {
591 cfg->left = std::clamp(preferred_left / viewport_width, 0.14f, 0.36f);
592 }
593 }
594
595 if (needs.right) {
596 const float preferred_right = ResolvePreferredRegionWidth(
597 window_manager, docked_panels, IsRightDockPosition);
598 if (preferred_right > 0.0f) {
599 cfg->right = std::clamp(preferred_right / viewport_width, 0.18f, 0.42f);
600 }
601 }
602}
603
604DockNodeIds BuildDockTree(ImGuiID dockspace_id, const DockSplitNeeds& needs,
605 const DockSplitConfig& cfg) {
606 DockNodeIds ids{};
607 ids.center = dockspace_id;
608
609 // Split major regions
610 if (needs.left) {
611 ids.left = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Left, cfg.left,
612 nullptr, &ids.center);
613 }
614 if (needs.right) {
615 ids.right = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Right,
616 cfg.right, nullptr, &ids.center);
617 }
618 if (needs.bottom) {
619 ids.bottom = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Down,
620 cfg.bottom, nullptr, &ids.center);
621 }
622 if (needs.top) {
623 ids.top = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Up, cfg.top,
624 nullptr, &ids.center);
625 }
626
627 // Sub-split Left region
628 if (ids.left && (needs.left_top || needs.left_bottom)) {
629 // If we only need one, the split still happens but we use the result accordingly
630 ids.left_bottom = ImGui::DockBuilderSplitNode(
631 ids.left, ImGuiDir_Down, cfg.vertical_split, nullptr, &ids.left_top);
632
633 // If one isn't needed, we technically don't have to split, but for a stable tree,
634 // we do it and get_dock_id will map to the leaf.
635 }
636
637 // Sub-split Right region
638 if (ids.right && (needs.right_top || needs.right_bottom)) {
639 ids.right_bottom = ImGui::DockBuilderSplitNode(
640 ids.right, ImGuiDir_Down, cfg.vertical_split, nullptr, &ids.right_top);
641 }
642
643 return ids;
644}
645
646} // namespace
647
648void LayoutManager::BuildLayoutFromPreset(EditorType type,
649 ImGuiID dockspace_id) {
650 DisableLazyDefaultDocking();
651 // Every preset build replaces the one shared DockBuilder tree. Any prior
652 // first-open record points at nodes that were just destroyed, including
653 // records belonging to another editor or ROM session.
654 lazy_default_dock_attempts_.clear();
655 auto preset = LayoutPresets::GetDefaultPreset(type);
656
657 if (!window_manager_) {
658 LOG_WARN("LayoutManager",
659 "WorkspaceWindowManager not available, skipping dock layout for "
660 "type %d",
661 static_cast<int>(type));
662 return;
663 }
664
665 const size_t session_id =
666 window_manager_ ? window_manager_->GetActiveSessionId() : 0;
667 const auto docked_panels = CollectDockedPanels(preset);
668
669 // On compact/touch layouts, collapse all panels into center tabs instead of
670 // splitting into left/right/bottom regions. This gives each panel full
671 // screen width and makes tab-switching more natural on touch.
672 const ImGuiViewport* viewport = ImGui::GetMainViewport();
673 const float viewport_width = viewport ? viewport->WorkSize.x : 0.0f;
674 const bool is_compact =
675#if defined(__APPLE__) && TARGET_OS_IOS == 1
676 [&]() {
677 static bool compact_mode = true;
678 constexpr float kEnterCompactWidth = 900.0f;
679 constexpr float kExitCompactWidth = 940.0f;
680 if (viewport_width <= 0.0f) {
681 return compact_mode;
682 }
683 compact_mode = compact_mode ? (viewport_width < kExitCompactWidth)
684 : (viewport_width < kEnterCompactWidth);
685 return compact_mode;
686 }();
687#else
688 (viewport_width > 0.0f && viewport_width < 900.0f);
689#endif
690
691 DockSplitNeeds needs{};
692 DockSplitConfig cfg{};
693 if (!is_compact) {
694 needs = ComputeSplitNeeds(docked_panels);
695 cfg = DockSplitConfig::ForEditor(type);
696 ApplyPreferredSplitWidths(&cfg, needs, viewport_width, window_manager_,
697 docked_panels);
698 }
699 // When compact, needs is all-false → BuildDockTree produces center-only.
700 DockNodeIds ids = BuildDockTree(dockspace_id, needs, cfg);
701
702 // Keep the live default-tree geometry even for editors that do not use lazy
703 // docking. If the user returns to Dungeon/Overworld without rebuilding the
704 // shared dockspace, their optional panels can still resolve valid nodes.
705 auto& state = lazy_default_dock_state_;
706 state.enabled = preset.dock_only_default_visible_panels;
707 state.compact = is_compact;
708 state.editor_type = type;
709 state.session_id = session_id;
710 state.dockspace = dockspace_id;
711 state.center = ids.center;
712 state.left = ids.left;
713 state.right = ids.right;
714 state.bottom = ids.bottom;
715 state.top = ids.top;
716 state.left_top = ids.left_top;
717 state.left_bottom = ids.left_bottom;
718 state.right_top = ids.right_top;
719 state.right_bottom = ids.right_bottom;
720 state.left_ratio = cfg.left;
721 state.right_ratio = cfg.right;
722 state.bottom_ratio = cfg.bottom;
723 state.top_ratio = cfg.top;
724 state.vertical_split = cfg.vertical_split;
725 if (state.enabled) {
726 state.attempted_panels = lazy_default_dock_attempts_[session_id][type];
727 }
728
729 auto get_dock_id = [&](DockPosition pos) -> ImGuiID {
730 switch (pos) {
731 case DockPosition::Left:
732 if (ids.left_top || ids.left_bottom) {
733 // If sub-nodes exist, default "Left" to Top to avoid stacking in parent
734 return ids.left_top ? ids.left_top : ids.left_bottom;
735 }
736 return ids.left ? ids.left : ids.center;
737 case DockPosition::Right:
738 if (ids.right_top || ids.right_bottom) {
739 return ids.right_top ? ids.right_top : ids.right_bottom;
740 }
741 return ids.right ? ids.right : ids.center;
742 case DockPosition::Bottom:
743 return ids.bottom ? ids.bottom : ids.center;
744 case DockPosition::Top:
745 return ids.top ? ids.top : ids.center;
746 case DockPosition::LeftTop:
747 return ids.left_top ? ids.left_top : (ids.left ? ids.left : ids.center);
748 case DockPosition::LeftBottom:
749 return ids.left_bottom ? ids.left_bottom
750 : (ids.left ? ids.left : ids.center);
751 case DockPosition::RightTop:
752 return ids.right_top ? ids.right_top
753 : (ids.right ? ids.right : ids.center);
754 case DockPosition::RightBottom:
755 return ids.right_bottom ? ids.right_bottom
756 : (ids.right ? ids.right : ids.center);
757 case DockPosition::Center:
758 default:
759 return ids.center;
760 }
761 };
762
763 std::vector<ImGuiID> auto_hide_nodes;
764
765 // Iterate through positioned windows and dock them
766 for (const auto& [panel_id, position] : docked_panels) {
767 const WindowDescriptor* desc =
768 window_manager_
769 ? window_manager_->GetWindowDescriptor(session_id, panel_id)
770 : nullptr;
771 if (!desc) {
772 LOG_WARN("LayoutManager",
773 "Preset references window '%s' that is not registered (session "
774 "%zu)",
775 panel_id.c_str(), session_id);
776 continue;
777 }
778
779 std::string window_title = window_manager_->GetWorkspaceWindowName(*desc);
780 if (window_title.empty()) {
781 LOG_WARN("LayoutManager",
782 "Cannot dock window '%s': missing window name (session %zu)",
783 panel_id.c_str(), session_id);
784 continue;
785 }
786
787 const ImGuiID dock_id = get_dock_id(position);
788 ImGui::DockBuilderDockWindow(window_title.c_str(), dock_id);
789
790 if (WindowContent* panel = window_manager_->GetWindowContent(panel_id);
791 panel && panel->PreferAutoHideTabBar() &&
792 std::find(auto_hide_nodes.begin(), auto_hide_nodes.end(), dock_id) ==
793 auto_hide_nodes.end()) {
794 if (ImGuiDockNode* node = ImGui::DockBuilderGetNode(dock_id)) {
795 node->LocalFlags |= ImGuiDockNodeFlags_AutoHideTabBar;
796 auto_hide_nodes.push_back(dock_id);
797 }
798 }
799 }
800}
801
802bool LayoutManager::DockDefaultPositionOnFirstOpen(
803 size_t session_id, const std::string& panel_id) {
804 return DockPresetPositionOnFirstOpen(session_id, panel_id,
805 /*include_default_visible=*/false);
806}
807
808bool LayoutManager::DockPresetPositionOnPanelOpen(size_t session_id,
809 const std::string& panel_id) {
810 return DockPresetPositionOnFirstOpen(session_id, panel_id,
811 /*include_default_visible=*/true);
812}
813
814bool LayoutManager::DockPresetPositionOnFirstOpen(
815 size_t session_id, const std::string& panel_id,
816 bool include_default_visible) {
817 auto& state = lazy_default_dock_state_;
818 if (!state.enabled || !window_manager_ || panel_id.empty() ||
819 session_id != state.session_id ||
820 !ImGui::DockBuilderGetNode(state.dockspace)) {
821 return false;
822 }
823
824 const PanelLayoutPreset preset =
825 LayoutPresets::GetDefaultPreset(state.editor_type);
826 const bool is_default_visible =
827 std::find(preset.default_visible_panels.begin(),
828 preset.default_visible_panels.end(),
829 panel_id) != preset.default_visible_panels.end();
831 (!include_default_visible && is_default_visible)) {
832 return false;
833 }
834
835 const auto position_it = preset.panel_positions.find(panel_id);
836 if (position_it == preset.panel_positions.end() ||
837 state.attempted_panels.contains(panel_id)) {
838 return false;
839 }
840
841 const WindowDescriptor* desc =
842 window_manager_->GetWindowDescriptor(session_id, panel_id);
843 if (!desc) {
844 return false;
845 }
846 const std::string window_title =
847 window_manager_->GetWorkspaceWindowName(*desc);
848 if (window_title.empty()) {
849 return false;
850 }
851
852 // A panel restored into a valid dock node already has an intentional
853 // position. Count that as its one shot without moving it.
854 if (ImGuiWindow* window = ImGui::FindWindowByName(window_title.c_str());
855 window && window->DockId != 0 &&
856 ImGui::DockBuilderGetNode(window->DockId)) {
857 MarkLazyDefaultDockAttempted(panel_id);
858 return false;
859 }
860 if (ImGuiWindowSettings* settings =
861 ImGui::FindWindowSettingsByID(ImHashStr(window_title.c_str()));
862 settings && settings->DockId != 0 &&
863 ImGui::DockBuilderGetNode(settings->DockId)) {
864 MarkLazyDefaultDockAttempted(panel_id);
865 return false;
866 }
867
868 auto split_center = [&](ImGuiDir direction, float ratio,
869 ImGuiID* region) -> ImGuiID {
870 if (*region != 0 && ImGui::DockBuilderGetNode(*region)) {
871 return *region;
872 }
873 if (state.center == 0 || !ImGui::DockBuilderGetNode(state.center)) {
874 return 0;
875 }
876 *region = ImGui::DockBuilderSplitNode(state.center, direction, ratio,
877 nullptr, &state.center);
878 return *region;
879 };
880
881 auto resolve_vertical_region = [&](bool right_side,
882 bool want_bottom) -> ImGuiID {
883 ImGuiID& region = right_side ? state.right : state.left;
884 ImGuiID& top = right_side ? state.right_top : state.left_top;
885 ImGuiID& bottom = right_side ? state.right_bottom : state.left_bottom;
886 const ImGuiDir side_direction = right_side ? ImGuiDir_Right : ImGuiDir_Left;
887 const float side_ratio = right_side ? state.right_ratio : state.left_ratio;
888 if (!split_center(side_direction, side_ratio, &region)) {
889 return 0;
890 }
891
892 if (want_bottom) {
893 if (bottom != 0 && ImGui::DockBuilderGetNode(bottom)) {
894 return bottom;
895 }
896 if (top == 0) {
897 bottom = region;
898 return bottom;
899 }
900 ImGuiID new_top = 0;
901 bottom = ImGui::DockBuilderSplitNode(
902 region, ImGuiDir_Down, state.vertical_split, nullptr, &new_top);
903 top = new_top;
904 return bottom;
905 }
906
907 if (top != 0 && ImGui::DockBuilderGetNode(top)) {
908 return top;
909 }
910 if (bottom == 0) {
911 top = region;
912 return top;
913 }
914 ImGuiID new_bottom = 0;
915 top = ImGui::DockBuilderSplitNode(
916 region, ImGuiDir_Up, 1.0f - state.vertical_split, nullptr, &new_bottom);
917 bottom = new_bottom;
918 return top;
919 };
920
921 ImGuiID target = 0;
922 if (state.compact) {
923 target = state.center;
924 } else {
925 switch (position_it->second) {
926 case DockPosition::Left:
927 case DockPosition::LeftTop:
928 target = resolve_vertical_region(false, false);
929 break;
930 case DockPosition::LeftBottom:
931 target = resolve_vertical_region(false, true);
932 break;
933 case DockPosition::Right:
934 case DockPosition::RightTop:
935 target = resolve_vertical_region(true, false);
936 break;
937 case DockPosition::RightBottom:
938 target = resolve_vertical_region(true, true);
939 break;
940 case DockPosition::Bottom:
941 target = split_center(ImGuiDir_Down, state.bottom_ratio, &state.bottom);
942 break;
943 case DockPosition::Top:
944 target = split_center(ImGuiDir_Up, state.top_ratio, &state.top);
945 break;
946 case DockPosition::Center:
947 default:
948 target = state.center;
949 break;
950 }
951 }
952
953 if (target == 0 || !ImGui::DockBuilderGetNode(target)) {
954 return false;
955 }
956
957 MarkLazyDefaultDockAttempted(panel_id);
958 ImGui::DockBuilderDockWindow(window_title.c_str(), target);
959 if (WindowContent* panel = window_manager_->GetWindowContent(panel_id);
960 panel && panel->PreferAutoHideTabBar()) {
961 if (ImGuiDockNode* node = ImGui::DockBuilderGetNode(target)) {
962 node->LocalFlags |= ImGuiDockNodeFlags_AutoHideTabBar;
963 }
964 }
965 ImGui::DockBuilderFinish(state.dockspace);
966 return true;
967}
968
969// Deprecated individual build methods - redirected to generic or kept empty
970void LayoutManager::BuildOverworldLayout(ImGuiID dockspace_id) {
971 BuildLayoutFromPreset(EditorType::kOverworld, dockspace_id);
972}
973void LayoutManager::BuildDungeonLayout(ImGuiID dockspace_id) {
974 BuildLayoutFromPreset(EditorType::kDungeon, dockspace_id);
975}
976void LayoutManager::BuildGraphicsLayout(ImGuiID dockspace_id) {
977 BuildLayoutFromPreset(EditorType::kGraphics, dockspace_id);
978}
979void LayoutManager::BuildPaletteLayout(ImGuiID dockspace_id) {
980 BuildLayoutFromPreset(EditorType::kPalette, dockspace_id);
981}
982void LayoutManager::BuildScreenLayout(ImGuiID dockspace_id) {
983 BuildLayoutFromPreset(EditorType::kScreen, dockspace_id);
984}
985void LayoutManager::BuildMusicLayout(ImGuiID dockspace_id) {
986 BuildLayoutFromPreset(EditorType::kMusic, dockspace_id);
987}
988void LayoutManager::BuildSpriteLayout(ImGuiID dockspace_id) {
989 BuildLayoutFromPreset(EditorType::kSprite, dockspace_id);
990}
991void LayoutManager::BuildMessageLayout(ImGuiID dockspace_id) {
992 BuildLayoutFromPreset(EditorType::kMessage, dockspace_id);
993}
994void LayoutManager::BuildAssemblyLayout(ImGuiID dockspace_id) {
995 BuildLayoutFromPreset(EditorType::kAssembly, dockspace_id);
996}
997void LayoutManager::BuildSettingsLayout(ImGuiID dockspace_id) {
998 BuildLayoutFromPreset(EditorType::kSettings, dockspace_id);
999}
1000void LayoutManager::BuildEmulatorLayout(ImGuiID dockspace_id) {
1001 BuildLayoutFromPreset(EditorType::kEmulator, dockspace_id);
1002}
1003
1004void LayoutManager::SaveCurrentLayout(const std::string& name, bool persist) {
1005 if (!window_manager_) {
1006 LOG_WARN("LayoutManager",
1007 "Cannot save layout '%s': WorkspaceWindowManager not available",
1008 name.c_str());
1009 return;
1010 }
1011
1012 const LayoutScope scope = GetActiveScope();
1013
1014 // Serialize current window visibility state
1015 size_t session_id = window_manager_->GetActiveSessionId();
1016 auto visibility_state = window_manager_->SerializeVisibilityState(session_id);
1017
1018 // Store in saved_layouts_ for later persistence
1019 saved_layouts_[name] = visibility_state;
1020 saved_pinned_layouts_[name] = window_manager_->SerializePinnedState();
1021 layout_scopes_[name] = scope;
1022
1023 // Also save ImGui docking layout to memory
1024 size_t ini_size = 0;
1025 const char* ini_data = ImGui::SaveIniSettingsToMemory(&ini_size);
1026 if (ini_data && ini_size > 0) {
1027 saved_imgui_layouts_[name] = std::string(ini_data, ini_size);
1028 }
1029
1030 if (persist) {
1031 SaveLayoutsToDisk(scope);
1032 }
1033
1034 LOG_INFO("LayoutManager", "Saved layout '%s' with %zu window states%s",
1035 name.c_str(), visibility_state.size(),
1036 persist ? "" : " (session-only)");
1037}
1038
1039void LayoutManager::LoadLayout(const std::string& name) {
1040 if (!window_manager_) {
1041 LOG_WARN("LayoutManager",
1042 "Cannot load layout '%s': WorkspaceWindowManager not available",
1043 name.c_str());
1044 return;
1045 }
1046
1047 // Find saved layout
1048 auto layout_it = saved_layouts_.find(name);
1049 if (layout_it == saved_layouts_.end()) {
1050 LOG_WARN("LayoutManager", "Layout '%s' not found", name.c_str());
1051 return;
1052 }
1053
1054 const auto imgui_it = saved_imgui_layouts_.find(name);
1055 const bool has_imgui_layout =
1056 imgui_it != saved_imgui_layouts_.end() && !imgui_it->second.empty();
1057 // Only a saved ImGui layout owns placement. Legacy visibility-only layouts
1058 // must leave the current lazy-default context intact.
1059 if (has_imgui_layout) {
1060 DisableLazyDefaultDocking();
1061 }
1062
1063 // Restore window visibility
1064 size_t session_id = window_manager_->GetActiveSessionId();
1065 window_manager_->RestoreVisibilityState(session_id, layout_it->second,
1066 /*publish_events=*/true);
1067
1068 auto pinned_it = saved_pinned_layouts_.find(name);
1069 if (pinned_it != saved_pinned_layouts_.end()) {
1070 window_manager_->RestorePinnedState(pinned_it->second);
1071 }
1072
1073 // Restore ImGui docking layout if available
1074 if (has_imgui_layout) {
1075 ImGui::LoadIniSettingsFromMemory(imgui_it->second.c_str(),
1076 imgui_it->second.size());
1077 ProtectRestoredLayoutFromDefaultInitialization();
1078 }
1079
1080 LOG_INFO("LayoutManager", "Loaded layout '%s'", name.c_str());
1081}
1082
1083void LayoutManager::CaptureTemporarySessionLayout(size_t session_id) {
1084 if (!window_manager_) {
1085 return;
1086 }
1087
1088 temp_session_id_ = session_id;
1089 temp_session_visibility_ =
1090 window_manager_->SerializeVisibilityState(session_id);
1091 temp_session_pinned_ = window_manager_->SerializePinnedState();
1092
1093 size_t ini_size = 0;
1094 const char* ini_data = ImGui::SaveIniSettingsToMemory(&ini_size);
1095 if (ini_data && ini_size > 0) {
1096 temp_session_imgui_layout_ = std::string(ini_data, ini_size);
1097 } else {
1098 temp_session_imgui_layout_.clear();
1099 }
1100
1101 has_temp_session_layout_ = true;
1102 LOG_INFO(
1103 "LayoutManager",
1104 "Captured temporary session layout for session %zu (%zu panel states)",
1105 session_id, temp_session_visibility_.size());
1106}
1107
1108bool LayoutManager::RestoreTemporarySessionLayout(size_t session_id,
1109 bool clear_after_restore) {
1110 if (!window_manager_ || !has_temp_session_layout_) {
1111 return false;
1112 }
1113
1114 if (session_id != temp_session_id_) {
1115 LOG_WARN("LayoutManager",
1116 "Session layout snapshot belongs to session %zu, requested %zu",
1117 temp_session_id_, session_id);
1118 return false;
1119 }
1120
1121 const bool has_imgui_layout = !temp_session_imgui_layout_.empty();
1122 if (has_imgui_layout) {
1123 DisableLazyDefaultDocking();
1124 }
1125
1126 window_manager_->RestoreVisibilityState(session_id, temp_session_visibility_,
1127 /*publish_events=*/true);
1128 window_manager_->RestorePinnedState(temp_session_pinned_);
1129
1130 if (has_imgui_layout) {
1131 ImGui::LoadIniSettingsFromMemory(temp_session_imgui_layout_.c_str(),
1132 temp_session_imgui_layout_.size());
1133 ProtectRestoredLayoutFromDefaultInitialization();
1134 }
1135
1136 if (clear_after_restore) {
1137 ClearTemporarySessionLayout();
1138 }
1139
1140 LOG_INFO("LayoutManager", "Restored temporary session layout for session %zu",
1141 session_id);
1142 return true;
1143}
1144
1145void LayoutManager::ClearTemporarySessionLayout() {
1146 has_temp_session_layout_ = false;
1147 temp_session_id_ = 0;
1148 temp_session_visibility_.clear();
1149 temp_session_pinned_.clear();
1150 temp_session_imgui_layout_.clear();
1151}
1152
1153bool LayoutManager::SaveNamedSnapshot(const std::string& name,
1154 size_t session_id) {
1155 if (!window_manager_ || name.empty()) {
1156 return false;
1157 }
1158 SessionSnapshot snapshot;
1159 snapshot.session_id = session_id;
1160 snapshot.visibility = window_manager_->SerializeVisibilityState(session_id);
1161 snapshot.pinned = window_manager_->SerializePinnedState();
1162 size_t ini_size = 0;
1163 const char* ini_data = ImGui::SaveIniSettingsToMemory(&ini_size);
1164 if (ini_data && ini_size > 0) {
1165 snapshot.imgui_layout.assign(ini_data, ini_size);
1166 }
1167 named_snapshots_[name] = std::move(snapshot);
1168 LOG_INFO("LayoutManager", "Saved named snapshot '%s' for session %zu",
1169 name.c_str(), session_id);
1170 return true;
1171}
1172
1173bool LayoutManager::RestoreNamedSnapshot(const std::string& name,
1174 size_t session_id,
1175 bool remove_after_restore) {
1176 if (!window_manager_) {
1177 return false;
1178 }
1179 auto it = named_snapshots_.find(name);
1180 if (it == named_snapshots_.end()) {
1181 return false;
1182 }
1183 const SessionSnapshot& snapshot = it->second;
1184 if (snapshot.session_id != session_id) {
1185 return false;
1186 }
1187 const bool has_imgui_layout = !snapshot.imgui_layout.empty();
1188 if (has_imgui_layout) {
1189 DisableLazyDefaultDocking();
1190 }
1191 window_manager_->RestoreVisibilityState(session_id, snapshot.visibility,
1192 /*publish_events=*/true);
1193 window_manager_->RestorePinnedState(snapshot.pinned);
1194 if (has_imgui_layout) {
1195 ImGui::LoadIniSettingsFromMemory(snapshot.imgui_layout.c_str(),
1196 snapshot.imgui_layout.size());
1197 ProtectRestoredLayoutFromDefaultInitialization();
1198 }
1199 if (remove_after_restore) {
1200 named_snapshots_.erase(it);
1201 }
1202 return true;
1203}
1204
1205bool LayoutManager::DeleteNamedSnapshot(const std::string& name) {
1206 auto it = named_snapshots_.find(name);
1207 if (it == named_snapshots_.end()) {
1208 return false;
1209 }
1210 named_snapshots_.erase(it);
1211 return true;
1212}
1213
1214std::vector<std::string> LayoutManager::ListNamedSnapshots(
1215 size_t session_id) const {
1216 std::vector<std::string> names;
1217 names.reserve(named_snapshots_.size());
1218 for (const auto& [name, snapshot] : named_snapshots_) {
1219 if (snapshot.session_id == session_id) {
1220 names.push_back(name);
1221 }
1222 }
1223 std::sort(names.begin(), names.end());
1224 return names;
1225}
1226
1227bool LayoutManager::HasNamedSnapshot(const std::string& name) const {
1228 return named_snapshots_.find(name) != named_snapshots_.end();
1229}
1230
1231bool LayoutManager::DeleteLayout(const std::string& name) {
1232 auto layout_it = saved_layouts_.find(name);
1233 if (layout_it == saved_layouts_.end()) {
1234 LOG_WARN("LayoutManager", "Cannot delete layout '%s': not found",
1235 name.c_str());
1236 return false;
1237 }
1238
1239 LayoutScope scope = GetActiveScope();
1240 auto scope_it = layout_scopes_.find(name);
1241 if (scope_it != layout_scopes_.end()) {
1242 scope = scope_it->second;
1243 }
1244
1245 saved_layouts_.erase(layout_it);
1246 saved_imgui_layouts_.erase(name);
1247 saved_pinned_layouts_.erase(name);
1248 layout_scopes_.erase(name);
1249
1250 SaveLayoutsToDisk(scope);
1251
1252 LOG_INFO("LayoutManager", "Deleted layout '%s'", name.c_str());
1253 return true;
1254}
1255
1256std::vector<LayoutProfile> LayoutManager::GetBuiltInProfiles() {
1257 return {
1258 {.id = "code",
1259 .label = "Code",
1260 .description = "Focused editing workspace with minimal panel noise",
1261 .preset_name = "Minimal",
1262 .open_agent_chat = false},
1263 {.id = "debug",
1264 .label = "Debug",
1265 .description = "Debugger-first workspace for tracing and memory tools",
1266 .preset_name = "Developer",
1267 .open_agent_chat = false},
1268 {.id = "mapping",
1269 .label = "Mapping",
1270 .description = "Map-centric layout for overworld or dungeon workflows",
1271 .preset_name = "Overworld Expert",
1272 .open_agent_chat = false},
1273 {.id = "chat",
1274 .label = "Chat",
1275 .description = "Collaboration-heavy layout with agent-centric tooling",
1276 .preset_name = "Modder",
1277 .open_agent_chat = true},
1278 };
1279}
1280
1281bool LayoutManager::ApplyBuiltInProfile(const std::string& profile_id,
1282 size_t session_id,
1283 EditorType editor_type,
1284 LayoutProfile* out_profile) {
1285 if (!window_manager_) {
1286 LOG_WARN("LayoutManager",
1287 "Cannot apply profile '%s': WorkspaceWindowManager not available",
1288 profile_id.c_str());
1289 return false;
1290 }
1291
1292 LayoutProfile matched_profile;
1293 bool found = false;
1294 for (const auto& profile : GetBuiltInProfiles()) {
1295 if (profile.id == profile_id) {
1296 matched_profile = profile;
1297 found = true;
1298 break;
1299 }
1300 }
1301
1302 if (!found) {
1303 LOG_WARN("LayoutManager", "Unknown layout profile id: %s",
1304 profile_id.c_str());
1305 return false;
1306 }
1307
1308 const std::string resolved_preset =
1309 ResolveProfilePresetName(profile_id, editor_type);
1310 if (!resolved_preset.empty()) {
1311 matched_profile.preset_name = resolved_preset;
1312 }
1313
1314 PanelLayoutPreset preset;
1315 if (!TryGetNamedPreset(matched_profile.preset_name, &preset)) {
1316 LOG_WARN("LayoutManager", "Unable to resolve preset '%s' for profile '%s'",
1317 matched_profile.preset_name.c_str(), profile_id.c_str());
1318 return false;
1319 }
1320
1321 // The requested rebuild will establish a fresh lazy-default state. Avoid
1322 // mutating the current dock tree while profile visibility is changing.
1323 DisableLazyDefaultDocking();
1324
1325 window_manager_->HideAllWindowsInSession(session_id);
1326 for (const auto& panel_id : preset.default_visible_panels) {
1327 window_manager_->OpenWindow(session_id, panel_id);
1328 }
1329
1330 RequestRebuild();
1331
1332 if (out_profile) {
1333 *out_profile = matched_profile;
1334 }
1335
1336 LOG_INFO("LayoutManager", "Applied profile '%s' via preset '%s'",
1337 profile_id.c_str(), matched_profile.preset_name.c_str());
1338 return true;
1339}
1340
1341std::vector<std::string> LayoutManager::GetSavedLayoutNames() const {
1342 std::vector<std::string> names;
1343 names.reserve(saved_layouts_.size());
1344 for (const auto& [name, _] : saved_layouts_) {
1345 names.push_back(name);
1346 }
1347 return names;
1348}
1349
1350bool LayoutManager::HasLayout(const std::string& name) const {
1351 return saved_layouts_.find(name) != saved_layouts_.end();
1352}
1353
1354void LayoutManager::LoadLayoutsFromDisk() {
1355 saved_layouts_.clear();
1356 saved_imgui_layouts_.clear();
1357 saved_pinned_layouts_.clear();
1358 layout_scopes_.clear();
1359
1360 LoadLayoutsFromDiskInternal(LayoutScope::kGlobal, /*merge=*/false);
1361 if (!project_layout_key_.empty()) {
1362 LoadLayoutsFromDiskInternal(LayoutScope::kProject, /*merge=*/true);
1363 }
1364}
1365
1366void LayoutManager::SetProjectLayoutKey(const std::string& key) {
1367 if (key.empty()) {
1368 UseGlobalLayouts();
1369 return;
1370 }
1371 project_layout_key_ = key;
1372 LoadLayoutsFromDisk();
1373}
1374
1375void LayoutManager::UseGlobalLayouts() {
1376 project_layout_key_.clear();
1377 LoadLayoutsFromDisk();
1378}
1379
1380LayoutScope LayoutManager::GetActiveScope() const {
1381 return project_layout_key_.empty() ? LayoutScope::kGlobal
1382 : LayoutScope::kProject;
1383}
1384
1385void LayoutManager::LoadLayoutsFromDiskInternal(LayoutScope scope, bool merge) {
1386 std::filesystem::path layout_path =
1387 GetLayoutsFilePath(scope, project_layout_key_);
1388 if (layout_path.empty()) {
1389 return;
1390 }
1391
1392 if (!std::filesystem::exists(layout_path)) {
1393 if (!merge) {
1394 LOG_INFO("LayoutManager", "No layouts file at %s",
1395 layout_path.string().c_str());
1396 }
1397 return;
1398 }
1399
1400 try {
1401 std::ifstream file(layout_path);
1402 if (!file.is_open()) {
1403 LOG_WARN("LayoutManager", "Failed to open layouts file: %s",
1404 layout_path.string().c_str());
1405 return;
1406 }
1407
1408 yaze::Json root;
1409 file >> root;
1410
1411 if (!root.contains("layouts") || !root["layouts"].is_object()) {
1412 LOG_WARN("LayoutManager", "Layouts file missing 'layouts' object: %s",
1413 layout_path.string().c_str());
1414 return;
1415 }
1416
1417 for (auto& [name, entry] : root["layouts"].items()) {
1418 if (!entry.is_object()) {
1419 continue;
1420 }
1421
1422 std::unordered_map<std::string, bool> windows;
1423 std::unordered_map<std::string, bool> pinned;
1424
1425 JsonToWindowMap(entry, &windows);
1426 if (entry.contains("pinned")) {
1427 JsonToBoolMap(entry["pinned"], &pinned);
1428 }
1429
1430 saved_layouts_[name] = std::move(windows);
1431 saved_pinned_layouts_[name] = std::move(pinned);
1432 layout_scopes_[name] = scope;
1433
1434 if (entry.contains("imgui_ini") && entry["imgui_ini"].is_string()) {
1435 saved_imgui_layouts_[name] = entry["imgui_ini"].get<std::string>();
1436 } else {
1437 saved_imgui_layouts_.erase(name);
1438 }
1439 }
1440
1441 LOG_INFO("LayoutManager", "Loaded layouts from %s",
1442 layout_path.string().c_str());
1443 } catch (const std::exception& e) {
1444 LOG_WARN("LayoutManager", "Failed to load layouts: %s", e.what());
1445 }
1446}
1447
1448void LayoutManager::SaveLayoutsToDisk(LayoutScope scope) const {
1449 std::filesystem::path layout_path =
1450 GetLayoutsFilePath(scope, project_layout_key_);
1451 if (layout_path.empty()) {
1452 LOG_WARN("LayoutManager", "No layout path resolved for scope");
1453 return;
1454 }
1455
1456 auto status =
1457 util::PlatformPaths::EnsureDirectoryExists(layout_path.parent_path());
1458 if (!status.ok()) {
1459 LOG_WARN("LayoutManager", "Failed to create layout directory: %s",
1460 status.ToString().c_str());
1461 return;
1462 }
1463
1464 try {
1465 yaze::Json root;
1466 root["version"] = 2;
1467 root["layouts"] = yaze::Json::object();
1468
1469 for (const auto& [name, windows] : saved_layouts_) {
1470 auto scope_it = layout_scopes_.find(name);
1471 if (scope_it != layout_scopes_.end() && scope_it->second != scope) {
1472 continue;
1473 }
1474
1475 yaze::Json entry;
1476 entry[kWindowsKey] = BoolMapToJson(windows);
1477
1478 auto pinned_it = saved_pinned_layouts_.find(name);
1479 if (pinned_it != saved_pinned_layouts_.end()) {
1480 entry["pinned"] = BoolMapToJson(pinned_it->second);
1481 }
1482
1483 auto imgui_it = saved_imgui_layouts_.find(name);
1484 if (imgui_it != saved_imgui_layouts_.end()) {
1485 entry["imgui_ini"] = imgui_it->second;
1486 }
1487
1488 root["layouts"][name] = entry;
1489 }
1490
1491 std::ofstream file(layout_path);
1492 if (!file.is_open()) {
1493 LOG_WARN("LayoutManager", "Failed to open layouts file for write: %s",
1494 layout_path.string().c_str());
1495 return;
1496 }
1497 file << root.dump(2);
1498 file.close();
1499 } catch (const std::exception& e) {
1500 LOG_WARN("LayoutManager", "Failed to save layouts: %s", e.what());
1501 }
1502}
1503
1504void LayoutManager::ResetToDefaultLayout(EditorType type) {
1505 DisableLazyDefaultDocking();
1506 layouts_initialized_[type] = false;
1507 LOG_INFO("LayoutManager", "Reset layout for editor type %d",
1508 static_cast<int>(type));
1509}
1510
1511bool LayoutManager::IsLayoutInitialized(EditorType type) const {
1512 auto it = layouts_initialized_.find(type);
1513 return it != layouts_initialized_.end() && it->second;
1514}
1515
1516void LayoutManager::MarkLayoutInitialized(EditorType type) {
1517 layouts_initialized_[type] = true;
1518 LOG_INFO("LayoutManager", "Marked layout for editor type %d as initialized",
1519 static_cast<int>(type));
1520}
1521
1522void LayoutManager::ClearInitializationFlags() {
1523 DisableLazyDefaultDocking();
1524 layouts_initialized_.clear();
1525 LOG_INFO("LayoutManager", "Cleared all layout initialization flags");
1526}
1527
1528std::string LayoutManager::GetWindowTitle(const std::string& card_id) const {
1529 if (!window_manager_) {
1530 return "";
1531 }
1532
1533 const size_t session_id = window_manager_->GetActiveSessionId();
1534 return window_manager_->GetWorkspaceWindowName(session_id, card_id);
1535}
1536
1537namespace {
1538
1541 switch (d) {
1542 case SD::kLeft:
1543 return ImGuiDir_Left;
1544 case SD::kRight:
1545 return ImGuiDir_Right;
1546 case SD::kUp:
1547 return ImGuiDir_Up;
1548 case SD::kDown:
1549 return ImGuiDir_Down;
1550 }
1551 return ImGuiDir_Left;
1552}
1553
1555 size_t session_id,
1556 const layout_designer::PanelEntry& panel) {
1557 if (wm) {
1558 if (const auto* desc =
1559 wm->GetWindowDescriptor(session_id, panel.panel_id)) {
1560 return wm->GetWorkspaceWindowName(*desc);
1561 }
1562 }
1563 // Fallback: reconstruct the ImGui window name using the same formula as
1564 // WindowDescriptor::GetImGuiWindowName so layouts referencing panels
1565 // that register late still find their windows when they come up.
1566 std::string label;
1567 if (!panel.icon.empty() && !panel.display_name.empty()) {
1568 label = panel.icon + " " + panel.display_name;
1569 } else if (!panel.display_name.empty()) {
1570 label = panel.display_name;
1571 } else if (!panel.icon.empty()) {
1572 label = panel.icon;
1573 }
1574 if (panel.panel_id.empty()) {
1575 return label;
1576 }
1577 return label.empty() ? panel.panel_id : (label + "##" + panel.panel_id);
1578}
1579
1581 const layout_designer::DockNode& node,
1582 ImGuiID target_id) {
1583 using NodeType = layout_designer::DockNode::Type;
1584 if (node.type == NodeType::kLeaf) {
1585 for (const auto& panel : node.panels) {
1586 const std::string title = ResolveDockWindowTitle(wm, session_id, panel);
1587 if (!title.empty()) {
1588 ImGui::DockBuilderDockWindow(title.c_str(), target_id);
1589 }
1590 }
1591 return;
1592 }
1593
1594 // Split node.
1595 const ImGuiDir dir = SplitDirectionToImGuiDir(node.split_direction);
1596 const float ratio = std::clamp(node.split_ratio, 0.05f, 0.95f);
1597 ImGuiID id_other = 0;
1598 const ImGuiID id_at_dir =
1599 ImGui::DockBuilderSplitNode(target_id, dir, ratio, nullptr, &id_other);
1600 if (node.child_a) {
1601 ApplyDockNodeRecursive(wm, session_id, *node.child_a, id_at_dir);
1602 }
1603 if (node.child_b) {
1604 ApplyDockNodeRecursive(wm, session_id, *node.child_b, id_other);
1605 }
1606}
1607
1609 std::string panel_id;
1610 std::string display_name;
1611 std::string icon;
1612};
1613
1614std::unique_ptr<layout_designer::DockNode> CaptureDockNodeRecursive(
1615 const ImGuiDockNode* node,
1616 const std::unordered_map<std::string, PanelLookupEntry>& by_window_name) {
1617 if (!node) {
1618 return layout_designer::DockNode::MakeLeaf({});
1619 }
1620
1621 if (node->IsSplitNode()) {
1622 auto child_a =
1623 CaptureDockNodeRecursive(node->ChildNodes[0], by_window_name);
1624 auto child_b =
1625 CaptureDockNodeRecursive(node->ChildNodes[1], by_window_name);
1626
1627 const bool vertical = node->SplitAxis == ImGuiAxis_Y;
1629 vertical ? layout_designer::SplitDirection::kUp
1630 : layout_designer::SplitDirection::kLeft;
1631
1632 float ratio = 0.5f;
1633 if (node->ChildNodes[0] && node->ChildNodes[1]) {
1634 if (vertical && node->Size.y > 0.0f) {
1635 ratio = node->ChildNodes[0]->Size.y / node->Size.y;
1636 } else if (!vertical && node->Size.x > 0.0f) {
1637 ratio = node->ChildNodes[0]->Size.x / node->Size.x;
1638 }
1639 }
1640 ratio = std::clamp(ratio, 0.05f, 0.95f);
1641 return layout_designer::DockNode::MakeSplit(dir, ratio, std::move(child_a),
1642 std::move(child_b));
1643 }
1644
1645 // Leaf.
1646 std::vector<layout_designer::PanelEntry> panels;
1647 panels.reserve(static_cast<size_t>(node->Windows.Size));
1648 int selected_index = -1;
1649 for (int i = 0; i < node->Windows.Size; ++i) {
1650 const ImGuiWindow* w = node->Windows[i];
1651 if (!w || !w->Name)
1652 continue;
1653 auto it = by_window_name.find(std::string(w->Name));
1654 if (it == by_window_name.end())
1655 continue;
1656 if (node->SelectedTabId != 0 && w->ID == node->SelectedTabId) {
1657 selected_index = static_cast<int>(panels.size());
1658 }
1659 panels.push_back(
1660 {it->second.panel_id, it->second.display_name, it->second.icon});
1661 }
1662 auto leaf = layout_designer::DockNode::MakeLeaf(std::move(panels));
1663 if (selected_index >= 0 &&
1664 selected_index < static_cast<int>(leaf->panels.size())) {
1665 leaf->active_tab_index = selected_index;
1666 }
1667 return leaf;
1668}
1669
1670// Recursively walk a DockTree and append every leaf's panel_ids into
1671// `out`. Used by ApplyDockTree to drive the visibility-open pass.
1673 std::vector<std::string>* out) {
1674 if (node.type == layout_designer::DockNode::Type::kLeaf) {
1675 for (const auto& p : node.panels) {
1676 out->push_back(p.panel_id);
1677 }
1678 return;
1679 }
1680 if (node.child_a)
1682 if (node.child_b)
1684}
1685
1686} // namespace
1687
1688absl::Status LayoutManager::ApplyDockTree(const layout_designer::DockTree& tree,
1689 ImGuiID dockspace_id) {
1690 if (!window_manager_) {
1691 return absl::FailedPreconditionError(
1692 "LayoutManager::ApplyDockTree: WorkspaceWindowManager not bound");
1693 }
1694 std::string validation_error;
1695 if (!tree.Validate(&validation_error)) {
1696 return absl::InvalidArgumentError("LayoutManager::ApplyDockTree: " +
1697 validation_error);
1698 }
1699
1700 // A custom tree owns all panel placement. Disable default first-open
1701 // docking before the visibility pass publishes open events.
1702 DisableLazyDefaultDocking();
1703
1704 // Visibility pass (Phase 8 review 2026-04-24, refined 2026-04-25):
1705 // - Open every panel referenced in the tree so users see what they
1706 // docked rather than empty slots.
1707 // - Close every non-pinned panel that is NOT in the tree, so a saved
1708 // subset doesn't leave previously-visible panels lingering as
1709 // floating ghosts after apply. Pinned panels are intentionally
1710 // persistent (rev-7 / rev-17 force-pinned panels like
1711 // `agent.oracle_ram`, `workflow.output`, `layout.designer`) and
1712 // must survive — closing them would silently hide cross-editor
1713 // functionality the user can't easily put back.
1714 std::vector<std::string> panel_ids;
1715 if (tree.root) {
1716 CollectPanelIdsInSubtree(*tree.root, &panel_ids);
1717 }
1718 const std::unordered_set<std::string> tree_id_set(panel_ids.begin(),
1719 panel_ids.end());
1720 const size_t session_id = window_manager_->GetActiveSessionId();
1721 for (const auto& id : panel_ids) {
1722 // Phase 9 review (2026-04-25): skip already-open panels. Mirrors
1723 // the close-pass guard below — OpenWindowImpl publishes
1724 // WindowVisibilityChanged(true) and runs `on_show` regardless of
1725 // prior state, so a no-op Re-apply (or a startup reapply where
1726 // every panel is already visible) would otherwise dirty settings
1727 // and fire a burst of redundant show events.
1728 if (window_manager_->IsWindowOpen(session_id, id))
1729 continue;
1730 window_manager_->OpenWindow(session_id, id);
1731 }
1732 for (const std::string& id :
1733 window_manager_->GetWindowsInSession(session_id)) {
1734 if (tree_id_set.count(id) > 0)
1735 continue;
1736 if (window_manager_->IsWindowPinned(session_id, id))
1737 continue;
1738 // Phase 8.2 review 3 (2026-04-25): skip already-closed panels.
1739 // CloseWindowImpl fires on_hide and publishes
1740 // WindowVisibilityChanged(false) regardless of prior state, so
1741 // blindly closing every non-tree panel produces a burst of
1742 // redundant hide events for unrelated already-hidden windows.
1743 if (!window_manager_->IsWindowOpen(session_id, id))
1744 continue;
1745 window_manager_->CloseWindow(id);
1746 }
1747
1748 ImGui::DockBuilderRemoveNode(dockspace_id);
1749 ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
1750
1751 ImVec2 size(1280.0f, 720.0f);
1752 if (const ImGuiViewport* vp = ImGui::GetMainViewport()) {
1753 if (vp->WorkSize.x > 0.0f && vp->WorkSize.y > 0.0f) {
1754 size = vp->WorkSize;
1755 }
1756 }
1757 ImGui::DockBuilderSetNodeSize(dockspace_id, size);
1758
1759 ApplyDockNodeRecursive(window_manager_, session_id, *tree.root, dockspace_id);
1760 ImGui::DockBuilderFinish(dockspace_id);
1761
1762 last_dockspace_id_ = dockspace_id;
1763
1764 // Init-tracking pass (Phase 8.2 review 2026-04-25; refined 8.2 review 3):
1765 // - When the user is in an editor (current_editor_type_ != kUnknown),
1766 // mark only that editor type initialized. Other editors keep their
1767 // lazy first-run init — their preset fires on activation as before.
1768 // - When no editor is active yet (kUnknown — startup-reapply OR
1769 // manual apply from the dashboard/settings shell BEFORE any editor
1770 // activation), the per-editor mark wouldn't protect anything, so
1771 // arm the one-shot `startup_reapply_pending_protection_` flag that
1772 // the next InitializeEditorLayout call consumes. Round-3 Codex
1773 // noted that without this branch, applying from a no-editor
1774 // context still left the original clobber bug intact.
1775 ProtectRestoredLayoutFromDefaultInitialization();
1776
1777 return absl::OkStatus();
1778}
1779
1780absl::Status LayoutManager::MaybeReapplyStartupLayout(UserSettings* settings) {
1781 if (startup_layout_consumed_) {
1782 return absl::OkStatus();
1783 }
1784 if (settings == nullptr) {
1785 startup_layout_consumed_ = true;
1786 return absl::OkStatus();
1787 }
1788
1789 const std::string& name = settings->prefs().last_applied_layout_name;
1790 if (name.empty()) {
1791 // Nothing to reapply. Mark consumed so we stop checking each frame.
1792 startup_layout_consumed_ = true;
1793 return absl::OkStatus();
1794 }
1795
1796 if (main_dockspace_id_ == 0) {
1797 // The controller hasn't bound the main dockspace yet — try again
1798 // next frame. Leave the flag clear.
1799 return absl::OkStatus();
1800 }
1801
1802 const auto& named_layouts = settings->prefs().named_layouts;
1803 const auto it = named_layouts.find(name);
1804 if (it == named_layouts.end()) {
1805 startup_layout_consumed_ = true;
1806 util::logf(
1807 "LayoutManager: startup layout '%s' missing from "
1808 "named_layouts; falling through to default.",
1809 name.c_str());
1810 return absl::NotFoundError(absl::StrCat("startup layout \"", name,
1811 "\" not found in named_layouts"));
1812 }
1813
1814 nlohmann::json parsed;
1815 try {
1816 parsed = nlohmann::json::parse(it->second);
1817 } catch (const nlohmann::json::parse_error& e) {
1818 startup_layout_consumed_ = true;
1819 util::logf("LayoutManager: startup layout '%s' JSON parse error: %s",
1820 name.c_str(), e.what());
1821 return absl::InvalidArgumentError(
1822 absl::StrCat("startup layout \"", name, "\" parse error: ", e.what()));
1823 }
1824
1825 auto tree_or = layout_designer::DockTreeFromJson(parsed);
1826 if (!tree_or.ok()) {
1827 startup_layout_consumed_ = true;
1828 util::logf("LayoutManager: startup layout '%s' failed to parse: %s",
1829 name.c_str(), std::string(tree_or.status().message()).c_str());
1830 return tree_or.status();
1831 }
1832
1833 std::string validation_error;
1834 if (!tree_or->Validate(&validation_error)) {
1835 startup_layout_consumed_ = true;
1836 util::logf("LayoutManager: startup layout '%s' failed validation: %s",
1837 name.c_str(), validation_error.c_str());
1838 return absl::InvalidArgumentError(absl::StrCat(
1839 "startup layout \"", name, "\" validation failed: ", validation_error));
1840 }
1841
1842 absl::Status apply_status = ApplyDockTree(*tree_or, main_dockspace_id_);
1843 startup_layout_consumed_ = true;
1844 if (!apply_status.ok()) {
1845 util::logf("LayoutManager: startup layout '%s' ApplyDockTree failed: %s",
1846 name.c_str(), std::string(apply_status.message()).c_str());
1847 }
1848 // ApplyDockTree itself arms `startup_reapply_pending_protection_`
1849 // because `current_editor_type_` is still `kUnknown` at this point
1850 // in the boot — no need to set it again here. Same arming covers
1851 // dashboard-time manual apply, which round-3 Codex flagged.
1852 return apply_status;
1853}
1854
1855absl::StatusOr<layout_designer::DockTree> LayoutManager::CaptureDockTree(
1856 ImGuiID dockspace_id) const {
1857 if (!window_manager_) {
1858 return absl::FailedPreconditionError(
1859 "LayoutManager::CaptureDockTree: WorkspaceWindowManager not bound");
1860 }
1861 ImGuiDockNode* root_node = ImGui::DockBuilderGetNode(dockspace_id);
1862 if (!root_node) {
1863 return absl::NotFoundError(
1864 "LayoutManager::CaptureDockTree: no dock node at given id");
1865 }
1866
1867 std::unordered_map<std::string, PanelLookupEntry> by_window_name;
1868 for (const auto& [panel_id, desc] :
1869 window_manager_->GetAllWindowDescriptors()) {
1870 const std::string title = window_manager_->GetWorkspaceWindowName(desc);
1871 if (title.empty())
1872 continue;
1873 by_window_name.emplace(
1874 title, PanelLookupEntry{panel_id, desc.display_name, desc.icon});
1875 }
1876
1878 tree.root = CaptureDockNodeRecursive(root_node, by_window_name);
1879 if (!tree.root) {
1880 tree.root = layout_designer::DockNode::MakeLeaf({});
1881 }
1882 return tree;
1883}
1884
1885} // namespace editor
1886} // 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
void DockOpenPresetPanels(EditorType type)
WorkspaceWindowManager * window_manager_
void ProtectRestoredLayoutFromDefaultInitialization()
std::unordered_map< size_t, std::unordered_map< EditorType, std::unordered_set< std::string > > > lazy_default_dock_attempts_
void RebuildLayout(EditorType type, ImGuiID dockspace_id)
Force rebuild of layout for a specific editor type.
LazyDefaultDockState lazy_default_dock_state_
std::unordered_map< EditorType, bool > layouts_initialized_
void RefreshLazyDefaultDockingContext(EditorType type, ImGuiID dockspace_id)
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)
bool DockPresetPositionOnFirstOpen(size_t session_id, const std::string &panel_id, bool include_default_visible)
void MarkLazyDefaultDockAttempted(const std::string &panel_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 GetDefaultPreset(EditorType type)
Get the default layout preset for an editor type.
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 IsWindowOpen(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:110
#define LOG_WARN(category, format,...)
Definition log.h:108
#define LOG_INFO(category, format,...)
Definition log.h:106
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:116
std::unordered_set< std::string > attempted_panels
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