#include "Application.h" #include "SandboxFrameBuilder.h" #include #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::UIInputEvent; using ::XCEngine::UI::UIInputEventType; using ::XCEngine::UI::UIInputModifiers; using ::XCEngine::UI::UIPoint; using ::XCEngine::UI::UIPointerButton; 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) + "..."; } std::string ExtractStateKeyTail(const std::string& stateKey) { if (stateKey.empty()) { return "-"; } const std::size_t separator = stateKey.find_last_of('/'); if (separator == std::string::npos || separator + 1u >= stateKey.size()) { return stateKey; } return stateKey.substr(separator + 1u); } std::string FormatFloat(float value) { std::ostringstream stream; stream.setf(std::ios::fixed, std::ios::floatfield); stream.precision(1); stream << value; return stream.str(); } std::string FormatPoint(const UIPoint& point) { return "(" + FormatFloat(point.x) + ", " + FormatFloat(point.y) + ")"; } std::string FormatRect(const UIRect& rect) { return "(" + FormatFloat(rect.x) + ", " + FormatFloat(rect.y) + ", " + FormatFloat(rect.width) + ", " + FormatFloat(rect.height) + ")"; } UIInputModifiers BuildInputModifiers(size_t wParam) { UIInputModifiers modifiers = {}; modifiers.shift = (wParam & MK_SHIFT) != 0; modifiers.control = (wParam & MK_CONTROL) != 0; modifiers.alt = (GetKeyState(VK_MENU) & 0x8000) != 0; modifiers.super = (GetKeyState(VK_LWIN) & 0x8000) != 0 || (GetKeyState(VK_RWIN) & 0x8000) != 0; return modifiers; } } // 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(); std::vector frameEvents = std::move(m_pendingInputEvents); m_pendingInputEvents.clear(); UIDrawData drawData = {}; if (m_useStructuredScreen && m_screenPlayer.IsLoaded()) { UIScreenFrameInput input = {}; input.viewportRect = UIRect(0.0f, 0.0f, width, height); input.events = std::move(frameEvents); 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); } void Application::QueuePointerEvent(UIInputEventType type, UIPointerButton button, WPARAM wParam, LPARAM lParam) { UIInputEvent event = {}; event.type = type; event.pointerButton = button; event.position = UIPoint( static_cast(GET_X_LPARAM(lParam)), static_cast(GET_Y_LPARAM(lParam))); event.modifiers = BuildInputModifiers(static_cast(wParam)); m_pendingInputEvents.push_back(event); } void Application::QueuePointerLeaveEvent() { UIInputEvent event = {}; event.type = UIInputEventType::PointerLeave; if (m_hwnd != nullptr) { POINT clientPoint = {}; GetCursorPos(&clientPoint); ScreenToClient(m_hwnd, &clientPoint); event.position = UIPoint(static_cast(clientPoint.x), static_cast(clientPoint.y)); } m_pendingInputEvents.push_back(event); } void Application::QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM lParam) { if (m_hwnd == nullptr) { return; } POINT screenPoint = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; ScreenToClient(m_hwnd, &screenPoint); UIInputEvent event = {}; event.type = UIInputEventType::PointerWheel; event.position = UIPoint(static_cast(screenPoint.x), static_cast(screenPoint.y)); event.wheelDelta = static_cast(wheelDelta); event.modifiers = BuildInputModifiers(static_cast(wParam)); m_pendingInputEvents.push_back(event); } 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(); 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 ? 420.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 (authoredMode) { const auto& inputDebug = m_documentHost.GetInputDebugSnapshot(); detailLines.push_back( "Hover | Focus: " + ExtractStateKeyTail(inputDebug.hoveredStateKey) + " | " + ExtractStateKeyTail(inputDebug.focusedStateKey)); detailLines.push_back( "Active | Capture: " + ExtractStateKeyTail(inputDebug.activeStateKey) + " | " + ExtractStateKeyTail(inputDebug.captureStateKey)); if (!inputDebug.lastEventType.empty()) { detailLines.push_back( "Last input: " + inputDebug.lastEventType + " at " + FormatPoint(inputDebug.pointerPosition)); detailLines.push_back( "Route: " + inputDebug.lastTargetKind + " -> " + ExtractStateKeyTail(inputDebug.lastTargetStateKey)); detailLines.push_back( "Result: " + (inputDebug.lastResult.empty() ? std::string("n/a") : inputDebug.lastResult)); } } if (m_autoScreenshot.HasPendingCapture()) { detailLines.push_back("Shot pending..."); } else if (!m_autoScreenshot.GetLastCaptureSummary().empty()) { detailLines.push_back(TruncateText(m_autoScreenshot.GetLastCaptureSummary(), 78u)); } else { detailLines.push_back("Screenshots: manual only (F12)"); } 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_MOUSEMOVE: if (application != nullptr) { if (!application->m_trackingMouseLeave) { TRACKMOUSEEVENT trackMouseEvent = {}; trackMouseEvent.cbSize = sizeof(trackMouseEvent); trackMouseEvent.dwFlags = TME_LEAVE; trackMouseEvent.hwndTrack = hwnd; if (TrackMouseEvent(&trackMouseEvent)) { application->m_trackingMouseLeave = true; } } application->QueuePointerEvent(UIInputEventType::PointerMove, UIPointerButton::None, wParam, lParam); return 0; } break; case WM_MOUSELEAVE: if (application != nullptr) { application->m_trackingMouseLeave = false; application->QueuePointerLeaveEvent(); return 0; } break; case WM_LBUTTONDOWN: if (application != nullptr) { SetFocus(hwnd); SetCapture(hwnd); application->QueuePointerEvent(UIInputEventType::PointerButtonDown, UIPointerButton::Left, wParam, lParam); return 0; } break; case WM_LBUTTONUP: if (application != nullptr) { if (GetCapture() == hwnd) { ReleaseCapture(); } application->QueuePointerEvent(UIInputEventType::PointerButtonUp, UIPointerButton::Left, wParam, lParam); return 0; } break; case WM_MOUSEWHEEL: if (application != nullptr) { application->QueuePointerWheelEvent(GET_WHEEL_DELTA_WPARAM(wParam), wParam, lParam); return 0; } break; case WM_KEYDOWN: if (application != nullptr && wParam == VK_F12) { application->m_autoScreenshot.RequestCapture("manual_f12"); 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