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") {
175 ImGuiID dockspace_id) {
179 state.dockspace != dockspace_id ||
180 !ImGui::DockBuilderGetNode(state.dockspace) ||
181 !ImGui::DockBuilderGetNode(state.center)) {
187 state.enabled =
true;
188 state.editor_type = type;
189 state.session_id = session_id;
207 const auto dock_if_open = [&](
const std::string& panel_id) {
214 dock_if_open(panel_id);
217 dock_if_open(panel_id);
230 ImGuiID dockspace_id) {
244 "Suppressed lazy preset init for editor type %d to preserve "
245 "startup-reapplied custom layout",
246 static_cast<int>(type));
264 "Layout for editor type %d already initialized, skipping",
265 static_cast<int>(type));
269 LOG_INFO(
"LayoutManager",
"Initializing layout for editor type %d",
270 static_cast<int>(type));
273 ImGui::DockBuilderRemoveNode(dockspace_id);
274 ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
276 ImVec2 dockspace_size = ImVec2(1280, 720);
277 if (
auto* viewport = ImGui::GetMainViewport()) {
278 dockspace_size = viewport->WorkSize;
282 if (last_size.x > 0.0f && last_size.y > 0.0f) {
283 dockspace_size = last_size;
285 ImGui::DockBuilderSetNodeSize(dockspace_id, dockspace_size);
294 ImGui::DockBuilderFinish(dockspace_id);
306 ImGuiDockNode* node = ImGui::DockBuilderGetNode(dockspace_id);
309 "Cannot rebuild layout: dockspace ID %u not found", dockspace_id);
313 LOG_INFO(
"LayoutManager",
"Forcing rebuild of layout for editor type %d",
314 static_cast<int>(type));
324 ImGui::DockBuilderRemoveNode(dockspace_id);
325 ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
326 ImVec2 dockspace_size = ImGui::GetMainViewport()->WorkSize;
328 if (last_size.x > 0.0f && last_size.y > 0.0f) {
329 dockspace_size = last_size;
331 ImGui::DockBuilderSetNodeSize(dockspace_id, dockspace_size);
340 ImGui::DockBuilderFinish(dockspace_id);
347 LOG_INFO(
"LayoutManager",
"Layout rebuild complete for editor type %d",
348 static_cast<int>(type));
356 float bottom = 0.22f;
358 float vertical_split = 0.52f;
364 case EditorType::kDungeon:
371 case EditorType::kOverworld:
377 case EditorType::kGraphics:
382 case EditorType::kPalette:
387 case EditorType::kSprite:
392 case EditorType::kScreen:
397 case EditorType::kMessage:
402 case EditorType::kAssembly:
407 case EditorType::kEmulator:
412 case EditorType::kAgent:
431 ImGuiID left_top = 0;
432 ImGuiID left_bottom = 0;
433 ImGuiID right_top = 0;
434 ImGuiID right_bottom = 0;
442 bool left_top =
false;
443 bool left_bottom =
false;
444 bool right_top =
false;
445 bool right_bottom =
false;
449 const std::string& panel_id) {
460 std::vector<std::pair<std::string, DockPosition>> docked_panels;
463 std::unordered_set<std::string> seen_panels;
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) ||
476 docked_panels.emplace_back(it->first, it->second);
477 seen_panels.insert(panel_id);
484 std::vector<std::pair<std::string, DockPosition>> remaining_panels;
487 if (seen_panels.contains(panel_id) ||
491 remaining_panels.emplace_back(panel_id, position);
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);
500 return lhs.first < rhs.first;
502 docked_panels.insert(docked_panels.end(), remaining_panels.begin(),
503 remaining_panels.end());
505 return docked_panels;
509 const std::vector<std::pair<std::string, DockPosition>>& docked_panels) {
511 for (
const auto& [_, pos] : docked_panels) {
527 needs.left_top =
true;
531 needs.left_bottom =
true;
535 needs.right_top =
true;
539 needs.right_bottom =
true;
561 const std::vector<std::pair<std::string, DockPosition>>& docked_panels,
563 if (!window_manager) {
567 float preferred_width = 0.0f;
568 for (
const auto& [panel_id, position] : docked_panels) {
569 if (!matches_region(position)) {
573 preferred_width = std::max(preferred_width, panel->GetPreferredWidth());
576 return preferred_width;
582 const std::vector<std::pair<std::string, DockPosition>>& docked_panels) {
583 if (!cfg || !window_manager || viewport_width <= 0.0f) {
590 if (preferred_left > 0.0f) {
591 cfg->
left = std::clamp(preferred_left / viewport_width, 0.14f, 0.36f);
598 if (preferred_right > 0.0f) {
599 cfg->
right = std::clamp(preferred_right / viewport_width, 0.18f, 0.42f);
607 ids.
center = dockspace_id;
611 ids.left = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Left, cfg.
left,
612 nullptr, &ids.center);
615 ids.right = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Right,
616 cfg.
right,
nullptr, &ids.center);
619 ids.bottom = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Down,
620 cfg.
bottom,
nullptr, &ids.center);
623 ids.top = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Up, cfg.
top,
624 nullptr, &ids.center);
630 ids.left_bottom = ImGui::DockBuilderSplitNode(
631 ids.left, ImGuiDir_Down, cfg.
vertical_split,
nullptr, &ids.left_top);
639 ids.right_bottom = ImGui::DockBuilderSplitNode(
640 ids.right, ImGuiDir_Down, cfg.
vertical_split,
nullptr, &ids.right_top);
649 ImGuiID dockspace_id) {
650 DisableLazyDefaultDocking();
654 lazy_default_dock_attempts_.clear();
655 auto preset = LayoutPresets::GetDefaultPreset(type);
657 if (!window_manager_) {
659 "WorkspaceWindowManager not available, skipping dock layout for "
661 static_cast<int>(type));
665 const size_t session_id =
666 window_manager_ ? window_manager_->GetActiveSessionId() : 0;
667 const auto docked_panels = CollectDockedPanels(preset);
672 const ImGuiViewport* viewport = ImGui::GetMainViewport();
673 const float viewport_width = viewport ? viewport->WorkSize.x : 0.0f;
674 const bool is_compact =
675#if defined(__APPLE__) && TARGET_OS_IOS == 1
677 static bool compact_mode =
true;
678 constexpr float kEnterCompactWidth = 900.0f;
679 constexpr float kExitCompactWidth = 940.0f;
680 if (viewport_width <= 0.0f) {
683 compact_mode = compact_mode ? (viewport_width < kExitCompactWidth)
684 : (viewport_width < kEnterCompactWidth);
688 (viewport_width > 0.0f && viewport_width < 900.0f);
691 DockSplitNeeds needs{};
692 DockSplitConfig cfg{};
694 needs = ComputeSplitNeeds(docked_panels);
695 cfg = DockSplitConfig::ForEditor(type);
696 ApplyPreferredSplitWidths(&cfg, needs, viewport_width, window_manager_,
700 DockNodeIds ids = BuildDockTree(dockspace_id, needs, cfg);
705 auto& state = lazy_default_dock_state_;
706 state.enabled = preset.dock_only_default_visible_panels;
707 state.compact = is_compact;
708 state.editor_type = type;
709 state.session_id = session_id;
710 state.dockspace = dockspace_id;
711 state.center = ids.center;
712 state.left = ids.left;
713 state.right = ids.right;
714 state.bottom = ids.bottom;
716 state.left_top = ids.left_top;
717 state.left_bottom = ids.left_bottom;
718 state.right_top = ids.right_top;
719 state.right_bottom = ids.right_bottom;
720 state.left_ratio = cfg.left;
721 state.right_ratio = cfg.right;
722 state.bottom_ratio = cfg.bottom;
723 state.top_ratio = cfg.top;
724 state.vertical_split = cfg.vertical_split;
726 state.attempted_panels = lazy_default_dock_attempts_[session_id][type];
731 case DockPosition::Left:
732 if (ids.left_top || ids.left_bottom) {
734 return ids.left_top ? ids.left_top : ids.left_bottom;
736 return ids.left ? ids.left : ids.center;
737 case DockPosition::Right:
738 if (ids.right_top || ids.right_bottom) {
739 return ids.right_top ? ids.right_top : ids.right_bottom;
741 return ids.right ? ids.right : ids.center;
742 case DockPosition::Bottom:
743 return ids.bottom ? ids.bottom : ids.center;
744 case DockPosition::Top:
745 return ids.top ? ids.top : ids.center;
746 case DockPosition::LeftTop:
747 return ids.left_top ? ids.left_top : (ids.left ? ids.left : ids.center);
748 case DockPosition::LeftBottom:
749 return ids.left_bottom ? ids.left_bottom
750 : (ids.left ? ids.left : ids.center);
751 case DockPosition::RightTop:
752 return ids.right_top ? ids.right_top
753 : (ids.right ? ids.right : ids.center);
754 case DockPosition::RightBottom:
755 return ids.right_bottom ? ids.right_bottom
756 : (ids.right ? ids.right : ids.center);
757 case DockPosition::Center:
763 std::vector<ImGuiID> auto_hide_nodes;
766 for (
const auto& [panel_id, position] : docked_panels) {
769 ? window_manager_->GetWindowDescriptor(session_id, panel_id)
773 "Preset references window '%s' that is not registered (session "
775 panel_id.c_str(), session_id);
779 std::string window_title = window_manager_->GetWorkspaceWindowName(*desc);
780 if (window_title.empty()) {
782 "Cannot dock window '%s': missing window name (session %zu)",
783 panel_id.c_str(), session_id);
787 const ImGuiID dock_id = get_dock_id(position);
788 ImGui::DockBuilderDockWindow(window_title.c_str(), dock_id);
790 if (
WindowContent* panel = window_manager_->GetWindowContent(panel_id);
791 panel && panel->PreferAutoHideTabBar() &&
792 std::find(auto_hide_nodes.begin(), auto_hide_nodes.end(), dock_id) ==
793 auto_hide_nodes.end()) {
794 if (ImGuiDockNode* node = ImGui::DockBuilderGetNode(dock_id)) {
795 node->LocalFlags |= ImGuiDockNodeFlags_AutoHideTabBar;
796 auto_hide_nodes.push_back(dock_id);
802bool LayoutManager::DockDefaultPositionOnFirstOpen(
803 size_t session_id,
const std::string& panel_id) {
804 return DockPresetPositionOnFirstOpen(session_id, panel_id,
808bool LayoutManager::DockPresetPositionOnPanelOpen(
size_t session_id,
809 const std::string& panel_id) {
810 return DockPresetPositionOnFirstOpen(session_id, panel_id,
814bool LayoutManager::DockPresetPositionOnFirstOpen(
815 size_t session_id,
const std::string& panel_id,
816 bool include_default_visible) {
817 auto& state = lazy_default_dock_state_;
818 if (!state.enabled || !window_manager_ || panel_id.empty() ||
819 session_id != state.session_id ||
820 !ImGui::DockBuilderGetNode(state.dockspace)) {
825 LayoutPresets::GetDefaultPreset(state.editor_type);
826 const bool is_default_visible =
831 (!include_default_visible && is_default_visible)) {
837 state.attempted_panels.contains(panel_id)) {
842 window_manager_->GetWindowDescriptor(session_id, panel_id);
846 const std::string window_title =
847 window_manager_->GetWorkspaceWindowName(*desc);
848 if (window_title.empty()) {
854 if (ImGuiWindow* window = ImGui::FindWindowByName(window_title.c_str());
855 window && window->DockId != 0 &&
856 ImGui::DockBuilderGetNode(window->DockId)) {
857 MarkLazyDefaultDockAttempted(panel_id);
860 if (ImGuiWindowSettings* settings =
861 ImGui::FindWindowSettingsByID(ImHashStr(window_title.c_str()));
862 settings && settings->DockId != 0 &&
863 ImGui::DockBuilderGetNode(settings->DockId)) {
864 MarkLazyDefaultDockAttempted(panel_id);
868 auto split_center = [&](ImGuiDir direction,
float ratio,
869 ImGuiID* region) -> ImGuiID {
870 if (*region != 0 && ImGui::DockBuilderGetNode(*region)) {
873 if (state.center == 0 || !ImGui::DockBuilderGetNode(state.center)) {
876 *region = ImGui::DockBuilderSplitNode(state.center, direction, ratio,
877 nullptr, &state.center);
881 auto resolve_vertical_region = [&](
bool right_side,
882 bool want_bottom) -> ImGuiID {
883 ImGuiID& region = right_side ? state.right : state.left;
884 ImGuiID& top = right_side ? state.right_top : state.left_top;
885 ImGuiID& bottom = right_side ? state.right_bottom : state.left_bottom;
886 const ImGuiDir side_direction = right_side ? ImGuiDir_Right : ImGuiDir_Left;
887 const float side_ratio = right_side ? state.right_ratio : state.left_ratio;
888 if (!split_center(side_direction, side_ratio, ®ion)) {
893 if (bottom != 0 && ImGui::DockBuilderGetNode(bottom)) {
901 bottom = ImGui::DockBuilderSplitNode(
902 region, ImGuiDir_Down, state.vertical_split,
nullptr, &new_top);
907 if (top != 0 && ImGui::DockBuilderGetNode(top)) {
914 ImGuiID new_bottom = 0;
915 top = ImGui::DockBuilderSplitNode(
916 region, ImGuiDir_Up, 1.0f - state.vertical_split,
nullptr, &new_bottom);
923 target = state.center;
925 switch (position_it->second) {
926 case DockPosition::Left:
927 case DockPosition::LeftTop:
928 target = resolve_vertical_region(
false,
false);
930 case DockPosition::LeftBottom:
931 target = resolve_vertical_region(
false,
true);
933 case DockPosition::Right:
934 case DockPosition::RightTop:
935 target = resolve_vertical_region(
true,
false);
937 case DockPosition::RightBottom:
938 target = resolve_vertical_region(
true,
true);
940 case DockPosition::Bottom:
941 target = split_center(ImGuiDir_Down, state.bottom_ratio, &state.bottom);
943 case DockPosition::Top:
944 target = split_center(ImGuiDir_Up, state.top_ratio, &state.top);
946 case DockPosition::Center:
948 target = state.center;
953 if (target == 0 || !ImGui::DockBuilderGetNode(target)) {
957 MarkLazyDefaultDockAttempted(panel_id);
958 ImGui::DockBuilderDockWindow(window_title.c_str(), target);
959 if (
WindowContent* panel = window_manager_->GetWindowContent(panel_id);
960 panel && panel->PreferAutoHideTabBar()) {
961 if (ImGuiDockNode* node = ImGui::DockBuilderGetNode(target)) {
962 node->LocalFlags |= ImGuiDockNodeFlags_AutoHideTabBar;
965 ImGui::DockBuilderFinish(state.dockspace);
970void LayoutManager::BuildOverworldLayout(ImGuiID dockspace_id) {
971 BuildLayoutFromPreset(EditorType::kOverworld, dockspace_id);
973void LayoutManager::BuildDungeonLayout(ImGuiID dockspace_id) {
974 BuildLayoutFromPreset(EditorType::kDungeon, dockspace_id);
976void LayoutManager::BuildGraphicsLayout(ImGuiID dockspace_id) {
977 BuildLayoutFromPreset(EditorType::kGraphics, dockspace_id);
979void LayoutManager::BuildPaletteLayout(ImGuiID dockspace_id) {
980 BuildLayoutFromPreset(EditorType::kPalette, dockspace_id);
982void LayoutManager::BuildScreenLayout(ImGuiID dockspace_id) {
983 BuildLayoutFromPreset(EditorType::kScreen, dockspace_id);
985void LayoutManager::BuildMusicLayout(ImGuiID dockspace_id) {
986 BuildLayoutFromPreset(EditorType::kMusic, dockspace_id);
988void LayoutManager::BuildSpriteLayout(ImGuiID dockspace_id) {
989 BuildLayoutFromPreset(EditorType::kSprite, dockspace_id);
991void LayoutManager::BuildMessageLayout(ImGuiID dockspace_id) {
992 BuildLayoutFromPreset(EditorType::kMessage, dockspace_id);
994void LayoutManager::BuildAssemblyLayout(ImGuiID dockspace_id) {
995 BuildLayoutFromPreset(EditorType::kAssembly, dockspace_id);
997void LayoutManager::BuildSettingsLayout(ImGuiID dockspace_id) {
998 BuildLayoutFromPreset(EditorType::kSettings, dockspace_id);
1000void LayoutManager::BuildEmulatorLayout(ImGuiID dockspace_id) {
1001 BuildLayoutFromPreset(EditorType::kEmulator, dockspace_id);
1004void LayoutManager::SaveCurrentLayout(
const std::string& name,
bool persist) {
1005 if (!window_manager_) {
1007 "Cannot save layout '%s': WorkspaceWindowManager not available",
1015 size_t session_id = window_manager_->GetActiveSessionId();
1016 auto visibility_state = window_manager_->SerializeVisibilityState(session_id);
1019 saved_layouts_[name] = visibility_state;
1020 saved_pinned_layouts_[name] = window_manager_->SerializePinnedState();
1021 layout_scopes_[name] = scope;
1024 size_t ini_size = 0;
1025 const char* ini_data = ImGui::SaveIniSettingsToMemory(&ini_size);
1026 if (ini_data && ini_size > 0) {
1027 saved_imgui_layouts_[name] = std::string(ini_data, ini_size);
1031 SaveLayoutsToDisk(scope);
1034 LOG_INFO(
"LayoutManager",
"Saved layout '%s' with %zu window states%s",
1035 name.c_str(), visibility_state.size(),
1036 persist ?
"" :
" (session-only)");
1039void LayoutManager::LoadLayout(
const std::string& name) {
1040 if (!window_manager_) {
1042 "Cannot load layout '%s': WorkspaceWindowManager not available",
1048 auto layout_it = saved_layouts_.find(name);
1049 if (layout_it == saved_layouts_.end()) {
1050 LOG_WARN(
"LayoutManager",
"Layout '%s' not found", name.c_str());
1054 const auto imgui_it = saved_imgui_layouts_.find(name);
1055 const bool has_imgui_layout =
1056 imgui_it != saved_imgui_layouts_.end() && !imgui_it->second.empty();
1059 if (has_imgui_layout) {
1060 DisableLazyDefaultDocking();
1064 size_t session_id = window_manager_->GetActiveSessionId();
1065 window_manager_->RestoreVisibilityState(session_id, layout_it->second,
1068 auto pinned_it = saved_pinned_layouts_.find(name);
1069 if (pinned_it != saved_pinned_layouts_.end()) {
1070 window_manager_->RestorePinnedState(pinned_it->second);
1074 if (has_imgui_layout) {
1075 ImGui::LoadIniSettingsFromMemory(imgui_it->second.c_str(),
1076 imgui_it->second.size());
1077 ProtectRestoredLayoutFromDefaultInitialization();
1080 LOG_INFO(
"LayoutManager",
"Loaded layout '%s'", name.c_str());
1083void LayoutManager::CaptureTemporarySessionLayout(
size_t session_id) {
1084 if (!window_manager_) {
1088 temp_session_id_ = session_id;
1089 temp_session_visibility_ =
1090 window_manager_->SerializeVisibilityState(session_id);
1091 temp_session_pinned_ = window_manager_->SerializePinnedState();
1093 size_t ini_size = 0;
1094 const char* ini_data = ImGui::SaveIniSettingsToMemory(&ini_size);
1095 if (ini_data && ini_size > 0) {
1096 temp_session_imgui_layout_ = std::string(ini_data, ini_size);
1098 temp_session_imgui_layout_.clear();
1101 has_temp_session_layout_ =
true;
1104 "Captured temporary session layout for session %zu (%zu panel states)",
1105 session_id, temp_session_visibility_.size());
1108bool LayoutManager::RestoreTemporarySessionLayout(
size_t session_id,
1109 bool clear_after_restore) {
1110 if (!window_manager_ || !has_temp_session_layout_) {
1114 if (session_id != temp_session_id_) {
1116 "Session layout snapshot belongs to session %zu, requested %zu",
1117 temp_session_id_, session_id);
1121 const bool has_imgui_layout = !temp_session_imgui_layout_.empty();
1122 if (has_imgui_layout) {
1123 DisableLazyDefaultDocking();
1126 window_manager_->RestoreVisibilityState(session_id, temp_session_visibility_,
1128 window_manager_->RestorePinnedState(temp_session_pinned_);
1130 if (has_imgui_layout) {
1131 ImGui::LoadIniSettingsFromMemory(temp_session_imgui_layout_.c_str(),
1132 temp_session_imgui_layout_.size());
1133 ProtectRestoredLayoutFromDefaultInitialization();
1136 if (clear_after_restore) {
1137 ClearTemporarySessionLayout();
1140 LOG_INFO(
"LayoutManager",
"Restored temporary session layout for session %zu",
1145void LayoutManager::ClearTemporarySessionLayout() {
1146 has_temp_session_layout_ =
false;
1147 temp_session_id_ = 0;
1148 temp_session_visibility_.clear();
1149 temp_session_pinned_.clear();
1150 temp_session_imgui_layout_.clear();
1153bool LayoutManager::SaveNamedSnapshot(
const std::string& name,
1154 size_t session_id) {
1155 if (!window_manager_ || name.empty()) {
1160 snapshot.
visibility = window_manager_->SerializeVisibilityState(session_id);
1161 snapshot.
pinned = window_manager_->SerializePinnedState();
1162 size_t ini_size = 0;
1163 const char* ini_data = ImGui::SaveIniSettingsToMemory(&ini_size);
1164 if (ini_data && ini_size > 0) {
1167 named_snapshots_[name] = std::move(snapshot);
1168 LOG_INFO(
"LayoutManager",
"Saved named snapshot '%s' for session %zu",
1169 name.c_str(), session_id);
1173bool LayoutManager::RestoreNamedSnapshot(
const std::string& name,
1175 bool remove_after_restore) {
1176 if (!window_manager_) {
1179 auto it = named_snapshots_.find(name);
1180 if (it == named_snapshots_.end()) {
1187 const bool has_imgui_layout = !snapshot.
imgui_layout.empty();
1188 if (has_imgui_layout) {
1189 DisableLazyDefaultDocking();
1191 window_manager_->RestoreVisibilityState(session_id, snapshot.
visibility,
1193 window_manager_->RestorePinnedState(snapshot.
pinned);
1194 if (has_imgui_layout) {
1195 ImGui::LoadIniSettingsFromMemory(snapshot.
imgui_layout.c_str(),
1197 ProtectRestoredLayoutFromDefaultInitialization();
1199 if (remove_after_restore) {
1200 named_snapshots_.erase(it);
1205bool LayoutManager::DeleteNamedSnapshot(
const std::string& name) {
1206 auto it = named_snapshots_.find(name);
1207 if (it == named_snapshots_.end()) {
1210 named_snapshots_.erase(it);
1214std::vector<std::string> LayoutManager::ListNamedSnapshots(
1215 size_t session_id)
const {
1216 std::vector<std::string> names;
1217 names.reserve(named_snapshots_.size());
1218 for (
const auto& [name, snapshot] : named_snapshots_) {
1219 if (snapshot.session_id == session_id) {
1220 names.push_back(name);
1223 std::sort(names.begin(), names.end());
1227bool LayoutManager::HasNamedSnapshot(
const std::string& name)
const {
1228 return named_snapshots_.find(name) != named_snapshots_.end();
1231bool LayoutManager::DeleteLayout(
const std::string& name) {
1232 auto layout_it = saved_layouts_.find(name);
1233 if (layout_it == saved_layouts_.end()) {
1234 LOG_WARN(
"LayoutManager",
"Cannot delete layout '%s': not found",
1240 auto scope_it = layout_scopes_.find(name);
1241 if (scope_it != layout_scopes_.end()) {
1242 scope = scope_it->second;
1245 saved_layouts_.erase(layout_it);
1246 saved_imgui_layouts_.erase(name);
1247 saved_pinned_layouts_.erase(name);
1248 layout_scopes_.erase(name);
1250 SaveLayoutsToDisk(scope);
1252 LOG_INFO(
"LayoutManager",
"Deleted layout '%s'", name.c_str());
1256std::vector<LayoutProfile> LayoutManager::GetBuiltInProfiles() {
1260 .description =
"Focused editing workspace with minimal panel noise",
1261 .preset_name =
"Minimal",
1262 .open_agent_chat =
false},
1265 .description =
"Debugger-first workspace for tracing and memory tools",
1266 .preset_name =
"Developer",
1267 .open_agent_chat =
false},
1270 .description =
"Map-centric layout for overworld or dungeon workflows",
1271 .preset_name =
"Overworld Expert",
1272 .open_agent_chat =
false},
1275 .description =
"Collaboration-heavy layout with agent-centric tooling",
1276 .preset_name =
"Modder",
1277 .open_agent_chat =
true},
1281bool LayoutManager::ApplyBuiltInProfile(
const std::string& profile_id,
1285 if (!window_manager_) {
1287 "Cannot apply profile '%s': WorkspaceWindowManager not available",
1288 profile_id.c_str());
1294 for (
const auto& profile : GetBuiltInProfiles()) {
1295 if (profile.id == profile_id) {
1296 matched_profile = profile;
1303 LOG_WARN(
"LayoutManager",
"Unknown layout profile id: %s",
1304 profile_id.c_str());
1308 const std::string resolved_preset =
1309 ResolveProfilePresetName(profile_id, editor_type);
1310 if (!resolved_preset.empty()) {
1315 if (!TryGetNamedPreset(matched_profile.
preset_name, &preset)) {
1316 LOG_WARN(
"LayoutManager",
"Unable to resolve preset '%s' for profile '%s'",
1317 matched_profile.
preset_name.c_str(), profile_id.c_str());
1323 DisableLazyDefaultDocking();
1325 window_manager_->HideAllWindowsInSession(session_id);
1327 window_manager_->OpenWindow(session_id, panel_id);
1333 *out_profile = matched_profile;
1336 LOG_INFO(
"LayoutManager",
"Applied profile '%s' via preset '%s'",
1337 profile_id.c_str(), matched_profile.
preset_name.c_str());
1341std::vector<std::string> LayoutManager::GetSavedLayoutNames()
const {
1342 std::vector<std::string> names;
1343 names.reserve(saved_layouts_.size());
1344 for (
const auto& [name, _] : saved_layouts_) {
1345 names.push_back(name);
1350bool LayoutManager::HasLayout(
const std::string& name)
const {
1351 return saved_layouts_.find(name) != saved_layouts_.end();
1354void LayoutManager::LoadLayoutsFromDisk() {
1355 saved_layouts_.clear();
1356 saved_imgui_layouts_.clear();
1357 saved_pinned_layouts_.clear();
1358 layout_scopes_.clear();
1360 LoadLayoutsFromDiskInternal(LayoutScope::kGlobal,
false);
1361 if (!project_layout_key_.empty()) {
1362 LoadLayoutsFromDiskInternal(LayoutScope::kProject,
true);
1366void LayoutManager::SetProjectLayoutKey(
const std::string& key) {
1371 project_layout_key_ = key;
1372 LoadLayoutsFromDisk();
1375void LayoutManager::UseGlobalLayouts() {
1376 project_layout_key_.clear();
1377 LoadLayoutsFromDisk();
1381 return project_layout_key_.empty() ? LayoutScope::kGlobal
1382 : LayoutScope::kProject;
1385void LayoutManager::LoadLayoutsFromDiskInternal(
LayoutScope scope,
bool merge) {
1386 std::filesystem::path layout_path =
1387 GetLayoutsFilePath(scope, project_layout_key_);
1388 if (layout_path.empty()) {
1392 if (!std::filesystem::exists(layout_path)) {
1394 LOG_INFO(
"LayoutManager",
"No layouts file at %s",
1395 layout_path.string().c_str());
1401 std::ifstream file(layout_path);
1402 if (!file.is_open()) {
1403 LOG_WARN(
"LayoutManager",
"Failed to open layouts file: %s",
1404 layout_path.string().c_str());
1412 LOG_WARN(
"LayoutManager",
"Layouts file missing 'layouts' object: %s",
1413 layout_path.string().c_str());
1417 for (
auto& [name, entry] : root[
"layouts"].
items()) {
1418 if (!entry.is_object()) {
1422 std::unordered_map<std::string, bool> windows;
1423 std::unordered_map<std::string, bool> pinned;
1425 JsonToWindowMap(entry, &windows);
1426 if (entry.contains(
"pinned")) {
1427 JsonToBoolMap(entry[
"pinned"], &pinned);
1430 saved_layouts_[name] = std::move(windows);
1431 saved_pinned_layouts_[name] = std::move(pinned);
1432 layout_scopes_[name] = scope;
1434 if (entry.contains(
"imgui_ini") && entry[
"imgui_ini"].is_string()) {
1435 saved_imgui_layouts_[name] = entry[
"imgui_ini"].get<std::string>();
1437 saved_imgui_layouts_.erase(name);
1441 LOG_INFO(
"LayoutManager",
"Loaded layouts from %s",
1442 layout_path.string().c_str());
1443 }
catch (
const std::exception& e) {
1444 LOG_WARN(
"LayoutManager",
"Failed to load layouts: %s", e.what());
1449 std::filesystem::path layout_path =
1450 GetLayoutsFilePath(scope, project_layout_key_);
1451 if (layout_path.empty()) {
1452 LOG_WARN(
"LayoutManager",
"No layout path resolved for scope");
1459 LOG_WARN(
"LayoutManager",
"Failed to create layout directory: %s",
1460 status.ToString().c_str());
1466 root[
"version"] = 2;
1469 for (
const auto& [name, windows] : saved_layouts_) {
1470 auto scope_it = layout_scopes_.find(name);
1471 if (scope_it != layout_scopes_.end() && scope_it->second != scope) {
1476 entry[kWindowsKey] = BoolMapToJson(windows);
1478 auto pinned_it = saved_pinned_layouts_.find(name);
1479 if (pinned_it != saved_pinned_layouts_.end()) {
1480 entry[
"pinned"] = BoolMapToJson(pinned_it->second);
1483 auto imgui_it = saved_imgui_layouts_.find(name);
1484 if (imgui_it != saved_imgui_layouts_.end()) {
1485 entry[
"imgui_ini"] = imgui_it->second;
1488 root[
"layouts"][name] = entry;
1491 std::ofstream file(layout_path);
1492 if (!file.is_open()) {
1493 LOG_WARN(
"LayoutManager",
"Failed to open layouts file for write: %s",
1494 layout_path.string().c_str());
1497 file << root.
dump(2);
1499 }
catch (
const std::exception& e) {
1500 LOG_WARN(
"LayoutManager",
"Failed to save layouts: %s", e.what());
1505 DisableLazyDefaultDocking();
1506 layouts_initialized_[type] =
false;
1507 LOG_INFO(
"LayoutManager",
"Reset layout for editor type %d",
1508 static_cast<int>(type));
1512 auto it = layouts_initialized_.find(type);
1513 return it != layouts_initialized_.end() && it->second;
1517 layouts_initialized_[type] =
true;
1518 LOG_INFO(
"LayoutManager",
"Marked layout for editor type %d as initialized",
1519 static_cast<int>(type));
1522void LayoutManager::ClearInitializationFlags() {
1523 DisableLazyDefaultDocking();
1524 layouts_initialized_.clear();
1525 LOG_INFO(
"LayoutManager",
"Cleared all layout initialization flags");
1528std::string LayoutManager::GetWindowTitle(
const std::string& card_id)
const {
1529 if (!window_manager_) {
1533 const size_t session_id = window_manager_->GetActiveSessionId();
1534 return window_manager_->GetWorkspaceWindowName(session_id, card_id);
1543 return ImGuiDir_Left;
1545 return ImGuiDir_Right;
1549 return ImGuiDir_Down;
1551 return ImGuiDir_Left;
1558 if (
const auto* desc =
1571 }
else if (!panel.
icon.empty()) {
1582 ImGuiID target_id) {
1584 if (node.
type == NodeType::kLeaf) {
1585 for (
const auto& panel : node.
panels) {
1587 if (!title.empty()) {
1588 ImGui::DockBuilderDockWindow(title.c_str(), target_id);
1596 const float ratio = std::clamp(node.
split_ratio, 0.05f, 0.95f);
1597 ImGuiID id_other = 0;
1598 const ImGuiID id_at_dir =
1599 ImGui::DockBuilderSplitNode(target_id, dir, ratio,
nullptr, &id_other);
1615 const ImGuiDockNode* node,
1616 const std::unordered_map<std::string, PanelLookupEntry>& by_window_name) {
1618 return layout_designer::DockNode::MakeLeaf({});
1621 if (node->IsSplitNode()) {
1627 const bool vertical = node->SplitAxis == ImGuiAxis_Y;
1629 vertical ? layout_designer::SplitDirection::kUp
1630 : layout_designer::SplitDirection::kLeft;
1633 if (node->ChildNodes[0] && node->ChildNodes[1]) {
1634 if (vertical && node->Size.y > 0.0f) {
1635 ratio = node->ChildNodes[0]->Size.y / node->Size.y;
1636 }
else if (!vertical && node->Size.x > 0.0f) {
1637 ratio = node->ChildNodes[0]->Size.x / node->Size.x;
1640 ratio = std::clamp(ratio, 0.05f, 0.95f);
1641 return layout_designer::DockNode::MakeSplit(dir, ratio, std::move(child_a),
1642 std::move(child_b));
1646 std::vector<layout_designer::PanelEntry> panels;
1647 panels.reserve(
static_cast<size_t>(node->Windows.Size));
1648 int selected_index = -1;
1649 for (
int i = 0; i < node->Windows.Size; ++i) {
1650 const ImGuiWindow* w = node->Windows[i];
1653 auto it = by_window_name.find(std::string(w->Name));
1654 if (it == by_window_name.end())
1656 if (node->SelectedTabId != 0 && w->ID == node->SelectedTabId) {
1657 selected_index =
static_cast<int>(panels.size());
1660 {it->second.panel_id, it->second.display_name, it->second.icon});
1662 auto leaf = layout_designer::DockNode::MakeLeaf(std::move(panels));
1663 if (selected_index >= 0 &&
1664 selected_index <
static_cast<int>(leaf->panels.size())) {
1665 leaf->active_tab_index = selected_index;
1673 std::vector<std::string>* out) {
1674 if (node.
type == layout_designer::DockNode::Type::kLeaf) {
1675 for (
const auto& p : node.
panels) {
1676 out->push_back(p.panel_id);
1689 ImGuiID dockspace_id) {
1690 if (!window_manager_) {
1691 return absl::FailedPreconditionError(
1692 "LayoutManager::ApplyDockTree: WorkspaceWindowManager not bound");
1694 std::string validation_error;
1695 if (!tree.
Validate(&validation_error)) {
1696 return absl::InvalidArgumentError(
"LayoutManager::ApplyDockTree: " +
1702 DisableLazyDefaultDocking();
1714 std::vector<std::string> panel_ids;
1716 CollectPanelIdsInSubtree(*tree.
root, &panel_ids);
1718 const std::unordered_set<std::string> tree_id_set(panel_ids.begin(),
1720 const size_t session_id = window_manager_->GetActiveSessionId();
1721 for (
const auto&
id : panel_ids) {
1728 if (window_manager_->IsWindowOpen(session_id,
id))
1730 window_manager_->OpenWindow(session_id,
id);
1732 for (
const std::string&
id :
1733 window_manager_->GetWindowsInSession(session_id)) {
1734 if (tree_id_set.count(
id) > 0)
1736 if (window_manager_->IsWindowPinned(session_id,
id))
1743 if (!window_manager_->IsWindowOpen(session_id,
id))
1745 window_manager_->CloseWindow(
id);
1748 ImGui::DockBuilderRemoveNode(dockspace_id);
1749 ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
1751 ImVec2 size(1280.0f, 720.0f);
1752 if (
const ImGuiViewport* vp = ImGui::GetMainViewport()) {
1753 if (vp->WorkSize.x > 0.0f && vp->WorkSize.y > 0.0f) {
1754 size = vp->WorkSize;
1757 ImGui::DockBuilderSetNodeSize(dockspace_id, size);
1759 ApplyDockNodeRecursive(window_manager_, session_id, *tree.
root, dockspace_id);
1760 ImGui::DockBuilderFinish(dockspace_id);
1762 last_dockspace_id_ = dockspace_id;
1775 ProtectRestoredLayoutFromDefaultInitialization();
1777 return absl::OkStatus();
1780absl::Status LayoutManager::MaybeReapplyStartupLayout(
UserSettings* settings) {
1781 if (startup_layout_consumed_) {
1782 return absl::OkStatus();
1784 if (settings ==
nullptr) {
1785 startup_layout_consumed_ =
true;
1786 return absl::OkStatus();
1792 startup_layout_consumed_ =
true;
1793 return absl::OkStatus();
1796 if (main_dockspace_id_ == 0) {
1799 return absl::OkStatus();
1803 const auto it = named_layouts.find(name);
1804 if (it == named_layouts.end()) {
1805 startup_layout_consumed_ =
true;
1807 "LayoutManager: startup layout '%s' missing from "
1808 "named_layouts; falling through to default.",
1810 return absl::NotFoundError(absl::StrCat(
"startup layout \"", name,
1811 "\" not found in named_layouts"));
1814 nlohmann::json parsed;
1816 parsed = nlohmann::json::parse(it->second);
1817 }
catch (
const nlohmann::json::parse_error& e) {
1818 startup_layout_consumed_ =
true;
1819 util::logf(
"LayoutManager: startup layout '%s' JSON parse error: %s",
1820 name.c_str(), e.what());
1821 return absl::InvalidArgumentError(
1822 absl::StrCat(
"startup layout \"", name,
"\" parse error: ", e.what()));
1825 auto tree_or = layout_designer::DockTreeFromJson(parsed);
1826 if (!tree_or.ok()) {
1827 startup_layout_consumed_ =
true;
1828 util::logf(
"LayoutManager: startup layout '%s' failed to parse: %s",
1829 name.c_str(), std::string(tree_or.status().message()).c_str());
1830 return tree_or.status();
1833 std::string validation_error;
1834 if (!tree_or->Validate(&validation_error)) {
1835 startup_layout_consumed_ =
true;
1836 util::logf(
"LayoutManager: startup layout '%s' failed validation: %s",
1837 name.c_str(), validation_error.c_str());
1838 return absl::InvalidArgumentError(absl::StrCat(
1839 "startup layout \"", name,
"\" validation failed: ", validation_error));
1842 absl::Status apply_status = ApplyDockTree(*tree_or, main_dockspace_id_);
1843 startup_layout_consumed_ =
true;
1844 if (!apply_status.ok()) {
1845 util::logf(
"LayoutManager: startup layout '%s' ApplyDockTree failed: %s",
1846 name.c_str(), std::string(apply_status.message()).c_str());
1852 return apply_status;
1855absl::StatusOr<layout_designer::DockTree> LayoutManager::CaptureDockTree(
1856 ImGuiID dockspace_id)
const {
1857 if (!window_manager_) {
1858 return absl::FailedPreconditionError(
1859 "LayoutManager::CaptureDockTree: WorkspaceWindowManager not bound");
1861 ImGuiDockNode* root_node = ImGui::DockBuilderGetNode(dockspace_id);
1863 return absl::NotFoundError(
1864 "LayoutManager::CaptureDockTree: no dock node at given id");
1867 std::unordered_map<std::string, PanelLookupEntry> by_window_name;
1868 for (
const auto& [panel_id, desc] :
1869 window_manager_->GetAllWindowDescriptors()) {
1870 const std::string title = window_manager_->GetWorkspaceWindowName(desc);
1873 by_window_name.emplace(
1874 title, PanelLookupEntry{panel_id, desc.display_name, desc.icon});
1878 tree.
root = CaptureDockNodeRecursive(root_node, by_window_name);
1880 tree.
root = layout_designer::DockNode::MakeLeaf({});
std::string dump(int=-1, char=' ', bool=false, int=0) const
bool contains(const std::string &) const
void DockOpenPresetPanels(EditorType type)
EditorType current_editor_type_
void DisableLazyDefaultDocking()
ImGuiID last_dockspace_id_
WorkspaceWindowManager * window_manager_
void ProtectRestoredLayoutFromDefaultInitialization()
std::unordered_map< size_t, std::unordered_map< EditorType, std::unordered_set< std::string > > > lazy_default_dock_attempts_
bool startup_reapply_pending_protection_
void RebuildLayout(EditorType type, ImGuiID dockspace_id)
Force rebuild of layout for a specific editor type.
LazyDefaultDockState lazy_default_dock_state_
std::unordered_map< EditorType, bool > layouts_initialized_
void RefreshLazyDefaultDockingContext(EditorType type, ImGuiID dockspace_id)
bool IsLayoutInitialized(EditorType type) const
Check if a layout has been initialized for an editor.
void MarkLayoutInitialized(EditorType type)
Mark a layout as initialized.
void InitializeEditorLayout(EditorType type, ImGuiID dockspace_id)
Initialize the default layout for a specific editor type.
void BuildLayoutFromPreset(EditorType type, ImGuiID dockspace_id)
bool DockPresetPositionOnFirstOpen(size_t session_id, const std::string &panel_id, bool include_default_visible)
void MarkLazyDefaultDockAttempted(const std::string &panel_id)
static PanelLayoutPreset GetLogicDebuggerPreset()
Get the "logic debugger" workspace preset (QA and debug focused)
static PanelLayoutPreset GetDungeonMasterPreset()
Get the "dungeon master" workspace preset.
static PanelLayoutPreset GetAudioEngineerPreset()
Get the "audio engineer" workspace preset (music focused)
static PanelLayoutPreset GetDesignerPreset()
Get the "designer" workspace preset (visual-focused)
static std::vector< std::string > GetDefaultWindows(EditorType type)
static PanelLayoutPreset GetOverworldArtistPreset()
Get the "overworld artist" workspace preset.
static PanelLayoutPreset GetDefaultPreset(EditorType type)
Get the default layout preset for an editor type.
static PanelLayoutPreset GetModderPreset()
Get the "modder" workspace preset (full-featured)
static PanelLayoutPreset GetMinimalPreset()
Get the "minimal" workspace preset (minimal cards)
static PanelLayoutPreset GetDeveloperPreset()
Get the "developer" workspace preset (debug-focused)
Manages user preferences and settings persistence.
Base interface for all logical window content components.
Central registry for all editor cards with session awareness and dependency injection.
std::string GetWorkspaceWindowName(size_t session_id, const std::string &base_window_id) const
Resolve the exact ImGui window name for a panel by base ID.
size_t GetActiveSessionId() const
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 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_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
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