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