Refine detached tab drag host behavior

This commit is contained in:
2026-04-14 16:56:30 +08:00
parent 2a9264cfe4
commit 3e6e997485
4 changed files with 261 additions and 20 deletions

View File

@@ -44,6 +44,8 @@ constexpr UINT kDefaultDpi = 96u;
constexpr float kBaseDpiScale = 96.0f;
constexpr float kBorderlessTitleBarHeightDips = 28.0f;
constexpr float kBorderlessTitleBarFontSize = 12.0f;
constexpr LONG kDetachedWindowDragOffsetXPixels = 40;
constexpr LONG kDetachedWindowDragOffsetYPixels = 12;
const UIColor kShellSurfaceColor(0.10f, 0.10f, 0.10f, 1.0f);
const UIColor kShellBorderColor(0.15f, 0.15f, 0.15f, 1.0f);
const UIColor kShellTextColor(0.92f, 0.92f, 0.92f, 1.0f);
@@ -451,6 +453,18 @@ bool IsPointInsideRect(
point.y <= rect.y + rect.height;
}
const Widgets::UIEditorDockHostTabStackLayout* FindDockHostTabStackLayoutByNodeId(
const Widgets::UIEditorDockHostLayout& layout,
std::string_view nodeId) {
for (const Widgets::UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) {
if (tabStack.nodeId == nodeId) {
return &tabStack;
}
}
return nullptr;
}
std::size_t ResolveCrossWindowDropInsertionIndex(
const Widgets::UIEditorDockHostTabStackLayout& tabStack,
const UIPoint& point) {
@@ -1010,10 +1024,10 @@ std::wstring Application::BuildManagedWindowTitle(
RECT Application::BuildDetachedWindowRect(const POINT& screenPoint) const {
RECT rect = {
screenPoint.x - 420,
screenPoint.y - 24,
screenPoint.x - 420 + 960,
screenPoint.y - 24 + 720
screenPoint.x - kDetachedWindowDragOffsetXPixels,
screenPoint.y - kDetachedWindowDragOffsetYPixels,
screenPoint.x - kDetachedWindowDragOffsetXPixels + 960,
screenPoint.y - kDetachedWindowDragOffsetYPixels + 720
};
const HMONITOR monitor = MonitorFromPoint(screenPoint, MONITOR_DEFAULTTONEAREST);
@@ -1056,6 +1070,7 @@ void Application::ResetManagedWindowInteractionState(ManagedWindowState& windowS
windowState.pendingGlobalTabDragNodeId.clear();
windowState.pendingGlobalTabDragPanelId.clear();
windowState.pendingGlobalTabDragScreenPoint = {};
windowState.pendingGlobalTabDragWindowOffset = {};
}
bool Application::SynchronizeManagedWindowsFromWindowSet(
@@ -1122,12 +1137,14 @@ void Application::BeginGlobalTabDragSession(
std::string_view panelWindowId,
std::string_view sourceNodeId,
std::string_view panelId,
const POINT& screenPoint) {
const POINT& screenPoint,
const POINT& windowDragOffset) {
m_globalTabDragSession.active = true;
m_globalTabDragSession.panelWindowId = std::string(panelWindowId);
m_globalTabDragSession.sourceNodeId = std::string(sourceNodeId);
m_globalTabDragSession.panelId = std::string(panelId);
m_globalTabDragSession.screenPoint = screenPoint;
m_globalTabDragSession.windowDragOffset = windowDragOffset;
}
void Application::EndGlobalTabDragSession() {
@@ -1187,6 +1204,112 @@ const Application::ManagedWindowState* Application::FindTopmostWindowStateAtScre
excludedWindowId);
}
POINT Application::ConvertClientDipsToScreenPixels(
const ManagedWindowState& windowState,
const UIPoint& point) const {
const float dpiScale = windowState.hostRuntime.GetDpiScale(kBaseDpiScale);
POINT clientPoint = {
static_cast<LONG>(point.x * dpiScale),
static_cast<LONG>(point.y * dpiScale)
};
if (windowState.hwnd != nullptr) {
ClientToScreen(windowState.hwnd, &clientPoint);
}
return clientPoint;
}
bool Application::TryResolveDraggedTabScreenRect(
const ManagedWindowState& windowState,
std::string_view nodeId,
std::string_view panelId,
RECT& outRect) const {
outRect = {};
const Widgets::UIEditorDockHostLayout& layout =
windowState.editorWorkspace
.GetShellFrame()
.workspaceInteractionFrame
.dockHostFrame
.layout;
const Widgets::UIEditorDockHostTabStackLayout* tabStack =
FindDockHostTabStackLayoutByNodeId(layout, nodeId);
if (tabStack == nullptr) {
return false;
}
std::size_t tabIndex = Widgets::UIEditorTabStripInvalidIndex;
for (std::size_t index = 0; index < tabStack->items.size(); ++index) {
if (tabStack->items[index].panelId == panelId) {
tabIndex = index;
break;
}
}
if (tabIndex == Widgets::UIEditorTabStripInvalidIndex ||
tabIndex >= tabStack->tabStripLayout.tabHeaderRects.size()) {
return false;
}
const UIRect& tabRect = tabStack->tabStripLayout.tabHeaderRects[tabIndex];
const POINT topLeft = ConvertClientDipsToScreenPixels(
windowState,
UIPoint(tabRect.x, tabRect.y));
const POINT bottomRight = ConvertClientDipsToScreenPixels(
windowState,
UIPoint(tabRect.x + tabRect.width, tabRect.y + tabRect.height));
outRect.left = topLeft.x;
outRect.top = topLeft.y;
outRect.right = bottomRight.x;
outRect.bottom = bottomRight.y;
return outRect.right > outRect.left && outRect.bottom > outRect.top;
}
POINT Application::ResolveGlobalTabDragWindowOffset(
const ManagedWindowState& windowState,
std::string_view nodeId,
std::string_view panelId,
const POINT& screenPoint) const {
RECT tabScreenRect = {};
if (TryResolveDraggedTabScreenRect(windowState, nodeId, panelId, tabScreenRect)) {
const LONG offsetX =
(std::clamp)(screenPoint.x - tabScreenRect.left, 0L, tabScreenRect.right - tabScreenRect.left);
const LONG offsetY =
(std::clamp)(screenPoint.y - tabScreenRect.top, 0L, tabScreenRect.bottom - tabScreenRect.top);
return POINT { offsetX, offsetY };
}
const float dpiScale = windowState.hostRuntime.GetDpiScale(kBaseDpiScale);
return POINT {
static_cast<LONG>(static_cast<float>(kDetachedWindowDragOffsetXPixels) * dpiScale),
static_cast<LONG>(static_cast<float>(kDetachedWindowDragOffsetYPixels) * dpiScale)
};
}
void Application::MoveGlobalTabDragWindow(
ManagedWindowState& windowState,
const POINT& screenPoint) const {
if (windowState.hwnd == nullptr) {
return;
}
RECT currentRect = {};
if (!GetWindowRect(windowState.hwnd, &currentRect)) {
return;
}
const LONG width = currentRect.right - currentRect.left;
const LONG height = currentRect.bottom - currentRect.top;
const LONG left = screenPoint.x - m_globalTabDragSession.windowDragOffset.x;
const LONG top = screenPoint.y - m_globalTabDragSession.windowDragOffset.y;
SetWindowPos(
windowState.hwnd,
nullptr,
left,
top,
width,
height,
SWP_NOZORDER | SWP_NOACTIVATE);
}
bool Application::TryStartGlobalTabDrag(ManagedWindowState& sourceWindowState) {
if (!sourceWindowState.pendingGlobalTabDragStart ||
sourceWindowState.pendingGlobalTabDragNodeId.empty() ||
@@ -1197,10 +1320,12 @@ bool Application::TryStartGlobalTabDrag(ManagedWindowState& sourceWindowState) {
const std::string sourceNodeId = sourceWindowState.pendingGlobalTabDragNodeId;
const std::string panelId = sourceWindowState.pendingGlobalTabDragPanelId;
const POINT screenPoint = sourceWindowState.pendingGlobalTabDragScreenPoint;
const POINT windowDragOffset = sourceWindowState.pendingGlobalTabDragWindowOffset;
sourceWindowState.pendingGlobalTabDragStart = false;
sourceWindowState.pendingGlobalTabDragNodeId.clear();
sourceWindowState.pendingGlobalTabDragPanelId.clear();
sourceWindowState.pendingGlobalTabDragScreenPoint = {};
sourceWindowState.pendingGlobalTabDragWindowOffset = {};
if (sourceWindowState.primary) {
UIEditorWindowWorkspaceController windowWorkspaceController(
@@ -1236,7 +1361,9 @@ bool Application::TryStartGlobalTabDrag(ManagedWindowState& sourceWindowState) {
detachedWindowState->windowId,
detachedWindowState->workspaceController.GetWorkspace().root.nodeId,
panelId,
screenPoint);
screenPoint,
windowDragOffset);
MoveGlobalTabDragWindow(*detachedWindowState, screenPoint);
SetCapture(detachedWindowState->hwnd);
SetForegroundWindow(detachedWindowState->hwnd);
LogRuntimeTrace(
@@ -1251,7 +1378,9 @@ bool Application::TryStartGlobalTabDrag(ManagedWindowState& sourceWindowState) {
sourceWindowState.windowId,
sourceNodeId,
panelId,
screenPoint);
screenPoint,
windowDragOffset);
MoveGlobalTabDragWindow(sourceWindowState, screenPoint);
if (sourceWindowState.hwnd != nullptr) {
SetCapture(sourceWindowState.hwnd);
}
@@ -1352,7 +1481,7 @@ bool Application::HandleGlobalTabDragPointerMove(HWND hwnd) {
return false;
}
const ManagedWindowState* ownerWindowState = FindWindowState(m_globalTabDragSession.panelWindowId);
ManagedWindowState* ownerWindowState = FindWindowState(m_globalTabDragSession.panelWindowId);
if (ownerWindowState == nullptr || ownerWindowState->hwnd != hwnd) {
return false;
}
@@ -1360,6 +1489,7 @@ bool Application::HandleGlobalTabDragPointerMove(HWND hwnd) {
POINT screenPoint = {};
if (GetCursorPos(&screenPoint)) {
m_globalTabDragSession.screenPoint = screenPoint;
MoveGlobalTabDragWindow(*ownerWindowState, screenPoint);
}
return true;
}
@@ -1443,6 +1573,59 @@ bool Application::HandleGlobalTabDragPointerButtonUp(HWND hwnd) {
return true;
}
void Application::AppendGlobalTabDragDropPreview(UIDrawList& drawList) const {
if (!m_globalTabDragSession.active || m_currentWindowState == nullptr) {
return;
}
if (m_currentWindowState->windowId == m_globalTabDragSession.panelWindowId) {
return;
}
const ManagedWindowState* targetWindowState = FindTopmostWindowStateAtScreenPoint(
m_globalTabDragSession.screenPoint,
m_globalTabDragSession.panelWindowId);
if (targetWindowState == nullptr || targetWindowState != m_currentWindowState) {
return;
}
const UIPoint targetPoint =
ConvertScreenPixelsToClientDips(*targetWindowState, m_globalTabDragSession.screenPoint);
const Widgets::UIEditorDockHostLayout& targetLayout =
targetWindowState->editorWorkspace
.GetShellFrame()
.workspaceInteractionFrame
.dockHostFrame
.layout;
CrossWindowDockDropTarget dropTarget = {};
if (!TryResolveCrossWindowDockDropTarget(targetLayout, targetPoint, dropTarget)) {
return;
}
Widgets::UIEditorDockHostDropPreviewState previewState = {};
previewState.visible = true;
previewState.sourceNodeId = m_globalTabDragSession.sourceNodeId;
previewState.sourcePanelId = m_globalTabDragSession.panelId;
previewState.targetNodeId = dropTarget.nodeId;
previewState.placement = dropTarget.placement;
previewState.insertionIndex = dropTarget.insertionIndex;
const Widgets::UIEditorDockHostDropPreviewLayout previewLayout =
Widgets::ResolveUIEditorDockHostDropPreviewLayout(targetLayout, previewState);
if (!previewLayout.visible) {
return;
}
const Widgets::UIEditorDockHostPalette& dockPalette =
ResolveUIEditorShellInteractionPalette().shellPalette.dockHostPalette;
drawList.AddFilledRect(
previewLayout.previewRect,
dockPalette.dropPreviewFillColor);
drawList.AddRectOutline(
previewLayout.previewRect,
dockPalette.dropPreviewBorderColor,
1.0f);
}
void Application::HandleDestroyedWindow(HWND hwnd) {
if (ManagedWindowState* windowState = FindWindowState(hwnd); windowState != nullptr) {
windowState->hwnd = nullptr;
@@ -1570,6 +1753,12 @@ void Application::RenderFrame() {
RequireCurrentWindowState().pendingGlobalTabDragPanelId =
dockHostInteractionState.activeTabDragPanelId;
RequireCurrentWindowState().pendingGlobalTabDragScreenPoint = screenPoint;
RequireCurrentWindowState().pendingGlobalTabDragWindowOffset =
ResolveGlobalTabDragWindowOffset(
RequireCurrentWindowState(),
dockHostInteractionState.activeTabDragNodeId,
dockHostInteractionState.activeTabDragPanelId,
screenPoint);
}
if (shellFrame.result.workspaceResult.dockHostResult.detachRequested) {
POINT screenPoint = {};
@@ -1588,6 +1777,7 @@ void Application::RenderFrame() {
ApplyHostedContentCaptureRequests();
ApplyCurrentCursor();
m_editorWorkspace.Append(drawList);
AppendGlobalTabDragDropPreview(drawList);
if (frameContext.canRenderViewports) {
m_editorWorkspace.RenderRequestedViewports(frameContext.renderContext);
}
@@ -1636,6 +1826,10 @@ bool Application::IsBorderlessWindowEnabled() const {
return true;
}
bool Application::HasBorderlessWindowChrome() const {
return m_currentWindowState != nullptr && m_currentWindowState->primary;
}
bool Application::IsBorderlessWindowMaximized() const {
return m_hostRuntime.IsBorderlessWindowMaximized();
}
@@ -1888,7 +2082,7 @@ Host::BorderlessWindowChromeLayout Application::ResolveBorderlessWindowChromeLay
}
Host::BorderlessWindowChromeHitTarget Application::HitTestBorderlessWindowChrome(LPARAM lParam) const {
if (!IsBorderlessWindowEnabled() || m_hwnd == nullptr) {
if (!HasBorderlessWindowChrome() || m_hwnd == nullptr) {
return Host::BorderlessWindowChromeHitTarget::None;
}
@@ -1931,6 +2125,10 @@ bool Application::UpdateBorderlessWindowChromeHover(LPARAM lParam) {
}
bool Application::HandleBorderlessWindowChromeButtonDown(LPARAM lParam) {
if (!HasBorderlessWindowChrome()) {
return false;
}
if (m_hostRuntime.GetHoveredBorderlessResizeEdge() != Host::BorderlessWindowResizeEdge::None ||
m_hostRuntime.IsBorderlessResizeActive()) {
return false;
@@ -1969,6 +2167,10 @@ bool Application::HandleBorderlessWindowChromeButtonDown(LPARAM lParam) {
}
bool Application::HandleBorderlessWindowChromeButtonUp(LPARAM lParam) {
if (!HasBorderlessWindowChrome()) {
return false;
}
if (m_hostRuntime.IsBorderlessWindowDragRestoreArmed()) {
ClearBorderlessWindowChromeDragRestoreState();
return true;
@@ -1997,6 +2199,10 @@ bool Application::HandleBorderlessWindowChromeButtonUp(LPARAM lParam) {
}
bool Application::HandleBorderlessWindowChromeDoubleClick(LPARAM lParam) {
if (!HasBorderlessWindowChrome()) {
return false;
}
if (m_hostRuntime.IsBorderlessWindowDragRestoreArmed()) {
ClearBorderlessWindowChromeDragRestoreState();
}
@@ -2010,6 +2216,10 @@ bool Application::HandleBorderlessWindowChromeDoubleClick(LPARAM lParam) {
}
bool Application::HandleBorderlessWindowChromeDragRestorePointerMove() {
if (!HasBorderlessWindowChrome()) {
return false;
}
if (!m_hostRuntime.IsBorderlessWindowDragRestoreArmed() || m_hwnd == nullptr) {
return false;
}
@@ -2101,7 +2311,7 @@ void Application::ClearBorderlessWindowChromeState() {
void Application::AppendBorderlessWindowChrome(
::XCEngine::UI::UIDrawList& drawList,
float clientWidthDips) const {
if (!IsBorderlessWindowEnabled()) {
if (!HasBorderlessWindowChrome()) {
return;
}
@@ -2412,7 +2622,7 @@ bool Application::ResolveRenderClientPixelSize(UINT& outWidth, UINT& outHeight)
}
UIRect Application::ResolveWorkspaceBounds(float clientWidthDips, float clientHeightDips) const {
if (!IsBorderlessWindowEnabled()) {
if (!HasBorderlessWindowChrome()) {
return UIRect(0.0f, 0.0f, clientWidthDips, clientHeightDips);
}

View File

@@ -75,6 +75,7 @@ private:
std::string pendingGlobalTabDragNodeId = {};
std::string pendingGlobalTabDragPanelId = {};
POINT pendingGlobalTabDragScreenPoint = {};
POINT pendingGlobalTabDragWindowOffset = {};
};
struct GlobalTabDragSession {
bool active = false;
@@ -82,6 +83,7 @@ private:
std::string sourceNodeId = {};
std::string panelId = {};
POINT screenPoint = {};
POINT windowDragOffset = {};
};
struct ManagedWindowCreateParams {
std::string windowId = {};
@@ -127,7 +129,8 @@ private:
std::string_view panelWindowId,
std::string_view sourceNodeId,
std::string_view panelId,
const POINT& screenPoint);
const POINT& screenPoint,
const POINT& windowDragOffset);
void EndGlobalTabDragSession();
bool HandleGlobalTabDragPointerMove(HWND hwnd);
bool HandleGlobalTabDragPointerButtonUp(HWND hwnd);
@@ -163,10 +166,26 @@ private:
LPCWSTR ResolveCurrentCursorResource() const;
float GetDpiScale() const;
float PixelsToDips(float pixels) const;
bool HasBorderlessWindowChrome() const;
::XCEngine::UI::UIPoint ConvertClientPixelsToDips(LONG x, LONG y) const;
::XCEngine::UI::UIPoint ConvertScreenPixelsToClientDips(
const ManagedWindowState& windowState,
const POINT& screenPoint) const;
POINT ConvertClientDipsToScreenPixels(
const ManagedWindowState& windowState,
const ::XCEngine::UI::UIPoint& point) const;
bool TryResolveDraggedTabScreenRect(
const ManagedWindowState& windowState,
std::string_view nodeId,
std::string_view panelId,
RECT& outRect) const;
POINT ResolveGlobalTabDragWindowOffset(
const ManagedWindowState& windowState,
std::string_view nodeId,
std::string_view panelId,
const POINT& screenPoint) const;
void MoveGlobalTabDragWindow(ManagedWindowState& windowState, const POINT& screenPoint) const;
void AppendGlobalTabDragDropPreview(::XCEngine::UI::UIDrawList& drawList) const;
std::string BuildCaptureStatusText() const;
void LogRuntimeTrace(std::string_view channel, std::string_view message) const;
void ApplyHostCaptureRequests(const UIEditorShellInteractionResult& result);

View File

@@ -75,21 +75,21 @@ struct UIEditorDockHostPalette {
UIEditorTabStripPalette tabStripPalette = {};
UIEditorPanelFramePalette panelFramePalette = {};
::XCEngine::UI::UIColor splitterColor =
::XCEngine::UI::UIColor(0.13f, 0.13f, 0.13f, 1.0f);
::XCEngine::UI::UIColor(0.14f, 0.14f, 0.14f, 1.0f);
::XCEngine::UI::UIColor splitterHoveredColor =
::XCEngine::UI::UIColor(0.13f, 0.13f, 0.13f, 1.0f);
::XCEngine::UI::UIColor(0.16f, 0.16f, 0.16f, 1.0f);
::XCEngine::UI::UIColor splitterActiveColor =
::XCEngine::UI::UIColor(0.13f, 0.13f, 0.13f, 1.0f);
::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f);
::XCEngine::UI::UIColor placeholderTitleColor =
::XCEngine::UI::UIColor(0.93f, 0.94f, 0.96f, 1.0f);
::XCEngine::UI::UIColor(0.92f, 0.92f, 0.92f, 1.0f);
::XCEngine::UI::UIColor placeholderTextColor =
::XCEngine::UI::UIColor(0.70f, 0.72f, 0.74f, 1.0f);
::XCEngine::UI::UIColor(0.72f, 0.72f, 0.72f, 1.0f);
::XCEngine::UI::UIColor placeholderMutedColor =
::XCEngine::UI::UIColor(0.58f, 0.59f, 0.62f, 1.0f);
::XCEngine::UI::UIColor(0.62f, 0.62f, 0.62f, 1.0f);
::XCEngine::UI::UIColor dropPreviewFillColor =
::XCEngine::UI::UIColor(0.88f, 0.88f, 0.88f, 0.14f);
::XCEngine::UI::UIColor(0.92f, 0.92f, 0.92f, 0.06f);
::XCEngine::UI::UIColor dropPreviewBorderColor =
::XCEngine::UI::UIColor(0.94f, 0.94f, 0.94f, 0.78f);
::XCEngine::UI::UIColor(0.95f, 0.95f, 0.95f, 0.55f);
};
struct UIEditorDockHostTabItemLayout {
@@ -165,6 +165,10 @@ UIEditorDockHostLayout BuildUIEditorDockHostLayout(
const UIEditorDockHostState& state = {},
const UIEditorDockHostMetrics& metrics = {});
UIEditorDockHostDropPreviewLayout ResolveUIEditorDockHostDropPreviewLayout(
const UIEditorDockHostLayout& layout,
const UIEditorDockHostDropPreviewState& state);
UIEditorDockHostHitTarget HitTestUIEditorDockHost(
const UIEditorDockHostLayout& layout,
const ::XCEngine::UI::UIPoint& point);

View File

@@ -653,6 +653,14 @@ UIEditorDockHostLayout BuildUIEditorDockHostLayout(
return layout;
}
UIEditorDockHostDropPreviewLayout ResolveUIEditorDockHostDropPreviewLayout(
const UIEditorDockHostLayout& layout,
const UIEditorDockHostDropPreviewState& state) {
UIEditorDockHostState dockHostState = {};
dockHostState.dropPreview = state;
return ResolveDropPreviewLayout(layout, dockHostState);
}
UIEditorDockHostHitTarget HitTestUIEditorDockHost(
const UIEditorDockHostLayout& layout,
const UIPoint& point) {