refactor(new_editor/docking): split dock host rendering and hit testing

This commit is contained in:
2026-04-15 11:18:48 +08:00
parent 701ddd9e9d
commit 1c84520738
4 changed files with 328 additions and 277 deletions

View File

@@ -67,6 +67,8 @@ set(XCUI_EDITOR_COLLECTION_SOURCES
set(XCUI_EDITOR_DOCKING_SOURCES
src/Docking/UIEditorDockHost.cpp
src/Docking/UIEditorDockHostHitTest.cpp
src/Docking/UIEditorDockHostRendering.cpp
src/Docking/UIEditorDockHostInteraction.cpp
)

View File

@@ -3,7 +3,6 @@
#include <XCEngine/UI/Widgets/UISplitterInteraction.h>
#include <algorithm>
#include <string>
#include <utility>
#include <vector>
@@ -11,9 +10,6 @@ namespace XCEngine::UI::Editor::Widgets {
namespace {
using ::XCEngine::UI::UIColor;
using ::XCEngine::UI::UIDrawList;
using ::XCEngine::UI::UIPoint;
using ::XCEngine::UI::UIRect;
using ::XCEngine::UI::UISize;
using ::XCEngine::UI::Layout::ArrangeUISplitter;
@@ -42,36 +38,6 @@ float GetMainExtent(const UISize& size, UIEditorWorkspaceSplitAxis axis) {
return axis == UIEditorWorkspaceSplitAxis::Horizontal ? size.width : size.height;
}
bool IsPointInsideRect(
const UIRect& rect,
const UIPoint& point) {
return point.x >= rect.x &&
point.x <= rect.x + rect.width &&
point.y >= rect.y &&
point.y <= rect.y + rect.height;
}
bool UsesExternalBodyPresentation(
const UIEditorDockHostForegroundOptions& options,
std::string_view panelId) {
return std::find(
options.externalBodyPanelIds.begin(),
options.externalBodyPanelIds.end(),
panelId) != options.externalBodyPanelIds.end();
}
const UIEditorDockHostTabStripVisualState* FindTabStripVisualState(
const UIEditorDockHostState& state,
std::string_view nodeId) {
for (const UIEditorDockHostTabStripVisualState& entry : state.tabStripStates) {
if (entry.nodeId == nodeId) {
return &entry;
}
}
return nullptr;
}
bool IsPanelOpenAndVisible(
const UIEditorWorkspaceSession& session,
std::string_view panelId) {
@@ -146,7 +112,8 @@ DockMeasureResult MeasureTabStackNode(
return {};
}
const auto measured = MeasureUITabStrip(measureItems, metrics.tabStripMetrics.layoutMetrics);
const auto measured =
MeasureUITabStrip(measureItems, metrics.tabStripMetrics.layoutMetrics);
DockMeasureResult result = {};
result.visible = true;
result.minimumSize = measured.minimumSize;
@@ -218,6 +185,18 @@ std::size_t ResolveSelectedVisibleTabIndex(
return 0u;
}
const UIEditorDockHostTabStripVisualState* FindTabStripVisualState(
const UIEditorDockHostState& state,
std::string_view nodeId) {
for (const UIEditorDockHostTabStripVisualState& entry : state.tabStripStates) {
if (entry.nodeId == nodeId) {
return &entry;
}
}
return nullptr;
}
UIEditorTabStripState BuildTabStripState(
const UIEditorDockHostState& state,
std::string_view nodeId,
@@ -230,7 +209,8 @@ UIEditorTabStripState BuildTabStripState(
}
tabState.selectedIndex = selectedIndex;
if (FindTabStripVisualState(state, nodeId) != nullptr || state.hoveredTarget.nodeId != nodeId) {
if (FindTabStripVisualState(state, nodeId) != nullptr ||
state.hoveredTarget.nodeId != nodeId) {
return tabState;
}
@@ -338,10 +318,15 @@ void LayoutTabStackNode(
tabStackLayout.items[selectedVisibleIndex].panelId;
tabStackLayout.tabStripState =
BuildTabStripState(state, node.nodeId, selectedVisibleIndex);
tabStackLayout.tabStripLayout =
BuildUIEditorTabStripLayout(bounds, tabStripItems, tabStackLayout.tabStripState, metrics.tabStripMetrics);
tabStackLayout.contentFrameState =
BuildTabContentFrameState(state, workspace, tabStackLayout.selectedPanelId);
tabStackLayout.tabStripLayout = BuildUIEditorTabStripLayout(
bounds,
tabStripItems,
tabStackLayout.tabStripState,
metrics.tabStripMetrics);
tabStackLayout.contentFrameState = BuildTabContentFrameState(
state,
workspace,
tabStackLayout.selectedPanelId);
tabStackLayout.contentFrameLayout = BuildUIEditorPanelFrameLayout(
tabStackLayout.tabStripLayout.contentRect,
tabStackLayout.contentFrameState,
@@ -412,7 +397,10 @@ void LayoutSplitNode(
splitterLayout.handleHitRect = ExpandUISplitterHandleHitRect(
splitterLayout.splitterLayout.handleRect,
ToLayoutAxis(node.splitAxis),
(std::max)(0.0f, (splitterLayout.metrics.hitThickness - splitterLayout.metrics.thickness) * 0.5f));
(std::max)(
0.0f,
(splitterLayout.metrics.hitThickness -
splitterLayout.metrics.thickness) * 0.5f));
splitterLayout.hovered =
state.hoveredTarget.kind == UIEditorDockHostHitTargetKind::SplitterHandle &&
state.hoveredTarget.nodeId == node.nodeId;
@@ -452,55 +440,30 @@ void LayoutNodeRecursive(
case UIEditorWorkspaceNodeKind::Panel:
return;
case UIEditorWorkspaceNodeKind::TabStack:
LayoutTabStackNode(node, bounds, panelRegistry, workspace, session, state, metrics, layout);
LayoutTabStackNode(
node,
bounds,
panelRegistry,
workspace,
session,
state,
metrics,
layout);
return;
case UIEditorWorkspaceNodeKind::Split:
LayoutSplitNode(node, bounds, panelRegistry, workspace, session, state, metrics, layout);
LayoutSplitNode(
node,
bounds,
panelRegistry,
workspace,
session,
state,
metrics,
layout);
return;
}
}
UIEditorDockHostHitTarget MakePanelHitTarget(
UIEditorDockHostHitTargetKind kind,
std::string_view nodeId,
std::string_view panelId) {
UIEditorDockHostHitTarget target = {};
target.kind = kind;
target.nodeId = std::string(nodeId);
target.panelId = std::string(panelId);
return target;
}
UIEditorDockHostHitTarget MapPanelFrameHitTarget(
UIEditorPanelFrameHitTarget hitTarget,
std::string_view nodeId,
std::string_view panelId) {
switch (hitTarget) {
case UIEditorPanelFrameHitTarget::Header:
return MakePanelHitTarget(UIEditorDockHostHitTargetKind::PanelHeader, nodeId, panelId);
case UIEditorPanelFrameHitTarget::Body:
return MakePanelHitTarget(UIEditorDockHostHitTargetKind::PanelBody, nodeId, panelId);
case UIEditorPanelFrameHitTarget::Footer:
return MakePanelHitTarget(UIEditorDockHostHitTargetKind::PanelFooter, nodeId, panelId);
case UIEditorPanelFrameHitTarget::CloseButton:
return MakePanelHitTarget(UIEditorDockHostHitTargetKind::PanelCloseButton, nodeId, panelId);
default:
return {};
}
}
UIColor ResolveSplitterColor(const UIEditorDockHostSplitterLayout& splitter, const UIEditorDockHostPalette& palette) {
if (splitter.active) {
return palette.splitterActiveColor;
}
if (splitter.hovered) {
return palette.splitterHoveredColor;
}
return palette.splitterColor;
}
const UIEditorDockHostTabStackLayout* FindTabStackLayoutByNodeId(
const UIEditorDockHostLayout& layout,
std::string_view nodeId) {
@@ -530,11 +493,7 @@ UIRect ResolveDropPreviewRect(
const UIRect bounds = tabStack.bounds;
switch (placement) {
case UIEditorWorkspaceDockPlacement::Left:
return UIRect(
bounds.x,
bounds.y,
bounds.width * 0.35f,
bounds.height);
return UIRect(bounds.x, bounds.y, bounds.width * 0.35f, bounds.height);
case UIEditorWorkspaceDockPlacement::Right:
return UIRect(
bounds.x + bounds.width * 0.65f,
@@ -542,11 +501,7 @@ UIRect ResolveDropPreviewRect(
bounds.width * 0.35f,
bounds.height);
case UIEditorWorkspaceDockPlacement::Top:
return UIRect(
bounds.x,
bounds.y,
bounds.width,
bounds.height * 0.35f);
return UIRect(bounds.x, bounds.y, bounds.width, bounds.height * 0.35f);
case UIEditorWorkspaceDockPlacement::Bottom:
return UIRect(
bounds.x,
@@ -653,187 +608,4 @@ UIEditorDockHostLayout BuildUIEditorDockHostLayout(
return layout;
}
UIEditorDockHostHitTarget HitTestUIEditorDockHost(
const UIEditorDockHostLayout& layout,
const UIPoint& point) {
for (std::size_t index = layout.splitters.size(); index > 0u; --index) {
const UIEditorDockHostSplitterLayout& splitter = layout.splitters[index - 1u];
if (IsPointInsideRect(splitter.handleHitRect, point)) {
UIEditorDockHostHitTarget target = {};
target.kind = UIEditorDockHostHitTargetKind::SplitterHandle;
target.nodeId = splitter.nodeId;
return target;
}
}
for (std::size_t index = layout.tabStacks.size(); index > 0u; --index) {
const UIEditorDockHostTabStackLayout& tabStack = layout.tabStacks[index - 1u];
const UIEditorTabStripHitTarget tabHit =
HitTestUIEditorTabStrip(tabStack.tabStripLayout, tabStack.tabStripState, point);
switch (tabHit.kind) {
case UIEditorTabStripHitTargetKind::CloseButton: {
UIEditorDockHostHitTarget target = {};
target.kind = UIEditorDockHostHitTargetKind::TabCloseButton;
target.nodeId = tabStack.nodeId;
target.index = tabHit.index;
if (tabHit.index < tabStack.items.size()) {
target.panelId = tabStack.items[tabHit.index].panelId;
}
return target;
}
case UIEditorTabStripHitTargetKind::Tab: {
UIEditorDockHostHitTarget target = {};
target.kind = UIEditorDockHostHitTargetKind::Tab;
target.nodeId = tabStack.nodeId;
target.index = tabHit.index;
if (tabHit.index < tabStack.items.size()) {
target.panelId = tabStack.items[tabHit.index].panelId;
}
return target;
}
case UIEditorTabStripHitTargetKind::HeaderBackground: {
UIEditorDockHostHitTarget target = {};
target.kind = UIEditorDockHostHitTargetKind::TabStripBackground;
target.nodeId = tabStack.nodeId;
return target;
}
default:
break;
}
const UIEditorPanelFrameHitTarget panelHit = HitTestUIEditorPanelFrame(
tabStack.contentFrameLayout,
tabStack.contentFrameState,
point);
if (panelHit != UIEditorPanelFrameHitTarget::None) {
return MapPanelFrameHitTarget(panelHit, tabStack.nodeId, tabStack.selectedPanelId);
}
}
return {};
}
void AppendUIEditorDockHostBackground(
UIDrawList& drawList,
const UIEditorDockHostLayout& layout,
const UIEditorDockHostPalette& palette,
const UIEditorDockHostMetrics& metrics) {
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
std::vector<UIEditorTabStripItem> tabItems = {};
tabItems.reserve(tabStack.items.size());
for (const UIEditorDockHostTabItemLayout& item : tabStack.items) {
UIEditorTabStripItem tabItem = {};
tabItem.tabId = item.panelId;
tabItem.title = item.title;
tabItem.closable = item.closable;
tabItems.push_back(std::move(tabItem));
}
AppendUIEditorTabStripBackground(
drawList,
tabStack.tabStripLayout,
tabStack.tabStripState,
palette.tabStripPalette,
metrics.tabStripMetrics);
}
for (const UIEditorDockHostSplitterLayout& splitter : layout.splitters) {
drawList.AddFilledRect(
splitter.splitterLayout.handleRect,
ResolveSplitterColor(splitter, palette),
metrics.splitterHandleRounding);
}
}
void AppendUIEditorDockHostForeground(
UIDrawList& drawList,
const UIEditorDockHostLayout& layout,
const UIEditorDockHostForegroundOptions& options,
const UIEditorDockHostPalette& palette,
const UIEditorDockHostMetrics& metrics) {
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
std::vector<UIEditorTabStripItem> tabItems = {};
tabItems.reserve(tabStack.items.size());
for (const UIEditorDockHostTabItemLayout& item : tabStack.items) {
UIEditorTabStripItem tabItem = {};
tabItem.tabId = item.panelId;
tabItem.title = item.title;
tabItem.closable = item.closable;
tabItems.push_back(std::move(tabItem));
}
AppendUIEditorTabStripForeground(
drawList,
tabStack.tabStripLayout,
tabItems,
tabStack.tabStripState,
palette.tabStripPalette,
metrics.tabStripMetrics);
if (UsesExternalBodyPresentation(options, tabStack.selectedPanelId)) {
continue;
}
}
if (layout.dropPreview.visible) {
drawList.AddFilledRect(
layout.dropPreview.previewRect,
palette.dropPreviewFillColor);
drawList.AddRectOutline(
layout.dropPreview.previewRect,
palette.dropPreviewBorderColor,
1.0f);
}
}
void AppendUIEditorDockHostForeground(
UIDrawList& drawList,
const UIEditorDockHostLayout& layout,
const UIEditorDockHostPalette& palette,
const UIEditorDockHostMetrics& metrics) {
AppendUIEditorDockHostForeground(
drawList,
layout,
UIEditorDockHostForegroundOptions{},
palette,
metrics);
}
void AppendUIEditorDockHost(
UIDrawList& drawList,
const UIRect& bounds,
const UIEditorPanelRegistry& panelRegistry,
const UIEditorWorkspaceModel& workspace,
const UIEditorWorkspaceSession& session,
const UIEditorDockHostState& state,
const UIEditorDockHostForegroundOptions& foregroundOptions,
const UIEditorDockHostPalette& palette,
const UIEditorDockHostMetrics& metrics) {
const UIEditorDockHostLayout layout =
BuildUIEditorDockHostLayout(bounds, panelRegistry, workspace, session, state, metrics);
AppendUIEditorDockHostBackground(drawList, layout, palette, metrics);
AppendUIEditorDockHostForeground(drawList, layout, foregroundOptions, palette, metrics);
}
void AppendUIEditorDockHost(
UIDrawList& drawList,
const UIRect& bounds,
const UIEditorPanelRegistry& panelRegistry,
const UIEditorWorkspaceModel& workspace,
const UIEditorWorkspaceSession& session,
const UIEditorDockHostState& state,
const UIEditorDockHostPalette& palette,
const UIEditorDockHostMetrics& metrics) {
AppendUIEditorDockHost(
drawList,
bounds,
panelRegistry,
workspace,
session,
state,
UIEditorDockHostForegroundOptions{},
palette,
metrics);
}
} // namespace XCEngine::UI::Editor::Widgets

View File

@@ -0,0 +1,116 @@
#include <XCEditor/Docking/UIEditorDockHost.h>
namespace XCEngine::UI::Editor::Widgets {
namespace {
using ::XCEngine::UI::UIPoint;
using ::XCEngine::UI::UIRect;
bool IsPointInsideRect(
const UIRect& rect,
const UIPoint& point) {
return point.x >= rect.x &&
point.x <= rect.x + rect.width &&
point.y >= rect.y &&
point.y <= rect.y + rect.height;
}
UIEditorDockHostHitTarget MakePanelHitTarget(
UIEditorDockHostHitTargetKind kind,
std::string_view nodeId,
std::string_view panelId) {
UIEditorDockHostHitTarget target = {};
target.kind = kind;
target.nodeId = std::string(nodeId);
target.panelId = std::string(panelId);
return target;
}
UIEditorDockHostHitTarget MapPanelFrameHitTarget(
UIEditorPanelFrameHitTarget hitTarget,
std::string_view nodeId,
std::string_view panelId) {
switch (hitTarget) {
case UIEditorPanelFrameHitTarget::Header:
return MakePanelHitTarget(UIEditorDockHostHitTargetKind::PanelHeader, nodeId, panelId);
case UIEditorPanelFrameHitTarget::Body:
return MakePanelHitTarget(UIEditorDockHostHitTargetKind::PanelBody, nodeId, panelId);
case UIEditorPanelFrameHitTarget::Footer:
return MakePanelHitTarget(UIEditorDockHostHitTargetKind::PanelFooter, nodeId, panelId);
case UIEditorPanelFrameHitTarget::CloseButton:
return MakePanelHitTarget(
UIEditorDockHostHitTargetKind::PanelCloseButton,
nodeId,
panelId);
default:
return {};
}
}
} // namespace
UIEditorDockHostHitTarget HitTestUIEditorDockHost(
const UIEditorDockHostLayout& layout,
const UIPoint& point) {
for (std::size_t index = layout.splitters.size(); index > 0u; --index) {
const UIEditorDockHostSplitterLayout& splitter = layout.splitters[index - 1u];
if (IsPointInsideRect(splitter.handleHitRect, point)) {
UIEditorDockHostHitTarget target = {};
target.kind = UIEditorDockHostHitTargetKind::SplitterHandle;
target.nodeId = splitter.nodeId;
return target;
}
}
for (std::size_t index = layout.tabStacks.size(); index > 0u; --index) {
const UIEditorDockHostTabStackLayout& tabStack = layout.tabStacks[index - 1u];
const UIEditorTabStripHitTarget tabHit =
HitTestUIEditorTabStrip(tabStack.tabStripLayout, tabStack.tabStripState, point);
switch (tabHit.kind) {
case UIEditorTabStripHitTargetKind::CloseButton: {
UIEditorDockHostHitTarget target = {};
target.kind = UIEditorDockHostHitTargetKind::TabCloseButton;
target.nodeId = tabStack.nodeId;
target.index = tabHit.index;
if (tabHit.index < tabStack.items.size()) {
target.panelId = tabStack.items[tabHit.index].panelId;
}
return target;
}
case UIEditorTabStripHitTargetKind::Tab: {
UIEditorDockHostHitTarget target = {};
target.kind = UIEditorDockHostHitTargetKind::Tab;
target.nodeId = tabStack.nodeId;
target.index = tabHit.index;
if (tabHit.index < tabStack.items.size()) {
target.panelId = tabStack.items[tabHit.index].panelId;
}
return target;
}
case UIEditorTabStripHitTargetKind::HeaderBackground: {
UIEditorDockHostHitTarget target = {};
target.kind = UIEditorDockHostHitTargetKind::TabStripBackground;
target.nodeId = tabStack.nodeId;
return target;
}
default:
break;
}
const UIEditorPanelFrameHitTarget panelHit = HitTestUIEditorPanelFrame(
tabStack.contentFrameLayout,
tabStack.contentFrameState,
point);
if (panelHit != UIEditorPanelFrameHitTarget::None) {
return MapPanelFrameHitTarget(
panelHit,
tabStack.nodeId,
tabStack.selectedPanelId);
}
}
return {};
}
} // namespace XCEngine::UI::Editor::Widgets

View File

@@ -0,0 +1,161 @@
#include <XCEditor/Docking/UIEditorDockHost.h>
#include <algorithm>
#include <utility>
namespace XCEngine::UI::Editor::Widgets {
namespace {
using ::XCEngine::UI::UIColor;
using ::XCEngine::UI::UIDrawList;
bool UsesExternalBodyPresentation(
const UIEditorDockHostForegroundOptions& options,
std::string_view panelId) {
return std::find(
options.externalBodyPanelIds.begin(),
options.externalBodyPanelIds.end(),
panelId) != options.externalBodyPanelIds.end();
}
UIColor ResolveSplitterColor(
const UIEditorDockHostSplitterLayout& splitter,
const UIEditorDockHostPalette& palette) {
if (splitter.active) {
return palette.splitterActiveColor;
}
if (splitter.hovered) {
return palette.splitterHoveredColor;
}
return palette.splitterColor;
}
} // namespace
void AppendUIEditorDockHostBackground(
UIDrawList& drawList,
const UIEditorDockHostLayout& layout,
const UIEditorDockHostPalette& palette,
const UIEditorDockHostMetrics& metrics) {
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
std::vector<UIEditorTabStripItem> tabItems = {};
tabItems.reserve(tabStack.items.size());
for (const UIEditorDockHostTabItemLayout& item : tabStack.items) {
UIEditorTabStripItem tabItem = {};
tabItem.tabId = item.panelId;
tabItem.title = item.title;
tabItem.closable = item.closable;
tabItems.push_back(std::move(tabItem));
}
AppendUIEditorTabStripBackground(
drawList,
tabStack.tabStripLayout,
tabStack.tabStripState,
palette.tabStripPalette,
metrics.tabStripMetrics);
}
for (const UIEditorDockHostSplitterLayout& splitter : layout.splitters) {
drawList.AddFilledRect(
splitter.splitterLayout.handleRect,
ResolveSplitterColor(splitter, palette),
metrics.splitterHandleRounding);
}
}
void AppendUIEditorDockHostForeground(
UIDrawList& drawList,
const UIEditorDockHostLayout& layout,
const UIEditorDockHostForegroundOptions& options,
const UIEditorDockHostPalette& palette,
const UIEditorDockHostMetrics& metrics) {
for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
std::vector<UIEditorTabStripItem> tabItems = {};
tabItems.reserve(tabStack.items.size());
for (const UIEditorDockHostTabItemLayout& item : tabStack.items) {
UIEditorTabStripItem tabItem = {};
tabItem.tabId = item.panelId;
tabItem.title = item.title;
tabItem.closable = item.closable;
tabItems.push_back(std::move(tabItem));
}
AppendUIEditorTabStripForeground(
drawList,
tabStack.tabStripLayout,
tabItems,
tabStack.tabStripState,
palette.tabStripPalette,
metrics.tabStripMetrics);
if (UsesExternalBodyPresentation(options, tabStack.selectedPanelId)) {
continue;
}
}
if (layout.dropPreview.visible) {
drawList.AddFilledRect(
layout.dropPreview.previewRect,
palette.dropPreviewFillColor);
drawList.AddRectOutline(
layout.dropPreview.previewRect,
palette.dropPreviewBorderColor,
1.0f);
}
}
void AppendUIEditorDockHostForeground(
UIDrawList& drawList,
const UIEditorDockHostLayout& layout,
const UIEditorDockHostPalette& palette,
const UIEditorDockHostMetrics& metrics) {
AppendUIEditorDockHostForeground(
drawList,
layout,
UIEditorDockHostForegroundOptions{},
palette,
metrics);
}
void AppendUIEditorDockHost(
UIDrawList& drawList,
const ::XCEngine::UI::UIRect& bounds,
const UIEditorPanelRegistry& panelRegistry,
const UIEditorWorkspaceModel& workspace,
const UIEditorWorkspaceSession& session,
const UIEditorDockHostState& state,
const UIEditorDockHostForegroundOptions& foregroundOptions,
const UIEditorDockHostPalette& palette,
const UIEditorDockHostMetrics& metrics) {
const UIEditorDockHostLayout layout =
BuildUIEditorDockHostLayout(bounds, panelRegistry, workspace, session, state, metrics);
AppendUIEditorDockHostBackground(drawList, layout, palette, metrics);
AppendUIEditorDockHostForeground(drawList, layout, foregroundOptions, palette, metrics);
}
void AppendUIEditorDockHost(
UIDrawList& drawList,
const ::XCEngine::UI::UIRect& bounds,
const UIEditorPanelRegistry& panelRegistry,
const UIEditorWorkspaceModel& workspace,
const UIEditorWorkspaceSession& session,
const UIEditorDockHostState& state,
const UIEditorDockHostPalette& palette,
const UIEditorDockHostMetrics& metrics) {
AppendUIEditorDockHost(
drawList,
bounds,
panelRegistry,
workspace,
session,
state,
UIEditorDockHostForegroundOptions{},
palette,
metrics);
}
} // namespace XCEngine::UI::Editor::Widgets