Center tab labels and unify dock cursor resolution
This commit is contained in:
@@ -482,6 +482,7 @@ void Application::RenderFrame() {
|
|||||||
}
|
}
|
||||||
ApplyHostCaptureRequests(m_shellFrame.result);
|
ApplyHostCaptureRequests(m_shellFrame.result);
|
||||||
UpdateLastStatus(m_shellFrame.result);
|
UpdateLastStatus(m_shellFrame.result);
|
||||||
|
ApplyCurrentCursor();
|
||||||
AppendUIEditorShellInteraction(
|
AppendUIEditorShellInteraction(
|
||||||
drawList,
|
drawList,
|
||||||
m_shellFrame,
|
m_shellFrame,
|
||||||
@@ -520,6 +521,29 @@ float Application::PixelsToDips(float pixels) const {
|
|||||||
return dpiScale > 0.0f ? pixels / dpiScale : pixels;
|
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 {
|
UIPoint Application::ConvertClientPixelsToDips(LONG x, LONG y) const {
|
||||||
return UIPoint(
|
return UIPoint(
|
||||||
PixelsToDips(static_cast<float>(x)),
|
PixelsToDips(static_cast<float>(x)),
|
||||||
@@ -877,6 +901,13 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP
|
|||||||
|
|
||||||
Application* application = GetApplicationFromWindow(hwnd);
|
Application* application = GetApplicationFromWindow(hwnd);
|
||||||
switch (message) {
|
switch (message) {
|
||||||
|
case WM_SETCURSOR:
|
||||||
|
if (application != nullptr &&
|
||||||
|
LOWORD(lParam) == HTCLIENT &&
|
||||||
|
application->ApplyCurrentCursor()) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case WM_DPICHANGED:
|
case WM_DPICHANGED:
|
||||||
if (application != nullptr && lParam != 0) {
|
if (application != nullptr && lParam != 0) {
|
||||||
application->OnDpiChanged(
|
application->OnDpiChanged(
|
||||||
|
|||||||
@@ -138,6 +138,12 @@ struct UIEditorDockHostLayout {
|
|||||||
UIEditorDockHostDropPreviewLayout dropPreview = {};
|
UIEditorDockHostDropPreviewLayout dropPreview = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class UIEditorDockHostCursorKind : std::uint8_t {
|
||||||
|
Arrow = 0,
|
||||||
|
ResizeEW,
|
||||||
|
ResizeNS
|
||||||
|
};
|
||||||
|
|
||||||
// Allows higher-level compose to own panel body presentation while DockHost
|
// Allows higher-level compose to own panel body presentation while DockHost
|
||||||
// keeps drawing the surrounding chrome/frame.
|
// keeps drawing the surrounding chrome/frame.
|
||||||
struct UIEditorDockHostForegroundOptions {
|
struct UIEditorDockHostForegroundOptions {
|
||||||
@@ -148,6 +154,9 @@ const UIEditorDockHostSplitterLayout* FindUIEditorDockHostSplitterLayout(
|
|||||||
const UIEditorDockHostLayout& layout,
|
const UIEditorDockHostLayout& layout,
|
||||||
std::string_view nodeId);
|
std::string_view nodeId);
|
||||||
|
|
||||||
|
UIEditorDockHostCursorKind ResolveUIEditorDockHostCursorKind(
|
||||||
|
const UIEditorDockHostLayout& layout);
|
||||||
|
|
||||||
UIEditorDockHostLayout BuildUIEditorDockHostLayout(
|
UIEditorDockHostLayout BuildUIEditorDockHostLayout(
|
||||||
const ::XCEngine::UI::UIRect& bounds,
|
const ::XCEngine::UI::UIRect& bounds,
|
||||||
const UIEditorPanelRegistry& panelRegistry,
|
const UIEditorPanelRegistry& panelRegistry,
|
||||||
|
|||||||
@@ -73,6 +73,21 @@ float ResolveTabTextTop(
|
|||||||
return rect.y + (std::max)(0.0f, (rect.height - kHeaderFontSize) * 0.5f) + metrics.labelInsetY;
|
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(
|
UIColor ResolveStripBorderColor(
|
||||||
const UIEditorTabStripState& state,
|
const UIEditorTabStripState& state,
|
||||||
const UIEditorTabStripPalette& palette) {
|
const UIEditorTabStripPalette& palette) {
|
||||||
@@ -387,8 +402,7 @@ void AppendUIEditorTabStripForeground(
|
|||||||
const UIEditorTabStripPalette& palette,
|
const UIEditorTabStripPalette& palette,
|
||||||
const UIEditorTabStripMetrics& metrics) {
|
const UIEditorTabStripMetrics& metrics) {
|
||||||
AppendHeaderContentSeparator(drawList, layout, palette, metrics);
|
AppendHeaderContentSeparator(drawList, layout, palette, metrics);
|
||||||
|
const float horizontalPadding = (std::max)(
|
||||||
const float leftInset = (std::max)(
|
|
||||||
ClampNonNegative(metrics.layoutMetrics.tabHorizontalPadding),
|
ClampNonNegative(metrics.layoutMetrics.tabHorizontalPadding),
|
||||||
ClampNonNegative(metrics.labelInsetX));
|
ClampNonNegative(metrics.labelInsetX));
|
||||||
|
|
||||||
@@ -396,14 +410,15 @@ void AppendUIEditorTabStripForeground(
|
|||||||
const UIRect& tabRect = layout.tabHeaderRects[index];
|
const UIRect& tabRect = layout.tabHeaderRects[index];
|
||||||
const bool selected = layout.selectedIndex == index;
|
const bool selected = layout.selectedIndex == index;
|
||||||
const bool hovered = state.hoveredIndex == index;
|
const bool hovered = state.hoveredIndex == index;
|
||||||
const float textLeft = tabRect.x + leftInset;
|
const float clipLeft = tabRect.x + horizontalPadding;
|
||||||
const float textRight = tabRect.x + tabRect.width - ClampNonNegative(metrics.layoutMetrics.tabHorizontalPadding);
|
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(
|
const UIRect clipRect(
|
||||||
textLeft,
|
clipLeft,
|
||||||
tabRect.y,
|
tabRect.y,
|
||||||
textRight - textLeft,
|
textRight - clipLeft,
|
||||||
tabRect.height);
|
tabRect.height);
|
||||||
drawList.PushClipRect(clipRect, true);
|
drawList.PushClipRect(clipRect, true);
|
||||||
drawList.AddText(
|
drawList.AddText(
|
||||||
|
|||||||
@@ -600,6 +600,31 @@ const UIEditorDockHostSplitterLayout* FindUIEditorDockHostSplitterLayout(
|
|||||||
return nullptr;
|
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(
|
UIEditorDockHostLayout BuildUIEditorDockHostLayout(
|
||||||
const UIRect& bounds,
|
const UIRect& bounds,
|
||||||
const UIEditorPanelRegistry& panelRegistry,
|
const UIEditorPanelRegistry& panelRegistry,
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ using XCEngine::UI::Editor::Widgets::AppendUIEditorDockHostForeground;
|
|||||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorDockHostLayout;
|
using XCEngine::UI::Editor::Widgets::BuildUIEditorDockHostLayout;
|
||||||
using XCEngine::UI::Editor::Widgets::FindUIEditorDockHostSplitterLayout;
|
using XCEngine::UI::Editor::Widgets::FindUIEditorDockHostSplitterLayout;
|
||||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorDockHost;
|
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::UIEditorDockHostHitTarget;
|
||||||
using XCEngine::UI::Editor::Widgets::UIEditorDockHostHitTargetKind;
|
using XCEngine::UI::Editor::Widgets::UIEditorDockHostHitTargetKind;
|
||||||
using XCEngine::UI::Editor::Widgets::UIEditorDockHostLayout;
|
using XCEngine::UI::Editor::Widgets::UIEditorDockHostLayout;
|
||||||
@@ -210,6 +212,77 @@ TEST(UIEditorDockHostTest, BackgroundAndForegroundEmitStableCompositeCommands) {
|
|||||||
EXPECT_GT(foreground.GetCommandCount(), 10u);
|
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) {
|
TEST(UIEditorDockHostTest, ForegroundDrawsUnifiedTabTitlesAcrossAllLeafStacks) {
|
||||||
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
const UIEditorPanelRegistry registry = BuildPanelRegistry();
|
||||||
const UIEditorWorkspaceModel workspace = BuildWorkspace();
|
const UIEditorWorkspaceModel workspace = BuildWorkspace();
|
||||||
|
|||||||
@@ -173,4 +173,41 @@ TEST(UIEditorTabStripTest, BackgroundAndForegroundEmitStableChromeCommands) {
|
|||||||
EXPECT_EQ(foregroundCommands[6].text, "Document B");
|
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
|
} // namespace
|
||||||
|
|||||||
Reference in New Issue
Block a user