2026-04-15 19:30:58 +08:00
|
|
|
#include "Workspace/WorkspaceModelInternal.h"
|
2026-04-06 04:27:54 +08:00
|
|
|
|
2026-04-15 11:08:18 +08:00
|
|
|
#include <algorithm>
|
2026-04-06 04:27:54 +08:00
|
|
|
#include <cmath>
|
|
|
|
|
#include <utility>
|
|
|
|
|
|
2026-04-15 19:30:58 +08:00
|
|
|
namespace XCEngine::UI::Editor::Internal {
|
2026-04-06 04:27:54 +08:00
|
|
|
|
|
|
|
|
UIEditorWorkspaceValidationResult MakeValidationError(
|
|
|
|
|
UIEditorWorkspaceValidationCode code,
|
|
|
|
|
std::string message) {
|
|
|
|
|
UIEditorWorkspaceValidationResult result = {};
|
|
|
|
|
result.code = code;
|
|
|
|
|
result.message = std::move(message);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool IsValidSplitRatio(float value) {
|
|
|
|
|
return std::isfinite(value) && value > 0.0f && value < 1.0f;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 21:50:31 +08:00
|
|
|
std::string BuildSingleTabPanelNodeId(std::string_view stackNodeId) {
|
|
|
|
|
if (stackNodeId.empty()) {
|
|
|
|
|
return "single-tab-panel";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return std::string(stackNodeId) + "__panel";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UIEditorWorkspaceNode WrapStandalonePanelAsTabStack(UIEditorWorkspaceNode panelNode) {
|
|
|
|
|
UIEditorWorkspaceNode panelChild = std::move(panelNode);
|
|
|
|
|
const std::string stackNodeId = panelChild.nodeId;
|
|
|
|
|
panelChild.nodeId = BuildSingleTabPanelNodeId(stackNodeId);
|
|
|
|
|
|
|
|
|
|
UIEditorWorkspaceNode tabStack = {};
|
|
|
|
|
tabStack.kind = UIEditorWorkspaceNodeKind::TabStack;
|
|
|
|
|
tabStack.nodeId = stackNodeId;
|
|
|
|
|
tabStack.selectedTabIndex = 0u;
|
|
|
|
|
tabStack.children.push_back(std::move(panelChild));
|
|
|
|
|
return tabStack;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 17:07:37 +08:00
|
|
|
void CollapseSplitNodeToOnlyChild(UIEditorWorkspaceNode& node) {
|
|
|
|
|
if (node.kind != UIEditorWorkspaceNodeKind::Split ||
|
|
|
|
|
node.children.size() != 1u) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UIEditorWorkspaceNode remainingChild = std::move(node.children.front());
|
|
|
|
|
node = std::move(remainingChild);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 21:50:31 +08:00
|
|
|
void CanonicalizeNodeRecursive(
|
|
|
|
|
UIEditorWorkspaceNode& node,
|
|
|
|
|
bool allowStandalonePanelLeaf) {
|
|
|
|
|
if (node.kind == UIEditorWorkspaceNodeKind::Panel) {
|
|
|
|
|
if (!allowStandalonePanelLeaf) {
|
|
|
|
|
node = WrapStandalonePanelAsTabStack(std::move(node));
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (UIEditorWorkspaceNode& child : node.children) {
|
|
|
|
|
CanonicalizeNodeRecursive(
|
|
|
|
|
child,
|
|
|
|
|
node.kind == UIEditorWorkspaceNodeKind::TabStack);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (node.kind == UIEditorWorkspaceNodeKind::TabStack &&
|
|
|
|
|
!node.children.empty() &&
|
|
|
|
|
node.selectedTabIndex >= node.children.size()) {
|
|
|
|
|
node.selectedTabIndex = node.children.size() - 1u;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (node.kind == UIEditorWorkspaceNodeKind::Split &&
|
|
|
|
|
node.children.size() == 1u) {
|
2026-04-11 17:07:37 +08:00
|
|
|
CollapseSplitNodeToOnlyChild(node);
|
2026-04-10 21:50:31 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 16:20:46 +08:00
|
|
|
const UIEditorPanelDescriptor& RequirePanelDescriptor(
|
|
|
|
|
const UIEditorPanelRegistry& registry,
|
|
|
|
|
std::string_view panelId) {
|
2026-04-15 11:08:18 +08:00
|
|
|
if (const UIEditorPanelDescriptor* descriptor =
|
|
|
|
|
FindUIEditorPanelDescriptor(registry, panelId);
|
2026-04-06 16:20:46 +08:00
|
|
|
descriptor != nullptr) {
|
|
|
|
|
return *descriptor;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const UIEditorPanelDescriptor fallbackDescriptor = {};
|
|
|
|
|
return fallbackDescriptor;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 04:27:54 +08:00
|
|
|
const UIEditorWorkspacePanelState* FindPanelRecursive(
|
|
|
|
|
const UIEditorWorkspaceNode& node,
|
|
|
|
|
std::string_view panelId) {
|
|
|
|
|
if (node.kind == UIEditorWorkspaceNodeKind::Panel) {
|
|
|
|
|
return node.panel.panelId == panelId ? &node.panel : nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const UIEditorWorkspaceNode& child : node.children) {
|
2026-04-15 11:08:18 +08:00
|
|
|
if (const UIEditorWorkspacePanelState* found =
|
|
|
|
|
FindPanelRecursive(child, panelId)) {
|
2026-04-06 04:27:54 +08:00
|
|
|
return found;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 02:21:43 +08:00
|
|
|
const UIEditorWorkspaceNode* FindNodeRecursive(
|
|
|
|
|
const UIEditorWorkspaceNode& node,
|
|
|
|
|
std::string_view nodeId) {
|
|
|
|
|
if (node.nodeId == nodeId) {
|
|
|
|
|
return &node;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const UIEditorWorkspaceNode& child : node.children) {
|
2026-04-15 11:08:18 +08:00
|
|
|
if (const UIEditorWorkspaceNode* found =
|
|
|
|
|
FindNodeRecursive(child, nodeId)) {
|
2026-04-07 02:21:43 +08:00
|
|
|
return found;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UIEditorWorkspaceNode* FindMutableNodeRecursive(
|
|
|
|
|
UIEditorWorkspaceNode& node,
|
|
|
|
|
std::string_view nodeId) {
|
|
|
|
|
if (node.nodeId == nodeId) {
|
|
|
|
|
return &node;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (UIEditorWorkspaceNode& child : node.children) {
|
2026-04-15 11:08:18 +08:00
|
|
|
if (UIEditorWorkspaceNode* found =
|
|
|
|
|
FindMutableNodeRecursive(child, nodeId)) {
|
2026-04-07 02:21:43 +08:00
|
|
|
return found;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 17:07:37 +08:00
|
|
|
bool FindNodePathRecursive(
|
|
|
|
|
const UIEditorWorkspaceNode& node,
|
|
|
|
|
std::string_view nodeId,
|
|
|
|
|
std::vector<std::size_t>& path) {
|
|
|
|
|
if (node.nodeId == nodeId) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
|
|
|
|
path.push_back(index);
|
|
|
|
|
if (FindNodePathRecursive(node.children[index], nodeId, path)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
path.pop_back();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UIEditorWorkspaceNode* ResolveMutableNodeByPath(
|
|
|
|
|
UIEditorWorkspaceNode& node,
|
|
|
|
|
const std::vector<std::size_t>& path) {
|
|
|
|
|
UIEditorWorkspaceNode* current = &node;
|
|
|
|
|
for (const std::size_t childIndex : path) {
|
|
|
|
|
if (childIndex >= current->children.size()) {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
current = ¤t->children[childIndex];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return current;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool IsPanelOpenAndVisibleInSession(
|
|
|
|
|
const UIEditorWorkspaceSession& session,
|
|
|
|
|
std::string_view panelId) {
|
2026-04-15 11:08:18 +08:00
|
|
|
const UIEditorPanelSessionState* state =
|
|
|
|
|
FindUIEditorPanelSessionState(session, panelId);
|
2026-04-11 17:07:37 +08:00
|
|
|
return state != nullptr && state->open && state->visible;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::size_t CountVisibleChildren(
|
|
|
|
|
const UIEditorWorkspaceNode& node,
|
|
|
|
|
const UIEditorWorkspaceSession& session) {
|
|
|
|
|
if (node.kind != UIEditorWorkspaceNodeKind::TabStack) {
|
|
|
|
|
return 0u;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::size_t visibleCount = 0u;
|
|
|
|
|
for (const UIEditorWorkspaceNode& child : node.children) {
|
|
|
|
|
if (child.kind == UIEditorWorkspaceNodeKind::Panel &&
|
|
|
|
|
IsPanelOpenAndVisibleInSession(session, child.panel.panelId)) {
|
|
|
|
|
++visibleCount;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return visibleCount;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::size_t ResolveActualInsertionIndexForVisibleInsertion(
|
|
|
|
|
const UIEditorWorkspaceNode& node,
|
|
|
|
|
const UIEditorWorkspaceSession& session,
|
|
|
|
|
std::size_t targetVisibleInsertionIndex) {
|
|
|
|
|
std::vector<std::size_t> visibleIndices = {};
|
|
|
|
|
visibleIndices.reserve(node.children.size());
|
|
|
|
|
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
|
|
|
|
const UIEditorWorkspaceNode& child = node.children[index];
|
|
|
|
|
if (child.kind == UIEditorWorkspaceNodeKind::Panel &&
|
|
|
|
|
IsPanelOpenAndVisibleInSession(session, child.panel.panelId)) {
|
|
|
|
|
visibleIndices.push_back(index);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (targetVisibleInsertionIndex == 0u) {
|
|
|
|
|
return visibleIndices.empty() ? 0u : visibleIndices.front();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (visibleIndices.empty()) {
|
|
|
|
|
return 0u;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (targetVisibleInsertionIndex >= visibleIndices.size()) {
|
|
|
|
|
return visibleIndices.back() + 1u;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return visibleIndices[targetVisibleInsertionIndex];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void FixTabStackSelectedIndex(
|
|
|
|
|
UIEditorWorkspaceNode& node,
|
|
|
|
|
std::string_view preferredPanelId) {
|
|
|
|
|
if (node.kind != UIEditorWorkspaceNodeKind::TabStack || node.children.empty()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
|
|
|
|
if (node.children[index].kind == UIEditorWorkspaceNodeKind::Panel &&
|
|
|
|
|
node.children[index].panel.panelId == preferredPanelId) {
|
|
|
|
|
node.selectedTabIndex = index;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (node.selectedTabIndex >= node.children.size()) {
|
|
|
|
|
node.selectedTabIndex = node.children.size() - 1u;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool RemoveNodeByIdRecursive(
|
|
|
|
|
UIEditorWorkspaceNode& node,
|
|
|
|
|
std::string_view nodeId) {
|
|
|
|
|
if (node.kind != UIEditorWorkspaceNodeKind::Split) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
|
|
|
|
if (node.children[index].nodeId == nodeId) {
|
2026-04-15 11:08:18 +08:00
|
|
|
node.children.erase(
|
|
|
|
|
node.children.begin() + static_cast<std::ptrdiff_t>(index));
|
2026-04-11 17:07:37 +08:00
|
|
|
if (node.children.size() == 1u) {
|
|
|
|
|
CollapseSplitNodeToOnlyChild(node);
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (UIEditorWorkspaceNode& child : node.children) {
|
|
|
|
|
if (RemoveNodeByIdRecursive(child, nodeId)) {
|
|
|
|
|
if (node.kind == UIEditorWorkspaceNodeKind::Split &&
|
|
|
|
|
node.children.size() == 1u) {
|
|
|
|
|
CollapseSplitNodeToOnlyChild(node);
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float ClampDockSplitRatio(float value) {
|
|
|
|
|
constexpr float kMinRatio = 0.1f;
|
|
|
|
|
constexpr float kMaxRatio = 0.9f;
|
|
|
|
|
return (std::min)(kMaxRatio, (std::max)(kMinRatio, value));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool IsLeadingDockPlacement(UIEditorWorkspaceDockPlacement placement) {
|
|
|
|
|
return placement == UIEditorWorkspaceDockPlacement::Left ||
|
|
|
|
|
placement == UIEditorWorkspaceDockPlacement::Top;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 11:08:18 +08:00
|
|
|
UIEditorWorkspaceSplitAxis ResolveDockSplitAxis(
|
|
|
|
|
UIEditorWorkspaceDockPlacement placement) {
|
2026-04-11 17:07:37 +08:00
|
|
|
return placement == UIEditorWorkspaceDockPlacement::Left ||
|
|
|
|
|
placement == UIEditorWorkspaceDockPlacement::Right
|
|
|
|
|
? UIEditorWorkspaceSplitAxis::Horizontal
|
|
|
|
|
: UIEditorWorkspaceSplitAxis::Vertical;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string MakeUniqueNodeId(
|
|
|
|
|
const UIEditorWorkspaceModel& workspace,
|
|
|
|
|
std::string base) {
|
|
|
|
|
if (base.empty()) {
|
|
|
|
|
base = "workspace-node";
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 11:08:18 +08:00
|
|
|
if (FindNodeRecursive(workspace.root, base) == nullptr) {
|
2026-04-11 17:07:37 +08:00
|
|
|
return base;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (std::size_t suffix = 1u; suffix < 1024u; ++suffix) {
|
|
|
|
|
const std::string candidate = base + "-" + std::to_string(suffix);
|
2026-04-15 11:08:18 +08:00
|
|
|
if (FindNodeRecursive(workspace.root, candidate) == nullptr) {
|
2026-04-11 17:07:37 +08:00
|
|
|
return candidate;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return base + "-overflow";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool TryExtractVisiblePanelFromTabStack(
|
|
|
|
|
UIEditorWorkspaceModel& workspace,
|
|
|
|
|
const UIEditorWorkspaceSession& session,
|
|
|
|
|
std::string_view sourceNodeId,
|
|
|
|
|
std::string_view panelId,
|
|
|
|
|
UIEditorWorkspaceNode& extractedPanel) {
|
|
|
|
|
std::vector<std::size_t> sourcePath = {};
|
|
|
|
|
if (!FindNodePathRecursive(workspace.root, sourceNodeId, sourcePath)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UIEditorWorkspaceNode* sourceStack =
|
|
|
|
|
ResolveMutableNodeByPath(workspace.root, sourcePath);
|
|
|
|
|
if (sourceStack == nullptr ||
|
|
|
|
|
sourceStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::size_t panelIndex = sourceStack->children.size();
|
|
|
|
|
for (std::size_t index = 0; index < sourceStack->children.size(); ++index) {
|
|
|
|
|
const UIEditorWorkspaceNode& child = sourceStack->children[index];
|
|
|
|
|
if (child.kind != UIEditorWorkspaceNodeKind::Panel) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (child.panel.panelId == panelId) {
|
|
|
|
|
if (!IsPanelOpenAndVisibleInSession(session, panelId)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
panelIndex = index;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (panelIndex >= sourceStack->children.size()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (sourcePath.empty() && sourceStack->children.size() == 1u) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string fallbackSelectedPanelId = {};
|
|
|
|
|
if (sourceStack->selectedTabIndex < sourceStack->children.size()) {
|
|
|
|
|
fallbackSelectedPanelId =
|
|
|
|
|
sourceStack->children[sourceStack->selectedTabIndex].panel.panelId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
extractedPanel = std::move(sourceStack->children[panelIndex]);
|
|
|
|
|
sourceStack->children.erase(
|
|
|
|
|
sourceStack->children.begin() + static_cast<std::ptrdiff_t>(panelIndex));
|
|
|
|
|
|
|
|
|
|
if (sourceStack->children.empty()) {
|
|
|
|
|
if (sourcePath.empty()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!RemoveNodeByIdRecursive(workspace.root, sourceNodeId)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (fallbackSelectedPanelId == panelId) {
|
|
|
|
|
const std::size_t nextIndex =
|
|
|
|
|
(std::min)(panelIndex, sourceStack->children.size() - 1u);
|
|
|
|
|
fallbackSelectedPanelId =
|
|
|
|
|
sourceStack->children[nextIndex].panel.panelId;
|
|
|
|
|
}
|
|
|
|
|
FixTabStackSelectedIndex(*sourceStack, fallbackSelectedPanelId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace));
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 04:27:54 +08:00
|
|
|
bool TryActivateRecursive(
|
|
|
|
|
UIEditorWorkspaceNode& node,
|
|
|
|
|
std::string_view panelId) {
|
|
|
|
|
switch (node.kind) {
|
|
|
|
|
case UIEditorWorkspaceNodeKind::Panel:
|
|
|
|
|
return node.panel.panelId == panelId;
|
|
|
|
|
|
|
|
|
|
case UIEditorWorkspaceNodeKind::TabStack:
|
|
|
|
|
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
|
|
|
|
UIEditorWorkspaceNode& child = node.children[index];
|
|
|
|
|
if (child.kind == UIEditorWorkspaceNodeKind::Panel &&
|
|
|
|
|
child.panel.panelId == panelId) {
|
|
|
|
|
node.selectedTabIndex = index;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
case UIEditorWorkspaceNodeKind::Split:
|
|
|
|
|
for (UIEditorWorkspaceNode& child : node.children) {
|
|
|
|
|
if (TryActivateRecursive(child, panelId)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CollectVisiblePanelsRecursive(
|
|
|
|
|
const UIEditorWorkspaceNode& node,
|
|
|
|
|
std::string_view activePanelId,
|
|
|
|
|
std::vector<UIEditorWorkspaceVisiblePanel>& outPanels) {
|
|
|
|
|
switch (node.kind) {
|
|
|
|
|
case UIEditorWorkspaceNodeKind::Panel: {
|
|
|
|
|
UIEditorWorkspaceVisiblePanel panel = {};
|
|
|
|
|
panel.panelId = node.panel.panelId;
|
|
|
|
|
panel.title = node.panel.title;
|
|
|
|
|
panel.active = node.panel.panelId == activePanelId;
|
|
|
|
|
panel.placeholder = node.panel.placeholder;
|
|
|
|
|
outPanels.push_back(std::move(panel));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case UIEditorWorkspaceNodeKind::TabStack:
|
|
|
|
|
if (node.selectedTabIndex < node.children.size()) {
|
|
|
|
|
CollectVisiblePanelsRecursive(
|
|
|
|
|
node.children[node.selectedTabIndex],
|
|
|
|
|
activePanelId,
|
|
|
|
|
outPanels);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
case UIEditorWorkspaceNodeKind::Split:
|
|
|
|
|
for (const UIEditorWorkspaceNode& child : node.children) {
|
|
|
|
|
CollectVisiblePanelsRecursive(child, activePanelId, outPanels);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UIEditorWorkspaceValidationResult ValidateNodeRecursive(
|
|
|
|
|
const UIEditorWorkspaceNode& node,
|
2026-04-19 00:03:25 +08:00
|
|
|
std::unordered_set<std::string>& nodeIds,
|
2026-04-06 04:27:54 +08:00
|
|
|
std::unordered_set<std::string>& panelIds) {
|
|
|
|
|
if (node.nodeId.empty()) {
|
|
|
|
|
return MakeValidationError(
|
|
|
|
|
UIEditorWorkspaceValidationCode::EmptyNodeId,
|
|
|
|
|
"Workspace node id must not be empty.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:03:25 +08:00
|
|
|
if (!nodeIds.insert(node.nodeId).second) {
|
|
|
|
|
return MakeValidationError(
|
|
|
|
|
UIEditorWorkspaceValidationCode::DuplicateNodeId,
|
|
|
|
|
"Workspace node id '" + node.nodeId +
|
|
|
|
|
"' is duplicated in the workspace tree.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 04:27:54 +08:00
|
|
|
switch (node.kind) {
|
|
|
|
|
case UIEditorWorkspaceNodeKind::Panel:
|
|
|
|
|
if (!node.children.empty()) {
|
|
|
|
|
return MakeValidationError(
|
|
|
|
|
UIEditorWorkspaceValidationCode::NonPanelTabChild,
|
|
|
|
|
"Panel node '" + node.nodeId + "' must not contain child nodes.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (node.panel.panelId.empty()) {
|
|
|
|
|
return MakeValidationError(
|
|
|
|
|
UIEditorWorkspaceValidationCode::EmptyPanelId,
|
|
|
|
|
"Panel node '" + node.nodeId + "' must define a panelId.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (node.panel.title.empty()) {
|
|
|
|
|
return MakeValidationError(
|
|
|
|
|
UIEditorWorkspaceValidationCode::EmptyPanelTitle,
|
|
|
|
|
"Panel node '" + node.nodeId + "' must define a title.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!panelIds.insert(node.panel.panelId).second) {
|
|
|
|
|
return MakeValidationError(
|
|
|
|
|
UIEditorWorkspaceValidationCode::DuplicatePanelId,
|
2026-04-15 11:08:18 +08:00
|
|
|
"Panel id '" + node.panel.panelId +
|
|
|
|
|
"' is duplicated in the workspace tree.");
|
2026-04-06 04:27:54 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
case UIEditorWorkspaceNodeKind::TabStack:
|
|
|
|
|
if (node.children.empty()) {
|
|
|
|
|
return MakeValidationError(
|
|
|
|
|
UIEditorWorkspaceValidationCode::EmptyTabStack,
|
|
|
|
|
"Tab stack '" + node.nodeId + "' must contain at least one panel.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (node.selectedTabIndex >= node.children.size()) {
|
|
|
|
|
return MakeValidationError(
|
|
|
|
|
UIEditorWorkspaceValidationCode::InvalidSelectedTabIndex,
|
|
|
|
|
"Tab stack '" + node.nodeId + "' selectedTabIndex is out of range.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const UIEditorWorkspaceNode& child : node.children) {
|
|
|
|
|
if (child.kind != UIEditorWorkspaceNodeKind::Panel) {
|
|
|
|
|
return MakeValidationError(
|
|
|
|
|
UIEditorWorkspaceValidationCode::NonPanelTabChild,
|
2026-04-15 11:08:18 +08:00
|
|
|
"Tab stack '" + node.nodeId +
|
|
|
|
|
"' may only contain panel leaf nodes.");
|
2026-04-06 04:27:54 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-15 11:08:18 +08:00
|
|
|
if (UIEditorWorkspaceValidationResult result =
|
2026-04-19 00:03:25 +08:00
|
|
|
ValidateNodeRecursive(child, nodeIds, panelIds);
|
2026-04-06 04:27:54 +08:00
|
|
|
!result.IsValid()) {
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
case UIEditorWorkspaceNodeKind::Split:
|
|
|
|
|
if (node.children.size() != 2u) {
|
|
|
|
|
return MakeValidationError(
|
|
|
|
|
UIEditorWorkspaceValidationCode::InvalidSplitChildCount,
|
2026-04-15 11:08:18 +08:00
|
|
|
"Split node '" + node.nodeId +
|
|
|
|
|
"' must contain exactly two child nodes.");
|
2026-04-06 04:27:54 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!IsValidSplitRatio(node.splitRatio)) {
|
|
|
|
|
return MakeValidationError(
|
|
|
|
|
UIEditorWorkspaceValidationCode::InvalidSplitRatio,
|
2026-04-15 11:08:18 +08:00
|
|
|
"Split node '" + node.nodeId +
|
|
|
|
|
"' must define a ratio in the open interval (0, 1).");
|
2026-04-06 04:27:54 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const UIEditorWorkspaceNode& child : node.children) {
|
2026-04-15 11:08:18 +08:00
|
|
|
if (UIEditorWorkspaceValidationResult result =
|
2026-04-19 00:03:25 +08:00
|
|
|
ValidateNodeRecursive(child, nodeIds, panelIds);
|
2026-04-06 04:27:54 +08:00
|
|
|
!result.IsValid()) {
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 19:30:58 +08:00
|
|
|
} // namespace XCEngine::UI::Editor::Internal
|
2026-04-15 11:08:18 +08:00
|
|
|
|
|
|
|
|
namespace XCEngine::UI::Editor {
|
2026-04-06 04:27:54 +08:00
|
|
|
|
2026-04-06 16:59:15 +08:00
|
|
|
bool AreUIEditorWorkspaceNodesEquivalent(
|
|
|
|
|
const UIEditorWorkspaceNode& lhs,
|
|
|
|
|
const UIEditorWorkspaceNode& rhs) {
|
|
|
|
|
if (lhs.kind != rhs.kind ||
|
|
|
|
|
lhs.nodeId != rhs.nodeId ||
|
|
|
|
|
lhs.splitAxis != rhs.splitAxis ||
|
|
|
|
|
lhs.splitRatio != rhs.splitRatio ||
|
|
|
|
|
lhs.selectedTabIndex != rhs.selectedTabIndex ||
|
|
|
|
|
lhs.panel.panelId != rhs.panel.panelId ||
|
|
|
|
|
lhs.panel.title != rhs.panel.title ||
|
|
|
|
|
lhs.panel.placeholder != rhs.panel.placeholder ||
|
|
|
|
|
lhs.children.size() != rhs.children.size()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (std::size_t index = 0; index < lhs.children.size(); ++index) {
|
2026-04-15 11:08:18 +08:00
|
|
|
if (!AreUIEditorWorkspaceNodesEquivalent(
|
|
|
|
|
lhs.children[index],
|
|
|
|
|
rhs.children[index])) {
|
2026-04-06 16:59:15 +08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool AreUIEditorWorkspaceModelsEquivalent(
|
|
|
|
|
const UIEditorWorkspaceModel& lhs,
|
|
|
|
|
const UIEditorWorkspaceModel& rhs) {
|
|
|
|
|
return lhs.activePanelId == rhs.activePanelId &&
|
|
|
|
|
AreUIEditorWorkspaceNodesEquivalent(lhs.root, rhs.root);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 04:27:54 +08:00
|
|
|
UIEditorWorkspaceNode BuildUIEditorWorkspacePanel(
|
|
|
|
|
std::string nodeId,
|
|
|
|
|
std::string panelId,
|
|
|
|
|
std::string title,
|
|
|
|
|
bool placeholder) {
|
|
|
|
|
UIEditorWorkspaceNode node = {};
|
|
|
|
|
node.kind = UIEditorWorkspaceNodeKind::Panel;
|
|
|
|
|
node.nodeId = std::move(nodeId);
|
|
|
|
|
node.panel.panelId = std::move(panelId);
|
|
|
|
|
node.panel.title = std::move(title);
|
|
|
|
|
node.panel.placeholder = placeholder;
|
|
|
|
|
return node;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 21:50:31 +08:00
|
|
|
UIEditorWorkspaceNode BuildUIEditorWorkspaceSingleTabStack(
|
|
|
|
|
std::string nodeId,
|
|
|
|
|
std::string panelId,
|
|
|
|
|
std::string title,
|
|
|
|
|
bool placeholder) {
|
|
|
|
|
UIEditorWorkspaceNode panel = BuildUIEditorWorkspacePanel(
|
2026-04-15 19:30:58 +08:00
|
|
|
Internal::BuildSingleTabPanelNodeId(nodeId),
|
2026-04-10 21:50:31 +08:00
|
|
|
std::move(panelId),
|
|
|
|
|
std::move(title),
|
|
|
|
|
placeholder);
|
|
|
|
|
return BuildUIEditorWorkspaceTabStack(
|
|
|
|
|
std::move(nodeId),
|
|
|
|
|
{ std::move(panel) },
|
|
|
|
|
0u);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 04:27:54 +08:00
|
|
|
UIEditorWorkspaceNode BuildUIEditorWorkspaceTabStack(
|
|
|
|
|
std::string nodeId,
|
|
|
|
|
std::vector<UIEditorWorkspaceNode> panels,
|
|
|
|
|
std::size_t selectedTabIndex) {
|
|
|
|
|
UIEditorWorkspaceNode node = {};
|
|
|
|
|
node.kind = UIEditorWorkspaceNodeKind::TabStack;
|
|
|
|
|
node.nodeId = std::move(nodeId);
|
|
|
|
|
node.selectedTabIndex = selectedTabIndex;
|
|
|
|
|
node.children = std::move(panels);
|
|
|
|
|
return node;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UIEditorWorkspaceNode BuildUIEditorWorkspaceSplit(
|
|
|
|
|
std::string nodeId,
|
|
|
|
|
UIEditorWorkspaceSplitAxis axis,
|
|
|
|
|
float splitRatio,
|
|
|
|
|
UIEditorWorkspaceNode primary,
|
|
|
|
|
UIEditorWorkspaceNode secondary) {
|
|
|
|
|
UIEditorWorkspaceNode node = {};
|
|
|
|
|
node.kind = UIEditorWorkspaceNodeKind::Split;
|
|
|
|
|
node.nodeId = std::move(nodeId);
|
|
|
|
|
node.splitAxis = axis;
|
|
|
|
|
node.splitRatio = splitRatio;
|
|
|
|
|
node.children.push_back(std::move(primary));
|
|
|
|
|
node.children.push_back(std::move(secondary));
|
|
|
|
|
return node;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 21:50:31 +08:00
|
|
|
UIEditorWorkspaceModel CanonicalizeUIEditorWorkspaceModel(
|
|
|
|
|
UIEditorWorkspaceModel workspace) {
|
2026-04-15 19:30:58 +08:00
|
|
|
Internal::CanonicalizeNodeRecursive(workspace.root, false);
|
2026-04-10 21:50:31 +08:00
|
|
|
return workspace;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 20:02:34 +08:00
|
|
|
} // namespace XCEngine::UI::Editor
|