Unify dock leaves around single-tab stacks

This commit is contained in:
2026-04-10 21:50:31 +08:00
parent b187c8970b
commit 977a4cf2a4
14 changed files with 258 additions and 195 deletions

View File

@@ -40,7 +40,7 @@ UIEditorWorkspaceModel BuildWorkspace() {
"workspace-top", "workspace-top",
UIEditorWorkspaceSplitAxis::Horizontal, UIEditorWorkspaceSplitAxis::Horizontal,
0.15f, 0.15f,
BuildUIEditorWorkspacePanel( BuildUIEditorWorkspaceSingleTabStack(
"hierarchy-panel", "hierarchy-panel",
"hierarchy", "hierarchy",
"Hierarchy", "Hierarchy",
@@ -64,7 +64,7 @@ UIEditorWorkspaceModel BuildWorkspace() {
false) false)
}, },
0u), 0u),
BuildUIEditorWorkspacePanel( BuildUIEditorWorkspaceSingleTabStack(
"inspector-panel", "inspector-panel",
"inspector", "inspector",
"Inspector", "Inspector",

View File

@@ -96,15 +96,6 @@ struct UIEditorDockHostSplitterLayout {
bool active = false; bool active = false;
}; };
struct UIEditorDockHostPanelLayout {
std::string nodeId = {};
std::string panelId = {};
std::string title = {};
bool active = false;
UIEditorPanelFrameState frameState = {};
UIEditorPanelFrameLayout frameLayout = {};
};
struct UIEditorDockHostTabStackLayout { struct UIEditorDockHostTabStackLayout {
std::string nodeId = {}; std::string nodeId = {};
std::string selectedPanelId = {}; std::string selectedPanelId = {};
@@ -119,7 +110,6 @@ struct UIEditorDockHostTabStackLayout {
struct UIEditorDockHostLayout { struct UIEditorDockHostLayout {
::XCEngine::UI::UIRect bounds = {}; ::XCEngine::UI::UIRect bounds = {};
std::vector<UIEditorDockHostSplitterLayout> splitters = {}; std::vector<UIEditorDockHostSplitterLayout> splitters = {};
std::vector<UIEditorDockHostPanelLayout> panels = {};
std::vector<UIEditorDockHostTabStackLayout> tabStacks = {}; std::vector<UIEditorDockHostTabStackLayout> tabStacks = {};
}; };

View File

@@ -78,6 +78,12 @@ UIEditorWorkspaceNode BuildUIEditorWorkspacePanel(
std::string title, std::string title,
bool placeholder = false); bool placeholder = false);
UIEditorWorkspaceNode BuildUIEditorWorkspaceSingleTabStack(
std::string nodeId,
std::string panelId,
std::string title,
bool placeholder = false);
UIEditorWorkspaceNode BuildUIEditorWorkspaceTabStack( UIEditorWorkspaceNode BuildUIEditorWorkspaceTabStack(
std::string nodeId, std::string nodeId,
std::vector<UIEditorWorkspaceNode> panels, std::vector<UIEditorWorkspaceNode> panels,
@@ -93,6 +99,9 @@ UIEditorWorkspaceNode BuildUIEditorWorkspaceSplit(
UIEditorWorkspaceValidationResult ValidateUIEditorWorkspace( UIEditorWorkspaceValidationResult ValidateUIEditorWorkspace(
const UIEditorWorkspaceModel& workspace); const UIEditorWorkspaceModel& workspace);
UIEditorWorkspaceModel CanonicalizeUIEditorWorkspaceModel(
UIEditorWorkspaceModel workspace);
std::vector<UIEditorWorkspaceVisiblePanel> CollectUIEditorWorkspaceVisiblePanels( std::vector<UIEditorWorkspaceVisiblePanel> CollectUIEditorWorkspaceVisiblePanels(
const UIEditorWorkspaceModel& workspace); const UIEditorWorkspaceModel& workspace);

View File

@@ -23,9 +23,6 @@ using ::XCEngine::UI::Layout::UITabStripMeasureItem;
using ::XCEngine::UI::Layout::UILayoutAxis; using ::XCEngine::UI::Layout::UILayoutAxis;
using ::XCEngine::UI::Widgets::ExpandUISplitterHandleHitRect; using ::XCEngine::UI::Widgets::ExpandUISplitterHandleHitRect;
constexpr std::string_view kStandalonePanelActiveFooter = "";
constexpr std::string_view kStandalonePanelInactiveFooter = "";
struct DockMeasureResult { struct DockMeasureResult {
bool visible = false; bool visible = false;
UISize minimumSize = {}; UISize minimumSize = {};
@@ -102,17 +99,6 @@ UIEditorPanelFrameMetrics BuildTabContentFrameMetrics(
return frameMetrics; return frameMetrics;
} }
UISize MeasurePanelMinimumSize(
const UIEditorDockHostMetrics& metrics) {
const UIEditorPanelFrameMetrics& frameMetrics = metrics.panelFrameMetrics;
return UISize(
metrics.minimumStandalonePanelBodySize.width +
ClampNonNegative(frameMetrics.contentPadding) * 2.0f,
ClampNonNegative(frameMetrics.headerHeight) +
metrics.minimumStandalonePanelBodySize.height +
ClampNonNegative(frameMetrics.contentPadding) * 2.0f);
}
UISize MeasureTabContentMinimumSize( UISize MeasureTabContentMinimumSize(
const UIEditorDockHostMetrics& metrics) { const UIEditorDockHostMetrics& metrics) {
const UIEditorPanelFrameMetrics frameMetrics = BuildTabContentFrameMetrics(metrics); const UIEditorPanelFrameMetrics frameMetrics = BuildTabContentFrameMetrics(metrics);
@@ -205,14 +191,8 @@ DockMeasureResult MeasureNodeRecursive(
const UIEditorWorkspaceSession& session, const UIEditorWorkspaceSession& session,
const UIEditorDockHostMetrics& metrics) { const UIEditorDockHostMetrics& metrics) {
switch (node.kind) { switch (node.kind) {
case UIEditorWorkspaceNodeKind::Panel: { case UIEditorWorkspaceNodeKind::Panel:
DockMeasureResult result = {}; return {};
result.visible = IsPanelOpenAndVisible(session, node.panel.panelId);
if (result.visible) {
result.minimumSize = MeasurePanelMinimumSize(metrics);
}
return result;
}
case UIEditorWorkspaceNodeKind::TabStack: case UIEditorWorkspaceNodeKind::TabStack:
return MeasureTabStackNode(node, panelRegistry, session, metrics); return MeasureTabStackNode(node, panelRegistry, session, metrics);
case UIEditorWorkspaceNodeKind::Split: case UIEditorWorkspaceNodeKind::Split:
@@ -269,38 +249,6 @@ UIEditorTabStripState BuildTabStripState(
return tabState; return tabState;
} }
UIEditorPanelFrameState BuildStandalonePanelFrameState(
const UIEditorDockHostState& state,
const UIEditorWorkspaceModel& workspace,
const UIEditorPanelDescriptor* descriptor,
const UIEditorWorkspaceNode& node) {
UIEditorPanelFrameState frameState = {};
frameState.active = workspace.activePanelId == node.panel.panelId;
frameState.focused = state.focused && frameState.active;
frameState.closable = descriptor != nullptr ? descriptor->canClose : true;
frameState.pinnable = false;
frameState.showFooter = true;
if (state.hoveredTarget.panelId != node.panel.panelId) {
return frameState;
}
switch (state.hoveredTarget.kind) {
case UIEditorDockHostHitTargetKind::PanelHeader:
case UIEditorDockHostHitTargetKind::PanelBody:
case UIEditorDockHostHitTargetKind::PanelFooter:
case UIEditorDockHostHitTargetKind::PanelCloseButton:
frameState.hovered = true;
break;
default:
break;
}
frameState.closeHovered =
state.hoveredTarget.kind == UIEditorDockHostHitTargetKind::PanelCloseButton;
return frameState;
}
UIEditorPanelFrameState BuildTabContentFrameState( UIEditorPanelFrameState BuildTabContentFrameState(
const UIEditorDockHostState& state, const UIEditorDockHostState& state,
const UIEditorWorkspaceModel& workspace, const UIEditorWorkspaceModel& workspace,
@@ -338,29 +286,6 @@ void LayoutNodeRecursive(
const UIEditorDockHostMetrics& metrics, const UIEditorDockHostMetrics& metrics,
UIEditorDockHostLayout& layout); UIEditorDockHostLayout& layout);
void LayoutPanelNode(
const UIEditorWorkspaceNode& node,
const UIRect& bounds,
const UIEditorPanelRegistry& panelRegistry,
const UIEditorWorkspaceModel& workspace,
const UIEditorDockHostState& state,
const UIEditorDockHostMetrics& metrics,
UIEditorDockHostLayout& layout) {
const UIEditorPanelDescriptor* descriptor =
FindPanelDescriptor(panelRegistry, node.panel.panelId);
UIEditorDockHostPanelLayout panelLayout = {};
panelLayout.nodeId = node.nodeId;
panelLayout.panelId = node.panel.panelId;
panelLayout.title = node.panel.title;
panelLayout.active = workspace.activePanelId == node.panel.panelId;
panelLayout.frameState =
BuildStandalonePanelFrameState(state, workspace, descriptor, node);
panelLayout.frameLayout =
BuildUIEditorPanelFrameLayout(bounds, panelLayout.frameState, metrics.panelFrameMetrics);
layout.panels.push_back(std::move(panelLayout));
}
void LayoutTabStackNode( void LayoutTabStackNode(
const UIEditorWorkspaceNode& node, const UIEditorWorkspaceNode& node,
const UIRect& bounds, const UIRect& bounds,
@@ -525,9 +450,6 @@ void LayoutNodeRecursive(
UIEditorDockHostLayout& layout) { UIEditorDockHostLayout& layout) {
switch (node.kind) { switch (node.kind) {
case UIEditorWorkspaceNodeKind::Panel: case UIEditorWorkspaceNodeKind::Panel:
if (IsPanelOpenAndVisible(session, node.panel.panelId)) {
LayoutPanelNode(node, bounds, panelRegistry, workspace, state, metrics, layout);
}
return; return;
case UIEditorWorkspaceNodeKind::TabStack: case UIEditorWorkspaceNodeKind::TabStack:
LayoutTabStackNode(node, bounds, panelRegistry, workspace, session, state, metrics, layout); LayoutTabStackNode(node, bounds, panelRegistry, workspace, session, state, metrics, layout);
@@ -606,11 +528,13 @@ UIEditorDockHostLayout BuildUIEditorDockHostLayout(
bounds.y, bounds.y,
ClampNonNegative(bounds.width), ClampNonNegative(bounds.width),
ClampNonNegative(bounds.height)); ClampNonNegative(bounds.height));
const UIEditorWorkspaceModel canonicalWorkspace =
CanonicalizeUIEditorWorkspaceModel(workspace);
LayoutNodeRecursive( LayoutNodeRecursive(
workspace.root, canonicalWorkspace.root,
layout.bounds, layout.bounds,
panelRegistry, panelRegistry,
workspace, canonicalWorkspace,
session, session,
state, state,
metrics, metrics,
@@ -675,17 +599,6 @@ UIEditorDockHostHitTarget HitTestUIEditorDockHost(
} }
} }
for (std::size_t index = layout.panels.size(); index > 0u; --index) {
const UIEditorDockHostPanelLayout& panel = layout.panels[index - 1u];
const UIEditorPanelFrameHitTarget hitTarget = HitTestUIEditorPanelFrame(
panel.frameLayout,
panel.frameState,
point);
if (hitTarget != UIEditorPanelFrameHitTarget::None) {
return MapPanelFrameHitTarget(hitTarget, panel.nodeId, panel.panelId);
}
}
return {}; return {};
} }
@@ -694,15 +607,6 @@ void AppendUIEditorDockHostBackground(
const UIEditorDockHostLayout& layout, const UIEditorDockHostLayout& layout,
const UIEditorDockHostPalette& palette, const UIEditorDockHostPalette& palette,
const UIEditorDockHostMetrics& metrics) { const UIEditorDockHostMetrics& metrics) {
for (const UIEditorDockHostPanelLayout& panel : layout.panels) {
AppendUIEditorPanelFrameBackground(
drawList,
panel.frameLayout,
panel.frameState,
palette.panelFramePalette,
metrics.panelFrameMetrics);
}
const UIEditorPanelFrameMetrics tabContentFrameMetrics = const UIEditorPanelFrameMetrics tabContentFrameMetrics =
BuildTabContentFrameMetrics(metrics); BuildTabContentFrameMetrics(metrics);
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) { for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
@@ -744,23 +648,6 @@ void AppendUIEditorDockHostForeground(
const UIEditorDockHostForegroundOptions& options, const UIEditorDockHostForegroundOptions& options,
const UIEditorDockHostPalette& palette, const UIEditorDockHostPalette& palette,
const UIEditorDockHostMetrics& metrics) { const UIEditorDockHostMetrics& metrics) {
for (const UIEditorDockHostPanelLayout& panel : layout.panels) {
AppendUIEditorPanelFrameForeground(
drawList,
panel.frameLayout,
panel.frameState,
UIEditorPanelFrameText{
panel.title,
{},
panel.active ? kStandalonePanelActiveFooter : kStandalonePanelInactiveFooter
},
palette.panelFramePalette,
metrics.panelFrameMetrics);
if (UsesExternalBodyPresentation(options, panel.panelId)) {
continue;
}
}
const UIEditorPanelFrameMetrics tabContentFrameMetrics = const UIEditorPanelFrameMetrics tabContentFrameMetrics =
BuildTabContentFrameMetrics(metrics); BuildTabContentFrameMetrics(metrics);
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) { for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {

View File

@@ -12,12 +12,6 @@ namespace {
const ::XCEngine::UI::UIRect* FindVisiblePanelBodyRect( const ::XCEngine::UI::UIRect* FindVisiblePanelBodyRect(
const Widgets::UIEditorDockHostLayout& layout, const Widgets::UIEditorDockHostLayout& layout,
std::string_view panelId) { std::string_view panelId) {
for (const Widgets::UIEditorDockHostPanelLayout& panel : layout.panels) {
if (panel.panelId == panelId) {
return &panel.frameLayout.bodyRect;
}
}
for (const Widgets::UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) { for (const Widgets::UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
if (tabStack.selectedPanelId == panelId) { if (tabStack.selectedPanelId == panelId) {
return &tabStack.contentFrameLayout.bodyRect; return &tabStack.contentFrameLayout.bodyRect;

View File

@@ -75,9 +75,9 @@ UIEditorWorkspaceController::UIEditorWorkspaceController(
UIEditorWorkspaceModel workspace, UIEditorWorkspaceModel workspace,
UIEditorWorkspaceSession session) UIEditorWorkspaceSession session)
: m_panelRegistry(std::move(panelRegistry)) : m_panelRegistry(std::move(panelRegistry))
, m_baselineWorkspace(workspace) , m_baselineWorkspace(CanonicalizeUIEditorWorkspaceModel(workspace))
, m_baselineSession(session) , m_baselineSession(session)
, m_workspace(std::move(workspace)) , m_workspace(m_baselineWorkspace)
, m_session(std::move(session)) { , m_session(std::move(session)) {
} }
@@ -178,6 +178,10 @@ const UIEditorPanelDescriptor* UIEditorWorkspaceController::FindPanelDescriptor(
UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::RestoreLayoutSnapshot( UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::RestoreLayoutSnapshot(
const UIEditorWorkspaceLayoutSnapshot& snapshot) { const UIEditorWorkspaceLayoutSnapshot& snapshot) {
UIEditorWorkspaceLayoutSnapshot canonicalSnapshot = snapshot;
canonicalSnapshot.workspace =
CanonicalizeUIEditorWorkspaceModel(std::move(canonicalSnapshot.workspace));
const UIEditorPanelRegistryValidationResult registryValidation = const UIEditorPanelRegistryValidationResult registryValidation =
ValidateUIEditorPanelRegistry(m_panelRegistry); ValidateUIEditorPanelRegistry(m_panelRegistry);
if (!registryValidation.IsValid()) { if (!registryValidation.IsValid()) {
@@ -187,7 +191,7 @@ UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::RestoreLayou
} }
const UIEditorWorkspaceValidationResult workspaceValidation = const UIEditorWorkspaceValidationResult workspaceValidation =
ValidateUIEditorWorkspace(snapshot.workspace); ValidateUIEditorWorkspace(canonicalSnapshot.workspace);
if (!workspaceValidation.IsValid()) { if (!workspaceValidation.IsValid()) {
return BuildLayoutOperationResult( return BuildLayoutOperationResult(
UIEditorWorkspaceLayoutOperationStatus::Rejected, UIEditorWorkspaceLayoutOperationStatus::Rejected,
@@ -195,15 +199,18 @@ UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::RestoreLayou
} }
const UIEditorWorkspaceSessionValidationResult sessionValidation = const UIEditorWorkspaceSessionValidationResult sessionValidation =
ValidateUIEditorWorkspaceSession(m_panelRegistry, snapshot.workspace, snapshot.session); ValidateUIEditorWorkspaceSession(
m_panelRegistry,
canonicalSnapshot.workspace,
canonicalSnapshot.session);
if (!sessionValidation.IsValid()) { if (!sessionValidation.IsValid()) {
return BuildLayoutOperationResult( return BuildLayoutOperationResult(
UIEditorWorkspaceLayoutOperationStatus::Rejected, UIEditorWorkspaceLayoutOperationStatus::Rejected,
"Layout session invalid: " + sessionValidation.message); "Layout session invalid: " + sessionValidation.message);
} }
if (AreUIEditorWorkspaceModelsEquivalent(m_workspace, snapshot.workspace) && if (AreUIEditorWorkspaceModelsEquivalent(m_workspace, canonicalSnapshot.workspace) &&
AreUIEditorWorkspaceSessionsEquivalent(m_session, snapshot.session)) { AreUIEditorWorkspaceSessionsEquivalent(m_session, canonicalSnapshot.session)) {
return BuildLayoutOperationResult( return BuildLayoutOperationResult(
UIEditorWorkspaceLayoutOperationStatus::NoOp, UIEditorWorkspaceLayoutOperationStatus::NoOp,
"Current state already matches the requested layout snapshot."); "Current state already matches the requested layout snapshot.");
@@ -211,8 +218,8 @@ UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::RestoreLayou
const UIEditorWorkspaceModel previousWorkspace = m_workspace; const UIEditorWorkspaceModel previousWorkspace = m_workspace;
const UIEditorWorkspaceSession previousSession = m_session; const UIEditorWorkspaceSession previousSession = m_session;
m_workspace = snapshot.workspace; m_workspace = canonicalSnapshot.workspace;
m_session = snapshot.session; m_session = canonicalSnapshot.session;
const UIEditorWorkspaceControllerValidationResult validation = ValidateState(); const UIEditorWorkspaceControllerValidationResult validation = ValidateState();
if (!validation.IsValid()) { if (!validation.IsValid()) {
@@ -441,10 +448,12 @@ UIEditorWorkspaceCommandResult UIEditorWorkspaceController::Dispatch(
UIEditorWorkspaceController BuildDefaultUIEditorWorkspaceController( UIEditorWorkspaceController BuildDefaultUIEditorWorkspaceController(
const UIEditorPanelRegistry& panelRegistry, const UIEditorPanelRegistry& panelRegistry,
const UIEditorWorkspaceModel& workspace) { const UIEditorWorkspaceModel& workspace) {
const UIEditorWorkspaceModel canonicalWorkspace =
CanonicalizeUIEditorWorkspaceModel(workspace);
return UIEditorWorkspaceController( return UIEditorWorkspaceController(
panelRegistry, panelRegistry,
workspace, canonicalWorkspace,
BuildDefaultUIEditorWorkspaceSession(panelRegistry, workspace)); BuildDefaultUIEditorWorkspaceSession(panelRegistry, canonicalWorkspace));
} }
} // namespace XCEngine::UI::Editor } // namespace XCEngine::UI::Editor

View File

@@ -391,7 +391,7 @@ UIEditorWorkspaceLayoutSnapshot BuildUIEditorWorkspaceLayoutSnapshot(
const UIEditorWorkspaceModel& workspace, const UIEditorWorkspaceModel& workspace,
const UIEditorWorkspaceSession& session) { const UIEditorWorkspaceSession& session) {
UIEditorWorkspaceLayoutSnapshot snapshot = {}; UIEditorWorkspaceLayoutSnapshot snapshot = {};
snapshot.workspace = workspace; snapshot.workspace = CanonicalizeUIEditorWorkspaceModel(workspace);
snapshot.session = session; snapshot.session = session;
return snapshot; return snapshot;
} }
@@ -461,6 +461,8 @@ UIEditorWorkspaceLayoutLoadResult DeserializeUIEditorWorkspaceLayoutSnapshot(
return rootResult; return rootResult;
} }
snapshot.workspace = CanonicalizeUIEditorWorkspaceModel(std::move(snapshot.workspace));
snapshot.session.panelStates.clear(); snapshot.session.panelStates.clear();
while (index < lines.size()) { while (index < lines.size()) {
UIEditorPanelSessionState state = {}; UIEditorPanelSessionState state = {};

View File

@@ -22,6 +22,55 @@ bool IsValidSplitRatio(float value) {
return std::isfinite(value) && value > 0.0f && value < 1.0f; return std::isfinite(value) && value > 0.0f && value < 1.0f;
} }
std::string BuildSingleTabPanelNodeId(std::string_view stackNodeId) {
if (stackNodeId.empty()) {
return "single-tab-panel";
}
return std::string(stackNodeId) + "__panel";
}
UIEditorWorkspaceNode WrapStandalonePanelAsTabStack(UIEditorWorkspaceNode panelNode) {
UIEditorWorkspaceNode panelChild = std::move(panelNode);
const std::string stackNodeId = panelChild.nodeId;
panelChild.nodeId = BuildSingleTabPanelNodeId(stackNodeId);
UIEditorWorkspaceNode tabStack = {};
tabStack.kind = UIEditorWorkspaceNodeKind::TabStack;
tabStack.nodeId = stackNodeId;
tabStack.selectedTabIndex = 0u;
tabStack.children.push_back(std::move(panelChild));
return tabStack;
}
void CanonicalizeNodeRecursive(
UIEditorWorkspaceNode& node,
bool allowStandalonePanelLeaf) {
if (node.kind == UIEditorWorkspaceNodeKind::Panel) {
if (!allowStandalonePanelLeaf) {
node = WrapStandalonePanelAsTabStack(std::move(node));
}
return;
}
for (UIEditorWorkspaceNode& child : node.children) {
CanonicalizeNodeRecursive(
child,
node.kind == UIEditorWorkspaceNodeKind::TabStack);
}
if (node.kind == UIEditorWorkspaceNodeKind::TabStack &&
!node.children.empty() &&
node.selectedTabIndex >= node.children.size()) {
node.selectedTabIndex = node.children.size() - 1u;
}
if (node.kind == UIEditorWorkspaceNodeKind::Split &&
node.children.size() == 1u) {
node = std::move(node.children.front());
}
}
const UIEditorPanelDescriptor& RequirePanelDescriptor( const UIEditorPanelDescriptor& RequirePanelDescriptor(
const UIEditorPanelRegistry& registry, const UIEditorPanelRegistry& registry,
std::string_view panelId) { std::string_view panelId) {
@@ -274,7 +323,7 @@ UIEditorWorkspaceModel BuildDefaultEditorShellWorkspaceModel() {
RequirePanelDescriptor(registry, "editor-foundation-root"); RequirePanelDescriptor(registry, "editor-foundation-root");
UIEditorWorkspaceModel workspace = {}; UIEditorWorkspaceModel workspace = {};
workspace.root = BuildUIEditorWorkspacePanel( workspace.root = BuildUIEditorWorkspaceSingleTabStack(
"editor-foundation-root-node", "editor-foundation-root-node",
rootPanel.panelId, rootPanel.panelId,
rootPanel.defaultTitle, rootPanel.defaultTitle,
@@ -297,6 +346,22 @@ UIEditorWorkspaceNode BuildUIEditorWorkspacePanel(
return node; return node;
} }
UIEditorWorkspaceNode BuildUIEditorWorkspaceSingleTabStack(
std::string nodeId,
std::string panelId,
std::string title,
bool placeholder) {
UIEditorWorkspaceNode panel = BuildUIEditorWorkspacePanel(
BuildSingleTabPanelNodeId(nodeId),
std::move(panelId),
std::move(title),
placeholder);
return BuildUIEditorWorkspaceTabStack(
std::move(nodeId),
{ std::move(panel) },
0u);
}
UIEditorWorkspaceNode BuildUIEditorWorkspaceTabStack( UIEditorWorkspaceNode BuildUIEditorWorkspaceTabStack(
std::string nodeId, std::string nodeId,
std::vector<UIEditorWorkspaceNode> panels, std::vector<UIEditorWorkspaceNode> panels,
@@ -345,6 +410,12 @@ UIEditorWorkspaceValidationResult ValidateUIEditorWorkspace(
return {}; return {};
} }
UIEditorWorkspaceModel CanonicalizeUIEditorWorkspaceModel(
UIEditorWorkspaceModel workspace) {
CanonicalizeNodeRecursive(workspace.root, false);
return workspace;
}
std::vector<UIEditorWorkspaceVisiblePanel> CollectUIEditorWorkspaceVisiblePanels( std::vector<UIEditorWorkspaceVisiblePanel> CollectUIEditorWorkspaceVisiblePanels(
const UIEditorWorkspaceModel& workspace) { const UIEditorWorkspaceModel& workspace) {
std::vector<UIEditorWorkspaceVisiblePanel> visiblePanels = {}; std::vector<UIEditorWorkspaceVisiblePanel> visiblePanels = {};

View File

@@ -586,7 +586,7 @@ private:
DrawCard(drawList, m_introRect, "这个测试验证什么功能?", "只验证 DockHost 基础交互 contract不做 editor 业务面板。"); DrawCard(drawList, m_introRect, "这个测试验证什么功能?", "只验证 DockHost 基础交互 contract不做 editor 业务面板。");
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 72.0f), "1. 验证 splitter drag 是否只通过 DockHostInteraction + WorkspaceController 完成。", kTextPrimary, 12.0f); drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 72.0f), "1. 验证 splitter drag 是否只通过 DockHostInteraction + WorkspaceController 完成。", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 94.0f), "2. 验证 tab activate / tab close / standalone panel activate / panel close。", kTextPrimary, 12.0f); drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 94.0f), "2. 验证 unified docktab activate / tab close / single-tab body activate。", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 116.0f), "3. 验证 active panel、visible panels、split ratio 是否统一收口到 controller。", kTextPrimary, 12.0f); drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 116.0f), "3. 验证 active panel、visible panels、split ratio 是否统一收口到 controller。", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 138.0f), "4. 验证 pointer capture / release 请求是否通过 contract 明确返回。", kTextPrimary, 12.0f); drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 138.0f), "4. 验证 pointer capture / release 请求是否通过 contract 明确返回。", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 162.0f), "建议操作:先拖中间 splitter再点 Document A。", kTextWeak, 11.0f); drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 162.0f), "建议操作:先拖中间 splitter再点 Document A。", kTextWeak, 11.0f);

View File

@@ -14,6 +14,7 @@ using XCEngine::UI::UIPoint;
using XCEngine::UI::UIRect; using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession; using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession;
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel; using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit; using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack; using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
using XCEngine::UI::Editor::TryHideUIEditorWorkspacePanel; using XCEngine::UI::Editor::TryHideUIEditorWorkspacePanel;
@@ -62,8 +63,8 @@ UIEditorWorkspaceModel BuildWorkspace() {
"right-split", "right-split",
UIEditorWorkspaceSplitAxis::Vertical, UIEditorWorkspaceSplitAxis::Vertical,
0.6f, 0.6f,
BuildUIEditorWorkspacePanel("details-node", "details", "Details", true), BuildUIEditorWorkspaceSingleTabStack("details-node", "details", "Details", true),
BuildUIEditorWorkspacePanel("console-node", "console", "Console", true))); BuildUIEditorWorkspaceSingleTabStack("console-node", "console", "Console", true)));
workspace.activePanelId = "doc-b"; workspace.activePanelId = "doc-b";
return workspace; return workspace;
} }
@@ -80,7 +81,7 @@ bool ContainsTextCommand(const UIDrawList& drawList, std::string_view text) {
} // namespace } // namespace
TEST(UIEditorDockHostTest, LayoutComposesSplitTabStackAndStandalonePanelsFromWorkspaceTree) { TEST(UIEditorDockHostTest, LayoutComposesOnlyUnifiedTabStacksFromWorkspaceTree) {
const UIEditorPanelRegistry registry = BuildPanelRegistry(); const UIEditorPanelRegistry registry = BuildPanelRegistry();
const UIEditorWorkspaceModel workspace = BuildWorkspace(); const UIEditorWorkspaceModel workspace = BuildWorkspace();
const UIEditorWorkspaceSession session = const UIEditorWorkspaceSession session =
@@ -93,8 +94,7 @@ TEST(UIEditorDockHostTest, LayoutComposesSplitTabStackAndStandalonePanelsFromWor
session); session);
ASSERT_EQ(layout.splitters.size(), 2u); ASSERT_EQ(layout.splitters.size(), 2u);
ASSERT_EQ(layout.tabStacks.size(), 1u); ASSERT_EQ(layout.tabStacks.size(), 3u);
ASSERT_EQ(layout.panels.size(), 2u);
const auto* rootSplitter = FindUIEditorDockHostSplitterLayout(layout, "root-split"); const auto* rootSplitter = FindUIEditorDockHostSplitterLayout(layout, "root-split");
ASSERT_NE(rootSplitter, nullptr); ASSERT_NE(rootSplitter, nullptr);
@@ -109,8 +109,10 @@ TEST(UIEditorDockHostTest, LayoutComposesSplitTabStackAndStandalonePanelsFromWor
EXPECT_EQ(tabStack.items[1].panelId, "doc-b"); EXPECT_EQ(tabStack.items[1].panelId, "doc-b");
EXPECT_EQ(tabStack.tabStripState.selectedIndex, 1u); EXPECT_EQ(tabStack.tabStripState.selectedIndex, 1u);
EXPECT_EQ(layout.panels[0].panelId, "details"); EXPECT_EQ(layout.tabStacks[1].nodeId, "details-node");
EXPECT_EQ(layout.panels[1].panelId, "console"); EXPECT_EQ(layout.tabStacks[1].selectedPanelId, "details");
EXPECT_EQ(layout.tabStacks[2].nodeId, "console-node");
EXPECT_EQ(layout.tabStacks[2].selectedPanelId, "console");
} }
TEST(UIEditorDockHostTest, HiddenBranchCollapsesAndVisibleBranchUsesFullBounds) { TEST(UIEditorDockHostTest, HiddenBranchCollapsesAndVisibleBranchUsesFullBounds) {
@@ -130,7 +132,6 @@ TEST(UIEditorDockHostTest, HiddenBranchCollapsesAndVisibleBranchUsesFullBounds)
EXPECT_TRUE(layout.splitters.empty()); EXPECT_TRUE(layout.splitters.empty());
ASSERT_EQ(layout.tabStacks.size(), 1u); ASSERT_EQ(layout.tabStacks.size(), 1u);
EXPECT_TRUE(layout.panels.empty());
EXPECT_FLOAT_EQ(layout.tabStacks.front().bounds.x, 10.0f); EXPECT_FLOAT_EQ(layout.tabStacks.front().bounds.x, 10.0f);
EXPECT_FLOAT_EQ(layout.tabStacks.front().bounds.y, 20.0f); EXPECT_FLOAT_EQ(layout.tabStacks.front().bounds.y, 20.0f);
EXPECT_FLOAT_EQ(layout.tabStacks.front().bounds.width, 640.0f); EXPECT_FLOAT_EQ(layout.tabStacks.front().bounds.width, 640.0f);
@@ -158,7 +159,7 @@ TEST(UIEditorDockHostTest, HitTestPrioritizesSplitterThenTabCloseThenPanelBody)
EXPECT_EQ(splitterHit.kind, UIEditorDockHostHitTargetKind::SplitterHandle); EXPECT_EQ(splitterHit.kind, UIEditorDockHostHitTargetKind::SplitterHandle);
EXPECT_EQ(splitterHit.nodeId, "root-split"); EXPECT_EQ(splitterHit.nodeId, "root-split");
ASSERT_EQ(layout.tabStacks.size(), 1u); ASSERT_EQ(layout.tabStacks.size(), 3u);
const auto& closeRect = layout.tabStacks.front().tabStripLayout.closeButtonRects[1]; const auto& closeRect = layout.tabStacks.front().tabStripLayout.closeButtonRects[1];
const auto tabCloseHit = HitTestUIEditorDockHost( const auto tabCloseHit = HitTestUIEditorDockHost(
layout, layout,
@@ -209,7 +210,7 @@ TEST(UIEditorDockHostTest, BackgroundAndForegroundEmitStableCompositeCommands) {
EXPECT_GT(foreground.GetCommandCount(), 10u); EXPECT_GT(foreground.GetCommandCount(), 10u);
} }
TEST(UIEditorDockHostTest, ForegroundByDefaultStillDrawsPlaceholderText) { TEST(UIEditorDockHostTest, ForegroundDrawsUnifiedTabTitlesAcrossAllLeafStacks) {
const UIEditorPanelRegistry registry = BuildPanelRegistry(); const UIEditorPanelRegistry registry = BuildPanelRegistry();
const UIEditorWorkspaceModel workspace = BuildWorkspace(); const UIEditorWorkspaceModel workspace = BuildWorkspace();
const UIEditorWorkspaceSession session = const UIEditorWorkspaceSession session =
@@ -224,11 +225,13 @@ TEST(UIEditorDockHostTest, ForegroundByDefaultStillDrawsPlaceholderText) {
UIDrawList foreground("DockHostForegroundDefault"); UIDrawList foreground("DockHostForegroundDefault");
AppendUIEditorDockHostForeground(foreground, layout); AppendUIEditorDockHostForeground(foreground, layout);
EXPECT_TRUE(ContainsTextCommand(foreground, "DockHost tab content placeholder")); EXPECT_TRUE(ContainsTextCommand(foreground, "Document A"));
EXPECT_TRUE(ContainsTextCommand(foreground, "DockHost standalone panel")); EXPECT_TRUE(ContainsTextCommand(foreground, "Document B"));
EXPECT_TRUE(ContainsTextCommand(foreground, "Details"));
EXPECT_TRUE(ContainsTextCommand(foreground, "Console"));
} }
TEST(UIEditorDockHostTest, ForegroundSkipsPlaceholderForExternalBodyPanelId) { TEST(UIEditorDockHostTest, ForegroundWithExternalBodyStillDrawsUnifiedTabTitles) {
const UIEditorPanelRegistry registry = BuildPanelRegistry(); const UIEditorPanelRegistry registry = BuildPanelRegistry();
const UIEditorWorkspaceModel workspace = BuildWorkspace(); const UIEditorWorkspaceModel workspace = BuildWorkspace();
const UIEditorWorkspaceSession session = const UIEditorWorkspaceSession session =
@@ -245,6 +248,8 @@ TEST(UIEditorDockHostTest, ForegroundSkipsPlaceholderForExternalBodyPanelId) {
options.externalBodyPanelIds = { "doc-b" }; options.externalBodyPanelIds = { "doc-b" };
AppendUIEditorDockHostForeground(foreground, layout, options); AppendUIEditorDockHostForeground(foreground, layout, options);
EXPECT_FALSE(ContainsTextCommand(foreground, "DockHost tab content placeholder")); EXPECT_TRUE(ContainsTextCommand(foreground, "Document A"));
EXPECT_TRUE(ContainsTextCommand(foreground, "DockHost standalone panel")); EXPECT_TRUE(ContainsTextCommand(foreground, "Document B"));
EXPECT_TRUE(ContainsTextCommand(foreground, "Details"));
EXPECT_TRUE(ContainsTextCommand(foreground, "Console"));
} }

View File

@@ -15,6 +15,7 @@ using XCEngine::UI::UIPointerButton;
using XCEngine::Input::KeyCode; using XCEngine::Input::KeyCode;
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController; using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController;
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel; using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit; using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack; using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
using XCEngine::UI::Editor::FindUIEditorPanelSessionState; using XCEngine::UI::Editor::FindUIEditorPanelSessionState;
@@ -24,6 +25,8 @@ using XCEngine::UI::Editor::UIEditorWorkspaceModel;
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis; using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
using XCEngine::UI::Editor::UpdateUIEditorDockHostInteraction; using XCEngine::UI::Editor::UpdateUIEditorDockHostInteraction;
using XCEngine::UI::Editor::Widgets::UIEditorDockHostHitTargetKind; using XCEngine::UI::Editor::Widgets::UIEditorDockHostHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorDockHostLayout;
using XCEngine::UI::Editor::Widgets::UIEditorDockHostTabStackLayout;
UIEditorPanelRegistry BuildPanelRegistry() { UIEditorPanelRegistry BuildPanelRegistry() {
UIEditorPanelRegistry registry = {}; UIEditorPanelRegistry registry = {};
@@ -53,8 +56,8 @@ UIEditorWorkspaceModel BuildWorkspace() {
"right-split", "right-split",
UIEditorWorkspaceSplitAxis::Vertical, UIEditorWorkspaceSplitAxis::Vertical,
0.6f, 0.6f,
BuildUIEditorWorkspacePanel("details-node", "details", "Details", true), BuildUIEditorWorkspaceSingleTabStack("details-node", "details", "Details", true),
BuildUIEditorWorkspacePanel("console-node", "console", "Console", true))); BuildUIEditorWorkspaceSingleTabStack("console-node", "console", "Console", true)));
workspace.activePanelId = "doc-b"; workspace.activePanelId = "doc-b";
return workspace; return workspace;
} }
@@ -99,6 +102,18 @@ UIPoint RectCenter(const UIRect& rect) {
return UIPoint(rect.x + rect.width * 0.5f, rect.y + rect.height * 0.5f); return UIPoint(rect.x + rect.width * 0.5f, rect.y + rect.height * 0.5f);
} }
const UIEditorDockHostTabStackLayout* FindTabStackByNodeId(
const UIEditorDockHostLayout& layout,
std::string_view nodeId) {
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
if (tabStack.nodeId == nodeId) {
return &tabStack;
}
}
return nullptr;
}
} // namespace } // namespace
TEST(UIEditorDockHostInteractionTest, SplitterDragUpdatesWorkspaceSplitRatio) { TEST(UIEditorDockHostInteractionTest, SplitterDragUpdatesWorkspaceSplitRatio) {
@@ -182,8 +197,9 @@ TEST(UIEditorDockHostInteractionTest, ClickingTabActivatesTargetPanel) {
controller, controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f), UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{}); {});
ASSERT_EQ(frame.layout.tabStacks.size(), 1u); const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
const UIRect docARect = frame.layout.tabStacks.front().tabStripLayout.tabHeaderRects[0]; ASSERT_NE(documentStack, nullptr);
const UIRect docARect = documentStack->tabStripLayout.tabHeaderRects[0];
frame = UpdateUIEditorDockHostInteraction( frame = UpdateUIEditorDockHostInteraction(
state, state,
@@ -209,8 +225,9 @@ TEST(UIEditorDockHostInteractionTest, ClickingTabActivatesTargetPanel) {
EXPECT_TRUE(frame.result.consumed); EXPECT_TRUE(frame.result.consumed);
EXPECT_TRUE(frame.result.commandExecuted); EXPECT_TRUE(frame.result.commandExecuted);
EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-a"); EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-a");
ASSERT_EQ(frame.layout.tabStacks.size(), 1u); documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
EXPECT_EQ(frame.layout.tabStacks.front().selectedPanelId, "doc-a"); ASSERT_NE(documentStack, nullptr);
EXPECT_EQ(documentStack->selectedPanelId, "doc-a");
} }
TEST(UIEditorDockHostInteractionTest, ClickingTabCloseClosesPanelThroughController) { TEST(UIEditorDockHostInteractionTest, ClickingTabCloseClosesPanelThroughController) {
@@ -223,8 +240,9 @@ TEST(UIEditorDockHostInteractionTest, ClickingTabCloseClosesPanelThroughControll
controller, controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f), UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{}); {});
ASSERT_EQ(frame.layout.tabStacks.size(), 1u); const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
const UIRect closeRect = frame.layout.tabStacks.front().tabStripLayout.closeButtonRects[1]; ASSERT_NE(documentStack, nullptr);
const UIRect closeRect = documentStack->tabStripLayout.closeButtonRects[1];
const UIPoint closeCenter = RectCenter(closeRect); const UIPoint closeCenter = RectCenter(closeRect);
frame = UpdateUIEditorDockHostInteraction( frame = UpdateUIEditorDockHostInteraction(
@@ -243,6 +261,14 @@ TEST(UIEditorDockHostInteractionTest, ClickingTabCloseClosesPanelThroughControll
EXPECT_TRUE(frame.result.consumed); EXPECT_TRUE(frame.result.consumed);
EXPECT_FALSE(frame.result.commandExecuted); EXPECT_FALSE(frame.result.commandExecuted);
frame = UpdateUIEditorDockHostInteraction(
state,
controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{ MakePointerDown(closeCenter.x, closeCenter.y) });
EXPECT_TRUE(frame.result.consumed);
EXPECT_FALSE(frame.result.commandExecuted);
frame = UpdateUIEditorDockHostInteraction( frame = UpdateUIEditorDockHostInteraction(
state, state,
controller, controller,
@@ -268,8 +294,9 @@ TEST(UIEditorDockHostInteractionTest, FocusedTabStripHandlesKeyboardNavigationTh
controller, controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f), UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{}); {});
ASSERT_EQ(frame.layout.tabStacks.size(), 1u); const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
const UIRect docARect = frame.layout.tabStacks.front().tabStripLayout.tabHeaderRects[0]; ASSERT_NE(documentStack, nullptr);
const UIRect docARect = documentStack->tabStripLayout.tabHeaderRects[0];
const UIPoint docACenter = RectCenter(docARect); const UIPoint docACenter = RectCenter(docARect);
frame = UpdateUIEditorDockHostInteraction( frame = UpdateUIEditorDockHostInteraction(
@@ -302,8 +329,9 @@ TEST(UIEditorDockHostInteractionTest, FocusedTabStripHandlesKeyboardNavigationTh
EXPECT_TRUE(frame.result.consumed); EXPECT_TRUE(frame.result.consumed);
EXPECT_TRUE(frame.result.commandExecuted); EXPECT_TRUE(frame.result.commandExecuted);
EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-b"); EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-b");
ASSERT_EQ(frame.layout.tabStacks.size(), 1u); documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
EXPECT_EQ(frame.layout.tabStacks.front().selectedPanelId, "doc-b"); ASSERT_NE(documentStack, nullptr);
EXPECT_EQ(documentStack->selectedPanelId, "doc-b");
} }
TEST(UIEditorDockHostInteractionTest, BatchedPointerMoveDownUpActivatesTabInSameUpdateCall) { TEST(UIEditorDockHostInteractionTest, BatchedPointerMoveDownUpActivatesTabInSameUpdateCall) {
@@ -316,9 +344,10 @@ TEST(UIEditorDockHostInteractionTest, BatchedPointerMoveDownUpActivatesTabInSame
controller, controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f), UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{}); {});
ASSERT_EQ(frame.layout.tabStacks.size(), 1u); const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
ASSERT_NE(documentStack, nullptr);
const UIPoint docACenter = const UIPoint docACenter =
RectCenter(frame.layout.tabStacks.front().tabStripLayout.tabHeaderRects[0]); RectCenter(documentStack->tabStripLayout.tabHeaderRects[0]);
frame = UpdateUIEditorDockHostInteraction( frame = UpdateUIEditorDockHostInteraction(
state, state,
@@ -332,11 +361,12 @@ TEST(UIEditorDockHostInteractionTest, BatchedPointerMoveDownUpActivatesTabInSame
EXPECT_TRUE(frame.result.consumed); EXPECT_TRUE(frame.result.consumed);
EXPECT_TRUE(frame.result.commandExecuted); EXPECT_TRUE(frame.result.commandExecuted);
EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-a"); EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-a");
ASSERT_EQ(frame.layout.tabStacks.size(), 1u); documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
EXPECT_EQ(frame.layout.tabStacks.front().selectedPanelId, "doc-a"); ASSERT_NE(documentStack, nullptr);
EXPECT_EQ(documentStack->selectedPanelId, "doc-a");
} }
TEST(UIEditorDockHostInteractionTest, ClickingStandalonePanelBodyActivatesTargetPanel) { TEST(UIEditorDockHostInteractionTest, ClickingSingleTabStackBodyActivatesTargetPanel) {
auto controller = auto controller =
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace()); BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
UIEditorDockHostInteractionState state = {}; UIEditorDockHostInteractionState state = {};
@@ -346,8 +376,9 @@ TEST(UIEditorDockHostInteractionTest, ClickingStandalonePanelBodyActivatesTarget
controller, controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f), UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{}); {});
ASSERT_EQ(frame.layout.panels.size(), 2u); const auto* detailsStack = FindTabStackByNodeId(frame.layout, "details-node");
const UIRect detailsBodyRect = frame.layout.panels[0].frameLayout.bodyRect; ASSERT_NE(detailsStack, nullptr);
const UIRect detailsBodyRect = detailsStack->contentFrameLayout.bodyRect;
const UIPoint detailsBodyCenter = RectCenter(detailsBodyRect); const UIPoint detailsBodyCenter = RectCenter(detailsBodyRect);
frame = UpdateUIEditorDockHostInteraction( frame = UpdateUIEditorDockHostInteraction(
@@ -368,7 +399,7 @@ TEST(UIEditorDockHostInteractionTest, ClickingStandalonePanelBodyActivatesTarget
EXPECT_EQ(controller.GetWorkspace().activePanelId, "details"); EXPECT_EQ(controller.GetWorkspace().activePanelId, "details");
} }
TEST(UIEditorDockHostInteractionTest, ClickingStandalonePanelCloseClosesPanelThroughController) { TEST(UIEditorDockHostInteractionTest, ClickingSingleTabStackTabCloseClosesPanelThroughController) {
auto controller = auto controller =
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace()); BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
UIEditorDockHostInteractionState state = {}; UIEditorDockHostInteractionState state = {};
@@ -378,8 +409,9 @@ TEST(UIEditorDockHostInteractionTest, ClickingStandalonePanelCloseClosesPanelThr
controller, controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f), UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{}); {});
ASSERT_EQ(frame.layout.panels.size(), 2u); const auto* consoleStack = FindTabStackByNodeId(frame.layout, "console-node");
const UIRect closeRect = frame.layout.panels[1].frameLayout.closeButtonRect; ASSERT_NE(consoleStack, nullptr);
const UIRect closeRect = consoleStack->tabStripLayout.closeButtonRects[0];
const UIPoint closeCenter = RectCenter(closeRect); const UIPoint closeCenter = RectCenter(closeRect);
frame = UpdateUIEditorDockHostInteraction( frame = UpdateUIEditorDockHostInteraction(
@@ -387,7 +419,7 @@ TEST(UIEditorDockHostInteractionTest, ClickingStandalonePanelCloseClosesPanelThr
controller, controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f), UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{ MakePointerMove(closeCenter.x, closeCenter.y) }); { MakePointerMove(closeCenter.x, closeCenter.y) });
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorDockHostHitTargetKind::PanelCloseButton); EXPECT_EQ(frame.result.hitTarget.kind, UIEditorDockHostHitTargetKind::TabCloseButton);
EXPECT_EQ(frame.result.hitTarget.panelId, "console"); EXPECT_EQ(frame.result.hitTarget.panelId, "console");
frame = UpdateUIEditorDockHostInteraction( frame = UpdateUIEditorDockHostInteraction(

View File

@@ -9,6 +9,7 @@ namespace {
using XCEngine::UI::UIRect; using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession; using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession;
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel; using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit; using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack; using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
using XCEngine::UI::Editor::CollectMountedUIEditorPanelContentHostPanelIds; using XCEngine::UI::Editor::CollectMountedUIEditorPanelContentHostPanelIds;
@@ -50,7 +51,7 @@ UIEditorWorkspaceModel BuildWorkspace() {
BuildUIEditorWorkspacePanel("console-node", "console", "Console", true) BuildUIEditorWorkspacePanel("console-node", "console", "Console", true)
}, },
1u), 1u),
BuildUIEditorWorkspacePanel("inspector-node", "inspector", "Inspector")); BuildUIEditorWorkspaceSingleTabStack("inspector-node", "inspector", "Inspector"));
workspace.activePanelId = "doc-b"; workspace.activePanelId = "doc-b";
return workspace; return workspace;
} }

View File

@@ -14,6 +14,7 @@ using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController;
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession; using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceLayoutSnapshot; using XCEngine::UI::Editor::BuildUIEditorWorkspaceLayoutSnapshot;
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel; using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit; using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack; using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
using XCEngine::UI::Editor::DeserializeUIEditorWorkspaceLayoutSnapshot; using XCEngine::UI::Editor::DeserializeUIEditorWorkspaceLayoutSnapshot;
@@ -28,6 +29,7 @@ using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind;
using XCEngine::UI::Editor::UIEditorWorkspaceCommandStatus; using XCEngine::UI::Editor::UIEditorWorkspaceCommandStatus;
using XCEngine::UI::Editor::UIEditorWorkspaceModel; using XCEngine::UI::Editor::UIEditorWorkspaceModel;
using XCEngine::UI::Editor::UIEditorWorkspaceSession; using XCEngine::UI::Editor::UIEditorWorkspaceSession;
using XCEngine::UI::Editor::UIEditorWorkspaceNodeKind;
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis; using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
UIEditorPanelRegistry BuildPanelRegistry() { UIEditorPanelRegistry BuildPanelRegistry() {
@@ -53,7 +55,7 @@ UIEditorWorkspaceModel BuildWorkspace() {
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true) BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
}, },
0u), 0u),
BuildUIEditorWorkspacePanel("details-node", "details", "Details", true)); BuildUIEditorWorkspaceSingleTabStack("details-node", "details", "Details", true));
workspace.activePanelId = "doc-a"; workspace.activePanelId = "doc-a";
return workspace; return workspace;
} }
@@ -128,6 +130,35 @@ TEST(UIEditorWorkspaceLayoutPersistenceTest, DeserializeRejectsMissingSessionRec
EXPECT_EQ(loadResult.code, UIEditorWorkspaceLayoutLoadCode::InvalidWorkspaceSession); EXPECT_EQ(loadResult.code, UIEditorWorkspaceLayoutLoadCode::InvalidWorkspaceSession);
} }
TEST(UIEditorWorkspaceLayoutPersistenceTest, DeserializeUpgradesLegacyStandalonePanelLeaves) {
const UIEditorPanelRegistry registry = BuildPanelRegistry();
const std::string legacySerialized =
"XCUI_EDITOR_WORKSPACE_LAYOUT 1\n"
"active \"doc-a\"\n"
"node_split \"root-split\" \"horizontal\" 0.66\n"
"node_tabstack \"document-tabs\" 0 2\n"
"node_panel \"doc-a-node\" \"doc-a\" \"Document A\" 1\n"
"node_panel \"doc-b-node\" \"doc-b\" \"Document B\" 1\n"
"node_panel \"details-node\" \"details\" \"Details\" 1\n"
"session \"doc-a\" 1 1\n"
"session \"doc-b\" 1 1\n"
"session \"details\" 1 1\n";
const auto loadResult =
DeserializeUIEditorWorkspaceLayoutSnapshot(registry, legacySerialized);
ASSERT_TRUE(loadResult.IsValid()) << loadResult.message;
ASSERT_EQ(loadResult.snapshot.workspace.root.kind, UIEditorWorkspaceNodeKind::Split);
ASSERT_EQ(loadResult.snapshot.workspace.root.children.size(), 2u);
EXPECT_EQ(
loadResult.snapshot.workspace.root.children[1].kind,
UIEditorWorkspaceNodeKind::TabStack);
ASSERT_EQ(loadResult.snapshot.workspace.root.children[1].children.size(), 1u);
EXPECT_EQ(
loadResult.snapshot.workspace.root.children[1].children[0].panel.panelId,
"details");
}
TEST(UIEditorWorkspaceLayoutPersistenceTest, RestoreSerializedLayoutRestoresSavedStateAfterFurtherMutations) { TEST(UIEditorWorkspaceLayoutPersistenceTest, RestoreSerializedLayoutRestoresSavedStateAfterFurtherMutations) {
const UIEditorPanelRegistry registry = BuildPanelRegistry(); const UIEditorPanelRegistry registry = BuildPanelRegistry();
UIEditorWorkspaceController controller = UIEditorWorkspaceController controller =

View File

@@ -9,8 +9,10 @@
namespace { namespace {
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel; using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit; using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack; using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
using XCEngine::UI::Editor::CanonicalizeUIEditorWorkspaceModel;
using XCEngine::UI::Editor::CollectUIEditorWorkspaceVisiblePanels; using XCEngine::UI::Editor::CollectUIEditorWorkspaceVisiblePanels;
using XCEngine::UI::Editor::ContainsUIEditorWorkspacePanel; using XCEngine::UI::Editor::ContainsUIEditorWorkspacePanel;
using XCEngine::UI::Editor::FindUIEditorWorkspaceActivePanel; using XCEngine::UI::Editor::FindUIEditorWorkspaceActivePanel;
@@ -73,7 +75,7 @@ TEST(UIEditorWorkspaceModelTest, VisiblePanelsOnlyIncludeSelectedTabsAcrossSplit
"root-split", "root-split",
UIEditorWorkspaceSplitAxis::Horizontal, UIEditorWorkspaceSplitAxis::Horizontal,
0.68f, 0.68f,
BuildUIEditorWorkspacePanel("left-panel-node", "left-panel", "Left Panel", true), BuildUIEditorWorkspaceSingleTabStack("left-panel-node", "left-panel", "Left Panel", true),
BuildUIEditorWorkspaceSplit( BuildUIEditorWorkspaceSplit(
"right-split", "right-split",
UIEditorWorkspaceSplitAxis::Vertical, UIEditorWorkspaceSplitAxis::Vertical,
@@ -85,7 +87,11 @@ TEST(UIEditorWorkspaceModelTest, VisiblePanelsOnlyIncludeSelectedTabsAcrossSplit
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true) BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
}, },
1u), 1u),
BuildUIEditorWorkspacePanel("bottom-panel-node", "bottom-panel", "Bottom Panel", true))); BuildUIEditorWorkspaceSingleTabStack(
"bottom-panel-node",
"bottom-panel",
"Bottom Panel",
true)));
workspace.activePanelId = "doc-b"; workspace.activePanelId = "doc-b";
const auto validation = ValidateUIEditorWorkspace(workspace); const auto validation = ValidateUIEditorWorkspace(workspace);
@@ -116,7 +122,7 @@ TEST(UIEditorWorkspaceModelTest, ActivatingHiddenPanelSelectsContainingTabAndUpd
BuildUIEditorWorkspacePanel("doc-c-node", "doc-c", "Document C", true) BuildUIEditorWorkspacePanel("doc-c-node", "doc-c", "Document C", true)
}, },
0u), 0u),
BuildUIEditorWorkspacePanel("details-node", "details", "Details", true)); BuildUIEditorWorkspaceSingleTabStack("details-node", "details", "Details", true));
ASSERT_TRUE(ContainsUIEditorWorkspacePanel(workspace, "doc-b")); ASSERT_TRUE(ContainsUIEditorWorkspacePanel(workspace, "doc-b"));
ASSERT_TRUE(TryActivateUIEditorWorkspacePanel(workspace, "doc-b")); ASSERT_TRUE(TryActivateUIEditorWorkspacePanel(workspace, "doc-b"));
@@ -150,8 +156,8 @@ TEST(UIEditorWorkspaceModelTest, SplitRatioMutationTargetsSplitNodeAndRejectsInv
"root-split", "root-split",
UIEditorWorkspaceSplitAxis::Horizontal, UIEditorWorkspaceSplitAxis::Horizontal,
0.62f, 0.62f,
BuildUIEditorWorkspacePanel("left-node", "left", "Left", true), BuildUIEditorWorkspaceSingleTabStack("left-node", "left", "Left", true),
BuildUIEditorWorkspacePanel("right-node", "right", "Right", true)); BuildUIEditorWorkspaceSingleTabStack("right-node", "right", "Right", true));
ASSERT_TRUE(TrySetUIEditorWorkspaceSplitRatio(workspace, "root-split", 0.35f)); ASSERT_TRUE(TrySetUIEditorWorkspaceSplitRatio(workspace, "root-split", 0.35f));
const auto* splitNode = FindUIEditorWorkspaceNode(workspace, "root-split"); const auto* splitNode = FindUIEditorWorkspaceNode(workspace, "root-split");
@@ -163,3 +169,29 @@ TEST(UIEditorWorkspaceModelTest, SplitRatioMutationTargetsSplitNodeAndRejectsInv
EXPECT_FALSE(TrySetUIEditorWorkspaceSplitRatio(workspace, "missing", 0.5f)); EXPECT_FALSE(TrySetUIEditorWorkspaceSplitRatio(workspace, "missing", 0.5f));
EXPECT_FALSE(TrySetUIEditorWorkspaceSplitRatio(workspace, "root-split", 1.0f)); EXPECT_FALSE(TrySetUIEditorWorkspaceSplitRatio(workspace, "root-split", 1.0f));
} }
TEST(UIEditorWorkspaceModelTest, CanonicalizeWrapsStandalonePanelsIntoSingleTabStacks) {
UIEditorWorkspaceModel workspace = {};
workspace.root = BuildUIEditorWorkspaceSplit(
"root-split",
UIEditorWorkspaceSplitAxis::Horizontal,
0.5f,
BuildUIEditorWorkspacePanel("left-node", "left", "Left", true),
BuildUIEditorWorkspacePanel("right-node", "right", "Right", true));
const UIEditorWorkspaceModel canonicalWorkspace =
CanonicalizeUIEditorWorkspaceModel(workspace);
ASSERT_EQ(canonicalWorkspace.root.kind, UIEditorWorkspaceNodeKind::Split);
ASSERT_EQ(canonicalWorkspace.root.children.size(), 2u);
EXPECT_EQ(canonicalWorkspace.root.children[0].kind, UIEditorWorkspaceNodeKind::TabStack);
EXPECT_EQ(canonicalWorkspace.root.children[0].nodeId, "left-node");
ASSERT_EQ(canonicalWorkspace.root.children[0].children.size(), 1u);
EXPECT_EQ(
canonicalWorkspace.root.children[0].children[0].kind,
UIEditorWorkspaceNodeKind::Panel);
EXPECT_EQ(
canonicalWorkspace.root.children[0].children[0].panel.panelId,
"left");
EXPECT_EQ(canonicalWorkspace.root.children[1].kind, UIEditorWorkspaceNodeKind::TabStack);
}