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",
UIEditorWorkspaceSplitAxis::Horizontal,
0.15f,
BuildUIEditorWorkspacePanel(
BuildUIEditorWorkspaceSingleTabStack(
"hierarchy-panel",
"hierarchy",
"Hierarchy",
@@ -64,7 +64,7 @@ UIEditorWorkspaceModel BuildWorkspace() {
false)
},
0u),
BuildUIEditorWorkspacePanel(
BuildUIEditorWorkspaceSingleTabStack(
"inspector-panel",
"inspector",
"Inspector",

View File

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

View File

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

View File

@@ -23,9 +23,6 @@ using ::XCEngine::UI::Layout::UITabStripMeasureItem;
using ::XCEngine::UI::Layout::UILayoutAxis;
using ::XCEngine::UI::Widgets::ExpandUISplitterHandleHitRect;
constexpr std::string_view kStandalonePanelActiveFooter = "";
constexpr std::string_view kStandalonePanelInactiveFooter = "";
struct DockMeasureResult {
bool visible = false;
UISize minimumSize = {};
@@ -102,17 +99,6 @@ UIEditorPanelFrameMetrics BuildTabContentFrameMetrics(
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(
const UIEditorDockHostMetrics& metrics) {
const UIEditorPanelFrameMetrics frameMetrics = BuildTabContentFrameMetrics(metrics);
@@ -205,14 +191,8 @@ DockMeasureResult MeasureNodeRecursive(
const UIEditorWorkspaceSession& session,
const UIEditorDockHostMetrics& metrics) {
switch (node.kind) {
case UIEditorWorkspaceNodeKind::Panel: {
DockMeasureResult result = {};
result.visible = IsPanelOpenAndVisible(session, node.panel.panelId);
if (result.visible) {
result.minimumSize = MeasurePanelMinimumSize(metrics);
}
return result;
}
case UIEditorWorkspaceNodeKind::Panel:
return {};
case UIEditorWorkspaceNodeKind::TabStack:
return MeasureTabStackNode(node, panelRegistry, session, metrics);
case UIEditorWorkspaceNodeKind::Split:
@@ -269,38 +249,6 @@ UIEditorTabStripState BuildTabStripState(
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(
const UIEditorDockHostState& state,
const UIEditorWorkspaceModel& workspace,
@@ -338,29 +286,6 @@ void LayoutNodeRecursive(
const UIEditorDockHostMetrics& metrics,
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(
const UIEditorWorkspaceNode& node,
const UIRect& bounds,
@@ -525,9 +450,6 @@ void LayoutNodeRecursive(
UIEditorDockHostLayout& layout) {
switch (node.kind) {
case UIEditorWorkspaceNodeKind::Panel:
if (IsPanelOpenAndVisible(session, node.panel.panelId)) {
LayoutPanelNode(node, bounds, panelRegistry, workspace, state, metrics, layout);
}
return;
case UIEditorWorkspaceNodeKind::TabStack:
LayoutTabStackNode(node, bounds, panelRegistry, workspace, session, state, metrics, layout);
@@ -606,11 +528,13 @@ UIEditorDockHostLayout BuildUIEditorDockHostLayout(
bounds.y,
ClampNonNegative(bounds.width),
ClampNonNegative(bounds.height));
const UIEditorWorkspaceModel canonicalWorkspace =
CanonicalizeUIEditorWorkspaceModel(workspace);
LayoutNodeRecursive(
workspace.root,
canonicalWorkspace.root,
layout.bounds,
panelRegistry,
workspace,
canonicalWorkspace,
session,
state,
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 {};
}
@@ -694,15 +607,6 @@ void AppendUIEditorDockHostBackground(
const UIEditorDockHostLayout& layout,
const UIEditorDockHostPalette& palette,
const UIEditorDockHostMetrics& metrics) {
for (const UIEditorDockHostPanelLayout& panel : layout.panels) {
AppendUIEditorPanelFrameBackground(
drawList,
panel.frameLayout,
panel.frameState,
palette.panelFramePalette,
metrics.panelFrameMetrics);
}
const UIEditorPanelFrameMetrics tabContentFrameMetrics =
BuildTabContentFrameMetrics(metrics);
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
@@ -744,23 +648,6 @@ void AppendUIEditorDockHostForeground(
const UIEditorDockHostForegroundOptions& options,
const UIEditorDockHostPalette& palette,
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 =
BuildTabContentFrameMetrics(metrics);
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {

View File

@@ -12,12 +12,6 @@ namespace {
const ::XCEngine::UI::UIRect* FindVisiblePanelBodyRect(
const Widgets::UIEditorDockHostLayout& layout,
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) {
if (tabStack.selectedPanelId == panelId) {
return &tabStack.contentFrameLayout.bodyRect;

View File

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

View File

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

View File

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