Files
XCEngine/new_editor/src/Shell/UIEditorWorkspaceModel.cpp

1014 lines
32 KiB
C++

#include <XCEditor/Shell/UIEditorPanelRegistry.h>
#include <XCEditor/Shell/UIEditorWorkspaceModel.h>
#include <XCEditor/Shell/UIEditorWorkspaceSession.h>
#include <cmath>
#include <unordered_set>
#include <utility>
namespace XCEngine::UI::Editor {
namespace {
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;
}
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;
}
void CollapseSplitNodeToOnlyChild(UIEditorWorkspaceNode& node) {
if (node.kind != UIEditorWorkspaceNodeKind::Split ||
node.children.size() != 1u) {
return;
}
// Move the remaining child through a temporary object first. Assigning
// directly from node.children.front() aliases a subobject of node and can
// trigger use-after-move when the vector storage is torn down.
UIEditorWorkspaceNode remainingChild = std::move(node.children.front());
node = std::move(remainingChild);
}
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) {
CollapseSplitNodeToOnlyChild(node);
}
}
const UIEditorPanelDescriptor& RequirePanelDescriptor(
const UIEditorPanelRegistry& registry,
std::string_view panelId) {
if (const UIEditorPanelDescriptor* descriptor = FindUIEditorPanelDescriptor(registry, panelId);
descriptor != nullptr) {
return *descriptor;
}
static const UIEditorPanelDescriptor fallbackDescriptor = {};
return fallbackDescriptor;
}
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) {
if (const UIEditorWorkspacePanelState* found = FindPanelRecursive(child, panelId)) {
return found;
}
}
return nullptr;
}
const UIEditorWorkspaceNode* FindNodeRecursive(
const UIEditorWorkspaceNode& node,
std::string_view nodeId) {
if (node.nodeId == nodeId) {
return &node;
}
for (const UIEditorWorkspaceNode& child : node.children) {
if (const UIEditorWorkspaceNode* found = FindNodeRecursive(child, nodeId)) {
return found;
}
}
return nullptr;
}
UIEditorWorkspaceNode* FindMutableNodeRecursive(
UIEditorWorkspaceNode& node,
std::string_view nodeId) {
if (node.nodeId == nodeId) {
return &node;
}
for (UIEditorWorkspaceNode& child : node.children) {
if (UIEditorWorkspaceNode* found = FindMutableNodeRecursive(child, nodeId)) {
return found;
}
}
return nullptr;
}
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 = &current->children[childIndex];
}
return current;
}
bool IsPanelOpenAndVisibleInSession(
const UIEditorWorkspaceSession& session,
std::string_view panelId) {
const UIEditorPanelSessionState* state = FindUIEditorPanelSessionState(session, panelId);
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) {
node.children.erase(node.children.begin() + static_cast<std::ptrdiff_t>(index));
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;
}
UIEditorWorkspaceSplitAxis ResolveDockSplitAxis(UIEditorWorkspaceDockPlacement placement) {
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";
}
if (FindUIEditorWorkspaceNode(workspace, base) == nullptr) {
return base;
}
for (std::size_t suffix = 1u; suffix < 1024u; ++suffix) {
const std::string candidate = base + "-" + std::to_string(suffix);
if (FindUIEditorWorkspaceNode(workspace, candidate) == nullptr) {
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;
}
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,
std::unordered_set<std::string>& panelIds) {
if (node.nodeId.empty()) {
return MakeValidationError(
UIEditorWorkspaceValidationCode::EmptyNodeId,
"Workspace node id must not be empty.");
}
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,
"Panel id '" + node.panel.panelId + "' is duplicated in the workspace tree.");
}
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,
"Tab stack '" + node.nodeId + "' may only contain panel leaf nodes.");
}
if (UIEditorWorkspaceValidationResult result = ValidateNodeRecursive(child, panelIds);
!result.IsValid()) {
return result;
}
}
return {};
case UIEditorWorkspaceNodeKind::Split:
if (node.children.size() != 2u) {
return MakeValidationError(
UIEditorWorkspaceValidationCode::InvalidSplitChildCount,
"Split node '" + node.nodeId + "' must contain exactly two child nodes.");
}
if (!IsValidSplitRatio(node.splitRatio)) {
return MakeValidationError(
UIEditorWorkspaceValidationCode::InvalidSplitRatio,
"Split node '" + node.nodeId + "' must define a ratio in the open interval (0, 1).");
}
for (const UIEditorWorkspaceNode& child : node.children) {
if (UIEditorWorkspaceValidationResult result = ValidateNodeRecursive(child, panelIds);
!result.IsValid()) {
return result;
}
}
return {};
}
return {};
}
} // namespace
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) {
if (!AreUIEditorWorkspaceNodesEquivalent(lhs.children[index], rhs.children[index])) {
return false;
}
}
return true;
}
bool AreUIEditorWorkspaceModelsEquivalent(
const UIEditorWorkspaceModel& lhs,
const UIEditorWorkspaceModel& rhs) {
return lhs.activePanelId == rhs.activePanelId &&
AreUIEditorWorkspaceNodesEquivalent(lhs.root, rhs.root);
}
UIEditorWorkspaceModel BuildDefaultEditorShellWorkspaceModel() {
const UIEditorPanelRegistry registry = BuildDefaultEditorShellPanelRegistry();
const UIEditorPanelDescriptor& rootPanel =
RequirePanelDescriptor(registry, "editor-foundation-root");
UIEditorWorkspaceModel workspace = {};
workspace.root = BuildUIEditorWorkspaceSingleTabStack(
"editor-foundation-root-node",
rootPanel.panelId,
rootPanel.defaultTitle,
rootPanel.placeholder);
workspace.activePanelId = rootPanel.panelId;
return workspace;
}
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;
}
UIEditorWorkspaceNode BuildUIEditorWorkspaceSingleTabStack(
std::string nodeId,
std::string panelId,
std::string title,
bool placeholder) {
UIEditorWorkspaceNode panel = BuildUIEditorWorkspacePanel(
BuildSingleTabPanelNodeId(nodeId),
std::move(panelId),
std::move(title),
placeholder);
return BuildUIEditorWorkspaceTabStack(
std::move(nodeId),
{ std::move(panel) },
0u);
}
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;
}
UIEditorWorkspaceValidationResult ValidateUIEditorWorkspace(
const UIEditorWorkspaceModel& workspace) {
std::unordered_set<std::string> panelIds = {};
UIEditorWorkspaceValidationResult result = ValidateNodeRecursive(workspace.root, panelIds);
if (!result.IsValid()) {
return result;
}
if (!workspace.activePanelId.empty()) {
const UIEditorWorkspacePanelState* activePanel = FindUIEditorWorkspaceActivePanel(workspace);
if (activePanel == nullptr) {
return MakeValidationError(
UIEditorWorkspaceValidationCode::InvalidActivePanelId,
"Active panel id '" + workspace.activePanelId + "' is missing or hidden by the current tab selection.");
}
}
return {};
}
UIEditorWorkspaceModel CanonicalizeUIEditorWorkspaceModel(
UIEditorWorkspaceModel workspace) {
CanonicalizeNodeRecursive(workspace.root, false);
return workspace;
}
std::vector<UIEditorWorkspaceVisiblePanel> CollectUIEditorWorkspaceVisiblePanels(
const UIEditorWorkspaceModel& workspace) {
std::vector<UIEditorWorkspaceVisiblePanel> visiblePanels = {};
CollectVisiblePanelsRecursive(workspace.root, workspace.activePanelId, visiblePanels);
return visiblePanels;
}
bool ContainsUIEditorWorkspacePanel(
const UIEditorWorkspaceModel& workspace,
std::string_view panelId) {
return FindPanelRecursive(workspace.root, panelId) != nullptr;
}
const UIEditorWorkspaceNode* FindUIEditorWorkspaceNode(
const UIEditorWorkspaceModel& workspace,
std::string_view nodeId) {
return FindNodeRecursive(workspace.root, nodeId);
}
const UIEditorWorkspacePanelState* FindUIEditorWorkspaceActivePanel(
const UIEditorWorkspaceModel& workspace) {
if (workspace.activePanelId.empty()) {
return nullptr;
}
std::vector<UIEditorWorkspaceVisiblePanel> visiblePanels =
CollectUIEditorWorkspaceVisiblePanels(workspace);
for (const UIEditorWorkspaceVisiblePanel& panel : visiblePanels) {
if (panel.panelId == workspace.activePanelId) {
return FindPanelRecursive(workspace.root, workspace.activePanelId);
}
}
return nullptr;
}
bool TryActivateUIEditorWorkspacePanel(
UIEditorWorkspaceModel& workspace,
std::string_view panelId) {
if (!TryActivateRecursive(workspace.root, panelId)) {
return false;
}
workspace.activePanelId = std::string(panelId);
return true;
}
bool TrySetUIEditorWorkspaceSplitRatio(
UIEditorWorkspaceModel& workspace,
std::string_view nodeId,
float splitRatio) {
if (!IsValidSplitRatio(splitRatio)) {
return false;
}
UIEditorWorkspaceNode* node = FindMutableNodeRecursive(workspace.root, nodeId);
if (node == nullptr || node->kind != UIEditorWorkspaceNodeKind::Split) {
return false;
}
if (std::fabs(node->splitRatio - splitRatio) <= 0.0001f) {
return false;
}
node->splitRatio = splitRatio;
return true;
}
bool TryReorderUIEditorWorkspaceTab(
UIEditorWorkspaceModel& workspace,
const UIEditorWorkspaceSession& session,
std::string_view nodeId,
std::string_view panelId,
std::size_t targetVisibleInsertionIndex) {
UIEditorWorkspaceNode* node = FindMutableNodeRecursive(workspace.root, nodeId);
if (node == nullptr || node->kind != UIEditorWorkspaceNodeKind::TabStack) {
return false;
}
std::vector<std::size_t> visibleChildIndices = {};
std::vector<UIEditorWorkspaceNode> reorderedVisibleChildren = {};
visibleChildIndices.reserve(node->children.size());
reorderedVisibleChildren.reserve(node->children.size());
std::size_t sourceVisibleIndex = 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) {
return false;
}
if (!IsPanelOpenAndVisibleInSession(session, child.panel.panelId)) {
continue;
}
if (child.panel.panelId == panelId) {
sourceVisibleIndex = visibleChildIndices.size();
}
visibleChildIndices.push_back(index);
reorderedVisibleChildren.push_back(child);
}
if (sourceVisibleIndex >= reorderedVisibleChildren.size() ||
targetVisibleInsertionIndex > reorderedVisibleChildren.size()) {
return false;
}
if (targetVisibleInsertionIndex == sourceVisibleIndex ||
targetVisibleInsertionIndex == sourceVisibleIndex + 1u) {
return false;
}
UIEditorWorkspaceNode movedChild =
std::move(reorderedVisibleChildren[sourceVisibleIndex]);
reorderedVisibleChildren.erase(
reorderedVisibleChildren.begin() + static_cast<std::ptrdiff_t>(sourceVisibleIndex));
std::size_t adjustedInsertionIndex = targetVisibleInsertionIndex;
if (adjustedInsertionIndex > sourceVisibleIndex) {
--adjustedInsertionIndex;
}
if (adjustedInsertionIndex > reorderedVisibleChildren.size()) {
adjustedInsertionIndex = reorderedVisibleChildren.size();
}
reorderedVisibleChildren.insert(
reorderedVisibleChildren.begin() +
static_cast<std::ptrdiff_t>(adjustedInsertionIndex),
std::move(movedChild));
std::string selectedPanelId = {};
if (node->selectedTabIndex < node->children.size()) {
selectedPanelId = node->children[node->selectedTabIndex].panel.panelId;
}
const std::vector<UIEditorWorkspaceNode> originalChildren = node->children;
std::size_t nextVisibleIndex = 0u;
for (std::size_t index = 0; index < originalChildren.size(); ++index) {
const UIEditorWorkspaceNode& originalChild = originalChildren[index];
if (!IsPanelOpenAndVisibleInSession(session, originalChild.panel.panelId)) {
node->children[index] = originalChild;
continue;
}
node->children[index] = reorderedVisibleChildren[nextVisibleIndex];
++nextVisibleIndex;
}
for (std::size_t index = 0; index < node->children.size(); ++index) {
if (node->children[index].panel.panelId == selectedPanelId) {
node->selectedTabIndex = index;
break;
}
}
return true;
}
bool TryMoveUIEditorWorkspaceTabToStack(
UIEditorWorkspaceModel& workspace,
const UIEditorWorkspaceSession& session,
std::string_view sourceNodeId,
std::string_view panelId,
std::string_view targetNodeId,
std::size_t targetVisibleInsertionIndex) {
if (sourceNodeId.empty() ||
panelId.empty() ||
targetNodeId.empty()) {
return false;
}
if (sourceNodeId == targetNodeId) {
return TryReorderUIEditorWorkspaceTab(
workspace,
session,
sourceNodeId,
panelId,
targetVisibleInsertionIndex);
}
const UIEditorWorkspaceNode* targetNode =
FindUIEditorWorkspaceNode(workspace, targetNodeId);
if (targetNode == nullptr ||
targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) {
return false;
}
if (targetVisibleInsertionIndex > CountVisibleChildren(*targetNode, session)) {
return false;
}
UIEditorWorkspaceNode extractedPanel = {};
if (!TryExtractVisiblePanelFromTabStack(
workspace,
session,
sourceNodeId,
panelId,
extractedPanel)) {
return false;
}
UIEditorWorkspaceNode* targetStack =
FindMutableNodeRecursive(workspace.root, targetNodeId);
if (targetStack == nullptr ||
targetStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
return false;
}
const std::size_t actualInsertionIndex =
ResolveActualInsertionIndexForVisibleInsertion(
*targetStack,
session,
targetVisibleInsertionIndex);
if (actualInsertionIndex > targetStack->children.size()) {
return false;
}
targetStack->children.insert(
targetStack->children.begin() +
static_cast<std::ptrdiff_t>(actualInsertionIndex),
std::move(extractedPanel));
targetStack->selectedTabIndex = actualInsertionIndex;
workspace.activePanelId = std::string(panelId);
workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace));
return true;
}
bool TryDockUIEditorWorkspaceTabRelative(
UIEditorWorkspaceModel& workspace,
const UIEditorWorkspaceSession& session,
std::string_view sourceNodeId,
std::string_view panelId,
std::string_view targetNodeId,
UIEditorWorkspaceDockPlacement placement,
float splitRatio) {
if (placement == UIEditorWorkspaceDockPlacement::Center) {
const UIEditorWorkspaceNode* targetNode =
FindUIEditorWorkspaceNode(workspace, targetNodeId);
if (targetNode == nullptr ||
targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) {
return false;
}
return TryMoveUIEditorWorkspaceTabToStack(
workspace,
session,
sourceNodeId,
panelId,
targetNodeId,
CountVisibleChildren(*targetNode, session));
}
if (sourceNodeId.empty() ||
panelId.empty() ||
targetNodeId.empty()) {
return false;
}
const UIEditorWorkspaceNode* sourceNode =
FindUIEditorWorkspaceNode(workspace, sourceNodeId);
const UIEditorWorkspaceNode* targetNode =
FindUIEditorWorkspaceNode(workspace, targetNodeId);
if (sourceNode == nullptr ||
targetNode == nullptr ||
sourceNode->kind != UIEditorWorkspaceNodeKind::TabStack ||
targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) {
return false;
}
if (sourceNodeId == targetNodeId &&
sourceNode->children.size() <= 1u) {
return false;
}
UIEditorWorkspaceNode extractedPanel = {};
if (!TryExtractVisiblePanelFromTabStack(
workspace,
session,
sourceNodeId,
panelId,
extractedPanel)) {
return false;
}
UIEditorWorkspaceNode* targetStack =
FindMutableNodeRecursive(workspace.root, targetNodeId);
if (targetStack == nullptr ||
targetStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
return false;
}
const std::string movedStackNodeId = MakeUniqueNodeId(
workspace,
std::string(targetNodeId) + "__dock_" + std::string(panelId) + "_stack");
UIEditorWorkspaceNode movedStack = {};
movedStack.kind = UIEditorWorkspaceNodeKind::TabStack;
movedStack.nodeId = movedStackNodeId;
movedStack.selectedTabIndex = 0u;
movedStack.children.push_back(std::move(extractedPanel));
UIEditorWorkspaceNode existingTarget = std::move(*targetStack);
UIEditorWorkspaceNode primary = {};
UIEditorWorkspaceNode secondary = {};
if (IsLeadingDockPlacement(placement)) {
primary = std::move(movedStack);
secondary = std::move(existingTarget);
} else {
primary = std::move(existingTarget);
secondary = std::move(movedStack);
}
const float requestedRatio = ClampDockSplitRatio(splitRatio);
const float resolvedSplitRatio =
IsLeadingDockPlacement(placement)
? requestedRatio
: (1.0f - requestedRatio);
*targetStack = BuildUIEditorWorkspaceSplit(
MakeUniqueNodeId(
workspace,
std::string(targetNodeId) + "__dock_split"),
ResolveDockSplitAxis(placement),
resolvedSplitRatio,
std::move(primary),
std::move(secondary));
workspace.activePanelId = std::string(panelId);
workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace));
return true;
}
} // namespace XCEngine::UI::Editor