#include "Application.h" #include "SandboxFrameBuilder.h" #include #include #include #include #include #include #include #ifndef XCNEWEDITOR_REPO_ROOT #define XCNEWEDITOR_REPO_ROOT "." #endif namespace XCEngine { namespace NewEditor { namespace { using ::XCEngine::UI::UIColor; using ::XCEngine::UI::UIDrawData; using ::XCEngine::UI::UIDrawList; using ::XCEngine::UI::UIPoint; using ::XCEngine::UI::UIRect; using ::XCEngine::UI::Runtime::UIScreenFrameInput; constexpr const wchar_t* kWindowClassName = L"XCNewEditorNativeSandbox"; constexpr const wchar_t* kWindowTitle = L"XCNewEditor Native Sandbox"; constexpr auto kReloadPollInterval = std::chrono::milliseconds(150); constexpr UIColor kOverlayBgColor(0.10f, 0.10f, 0.10f, 0.95f); constexpr UIColor kOverlayBorderColor(0.25f, 0.25f, 0.25f, 1.0f); constexpr UIColor kOverlayTextPrimary(0.93f, 0.93f, 0.93f, 1.0f); constexpr UIColor kOverlayTextMuted(0.70f, 0.70f, 0.70f, 1.0f); constexpr UIColor kOverlaySuccess(0.82f, 0.82f, 0.82f, 1.0f); constexpr UIColor kOverlayFallback(0.56f, 0.56f, 0.56f, 1.0f); Application* GetApplicationFromWindow(HWND hwnd) { return reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); } std::string TruncateText(const std::string& text, std::size_t maxLength) { if (text.size() <= maxLength) { return text; } if (maxLength <= 3u) { return text.substr(0, maxLength); } return text.substr(0, maxLength - 3u) + "..."; } } // namespace Application::Application() : m_screenPlayer(m_documentHost) { } int Application::Run(HINSTANCE hInstance, int nCmdShow) { if (!Initialize(hInstance, nCmdShow)) { Shutdown(); return 1; } MSG message = {}; while (message.message != WM_QUIT) { if (PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) { TranslateMessage(&message); DispatchMessageW(&message); continue; } RenderFrame(); Sleep(8); } Shutdown(); return static_cast(message.wParam); } bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) { m_hInstance = hInstance; WNDCLASSEXW windowClass = {}; windowClass.cbSize = sizeof(windowClass); windowClass.style = CS_HREDRAW | CS_VREDRAW; windowClass.lpfnWndProc = &Application::WndProc; windowClass.hInstance = hInstance; windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW); windowClass.lpszClassName = kWindowClassName; m_windowClassAtom = RegisterClassExW(&windowClass); if (m_windowClassAtom == 0) { return false; } m_hwnd = CreateWindowExW( 0, kWindowClassName, kWindowTitle, WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, 1440, 900, nullptr, nullptr, hInstance, this); if (m_hwnd == nullptr) { return false; } ShowWindow(m_hwnd, nCmdShow); UpdateWindow(m_hwnd); if (!m_renderer.Initialize(m_hwnd)) { return false; } m_startTime = std::chrono::steady_clock::now(); m_lastFrameTime = m_startTime; m_autoScreenshot.Initialize(ResolveRepoRelativePath("new_editor/captures")); LoadStructuredScreen("startup"); return true; } void Application::Shutdown() { m_autoScreenshot.Shutdown(); m_screenPlayer.Unload(); m_trackedFiles.clear(); m_screenAsset = {}; m_useStructuredScreen = false; m_runtimeStatus.clear(); m_runtimeError.clear(); m_frameIndex = 0; m_renderer.Shutdown(); if (m_hwnd != nullptr && IsWindow(m_hwnd)) { DestroyWindow(m_hwnd); } m_hwnd = nullptr; if (m_windowClassAtom != 0 && m_hInstance != nullptr) { UnregisterClassW(kWindowClassName, m_hInstance); m_windowClassAtom = 0; } } void Application::RenderFrame() { if (m_hwnd == nullptr) { return; } RECT clientRect = {}; GetClientRect(m_hwnd, &clientRect); const float width = static_cast((std::max)(clientRect.right - clientRect.left, 1L)); const float height = static_cast((std::max)(clientRect.bottom - clientRect.top, 1L)); const auto now = std::chrono::steady_clock::now(); const double timeSeconds = std::chrono::duration(now - m_startTime).count(); double deltaTimeSeconds = std::chrono::duration(now - m_lastFrameTime).count(); if (deltaTimeSeconds <= 0.0) { deltaTimeSeconds = 1.0 / 60.0; } m_lastFrameTime = now; RefreshStructuredScreen(); UIDrawData drawData = {}; if (m_useStructuredScreen && m_screenPlayer.IsLoaded()) { UIScreenFrameInput input = {}; input.viewportRect = UIRect(0.0f, 0.0f, width, height); input.deltaTimeSeconds = deltaTimeSeconds; input.frameIndex = ++m_frameIndex; input.focused = GetForegroundWindow() == m_hwnd; const auto& frame = m_screenPlayer.Update(input); for (const auto& drawList : frame.drawData.GetDrawLists()) { drawData.AddDrawList(drawList); } m_runtimeStatus = "Authored XCUI"; m_runtimeError = frame.errorMessage; } if (drawData.Empty()) { SandboxFrameOptions options = {}; options.width = width; options.height = height; options.timeSeconds = timeSeconds; drawData = BuildSandboxFrame(options); m_runtimeStatus = "Fallback Sandbox"; if (m_runtimeError.empty() && !m_screenPlayer.IsLoaded()) { m_runtimeError = m_screenPlayer.GetLastError(); } } AppendRuntimeOverlay(drawData, width, height); const bool framePresented = m_renderer.Render(drawData); m_autoScreenshot.CaptureIfRequested( m_renderer, drawData, static_cast(width), static_cast(height), framePresented); } void Application::OnResize(UINT width, UINT height) { if (width == 0 || height == 0) { return; } m_renderer.Resize(width, height); } bool Application::LoadStructuredScreen(const char* triggerReason) { m_screenAsset = {}; m_screenAsset.screenId = "new_editor.editor_shell"; m_screenAsset.documentPath = ResolveRepoRelativePath("new_editor/ui/views/editor_shell.xcui").string(); m_screenAsset.themePath = ResolveRepoRelativePath("new_editor/ui/themes/editor_shell.xctheme").string(); const bool loaded = m_screenPlayer.Load(m_screenAsset); m_useStructuredScreen = loaded; m_runtimeStatus = loaded ? "Authored XCUI" : "Fallback Sandbox"; m_runtimeError = loaded ? std::string() : m_screenPlayer.GetLastError(); RebuildTrackedFileStates(); std::string screenshotReason = triggerReason != nullptr ? triggerReason : "update"; screenshotReason += loaded ? "_authored" : "_fallback"; m_autoScreenshot.RequestCapture(std::move(screenshotReason)); return loaded; } void Application::RefreshStructuredScreen() { const auto now = std::chrono::steady_clock::now(); if (m_lastReloadPollTime.time_since_epoch().count() != 0 && now - m_lastReloadPollTime < kReloadPollInterval) { return; } m_lastReloadPollTime = now; if (DetectTrackedFileChange()) { LoadStructuredScreen("reload"); } } void Application::RebuildTrackedFileStates() { namespace fs = std::filesystem; m_trackedFiles.clear(); std::unordered_set seenPaths = {}; std::error_code errorCode = {}; auto appendTrackedPath = [&](const std::string& rawPath) { if (rawPath.empty()) { return; } const fs::path normalizedPath = fs::path(rawPath).lexically_normal(); const std::string key = normalizedPath.string(); if (!seenPaths.insert(key).second) { return; } TrackedFileState state = {}; state.path = normalizedPath; state.exists = fs::exists(normalizedPath, errorCode); errorCode.clear(); if (state.exists) { state.writeTime = fs::last_write_time(normalizedPath, errorCode); errorCode.clear(); } m_trackedFiles.push_back(std::move(state)); }; appendTrackedPath(m_screenAsset.documentPath); appendTrackedPath(m_screenAsset.themePath); if (const auto* document = m_screenPlayer.GetDocument(); document != nullptr) { for (const std::string& dependency : document->dependencies) { appendTrackedPath(dependency); } } } bool Application::DetectTrackedFileChange() const { namespace fs = std::filesystem; std::error_code errorCode = {}; for (const TrackedFileState& trackedFile : m_trackedFiles) { const bool existsNow = fs::exists(trackedFile.path, errorCode); errorCode.clear(); if (existsNow != trackedFile.exists) { return true; } if (!existsNow) { continue; } const auto writeTimeNow = fs::last_write_time(trackedFile.path, errorCode); errorCode.clear(); if (writeTimeNow != trackedFile.writeTime) { return true; } } return false; } void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float height) const { const bool authoredMode = m_useStructuredScreen && m_screenPlayer.IsLoaded(); const float panelWidth = authoredMode ? 256.0f : 360.0f; std::vector detailLines = {}; detailLines.push_back( authoredMode ? "Hot reload watches authored UI resources." : "Using native fallback while authored UI is invalid."); if (m_autoScreenshot.HasPendingCapture()) { detailLines.push_back("Shot pending..."); } else if (!m_autoScreenshot.GetLastCaptureSummary().empty()) { detailLines.push_back(TruncateText(m_autoScreenshot.GetLastCaptureSummary(), 78u)); } if (!m_runtimeError.empty()) { detailLines.push_back(TruncateText(m_runtimeError, 78u)); } else if (!m_autoScreenshot.GetLastCaptureError().empty()) { detailLines.push_back(TruncateText(m_autoScreenshot.GetLastCaptureError(), 78u)); } const float panelHeight = 38.0f + static_cast(detailLines.size()) * 18.0f; const UIRect panelRect(width - panelWidth - 16.0f, height - panelHeight - 42.0f, panelWidth, panelHeight); UIDrawList& overlay = drawData.EmplaceDrawList("NewEditor Runtime Overlay"); overlay.AddFilledRect(panelRect, kOverlayBgColor, 10.0f); overlay.AddRectOutline(panelRect, kOverlayBorderColor, 1.0f, 10.0f); overlay.AddFilledRect( UIRect(panelRect.x + 12.0f, panelRect.y + 14.0f, 8.0f, 8.0f), authoredMode ? kOverlaySuccess : kOverlayFallback, 4.0f); overlay.AddText( UIPoint(panelRect.x + 28.0f, panelRect.y + 10.0f), m_runtimeStatus.empty() ? "Runtime State" : m_runtimeStatus, kOverlayTextPrimary, 14.0f); float detailY = panelRect.y + 30.0f; for (std::size_t index = 0; index < detailLines.size(); ++index) { const bool lastLine = index + 1u == detailLines.size(); overlay.AddText( UIPoint(panelRect.x + 28.0f, detailY), detailLines[index], lastLine && (!m_runtimeError.empty() || !m_autoScreenshot.GetLastCaptureError().empty()) ? kOverlayFallback : kOverlayTextMuted, 12.0f); detailY += 18.0f; } } std::filesystem::path Application::ResolveRepoRelativePath(const char* relativePath) { return (std::filesystem::path(XCNEWEDITOR_REPO_ROOT) / relativePath).lexically_normal(); } LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { if (message == WM_NCCREATE) { const auto* createStruct = reinterpret_cast(lParam); auto* application = reinterpret_cast(createStruct->lpCreateParams); SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(application)); return TRUE; } Application* application = GetApplicationFromWindow(hwnd); switch (message) { case WM_SIZE: if (application != nullptr && wParam != SIZE_MINIMIZED) { application->OnResize(static_cast(LOWORD(lParam)), static_cast(HIWORD(lParam))); } return 0; case WM_PAINT: if (application != nullptr) { PAINTSTRUCT paintStruct = {}; BeginPaint(hwnd, &paintStruct); application->RenderFrame(); EndPaint(hwnd, &paintStruct); return 0; } break; case WM_ERASEBKGND: return 1; case WM_DESTROY: if (application != nullptr) { application->m_hwnd = nullptr; } PostQuitMessage(0); return 0; default: break; } return DefWindowProcW(hwnd, message, wParam, lParam); } int RunNewEditor(HINSTANCE hInstance, int nCmdShow) { Application application = {}; return application.Run(hInstance, nCmdShow); } } // namespace NewEditor } // namespace XCEngine