#include "Platform/Win32/EditorWindowRuntimeController.h" #include "Bootstrap/EditorResources.h" #include "Composition/EditorContext.h" #include "Platform/Win32/EditorWindowSupport.h" #include "Support/EmbeddedPngLoader.h" #include #include #include #include #include #include #include namespace XCEngine::UI::Editor::App { using App::LoadEmbeddedPngTexture; using namespace EditorWindowSupport; namespace { constexpr float kFrameTimeSmoothingFactor = 0.12f; constexpr float kFrameStatsDisplayRefreshIntervalSeconds = 0.25f; } EditorWindowRuntimeController::EditorWindowRuntimeController( std::unique_ptr contentController) : m_contentController(std::move(contentController)) {} EditorWindowRuntimeController::~EditorWindowRuntimeController() = default; bool EditorWindowRuntimeController::IsReady() const { return m_ready; } const UIEditorWorkspaceController* EditorWindowRuntimeController::TryGetWorkspaceController() const { return m_contentController != nullptr ? m_contentController->TryGetWorkspaceController() : nullptr; } UIEditorWorkspaceController* EditorWindowRuntimeController::TryGetMutableWorkspaceController() { return m_contentController != nullptr ? m_contentController->TryGetMutableWorkspaceController() : nullptr; } const UIEditorWorkspaceController& EditorWindowRuntimeController::GetWorkspaceController() const { const UIEditorWorkspaceController* workspaceController = TryGetWorkspaceController(); assert(workspaceController != nullptr); return *workspaceController; } UIEditorWorkspaceController& EditorWindowRuntimeController::GetMutableWorkspaceController() { UIEditorWorkspaceController* workspaceController = TryGetMutableWorkspaceController(); assert(workspaceController != nullptr); return *workspaceController; } void EditorWindowRuntimeController::ReplaceWorkspaceController( UIEditorWorkspaceController workspaceController) { assert(m_contentController != nullptr); m_contentController->ReplaceWorkspaceController(std::move(workspaceController)); } const UIEditorShellInteractionFrame& EditorWindowRuntimeController::GetShellFrame() const { assert(m_contentController != nullptr); return m_contentController->GetShellFrame(); } const UIEditorShellInteractionState& EditorWindowRuntimeController::GetShellInteractionState() const { assert(m_contentController != nullptr); return m_contentController->GetShellInteractionState(); } void EditorWindowRuntimeController::SetExternalDockHostDropPreview( const Widgets::UIEditorDockHostDropPreviewState& preview) { assert(m_contentController != nullptr); m_contentController->SetExternalDockHostDropPreview(preview); } void EditorWindowRuntimeController::ClearExternalDockHostDropPreview() { if (m_contentController != nullptr) { m_contentController->ClearExternalDockHostDropPreview(); } } bool EditorWindowRuntimeController::TryResolveDockTabDragHotspot( std::string_view nodeId, std::string_view panelId, const ::XCEngine::UI::UIPoint& point, ::XCEngine::UI::UIPoint& outHotspot) const { return m_contentController != nullptr && m_contentController->TryResolveDockTabDragHotspot( nodeId, panelId, point, outHotspot); } UIEditorDockHostTabDropTarget EditorWindowRuntimeController::ResolveDockTabDropTarget( const ::XCEngine::UI::UIPoint& point) const { assert(m_contentController != nullptr); return m_contentController->ResolveDockTabDropTarget(point); } bool EditorWindowRuntimeController::HasHostedContentCapture() const { return m_contentController != nullptr && m_contentController->HasHostedContentCapture(); } bool EditorWindowRuntimeController::HasShellInteractiveCapture() const { return m_contentController != nullptr && m_contentController->HasShellInteractiveCapture(); } bool EditorWindowRuntimeController::HasInteractiveCapture() const { return m_contentController != nullptr && m_contentController->HasInteractiveCapture(); } EditorWindowContentCursorKind EditorWindowRuntimeController::GetHostedContentCursorKind() const { return m_contentController != nullptr ? m_contentController->GetHostedContentCursorKind() : EditorWindowContentCursorKind::Arrow; } EditorWindowContentCursorKind EditorWindowRuntimeController::GetDockCursorKind() const { return m_contentController != nullptr ? m_contentController->GetDockCursorKind() : EditorWindowContentCursorKind::Arrow; } ::XCEngine::UI::UISize EditorWindowRuntimeController::ResolveMinimumOuterSize() const { assert(m_contentController != nullptr); return m_contentController->ResolveMinimumOuterSize(); } bool EditorWindowRuntimeController::ShouldUseDetachedTitleBarTabStrip() const { return m_contentController != nullptr && m_contentController->ShouldUseDetachedTitleBarTabStrip(); } std::string EditorWindowRuntimeController::ResolveTabStripTitleText( std::string_view fallbackTitle) const { return m_contentController != nullptr ? m_contentController->ResolveTabStripTitleText(fallbackTitle) : std::string(fallbackTitle); } std::string EditorWindowRuntimeController::ResolveDetachedWindowTitleText( std::string_view fallbackWindowTitle) const { return m_contentController != nullptr ? m_contentController->ResolveDetachedWindowTitleText(fallbackWindowTitle) : std::string(fallbackWindowTitle); } void EditorWindowRuntimeController::SetDpiScale(float dpiScale) { m_dpiScale = dpiScale > 0.0f ? dpiScale : 1.0f; m_textSystem.SetDpiScale(m_dpiScale); m_uiRenderer.SetDpiScale(m_dpiScale); } ::XCEngine::UI::Editor::UIEditorTextMeasurer& EditorWindowRuntimeController::GetTextMeasurer() { return m_textSystem; } const ::XCEngine::UI::Editor::UIEditorTextMeasurer& EditorWindowRuntimeController::GetTextMeasurer() const { return m_textSystem; } const ::XCEngine::UI::UITextureHandle& EditorWindowRuntimeController::GetTitleBarLogoIcon() const { return m_titleBarLogoIcon; } bool EditorWindowRuntimeController::Initialize( HWND hwnd, const std::filesystem::path& repoRoot, EditorContext& editorContext, const std::filesystem::path& captureRoot, bool autoCaptureOnStartup) { if (hwnd == nullptr) { LogRuntimeTrace("app", "window initialize skipped: hwnd is null"); return false; } RECT clientRect = {}; GetClientRect(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(hwnd, clientWidth, clientHeight)) { LogRuntimeTrace("app", "d3d12 window renderer initialization failed"); return false; } if (!m_textureHost.Initialize(m_windowRenderer)) { LogRuntimeTrace("app", "d3d12 ui texture host initialization failed"); m_windowRenderer.Shutdown(); return false; } if (!m_textSystem.Initialize()) { LogRuntimeTrace("app", "d3d12 ui text system initialization failed"); m_textureHost.Shutdown(); m_windowRenderer.Shutdown(); return false; } m_textSystem.SetDpiScale(m_dpiScale); if (!m_uiRenderer.Initialize(m_windowRenderer, m_textureHost, m_textSystem)) { LogRuntimeTrace("app", "d3d12 ui renderer initialization failed"); m_textSystem.Shutdown(); m_textureHost.Shutdown(); m_windowRenderer.Shutdown(); return false; } m_uiRenderer.SetDpiScale(m_dpiScale); const Host::D3D12WindowRenderLoopAttachResult attachResult = m_windowRenderLoop.Attach(m_uiRenderer, m_windowRenderer); if (!attachResult.warning.empty()) { LogRuntimeTrace("app", attachResult.warning); } editorContext.AttachTextMeasurer(m_textSystem); assert(m_contentController != nullptr); m_contentController->Initialize(EditorWindowContentInitializationContext{ .repoRoot = repoRoot, .editorContext = editorContext, .textureHost = m_textureHost, .textMeasurer = m_textSystem, .viewportRenderer = m_windowRenderer, }); m_contentController->SetViewportSurfacePresentationEnabled( attachResult.hasViewportSurfacePresentation); std::string titleBarLogoError = {}; if (!LoadEmbeddedPngTexture( m_textureHost, IDR_PNG_LOGO_ICON, m_titleBarLogoIcon, titleBarLogoError)) { LogRuntimeTrace("icons", "titlebar logo_icon.png: " + titleBarLogoError); } if (const UIEditorWorkspaceController* workspaceController = TryGetWorkspaceController(); workspaceController != nullptr) { LogRuntimeTrace( "app", "shell runtime initialized: " + editorContext.DescribeWorkspaceState( *workspaceController, m_contentController->GetShellInteractionState())); } else { LogRuntimeTrace("app", "window content initialized: non-workspace content"); } ResetFrameTiming(); m_ready = true; m_screenshotController.Initialize(captureRoot); if (autoCaptureOnStartup) { m_screenshotController.RequestCapture("startup"); editorContext.SetStatus("Capture", "Startup capture requested."); } return true; } void EditorWindowRuntimeController::Shutdown() { m_ready = false; ResetFrameTiming(); LogRuntimeTrace("window-close", "EditorWindowRuntimeController::Shutdown stage=WaitForGpuIdle"); m_windowRenderer.WaitForGpuIdle(); LogRuntimeTrace( "window-close", "EditorWindowRuntimeController::Shutdown stage=ScreenshotController"); m_screenshotController.Shutdown(); LogRuntimeTrace("window-close", "EditorWindowRuntimeController::Shutdown stage=WindowContent"); if (m_contentController != nullptr) { m_contentController->Shutdown(); } LogRuntimeTrace("window-close", "EditorWindowRuntimeController::Shutdown stage=RenderLoopDetach"); m_windowRenderLoop.Detach(); LogRuntimeTrace("window-close", "EditorWindowRuntimeController::Shutdown stage=UiRenderer"); m_uiRenderer.Shutdown(); LogRuntimeTrace("window-close", "EditorWindowRuntimeController::Shutdown stage=TextSystem"); m_textSystem.Shutdown(); LogRuntimeTrace("window-close", "EditorWindowRuntimeController::Shutdown stage=TitleBarLogo"); m_textureHost.ReleaseTexture(m_titleBarLogoIcon); LogRuntimeTrace("window-close", "EditorWindowRuntimeController::Shutdown stage=TextureHost"); m_textureHost.Shutdown(); LogRuntimeTrace("window-close", "EditorWindowRuntimeController::Shutdown stage=WindowRenderer"); m_windowRenderer.Shutdown(); m_dpiScale = 1.0f; LogRuntimeTrace("window-close", "EditorWindowRuntimeController::Shutdown end"); } void EditorWindowRuntimeController::ResetInteractionState() { if (m_contentController != nullptr) { m_contentController->ResetInteractionState(); } ResetFrameTiming(); } bool EditorWindowRuntimeController::ApplyResize(UINT width, UINT height) { if (!m_ready || width == 0u || height == 0u) { return false; } const Host::D3D12WindowRenderLoopResizeResult resizeResult = m_windowRenderLoop.ApplyResize(width, height); if (m_contentController != nullptr) { m_contentController->SetViewportSurfacePresentationEnabled( resizeResult.hasViewportSurfacePresentation); } if (!resizeResult.windowRendererWarning.empty()) { LogRuntimeTrace("present", resizeResult.windowRendererWarning); } return resizeResult.hasViewportSurfacePresentation; } Host::D3D12WindowRenderLoopFrameContext EditorWindowRuntimeController::BeginFrame() { UpdateFrameTiming(); return m_windowRenderLoop.BeginFrame(); } Host::D3D12WindowRenderLoopPresentResult EditorWindowRuntimeController::Present( const ::XCEngine::UI::UIDrawData& drawData) { std::filesystem::path capturePath = {}; const bool captureRequested = m_screenshotController.TryBeginCapture(capturePath); Host::D3D12WindowRenderLoopPresentResult result = m_windowRenderLoop.Present( drawData, captureRequested ? &capturePath : nullptr); if (captureRequested) { const bool captureSucceeded = result.framePresented && result.captureSucceeded; if (captureSucceeded) { m_screenshotController.CompleteCaptureSuccess(capturePath); LogRuntimeTrace("capture", "native d3d12 capture succeeded: " + capturePath.string()); } else { std::string captureError = result.captureError; if (captureError.empty()) { captureError = !result.warning.empty() ? result.warning : "Screenshot capture did not complete."; } m_screenshotController.CompleteCaptureFailure(std::move(captureError)); LogRuntimeTrace( "capture", "native d3d12 capture failed: " + m_screenshotController.GetLastCaptureError()); } } return result; } EditorWindowFrameTransferRequests EditorWindowRuntimeController::UpdateAndAppend( const EditorWindowContentFrameContext& context, ::XCEngine::UI::UIDrawData& drawData) { assert(m_contentController != nullptr); return m_contentController->UpdateAndAppend(context, drawData); } void EditorWindowRuntimeController::RenderRequestedViewports( const ::XCEngine::Rendering::RenderContext& renderContext) { if (m_contentController != nullptr) { m_contentController->RenderRequestedViewports(renderContext); } } void EditorWindowRuntimeController::RequestManualScreenshot(std::string reason) { m_screenshotController.RequestCapture(std::move(reason)); } std::string EditorWindowRuntimeController::BuildCaptureStatusText() const { if (m_screenshotController.HasPendingCapture()) { return "Shot pending..."; } if (!m_screenshotController.GetLastCaptureError().empty()) { return TruncateText(m_screenshotController.GetLastCaptureError(), 38u); } if (!m_screenshotController.GetLastCaptureSummary().empty()) { return TruncateText(m_screenshotController.GetLastCaptureSummary(), 38u); } return {}; } std::string EditorWindowRuntimeController::BuildFrameRateText() const { return m_frameRateText; } void EditorWindowRuntimeController::ResetFrameTiming() { m_lastFrameTime = {}; m_hasLastFrameTime = false; m_smoothedDeltaTimeSeconds = 0.0f; m_frameStatsDisplayAccumulatorSeconds = 0.0f; m_displayFps = 0.0f; m_displayFrameTimeMs = 0.0f; m_frameRateText.clear(); } void EditorWindowRuntimeController::UpdateFrameTiming() { const auto now = std::chrono::steady_clock::now(); if (!m_hasLastFrameTime) { m_lastFrameTime = now; m_hasLastFrameTime = true; return; } const float deltaTime = std::chrono::duration(now - m_lastFrameTime).count(); m_lastFrameTime = now; if (deltaTime <= 0.0f) { return; } if (m_smoothedDeltaTimeSeconds <= 0.0f) { m_smoothedDeltaTimeSeconds = deltaTime; } else { m_smoothedDeltaTimeSeconds += (deltaTime - m_smoothedDeltaTimeSeconds) * kFrameTimeSmoothingFactor; } m_displayFrameTimeMs = m_smoothedDeltaTimeSeconds * 1000.0f; m_displayFps = m_smoothedDeltaTimeSeconds > 0.0f ? 1.0f / m_smoothedDeltaTimeSeconds : 0.0f; m_frameStatsDisplayAccumulatorSeconds += deltaTime; if (m_frameRateText.empty() || m_frameStatsDisplayAccumulatorSeconds >= kFrameStatsDisplayRefreshIntervalSeconds) { RefreshDisplayedFrameStats(); m_frameStatsDisplayAccumulatorSeconds = 0.0f; } } void EditorWindowRuntimeController::RefreshDisplayedFrameStats() { if (m_displayFps <= 0.0f || m_displayFrameTimeMs <= 0.0f) { m_frameRateText.clear(); return; } const int roundedFps = (std::max)(0, static_cast(std::lround(m_displayFps))); const int roundedFrameTimeMs = (std::max)(0, static_cast(std::lround(m_displayFrameTimeMs))); char buffer[48] = {}; std::snprintf( buffer, sizeof(buffer), "FPS %3d | %2d ms", roundedFps, roundedFrameTimeMs); m_frameRateText = buffer; } } // namespace XCEngine::UI::Editor::App