11#include <unordered_set>
14#include "absl/strings/str_cat.h"
20#include "imgui/imgui.h"
21#include "imgui/imgui_internal.h"
24#include <TargetConditionals.h>
45 for (
const auto& window_id : default_windows) {
49 LOG_INFO(
"LayoutManager",
"Showing %zu default windows for editor type %d",
50 default_windows.size(),
static_cast<int>(type));
55 for (
const auto& [key, value] : map) {
62 std::unordered_map<std::string, bool>* map) {
67 for (
const auto& [key, value] : obj.
items()) {
68 if (value.is_boolean()) {
69 (*map)[key] = value.get<
bool>();
75 std::unordered_map<std::string, bool>* windows) {
91 const std::string& project_key) {
93 if (!layouts_dir.ok()) {
98 std::filesystem::path projects_dir = *layouts_dir /
"projects";
100 return projects_dir / (project_key +
".json");
107 return *layouts_dir /
"layouts.json";
112 if (profile_id ==
"mapping") {
114 return "Dungeon Expert";
116 return "Overworld Expert";
118 if (profile_id ==
"code") {
121 if (profile_id ==
"debug") {
124 if (profile_id ==
"chat") {
137 ImGuiID dockspace_id) {
145 !ImGui::DockBuilderGetNode(state.dockspace) ||
146 !ImGui::DockBuilderGetNode(state.center)) {
152 state.enabled =
true;
153 state.editor_type = type;
154 state.session_id = session_id;
172 const auto dock_if_open = [&](
const std::string& panel_id) {
179 dock_if_open(panel_id);
182 dock_if_open(panel_id);
195 ImGuiID dockspace_id) {
209 "Suppressed lazy preset init for editor type %d to preserve "
210 "startup-reapplied custom layout",
211 static_cast<int>(type));
230 "Layout for editor type %d already initialized, skipping",
231 static_cast<int>(type));
235 LOG_INFO(
"LayoutManager",
"Initializing layout for editor type %d",
236 static_cast<int>(type));
240 ImGui::DockBuilderRemoveNode(dockspace_id);
241 ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
252 ImGui::DockBuilderFinish(dockspace_id);
265 ImGuiDockNode* node = ImGui::DockBuilderGetNode(dockspace_id);
268 "Cannot rebuild layout: dockspace ID %u not found", dockspace_id);
272 LOG_INFO(
"LayoutManager",
"Forcing rebuild of layout for editor type %d",
273 static_cast<int>(type));
284 ImGui::DockBuilderRemoveNode(dockspace_id);
285 ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
295 ImGui::DockBuilderFinish(dockspace_id);
302 LOG_INFO(
"LayoutManager",
"Layout rebuild complete for editor type %d",
303 static_cast<int>(type));
311 float bottom = 0.22f;
313 float vertical_split = 0.52f;
319 case EditorType::kDungeon:
326 case EditorType::kOverworld:
332 case EditorType::kGraphics:
337 case EditorType::kPalette:
342 case EditorType::kSprite:
347 case EditorType::kScreen:
352 case EditorType::kMessage:
357 case EditorType::kAssembly:
362 case EditorType::kEmulator:
367 case EditorType::kAgent:
386 ImGuiID left_top = 0;
387 ImGuiID left_bottom = 0;
388 ImGuiID right_top = 0;
389 ImGuiID right_bottom = 0;
397 bool left_top =
false;
398 bool left_bottom =
false;
399 bool right_top =
false;
400 bool right_bottom =
false;
404 const std::string& panel_id) {
415 std::vector<std::pair<std::string, DockPosition>> docked_panels;
418 std::unordered_set<std::string> seen_panels;
421 auto append_ordered = [&](
const std::vector<std::string>& ordered_ids) {
422 for (
const auto& panel_id : ordered_ids) {
423 if (seen_panels.contains(panel_id) ||
431 docked_panels.emplace_back(it->first, it->second);
432 seen_panels.insert(panel_id);
439 std::vector<std::pair<std::string, DockPosition>> remaining_panels;
442 if (seen_panels.contains(panel_id) ||
446 remaining_panels.emplace_back(panel_id, position);
449 std::sort(remaining_panels.begin(), remaining_panels.end(),
450 [](
const auto& lhs,
const auto& rhs) {
451 if (lhs.second != rhs.second) {
452 return static_cast<int>(lhs.second) <
453 static_cast<int>(rhs.second);
455 return lhs.first < rhs.first;
457 docked_panels.insert(docked_panels.end(), remaining_panels.begin(),
458 remaining_panels.end());
460 return docked_panels;
464 const std::vector<std::pair<std::string, DockPosition>>& docked_panels) {
466 for (
const auto& [_, pos] : docked_panels) {
482 needs.left_top =
true;
486 needs.left_bottom =
true;
490 needs.right_top =
true;
494 needs.right_bottom =
true;
523 const std::vector<std::pair<std::string, DockPosition>>& docked_panels,
526 if (!window_manager) {
530 bool first_in_region =
true;
531 for (
const auto& [panel_id, position] : docked_panels) {
532 if (!matches_region(position)) {
540 if (first_in_region) {
541 first_in_region =
false;
546 return {width,
true};
549 preference.
width = std::max(preference.
width, width);
559 if (available_width <= 0.0f) {
562 return std::clamp(preferred_width / available_width, 0.05f, max_ratio);
568 const std::vector<std::pair<std::string, DockPosition>>& docked_panels) {
569 if (!cfg || !window_manager || viewport_width <= 0.0f) {
572 const float split_width =
573 dockspace_width > 0.0f ? dockspace_width : viewport_width;
578 if (preferred_left.exact) {
580 }
else if (preferred_left.width > 0.0f) {
582 std::clamp(preferred_left.width / viewport_width, 0.14f, 0.36f);
589 if (preferred_right.exact) {
590 const float remaining =
591 needs.
left ? split_width * (1.0f - cfg->
left) : split_width;
593 }
else if (preferred_right.width > 0.0f) {
595 std::clamp(preferred_right.width / viewport_width, 0.18f, 0.42f);
603 ids.
center = dockspace_id;
607 ids.left = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Left, cfg.
left,
608 nullptr, &ids.center);
611 ids.right = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Right,
612 cfg.
right,
nullptr, &ids.center);
615 ids.bottom = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Down,
616 cfg.
bottom,
nullptr, &ids.center);
619 ids.top = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Up, cfg.
top,
620 nullptr, &ids.center);
626 ids.left_bottom = ImGui::DockBuilderSplitNode(
627 ids.left, ImGuiDir_Down, cfg.
vertical_split,
nullptr, &ids.left_top);
635 ids.right_bottom = ImGui::DockBuilderSplitNode(
636 ids.right, ImGuiDir_Down, cfg.
vertical_split,
nullptr, &ids.right_top);
664 return (dir == ImGuiDir_Left || dir == ImGuiDir_Right) ? ImGuiAxis_X
674 if (node->LocalFlags & ImGuiDockNodeFlags_CentralNode) {
677 for (ImGuiDockNode* child : node->ChildNodes) {
686 ImGuiDockNode* node = ImGui::DockBuilderGetNode(node_id);
687 while (node && node->ParentNode) {
688 node = node->ParentNode;
690 return node ? node->ID : 0;
694ImGuiDir
ChildSide(
const ImGuiDockNode* parent,
const ImGuiDockNode* child) {
695 const bool first = parent->ChildNodes[0] == child;
696 if (parent->SplitAxis == ImGuiAxis_X) {
697 return first ? ImGuiDir_Left : ImGuiDir_Right;
699 return first ? ImGuiDir_Up : ImGuiDir_Down;
705 const ImGuiDockNode* root = node ? node->ParentNode :
nullptr;
706 if (!root || root->ParentNode || !root->IsDockSpace() ||
707 !node->IsLeafNode() ||
708 (node->LocalFlags & ImGuiDockNodeFlags_CentralNode)) {
709 return ImGuiDir_None;
711 *size = node->Size[root->SplitAxis];
716 if (
auto* manager =
static_cast<LayoutManager*
>(handler->UserData)) {
717 manager->ResetLayoutIniSettings();
723 if (std::strcmp(name,
"Data") != 0) {
726 return handler->UserData;
735 if (
auto* manager =
static_cast<LayoutManager*
>(handler->UserData)) {
736 manager->ApplyLayoutIniSettings();
741 ImGuiTextBuffer* buf) {
742 if (
auto* manager =
static_cast<LayoutManager*
>(handler->UserData)) {
743 manager->WriteLayoutIniSettings(buf);
749 const char* ini_path = ImGui::GetIO().IniFilename;
750 if (!ini_path || ini_path[0] ==
'\0') {
754 const std::filesystem::path source(ini_path);
755 const std::filesystem::path backup(std::string(ini_path) +
".pre-b2");
756 if (!std::filesystem::exists(source, ec) ||
757 std::filesystem::exists(backup, ec)) {
760 std::filesystem::copy_file(source, backup, ec);
762 LOG_WARN(
"LayoutManager",
"Could not back up %s: %s", ini_path,
763 ec.message().c_str());
765 LOG_INFO(
"LayoutManager",
"Backed up legacy layout to %s",
766 backup.string().c_str());
772LayoutManager::~LayoutManager() {
773 if (!ImGui::GetCurrentContext()) {
776 if (ImGuiSettingsHandler* handler =
777 ImGui::FindSettingsHandler(kLayoutIniTypeName);
778 handler && handler->UserData ==
this) {
779 handler->UserData =
nullptr;
783ImGuiID LayoutManager::OuterDockspaceId() {
784 return ImHashStr(kOuterDockspaceName);
790 return ImHashStr(absl::StrCat(kEditorDockspacePrefix, name).c_str());
793ImGuiID LayoutManager::ActiveEditorDockspaceId()
const {
794 if (active_editor_type_ == EditorType::kUnknown) {
797 return EditorDockspaceId(active_editor_type_);
800ImGuiID LayoutManager::GetMainDockspaceId()
const {
801 if (
const ImGuiID active = ActiveEditorDockspaceId()) {
804 return main_dockspace_id_;
807ImGuiID LayoutManager::FindPinNode()
const {
808 ImGuiDockNode* outer = ImGui::DockBuilderGetNode(OuterDockspaceId());
809 if (!outer || !outer->IsSplitNode()) {
812 for (ImGuiDockNode* child : outer->ChildNodes) {
813 if (child && child->IsLeafNode() && !FindCentralLeaf(child)) {
820ImGuiID LayoutManager::EnsurePinNode() {
821 if (
const ImGuiID pin = FindPinNode()) {
824 const ImGuiID outer_id = OuterDockspaceId();
825 ImGuiDockNode* outer = ImGui::DockBuilderGetNode(outer_id);
828 if (!outer || outer->IsSplitNode()) {
833 const ImGuiAxis axis = PinAxis(pin_dir_);
835 outer->Size[axis] - ImGui::GetStyle().DockingSeparatorSize;
836 float ratio = kPinRatio;
838 if (pin_size_ > 0.0f && avail > 0.0f) {
839 size = ImClamp(pin_size_, kPinMinRatio * avail, kPinMaxRatio * avail);
840 ratio = size / avail;
844 ImGui::DockBuilderSplitNode(outer_id, pin_dir_, ratio,
nullptr, ¢ral);
847 if (ImGuiDockNode* node = ImGui::DockBuilderGetNode(pin);
848 node && size > 0.0f) {
849 node->SizeRef[axis] = size;
851 ImGui::DockBuilderFinish(outer_id);
855void LayoutManager::RecordPinSpot() {
856 ImGuiDockNode* outer = ImGui::DockBuilderGetNode(OuterDockspaceId());
857 ImGuiDockNode* pin = ImGui::DockBuilderGetNode(FindPinNode());
858 if (!outer || !pin) {
861 const float size = pin->Size[outer->SplitAxis];
865 pin_dir_ = ChildSide(outer, pin);
869ImGuiID LayoutManager::MovePinSpot(ImGuiDir dir,
float size) {
870 ImGuiID pin = FindPinNode();
871 if (pin != 0 && dir == pin_dir_) {
875 std::vector<std::string> moved;
876 if (ImGuiDockNode* node = ImGui::DockBuilderGetNode(pin)) {
877 for (ImGuiWindow* window : node->Windows) {
878 moved.emplace_back(window->Name);
880 ImGui::DockBuilderRemoveNode(pin);
886 pin = EnsurePinNode();
888 for (
const std::string& name : moved) {
889 ImGui::DockBuilderDockWindow(name.c_str(), pin);
895ImVec2 LayoutManager::EditorDockspaceSize()
const {
896 if (editor_host_size_.x > 0.0f && editor_host_size_.y > 0.0f) {
897 return editor_host_size_;
900 if (last_size.x > 0.0f && last_size.y > 0.0f) {
903 if (
const ImGuiViewport* viewport = ImGui::GetMainViewport();
904 viewport && viewport->WorkSize.x > 0.0f && viewport->WorkSize.y > 0.0f) {
905 return viewport->WorkSize;
907 return ImVec2(1280.0f, 720.0f);
910void LayoutManager::StashLazyDefaultDockState() {
911 const auto& state = lazy_default_dock_state_;
912 if (state.editor_type == EditorType::kUnknown || state.dockspace == 0) {
915 lazy_states_by_type_[state.editor_type] = state;
918bool LayoutManager::RestoreLazyDefaultDockState(
EditorType type,
919 ImGuiID dockspace_id) {
920 auto& state = lazy_default_dock_state_;
922 if (dockspace_id != EditorDockspaceId(type) ||
923 (state.editor_type == type && state.dockspace == dockspace_id)) {
927 StashLazyDefaultDockState();
928 if (
auto it = lazy_states_by_type_.find(type);
929 it != lazy_states_by_type_.end() &&
930 it->second.dockspace == dockspace_id &&
931 ImGui::DockBuilderGetNode(it->second.center)) {
938 ImGuiDockNode* center =
939 FindCentralLeaf(ImGui::DockBuilderGetNode(dockspace_id));
941 DisableLazyDefaultDocking();
944 const DockSplitConfig cfg = DockSplitConfig::ForEditor(type);
948 window_manager_ ? window_manager_->GetActiveSessionId() : 0;
949 state.editor_type = type;
950 state.dockspace = dockspace_id;
951 state.center = center->ID;
952 state.left_ratio = cfg.left;
953 state.right_ratio = cfg.right;
954 state.bottom_ratio = cfg.bottom;
955 state.top_ratio = cfg.top;
956 state.vertical_split = cfg.vertical_split;
960void LayoutManager::DrawEditorHost() {
961 ImGuiDockNode* central =
962 FindCentralLeaf(ImGui::DockBuilderGetNode(OuterDockspaceId()));
969 ImGuiDockNodeFlags host_node_flags =
970 ImGuiDockNodeFlags_NoUndocking | ImGuiDockNodeFlags_NoDockingSplit;
971 host_node_flags |= ImGuiDockNodeFlags_NoTabBar;
972 host_node_flags |= ImGuiDockNodeFlags_NoDockingOverMe;
973 host_node_flags |= ImGuiDockNodeFlags_NoWindowMenuButton;
974 host_node_flags |= ImGuiDockNodeFlags_NoCloseButton;
975 ImGuiWindowClass host_class;
976 host_class.DockNodeFlagsOverrideSet = host_node_flags;
977 ImGui::SetNextWindowClass(&host_class);
978 ImGui::SetNextWindowDockID(central->ID, ImGuiCond_Always);
982 constexpr ImGuiWindowFlags kHostFlags =
983 ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse |
984 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse |
985 ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoBringToFrontOnFocus |
986 ImGuiWindowFlags_NoNavFocus;
987 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
988 ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
989 ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
990 if (ImGui::Begin(kEditorHostWindowName,
nullptr, kHostFlags)) {
991 editor_host_size_ = ImGui::GetContentRegionAvail();
993 ImGui::PopStyleVar(3);
996 const ImGuiID active_id = ActiveEditorDockspaceId();
998 const ImGuiID
id = EditorDockspaceId(
static_cast<EditorType>(index));
999 if (
id != active_id && ImGui::DockBuilderGetNode(
id)) {
1000 ImGui::DockSpace(
id, ImVec2(0.0f, 0.0f),
1001 ImGuiDockNodeFlags_KeepAliveOnly);
1004 if (active_id != 0) {
1005 ImGui::DockSpace(active_id, ImVec2(0.0f, 0.0f),
1006 ImGuiDockNodeFlags_PassthruCentralNode);
1011 EnforcePinnedPanels(FindPinNode());
1014void LayoutManager::EnforcePinnedPanels(ImGuiID pin_node) {
1015 if (!window_manager_) {
1018 const ImGuiID outer_id = OuterDockspaceId();
1019 const size_t session_id = window_manager_->GetActiveSessionId();
1020 const auto find_window = [&](
const std::string& panel_id) -> ImGuiWindow* {
1022 window_manager_->GetWindowDescriptor(session_id, panel_id);
1026 const std::string title = window_manager_->GetWorkspaceWindowName(*desc);
1027 return title.empty() ? nullptr : ImGui::FindWindowByName(title.c_str());
1034 std::unordered_set<std::string> pinned_now;
1035 for (
const auto& panel_id : window_manager_->GetPinnedWindows(session_id)) {
1036 ImGuiWindow* window = find_window(panel_id);
1040 pinned_now.insert(window->Name);
1041 if (window->DockId == 0 || DockRootId(window->DockId) == outer_id) {
1044 float edge_size = 0.0f;
1045 const ImGuiDir edge = DockspaceEdgeSide(
1046 ImGui::DockBuilderGetNode(window->DockId), &edge_size);
1047 if (edge != ImGuiDir_None && pinned_last_frame_.contains(window->Name)) {
1048 pin_node = MovePinSpot(edge, edge_size);
1050 if (pin_node == 0) {
1051 pin_node = EnsurePinNode();
1053 if (pin_node != 0) {
1054 ImGui::DockBuilderDockWindow(window->Name, pin_node);
1057 pinned_last_frame_ = std::move(pinned_now);
1060 const ImGuiID home_dockspace = ActiveEditorDockspaceId();
1061 if (home_dockspace == 0) {
1065 const bool preset_regions_live =
1066 lazy_default_dock_state_.editor_type == active_editor_type_ &&
1067 lazy_default_dock_state_.dockspace == home_dockspace;
1068 for (
const auto& desc : window_manager_->GetWindowsInCategory(
1069 session_id, window_manager_->GetActiveCategory())) {
1070 if (window_manager_->IsWindowPinned(session_id, desc.card_id)) {
1073 ImGuiWindow* window = find_window(desc.card_id);
1074 if (!window || window->DockId == 0 ||
1075 DockRootId(window->DockId) != outer_id) {
1078 if (preset_regions_live &&
1079 DockPresetPositionOnFirstOpen(session_id, desc.card_id,
1084 if (ImGuiDockNode* center =
1085 FindCentralLeaf(ImGui::DockBuilderGetNode(home_dockspace))) {
1086 ImGui::DockBuilderDockWindow(window->Name, center->ID);
1091void LayoutManager::RegisterLayoutIniHandler() {
1092 if (!ImGui::GetCurrentContext()) {
1095 if (ImGuiSettingsHandler* existing =
1096 ImGui::FindSettingsHandler(kLayoutIniTypeName)) {
1097 existing->UserData =
this;
1100 ImGuiSettingsHandler handler;
1101 handler.TypeName = kLayoutIniTypeName;
1102 handler.TypeHash = ImHashStr(kLayoutIniTypeName);
1103 handler.ClearAllFn = LayoutIniClearAll;
1104 handler.ReadInitFn = LayoutIniClearAll;
1105 handler.ReadOpenFn = LayoutIniReadOpen;
1106 handler.ReadLineFn = LayoutIniReadLine;
1107 handler.ApplyAllFn = LayoutIniApplyAll;
1108 handler.WriteAllFn = LayoutIniWriteAll;
1109 handler.UserData =
this;
1110 ImGui::AddSettingsHandler(&handler);
1113void LayoutManager::ResetLayoutIniSettings() {
1117void LayoutManager::ReadLayoutIniLine(
const char* line) {
1123 if (std::sscanf(line,
"Revision=%d", &revision) == 1) {
1124 layout_ini_.revision = revision;
1127 float pin_size = 0.0f;
1128 if (std::sscanf(line,
"PinSize=%f", &pin_size) == 1) {
1129 layout_ini_.pin_size = pin_size;
1132 constexpr size_t kPinSideKeyLength =
sizeof(kLayoutIniPinSideKey) - 1;
1133 if (std::strncmp(line, kLayoutIniPinSideKey, kPinSideKeyLength) == 0) {
1134 const char* side = line + kPinSideKeyLength;
1135 for (
size_t dir = 0; dir < kPinSideNames.size(); ++dir) {
1136 if (std::strcmp(side, kPinSideNames[dir]) == 0) {
1137 layout_ini_.pin_dir =
static_cast<ImGuiDir
>(dir);
1142 constexpr size_t kKeyLength =
sizeof(kLayoutIniInitializedKey) - 1;
1143 if (std::strncmp(line, kLayoutIniInitializedKey, kKeyLength) != 0) {
1146 const char* name = line + kKeyLength;
1147 for (
size_t index = 1; index <
kEditorNames.size(); ++index) {
1149 layout_ini_.initialized.push_back(
static_cast<EditorType>(index));
1155void LayoutManager::ApplyLayoutIniSettings() {
1156 ImGuiContext* ctx = ImGui::GetCurrentContext();
1160 ClearInitializationFlags();
1161 const bool first_load = !layout_ini_backup_checked_;
1162 layout_ini_backup_checked_ =
true;
1165 if (layout_ini_.pin_dir != ImGuiDir_None) {
1166 pin_dir_ = layout_ini_.pin_dir;
1168 if (layout_ini_.pin_size > 0.0f) {
1169 pin_size_ = layout_ini_.pin_size;
1172 if (layout_ini_.section_seen && layout_ini_.revision == kLayoutIniRevision) {
1174 for (
const EditorType type : layout_ini_.initialized) {
1175 if (ImGui::DockContextFindNodeByID(ctx, EditorDockspaceId(type))) {
1176 MarkLayoutInitialized(type);
1183 BackupLegacyLayoutIni();
1185 ctx->DockContext.NodesSettings.clear();
1186 ImGui::DockContextClearNodes(ctx, 0,
true);
1188 "Layout revision %d does not match %d; reset dock layouts",
1189 layout_ini_.revision, kLayoutIniRevision);
1192 if (ctx->Windows.Size > 0) {
1198void LayoutManager::WriteLayoutIniSettings(ImGuiTextBuffer* buf)
const {
1202 buf->appendf(
"[%s][Data]\n", kLayoutIniTypeName);
1203 buf->appendf(
"Revision=%d\n", kLayoutIniRevision);
1205 const auto type =
static_cast<EditorType>(index);
1206 if (IsLayoutInitialized(type) &&
1207 ImGui::DockBuilderGetNode(EditorDockspaceId(type))) {
1208 buf->appendf(
"%s%s\n", kLayoutIniInitializedKey,
kEditorNames[index]);
1211 if (pin_dir_ >= 0 &&
static_cast<size_t>(pin_dir_) < kPinSideNames.size()) {
1212 buf->appendf(
"%s%s\n", kLayoutIniPinSideKey,
1213 kPinSideNames[
static_cast<size_t>(pin_dir_)]);
1215 if (pin_size_ > 0.0f) {
1216 buf->appendf(
"PinSize=%.0f\n", pin_size_);
1222 ImGuiID dockspace_id) {
1227 StashLazyDefaultDockState();
1228 DisableLazyDefaultDocking();
1229 lazy_states_by_type_.erase(type);
1230 if (dockspace_id == EditorDockspaceId(type)) {
1231 for (
auto& [session, by_type] : lazy_default_dock_attempts_) {
1232 by_type.erase(type);
1235 lazy_default_dock_attempts_.clear();
1237 auto preset = LayoutPresets::GetDefaultPreset(type);
1239 if (!window_manager_) {
1241 "WorkspaceWindowManager not available, skipping dock layout for "
1243 static_cast<int>(type));
1247 const size_t session_id =
1248 window_manager_ ? window_manager_->GetActiveSessionId() : 0;
1249 const auto docked_panels = CollectDockedPanels(preset);
1254 const ImGuiViewport* viewport = ImGui::GetMainViewport();
1255 const float viewport_width = viewport ? viewport->WorkSize.x : 0.0f;
1256 const bool is_compact =
1257#if defined(__APPLE__) && TARGET_OS_IOS == 1
1259 static bool compact_mode =
true;
1260 constexpr float kEnterCompactWidth = 900.0f;
1261 constexpr float kExitCompactWidth = 940.0f;
1262 if (viewport_width <= 0.0f) {
1263 return compact_mode;
1265 compact_mode = compact_mode ? (viewport_width < kExitCompactWidth)
1266 : (viewport_width < kEnterCompactWidth);
1267 return compact_mode;
1270 (viewport_width > 0.0f && viewport_width < 900.0f);
1273 DockSplitNeeds needs{};
1274 DockSplitConfig cfg{};
1276 needs = ComputeSplitNeeds(docked_panels);
1277 cfg = DockSplitConfig::ForEditor(type);
1278 const ImGuiDockNode* dockspace_node =
1279 ImGui::DockBuilderGetNode(dockspace_id);
1280 const float dockspace_width =
1281 dockspace_node ? dockspace_node->Size.x : 0.0f;
1282 ApplyPreferredSplitWidths(&cfg, needs, viewport_width, dockspace_width,
1283 window_manager_, docked_panels);
1286 DockNodeIds ids = BuildDockTree(dockspace_id, needs, cfg);
1291 auto& state = lazy_default_dock_state_;
1292 state.enabled = preset.dock_only_default_visible_panels;
1293 state.compact = is_compact;
1294 state.editor_type = type;
1295 state.session_id = session_id;
1296 state.dockspace = dockspace_id;
1297 state.center = ids.center;
1298 state.left = ids.left;
1299 state.right = ids.right;
1300 state.bottom = ids.bottom;
1301 state.top = ids.top;
1302 state.left_top = ids.left_top;
1303 state.left_bottom = ids.left_bottom;
1304 state.right_top = ids.right_top;
1305 state.right_bottom = ids.right_bottom;
1306 state.left_ratio = cfg.left;
1307 state.right_ratio = cfg.right;
1308 state.bottom_ratio = cfg.bottom;
1309 state.top_ratio = cfg.top;
1310 state.vertical_split = cfg.vertical_split;
1311 if (state.enabled) {
1312 state.attempted_panels = lazy_default_dock_attempts_[session_id][type];
1317 case DockPosition::Left:
1318 if (ids.left_top || ids.left_bottom) {
1320 return ids.left_top ? ids.left_top : ids.left_bottom;
1322 return ids.left ? ids.left : ids.center;
1323 case DockPosition::Right:
1324 if (ids.right_top || ids.right_bottom) {
1325 return ids.right_top ? ids.right_top : ids.right_bottom;
1327 return ids.right ? ids.right : ids.center;
1328 case DockPosition::Bottom:
1329 return ids.bottom ? ids.bottom : ids.center;
1330 case DockPosition::Top:
1331 return ids.top ? ids.top : ids.center;
1332 case DockPosition::LeftTop:
1333 return ids.left_top ? ids.left_top : (ids.left ? ids.left : ids.center);
1334 case DockPosition::LeftBottom:
1335 return ids.left_bottom ? ids.left_bottom
1336 : (ids.left ? ids.left : ids.center);
1337 case DockPosition::RightTop:
1338 return ids.right_top ? ids.right_top
1339 : (ids.right ? ids.right : ids.center);
1340 case DockPosition::RightBottom:
1341 return ids.right_bottom ? ids.right_bottom
1342 : (ids.right ? ids.right : ids.center);
1343 case DockPosition::Center:
1349 std::vector<ImGuiID> auto_hide_nodes;
1352 for (
const auto& [panel_id, position] : docked_panels) {
1355 ? window_manager_->GetWindowDescriptor(session_id, panel_id)
1359 "Preset references window '%s' that is not registered (session "
1361 panel_id.c_str(), session_id);
1365 std::string window_title = window_manager_->GetWorkspaceWindowName(*desc);
1366 if (window_title.empty()) {
1368 "Cannot dock window '%s': missing window name (session %zu)",
1369 panel_id.c_str(), session_id);
1374 if (window_manager_->IsWindowPinned(session_id, panel_id)) {
1375 if (
const ImGuiID pin_node = EnsurePinNode()) {
1376 ImGui::DockBuilderDockWindow(window_title.c_str(), pin_node);
1381 const ImGuiID dock_id = get_dock_id(position);
1382 ImGui::DockBuilderDockWindow(window_title.c_str(), dock_id);
1384 if (
WindowContent* panel = window_manager_->GetWindowContent(panel_id);
1385 panel && panel->PreferAutoHideTabBar() &&
1386 std::find(auto_hide_nodes.begin(), auto_hide_nodes.end(), dock_id) ==
1387 auto_hide_nodes.end()) {
1388 if (ImGuiDockNode* node = ImGui::DockBuilderGetNode(dock_id)) {
1389 node->LocalFlags |= ImGuiDockNodeFlags_AutoHideTabBar;
1390 auto_hide_nodes.push_back(dock_id);
1396bool LayoutManager::DockDefaultPositionOnFirstOpen(
1397 size_t session_id,
const std::string& panel_id) {
1398 return DockPresetPositionOnFirstOpen(session_id, panel_id,
1402bool LayoutManager::DockPresetPositionOnPanelOpen(
size_t session_id,
1403 const std::string& panel_id) {
1404 return DockPresetPositionOnFirstOpen(session_id, panel_id,
1408bool LayoutManager::DockPresetPositionOnFirstOpen(
size_t session_id,
1409 const std::string& panel_id,
1410 bool include_default_visible,
1411 bool allow_redock) {
1412 auto& state = lazy_default_dock_state_;
1415 if ((!state.enabled && !allow_redock) || !window_manager_ ||
1416 panel_id.empty() || session_id != state.session_id ||
1417 !ImGui::DockBuilderGetNode(state.dockspace)) {
1422 LayoutPresets::GetDefaultPreset(state.editor_type);
1423 const bool is_default_visible =
1428 (!include_default_visible && is_default_visible)) {
1434 (!allow_redock && state.attempted_panels.contains(panel_id))) {
1439 window_manager_->GetWindowDescriptor(session_id, panel_id);
1443 const std::string window_title =
1444 window_manager_->GetWorkspaceWindowName(*desc);
1445 if (window_title.empty()) {
1451 if (!allow_redock) {
1452 if (ImGuiWindow* window = ImGui::FindWindowByName(window_title.c_str());
1453 window && window->DockId != 0 &&
1454 ImGui::DockBuilderGetNode(window->DockId)) {
1455 MarkLazyDefaultDockAttempted(panel_id);
1458 if (ImGuiWindowSettings* settings =
1459 ImGui::FindWindowSettingsByID(ImHashStr(window_title.c_str()));
1460 settings && settings->DockId != 0 &&
1461 ImGui::DockBuilderGetNode(settings->DockId)) {
1462 MarkLazyDefaultDockAttempted(panel_id);
1467 auto split_center = [&](ImGuiDir direction,
float ratio,
1468 ImGuiID* region) -> ImGuiID {
1469 if (*region != 0 && ImGui::DockBuilderGetNode(*region)) {
1472 if (state.center == 0 || !ImGui::DockBuilderGetNode(state.center)) {
1475 *region = ImGui::DockBuilderSplitNode(state.center, direction, ratio,
1476 nullptr, &state.center);
1480 auto resolve_vertical_region = [&](
bool right_side,
1481 bool want_bottom) -> ImGuiID {
1482 ImGuiID& region = right_side ? state.right : state.left;
1483 ImGuiID& top = right_side ? state.right_top : state.left_top;
1484 ImGuiID& bottom = right_side ? state.right_bottom : state.left_bottom;
1485 const ImGuiDir side_direction = right_side ? ImGuiDir_Right : ImGuiDir_Left;
1486 const float side_ratio = right_side ? state.right_ratio : state.left_ratio;
1487 if (!split_center(side_direction, side_ratio, ®ion)) {
1492 if (bottom != 0 && ImGui::DockBuilderGetNode(bottom)) {
1499 ImGuiID new_top = 0;
1500 bottom = ImGui::DockBuilderSplitNode(
1501 region, ImGuiDir_Down, state.vertical_split,
nullptr, &new_top);
1506 if (top != 0 && ImGui::DockBuilderGetNode(top)) {
1513 ImGuiID new_bottom = 0;
1514 top = ImGui::DockBuilderSplitNode(
1515 region, ImGuiDir_Up, 1.0f - state.vertical_split,
nullptr, &new_bottom);
1516 bottom = new_bottom;
1521 if (state.compact) {
1522 target = state.center;
1524 switch (position_it->second) {
1525 case DockPosition::Left:
1526 case DockPosition::LeftTop:
1527 target = resolve_vertical_region(
false,
false);
1529 case DockPosition::LeftBottom:
1530 target = resolve_vertical_region(
false,
true);
1532 case DockPosition::Right:
1533 case DockPosition::RightTop:
1534 target = resolve_vertical_region(
true,
false);
1536 case DockPosition::RightBottom:
1537 target = resolve_vertical_region(
true,
true);
1539 case DockPosition::Bottom:
1540 target = split_center(ImGuiDir_Down, state.bottom_ratio, &state.bottom);
1542 case DockPosition::Top:
1543 target = split_center(ImGuiDir_Up, state.top_ratio, &state.top);
1545 case DockPosition::Center:
1547 target = state.center;
1552 if (target == 0 || !ImGui::DockBuilderGetNode(target)) {
1556 MarkLazyDefaultDockAttempted(panel_id);
1557 ImGui::DockBuilderDockWindow(window_title.c_str(), target);
1558 if (
WindowContent* panel = window_manager_->GetWindowContent(panel_id);
1559 panel && panel->PreferAutoHideTabBar()) {
1560 if (ImGuiDockNode* node = ImGui::DockBuilderGetNode(target)) {
1561 node->LocalFlags |= ImGuiDockNodeFlags_AutoHideTabBar;
1564 ImGui::DockBuilderFinish(state.dockspace);
1568void LayoutManager::SaveCurrentLayout(
const std::string& name,
bool persist) {
1569 if (!window_manager_) {
1571 "Cannot save layout '%s': WorkspaceWindowManager not available",
1579 size_t session_id = window_manager_->GetActiveSessionId();
1580 auto visibility_state = window_manager_->SerializeVisibilityState(session_id);
1583 saved_layouts_[name] = visibility_state;
1584 saved_pinned_layouts_[name] = window_manager_->SerializePinnedState();
1585 layout_scopes_[name] = scope;
1588 size_t ini_size = 0;
1589 const char* ini_data = ImGui::SaveIniSettingsToMemory(&ini_size);
1590 if (ini_data && ini_size > 0) {
1591 saved_imgui_layouts_[name] = std::string(ini_data, ini_size);
1595 SaveLayoutsToDisk(scope);
1598 LOG_INFO(
"LayoutManager",
"Saved layout '%s' with %zu window states%s",
1599 name.c_str(), visibility_state.size(),
1600 persist ?
"" :
" (session-only)");
1603void LayoutManager::LoadLayout(
const std::string& name) {
1604 if (!window_manager_) {
1606 "Cannot load layout '%s': WorkspaceWindowManager not available",
1612 auto layout_it = saved_layouts_.find(name);
1613 if (layout_it == saved_layouts_.end()) {
1614 LOG_WARN(
"LayoutManager",
"Layout '%s' not found", name.c_str());
1618 const auto imgui_it = saved_imgui_layouts_.find(name);
1619 const bool has_imgui_layout =
1620 imgui_it != saved_imgui_layouts_.end() && !imgui_it->second.empty();
1623 if (has_imgui_layout) {
1624 DisableLazyDefaultDocking();
1628 size_t session_id = window_manager_->GetActiveSessionId();
1629 window_manager_->RestoreVisibilityState(session_id, layout_it->second,
1632 auto pinned_it = saved_pinned_layouts_.find(name);
1633 if (pinned_it != saved_pinned_layouts_.end()) {
1634 window_manager_->RestorePinnedState(pinned_it->second);
1638 if (has_imgui_layout) {
1639 ImGui::LoadIniSettingsFromMemory(imgui_it->second.c_str(),
1640 imgui_it->second.size());
1641 ProtectRestoredLayoutFromDefaultInitialization();
1644 LOG_INFO(
"LayoutManager",
"Loaded layout '%s'", name.c_str());
1647void LayoutManager::CaptureTemporarySessionLayout(
size_t session_id) {
1648 if (!window_manager_) {
1652 temp_session_id_ = session_id;
1653 temp_session_visibility_ =
1654 window_manager_->SerializeVisibilityState(session_id);
1655 temp_session_pinned_ = window_manager_->SerializePinnedState();
1657 size_t ini_size = 0;
1658 const char* ini_data = ImGui::SaveIniSettingsToMemory(&ini_size);
1659 if (ini_data && ini_size > 0) {
1660 temp_session_imgui_layout_ = std::string(ini_data, ini_size);
1662 temp_session_imgui_layout_.clear();
1665 has_temp_session_layout_ =
true;
1668 "Captured temporary session layout for session %zu (%zu panel states)",
1669 session_id, temp_session_visibility_.size());
1672bool LayoutManager::RestoreTemporarySessionLayout(
size_t session_id,
1673 bool clear_after_restore) {
1674 if (!window_manager_ || !has_temp_session_layout_) {
1678 if (session_id != temp_session_id_) {
1680 "Session layout snapshot belongs to session %zu, requested %zu",
1681 temp_session_id_, session_id);
1685 const bool has_imgui_layout = !temp_session_imgui_layout_.empty();
1686 if (has_imgui_layout) {
1687 DisableLazyDefaultDocking();
1690 window_manager_->RestoreVisibilityState(session_id, temp_session_visibility_,
1692 window_manager_->RestorePinnedState(temp_session_pinned_);
1694 if (has_imgui_layout) {
1695 ImGui::LoadIniSettingsFromMemory(temp_session_imgui_layout_.c_str(),
1696 temp_session_imgui_layout_.size());
1697 ProtectRestoredLayoutFromDefaultInitialization();
1700 if (clear_after_restore) {
1701 ClearTemporarySessionLayout();
1704 LOG_INFO(
"LayoutManager",
"Restored temporary session layout for session %zu",
1709void LayoutManager::ClearTemporarySessionLayout() {
1710 has_temp_session_layout_ =
false;
1711 temp_session_id_ = 0;
1712 temp_session_visibility_.clear();
1713 temp_session_pinned_.clear();
1714 temp_session_imgui_layout_.clear();
1717bool LayoutManager::SaveNamedSnapshot(
const std::string& name,
1718 size_t session_id) {
1719 if (!window_manager_ || name.empty()) {
1724 snapshot.
visibility = window_manager_->SerializeVisibilityState(session_id);
1725 snapshot.
pinned = window_manager_->SerializePinnedState();
1726 size_t ini_size = 0;
1727 const char* ini_data = ImGui::SaveIniSettingsToMemory(&ini_size);
1728 if (ini_data && ini_size > 0) {
1731 named_snapshots_[name] = std::move(snapshot);
1732 LOG_INFO(
"LayoutManager",
"Saved named snapshot '%s' for session %zu",
1733 name.c_str(), session_id);
1737bool LayoutManager::RestoreNamedSnapshot(
const std::string& name,
1739 bool remove_after_restore) {
1740 if (!window_manager_) {
1743 auto it = named_snapshots_.find(name);
1744 if (it == named_snapshots_.end()) {
1751 const bool has_imgui_layout = !snapshot.
imgui_layout.empty();
1752 if (has_imgui_layout) {
1753 DisableLazyDefaultDocking();
1755 window_manager_->RestoreVisibilityState(session_id, snapshot.
visibility,
1757 window_manager_->RestorePinnedState(snapshot.
pinned);
1758 if (has_imgui_layout) {
1759 ImGui::LoadIniSettingsFromMemory(snapshot.
imgui_layout.c_str(),
1761 ProtectRestoredLayoutFromDefaultInitialization();
1763 if (remove_after_restore) {
1764 named_snapshots_.erase(it);
1769bool LayoutManager::DeleteNamedSnapshot(
const std::string& name) {
1770 auto it = named_snapshots_.find(name);
1771 if (it == named_snapshots_.end()) {
1774 named_snapshots_.erase(it);
1778std::vector<std::string> LayoutManager::ListNamedSnapshots(
1779 size_t session_id)
const {
1780 std::vector<std::string> names;
1781 names.reserve(named_snapshots_.size());
1782 for (
const auto& [name, snapshot] : named_snapshots_) {
1783 if (snapshot.session_id == session_id) {
1784 names.push_back(name);
1787 std::sort(names.begin(), names.end());
1791bool LayoutManager::HasNamedSnapshot(
const std::string& name)
const {
1792 return named_snapshots_.find(name) != named_snapshots_.end();
1795bool LayoutManager::DeleteLayout(
const std::string& name) {
1796 auto layout_it = saved_layouts_.find(name);
1797 if (layout_it == saved_layouts_.end()) {
1798 LOG_WARN(
"LayoutManager",
"Cannot delete layout '%s': not found",
1804 auto scope_it = layout_scopes_.find(name);
1805 if (scope_it != layout_scopes_.end()) {
1806 scope = scope_it->second;
1809 saved_layouts_.erase(layout_it);
1810 saved_imgui_layouts_.erase(name);
1811 saved_pinned_layouts_.erase(name);
1812 layout_scopes_.erase(name);
1814 SaveLayoutsToDisk(scope);
1816 LOG_INFO(
"LayoutManager",
"Deleted layout '%s'", name.c_str());
1820std::vector<LayoutProfile> LayoutManager::GetBuiltInProfiles() {
1824 .description =
"Focused editing workspace with minimal panel noise",
1825 .preset_name =
"Minimal",
1826 .open_agent_chat =
false},
1829 .description =
"Debugger-first workspace for tracing and memory tools",
1830 .preset_name =
"Developer",
1831 .open_agent_chat =
false},
1834 .description =
"Map-centric layout for overworld or dungeon workflows",
1835 .preset_name =
"Overworld Expert",
1836 .open_agent_chat =
false},
1839 .description =
"Collaboration-heavy layout with agent-centric tooling",
1840 .preset_name =
"Modder",
1841 .open_agent_chat =
true},
1845bool LayoutManager::ApplyBuiltInProfile(
const std::string& profile_id,
1849 if (!window_manager_) {
1851 "Cannot apply profile '%s': WorkspaceWindowManager not available",
1852 profile_id.c_str());
1858 for (
const auto& profile : GetBuiltInProfiles()) {
1859 if (profile.id == profile_id) {
1860 matched_profile = profile;
1867 LOG_WARN(
"LayoutManager",
"Unknown layout profile id: %s",
1868 profile_id.c_str());
1872 const std::string resolved_preset =
1873 ResolveProfilePresetName(profile_id, editor_type);
1874 if (!resolved_preset.empty()) {
1879 if (!LayoutPresets::TryGetNamedPreset(matched_profile.
preset_name, &preset)) {
1880 LOG_WARN(
"LayoutManager",
"Unable to resolve preset '%s' for profile '%s'",
1881 matched_profile.
preset_name.c_str(), profile_id.c_str());
1887 DisableLazyDefaultDocking();
1889 window_manager_->HideAllWindowsInSession(session_id);
1891 window_manager_->OpenWindow(session_id, panel_id);
1897 *out_profile = matched_profile;
1900 LOG_INFO(
"LayoutManager",
"Applied profile '%s' via preset '%s'",
1901 profile_id.c_str(), matched_profile.
preset_name.c_str());
1905std::vector<std::string> LayoutManager::GetSavedLayoutNames()
const {
1906 std::vector<std::string> names;
1907 names.reserve(saved_layouts_.size());
1908 for (
const auto& [name, _] : saved_layouts_) {
1909 names.push_back(name);
1914bool LayoutManager::HasLayout(
const std::string& name)
const {
1915 return saved_layouts_.find(name) != saved_layouts_.end();
1918void LayoutManager::LoadLayoutsFromDisk() {
1919 saved_layouts_.clear();
1920 saved_imgui_layouts_.clear();
1921 saved_pinned_layouts_.clear();
1922 layout_scopes_.clear();
1924 LoadLayoutsFromDiskInternal(LayoutScope::kGlobal,
false);
1925 if (!project_layout_key_.empty()) {
1926 LoadLayoutsFromDiskInternal(LayoutScope::kProject,
true);
1930void LayoutManager::SetProjectLayoutKey(
const std::string& key) {
1935 project_layout_key_ = key;
1936 LoadLayoutsFromDisk();
1939void LayoutManager::UseGlobalLayouts() {
1940 project_layout_key_.clear();
1941 LoadLayoutsFromDisk();
1945 return project_layout_key_.empty() ? LayoutScope::kGlobal
1946 : LayoutScope::kProject;
1949void LayoutManager::LoadLayoutsFromDiskInternal(
LayoutScope scope,
bool merge) {
1950 std::filesystem::path layout_path =
1951 GetLayoutsFilePath(scope, project_layout_key_);
1952 if (layout_path.empty()) {
1956 if (!std::filesystem::exists(layout_path)) {
1958 LOG_INFO(
"LayoutManager",
"No layouts file at %s",
1959 layout_path.string().c_str());
1965 std::ifstream file(layout_path);
1966 if (!file.is_open()) {
1967 LOG_WARN(
"LayoutManager",
"Failed to open layouts file: %s",
1968 layout_path.string().c_str());
1976 LOG_WARN(
"LayoutManager",
"Layouts file missing 'layouts' object: %s",
1977 layout_path.string().c_str());
1981 for (
auto& [name, entry] : root[
"layouts"].
items()) {
1982 if (!entry.is_object()) {
1986 std::unordered_map<std::string, bool> windows;
1987 std::unordered_map<std::string, bool> pinned;
1989 JsonToWindowMap(entry, &windows);
1990 if (entry.contains(
"pinned")) {
1991 JsonToBoolMap(entry[
"pinned"], &pinned);
1994 saved_layouts_[name] = std::move(windows);
1995 saved_pinned_layouts_[name] = std::move(pinned);
1996 layout_scopes_[name] = scope;
1998 if (entry.contains(
"imgui_ini") && entry[
"imgui_ini"].is_string()) {
1999 saved_imgui_layouts_[name] = entry[
"imgui_ini"].get<std::string>();
2001 saved_imgui_layouts_.erase(name);
2005 LOG_INFO(
"LayoutManager",
"Loaded layouts from %s",
2006 layout_path.string().c_str());
2007 }
catch (
const std::exception& e) {
2008 LOG_WARN(
"LayoutManager",
"Failed to load layouts: %s", e.what());
2013 std::filesystem::path layout_path =
2014 GetLayoutsFilePath(scope, project_layout_key_);
2015 if (layout_path.empty()) {
2016 LOG_WARN(
"LayoutManager",
"No layout path resolved for scope");
2023 LOG_WARN(
"LayoutManager",
"Failed to create layout directory: %s",
2024 status.ToString().c_str());
2030 root[
"version"] = 2;
2033 for (
const auto& [name, windows] : saved_layouts_) {
2034 auto scope_it = layout_scopes_.find(name);
2035 if (scope_it != layout_scopes_.end() && scope_it->second != scope) {
2040 entry[kWindowsKey] = BoolMapToJson(windows);
2042 auto pinned_it = saved_pinned_layouts_.find(name);
2043 if (pinned_it != saved_pinned_layouts_.end()) {
2044 entry[
"pinned"] = BoolMapToJson(pinned_it->second);
2047 auto imgui_it = saved_imgui_layouts_.find(name);
2048 if (imgui_it != saved_imgui_layouts_.end()) {
2049 entry[
"imgui_ini"] = imgui_it->second;
2052 root[
"layouts"][name] = entry;
2055 std::ofstream file(layout_path);
2056 if (!file.is_open()) {
2057 LOG_WARN(
"LayoutManager",
"Failed to open layouts file for write: %s",
2058 layout_path.string().c_str());
2061 file << root.
dump(2);
2063 }
catch (
const std::exception& e) {
2064 LOG_WARN(
"LayoutManager",
"Failed to save layouts: %s", e.what());
2069 DisableLazyDefaultDocking();
2070 layouts_initialized_[type] =
false;
2071 LOG_INFO(
"LayoutManager",
"Reset layout for editor type %d",
2072 static_cast<int>(type));
2076 auto it = layouts_initialized_.find(type);
2077 return it != layouts_initialized_.end() && it->second;
2081 layouts_initialized_[type] =
true;
2082 LOG_INFO(
"LayoutManager",
"Marked layout for editor type %d as initialized",
2083 static_cast<int>(type));
2086void LayoutManager::ClearInitializationFlags() {
2087 DisableLazyDefaultDocking();
2088 lazy_states_by_type_.clear();
2089 layouts_initialized_.clear();
2090 LOG_INFO(
"LayoutManager",
"Cleared all layout initialization flags");
2093std::string LayoutManager::GetWindowTitle(
const std::string& card_id)
const {
2094 if (!window_manager_) {
2098 const size_t session_id = window_manager_->GetActiveSessionId();
2099 return window_manager_->GetWorkspaceWindowName(session_id, card_id);
2108 return ImGuiDir_Left;
2110 return ImGuiDir_Right;
2114 return ImGuiDir_Down;
2116 return ImGuiDir_Left;
2123 if (
const auto* desc =
2136 }
else if (!panel.
icon.empty()) {
2148 const std::function<
void(ImGuiID, ImGuiID, ImGuiID, std::vector<ImGuiID>)>&
2151 if (node.
type == NodeType::kLeaf) {
2152 std::vector<ImGuiID> tab_ids;
2153 ImGuiID selected_id = 0;
2154 ImGuiID selected_window_id = 0;
2156 for (
const auto& panel : node.
panels) {
2158 if (!title.empty()) {
2159 ImGui::DockBuilderDockWindow(title.c_str(), target_id);
2160 const ImGuiID window_id = ImHashStr(title.c_str());
2161 const ImGuiID tab_id = ImHashStr(
"#TAB", 0, window_id);
2162 tab_ids.push_back(tab_id);
2164 selected_id = tab_id;
2165 selected_window_id = window_id;
2170 if (selected_id && tab_ids.size() > 1) {
2171 queue_selection(target_id, selected_id, selected_window_id,
2172 std::move(tab_ids));
2179 const float ratio = std::clamp(node.
split_ratio, 0.05f, 0.95f);
2180 ImGuiID id_other = 0;
2181 const ImGuiID id_at_dir =
2182 ImGui::DockBuilderSplitNode(target_id, dir, ratio,
nullptr, &id_other);
2200 const ImGuiDockNode* node,
2201 const std::unordered_map<std::string, PanelLookupEntry>& by_window_name) {
2203 return layout_designer::DockNode::MakeLeaf({});
2206 if (node->IsSplitNode()) {
2212 const bool vertical = node->SplitAxis == ImGuiAxis_Y;
2214 vertical ? layout_designer::SplitDirection::kUp
2215 : layout_designer::SplitDirection::kLeft;
2218 if (node->ChildNodes[0] && node->ChildNodes[1]) {
2219 if (vertical && node->Size.y > 0.0f) {
2220 ratio = node->ChildNodes[0]->Size.y / node->Size.y;
2221 }
else if (!vertical && node->Size.x > 0.0f) {
2222 ratio = node->ChildNodes[0]->Size.x / node->Size.x;
2225 ratio = std::clamp(ratio, 0.05f, 0.95f);
2226 return layout_designer::DockNode::MakeSplit(dir, ratio, std::move(child_a),
2227 std::move(child_b));
2231 std::vector<layout_designer::PanelEntry> panels;
2232 panels.reserve(
static_cast<size_t>(node->Windows.Size));
2233 int selected_index = -1;
2234 for (
int i = 0; i < node->Windows.Size; ++i) {
2235 const ImGuiWindow* w = node->Windows[i];
2238 auto it = by_window_name.find(std::string(w->Name));
2239 if (it == by_window_name.end())
2241 if (node->SelectedTabId != 0 && w->ID == node->SelectedTabId) {
2242 selected_index =
static_cast<int>(panels.size());
2245 {it->second.panel_id, it->second.display_name, it->second.icon});
2247 auto leaf = layout_designer::DockNode::MakeLeaf(std::move(panels));
2248 if (selected_index >= 0 &&
2249 selected_index <
static_cast<int>(leaf->panels.size())) {
2250 leaf->active_tab_index = selected_index;
2258 std::vector<std::string>* out) {
2259 if (node.
type == layout_designer::DockNode::Type::kLeaf) {
2260 for (
const auto& p : node.
panels) {
2261 out->push_back(p.panel_id);
2274 ImGuiID dockspace_id) {
2275 if (!window_manager_) {
2276 return absl::FailedPreconditionError(
2277 "LayoutManager::ApplyDockTree: WorkspaceWindowManager not bound");
2279 std::string validation_error;
2280 if (!tree.
Validate(&validation_error)) {
2281 return absl::InvalidArgumentError(
"LayoutManager::ApplyDockTree: " +
2287 DisableLazyDefaultDocking();
2288 pending_dock_tab_selections_.clear();
2300 std::vector<std::string> panel_ids;
2302 CollectPanelIdsInSubtree(*tree.
root, &panel_ids);
2304 const std::unordered_set<std::string> tree_id_set(panel_ids.begin(),
2306 const size_t session_id = window_manager_->GetActiveSessionId();
2307 for (
const auto&
id : panel_ids) {
2314 if (window_manager_->IsWindowOpen(session_id,
id))
2316 window_manager_->OpenWindow(session_id,
id);
2318 for (
const std::string&
id :
2319 window_manager_->GetWindowsInSession(session_id)) {
2320 if (tree_id_set.count(
id) > 0)
2322 if (window_manager_->IsWindowPinned(session_id,
id))
2329 if (!window_manager_->IsWindowOpen(session_id,
id))
2331 window_manager_->CloseWindow(
id);
2334 ImGui::DockBuilderRemoveNode(dockspace_id);
2335 ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
2337 ImGui::DockBuilderSetNodeSize(dockspace_id, EditorDockspaceSize());
2339 ApplyDockNodeRecursive(
2340 window_manager_, session_id, *tree.
root, dockspace_id,
2341 [
this, session_id](ImGuiID node_id, ImGuiID selected_id,
2342 ImGuiID selected_window_id,
2343 std::vector<ImGuiID> tab_ids) {
2344 pending_dock_tab_selections_.push_back(
2345 {node_id, selected_id, selected_window_id, std::move(tab_ids),
2346 session_id, ImGui::GetFrameCount()});
2348 ImGui::DockBuilderFinish(dockspace_id);
2350 last_dockspace_id_ = dockspace_id;
2363 ProtectRestoredLayoutFromDefaultInitialization();
2365 return absl::OkStatus();
2368void LayoutManager::ProcessPendingDockTabSelections() {
2369 const ImGuiContext* context = ImGui::GetCurrentContext();
2370 if (pending_dock_tab_selections_.empty() || !context ||
2371 !context->WithinFrameScope) {
2374 const ImGuiIO& io = ImGui::GetIO();
2375 bool input = io.MouseWheel != 0.0f || io.MouseWheelH != 0.0f;
2376 for (
int button = 0; button < ImGuiMouseButton_COUNT; ++button) {
2377 input |= ImGui::IsMouseClicked(button);
2379 for (
int key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; ++key) {
2380 input |= ImGui::IsKeyPressed(
static_cast<ImGuiKey
>(key),
false);
2382 const int frame = ImGui::GetFrameCount();
2383 std::erase_if(pending_dock_tab_selections_, [&](
const auto& pending) {
2384 if (!window_manager_ ||
2385 window_manager_->GetActiveSessionId() != pending.session_id ||
2386 (frame > pending.applied_frame && input)) {
2389 ImGuiDockNode* node = ImGui::DockBuilderGetNode(pending.node_id);
2393 if (frame <= pending.applied_frame || !node->TabBar ||
2394 !node->TabBar->SelectedTabId) {
2397 for (
const ImGuiID tab_id : pending.tab_ids) {
2398 if (!ImGui::TabBarFindTabByID(node->TabBar, tab_id)) {
2402 ImGuiWindow* selected = ImGui::FindWindowByID(pending.selected_window_id);
2403 if (!selected || selected->DockNode != node ||
2404 selected->TabId != pending.selected_id) {
2407 ImGuiWindow* focused = GImGui->NavWindow;
2408 if (focused && focused->RootWindow->DockNode == node &&
2409 focused->RootWindow != selected->RootWindow) {
2410 if (!GImGui->OpenPopupStack.empty()) {
2413 ImGui::FocusWindow(selected);
2415 node->TabBar->NextSelectedTabId = selected->TabId;
2420absl::Status LayoutManager::MaybeReapplyStartupLayout(
UserSettings* settings) {
2421 if (startup_layout_consumed_) {
2422 return absl::OkStatus();
2424 if (settings ==
nullptr) {
2425 startup_layout_consumed_ =
true;
2426 return absl::OkStatus();
2432 startup_layout_consumed_ =
true;
2433 return absl::OkStatus();
2436 const ImGuiID target_dockspace = GetMainDockspaceId();
2437 if (target_dockspace == 0) {
2440 return absl::OkStatus();
2444 const auto it = named_layouts.find(name);
2445 if (it == named_layouts.end()) {
2446 startup_layout_consumed_ =
true;
2448 "LayoutManager: startup layout '%s' missing from "
2449 "named_layouts; falling through to default.",
2451 return absl::NotFoundError(absl::StrCat(
"startup layout \"", name,
2452 "\" not found in named_layouts"));
2455 nlohmann::json parsed;
2457 parsed = nlohmann::json::parse(it->second);
2458 }
catch (
const nlohmann::json::parse_error& e) {
2459 startup_layout_consumed_ =
true;
2460 util::logf(
"LayoutManager: startup layout '%s' JSON parse error: %s",
2461 name.c_str(), e.what());
2462 return absl::InvalidArgumentError(
2463 absl::StrCat(
"startup layout \"", name,
"\" parse error: ", e.what()));
2466 auto tree_or = layout_designer::DockTreeFromJson(parsed);
2467 if (!tree_or.ok()) {
2468 startup_layout_consumed_ =
true;
2469 util::logf(
"LayoutManager: startup layout '%s' failed to parse: %s",
2470 name.c_str(), std::string(tree_or.status().message()).c_str());
2471 return tree_or.status();
2474 std::string validation_error;
2475 if (!tree_or->Validate(&validation_error)) {
2476 startup_layout_consumed_ =
true;
2477 util::logf(
"LayoutManager: startup layout '%s' failed validation: %s",
2478 name.c_str(), validation_error.c_str());
2479 return absl::InvalidArgumentError(absl::StrCat(
2480 "startup layout \"", name,
"\" validation failed: ", validation_error));
2483 absl::Status apply_status = ApplyDockTree(*tree_or, target_dockspace);
2484 startup_layout_consumed_ =
true;
2485 if (!apply_status.ok()) {
2486 util::logf(
"LayoutManager: startup layout '%s' ApplyDockTree failed: %s",
2487 name.c_str(), std::string(apply_status.message()).c_str());
2493 return apply_status;
2496absl::StatusOr<layout_designer::DockTree> LayoutManager::CaptureDockTree(
2497 ImGuiID dockspace_id)
const {
2498 if (!window_manager_) {
2499 return absl::FailedPreconditionError(
2500 "LayoutManager::CaptureDockTree: WorkspaceWindowManager not bound");
2502 ImGuiDockNode* root_node = ImGui::DockBuilderGetNode(dockspace_id);
2504 return absl::NotFoundError(
2505 "LayoutManager::CaptureDockTree: no dock node at given id");
2508 std::unordered_map<std::string, PanelLookupEntry> by_window_name;
2509 for (
const auto& [panel_id, desc] :
2510 window_manager_->GetAllWindowDescriptors()) {
2511 const std::string title = window_manager_->GetWorkspaceWindowName(desc);
2514 by_window_name.emplace(
2515 title, PanelLookupEntry{panel_id, desc.display_name, desc.icon});
2519 tree.
root = CaptureDockNodeRecursive(root_node, by_window_name);
2521 tree.
root = layout_designer::DockNode::MakeLeaf({});
std::string dump(int=-1, char=' ', bool=false, int=0) const
bool contains(const std::string &) const
Manages ImGui DockBuilder layouts for each editor type.
void DockOpenPresetPanels(EditorType type)
EditorType current_editor_type_
void DisableLazyDefaultDocking()
std::vector< PendingDockTabSelection > pending_dock_tab_selections_
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_
bool DockPresetPositionOnFirstOpen(size_t session_id, const std::string &panel_id, bool include_default_visible, bool allow_redock=false)
EditorType active_editor_type_
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.
bool RestoreLazyDefaultDockState(EditorType type, ImGuiID dockspace_id)
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)
ImVec2 EditorDockspaceSize() const
void MarkLazyDefaultDockAttempted(const std::string &panel_id)
static std::vector< std::string > GetDefaultWindows(EditorType type)
static PanelLayoutPreset GetDefaultPreset(EditorType type)
Get the default layout preset for an editor type.
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,...)
constexpr char kOuterDockspaceName[]
bool IsLeftDockPosition(DockPosition pos)
void ApplyDockNodeRecursive(WorkspaceWindowManager *wm, size_t session_id, const layout_designer::DockNode &node, ImGuiID target_id, const std::function< void(ImGuiID, ImGuiID, ImGuiID, std::vector< ImGuiID >)> &queue_selection)
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)
ImGuiDir ChildSide(const ImGuiDockNode *parent, const ImGuiDockNode *child)
ImGuiAxis PinAxis(ImGuiDir dir)
ImGuiID DockRootId(ImGuiID node_id)
void * LayoutIniReadOpen(ImGuiContext *, ImGuiSettingsHandler *handler, const char *name)
std::string ResolveProfilePresetName(const std::string &profile_id, EditorType editor_type)
void LayoutIniClearAll(ImGuiContext *, ImGuiSettingsHandler *handler)
void CollectPanelIdsInSubtree(const layout_designer::DockNode &node, std::vector< std::string > *out)
void LayoutIniApplyAll(ImGuiContext *, ImGuiSettingsHandler *handler)
constexpr char kEditorDockspacePrefix[]
void LayoutIniReadLine(ImGuiContext *, ImGuiSettingsHandler *, void *entry, const char *line)
yaze::Json BoolMapToJson(const std::unordered_map< std::string, bool > &map)
constexpr char kLegacyPanelsKey[]
constexpr std::array< const char *, 4 > kPinSideNames
constexpr char kLayoutIniInitializedKey[]
constexpr float kPinRatio
ImGuiDockNode * FindCentralLeaf(ImGuiDockNode *node)
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 BackupLegacyLayoutIni()
void LayoutIniWriteAll(ImGuiContext *, ImGuiSettingsHandler *handler, ImGuiTextBuffer *buf)
constexpr char kLayoutIniTypeName[]
constexpr float kPinMaxRatio
void JsonToBoolMap(const yaze::Json &obj, std::unordered_map< std::string, bool > *map)
constexpr char kLayoutIniPinSideKey[]
bool ShouldDockPanelInDefaultLayout(const PanelLayoutPreset &preset, const std::string &panel_id)
constexpr char kEditorHostWindowName[]
constexpr char kWindowsKey[]
std::unique_ptr< layout_designer::DockNode > CaptureDockNodeRecursive(const ImGuiDockNode *node, const std::unordered_map< std::string, PanelLookupEntry > &by_window_name)
constexpr float kPinMinRatio
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)
ImGuiDir DockspaceEdgeSide(const ImGuiDockNode *node, float *size)
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.
constexpr std::array< const char *, 15 > kEditorNames
size_t EditorTypeIndex(EditorType type)
constexpr size_t kEditorTypeCount
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