Center tab labels and unify dock cursor resolution
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user