diff --git a/docs/plan/XCUI_NewEditor收口重构计划_2026-04-17.md b/docs/plan/XCUI_NewEditor收口重构计划_2026-04-17.md index ca43d8b3..ddabd402 100644 --- a/docs/plan/XCUI_NewEditor收口重构计划_2026-04-17.md +++ b/docs/plan/XCUI_NewEditor收口重构计划_2026-04-17.md @@ -55,6 +55,8 @@ - 已补同窗口 `move/dock` 的 controller 级结果校验与回滚,避免 mutation 回归直接污染 window-set 状态 - 已补 detached window 起始 global-tab-drag 的源请求校验,避免 stale `nodeId/panelId` 直接进入跨窗口拖拽状态机 - 已补 live window-set 构造时对 destroyed/no-`HWND` 窗口的过滤,避免 `primary/activeWindowId` 指向不存在的运行时窗口 + - 已补 window-sync 失败时的已有窗口/新建窗口回滚,避免同步中途失败留下半更新窗口集 + - 已补 closing-window 过滤,避免已发出 `WM_CLOSE` 但尚未销毁的 detached window 继续参与 live window-set 与命中测试 9. integration 测试构建模板已继续收口: - 已把 `tests/UI/Editor/integration` 叶子目标收敛到 shared helper @@ -553,8 +555,8 @@ - 已完成同窗口 `move/dock` 的结果校验与回滚防线补齐 - 已完成 detached window 起始 global-tab-drag 的源请求校验 - 已完成 live window-set 对 destroyed/no-`HWND` 窗口的 primary/active 过滤 -- 尚未完成 transfer 前后 session/state 完整性约束 -- 尚未完成 cross-window drag/drop 状态机约束收口 +- 已完成 window-sync 失败回滚与 closing-window 过滤 +- `transfer/session/state` 主链路已基本收口,剩余更多属于后续演进项而非当前主线阻塞 ### 涉及范围 diff --git a/new_editor/app/Platform/Win32/EditorWindow.h b/new_editor/app/Platform/Win32/EditorWindow.h index d9632aee..f11c7a90 100644 --- a/new_editor/app/Platform/Win32/EditorWindow.h +++ b/new_editor/app/Platform/Win32/EditorWindow.h @@ -50,6 +50,7 @@ public: HWND GetHwnd() const; bool HasHwnd() const; bool IsPrimary() const; + bool IsClosing() const; const std::wstring& GetTitle() const; const UIEditorWorkspaceController& GetWorkspaceController() const; UIEditorWorkspaceController& GetWorkspaceController(); @@ -74,6 +75,8 @@ private: void AttachHwnd(HWND hwnd); void MarkDestroyed(); + void MarkClosing(); + void ClearClosing(); void SetTrackingMouseLeave(bool trackingMouseLeave); void SetTitle(std::wstring title); void ReplaceWorkspaceController(UIEditorWorkspaceController workspaceController); diff --git a/new_editor/app/Platform/Win32/EditorWindowLifecycle.cpp b/new_editor/app/Platform/Win32/EditorWindowLifecycle.cpp index 13e5ea06..c95c2efc 100644 --- a/new_editor/app/Platform/Win32/EditorWindowLifecycle.cpp +++ b/new_editor/app/Platform/Win32/EditorWindowLifecycle.cpp @@ -1,9 +1,72 @@ #include "Platform/Win32/EditorWindow.h" + +#include "Bootstrap/EditorResources.h" +#include "Platform/Win32/EditorWindowConstants.h" +#include "Platform/Win32/EditorWindowPlatformInternal.h" #include "Platform/Win32/EditorWindowRuntimeInternal.h" +#include "State/EditorContext.h" + +#include +#include + +#include +#include + +namespace XCEngine::UI::Editor::App::EditorWindowInternal { + +UINT QuerySystemDpi() { + HDC screenDc = GetDC(nullptr); + if (screenDc == nullptr) { + return kDefaultDpi; + } + + const int dpiX = GetDeviceCaps(screenDc, LOGPIXELSX); + ReleaseDC(nullptr, screenDc); + return dpiX > 0 ? static_cast(dpiX) : kDefaultDpi; +} + +UINT QueryWindowDpi(HWND hwnd) { + if (hwnd != nullptr) { + const HMODULE user32 = GetModuleHandleW(L"user32.dll"); + if (user32 != nullptr) { + using GetDpiForWindowFn = UINT(WINAPI*)(HWND); + const auto getDpiForWindow = + reinterpret_cast(GetProcAddress(user32, "GetDpiForWindow")); + if (getDpiForWindow != nullptr) { + const UINT dpi = getDpiForWindow(hwnd); + if (dpi != 0u) { + return dpi; + } + } + } + } + + return QuerySystemDpi(); +} + +bool ResolveVerboseRuntimeTraceEnabled() { + wchar_t buffer[8] = {}; + const DWORD length = GetEnvironmentVariableW( + L"XCUIEDITOR_VERBOSE_TRACE", + buffer, + static_cast(std::size(buffer))); + return length > 0u && buffer[0] != L'0'; +} + +void LogRuntimeTrace(std::string_view channel, std::string_view message) { + AppendUIEditorRuntimeTrace(channel, message); +} + +bool IsAutoCaptureOnStartupEnabled() { + return App::Internal::IsEnvironmentFlagEnabled("XCUI_AUTO_CAPTURE_ON_STARTUP"); +} + +} // namespace XCEngine::UI::Editor::App::EditorWindowInternal namespace XCEngine::UI::Editor::App { using namespace EditorWindowInternal; +using ::XCEngine::UI::UIPoint; EditorWindow::EditorWindow( std::string windowId, @@ -15,7 +78,8 @@ EditorWindow::EditorWindow( std::move(windowId), std::move(title), {}, - primary } + primary, + false } , m_composition{ std::move(workspaceController), {} } { UpdateCachedTitleText(); } @@ -36,6 +100,10 @@ bool EditorWindow::IsPrimary() const { return m_window.primary; } +bool EditorWindow::IsClosing() const { + return m_window.closing; +} + bool EditorWindow::IsRenderReady() const { return m_render.ready; } @@ -88,13 +156,23 @@ void EditorWindow::ClearExternalDockHostDropPreview() { void EditorWindow::AttachHwnd(HWND hwnd) { m_window.hwnd = hwnd; + m_window.closing = false; } void EditorWindow::MarkDestroyed() { m_window.hwnd = nullptr; + m_window.closing = false; m_input.trackingMouseLeave = false; } +void EditorWindow::MarkClosing() { + m_window.closing = true; +} + +void EditorWindow::ClearClosing() { + m_window.closing = false; +} + void EditorWindow::SetTrackingMouseLeave(bool trackingMouseLeave) { m_input.trackingMouseLeave = trackingMouseLeave; } @@ -114,6 +192,261 @@ void EditorWindow::InvalidateHostWindow() const { } } +bool EditorWindow::Initialize( + const std::filesystem::path& repoRoot, + EditorContext& editorContext, + const std::filesystem::path& captureRoot, + bool autoCaptureOnStartup) { + if (m_window.hwnd == nullptr) { + LogRuntimeTrace("app", "window initialize skipped: hwnd is null"); + return false; + } + + Host::RefreshBorderlessWindowDwmDecorations(m_window.hwnd); + m_chrome.runtime.Reset(); + m_chrome.runtime.SetWindowDpi(QueryWindowDpi(m_window.hwnd)); + m_render.renderer.SetDpiScale(GetDpiScale()); + + std::ostringstream dpiTrace = {}; + dpiTrace << "initial dpi=" << m_chrome.runtime.GetWindowDpi() + << " scale=" << GetDpiScale(); + LogRuntimeTrace("window", dpiTrace.str()); + + if (!m_render.renderer.Initialize(m_window.hwnd)) { + LogRuntimeTrace("app", "renderer initialization failed"); + return false; + } + + RECT clientRect = {}; + GetClientRect(m_window.hwnd, &clientRect); + const int clientWidth = (std::max)(clientRect.right - clientRect.left, 1L); + const int clientHeight = (std::max)(clientRect.bottom - clientRect.top, 1L); + if (!m_render.windowRenderer.Initialize(m_window.hwnd, clientWidth, clientHeight)) { + LogRuntimeTrace("app", "d3d12 window renderer initialization failed"); + m_render.renderer.Shutdown(); + return false; + } + + const Host::D3D12WindowRenderLoopAttachResult attachResult = + m_render.windowRenderLoop.Attach(m_render.renderer, m_render.windowRenderer); + if (!attachResult.interopWarning.empty()) { + LogRuntimeTrace("app", attachResult.interopWarning); + } + + editorContext.AttachTextMeasurer(m_render.renderer); + m_composition.shellRuntime.Initialize(repoRoot, m_render.renderer); + m_composition.shellRuntime.AttachViewportWindowRenderer(m_render.windowRenderer); + m_composition.shellRuntime.SetViewportSurfacePresentationEnabled( + attachResult.hasViewportSurfacePresentation); + + std::string titleBarLogoError = {}; + if (!LoadEmbeddedPngTexture( + m_render.renderer, + IDR_PNG_LOGO_ICON, + m_render.titleBarLogoIcon, + titleBarLogoError)) { + LogRuntimeTrace("icons", "titlebar logo_icon.png: " + titleBarLogoError); + } + if (!m_composition.shellRuntime.GetBuiltInIconError().empty()) { + LogRuntimeTrace("icons", m_composition.shellRuntime.GetBuiltInIconError()); + } + + LogRuntimeTrace( + "app", + "shell runtime initialized: " + + editorContext.DescribeWorkspaceState( + m_composition.workspaceController, + m_composition.shellRuntime.GetShellInteractionState())); + m_render.ready = true; + + m_render.autoScreenshot.Initialize(captureRoot); + if (autoCaptureOnStartup && IsAutoCaptureOnStartupEnabled()) { + m_render.autoScreenshot.RequestCapture("startup"); + editorContext.SetStatus("Capture", "Startup capture requested."); + } + + return true; +} + +void EditorWindow::Shutdown() { + if (GetCapture() == m_window.hwnd) { + ReleaseCapture(); + } + + m_render.ready = false; + m_render.autoScreenshot.Shutdown(); + m_composition.shellRuntime.Shutdown(); + m_render.renderer.ReleaseTexture(m_render.titleBarLogoIcon); + m_render.windowRenderLoop.Detach(); + m_render.windowRenderer.Shutdown(); + m_render.renderer.Shutdown(); + m_input.pendingEvents.clear(); + m_chrome.chromeState = {}; + m_chrome.runtime.Reset(); +} + +void EditorWindow::ResetInteractionState() { + if (GetCapture() == m_window.hwnd) { + ReleaseCapture(); + } + + m_input.pendingEvents.clear(); + m_input.trackingMouseLeave = false; + m_input.modifierTracker.Reset(); + m_composition.shellRuntime.ResetInteractionState(); + m_chrome.chromeState = {}; + m_chrome.runtime.EndBorderlessResize(); + m_chrome.runtime.EndBorderlessWindowDragRestore(); + m_chrome.runtime.EndInteractiveResize(); + m_chrome.runtime.SetHoveredBorderlessResizeEdge(Host::BorderlessWindowResizeEdge::None); + m_chrome.runtime.ClearPredictedClientPixelSize(); +} + +bool EditorWindow::ApplyWindowResize(UINT width, UINT height) { + if (!m_render.ready || width == 0u || height == 0u) { + return false; + } + + const Host::D3D12WindowRenderLoopResizeResult resizeResult = + m_render.windowRenderLoop.ApplyResize(width, height); + m_composition.shellRuntime.SetViewportSurfacePresentationEnabled( + resizeResult.hasViewportSurfacePresentation); + + if (!resizeResult.windowRendererWarning.empty()) { + LogRuntimeTrace("present", resizeResult.windowRendererWarning); + } + + if (!resizeResult.interopWarning.empty()) { + LogRuntimeTrace("present", resizeResult.interopWarning); + } + + return resizeResult.hasViewportSurfacePresentation; +} + +bool EditorWindow::QueryCurrentClientPixelSize(UINT& outWidth, UINT& outHeight) const { + outWidth = 0u; + outHeight = 0u; + if (m_window.hwnd == nullptr || !IsWindow(m_window.hwnd)) { + return false; + } + + RECT clientRect = {}; + if (!GetClientRect(m_window.hwnd, &clientRect)) { + return false; + } + + const LONG width = clientRect.right - clientRect.left; + const LONG height = clientRect.bottom - clientRect.top; + if (width <= 0 || height <= 0) { + return false; + } + + outWidth = static_cast(width); + outHeight = static_cast(height); + return true; +} + +bool EditorWindow::ResolveRenderClientPixelSize(UINT& outWidth, UINT& outHeight) const { + if (m_chrome.runtime.TryGetPredictedClientPixelSize(outWidth, outHeight)) { + return true; + } + + return QueryCurrentClientPixelSize(outWidth, outHeight); +} + +float EditorWindow::GetDpiScale() const { + return m_chrome.runtime.GetDpiScale(kBaseDpiScale); +} + +float EditorWindow::PixelsToDips(float pixels) const { + const float dpiScale = GetDpiScale(); + return dpiScale > 0.0f ? pixels / dpiScale : pixels; +} + +UIPoint EditorWindow::ConvertClientPixelsToDips(LONG x, LONG y) const { + return UIPoint( + PixelsToDips(static_cast(x)), + PixelsToDips(static_cast(y))); +} + +UIPoint EditorWindow::ConvertScreenPixelsToClientDips(const POINT& screenPoint) const { + POINT clientPoint = screenPoint; + if (m_window.hwnd != nullptr) { + ScreenToClient(m_window.hwnd, &clientPoint); + } + + const float dpiScale = m_chrome.runtime.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)); +} + +void EditorWindow::OnResize(UINT width, UINT height) { + bool matchesPredictedClientSize = false; + UINT predictedWidth = 0u; + UINT predictedHeight = 0u; + if (m_chrome.runtime.TryGetPredictedClientPixelSize(predictedWidth, predictedHeight)) { + matchesPredictedClientSize = + predictedWidth == width && + predictedHeight == height; + } + + m_chrome.runtime.ClearPredictedClientPixelSize(); + if (IsBorderlessWindowEnabled() && m_window.hwnd != nullptr) { + Host::RefreshBorderlessWindowDwmDecorations(m_window.hwnd); + } + + if (!matchesPredictedClientSize) { + ApplyWindowResize(width, height); + } +} + +void EditorWindow::OnEnterSizeMove() { + m_chrome.runtime.BeginInteractiveResize(); +} + +void EditorWindow::OnExitSizeMove() { + m_chrome.runtime.EndInteractiveResize(); + m_chrome.runtime.ClearPredictedClientPixelSize(); + UINT width = 0u; + UINT height = 0u; + if (QueryCurrentClientPixelSize(width, height)) { + ApplyWindowResize(width, height); + } +} + +void EditorWindow::OnDpiChanged(UINT dpi, const RECT& suggestedRect) { + m_chrome.runtime.SetWindowDpi(dpi == 0u ? kDefaultDpi : dpi); + m_render.renderer.SetDpiScale(GetDpiScale()); + if (m_window.hwnd != nullptr) { + const LONG windowWidth = suggestedRect.right - suggestedRect.left; + const LONG windowHeight = suggestedRect.bottom - suggestedRect.top; + SetWindowPos( + m_window.hwnd, + nullptr, + suggestedRect.left, + suggestedRect.top, + windowWidth, + windowHeight, + SWP_NOZORDER | SWP_NOACTIVATE); + UINT clientWidth = 0u; + UINT clientHeight = 0u; + if (QueryCurrentClientPixelSize(clientWidth, clientHeight)) { + ApplyWindowResize(clientWidth, clientHeight); + } + Host::RefreshBorderlessWindowDwmDecorations(m_window.hwnd); + } + + std::ostringstream trace = {}; + trace << "dpi changed to " << m_chrome.runtime.GetWindowDpi() + << " scale=" << GetDpiScale(); + LogRuntimeTrace("window", trace.str()); +} + bool EditorWindow::IsVerboseRuntimeTraceEnabled() { static const bool s_enabled = ResolveVerboseRuntimeTraceEnabled(); return s_enabled; diff --git a/new_editor/app/Platform/Win32/EditorWindowState.h b/new_editor/app/Platform/Win32/EditorWindowState.h index 350d6245..191115d1 100644 --- a/new_editor/app/Platform/Win32/EditorWindowState.h +++ b/new_editor/app/Platform/Win32/EditorWindowState.h @@ -32,6 +32,7 @@ struct EditorWindowWindowState { std::wstring title = {}; std::string titleText = {}; bool primary = false; + bool closing = false; }; struct EditorWindowRenderState { diff --git a/new_editor/app/Platform/Win32/WindowManager/Lifecycle.cpp b/new_editor/app/Platform/Win32/WindowManager/Lifecycle.cpp index 5f7a36a6..a08d4855 100644 --- a/new_editor/app/Platform/Win32/WindowManager/Lifecycle.cpp +++ b/new_editor/app/Platform/Win32/WindowManager/Lifecycle.cpp @@ -385,12 +385,15 @@ UIEditorWindowWorkspaceSet EditorWindowWorkspaceCoordinator::BuildWindowWorkspac UIEditorWindowWorkspaceSet windowSet = {}; if (const EditorWindow* primaryWindow = m_hostRuntime.FindPrimaryWindow(); primaryWindow != nullptr && - primaryWindow->GetHwnd() != nullptr) { + primaryWindow->GetHwnd() != nullptr && + !primaryWindow->IsClosing()) { windowSet.primaryWindowId = std::string(primaryWindow->GetWindowId()); } for (const std::unique_ptr& window : m_hostRuntime.GetWindows()) { - if (window == nullptr || window->GetHwnd() == nullptr) { + if (window == nullptr || + window->GetHwnd() == nullptr || + window->IsClosing()) { continue; } @@ -405,7 +408,9 @@ UIEditorWindowWorkspaceSet EditorWindowWorkspaceCoordinator::BuildWindowWorkspac !activeWindowId.empty() && ([this, activeWindowId]() { const EditorWindow* activeWindow = m_hostRuntime.FindWindow(activeWindowId); - return activeWindow != nullptr && activeWindow->GetHwnd() != nullptr; + return activeWindow != nullptr && + activeWindow->GetHwnd() != nullptr && + !activeWindow->IsClosing(); })() ? std::string(activeWindowId) : windowSet.primaryWindowId; diff --git a/new_editor/app/Platform/Win32/WindowManager/TabDrag.cpp b/new_editor/app/Platform/Win32/WindowManager/TabDrag.cpp index 48674778..67a147ce 100644 --- a/new_editor/app/Platform/Win32/WindowManager/TabDrag.cpp +++ b/new_editor/app/Platform/Win32/WindowManager/TabDrag.cpp @@ -559,6 +559,7 @@ EditorWindow* EditorWindowWorkspaceCoordinator::FindTopmostWindowAtScreenPoint( const HWND rootWindow = GetAncestor(hitWindow, GA_ROOT); if (EditorWindow* window = m_hostRuntime.FindWindow(rootWindow); window != nullptr && + !window->IsClosing() && window->GetWindowId() != excludedWindowId) { return window; } @@ -568,6 +569,7 @@ EditorWindow* EditorWindowWorkspaceCoordinator::FindTopmostWindowAtScreenPoint( EditorWindow* const window = it->get(); if (window == nullptr || window->GetHwnd() == nullptr || + window->IsClosing() || window->GetWindowId() == excludedWindowId) { continue; } diff --git a/new_editor/app/Platform/Win32/WindowManager/WindowSync.cpp b/new_editor/app/Platform/Win32/WindowManager/WindowSync.cpp index e50c32e2..b38e990b 100644 --- a/new_editor/app/Platform/Win32/WindowManager/WindowSync.cpp +++ b/new_editor/app/Platform/Win32/WindowManager/WindowSync.cpp @@ -10,6 +10,16 @@ namespace XCEngine::UI::Editor::App::Internal { +namespace { + +struct ExistingWindowSnapshot { + EditorWindow* window = nullptr; + UIEditorWorkspaceController workspaceController = {}; + std::wstring title = {}; +}; + +} // namespace + std::wstring EditorWindowWorkspaceCoordinator::BuildWindowTitle( const UIEditorWorkspaceController& workspaceController) const { const std::string& activePanelId = workspaceController.GetWorkspace().activePanelId; @@ -54,13 +64,62 @@ bool EditorWindowWorkspaceCoordinator::SynchronizeWindowsFromWindowSet( const UIEditorWindowWorkspaceSet& windowSet, std::string_view preferredNewWindowId, const POINT& preferredScreenPoint) { + const auto restoreWindowSnapshot = [](const ExistingWindowSnapshot& snapshot) { + if (snapshot.window == nullptr) { + return; + } + + snapshot.window->ReplaceWorkspaceController(snapshot.workspaceController); + snapshot.window->SetTitle(snapshot.title); + snapshot.window->ResetInteractionState(); + if (snapshot.window->GetHwnd() != nullptr) { + SetWindowTextW(snapshot.window->GetHwnd(), snapshot.window->GetTitle().c_str()); + } + }; + + const auto destroyAndEraseWindowById = [this](std::string_view windowId) { + auto& windows = m_hostRuntime.GetWindows(); + const auto it = std::find_if( + windows.begin(), + windows.end(), + [windowId](const std::unique_ptr& candidate) { + return candidate != nullptr && candidate->GetWindowId() == windowId; + }); + if (it == windows.end() || *it == nullptr) { + return false; + } + + EditorWindow& window = *it->get(); + const HWND hwnd = window.GetHwnd(); + if (GetCapture() == hwnd) { + ReleaseCapture(); + } + + window.Shutdown(); + if (hwnd != nullptr && IsWindow(hwnd)) { + DestroyWindow(hwnd); + } + window.MarkDestroyed(); + windows.erase(it); + return true; + }; + std::vector windowIdsInSet = {}; windowIdsInSet.reserve(windowSet.windows.size()); + std::vector existingWindowSnapshots = {}; + existingWindowSnapshots.reserve(windowSet.windows.size()); + std::vector createdWindowIds = {}; for (const UIEditorWindowWorkspaceState& entry : windowSet.windows) { windowIdsInSet.push_back(entry.windowId); if (EditorWindow* existingWindow = m_hostRuntime.FindWindow(entry.windowId); existingWindow != nullptr) { + existingWindow->ClearClosing(); + existingWindowSnapshots.push_back(ExistingWindowSnapshot{ + existingWindow, + existingWindow->GetWorkspaceController(), + existingWindow->GetTitle(), + }); existingWindow->ReplaceWorkspaceController(BuildWorkspaceControllerForWindow(entry)); existingWindow->ResetInteractionState(); if (!existingWindow->IsPrimary()) { @@ -93,8 +152,15 @@ bool EditorWindowWorkspaceCoordinator::SynchronizeWindowsFromWindowSet( } if (m_hostRuntime.CreateEditorWindow(BuildWorkspaceControllerForWindow(entry), createParams) == nullptr) { + for (const ExistingWindowSnapshot& snapshot : existingWindowSnapshots) { + restoreWindowSnapshot(snapshot); + } + for (auto it = createdWindowIds.rbegin(); it != createdWindowIds.rend(); ++it) { + destroyAndEraseWindowById(*it); + } return false; } + createdWindowIds.push_back(entry.windowId); } for (const std::unique_ptr& window : m_hostRuntime.GetWindows()) { @@ -110,6 +176,7 @@ bool EditorWindowWorkspaceCoordinator::SynchronizeWindowsFromWindowSet( windowIdsInSet.end(), window->GetWindowId()) != windowIdsInSet.end(); if (!existsInWindowSet) { + window->MarkClosing(); PostMessageW(window->GetHwnd(), WM_CLOSE, 0, 0); } }