7#include <unordered_set>
10#include "absl/strings/str_cat.h"
16#include "imgui/imgui.h"
17#include "imgui/imgui_internal.h"
20#include <TargetConditionals.h>
41 for (
const auto& window_id : default_windows) {
45 LOG_INFO(
"LayoutManager",
"Showing %zu default windows for editor type %d",
46 default_windows.size(),
static_cast<int>(type));
51 for (
const auto& [key, value] : map) {
58 std::unordered_map<std::string, bool>* map) {
63 for (
const auto& [key, value] : obj.
items()) {
64 if (value.is_boolean()) {
65 (*map)[key] = value.get<
bool>();
71 std::unordered_map<std::string, bool>* windows) {
87 const std::string& project_key) {
89 if (!layouts_dir.ok()) {
94 std::filesystem::path projects_dir = *layouts_dir /
"projects";
96 return projects_dir / (project_key +
".json");
103 return *layouts_dir /
"layouts.json";
112 if (preset_name ==
"Minimal") {
116 if (preset_name ==
"Developer") {
120 if (preset_name ==
"Designer") {
124 if (preset_name ==
"Modder") {
128 if (preset_name ==
"Overworld Expert") {
132 if (preset_name ==
"Dungeon Expert") {
136 if (preset_name ==
"Testing") {
140 if (preset_name ==
"Audio") {
150 if (profile_id ==
"mapping") {
152 return "Dungeon Expert";
154 return "Overworld Expert";
156 if (profile_id ==
"code") {
159 if (profile_id ==
"debug") {
162 if (profile_id ==
"chat") {
171 ImGuiID dockspace_id) {
184 "Suppressed lazy preset init for editor type %d to preserve "
185 "startup-reapplied custom layout",
186 static_cast<int>(type));
193 "Layout for editor type %d already initialized, skipping",
194 static_cast<int>(type));
202 LOG_INFO(
"LayoutManager",
"Initializing layout for editor type %d",
203 static_cast<int>(type));
206 ImGui::DockBuilderRemoveNode(dockspace_id);
207 ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
209 ImVec2 dockspace_size = ImVec2(1280, 720);
210 if (
auto* viewport = ImGui::GetMainViewport()) {
211 dockspace_size = viewport->WorkSize;
215 if (last_size.x > 0.0f && last_size.y > 0.0f) {
216 dockspace_size = last_size;
218 ImGui::DockBuilderSetNodeSize(dockspace_id, dockspace_size);
227 ImGui::DockBuilderFinish(dockspace_id);
235 ImGuiDockNode* node = ImGui::DockBuilderGetNode(dockspace_id);
238 "Cannot rebuild layout: dockspace ID %u not found", dockspace_id);
242 LOG_INFO(
"LayoutManager",
"Forcing rebuild of layout for editor type %d",
243 static_cast<int>(type));
253 ImGui::DockBuilderRemoveNode(dockspace_id);
254 ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
255 ImVec2 dockspace_size = ImGui::GetMainViewport()->WorkSize;
257 if (last_size.x > 0.0f && last_size.y > 0.0f) {
258 dockspace_size = last_size;
260 ImGui::DockBuilderSetNodeSize(dockspace_id, dockspace_size);
269 ImGui::DockBuilderFinish(dockspace_id);
274 LOG_INFO(
"LayoutManager",
"Layout rebuild complete for editor type %d",
275 static_cast<int>(type));
283 float bottom = 0.22f;
285 float vertical_split = 0.52f;
291 case EditorType::kDungeon:
298 case EditorType::kOverworld:
304 case EditorType::kGraphics:
309 case EditorType::kPalette:
314 case EditorType::kSprite:
319 case EditorType::kScreen:
324 case EditorType::kMessage:
329 case EditorType::kAssembly:
334 case EditorType::kEmulator:
339 case EditorType::kAgent:
358 ImGuiID left_top = 0;
359 ImGuiID left_bottom = 0;
360 ImGuiID right_top = 0;
361 ImGuiID right_bottom = 0;
369 bool left_top =
false;
370 bool left_bottom =
false;
371 bool right_top =
false;
372 bool right_bottom =
false;
376 const std::string& panel_id) {
387 std::vector<std::pair<std::string, DockPosition>> docked_panels;
390 std::unordered_set<std::string> seen_panels;
393 auto append_ordered = [&](
const std::vector<std::string>& ordered_ids) {
394 for (
const auto& panel_id : ordered_ids) {
395 if (seen_panels.contains(panel_id) ||
403 docked_panels.emplace_back(it->first, it->second);
404 seen_panels.insert(panel_id);
411 std::vector<std::pair<std::string, DockPosition>> remaining_panels;
414 if (seen_panels.contains(panel_id) ||
418 remaining_panels.emplace_back(panel_id, position);
421 std::sort(remaining_panels.begin(), remaining_panels.end(),
422 [](
const auto& lhs,
const auto& rhs) {
423 if (lhs.second != rhs.second) {
424 return static_cast<int>(lhs.second) <
425 static_cast<int>(rhs.second);
427 return lhs.first < rhs.first;
429 docked_panels.insert(docked_panels.end(), remaining_panels.begin(),
430 remaining_panels.end());
432 return docked_panels;
436 const std::vector<std::pair<std::string, DockPosition>>& docked_panels) {
438 for (
const auto& [_, pos] : docked_panels) {
454 needs.left_top =
true;
458 needs.left_bottom =
true;
462 needs.right_top =
true;
466 needs.right_bottom =
true;
488 const std::vector<std::pair<std::string, DockPosition>>& docked_panels,
490 if (!window_manager) {
494 float preferred_width = 0.0f;
495 for (
const auto& [panel_id, position] : docked_panels) {
496 if (!matches_region(position)) {
500 preferred_width = std::max(preferred_width, panel->GetPreferredWidth());
503 return preferred_width;
509 const std::vector<std::pair<std::string, DockPosition>>& docked_panels) {
510 if (!cfg || !window_manager || viewport_width <= 0.0f) {
517 if (preferred_left > 0.0f) {
518 cfg->
left = std::clamp(preferred_left / viewport_width, 0.14f, 0.36f);
525 if (preferred_right > 0.0f) {
526 cfg->
right = std::clamp(preferred_right / viewport_width, 0.18f, 0.42f);
534 ids.
center = dockspace_id;
538 ids.left = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Left, cfg.
left,
539 nullptr, &ids.center);
542 ids.right = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Right,
543 cfg.
right,
nullptr, &ids.center);
546 ids.bottom = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Down,
547 cfg.
bottom,
nullptr, &ids.center);
550 ids.top = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Up, cfg.
top,
551 nullptr, &ids.center);
557 ids.left_bottom = ImGui::DockBuilderSplitNode(
558 ids.left, ImGuiDir_Down, cfg.
vertical_split,
nullptr, &ids.left_top);
566 ids.right_bottom = ImGui::DockBuilderSplitNode(
567 ids.right, ImGuiDir_Down, cfg.
vertical_split,
nullptr, &ids.right_top);
576 ImGuiID dockspace_id) {
577 auto preset = LayoutPresets::GetDefaultPreset(type);
579 if (!window_manager_) {
581 "WorkspaceWindowManager not available, skipping dock layout for "
583 static_cast<int>(type));
587 const size_t session_id =
588 window_manager_ ? window_manager_->GetActiveSessionId() : 0;
589 const auto docked_panels = CollectDockedPanels(preset);
594 const ImGuiViewport* viewport = ImGui::GetMainViewport();
595 const float viewport_width = viewport ? viewport->WorkSize.x : 0.0f;
596 const bool is_compact =
597#if defined(__APPLE__) && TARGET_OS_IOS == 1
599 static bool compact_mode =
true;
600 constexpr float kEnterCompactWidth = 900.0f;
601 constexpr float kExitCompactWidth = 940.0f;
602 if (viewport_width <= 0.0f) {
605 compact_mode = compact_mode ? (viewport_width < kExitCompactWidth)
606 : (viewport_width < kEnterCompactWidth);
610 (viewport_width > 0.0f && viewport_width < 900.0f);
613 DockSplitNeeds needs{};
614 DockSplitConfig cfg{};
616 needs = ComputeSplitNeeds(docked_panels);
617 cfg = DockSplitConfig::ForEditor(type);
618 ApplyPreferredSplitWidths(&cfg, needs, viewport_width, window_manager_,
622 DockNodeIds ids = BuildDockTree(dockspace_id, needs, cfg);
626 case DockPosition::Left:
627 if (ids.left_top || ids.left_bottom) {
629 return ids.left_top ? ids.left_top : ids.left_bottom;
631 return ids.left ? ids.left : ids.center;
632 case DockPosition::Right:
633 if (ids.right_top || ids.right_bottom) {
634 return ids.right_top ? ids.right_top : ids.right_bottom;
636 return ids.right ? ids.right : ids.center;
637 case DockPosition::Bottom:
638 return ids.bottom ? ids.bottom : ids.center;
639 case DockPosition::Top:
640 return ids.top ? ids.top : ids.center;
641 case DockPosition::LeftTop:
642 return ids.left_top ? ids.left_top : (ids.left ? ids.left : ids.center);
643 case DockPosition::LeftBottom:
644 return ids.left_bottom ? ids.left_bottom
645 : (ids.left ? ids.left : ids.center);
646 case DockPosition::RightTop:
647 return ids.right_top ? ids.right_top
648 : (ids.right ? ids.right : ids.center);
649 case DockPosition::RightBottom:
650 return ids.right_bottom ? ids.right_bottom
651 : (ids.right ? ids.right : ids.center);
652 case DockPosition::Center:
658 std::vector<ImGuiID> auto_hide_nodes;
661 for (
const auto& [panel_id, position] : docked_panels) {
664 ? window_manager_->GetWindowDescriptor(session_id, panel_id)
668 "Preset references window '%s' that is not registered (session "
670 panel_id.c_str(), session_id);
674 std::string window_title = window_manager_->GetWorkspaceWindowName(*desc);
675 if (window_title.empty()) {
677 "Cannot dock window '%s': missing window name (session %zu)",
678 panel_id.c_str(), session_id);
682 const ImGuiID dock_id = get_dock_id(position);
683 ImGui::DockBuilderDockWindow(window_title.c_str(), dock_id);
685 if (
WindowContent* panel = window_manager_->GetWindowContent(panel_id);
686 panel && panel->PreferAutoHideTabBar() &&
687 std::find(auto_hide_nodes.begin(), auto_hide_nodes.end(), dock_id) ==
688 auto_hide_nodes.end()) {
689 if (ImGuiDockNode* node = ImGui::DockBuilderGetNode(dock_id)) {
690 node->LocalFlags |= ImGuiDockNodeFlags_AutoHideTabBar;
691 auto_hide_nodes.push_back(dock_id);
698void LayoutManager::BuildOverworldLayout(ImGuiID dockspace_id) {
699 BuildLayoutFromPreset(EditorType::kOverworld, dockspace_id);
701void LayoutManager::BuildDungeonLayout(ImGuiID dockspace_id) {
702 BuildLayoutFromPreset(EditorType::kDungeon, dockspace_id);
704void LayoutManager::BuildGraphicsLayout(ImGuiID dockspace_id) {
705 BuildLayoutFromPreset(EditorType::kGraphics, dockspace_id);
707void LayoutManager::BuildPaletteLayout(ImGuiID dockspace_id) {
708 BuildLayoutFromPreset(EditorType::kPalette, dockspace_id);
710void LayoutManager::BuildScreenLayout(ImGuiID dockspace_id) {
711 BuildLayoutFromPreset(EditorType::kScreen, dockspace_id);
713void LayoutManager::BuildMusicLayout(ImGuiID dockspace_id) {
714 BuildLayoutFromPreset(EditorType::kMusic, dockspace_id);
716void LayoutManager::BuildSpriteLayout(ImGuiID dockspace_id) {
717 BuildLayoutFromPreset(EditorType::kSprite, dockspace_id);
719void LayoutManager::BuildMessageLayout(ImGuiID dockspace_id) {
720 BuildLayoutFromPreset(EditorType::kMessage, dockspace_id);
722void LayoutManager::BuildAssemblyLayout(ImGuiID dockspace_id) {
723 BuildLayoutFromPreset(EditorType::kAssembly, dockspace_id);
725void LayoutManager::BuildSettingsLayout(ImGuiID dockspace_id) {
726 BuildLayoutFromPreset(EditorType::kSettings, dockspace_id);
728void LayoutManager::BuildEmulatorLayout(ImGuiID dockspace_id) {
729 BuildLayoutFromPreset(EditorType::kEmulator, dockspace_id);
732void LayoutManager::SaveCurrentLayout(
const std::string& name,
bool persist) {
733 if (!window_manager_) {
735 "Cannot save layout '%s': WorkspaceWindowManager not available",
743 size_t session_id = window_manager_->GetActiveSessionId();
744 auto visibility_state = window_manager_->SerializeVisibilityState(session_id);
747 saved_layouts_[name] = visibility_state;
748 saved_pinned_layouts_[name] = window_manager_->SerializePinnedState();
749 layout_scopes_[name] = scope;
753 const char* ini_data = ImGui::SaveIniSettingsToMemory(&ini_size);
754 if (ini_data && ini_size > 0) {
755 saved_imgui_layouts_[name] = std::string(ini_data, ini_size);
759 SaveLayoutsToDisk(scope);
762 LOG_INFO(
"LayoutManager",
"Saved layout '%s' with %zu window states%s",
763 name.c_str(), visibility_state.size(),
764 persist ?
"" :
" (session-only)");
767void LayoutManager::LoadLayout(
const std::string& name) {
768 if (!window_manager_) {
770 "Cannot load layout '%s': WorkspaceWindowManager not available",
776 auto layout_it = saved_layouts_.find(name);
777 if (layout_it == saved_layouts_.end()) {
778 LOG_WARN(
"LayoutManager",
"Layout '%s' not found", name.c_str());
783 size_t session_id = window_manager_->GetActiveSessionId();
784 window_manager_->RestoreVisibilityState(session_id, layout_it->second,
787 auto pinned_it = saved_pinned_layouts_.find(name);
788 if (pinned_it != saved_pinned_layouts_.end()) {
789 window_manager_->RestorePinnedState(pinned_it->second);
793 auto imgui_it = saved_imgui_layouts_.find(name);
794 if (imgui_it != saved_imgui_layouts_.end() && !imgui_it->second.empty()) {
795 ImGui::LoadIniSettingsFromMemory(imgui_it->second.c_str(),
796 imgui_it->second.size());
799 LOG_INFO(
"LayoutManager",
"Loaded layout '%s'", name.c_str());
802void LayoutManager::CaptureTemporarySessionLayout(
size_t session_id) {
803 if (!window_manager_) {
807 temp_session_id_ = session_id;
808 temp_session_visibility_ =
809 window_manager_->SerializeVisibilityState(session_id);
810 temp_session_pinned_ = window_manager_->SerializePinnedState();
813 const char* ini_data = ImGui::SaveIniSettingsToMemory(&ini_size);
814 if (ini_data && ini_size > 0) {
815 temp_session_imgui_layout_ = std::string(ini_data, ini_size);
817 temp_session_imgui_layout_.clear();
820 has_temp_session_layout_ =
true;
823 "Captured temporary session layout for session %zu (%zu panel states)",
824 session_id, temp_session_visibility_.size());
827bool LayoutManager::RestoreTemporarySessionLayout(
size_t session_id,
828 bool clear_after_restore) {
829 if (!window_manager_ || !has_temp_session_layout_) {
833 if (session_id != temp_session_id_) {
835 "Session layout snapshot belongs to session %zu, requested %zu",
836 temp_session_id_, session_id);
840 window_manager_->RestoreVisibilityState(session_id, temp_session_visibility_,
842 window_manager_->RestorePinnedState(temp_session_pinned_);
844 if (!temp_session_imgui_layout_.empty()) {
845 ImGui::LoadIniSettingsFromMemory(temp_session_imgui_layout_.c_str(),
846 temp_session_imgui_layout_.size());
849 if (clear_after_restore) {
850 ClearTemporarySessionLayout();
853 LOG_INFO(
"LayoutManager",
"Restored temporary session layout for session %zu",
858void LayoutManager::ClearTemporarySessionLayout() {
859 has_temp_session_layout_ =
false;
860 temp_session_id_ = 0;
861 temp_session_visibility_.clear();
862 temp_session_pinned_.clear();
863 temp_session_imgui_layout_.clear();
866bool LayoutManager::SaveNamedSnapshot(
const std::string& name,
868 if (!window_manager_ || name.empty()) {
873 snapshot.
visibility = window_manager_->SerializeVisibilityState(session_id);
874 snapshot.
pinned = window_manager_->SerializePinnedState();
876 const char* ini_data = ImGui::SaveIniSettingsToMemory(&ini_size);
877 if (ini_data && ini_size > 0) {
880 named_snapshots_[name] = std::move(snapshot);
881 LOG_INFO(
"LayoutManager",
"Saved named snapshot '%s' for session %zu",
882 name.c_str(), session_id);
886bool LayoutManager::RestoreNamedSnapshot(
const std::string& name,
888 bool remove_after_restore) {
889 if (!window_manager_) {
892 auto it = named_snapshots_.find(name);
893 if (it == named_snapshots_.end()) {
900 window_manager_->RestoreVisibilityState(session_id, snapshot.
visibility,
902 window_manager_->RestorePinnedState(snapshot.
pinned);
904 ImGui::LoadIniSettingsFromMemory(snapshot.
imgui_layout.c_str(),
907 if (remove_after_restore) {
908 named_snapshots_.erase(it);
913bool LayoutManager::DeleteNamedSnapshot(
const std::string& name) {
914 auto it = named_snapshots_.find(name);
915 if (it == named_snapshots_.end()) {
918 named_snapshots_.erase(it);
922std::vector<std::string> LayoutManager::ListNamedSnapshots(
923 size_t session_id)
const {
924 std::vector<std::string> names;
925 names.reserve(named_snapshots_.size());
926 for (
const auto& [name, snapshot] : named_snapshots_) {
927 if (snapshot.session_id == session_id) {
928 names.push_back(name);
931 std::sort(names.begin(), names.end());
935bool LayoutManager::HasNamedSnapshot(
const std::string& name)
const {
936 return named_snapshots_.find(name) != named_snapshots_.end();
939bool LayoutManager::DeleteLayout(
const std::string& name) {
940 auto layout_it = saved_layouts_.find(name);
941 if (layout_it == saved_layouts_.end()) {
942 LOG_WARN(
"LayoutManager",
"Cannot delete layout '%s': not found",
948 auto scope_it = layout_scopes_.find(name);
949 if (scope_it != layout_scopes_.end()) {
950 scope = scope_it->second;
953 saved_layouts_.erase(layout_it);
954 saved_imgui_layouts_.erase(name);
955 saved_pinned_layouts_.erase(name);
956 layout_scopes_.erase(name);
958 SaveLayoutsToDisk(scope);
960 LOG_INFO(
"LayoutManager",
"Deleted layout '%s'", name.c_str());
964std::vector<LayoutProfile> LayoutManager::GetBuiltInProfiles() {
968 .description =
"Focused editing workspace with minimal panel noise",
969 .preset_name =
"Minimal",
970 .open_agent_chat =
false},
973 .description =
"Debugger-first workspace for tracing and memory tools",
974 .preset_name =
"Developer",
975 .open_agent_chat =
false},
978 .description =
"Map-centric layout for overworld or dungeon workflows",
979 .preset_name =
"Overworld Expert",
980 .open_agent_chat =
false},
983 .description =
"Collaboration-heavy layout with agent-centric tooling",
984 .preset_name =
"Modder",
985 .open_agent_chat =
true},
989bool LayoutManager::ApplyBuiltInProfile(
const std::string& profile_id,
993 if (!window_manager_) {
995 "Cannot apply profile '%s': WorkspaceWindowManager not available",
1002 for (
const auto& profile : GetBuiltInProfiles()) {
1003 if (profile.id == profile_id) {
1004 matched_profile = profile;
1011 LOG_WARN(
"LayoutManager",
"Unknown layout profile id: %s",
1012 profile_id.c_str());
1016 const std::string resolved_preset =
1017 ResolveProfilePresetName(profile_id, editor_type);
1018 if (!resolved_preset.empty()) {
1023 if (!TryGetNamedPreset(matched_profile.
preset_name, &preset)) {
1024 LOG_WARN(
"LayoutManager",
"Unable to resolve preset '%s' for profile '%s'",
1025 matched_profile.
preset_name.c_str(), profile_id.c_str());
1029 window_manager_->HideAllWindowsInSession(session_id);
1031 window_manager_->OpenWindow(session_id, panel_id);
1037 *out_profile = matched_profile;
1040 LOG_INFO(
"LayoutManager",
"Applied profile '%s' via preset '%s'",
1041 profile_id.c_str(), matched_profile.
preset_name.c_str());
1045std::vector<std::string> LayoutManager::GetSavedLayoutNames()
const {
1046 std::vector<std::string> names;
1047 names.reserve(saved_layouts_.size());
1048 for (
const auto& [name, _] : saved_layouts_) {
1049 names.push_back(name);
1054bool LayoutManager::HasLayout(
const std::string& name)
const {
1055 return saved_layouts_.find(name) != saved_layouts_.end();
1058void LayoutManager::LoadLayoutsFromDisk() {
1059 saved_layouts_.clear();
1060 saved_imgui_layouts_.clear();
1061 saved_pinned_layouts_.clear();
1062 layout_scopes_.clear();
1064 LoadLayoutsFromDiskInternal(LayoutScope::kGlobal,
false);
1065 if (!project_layout_key_.empty()) {
1066 LoadLayoutsFromDiskInternal(LayoutScope::kProject,
true);
1070void LayoutManager::SetProjectLayoutKey(
const std::string& key) {
1075 project_layout_key_ = key;
1076 LoadLayoutsFromDisk();
1079void LayoutManager::UseGlobalLayouts() {
1080 project_layout_key_.clear();
1081 LoadLayoutsFromDisk();
1085 return project_layout_key_.empty() ? LayoutScope::kGlobal
1086 : LayoutScope::kProject;
1089void LayoutManager::LoadLayoutsFromDiskInternal(
LayoutScope scope,
bool merge) {
1090 std::filesystem::path layout_path =
1091 GetLayoutsFilePath(scope, project_layout_key_);
1092 if (layout_path.empty()) {
1096 if (!std::filesystem::exists(layout_path)) {
1098 LOG_INFO(
"LayoutManager",
"No layouts file at %s",
1099 layout_path.string().c_str());
1105 std::ifstream file(layout_path);
1106 if (!file.is_open()) {
1107 LOG_WARN(
"LayoutManager",
"Failed to open layouts file: %s",
1108 layout_path.string().c_str());
1116 LOG_WARN(
"LayoutManager",
"Layouts file missing 'layouts' object: %s",
1117 layout_path.string().c_str());
1121 for (
auto& [name, entry] : root[
"layouts"].
items()) {
1122 if (!entry.is_object()) {
1126 std::unordered_map<std::string, bool> windows;
1127 std::unordered_map<std::string, bool> pinned;
1129 JsonToWindowMap(entry, &windows);
1130 if (entry.contains(
"pinned")) {
1131 JsonToBoolMap(entry[
"pinned"], &pinned);
1134 saved_layouts_[name] = std::move(windows);
1135 saved_pinned_layouts_[name] = std::move(pinned);
1136 layout_scopes_[name] = scope;
1138 if (entry.contains(
"imgui_ini") && entry[
"imgui_ini"].is_string()) {
1139 saved_imgui_layouts_[name] = entry[
"imgui_ini"].get<std::string>();
1141 saved_imgui_layouts_.erase(name);
1145 LOG_INFO(
"LayoutManager",
"Loaded layouts from %s",
1146 layout_path.string().c_str());
1147 }
catch (
const std::exception& e) {
1148 LOG_WARN(
"LayoutManager",
"Failed to load layouts: %s", e.what());
1153 std::filesystem::path layout_path =
1154 GetLayoutsFilePath(scope, project_layout_key_);
1155 if (layout_path.empty()) {
1156 LOG_WARN(
"LayoutManager",
"No layout path resolved for scope");
1163 LOG_WARN(
"LayoutManager",
"Failed to create layout directory: %s",
1164 status.ToString().c_str());
1170 root[
"version"] = 2;
1173 for (
const auto& [name, windows] : saved_layouts_) {
1174 auto scope_it = layout_scopes_.find(name);
1175 if (scope_it != layout_scopes_.end() && scope_it->second != scope) {
1180 entry[kWindowsKey] = BoolMapToJson(windows);
1182 auto pinned_it = saved_pinned_layouts_.find(name);
1183 if (pinned_it != saved_pinned_layouts_.end()) {
1184 entry[
"pinned"] = BoolMapToJson(pinned_it->second);
1187 auto imgui_it = saved_imgui_layouts_.find(name);
1188 if (imgui_it != saved_imgui_layouts_.end()) {
1189 entry[
"imgui_ini"] = imgui_it->second;
1192 root[
"layouts"][name] = entry;
1195 std::ofstream file(layout_path);
1196 if (!file.is_open()) {
1197 LOG_WARN(
"LayoutManager",
"Failed to open layouts file for write: %s",
1198 layout_path.string().c_str());
1201 file << root.
dump(2);
1203 }
catch (
const std::exception& e) {
1204 LOG_WARN(
"LayoutManager",
"Failed to save layouts: %s", e.what());
1209 layouts_initialized_[type] =
false;
1210 LOG_INFO(
"LayoutManager",
"Reset layout for editor type %d",
1211 static_cast<int>(type));
1215 auto it = layouts_initialized_.find(type);
1216 return it != layouts_initialized_.end() && it->second;
1220 layouts_initialized_[type] =
true;
1221 LOG_INFO(
"LayoutManager",
"Marked layout for editor type %d as initialized",
1222 static_cast<int>(type));
1225void LayoutManager::ClearInitializationFlags() {
1226 layouts_initialized_.clear();
1227 LOG_INFO(
"LayoutManager",
"Cleared all layout initialization flags");
1230std::string LayoutManager::GetWindowTitle(
const std::string& card_id)
const {
1231 if (!window_manager_) {
1235 const size_t session_id = window_manager_->GetActiveSessionId();
1236 return window_manager_->GetWorkspaceWindowName(session_id, card_id);
1245 return ImGuiDir_Left;
1247 return ImGuiDir_Right;
1251 return ImGuiDir_Down;
1253 return ImGuiDir_Left;
1260 if (
const auto* desc =
1273 }
else if (!panel.
icon.empty()) {
1284 ImGuiID target_id) {
1286 if (node.
type == NodeType::kLeaf) {
1287 for (
const auto& panel : node.
panels) {
1289 if (!title.empty()) {
1290 ImGui::DockBuilderDockWindow(title.c_str(), target_id);
1298 const float ratio = std::clamp(node.
split_ratio, 0.05f, 0.95f);
1299 ImGuiID id_other = 0;
1300 const ImGuiID id_at_dir =
1301 ImGui::DockBuilderSplitNode(target_id, dir, ratio,
nullptr, &id_other);
1317 const ImGuiDockNode* node,
1318 const std::unordered_map<std::string, PanelLookupEntry>& by_window_name) {
1320 return layout_designer::DockNode::MakeLeaf({});
1323 if (node->IsSplitNode()) {
1329 const bool vertical = node->SplitAxis == ImGuiAxis_Y;
1331 vertical ? layout_designer::SplitDirection::kUp
1332 : layout_designer::SplitDirection::kLeft;
1335 if (node->ChildNodes[0] && node->ChildNodes[1]) {
1336 if (vertical && node->Size.y > 0.0f) {
1337 ratio = node->ChildNodes[0]->Size.y / node->Size.y;
1338 }
else if (!vertical && node->Size.x > 0.0f) {
1339 ratio = node->ChildNodes[0]->Size.x / node->Size.x;
1342 ratio = std::clamp(ratio, 0.05f, 0.95f);
1343 return layout_designer::DockNode::MakeSplit(dir, ratio, std::move(child_a),
1344 std::move(child_b));
1348 std::vector<layout_designer::PanelEntry> panels;
1349 panels.reserve(
static_cast<size_t>(node->Windows.Size));
1350 int selected_index = -1;
1351 for (
int i = 0; i < node->Windows.Size; ++i) {
1352 const ImGuiWindow* w = node->Windows[i];
1355 auto it = by_window_name.find(std::string(w->Name));
1356 if (it == by_window_name.end())
1358 if (node->SelectedTabId != 0 && w->ID == node->SelectedTabId) {
1359 selected_index =
static_cast<int>(panels.size());
1362 {it->second.panel_id, it->second.display_name, it->second.icon});
1364 auto leaf = layout_designer::DockNode::MakeLeaf(std::move(panels));
1365 if (selected_index >= 0 &&
1366 selected_index <
static_cast<int>(leaf->panels.size())) {
1367 leaf->active_tab_index = selected_index;
1375 std::vector<std::string>* out) {
1376 if (node.
type == layout_designer::DockNode::Type::kLeaf) {
1377 for (
const auto& p : node.
panels) {
1378 out->push_back(p.panel_id);
1391 ImGuiID dockspace_id) {
1392 if (!window_manager_) {
1393 return absl::FailedPreconditionError(
1394 "LayoutManager::ApplyDockTree: WorkspaceWindowManager not bound");
1396 std::string validation_error;
1397 if (!tree.
Validate(&validation_error)) {
1398 return absl::InvalidArgumentError(
"LayoutManager::ApplyDockTree: " +
1412 std::vector<std::string> panel_ids;
1414 CollectPanelIdsInSubtree(*tree.
root, &panel_ids);
1416 const std::unordered_set<std::string> tree_id_set(panel_ids.begin(),
1418 const size_t session_id = window_manager_->GetActiveSessionId();
1419 for (
const auto&
id : panel_ids) {
1426 if (window_manager_->IsWindowOpen(session_id,
id))
1428 window_manager_->OpenWindow(session_id,
id);
1430 for (
const std::string&
id :
1431 window_manager_->GetWindowsInSession(session_id)) {
1432 if (tree_id_set.count(
id) > 0)
1434 if (window_manager_->IsWindowPinned(session_id,
id))
1441 if (!window_manager_->IsWindowOpen(session_id,
id))
1443 window_manager_->CloseWindow(
id);
1446 ImGui::DockBuilderRemoveNode(dockspace_id);
1447 ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
1449 ImVec2 size(1280.0f, 720.0f);
1450 if (
const ImGuiViewport* vp = ImGui::GetMainViewport()) {
1451 if (vp->WorkSize.x > 0.0f && vp->WorkSize.y > 0.0f) {
1452 size = vp->WorkSize;
1455 ImGui::DockBuilderSetNodeSize(dockspace_id, size);
1457 ApplyDockNodeRecursive(window_manager_, session_id, *tree.
root, dockspace_id);
1458 ImGui::DockBuilderFinish(dockspace_id);
1460 last_dockspace_id_ = dockspace_id;
1473 if (current_editor_type_ != EditorType::kUnknown) {
1474 MarkLayoutInitialized(current_editor_type_);
1476 startup_reapply_pending_protection_ =
true;
1479 return absl::OkStatus();
1482absl::Status LayoutManager::MaybeReapplyStartupLayout(
UserSettings* settings) {
1483 if (startup_layout_consumed_) {
1484 return absl::OkStatus();
1486 if (settings ==
nullptr) {
1487 startup_layout_consumed_ =
true;
1488 return absl::OkStatus();
1494 startup_layout_consumed_ =
true;
1495 return absl::OkStatus();
1498 if (main_dockspace_id_ == 0) {
1501 return absl::OkStatus();
1505 const auto it = named_layouts.find(name);
1506 if (it == named_layouts.end()) {
1507 startup_layout_consumed_ =
true;
1509 "LayoutManager: startup layout '%s' missing from "
1510 "named_layouts; falling through to default.",
1512 return absl::NotFoundError(absl::StrCat(
"startup layout \"", name,
1513 "\" not found in named_layouts"));
1516 nlohmann::json parsed;
1518 parsed = nlohmann::json::parse(it->second);
1519 }
catch (
const nlohmann::json::parse_error& e) {
1520 startup_layout_consumed_ =
true;
1521 util::logf(
"LayoutManager: startup layout '%s' JSON parse error: %s",
1522 name.c_str(), e.what());
1523 return absl::InvalidArgumentError(
1524 absl::StrCat(
"startup layout \"", name,
"\" parse error: ", e.what()));
1527 auto tree_or = layout_designer::DockTreeFromJson(parsed);
1528 if (!tree_or.ok()) {
1529 startup_layout_consumed_ =
true;
1530 util::logf(
"LayoutManager: startup layout '%s' failed to parse: %s",
1531 name.c_str(), std::string(tree_or.status().message()).c_str());
1532 return tree_or.status();
1535 std::string validation_error;
1536 if (!tree_or->Validate(&validation_error)) {
1537 startup_layout_consumed_ =
true;
1538 util::logf(
"LayoutManager: startup layout '%s' failed validation: %s",
1539 name.c_str(), validation_error.c_str());
1540 return absl::InvalidArgumentError(absl::StrCat(
1541 "startup layout \"", name,
"\" validation failed: ", validation_error));
1544 absl::Status apply_status = ApplyDockTree(*tree_or, main_dockspace_id_);
1545 startup_layout_consumed_ =
true;
1546 if (!apply_status.ok()) {
1547 util::logf(
"LayoutManager: startup layout '%s' ApplyDockTree failed: %s",
1548 name.c_str(), std::string(apply_status.message()).c_str());
1554 return apply_status;
1557absl::StatusOr<layout_designer::DockTree> LayoutManager::CaptureDockTree(
1558 ImGuiID dockspace_id)
const {
1559 if (!window_manager_) {
1560 return absl::FailedPreconditionError(
1561 "LayoutManager::CaptureDockTree: WorkspaceWindowManager not bound");
1563 ImGuiDockNode* root_node = ImGui::DockBuilderGetNode(dockspace_id);
1565 return absl::NotFoundError(
1566 "LayoutManager::CaptureDockTree: no dock node at given id");
1569 std::unordered_map<std::string, PanelLookupEntry> by_window_name;
1570 for (
const auto& [panel_id, desc] :
1571 window_manager_->GetAllWindowDescriptors()) {
1572 const std::string title = window_manager_->GetWorkspaceWindowName(desc);
1575 by_window_name.emplace(
1576 title, PanelLookupEntry{panel_id, desc.display_name, desc.icon});
1580 tree.
root = CaptureDockNodeRecursive(root_node, by_window_name);
1582 tree.
root = layout_designer::DockNode::MakeLeaf({});
std::string dump(int=-1, char=' ', bool=false, int=0) const
bool contains(const std::string &) const
EditorType current_editor_type_
ImGuiID last_dockspace_id_
WorkspaceWindowManager * window_manager_
bool startup_reapply_pending_protection_
void RebuildLayout(EditorType type, ImGuiID dockspace_id)
Force rebuild of layout for a specific editor type.
std::unordered_map< EditorType, bool > layouts_initialized_
bool IsLayoutInitialized(EditorType type) const
Check if a layout has been initialized for an editor.
void MarkLayoutInitialized(EditorType type)
Mark a layout as initialized.
void InitializeEditorLayout(EditorType type, ImGuiID dockspace_id)
Initialize the default layout for a specific editor type.
void BuildLayoutFromPreset(EditorType type, ImGuiID dockspace_id)
static PanelLayoutPreset GetLogicDebuggerPreset()
Get the "logic debugger" workspace preset (QA and debug focused)
static PanelLayoutPreset GetDungeonMasterPreset()
Get the "dungeon master" workspace preset.
static PanelLayoutPreset GetAudioEngineerPreset()
Get the "audio engineer" workspace preset (music focused)
static PanelLayoutPreset GetDesignerPreset()
Get the "designer" workspace preset (visual-focused)
static std::vector< std::string > GetDefaultWindows(EditorType type)
static PanelLayoutPreset GetOverworldArtistPreset()
Get the "overworld artist" workspace preset.
static PanelLayoutPreset GetModderPreset()
Get the "modder" workspace preset (full-featured)
static PanelLayoutPreset GetMinimalPreset()
Get the "minimal" workspace preset (minimal cards)
static PanelLayoutPreset GetDeveloperPreset()
Get the "developer" workspace preset (debug-focused)
Manages user preferences and settings persistence.
Base interface for all logical window content components.
Central registry for all editor cards with session awareness and dependency injection.
std::string GetWorkspaceWindowName(size_t session_id, const std::string &base_window_id) const
Resolve the exact ImGui window name for a panel by base ID.
const WindowDescriptor * GetWindowDescriptor(size_t session_id, const std::string &base_window_id) const
bool OpenWindow(size_t session_id, const std::string &base_window_id)
WindowContent * GetWindowContent(const std::string &window_id)
Get a WindowContent instance by ID.
static ImVec2 GetLastDockspaceSize()
#define LOG_ERROR(category, format,...)
#define LOG_WARN(category, format,...)
#define LOG_INFO(category, format,...)
bool IsLeftDockPosition(DockPosition pos)
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))
bool IsRightDockPosition(DockPosition pos)
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)
constexpr char kLegacyPanelsKey[]
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)
constexpr char kWindowsKey[]
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)
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
bool dock_only_default_visible_panels
std::unordered_map< std::string, std::string > named_layouts
std::string last_applied_layout_name
Metadata for a dockable editor window (formerly PanelInfo)
static DockSplitConfig ForEditor(EditorType type)
Represents a dock node in the layout tree.
std::unique_ptr< DockNode > child_a
std::unique_ptr< DockNode > child_b
SplitDirection split_direction
std::vector< PanelEntry > panels
std::unique_ptr< DockNode > root
bool Validate(std::string *error) const