refactor(new_editor/app): reorganize host structure and add smoke test

This commit is contained in:
2026-04-15 08:24:06 +08:00
parent 3617b4840b
commit 9e5954cf0a
235 changed files with 11157 additions and 10028 deletions

View File

@@ -0,0 +1,446 @@
#include <XCEditor/Workspace/UIEditorWindowWorkspaceController.h>
#include <XCEditor/Workspace/UIEditorWorkspaceTransfer.h>
#include <algorithm>
#include <utility>
namespace XCEngine::UI::Editor {
namespace {
bool IsSinglePanelRootWindow(
const UIEditorWindowWorkspaceState& state,
std::string_view panelId) {
const UIEditorWorkspaceNode& root = state.workspace.root;
return root.kind == UIEditorWorkspaceNodeKind::TabStack &&
root.children.size() == 1u &&
root.children.front().kind == UIEditorWorkspaceNodeKind::Panel &&
root.children.front().panel.panelId == panelId &&
state.session.panelStates.size() == 1u &&
state.session.panelStates.front().panelId == panelId;
}
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, 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, 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) {
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);
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) {
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);
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

View File

@@ -0,0 +1,145 @@
#include <XCEditor/Workspace/UIEditorWindowWorkspaceModel.h>
#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 = {};
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);
}
}
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

View File

@@ -0,0 +1,332 @@
#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;
}
std::vector<UIEditorPanelContentHostBinding> BuildContentHostBindings(
const UIEditorPanelRegistry& panelRegistry,
const std::vector<UIEditorWorkspacePanelPresentationModel>& presentations) {
std::vector<UIEditorPanelContentHostBinding> bindings = {};
bindings.reserve(presentations.size());
for (const UIEditorWorkspacePanelPresentationModel& presentation : presentations) {
const UIEditorPanelDescriptor* descriptor =
FindUIEditorPanelDescriptor(panelRegistry, presentation.panelId);
if (descriptor == nullptr ||
descriptor->presentationKind != presentation.kind ||
!IsUIEditorPanelPresentationExternallyHosted(presentation.kind)) {
continue;
}
UIEditorPanelContentHostBinding binding = {};
binding.panelId = presentation.panelId;
binding.kind = presentation.kind;
bindings.push_back(std::move(binding));
}
return bindings;
}
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 ::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) {
UIEditorWorkspaceComposeRequest request = {};
request.dockHostLayout = BuildUIEditorDockHostLayout(
bounds,
panelRegistry,
workspace,
session,
dockHostState,
dockHostMetrics);
const std::vector<UIEditorPanelContentHostBinding> contentHostBindings =
BuildContentHostBindings(panelRegistry, presentations);
request.contentHostRequest = ResolveUIEditorPanelContentHostRequest(
request.dockHostLayout,
panelRegistry,
contentHostBindings);
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.viewportShellRequest = ResolveUIEditorViewportShellRequest(
mountRequest.bounds,
presentation->viewportShellModel.spec,
viewportMetrics);
request.viewportRequests.push_back(std::move(viewportRequest));
}
return request;
}
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) {
UIEditorWorkspaceComposeFrame frame = {};
frame.dockHostLayout = BuildUIEditorDockHostLayout(
bounds,
panelRegistry,
workspace,
session,
dockHostState,
dockHostMetrics);
const std::vector<UIEditorPanelContentHostBinding> contentHostBindings =
BuildContentHostBindings(panelRegistry, presentations);
const UIEditorPanelContentHostRequest contentHostRequest =
ResolveUIEditorPanelContentHostRequest(
frame.dockHostLayout,
panelRegistry,
contentHostBindings);
frame.contentHostFrame = UpdateUIEditorPanelContentHost(
state.contentHostState,
contentHostRequest,
panelRegistry,
contentHostBindings);
TrimObsoleteViewportPresentationStates(state, panelRegistry, presentations);
for (const UIEditorWorkspacePanelPresentationModel& presentation : presentations) {
if (!SupportsExternalViewportPresentation(panelRegistry, presentation)) {
continue;
}
const UIEditorPanelContentHostPanelState* contentHostPanelState =
FindUIEditorPanelContentHostPanelState(frame.contentHostFrame, presentation.panelId);
if (contentHostPanelState == nullptr || !contentHostPanelState->mounted) {
ResetHiddenViewportPresentationState(state, presentation.panelId);
continue;
}
UIEditorWorkspacePanelPresentationState& panelState =
EnsurePanelState(state, presentation.panelId);
UIEditorWorkspaceViewportComposeFrame viewportFrame = {};
viewportFrame.panelId = presentation.panelId;
viewportFrame.bounds = contentHostPanelState->bounds;
viewportFrame.viewportShellModel = presentation.viewportShellModel;
viewportFrame.viewportShellFrame = UpdateUIEditorViewportShell(
panelState.viewportShellState,
contentHostPanelState->bounds,
presentation.viewportShellModel,
inputEvents,
viewportMetrics);
frame.viewportFrames.push_back(std::move(viewportFrame));
}
return frame;
}
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) {
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);
}
UIEditorDockHostForegroundOptions foregroundOptions = {};
foregroundOptions.externalBodyPanelIds =
CollectUIEditorWorkspaceComposeExternalBodyPanelIds(frame);
AppendUIEditorDockHostForeground(
drawList,
frame.dockHostLayout,
foregroundOptions,
dockHostPalette,
dockHostMetrics);
}
} // namespace XCEngine::UI::Editor

View File

@@ -0,0 +1,805 @@
#include <XCEditor/Workspace/UIEditorWorkspaceController.h>
#include <XCEditor/Foundation/UIEditorRuntimeTrace.h>
#include <cmath>
#include <sstream>
#include <utility>
namespace XCEngine::UI::Editor {
namespace {
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;
}
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) {
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
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 = 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 = 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);
}
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::ReorderTab(
std::string_view nodeId,
std::string_view panelId,
std::size_t targetVisibleInsertionIndex) {
const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
if (!validation.IsValid()) {
return BuildLayoutOperationResult(
UIEditorWorkspaceLayoutOperationStatus::Rejected,
"Controller state invalid: " + validation.message);
}
if (nodeId.empty()) {
return BuildLayoutOperationResult(
UIEditorWorkspaceLayoutOperationStatus::Rejected,
"ReorderTab requires a tab stack node id.");
}
if (panelId.empty()) {
return BuildLayoutOperationResult(
UIEditorWorkspaceLayoutOperationStatus::Rejected,
"ReorderTab requires a panel id.");
}
const UIEditorWorkspaceNode* tabStack = FindUIEditorWorkspaceNode(m_workspace, nodeId);
if (tabStack == nullptr || tabStack->kind != UIEditorWorkspaceNodeKind::TabStack) {
return BuildLayoutOperationResult(
UIEditorWorkspaceLayoutOperationStatus::Rejected,
"ReorderTab target tab stack is missing.");
}
const VisibleTabStackInfo tabInfo =
ResolveVisibleTabStackInfo(*tabStack, m_session, panelId);
if (!tabInfo.panelExists) {
return BuildLayoutOperationResult(
UIEditorWorkspaceLayoutOperationStatus::Rejected,
"ReorderTab target panel is missing from the specified tab stack.");
}
if (!tabInfo.panelVisible) {
return BuildLayoutOperationResult(
UIEditorWorkspaceLayoutOperationStatus::Rejected,
"ReorderTab only supports open and visible tabs.");
}
if (targetVisibleInsertionIndex > tabInfo.visibleTabCount) {
return BuildLayoutOperationResult(
UIEditorWorkspaceLayoutOperationStatus::Rejected,
"ReorderTab target visible insertion index is out of range.");
}
if (targetVisibleInsertionIndex == tabInfo.currentVisibleIndex ||
targetVisibleInsertionIndex == tabInfo.currentVisibleIndex + 1u) {
return BuildLayoutOperationResult(
UIEditorWorkspaceLayoutOperationStatus::NoOp,
"Visible tab order already matches the requested insertion.");
}
const UIEditorWorkspaceModel previousWorkspace = m_workspace;
if (!TryReorderUIEditorWorkspaceTab(
m_workspace,
m_session,
nodeId,
panelId,
targetVisibleInsertionIndex)) {
return BuildLayoutOperationResult(
UIEditorWorkspaceLayoutOperationStatus::Rejected,
"Tab reorder rejected.");
}
if (AreUIEditorWorkspaceModelsEquivalent(previousWorkspace, m_workspace)) {
return BuildLayoutOperationResult(
UIEditorWorkspaceLayoutOperationStatus::NoOp,
"Visible tab order already matches the requested insertion.");
}
const UIEditorWorkspaceControllerValidationResult postValidation = ValidateState();
if (!postValidation.IsValid()) {
m_workspace = previousWorkspace;
return BuildLayoutOperationResult(
UIEditorWorkspaceLayoutOperationStatus::Rejected,
"Tab reorder produced invalid controller state: " + postValidation.message);
}
return BuildLayoutOperationResult(
UIEditorWorkspaceLayoutOperationStatus::Changed,
"Tab reordered.");
}
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 ReorderTab(sourceNodeId, panelId, targetVisibleInsertionIndex);
}
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 VisibleTabStackInfo sourceInfo =
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 = 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 VisibleTabStackInfo sourceInfo =
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.");
}
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.");
}
UIEditorWorkspaceController BuildDefaultUIEditorWorkspaceController(
const UIEditorPanelRegistry& panelRegistry,
const UIEditorWorkspaceModel& workspace) {
const UIEditorWorkspaceModel canonicalWorkspace =
CanonicalizeUIEditorWorkspaceModel(workspace);
return UIEditorWorkspaceController(
panelRegistry,
canonicalWorkspace,
BuildDefaultUIEditorWorkspaceSession(panelRegistry, canonicalWorkspace));
}
} // namespace XCEngine::UI::Editor

View File

@@ -0,0 +1,88 @@
#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();
}
} // 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) {
UIEditorWorkspaceInteractionFrame frame = {};
frame.dockHostFrame = UpdateUIEditorDockHostInteraction(
state.dockHostInteractionState,
controller,
bounds,
inputEvents,
dockHostMetrics);
frame.composeFrame = UpdateUIEditorWorkspaceCompose(
state.composeState,
bounds,
controller.GetPanelRegistry(),
controller.GetWorkspace(),
controller.GetSession(),
model.workspacePresentations,
inputEvents,
state.dockHostInteractionState.dockHostState,
dockHostMetrics,
viewportMetrics);
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

View File

@@ -0,0 +1,499 @@
#include <XCEditor/Workspace/UIEditorWorkspaceLayoutPersistence.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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,445 @@
#include <XCEditor/Workspace/UIEditorWorkspaceSession.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

View File

@@ -0,0 +1,209 @@
#include <XCEditor/Workspace/UIEditorWorkspaceTransfer.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