Refine XCEditor docking and DPI rendering

This commit is contained in:
2026-04-11 17:07:37 +08:00
parent 35d3d6328b
commit 2958dcc491
46 changed files with 4839 additions and 471 deletions

View File

@@ -1,12 +1,22 @@
#include <XCEditor/Shell/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) {
@@ -22,6 +32,58 @@ std::vector<std::string> CollectVisiblePanelIds(
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) {
@@ -297,6 +359,290 @@ UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::SetSplitRati
"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();