diff --git a/docs/plan/xcui_editor_windowing_architecture_plan.md b/docs/plan/xcui_editor_windowing_architecture_plan.md index c2e44cc7..d8e80102 100644 --- a/docs/plan/xcui_editor_windowing_architecture_plan.md +++ b/docs/plan/xcui_editor_windowing_architecture_plan.md @@ -1,8 +1,14 @@ # XCUI Editor Windowing Plan +## 阶段门禁 + +- 每完成一个阶段,必须先编译 `XCUIEditorApp`。 +- 编译通过后,必须实际运行 editor,并执行一次 12 秒冒烟测试。 +- 只有在编译与 12 秒冒烟测试都通过后,才能提交并推送该阶段改动。 + 更新日期: `2026-04-26` -状态: `In Progress, authority/planner、显式 mutation request、destroy reconciliation 已收口,下一阶段聚焦 live projection 去副本化` +状态: `In Progress, live projection payload、常规 sync/presentation 去副本化已收口,下一阶段聚焦交互 / 过渡路径去 controller 读依赖` ## 1. 长期目标 @@ -40,13 +46,16 @@ EditorWindowSystem authoritative window set - `EditorWindowWorkspaceCoordinator` 已不再直接写 authority store。 - live frame 内工作区变化已经通过显式 `workspaceMutation` request 回到 app 层。 - `editor_windowing_phase1_tests` 已覆盖显式 mutation request commit、unknown `windowId` rejection、detached window create/close、destroyed primary closeout 等基础规则。 +- workspace window 已引入显式 `EditorWorkspaceWindowProjection`,标题、minimum size、detached title bar/tab strip 文案改由 projection / authoritative state 导出。 +- `EditorWindowWorkspaceCoordinator` 的常规同步路径已改为 `RefreshWorkspaceProjection(...)`,不再依赖 `ReplaceWorkspaceController(...)` 整体替换 live controller。 +- `xcui_editor_app_smoke` 对应 runner 已支持 12 秒 smoke auto-exit 门禁,能作为阶段收口验证入口。 这意味着前一轮 subplan 里围绕“authority 回写收口”“显式 request 成为主输入”“destroy reconciliation 回到 app 层”的工作已经完成,后续不再继续保留为独立 subplan。 当前离长期目标仍有两个核心缺口: -- live content 仍持有可变 `UIEditorWorkspaceController`,projection 和 authority 的边界还不够硬。 -- `EditorWindowWorkspaceCoordinator` 仍然过厚,把 host 执行、projection 替换、标题刷新、drag/detach 流程和失败回滚串在同一层。 +- live content 仍持有可变 `UIEditorWorkspaceController`,且 drag bootstrap、少量 runtime/过渡路径仍会直接读 controller。 +- `EditorWindowWorkspaceCoordinator` 仍然过厚,把 host 执行、drag/detach 流程、标题刷新和失败回滚串在同一层。 ## 3. 下一个阶段执行计划 diff --git a/editor/AGENT.md b/editor/AGENT.md index 06ddf599..fd3e6ee8 100644 --- a/editor/AGENT.md +++ b/editor/AGENT.md @@ -130,7 +130,8 @@ EditorContext::BuildWorkspaceController() 当前真实行为是: - `EditorWorkspaceWindowContentController::UpdateAndAppend(...)` 会在 layout snapshot 发生变化时发出显式 `workspaceMutation` request。 -- `EditorWindow::TryGetWorkspaceController()` / `ReplaceWorkspaceController(...)` 仍存在,并且仍被 host 同步 / presentation 路径使用。 +- `EditorWorkspaceWindowContentController` 现在持有显式 `EditorWorkspaceWindowProjection`,host 常规同步 / presentation 路径优先消费 projection refresh,而不是整体替换 live controller。 +- `EditorWindow::TryGetWorkspaceController()` 仍保留给局部交互 / 过渡路径使用,但它不再是常规 host 同步主接口。 - 每个 workspace window 依然持有自己的一份 live `UIEditorWorkspaceController`。 因此: @@ -246,9 +247,9 @@ Application::Run - 除 `XCUIEditorLib` 外,app core、windowing、Win32 host、rendering host 还没有真实拆成独立 target。 - `EditorContext` 仍然很重,混合了 shell asset、项目/场景运行时、selection、command focus、utility window request 和状态输出。 - `EditorShellRuntime` 仍然很重,既负责 shell compose / interaction,也负责 hosted panel、viewport request 和 draw append。 -- `EditorWorkspaceWindowContentController` 仍持有 live mutable `UIEditorWorkspaceController`,projection 和 authority 的边界还不够硬。 +- `EditorWorkspaceWindowContentController` 仍持有 live mutable `UIEditorWorkspaceController`,只是 host 常规同步 / presentation 已经切到 projection refresh;projection 和 authority 的边界仍未完全硬化。 - `EditorWindowWorkspaceCoordinator` 仍然过厚,还没有收缩成纯粹的 host executor + native event bridge。 -- `EditorWindow::TryGetWorkspaceController()` / `ReplaceWorkspaceController()` 仍是当前同步路径的一部分,说明 live projection 去状态化还没完成。 +- `EditorWindow::TryGetWorkspaceController()` 仍存在于交互 / 过渡路径,说明 live projection 去状态化还没完成;但常规同步路径已经不再依赖 `ReplaceWorkspaceController()`。 这些都是当前事实,不要在新的规范文档或代码注释里把它们写成“已经解决”。 diff --git a/editor/app/Bootstrap/Application.cpp b/editor/app/Bootstrap/Application.cpp index 3fc01554..8547c1e8 100644 --- a/editor/app/Bootstrap/Application.cpp +++ b/editor/app/Bootstrap/Application.cpp @@ -22,6 +22,7 @@ namespace { constexpr const wchar_t* kWindowClassName = L"XCEditorShellHost"; constexpr const wchar_t* kWindowTitle = L"Main Scene * - Main.xx - XCEngine Editor"; constexpr DWORD kBorderlessWindowStyle = WS_POPUP | WS_THICKFRAME; +constexpr int kDefaultSmokeTestDurationSeconds = 12; void EnableDpiAwareness() { const HMODULE user32 = GetModuleHandleW(L"user32.dll"); @@ -152,6 +153,24 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) { createParams.primary = true; createParams.autoCaptureOnStartup = App::IsEnvironmentFlagEnabled("XCUI_AUTO_CAPTURE_ON_STARTUP"); + m_smokeTestEnabled = App::IsEnvironmentFlagEnabled("XCUIEDITOR_SMOKE_TEST"); + m_smokeTestCloseRequested = false; + m_smokeTestRenderedFrameCount = 0; + m_smokeTestFrameLimit = m_smokeTestEnabled + ? App::TryGetEnvironmentInt("XCUIEDITOR_SMOKE_TEST_FRAME_LIMIT").value_or(0) + : 0; + if (m_smokeTestFrameLimit < 0) { + m_smokeTestFrameLimit = 0; + } + int smokeTestDurationSeconds = m_smokeTestEnabled + ? App::TryGetEnvironmentInt("XCUIEDITOR_SMOKE_TEST_DURATION_SECONDS") + .value_or(kDefaultSmokeTestDurationSeconds) + : 0; + if (smokeTestDurationSeconds < 0) { + smokeTestDurationSeconds = 0; + } + m_smokeTestDuration = std::chrono::seconds(smokeTestDurationSeconds); + m_smokeTestStartTime = {}; UIEditorWorkspaceController primaryWorkspaceController = m_editorContext->BuildWorkspaceController(); std::string windowSystemError = {}; @@ -172,6 +191,13 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) { } AppendUIEditorRuntimeTrace("app", "initialize end"); + if (m_smokeTestEnabled) { + m_smokeTestStartTime = std::chrono::steady_clock::now(); + AppendUIEditorRuntimeTrace( + "smoke", + "enabled durationSeconds=" + std::to_string(smokeTestDurationSeconds) + + " frameLimit=" + std::to_string(m_smokeTestFrameLimit)); + } return true; } @@ -193,9 +219,16 @@ void Application::Shutdown() { if (m_windowClassAtom != 0 && m_hInstance != nullptr) { UnregisterClassW(kWindowClassName, m_hInstance); - m_windowClassAtom = 0; + m_windowClassAtom = 0; } + m_smokeTestStartTime = {}; + m_smokeTestDuration = std::chrono::milliseconds::zero(); + m_smokeTestFrameLimit = 0; + m_smokeTestRenderedFrameCount = 0; + m_smokeTestEnabled = false; + m_smokeTestCloseRequested = false; + AppendUIEditorRuntimeTrace("app", "shutdown end"); ShutdownUIEditorRuntimeTrace(); } @@ -256,6 +289,30 @@ int Application::Run(HINSTANCE hInstance, int nCmdShow) { } m_windowManager->RenderAllWindows(); + if (m_smokeTestEnabled && !m_smokeTestCloseRequested) { + ++m_smokeTestRenderedFrameCount; + const bool reachedFrameLimit = + m_smokeTestFrameLimit > 0 && + m_smokeTestRenderedFrameCount >= m_smokeTestFrameLimit; + const bool reachedDuration = + m_smokeTestDuration.count() > 0 && + m_smokeTestStartTime != std::chrono::steady_clock::time_point{} && + (std::chrono::steady_clock::now() - m_smokeTestStartTime) >= + m_smokeTestDuration; + if (reachedFrameLimit || reachedDuration) { + AppendUIEditorRuntimeTrace( + "smoke", + "auto-exit requested after duration/frame limit"); + m_smokeTestCloseRequested = true; + if (App::EditorWindow* primaryWindow = m_windowManager->FindPrimaryWindow(); + primaryWindow != nullptr && + primaryWindow->GetHwnd() != nullptr) { + PostMessageW(primaryWindow->GetHwnd(), WM_CLOSE, 0, 0); + } else { + PostQuitMessage(0); + } + } + } } else { break; } diff --git a/editor/app/Bootstrap/Application.h b/editor/app/Bootstrap/Application.h index ab323a2a..6c874852 100644 --- a/editor/app/Bootstrap/Application.h +++ b/editor/app/Bootstrap/Application.h @@ -6,6 +6,7 @@ #include +#include #include #include @@ -47,10 +48,16 @@ private: HINSTANCE m_hInstance = nullptr; ATOM m_windowClassAtom = 0; std::filesystem::path m_repoRoot = {}; + std::chrono::steady_clock::time_point m_smokeTestStartTime = {}; + std::chrono::milliseconds m_smokeTestDuration = std::chrono::milliseconds::zero(); std::unique_ptr m_editorContext = {}; std::unique_ptr m_windowSystem = {}; std::unique_ptr m_windowManager = {}; std::unique_ptr m_systemInteractionHost = {}; + int m_smokeTestFrameLimit = 0; + int m_smokeTestRenderedFrameCount = 0; + bool m_smokeTestEnabled = false; + bool m_smokeTestCloseRequested = false; }; int RunXCUIEditorApp(HINSTANCE hInstance, int nCmdShow); diff --git a/editor/app/Platform/Win32/Windowing/EditorWindow.cpp b/editor/app/Platform/Win32/Windowing/EditorWindow.cpp index 5bfb0acc..e6768941 100644 --- a/editor/app/Platform/Win32/Windowing/EditorWindow.cpp +++ b/editor/app/Platform/Win32/Windowing/EditorWindow.cpp @@ -154,6 +154,13 @@ const UIEditorWorkspaceController* EditorWindow::TryGetWorkspaceController() con : nullptr; } +const EditorWorkspaceWindowProjection* EditorWindow::TryGetWorkspaceProjection() const { + const EditorWindowWorkspaceBinding* workspaceBinding = m_runtime->TryGetWorkspaceBinding(); + return workspaceBinding != nullptr + ? workspaceBinding->TryGetWorkspaceProjection() + : nullptr; +} + const UIEditorWorkspaceController& EditorWindow::GetWorkspaceController() const { const UIEditorWorkspaceController* workspaceController = TryGetWorkspaceController(); assert(workspaceController != nullptr); @@ -197,10 +204,10 @@ void EditorWindow::SetTitle(std::wstring title) { m_session->SetTitle(std::move(title)); } -void EditorWindow::ReplaceWorkspaceController(UIEditorWorkspaceController workspaceController) { +void EditorWindow::RefreshWorkspaceProjection(EditorWorkspaceWindowProjection projection) { EditorWindowWorkspaceBinding* workspaceBinding = m_runtime->TryGetWorkspaceBinding(); assert(workspaceBinding != nullptr); - workspaceBinding->ReplaceWorkspaceController(std::move(workspaceController)); + workspaceBinding->RefreshWorkspaceProjection(std::move(projection)); } void EditorWindow::InvalidateHostWindow() const { diff --git a/editor/app/Platform/Win32/Windowing/EditorWindow.h b/editor/app/Platform/Win32/Windowing/EditorWindow.h index 3b92e97b..ce5c9bbb 100644 --- a/editor/app/Platform/Win32/Windowing/EditorWindow.h +++ b/editor/app/Platform/Win32/Windowing/EditorWindow.h @@ -62,6 +62,7 @@ class EditorWindowMessageDispatcher; class EditorWindowRuntimeController; class EditorWindowWorkspaceCoordinator; class EditorWindowSession; +struct EditorWorkspaceWindowProjection; class EditorWindow { public: @@ -93,6 +94,7 @@ public: const std::wstring& GetTitle() const; std::string_view GetCachedTitleText() const; const UIEditorWorkspaceController* TryGetWorkspaceController() const; + const EditorWorkspaceWindowProjection* TryGetWorkspaceProjection() const; const UIEditorWorkspaceController& GetWorkspaceController() const; EditorWindowDockHostBinding* TryGetDockHostBinding(); const EditorWindowDockHostBinding* TryGetDockHostBinding() const; @@ -124,7 +126,7 @@ private: void MarkClosing(); void SetPrimary(bool primary); void SetTitle(std::wstring title); - void ReplaceWorkspaceController(UIEditorWorkspaceController workspaceController); + void RefreshWorkspaceProjection(EditorWorkspaceWindowProjection projection); bool Initialize( const std::filesystem::path& repoRoot, diff --git a/editor/app/Platform/Win32/Windowing/EditorWindowWorkspaceCoordinator.cpp b/editor/app/Platform/Win32/Windowing/EditorWindowWorkspaceCoordinator.cpp index 60d99103..222c1805 100644 --- a/editor/app/Platform/Win32/Windowing/EditorWindowWorkspaceCoordinator.cpp +++ b/editor/app/Platform/Win32/Windowing/EditorWindowWorkspaceCoordinator.cpp @@ -18,6 +18,7 @@ #include #include #include +#include namespace XCEngine::UI::Editor::App { @@ -25,8 +26,7 @@ namespace { struct ExistingWindowSnapshot { std::string windowId = {}; - UIEditorWorkspaceController workspaceController = {}; - std::wstring title = {}; + EditorWorkspaceWindowProjection projection = {}; bool primary = false; }; @@ -49,6 +49,12 @@ POINT ToNativePoint(const EditorWindowScreenPoint& point) { }; } +std::wstring_view ResolvePrimaryWindowTitle(const EditorWindowHostRuntime& hostRuntime) { + return hostRuntime.GetHostConfig().primaryWindowTitle != nullptr + ? std::wstring_view(hostRuntime.GetHostConfig().primaryWindowTitle) + : std::wstring_view{}; +} + bool CanStartGlobalTabDragFromWindow( const EditorWindow& sourceWindow, std::string_view sourceNodeId, @@ -106,6 +112,7 @@ void EditorWindowWorkspaceCoordinator::BindLifecycleCoordinator( } void EditorWindowWorkspaceCoordinator::RegisterExistingWindow(EditorWindow& window) { + RefreshWorkspaceProjectionFromAuthoritativeState(window); RefreshWindowTitle(window); } @@ -117,15 +124,11 @@ void EditorWindowWorkspaceCoordinator::RefreshWindowPresentation(EditorWindow& w } void EditorWindowWorkspaceCoordinator::HandleNativeWindowDestroyed(std::string_view windowId) { - const std::wstring_view primaryWindowTitle = - m_hostRuntime.GetHostConfig().primaryWindowTitle != nullptr - ? std::wstring_view(m_hostRuntime.GetHostConfig().primaryWindowTitle) - : std::wstring_view{}; std::string error = {}; EditorWindowSynchronizationPlan plan = m_windowSystem.BuildPlanForDestroyedWindow( windowId, CaptureHostSnapshots(), - primaryWindowTitle, + ResolvePrimaryWindowTitle(m_hostRuntime), error); if (!plan.valid) { LogRuntimeTrace( @@ -158,28 +161,79 @@ EditorWindowWorkspaceCoordinator::BuildWorkspaceMutationController() const { UIEditorWindowWorkspaceState EditorWindowWorkspaceCoordinator::BuildWindowStateForWindow( const EditorWindow& window) const { + if (const EditorWorkspaceWindowProjection* projection = window.TryGetWorkspaceProjection(); + projection != nullptr) { + return projection->windowState; + } + UIEditorWindowWorkspaceState state = {}; - state.windowId = std::string(window.GetWindowId()); - const UIEditorWorkspaceController* const workspaceController = - window.TryGetWorkspaceController(); - if (workspaceController != nullptr) { + if (const UIEditorWorkspaceController* workspaceController = window.TryGetWorkspaceController(); + workspaceController != nullptr) { + state.windowId = std::string(window.GetWindowId()); state.workspace = workspaceController->GetWorkspace(); state.session = workspaceController->GetSession(); } return state; } +EditorWorkspaceWindowProjection EditorWindowWorkspaceCoordinator::BuildWorkspaceProjectionForState( + const UIEditorWindowWorkspaceState& windowState, + bool primary, + std::wstring title) const { + EditorWorkspaceWindowProjection projection = BuildEditorWorkspaceWindowProjection( + ResolvePrimaryWindowTitle(m_hostRuntime), + m_windowSystem.GetPanelRegistry(), + windowState, + primary); + if (!title.empty()) { + projection.windowTitle = std::move(title); + } + return projection; +} + +EditorWorkspaceWindowProjection EditorWindowWorkspaceCoordinator::BuildWorkspaceProjectionForWindow( + const EditorWindow& window) const { + if (const EditorWorkspaceWindowProjection* projection = window.TryGetWorkspaceProjection(); + projection != nullptr) { + return *projection; + } + + return BuildWorkspaceProjectionForState( + BuildWindowStateForWindow(window), + window.IsPrimary(), + window.GetTitle()); +} + +bool EditorWindowWorkspaceCoordinator::RefreshWorkspaceProjectionFromAuthoritativeState( + EditorWindow& window) const { + if (!window.IsWorkspaceWindow()) { + return false; + } + + const UIEditorWindowWorkspaceState* authoritativeState = + FindUIEditorWindowWorkspaceState( + m_windowSystem.GetWindowSet(), + window.GetWindowId()); + if (authoritativeState == nullptr) { + return false; + } + + const bool primary = m_windowSystem.IsPrimaryWindowId(window.GetWindowId()); + window.SetPrimary(primary); + window.RefreshWorkspaceProjection(BuildWorkspaceProjectionForState( + *authoritativeState, + primary, + {})); + return true; +} + bool EditorWindowWorkspaceCoordinator::CommitLiveWindowMutation( const EditorWindowWorkspaceMutationRequest& request) { - const std::wstring_view primaryWindowTitle = - m_hostRuntime.GetHostConfig().primaryWindowTitle != nullptr - ? std::wstring_view(m_hostRuntime.GetHostConfig().primaryWindowTitle) - : std::wstring_view{}; std::string error = {}; EditorWindowSynchronizationPlan plan = m_windowSystem.BuildPlanForWorkspaceMutationRequest( request, CaptureHostSnapshots(), - primaryWindowTitle, + ResolvePrimaryWindowTitle(m_hostRuntime), error); if (!plan.valid) { LogRuntimeTrace( @@ -193,19 +247,12 @@ bool EditorWindowWorkspaceCoordinator::CommitLiveWindowMutation( } void EditorWindowWorkspaceCoordinator::RefreshWindowTitle(EditorWindow& window) const { - if (window.TryGetWorkspaceController() == nullptr) { + const EditorWorkspaceWindowProjection* projection = window.TryGetWorkspaceProjection(); + if (projection == nullptr || projection->windowTitle.empty()) { return; } - const std::wstring_view primaryWindowTitle = - m_hostRuntime.GetHostConfig().primaryWindowTitle != nullptr - ? std::wstring_view(m_hostRuntime.GetHostConfig().primaryWindowTitle) - : std::wstring_view{}; - const std::wstring title = ResolveEditorWindowPresentationTitle( - primaryWindowTitle, - m_windowSystem.GetPanelRegistry(), - BuildWindowStateForWindow(window), - window.IsPrimary()); + const std::wstring& title = projection->windowTitle; if (title == window.GetTitle()) { return; } @@ -239,9 +286,7 @@ bool EditorWindowWorkspaceCoordinator::SynchronizeWindowsFromWindowSet( EditorWindowSynchronizationPlan plan = m_windowSystem.BuildPlanForWindowSet( windowSet, CaptureHostSnapshots(), - m_hostRuntime.GetHostConfig().primaryWindowTitle != nullptr - ? std::wstring_view(m_hostRuntime.GetHostConfig().primaryWindowTitle) - : std::wstring_view{}, + ResolvePrimaryWindowTitle(m_hostRuntime), preferredNewWindowId, preferredPlacement, error); @@ -271,7 +316,8 @@ std::vector EditorWindowWorkspaceCoordinator::CaptureH snapshot.destroyed = window->IsDestroyed(); snapshot.hasNativeWindow = window->GetHwnd() != nullptr; snapshot.title = window->GetTitle(); - if (window->TryGetWorkspaceController() != nullptr) { + if (window->TryGetWorkspaceProjection() != nullptr || + window->TryGetWorkspaceController() != nullptr) { snapshot.hasWorkspaceProjection = true; snapshot.workspaceState = BuildWindowStateForWindow(*window); } @@ -289,13 +335,10 @@ bool EditorWindowWorkspaceCoordinator::ApplySynchronizationPlan( return; } - window->ReplaceWorkspaceController(snapshot.workspaceController); window->SetPrimary(snapshot.primary); - window->SetTitle(snapshot.title); + window->RefreshWorkspaceProjection(snapshot.projection); window->ResetInteractionState(); - if (window->GetHwnd() != nullptr) { - SetWindowTextW(window->GetHwnd(), window->GetTitle().c_str()); - } + RefreshWindowTitle(*window); }; const auto destroyAndEraseWindowById = [this](std::string_view windowId) { @@ -326,19 +369,17 @@ bool EditorWindowWorkspaceCoordinator::ApplySynchronizationPlan( existingWindowSnapshots.push_back(ExistingWindowSnapshot{ std::string(existingWindow->GetWindowId()), - existingWindow->GetWorkspaceController(), - existingWindow->GetTitle(), + BuildWorkspaceProjectionForWindow(*existingWindow), existingWindow->IsPrimary(), }); + EditorWorkspaceWindowProjection projection = BuildWorkspaceProjectionForState( + action.update.windowState, + action.update.primary, + action.update.title); existingWindow->SetPrimary(action.update.primary); - existingWindow->ReplaceWorkspaceController(BuildWorkspaceControllerForWindowState( - m_windowSystem.GetPanelRegistry(), - action.update.windowState)); + existingWindow->RefreshWorkspaceProjection(std::move(projection)); existingWindow->ResetInteractionState(); - existingWindow->SetTitle(action.update.title); - if (existingWindow->GetHwnd() != nullptr) { - SetWindowTextW(existingWindow->GetHwnd(), existingWindow->GetTitle().c_str()); - } + RefreshWindowTitle(*existingWindow); break; } case EditorWindowSynchronizationActionKind::CreateWorkspaceWindow: { @@ -354,11 +395,12 @@ bool EditorWindowWorkspaceCoordinator::ApplySynchronizationPlan( createParams.initialHeight = action.create.placement.initialHeight; } - if (m_hostRuntime.CreateWorkspaceWindow( + EditorWindow* const createdWindow = m_hostRuntime.CreateWorkspaceWindow( BuildWorkspaceControllerForWindowState( m_windowSystem.GetPanelRegistry(), action.create.windowState), - createParams) == nullptr) { + createParams); + if (createdWindow == nullptr) { for (const ExistingWindowSnapshot& snapshot : existingWindowSnapshots) { restoreWindowSnapshot(snapshot); } @@ -367,6 +409,11 @@ bool EditorWindowWorkspaceCoordinator::ApplySynchronizationPlan( } return false; } + createdWindow->RefreshWorkspaceProjection(BuildWorkspaceProjectionForState( + action.create.windowState, + action.create.primary, + action.create.title)); + RefreshWindowTitle(*createdWindow); createdWindowIds.push_back(action.create.windowState.windowId); break; } @@ -702,9 +749,16 @@ bool EditorWindowWorkspaceCoordinator::TryStartGlobalTabDrag( return false; } + const EditorWorkspaceWindowProjection* detachedProjection = + detachedWindow->TryGetWorkspaceProjection(); + if (detachedProjection == nullptr) { + LogRuntimeTrace("drag", "detached drag window projection was not initialized."); + return false; + } + BeginGlobalTabDragSession( detachedWindow->GetWindowId(), - detachedWindow->GetWorkspaceController().GetWorkspace().root.nodeId, + detachedProjection->windowState.workspace.root.nodeId, request.panelId, ToNativePoint(request.screenPoint), dragHotspot); diff --git a/editor/app/Platform/Win32/Windowing/EditorWindowWorkspaceCoordinator.h b/editor/app/Platform/Win32/Windowing/EditorWindowWorkspaceCoordinator.h index 84e520c1..c01a495d 100644 --- a/editor/app/Platform/Win32/Windowing/EditorWindowWorkspaceCoordinator.h +++ b/editor/app/Platform/Win32/Windowing/EditorWindowWorkspaceCoordinator.h @@ -4,6 +4,7 @@ #define NOMINMAX #endif +#include "Windowing/EditorWorkspaceWindowProjection.h" #include "Windowing/Frame/EditorWindowTransferRequests.h" #include "Windowing/System/EditorWindowSynchronizationPlan.h" @@ -75,6 +76,13 @@ private: LONG preferredHeight = 0); UIEditorWindowWorkspaceState BuildWindowStateForWindow(const EditorWindow& window) const; bool CommitLiveWindowMutation(const EditorWindowWorkspaceMutationRequest& request); + EditorWorkspaceWindowProjection BuildWorkspaceProjectionForState( + const UIEditorWindowWorkspaceState& windowState, + bool primary, + std::wstring title) const; + EditorWorkspaceWindowProjection BuildWorkspaceProjectionForWindow( + const EditorWindow& window) const; + bool RefreshWorkspaceProjectionFromAuthoritativeState(EditorWindow& window) const; void RefreshWindowTitle(EditorWindow& window) const; void BeginGlobalTabDragSession( std::string_view panelWindowId, diff --git a/editor/app/Windowing/Content/EditorWindowContentController.h b/editor/app/Windowing/Content/EditorWindowContentController.h index d95cde47..a1351f64 100644 --- a/editor/app/Windowing/Content/EditorWindowContentController.h +++ b/editor/app/Windowing/Content/EditorWindowContentController.h @@ -1,5 +1,6 @@ #pragma once +#include "Windowing/EditorWorkspaceWindowProjection.h" #include "Windowing/Frame/EditorWindowTransferRequests.h" #include @@ -73,7 +74,8 @@ public: virtual ~EditorWindowWorkspaceBinding() = default; virtual const UIEditorWorkspaceController* TryGetWorkspaceController() const = 0; - virtual void ReplaceWorkspaceController(UIEditorWorkspaceController workspaceController) = 0; + virtual const EditorWorkspaceWindowProjection* TryGetWorkspaceProjection() const = 0; + virtual void RefreshWorkspaceProjection(EditorWorkspaceWindowProjection projection) = 0; }; class EditorWindowDockHostBinding { diff --git a/editor/app/Windowing/Content/EditorWorkspaceWindowContentController.cpp b/editor/app/Windowing/Content/EditorWorkspaceWindowContentController.cpp index f1b2e772..cba85d52 100644 --- a/editor/app/Windowing/Content/EditorWorkspaceWindowContentController.cpp +++ b/editor/app/Windowing/Content/EditorWorkspaceWindowContentController.cpp @@ -30,13 +30,30 @@ EditorWindowContentCursorKind ToContentCursorKind( } } +EditorWorkspaceWindowProjection BuildWorkspaceProjectionFromController( + const UIEditorWorkspaceController& workspaceController, + std::string_view windowId) { + EditorWorkspaceWindowProjection projection = {}; + projection.windowState.windowId = std::string(windowId); + projection.windowState.workspace = workspaceController.GetWorkspace(); + projection.windowState.session = workspaceController.GetSession(); + projection.minimumOuterSize = ResolveUIEditorDetachedWorkspaceMinimumOuterSize( + workspaceController); + projection.useDetachedTitleBarTabStrip = HasUIEditorSingleVisibleRootTab(workspaceController); + projection.tabStripTitleText = ResolveUIEditorDetachedWorkspaceTitle(workspaceController); + projection.detachedWindowTitleText = ResolveUIEditorDetachedWorkspaceTitle(workspaceController); + return projection; +} + } // namespace EditorWorkspaceWindowContentController::EditorWorkspaceWindowContentController( std::string windowId, UIEditorWorkspaceController workspaceController) : m_windowId(std::move(windowId)), - m_workspaceController(std::move(workspaceController)) {} + m_workspaceController(std::move(workspaceController)) { + m_projection = BuildWorkspaceProjectionFromController(m_workspaceController, m_windowId); +} EditorWorkspaceWindowContentController::~EditorWorkspaceWindowContentController() = default; @@ -85,9 +102,27 @@ EditorWorkspaceWindowContentController::TryGetWorkspaceController() const { return &m_workspaceController; } -void EditorWorkspaceWindowContentController::ReplaceWorkspaceController( - UIEditorWorkspaceController workspaceController) { - m_workspaceController = std::move(workspaceController); +const EditorWorkspaceWindowProjection* +EditorWorkspaceWindowContentController::TryGetWorkspaceProjection() const { + return &m_projection; +} + +void EditorWorkspaceWindowContentController::RefreshWorkspaceProjection( + EditorWorkspaceWindowProjection projection) { + projection.windowState.windowId = m_windowId; + const auto currentSnapshot = BuildUIEditorWorkspaceLayoutSnapshot( + m_workspaceController.GetWorkspace(), + m_workspaceController.GetSession()); + const auto nextSnapshot = BuildUIEditorWorkspaceLayoutSnapshot( + projection.windowState.workspace, + projection.windowState.session); + if (!AreUIEditorWorkspaceLayoutSnapshotsEquivalent(currentSnapshot, nextSnapshot)) { + m_workspaceController = UIEditorWorkspaceController( + m_workspaceController.GetPanelRegistry(), + projection.windowState.workspace, + projection.windowState.session); + } + m_projection = std::move(projection); } void EditorWorkspaceWindowContentController::Initialize( @@ -205,21 +240,25 @@ EditorWindowContentCursorKind EditorWorkspaceWindowContentController::GetDockCur } ::XCEngine::UI::UISize EditorWorkspaceWindowContentController::ResolveMinimumOuterSize() const { - return ResolveUIEditorDetachedWorkspaceMinimumOuterSize(m_workspaceController); + return m_projection.minimumOuterSize; } bool EditorWorkspaceWindowContentController::ShouldUseDetachedTitleBarTabStrip() const { - return HasUIEditorSingleVisibleRootTab(m_workspaceController); + return m_projection.useDetachedTitleBarTabStrip; } std::string EditorWorkspaceWindowContentController::ResolveTabStripTitleText( std::string_view fallbackTitle) const { - return ResolveUIEditorDetachedWorkspaceTitle(m_workspaceController, fallbackTitle); + return m_projection.tabStripTitleText.empty() + ? std::string(fallbackTitle) + : m_projection.tabStripTitleText; } std::string EditorWorkspaceWindowContentController::ResolveDetachedWindowTitleText( std::string_view fallbackWindowTitle) const { - return ResolveUIEditorDetachedWorkspaceTitle(m_workspaceController, fallbackWindowTitle); + return m_projection.detachedWindowTitleText.empty() + ? std::string(fallbackWindowTitle) + : m_projection.detachedWindowTitleText; } std::unique_ptr CreateEditorWorkspaceWindowContentController( diff --git a/editor/app/Windowing/Content/EditorWorkspaceWindowContentController.h b/editor/app/Windowing/Content/EditorWorkspaceWindowContentController.h index 3bf758a4..e6b18df1 100644 --- a/editor/app/Windowing/Content/EditorWorkspaceWindowContentController.h +++ b/editor/app/Windowing/Content/EditorWorkspaceWindowContentController.h @@ -30,7 +30,8 @@ public: const EditorWindowInputFeedbackBinding* TryGetInputFeedbackBinding() const override; const EditorWindowTitleBarBinding* TryGetTitleBarBinding() const override; const UIEditorWorkspaceController* TryGetWorkspaceController() const override; - void ReplaceWorkspaceController(UIEditorWorkspaceController workspaceController) override; + const EditorWorkspaceWindowProjection* TryGetWorkspaceProjection() const override; + void RefreshWorkspaceProjection(EditorWorkspaceWindowProjection projection) override; void Initialize(const EditorWindowContentInitializationContext& context) override; void Shutdown() override; @@ -73,6 +74,7 @@ public: private: std::string m_windowId = {}; UIEditorWorkspaceController m_workspaceController = {}; + EditorWorkspaceWindowProjection m_projection = {}; EditorShellRuntime m_shellRuntime = {}; EditorWindowFrameOrchestrator m_frameOrchestrator = {}; }; diff --git a/editor/app/Windowing/EditorWorkspaceWindowProjection.h b/editor/app/Windowing/EditorWorkspaceWindowProjection.h new file mode 100644 index 00000000..e0305035 --- /dev/null +++ b/editor/app/Windowing/EditorWorkspaceWindowProjection.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +#include + +namespace XCEngine::UI::Editor::App { + +struct EditorWorkspaceWindowProjection { + UIEditorWindowWorkspaceState windowState = {}; + ::XCEngine::UI::UISize minimumOuterSize = ::XCEngine::UI::UISize(640.0f, 360.0f); + bool useDetachedTitleBarTabStrip = false; + std::string tabStripTitleText = {}; + std::string detachedWindowTitleText = {}; + std::wstring windowTitle = {}; +}; + +} // namespace XCEngine::UI::Editor::App diff --git a/editor/app/Windowing/System/EditorWindowPresentationPolicy.cpp b/editor/app/Windowing/System/EditorWindowPresentationPolicy.cpp index 607ff6c8..c28b7205 100644 --- a/editor/app/Windowing/System/EditorWindowPresentationPolicy.cpp +++ b/editor/app/Windowing/System/EditorWindowPresentationPolicy.cpp @@ -23,6 +23,29 @@ UIEditorWorkspaceController BuildWorkspaceControllerForWindowState( windowState.session); } +EditorWorkspaceWindowProjection BuildEditorWorkspaceWindowProjection( + std::wstring_view primaryWindowTitle, + const UIEditorPanelRegistry& panelRegistry, + const UIEditorWindowWorkspaceState& windowState, + bool primary) { + const UIEditorWorkspaceController workspaceController = + BuildWorkspaceControllerForWindowState(panelRegistry, windowState); + + EditorWorkspaceWindowProjection projection = {}; + projection.windowState = windowState; + projection.minimumOuterSize = ResolveUIEditorDetachedWorkspaceMinimumOuterSize( + workspaceController); + projection.useDetachedTitleBarTabStrip = HasUIEditorSingleVisibleRootTab(workspaceController); + projection.tabStripTitleText = ResolveUIEditorDetachedWorkspaceTitle(workspaceController); + projection.detachedWindowTitleText = ResolveUIEditorDetachedWorkspaceTitle(workspaceController); + projection.windowTitle = ResolveEditorWindowPresentationTitle( + primaryWindowTitle, + panelRegistry, + windowState, + primary); + return projection; +} + std::wstring ResolveEditorWindowPresentationTitle( std::wstring_view primaryWindowTitle, const UIEditorPanelRegistry& panelRegistry, diff --git a/editor/app/Windowing/System/EditorWindowPresentationPolicy.h b/editor/app/Windowing/System/EditorWindowPresentationPolicy.h index a63cbb38..a0d0b275 100644 --- a/editor/app/Windowing/System/EditorWindowPresentationPolicy.h +++ b/editor/app/Windowing/System/EditorWindowPresentationPolicy.h @@ -1,5 +1,7 @@ #pragma once +#include "Windowing/EditorWorkspaceWindowProjection.h" + #include #include @@ -12,6 +14,12 @@ UIEditorWorkspaceController BuildWorkspaceControllerForWindowState( const UIEditorPanelRegistry& panelRegistry, const UIEditorWindowWorkspaceState& windowState); +EditorWorkspaceWindowProjection BuildEditorWorkspaceWindowProjection( + std::wstring_view primaryWindowTitle, + const UIEditorPanelRegistry& panelRegistry, + const UIEditorWindowWorkspaceState& windowState, + bool primary); + std::wstring ResolveEditorWindowPresentationTitle( std::wstring_view primaryWindowTitle, const UIEditorPanelRegistry& panelRegistry, diff --git a/tests/UI/Editor/smoke/xcui_editor_app_smoke_runner.cpp b/tests/UI/Editor/smoke/xcui_editor_app_smoke_runner.cpp index d538e18e..9817b244 100644 --- a/tests/UI/Editor/smoke/xcui_editor_app_smoke_runner.cpp +++ b/tests/UI/Editor/smoke/xcui_editor_app_smoke_runner.cpp @@ -17,7 +17,7 @@ namespace { constexpr auto kLaunchTimeout = std::chrono::seconds(20); -constexpr auto kShutdownTimeout = std::chrono::seconds(10); +constexpr auto kShutdownTimeout = std::chrono::seconds(25); constexpr auto kPollInterval = std::chrono::milliseconds(100); constexpr const char* kReadyTrace = "[app] shell runtime initialized:"; @@ -125,7 +125,8 @@ bool LaunchEditorProcess( const std::filesystem::path& executablePath, PROCESS_INFORMATION& outProcessInfo) { SetEnvironmentVariableW(L"XCUI_AUTO_CAPTURE_ON_STARTUP", L"0"); - SetEnvironmentVariableW(L"XCUIEDITOR_SMOKE_TEST", nullptr); + SetEnvironmentVariableW(L"XCUIEDITOR_SMOKE_TEST", L"1"); + SetEnvironmentVariableW(L"XCUIEDITOR_SMOKE_TEST_DURATION_SECONDS", L"12"); SetEnvironmentVariableW(L"XCUIEDITOR_SMOKE_TEST_FRAME_LIMIT", nullptr); STARTUPINFOW startupInfo = {}; @@ -151,23 +152,15 @@ bool LaunchEditorProcess( bool WaitForEditorReady( HANDLE processHandle, - DWORD processId, - const std::filesystem::path& runtimeLogPath, - HWND& outWindow) { + const std::filesystem::path& runtimeLogPath) { const auto deadline = std::chrono::steady_clock::now() + kLaunchTimeout; - outWindow = nullptr; while (std::chrono::steady_clock::now() < deadline) { if (WaitForSingleObject(processHandle, 0) == WAIT_OBJECT_0) { return false; } - const bool shellReady = FileContains(runtimeLogPath, kReadyTrace); - const HWND visibleWindow = FindProcessWindow(processId, true); - const HWND processWindow = - visibleWindow != nullptr ? visibleWindow : FindProcessWindow(processId, false); - if (shellReady && processWindow != nullptr) { - outWindow = processWindow; + if (FileContains(runtimeLogPath, kReadyTrace)) { return true; } @@ -271,12 +264,9 @@ int wmain(int argc, wchar_t* argv[]) { return 1; } - HWND mainWindow = nullptr; if (!WaitForEditorReady( processInfo.hProcess, - processInfo.dwProcessId, - runtimeLogPath, - mainWindow)) { + runtimeLogPath)) { const DWORD waitState = WaitForSingleObject(processInfo.hProcess, 0); const std::string message = waitState == WAIT_OBJECT_0 @@ -288,13 +278,6 @@ int wmain(int argc, wchar_t* argv[]) { return 1; } - if (!PostMessageW(mainWindow, WM_CLOSE, 0, 0)) { - PrintFailure("failed to post WM_CLOSE to XCUIEditorApp"); - TerminateProcess(processInfo.hProcess, 3); - CloseProcessHandles(processInfo); - return 1; - } - DWORD exitCode = 0; if (!WaitForEditorExit( processInfo.hProcess, diff --git a/tests/UI/Editor/unit/test_editor_window_synchronization_planner.cpp b/tests/UI/Editor/unit/test_editor_window_synchronization_planner.cpp index ad14d167..dc4ab252 100644 --- a/tests/UI/Editor/unit/test_editor_window_synchronization_planner.cpp +++ b/tests/UI/Editor/unit/test_editor_window_synchronization_planner.cpp @@ -1,6 +1,7 @@ #include #include "Windowing/Frame/EditorWindowTransferRequests.h" +#include "Windowing/System/EditorWindowPresentationPolicy.h" #include "Windowing/System/EditorWindowSystem.h" #include @@ -15,6 +16,7 @@ using XCEngine::UI::Editor::App::EditorWindowSynchronizationActionKind; using XCEngine::UI::Editor::App::EditorWindowSynchronizationPlannerInput; using XCEngine::UI::Editor::App::EditorWindowSystem; using XCEngine::UI::Editor::App::EditorWindowWorkspaceMutationRequest; +using XCEngine::UI::Editor::App::BuildEditorWorkspaceWindowProjection; using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController; using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel; using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack; @@ -265,6 +267,64 @@ TEST(EditorWindowSynchronizationPlannerTest, ProducesUpdateActionWhenPrimaryWind EXPECT_EQ(plan.actions[1].update.title, L"Main Scene - XCEngine Editor"); } +TEST(EditorWindowSynchronizationPlannerTest, WorkspaceProjectionBuildsDetachedPresentationFields) { + UIEditorPanelRegistry registry = {}; + registry.panels = { + { "doc-a", "Document A", {}, true, true, true, ::XCEngine::UI::UISize(800.0f, 600.0f) }, + { "inspector", "Inspector", {}, true, true, true, ::XCEngine::UI::UISize(420.0f, 360.0f) }, + }; + + UIEditorWorkspaceModel workspace = {}; + workspace.root = BuildUIEditorWorkspaceSingleTabStack( + "inspector-stack", + "inspector", + "Inspector", + true); + workspace.activePanelId = "inspector"; + + const auto workspaceController = + BuildDefaultUIEditorWorkspaceController(registry, workspace); + XCEngine::UI::Editor::UIEditorWindowWorkspaceState windowState = {}; + windowState.windowId = "detached-inspector"; + windowState.workspace = workspaceController.GetWorkspace(); + windowState.session = workspaceController.GetSession(); + + const auto projection = BuildEditorWorkspaceWindowProjection( + L"Main Scene - XCEngine Editor", + registry, + windowState, + false); + + EXPECT_EQ(projection.windowState.windowId, "detached-inspector"); + EXPECT_TRUE(projection.useDetachedTitleBarTabStrip); + EXPECT_EQ(projection.tabStripTitleText, "Inspector"); + EXPECT_EQ(projection.detachedWindowTitleText, "Inspector"); + EXPECT_EQ(projection.windowTitle, L"Inspector - XCEngine Editor"); + EXPECT_FLOAT_EQ(projection.minimumOuterSize.width, 420.0f); + EXPECT_FLOAT_EQ(projection.minimumOuterSize.height, 360.0f); +} + +TEST(EditorWindowSynchronizationPlannerTest, WorkspaceProjectionBuildsPrimaryPresentationFields) { + const UIEditorPanelRegistry registry = BuildPanelRegistry(); + const auto workspaceController = + BuildDefaultUIEditorWorkspaceController(registry, BuildWorkspace()); + XCEngine::UI::Editor::UIEditorWindowWorkspaceState windowState = {}; + windowState.windowId = "main"; + windowState.workspace = workspaceController.GetWorkspace(); + windowState.session = workspaceController.GetSession(); + + const auto projection = BuildEditorWorkspaceWindowProjection( + L"Main Scene - XCEngine Editor", + registry, + windowState, + true); + + EXPECT_EQ(projection.windowTitle, L"Main Scene - XCEngine Editor"); + EXPECT_FALSE(projection.useDetachedTitleBarTabStrip); + EXPECT_FLOAT_EQ(projection.minimumOuterSize.width, 640.0f); + EXPECT_FLOAT_EQ(projection.minimumOuterSize.height, 360.0f); +} + TEST(EditorWindowSynchronizationPlannerTest, RejectsInvalidTargetWindowSet) { EditorWindowSystem system = BuildSystem(); XCEngine::UI::Editor::UIEditorWindowWorkspaceSet invalidWindowSet = {};