关键节点
This commit is contained in:
308
editor/src/Workspace/SplitterDragCorrection/Chain.cpp
Normal file
308
editor/src/Workspace/SplitterDragCorrection/Chain.cpp
Normal file
@@ -0,0 +1,308 @@
|
||||
#include "Workspace/SplitterDragCorrection/Internal.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace XCEngine::UI::Editor::Internal {
|
||||
|
||||
namespace {
|
||||
|
||||
bool FindWorkspaceNodePathRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
std::string_view targetNodeId,
|
||||
std::vector<const UIEditorWorkspaceNode*>& path) {
|
||||
path.push_back(&node);
|
||||
if (node.nodeId == targetNodeId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
if (FindWorkspaceNodePathRecursive(child, targetNodeId, path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
path.pop_back();
|
||||
return false;
|
||||
}
|
||||
|
||||
std::size_t CollectSplitChainLayoutRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
const Widgets::UIEditorDockHostMetrics& metrics,
|
||||
UIEditorWorkspaceSplitAxis chainAxis,
|
||||
SplitChainLayout& outChain) {
|
||||
const DockHostNodeMeasureResult nodeMeasure =
|
||||
MeasureDockHostNode(node, panelRegistry, session, metrics);
|
||||
if (!nodeMeasure.visible) {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
if (node.kind != UIEditorWorkspaceNodeKind::Split ||
|
||||
node.splitAxis != chainAxis) {
|
||||
outChain.leaves.push_back(
|
||||
{ GetMainExtent(bounds, chainAxis), GetMainExtent(nodeMeasure.minimumSize, chainAxis) });
|
||||
return 1u;
|
||||
}
|
||||
|
||||
const DockHostNodeMeasureResult primaryMeasure =
|
||||
MeasureDockHostNode(node.children[0], panelRegistry, session, metrics);
|
||||
const DockHostNodeMeasureResult secondaryMeasure =
|
||||
MeasureDockHostNode(node.children[1], panelRegistry, session, metrics);
|
||||
if (!primaryMeasure.visible) {
|
||||
return CollectSplitChainLayoutRecursive(
|
||||
node.children[1],
|
||||
bounds,
|
||||
panelRegistry,
|
||||
session,
|
||||
metrics,
|
||||
chainAxis,
|
||||
outChain);
|
||||
}
|
||||
if (!secondaryMeasure.visible) {
|
||||
return CollectSplitChainLayoutRecursive(
|
||||
node.children[0],
|
||||
bounds,
|
||||
panelRegistry,
|
||||
session,
|
||||
metrics,
|
||||
chainAxis,
|
||||
outChain);
|
||||
}
|
||||
|
||||
::XCEngine::UI::Layout::UISplitterConstraints constraints = {};
|
||||
constraints.primaryMin = GetMainExtent(primaryMeasure.minimumSize, node.splitAxis);
|
||||
constraints.secondaryMin = GetMainExtent(secondaryMeasure.minimumSize, node.splitAxis);
|
||||
const auto arranged = ::XCEngine::UI::Layout::ArrangeUISplitter(
|
||||
bounds,
|
||||
ToUILayoutAxis(node.splitAxis),
|
||||
node.splitRatio,
|
||||
constraints,
|
||||
metrics.splitterMetrics);
|
||||
|
||||
const std::size_t leafStart = outChain.leaves.size();
|
||||
const std::size_t primaryLeafCount = CollectSplitChainLayoutRecursive(
|
||||
node.children[0],
|
||||
arranged.primaryRect,
|
||||
panelRegistry,
|
||||
session,
|
||||
metrics,
|
||||
chainAxis,
|
||||
outChain);
|
||||
const std::size_t secondaryLeafCount = CollectSplitChainLayoutRecursive(
|
||||
node.children[1],
|
||||
arranged.secondaryRect,
|
||||
panelRegistry,
|
||||
session,
|
||||
metrics,
|
||||
chainAxis,
|
||||
outChain);
|
||||
|
||||
if (primaryLeafCount > 0u && secondaryLeafCount > 0u) {
|
||||
outChain.splits.push_back({ node.nodeId, leafStart, primaryLeafCount });
|
||||
}
|
||||
return primaryLeafCount + secondaryLeafCount;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const UIEditorWorkspaceNode* FindSameAxisChainRoot(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
std::string_view splitNodeId) {
|
||||
std::vector<const UIEditorWorkspaceNode*> path = {};
|
||||
if (!FindWorkspaceNodePathRecursive(workspace.root, splitNodeId, path) ||
|
||||
path.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceNode* root = path.back();
|
||||
if (root->kind != UIEditorWorkspaceNodeKind::Split) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceSplitAxis axis = root->splitAxis;
|
||||
for (std::size_t index = path.size(); index > 1u; --index) {
|
||||
const UIEditorWorkspaceNode* parent = path[index - 2u];
|
||||
if (parent->kind != UIEditorWorkspaceNodeKind::Split ||
|
||||
parent->splitAxis != axis) {
|
||||
break;
|
||||
}
|
||||
root = parent;
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
bool BuildSplitChainLayout(
|
||||
const UIEditorWorkspaceLayoutSnapshot& snapshot,
|
||||
const Widgets::UIEditorDockHostLayout& layout,
|
||||
std::string_view splitNodeId,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const Widgets::UIEditorDockHostMetrics& metrics,
|
||||
SplitChainLayout& outChain) {
|
||||
const UIEditorWorkspaceNode* chainRoot =
|
||||
FindSameAxisChainRoot(snapshot.workspace, splitNodeId);
|
||||
if (chainRoot == nullptr || chainRoot->kind != UIEditorWorkspaceNodeKind::Split) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Widgets::UIEditorDockHostSplitterLayout* rootSplitter =
|
||||
Widgets::FindUIEditorDockHostSplitterLayout(layout, chainRoot->nodeId);
|
||||
if (rootSplitter == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outChain = {};
|
||||
outChain.root = chainRoot;
|
||||
outChain.axis = chainRoot->splitAxis;
|
||||
CollectSplitChainLayoutRecursive(
|
||||
*chainRoot,
|
||||
rootSplitter->bounds,
|
||||
panelRegistry,
|
||||
snapshot.session,
|
||||
metrics,
|
||||
outChain.axis,
|
||||
outChain);
|
||||
return outChain.leaves.size() >= 2u;
|
||||
}
|
||||
|
||||
const SplitChainSplitInfo* FindSplitChainSplitInfo(
|
||||
const SplitChainLayout& chain,
|
||||
std::string_view nodeId) {
|
||||
const auto it = std::find_if(
|
||||
chain.splits.begin(),
|
||||
chain.splits.end(),
|
||||
[nodeId](const SplitChainSplitInfo& split) {
|
||||
return split.nodeId == nodeId;
|
||||
});
|
||||
return it == chain.splits.end() ? nullptr : &(*it);
|
||||
}
|
||||
|
||||
float ClampLocalBoundaryDelta(
|
||||
const SplitChainLeaf& leading,
|
||||
const SplitChainLeaf& trailing,
|
||||
float delta) {
|
||||
const float minimumDelta = leading.minimumExtent - leading.extent;
|
||||
const float maximumDelta = trailing.extent - trailing.minimumExtent;
|
||||
return (std::clamp)(delta, minimumDelta, maximumDelta);
|
||||
}
|
||||
|
||||
SplitRatioResolveResult ResolveSplitChainRatiosRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
UIEditorWorkspaceSplitAxis chainAxis,
|
||||
const std::vector<float>& leafExtents,
|
||||
float handleThickness,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
const Widgets::UIEditorDockHostMetrics& metrics,
|
||||
std::size_t& leafCursor,
|
||||
std::vector<SplitRatioAssignment>& outAssignments) {
|
||||
const DockHostNodeMeasureResult nodeMeasure =
|
||||
MeasureDockHostNode(node, panelRegistry, session, metrics);
|
||||
if (!nodeMeasure.visible) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (node.kind != UIEditorWorkspaceNodeKind::Split ||
|
||||
node.splitAxis != chainAxis) {
|
||||
if (leafCursor >= leafExtents.size()) {
|
||||
return {};
|
||||
}
|
||||
const float extent = leafExtents[leafCursor];
|
||||
++leafCursor;
|
||||
return { 1u, extent };
|
||||
}
|
||||
|
||||
const DockHostNodeMeasureResult primaryMeasure =
|
||||
MeasureDockHostNode(node.children[0], panelRegistry, session, metrics);
|
||||
const DockHostNodeMeasureResult secondaryMeasure =
|
||||
MeasureDockHostNode(node.children[1], panelRegistry, session, metrics);
|
||||
if (!primaryMeasure.visible) {
|
||||
return ResolveSplitChainRatiosRecursive(
|
||||
node.children[1],
|
||||
chainAxis,
|
||||
leafExtents,
|
||||
handleThickness,
|
||||
panelRegistry,
|
||||
session,
|
||||
metrics,
|
||||
leafCursor,
|
||||
outAssignments);
|
||||
}
|
||||
if (!secondaryMeasure.visible) {
|
||||
return ResolveSplitChainRatiosRecursive(
|
||||
node.children[0],
|
||||
chainAxis,
|
||||
leafExtents,
|
||||
handleThickness,
|
||||
panelRegistry,
|
||||
session,
|
||||
metrics,
|
||||
leafCursor,
|
||||
outAssignments);
|
||||
}
|
||||
|
||||
const SplitRatioResolveResult primary = ResolveSplitChainRatiosRecursive(
|
||||
node.children[0],
|
||||
chainAxis,
|
||||
leafExtents,
|
||||
handleThickness,
|
||||
panelRegistry,
|
||||
session,
|
||||
metrics,
|
||||
leafCursor,
|
||||
outAssignments);
|
||||
const SplitRatioResolveResult secondary = ResolveSplitChainRatiosRecursive(
|
||||
node.children[1],
|
||||
chainAxis,
|
||||
leafExtents,
|
||||
handleThickness,
|
||||
panelRegistry,
|
||||
session,
|
||||
metrics,
|
||||
leafCursor,
|
||||
outAssignments);
|
||||
|
||||
if (primary.leafCount == 0u || secondary.leafCount == 0u) {
|
||||
return primary.leafCount > 0u ? primary : secondary;
|
||||
}
|
||||
|
||||
const float usableExtent = primary.totalExtent + secondary.totalExtent;
|
||||
const float splitRatio =
|
||||
usableExtent <= 0.0f ? node.splitRatio : primary.totalExtent / usableExtent;
|
||||
outAssignments.push_back({ node.nodeId, splitRatio });
|
||||
return {
|
||||
primary.leafCount + secondary.leafCount,
|
||||
primary.totalExtent + secondary.totalExtent + handleThickness
|
||||
};
|
||||
}
|
||||
|
||||
::XCEngine::UI::Layout::UISplitterLayoutResult BuildRequestedSplitterLayoutFromPointer(
|
||||
const Widgets::UIEditorDockHostSplitterLayout& splitter,
|
||||
const ::XCEngine::UI::UIPoint& pointerPosition) {
|
||||
::XCEngine::UI::Layout::UISplitterLayoutOptions options = {};
|
||||
options.axis = ToUILayoutAxis(splitter.axis);
|
||||
options.ratio = splitter.splitterLayout.splitRatio;
|
||||
options.handleThickness = splitter.metrics.thickness;
|
||||
options.minPrimaryExtent = splitter.constraints.primaryMin;
|
||||
options.minSecondaryExtent = splitter.constraints.secondaryMin;
|
||||
|
||||
const float pointerMainPosition =
|
||||
splitter.axis == UIEditorWorkspaceSplitAxis::Horizontal
|
||||
? pointerPosition.x
|
||||
: pointerPosition.y;
|
||||
const float requestedRatio =
|
||||
::XCEngine::UI::Layout::ResolveSplitterRatioFromPointerPosition(
|
||||
options,
|
||||
splitter.bounds,
|
||||
pointerMainPosition);
|
||||
return ::XCEngine::UI::Layout::ArrangeUISplitter(
|
||||
splitter.bounds,
|
||||
options.axis,
|
||||
requestedRatio,
|
||||
splitter.constraints,
|
||||
splitter.metrics);
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor::Internal
|
||||
179
editor/src/Workspace/SplitterDragCorrection/Correction.cpp
Normal file
179
editor/src/Workspace/SplitterDragCorrection/Correction.cpp
Normal file
@@ -0,0 +1,179 @@
|
||||
#include "Workspace/SplitterDragCorrection/Internal.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
bool IsUIEditorWorkspaceSplitterDragCorrectionActive(
|
||||
const UIEditorWorkspaceSplitterDragCorrectionState& state) {
|
||||
return !state.splitNodeId.empty();
|
||||
}
|
||||
|
||||
void ResetUIEditorWorkspaceSplitterDragCorrectionState(
|
||||
UIEditorWorkspaceSplitterDragCorrectionState& state) {
|
||||
state = {};
|
||||
}
|
||||
|
||||
void BeginUIEditorWorkspaceSplitterDragCorrection(
|
||||
UIEditorWorkspaceSplitterDragCorrectionState& state,
|
||||
std::string_view splitNodeId,
|
||||
const UIEditorWorkspaceLayoutSnapshot& baselineSnapshot,
|
||||
const Widgets::UIEditorDockHostLayout& baselineDockLayout) {
|
||||
state.splitNodeId = std::string(splitNodeId);
|
||||
state.baselineSnapshot = baselineSnapshot;
|
||||
state.baselineDockLayout = baselineDockLayout;
|
||||
}
|
||||
|
||||
bool TryBuildUIEditorWorkspaceSplitterDragCorrectedSnapshot(
|
||||
const UIEditorWorkspaceSplitterDragCorrectionState& state,
|
||||
const UIPoint& pointerPosition,
|
||||
bool hasPointerPosition,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const Widgets::UIEditorDockHostMetrics& metrics,
|
||||
UIEditorWorkspaceLayoutSnapshot& outSnapshot) {
|
||||
if (!IsUIEditorWorkspaceSplitterDragCorrectionActive(state) ||
|
||||
!hasPointerPosition) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Widgets::UIEditorDockHostSplitterLayout* baselineSplitter =
|
||||
Widgets::FindUIEditorDockHostSplitterLayout(
|
||||
state.baselineDockLayout,
|
||||
state.splitNodeId);
|
||||
if (baselineSplitter == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Internal::SplitChainLayout chain = {};
|
||||
if (!Internal::BuildSplitChainLayout(
|
||||
state.baselineSnapshot,
|
||||
state.baselineDockLayout,
|
||||
state.splitNodeId,
|
||||
panelRegistry,
|
||||
metrics,
|
||||
chain)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Internal::SplitChainSplitInfo* activeSplit =
|
||||
Internal::FindSplitChainSplitInfo(chain, state.splitNodeId);
|
||||
if (activeSplit == nullptr || activeSplit->primaryLeafCount == 0u) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::size_t boundaryLeafIndex =
|
||||
activeSplit->leafStart + activeSplit->primaryLeafCount - 1u;
|
||||
if (boundaryLeafIndex + 1u >= chain.leaves.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto requestedLayout = Internal::BuildRequestedSplitterLayoutFromPointer(
|
||||
*baselineSplitter,
|
||||
pointerPosition);
|
||||
const float delta = Internal::ClampLocalBoundaryDelta(
|
||||
chain.leaves[boundaryLeafIndex],
|
||||
chain.leaves[boundaryLeafIndex + 1u],
|
||||
requestedLayout.primaryExtent - baselineSplitter->splitterLayout.primaryExtent);
|
||||
if (std::fabs(delta) <= 0.0001f) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<float> leafExtents = {};
|
||||
leafExtents.reserve(chain.leaves.size());
|
||||
for (const Internal::SplitChainLeaf& leaf : chain.leaves) {
|
||||
leafExtents.push_back(leaf.extent);
|
||||
}
|
||||
leafExtents[boundaryLeafIndex] += delta;
|
||||
leafExtents[boundaryLeafIndex + 1u] -= delta;
|
||||
|
||||
outSnapshot = state.baselineSnapshot;
|
||||
std::vector<Internal::SplitRatioAssignment> ratioAssignments = {};
|
||||
std::size_t leafCursor = 0u;
|
||||
Internal::ResolveSplitChainRatiosRecursive(
|
||||
*chain.root,
|
||||
chain.axis,
|
||||
leafExtents,
|
||||
metrics.splitterMetrics.thickness,
|
||||
panelRegistry,
|
||||
outSnapshot.session,
|
||||
metrics,
|
||||
leafCursor,
|
||||
ratioAssignments);
|
||||
if (leafCursor != leafExtents.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
for (const Internal::SplitRatioAssignment& assignment : ratioAssignments) {
|
||||
const UIEditorWorkspaceNode* currentNode =
|
||||
FindUIEditorWorkspaceNode(outSnapshot.workspace, assignment.nodeId);
|
||||
if (currentNode == nullptr || currentNode->kind != UIEditorWorkspaceNodeKind::Split) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (std::fabs(currentNode->splitRatio - assignment.splitRatio) <= 0.0001f) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TrySetUIEditorWorkspaceSplitRatio(
|
||||
outSnapshot.workspace,
|
||||
assignment.nodeId,
|
||||
assignment.splitRatio)) {
|
||||
return false;
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool TryApplyUIEditorWorkspaceSplitterDragCorrection(
|
||||
UIEditorWorkspaceSplitterDragCorrectionState& state,
|
||||
const UIEditorDockHostInteractionState& dockHostInteractionState,
|
||||
const UIEditorWorkspaceLayoutSnapshot& preUpdateSnapshot,
|
||||
const Widgets::UIEditorDockHostLayout& preUpdateDockLayout,
|
||||
UIEditorWorkspaceController& workspaceController,
|
||||
const Widgets::UIEditorDockHostMetrics& metrics) {
|
||||
const std::string& activeSplitterNodeId =
|
||||
dockHostInteractionState.dockHostState.activeSplitterNodeId;
|
||||
if (!dockHostInteractionState.splitterDragState.active ||
|
||||
activeSplitterNodeId.empty()) {
|
||||
ResetUIEditorWorkspaceSplitterDragCorrectionState(state);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsUIEditorWorkspaceSplitterDragCorrectionActive(state) ||
|
||||
state.splitNodeId != activeSplitterNodeId) {
|
||||
BeginUIEditorWorkspaceSplitterDragCorrection(
|
||||
state,
|
||||
activeSplitterNodeId,
|
||||
preUpdateSnapshot,
|
||||
preUpdateDockLayout);
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutSnapshot correctedSnapshot = {};
|
||||
if (!TryBuildUIEditorWorkspaceSplitterDragCorrectedSnapshot(
|
||||
state,
|
||||
dockHostInteractionState.pointerPosition,
|
||||
dockHostInteractionState.hasPointerPosition,
|
||||
workspaceController.GetPanelRegistry(),
|
||||
metrics,
|
||||
correctedSnapshot)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceLayoutSnapshot currentSnapshot =
|
||||
workspaceController.CaptureLayoutSnapshot();
|
||||
if (AreUIEditorWorkspaceLayoutSnapshotsEquivalent(
|
||||
currentSnapshot,
|
||||
correctedSnapshot)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceLayoutOperationResult correctionResult =
|
||||
workspaceController.RestoreLayoutSnapshot(correctedSnapshot);
|
||||
return correctionResult.status == UIEditorWorkspaceLayoutOperationStatus::Changed;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
74
editor/src/Workspace/SplitterDragCorrection/Internal.h
Normal file
74
editor/src/Workspace/SplitterDragCorrection/Internal.h
Normal file
@@ -0,0 +1,74 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceSplitterDragCorrection.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceMutation.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceQueries.h>
|
||||
|
||||
#include "Docking/DockHostMeasureInternal.h"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::UI::Editor::Internal {
|
||||
|
||||
struct SplitChainLeaf {
|
||||
float extent = 0.0f;
|
||||
float minimumExtent = 0.0f;
|
||||
};
|
||||
|
||||
struct SplitChainSplitInfo {
|
||||
std::string nodeId = {};
|
||||
std::size_t leafStart = 0u;
|
||||
std::size_t primaryLeafCount = 0u;
|
||||
};
|
||||
|
||||
struct SplitChainLayout {
|
||||
const UIEditorWorkspaceNode* root = nullptr;
|
||||
UIEditorWorkspaceSplitAxis axis = UIEditorWorkspaceSplitAxis::Horizontal;
|
||||
std::vector<SplitChainLeaf> leaves = {};
|
||||
std::vector<SplitChainSplitInfo> splits = {};
|
||||
};
|
||||
|
||||
struct SplitRatioAssignment {
|
||||
std::string nodeId = {};
|
||||
float splitRatio = 0.5f;
|
||||
};
|
||||
|
||||
struct SplitRatioResolveResult {
|
||||
std::size_t leafCount = 0u;
|
||||
float totalExtent = 0.0f;
|
||||
};
|
||||
|
||||
const UIEditorWorkspaceNode* FindSameAxisChainRoot(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
std::string_view splitNodeId);
|
||||
bool BuildSplitChainLayout(
|
||||
const UIEditorWorkspaceLayoutSnapshot& snapshot,
|
||||
const Widgets::UIEditorDockHostLayout& layout,
|
||||
std::string_view splitNodeId,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const Widgets::UIEditorDockHostMetrics& metrics,
|
||||
SplitChainLayout& outChain);
|
||||
const SplitChainSplitInfo* FindSplitChainSplitInfo(
|
||||
const SplitChainLayout& chain,
|
||||
std::string_view nodeId);
|
||||
float ClampLocalBoundaryDelta(
|
||||
const SplitChainLeaf& leading,
|
||||
const SplitChainLeaf& trailing,
|
||||
float delta);
|
||||
SplitRatioResolveResult ResolveSplitChainRatiosRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
UIEditorWorkspaceSplitAxis chainAxis,
|
||||
const std::vector<float>& leafExtents,
|
||||
float handleThickness,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
const Widgets::UIEditorDockHostMetrics& metrics,
|
||||
std::size_t& leafCursor,
|
||||
std::vector<SplitRatioAssignment>& outAssignments);
|
||||
::XCEngine::UI::Layout::UISplitterLayoutResult BuildRequestedSplitterLayoutFromPointer(
|
||||
const Widgets::UIEditorDockHostSplitterLayout& splitter,
|
||||
const ::XCEngine::UI::UIPoint& pointerPosition);
|
||||
|
||||
} // namespace XCEngine::UI::Editor::Internal
|
||||
82
editor/src/Workspace/UIEditorDetachedWindowPolicy.cpp
Normal file
82
editor/src/Workspace/UIEditorDetachedWindowPolicy.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
#include <XCEditor/Workspace/UIEditorDetachedWindowPolicy.h>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
bool IsRootPanelVisible(
|
||||
const UIEditorWorkspaceController& controller,
|
||||
std::string_view panelId) {
|
||||
const UIEditorPanelSessionState* panelState =
|
||||
FindUIEditorPanelSessionState(controller.GetSession(), panelId);
|
||||
return panelState != nullptr && panelState->open && panelState->visible;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const UIEditorPanelDescriptor* ResolveUIEditorSingleVisibleRootPanelDescriptor(
|
||||
const UIEditorWorkspaceController& controller) {
|
||||
const UIEditorWorkspaceNode& root = controller.GetWorkspace().root;
|
||||
if (root.kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const UIEditorPanelRegistry& panelRegistry = controller.GetPanelRegistry();
|
||||
const UIEditorPanelDescriptor* visibleDescriptor = nullptr;
|
||||
std::size_t visibleCount = 0u;
|
||||
for (const UIEditorWorkspaceNode& child : root.children) {
|
||||
if (child.kind != UIEditorWorkspaceNodeKind::Panel ||
|
||||
!IsRootPanelVisible(controller, child.panel.panelId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const UIEditorPanelDescriptor* descriptor =
|
||||
FindUIEditorPanelDescriptor(panelRegistry, child.panel.panelId);
|
||||
if (descriptor == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
++visibleCount;
|
||||
visibleDescriptor = descriptor;
|
||||
if (visibleCount > 1u) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
return visibleCount == 1u ? visibleDescriptor : nullptr;
|
||||
}
|
||||
|
||||
bool HasUIEditorSingleVisibleRootTab(
|
||||
const UIEditorWorkspaceController& controller) {
|
||||
return ResolveUIEditorSingleVisibleRootPanelDescriptor(controller) != nullptr;
|
||||
}
|
||||
|
||||
std::string ResolveUIEditorDetachedWorkspaceTitle(
|
||||
const UIEditorWorkspaceController& controller,
|
||||
std::string_view fallbackTitle) {
|
||||
const std::string& activePanelId = controller.GetWorkspace().activePanelId;
|
||||
if (const UIEditorPanelDescriptor* descriptor =
|
||||
FindUIEditorPanelDescriptor(controller.GetPanelRegistry(), activePanelId);
|
||||
descriptor != nullptr &&
|
||||
!descriptor->defaultTitle.empty()) {
|
||||
return descriptor->defaultTitle;
|
||||
}
|
||||
|
||||
return std::string(fallbackTitle);
|
||||
}
|
||||
|
||||
::XCEngine::UI::UISize ResolveUIEditorDetachedWorkspaceMinimumOuterSize(
|
||||
const UIEditorWorkspaceController& controller,
|
||||
const ::XCEngine::UI::UISize& fallbackSize) {
|
||||
if (const UIEditorPanelDescriptor* descriptor =
|
||||
ResolveUIEditorSingleVisibleRootPanelDescriptor(controller);
|
||||
descriptor != nullptr &&
|
||||
descriptor->minimumDetachedWindowSize.width > 0.0f &&
|
||||
descriptor->minimumDetachedWindowSize.height > 0.0f) {
|
||||
return descriptor->minimumDetachedWindowSize;
|
||||
}
|
||||
|
||||
return fallbackSize;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
476
editor/src/Workspace/UIEditorWindowWorkspaceController.cpp
Normal file
476
editor/src/Workspace/UIEditorWindowWorkspaceController.cpp
Normal file
@@ -0,0 +1,476 @@
|
||||
#include <XCEditor/Workspace/UIEditorWindowWorkspaceController.h>
|
||||
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceMutation.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceTransfer.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
bool IsSinglePanelRootWindow(
|
||||
const UIEditorWindowWorkspaceState& state,
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId) {
|
||||
const UIEditorWorkspaceNode& root = state.workspace.root;
|
||||
return root.kind == UIEditorWorkspaceNodeKind::TabStack &&
|
||||
root.nodeId == sourceNodeId &&
|
||||
root.children.size() == 1u &&
|
||||
root.selectedTabIndex == 0u &&
|
||||
root.children.front().kind == UIEditorWorkspaceNodeKind::Panel &&
|
||||
root.children.front().panel.panelId == panelId &&
|
||||
state.session.panelStates.size() == 1u &&
|
||||
state.session.panelStates.front().panelId == panelId &&
|
||||
state.session.panelStates.front().open &&
|
||||
state.session.panelStates.front().visible;
|
||||
}
|
||||
|
||||
bool TryExtractPanelFromWindow(
|
||||
UIEditorWindowWorkspaceSet& windowSet,
|
||||
std::string_view sourceWindowId,
|
||||
std::string_view primaryWindowId,
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
UIEditorWorkspaceExtractedPanel& extractedPanel) {
|
||||
UIEditorWindowWorkspaceState* sourceWindow =
|
||||
FindMutableUIEditorWindowWorkspaceState(windowSet, sourceWindowId);
|
||||
if (sourceWindow == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sourceWindowId != primaryWindowId &&
|
||||
IsSinglePanelRootWindow(*sourceWindow, sourceNodeId, panelId)) {
|
||||
extractedPanel.panelNode = std::move(sourceWindow->workspace.root.children.front());
|
||||
extractedPanel.sessionState = sourceWindow->session.panelStates.front();
|
||||
windowSet.windows.erase(
|
||||
std::remove_if(
|
||||
windowSet.windows.begin(),
|
||||
windowSet.windows.end(),
|
||||
[sourceWindowId](const UIEditorWindowWorkspaceState& state) {
|
||||
return state.windowId == sourceWindowId;
|
||||
}),
|
||||
windowSet.windows.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
return TryExtractUIEditorWorkspaceVisiblePanel(
|
||||
sourceWindow->workspace,
|
||||
sourceWindow->session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
extractedPanel);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string_view GetUIEditorWindowWorkspaceOperationStatusName(
|
||||
UIEditorWindowWorkspaceOperationStatus status) {
|
||||
switch (status) {
|
||||
case UIEditorWindowWorkspaceOperationStatus::Changed:
|
||||
return "Changed";
|
||||
case UIEditorWindowWorkspaceOperationStatus::NoOp:
|
||||
return "NoOp";
|
||||
case UIEditorWindowWorkspaceOperationStatus::Rejected:
|
||||
return "Rejected";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
UIEditorWindowWorkspaceController::UIEditorWindowWorkspaceController(
|
||||
UIEditorPanelRegistry panelRegistry,
|
||||
UIEditorWindowWorkspaceSet windowSet)
|
||||
: m_panelRegistry(std::move(panelRegistry))
|
||||
, m_windowSet(std::move(windowSet)) {
|
||||
}
|
||||
|
||||
UIEditorWindowWorkspaceValidationResult UIEditorWindowWorkspaceController::ValidateState() const {
|
||||
return ValidateUIEditorWindowWorkspaceSet(m_panelRegistry, m_windowSet);
|
||||
}
|
||||
|
||||
UIEditorWindowWorkspaceOperationResult UIEditorWindowWorkspaceController::BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus status,
|
||||
std::string message,
|
||||
std::string_view sourceWindowId,
|
||||
std::string_view targetWindowId,
|
||||
std::string_view panelId) const {
|
||||
UIEditorWindowWorkspaceOperationResult result = {};
|
||||
result.status = status;
|
||||
result.message = std::move(message);
|
||||
result.sourceWindowId = std::string(sourceWindowId);
|
||||
result.targetWindowId = std::string(targetWindowId);
|
||||
result.panelId = std::string(panelId);
|
||||
result.activeWindowId = m_windowSet.activeWindowId;
|
||||
result.windowIds.reserve(m_windowSet.windows.size());
|
||||
for (const UIEditorWindowWorkspaceState& state : m_windowSet.windows) {
|
||||
result.windowIds.push_back(state.windowId);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string UIEditorWindowWorkspaceController::MakeUniqueWindowId(std::string_view base) const {
|
||||
std::string resolvedBase = base.empty()
|
||||
? std::string("detached-window")
|
||||
: std::string(base);
|
||||
if (FindUIEditorWindowWorkspaceState(m_windowSet, resolvedBase) == nullptr) {
|
||||
return resolvedBase;
|
||||
}
|
||||
|
||||
for (std::size_t suffix = 1u; suffix < 1024u; ++suffix) {
|
||||
const std::string candidate = resolvedBase + "-" + std::to_string(suffix);
|
||||
if (FindUIEditorWindowWorkspaceState(m_windowSet, candidate) == nullptr) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedBase + "-overflow";
|
||||
}
|
||||
|
||||
UIEditorWindowWorkspaceOperationResult UIEditorWindowWorkspaceController::DetachPanelToNewWindow(
|
||||
std::string_view sourceWindowId,
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view preferredNewWindowId) {
|
||||
const UIEditorWindowWorkspaceValidationResult stateValidation = ValidateState();
|
||||
if (!stateValidation.IsValid()) {
|
||||
return BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
||||
"Window workspace state invalid: " + stateValidation.message,
|
||||
sourceWindowId,
|
||||
{},
|
||||
panelId);
|
||||
}
|
||||
|
||||
const UIEditorWindowWorkspaceState* sourceWindow =
|
||||
FindUIEditorWindowWorkspaceState(m_windowSet, sourceWindowId);
|
||||
if (sourceWindow == nullptr) {
|
||||
return BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
||||
"Source window not found.",
|
||||
sourceWindowId,
|
||||
{},
|
||||
panelId);
|
||||
}
|
||||
|
||||
if (sourceWindowId != m_windowSet.primaryWindowId &&
|
||||
IsSinglePanelRootWindow(*sourceWindow, sourceNodeId, panelId)) {
|
||||
return BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus::NoOp,
|
||||
"Panel already occupies its own detached window.",
|
||||
sourceWindowId,
|
||||
sourceWindowId,
|
||||
panelId);
|
||||
}
|
||||
|
||||
const UIEditorWindowWorkspaceSet windowSetBefore = m_windowSet;
|
||||
UIEditorWorkspaceExtractedPanel extractedPanel = {};
|
||||
if (!TryExtractPanelFromWindow(
|
||||
m_windowSet,
|
||||
sourceWindowId,
|
||||
m_windowSet.primaryWindowId,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
extractedPanel)) {
|
||||
m_windowSet = windowSetBefore;
|
||||
return BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
||||
"Failed to extract panel from source window.",
|
||||
sourceWindowId,
|
||||
{},
|
||||
panelId);
|
||||
}
|
||||
|
||||
const std::string newWindowId = MakeUniqueWindowId(
|
||||
preferredNewWindowId.empty()
|
||||
? std::string(panelId) + "-window"
|
||||
: std::string(preferredNewWindowId));
|
||||
UIEditorWindowWorkspaceState detachedWindow = {};
|
||||
detachedWindow.windowId = newWindowId;
|
||||
detachedWindow.workspace =
|
||||
BuildUIEditorDetachedWorkspaceFromExtractedPanel(
|
||||
newWindowId + "-root",
|
||||
extractedPanel);
|
||||
detachedWindow.session =
|
||||
BuildUIEditorDetachedWorkspaceSessionFromExtractedPanel(extractedPanel);
|
||||
m_windowSet.windows.push_back(std::move(detachedWindow));
|
||||
m_windowSet.activeWindowId = newWindowId;
|
||||
|
||||
const UIEditorWindowWorkspaceValidationResult validation = ValidateState();
|
||||
if (!validation.IsValid()) {
|
||||
m_windowSet = windowSetBefore;
|
||||
return BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
||||
"Detach produced invalid state: " + validation.message,
|
||||
sourceWindowId,
|
||||
newWindowId,
|
||||
panelId);
|
||||
}
|
||||
|
||||
return BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus::Changed,
|
||||
"Panel detached into a new window.",
|
||||
sourceWindowId,
|
||||
newWindowId,
|
||||
panelId);
|
||||
}
|
||||
|
||||
UIEditorWindowWorkspaceOperationResult UIEditorWindowWorkspaceController::MovePanelToStack(
|
||||
std::string_view sourceWindowId,
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view targetWindowId,
|
||||
std::string_view targetNodeId,
|
||||
std::size_t targetVisibleInsertionIndex) {
|
||||
const UIEditorWindowWorkspaceValidationResult stateValidation = ValidateState();
|
||||
if (!stateValidation.IsValid()) {
|
||||
return BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
||||
"Window workspace state invalid: " + stateValidation.message,
|
||||
sourceWindowId,
|
||||
targetWindowId,
|
||||
panelId);
|
||||
}
|
||||
|
||||
if (sourceWindowId == targetWindowId) {
|
||||
const UIEditorWindowWorkspaceSet windowSetBefore = m_windowSet;
|
||||
UIEditorWindowWorkspaceState* window =
|
||||
FindMutableUIEditorWindowWorkspaceState(m_windowSet, sourceWindowId);
|
||||
if (window == nullptr) {
|
||||
return BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
||||
"Source window not found.",
|
||||
sourceWindowId,
|
||||
targetWindowId,
|
||||
panelId);
|
||||
}
|
||||
|
||||
if (!TryMoveUIEditorWorkspaceTabToStack(
|
||||
window->workspace,
|
||||
window->session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
targetNodeId,
|
||||
targetVisibleInsertionIndex)) {
|
||||
return BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
||||
"Move operation rejected by the workspace model.",
|
||||
sourceWindowId,
|
||||
targetWindowId,
|
||||
panelId);
|
||||
}
|
||||
|
||||
m_windowSet.activeWindowId = std::string(targetWindowId);
|
||||
const UIEditorWindowWorkspaceValidationResult validation = ValidateState();
|
||||
if (!validation.IsValid()) {
|
||||
m_windowSet = windowSetBefore;
|
||||
return BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
||||
"Move produced invalid state: " + validation.message,
|
||||
sourceWindowId,
|
||||
targetWindowId,
|
||||
panelId);
|
||||
}
|
||||
|
||||
return BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus::Changed,
|
||||
"Panel moved within the same window.",
|
||||
sourceWindowId,
|
||||
targetWindowId,
|
||||
panelId);
|
||||
}
|
||||
|
||||
const UIEditorWindowWorkspaceSet windowSetBefore = m_windowSet;
|
||||
UIEditorWorkspaceExtractedPanel extractedPanel = {};
|
||||
if (!TryExtractPanelFromWindow(
|
||||
m_windowSet,
|
||||
sourceWindowId,
|
||||
m_windowSet.primaryWindowId,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
extractedPanel)) {
|
||||
m_windowSet = windowSetBefore;
|
||||
return BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
||||
"Failed to extract panel from source window.",
|
||||
sourceWindowId,
|
||||
targetWindowId,
|
||||
panelId);
|
||||
}
|
||||
|
||||
UIEditorWindowWorkspaceState* targetWindow =
|
||||
FindMutableUIEditorWindowWorkspaceState(m_windowSet, targetWindowId);
|
||||
if (targetWindow == nullptr ||
|
||||
!TryInsertExtractedUIEditorWorkspacePanelToStack(
|
||||
targetWindow->workspace,
|
||||
targetWindow->session,
|
||||
std::move(extractedPanel),
|
||||
targetNodeId,
|
||||
targetVisibleInsertionIndex)) {
|
||||
m_windowSet = windowSetBefore;
|
||||
return BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
||||
"Failed to insert the extracted panel into the target stack.",
|
||||
sourceWindowId,
|
||||
targetWindowId,
|
||||
panelId);
|
||||
}
|
||||
|
||||
m_windowSet.activeWindowId = std::string(targetWindowId);
|
||||
const UIEditorWindowWorkspaceValidationResult validation = ValidateState();
|
||||
if (!validation.IsValid()) {
|
||||
m_windowSet = windowSetBefore;
|
||||
return BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
||||
"Move produced invalid state: " + validation.message,
|
||||
sourceWindowId,
|
||||
targetWindowId,
|
||||
panelId);
|
||||
}
|
||||
|
||||
return BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus::Changed,
|
||||
"Panel moved across windows.",
|
||||
sourceWindowId,
|
||||
targetWindowId,
|
||||
panelId);
|
||||
}
|
||||
|
||||
UIEditorWindowWorkspaceOperationResult UIEditorWindowWorkspaceController::DockPanelRelative(
|
||||
std::string_view sourceWindowId,
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view targetWindowId,
|
||||
std::string_view targetNodeId,
|
||||
UIEditorWorkspaceDockPlacement placement,
|
||||
float splitRatio) {
|
||||
const UIEditorWindowWorkspaceValidationResult stateValidation = ValidateState();
|
||||
if (!stateValidation.IsValid()) {
|
||||
return BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
||||
"Window workspace state invalid: " + stateValidation.message,
|
||||
sourceWindowId,
|
||||
targetWindowId,
|
||||
panelId);
|
||||
}
|
||||
|
||||
if (sourceWindowId == targetWindowId) {
|
||||
const UIEditorWindowWorkspaceSet windowSetBefore = m_windowSet;
|
||||
UIEditorWindowWorkspaceState* window =
|
||||
FindMutableUIEditorWindowWorkspaceState(m_windowSet, sourceWindowId);
|
||||
if (window == nullptr) {
|
||||
return BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
||||
"Source window not found.",
|
||||
sourceWindowId,
|
||||
targetWindowId,
|
||||
panelId);
|
||||
}
|
||||
|
||||
if (!TryDockUIEditorWorkspaceTabRelative(
|
||||
window->workspace,
|
||||
window->session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
targetNodeId,
|
||||
placement,
|
||||
splitRatio)) {
|
||||
return BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
||||
"Dock operation rejected by the workspace model.",
|
||||
sourceWindowId,
|
||||
targetWindowId,
|
||||
panelId);
|
||||
}
|
||||
|
||||
m_windowSet.activeWindowId = std::string(targetWindowId);
|
||||
const UIEditorWindowWorkspaceValidationResult validation = ValidateState();
|
||||
if (!validation.IsValid()) {
|
||||
m_windowSet = windowSetBefore;
|
||||
return BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
||||
"Dock produced invalid state: " + validation.message,
|
||||
sourceWindowId,
|
||||
targetWindowId,
|
||||
panelId);
|
||||
}
|
||||
|
||||
return BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus::Changed,
|
||||
"Panel docked within the same window.",
|
||||
sourceWindowId,
|
||||
targetWindowId,
|
||||
panelId);
|
||||
}
|
||||
|
||||
const UIEditorWindowWorkspaceSet windowSetBefore = m_windowSet;
|
||||
UIEditorWorkspaceExtractedPanel extractedPanel = {};
|
||||
if (!TryExtractPanelFromWindow(
|
||||
m_windowSet,
|
||||
sourceWindowId,
|
||||
m_windowSet.primaryWindowId,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
extractedPanel)) {
|
||||
m_windowSet = windowSetBefore;
|
||||
return BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
||||
"Failed to extract panel from source window.",
|
||||
sourceWindowId,
|
||||
targetWindowId,
|
||||
panelId);
|
||||
}
|
||||
|
||||
UIEditorWindowWorkspaceState* targetWindow =
|
||||
FindMutableUIEditorWindowWorkspaceState(m_windowSet, targetWindowId);
|
||||
if (targetWindow == nullptr ||
|
||||
!TryDockExtractedUIEditorWorkspacePanelRelative(
|
||||
targetWindow->workspace,
|
||||
targetWindow->session,
|
||||
std::move(extractedPanel),
|
||||
targetNodeId,
|
||||
placement,
|
||||
splitRatio)) {
|
||||
m_windowSet = windowSetBefore;
|
||||
return BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
||||
"Failed to dock the extracted panel into the target window.",
|
||||
sourceWindowId,
|
||||
targetWindowId,
|
||||
panelId);
|
||||
}
|
||||
|
||||
m_windowSet.activeWindowId = std::string(targetWindowId);
|
||||
const UIEditorWindowWorkspaceValidationResult validation = ValidateState();
|
||||
if (!validation.IsValid()) {
|
||||
m_windowSet = windowSetBefore;
|
||||
return BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus::Rejected,
|
||||
"Dock produced invalid state: " + validation.message,
|
||||
sourceWindowId,
|
||||
targetWindowId,
|
||||
panelId);
|
||||
}
|
||||
|
||||
return BuildOperationResult(
|
||||
UIEditorWindowWorkspaceOperationStatus::Changed,
|
||||
"Panel docked across windows.",
|
||||
sourceWindowId,
|
||||
targetWindowId,
|
||||
panelId);
|
||||
}
|
||||
|
||||
UIEditorWindowWorkspaceController BuildDefaultUIEditorWindowWorkspaceController(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
std::string primaryWindowId) {
|
||||
return UIEditorWindowWorkspaceController(
|
||||
panelRegistry,
|
||||
BuildDefaultUIEditorWindowWorkspaceSet(
|
||||
panelRegistry,
|
||||
workspace,
|
||||
std::move(primaryWindowId)));
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
159
editor/src/Workspace/UIEditorWindowWorkspaceModel.cpp
Normal file
159
editor/src/Workspace/UIEditorWindowWorkspaceModel.cpp
Normal file
@@ -0,0 +1,159 @@
|
||||
#include <XCEditor/Workspace/UIEditorWindowWorkspaceModel.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceValidation.h>
|
||||
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
UIEditorWindowWorkspaceValidationResult MakeValidationError(
|
||||
UIEditorWindowWorkspaceValidationCode code,
|
||||
std::string message) {
|
||||
UIEditorWindowWorkspaceValidationResult result = {};
|
||||
result.code = code;
|
||||
result.message = std::move(message);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UIEditorWindowWorkspaceSet BuildDefaultUIEditorWindowWorkspaceSet(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
std::string primaryWindowId) {
|
||||
if (primaryWindowId.empty()) {
|
||||
primaryWindowId = "main-window";
|
||||
}
|
||||
|
||||
UIEditorWindowWorkspaceSet windowSet = {};
|
||||
windowSet.primaryWindowId = primaryWindowId;
|
||||
windowSet.activeWindowId = primaryWindowId;
|
||||
|
||||
UIEditorWindowWorkspaceState state = {};
|
||||
state.windowId = primaryWindowId;
|
||||
state.workspace = CanonicalizeUIEditorWorkspaceModel(workspace);
|
||||
state.session = BuildDefaultUIEditorWorkspaceSession(panelRegistry, state.workspace);
|
||||
windowSet.windows.push_back(std::move(state));
|
||||
return windowSet;
|
||||
}
|
||||
|
||||
const UIEditorWindowWorkspaceState* FindUIEditorWindowWorkspaceState(
|
||||
const UIEditorWindowWorkspaceSet& windowSet,
|
||||
std::string_view windowId) {
|
||||
for (const UIEditorWindowWorkspaceState& state : windowSet.windows) {
|
||||
if (state.windowId == windowId) {
|
||||
return &state;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UIEditorWindowWorkspaceState* FindMutableUIEditorWindowWorkspaceState(
|
||||
UIEditorWindowWorkspaceSet& windowSet,
|
||||
std::string_view windowId) {
|
||||
for (UIEditorWindowWorkspaceState& state : windowSet.windows) {
|
||||
if (state.windowId == windowId) {
|
||||
return &state;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UIEditorWindowWorkspaceValidationResult ValidateUIEditorWindowWorkspaceSet(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWindowWorkspaceSet& windowSet) {
|
||||
const UIEditorPanelRegistryValidationResult registryValidation =
|
||||
ValidateUIEditorPanelRegistry(panelRegistry);
|
||||
if (!registryValidation.IsValid()) {
|
||||
return MakeValidationError(
|
||||
UIEditorWindowWorkspaceValidationCode::InvalidPanelRegistry,
|
||||
registryValidation.message);
|
||||
}
|
||||
|
||||
if (windowSet.primaryWindowId.empty()) {
|
||||
return MakeValidationError(
|
||||
UIEditorWindowWorkspaceValidationCode::MissingPrimaryWindow,
|
||||
"Primary window id must not be empty.");
|
||||
}
|
||||
|
||||
if (windowSet.activeWindowId.empty()) {
|
||||
return MakeValidationError(
|
||||
UIEditorWindowWorkspaceValidationCode::MissingActiveWindow,
|
||||
"Active window id must not be empty.");
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> seenWindowIds = {};
|
||||
std::unordered_map<std::string, std::string> panelOwners = {};
|
||||
bool hasPrimaryWindow = false;
|
||||
bool hasActiveWindow = false;
|
||||
for (const UIEditorWindowWorkspaceState& state : windowSet.windows) {
|
||||
if (state.windowId.empty()) {
|
||||
return MakeValidationError(
|
||||
UIEditorWindowWorkspaceValidationCode::EmptyWindowId,
|
||||
"Window id must not be empty.");
|
||||
}
|
||||
|
||||
if (!seenWindowIds.insert(state.windowId).second) {
|
||||
return MakeValidationError(
|
||||
UIEditorWindowWorkspaceValidationCode::DuplicateWindowId,
|
||||
"Window id '" + state.windowId + "' is duplicated.");
|
||||
}
|
||||
|
||||
if (state.windowId == windowSet.primaryWindowId) {
|
||||
hasPrimaryWindow = true;
|
||||
}
|
||||
if (state.windowId == windowSet.activeWindowId) {
|
||||
hasActiveWindow = true;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceValidationResult workspaceValidation =
|
||||
ValidateUIEditorWorkspace(state.workspace);
|
||||
if (!workspaceValidation.IsValid()) {
|
||||
return MakeValidationError(
|
||||
UIEditorWindowWorkspaceValidationCode::InvalidWorkspace,
|
||||
"Window '" + state.windowId + "' workspace invalid: " +
|
||||
workspaceValidation.message);
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceSessionValidationResult sessionValidation =
|
||||
ValidateUIEditorWorkspaceSession(panelRegistry, state.workspace, state.session);
|
||||
if (!sessionValidation.IsValid()) {
|
||||
return MakeValidationError(
|
||||
UIEditorWindowWorkspaceValidationCode::InvalidSession,
|
||||
"Window '" + state.windowId + "' session invalid: " +
|
||||
sessionValidation.message);
|
||||
}
|
||||
|
||||
for (const UIEditorPanelSessionState& panelState : state.session.panelStates) {
|
||||
const auto [ownerIt, inserted] =
|
||||
panelOwners.emplace(panelState.panelId, state.windowId);
|
||||
if (!inserted) {
|
||||
return MakeValidationError(
|
||||
UIEditorWindowWorkspaceValidationCode::DuplicatePanelAcrossWindows,
|
||||
"Panel '" + panelState.panelId + "' is present in both window '" +
|
||||
ownerIt->second + "' and window '" + state.windowId + "'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasPrimaryWindow) {
|
||||
return MakeValidationError(
|
||||
UIEditorWindowWorkspaceValidationCode::MissingPrimaryWindow,
|
||||
"Primary window '" + windowSet.primaryWindowId + "' does not exist.");
|
||||
}
|
||||
|
||||
if (!hasActiveWindow) {
|
||||
return MakeValidationError(
|
||||
UIEditorWindowWorkspaceValidationCode::MissingActiveWindow,
|
||||
"Active window '" + windowSet.activeWindowId + "' does not exist.");
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
380
editor/src/Workspace/UIEditorWorkspaceCompose.cpp
Normal file
380
editor/src/Workspace/UIEditorWorkspaceCompose.cpp
Normal file
@@ -0,0 +1,380 @@
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceCompose.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
using Widgets::AppendUIEditorDockHostBackground;
|
||||
using Widgets::AppendUIEditorDockHostForeground;
|
||||
using Widgets::AppendUIEditorViewportSlotBackground;
|
||||
using Widgets::AppendUIEditorViewportSlotForeground;
|
||||
using Widgets::BuildUIEditorDockHostLayout;
|
||||
using Widgets::UIEditorDockHostForegroundOptions;
|
||||
|
||||
const UIEditorWorkspacePanelPresentationState* FindPanelStateImpl(
|
||||
const UIEditorWorkspaceComposeState& state,
|
||||
std::string_view panelId) {
|
||||
for (const UIEditorWorkspacePanelPresentationState& panelState : state.panelStates) {
|
||||
if (panelState.panelId == panelId) {
|
||||
return &panelState;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UIEditorWorkspacePanelPresentationState* FindMutablePanelStateImpl(
|
||||
UIEditorWorkspaceComposeState& state,
|
||||
std::string_view panelId) {
|
||||
for (UIEditorWorkspacePanelPresentationState& panelState : state.panelStates) {
|
||||
if (panelState.panelId == panelId) {
|
||||
return &panelState;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool SupportsExternalViewportPresentation(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspacePanelPresentationModel& presentation) {
|
||||
if (presentation.kind != UIEditorPanelPresentationKind::ViewportShell) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const UIEditorPanelDescriptor* descriptor =
|
||||
FindUIEditorPanelDescriptor(panelRegistry, presentation.panelId);
|
||||
return descriptor != nullptr &&
|
||||
descriptor->presentationKind == UIEditorPanelPresentationKind::ViewportShell;
|
||||
}
|
||||
|
||||
UIEditorWorkspacePanelPresentationState& EnsurePanelState(
|
||||
UIEditorWorkspaceComposeState& state,
|
||||
std::string_view panelId) {
|
||||
for (UIEditorWorkspacePanelPresentationState& panelState : state.panelStates) {
|
||||
if (panelState.panelId == panelId) {
|
||||
return panelState;
|
||||
}
|
||||
}
|
||||
|
||||
UIEditorWorkspacePanelPresentationState panelState = {};
|
||||
panelState.panelId = std::string(panelId);
|
||||
state.panelStates.push_back(std::move(panelState));
|
||||
return state.panelStates.back();
|
||||
}
|
||||
|
||||
void ResetHiddenViewportPresentationState(
|
||||
UIEditorWorkspaceComposeState& state,
|
||||
std::string_view panelId) {
|
||||
UIEditorWorkspacePanelPresentationState* panelState =
|
||||
FindMutablePanelStateImpl(state, panelId);
|
||||
if (panelState == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
panelState->viewportShellState = {};
|
||||
}
|
||||
|
||||
void TrimObsoleteViewportPresentationStates(
|
||||
UIEditorWorkspaceComposeState& state,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const std::vector<UIEditorWorkspacePanelPresentationModel>& presentations) {
|
||||
state.panelStates.erase(
|
||||
std::remove_if(
|
||||
state.panelStates.begin(),
|
||||
state.panelStates.end(),
|
||||
[&](const UIEditorWorkspacePanelPresentationState& panelState) {
|
||||
for (const UIEditorWorkspacePanelPresentationModel& presentation : presentations) {
|
||||
if (presentation.panelId == panelState.panelId &&
|
||||
SupportsExternalViewportPresentation(panelRegistry, presentation)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
state.panelStates.end());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const UIEditorWorkspacePanelPresentationModel* FindUIEditorWorkspacePanelPresentationModel(
|
||||
const std::vector<UIEditorWorkspacePanelPresentationModel>& presentations,
|
||||
std::string_view panelId) {
|
||||
for (const UIEditorWorkspacePanelPresentationModel& presentation : presentations) {
|
||||
if (presentation.panelId == panelId) {
|
||||
return &presentation;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const UIEditorWorkspacePanelPresentationState* FindUIEditorWorkspacePanelPresentationState(
|
||||
const UIEditorWorkspaceComposeState& state,
|
||||
std::string_view panelId) {
|
||||
return FindPanelStateImpl(state, panelId);
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceViewportComposeRequest* FindUIEditorWorkspaceViewportPresentationRequest(
|
||||
const UIEditorWorkspaceComposeRequest& request,
|
||||
std::string_view panelId) {
|
||||
for (const UIEditorWorkspaceViewportComposeRequest& viewportRequest : request.viewportRequests) {
|
||||
if (viewportRequest.panelId == panelId) {
|
||||
return &viewportRequest;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceViewportComposeFrame* FindUIEditorWorkspaceViewportPresentationFrame(
|
||||
const UIEditorWorkspaceComposeFrame& frame,
|
||||
std::string_view panelId) {
|
||||
for (const UIEditorWorkspaceViewportComposeFrame& viewportFrame : frame.viewportFrames) {
|
||||
if (viewportFrame.panelId == panelId) {
|
||||
return &viewportFrame;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceComposeRequest ResolveUIEditorWorkspaceComposeRequest(
|
||||
const Widgets::UIEditorDockHostLayout& dockHostLayout,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const std::vector<UIEditorWorkspacePanelPresentationModel>& presentations,
|
||||
const Widgets::UIEditorViewportSlotMetrics& viewportMetrics,
|
||||
const UIEditorTextMeasurer* textMeasurer) {
|
||||
UIEditorWorkspaceComposeRequest request = {};
|
||||
request.dockHostLayout = dockHostLayout;
|
||||
request.contentHostRequest = ResolveUIEditorPanelContentHostRequest(
|
||||
request.dockHostLayout,
|
||||
panelRegistry);
|
||||
request.viewportRequests.reserve(request.contentHostRequest.mountRequests.size());
|
||||
|
||||
for (const UIEditorPanelContentHostMountRequest& mountRequest :
|
||||
request.contentHostRequest.mountRequests) {
|
||||
if (mountRequest.kind != UIEditorPanelPresentationKind::ViewportShell) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const UIEditorWorkspacePanelPresentationModel* presentation =
|
||||
FindUIEditorWorkspacePanelPresentationModel(presentations, mountRequest.panelId);
|
||||
if (presentation == nullptr ||
|
||||
!SupportsExternalViewportPresentation(panelRegistry, *presentation)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceViewportComposeRequest viewportRequest = {};
|
||||
viewportRequest.panelId = mountRequest.panelId;
|
||||
viewportRequest.bounds = mountRequest.bounds;
|
||||
viewportRequest.viewportShellModel = ResolveUIEditorViewportShellMeasuredModel(
|
||||
presentation->viewportShellModel,
|
||||
viewportMetrics,
|
||||
textMeasurer);
|
||||
viewportRequest.viewportShellRequest = ResolveUIEditorViewportShellRequest(
|
||||
mountRequest.bounds,
|
||||
viewportRequest.viewportShellModel.spec,
|
||||
viewportMetrics);
|
||||
request.viewportRequests.push_back(std::move(viewportRequest));
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceComposeRequest ResolveUIEditorWorkspaceComposeRequest(
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
const std::vector<UIEditorWorkspacePanelPresentationModel>& presentations,
|
||||
const Widgets::UIEditorDockHostState& dockHostState,
|
||||
const Widgets::UIEditorDockHostMetrics& dockHostMetrics,
|
||||
const Widgets::UIEditorViewportSlotMetrics& viewportMetrics,
|
||||
const UIEditorTextMeasurer* textMeasurer) {
|
||||
const Widgets::UIEditorDockHostLayout dockHostLayout = BuildUIEditorDockHostLayout(
|
||||
bounds,
|
||||
panelRegistry,
|
||||
workspace,
|
||||
session,
|
||||
dockHostState,
|
||||
dockHostMetrics,
|
||||
textMeasurer);
|
||||
return ResolveUIEditorWorkspaceComposeRequest(
|
||||
dockHostLayout,
|
||||
panelRegistry,
|
||||
presentations,
|
||||
viewportMetrics,
|
||||
textMeasurer);
|
||||
}
|
||||
|
||||
UIEditorWorkspaceComposeFrame UpdateUIEditorWorkspaceCompose(
|
||||
UIEditorWorkspaceComposeState& state,
|
||||
const UIEditorWorkspaceComposeRequest& request,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const std::vector<UIEditorWorkspacePanelPresentationModel>& presentations,
|
||||
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
|
||||
const UIEditorWorkspaceInputOwner* inputOwner) {
|
||||
UIEditorWorkspaceComposeFrame frame = {};
|
||||
frame.dockHostLayout = request.dockHostLayout;
|
||||
frame.contentHostFrame = UpdateUIEditorPanelContentHost(
|
||||
state.contentHostState,
|
||||
request.contentHostRequest,
|
||||
panelRegistry);
|
||||
TrimObsoleteViewportPresentationStates(state, panelRegistry, presentations);
|
||||
frame.viewportFrames.reserve(request.viewportRequests.size());
|
||||
|
||||
for (const UIEditorWorkspacePanelPresentationModel& presentation : presentations) {
|
||||
if (!SupportsExternalViewportPresentation(panelRegistry, presentation)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceViewportComposeRequest* viewportRequest =
|
||||
FindUIEditorWorkspaceViewportPresentationRequest(request, presentation.panelId);
|
||||
if (viewportRequest == nullptr) {
|
||||
ResetHiddenViewportPresentationState(state, presentation.panelId);
|
||||
continue;
|
||||
}
|
||||
|
||||
UIEditorWorkspacePanelPresentationState& panelState =
|
||||
EnsurePanelState(state, presentation.panelId);
|
||||
|
||||
UIEditorWorkspaceViewportComposeFrame viewportFrame = {};
|
||||
viewportFrame.panelId = presentation.panelId;
|
||||
viewportFrame.bounds = viewportRequest->bounds;
|
||||
viewportFrame.viewportShellModel = viewportRequest->viewportShellModel;
|
||||
UIEditorViewportInputBridgeRequest inputRequest = {};
|
||||
if (inputOwner != nullptr) {
|
||||
inputRequest.focusMode = UIEditorViewportInputBridgeFocusMode::External;
|
||||
inputRequest.focused =
|
||||
IsUIEditorWorkspaceViewportInputOwner(*inputOwner, presentation.panelId);
|
||||
}
|
||||
viewportFrame.viewportShellFrame = UpdateUIEditorViewportShell(
|
||||
panelState.viewportShellState,
|
||||
viewportRequest->viewportShellRequest,
|
||||
viewportFrame.viewportShellModel,
|
||||
inputEvents,
|
||||
inputRequest);
|
||||
frame.viewportFrames.push_back(std::move(viewportFrame));
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceComposeFrame UpdateUIEditorWorkspaceCompose(
|
||||
UIEditorWorkspaceComposeState& state,
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
const std::vector<UIEditorWorkspacePanelPresentationModel>& presentations,
|
||||
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
|
||||
const Widgets::UIEditorDockHostState& dockHostState,
|
||||
const Widgets::UIEditorDockHostMetrics& dockHostMetrics,
|
||||
const Widgets::UIEditorViewportSlotMetrics& viewportMetrics,
|
||||
const UIEditorWorkspaceInputOwner* inputOwner,
|
||||
const UIEditorTextMeasurer* textMeasurer) {
|
||||
const UIEditorWorkspaceComposeRequest request = ResolveUIEditorWorkspaceComposeRequest(
|
||||
bounds,
|
||||
panelRegistry,
|
||||
workspace,
|
||||
session,
|
||||
presentations,
|
||||
dockHostState,
|
||||
dockHostMetrics,
|
||||
viewportMetrics,
|
||||
textMeasurer);
|
||||
return UpdateUIEditorWorkspaceCompose(
|
||||
state,
|
||||
request,
|
||||
panelRegistry,
|
||||
presentations,
|
||||
inputEvents,
|
||||
inputOwner);
|
||||
}
|
||||
|
||||
std::vector<std::string> CollectUIEditorWorkspaceComposeExternalBodyPanelIds(
|
||||
const UIEditorWorkspaceComposeFrame& frame) {
|
||||
return CollectMountedUIEditorPanelContentHostPanelIds(frame.contentHostFrame);
|
||||
}
|
||||
|
||||
void AppendUIEditorWorkspaceCompose(
|
||||
::XCEngine::UI::UIDrawList& drawList,
|
||||
const UIEditorWorkspaceComposeFrame& frame,
|
||||
const Widgets::UIEditorDockHostPalette& dockHostPalette,
|
||||
const Widgets::UIEditorDockHostMetrics& dockHostMetrics,
|
||||
const Widgets::UIEditorViewportSlotPalette& viewportPalette,
|
||||
const Widgets::UIEditorViewportSlotMetrics& viewportMetrics,
|
||||
const UIEditorWorkspaceComposeAppendOptions& options) {
|
||||
AppendUIEditorDockHostBackground(
|
||||
drawList,
|
||||
frame.dockHostLayout,
|
||||
dockHostPalette,
|
||||
dockHostMetrics);
|
||||
|
||||
for (const UIEditorWorkspaceViewportComposeFrame& viewportFrame : frame.viewportFrames) {
|
||||
AppendUIEditorViewportSlotBackground(
|
||||
drawList,
|
||||
viewportFrame.viewportShellFrame.slotLayout,
|
||||
viewportFrame.viewportShellModel.spec.toolItems,
|
||||
viewportFrame.viewportShellModel.spec.statusSegments,
|
||||
viewportFrame.viewportShellFrame.slotState,
|
||||
viewportPalette,
|
||||
viewportMetrics);
|
||||
AppendUIEditorViewportSlotForeground(
|
||||
drawList,
|
||||
viewportFrame.viewportShellFrame.slotLayout,
|
||||
viewportFrame.viewportShellModel.spec.chrome,
|
||||
viewportFrame.viewportShellModel.frame,
|
||||
viewportFrame.viewportShellModel.spec.toolItems,
|
||||
viewportFrame.viewportShellModel.spec.statusSegments,
|
||||
viewportFrame.viewportShellFrame.slotState,
|
||||
viewportPalette,
|
||||
viewportMetrics,
|
||||
Widgets::UIEditorViewportSlotForegroundAppendOptions{
|
||||
options.includeViewportTextures
|
||||
});
|
||||
}
|
||||
|
||||
UIEditorDockHostForegroundOptions foregroundOptions = {};
|
||||
foregroundOptions.externalBodyPanelIds =
|
||||
CollectUIEditorWorkspaceComposeExternalBodyPanelIds(frame);
|
||||
foregroundOptions.deferDropPreviewOverlay = options.deferDockPreviewOverlay;
|
||||
AppendUIEditorDockHostForeground(
|
||||
drawList,
|
||||
frame.dockHostLayout,
|
||||
foregroundOptions,
|
||||
dockHostPalette,
|
||||
dockHostMetrics);
|
||||
}
|
||||
|
||||
void AppendUIEditorWorkspaceComposeOverlay(
|
||||
::XCEngine::UI::UIDrawList& drawList,
|
||||
const UIEditorWorkspaceComposeFrame& frame,
|
||||
const Widgets::UIEditorDockHostPalette& dockHostPalette,
|
||||
const Widgets::UIEditorDockHostMetrics& dockHostMetrics) {
|
||||
AppendUIEditorDockHostOverlay(
|
||||
drawList,
|
||||
frame.dockHostLayout,
|
||||
dockHostPalette,
|
||||
dockHostMetrics);
|
||||
}
|
||||
|
||||
void AppendUIEditorWorkspaceComposeViewportTextures(
|
||||
::XCEngine::UI::UIDrawList& drawList,
|
||||
const UIEditorWorkspaceComposeFrame& frame,
|
||||
const Widgets::UIEditorViewportSlotPalette& viewportPalette) {
|
||||
for (const UIEditorWorkspaceViewportComposeFrame& viewportFrame : frame.viewportFrames) {
|
||||
Widgets::AppendUIEditorViewportSlotSurfaceTexture(
|
||||
drawList,
|
||||
viewportFrame.viewportShellFrame.slotLayout,
|
||||
viewportFrame.viewportShellModel.frame,
|
||||
viewportPalette);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
804
editor/src/Workspace/UIEditorWorkspaceController.cpp
Normal file
804
editor/src/Workspace/UIEditorWorkspaceController.cpp
Normal file
@@ -0,0 +1,804 @@
|
||||
#include "Workspace/WorkspaceControllerInternal.h"
|
||||
|
||||
#include <XCEditor/Foundation/UIEditorRuntimeTrace.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceMutation.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor::Internal {
|
||||
|
||||
bool IsPanelOpenAndVisible(
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
const UIEditorPanelSessionState* panelState =
|
||||
FindUIEditorPanelSessionState(session, panelId);
|
||||
return panelState != nullptr && panelState->open && panelState->visible;
|
||||
}
|
||||
|
||||
std::vector<std::string> CollectVisiblePanelIds(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session) {
|
||||
const std::vector<UIEditorWorkspaceVisiblePanel> panels =
|
||||
CollectUIEditorWorkspaceVisiblePanels(workspace, session);
|
||||
|
||||
std::vector<std::string> ids = {};
|
||||
ids.reserve(panels.size());
|
||||
for (const UIEditorWorkspaceVisiblePanel& panel : panels) {
|
||||
ids.push_back(panel.panelId);
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
VisibleTabStackInfo ResolveVisibleTabStackInfo(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
VisibleTabStackInfo info = {};
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
if (child.kind != UIEditorWorkspaceNodeKind::Panel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool visible = IsPanelOpenAndVisible(session, child.panel.panelId);
|
||||
if (child.panel.panelId == panelId) {
|
||||
info.panelExists = true;
|
||||
info.panelVisible = visible;
|
||||
if (visible) {
|
||||
info.currentVisibleIndex = info.visibleTabCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
++info.visibleTabCount;
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
std::size_t CountVisibleTabs(
|
||||
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 &&
|
||||
IsPanelOpenAndVisible(session, child.panel.panelId)) {
|
||||
++visibleCount;
|
||||
}
|
||||
}
|
||||
|
||||
return visibleCount;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor::Internal
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
std::string_view GetUIEditorWorkspaceCommandKindName(UIEditorWorkspaceCommandKind kind) {
|
||||
switch (kind) {
|
||||
case UIEditorWorkspaceCommandKind::OpenPanel:
|
||||
return "OpenPanel";
|
||||
case UIEditorWorkspaceCommandKind::ClosePanel:
|
||||
return "ClosePanel";
|
||||
case UIEditorWorkspaceCommandKind::ShowPanel:
|
||||
return "ShowPanel";
|
||||
case UIEditorWorkspaceCommandKind::HidePanel:
|
||||
return "HidePanel";
|
||||
case UIEditorWorkspaceCommandKind::ActivatePanel:
|
||||
return "ActivatePanel";
|
||||
case UIEditorWorkspaceCommandKind::ResetWorkspace:
|
||||
return "ResetWorkspace";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
std::string_view GetUIEditorWorkspaceCommandStatusName(
|
||||
UIEditorWorkspaceCommandStatus status) {
|
||||
switch (status) {
|
||||
case UIEditorWorkspaceCommandStatus::Changed:
|
||||
return "Changed";
|
||||
case UIEditorWorkspaceCommandStatus::NoOp:
|
||||
return "NoOp";
|
||||
case UIEditorWorkspaceCommandStatus::Rejected:
|
||||
return "Rejected";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
std::string_view GetUIEditorWorkspaceLayoutOperationStatusName(
|
||||
UIEditorWorkspaceLayoutOperationStatus status) {
|
||||
switch (status) {
|
||||
case UIEditorWorkspaceLayoutOperationStatus::Changed:
|
||||
return "Changed";
|
||||
case UIEditorWorkspaceLayoutOperationStatus::NoOp:
|
||||
return "NoOp";
|
||||
case UIEditorWorkspaceLayoutOperationStatus::Rejected:
|
||||
return "Rejected";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
UIEditorWorkspaceController::UIEditorWorkspaceController(
|
||||
UIEditorPanelRegistry panelRegistry,
|
||||
UIEditorWorkspaceModel workspace,
|
||||
UIEditorWorkspaceSession session)
|
||||
: m_panelRegistry(std::move(panelRegistry))
|
||||
, m_baselineWorkspace(CanonicalizeUIEditorWorkspaceModel(workspace))
|
||||
, m_baselineSession(session)
|
||||
, m_workspace(m_baselineWorkspace)
|
||||
, m_session(std::move(session)) {
|
||||
}
|
||||
|
||||
UIEditorWorkspaceControllerValidationResult UIEditorWorkspaceController::ValidateState() const {
|
||||
const UIEditorPanelRegistryValidationResult registryValidation =
|
||||
ValidateUIEditorPanelRegistry(m_panelRegistry);
|
||||
if (!registryValidation.IsValid()) {
|
||||
UIEditorWorkspaceControllerValidationResult result = {};
|
||||
result.code = UIEditorWorkspaceControllerValidationCode::InvalidPanelRegistry;
|
||||
result.message = registryValidation.message;
|
||||
return result;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceValidationResult workspaceValidation =
|
||||
ValidateUIEditorWorkspace(m_workspace);
|
||||
if (!workspaceValidation.IsValid()) {
|
||||
UIEditorWorkspaceControllerValidationResult result = {};
|
||||
result.code = UIEditorWorkspaceControllerValidationCode::InvalidWorkspace;
|
||||
result.message = workspaceValidation.message;
|
||||
return result;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceSessionValidationResult sessionValidation =
|
||||
ValidateUIEditorWorkspaceSession(m_panelRegistry, m_workspace, m_session);
|
||||
if (!sessionValidation.IsValid()) {
|
||||
UIEditorWorkspaceControllerValidationResult result = {};
|
||||
result.code = UIEditorWorkspaceControllerValidationCode::InvalidWorkspaceSession;
|
||||
result.message = sessionValidation.message;
|
||||
return result;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutSnapshot UIEditorWorkspaceController::CaptureLayoutSnapshot() const {
|
||||
return BuildUIEditorWorkspaceLayoutSnapshot(m_workspace, m_session);
|
||||
}
|
||||
|
||||
UIEditorWorkspaceCommandResult UIEditorWorkspaceController::BuildResult(
|
||||
const UIEditorWorkspaceCommand& command,
|
||||
UIEditorWorkspaceCommandStatus status,
|
||||
std::string message) const {
|
||||
UIEditorWorkspaceCommandResult result = {};
|
||||
result.kind = command.kind;
|
||||
result.status = status;
|
||||
result.panelId = command.panelId;
|
||||
result.message = std::move(message);
|
||||
result.activePanelId = m_workspace.activePanelId;
|
||||
result.visiblePanelIds = Internal::CollectVisiblePanelIds(m_workspace, m_session);
|
||||
return result;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus status,
|
||||
std::string message) const {
|
||||
UIEditorWorkspaceLayoutOperationResult result = {};
|
||||
result.status = status;
|
||||
result.message = std::move(message);
|
||||
result.activePanelId = m_workspace.activePanelId;
|
||||
result.visiblePanelIds = Internal::CollectVisiblePanelIds(m_workspace, m_session);
|
||||
return result;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceCommandResult UIEditorWorkspaceController::FinalizeMutation(
|
||||
const UIEditorWorkspaceCommand& command,
|
||||
bool changed,
|
||||
std::string changedMessage,
|
||||
std::string unexpectedFailureMessage,
|
||||
const UIEditorWorkspaceModel& previousWorkspace,
|
||||
const UIEditorWorkspaceSession& previousSession) {
|
||||
if (!changed) {
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::Rejected,
|
||||
std::move(unexpectedFailureMessage));
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
||||
if (!validation.IsValid()) {
|
||||
m_workspace = previousWorkspace;
|
||||
m_session = previousSession;
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::Rejected,
|
||||
"Command produced invalid workspace state: " + validation.message);
|
||||
}
|
||||
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::Changed,
|
||||
std::move(changedMessage));
|
||||
}
|
||||
|
||||
const UIEditorPanelDescriptor* UIEditorWorkspaceController::FindPanelDescriptor(
|
||||
std::string_view panelId) const {
|
||||
return FindUIEditorPanelDescriptor(m_panelRegistry, panelId);
|
||||
}
|
||||
|
||||
UIEditorWorkspaceController BuildDefaultUIEditorWorkspaceController(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace) {
|
||||
const UIEditorWorkspaceModel canonicalWorkspace =
|
||||
CanonicalizeUIEditorWorkspaceModel(workspace);
|
||||
return UIEditorWorkspaceController(
|
||||
panelRegistry,
|
||||
canonicalWorkspace,
|
||||
BuildDefaultUIEditorWorkspaceSession(panelRegistry, canonicalWorkspace));
|
||||
}
|
||||
|
||||
UIEditorWorkspaceCommandResult UIEditorWorkspaceController::Dispatch(
|
||||
const UIEditorWorkspaceCommand& command) {
|
||||
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
||||
if (command.kind != UIEditorWorkspaceCommandKind::ResetWorkspace &&
|
||||
!validation.IsValid()) {
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::Rejected,
|
||||
"Controller state invalid: " + validation.message);
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceModel previousWorkspace = m_workspace;
|
||||
const UIEditorWorkspaceSession previousSession = m_session;
|
||||
const UIEditorPanelSessionState* panelState =
|
||||
command.kind == UIEditorWorkspaceCommandKind::ResetWorkspace
|
||||
? nullptr
|
||||
: FindUIEditorPanelSessionState(m_session, command.panelId);
|
||||
const UIEditorPanelDescriptor* panelDescriptor =
|
||||
command.kind == UIEditorWorkspaceCommandKind::ResetWorkspace
|
||||
? nullptr
|
||||
: FindPanelDescriptor(command.panelId);
|
||||
|
||||
switch (command.kind) {
|
||||
case UIEditorWorkspaceCommandKind::OpenPanel:
|
||||
if (command.panelId.empty()) {
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::Rejected,
|
||||
"OpenPanel requires a panelId.");
|
||||
}
|
||||
if (panelDescriptor == nullptr || panelState == nullptr) {
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::Rejected,
|
||||
"OpenPanel target panel is missing.");
|
||||
}
|
||||
if (panelState->open && panelState->visible) {
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::NoOp,
|
||||
"Panel is already open and visible.");
|
||||
}
|
||||
return FinalizeMutation(
|
||||
command,
|
||||
TryOpenUIEditorWorkspacePanel(
|
||||
m_panelRegistry,
|
||||
m_workspace,
|
||||
m_session,
|
||||
command.panelId),
|
||||
"Panel opened and activated.",
|
||||
"OpenPanel failed unexpectedly.",
|
||||
previousWorkspace,
|
||||
previousSession);
|
||||
|
||||
case UIEditorWorkspaceCommandKind::ClosePanel:
|
||||
if (command.panelId.empty()) {
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::Rejected,
|
||||
"ClosePanel requires a panelId.");
|
||||
}
|
||||
if (panelDescriptor == nullptr || panelState == nullptr) {
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::Rejected,
|
||||
"ClosePanel target panel is missing.");
|
||||
}
|
||||
if (!panelDescriptor->canClose) {
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::Rejected,
|
||||
"Panel cannot be closed.");
|
||||
}
|
||||
if (!panelState->open) {
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::NoOp,
|
||||
"Panel is already closed.");
|
||||
}
|
||||
return FinalizeMutation(
|
||||
command,
|
||||
TryCloseUIEditorWorkspacePanel(
|
||||
m_panelRegistry,
|
||||
m_workspace,
|
||||
m_session,
|
||||
command.panelId),
|
||||
"Panel closed.",
|
||||
"ClosePanel failed unexpectedly.",
|
||||
previousWorkspace,
|
||||
previousSession);
|
||||
|
||||
case UIEditorWorkspaceCommandKind::ShowPanel:
|
||||
if (command.panelId.empty()) {
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::Rejected,
|
||||
"ShowPanel requires a panelId.");
|
||||
}
|
||||
if (panelDescriptor == nullptr || panelState == nullptr) {
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::Rejected,
|
||||
"ShowPanel target panel is missing.");
|
||||
}
|
||||
if (!panelState->open) {
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::Rejected,
|
||||
"Closed panel must be opened before it can be shown.");
|
||||
}
|
||||
if (panelState->visible) {
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::NoOp,
|
||||
"Panel is already visible.");
|
||||
}
|
||||
return FinalizeMutation(
|
||||
command,
|
||||
TryShowUIEditorWorkspacePanel(
|
||||
m_panelRegistry,
|
||||
m_workspace,
|
||||
m_session,
|
||||
command.panelId),
|
||||
"Panel shown and activated.",
|
||||
"ShowPanel failed unexpectedly.",
|
||||
previousWorkspace,
|
||||
previousSession);
|
||||
|
||||
case UIEditorWorkspaceCommandKind::HidePanel:
|
||||
if (command.panelId.empty()) {
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::Rejected,
|
||||
"HidePanel requires a panelId.");
|
||||
}
|
||||
if (panelDescriptor == nullptr || panelState == nullptr) {
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::Rejected,
|
||||
"HidePanel target panel is missing.");
|
||||
}
|
||||
if (!panelDescriptor->canHide) {
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::Rejected,
|
||||
"Panel cannot be hidden.");
|
||||
}
|
||||
if (!panelState->open) {
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::Rejected,
|
||||
"Closed panel cannot be hidden.");
|
||||
}
|
||||
if (!panelState->visible) {
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::NoOp,
|
||||
"Panel is already hidden.");
|
||||
}
|
||||
return FinalizeMutation(
|
||||
command,
|
||||
TryHideUIEditorWorkspacePanel(
|
||||
m_panelRegistry,
|
||||
m_workspace,
|
||||
m_session,
|
||||
command.panelId),
|
||||
"Panel hidden and active panel re-resolved.",
|
||||
"HidePanel failed unexpectedly.",
|
||||
previousWorkspace,
|
||||
previousSession);
|
||||
|
||||
case UIEditorWorkspaceCommandKind::ActivatePanel:
|
||||
if (command.panelId.empty()) {
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::Rejected,
|
||||
"ActivatePanel requires a panelId.");
|
||||
}
|
||||
if (panelDescriptor == nullptr || panelState == nullptr) {
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::Rejected,
|
||||
"ActivatePanel target panel is missing.");
|
||||
}
|
||||
if (!panelState->open || !panelState->visible) {
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::Rejected,
|
||||
"Only open and visible panels can be activated.");
|
||||
}
|
||||
if (m_workspace.activePanelId == command.panelId) {
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::NoOp,
|
||||
"Panel is already active.");
|
||||
}
|
||||
return FinalizeMutation(
|
||||
command,
|
||||
TryActivateUIEditorWorkspacePanel(
|
||||
m_panelRegistry,
|
||||
m_workspace,
|
||||
m_session,
|
||||
command.panelId),
|
||||
"Panel activated.",
|
||||
"ActivatePanel failed unexpectedly.",
|
||||
previousWorkspace,
|
||||
previousSession);
|
||||
|
||||
case UIEditorWorkspaceCommandKind::ResetWorkspace:
|
||||
if (AreUIEditorWorkspaceModelsEquivalent(m_workspace, m_baselineWorkspace) &&
|
||||
AreUIEditorWorkspaceSessionsEquivalent(m_session, m_baselineSession)) {
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::NoOp,
|
||||
"Workspace already matches the baseline state.");
|
||||
}
|
||||
|
||||
m_workspace = m_baselineWorkspace;
|
||||
m_session = m_baselineSession;
|
||||
return FinalizeMutation(
|
||||
command,
|
||||
true,
|
||||
"Workspace reset to baseline.",
|
||||
"ResetWorkspace failed unexpectedly.",
|
||||
previousWorkspace,
|
||||
previousSession);
|
||||
}
|
||||
|
||||
return BuildResult(
|
||||
command,
|
||||
UIEditorWorkspaceCommandStatus::Rejected,
|
||||
"Unknown command kind.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::RestoreLayoutSnapshot(
|
||||
const UIEditorWorkspaceLayoutSnapshot& snapshot) {
|
||||
UIEditorWorkspaceLayoutSnapshot canonicalSnapshot = snapshot;
|
||||
canonicalSnapshot.workspace =
|
||||
CanonicalizeUIEditorWorkspaceModel(std::move(canonicalSnapshot.workspace));
|
||||
|
||||
const UIEditorPanelRegistryValidationResult registryValidation =
|
||||
ValidateUIEditorPanelRegistry(m_panelRegistry);
|
||||
if (!registryValidation.IsValid()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Panel registry invalid: " + registryValidation.message);
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceValidationResult workspaceValidation =
|
||||
ValidateUIEditorWorkspace(canonicalSnapshot.workspace);
|
||||
if (!workspaceValidation.IsValid()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Layout workspace invalid: " + workspaceValidation.message);
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceSessionValidationResult sessionValidation =
|
||||
ValidateUIEditorWorkspaceSession(
|
||||
m_panelRegistry,
|
||||
canonicalSnapshot.workspace,
|
||||
canonicalSnapshot.session);
|
||||
if (!sessionValidation.IsValid()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Layout session invalid: " + sessionValidation.message);
|
||||
}
|
||||
|
||||
if (AreUIEditorWorkspaceModelsEquivalent(m_workspace, canonicalSnapshot.workspace) &&
|
||||
AreUIEditorWorkspaceSessionsEquivalent(m_session, canonicalSnapshot.session)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::NoOp,
|
||||
"Current state already matches the requested layout snapshot.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceModel previousWorkspace = m_workspace;
|
||||
const UIEditorWorkspaceSession previousSession = m_session;
|
||||
m_workspace = canonicalSnapshot.workspace;
|
||||
m_session = canonicalSnapshot.session;
|
||||
|
||||
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
||||
if (!validation.IsValid()) {
|
||||
m_workspace = previousWorkspace;
|
||||
m_session = previousSession;
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Restored layout produced invalid controller state: " + validation.message);
|
||||
}
|
||||
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Changed,
|
||||
"Layout restored.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::RestoreSerializedLayout(
|
||||
std::string_view serializedLayout) {
|
||||
const UIEditorWorkspaceLayoutLoadResult loadResult =
|
||||
DeserializeUIEditorWorkspaceLayoutSnapshot(m_panelRegistry, serializedLayout);
|
||||
if (!loadResult.IsValid()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Serialized layout rejected: " + loadResult.message);
|
||||
}
|
||||
|
||||
return RestoreLayoutSnapshot(loadResult.snapshot);
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::SetSplitRatio(
|
||||
std::string_view nodeId,
|
||||
float splitRatio) {
|
||||
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
||||
if (!validation.IsValid()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Controller state invalid: " + validation.message);
|
||||
}
|
||||
|
||||
if (nodeId.empty()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"SetSplitRatio requires a split node id.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceNode* splitNode = FindUIEditorWorkspaceNode(m_workspace, nodeId);
|
||||
if (splitNode == nullptr || splitNode->kind != UIEditorWorkspaceNodeKind::Split) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"SetSplitRatio target split node is missing.");
|
||||
}
|
||||
|
||||
if (std::fabs(splitNode->splitRatio - splitRatio) <= 0.0001f) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::NoOp,
|
||||
"Split ratio already matches the requested value.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceModel previousWorkspace = m_workspace;
|
||||
if (!TrySetUIEditorWorkspaceSplitRatio(m_workspace, nodeId, splitRatio)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Split ratio update rejected.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceControllerValidationResult postValidation = ValidateState();
|
||||
if (!postValidation.IsValid()) {
|
||||
m_workspace = previousWorkspace;
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Split ratio update produced invalid controller state: " + postValidation.message);
|
||||
}
|
||||
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Changed,
|
||||
"Split ratio updated.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::MoveTabToStack(
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view targetNodeId,
|
||||
std::size_t targetVisibleInsertionIndex) {
|
||||
{
|
||||
std::ostringstream trace = {};
|
||||
trace << "MoveTabToStack begin sourceNode=" << sourceNodeId
|
||||
<< " panel=" << panelId
|
||||
<< " targetNode=" << targetNodeId
|
||||
<< " insertion=" << targetVisibleInsertionIndex;
|
||||
AppendUIEditorRuntimeTrace("workspace", trace.str());
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
||||
if (!validation.IsValid()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Controller state invalid: " + validation.message);
|
||||
}
|
||||
|
||||
if (sourceNodeId.empty() || targetNodeId.empty()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack requires both source and target tab stack ids.");
|
||||
}
|
||||
|
||||
if (panelId.empty()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack requires a panel id.");
|
||||
}
|
||||
|
||||
if (sourceNodeId == targetNodeId) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack requires distinct source and target tab stack ids.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceNode* sourceTabStack =
|
||||
FindUIEditorWorkspaceNode(m_workspace, sourceNodeId);
|
||||
const UIEditorWorkspaceNode* targetTabStack =
|
||||
FindUIEditorWorkspaceNode(m_workspace, targetNodeId);
|
||||
if (sourceTabStack == nullptr ||
|
||||
targetTabStack == nullptr ||
|
||||
sourceTabStack->kind != UIEditorWorkspaceNodeKind::TabStack ||
|
||||
targetTabStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack source or target tab stack is missing.");
|
||||
}
|
||||
|
||||
const Internal::VisibleTabStackInfo sourceInfo =
|
||||
Internal::ResolveVisibleTabStackInfo(*sourceTabStack, m_session, panelId);
|
||||
if (!sourceInfo.panelExists) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack target panel is missing from the source tab stack.");
|
||||
}
|
||||
|
||||
if (!sourceInfo.panelVisible) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack only supports open and visible tabs.");
|
||||
}
|
||||
|
||||
const std::size_t visibleTargetCount =
|
||||
Internal::CountVisibleTabs(*targetTabStack, m_session);
|
||||
if (targetVisibleInsertionIndex > visibleTargetCount) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack target visible insertion index is out of range.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceModel previousWorkspace = m_workspace;
|
||||
if (!TryMoveUIEditorWorkspaceTabToStack(
|
||||
m_workspace,
|
||||
m_session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
targetNodeId,
|
||||
targetVisibleInsertionIndex)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack rejected.");
|
||||
}
|
||||
|
||||
if (AreUIEditorWorkspaceModelsEquivalent(previousWorkspace, m_workspace)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::NoOp,
|
||||
"Tab already matches the requested target stack insertion.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceControllerValidationResult postValidation = ValidateState();
|
||||
if (!postValidation.IsValid()) {
|
||||
m_workspace = previousWorkspace;
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"MoveTabToStack produced invalid controller state: " + postValidation.message);
|
||||
}
|
||||
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Changed,
|
||||
"Tab moved to target stack.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::DockTabRelative(
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
std::string_view targetNodeId,
|
||||
UIEditorWorkspaceDockPlacement placement,
|
||||
float splitRatio) {
|
||||
{
|
||||
std::ostringstream trace = {};
|
||||
trace << "DockTabRelative begin sourceNode=" << sourceNodeId
|
||||
<< " panel=" << panelId
|
||||
<< " targetNode=" << targetNodeId
|
||||
<< " placement=" << static_cast<int>(placement)
|
||||
<< " splitRatio=" << splitRatio;
|
||||
AppendUIEditorRuntimeTrace("workspace", trace.str());
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
|
||||
if (!validation.IsValid()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"Controller state invalid: " + validation.message);
|
||||
}
|
||||
|
||||
if (sourceNodeId.empty() || targetNodeId.empty()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative requires both source and target tab stack ids.");
|
||||
}
|
||||
|
||||
if (panelId.empty()) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative requires a panel id.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceNode* sourceTabStack =
|
||||
FindUIEditorWorkspaceNode(m_workspace, sourceNodeId);
|
||||
const UIEditorWorkspaceNode* targetTabStack =
|
||||
FindUIEditorWorkspaceNode(m_workspace, targetNodeId);
|
||||
if (sourceTabStack == nullptr ||
|
||||
targetTabStack == nullptr ||
|
||||
sourceTabStack->kind != UIEditorWorkspaceNodeKind::TabStack ||
|
||||
targetTabStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative source or target tab stack is missing.");
|
||||
}
|
||||
|
||||
const Internal::VisibleTabStackInfo sourceInfo =
|
||||
Internal::ResolveVisibleTabStackInfo(*sourceTabStack, m_session, panelId);
|
||||
if (!sourceInfo.panelExists) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative target panel is missing from the source tab stack.");
|
||||
}
|
||||
|
||||
if (!sourceInfo.panelVisible) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative only supports open and visible tabs.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceModel previousWorkspace = m_workspace;
|
||||
if (!TryDockUIEditorWorkspaceTabRelative(
|
||||
m_workspace,
|
||||
m_session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
targetNodeId,
|
||||
placement,
|
||||
splitRatio)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative rejected.");
|
||||
}
|
||||
|
||||
if (AreUIEditorWorkspaceModelsEquivalent(previousWorkspace, m_workspace)) {
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::NoOp,
|
||||
"Dock layout already matches the requested placement.");
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceControllerValidationResult postValidation = ValidateState();
|
||||
if (!postValidation.IsValid()) {
|
||||
m_workspace = previousWorkspace;
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Rejected,
|
||||
"DockTabRelative produced invalid controller state: " + postValidation.message);
|
||||
}
|
||||
|
||||
return BuildLayoutOperationResult(
|
||||
UIEditorWorkspaceLayoutOperationStatus::Changed,
|
||||
"Tab docked relative to target stack.");
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
143
editor/src/Workspace/UIEditorWorkspaceInputOwner.cpp
Normal file
143
editor/src/Workspace/UIEditorWorkspaceInputOwner.cpp
Normal file
@@ -0,0 +1,143 @@
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceInputOwner.h>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
using Widgets::HitTestUIEditorDockHost;
|
||||
using Widgets::UIEditorDockHostHitTargetKind;
|
||||
|
||||
bool ContainsPoint(const UIRect& rect, const UIPoint& point) {
|
||||
return point.x >= rect.x &&
|
||||
point.x <= rect.x + rect.width &&
|
||||
point.y >= rect.y &&
|
||||
point.y <= rect.y + rect.height;
|
||||
}
|
||||
|
||||
bool HasUsableBounds(const UIRect& rect) {
|
||||
return rect.width > 0.0f && rect.height > 0.0f;
|
||||
}
|
||||
|
||||
bool IsMountedContentPanelValid(
|
||||
const UIEditorPanelContentHostPanelState& panelState) {
|
||||
return panelState.mounted && HasUsableBounds(panelState.bounds);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string_view GetUIEditorWorkspaceInputOwnerKindName(
|
||||
UIEditorWorkspaceInputOwnerKind kind) {
|
||||
switch (kind) {
|
||||
case UIEditorWorkspaceInputOwnerKind::None:
|
||||
return "None";
|
||||
case UIEditorWorkspaceInputOwnerKind::DockHost:
|
||||
return "DockHost";
|
||||
case UIEditorWorkspaceInputOwnerKind::HostedPanel:
|
||||
return "HostedPanel";
|
||||
case UIEditorWorkspaceInputOwnerKind::Viewport:
|
||||
return "Viewport";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
bool AreUIEditorWorkspaceInputOwnersEquivalent(
|
||||
const UIEditorWorkspaceInputOwner& lhs,
|
||||
const UIEditorWorkspaceInputOwner& rhs) {
|
||||
return lhs.kind == rhs.kind && lhs.panelId == rhs.panelId;
|
||||
}
|
||||
|
||||
bool IsUIEditorWorkspaceDockHostInputOwner(
|
||||
const UIEditorWorkspaceInputOwner& owner) {
|
||||
return owner.kind == UIEditorWorkspaceInputOwnerKind::DockHost;
|
||||
}
|
||||
|
||||
bool IsUIEditorWorkspaceHostedPanelInputOwner(
|
||||
const UIEditorWorkspaceInputOwner& owner,
|
||||
std::string_view panelId) {
|
||||
return owner.kind == UIEditorWorkspaceInputOwnerKind::HostedPanel &&
|
||||
(panelId.empty() || owner.panelId == panelId);
|
||||
}
|
||||
|
||||
bool IsUIEditorWorkspaceViewportInputOwner(
|
||||
const UIEditorWorkspaceInputOwner& owner,
|
||||
std::string_view panelId) {
|
||||
return owner.kind == UIEditorWorkspaceInputOwnerKind::Viewport &&
|
||||
(panelId.empty() || owner.panelId == panelId);
|
||||
}
|
||||
|
||||
UIEditorWorkspaceInputOwner ResolveUIEditorWorkspacePointerInputOwner(
|
||||
const Widgets::UIEditorDockHostLayout& dockHostLayout,
|
||||
const UIEditorPanelContentHostFrame& contentHostFrame,
|
||||
const UIPoint& pointerPosition) {
|
||||
for (const UIEditorPanelContentHostPanelState& panelState :
|
||||
contentHostFrame.panelStates) {
|
||||
if (!IsMountedContentPanelValid(panelState) ||
|
||||
!ContainsPoint(panelState.bounds, pointerPosition)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceInputOwner owner = {};
|
||||
owner.panelId = panelState.panelId;
|
||||
owner.kind = panelState.kind == UIEditorPanelPresentationKind::ViewportShell
|
||||
? UIEditorWorkspaceInputOwnerKind::Viewport
|
||||
: UIEditorWorkspaceInputOwnerKind::HostedPanel;
|
||||
return owner;
|
||||
}
|
||||
|
||||
const Widgets::UIEditorDockHostHitTarget hitTarget =
|
||||
HitTestUIEditorDockHost(dockHostLayout, pointerPosition);
|
||||
switch (hitTarget.kind) {
|
||||
case UIEditorDockHostHitTargetKind::SplitterHandle:
|
||||
case UIEditorDockHostHitTargetKind::TabStripBackground:
|
||||
case UIEditorDockHostHitTargetKind::Tab:
|
||||
case UIEditorDockHostHitTargetKind::PanelHeader:
|
||||
case UIEditorDockHostHitTargetKind::PanelBody:
|
||||
case UIEditorDockHostHitTargetKind::PanelFooter:
|
||||
return { UIEditorWorkspaceInputOwnerKind::DockHost, {} };
|
||||
|
||||
case UIEditorDockHostHitTargetKind::None:
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
UIEditorWorkspaceInputOwner NormalizeUIEditorWorkspaceInputOwner(
|
||||
UIEditorWorkspaceInputOwner owner,
|
||||
const Widgets::UIEditorDockHostLayout& dockHostLayout,
|
||||
const UIEditorPanelContentHostFrame& contentHostFrame) {
|
||||
switch (owner.kind) {
|
||||
case UIEditorWorkspaceInputOwnerKind::None:
|
||||
return {};
|
||||
|
||||
case UIEditorWorkspaceInputOwnerKind::DockHost:
|
||||
return HasUsableBounds(dockHostLayout.bounds)
|
||||
? owner
|
||||
: UIEditorWorkspaceInputOwner {};
|
||||
|
||||
case UIEditorWorkspaceInputOwnerKind::HostedPanel:
|
||||
case UIEditorWorkspaceInputOwnerKind::Viewport:
|
||||
for (const UIEditorPanelContentHostPanelState& panelState :
|
||||
contentHostFrame.panelStates) {
|
||||
if (!IsMountedContentPanelValid(panelState) ||
|
||||
panelState.panelId != owner.panelId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceInputOwnerKind expectedKind =
|
||||
panelState.kind == UIEditorPanelPresentationKind::ViewportShell
|
||||
? UIEditorWorkspaceInputOwnerKind::Viewport
|
||||
: UIEditorWorkspaceInputOwnerKind::HostedPanel;
|
||||
return owner.kind == expectedKind
|
||||
? owner
|
||||
: UIEditorWorkspaceInputOwner {};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
187
editor/src/Workspace/UIEditorWorkspaceInteraction.cpp
Normal file
187
editor/src/Workspace/UIEditorWorkspaceInteraction.cpp
Normal file
@@ -0,0 +1,187 @@
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceInteraction.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
bool HasMeaningfulViewportInputFrame(const UIEditorViewportInputBridgeFrame& frame) {
|
||||
return frame.pointerMoved ||
|
||||
frame.pointerPressedInside ||
|
||||
frame.pointerReleasedInside ||
|
||||
frame.focusGained ||
|
||||
frame.focusLost ||
|
||||
frame.captureStarted ||
|
||||
frame.captureEnded ||
|
||||
frame.wheelDelta != 0.0f ||
|
||||
!frame.pressedKeyCodes.empty() ||
|
||||
!frame.releasedKeyCodes.empty() ||
|
||||
!frame.characters.empty();
|
||||
}
|
||||
|
||||
UIEditorWorkspaceInputOwner ResolveNextInputOwner(
|
||||
UIEditorWorkspaceInputOwner currentOwner,
|
||||
const Widgets::UIEditorDockHostLayout& dockHostLayout,
|
||||
const UIEditorPanelContentHostFrame& contentHostFrame,
|
||||
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents) {
|
||||
using ::XCEngine::UI::UIInputEventType;
|
||||
|
||||
currentOwner = NormalizeUIEditorWorkspaceInputOwner(
|
||||
std::move(currentOwner),
|
||||
dockHostLayout,
|
||||
contentHostFrame);
|
||||
for (const ::XCEngine::UI::UIInputEvent& event : inputEvents) {
|
||||
switch (event.type) {
|
||||
case UIInputEventType::FocusLost:
|
||||
currentOwner = {};
|
||||
break;
|
||||
|
||||
case UIInputEventType::PointerButtonDown:
|
||||
currentOwner = ResolveUIEditorWorkspacePointerInputOwner(
|
||||
dockHostLayout,
|
||||
contentHostFrame,
|
||||
event.position);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return NormalizeUIEditorWorkspaceInputOwner(
|
||||
std::move(currentOwner),
|
||||
dockHostLayout,
|
||||
contentHostFrame);
|
||||
}
|
||||
|
||||
std::string ResolveFocusedPanelId(const UIEditorWorkspaceInputOwner& owner) {
|
||||
switch (owner.kind) {
|
||||
case UIEditorWorkspaceInputOwnerKind::HostedPanel:
|
||||
case UIEditorWorkspaceInputOwnerKind::Viewport:
|
||||
return owner.panelId;
|
||||
|
||||
case UIEditorWorkspaceInputOwnerKind::DockHost:
|
||||
case UIEditorWorkspaceInputOwnerKind::None:
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UIEditorWorkspaceInteractionFrame UpdateUIEditorWorkspaceInteraction(
|
||||
UIEditorWorkspaceInteractionState& state,
|
||||
UIEditorWorkspaceController& controller,
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const UIEditorWorkspaceInteractionModel& model,
|
||||
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
|
||||
const Widgets::UIEditorDockHostMetrics& dockHostMetrics,
|
||||
const Widgets::UIEditorViewportSlotMetrics& viewportMetrics,
|
||||
const UIEditorTextMeasurer* textMeasurer) {
|
||||
UIEditorWorkspaceInteractionFrame frame = {};
|
||||
frame.previousInputOwner = state.inputOwner;
|
||||
frame.dockHostFrame = UpdateUIEditorDockHostInteraction(
|
||||
state.dockHostInteractionState,
|
||||
controller,
|
||||
bounds,
|
||||
inputEvents,
|
||||
dockHostMetrics,
|
||||
textMeasurer);
|
||||
|
||||
const UIEditorWorkspaceComposeRequest previewRequest =
|
||||
ResolveUIEditorWorkspaceComposeRequest(
|
||||
frame.dockHostFrame.layout,
|
||||
controller.GetPanelRegistry(),
|
||||
model.workspacePresentations,
|
||||
viewportMetrics,
|
||||
textMeasurer);
|
||||
const UIEditorPanelContentHostFrame previewContentHostFrame =
|
||||
BuildUIEditorPanelContentHostFrame(
|
||||
previewRequest.contentHostRequest,
|
||||
controller.GetPanelRegistry());
|
||||
state.inputOwner = ResolveNextInputOwner(
|
||||
state.inputOwner,
|
||||
previewRequest.dockHostLayout,
|
||||
previewContentHostFrame,
|
||||
inputEvents);
|
||||
frame.inputOwner = state.inputOwner;
|
||||
frame.inputOwnerChanged = !AreUIEditorWorkspaceInputOwnersEquivalent(
|
||||
frame.previousInputOwner,
|
||||
frame.inputOwner);
|
||||
|
||||
const bool dockFocused =
|
||||
IsUIEditorWorkspaceDockHostInputOwner(frame.inputOwner);
|
||||
const bool dockFocusChanged =
|
||||
state.dockHostInteractionState.dockHostState.focused != dockFocused;
|
||||
state.dockHostInteractionState.dockHostState.focused = dockFocused;
|
||||
|
||||
UIEditorWorkspaceComposeRequest composeRequest = previewRequest;
|
||||
// Dock focus only changes visual state; hosted content mounts and viewport
|
||||
// bounds remain stable for this frame.
|
||||
if (dockFocusChanged) {
|
||||
composeRequest.dockHostLayout = Widgets::BuildUIEditorDockHostLayout(
|
||||
bounds,
|
||||
controller.GetPanelRegistry(),
|
||||
controller.GetWorkspace(),
|
||||
controller.GetSession(),
|
||||
state.dockHostInteractionState.dockHostState,
|
||||
dockHostMetrics,
|
||||
textMeasurer);
|
||||
}
|
||||
|
||||
frame.dockHostFrame.layout = composeRequest.dockHostLayout;
|
||||
frame.dockHostFrame.focused = dockFocused;
|
||||
frame.composeFrame = UpdateUIEditorWorkspaceCompose(
|
||||
state.composeState,
|
||||
composeRequest,
|
||||
controller.GetPanelRegistry(),
|
||||
model.workspacePresentations,
|
||||
inputEvents,
|
||||
&frame.inputOwner);
|
||||
frame.panelHostLifecycleFrame = UpdateUIEditorPanelHostLifecycle(
|
||||
state.panelHostLifecycleState,
|
||||
controller.GetPanelRegistry(),
|
||||
controller.GetWorkspace(),
|
||||
controller.GetSession(),
|
||||
UIEditorPanelHostLifecycleRequest{
|
||||
.focusedPanelId = ResolveFocusedPanelId(frame.inputOwner),
|
||||
});
|
||||
|
||||
frame.result.dockHostResult = frame.dockHostFrame.result;
|
||||
frame.result.consumed = frame.dockHostFrame.result.consumed;
|
||||
frame.result.requestPointerCapture = frame.dockHostFrame.result.requestPointerCapture;
|
||||
frame.result.releasePointerCapture = frame.dockHostFrame.result.releasePointerCapture;
|
||||
|
||||
for (const UIEditorWorkspaceViewportComposeFrame& viewportFrame : frame.composeFrame.viewportFrames) {
|
||||
const UIEditorViewportInputBridgeFrame& inputFrame =
|
||||
viewportFrame.viewportShellFrame.inputFrame;
|
||||
const bool meaningfulInput = HasMeaningfulViewportInputFrame(inputFrame);
|
||||
|
||||
if (!meaningfulInput &&
|
||||
!inputFrame.captureStarted &&
|
||||
!inputFrame.captureEnded) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (frame.result.viewportPanelId.empty()) {
|
||||
frame.result.viewportPanelId = viewportFrame.panelId;
|
||||
frame.result.viewportInputFrame = inputFrame;
|
||||
}
|
||||
frame.result.viewportInteractionChanged =
|
||||
frame.result.viewportInteractionChanged || meaningfulInput;
|
||||
frame.result.requestPointerCapture =
|
||||
frame.result.requestPointerCapture || inputFrame.captureStarted;
|
||||
frame.result.releasePointerCapture =
|
||||
frame.result.releasePointerCapture || inputFrame.captureEnded;
|
||||
}
|
||||
|
||||
frame.result.consumed =
|
||||
frame.result.consumed ||
|
||||
frame.result.viewportInteractionChanged ||
|
||||
frame.result.requestPointerCapture ||
|
||||
frame.result.releasePointerCapture;
|
||||
return frame;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
500
editor/src/Workspace/UIEditorWorkspaceLayoutPersistence.cpp
Normal file
500
editor/src/Workspace/UIEditorWorkspaceLayoutPersistence.cpp
Normal file
@@ -0,0 +1,500 @@
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceLayoutPersistence.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceValidation.h>
|
||||
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::string_view kLayoutHeader = "XCUI_EDITOR_WORKSPACE_LAYOUT";
|
||||
constexpr int kLayoutVersion = 1;
|
||||
|
||||
struct LayoutLine {
|
||||
std::size_t number = 0u;
|
||||
std::string text = {};
|
||||
};
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode code,
|
||||
std::string message) {
|
||||
UIEditorWorkspaceLayoutLoadResult result = {};
|
||||
result.code = code;
|
||||
result.message = std::move(message);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool HasTrailingTokens(std::istringstream& stream) {
|
||||
stream >> std::ws;
|
||||
return !stream.eof();
|
||||
}
|
||||
|
||||
std::string MakeLinePrefix(const LayoutLine& line) {
|
||||
return "Line " + std::to_string(line.number) + ": ";
|
||||
}
|
||||
|
||||
bool ParseBinaryFlag(
|
||||
std::istringstream& stream,
|
||||
const LayoutLine& line,
|
||||
int& outValue,
|
||||
UIEditorWorkspaceLayoutLoadResult& outError,
|
||||
UIEditorWorkspaceLayoutLoadCode code,
|
||||
std::string_view fieldName) {
|
||||
if (!(stream >> outValue) || (outValue != 0 && outValue != 1)) {
|
||||
outError = MakeLoadError(
|
||||
code,
|
||||
MakeLinePrefix(line) + std::string(fieldName) + " must be encoded as 0 or 1.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseNodeLine(
|
||||
const LayoutLine& line,
|
||||
std::string& outTag,
|
||||
std::istringstream& outStream) {
|
||||
outStream = std::istringstream(line.text);
|
||||
return static_cast<bool>(outStream >> outTag);
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParseNodeRecursive(
|
||||
const std::vector<LayoutLine>& lines,
|
||||
std::size_t& index,
|
||||
UIEditorWorkspaceNode& outNode);
|
||||
|
||||
std::string SerializeAxis(UIEditorWorkspaceSplitAxis axis) {
|
||||
return axis == UIEditorWorkspaceSplitAxis::Vertical ? "vertical" : "horizontal";
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParseAxis(
|
||||
std::string_view text,
|
||||
UIEditorWorkspaceSplitAxis& outAxis,
|
||||
const LayoutLine& line) {
|
||||
if (text == "horizontal") {
|
||||
outAxis = UIEditorWorkspaceSplitAxis::Horizontal;
|
||||
return {};
|
||||
}
|
||||
|
||||
if (text == "vertical") {
|
||||
outAxis = UIEditorWorkspaceSplitAxis::Vertical;
|
||||
return {};
|
||||
}
|
||||
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidNodeRecord,
|
||||
MakeLinePrefix(line) + "split axis must be \"horizontal\" or \"vertical\".");
|
||||
}
|
||||
|
||||
void SerializeNodeRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
std::ostringstream& stream) {
|
||||
switch (node.kind) {
|
||||
case UIEditorWorkspaceNodeKind::Panel:
|
||||
stream << "node_panel "
|
||||
<< std::quoted(node.nodeId) << ' '
|
||||
<< std::quoted(node.panel.panelId) << ' '
|
||||
<< std::quoted(node.panel.title) << ' '
|
||||
<< (node.panel.placeholder ? 1 : 0) << '\n';
|
||||
return;
|
||||
|
||||
case UIEditorWorkspaceNodeKind::TabStack:
|
||||
stream << "node_tabstack "
|
||||
<< std::quoted(node.nodeId) << ' '
|
||||
<< node.selectedTabIndex << ' '
|
||||
<< node.children.size() << '\n';
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
SerializeNodeRecursive(child, stream);
|
||||
}
|
||||
return;
|
||||
|
||||
case UIEditorWorkspaceNodeKind::Split:
|
||||
stream << "node_split "
|
||||
<< std::quoted(node.nodeId) << ' '
|
||||
<< std::quoted(SerializeAxis(node.splitAxis)) << ' '
|
||||
<< std::setprecision(std::numeric_limits<float>::max_digits10)
|
||||
<< node.splitRatio << '\n';
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
SerializeNodeRecursive(child, stream);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<LayoutLine> CollectNonEmptyLines(std::string_view serializedLayout) {
|
||||
std::vector<LayoutLine> lines = {};
|
||||
std::istringstream stream{ std::string(serializedLayout) };
|
||||
std::string text = {};
|
||||
std::size_t lineNumber = 0u;
|
||||
while (std::getline(stream, text)) {
|
||||
++lineNumber;
|
||||
if (!text.empty() && text.back() == '\r') {
|
||||
text.pop_back();
|
||||
}
|
||||
|
||||
const std::size_t first = text.find_first_not_of(" \t");
|
||||
if (first == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
lines.push_back({ lineNumber, text });
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParseHeader(const LayoutLine& line) {
|
||||
std::istringstream stream(line.text);
|
||||
std::string header = {};
|
||||
int version = 0;
|
||||
if (!(stream >> header >> version) || HasTrailingTokens(stream)) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidHeader,
|
||||
MakeLinePrefix(line) + "invalid layout header.");
|
||||
}
|
||||
|
||||
if (header != kLayoutHeader) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidHeader,
|
||||
MakeLinePrefix(line) + "layout header magic is invalid.");
|
||||
}
|
||||
|
||||
if (version != kLayoutVersion) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::UnsupportedVersion,
|
||||
MakeLinePrefix(line) + "unsupported layout version " + std::to_string(version) + ".");
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParseActiveRecord(
|
||||
const LayoutLine& line,
|
||||
std::string& outActivePanelId) {
|
||||
std::istringstream stream(line.text);
|
||||
std::string tag = {};
|
||||
if (!(stream >> tag) || tag != "active" ||
|
||||
!(stream >> std::quoted(outActivePanelId)) ||
|
||||
HasTrailingTokens(stream)) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::MissingActiveRecord,
|
||||
MakeLinePrefix(line) + "active record must be `active \"panel-id\"`.");
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParsePanelNode(
|
||||
std::istringstream& stream,
|
||||
const LayoutLine& line,
|
||||
UIEditorWorkspaceNode& outNode) {
|
||||
std::string nodeId = {};
|
||||
std::string panelId = {};
|
||||
std::string title = {};
|
||||
int placeholderValue = 0;
|
||||
if (!(stream >> std::quoted(nodeId) >> std::quoted(panelId) >> std::quoted(title))) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidNodeRecord,
|
||||
MakeLinePrefix(line) + "panel node record is malformed.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult boolParse = {};
|
||||
if (!ParseBinaryFlag(
|
||||
stream,
|
||||
line,
|
||||
placeholderValue,
|
||||
boolParse,
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidNodeRecord,
|
||||
"placeholder")) {
|
||||
return boolParse;
|
||||
}
|
||||
|
||||
if (HasTrailingTokens(stream)) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidNodeRecord,
|
||||
MakeLinePrefix(line) + "panel node record contains trailing tokens.");
|
||||
}
|
||||
|
||||
outNode = BuildUIEditorWorkspacePanel(
|
||||
std::move(nodeId),
|
||||
std::move(panelId),
|
||||
std::move(title),
|
||||
placeholderValue != 0);
|
||||
return {};
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParseTabStackNode(
|
||||
const std::vector<LayoutLine>& lines,
|
||||
std::size_t& index,
|
||||
std::istringstream& stream,
|
||||
const LayoutLine& line,
|
||||
UIEditorWorkspaceNode& outNode) {
|
||||
std::string nodeId = {};
|
||||
std::size_t selectedTabIndex = 0u;
|
||||
std::size_t childCount = 0u;
|
||||
if (!(stream >> std::quoted(nodeId) >> selectedTabIndex >> childCount) ||
|
||||
HasTrailingTokens(stream)) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidNodeRecord,
|
||||
MakeLinePrefix(line) + "tab stack node record is malformed.");
|
||||
}
|
||||
|
||||
outNode = {};
|
||||
outNode.kind = UIEditorWorkspaceNodeKind::TabStack;
|
||||
outNode.nodeId = std::move(nodeId);
|
||||
outNode.selectedTabIndex = selectedTabIndex;
|
||||
outNode.children.reserve(childCount);
|
||||
for (std::size_t childIndex = 0; childIndex < childCount; ++childIndex) {
|
||||
UIEditorWorkspaceNode child = {};
|
||||
UIEditorWorkspaceLayoutLoadResult childResult = ParseNodeRecursive(lines, index, child);
|
||||
if (!childResult.IsValid()) {
|
||||
return childResult;
|
||||
}
|
||||
outNode.children.push_back(std::move(child));
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParseSplitNode(
|
||||
const std::vector<LayoutLine>& lines,
|
||||
std::size_t& index,
|
||||
std::istringstream& stream,
|
||||
const LayoutLine& line,
|
||||
UIEditorWorkspaceNode& outNode) {
|
||||
std::string nodeId = {};
|
||||
std::string axisText = {};
|
||||
float splitRatio = 0.0f;
|
||||
if (!(stream >> std::quoted(nodeId) >> std::quoted(axisText) >> splitRatio) ||
|
||||
HasTrailingTokens(stream)) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidNodeRecord,
|
||||
MakeLinePrefix(line) + "split node record is malformed.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceSplitAxis axis = UIEditorWorkspaceSplitAxis::Horizontal;
|
||||
if (UIEditorWorkspaceLayoutLoadResult axisResult = ParseAxis(axisText, axis, line);
|
||||
!axisResult.IsValid()) {
|
||||
return axisResult;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceNode primary = {};
|
||||
UIEditorWorkspaceNode secondary = {};
|
||||
if (UIEditorWorkspaceLayoutLoadResult primaryResult = ParseNodeRecursive(lines, index, primary);
|
||||
!primaryResult.IsValid()) {
|
||||
return primaryResult;
|
||||
}
|
||||
|
||||
if (UIEditorWorkspaceLayoutLoadResult secondaryResult = ParseNodeRecursive(lines, index, secondary);
|
||||
!secondaryResult.IsValid()) {
|
||||
return secondaryResult;
|
||||
}
|
||||
|
||||
outNode = BuildUIEditorWorkspaceSplit(
|
||||
std::move(nodeId),
|
||||
axis,
|
||||
splitRatio,
|
||||
std::move(primary),
|
||||
std::move(secondary));
|
||||
return {};
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParseNodeRecursive(
|
||||
const std::vector<LayoutLine>& lines,
|
||||
std::size_t& index,
|
||||
UIEditorWorkspaceNode& outNode) {
|
||||
if (index >= lines.size()) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::UnexpectedEndOfInput,
|
||||
"Unexpected end of input while parsing workspace nodes.");
|
||||
}
|
||||
|
||||
const LayoutLine& line = lines[index++];
|
||||
std::istringstream stream = {};
|
||||
std::string tag = {};
|
||||
if (!ParseNodeLine(line, tag, stream)) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidNodeRecord,
|
||||
MakeLinePrefix(line) + "workspace node record is empty.");
|
||||
}
|
||||
|
||||
if (tag == "node_panel") {
|
||||
return ParsePanelNode(stream, line, outNode);
|
||||
}
|
||||
|
||||
if (tag == "node_tabstack") {
|
||||
return ParseTabStackNode(lines, index, stream, line, outNode);
|
||||
}
|
||||
|
||||
if (tag == "node_split") {
|
||||
return ParseSplitNode(lines, index, stream, line, outNode);
|
||||
}
|
||||
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidNodeRecord,
|
||||
MakeLinePrefix(line) + "unknown workspace node tag '" + tag + "'.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult ParseSessionRecord(
|
||||
const LayoutLine& line,
|
||||
UIEditorPanelSessionState& outState) {
|
||||
std::istringstream stream(line.text);
|
||||
std::string tag = {};
|
||||
int openValue = 0;
|
||||
int visibleValue = 0;
|
||||
if (!(stream >> tag) || tag != "session" ||
|
||||
!(stream >> std::quoted(outState.panelId))) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidSessionRecord,
|
||||
MakeLinePrefix(line) + "session record must start with `session`.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult boolParse = {};
|
||||
if (!ParseBinaryFlag(
|
||||
stream,
|
||||
line,
|
||||
openValue,
|
||||
boolParse,
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidSessionRecord,
|
||||
"open")) {
|
||||
return boolParse;
|
||||
}
|
||||
|
||||
if (!ParseBinaryFlag(
|
||||
stream,
|
||||
line,
|
||||
visibleValue,
|
||||
boolParse,
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidSessionRecord,
|
||||
"visible")) {
|
||||
return boolParse;
|
||||
}
|
||||
|
||||
if (HasTrailingTokens(stream)) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidSessionRecord,
|
||||
MakeLinePrefix(line) + "session record contains trailing tokens.");
|
||||
}
|
||||
|
||||
outState.open = openValue != 0;
|
||||
outState.visible = visibleValue != 0;
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UIEditorWorkspaceLayoutSnapshot BuildUIEditorWorkspaceLayoutSnapshot(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session) {
|
||||
UIEditorWorkspaceLayoutSnapshot snapshot = {};
|
||||
snapshot.workspace = CanonicalizeUIEditorWorkspaceModel(workspace);
|
||||
snapshot.session = session;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
bool AreUIEditorWorkspaceLayoutSnapshotsEquivalent(
|
||||
const UIEditorWorkspaceLayoutSnapshot& lhs,
|
||||
const UIEditorWorkspaceLayoutSnapshot& rhs) {
|
||||
return AreUIEditorWorkspaceModelsEquivalent(lhs.workspace, rhs.workspace) &&
|
||||
AreUIEditorWorkspaceSessionsEquivalent(lhs.session, rhs.session);
|
||||
}
|
||||
|
||||
std::string SerializeUIEditorWorkspaceLayoutSnapshot(
|
||||
const UIEditorWorkspaceLayoutSnapshot& snapshot) {
|
||||
std::ostringstream stream = {};
|
||||
stream << kLayoutHeader << ' ' << kLayoutVersion << '\n';
|
||||
stream << "active " << std::quoted(snapshot.workspace.activePanelId) << '\n';
|
||||
SerializeNodeRecursive(snapshot.workspace.root, stream);
|
||||
for (const UIEditorPanelSessionState& state : snapshot.session.panelStates) {
|
||||
stream << "session "
|
||||
<< std::quoted(state.panelId) << ' '
|
||||
<< (state.open ? 1 : 0) << ' '
|
||||
<< (state.visible ? 1 : 0) << '\n';
|
||||
}
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult DeserializeUIEditorWorkspaceLayoutSnapshot(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
std::string_view serializedLayout) {
|
||||
const UIEditorPanelRegistryValidationResult registryValidation =
|
||||
ValidateUIEditorPanelRegistry(panelRegistry);
|
||||
if (!registryValidation.IsValid()) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidPanelRegistry,
|
||||
"Panel registry invalid: " + registryValidation.message);
|
||||
}
|
||||
|
||||
const std::vector<LayoutLine> lines = CollectNonEmptyLines(serializedLayout);
|
||||
if (lines.empty()) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::EmptyInput,
|
||||
"Serialized layout input is empty.");
|
||||
}
|
||||
|
||||
if (UIEditorWorkspaceLayoutLoadResult headerResult = ParseHeader(lines.front());
|
||||
!headerResult.IsValid()) {
|
||||
return headerResult;
|
||||
}
|
||||
|
||||
if (lines.size() < 2u) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::MissingActiveRecord,
|
||||
"Serialized layout is missing the active panel record.");
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutSnapshot snapshot = {};
|
||||
if (UIEditorWorkspaceLayoutLoadResult activeResult =
|
||||
ParseActiveRecord(lines[1], snapshot.workspace.activePanelId);
|
||||
!activeResult.IsValid()) {
|
||||
return activeResult;
|
||||
}
|
||||
|
||||
std::size_t index = 2u;
|
||||
if (UIEditorWorkspaceLayoutLoadResult rootResult =
|
||||
ParseNodeRecursive(lines, index, snapshot.workspace.root);
|
||||
!rootResult.IsValid()) {
|
||||
return rootResult;
|
||||
}
|
||||
|
||||
snapshot.workspace = CanonicalizeUIEditorWorkspaceModel(std::move(snapshot.workspace));
|
||||
|
||||
snapshot.session.panelStates.clear();
|
||||
while (index < lines.size()) {
|
||||
UIEditorPanelSessionState state = {};
|
||||
if (UIEditorWorkspaceLayoutLoadResult stateResult =
|
||||
ParseSessionRecord(lines[index], state);
|
||||
!stateResult.IsValid()) {
|
||||
return stateResult;
|
||||
}
|
||||
snapshot.session.panelStates.push_back(std::move(state));
|
||||
++index;
|
||||
}
|
||||
|
||||
if (UIEditorWorkspaceValidationResult workspaceValidation =
|
||||
ValidateUIEditorWorkspace(snapshot.workspace);
|
||||
!workspaceValidation.IsValid()) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidWorkspace,
|
||||
workspaceValidation.message);
|
||||
}
|
||||
|
||||
if (UIEditorWorkspaceSessionValidationResult sessionValidation =
|
||||
ValidateUIEditorWorkspaceSession(panelRegistry, snapshot.workspace, snapshot.session);
|
||||
!sessionValidation.IsValid()) {
|
||||
return MakeLoadError(
|
||||
UIEditorWorkspaceLayoutLoadCode::InvalidWorkspaceSession,
|
||||
sessionValidation.message);
|
||||
}
|
||||
|
||||
UIEditorWorkspaceLayoutLoadResult result = {};
|
||||
result.snapshot = std::move(snapshot);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
1015
editor/src/Workspace/UIEditorWorkspaceModel.cpp
Normal file
1015
editor/src/Workspace/UIEditorWorkspaceModel.cpp
Normal file
File diff suppressed because it is too large
Load Diff
447
editor/src/Workspace/UIEditorWorkspaceSession.cpp
Normal file
447
editor/src/Workspace/UIEditorWorkspaceSession.cpp
Normal file
@@ -0,0 +1,447 @@
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceSession.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceMutation.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceQueries.h>
|
||||
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
UIEditorWorkspaceSessionValidationResult MakeValidationError(
|
||||
UIEditorWorkspaceSessionValidationCode code,
|
||||
std::string message) {
|
||||
UIEditorWorkspaceSessionValidationResult result = {};
|
||||
result.code = code;
|
||||
result.message = std::move(message);
|
||||
return result;
|
||||
}
|
||||
|
||||
UIEditorPanelSessionState* FindMutablePanelSessionState(
|
||||
UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
for (UIEditorPanelSessionState& state : session.panelStates) {
|
||||
if (state.panelId == panelId) {
|
||||
return &state;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const UIEditorPanelDescriptor* FindPanelDescriptor(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
std::string_view panelId) {
|
||||
return FindUIEditorPanelDescriptor(panelRegistry, panelId);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void CollectWorkspacePanelIdsRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
std::vector<std::string>& outPanelIds) {
|
||||
if (node.kind == UIEditorWorkspaceNodeKind::Panel) {
|
||||
outPanelIds.push_back(node.panel.panelId);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
CollectWorkspacePanelIdsRecursive(child, outPanelIds);
|
||||
}
|
||||
}
|
||||
|
||||
bool IsPanelOpenAndVisible(
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
const UIEditorPanelSessionState* state = FindUIEditorPanelSessionState(session, panelId);
|
||||
return state != nullptr && state->open && state->visible;
|
||||
}
|
||||
|
||||
bool IsPanelSelectable(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
return !panelId.empty() &&
|
||||
IsPanelOpenAndVisible(session, panelId) &&
|
||||
ContainsUIEditorWorkspacePanel(workspace, panelId);
|
||||
}
|
||||
|
||||
std::size_t ResolveVisibleTabIndex(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorWorkspaceSession& session) {
|
||||
if (node.kind != UIEditorWorkspaceNodeKind::TabStack || node.children.empty()) {
|
||||
return node.selectedTabIndex;
|
||||
}
|
||||
|
||||
if (node.selectedTabIndex < node.children.size()) {
|
||||
const UIEditorWorkspaceNode& selectedChild = node.children[node.selectedTabIndex];
|
||||
if (selectedChild.kind == UIEditorWorkspaceNodeKind::Panel &&
|
||||
IsPanelOpenAndVisible(session, selectedChild.panel.panelId)) {
|
||||
return node.selectedTabIndex;
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < node.children.size(); ++index) {
|
||||
const UIEditorWorkspaceNode& child = node.children[index];
|
||||
if (child.kind == UIEditorWorkspaceNodeKind::Panel &&
|
||||
IsPanelOpenAndVisible(session, child.panel.panelId)) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return node.children.size();
|
||||
}
|
||||
|
||||
void CollectVisiblePanelsRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view activePanelId,
|
||||
std::vector<UIEditorWorkspaceVisiblePanel>& outPanels) {
|
||||
switch (node.kind) {
|
||||
case UIEditorWorkspaceNodeKind::Panel: {
|
||||
if (!IsPanelOpenAndVisible(session, node.panel.panelId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
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: {
|
||||
const std::size_t resolvedIndex = ResolveVisibleTabIndex(node, session);
|
||||
if (resolvedIndex < node.children.size()) {
|
||||
CollectVisiblePanelsRecursive(
|
||||
node.children[resolvedIndex],
|
||||
session,
|
||||
activePanelId,
|
||||
outPanels);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
case UIEditorWorkspaceNodeKind::Split:
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
CollectVisiblePanelsRecursive(child, session, activePanelId, outPanels);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void NormalizeSessionStatesAgainstRegistry(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
UIEditorWorkspaceSession& session) {
|
||||
for (UIEditorPanelSessionState& state : session.panelStates) {
|
||||
if (!state.open) {
|
||||
state.visible = false;
|
||||
}
|
||||
|
||||
const UIEditorPanelDescriptor* descriptor = FindPanelDescriptor(panelRegistry, state.panelId);
|
||||
if (descriptor == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!descriptor->canClose) {
|
||||
state.open = true;
|
||||
state.visible = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!descriptor->canHide && state.open) {
|
||||
state.visible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NormalizeWorkspaceSession(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
std::string_view preferredActivePanelId) {
|
||||
NormalizeSessionStatesAgainstRegistry(panelRegistry, session);
|
||||
|
||||
std::string targetActivePanelId = {};
|
||||
if (IsPanelSelectable(workspace, session, preferredActivePanelId)) {
|
||||
targetActivePanelId = std::string(preferredActivePanelId);
|
||||
} else if (IsPanelSelectable(workspace, session, workspace.activePanelId)) {
|
||||
targetActivePanelId = workspace.activePanelId;
|
||||
} else {
|
||||
const std::vector<UIEditorWorkspaceVisiblePanel> visiblePanels =
|
||||
CollectUIEditorWorkspaceVisiblePanels(workspace, session);
|
||||
if (!visiblePanels.empty()) {
|
||||
targetActivePanelId = visiblePanels.front().panelId;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetActivePanelId.empty()) {
|
||||
workspace.activePanelId.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
TryActivateUIEditorWorkspacePanel(workspace, targetActivePanelId);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UIEditorWorkspaceSession BuildDefaultUIEditorWorkspaceSession(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace) {
|
||||
UIEditorWorkspaceSession session = {};
|
||||
std::vector<std::string> panelIds = {};
|
||||
CollectWorkspacePanelIdsRecursive(workspace.root, panelIds);
|
||||
session.panelStates.reserve(panelIds.size());
|
||||
for (std::string& panelId : panelIds) {
|
||||
UIEditorPanelSessionState state = {};
|
||||
state.panelId = std::move(panelId);
|
||||
session.panelStates.push_back(std::move(state));
|
||||
}
|
||||
|
||||
NormalizeSessionStatesAgainstRegistry(panelRegistry, session);
|
||||
return session;
|
||||
}
|
||||
|
||||
const UIEditorPanelSessionState* FindUIEditorPanelSessionState(
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
for (const UIEditorPanelSessionState& state : session.panelStates) {
|
||||
if (state.panelId == panelId) {
|
||||
return &state;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceSessionValidationResult ValidateUIEditorWorkspaceSession(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session) {
|
||||
std::vector<std::string> workspacePanelIds = {};
|
||||
CollectWorkspacePanelIdsRecursive(workspace.root, workspacePanelIds);
|
||||
|
||||
std::unordered_set<std::string> expectedPanelIds = {};
|
||||
expectedPanelIds.insert(workspacePanelIds.begin(), workspacePanelIds.end());
|
||||
|
||||
std::unordered_set<std::string> seenPanelIds = {};
|
||||
for (const UIEditorPanelSessionState& state : session.panelStates) {
|
||||
if (!seenPanelIds.insert(state.panelId).second) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceSessionValidationCode::DuplicatePanelId,
|
||||
"Workspace session contains duplicated panel state '" + state.panelId + "'.");
|
||||
}
|
||||
|
||||
if (!expectedPanelIds.contains(state.panelId)) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceSessionValidationCode::UnknownPanelId,
|
||||
"Workspace session state '" + state.panelId + "' is not present in the workspace tree.");
|
||||
}
|
||||
|
||||
if (!state.open && state.visible) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceSessionValidationCode::ClosedPanelVisible,
|
||||
"Workspace session state '" + state.panelId + "' cannot be visible while closed.");
|
||||
}
|
||||
|
||||
const UIEditorPanelDescriptor* descriptor = FindPanelDescriptor(panelRegistry, state.panelId);
|
||||
if (descriptor == nullptr) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceSessionValidationCode::UnknownPanelId,
|
||||
"Workspace session state '" + state.panelId + "' is missing from the panel registry.");
|
||||
}
|
||||
|
||||
if (!descriptor->canClose && !state.open) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceSessionValidationCode::NonCloseablePanelClosed,
|
||||
"Workspace session state '" + state.panelId + "' cannot be closed.");
|
||||
}
|
||||
|
||||
if (!descriptor->canHide && state.open && !state.visible) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceSessionValidationCode::NonHideablePanelHidden,
|
||||
"Workspace session state '" + state.panelId + "' cannot be hidden.");
|
||||
}
|
||||
}
|
||||
|
||||
for (const std::string& panelId : workspacePanelIds) {
|
||||
if (!seenPanelIds.contains(panelId)) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceSessionValidationCode::MissingPanelState,
|
||||
"Workspace panel '" + panelId + "' is missing from the workspace session.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!workspace.activePanelId.empty() &&
|
||||
FindUIEditorWorkspaceActivePanel(workspace, session) == nullptr) {
|
||||
return MakeValidationError(
|
||||
UIEditorWorkspaceSessionValidationCode::InvalidActivePanelId,
|
||||
"Active panel id '" + workspace.activePanelId + "' is missing, closed, or hidden.");
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
bool AreUIEditorWorkspaceSessionsEquivalent(
|
||||
const UIEditorWorkspaceSession& lhs,
|
||||
const UIEditorWorkspaceSession& rhs) {
|
||||
if (lhs.panelStates.size() != rhs.panelStates.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < lhs.panelStates.size(); ++index) {
|
||||
const UIEditorPanelSessionState& lhsState = lhs.panelStates[index];
|
||||
const UIEditorPanelSessionState& rhsState = rhs.panelStates[index];
|
||||
if (lhsState.panelId != rhsState.panelId ||
|
||||
lhsState.open != rhsState.open ||
|
||||
lhsState.visible != rhsState.visible) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<UIEditorWorkspaceVisiblePanel> CollectUIEditorWorkspaceVisiblePanels(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session) {
|
||||
std::vector<UIEditorWorkspaceVisiblePanel> visiblePanels = {};
|
||||
CollectVisiblePanelsRecursive(workspace.root, session, workspace.activePanelId, visiblePanels);
|
||||
return visiblePanels;
|
||||
}
|
||||
|
||||
const UIEditorWorkspacePanelState* FindUIEditorWorkspaceActivePanel(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session) {
|
||||
if (workspace.activePanelId.empty() ||
|
||||
!IsPanelOpenAndVisible(session, workspace.activePanelId)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const std::vector<UIEditorWorkspaceVisiblePanel> visiblePanels =
|
||||
CollectUIEditorWorkspaceVisiblePanels(workspace, session);
|
||||
for (const UIEditorWorkspaceVisiblePanel& panel : visiblePanels) {
|
||||
if (panel.panelId == workspace.activePanelId) {
|
||||
return FindPanelRecursive(workspace.root, workspace.activePanelId);
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool TryOpenUIEditorWorkspacePanel(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
const UIEditorWorkspaceModel workspaceBefore = workspace;
|
||||
const UIEditorWorkspaceSession sessionBefore = session;
|
||||
|
||||
UIEditorPanelSessionState* state = FindMutablePanelSessionState(session, panelId);
|
||||
if (state == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state->open = true;
|
||||
state->visible = true;
|
||||
NormalizeWorkspaceSession(panelRegistry, workspace, session, panelId);
|
||||
return !AreUIEditorWorkspaceModelsEquivalent(workspaceBefore, workspace) ||
|
||||
!AreUIEditorWorkspaceSessionsEquivalent(sessionBefore, session);
|
||||
}
|
||||
|
||||
bool TryCloseUIEditorWorkspacePanel(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
const UIEditorWorkspaceModel workspaceBefore = workspace;
|
||||
const UIEditorWorkspaceSession sessionBefore = session;
|
||||
|
||||
UIEditorPanelSessionState* state = FindMutablePanelSessionState(session, panelId);
|
||||
const UIEditorPanelDescriptor* descriptor = FindPanelDescriptor(panelRegistry, panelId);
|
||||
if (state == nullptr || descriptor == nullptr || !descriptor->canClose) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state->open = false;
|
||||
state->visible = false;
|
||||
NormalizeWorkspaceSession(panelRegistry, workspace, session, {});
|
||||
return !AreUIEditorWorkspaceModelsEquivalent(workspaceBefore, workspace) ||
|
||||
!AreUIEditorWorkspaceSessionsEquivalent(sessionBefore, session);
|
||||
}
|
||||
|
||||
bool TryShowUIEditorWorkspacePanel(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
const UIEditorWorkspaceModel workspaceBefore = workspace;
|
||||
const UIEditorWorkspaceSession sessionBefore = session;
|
||||
|
||||
UIEditorPanelSessionState* state = FindMutablePanelSessionState(session, panelId);
|
||||
const UIEditorPanelDescriptor* descriptor = FindPanelDescriptor(panelRegistry, panelId);
|
||||
if (state == nullptr || descriptor == nullptr || !state->open || !descriptor->canHide) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state->visible = true;
|
||||
NormalizeWorkspaceSession(panelRegistry, workspace, session, panelId);
|
||||
return !AreUIEditorWorkspaceModelsEquivalent(workspaceBefore, workspace) ||
|
||||
!AreUIEditorWorkspaceSessionsEquivalent(sessionBefore, session);
|
||||
}
|
||||
|
||||
bool TryHideUIEditorWorkspacePanel(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
const UIEditorWorkspaceModel workspaceBefore = workspace;
|
||||
const UIEditorWorkspaceSession sessionBefore = session;
|
||||
|
||||
UIEditorPanelSessionState* state = FindMutablePanelSessionState(session, panelId);
|
||||
const UIEditorPanelDescriptor* descriptor = FindPanelDescriptor(panelRegistry, panelId);
|
||||
if (state == nullptr || descriptor == nullptr || !state->open || !descriptor->canHide) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state->visible = false;
|
||||
NormalizeWorkspaceSession(panelRegistry, workspace, session, {});
|
||||
return !AreUIEditorWorkspaceModelsEquivalent(workspaceBefore, workspace) ||
|
||||
!AreUIEditorWorkspaceSessionsEquivalent(sessionBefore, session);
|
||||
}
|
||||
|
||||
bool TryActivateUIEditorWorkspacePanel(
|
||||
const UIEditorPanelRegistry& panelRegistry,
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
const UIEditorWorkspaceModel workspaceBefore = workspace;
|
||||
const UIEditorWorkspaceSession sessionBefore = session;
|
||||
|
||||
if (!IsPanelSelectable(workspace, session, panelId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
NormalizeWorkspaceSession(panelRegistry, workspace, session, panelId);
|
||||
return !AreUIEditorWorkspaceModelsEquivalent(workspaceBefore, workspace) ||
|
||||
!AreUIEditorWorkspaceSessionsEquivalent(sessionBefore, session);
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
211
editor/src/Workspace/UIEditorWorkspaceTransfer.cpp
Normal file
211
editor/src/Workspace/UIEditorWorkspaceTransfer.cpp
Normal file
@@ -0,0 +1,211 @@
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceTransfer.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceMutation.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceQueries.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
bool IsPanelOpenAndVisible(
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId) {
|
||||
const UIEditorPanelSessionState* state = FindUIEditorPanelSessionState(session, panelId);
|
||||
return state != nullptr && state->open && state->visible;
|
||||
}
|
||||
|
||||
void CollectWorkspacePanelIdsRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
std::vector<std::string>& outPanelIds) {
|
||||
if (node.kind == UIEditorWorkspaceNodeKind::Panel) {
|
||||
outPanelIds.push_back(node.panel.panelId);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const UIEditorWorkspaceNode& child : node.children) {
|
||||
CollectWorkspacePanelIdsRecursive(child, outPanelIds);
|
||||
}
|
||||
}
|
||||
|
||||
void ReorderSessionStatesToMatchWorkspace(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session) {
|
||||
std::vector<std::string> orderedPanelIds = {};
|
||||
CollectWorkspacePanelIdsRecursive(workspace.root, orderedPanelIds);
|
||||
|
||||
std::vector<UIEditorPanelSessionState> orderedStates = {};
|
||||
orderedStates.reserve(orderedPanelIds.size());
|
||||
for (const std::string& panelId : orderedPanelIds) {
|
||||
auto it = std::find_if(
|
||||
session.panelStates.begin(),
|
||||
session.panelStates.end(),
|
||||
[&panelId](const UIEditorPanelSessionState& state) {
|
||||
return state.panelId == panelId;
|
||||
});
|
||||
if (it != session.panelStates.end()) {
|
||||
orderedStates.push_back(*it);
|
||||
}
|
||||
}
|
||||
|
||||
session.panelStates = std::move(orderedStates);
|
||||
}
|
||||
|
||||
void NormalizeActivePanelAfterMutation(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view preferredPanelId) {
|
||||
if (!preferredPanelId.empty() &&
|
||||
IsPanelOpenAndVisible(session, preferredPanelId) &&
|
||||
ContainsUIEditorWorkspacePanel(workspace, preferredPanelId)) {
|
||||
TryActivateUIEditorWorkspacePanel(workspace, preferredPanelId);
|
||||
return;
|
||||
}
|
||||
|
||||
const std::vector<UIEditorWorkspaceVisiblePanel> visiblePanels =
|
||||
CollectUIEditorWorkspaceVisiblePanels(workspace, session);
|
||||
if (visiblePanels.empty()) {
|
||||
workspace.activePanelId.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
TryActivateUIEditorWorkspacePanel(workspace, visiblePanels.front().panelId);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool TryExtractUIEditorWorkspaceVisiblePanel(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
UIEditorWorkspaceExtractedPanel& extractedPanel) {
|
||||
const UIEditorWorkspaceModel workspaceBefore = workspace;
|
||||
const UIEditorWorkspaceSession sessionBefore = session;
|
||||
|
||||
UIEditorWorkspaceNode panelNode = {};
|
||||
if (!TryExtractUIEditorWorkspaceVisiblePanelNode(
|
||||
workspace,
|
||||
session,
|
||||
sourceNodeId,
|
||||
panelId,
|
||||
panelNode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto sessionIt = std::find_if(
|
||||
session.panelStates.begin(),
|
||||
session.panelStates.end(),
|
||||
[panelId](const UIEditorPanelSessionState& state) {
|
||||
return state.panelId == panelId;
|
||||
});
|
||||
if (sessionIt == session.panelStates.end()) {
|
||||
workspace = workspaceBefore;
|
||||
session = sessionBefore;
|
||||
return false;
|
||||
}
|
||||
|
||||
extractedPanel.panelNode = std::move(panelNode);
|
||||
extractedPanel.sessionState = *sessionIt;
|
||||
session.panelStates.erase(sessionIt);
|
||||
ReorderSessionStatesToMatchWorkspace(workspace, session);
|
||||
NormalizeActivePanelAfterMutation(workspace, session, workspace.activePanelId);
|
||||
return true;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceModel BuildUIEditorDetachedWorkspaceFromExtractedPanel(
|
||||
std::string rootNodeId,
|
||||
UIEditorWorkspaceExtractedPanel extractedPanel) {
|
||||
if (rootNodeId.empty()) {
|
||||
rootNodeId = "detached-workspace-root";
|
||||
}
|
||||
|
||||
const std::string panelId = extractedPanel.panelNode.panel.panelId;
|
||||
|
||||
UIEditorWorkspaceModel workspace = {};
|
||||
workspace.root.kind = UIEditorWorkspaceNodeKind::TabStack;
|
||||
workspace.root.nodeId = std::move(rootNodeId);
|
||||
workspace.root.selectedTabIndex = 0u;
|
||||
workspace.root.children.push_back(std::move(extractedPanel.panelNode));
|
||||
workspace.activePanelId = panelId;
|
||||
return workspace;
|
||||
}
|
||||
|
||||
UIEditorWorkspaceSession BuildUIEditorDetachedWorkspaceSessionFromExtractedPanel(
|
||||
UIEditorWorkspaceExtractedPanel extractedPanel) {
|
||||
UIEditorWorkspaceSession session = {};
|
||||
session.panelStates.push_back(std::move(extractedPanel.sessionState));
|
||||
return session;
|
||||
}
|
||||
|
||||
bool TryInsertExtractedUIEditorWorkspacePanelToStack(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
UIEditorWorkspaceExtractedPanel extractedPanel,
|
||||
std::string_view targetNodeId,
|
||||
std::size_t targetVisibleInsertionIndex) {
|
||||
if (extractedPanel.panelNode.kind != UIEditorWorkspaceNodeKind::Panel ||
|
||||
extractedPanel.sessionState.panelId != extractedPanel.panelNode.panel.panelId ||
|
||||
extractedPanel.sessionState.panelId.empty() ||
|
||||
FindUIEditorPanelSessionState(session, extractedPanel.sessionState.panelId) != nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceModel workspaceBefore = workspace;
|
||||
const UIEditorWorkspaceSession sessionBefore = session;
|
||||
session.panelStates.push_back(extractedPanel.sessionState);
|
||||
|
||||
if (!TryInsertUIEditorWorkspacePanelNodeToStack(
|
||||
workspace,
|
||||
session,
|
||||
std::move(extractedPanel.panelNode),
|
||||
targetNodeId,
|
||||
targetVisibleInsertionIndex)) {
|
||||
workspace = workspaceBefore;
|
||||
session = sessionBefore;
|
||||
return false;
|
||||
}
|
||||
|
||||
ReorderSessionStatesToMatchWorkspace(workspace, session);
|
||||
NormalizeActivePanelAfterMutation(workspace, session, extractedPanel.sessionState.panelId);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryDockExtractedUIEditorWorkspacePanelRelative(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
UIEditorWorkspaceSession& session,
|
||||
UIEditorWorkspaceExtractedPanel extractedPanel,
|
||||
std::string_view targetNodeId,
|
||||
UIEditorWorkspaceDockPlacement placement,
|
||||
float splitRatio) {
|
||||
if (extractedPanel.panelNode.kind != UIEditorWorkspaceNodeKind::Panel ||
|
||||
extractedPanel.sessionState.panelId != extractedPanel.panelNode.panel.panelId ||
|
||||
extractedPanel.sessionState.panelId.empty() ||
|
||||
FindUIEditorPanelSessionState(session, extractedPanel.sessionState.panelId) != nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const UIEditorWorkspaceModel workspaceBefore = workspace;
|
||||
const UIEditorWorkspaceSession sessionBefore = session;
|
||||
session.panelStates.push_back(extractedPanel.sessionState);
|
||||
|
||||
if (!TryDockUIEditorWorkspacePanelNodeRelative(
|
||||
workspace,
|
||||
session,
|
||||
std::move(extractedPanel.panelNode),
|
||||
targetNodeId,
|
||||
placement,
|
||||
splitRatio)) {
|
||||
workspace = workspaceBefore;
|
||||
session = sessionBefore;
|
||||
return false;
|
||||
}
|
||||
|
||||
ReorderSessionStatesToMatchWorkspace(workspace, session);
|
||||
NormalizeActivePanelAfterMutation(workspace, session, extractedPanel.sessionState.panelId);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
38
editor/src/Workspace/WorkspaceControllerInternal.h
Normal file
38
editor/src/Workspace/WorkspaceControllerInternal.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceController.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceMutation.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceQueries.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceValidation.h>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::UI::Editor::Internal {
|
||||
|
||||
bool IsPanelOpenAndVisible(
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId);
|
||||
|
||||
std::vector<std::string> CollectVisiblePanelIds(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session);
|
||||
|
||||
struct VisibleTabStackInfo {
|
||||
bool panelExists = false;
|
||||
bool panelVisible = false;
|
||||
std::size_t currentVisibleIndex = 0u;
|
||||
std::size_t visibleTabCount = 0u;
|
||||
};
|
||||
|
||||
VisibleTabStackInfo ResolveVisibleTabStackInfo(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId);
|
||||
|
||||
std::size_t CountVisibleTabs(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorWorkspaceSession& session);
|
||||
|
||||
} // namespace XCEngine::UI::Editor::Internal
|
||||
97
editor/src/Workspace/WorkspaceModelInternal.h
Normal file
97
editor/src/Workspace/WorkspaceModelInternal.h
Normal file
@@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEditor/Panels/UIEditorPanelRegistry.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceModel.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceQueries.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceSession.h>
|
||||
#include <XCEditor/Workspace/UIEditorWorkspaceValidation.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::UI::Editor::Internal {
|
||||
|
||||
UIEditorWorkspaceValidationResult MakeValidationError(
|
||||
UIEditorWorkspaceValidationCode code,
|
||||
std::string message);
|
||||
|
||||
bool IsValidSplitRatio(float value);
|
||||
std::string BuildSingleTabPanelNodeId(std::string_view stackNodeId);
|
||||
UIEditorWorkspaceNode WrapStandalonePanelAsTabStack(UIEditorWorkspaceNode panelNode);
|
||||
void CollapseSplitNodeToOnlyChild(UIEditorWorkspaceNode& node);
|
||||
void CanonicalizeNodeRecursive(
|
||||
UIEditorWorkspaceNode& node,
|
||||
bool allowStandalonePanelLeaf);
|
||||
|
||||
const UIEditorPanelDescriptor& RequirePanelDescriptor(
|
||||
const UIEditorPanelRegistry& registry,
|
||||
std::string_view panelId);
|
||||
|
||||
const UIEditorWorkspacePanelState* FindPanelRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
std::string_view panelId);
|
||||
const UIEditorWorkspaceNode* FindNodeRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
std::string_view nodeId);
|
||||
UIEditorWorkspaceNode* FindMutableNodeRecursive(
|
||||
UIEditorWorkspaceNode& node,
|
||||
std::string_view nodeId);
|
||||
|
||||
bool FindNodePathRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
std::string_view nodeId,
|
||||
std::vector<std::size_t>& path);
|
||||
UIEditorWorkspaceNode* ResolveMutableNodeByPath(
|
||||
UIEditorWorkspaceNode& node,
|
||||
const std::vector<std::size_t>& path);
|
||||
|
||||
bool IsPanelOpenAndVisibleInSession(
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view panelId);
|
||||
std::size_t CountVisibleChildren(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorWorkspaceSession& session);
|
||||
std::size_t ResolveActualInsertionIndexForVisibleInsertion(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::size_t targetVisibleInsertionIndex);
|
||||
|
||||
void FixTabStackSelectedIndex(
|
||||
UIEditorWorkspaceNode& node,
|
||||
std::string_view preferredPanelId);
|
||||
bool RemoveNodeByIdRecursive(
|
||||
UIEditorWorkspaceNode& node,
|
||||
std::string_view nodeId);
|
||||
|
||||
float ClampDockSplitRatio(float value);
|
||||
bool IsLeadingDockPlacement(UIEditorWorkspaceDockPlacement placement);
|
||||
UIEditorWorkspaceSplitAxis ResolveDockSplitAxis(
|
||||
UIEditorWorkspaceDockPlacement placement);
|
||||
std::string MakeUniqueNodeId(
|
||||
const UIEditorWorkspaceModel& workspace,
|
||||
std::string base);
|
||||
|
||||
bool TryExtractVisiblePanelFromTabStack(
|
||||
UIEditorWorkspaceModel& workspace,
|
||||
const UIEditorWorkspaceSession& session,
|
||||
std::string_view sourceNodeId,
|
||||
std::string_view panelId,
|
||||
UIEditorWorkspaceNode& extractedPanel);
|
||||
bool TryActivateRecursive(
|
||||
UIEditorWorkspaceNode& node,
|
||||
std::string_view panelId);
|
||||
|
||||
void CollectVisiblePanelsRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
std::string_view activePanelId,
|
||||
std::vector<UIEditorWorkspaceVisiblePanel>& outPanels);
|
||||
|
||||
UIEditorWorkspaceValidationResult ValidateNodeRecursive(
|
||||
const UIEditorWorkspaceNode& node,
|
||||
std::unordered_set<std::string>& nodeIds,
|
||||
std::unordered_set<std::string>& panelIds);
|
||||
|
||||
} // namespace XCEngine::UI::Editor::Internal
|
||||
Reference in New Issue
Block a user