#include "Windowing/Runtime/EditorWindowRuntimeController.h" #include "Bootstrap/EditorResources.h" #include "Support/EmbeddedPngLoader.h" #include "Support/TextFormat.h" #include #include #include #include #include #include #include #include #include namespace XCEngine::UI::Editor::App { using App::LoadEmbeddedPngTexture; using App::TruncateText; namespace { constexpr float kFrameTimeSmoothingFactor = 0.12f; constexpr float kFrameStatsDisplayRefreshIntervalSeconds = 0.25f; void LogRuntimeTrace(std::string_view channel, std::string_view message) { AppendUIEditorRuntimeTrace(channel, message); } } EditorWindowRuntimeController::EditorWindowRuntimeController( EditorContext& editorContext, std::unique_ptr contentController, std::unique_ptr renderRuntime) : m_editorContext(editorContext) , m_renderRuntime(std::move(renderRuntime)) , m_contentController(std::move(contentController)) {} EditorWindowRuntimeController::~EditorWindowRuntimeController() = default; bool EditorWindowRuntimeController::IsReady() const { return m_ready && m_renderRuntime != nullptr && m_renderRuntime->IsReady(); } EditorWindowContentCapabilities EditorWindowRuntimeController::GetCapabilities() const { return m_contentController != nullptr ? m_contentController->GetCapabilities() : EditorWindowContentCapabilities{}; } EditorWindowWorkspaceBinding* EditorWindowRuntimeController::TryGetWorkspaceBinding() { return m_contentController != nullptr ? m_contentController->TryGetWorkspaceBinding() : nullptr; } const EditorWindowWorkspaceBinding* EditorWindowRuntimeController::TryGetWorkspaceBinding() const { return m_contentController != nullptr ? m_contentController->TryGetWorkspaceBinding() : nullptr; } EditorWindowDockHostBinding* EditorWindowRuntimeController::TryGetDockHostBinding() { return m_contentController != nullptr ? m_contentController->TryGetDockHostBinding() : nullptr; } const EditorWindowDockHostBinding* EditorWindowRuntimeController::TryGetDockHostBinding() const { return m_contentController != nullptr ? m_contentController->TryGetDockHostBinding() : nullptr; } const EditorWindowInputFeedbackBinding* EditorWindowRuntimeController::TryGetInputFeedbackBinding() const { return m_contentController != nullptr ? m_contentController->TryGetInputFeedbackBinding() : nullptr; } const EditorWindowTitleBarBinding* EditorWindowRuntimeController::TryGetTitleBarBinding() const { return m_contentController != nullptr ? m_contentController->TryGetTitleBarBinding() : nullptr; } 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(); } ::XCEngine::UI::UISize EditorWindowRuntimeController::ResolveMinimumOuterSize() const { assert(m_contentController != nullptr); return m_contentController->ResolveMinimumOuterSize(); } void EditorWindowRuntimeController::SetDpiScale(float dpiScale) { m_dpiScale = dpiScale > 0.0f ? dpiScale : 1.0f; if (m_renderRuntime != nullptr) { m_renderRuntime->SetDpiScale(m_dpiScale); } } ::XCEngine::UI::Editor::UIEditorTextMeasurer& EditorWindowRuntimeController::GetTextMeasurer() { assert(m_renderRuntime != nullptr); return m_renderRuntime->GetTextMeasurer(); } const ::XCEngine::UI::Editor::UIEditorTextMeasurer& EditorWindowRuntimeController::GetTextMeasurer() const { assert(m_renderRuntime != nullptr); return m_renderRuntime->GetTextMeasurer(); } const ::XCEngine::UI::UITextureHandle& EditorWindowRuntimeController::GetTitleBarLogoIcon() const { return m_titleBarLogoIcon; } bool EditorWindowRuntimeController::Initialize( const Rendering::Host::EditorWindowRenderRuntimeSurface& renderSurface, const std::filesystem::path& repoRoot, const std::filesystem::path& captureRoot, bool autoCaptureOnStartup) { if (m_renderRuntime == nullptr) { LogRuntimeTrace("app", "window initialize failed: render runtime is null"); return false; } const Rendering::Host::EditorWindowRenderRuntimeInitializeResult initializeResult = m_renderRuntime->Initialize(renderSurface); if (!initializeResult.success) { LogRuntimeTrace("app", initializeResult.errorMessage.empty() ? "window render runtime initialization failed" : initializeResult.errorMessage); return false; } if (!initializeResult.warning.empty()) { LogRuntimeTrace("app", initializeResult.warning); } assert(m_contentController != nullptr); m_contentController->PrepareEditorContext( m_editorContext, m_renderRuntime->GetTextMeasurer()); m_contentController->Initialize(EditorWindowContentInitializationContext{ .repoRoot = repoRoot, .editorContext = m_editorContext, .textureHost = m_renderRuntime->GetTextureHost(), .textMeasurer = m_renderRuntime->GetTextMeasurer(), .viewportRenderer = m_renderRuntime->GetViewportRenderHost(), }); m_contentController->SetViewportSurfacePresentationEnabled( initializeResult.hasViewportSurfacePresentation); std::string titleBarLogoError = {}; if (!LoadEmbeddedPngTexture( m_renderRuntime->GetTextureHost(), IDR_PNG_LOGO_ICON, m_titleBarLogoIcon, titleBarLogoError)) { LogRuntimeTrace("icons", "titlebar logo_icon.png: " + titleBarLogoError); } if (const EditorWindowWorkspaceBinding* workspaceBinding = TryGetWorkspaceBinding(); workspaceBinding != nullptr) { LogRuntimeTrace( "app", "shell runtime initialized: workspace content"); } else { LogRuntimeTrace("app", "window content initialized: non-workspace content"); } ResetFrameTiming(); m_ready = true; m_screenshotController.Initialize(captureRoot); if (autoCaptureOnStartup) { m_screenshotController.RequestCapture("startup"); m_contentController->NotifyStartupCaptureRequested(m_editorContext); } return true; } void EditorWindowRuntimeController::Shutdown() { m_ready = false; ResetFrameTiming(); LogRuntimeTrace("window-close", "EditorWindowRuntimeController::Shutdown stage=WaitForGpuIdle"); if (m_renderRuntime != nullptr) { m_renderRuntime->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=TitleBarLogo"); if (m_renderRuntime != nullptr) { m_renderRuntime->GetTextureHost().ReleaseTexture(m_titleBarLogoIcon); LogRuntimeTrace("window-close", "EditorWindowRuntimeController::Shutdown stage=RenderRuntime"); m_renderRuntime->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(std::uint32_t width, std::uint32_t height) { if (!IsReady() || width == 0u || height == 0u) { return false; } const Rendering::Host::EditorWindowRenderRuntimeResizeResult resizeResult = m_renderRuntime->ApplyResize(width, height); if (m_contentController != nullptr) { m_contentController->SetViewportSurfacePresentationEnabled( resizeResult.hasViewportSurfacePresentation); } if (!resizeResult.warning.empty()) { LogRuntimeTrace("present", resizeResult.warning); } return resizeResult.hasViewportSurfacePresentation; } void EditorWindowRuntimeController::PrepareEditorContext() { if (m_contentController != nullptr && m_renderRuntime != nullptr) { m_contentController->PrepareEditorContext( m_editorContext, m_renderRuntime->GetTextMeasurer()); } } bool EditorWindowRuntimeController::IsEditorContextValid() const { return m_contentController != nullptr && m_contentController->IsEditorContextValid(m_editorContext); } void EditorWindowRuntimeController::AppendInvalidFrame( ::XCEngine::UI::UIDrawList& drawList) const { if (m_contentController != nullptr) { m_contentController->AppendInvalidFrame(m_editorContext, drawList); } } Rendering::Host::EditorWindowRenderRuntimeFrameContext EditorWindowRuntimeController::BeginFrame() { UpdateFrameTiming(); return m_renderRuntime != nullptr ? m_renderRuntime->BeginFrame() : Rendering::Host::EditorWindowRenderRuntimeFrameContext{}; } Rendering::Host::EditorWindowRenderRuntimePresentResult EditorWindowRuntimeController::Present( const ::XCEngine::UI::UIDrawData& drawData) { if (m_renderRuntime == nullptr) { Rendering::Host::EditorWindowRenderRuntimePresentResult result = {}; result.warning = "window render runtime is null."; return result; } std::filesystem::path capturePath = {}; const bool captureRequested = m_screenshotController.TryBeginCapture(capturePath); Rendering::Host::EditorWindowRenderRuntimePresentResult result = m_renderRuntime->Present( drawData, captureRequested ? &capturePath : nullptr); if (captureRequested) { const bool captureSucceeded = result.framePresented && result.captureSucceeded; if (captureSucceeded) { m_screenshotController.CompleteCaptureSuccess(capturePath); LogRuntimeTrace("capture", "native 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 capture failed: " + m_screenshotController.GetLastCaptureError()); } } return result; } EditorWindowFrameTransferRequests EditorWindowRuntimeController::UpdateAndAppend( const ::XCEngine::UI::UIRect& bounds, const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, std::optional cursorScreenPoint, bool primary, bool globalTabDragActive, bool useDetachedTitleBarTabStrip, ::XCEngine::UI::UIDrawData& drawData) { assert(m_contentController != nullptr); return m_contentController->UpdateAndAppend( EditorWindowContentFrameContext{ .editorContext = m_editorContext, .bounds = bounds, .inputEvents = inputEvents, .cursorScreenPoint = cursorScreenPoint, .captureStatusText = BuildCaptureStatusText(), .primary = primary, .globalTabDragActive = globalTabDragActive, .useDetachedTitleBarTabStrip = useDetachedTitleBarTabStrip, }, 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