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;
568 const std::vector<std::pair<std::string, DockPosition>>& docked_panels,
571 if (!window_manager) {
575 bool first_in_region =
true;
576 for (
const auto& [panel_id, position] : docked_panels) {
577 if (!matches_region(position)) {
585 if (first_in_region) {
586 first_in_region =
false;
591 return {width,
true};
594 preference.
width = std::max(preference.
width, width);
604 if (available_width <= 0.0f) {
607 return std::clamp(preferred_width / available_width, 0.05f, max_ratio);
613 const std::vector<std::pair<std::string, DockPosition>>& docked_panels) {
614 if (!cfg || !window_manager || viewport_width <= 0.0f) {
617 const float split_width =
618 dockspace_width > 0.0f ? dockspace_width : viewport_width;
623 if (preferred_left.exact) {
625 }
else if (preferred_left.width > 0.0f) {
627 std::clamp(preferred_left.width / viewport_width, 0.14f, 0.36f);
634 if (preferred_right.exact) {
635 const float remaining =
636 needs.
left ? split_width * (1.0f - cfg->
left) : split_width;
638 }
else if (preferred_right.width > 0.0f) {
640 std::clamp(preferred_right.width / viewport_width, 0.18f, 0.42f);
648 ids.
center = dockspace_id;
652 ids.left = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Left, cfg.
left,
653 nullptr, &ids.center);
656 ids.right = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Right,
657 cfg.
right,
nullptr, &ids.center);
660 ids.bottom = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Down,
661 cfg.
bottom,
nullptr, &ids.center);
664 ids.top = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Up, cfg.
top,
665 nullptr, &ids.center);
671 ids.left_bottom = ImGui::DockBuilderSplitNode(
672 ids.left, ImGuiDir_Down, cfg.
vertical_split,
nullptr, &ids.left_top);
680 ids.right_bottom = ImGui::DockBuilderSplitNode(
681 ids.right, ImGuiDir_Down, cfg.
vertical_split,
nullptr, &ids.right_top);
690 ImGuiID dockspace_id) {
691 DisableLazyDefaultDocking();
695 lazy_default_dock_attempts_.clear();
696 auto preset = LayoutPresets::GetDefaultPreset(type);
698 if (!window_manager_) {
700 "WorkspaceWindowManager not available, skipping dock layout for "
702 static_cast<int>(type));
706 const size_t session_id =
707 window_manager_ ? window_manager_->GetActiveSessionId() : 0;
708 const auto docked_panels = CollectDockedPanels(preset);
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
718 static bool compact_mode =
true;
719 constexpr float kEnterCompactWidth = 900.0f;
720 constexpr float kExitCompactWidth = 940.0f;
721 if (viewport_width <= 0.0f) {
724 compact_mode = compact_mode ? (viewport_width < kExitCompactWidth)
725 : (viewport_width < kEnterCompactWidth);
729 (viewport_width > 0.0f && viewport_width < 900.0f);
732 DockSplitNeeds needs{};
733 DockSplitConfig cfg{};
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);
745 DockNodeIds ids = BuildDockTree(dockspace_id, needs, cfg);
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;
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;
771 state.attempted_panels = lazy_default_dock_attempts_[session_id][type];
776 case DockPosition::Left:
777 if (ids.left_top || ids.left_bottom) {
779 return ids.left_top ? ids.left_top : ids.left_bottom;
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;
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:
808 std::vector<ImGuiID> auto_hide_nodes;
811 for (
const auto& [panel_id, position] : docked_panels) {
814 ? window_manager_->GetWindowDescriptor(session_id, panel_id)
818 "Preset references window '%s' that is not registered (session "
820 panel_id.c_str(), session_id);
824 std::string window_title = window_manager_->GetWorkspaceWindowName(*desc);
825 if (window_title.empty()) {
827 "Cannot dock window '%s': missing window name (session %zu)",
828 panel_id.c_str(), session_id);
832 const ImGuiID dock_id = get_dock_id(position);
833 ImGui::DockBuilderDockWindow(window_title.c_str(), dock_id);
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);
847bool LayoutManager::DockDefaultPositionOnFirstOpen(
848 size_t session_id,
const std::string& panel_id) {
849 return DockPresetPositionOnFirstOpen(session_id, panel_id,
853bool LayoutManager::DockPresetPositionOnPanelOpen(
size_t session_id,
854 const std::string& panel_id) {
855 return DockPresetPositionOnFirstOpen(session_id, panel_id,
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)) {
870 LayoutPresets::GetDefaultPreset(state.editor_type);
871 const bool is_default_visible =
876 (!include_default_visible && is_default_visible)) {
882 state.attempted_panels.contains(panel_id)) {
887 window_manager_->GetWindowDescriptor(session_id, panel_id);
891 const std::string window_title =
892 window_manager_->GetWorkspaceWindowName(*desc);
893 if (window_title.empty()) {
899 if (ImGuiWindow* window = ImGui::FindWindowByName(window_title.c_str());
900 window && window->DockId != 0 &&
901 ImGui::DockBuilderGetNode(window->DockId)) {
902 MarkLazyDefaultDockAttempted(panel_id);
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);
913 auto split_center = [&](ImGuiDir direction,
float ratio,
914 ImGuiID* region) -> ImGuiID {
915 if (*region != 0 && ImGui::DockBuilderGetNode(*region)) {
918 if (state.center == 0 || !ImGui::DockBuilderGetNode(state.center)) {
921 *region = ImGui::DockBuilderSplitNode(state.center, direction, ratio,
922 nullptr, &state.center);
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, ®ion)) {
938 if (bottom != 0 && ImGui::DockBuilderGetNode(bottom)) {
946 bottom = ImGui::DockBuilderSplitNode(
947 region, ImGuiDir_Down, state.vertical_split,
nullptr, &new_top);
952 if (top != 0 && ImGui::DockBuilderGetNode(top)) {
959 ImGuiID new_bottom = 0;
960 top = ImGui::DockBuilderSplitNode(
961 region, ImGuiDir_Up, 1.0f - state.vertical_split,
nullptr, &new_bottom);
968 target = state.center;
970 switch (position_it->second) {
971 case DockPosition::Left:
972 case DockPosition::LeftTop:
973 target = resolve_vertical_region(
false,
false);
975 case DockPosition::LeftBottom:
976 target = resolve_vertical_region(
false,
true);
978 case DockPosition::Right:
979 case DockPosition::RightTop:
980 target = resolve_vertical_region(
true,
false);
982 case DockPosition::RightBottom:
983 target = resolve_vertical_region(
true,
true);
985 case DockPosition::Bottom:
986 target = split_center(ImGuiDir_Down, state.bottom_ratio, &state.bottom);
988 case DockPosition::Top:
989 target = split_center(ImGuiDir_Up, state.top_ratio, &state.top);
991 case DockPosition::Center:
993 target = state.center;
998 if (target == 0 || !ImGui::DockBuilderGetNode(target)) {
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;
1010 ImGui::DockBuilderFinish(state.dockspace);
1015void LayoutManager::BuildOverworldLayout(ImGuiID dockspace_id) {
1016 BuildLayoutFromPreset(EditorType::kOverworld, dockspace_id);
1018void LayoutManager::BuildDungeonLayout(ImGuiID dockspace_id) {
1019 BuildLayoutFromPreset(EditorType::kDungeon, dockspace_id);
1021void LayoutManager::BuildGraphicsLayout(ImGuiID dockspace_id) {
1022 BuildLayoutFromPreset(EditorType::kGraphics, dockspace_id);
1024void LayoutManager::BuildPaletteLayout(ImGuiID dockspace_id) {
1025 BuildLayoutFromPreset(EditorType::kPalette, dockspace_id);
1027void LayoutManager::BuildScreenLayout(ImGuiID dockspace_id) {
1028 BuildLayoutFromPreset(EditorType::kScreen, dockspace_id);
1030void LayoutManager::BuildMusicLayout(ImGuiID dockspace_id) {
1031 BuildLayoutFromPreset(EditorType::kMusic, dockspace_id);
1033void LayoutManager::BuildSpriteLayout(ImGuiID dockspace_id) {
1034 BuildLayoutFromPreset(EditorType::kSprite, dockspace_id);
1036void LayoutManager::BuildMessageLayout(ImGuiID dockspace_id) {
1037 BuildLayoutFromPreset(EditorType::kMessage, dockspace_id);
1039void LayoutManager::BuildAssemblyLayout(ImGuiID dockspace_id) {
1040 BuildLayoutFromPreset(EditorType::kAssembly, dockspace_id);
1042void LayoutManager::BuildSettingsLayout(ImGuiID dockspace_id) {
1043 BuildLayoutFromPreset(EditorType::kSettings, dockspace_id);
1045void LayoutManager::BuildEmulatorLayout(ImGuiID dockspace_id) {
1046 BuildLayoutFromPreset(EditorType::kEmulator, dockspace_id);
1049void LayoutManager::SaveCurrentLayout(
const std::string& name,
bool persist) {
1050 if (!window_manager_) {
1052 "Cannot save layout '%s': WorkspaceWindowManager not available",
1060 size_t session_id = window_manager_->GetActiveSessionId();
1061 auto visibility_state = window_manager_->SerializeVisibilityState(session_id);
1064 saved_layouts_[name] = visibility_state;
1065 saved_pinned_layouts_[name] = window_manager_->SerializePinnedState();
1066 layout_scopes_[name] = scope;
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);
1076 SaveLayoutsToDisk(scope);
1079 LOG_INFO(
"LayoutManager",
"Saved layout '%s' with %zu window states%s",
1080 name.c_str(), visibility_state.size(),
1081 persist ?
"" :
" (session-only)");
1084void LayoutManager::LoadLayout(
const std::string& name) {
1085 if (!window_manager_) {
1087 "Cannot load layout '%s': WorkspaceWindowManager not available",
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());
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();
1104 if (has_imgui_layout) {
1105 DisableLazyDefaultDocking();
1109 size_t session_id = window_manager_->GetActiveSessionId();
1110 window_manager_->RestoreVisibilityState(session_id, layout_it->second,
1113 auto pinned_it = saved_pinned_layouts_.find(name);
1114 if (pinned_it != saved_pinned_layouts_.end()) {
1115 window_manager_->RestorePinnedState(pinned_it->second);
1119 if (has_imgui_layout) {
1120 ImGui::LoadIniSettingsFromMemory(imgui_it->second.c_str(),
1121 imgui_it->second.size());
1122 ProtectRestoredLayoutFromDefaultInitialization();
1125 LOG_INFO(
"LayoutManager",
"Loaded layout '%s'", name.c_str());
1128void LayoutManager::CaptureTemporarySessionLayout(
size_t session_id) {
1129 if (!window_manager_) {
1133 temp_session_id_ = session_id;
1134 temp_session_visibility_ =
1135 window_manager_->SerializeVisibilityState(session_id);
1136 temp_session_pinned_ = window_manager_->SerializePinnedState();
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);
1143 temp_session_imgui_layout_.clear();
1146 has_temp_session_layout_ =
true;
1149 "Captured temporary session layout for session %zu (%zu panel states)",
1150 session_id, temp_session_visibility_.size());
1153bool LayoutManager::RestoreTemporarySessionLayout(
size_t session_id,
1154 bool clear_after_restore) {
1155 if (!window_manager_ || !has_temp_session_layout_) {
1159 if (session_id != temp_session_id_) {
1161 "Session layout snapshot belongs to session %zu, requested %zu",
1162 temp_session_id_, session_id);
1166 const bool has_imgui_layout = !temp_session_imgui_layout_.empty();
1167 if (has_imgui_layout) {
1168 DisableLazyDefaultDocking();
1171 window_manager_->RestoreVisibilityState(session_id, temp_session_visibility_,
1173 window_manager_->RestorePinnedState(temp_session_pinned_);
1175 if (has_imgui_layout) {
1176 ImGui::LoadIniSettingsFromMemory(temp_session_imgui_layout_.c_str(),
1177 temp_session_imgui_layout_.size());
1178 ProtectRestoredLayoutFromDefaultInitialization();
1181 if (clear_after_restore) {
1182 ClearTemporarySessionLayout();
1185 LOG_INFO(
"LayoutManager",
"Restored temporary session layout for session %zu",
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();
1198bool LayoutManager::SaveNamedSnapshot(
const std::string& name,
1199 size_t session_id) {
1200 if (!window_manager_ || name.empty()) {
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) {
1212 named_snapshots_[name] = std::move(snapshot);
1213 LOG_INFO(
"LayoutManager",
"Saved named snapshot '%s' for session %zu",
1214 name.c_str(), session_id);
1218bool LayoutManager::RestoreNamedSnapshot(
const std::string& name,
1220 bool remove_after_restore) {
1221 if (!window_manager_) {
1224 auto it = named_snapshots_.find(name);
1225 if (it == named_snapshots_.end()) {
1232 const bool has_imgui_layout = !snapshot.
imgui_layout.empty();
1233 if (has_imgui_layout) {
1234 DisableLazyDefaultDocking();
1236 window_manager_->RestoreVisibilityState(session_id, snapshot.
visibility,
1238 window_manager_->RestorePinnedState(snapshot.
pinned);
1239 if (has_imgui_layout) {
1240 ImGui::LoadIniSettingsFromMemory(snapshot.
imgui_layout.c_str(),
1242 ProtectRestoredLayoutFromDefaultInitialization();
1244 if (remove_after_restore) {
1245 named_snapshots_.erase(it);
1250bool LayoutManager::DeleteNamedSnapshot(
const std::string& name) {
1251 auto it = named_snapshots_.find(name);
1252 if (it == named_snapshots_.end()) {
1255 named_snapshots_.erase(it);
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);
1268 std::sort(names.begin(), names.end());
1272bool LayoutManager::HasNamedSnapshot(
const std::string& name)
const {
1273 return named_snapshots_.find(name) != named_snapshots_.end();
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",
1285 auto scope_it = layout_scopes_.find(name);
1286 if (scope_it != layout_scopes_.end()) {
1287 scope = scope_it->second;
1290 saved_layouts_.erase(layout_it);
1291 saved_imgui_layouts_.erase(name);
1292 saved_pinned_layouts_.erase(name);
1293 layout_scopes_.erase(name);
1295 SaveLayoutsToDisk(scope);
1297 LOG_INFO(
"LayoutManager",
"Deleted layout '%s'", name.c_str());
1301std::vector<LayoutProfile> LayoutManager::GetBuiltInProfiles() {
1305 .description =
"Focused editing workspace with minimal panel noise",
1306 .preset_name =
"Minimal",
1307 .open_agent_chat =
false},
1310 .description =
"Debugger-first workspace for tracing and memory tools",
1311 .preset_name =
"Developer",
1312 .open_agent_chat =
false},
1315 .description =
"Map-centric layout for overworld or dungeon workflows",
1316 .preset_name =
"Overworld Expert",
1317 .open_agent_chat =
false},
1320 .description =
"Collaboration-heavy layout with agent-centric tooling",
1321 .preset_name =
"Modder",
1322 .open_agent_chat =
true},
1326bool LayoutManager::ApplyBuiltInProfile(
const std::string& profile_id,
1330 if (!window_manager_) {
1332 "Cannot apply profile '%s': WorkspaceWindowManager not available",
1333 profile_id.c_str());
1339 for (
const auto& profile : GetBuiltInProfiles()) {
1340 if (profile.id == profile_id) {
1341 matched_profile = profile;
1348 LOG_WARN(
"LayoutManager",
"Unknown layout profile id: %s",
1349 profile_id.c_str());
1353 const std::string resolved_preset =
1354 ResolveProfilePresetName(profile_id, editor_type);
1355 if (!resolved_preset.empty()) {
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());
1368 DisableLazyDefaultDocking();
1370 window_manager_->HideAllWindowsInSession(session_id);
1372 window_manager_->OpenWindow(session_id, panel_id);
1378 *out_profile = matched_profile;
1381 LOG_INFO(
"LayoutManager",
"Applied profile '%s' via preset '%s'",
1382 profile_id.c_str(), matched_profile.
preset_name.c_str());
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);
1395bool LayoutManager::HasLayout(
const std::string& name)
const {
1396 return saved_layouts_.find(name) != saved_layouts_.end();
1399void LayoutManager::LoadLayoutsFromDisk() {
1400 saved_layouts_.clear();
1401 saved_imgui_layouts_.clear();
1402 saved_pinned_layouts_.clear();
1403 layout_scopes_.clear();
1405 LoadLayoutsFromDiskInternal(LayoutScope::kGlobal,
false);
1406 if (!project_layout_key_.empty()) {
1407 LoadLayoutsFromDiskInternal(LayoutScope::kProject,
true);
1411void LayoutManager::SetProjectLayoutKey(
const std::string& key) {
1416 project_layout_key_ = key;
1417 LoadLayoutsFromDisk();
1420void LayoutManager::UseGlobalLayouts() {
1421 project_layout_key_.clear();
1422 LoadLayoutsFromDisk();
1426 return project_layout_key_.empty() ? LayoutScope::kGlobal
1427 : LayoutScope::kProject;
1430void LayoutManager::LoadLayoutsFromDiskInternal(
LayoutScope scope,
bool merge) {
1431 std::filesystem::path layout_path =
1432 GetLayoutsFilePath(scope, project_layout_key_);
1433 if (layout_path.empty()) {
1437 if (!std::filesystem::exists(layout_path)) {
1439 LOG_INFO(
"LayoutManager",
"No layouts file at %s",
1440 layout_path.string().c_str());
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());
1457 LOG_WARN(
"LayoutManager",
"Layouts file missing 'layouts' object: %s",
1458 layout_path.string().c_str());
1462 for (
auto& [name, entry] : root[
"layouts"].
items()) {
1463 if (!entry.is_object()) {
1467 std::unordered_map<std::string, bool> windows;
1468 std::unordered_map<std::string, bool> pinned;
1470 JsonToWindowMap(entry, &windows);
1471 if (entry.contains(
"pinned")) {
1472 JsonToBoolMap(entry[
"pinned"], &pinned);
1475 saved_layouts_[name] = std::move(windows);
1476 saved_pinned_layouts_[name] = std::move(pinned);
1477 layout_scopes_[name] = scope;
1479 if (entry.contains(
"imgui_ini") && entry[
"imgui_ini"].is_string()) {
1480 saved_imgui_layouts_[name] = entry[
"imgui_ini"].get<std::string>();
1482 saved_imgui_layouts_.erase(name);
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());
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");
1504 LOG_WARN(
"LayoutManager",
"Failed to create layout directory: %s",
1505 status.ToString().c_str());
1511 root[
"version"] = 2;
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) {
1521 entry[kWindowsKey] = BoolMapToJson(windows);
1523 auto pinned_it = saved_pinned_layouts_.find(name);
1524 if (pinned_it != saved_pinned_layouts_.end()) {
1525 entry[
"pinned"] = BoolMapToJson(pinned_it->second);
1528 auto imgui_it = saved_imgui_layouts_.find(name);
1529 if (imgui_it != saved_imgui_layouts_.end()) {
1530 entry[
"imgui_ini"] = imgui_it->second;
1533 root[
"layouts"][name] = entry;
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());
1542 file << root.
dump(2);
1544 }
catch (
const std::exception& e) {
1545 LOG_WARN(
"LayoutManager",
"Failed to save layouts: %s", e.what());
1550 DisableLazyDefaultDocking();
1551 layouts_initialized_[type] =
false;
1552 LOG_INFO(
"LayoutManager",
"Reset layout for editor type %d",
1553 static_cast<int>(type));
1557 auto it = layouts_initialized_.find(type);
1558 return it != layouts_initialized_.end() && it->second;
1562 layouts_initialized_[type] =
true;
1563 LOG_INFO(
"LayoutManager",
"Marked layout for editor type %d as initialized",
1564 static_cast<int>(type));
1567void LayoutManager::ClearInitializationFlags() {
1568 DisableLazyDefaultDocking();
1569 layouts_initialized_.clear();
1570 LOG_INFO(
"LayoutManager",
"Cleared all layout initialization flags");
1573std::string LayoutManager::GetWindowTitle(
const std::string& card_id)
const {
1574 if (!window_manager_) {
1578 const size_t session_id = window_manager_->GetActiveSessionId();
1579 return window_manager_->GetWorkspaceWindowName(session_id, card_id);
1588 return ImGuiDir_Left;
1590 return ImGuiDir_Right;
1594 return ImGuiDir_Down;
1596 return ImGuiDir_Left;
1603 if (
const auto* desc =
1616 }
else if (!panel.
icon.empty()) {
1627 ImGuiID target_id) {
1629 if (node.
type == NodeType::kLeaf) {
1630 for (
const auto& panel : node.
panels) {
1632 if (!title.empty()) {
1633 ImGui::DockBuilderDockWindow(title.c_str(), target_id);
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);
1660 const ImGuiDockNode* node,
1661 const std::unordered_map<std::string, PanelLookupEntry>& by_window_name) {
1663 return layout_designer::DockNode::MakeLeaf({});
1666 if (node->IsSplitNode()) {
1672 const bool vertical = node->SplitAxis == ImGuiAxis_Y;
1674 vertical ? layout_designer::SplitDirection::kUp
1675 : layout_designer::SplitDirection::kLeft;
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;
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));
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];
1698 auto it = by_window_name.find(std::string(w->Name));
1699 if (it == by_window_name.end())
1701 if (node->SelectedTabId != 0 && w->ID == node->SelectedTabId) {
1702 selected_index =
static_cast<int>(panels.size());
1705 {it->second.panel_id, it->second.display_name, it->second.icon});
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;
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);
1734 ImGuiID dockspace_id) {
1735 if (!window_manager_) {
1736 return absl::FailedPreconditionError(
1737 "LayoutManager::ApplyDockTree: WorkspaceWindowManager not bound");
1739 std::string validation_error;
1740 if (!tree.
Validate(&validation_error)) {
1741 return absl::InvalidArgumentError(
"LayoutManager::ApplyDockTree: " +
1747 DisableLazyDefaultDocking();
1759 std::vector<std::string> panel_ids;
1761 CollectPanelIdsInSubtree(*tree.
root, &panel_ids);
1763 const std::unordered_set<std::string> tree_id_set(panel_ids.begin(),
1765 const size_t session_id = window_manager_->GetActiveSessionId();
1766 for (
const auto&
id : panel_ids) {
1773 if (window_manager_->IsWindowOpen(session_id,
id))
1775 window_manager_->OpenWindow(session_id,
id);
1777 for (
const std::string&
id :
1778 window_manager_->GetWindowsInSession(session_id)) {
1779 if (tree_id_set.count(
id) > 0)
1781 if (window_manager_->IsWindowPinned(session_id,
id))
1788 if (!window_manager_->IsWindowOpen(session_id,
id))
1790 window_manager_->CloseWindow(
id);
1793 ImGui::DockBuilderRemoveNode(dockspace_id);
1794 ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
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;
1802 ImGui::DockBuilderSetNodeSize(dockspace_id, size);
1804 ApplyDockNodeRecursive(window_manager_, session_id, *tree.
root, dockspace_id);
1805 ImGui::DockBuilderFinish(dockspace_id);
1807 last_dockspace_id_ = dockspace_id;
1820 ProtectRestoredLayoutFromDefaultInitialization();
1822 return absl::OkStatus();
1825absl::Status LayoutManager::MaybeReapplyStartupLayout(
UserSettings* settings) {
1826 if (startup_layout_consumed_) {
1827 return absl::OkStatus();
1829 if (settings ==
nullptr) {
1830 startup_layout_consumed_ =
true;
1831 return absl::OkStatus();
1837 startup_layout_consumed_ =
true;
1838 return absl::OkStatus();
1841 if (main_dockspace_id_ == 0) {
1844 return absl::OkStatus();
1848 const auto it = named_layouts.find(name);
1849 if (it == named_layouts.end()) {
1850 startup_layout_consumed_ =
true;
1852 "LayoutManager: startup layout '%s' missing from "
1853 "named_layouts; falling through to default.",
1855 return absl::NotFoundError(absl::StrCat(
"startup layout \"", name,
1856 "\" not found in named_layouts"));
1859 nlohmann::json parsed;
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()));
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();
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));
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());
1897 return apply_status;
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");
1906 ImGuiDockNode* root_node = ImGui::DockBuilderGetNode(dockspace_id);
1908 return absl::NotFoundError(
1909 "LayoutManager::CaptureDockTree: no dock node at given id");
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);
1918 by_window_name.emplace(
1919 title, PanelLookupEntry{panel_id, desc.display_name, desc.icon});
1923 tree.
root = CaptureDockNodeRecursive(root_node, by_window_name);
1925 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.
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.
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)
bool IsRightDockPosition(DockPosition pos)
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)
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)
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)
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