yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
layout_manager.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <cstdio>
6#include <cstring>
7#include <filesystem>
8#include <fstream>
9#include <functional>
10#include <string>
11#include <unordered_set>
12#include <utility>
13
14#include "absl/strings/str_cat.h"
20#include "imgui/imgui.h"
21#include "imgui/imgui_internal.h"
22
23#if defined(__APPLE__)
24#include <TargetConditionals.h>
25#endif
26#include "util/json.h"
27#include "util/log.h"
28#include "util/platform_paths.h"
29
30namespace yaze {
31namespace editor {
32
33namespace {
34
35constexpr char kLegacyPanelsKey[] = "panels";
36constexpr char kWindowsKey[] = "windows";
37
38// Helper function to show default windows from LayoutPresets
40 EditorType type) {
41 if (!registry)
42 return;
43
44 auto default_windows = LayoutPresets::GetDefaultWindows(type);
45 for (const auto& window_id : default_windows) {
46 registry->OpenWindow(window_id);
47 }
48
49 LOG_INFO("LayoutManager", "Showing %zu default windows for editor type %d",
50 default_windows.size(), static_cast<int>(type));
51}
52
53yaze::Json BoolMapToJson(const std::unordered_map<std::string, bool>& map) {
55 for (const auto& [key, value] : map) {
56 obj[key] = value;
57 }
58 return obj;
59}
60
61void JsonToBoolMap(const yaze::Json& obj,
62 std::unordered_map<std::string, bool>* map) {
63 if (!map || !obj.is_object()) {
64 return;
65 }
66 map->clear();
67 for (const auto& [key, value] : obj.items()) {
68 if (value.is_boolean()) {
69 (*map)[key] = value.get<bool>();
70 }
71 }
72}
73
74void JsonToWindowMap(const yaze::Json& entry,
75 std::unordered_map<std::string, bool>* windows) {
76 if (!windows || !entry.is_object()) {
77 return;
78 }
79 if (entry.contains(kWindowsKey)) {
80 JsonToBoolMap(entry[kWindowsKey], windows);
81 return;
82 }
83 if (entry.contains(kLegacyPanelsKey)) {
84 JsonToBoolMap(entry[kLegacyPanelsKey], windows);
85 return;
86 }
87 windows->clear();
88}
89
90std::filesystem::path GetLayoutsFilePath(LayoutScope scope,
91 const std::string& project_key) {
92 auto layouts_dir = util::PlatformPaths::GetAppDataSubdirectory("layouts");
93 if (!layouts_dir.ok()) {
94 return {};
95 }
96
97 if (scope == LayoutScope::kProject && !project_key.empty()) {
98 std::filesystem::path projects_dir = *layouts_dir / "projects";
100 return projects_dir / (project_key + ".json");
101 }
102
103 if (scope == LayoutScope::kProject) {
104 return {};
105 }
106
107 return *layouts_dir / "layouts.json";
108}
109
110std::string ResolveProfilePresetName(const std::string& profile_id,
111 EditorType editor_type) {
112 if (profile_id == "mapping") {
113 if (editor_type == EditorType::kDungeon) {
114 return "Dungeon Expert";
115 }
116 return "Overworld Expert";
117 }
118 if (profile_id == "code") {
119 return "Minimal";
120 }
121 if (profile_id == "debug") {
122 return "Developer";
123 }
124 if (profile_id == "chat") {
125 return "Modder";
126 }
127 return "";
128}
129
130} // namespace
131
135
137 ImGuiID dockspace_id) {
139 // Each editor owns its dockspace, so resume that editor's stashed geometry
140 // instead of treating another editor's state as stale.
141 const bool restored = RestoreLazyDefaultDockState(type, dockspace_id);
142 auto& state = lazy_default_dock_state_;
143 if (!restored || !preset.dock_only_default_visible_panels ||
144 !window_manager_ || state.dockspace != dockspace_id ||
145 !ImGui::DockBuilderGetNode(state.dockspace) ||
146 !ImGui::DockBuilderGetNode(state.center)) {
147 state.enabled = false;
148 return;
149 }
150
151 const size_t session_id = window_manager_->GetActiveSessionId();
152 state.enabled = true;
153 state.editor_type = type;
154 state.session_id = session_id;
155 state.attempted_panels = lazy_default_dock_attempts_[session_id][type];
156}
157
158void LayoutManager::MarkLazyDefaultDockAttempted(const std::string& panel_id) {
159 auto& state = lazy_default_dock_state_;
160 state.attempted_panels.insert(panel_id);
161 lazy_default_dock_attempts_[state.session_id][state.editor_type].insert(
162 panel_id);
163}
164
166 if (!window_manager_) {
167 return;
168 }
169
170 const size_t session_id = window_manager_->GetActiveSessionId();
172 const auto dock_if_open = [&](const std::string& panel_id) {
173 if (window_manager_->IsWindowOpen(session_id, panel_id)) {
174 DockPresetPositionOnFirstOpen(session_id, panel_id,
175 /*include_default_visible=*/true);
176 }
177 };
178 for (const auto& panel_id : preset.default_visible_panels) {
179 dock_if_open(panel_id);
180 }
181 for (const auto& panel_id : preset.optional_panels) {
182 dock_if_open(panel_id);
183 }
184}
185
193
195 ImGuiID dockspace_id) {
196 // Phase 8.2 review (2026-04-25): one-shot protection set by
197 // MaybeReapplyStartupLayout. The very first lazy preset init after a
198 // successful startup-reapply must NOT rebuild the dockspace —
199 // doing so would silently overwrite the user's saved layout on
200 // their first editor activation. Consume the flag, mark the type
201 // initialized so subsequent activations behave normally, and bail.
205 last_dockspace_id_ = dockspace_id;
208 LOG_INFO("LayoutManager",
209 "Suppressed lazy preset init for editor type %d to preserve "
210 "startup-reapplied custom layout",
211 static_cast<int>(type));
212 return;
213 }
214
215 // Refresh the active editor/session even when its default dock tree was
216 // initialized earlier. Lazy optional-panel placement uses the live tree but
217 // keeps its one-shot history scoped to this ROM session and editor.
218 last_dockspace_id_ = dockspace_id;
220 active_editor_type_ = type;
221
222 // Don't reinitialize if already set up
223 if (IsLayoutInitialized(type)) {
224 RefreshLazyDefaultDockingContext(type, dockspace_id);
225 // Session visibility restoration intentionally does not publish panel-open
226 // events. Sweep the restored state after refreshing the session context so
227 // an already-visible optional panel still receives its one-shot default.
229 LOG_INFO("LayoutManager",
230 "Layout for editor type %d already initialized, skipping",
231 static_cast<int>(type));
232 return;
233 }
234
235 LOG_INFO("LayoutManager", "Initializing layout for editor type %d",
236 static_cast<int>(type));
237
238 // Clear existing layout for this dockspace
240 ImGui::DockBuilderRemoveNode(dockspace_id);
241 ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
242
243 ImGui::DockBuilderSetNodeSize(dockspace_id, EditorDockspaceSize());
244
245 // Build layout based on editor type using generic builder
246 BuildLayoutFromPreset(type, dockspace_id);
247
248 // Show default windows from LayoutPresets (single source of truth)
249 ShowDefaultWindowsForEditor(window_manager_, type);
250
251 // Finalize the layout
252 ImGui::DockBuilderFinish(dockspace_id);
253
254 // Visibility may have been restored before this editor's first layout
255 // build. Give already-open optional panels the same first-open placement.
257
258 // Mark as initialized
260}
261
262void LayoutManager::RebuildLayout(EditorType type, ImGuiID dockspace_id) {
264 // Validate dockspace exists
265 ImGuiDockNode* node = ImGui::DockBuilderGetNode(dockspace_id);
266 if (!node) {
267 LOG_ERROR("LayoutManager",
268 "Cannot rebuild layout: dockspace ID %u not found", dockspace_id);
269 return;
270 }
271
272 LOG_INFO("LayoutManager", "Forcing rebuild of layout for editor type %d",
273 static_cast<int>(type));
274
275 // Store dockspace ID and current editor type
276 last_dockspace_id_ = dockspace_id;
278 active_editor_type_ = type;
279
280 // Clear the layout initialization flag to force rebuild
281 layouts_initialized_[type] = false;
282
283 // Clear existing layout for this dockspace
284 ImGui::DockBuilderRemoveNode(dockspace_id);
285 ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
286 ImGui::DockBuilderSetNodeSize(dockspace_id, EditorDockspaceSize());
287
288 // Build layout based on editor type using generic builder
289 BuildLayoutFromPreset(type, dockspace_id);
290
291 // Show default cards from LayoutPresets (single source of truth)
292 ShowDefaultWindowsForEditor(window_manager_, type);
293
294 // Finalize the layout
295 ImGui::DockBuilderFinish(dockspace_id);
296
298
299 // Mark as initialized
301
302 LOG_INFO("LayoutManager", "Layout rebuild complete for editor type %d",
303 static_cast<int>(type));
304}
305
306namespace {
307
309 float left = 0.17f;
310 float right = 0.24f;
311 float bottom = 0.22f;
312 float top = 0.12f;
313 float vertical_split = 0.52f;
314
315 // Per-editor type configuration
317 DockSplitConfig cfg;
318 switch (type) {
319 case EditorType::kDungeon:
320 // Dungeon: reserve more right-side space for object/property workflows.
321 cfg.left = 0.16f;
322 cfg.right = 0.26f;
323 cfg.bottom = 0.20f;
324 cfg.vertical_split = 0.50f;
325 break;
326 case EditorType::kOverworld:
327 cfg.left = 0.22f;
328 cfg.right = 0.26f;
329 cfg.bottom = 0.23f;
330 cfg.vertical_split = 0.42f;
331 break;
332 case EditorType::kGraphics:
333 cfg.left = 0.16f;
334 cfg.right = 0.24f;
335 cfg.bottom = 0.20f;
336 break;
337 case EditorType::kPalette:
338 cfg.left = 0.16f;
339 cfg.right = 0.22f;
340 cfg.bottom = 0.20f;
341 break;
342 case EditorType::kSprite:
343 cfg.left = 0.18f;
344 cfg.right = 0.24f;
345 cfg.bottom = 0.20f;
346 break;
347 case EditorType::kScreen:
348 cfg.left = 0.16f;
349 cfg.right = 0.22f;
350 cfg.bottom = 0.20f;
351 break;
352 case EditorType::kMessage:
353 cfg.left = 0.20f;
354 cfg.right = 0.26f;
355 cfg.bottom = 0.18f;
356 break;
357 case EditorType::kAssembly:
358 cfg.left = 0.24f;
359 cfg.right = 0.16f;
360 cfg.bottom = 0.20f;
361 break;
362 case EditorType::kEmulator:
363 cfg.left = 0.14f;
364 cfg.right = 0.28f;
365 cfg.bottom = 0.22f;
366 break;
367 case EditorType::kAgent:
368 cfg.left = 0.16f;
369 cfg.right = 0.30f;
370 cfg.bottom = 0.24f;
371 break;
372 default:
373 // Use defaults
374 break;
375 }
376 return cfg;
377 }
378};
379
381 ImGuiID center = 0;
382 ImGuiID left = 0;
383 ImGuiID right = 0;
384 ImGuiID bottom = 0;
385 ImGuiID top = 0;
386 ImGuiID left_top = 0;
387 ImGuiID left_bottom = 0;
388 ImGuiID right_top = 0;
389 ImGuiID right_bottom = 0;
390};
391
393 bool left = false;
394 bool right = false;
395 bool bottom = false;
396 bool top = false;
397 bool left_top = false;
398 bool left_bottom = false;
399 bool right_top = false;
400 bool right_bottom = false;
401};
402
404 const std::string& panel_id) {
406 return true;
407 }
408 return std::find(preset.default_visible_panels.begin(),
409 preset.default_visible_panels.end(),
410 panel_id) != preset.default_visible_panels.end();
411}
412
413std::vector<std::pair<std::string, DockPosition>> CollectDockedPanels(
414 const PanelLayoutPreset& preset) {
415 std::vector<std::pair<std::string, DockPosition>> docked_panels;
416 docked_panels.reserve(preset.panel_positions.size());
417
418 std::unordered_set<std::string> seen_panels;
419 seen_panels.reserve(preset.panel_positions.size());
420
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) ||
424 !ShouldDockPanelInDefaultLayout(preset, panel_id)) {
425 continue;
426 }
427 auto it = preset.panel_positions.find(panel_id);
428 if (it == preset.panel_positions.end()) {
429 continue;
430 }
431 docked_panels.emplace_back(it->first, it->second);
432 seen_panels.insert(panel_id);
433 }
434 };
435
436 append_ordered(preset.default_visible_panels);
437 append_ordered(preset.optional_panels);
438
439 std::vector<std::pair<std::string, DockPosition>> remaining_panels;
440 remaining_panels.reserve(preset.panel_positions.size());
441 for (const auto& [panel_id, position] : preset.panel_positions) {
442 if (seen_panels.contains(panel_id) ||
443 !ShouldDockPanelInDefaultLayout(preset, panel_id)) {
444 continue;
445 }
446 remaining_panels.emplace_back(panel_id, position);
447 }
448
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);
454 }
455 return lhs.first < rhs.first;
456 });
457 docked_panels.insert(docked_panels.end(), remaining_panels.begin(),
458 remaining_panels.end());
459
460 return docked_panels;
461}
462
464 const std::vector<std::pair<std::string, DockPosition>>& docked_panels) {
465 DockSplitNeeds needs{};
466 for (const auto& [_, pos] : docked_panels) {
467 switch (pos) {
469 needs.left = true;
470 break;
472 needs.right = true;
473 break;
475 needs.bottom = true;
476 break;
478 needs.top = true;
479 break;
481 needs.left = true;
482 needs.left_top = true;
483 break;
485 needs.left = true;
486 needs.left_bottom = true;
487 break;
489 needs.right = true;
490 needs.right_top = true;
491 break;
493 needs.right = true;
494 needs.right_bottom = true;
495 break;
497 default:
498 break;
499 }
500 }
501 return needs;
502}
503
508
510 return pos == DockPosition::Left || pos == DockPosition::LeftTop ||
512}
513
515 float width = 0.0f;
516 // True when the region's first docked panel reports an exact content width
517 // (HasExactPreferredWidth); the region then takes exactly that width.
518 bool exact = false;
519};
520
522 WorkspaceWindowManager* window_manager,
523 const std::vector<std::pair<std::string, DockPosition>>& docked_panels,
524 bool (*matches_region)(DockPosition)) {
525 RegionWidthPreference preference;
526 if (!window_manager) {
527 return preference;
528 }
529
530 bool first_in_region = true;
531 for (const auto& [panel_id, position] : docked_panels) {
532 if (!matches_region(position)) {
533 continue;
534 }
535 WindowContent* panel = window_manager->GetWindowContent(panel_id);
536 if (!panel) {
537 continue;
538 }
539 const float width = panel->GetPreferredWidth();
540 if (first_in_region) {
541 first_in_region = false;
542 // docked_panels lists default-visible panels first, so the first panel
543 // in a region is the one the user sees on top. A fixed-width grid there
544 // owns the region width; panels stacked below it adapt.
545 if (panel->HasExactPreferredWidth() && width > 0.0f) {
546 return {width, true};
547 }
548 }
549 preference.width = std::max(preference.width, width);
550 }
551 return preference;
552}
553
554// Ratio for an exact-width region. Splits are relative to the node being
555// split (the dockspace minus any region split off before it), not the
556// viewport, so compute against that width.
557float ExactRegionRatio(float preferred_width, float available_width,
558 float max_ratio) {
559 if (available_width <= 0.0f) {
560 return max_ratio;
561 }
562 return std::clamp(preferred_width / available_width, 0.05f, max_ratio);
563}
564
566 DockSplitConfig* cfg, const DockSplitNeeds& needs, float viewport_width,
567 float dockspace_width, WorkspaceWindowManager* window_manager,
568 const std::vector<std::pair<std::string, DockPosition>>& docked_panels) {
569 if (!cfg || !window_manager || viewport_width <= 0.0f) {
570 return;
571 }
572 const float split_width =
573 dockspace_width > 0.0f ? dockspace_width : viewport_width;
574
575 if (needs.left) {
576 const auto preferred_left = ResolvePreferredRegionWidth(
577 window_manager, docked_panels, IsLeftDockPosition);
578 if (preferred_left.exact) {
579 cfg->left = ExactRegionRatio(preferred_left.width, split_width, 0.36f);
580 } else if (preferred_left.width > 0.0f) {
581 cfg->left =
582 std::clamp(preferred_left.width / viewport_width, 0.14f, 0.36f);
583 }
584 }
585
586 if (needs.right) {
587 const auto preferred_right = ResolvePreferredRegionWidth(
588 window_manager, docked_panels, IsRightDockPosition);
589 if (preferred_right.exact) {
590 const float remaining =
591 needs.left ? split_width * (1.0f - cfg->left) : split_width;
592 cfg->right = ExactRegionRatio(preferred_right.width, remaining, 0.42f);
593 } else if (preferred_right.width > 0.0f) {
594 cfg->right =
595 std::clamp(preferred_right.width / viewport_width, 0.18f, 0.42f);
596 }
597 }
598}
599
600DockNodeIds BuildDockTree(ImGuiID dockspace_id, const DockSplitNeeds& needs,
601 const DockSplitConfig& cfg) {
602 DockNodeIds ids{};
603 ids.center = dockspace_id;
604
605 // Split major regions
606 if (needs.left) {
607 ids.left = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Left, cfg.left,
608 nullptr, &ids.center);
609 }
610 if (needs.right) {
611 ids.right = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Right,
612 cfg.right, nullptr, &ids.center);
613 }
614 if (needs.bottom) {
615 ids.bottom = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Down,
616 cfg.bottom, nullptr, &ids.center);
617 }
618 if (needs.top) {
619 ids.top = ImGui::DockBuilderSplitNode(ids.center, ImGuiDir_Up, cfg.top,
620 nullptr, &ids.center);
621 }
622
623 // Sub-split Left region
624 if (ids.left && (needs.left_top || needs.left_bottom)) {
625 // If we only need one, the split still happens but we use the result accordingly
626 ids.left_bottom = ImGui::DockBuilderSplitNode(
627 ids.left, ImGuiDir_Down, cfg.vertical_split, nullptr, &ids.left_top);
628
629 // If one isn't needed, we technically don't have to split, but for a stable tree,
630 // we do it and get_dock_id will map to the leaf.
631 }
632
633 // Sub-split Right region
634 if (ids.right && (needs.right_top || needs.right_bottom)) {
635 ids.right_bottom = ImGui::DockBuilderSplitNode(
636 ids.right, ImGuiDir_Down, cfg.vertical_split, nullptr, &ids.right_top);
637 }
638
639 return ids;
640}
641
642} // namespace
643
644namespace {
645
646constexpr char kOuterDockspaceName[] = "yaze.MainDockSpace";
647constexpr char kEditorDockspacePrefix[] = "yaze.EditorDock/";
648constexpr char kEditorHostWindowName[] = "###YazeEditorHost";
649constexpr char kLayoutIniTypeName[] = "YazeLayout";
650constexpr char kLayoutIniInitializedKey[] = "Initialized=";
651constexpr char kLayoutIniPinSideKey[] = "PinSide=";
652
653// Share of the outer dockspace given to a new pin node with no saved size,
654// and the range a saved size is clamped to on a smaller window.
655constexpr float kPinRatio = 0.22f;
656constexpr float kPinMinRatio = 0.05f;
657constexpr float kPinMaxRatio = 0.6f;
658
659// imgui.ini names for ImGuiDir_Left..ImGuiDir_Down.
660constexpr std::array<const char*, 4> kPinSideNames = {"Left", "Right", "Up",
661 "Down"};
662
663ImGuiAxis PinAxis(ImGuiDir dir) {
664 return (dir == ImGuiDir_Left || dir == ImGuiDir_Right) ? ImGuiAxis_X
665 : ImGuiAxis_Y;
666}
667
668// Central leaf under `node`. Reads LocalFlags because ImGuiDockNode::
669// CentralNode on the root is only refreshed on the next frame after a split.
670ImGuiDockNode* FindCentralLeaf(ImGuiDockNode* node) {
671 if (!node) {
672 return nullptr;
673 }
674 if (node->LocalFlags & ImGuiDockNodeFlags_CentralNode) {
675 return node;
676 }
677 for (ImGuiDockNode* child : node->ChildNodes) {
678 if (ImGuiDockNode* found = FindCentralLeaf(child)) {
679 return found;
680 }
681 }
682 return nullptr;
683}
684
685ImGuiID DockRootId(ImGuiID node_id) {
686 ImGuiDockNode* node = ImGui::DockBuilderGetNode(node_id);
687 while (node && node->ParentNode) {
688 node = node->ParentNode;
689 }
690 return node ? node->ID : 0;
691}
692
693// Side of split node `parent` that its child `child` occupies.
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;
698 }
699 return first ? ImGuiDir_Up : ImGuiDir_Down;
700}
701
702// A leaf split straight off a dockspace root is what an ImGui outer-edge drop
703// makes. Returns its side and its size along the split axis, else None.
704ImGuiDir DockspaceEdgeSide(const ImGuiDockNode* node, float* size) {
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;
710 }
711 *size = node->Size[root->SplitAxis];
712 return ChildSide(root, node);
713}
714
715void LayoutIniClearAll(ImGuiContext*, ImGuiSettingsHandler* handler) {
716 if (auto* manager = static_cast<LayoutManager*>(handler->UserData)) {
717 manager->ResetLayoutIniSettings();
718 }
719}
720
721void* LayoutIniReadOpen(ImGuiContext*, ImGuiSettingsHandler* handler,
722 const char* name) {
723 if (std::strcmp(name, "Data") != 0) {
724 return nullptr;
725 }
726 return handler->UserData;
727}
728
729void LayoutIniReadLine(ImGuiContext*, ImGuiSettingsHandler*, void* entry,
730 const char* line) {
731 static_cast<LayoutManager*>(entry)->ReadLayoutIniLine(line);
732}
733
734void LayoutIniApplyAll(ImGuiContext*, ImGuiSettingsHandler* handler) {
735 if (auto* manager = static_cast<LayoutManager*>(handler->UserData)) {
736 manager->ApplyLayoutIniSettings();
737 }
738}
739
740void LayoutIniWriteAll(ImGuiContext*, ImGuiSettingsHandler* handler,
741 ImGuiTextBuffer* buf) {
742 if (auto* manager = static_cast<LayoutManager*>(handler->UserData)) {
743 manager->WriteLayoutIniSettings(buf);
744 }
745}
746
747// Keep the pre-B2 imgui.ini once, before its dock trees are dropped.
749 const char* ini_path = ImGui::GetIO().IniFilename;
750 if (!ini_path || ini_path[0] == '\0') {
751 return;
752 }
753 std::error_code ec;
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)) {
758 return;
759 }
760 std::filesystem::copy_file(source, backup, ec);
761 if (ec) {
762 LOG_WARN("LayoutManager", "Could not back up %s: %s", ini_path,
763 ec.message().c_str());
764 } else {
765 LOG_INFO("LayoutManager", "Backed up legacy layout to %s",
766 backup.string().c_str());
767 }
768}
769
770} // namespace
771
772LayoutManager::~LayoutManager() {
773 if (!ImGui::GetCurrentContext()) {
774 return;
775 }
776 if (ImGuiSettingsHandler* handler =
777 ImGui::FindSettingsHandler(kLayoutIniTypeName);
778 handler && handler->UserData == this) {
779 handler->UserData = nullptr;
780 }
781}
782
783ImGuiID LayoutManager::OuterDockspaceId() {
784 return ImHashStr(kOuterDockspaceName);
785}
786
787ImGuiID LayoutManager::EditorDockspaceId(EditorType type) {
788 const size_t index = EditorTypeIndex(type);
789 const char* name = index < kEditorNames.size() ? kEditorNames[index] : "";
790 return ImHashStr(absl::StrCat(kEditorDockspacePrefix, name).c_str());
791}
792
793ImGuiID LayoutManager::ActiveEditorDockspaceId() const {
794 if (active_editor_type_ == EditorType::kUnknown) {
795 return 0;
796 }
797 return EditorDockspaceId(active_editor_type_);
798}
799
800ImGuiID LayoutManager::GetMainDockspaceId() const {
801 if (const ImGuiID active = ActiveEditorDockspaceId()) {
802 return active;
803 }
804 return main_dockspace_id_;
805}
806
807ImGuiID LayoutManager::FindPinNode() const {
808 ImGuiDockNode* outer = ImGui::DockBuilderGetNode(OuterDockspaceId());
809 if (!outer || !outer->IsSplitNode()) {
810 return 0;
811 }
812 for (ImGuiDockNode* child : outer->ChildNodes) {
813 if (child && child->IsLeafNode() && !FindCentralLeaf(child)) {
814 return child->ID;
815 }
816 }
817 return 0;
818}
819
820ImGuiID LayoutManager::EnsurePinNode() {
821 if (const ImGuiID pin = FindPinNode()) {
822 return pin;
823 }
824 const ImGuiID outer_id = OuterDockspaceId();
825 ImGuiDockNode* outer = ImGui::DockBuilderGetNode(outer_id);
826 // The outer dockspace takes no user splits, so a split root without a pin
827 // leaf is not a tree this code built. Leave it alone.
828 if (!outer || outer->IsSplitNode()) {
829 return 0;
830 }
831 // DockNodeTreeSplit sizes the new node from the outer size minus the
832 // separator, so the saved pixel size maps back to a ratio of that.
833 const ImGuiAxis axis = PinAxis(pin_dir_);
834 const float avail =
835 outer->Size[axis] - ImGui::GetStyle().DockingSeparatorSize;
836 float ratio = kPinRatio;
837 float size = 0.0f;
838 if (pin_size_ > 0.0f && avail > 0.0f) {
839 size = ImClamp(pin_size_, kPinMinRatio * avail, kPinMaxRatio * avail);
840 ratio = size / avail;
841 }
842 ImGuiID central = 0;
843 const ImGuiID pin =
844 ImGui::DockBuilderSplitNode(outer_id, pin_dir_, ratio, nullptr, &central);
845 // The split truncates from the far side, so a right or bottom pin can come
846 // out a pixel large. Layout keeps a non-central child's SizeRef exactly.
847 if (ImGuiDockNode* node = ImGui::DockBuilderGetNode(pin);
848 node && size > 0.0f) {
849 node->SizeRef[axis] = size;
850 }
851 ImGui::DockBuilderFinish(outer_id);
852 return pin;
853}
854
855void LayoutManager::RecordPinSpot() {
856 ImGuiDockNode* outer = ImGui::DockBuilderGetNode(OuterDockspaceId());
857 ImGuiDockNode* pin = ImGui::DockBuilderGetNode(FindPinNode());
858 if (!outer || !pin) {
859 return;
860 }
861 const float size = pin->Size[outer->SplitAxis];
862 if (size <= 0.0f) {
863 return; // Split this frame; laid out on the next one.
864 }
865 pin_dir_ = ChildSide(outer, pin);
866 pin_size_ = size;
867}
868
869ImGuiID LayoutManager::MovePinSpot(ImGuiDir dir, float size) {
870 ImGuiID pin = FindPinNode();
871 if (pin != 0 && dir == pin_dir_) {
872 return pin;
873 }
874 // Rebuild the pin node on the new side and carry its panels over.
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);
879 }
880 ImGui::DockBuilderRemoveNode(pin);
881 }
882 pin_dir_ = dir;
883 if (size > 0.0f) {
884 pin_size_ = size;
885 }
886 pin = EnsurePinNode();
887 if (pin != 0) {
888 for (const std::string& name : moved) {
889 ImGui::DockBuilderDockWindow(name.c_str(), pin);
890 }
891 }
892 return pin;
893}
894
895ImVec2 LayoutManager::EditorDockspaceSize() const {
896 if (editor_host_size_.x > 0.0f && editor_host_size_.y > 0.0f) {
897 return editor_host_size_;
898 }
899 const ImVec2 last_size = gui::DockSpaceRenderer::GetLastDockspaceSize();
900 if (last_size.x > 0.0f && last_size.y > 0.0f) {
901 return last_size;
902 }
903 if (const ImGuiViewport* viewport = ImGui::GetMainViewport();
904 viewport && viewport->WorkSize.x > 0.0f && viewport->WorkSize.y > 0.0f) {
905 return viewport->WorkSize;
906 }
907 return ImVec2(1280.0f, 720.0f);
908}
909
910void LayoutManager::StashLazyDefaultDockState() {
911 const auto& state = lazy_default_dock_state_;
912 if (state.editor_type == EditorType::kUnknown || state.dockspace == 0) {
913 return;
914 }
915 lazy_states_by_type_[state.editor_type] = state;
916}
917
918bool LayoutManager::RestoreLazyDefaultDockState(EditorType type,
919 ImGuiID dockspace_id) {
920 auto& state = lazy_default_dock_state_;
921 // A dockspace shared by every editor (test override) keeps one live state.
922 if (dockspace_id != EditorDockspaceId(type) ||
923 (state.editor_type == type && state.dockspace == dockspace_id)) {
924 return true;
925 }
926
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)) {
932 state = it->second;
933 return true;
934 }
935
936 // No stash: the tree came from imgui.ini. Resume from its central leaf;
937 // optional panels split new regions off it on first open.
938 ImGuiDockNode* center =
939 FindCentralLeaf(ImGui::DockBuilderGetNode(dockspace_id));
940 if (!center) {
941 DisableLazyDefaultDocking();
942 return false;
943 }
944 const DockSplitConfig cfg = DockSplitConfig::ForEditor(type);
945 state = LazyDefaultDockState{};
946 state.enabled = true;
947 state.session_id =
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;
957 return true;
958}
959
960void LayoutManager::DrawEditorHost() {
961 ImGuiDockNode* central =
962 FindCentralLeaf(ImGui::DockBuilderGetNode(OuterDockspaceId()));
963 if (!central) {
964 return; // DockSpaceWindow has not submitted the outer dockspace yet.
965 }
966
967 // Public and internal ImGuiDockNodeFlags are separate enums; combine them
968 // through the int typedef.
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);
979
980 // The host keeps its ini entry: the outer tree is pruned at boot when no
981 // saved window references it.
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();
992 }
993 ImGui::PopStyleVar(3);
994
995 // Inactive editor trees stay alive so their panels keep their nodes.
996 const ImGuiID active_id = ActiveEditorDockspaceId();
997 for (size_t index = 1; index < kEditorTypeCount; ++index) {
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);
1002 }
1003 }
1004 if (active_id != 0) {
1005 ImGui::DockSpace(active_id, ImVec2(0.0f, 0.0f),
1006 ImGuiDockNodeFlags_PassthruCentralNode);
1007 }
1008 ImGui::End();
1009
1010 RecordPinSpot();
1011 EnforcePinnedPanels(FindPinNode());
1012}
1013
1014void LayoutManager::EnforcePinnedPanels(ImGuiID pin_node) {
1015 if (!window_manager_) {
1016 return;
1017 }
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* {
1021 const WindowDescriptor* desc =
1022 window_manager_->GetWindowDescriptor(session_id, panel_id);
1023 if (!desc) {
1024 return nullptr;
1025 }
1026 const std::string title = window_manager_->GetWorkspaceWindowName(*desc);
1027 return title.empty() ? nullptr : ImGui::FindWindowByName(title.c_str());
1028 };
1029
1030 // Docked pinned panels belong in the pin node. Floating ones stay floating.
1031 // A panel that was already pinned last frame and now sits on an editor's
1032 // outer edge was dragged there: the pin spot moves to that side. A panel
1033 // pinned this frame keeps the current side wherever it was docked.
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);
1037 if (!window) {
1038 continue;
1039 }
1040 pinned_now.insert(window->Name);
1041 if (window->DockId == 0 || DockRootId(window->DockId) == outer_id) {
1042 continue;
1043 }
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);
1049 }
1050 if (pin_node == 0) {
1051 pin_node = EnsurePinNode();
1052 }
1053 if (pin_node != 0) {
1054 ImGui::DockBuilderDockWindow(window->Name, pin_node);
1055 }
1056 }
1057 pinned_last_frame_ = std::move(pinned_now);
1058
1059 // An unpinned panel left in the pin node goes back to the active editor.
1060 const ImGuiID home_dockspace = ActiveEditorDockspaceId();
1061 if (home_dockspace == 0) {
1062 return;
1063 }
1064 // Preset regions are only valid while the live lazy state is this editor's.
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)) {
1071 continue;
1072 }
1073 ImGuiWindow* window = find_window(desc.card_id);
1074 if (!window || window->DockId == 0 ||
1075 DockRootId(window->DockId) != outer_id) {
1076 continue;
1077 }
1078 if (preset_regions_live &&
1079 DockPresetPositionOnFirstOpen(session_id, desc.card_id,
1080 /*include_default_visible=*/true,
1081 /*allow_redock=*/true)) {
1082 continue;
1083 }
1084 if (ImGuiDockNode* center =
1085 FindCentralLeaf(ImGui::DockBuilderGetNode(home_dockspace))) {
1086 ImGui::DockBuilderDockWindow(window->Name, center->ID);
1087 }
1088 }
1089}
1090
1091void LayoutManager::RegisterLayoutIniHandler() {
1092 if (!ImGui::GetCurrentContext()) {
1093 return;
1094 }
1095 if (ImGuiSettingsHandler* existing =
1096 ImGui::FindSettingsHandler(kLayoutIniTypeName)) {
1097 existing->UserData = this;
1098 return;
1099 }
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);
1111}
1112
1113void LayoutManager::ResetLayoutIniSettings() {
1114 layout_ini_ = LayoutIniState{};
1115}
1116
1117void LayoutManager::ReadLayoutIniLine(const char* line) {
1118 if (!line) {
1119 return;
1120 }
1121 layout_ini_.section_seen = true;
1122 int revision = 0;
1123 if (std::sscanf(line, "Revision=%d", &revision) == 1) {
1124 layout_ini_.revision = revision;
1125 return;
1126 }
1127 float pin_size = 0.0f;
1128 if (std::sscanf(line, "PinSize=%f", &pin_size) == 1) {
1129 layout_ini_.pin_size = pin_size;
1130 return;
1131 }
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);
1138 }
1139 }
1140 return;
1141 }
1142 constexpr size_t kKeyLength = sizeof(kLayoutIniInitializedKey) - 1;
1143 if (std::strncmp(line, kLayoutIniInitializedKey, kKeyLength) != 0) {
1144 return;
1145 }
1146 const char* name = line + kKeyLength;
1147 for (size_t index = 1; index < kEditorNames.size(); ++index) {
1148 if (std::strcmp(name, kEditorNames[index]) == 0) {
1149 layout_ini_.initialized.push_back(static_cast<EditorType>(index));
1150 return;
1151 }
1152 }
1153}
1154
1155void LayoutManager::ApplyLayoutIniSettings() {
1156 ImGuiContext* ctx = ImGui::GetCurrentContext();
1157 if (!ctx) {
1158 return;
1159 }
1160 ClearInitializationFlags();
1161 const bool first_load = !layout_ini_backup_checked_;
1162 layout_ini_backup_checked_ = true;
1163
1164 // The pin spot is a preference, not a tree: keep it across a tree reset.
1165 if (layout_ini_.pin_dir != ImGuiDir_None) {
1166 pin_dir_ = layout_ini_.pin_dir;
1167 }
1168 if (layout_ini_.pin_size > 0.0f) {
1169 pin_size_ = layout_ini_.pin_size;
1170 }
1171
1172 if (layout_ini_.section_seen && layout_ini_.revision == kLayoutIniRevision) {
1173 // Editors whose saved tree survived load skip the default preset build.
1174 for (const EditorType type : layout_ini_.initialized) {
1175 if (ImGui::DockContextFindNodeByID(ctx, EditorDockspaceId(type))) {
1176 MarkLayoutInitialized(type);
1177 }
1178 }
1179 } else {
1180 // Older layouts docked every editor into one shared tree. Drop all dock
1181 // trees so each editor starts from its default preset.
1182 if (first_load) {
1183 BackupLegacyLayoutIni();
1184 }
1185 ctx->DockContext.NodesSettings.clear();
1186 ImGui::DockContextClearNodes(ctx, 0, true);
1187 LOG_INFO("LayoutManager",
1188 "Layout revision %d does not match %d; reset dock layouts",
1189 layout_ini_.revision, kLayoutIniRevision);
1190 // A mid-session load (reset, pre-B2 named layout) left the live editor
1191 // without a tree. The boot load has no windows yet and needs no rebuild.
1192 if (ctx->Windows.Size > 0) {
1193 RequestRebuild();
1194 }
1195 }
1196}
1197
1198void LayoutManager::WriteLayoutIniSettings(ImGuiTextBuffer* buf) const {
1199 if (!buf) {
1200 return;
1201 }
1202 buf->appendf("[%s][Data]\n", kLayoutIniTypeName);
1203 buf->appendf("Revision=%d\n", kLayoutIniRevision);
1204 for (size_t index = 1; index < kEditorTypeCount; ++index) {
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]);
1209 }
1210 }
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_)]);
1214 }
1215 if (pin_size_ > 0.0f) {
1216 buf->appendf("PinSize=%.0f\n", pin_size_);
1217 }
1218 buf->append("\n");
1219}
1220
1221void LayoutManager::BuildLayoutFromPreset(EditorType type,
1222 ImGuiID dockspace_id) {
1223 // Keep the outgoing editor's geometry; its dockspace is untouched by this
1224 // build. Only this editor's first-open records point at destroyed nodes,
1225 // unless every editor shares one dockspace (test override): then the build
1226 // destroys all of them.
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);
1233 }
1234 } else {
1235 lazy_default_dock_attempts_.clear();
1236 }
1237 auto preset = LayoutPresets::GetDefaultPreset(type);
1238
1239 if (!window_manager_) {
1240 LOG_WARN("LayoutManager",
1241 "WorkspaceWindowManager not available, skipping dock layout for "
1242 "type %d",
1243 static_cast<int>(type));
1244 return;
1245 }
1246
1247 const size_t session_id =
1248 window_manager_ ? window_manager_->GetActiveSessionId() : 0;
1249 const auto docked_panels = CollectDockedPanels(preset);
1250
1251 // On compact/touch layouts, collapse all panels into center tabs instead of
1252 // splitting into left/right/bottom regions. This gives each panel full
1253 // screen width and makes tab-switching more natural on touch.
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
1258 [&]() {
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;
1264 }
1265 compact_mode = compact_mode ? (viewport_width < kExitCompactWidth)
1266 : (viewport_width < kEnterCompactWidth);
1267 return compact_mode;
1268 }();
1269#else
1270 (viewport_width > 0.0f && viewport_width < 900.0f);
1271#endif
1272
1273 DockSplitNeeds needs{};
1274 DockSplitConfig cfg{};
1275 if (!is_compact) {
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);
1284 }
1285 // When compact, needs is all-false → BuildDockTree produces center-only.
1286 DockNodeIds ids = BuildDockTree(dockspace_id, needs, cfg);
1287
1288 // Keep the live default-tree geometry even for editors that do not use lazy
1289 // docking. If the user returns to Dungeon/Overworld without rebuilding the
1290 // shared dockspace, their optional panels can still resolve valid nodes.
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];
1313 }
1314
1315 auto get_dock_id = [&](DockPosition pos) -> ImGuiID {
1316 switch (pos) {
1317 case DockPosition::Left:
1318 if (ids.left_top || ids.left_bottom) {
1319 // If sub-nodes exist, default "Left" to Top to avoid stacking in parent
1320 return ids.left_top ? ids.left_top : ids.left_bottom;
1321 }
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;
1326 }
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:
1344 default:
1345 return ids.center;
1346 }
1347 };
1348
1349 std::vector<ImGuiID> auto_hide_nodes;
1350
1351 // Iterate through positioned windows and dock them
1352 for (const auto& [panel_id, position] : docked_panels) {
1353 const WindowDescriptor* desc =
1354 window_manager_
1355 ? window_manager_->GetWindowDescriptor(session_id, panel_id)
1356 : nullptr;
1357 if (!desc) {
1358 LOG_WARN("LayoutManager",
1359 "Preset references window '%s' that is not registered (session "
1360 "%zu)",
1361 panel_id.c_str(), session_id);
1362 continue;
1363 }
1364
1365 std::string window_title = window_manager_->GetWorkspaceWindowName(*desc);
1366 if (window_title.empty()) {
1367 LOG_WARN("LayoutManager",
1368 "Cannot dock window '%s': missing window name (session %zu)",
1369 panel_id.c_str(), session_id);
1370 continue;
1371 }
1372
1373 // Pinned panels live in the shared pin node, never in an editor tree.
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);
1377 }
1378 continue;
1379 }
1380
1381 const ImGuiID dock_id = get_dock_id(position);
1382 ImGui::DockBuilderDockWindow(window_title.c_str(), dock_id);
1383
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);
1391 }
1392 }
1393 }
1394}
1395
1396bool LayoutManager::DockDefaultPositionOnFirstOpen(
1397 size_t session_id, const std::string& panel_id) {
1398 return DockPresetPositionOnFirstOpen(session_id, panel_id,
1399 /*include_default_visible=*/false);
1400}
1401
1402bool LayoutManager::DockPresetPositionOnPanelOpen(size_t session_id,
1403 const std::string& panel_id) {
1404 return DockPresetPositionOnFirstOpen(session_id, panel_id,
1405 /*include_default_visible=*/true);
1406}
1407
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_;
1413 // allow_redock sends a panel home from the pin node. The live geometry is
1414 // kept for every preset, so it does not require lazy docking.
1415 if ((!state.enabled && !allow_redock) || !window_manager_ ||
1416 panel_id.empty() || session_id != state.session_id ||
1417 !ImGui::DockBuilderGetNode(state.dockspace)) {
1418 return false;
1419 }
1420
1421 const PanelLayoutPreset preset =
1422 LayoutPresets::GetDefaultPreset(state.editor_type);
1423 const bool is_default_visible =
1424 std::find(preset.default_visible_panels.begin(),
1425 preset.default_visible_panels.end(),
1426 panel_id) != preset.default_visible_panels.end();
1427 if ((!preset.dock_only_default_visible_panels && !allow_redock) ||
1428 (!include_default_visible && is_default_visible)) {
1429 return false;
1430 }
1431
1432 const auto position_it = preset.panel_positions.find(panel_id);
1433 if (position_it == preset.panel_positions.end() ||
1434 (!allow_redock && state.attempted_panels.contains(panel_id))) {
1435 return false;
1436 }
1437
1438 const WindowDescriptor* desc =
1439 window_manager_->GetWindowDescriptor(session_id, panel_id);
1440 if (!desc) {
1441 return false;
1442 }
1443 const std::string window_title =
1444 window_manager_->GetWorkspaceWindowName(*desc);
1445 if (window_title.empty()) {
1446 return false;
1447 }
1448
1449 // A panel restored into a valid dock node already has an intentional
1450 // position. Count that as its one shot without moving it.
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);
1456 return false;
1457 }
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);
1463 return false;
1464 }
1465 }
1466
1467 auto split_center = [&](ImGuiDir direction, float ratio,
1468 ImGuiID* region) -> ImGuiID {
1469 if (*region != 0 && ImGui::DockBuilderGetNode(*region)) {
1470 return *region;
1471 }
1472 if (state.center == 0 || !ImGui::DockBuilderGetNode(state.center)) {
1473 return 0;
1474 }
1475 *region = ImGui::DockBuilderSplitNode(state.center, direction, ratio,
1476 nullptr, &state.center);
1477 return *region;
1478 };
1479
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, &region)) {
1488 return 0;
1489 }
1490
1491 if (want_bottom) {
1492 if (bottom != 0 && ImGui::DockBuilderGetNode(bottom)) {
1493 return bottom;
1494 }
1495 if (top == 0) {
1496 bottom = region;
1497 return bottom;
1498 }
1499 ImGuiID new_top = 0;
1500 bottom = ImGui::DockBuilderSplitNode(
1501 region, ImGuiDir_Down, state.vertical_split, nullptr, &new_top);
1502 top = new_top;
1503 return bottom;
1504 }
1505
1506 if (top != 0 && ImGui::DockBuilderGetNode(top)) {
1507 return top;
1508 }
1509 if (bottom == 0) {
1510 top = region;
1511 return top;
1512 }
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;
1517 return top;
1518 };
1519
1520 ImGuiID target = 0;
1521 if (state.compact) {
1522 target = state.center;
1523 } else {
1524 switch (position_it->second) {
1525 case DockPosition::Left:
1526 case DockPosition::LeftTop:
1527 target = resolve_vertical_region(false, false);
1528 break;
1529 case DockPosition::LeftBottom:
1530 target = resolve_vertical_region(false, true);
1531 break;
1532 case DockPosition::Right:
1533 case DockPosition::RightTop:
1534 target = resolve_vertical_region(true, false);
1535 break;
1536 case DockPosition::RightBottom:
1537 target = resolve_vertical_region(true, true);
1538 break;
1539 case DockPosition::Bottom:
1540 target = split_center(ImGuiDir_Down, state.bottom_ratio, &state.bottom);
1541 break;
1542 case DockPosition::Top:
1543 target = split_center(ImGuiDir_Up, state.top_ratio, &state.top);
1544 break;
1545 case DockPosition::Center:
1546 default:
1547 target = state.center;
1548 break;
1549 }
1550 }
1551
1552 if (target == 0 || !ImGui::DockBuilderGetNode(target)) {
1553 return false;
1554 }
1555
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;
1562 }
1563 }
1564 ImGui::DockBuilderFinish(state.dockspace);
1565 return true;
1566}
1567
1568void LayoutManager::SaveCurrentLayout(const std::string& name, bool persist) {
1569 if (!window_manager_) {
1570 LOG_WARN("LayoutManager",
1571 "Cannot save layout '%s': WorkspaceWindowManager not available",
1572 name.c_str());
1573 return;
1574 }
1575
1576 const LayoutScope scope = GetActiveScope();
1577
1578 // Serialize current window visibility state
1579 size_t session_id = window_manager_->GetActiveSessionId();
1580 auto visibility_state = window_manager_->SerializeVisibilityState(session_id);
1581
1582 // Store in saved_layouts_ for later persistence
1583 saved_layouts_[name] = visibility_state;
1584 saved_pinned_layouts_[name] = window_manager_->SerializePinnedState();
1585 layout_scopes_[name] = scope;
1586
1587 // Also save ImGui docking layout to memory
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);
1592 }
1593
1594 if (persist) {
1595 SaveLayoutsToDisk(scope);
1596 }
1597
1598 LOG_INFO("LayoutManager", "Saved layout '%s' with %zu window states%s",
1599 name.c_str(), visibility_state.size(),
1600 persist ? "" : " (session-only)");
1601}
1602
1603void LayoutManager::LoadLayout(const std::string& name) {
1604 if (!window_manager_) {
1605 LOG_WARN("LayoutManager",
1606 "Cannot load layout '%s': WorkspaceWindowManager not available",
1607 name.c_str());
1608 return;
1609 }
1610
1611 // Find saved layout
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());
1615 return;
1616 }
1617
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();
1621 // Only a saved ImGui layout owns placement. Legacy visibility-only layouts
1622 // must leave the current lazy-default context intact.
1623 if (has_imgui_layout) {
1624 DisableLazyDefaultDocking();
1625 }
1626
1627 // Restore window visibility
1628 size_t session_id = window_manager_->GetActiveSessionId();
1629 window_manager_->RestoreVisibilityState(session_id, layout_it->second,
1630 /*publish_events=*/true);
1631
1632 auto pinned_it = saved_pinned_layouts_.find(name);
1633 if (pinned_it != saved_pinned_layouts_.end()) {
1634 window_manager_->RestorePinnedState(pinned_it->second);
1635 }
1636
1637 // Restore ImGui docking layout if available
1638 if (has_imgui_layout) {
1639 ImGui::LoadIniSettingsFromMemory(imgui_it->second.c_str(),
1640 imgui_it->second.size());
1641 ProtectRestoredLayoutFromDefaultInitialization();
1642 }
1643
1644 LOG_INFO("LayoutManager", "Loaded layout '%s'", name.c_str());
1645}
1646
1647void LayoutManager::CaptureTemporarySessionLayout(size_t session_id) {
1648 if (!window_manager_) {
1649 return;
1650 }
1651
1652 temp_session_id_ = session_id;
1653 temp_session_visibility_ =
1654 window_manager_->SerializeVisibilityState(session_id);
1655 temp_session_pinned_ = window_manager_->SerializePinnedState();
1656
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);
1661 } else {
1662 temp_session_imgui_layout_.clear();
1663 }
1664
1665 has_temp_session_layout_ = true;
1666 LOG_INFO(
1667 "LayoutManager",
1668 "Captured temporary session layout for session %zu (%zu panel states)",
1669 session_id, temp_session_visibility_.size());
1670}
1671
1672bool LayoutManager::RestoreTemporarySessionLayout(size_t session_id,
1673 bool clear_after_restore) {
1674 if (!window_manager_ || !has_temp_session_layout_) {
1675 return false;
1676 }
1677
1678 if (session_id != temp_session_id_) {
1679 LOG_WARN("LayoutManager",
1680 "Session layout snapshot belongs to session %zu, requested %zu",
1681 temp_session_id_, session_id);
1682 return false;
1683 }
1684
1685 const bool has_imgui_layout = !temp_session_imgui_layout_.empty();
1686 if (has_imgui_layout) {
1687 DisableLazyDefaultDocking();
1688 }
1689
1690 window_manager_->RestoreVisibilityState(session_id, temp_session_visibility_,
1691 /*publish_events=*/true);
1692 window_manager_->RestorePinnedState(temp_session_pinned_);
1693
1694 if (has_imgui_layout) {
1695 ImGui::LoadIniSettingsFromMemory(temp_session_imgui_layout_.c_str(),
1696 temp_session_imgui_layout_.size());
1697 ProtectRestoredLayoutFromDefaultInitialization();
1698 }
1699
1700 if (clear_after_restore) {
1701 ClearTemporarySessionLayout();
1702 }
1703
1704 LOG_INFO("LayoutManager", "Restored temporary session layout for session %zu",
1705 session_id);
1706 return true;
1707}
1708
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();
1715}
1716
1717bool LayoutManager::SaveNamedSnapshot(const std::string& name,
1718 size_t session_id) {
1719 if (!window_manager_ || name.empty()) {
1720 return false;
1721 }
1722 SessionSnapshot snapshot;
1723 snapshot.session_id = session_id;
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) {
1729 snapshot.imgui_layout.assign(ini_data, ini_size);
1730 }
1731 named_snapshots_[name] = std::move(snapshot);
1732 LOG_INFO("LayoutManager", "Saved named snapshot '%s' for session %zu",
1733 name.c_str(), session_id);
1734 return true;
1735}
1736
1737bool LayoutManager::RestoreNamedSnapshot(const std::string& name,
1738 size_t session_id,
1739 bool remove_after_restore) {
1740 if (!window_manager_) {
1741 return false;
1742 }
1743 auto it = named_snapshots_.find(name);
1744 if (it == named_snapshots_.end()) {
1745 return false;
1746 }
1747 const SessionSnapshot& snapshot = it->second;
1748 if (snapshot.session_id != session_id) {
1749 return false;
1750 }
1751 const bool has_imgui_layout = !snapshot.imgui_layout.empty();
1752 if (has_imgui_layout) {
1753 DisableLazyDefaultDocking();
1754 }
1755 window_manager_->RestoreVisibilityState(session_id, snapshot.visibility,
1756 /*publish_events=*/true);
1757 window_manager_->RestorePinnedState(snapshot.pinned);
1758 if (has_imgui_layout) {
1759 ImGui::LoadIniSettingsFromMemory(snapshot.imgui_layout.c_str(),
1760 snapshot.imgui_layout.size());
1761 ProtectRestoredLayoutFromDefaultInitialization();
1762 }
1763 if (remove_after_restore) {
1764 named_snapshots_.erase(it);
1765 }
1766 return true;
1767}
1768
1769bool LayoutManager::DeleteNamedSnapshot(const std::string& name) {
1770 auto it = named_snapshots_.find(name);
1771 if (it == named_snapshots_.end()) {
1772 return false;
1773 }
1774 named_snapshots_.erase(it);
1775 return true;
1776}
1777
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);
1785 }
1786 }
1787 std::sort(names.begin(), names.end());
1788 return names;
1789}
1790
1791bool LayoutManager::HasNamedSnapshot(const std::string& name) const {
1792 return named_snapshots_.find(name) != named_snapshots_.end();
1793}
1794
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",
1799 name.c_str());
1800 return false;
1801 }
1802
1803 LayoutScope scope = GetActiveScope();
1804 auto scope_it = layout_scopes_.find(name);
1805 if (scope_it != layout_scopes_.end()) {
1806 scope = scope_it->second;
1807 }
1808
1809 saved_layouts_.erase(layout_it);
1810 saved_imgui_layouts_.erase(name);
1811 saved_pinned_layouts_.erase(name);
1812 layout_scopes_.erase(name);
1813
1814 SaveLayoutsToDisk(scope);
1815
1816 LOG_INFO("LayoutManager", "Deleted layout '%s'", name.c_str());
1817 return true;
1818}
1819
1820std::vector<LayoutProfile> LayoutManager::GetBuiltInProfiles() {
1821 return {
1822 {.id = "code",
1823 .label = "Code",
1824 .description = "Focused editing workspace with minimal panel noise",
1825 .preset_name = "Minimal",
1826 .open_agent_chat = false},
1827 {.id = "debug",
1828 .label = "Debug",
1829 .description = "Debugger-first workspace for tracing and memory tools",
1830 .preset_name = "Developer",
1831 .open_agent_chat = false},
1832 {.id = "mapping",
1833 .label = "Mapping",
1834 .description = "Map-centric layout for overworld or dungeon workflows",
1835 .preset_name = "Overworld Expert",
1836 .open_agent_chat = false},
1837 {.id = "chat",
1838 .label = "Chat",
1839 .description = "Collaboration-heavy layout with agent-centric tooling",
1840 .preset_name = "Modder",
1841 .open_agent_chat = true},
1842 };
1843}
1844
1845bool LayoutManager::ApplyBuiltInProfile(const std::string& profile_id,
1846 size_t session_id,
1847 EditorType editor_type,
1848 LayoutProfile* out_profile) {
1849 if (!window_manager_) {
1850 LOG_WARN("LayoutManager",
1851 "Cannot apply profile '%s': WorkspaceWindowManager not available",
1852 profile_id.c_str());
1853 return false;
1854 }
1855
1856 LayoutProfile matched_profile;
1857 bool found = false;
1858 for (const auto& profile : GetBuiltInProfiles()) {
1859 if (profile.id == profile_id) {
1860 matched_profile = profile;
1861 found = true;
1862 break;
1863 }
1864 }
1865
1866 if (!found) {
1867 LOG_WARN("LayoutManager", "Unknown layout profile id: %s",
1868 profile_id.c_str());
1869 return false;
1870 }
1871
1872 const std::string resolved_preset =
1873 ResolveProfilePresetName(profile_id, editor_type);
1874 if (!resolved_preset.empty()) {
1875 matched_profile.preset_name = resolved_preset;
1876 }
1877
1878 PanelLayoutPreset preset;
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());
1882 return false;
1883 }
1884
1885 // The requested rebuild will establish a fresh lazy-default state. Avoid
1886 // mutating the current dock tree while profile visibility is changing.
1887 DisableLazyDefaultDocking();
1888
1889 window_manager_->HideAllWindowsInSession(session_id);
1890 for (const auto& panel_id : preset.default_visible_panels) {
1891 window_manager_->OpenWindow(session_id, panel_id);
1892 }
1893
1894 RequestRebuild();
1895
1896 if (out_profile) {
1897 *out_profile = matched_profile;
1898 }
1899
1900 LOG_INFO("LayoutManager", "Applied profile '%s' via preset '%s'",
1901 profile_id.c_str(), matched_profile.preset_name.c_str());
1902 return true;
1903}
1904
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);
1910 }
1911 return names;
1912}
1913
1914bool LayoutManager::HasLayout(const std::string& name) const {
1915 return saved_layouts_.find(name) != saved_layouts_.end();
1916}
1917
1918void LayoutManager::LoadLayoutsFromDisk() {
1919 saved_layouts_.clear();
1920 saved_imgui_layouts_.clear();
1921 saved_pinned_layouts_.clear();
1922 layout_scopes_.clear();
1923
1924 LoadLayoutsFromDiskInternal(LayoutScope::kGlobal, /*merge=*/false);
1925 if (!project_layout_key_.empty()) {
1926 LoadLayoutsFromDiskInternal(LayoutScope::kProject, /*merge=*/true);
1927 }
1928}
1929
1930void LayoutManager::SetProjectLayoutKey(const std::string& key) {
1931 if (key.empty()) {
1932 UseGlobalLayouts();
1933 return;
1934 }
1935 project_layout_key_ = key;
1936 LoadLayoutsFromDisk();
1937}
1938
1939void LayoutManager::UseGlobalLayouts() {
1940 project_layout_key_.clear();
1941 LoadLayoutsFromDisk();
1942}
1943
1944LayoutScope LayoutManager::GetActiveScope() const {
1945 return project_layout_key_.empty() ? LayoutScope::kGlobal
1946 : LayoutScope::kProject;
1947}
1948
1949void LayoutManager::LoadLayoutsFromDiskInternal(LayoutScope scope, bool merge) {
1950 std::filesystem::path layout_path =
1951 GetLayoutsFilePath(scope, project_layout_key_);
1952 if (layout_path.empty()) {
1953 return;
1954 }
1955
1956 if (!std::filesystem::exists(layout_path)) {
1957 if (!merge) {
1958 LOG_INFO("LayoutManager", "No layouts file at %s",
1959 layout_path.string().c_str());
1960 }
1961 return;
1962 }
1963
1964 try {
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());
1969 return;
1970 }
1971
1972 yaze::Json root;
1973 file >> root;
1974
1975 if (!root.contains("layouts") || !root["layouts"].is_object()) {
1976 LOG_WARN("LayoutManager", "Layouts file missing 'layouts' object: %s",
1977 layout_path.string().c_str());
1978 return;
1979 }
1980
1981 for (auto& [name, entry] : root["layouts"].items()) {
1982 if (!entry.is_object()) {
1983 continue;
1984 }
1985
1986 std::unordered_map<std::string, bool> windows;
1987 std::unordered_map<std::string, bool> pinned;
1988
1989 JsonToWindowMap(entry, &windows);
1990 if (entry.contains("pinned")) {
1991 JsonToBoolMap(entry["pinned"], &pinned);
1992 }
1993
1994 saved_layouts_[name] = std::move(windows);
1995 saved_pinned_layouts_[name] = std::move(pinned);
1996 layout_scopes_[name] = scope;
1997
1998 if (entry.contains("imgui_ini") && entry["imgui_ini"].is_string()) {
1999 saved_imgui_layouts_[name] = entry["imgui_ini"].get<std::string>();
2000 } else {
2001 saved_imgui_layouts_.erase(name);
2002 }
2003 }
2004
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());
2009 }
2010}
2011
2012void LayoutManager::SaveLayoutsToDisk(LayoutScope scope) const {
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");
2017 return;
2018 }
2019
2020 auto status =
2021 util::PlatformPaths::EnsureDirectoryExists(layout_path.parent_path());
2022 if (!status.ok()) {
2023 LOG_WARN("LayoutManager", "Failed to create layout directory: %s",
2024 status.ToString().c_str());
2025 return;
2026 }
2027
2028 try {
2029 yaze::Json root;
2030 root["version"] = 2;
2031 root["layouts"] = yaze::Json::object();
2032
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) {
2036 continue;
2037 }
2038
2039 yaze::Json entry;
2040 entry[kWindowsKey] = BoolMapToJson(windows);
2041
2042 auto pinned_it = saved_pinned_layouts_.find(name);
2043 if (pinned_it != saved_pinned_layouts_.end()) {
2044 entry["pinned"] = BoolMapToJson(pinned_it->second);
2045 }
2046
2047 auto imgui_it = saved_imgui_layouts_.find(name);
2048 if (imgui_it != saved_imgui_layouts_.end()) {
2049 entry["imgui_ini"] = imgui_it->second;
2050 }
2051
2052 root["layouts"][name] = entry;
2053 }
2054
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());
2059 return;
2060 }
2061 file << root.dump(2);
2062 file.close();
2063 } catch (const std::exception& e) {
2064 LOG_WARN("LayoutManager", "Failed to save layouts: %s", e.what());
2065 }
2066}
2067
2068void LayoutManager::ResetToDefaultLayout(EditorType type) {
2069 DisableLazyDefaultDocking();
2070 layouts_initialized_[type] = false;
2071 LOG_INFO("LayoutManager", "Reset layout for editor type %d",
2072 static_cast<int>(type));
2073}
2074
2075bool LayoutManager::IsLayoutInitialized(EditorType type) const {
2076 auto it = layouts_initialized_.find(type);
2077 return it != layouts_initialized_.end() && it->second;
2078}
2079
2080void LayoutManager::MarkLayoutInitialized(EditorType type) {
2081 layouts_initialized_[type] = true;
2082 LOG_INFO("LayoutManager", "Marked layout for editor type %d as initialized",
2083 static_cast<int>(type));
2084}
2085
2086void LayoutManager::ClearInitializationFlags() {
2087 DisableLazyDefaultDocking();
2088 lazy_states_by_type_.clear();
2089 layouts_initialized_.clear();
2090 LOG_INFO("LayoutManager", "Cleared all layout initialization flags");
2091}
2092
2093std::string LayoutManager::GetWindowTitle(const std::string& card_id) const {
2094 if (!window_manager_) {
2095 return "";
2096 }
2097
2098 const size_t session_id = window_manager_->GetActiveSessionId();
2099 return window_manager_->GetWorkspaceWindowName(session_id, card_id);
2100}
2101
2102namespace {
2103
2106 switch (d) {
2107 case SD::kLeft:
2108 return ImGuiDir_Left;
2109 case SD::kRight:
2110 return ImGuiDir_Right;
2111 case SD::kUp:
2112 return ImGuiDir_Up;
2113 case SD::kDown:
2114 return ImGuiDir_Down;
2115 }
2116 return ImGuiDir_Left;
2117}
2118
2120 size_t session_id,
2121 const layout_designer::PanelEntry& panel) {
2122 if (wm) {
2123 if (const auto* desc =
2124 wm->GetWindowDescriptor(session_id, panel.panel_id)) {
2125 return wm->GetWorkspaceWindowName(*desc);
2126 }
2127 }
2128 // Fallback: reconstruct the ImGui window name using the same formula as
2129 // WindowDescriptor::GetImGuiWindowName so layouts referencing panels
2130 // that register late still find their windows when they come up.
2131 std::string label;
2132 if (!panel.icon.empty() && !panel.display_name.empty()) {
2133 label = panel.icon + " " + panel.display_name;
2134 } else if (!panel.display_name.empty()) {
2135 label = panel.display_name;
2136 } else if (!panel.icon.empty()) {
2137 label = panel.icon;
2138 }
2139 if (panel.panel_id.empty()) {
2140 return label;
2141 }
2142 return label.empty() ? panel.panel_id : (label + "##" + panel.panel_id);
2143}
2144
2146 WorkspaceWindowManager* wm, size_t session_id,
2147 const layout_designer::DockNode& node, ImGuiID target_id,
2148 const std::function<void(ImGuiID, ImGuiID, ImGuiID, std::vector<ImGuiID>)>&
2149 queue_selection) {
2150 using NodeType = layout_designer::DockNode::Type;
2151 if (node.type == NodeType::kLeaf) {
2152 std::vector<ImGuiID> tab_ids;
2153 ImGuiID selected_id = 0;
2154 ImGuiID selected_window_id = 0;
2155 size_t index = 0;
2156 for (const auto& panel : node.panels) {
2157 const std::string title = ResolveDockWindowTitle(wm, session_id, panel);
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);
2163 if (index == static_cast<size_t>(node.active_tab_index)) {
2164 selected_id = tab_id;
2165 selected_window_id = window_id;
2166 }
2167 }
2168 ++index;
2169 }
2170 if (selected_id && tab_ids.size() > 1) {
2171 queue_selection(target_id, selected_id, selected_window_id,
2172 std::move(tab_ids));
2173 }
2174 return;
2175 }
2176
2177 // Split node.
2178 const ImGuiDir dir = SplitDirectionToImGuiDir(node.split_direction);
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);
2183 if (node.child_a) {
2184 ApplyDockNodeRecursive(wm, session_id, *node.child_a, id_at_dir,
2185 queue_selection);
2186 }
2187 if (node.child_b) {
2188 ApplyDockNodeRecursive(wm, session_id, *node.child_b, id_other,
2189 queue_selection);
2190 }
2191}
2192
2194 std::string panel_id;
2195 std::string display_name;
2196 std::string icon;
2197};
2198
2199std::unique_ptr<layout_designer::DockNode> CaptureDockNodeRecursive(
2200 const ImGuiDockNode* node,
2201 const std::unordered_map<std::string, PanelLookupEntry>& by_window_name) {
2202 if (!node) {
2203 return layout_designer::DockNode::MakeLeaf({});
2204 }
2205
2206 if (node->IsSplitNode()) {
2207 auto child_a =
2208 CaptureDockNodeRecursive(node->ChildNodes[0], by_window_name);
2209 auto child_b =
2210 CaptureDockNodeRecursive(node->ChildNodes[1], by_window_name);
2211
2212 const bool vertical = node->SplitAxis == ImGuiAxis_Y;
2214 vertical ? layout_designer::SplitDirection::kUp
2215 : layout_designer::SplitDirection::kLeft;
2216
2217 float ratio = 0.5f;
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;
2223 }
2224 }
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));
2228 }
2229
2230 // Leaf.
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];
2236 if (!w || !w->Name)
2237 continue;
2238 auto it = by_window_name.find(std::string(w->Name));
2239 if (it == by_window_name.end())
2240 continue;
2241 if (node->SelectedTabId != 0 && w->ID == node->SelectedTabId) {
2242 selected_index = static_cast<int>(panels.size());
2243 }
2244 panels.push_back(
2245 {it->second.panel_id, it->second.display_name, it->second.icon});
2246 }
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;
2251 }
2252 return leaf;
2253}
2254
2255// Recursively walk a DockTree and append every leaf's panel_ids into
2256// `out`. Used by ApplyDockTree to drive the visibility-open pass.
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);
2262 }
2263 return;
2264 }
2265 if (node.child_a)
2267 if (node.child_b)
2269}
2270
2271} // namespace
2272
2273absl::Status LayoutManager::ApplyDockTree(const layout_designer::DockTree& tree,
2274 ImGuiID dockspace_id) {
2275 if (!window_manager_) {
2276 return absl::FailedPreconditionError(
2277 "LayoutManager::ApplyDockTree: WorkspaceWindowManager not bound");
2278 }
2279 std::string validation_error;
2280 if (!tree.Validate(&validation_error)) {
2281 return absl::InvalidArgumentError("LayoutManager::ApplyDockTree: " +
2282 validation_error);
2283 }
2284
2285 // A custom tree owns all panel placement. Disable default first-open
2286 // docking before the visibility pass publishes open events.
2287 DisableLazyDefaultDocking();
2288 pending_dock_tab_selections_.clear();
2289
2290 // Visibility pass (Phase 8 review 2026-04-24, refined 2026-04-25):
2291 // - Open every panel referenced in the tree so users see what they
2292 // docked rather than empty slots.
2293 // - Close every non-pinned panel that is NOT in the tree, so a saved
2294 // subset doesn't leave previously-visible panels lingering as
2295 // floating ghosts after apply. Pinned panels are intentionally
2296 // persistent (rev-7 / rev-17 force-pinned panels like
2297 // `agent.oracle_ram`, `workflow.output`, `layout.designer`) and
2298 // must survive — closing them would silently hide cross-editor
2299 // functionality the user can't easily put back.
2300 std::vector<std::string> panel_ids;
2301 if (tree.root) {
2302 CollectPanelIdsInSubtree(*tree.root, &panel_ids);
2303 }
2304 const std::unordered_set<std::string> tree_id_set(panel_ids.begin(),
2305 panel_ids.end());
2306 const size_t session_id = window_manager_->GetActiveSessionId();
2307 for (const auto& id : panel_ids) {
2308 // Phase 9 review (2026-04-25): skip already-open panels. Mirrors
2309 // the close-pass guard below — OpenWindowImpl publishes
2310 // WindowVisibilityChanged(true) and runs `on_show` regardless of
2311 // prior state, so a no-op Re-apply (or a startup reapply where
2312 // every panel is already visible) would otherwise dirty settings
2313 // and fire a burst of redundant show events.
2314 if (window_manager_->IsWindowOpen(session_id, id))
2315 continue;
2316 window_manager_->OpenWindow(session_id, id);
2317 }
2318 for (const std::string& id :
2319 window_manager_->GetWindowsInSession(session_id)) {
2320 if (tree_id_set.count(id) > 0)
2321 continue;
2322 if (window_manager_->IsWindowPinned(session_id, id))
2323 continue;
2324 // Phase 8.2 review 3 (2026-04-25): skip already-closed panels.
2325 // CloseWindowImpl fires on_hide and publishes
2326 // WindowVisibilityChanged(false) regardless of prior state, so
2327 // blindly closing every non-tree panel produces a burst of
2328 // redundant hide events for unrelated already-hidden windows.
2329 if (!window_manager_->IsWindowOpen(session_id, id))
2330 continue;
2331 window_manager_->CloseWindow(id);
2332 }
2333
2334 ImGui::DockBuilderRemoveNode(dockspace_id);
2335 ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
2336
2337 ImGui::DockBuilderSetNodeSize(dockspace_id, EditorDockspaceSize());
2338
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()});
2347 });
2348 ImGui::DockBuilderFinish(dockspace_id);
2349
2350 last_dockspace_id_ = dockspace_id;
2351
2352 // Init-tracking pass (Phase 8.2 review 2026-04-25; refined 8.2 review 3):
2353 // - When the user is in an editor (current_editor_type_ != kUnknown),
2354 // mark only that editor type initialized. Other editors keep their
2355 // lazy first-run init — their preset fires on activation as before.
2356 // - When no editor is active yet (kUnknown — startup-reapply OR
2357 // manual apply from the dashboard/settings shell BEFORE any editor
2358 // activation), the per-editor mark wouldn't protect anything, so
2359 // arm the one-shot `startup_reapply_pending_protection_` flag that
2360 // the next InitializeEditorLayout call consumes. Round-3 Codex
2361 // noted that without this branch, applying from a no-editor
2362 // context still left the original clobber bug intact.
2363 ProtectRestoredLayoutFromDefaultInitialization();
2364
2365 return absl::OkStatus();
2366}
2367
2368void LayoutManager::ProcessPendingDockTabSelections() {
2369 const ImGuiContext* context = ImGui::GetCurrentContext();
2370 if (pending_dock_tab_selections_.empty() || !context ||
2371 !context->WithinFrameScope) {
2372 return;
2373 }
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);
2378 }
2379 for (int key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; ++key) {
2380 input |= ImGui::IsKeyPressed(static_cast<ImGuiKey>(key), false);
2381 }
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)) {
2387 return true;
2388 }
2389 ImGuiDockNode* node = ImGui::DockBuilderGetNode(pending.node_id);
2390 if (!node) {
2391 return true;
2392 }
2393 if (frame <= pending.applied_frame || !node->TabBar ||
2394 !node->TabBar->SelectedTabId) {
2395 return false;
2396 }
2397 for (const ImGuiID tab_id : pending.tab_ids) {
2398 if (!ImGui::TabBarFindTabByID(node->TabBar, tab_id)) {
2399 return false;
2400 }
2401 }
2402 ImGuiWindow* selected = ImGui::FindWindowByID(pending.selected_window_id);
2403 if (!selected || selected->DockNode != node ||
2404 selected->TabId != pending.selected_id) {
2405 return false;
2406 }
2407 ImGuiWindow* focused = GImGui->NavWindow;
2408 if (focused && focused->RootWindow->DockNode == node &&
2409 focused->RootWindow != selected->RootWindow) {
2410 if (!GImGui->OpenPopupStack.empty()) {
2411 return false;
2412 }
2413 ImGui::FocusWindow(selected);
2414 }
2415 node->TabBar->NextSelectedTabId = selected->TabId;
2416 return true;
2417 });
2418}
2419
2420absl::Status LayoutManager::MaybeReapplyStartupLayout(UserSettings* settings) {
2421 if (startup_layout_consumed_) {
2422 return absl::OkStatus();
2423 }
2424 if (settings == nullptr) {
2425 startup_layout_consumed_ = true;
2426 return absl::OkStatus();
2427 }
2428
2429 const std::string& name = settings->prefs().last_applied_layout_name;
2430 if (name.empty()) {
2431 // Nothing to reapply. Mark consumed so we stop checking each frame.
2432 startup_layout_consumed_ = true;
2433 return absl::OkStatus();
2434 }
2435
2436 const ImGuiID target_dockspace = GetMainDockspaceId();
2437 if (target_dockspace == 0) {
2438 // No editor dockspace is active yet. Named layouts apply to one editor
2439 // tree, so wait for the first editor. Leave the flag clear.
2440 return absl::OkStatus();
2441 }
2442
2443 const auto& named_layouts = settings->prefs().named_layouts;
2444 const auto it = named_layouts.find(name);
2445 if (it == named_layouts.end()) {
2446 startup_layout_consumed_ = true;
2447 util::logf(
2448 "LayoutManager: startup layout '%s' missing from "
2449 "named_layouts; falling through to default.",
2450 name.c_str());
2451 return absl::NotFoundError(absl::StrCat("startup layout \"", name,
2452 "\" not found in named_layouts"));
2453 }
2454
2455 nlohmann::json parsed;
2456 try {
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()));
2464 }
2465
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();
2472 }
2473
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));
2481 }
2482
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());
2488 }
2489 // ApplyDockTree itself arms `startup_reapply_pending_protection_`
2490 // because `current_editor_type_` is still `kUnknown` at this point
2491 // in the boot — no need to set it again here. Same arming covers
2492 // dashboard-time manual apply, which round-3 Codex flagged.
2493 return apply_status;
2494}
2495
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");
2501 }
2502 ImGuiDockNode* root_node = ImGui::DockBuilderGetNode(dockspace_id);
2503 if (!root_node) {
2504 return absl::NotFoundError(
2505 "LayoutManager::CaptureDockTree: no dock node at given id");
2506 }
2507
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);
2512 if (title.empty())
2513 continue;
2514 by_window_name.emplace(
2515 title, PanelLookupEntry{panel_id, desc.display_name, desc.icon});
2516 }
2517
2519 tree.root = CaptureDockNodeRecursive(root_node, by_window_name);
2520 if (!tree.root) {
2521 tree.root = layout_designer::DockNode::MakeLeaf({});
2522 }
2523 return tree;
2524}
2525
2526} // namespace editor
2527} // namespace yaze
bool is_object() const
Definition json.h:57
static Json object()
Definition json.h:34
items_view items()
Definition json.h:88
std::string dump(int=-1, char=' ', bool=false, int=0) const
Definition json.h:91
bool contains(const std::string &) const
Definition json.h:53
Manages ImGui DockBuilder layouts for each editor type.
void DockOpenPresetPanels(EditorType type)
std::vector< PendingDockTabSelection > pending_dock_tab_selections_
WorkspaceWindowManager * window_manager_
void ProtectRestoredLayoutFromDefaultInitialization()
std::unordered_map< size_t, std::unordered_map< EditorType, std::unordered_set< std::string > > > lazy_default_dock_attempts_
void RebuildLayout(EditorType type, ImGuiID dockspace_id)
Force rebuild of layout for a specific editor type.
LazyDefaultDockState lazy_default_dock_state_
bool DockPresetPositionOnFirstOpen(size_t session_id, const std::string &panel_id, bool include_default_visible, bool allow_redock=false)
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)
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.
const WindowDescriptor * GetWindowDescriptor(size_t session_id, const std::string &base_window_id) const
bool IsWindowOpen(size_t session_id, const std::string &base_window_id) const
bool OpenWindow(size_t session_id, const std::string &base_window_id)
WindowContent * GetWindowContent(const std::string &window_id)
Get a WindowContent instance by ID.
static absl::StatusOr< std::filesystem::path > GetAppDataSubdirectory(const std::string &subdir)
Get a subdirectory within the app data folder.
static absl::Status EnsureDirectoryExists(const std::filesystem::path &path)
Ensure a directory exists, creating it if necessary.
@ SD
Definition zelda.h:44
#define LOG_ERROR(category, format,...)
Definition log.h:140
#define LOG_WARN(category, format,...)
Definition log.h:138
#define LOG_INFO(category, format,...)
Definition log.h:136
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)
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)
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)
void LayoutIniReadLine(ImGuiContext *, ImGuiSettingsHandler *, void *entry, const char *line)
yaze::Json BoolMapToJson(const std::unordered_map< std::string, bool > &map)
constexpr std::array< const char *, 4 > kPinSideNames
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 LayoutIniWriteAll(ImGuiContext *, ImGuiSettingsHandler *handler, ImGuiTextBuffer *buf)
void JsonToBoolMap(const yaze::Json &obj, std::unordered_map< std::string, bool > *map)
bool ShouldDockPanelInDefaultLayout(const PanelLayoutPreset &preset, const std::string &panel_id)
std::unique_ptr< layout_designer::DockNode > CaptureDockNodeRecursive(const ImGuiDockNode *node, const std::unordered_map< std::string, PanelLookupEntry > &by_window_name)
RegionWidthPreference ResolvePreferredRegionWidth(WorkspaceWindowManager *window_manager, const std::vector< std::pair< std::string, DockPosition > > &docked_panels, bool(*matches_region)(DockPosition))
std::string ResolveDockWindowTitle(const WorkspaceWindowManager *wm, size_t session_id, const layout_designer::PanelEntry &panel)
DockSplitNeeds ComputeSplitNeeds(const std::vector< std::pair< std::string, DockPosition > > &docked_panels)
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
Definition editor.h:232
size_t EditorTypeIndex(EditorType type)
Definition editor.h:242
constexpr size_t kEditorTypeCount
Definition editor.h:239
std::unordered_set< std::string > attempted_panels
std::unordered_map< std::string, bool > visibility
std::unordered_map< std::string, bool > pinned
Built-in workflow-oriented layout profiles.
Defines default panel visibility for an editor type.
std::vector< std::string > optional_panels
std::unordered_map< std::string, DockPosition > panel_positions
std::vector< std::string > default_visible_panels
std::unordered_map< std::string, std::string > named_layouts
Metadata for a dockable editor window (formerly PanelInfo)
Represents a dock node in the layout tree.
Definition dock_tree.h:61
std::unique_ptr< DockNode > child_a
Definition dock_tree.h:78
std::unique_ptr< DockNode > child_b
Definition dock_tree.h:79
std::vector< PanelEntry > panels
Definition dock_tree.h:72
std::unique_ptr< DockNode > root
Definition dock_tree.h:115
bool Validate(std::string *error) const
Definition dock_tree.cc:222