Center tab labels and unify dock cursor resolution

This commit is contained in:
2026-04-11 17:37:13 +08:00
parent 2958dcc491
commit 443c56ed08
6 changed files with 197 additions and 7 deletions

View File

@@ -482,6 +482,7 @@ void Application::RenderFrame() {
}
ApplyHostCaptureRequests(m_shellFrame.result);
UpdateLastStatus(m_shellFrame.result);
ApplyCurrentCursor();
AppendUIEditorShellInteraction(
drawList,
m_shellFrame,
@@ -520,6 +521,29 @@ float Application::PixelsToDips(float pixels) const {
return dpiScale > 0.0f ? pixels / dpiScale : pixels;
}
LPCWSTR Application::ResolveCurrentCursorResource() const {
switch (Widgets::ResolveUIEditorDockHostCursorKind(
m_shellFrame.workspaceInteractionFrame.dockHostFrame.layout)) {
case Widgets::UIEditorDockHostCursorKind::ResizeEW:
return IDC_SIZEWE;
case Widgets::UIEditorDockHostCursorKind::ResizeNS:
return IDC_SIZENS;
case Widgets::UIEditorDockHostCursorKind::Arrow:
default:
return IDC_ARROW;
}
}
bool Application::ApplyCurrentCursor() const {
const HCURSOR cursor = LoadCursorW(nullptr, ResolveCurrentCursorResource());
if (cursor == nullptr) {
return false;
}
SetCursor(cursor);
return true;
}
UIPoint Application::ConvertClientPixelsToDips(LONG x, LONG y) const {
return UIPoint(
PixelsToDips(static_cast<float>(x)),
@@ -877,6 +901,13 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP
Application* application = GetApplicationFromWindow(hwnd);
switch (message) {
case WM_SETCURSOR:
if (application != nullptr &&
LOWORD(lParam) == HTCLIENT &&
application->ApplyCurrentCursor()) {
return TRUE;
}
break;
case WM_DPICHANGED:
if (application != nullptr && lParam != 0) {
application->OnDpiChanged(

View File

@@ -138,6 +138,12 @@ struct UIEditorDockHostLayout {
UIEditorDockHostDropPreviewLayout dropPreview = {};
};
enum class UIEditorDockHostCursorKind : std::uint8_t {
Arrow = 0,
ResizeEW,
ResizeNS
};
// Allows higher-level compose to own panel body presentation while DockHost
// keeps drawing the surrounding chrome/frame.
struct UIEditorDockHostForegroundOptions {
@@ -148,6 +154,9 @@ const UIEditorDockHostSplitterLayout* FindUIEditorDockHostSplitterLayout(
const UIEditorDockHostLayout& layout,
std::string_view nodeId);
UIEditorDockHostCursorKind ResolveUIEditorDockHostCursorKind(
const UIEditorDockHostLayout& layout);
UIEditorDockHostLayout BuildUIEditorDockHostLayout(
const ::XCEngine::UI::UIRect& bounds,
const UIEditorPanelRegistry& panelRegistry,

View File

@@ -73,6 +73,21 @@ float ResolveTabTextTop(
return rect.y + (std::max)(0.0f, (rect.height - kHeaderFontSize) * 0.5f) + metrics.labelInsetY;
}
float ResolveTabTextLeft(
const UIRect& rect,
const UIEditorTabStripItem& item,
const UIEditorTabStripMetrics& metrics) {
const float padding =
(std::max)(
ClampNonNegative(metrics.layoutMetrics.tabHorizontalPadding),
ClampNonNegative(metrics.labelInsetX));
const float availableLeft = rect.x + padding;
const float availableRight = rect.x + rect.width - padding;
const float availableWidth = (std::max)(availableRight - availableLeft, 0.0f);
const float labelWidth = ResolveEstimatedLabelWidth(item, metrics);
return availableLeft + (std::max)(0.0f, (availableWidth - labelWidth) * 0.5f);
}
UIColor ResolveStripBorderColor(
const UIEditorTabStripState& state,
const UIEditorTabStripPalette& palette) {
@@ -387,8 +402,7 @@ void AppendUIEditorTabStripForeground(
const UIEditorTabStripPalette& palette,
const UIEditorTabStripMetrics& metrics) {
AppendHeaderContentSeparator(drawList, layout, palette, metrics);
const float leftInset = (std::max)(
const float horizontalPadding = (std::max)(
ClampNonNegative(metrics.layoutMetrics.tabHorizontalPadding),
ClampNonNegative(metrics.labelInsetX));
@@ -396,14 +410,15 @@ void AppendUIEditorTabStripForeground(
const UIRect& tabRect = layout.tabHeaderRects[index];
const bool selected = layout.selectedIndex == index;
const bool hovered = state.hoveredIndex == index;
const float textLeft = tabRect.x + leftInset;
const float textRight = tabRect.x + tabRect.width - ClampNonNegative(metrics.layoutMetrics.tabHorizontalPadding);
const float clipLeft = tabRect.x + horizontalPadding;
const float textLeft = ResolveTabTextLeft(tabRect, items[index], metrics);
const float textRight = tabRect.x + tabRect.width - horizontalPadding;
if (textRight > textLeft) {
if (textRight > clipLeft) {
const UIRect clipRect(
textLeft,
clipLeft,
tabRect.y,
textRight - textLeft,
textRight - clipLeft,
tabRect.height);
drawList.PushClipRect(clipRect, true);
drawList.AddText(

View File

@@ -600,6 +600,31 @@ const UIEditorDockHostSplitterLayout* FindUIEditorDockHostSplitterLayout(
return nullptr;
}
UIEditorDockHostCursorKind ResolveUIEditorDockHostCursorKind(
const UIEditorDockHostLayout& layout) {
for (const UIEditorDockHostSplitterLayout& splitter : layout.splitters) {
if (!splitter.active) {
continue;
}
return splitter.axis == UIEditorWorkspaceSplitAxis::Horizontal
? UIEditorDockHostCursorKind::ResizeEW
: UIEditorDockHostCursorKind::ResizeNS;
}
for (const UIEditorDockHostSplitterLayout& splitter : layout.splitters) {
if (!splitter.hovered) {
continue;
}
return splitter.axis == UIEditorWorkspaceSplitAxis::Horizontal
? UIEditorDockHostCursorKind::ResizeEW
: UIEditorDockHostCursorKind::ResizeNS;
}
return UIEditorDockHostCursorKind::Arrow;
}
UIEditorDockHostLayout BuildUIEditorDockHostLayout(
const UIRect& bounds,
const UIEditorPanelRegistry& panelRegistry,

View File

@@ -27,6 +27,8 @@ using XCEngine::UI::Editor::Widgets::AppendUIEditorDockHostForeground;
using XCEngine::UI::Editor::Widgets::BuildUIEditorDockHostLayout;
using XCEngine::UI::Editor::Widgets::FindUIEditorDockHostSplitterLayout;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorDockHost;
using XCEngine::UI::Editor::Widgets::ResolveUIEditorDockHostCursorKind;
using XCEngine::UI::Editor::Widgets::UIEditorDockHostCursorKind;
using XCEngine::UI::Editor::Widgets::UIEditorDockHostHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorDockHostHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorDockHostLayout;
@@ -210,6 +212,77 @@ TEST(UIEditorDockHostTest, BackgroundAndForegroundEmitStableCompositeCommands) {
EXPECT_GT(foreground.GetCommandCount(), 10u);
}
TEST(UIEditorDockHostTest, CursorFallsBackToArrowWhenNoSplitterIsHoveredOrActive) {
const UIEditorPanelRegistry registry = BuildPanelRegistry();
const UIEditorWorkspaceModel workspace = BuildWorkspace();
const UIEditorWorkspaceSession session =
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
const UIEditorDockHostLayout layout = BuildUIEditorDockHostLayout(
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
registry,
workspace,
session);
EXPECT_EQ(ResolveUIEditorDockHostCursorKind(layout), UIEditorDockHostCursorKind::Arrow);
}
TEST(UIEditorDockHostTest, CursorUsesHoveredSplitterAxisWhenPointerRestsOnHandle) {
const UIEditorPanelRegistry registry = BuildPanelRegistry();
const UIEditorWorkspaceModel workspace = BuildWorkspace();
const UIEditorWorkspaceSession session =
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
UIEditorDockHostState state = {};
state.hoveredTarget = UIEditorDockHostHitTarget{
UIEditorDockHostHitTargetKind::SplitterHandle,
"root-split",
{},
UIEditorTabStripInvalidIndex
};
const UIEditorDockHostLayout horizontalLayout = BuildUIEditorDockHostLayout(
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
registry,
workspace,
session,
state);
EXPECT_EQ(ResolveUIEditorDockHostCursorKind(horizontalLayout), UIEditorDockHostCursorKind::ResizeEW);
state.hoveredTarget.nodeId = "right-split";
const UIEditorDockHostLayout verticalLayout = BuildUIEditorDockHostLayout(
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
registry,
workspace,
session,
state);
EXPECT_EQ(ResolveUIEditorDockHostCursorKind(verticalLayout), UIEditorDockHostCursorKind::ResizeNS);
}
TEST(UIEditorDockHostTest, CursorPrefersActiveSplitterOverHoveredSplitter) {
const UIEditorPanelRegistry registry = BuildPanelRegistry();
const UIEditorWorkspaceModel workspace = BuildWorkspace();
const UIEditorWorkspaceSession session =
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
UIEditorDockHostState state = {};
state.hoveredTarget = UIEditorDockHostHitTarget{
UIEditorDockHostHitTargetKind::SplitterHandle,
"root-split",
{},
UIEditorTabStripInvalidIndex
};
state.activeSplitterNodeId = "right-split";
const UIEditorDockHostLayout layout = BuildUIEditorDockHostLayout(
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
registry,
workspace,
session,
state);
EXPECT_EQ(ResolveUIEditorDockHostCursorKind(layout), UIEditorDockHostCursorKind::ResizeNS);
}
TEST(UIEditorDockHostTest, ForegroundDrawsUnifiedTabTitlesAcrossAllLeafStacks) {
const UIEditorPanelRegistry registry = BuildPanelRegistry();
const UIEditorWorkspaceModel workspace = BuildWorkspace();

View File

@@ -173,4 +173,41 @@ TEST(UIEditorTabStripTest, BackgroundAndForegroundEmitStableChromeCommands) {
EXPECT_EQ(foregroundCommands[6].text, "Document B");
}
TEST(UIEditorTabStripTest, ForegroundCentersTabLabelsWithinHeaderContentArea) {
UIEditorTabStripMetrics metrics = {};
metrics.layoutMetrics.headerHeight = 22.0f;
metrics.layoutMetrics.tabMinWidth = 68.0f;
metrics.layoutMetrics.tabHorizontalPadding = 8.0f;
metrics.layoutMetrics.tabGap = 1.0f;
metrics.labelInsetX = 8.0f;
const std::vector<UIEditorTabStripItem> items = {
{ "doc-a", "Scene", true, 30.0f },
{ "doc-b", "Game", true, 24.0f }
};
UIEditorTabStripState state = {};
state.selectedIndex = 0u;
const UIEditorTabStripLayout layout =
BuildUIEditorTabStripLayout(UIRect(10.0f, 20.0f, 220.0f, 120.0f), items, state, metrics);
UIDrawList foreground("TabStripForeground");
AppendUIEditorTabStripForeground(foreground, layout, items, state, {}, metrics);
const auto& commands = foreground.GetCommands();
ASSERT_EQ(commands[3].type, UIDrawCommandType::Text);
ASSERT_EQ(commands[6].type, UIDrawCommandType::Text);
const float padding = 8.0f;
const float firstExpectedX =
layout.tabHeaderRects[0].x + padding +
((layout.tabHeaderRects[0].width - padding * 2.0f) - items[0].desiredHeaderLabelWidth) * 0.5f;
const float secondExpectedX =
layout.tabHeaderRects[1].x + padding +
((layout.tabHeaderRects[1].width - padding * 2.0f) - items[1].desiredHeaderLabelWidth) * 0.5f;
EXPECT_FLOAT_EQ(commands[3].position.x, firstExpectedX);
EXPECT_FLOAT_EQ(commands[6].position.x, secondExpectedX);
}
} // namespace