From 307259091e44fe563bf31e678d15bd317eed1bf7 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Tue, 14 Apr 2026 16:19:23 +0800 Subject: [PATCH] Implement multi-window detached tab host flow --- new_editor/app/Application.cpp | 1094 ++++++++++++++++- new_editor/app/Application.h | 124 +- new_editor/app/Core/ProductEditorContext.cpp | 6 +- new_editor/app/Core/ProductEditorContext.h | 4 +- .../app/Panels/ProductHierarchyPanel.cpp | 9 +- new_editor/app/Panels/ProductHierarchyPanel.h | 1 + new_editor/app/Panels/ProductProjectPanel.cpp | 32 +- new_editor/app/Panels/ProductProjectPanel.h | 1 + new_editor/app/Shell/ProductShellAsset.cpp | 9 +- new_editor/app/Shell/ProductShellAsset.h | 9 +- .../app/Workspace/ProductEditorWorkspace.cpp | 13 +- .../app/Workspace/ProductEditorWorkspace.h | 4 +- 12 files changed, 1210 insertions(+), 96 deletions(-) diff --git a/new_editor/app/Application.cpp b/new_editor/app/Application.cpp index 06c6f327..94519a65 100644 --- a/new_editor/app/Application.cpp +++ b/new_editor/app/Application.cpp @@ -10,8 +10,10 @@ #include #include +#include #include #include +#include #include #include @@ -42,6 +44,10 @@ constexpr UINT kDefaultDpi = 96u; constexpr float kBaseDpiScale = 96.0f; constexpr float kBorderlessTitleBarHeightDips = 28.0f; constexpr float kBorderlessTitleBarFontSize = 12.0f; +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); +const UIColor kShellMutedTextColor(0.70f, 0.70f, 0.70f, 1.0f); constexpr DWORD kBorderlessWindowStyle = WS_POPUP | WS_THICKFRAME; @@ -429,8 +435,223 @@ std::string DescribeHierarchyPanelEvent(const App::ProductHierarchyPanel::Event& return stream.str(); } +struct CrossWindowDockDropTarget { + bool valid = false; + std::string nodeId = {}; + UIEditorWorkspaceDockPlacement placement = UIEditorWorkspaceDockPlacement::Center; + std::size_t insertionIndex = Widgets::UIEditorTabStripInvalidIndex; +}; + +bool IsPointInsideRect( + const UIRect& rect, + const UIPoint& point) { + return point.x >= rect.x && + point.x <= rect.x + rect.width && + point.y >= rect.y && + point.y <= rect.y + rect.height; +} + +std::size_t ResolveCrossWindowDropInsertionIndex( + const Widgets::UIEditorDockHostTabStackLayout& tabStack, + const UIPoint& point) { + if (!IsPointInsideRect(tabStack.tabStripLayout.headerRect, point)) { + return Widgets::UIEditorTabStripInvalidIndex; + } + + std::size_t insertionIndex = 0u; + for (const UIRect& rect : tabStack.tabStripLayout.tabHeaderRects) { + const float midpoint = rect.x + rect.width * 0.5f; + if (point.x > midpoint) { + ++insertionIndex; + } + } + return insertionIndex; +} + +UIEditorWorkspaceDockPlacement ResolveCrossWindowDockPlacement( + const Widgets::UIEditorDockHostTabStackLayout& tabStack, + const UIPoint& point) { + if (IsPointInsideRect(tabStack.tabStripLayout.headerRect, point)) { + return UIEditorWorkspaceDockPlacement::Center; + } + + const float leftDistance = point.x - tabStack.bounds.x; + const float rightDistance = tabStack.bounds.x + tabStack.bounds.width - point.x; + const float topDistance = point.y - tabStack.bounds.y; + const float bottomDistance = tabStack.bounds.y + tabStack.bounds.height - point.y; + const float minHorizontalThreshold = tabStack.bounds.width * 0.25f; + const float minVerticalThreshold = tabStack.bounds.height * 0.25f; + const float nearestEdge = + (std::min)((std::min)(leftDistance, rightDistance), (std::min)(topDistance, bottomDistance)); + + if (nearestEdge == leftDistance && leftDistance <= minHorizontalThreshold) { + return UIEditorWorkspaceDockPlacement::Left; + } + if (nearestEdge == rightDistance && rightDistance <= minHorizontalThreshold) { + return UIEditorWorkspaceDockPlacement::Right; + } + if (nearestEdge == topDistance && topDistance <= minVerticalThreshold) { + return UIEditorWorkspaceDockPlacement::Top; + } + if (nearestEdge == bottomDistance && bottomDistance <= minVerticalThreshold) { + return UIEditorWorkspaceDockPlacement::Bottom; + } + + return UIEditorWorkspaceDockPlacement::Center; +} + +bool TryResolveCrossWindowDockDropTarget( + const Widgets::UIEditorDockHostLayout& layout, + const UIPoint& point, + CrossWindowDockDropTarget& outTarget) { + outTarget = {}; + if (!IsPointInsideRect(layout.bounds, point)) { + return false; + } + + const Widgets::UIEditorDockHostHitTarget hitTarget = + Widgets::HitTestUIEditorDockHost(layout, point); + for (const Widgets::UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) { + if ((!hitTarget.nodeId.empty() && tabStack.nodeId != hitTarget.nodeId) || + !IsPointInsideRect(tabStack.bounds, point)) { + continue; + } + + outTarget.valid = true; + outTarget.nodeId = tabStack.nodeId; + outTarget.placement = ResolveCrossWindowDockPlacement(tabStack, point); + if (outTarget.placement == UIEditorWorkspaceDockPlacement::Center) { + outTarget.insertionIndex = ResolveCrossWindowDropInsertionIndex(tabStack, point); + if (outTarget.insertionIndex == Widgets::UIEditorTabStripInvalidIndex) { + outTarget.insertionIndex = tabStack.items.size(); + } + } + return true; + } + + for (const Widgets::UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) { + if (!IsPointInsideRect(tabStack.bounds, point)) { + continue; + } + + outTarget.valid = true; + outTarget.nodeId = tabStack.nodeId; + outTarget.placement = ResolveCrossWindowDockPlacement(tabStack, point); + if (outTarget.placement == UIEditorWorkspaceDockPlacement::Center) { + outTarget.insertionIndex = tabStack.items.size(); + } + return true; + } + + return false; +} + } // namespace +Application::Application() = default; +Application::~Application() = default; + +Application::ManagedWindowState* Application::FindWindowState(HWND hwnd) { + if (hwnd == nullptr) { + return nullptr; + } + + for (const std::unique_ptr& windowState : m_windows) { + if (windowState != nullptr && windowState->hwnd == hwnd) { + return windowState.get(); + } + } + + return nullptr; +} + +const Application::ManagedWindowState* Application::FindWindowState(HWND hwnd) const { + if (hwnd == nullptr) { + return nullptr; + } + + for (const std::unique_ptr& windowState : m_windows) { + if (windowState != nullptr && windowState->hwnd == hwnd) { + return windowState.get(); + } + } + + return nullptr; +} + +Application::ManagedWindowState* Application::FindWindowState(std::string_view windowId) { + if (windowId.empty()) { + return nullptr; + } + + for (const std::unique_ptr& windowState : m_windows) { + if (windowState != nullptr && windowState->windowId == windowId) { + return windowState.get(); + } + } + + return nullptr; +} + +const Application::ManagedWindowState* Application::FindWindowState(std::string_view windowId) const { + if (windowId.empty()) { + return nullptr; + } + + for (const std::unique_ptr& windowState : m_windows) { + if (windowState != nullptr && windowState->windowId == windowId) { + return windowState.get(); + } + } + + return nullptr; +} + +Application::ManagedWindowState* Application::FindPrimaryWindowState() { + for (const std::unique_ptr& windowState : m_windows) { + if (windowState != nullptr && windowState->primary) { + return windowState.get(); + } + } + + return nullptr; +} + +const Application::ManagedWindowState* Application::FindPrimaryWindowState() const { + for (const std::unique_ptr& windowState : m_windows) { + if (windowState != nullptr && windowState->primary) { + return windowState.get(); + } + } + + return nullptr; +} + +Application::ManagedWindowState& Application::RequireCurrentWindowState() { + assert(m_currentWindowState != nullptr); + return *m_currentWindowState; +} + +const Application::ManagedWindowState& Application::RequireCurrentWindowState() const { + assert(m_currentWindowState != nullptr); + return *m_currentWindowState; +} + +#define m_hwnd RequireCurrentWindowState().hwnd +#define m_renderer RequireCurrentWindowState().renderer +#define m_windowRenderer RequireCurrentWindowState().windowRenderer +#define m_windowRenderLoop RequireCurrentWindowState().windowRenderLoop +#define m_autoScreenshot RequireCurrentWindowState().autoScreenshot +#define m_inputModifierTracker RequireCurrentWindowState().inputModifierTracker +#define m_workspaceController RequireCurrentWindowState().workspaceController +#define m_editorWorkspace RequireCurrentWindowState().editorWorkspace +#define m_pendingInputEvents RequireCurrentWindowState().pendingInputEvents +#define m_trackingMouseLeave RequireCurrentWindowState().trackingMouseLeave +#define m_renderReady RequireCurrentWindowState().renderReady +#define m_titleBarLogoIcon RequireCurrentWindowState().titleBarLogoIcon +#define m_borderlessWindowChromeState RequireCurrentWindowState().borderlessWindowChromeState +#define m_hostRuntime RequireCurrentWindowState().hostRuntime + int Application::Run(HINSTANCE hInstance, int nCmdShow) { if (!Initialize(hInstance, nCmdShow)) { Shutdown(); @@ -438,46 +659,88 @@ int Application::Run(HINSTANCE hInstance, int nCmdShow) { } MSG message = {}; - while (message.message != WM_QUIT) { - if (PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) { + while (true) { + while (PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) { + if (message.message == WM_QUIT) { + Shutdown(); + return static_cast(message.wParam); + } + TranslateMessage(&message); DispatchMessageW(&message); - continue; } - RenderFrame(); + DestroyClosedWindows(); + ProcessPendingGlobalTabDragStarts(); + ProcessPendingDetachRequests(); + DestroyClosedWindows(); + if (m_windows.empty()) { + break; + } + + RenderAllWindows(); } Shutdown(); - return static_cast(message.wParam); + return 0; } bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) { m_hInstance = hInstance; + m_repoRoot = ResolveRepoRootPath(); + m_shutdownRequested = false; EnableDpiAwareness(); - const std::filesystem::path repoRoot = ResolveRepoRootPath(); const std::filesystem::path logRoot = GetExecutableDirectory() / "logs"; InitializeUIEditorRuntimeTrace(logRoot); SetUnhandledExceptionFilter(&Application::HandleUnhandledException); LogRuntimeTrace("app", "initialize begin"); - if (!m_editorContext.Initialize(repoRoot)) { + if (!m_editorContext.Initialize(m_repoRoot)) { LogRuntimeTrace( "app", "shell asset validation failed: " + m_editorContext.GetValidationMessage()); return false; } - m_workspaceController = m_editorContext.BuildWorkspaceController(); + if (!RegisterWindowClass()) { + return false; + } + m_editorContext.SetExitRequestHandler([this]() { + if (ManagedWindowState* primaryWindowState = FindPrimaryWindowState(); + primaryWindowState != nullptr && + primaryWindowState->hwnd != nullptr) { + PostMessageW(primaryWindowState->hwnd, WM_CLOSE, 0, 0); + } + }); + + ManagedWindowCreateParams primaryParams = {}; + primaryParams.windowId = "main-window"; + primaryParams.title = std::wstring(kWindowTitle); + primaryParams.showCommand = nCmdShow; + primaryParams.primary = true; + primaryParams.autoCaptureOnStartup = true; + if (CreateManagedWindow(m_editorContext.BuildWorkspaceController(), primaryParams) == nullptr) { + LogRuntimeTrace("app", "primary window creation failed"); + return false; + } + + LogRuntimeTrace("app", "initialize completed"); + return true; +} + +bool Application::RegisterWindowClass() { + if (m_windowClassAtom != 0) { + return true; + } WNDCLASSEXW windowClass = {}; windowClass.cbSize = sizeof(windowClass); windowClass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS; windowClass.lpfnWndProc = &Application::WndProc; - windowClass.hInstance = hInstance; + windowClass.hInstance = m_hInstance; windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW); windowClass.hIcon = static_cast( LoadImageW( - hInstance, + m_hInstance, MAKEINTRESOURCEW(IDI_APP_ICON), IMAGE_ICON, 0, @@ -485,7 +748,7 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) { LR_DEFAULTSIZE)); windowClass.hIconSm = static_cast( LoadImageW( - hInstance, + m_hInstance, MAKEINTRESOURCEW(IDI_APP_ICON_SMALL), IMAGE_ICON, GetSystemMetrics(SM_CXSMICON), @@ -498,67 +761,119 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) { return false; } - m_hwnd = CreateWindowExW( + return true; +} + +Application::ManagedWindowState* Application::CreateManagedWindow( + UIEditorWorkspaceController workspaceController, + const ManagedWindowCreateParams& params) { + auto windowState = std::make_unique(); + windowState->windowId = params.windowId; + windowState->title = params.title.empty() ? std::wstring(L"XCEngine Editor") : params.title; + windowState->primary = params.primary; + windowState->workspaceController = std::move(workspaceController); + ManagedWindowState* const rawWindowState = windowState.get(); + m_windows.push_back(std::move(windowState)); + + const auto eraseRawWindowState = [this, rawWindowState]() { + const auto it = std::find_if( + m_windows.begin(), + m_windows.end(), + [rawWindowState](const std::unique_ptr& candidate) { + return candidate.get() == rawWindowState; + }); + if (it != m_windows.end()) { + m_windows.erase(it); + } + }; + + ManagedWindowState* const previousWindowState = m_currentWindowState; + m_currentWindowState = rawWindowState; + m_pendingCreateWindowState = rawWindowState; + rawWindowState->hwnd = CreateWindowExW( WS_EX_APPWINDOW, kWindowClassName, - kWindowTitle, + rawWindowState->title.c_str(), kBorderlessWindowStyle, - CW_USEDEFAULT, - CW_USEDEFAULT, - 1540, - 940, + params.initialX, + params.initialY, + params.initialWidth, + params.initialHeight, nullptr, nullptr, - hInstance, + m_hInstance, this); - if (m_hwnd == nullptr) { - LogRuntimeTrace("app", "window creation failed"); - return false; + m_pendingCreateWindowState = nullptr; + if (rawWindowState->hwnd == nullptr) { + m_currentWindowState = previousWindowState; + eraseRawWindowState(); + return nullptr; } - if (windowClass.hIcon != nullptr) { - SendMessageW(m_hwnd, WM_SETICON, ICON_BIG, reinterpret_cast(windowClass.hIcon)); + + auto failWindowInitialization = [&](std::string_view message) { + LogRuntimeTrace("app", std::string(message)); + DestroyManagedWindow(*rawWindowState); + m_currentWindowState = previousWindowState; + eraseRawWindowState(); + return static_cast(nullptr); + }; + + const HICON bigIcon = static_cast( + LoadImageW( + m_hInstance, + MAKEINTRESOURCEW(IDI_APP_ICON), + IMAGE_ICON, + 0, + 0, + LR_DEFAULTSIZE)); + const HICON smallIcon = static_cast( + LoadImageW( + m_hInstance, + MAKEINTRESOURCEW(IDI_APP_ICON_SMALL), + IMAGE_ICON, + GetSystemMetrics(SM_CXSMICON), + GetSystemMetrics(SM_CYSMICON), + LR_DEFAULTCOLOR)); + if (bigIcon != nullptr) { + SendMessageW(m_hwnd, WM_SETICON, ICON_BIG, reinterpret_cast(bigIcon)); } - if (windowClass.hIconSm != nullptr) { - SendMessageW(m_hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast(windowClass.hIconSm)); + if (smallIcon != nullptr) { + SendMessageW(m_hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast(smallIcon)); } + Host::RefreshBorderlessWindowDwmDecorations(m_hwnd); m_hostRuntime.Reset(); m_hostRuntime.SetWindowDpi(QueryWindowDpi(m_hwnd)); m_renderer.SetDpiScale(GetDpiScale()); - m_editorContext.SetExitRequestHandler([this]() { - if (m_hwnd != nullptr) { - PostMessageW(m_hwnd, WM_CLOSE, 0, 0); - } - }); std::ostringstream dpiTrace = {}; dpiTrace << "initial dpi=" << m_hostRuntime.GetWindowDpi() << " scale=" << GetDpiScale(); LogRuntimeTrace("window", dpiTrace.str()); if (!m_renderer.Initialize(m_hwnd)) { - LogRuntimeTrace("app", "renderer initialization failed"); - return false; + return failWindowInitialization("renderer initialization failed"); } + RECT clientRect = {}; GetClientRect(m_hwnd, &clientRect); const int clientWidth = (std::max)(clientRect.right - clientRect.left, 1L); const int clientHeight = (std::max)(clientRect.bottom - clientRect.top, 1L); if (!m_windowRenderer.Initialize(m_hwnd, clientWidth, clientHeight)) { - LogRuntimeTrace("app", "d3d12 window renderer initialization failed"); - return false; + return failWindowInitialization("d3d12 window renderer initialization failed"); } + const Host::D3D12WindowRenderLoopAttachResult attachResult = m_windowRenderLoop.Attach(m_renderer, m_windowRenderer); if (!attachResult.interopWarning.empty()) { - LogRuntimeTrace( - "app", - attachResult.interopWarning); + LogRuntimeTrace("app", attachResult.interopWarning); } + m_editorContext.AttachTextMeasurer(m_renderer); - m_editorWorkspace.Initialize(repoRoot, m_renderer); + m_editorWorkspace.Initialize(m_repoRoot, m_renderer); m_editorWorkspace.AttachViewportWindowRenderer(m_windowRenderer); m_editorWorkspace.SetViewportSurfacePresentationEnabled( attachResult.hasViewportSurfacePresentation); + std::string titleBarLogoError = {}; if (!LoadEmbeddedPngTexture(m_renderer, IDR_PNG_LOGO_ICON, m_titleBarLogoIcon, titleBarLogoError)) { LogRuntimeTrace("icons", "titlebar logo_icon.png: " + titleBarLogoError); @@ -566,6 +881,7 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) { if (!m_editorWorkspace.GetBuiltInIconError().empty()) { LogRuntimeTrace("icons", m_editorWorkspace.GetBuiltInIconError()); } + LogRuntimeTrace( "app", "workspace initialized: " + @@ -574,36 +890,591 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) { m_editorWorkspace.GetShellInteractionState())); m_renderReady = true; - ShowWindow(m_hwnd, nCmdShow); + ShowWindow(m_hwnd, params.showCommand); UpdateWindow(m_hwnd); m_autoScreenshot.Initialize(m_editorContext.GetShellAsset().captureRootPath); - if (IsAutoCaptureOnStartupEnabled()) { + if (params.autoCaptureOnStartup && IsAutoCaptureOnStartupEnabled()) { m_autoScreenshot.RequestCapture("startup"); m_editorContext.SetStatus("Capture", "Startup capture requested."); } - LogRuntimeTrace("app", "initialize completed"); + + m_currentWindowState = previousWindowState; + return rawWindowState; +} + +void Application::DestroyManagedWindow(ManagedWindowState& windowState) { + if (GetCapture() == windowState.hwnd) { + ReleaseCapture(); + } + + windowState.renderReady = false; + windowState.autoScreenshot.Shutdown(); + windowState.editorWorkspace.Shutdown(); + windowState.renderer.ReleaseTexture(windowState.titleBarLogoIcon); + windowState.windowRenderLoop.Detach(); + windowState.windowRenderer.Shutdown(); + windowState.renderer.Shutdown(); + if (windowState.hwnd != nullptr && IsWindow(windowState.hwnd)) { + DestroyWindow(windowState.hwnd); + } + windowState.hwnd = nullptr; +} + +void Application::DestroyClosedWindows() { + for (auto it = m_windows.begin(); it != m_windows.end();) { + ManagedWindowState* const windowState = it->get(); + if (windowState == nullptr || windowState->hwnd != nullptr) { + ++it; + continue; + } + + if (m_currentWindowState == windowState) { + m_currentWindowState = nullptr; + } + if (m_pendingCreateWindowState == windowState) { + m_pendingCreateWindowState = nullptr; + } + + DestroyManagedWindow(*windowState); + it = m_windows.erase(it); + } +} + +void Application::RenderAllWindows() { + for (const std::unique_ptr& windowState : m_windows) { + if (windowState == nullptr || windowState->hwnd == nullptr) { + continue; + } + + ManagedWindowState* const previousWindowState = m_currentWindowState; + m_currentWindowState = windowState.get(); + RenderFrame(); + m_currentWindowState = previousWindowState; + } +} + +UIEditorWindowWorkspaceSet Application::BuildWindowWorkspaceSet(std::string_view activeWindowId) const { + UIEditorWindowWorkspaceSet windowSet = {}; + if (const ManagedWindowState* primaryWindowState = FindPrimaryWindowState(); + primaryWindowState != nullptr) { + windowSet.primaryWindowId = primaryWindowState->windowId; + } + + for (const std::unique_ptr& windowState : m_windows) { + if (windowState == nullptr || windowState->hwnd == nullptr) { + continue; + } + + UIEditorWindowWorkspaceState entry = {}; + entry.windowId = windowState->windowId; + entry.workspace = windowState->workspaceController.GetWorkspace(); + entry.session = windowState->workspaceController.GetSession(); + windowSet.windows.push_back(std::move(entry)); + } + + if (!activeWindowId.empty() && FindWindowState(activeWindowId) != nullptr) { + windowSet.activeWindowId = std::string(activeWindowId); + } else if (m_currentWindowState != nullptr && m_currentWindowState->hwnd != nullptr) { + windowSet.activeWindowId = m_currentWindowState->windowId; + } else { + windowSet.activeWindowId = windowSet.primaryWindowId; + } + + return windowSet; +} + +UIEditorWorkspaceController Application::BuildWorkspaceControllerForWindow( + const UIEditorWindowWorkspaceState& windowState) const { + return UIEditorWorkspaceController( + m_editorContext.GetShellAsset().panelRegistry, + windowState.workspace, + windowState.session); +} + +std::wstring Application::BuildManagedWindowTitle( + const UIEditorWorkspaceController& workspaceController) const { + const std::string& activePanelId = workspaceController.GetWorkspace().activePanelId; + if (const UIEditorPanelDescriptor* descriptor = + FindUIEditorPanelDescriptor( + workspaceController.GetPanelRegistry(), + activePanelId); + descriptor != nullptr && + !descriptor->defaultTitle.empty()) { + const std::string titleText = descriptor->defaultTitle + " - XCEngine Editor"; + return std::wstring(titleText.begin(), titleText.end()); + } + + return std::wstring(L"XCEngine Editor"); +} + +RECT Application::BuildDetachedWindowRect(const POINT& screenPoint) const { + RECT rect = { + screenPoint.x - 420, + screenPoint.y - 24, + screenPoint.x - 420 + 960, + screenPoint.y - 24 + 720 + }; + + const HMONITOR monitor = MonitorFromPoint(screenPoint, MONITOR_DEFAULTTONEAREST); + MONITORINFO monitorInfo = {}; + monitorInfo.cbSize = sizeof(monitorInfo); + if (monitor != nullptr && GetMonitorInfoW(monitor, &monitorInfo)) { + const RECT& workArea = monitorInfo.rcWork; + const LONG width = rect.right - rect.left; + const LONG height = rect.bottom - rect.top; + rect.left = (std::max)(workArea.left, (std::min)(rect.left, workArea.right - width)); + rect.top = (std::max)(workArea.top, (std::min)(rect.top, workArea.bottom - height)); + rect.right = rect.left + width; + rect.bottom = rect.top + height; + } + + return rect; +} + +void Application::ResetManagedWindowInteractionState(ManagedWindowState& windowState) { + if (GetCapture() == windowState.hwnd) { + ReleaseCapture(); + } + + windowState.pendingInputEvents.clear(); + windowState.trackingMouseLeave = false; + windowState.inputModifierTracker.Reset(); + windowState.editorWorkspace.ResetInteractionState(); + windowState.borderlessWindowChromeState = {}; + windowState.hostRuntime.EndBorderlessResize(); + windowState.hostRuntime.EndBorderlessWindowDragRestore(); + windowState.hostRuntime.EndInteractiveResize(); + windowState.hostRuntime.SetHoveredBorderlessResizeEdge( + Host::BorderlessWindowResizeEdge::None); + windowState.hostRuntime.ClearPredictedClientPixelSize(); + windowState.detachRequested = false; + windowState.detachedNodeId.clear(); + windowState.detachedPanelId.clear(); + windowState.detachScreenPoint = {}; + windowState.pendingGlobalTabDragStart = false; + windowState.pendingGlobalTabDragNodeId.clear(); + windowState.pendingGlobalTabDragPanelId.clear(); + windowState.pendingGlobalTabDragScreenPoint = {}; +} + +bool Application::SynchronizeManagedWindowsFromWindowSet( + const UIEditorWindowWorkspaceSet& windowSet, + std::string_view preferredNewWindowId, + const POINT& preferredScreenPoint) { + std::vector windowIdsInSet = {}; + windowIdsInSet.reserve(windowSet.windows.size()); + + for (const UIEditorWindowWorkspaceState& entry : windowSet.windows) { + windowIdsInSet.push_back(entry.windowId); + if (ManagedWindowState* existingWindowState = FindWindowState(entry.windowId); + existingWindowState != nullptr) { + existingWindowState->workspaceController = BuildWorkspaceControllerForWindow(entry); + ResetManagedWindowInteractionState(*existingWindowState); + if (!existingWindowState->primary) { + existingWindowState->title = BuildManagedWindowTitle(existingWindowState->workspaceController); + if (existingWindowState->hwnd != nullptr) { + SetWindowTextW(existingWindowState->hwnd, existingWindowState->title.c_str()); + } + } + continue; + } + + ManagedWindowCreateParams createParams = {}; + createParams.windowId = entry.windowId; + createParams.primary = entry.windowId == windowSet.primaryWindowId; + createParams.title = + createParams.primary + ? std::wstring(kWindowTitle) + : BuildManagedWindowTitle(BuildWorkspaceControllerForWindow(entry)); + if (entry.windowId == preferredNewWindowId) { + const RECT detachedRect = BuildDetachedWindowRect(preferredScreenPoint); + createParams.initialX = detachedRect.left; + createParams.initialY = detachedRect.top; + createParams.initialWidth = detachedRect.right - detachedRect.left; + createParams.initialHeight = detachedRect.bottom - detachedRect.top; + } + + if (CreateManagedWindow(BuildWorkspaceControllerForWindow(entry), createParams) == nullptr) { + return false; + } + } + + for (const std::unique_ptr& windowState : m_windows) { + if (windowState == nullptr || + windowState->hwnd == nullptr || + windowState->primary) { + continue; + } + + const bool existsInWindowSet = + std::find(windowIdsInSet.begin(), windowIdsInSet.end(), windowState->windowId) != + windowIdsInSet.end(); + if (!existsInWindowSet) { + PostMessageW(windowState->hwnd, WM_CLOSE, 0, 0); + } + } + return true; } +void Application::BeginGlobalTabDragSession( + std::string_view panelWindowId, + std::string_view sourceNodeId, + std::string_view panelId, + const POINT& screenPoint) { + 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; +} + +void Application::EndGlobalTabDragSession() { + if (!m_globalTabDragSession.active) { + return; + } + + if (ManagedWindowState* ownerWindowState = + FindWindowState(m_globalTabDragSession.panelWindowId); + ownerWindowState != nullptr && + ownerWindowState->hwnd != nullptr && + GetCapture() == ownerWindowState->hwnd) { + ReleaseCapture(); + } + + m_globalTabDragSession = {}; +} + +Application::ManagedWindowState* Application::FindTopmostWindowStateAtScreenPoint( + const POINT& screenPoint, + std::string_view excludedWindowId) { + if (const HWND hitWindow = WindowFromPoint(screenPoint); hitWindow != nullptr) { + const HWND rootWindow = GetAncestor(hitWindow, GA_ROOT); + if (ManagedWindowState* windowState = FindWindowState(rootWindow); + windowState != nullptr && + windowState->windowId != excludedWindowId) { + return windowState; + } + } + + for (auto it = m_windows.rbegin(); it != m_windows.rend(); ++it) { + ManagedWindowState* const windowState = it->get(); + if (windowState == nullptr || + windowState->hwnd == nullptr || + windowState->windowId == excludedWindowId) { + continue; + } + + RECT windowRect = {}; + if (GetWindowRect(windowState->hwnd, &windowRect) && + screenPoint.x >= windowRect.left && + screenPoint.x < windowRect.right && + screenPoint.y >= windowRect.top && + screenPoint.y < windowRect.bottom) { + return windowState; + } + } + + return nullptr; +} + +const Application::ManagedWindowState* Application::FindTopmostWindowStateAtScreenPoint( + const POINT& screenPoint, + std::string_view excludedWindowId) const { + return const_cast(this)->FindTopmostWindowStateAtScreenPoint( + screenPoint, + excludedWindowId); +} + +bool Application::TryStartGlobalTabDrag(ManagedWindowState& sourceWindowState) { + if (!sourceWindowState.pendingGlobalTabDragStart || + sourceWindowState.pendingGlobalTabDragNodeId.empty() || + sourceWindowState.pendingGlobalTabDragPanelId.empty()) { + return false; + } + + const std::string sourceNodeId = sourceWindowState.pendingGlobalTabDragNodeId; + const std::string panelId = sourceWindowState.pendingGlobalTabDragPanelId; + const POINT screenPoint = sourceWindowState.pendingGlobalTabDragScreenPoint; + sourceWindowState.pendingGlobalTabDragStart = false; + sourceWindowState.pendingGlobalTabDragNodeId.clear(); + sourceWindowState.pendingGlobalTabDragPanelId.clear(); + sourceWindowState.pendingGlobalTabDragScreenPoint = {}; + + if (sourceWindowState.primary) { + UIEditorWindowWorkspaceController windowWorkspaceController( + m_editorContext.GetShellAsset().panelRegistry, + BuildWindowWorkspaceSet(sourceWindowState.windowId)); + const UIEditorWindowWorkspaceOperationResult result = + windowWorkspaceController.DetachPanelToNewWindow( + sourceWindowState.windowId, + sourceNodeId, + panelId); + if (result.status != UIEditorWindowWorkspaceOperationStatus::Changed) { + LogRuntimeTrace( + "drag", + "failed to start global tab drag from primary window: " + result.message); + return false; + } + + if (!SynchronizeManagedWindowsFromWindowSet( + windowWorkspaceController.GetWindowSet(), + result.targetWindowId, + screenPoint)) { + LogRuntimeTrace("drag", "failed to synchronize detached drag window state"); + return false; + } + + ManagedWindowState* detachedWindowState = FindWindowState(result.targetWindowId); + if (detachedWindowState == nullptr || detachedWindowState->hwnd == nullptr) { + LogRuntimeTrace("drag", "detached drag window was not created."); + return false; + } + + BeginGlobalTabDragSession( + detachedWindowState->windowId, + detachedWindowState->workspaceController.GetWorkspace().root.nodeId, + panelId, + screenPoint); + SetCapture(detachedWindowState->hwnd); + SetForegroundWindow(detachedWindowState->hwnd); + LogRuntimeTrace( + "drag", + "started global tab drag by detaching panel '" + panelId + + "' into window '" + detachedWindowState->windowId + "'"); + return true; + } + + ResetManagedWindowInteractionState(sourceWindowState); + BeginGlobalTabDragSession( + sourceWindowState.windowId, + sourceNodeId, + panelId, + screenPoint); + if (sourceWindowState.hwnd != nullptr) { + SetCapture(sourceWindowState.hwnd); + } + LogRuntimeTrace( + "drag", + "started global tab drag from detached window '" + sourceWindowState.windowId + + "' panel '" + panelId + "'"); + return true; +} + +void Application::ProcessPendingGlobalTabDragStarts() { + if (m_globalTabDragSession.active) { + return; + } + + for (const std::unique_ptr& windowState : m_windows) { + if (windowState == nullptr || windowState->hwnd == nullptr) { + continue; + } + + if (TryStartGlobalTabDrag(*windowState)) { + return; + } + } +} + +bool Application::TryProcessDetachRequest(ManagedWindowState& sourceWindowState) { + if (!sourceWindowState.detachRequested || + sourceWindowState.detachedNodeId.empty() || + sourceWindowState.detachedPanelId.empty()) { + return false; + } + + const std::string sourceWindowId = sourceWindowState.windowId; + const std::string sourceNodeId = sourceWindowState.detachedNodeId; + const std::string panelId = sourceWindowState.detachedPanelId; + const POINT screenPoint = sourceWindowState.detachScreenPoint; + sourceWindowState.detachRequested = false; + sourceWindowState.detachedNodeId.clear(); + sourceWindowState.detachedPanelId.clear(); + sourceWindowState.detachScreenPoint = {}; + + UIEditorWindowWorkspaceController windowWorkspaceController( + m_editorContext.GetShellAsset().panelRegistry, + BuildWindowWorkspaceSet(sourceWindowId)); + const UIEditorWindowWorkspaceOperationResult result = + windowWorkspaceController.DetachPanelToNewWindow( + sourceWindowId, + sourceNodeId, + panelId); + if (result.status != UIEditorWindowWorkspaceOperationStatus::Changed) { + LogRuntimeTrace("detach", "detach request rejected: " + result.message); + return false; + } + + if (!SynchronizeManagedWindowsFromWindowSet( + windowWorkspaceController.GetWindowSet(), + result.targetWindowId, + screenPoint)) { + LogRuntimeTrace("detach", "failed to synchronize detached window state"); + return false; + } + + if (ManagedWindowState* detachedWindowState = FindWindowState(result.targetWindowId); + detachedWindowState != nullptr && + detachedWindowState->hwnd != nullptr) { + SetForegroundWindow(detachedWindowState->hwnd); + } + + LogRuntimeTrace( + "detach", + "detached panel '" + panelId + "' from window '" + sourceWindowId + + "' to window '" + result.targetWindowId + "'"); + return true; +} + +void Application::ProcessPendingDetachRequests() { + if (m_globalTabDragSession.active) { + return; + } + + std::vector windowsToProcess = {}; + for (const std::unique_ptr& windowState : m_windows) { + if (windowState != nullptr && windowState->hwnd != nullptr && windowState->detachRequested) { + windowsToProcess.push_back(windowState.get()); + } + } + + for (ManagedWindowState* windowState : windowsToProcess) { + if (windowState != nullptr) { + TryProcessDetachRequest(*windowState); + } + } +} + +bool Application::HandleGlobalTabDragPointerMove(HWND hwnd) { + if (!m_globalTabDragSession.active) { + return false; + } + + const ManagedWindowState* ownerWindowState = FindWindowState(m_globalTabDragSession.panelWindowId); + if (ownerWindowState == nullptr || ownerWindowState->hwnd != hwnd) { + return false; + } + + POINT screenPoint = {}; + if (GetCursorPos(&screenPoint)) { + m_globalTabDragSession.screenPoint = screenPoint; + } + return true; +} + +bool Application::HandleGlobalTabDragPointerButtonUp(HWND hwnd) { + if (!m_globalTabDragSession.active) { + return false; + } + + const ManagedWindowState* ownerWindowState = FindWindowState(m_globalTabDragSession.panelWindowId); + if (ownerWindowState == nullptr || ownerWindowState->hwnd != hwnd) { + return false; + } + + POINT screenPoint = m_globalTabDragSession.screenPoint; + GetCursorPos(&screenPoint); + + const std::string panelWindowId = m_globalTabDragSession.panelWindowId; + const std::string sourceNodeId = m_globalTabDragSession.sourceNodeId; + const std::string panelId = m_globalTabDragSession.panelId; + EndGlobalTabDragSession(); + + ManagedWindowState* targetWindowState = + FindTopmostWindowStateAtScreenPoint(screenPoint, panelWindowId); + if (targetWindowState == nullptr || targetWindowState->hwnd == nullptr) { + return true; + } + + const UIPoint targetPoint = + ConvertScreenPixelsToClientDips(*targetWindowState, screenPoint); + const Widgets::UIEditorDockHostLayout& targetLayout = + targetWindowState->editorWorkspace + .GetShellFrame() + .workspaceInteractionFrame + .dockHostFrame + .layout; + CrossWindowDockDropTarget dropTarget = {}; + if (!TryResolveCrossWindowDockDropTarget(targetLayout, targetPoint, dropTarget)) { + return true; + } + + UIEditorWindowWorkspaceController windowWorkspaceController( + m_editorContext.GetShellAsset().panelRegistry, + BuildWindowWorkspaceSet(targetWindowState->windowId)); + const UIEditorWindowWorkspaceOperationResult result = + dropTarget.placement == UIEditorWorkspaceDockPlacement::Center + ? windowWorkspaceController.MovePanelToStack( + panelWindowId, + sourceNodeId, + panelId, + targetWindowState->windowId, + dropTarget.nodeId, + dropTarget.insertionIndex) + : windowWorkspaceController.DockPanelRelative( + panelWindowId, + sourceNodeId, + panelId, + targetWindowState->windowId, + dropTarget.nodeId, + dropTarget.placement); + if (result.status != UIEditorWindowWorkspaceOperationStatus::Changed) { + LogRuntimeTrace("drag", "cross-window drop rejected: " + result.message); + return true; + } + + if (!SynchronizeManagedWindowsFromWindowSet( + windowWorkspaceController.GetWindowSet(), + {}, + screenPoint)) { + LogRuntimeTrace("drag", "failed to synchronize windows after cross-window drop"); + return true; + } + + if (targetWindowState->hwnd != nullptr) { + SetForegroundWindow(targetWindowState->hwnd); + } + LogRuntimeTrace( + "drag", + "committed cross-window drop panel '" + panelId + + "' into window '" + targetWindowState->windowId + "'"); + return true; +} + +void Application::HandleDestroyedWindow(HWND hwnd) { + if (ManagedWindowState* windowState = FindWindowState(hwnd); windowState != nullptr) { + windowState->hwnd = nullptr; + if (windowState->primary) { + m_shutdownRequested = true; + for (const std::unique_ptr& otherWindowState : m_windows) { + if (otherWindowState != nullptr && + otherWindowState.get() != windowState && + otherWindowState->hwnd != nullptr) { + PostMessageW(otherWindowState->hwnd, WM_CLOSE, 0, 0); + } + } + } + } +} + void Application::Shutdown() { LogRuntimeTrace("app", "shutdown begin"); - m_renderReady = false; - if (GetCapture() == m_hwnd) { - ReleaseCapture(); - } + m_shutdownRequested = true; + for (const std::unique_ptr& windowState : m_windows) { + if (windowState == nullptr) { + continue; + } - m_autoScreenshot.Shutdown(); - m_editorWorkspace.Shutdown(); - m_renderer.ReleaseTexture(m_titleBarLogoIcon); - m_windowRenderLoop.Detach(); - m_windowRenderer.Shutdown(); - m_renderer.Shutdown(); - - if (m_hwnd != nullptr && IsWindow(m_hwnd)) { - DestroyWindow(m_hwnd); + ManagedWindowState* const previousWindowState = m_currentWindowState; + m_currentWindowState = windowState.get(); + DestroyManagedWindow(*windowState); + m_currentWindowState = previousWindowState; } - m_hwnd = nullptr; + m_windows.clear(); + m_currentWindowState = nullptr; + m_pendingCreateWindowState = nullptr; if (m_windowClassAtom != 0 && m_hInstance != nullptr) { UnregisterClassW(kWindowClassName, m_hInstance); @@ -632,7 +1503,7 @@ void Application::RenderFrame() { UIDrawList& drawList = drawData.EmplaceDrawList("XCEditorShell"); drawList.AddFilledRect( UIRect(0.0f, 0.0f, width, height), - UIColor(0.10f, 0.10f, 0.10f, 1.0f)); + kShellSurfaceColor); if (m_editorContext.IsValid()) { std::vector frameEvents = std::move(m_pendingInputEvents); @@ -652,14 +1523,23 @@ void Application::RenderFrame() { LogRuntimeTrace("viewport", frameContext.warning); } + m_editorContext.AttachTextMeasurer(m_renderer); m_editorWorkspace.Update( m_editorContext, m_workspaceController, workspaceBounds, frameEvents, - BuildCaptureStatusText()); + BuildCaptureStatusText(), + RequireCurrentWindowState().primary + ? App::ProductEditorShellVariant::Primary + : App::ProductEditorShellVariant::DetachedWindow); const UIEditorShellInteractionFrame& shellFrame = m_editorWorkspace.GetShellFrame(); + const UIEditorDockHostInteractionState& dockHostInteractionState = + m_editorWorkspace + .GetShellInteractionState() + .workspaceInteractionState + .dockHostInteractionState; if (IsVerboseRuntimeTraceEnabled() && (!frameEvents.empty() || shellFrame.result.workspaceResult.dockHostResult.layoutChanged || @@ -679,6 +1559,28 @@ void Application::RenderFrame() { "frame", frameTrace.str()); } + if (!m_globalTabDragSession.active && + !dockHostInteractionState.activeTabDragNodeId.empty() && + !dockHostInteractionState.activeTabDragPanelId.empty()) { + POINT screenPoint = {}; + GetCursorPos(&screenPoint); + RequireCurrentWindowState().pendingGlobalTabDragStart = true; + RequireCurrentWindowState().pendingGlobalTabDragNodeId = + dockHostInteractionState.activeTabDragNodeId; + RequireCurrentWindowState().pendingGlobalTabDragPanelId = + dockHostInteractionState.activeTabDragPanelId; + RequireCurrentWindowState().pendingGlobalTabDragScreenPoint = screenPoint; + } + if (shellFrame.result.workspaceResult.dockHostResult.detachRequested) { + POINT screenPoint = {}; + GetCursorPos(&screenPoint); + RequireCurrentWindowState().detachRequested = true; + RequireCurrentWindowState().detachedNodeId = + shellFrame.result.workspaceResult.dockHostResult.detachedNodeId; + RequireCurrentWindowState().detachedPanelId = + shellFrame.result.workspaceResult.dockHostResult.detachedPanelId; + RequireCurrentWindowState().detachScreenPoint = screenPoint; + } ApplyHostCaptureRequests(shellFrame.result); for (const App::ProductEditorWorkspaceTraceEntry& entry : m_editorWorkspace.GetTraceEntries()) { LogRuntimeTrace(entry.channel, entry.message); @@ -693,14 +1595,14 @@ void Application::RenderFrame() { drawList.AddText( UIPoint(28.0f, 28.0f), "Editor shell asset invalid.", - UIColor(0.92f, 0.92f, 0.92f, 1.0f), + kShellTextColor, 16.0f); drawList.AddText( UIPoint(28.0f, 54.0f), m_editorContext.GetValidationMessage().empty() ? std::string("Unknown validation error.") : m_editorContext.GetValidationMessage(), - UIColor(0.72f, 0.72f, 0.72f, 1.0f), + kShellMutedTextColor, 12.0f); } @@ -1214,13 +2116,13 @@ void Application::AppendBorderlessWindowChrome( (std::max)(0.0f, (layout.titleBarRect.height - iconExtent) * 0.5f); drawList.AddFilledRect( layout.titleBarRect, - UIColor(0.10f, 0.10f, 0.10f, 1.0f)); + kShellSurfaceColor); drawList.AddLine( UIPoint(layout.titleBarRect.x, layout.titleBarRect.y + layout.titleBarRect.height), UIPoint( layout.titleBarRect.x + layout.titleBarRect.width, layout.titleBarRect.y + layout.titleBarRect.height), - UIColor(0.14f, 0.14f, 0.14f, 1.0f), + kShellBorderColor, 1.0f); if (m_titleBarLogoIcon.IsValid()) { drawList.AddImage( @@ -1234,7 +2136,7 @@ void Application::AppendBorderlessWindowChrome( layout.titleBarRect.y + (std::max)(0.0f, (layout.titleBarRect.height - kBorderlessTitleBarFontSize) * 0.5f - 1.0f)), kWindowTitleText, - UIColor(0.92f, 0.92f, 0.92f, 1.0f), + kShellTextColor, kBorderlessTitleBarFontSize); Host::AppendBorderlessWindowChrome( drawList, @@ -1363,6 +2265,20 @@ UIPoint Application::ConvertClientPixelsToDips(LONG x, LONG y) const { PixelsToDips(static_cast(y))); } +UIPoint Application::ConvertScreenPixelsToClientDips( + const ManagedWindowState& windowState, + const POINT& screenPoint) const { + POINT clientPoint = screenPoint; + if (windowState.hwnd != nullptr) { + ScreenToClient(windowState.hwnd, &clientPoint); + } + + const float dpiScale = windowState.hostRuntime.GetDpiScale(kBaseDpiScale); + return UIPoint( + dpiScale > 0.0f ? static_cast(clientPoint.x) / dpiScale : static_cast(clientPoint.x), + dpiScale > 0.0f ? static_cast(clientPoint.y) / dpiScale : static_cast(clientPoint.y)); +} + std::string Application::BuildCaptureStatusText() const { if (m_autoScreenshot.HasPendingCapture()) { return "Shot pending..."; @@ -1658,10 +2574,23 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP message, lParam, dispatcherResult)) { + const auto* createStruct = reinterpret_cast(lParam); + if (createStruct != nullptr) { + if (Application* application = + reinterpret_cast(createStruct->lpCreateParams); + application != nullptr && + application->m_pendingCreateWindowState != nullptr && + application->m_pendingCreateWindowState->hwnd == nullptr) { + application->m_pendingCreateWindowState->hwnd = hwnd; + } + } return dispatcherResult; } Application* application = Host::WindowMessageDispatcher::GetApplicationFromWindow(hwnd); + if (application != nullptr) { + application->m_currentWindowState = application->FindWindowState(hwnd); + } if (application != nullptr && Host::WindowMessageDispatcher::TryDispatch( hwnd, @@ -1676,6 +2605,9 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP switch (message) { case WM_MOUSEMOVE: if (application != nullptr) { + if (application->HandleGlobalTabDragPointerMove(hwnd)) { + return 0; + } if (application->HandleBorderlessWindowResizePointerMove()) { return 0; } @@ -1747,6 +2679,9 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP break; case WM_LBUTTONUP: if (application != nullptr) { + if (application->HandleGlobalTabDragPointerButtonUp(hwnd)) { + return 0; + } if (application->HandleBorderlessWindowResizeButtonUp()) { return 0; } @@ -1788,6 +2723,16 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP } break; case WM_CAPTURECHANGED: + if (application != nullptr && + application->m_globalTabDragSession.active && + application->m_globalTabDragSession.panelWindowId == + (application->m_currentWindowState != nullptr + ? application->m_currentWindowState->windowId + : std::string()) && + reinterpret_cast(lParam) != hwnd) { + application->EndGlobalTabDragSession(); + return 0; + } if (application != nullptr && reinterpret_cast(lParam) != hwnd && application->HasInteractiveCaptureState()) { @@ -1831,9 +2776,15 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP return 1; case WM_DESTROY: if (application != nullptr) { - application->m_hwnd = nullptr; + if (application->m_globalTabDragSession.active && + application->m_globalTabDragSession.panelWindowId == + (application->m_currentWindowState != nullptr + ? application->m_currentWindowState->windowId + : std::string())) { + application->EndGlobalTabDragSession(); + } + application->HandleDestroyedWindow(hwnd); } - PostQuitMessage(0); return 0; default: break; @@ -1842,6 +2793,21 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP return DefWindowProcW(hwnd, message, wParam, lParam); } +#undef m_hwnd +#undef m_renderer +#undef m_windowRenderer +#undef m_windowRenderLoop +#undef m_autoScreenshot +#undef m_inputModifierTracker +#undef m_workspaceController +#undef m_editorWorkspace +#undef m_pendingInputEvents +#undef m_trackingMouseLeave +#undef m_renderReady +#undef m_titleBarLogoIcon +#undef m_borderlessWindowChromeState +#undef m_hostRuntime + int RunXCUIEditorApp(HINSTANCE hInstance, int nCmdShow) { Application application; return application.Run(hInstance, nCmdShow); diff --git a/new_editor/app/Application.h b/new_editor/app/Application.h index 157dabe6..7aeac2f3 100644 --- a/new_editor/app/Application.h +++ b/new_editor/app/Application.h @@ -17,6 +17,7 @@ #include "Workspace/ProductEditorWorkspace.h" #include +#include #include #include @@ -24,6 +25,7 @@ #include #include +#include #include #include #include @@ -36,17 +38,114 @@ namespace XCEngine::UI::Editor { class Application { public: - Application() = default; + Application(); + ~Application(); + + Application(const Application&) = delete; + Application& operator=(const Application&) = delete; + Application(Application&&) = delete; + Application& operator=(Application&&) = delete; int Run(HINSTANCE hInstance, int nCmdShow); private: + struct ManagedWindowState { + HWND hwnd = nullptr; + std::string windowId = {}; + std::wstring title = {}; + bool primary = false; + ::XCEngine::UI::Editor::Host::NativeRenderer renderer = {}; + ::XCEngine::UI::Editor::Host::D3D12WindowRenderer windowRenderer = {}; + ::XCEngine::UI::Editor::Host::D3D12WindowRenderLoop windowRenderLoop = {}; + ::XCEngine::UI::Editor::Host::AutoScreenshotController autoScreenshot = {}; + ::XCEngine::UI::Editor::Host::InputModifierTracker inputModifierTracker = {}; + UIEditorWorkspaceController workspaceController = {}; + App::ProductEditorWorkspace editorWorkspace = {}; + std::vector<::XCEngine::UI::UIInputEvent> pendingInputEvents = {}; + bool trackingMouseLeave = false; + bool renderReady = false; + ::XCEngine::UI::UITextureHandle titleBarLogoIcon = {}; + ::XCEngine::UI::Editor::Host::BorderlessWindowChromeState borderlessWindowChromeState = {}; + ::XCEngine::UI::Editor::Host::HostRuntimeState hostRuntime = {}; + bool detachRequested = false; + std::string detachedNodeId = {}; + std::string detachedPanelId = {}; + POINT detachScreenPoint = {}; + bool pendingGlobalTabDragStart = false; + std::string pendingGlobalTabDragNodeId = {}; + std::string pendingGlobalTabDragPanelId = {}; + POINT pendingGlobalTabDragScreenPoint = {}; + }; + struct GlobalTabDragSession { + bool active = false; + std::string panelWindowId = {}; + std::string sourceNodeId = {}; + std::string panelId = {}; + POINT screenPoint = {}; + }; + struct ManagedWindowCreateParams { + std::string windowId = {}; + std::wstring title = {}; + int initialX = CW_USEDEFAULT; + int initialY = CW_USEDEFAULT; + int initialWidth = 1540; + int initialHeight = 940; + int showCommand = SW_SHOW; + bool primary = false; + bool autoCaptureOnStartup = false; + }; + friend class ::XCEngine::UI::Editor::Host::WindowMessageDispatcher; static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam); bool Initialize(HINSTANCE hInstance, int nCmdShow); void Shutdown(); + bool RegisterWindowClass(); + ManagedWindowState* CreateManagedWindow( + UIEditorWorkspaceController workspaceController, + const ManagedWindowCreateParams& params); + void DestroyManagedWindow(ManagedWindowState& windowState); + void DestroyClosedWindows(); + void RenderAllWindows(); + void ProcessPendingGlobalTabDragStarts(); + void ProcessPendingDetachRequests(); + bool TryStartGlobalTabDrag(ManagedWindowState& sourceWindowState); + bool TryProcessDetachRequest(ManagedWindowState& sourceWindowState); + UIEditorWindowWorkspaceSet BuildWindowWorkspaceSet(std::string_view activeWindowId = {}) const; + bool SynchronizeManagedWindowsFromWindowSet( + const UIEditorWindowWorkspaceSet& windowSet, + std::string_view preferredNewWindowId, + const POINT& preferredScreenPoint); + UIEditorWorkspaceController BuildWorkspaceControllerForWindow( + const UIEditorWindowWorkspaceState& windowState) const; + std::wstring BuildManagedWindowTitle(const UIEditorWorkspaceController& workspaceController) const; + RECT BuildDetachedWindowRect(const POINT& screenPoint) const; + void ResetManagedWindowInteractionState(ManagedWindowState& windowState); + void HandleDestroyedWindow(HWND hwnd); + void BeginGlobalTabDragSession( + std::string_view panelWindowId, + std::string_view sourceNodeId, + std::string_view panelId, + const POINT& screenPoint); + void EndGlobalTabDragSession(); + bool HandleGlobalTabDragPointerMove(HWND hwnd); + bool HandleGlobalTabDragPointerButtonUp(HWND hwnd); + ManagedWindowState* FindTopmostWindowStateAtScreenPoint( + const POINT& screenPoint, + std::string_view excludedWindowId = {}); + const ManagedWindowState* FindTopmostWindowStateAtScreenPoint( + const POINT& screenPoint, + std::string_view excludedWindowId = {}) const; + ManagedWindowState* FindWindowState(HWND hwnd); + const ManagedWindowState* FindWindowState(HWND hwnd) const; + ManagedWindowState* FindWindowState(std::string_view windowId); + const ManagedWindowState* FindWindowState(std::string_view windowId) const; + ManagedWindowState* FindPrimaryWindowState(); + const ManagedWindowState* FindPrimaryWindowState() const; + ManagedWindowState& RequireCurrentWindowState(); + const ManagedWindowState& RequireCurrentWindowState() const; + void RenderFrame(); void OnPaintMessage(); void OnResize(UINT width, UINT height); @@ -65,6 +164,9 @@ private: float GetDpiScale() const; float PixelsToDips(float pixels) const; ::XCEngine::UI::UIPoint ConvertClientPixelsToDips(LONG x, LONG y) const; + ::XCEngine::UI::UIPoint ConvertScreenPixelsToClientDips( + const ManagedWindowState& windowState, + const POINT& screenPoint) const; std::string BuildCaptureStatusText() const; void LogRuntimeTrace(std::string_view channel, std::string_view message) const; void ApplyHostCaptureRequests(const UIEditorShellInteractionResult& result); @@ -118,23 +220,15 @@ private: static LONG WINAPI HandleUnhandledException(EXCEPTION_POINTERS* exceptionInfo); static bool IsVerboseRuntimeTraceEnabled(); - HWND m_hwnd = nullptr; HINSTANCE m_hInstance = nullptr; ATOM m_windowClassAtom = 0; - ::XCEngine::UI::Editor::Host::NativeRenderer m_renderer = {}; - ::XCEngine::UI::Editor::Host::D3D12WindowRenderer m_windowRenderer = {}; - ::XCEngine::UI::Editor::Host::D3D12WindowRenderLoop m_windowRenderLoop = {}; - ::XCEngine::UI::Editor::Host::AutoScreenshotController m_autoScreenshot = {}; - ::XCEngine::UI::Editor::Host::InputModifierTracker m_inputModifierTracker = {}; + std::filesystem::path m_repoRoot = {}; App::ProductEditorContext m_editorContext = {}; - UIEditorWorkspaceController m_workspaceController = {}; - App::ProductEditorWorkspace m_editorWorkspace = {}; - std::vector<::XCEngine::UI::UIInputEvent> m_pendingInputEvents = {}; - bool m_trackingMouseLeave = false; - bool m_renderReady = false; - ::XCEngine::UI::UITextureHandle m_titleBarLogoIcon = {}; - ::XCEngine::UI::Editor::Host::BorderlessWindowChromeState m_borderlessWindowChromeState = {}; - ::XCEngine::UI::Editor::Host::HostRuntimeState m_hostRuntime = {}; + std::vector> m_windows = {}; + GlobalTabDragSession m_globalTabDragSession = {}; + ManagedWindowState* m_currentWindowState = nullptr; + ManagedWindowState* m_pendingCreateWindowState = nullptr; + bool m_shutdownRequested = false; }; int RunXCUIEditorApp(HINSTANCE hInstance, int nCmdShow); diff --git a/new_editor/app/Core/ProductEditorContext.cpp b/new_editor/app/Core/ProductEditorContext.cpp index a7dac444..409ed7ef 100644 --- a/new_editor/app/Core/ProductEditorContext.cpp +++ b/new_editor/app/Core/ProductEditorContext.cpp @@ -100,12 +100,14 @@ const UIEditorShellInteractionServices& ProductEditorContext::GetShellServices() UIEditorShellInteractionDefinition ProductEditorContext::BuildShellDefinition( const UIEditorWorkspaceController& workspaceController, - std::string_view captureText) const { + std::string_view captureText, + ProductEditorShellVariant variant) const { return BuildProductShellInteractionDefinition( m_shellAsset, workspaceController, ComposeStatusText(m_lastStatus, m_lastMessage), - captureText); + captureText, + variant); } void ProductEditorContext::SetReadyStatus() { diff --git a/new_editor/app/Core/ProductEditorContext.h b/new_editor/app/Core/ProductEditorContext.h index 5e7be258..f3e2364e 100644 --- a/new_editor/app/Core/ProductEditorContext.h +++ b/new_editor/app/Core/ProductEditorContext.h @@ -2,6 +2,7 @@ #include "Commands/ProductEditorHostCommandBridge.h" #include "Core/ProductEditorSession.h" +#include "Shell/ProductShellAsset.h" #include #include @@ -34,7 +35,8 @@ public: UIEditorShellInteractionDefinition BuildShellDefinition( const UIEditorWorkspaceController& workspaceController, - std::string_view captureText) const; + std::string_view captureText, + ProductEditorShellVariant variant = ProductEditorShellVariant::Primary) const; void SetReadyStatus(); void SetStatus(std::string status, std::string message); diff --git a/new_editor/app/Panels/ProductHierarchyPanel.cpp b/new_editor/app/Panels/ProductHierarchyPanel.cpp index 7d8ab331..dcd7bd06 100644 --- a/new_editor/app/Panels/ProductHierarchyPanel.cpp +++ b/new_editor/app/Panels/ProductHierarchyPanel.cpp @@ -32,7 +32,7 @@ using Widgets::UIEditorTreeViewInvalidIndex; constexpr std::string_view kHierarchyPanelId = "hierarchy"; constexpr float kDragThreshold = 4.0f; -constexpr UIColor kDragPreviewColor(0.82f, 0.82f, 0.82f, 0.55f); +constexpr UIColor kDragPreviewColor(0.92f, 0.92f, 0.92f, 0.42f); bool ContainsPoint(const UIRect& rect, const UIPoint& point) { return point.x >= rect.x && @@ -143,6 +143,13 @@ void ProductHierarchyPanel::SetBuiltInIcons(const ProductBuiltInIcons* icons) { RebuildItems(); } +void ProductHierarchyPanel::ResetInteractionState() { + m_treeInteractionState = {}; + m_treeFrame = {}; + m_dragState = {}; + ResetTransientState(); +} + const UIEditorPanelContentHostPanelState* ProductHierarchyPanel::FindMountedHierarchyPanel( const UIEditorPanelContentHostFrame& contentHostFrame) const { for (const UIEditorPanelContentHostPanelState& panelState : contentHostFrame.panelStates) { diff --git a/new_editor/app/Panels/ProductHierarchyPanel.h b/new_editor/app/Panels/ProductHierarchyPanel.h index 39743575..5651f6e9 100644 --- a/new_editor/app/Panels/ProductHierarchyPanel.h +++ b/new_editor/app/Panels/ProductHierarchyPanel.h @@ -36,6 +36,7 @@ public: void Initialize(); void SetBuiltInIcons(const ProductBuiltInIcons* icons); + void ResetInteractionState(); void Update( const UIEditorPanelContentHostFrame& contentHostFrame, const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, diff --git a/new_editor/app/Panels/ProductProjectPanel.cpp b/new_editor/app/Panels/ProductProjectPanel.cpp index 6b1e79bf..976ad157 100644 --- a/new_editor/app/Panels/ProductProjectPanel.cpp +++ b/new_editor/app/Panels/ProductProjectPanel.cpp @@ -53,16 +53,16 @@ constexpr float kHeaderFontSize = 12.0f; constexpr float kTileLabelFontSize = 11.0f; constexpr UIColor kSurfaceColor(0.10f, 0.10f, 0.10f, 1.0f); -constexpr UIColor kPaneColor(0.11f, 0.11f, 0.11f, 1.0f); -constexpr UIColor kHeaderColor(0.12f, 0.12f, 0.12f, 1.0f); -constexpr UIColor kTextPrimary(0.840f, 0.840f, 0.840f, 1.0f); +constexpr UIColor kPaneColor(0.10f, 0.10f, 0.10f, 1.0f); +constexpr UIColor kHeaderColor(0.11f, 0.11f, 0.11f, 1.0f); +constexpr UIColor kTextPrimary(0.880f, 0.880f, 0.880f, 1.0f); constexpr UIColor kTextStrong(0.930f, 0.930f, 0.930f, 1.0f); -constexpr UIColor kTextMuted(0.580f, 0.580f, 0.580f, 1.0f); +constexpr UIColor kTextMuted(0.640f, 0.640f, 0.640f, 1.0f); constexpr UIColor kTileHoverColor(0.14f, 0.14f, 0.14f, 1.0f); -constexpr UIColor kTileSelectedColor(0.17f, 0.17f, 0.17f, 1.0f); -constexpr UIColor kTilePreviewFillColor(0.20f, 0.20f, 0.20f, 1.0f); -constexpr UIColor kTilePreviewShadeColor(0.15f, 0.15f, 0.15f, 1.0f); -constexpr UIColor kTilePreviewOutlineColor(0.920f, 0.920f, 0.920f, 0.25f); +constexpr UIColor kTileSelectedColor(0.18f, 0.18f, 0.18f, 1.0f); +constexpr UIColor kTilePreviewFillColor(0.15f, 0.15f, 0.15f, 1.0f); +constexpr UIColor kTilePreviewShadeColor(0.12f, 0.12f, 0.12f, 1.0f); +constexpr UIColor kTilePreviewOutlineColor(0.920f, 0.920f, 0.920f, 0.20f); bool ContainsPoint(const UIRect& rect, const UIPoint& point) { return point.x >= rect.x && @@ -237,6 +237,22 @@ void ProductProjectPanel::SetTextMeasurer(const UIEditorTextMeasurer* textMeasur m_textMeasurer = textMeasurer; } +void ProductProjectPanel::ResetInteractionState() { + m_treeInteractionState = {}; + m_treeFrame = {}; + m_frameEvents.clear(); + m_layout = {}; + m_hoveredAssetItemId.clear(); + m_lastPrimaryClickedAssetId.clear(); + m_hoveredBreadcrumbIndex = kInvalidLayoutIndex; + m_pressedBreadcrumbIndex = kInvalidLayoutIndex; + m_visible = false; + m_splitterHovered = false; + m_splitterDragging = false; + m_requestPointerCapture = false; + m_requestPointerRelease = false; +} + ProductProjectPanel::CursorKind ProductProjectPanel::GetCursorKind() const { return (m_splitterHovered || m_splitterDragging) ? CursorKind::ResizeEW : CursorKind::Arrow; } diff --git a/new_editor/app/Panels/ProductProjectPanel.h b/new_editor/app/Panels/ProductProjectPanel.h index cb3f4574..f20703b3 100644 --- a/new_editor/app/Panels/ProductProjectPanel.h +++ b/new_editor/app/Panels/ProductProjectPanel.h @@ -58,6 +58,7 @@ public: void Initialize(const std::filesystem::path& repoRoot); void SetBuiltInIcons(const ProductBuiltInIcons* icons); void SetTextMeasurer(const ::XCEngine::UI::Editor::UIEditorTextMeasurer* textMeasurer); + void ResetInteractionState(); void Update( const UIEditorPanelContentHostFrame& contentHostFrame, const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, diff --git a/new_editor/app/Shell/ProductShellAsset.cpp b/new_editor/app/Shell/ProductShellAsset.cpp index 4a8298b4..e4c1b887 100644 --- a/new_editor/app/Shell/ProductShellAsset.cpp +++ b/new_editor/app/Shell/ProductShellAsset.cpp @@ -488,8 +488,15 @@ UIEditorShellInteractionDefinition BuildProductShellInteractionDefinition( const EditorShellAsset& asset, const UIEditorWorkspaceController& controller, std::string_view statusText, - std::string_view captureText) { + std::string_view captureText, + ProductEditorShellVariant variant) { UIEditorShellInteractionDefinition definition = asset.shellDefinition; + if (variant == ProductEditorShellVariant::DetachedWindow) { + definition.menuModel = {}; + definition.toolbarButtons.clear(); + definition.statusSegments.clear(); + } + const std::string activeTitle = ResolvePanelTitle(asset.panelRegistry, controller.GetWorkspace().activePanelId); const std::string resolvedStatus = diff --git a/new_editor/app/Shell/ProductShellAsset.h b/new_editor/app/Shell/ProductShellAsset.h index 3fa67f35..f40fe207 100644 --- a/new_editor/app/Shell/ProductShellAsset.h +++ b/new_editor/app/Shell/ProductShellAsset.h @@ -3,17 +3,24 @@ #include #include +#include #include #include namespace XCEngine::UI::Editor::App { +enum class ProductEditorShellVariant : std::uint8_t { + Primary = 0, + DetachedWindow +}; + EditorShellAsset BuildProductShellAsset(const std::filesystem::path& repoRoot); UIEditorShellInteractionDefinition BuildProductShellInteractionDefinition( const EditorShellAsset& asset, const UIEditorWorkspaceController& controller, std::string_view statusText, - std::string_view captureText); + std::string_view captureText, + ProductEditorShellVariant variant = ProductEditorShellVariant::Primary); } // namespace XCEngine::UI::Editor::App diff --git a/new_editor/app/Workspace/ProductEditorWorkspace.cpp b/new_editor/app/Workspace/ProductEditorWorkspace.cpp index cf29a53c..70b646f2 100644 --- a/new_editor/app/Workspace/ProductEditorWorkspace.cpp +++ b/new_editor/app/Workspace/ProductEditorWorkspace.cpp @@ -248,16 +248,25 @@ void ProductEditorWorkspace::Shutdown() { m_builtInIcons.Shutdown(); } +void ProductEditorWorkspace::ResetInteractionState() { + m_shellFrame = {}; + m_shellInteractionState = {}; + m_traceEntries.clear(); + m_hierarchyPanel.ResetInteractionState(); + m_projectPanel.ResetInteractionState(); +} + void ProductEditorWorkspace::Update( ProductEditorContext& context, UIEditorWorkspaceController& workspaceController, const ::XCEngine::UI::UIRect& bounds, const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, - std::string_view captureText) { + std::string_view captureText, + ProductEditorShellVariant shellVariant) { const auto& metrics = ResolveUIEditorShellInteractionMetrics(); context.SyncSessionFromWorkspace(workspaceController); UIEditorShellInteractionDefinition definition = - context.BuildShellDefinition(workspaceController, captureText); + context.BuildShellDefinition(workspaceController, captureText, shellVariant); m_viewportHostService.BeginFrame(); definition.workspacePresentations = BuildWorkspacePresentations(definition); const std::vector shellEvents = diff --git a/new_editor/app/Workspace/ProductEditorWorkspace.h b/new_editor/app/Workspace/ProductEditorWorkspace.h index c1408592..12f6e0b6 100644 --- a/new_editor/app/Workspace/ProductEditorWorkspace.h +++ b/new_editor/app/Workspace/ProductEditorWorkspace.h @@ -29,6 +29,7 @@ public: const std::filesystem::path& repoRoot, Host::NativeRenderer& renderer); void Shutdown(); + void ResetInteractionState(); void AttachViewportWindowRenderer(Host::D3D12WindowRenderer& renderer); void DetachViewportWindowRenderer(); void SetViewportSurfacePresentationEnabled(bool enabled); @@ -38,7 +39,8 @@ public: UIEditorWorkspaceController& workspaceController, const ::XCEngine::UI::UIRect& bounds, const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, - std::string_view captureText); + std::string_view captureText, + ProductEditorShellVariant shellVariant = ProductEditorShellVariant::Primary); void RenderRequestedViewports( const ::XCEngine::Rendering::RenderContext& renderContext); void Append(::XCEngine::UI::UIDrawList& drawList) const;