diff --git a/new_editor/app/Application.cpp b/new_editor/app/Application.cpp index 3050946d..56e95519 100644 --- a/new_editor/app/Application.cpp +++ b/new_editor/app/Application.cpp @@ -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(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( diff --git a/new_editor/include/XCEditor/Shell/UIEditorDockHost.h b/new_editor/include/XCEditor/Shell/UIEditorDockHost.h index cb970aa6..11884b71 100644 --- a/new_editor/include/XCEditor/Shell/UIEditorDockHost.h +++ b/new_editor/include/XCEditor/Shell/UIEditorDockHost.h @@ -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, diff --git a/new_editor/src/Collections/UIEditorTabStrip.cpp b/new_editor/src/Collections/UIEditorTabStrip.cpp index 9258ce74..e37c4d88 100644 --- a/new_editor/src/Collections/UIEditorTabStrip.cpp +++ b/new_editor/src/Collections/UIEditorTabStrip.cpp @@ -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( diff --git a/new_editor/src/Shell/UIEditorDockHost.cpp b/new_editor/src/Shell/UIEditorDockHost.cpp index 5f3ee854..f20a3897 100644 --- a/new_editor/src/Shell/UIEditorDockHost.cpp +++ b/new_editor/src/Shell/UIEditorDockHost.cpp @@ -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, diff --git a/tests/UI/Editor/unit/test_ui_editor_dock_host.cpp b/tests/UI/Editor/unit/test_ui_editor_dock_host.cpp index eb39c982..8f038dec 100644 --- a/tests/UI/Editor/unit/test_ui_editor_dock_host.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_dock_host.cpp @@ -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(); diff --git a/tests/UI/Editor/unit/test_ui_editor_tab_strip.cpp b/tests/UI/Editor/unit/test_ui_editor_tab_strip.cpp index 425aa028..aa2af8e0 100644 --- a/tests/UI/Editor/unit/test_ui_editor_tab_strip.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_tab_strip.cpp @@ -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 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