#include "Application.h" #include "Shell/ProductShellAsset.h" #include #include #include #include #include #include #include #ifndef XCUIEDITOR_REPO_ROOT #define XCUIEDITOR_REPO_ROOT "." #endif namespace XCEngine::UI::Editor { namespace { using ::XCEngine::Input::KeyCode; using ::XCEngine::UI::UIColor; using ::XCEngine::UI::UIDrawData; using ::XCEngine::UI::UIDrawList; using ::XCEngine::UI::UIInputEvent; using ::XCEngine::UI::UIInputEventType; using ::XCEngine::UI::UIPoint; using ::XCEngine::UI::UIPointerButton; using ::XCEngine::UI::UIRect; using App::BuildProductShellAsset; using App::BuildProductShellInteractionDefinition; constexpr const wchar_t* kWindowClassName = L"XCEditorShellHost"; constexpr const wchar_t* kWindowTitle = L"Main Scene * - Main.xx - XCEngine Editor"; 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(0u, maxLength); } return text.substr(0u, maxLength - 3u) + "..."; } bool IsAutoCaptureOnStartupEnabled() { const char* value = std::getenv("XCUI_AUTO_CAPTURE_ON_STARTUP"); if (value == nullptr || value[0] == '\0') { return false; } std::string normalized = value; std::transform( normalized.begin(), normalized.end(), normalized.begin(), [](unsigned char character) { return static_cast(std::tolower(character)); }); return normalized != "0" && normalized != "false" && normalized != "off" && normalized != "no"; } std::int32_t MapVirtualKeyToUIKeyCode(WPARAM wParam) { switch (wParam) { case 'A': return static_cast(KeyCode::A); case 'B': return static_cast(KeyCode::B); case 'C': return static_cast(KeyCode::C); case 'D': return static_cast(KeyCode::D); case 'E': return static_cast(KeyCode::E); case 'F': return static_cast(KeyCode::F); case 'G': return static_cast(KeyCode::G); case 'H': return static_cast(KeyCode::H); case 'I': return static_cast(KeyCode::I); case 'J': return static_cast(KeyCode::J); case 'K': return static_cast(KeyCode::K); case 'L': return static_cast(KeyCode::L); case 'M': return static_cast(KeyCode::M); case 'N': return static_cast(KeyCode::N); case 'O': return static_cast(KeyCode::O); case 'P': return static_cast(KeyCode::P); case 'Q': return static_cast(KeyCode::Q); case 'R': return static_cast(KeyCode::R); case 'S': return static_cast(KeyCode::S); case 'T': return static_cast(KeyCode::T); case 'U': return static_cast(KeyCode::U); case 'V': return static_cast(KeyCode::V); case 'W': return static_cast(KeyCode::W); case 'X': return static_cast(KeyCode::X); case 'Y': return static_cast(KeyCode::Y); case 'Z': return static_cast(KeyCode::Z); case '0': return static_cast(KeyCode::Zero); case '1': return static_cast(KeyCode::One); case '2': return static_cast(KeyCode::Two); case '3': return static_cast(KeyCode::Three); case '4': return static_cast(KeyCode::Four); case '5': return static_cast(KeyCode::Five); case '6': return static_cast(KeyCode::Six); case '7': return static_cast(KeyCode::Seven); case '8': return static_cast(KeyCode::Eight); case '9': return static_cast(KeyCode::Nine); case VK_SPACE: return static_cast(KeyCode::Space); case VK_TAB: return static_cast(KeyCode::Tab); case VK_RETURN: return static_cast(KeyCode::Enter); case VK_ESCAPE: return static_cast(KeyCode::Escape); case VK_SHIFT: return static_cast(KeyCode::LeftShift); case VK_CONTROL: return static_cast(KeyCode::LeftCtrl); case VK_MENU: return static_cast(KeyCode::LeftAlt); case VK_UP: return static_cast(KeyCode::Up); case VK_DOWN: return static_cast(KeyCode::Down); case VK_LEFT: return static_cast(KeyCode::Left); case VK_RIGHT: return static_cast(KeyCode::Right); case VK_HOME: return static_cast(KeyCode::Home); case VK_END: return static_cast(KeyCode::End); case VK_PRIOR: return static_cast(KeyCode::PageUp); case VK_NEXT: return static_cast(KeyCode::PageDown); case VK_DELETE: return static_cast(KeyCode::Delete); case VK_BACK: return static_cast(KeyCode::Backspace); case VK_F1: return static_cast(KeyCode::F1); case VK_F2: return static_cast(KeyCode::F2); case VK_F3: return static_cast(KeyCode::F3); case VK_F4: return static_cast(KeyCode::F4); case VK_F5: return static_cast(KeyCode::F5); case VK_F6: return static_cast(KeyCode::F6); case VK_F7: return static_cast(KeyCode::F7); case VK_F8: return static_cast(KeyCode::F8); case VK_F9: return static_cast(KeyCode::F9); case VK_F10: return static_cast(KeyCode::F10); case VK_F11: return static_cast(KeyCode::F11); case VK_F12: return static_cast(KeyCode::F12); default: return static_cast(KeyCode::None); } } bool IsRepeatKeyMessage(LPARAM lParam) { return (static_cast(lParam) & (1ul << 30)) != 0ul; } } // namespace 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; m_shellAsset = BuildProductShellAsset(ResolveRepoRootPath()); m_shellValidation = ValidateEditorShellAsset(m_shellAsset); m_validationMessage = m_shellValidation.message; if (!m_shellValidation.IsValid()) { return false; } m_workspaceController = UIEditorWorkspaceController( m_shellAsset.panelRegistry, m_shellAsset.workspace, m_shellAsset.workspaceSession); m_shortcutManager = BuildEditorShellShortcutManager(m_shellAsset); m_shortcutManager.SetHostCommandHandler(this); m_shellServices = {}; m_shellServices.commandDispatcher = &m_shortcutManager.GetCommandDispatcher(); m_shellServices.shortcutManager = &m_shortcutManager; m_lastStatus = "Ready"; m_lastMessage = "Old editor shell baseline loaded."; 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, 1540, 940, 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_autoScreenshot.Initialize(m_shellAsset.captureRootPath); if (IsAutoCaptureOnStartupEnabled()) { m_autoScreenshot.RequestCapture("startup"); m_lastStatus = "Capture"; m_lastMessage = "Startup capture requested."; } return true; } void Application::Shutdown() { if (GetCapture() == m_hwnd) { ReleaseCapture(); } m_autoScreenshot.Shutdown(); 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)); UIDrawData drawData = {}; UIDrawList& drawList = drawData.EmplaceDrawList("XCEditorShell"); drawList.AddFilledRect( UIRect(0.0f, 0.0f, width, height), UIColor(0.10f, 0.10f, 0.10f, 1.0f)); if (m_shellValidation.IsValid()) { const auto& metrics = ResolveUIEditorShellInteractionMetrics(); const auto& palette = ResolveUIEditorShellInteractionPalette(); const UIEditorShellInteractionDefinition definition = BuildShellDefinition(); std::vector frameEvents = std::move(m_pendingInputEvents); m_pendingInputEvents.clear(); m_shellFrame = UpdateUIEditorShellInteraction( m_shellInteractionState, m_workspaceController, UIRect(0.0f, 0.0f, width, height), definition, frameEvents, m_shellServices, metrics); ApplyHostCaptureRequests(m_shellFrame.result); UpdateLastStatus(m_shellFrame.result); AppendUIEditorShellInteraction( drawList, m_shellFrame, m_shellInteractionState, palette, metrics); } else { drawList.AddText( UIPoint(28.0f, 28.0f), "Editor shell asset invalid.", UIColor(0.92f, 0.92f, 0.92f, 1.0f), 16.0f); drawList.AddText( UIPoint(28.0f, 54.0f), m_validationMessage.empty() ? std::string("Unknown validation error.") : m_validationMessage, UIColor(0.72f, 0.72f, 0.72f, 1.0f), 12.0f); } 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::ApplyHostCaptureRequests(const UIEditorShellInteractionResult& result) { if (result.requestPointerCapture && GetCapture() != m_hwnd) { SetCapture(m_hwnd); } if (result.releasePointerCapture && GetCapture() == m_hwnd) { ReleaseCapture(); } } bool Application::HasInteractiveCaptureState() const { if (m_shellInteractionState.workspaceInteractionState.dockHostInteractionState.splitterDragState.active) { return true; } for (const auto& panelState : m_shellInteractionState.workspaceInteractionState.composeState.panelStates) { if (panelState.viewportShellState.inputBridgeState.captured) { return true; } } return false; } UIEditorShellInteractionDefinition Application::BuildShellDefinition() const { std::string statusText = m_lastStatus; if (!m_lastMessage.empty()) { statusText += statusText.empty() ? m_lastMessage : ": " + m_lastMessage; } std::string captureText = {}; if (m_autoScreenshot.HasPendingCapture()) { captureText = "Shot pending..."; } else if (!m_autoScreenshot.GetLastCaptureError().empty()) { captureText = TruncateText(m_autoScreenshot.GetLastCaptureError(), 38u); } else if (!m_autoScreenshot.GetLastCaptureSummary().empty()) { captureText = TruncateText(m_autoScreenshot.GetLastCaptureSummary(), 38u); } return BuildProductShellInteractionDefinition( m_shellAsset, m_workspaceController, statusText, captureText); } void Application::UpdateLastStatus(const UIEditorShellInteractionResult& result) { if (result.commandDispatched) { m_lastStatus = std::string(GetUIEditorCommandDispatchStatusName(result.commandDispatchResult.status)); m_lastMessage = result.commandDispatchResult.message.empty() ? result.commandDispatchResult.displayName : result.commandDispatchResult.message; return; } if (result.workspaceResult.dockHostResult.layoutChanged) { m_lastStatus = "Layout"; m_lastMessage = result.workspaceResult.dockHostResult.layoutResult.message; return; } if (result.workspaceResult.dockHostResult.commandExecuted) { m_lastStatus = "Workspace"; m_lastMessage = result.workspaceResult.dockHostResult.commandResult.message; return; } if (!result.viewportPanelId.empty()) { m_lastStatus = result.viewportPanelId; if (result.viewportInputFrame.captureStarted) { m_lastMessage = "Viewport capture started."; } else if (result.viewportInputFrame.captureEnded) { m_lastMessage = "Viewport capture ended."; } else if (result.viewportInputFrame.focusGained) { m_lastMessage = "Viewport focused."; } else if (result.viewportInputFrame.focusLost) { m_lastMessage = "Viewport focus lost."; } else if (result.viewportInputFrame.pointerPressedInside) { m_lastMessage = "Viewport pointer down."; } else if (result.viewportInputFrame.pointerReleasedInside) { m_lastMessage = "Viewport pointer up."; } else if (result.viewportInputFrame.pointerMoved) { m_lastMessage = "Viewport pointer move."; } else if (result.viewportInputFrame.wheelDelta != 0.0f) { m_lastMessage = "Viewport wheel."; } return; } if (result.menuMutation.changed) { if (!result.itemId.empty() && !result.menuMutation.openedPopupId.empty()) { m_lastStatus = "Menu"; m_lastMessage = result.itemId + " opened child popup."; } else if (!result.menuId.empty() && !result.menuMutation.openedPopupId.empty()) { m_lastStatus = "Menu"; m_lastMessage = result.menuId + " opened."; } else { m_lastStatus = "Menu"; m_lastMessage = "Popup chain dismissed."; } } } 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 = m_inputModifierTracker.BuildPointerModifiers(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 = m_inputModifierTracker.BuildPointerModifiers(static_cast(wParam)); m_pendingInputEvents.push_back(event); } void Application::QueueKeyEvent(UIInputEventType type, WPARAM wParam, LPARAM lParam) { UIInputEvent event = {}; event.type = type; event.keyCode = MapVirtualKeyToUIKeyCode(wParam); event.modifiers = m_inputModifierTracker.ApplyKeyMessage(type, wParam, lParam); event.repeat = IsRepeatKeyMessage(lParam); m_pendingInputEvents.push_back(event); } void Application::QueueCharacterEvent(WPARAM wParam, LPARAM) { UIInputEvent event = {}; event.type = UIInputEventType::Character; event.character = static_cast(wParam); event.modifiers = m_inputModifierTracker.GetCurrentModifiers(); m_pendingInputEvents.push_back(event); } void Application::QueueWindowFocusEvent(UIInputEventType type) { UIInputEvent event = {}; event.type = type; m_pendingInputEvents.push_back(event); } std::filesystem::path Application::ResolveRepoRootPath() { std::string root = XCUIEDITOR_REPO_ROOT; if (root.size() >= 2u && root.front() == '"' && root.back() == '"') { root = root.substr(1u, root.size() - 2u); } return std::filesystem::path(root).lexically_normal(); } UIEditorHostCommandEvaluationResult Application::EvaluateHostCommand( std::string_view commandId) const { UIEditorHostCommandEvaluationResult result = {}; if (commandId == "file.exit") { result.executable = true; result.message = "Exit command is ready."; return result; } if (commandId == "help.about") { result.executable = true; result.message = "About placeholder is ready."; return result; } if (commandId.rfind("file.", 0u) == 0u) { result.message = "Document command bridge is not attached yet."; return result; } if (commandId.rfind("edit.", 0u) == 0u) { result.message = "Edit route bridge is not attached yet."; return result; } if (commandId.rfind("assets.", 0u) == 0u) { result.message = "Asset pipeline bridge is not attached yet."; return result; } if (commandId.rfind("run.", 0u) == 0u) { result.message = "Runtime bridge is not attached yet."; return result; } if (commandId.rfind("scripts.", 0u) == 0u) { result.message = "Script pipeline bridge is not attached yet."; return result; } result.message = "Host command is not attached yet."; return result; } UIEditorHostCommandDispatchResult Application::DispatchHostCommand( std::string_view commandId) { UIEditorHostCommandDispatchResult result = {}; if (commandId == "file.exit") { result.commandExecuted = true; result.message = "Exit requested."; if (m_hwnd != nullptr) { PostMessageW(m_hwnd, WM_CLOSE, 0, 0); } return result; } if (commandId == "help.about") { result.commandExecuted = true; result.message = "About dialog will be wired after modal layer lands."; m_lastStatus = "About"; m_lastMessage = result.message; return result; } result.message = "Host command dispatch rejected."; return result; } 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); application->QueuePointerEvent( UIInputEventType::PointerButtonDown, UIPointerButton::Left, wParam, lParam); return 0; } break; case WM_LBUTTONUP: if (application != nullptr) { 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_SETFOCUS: if (application != nullptr) { application->m_inputModifierTracker.SyncFromSystemState(); application->QueueWindowFocusEvent(UIInputEventType::FocusGained); return 0; } break; case WM_KILLFOCUS: if (application != nullptr) { application->m_inputModifierTracker.Reset(); application->QueueWindowFocusEvent(UIInputEventType::FocusLost); return 0; } break; case WM_CAPTURECHANGED: if (application != nullptr && reinterpret_cast(lParam) != hwnd && application->HasInteractiveCaptureState()) { application->QueueWindowFocusEvent(UIInputEventType::FocusLost); return 0; } break; case WM_KEYDOWN: case WM_SYSKEYDOWN: if (application != nullptr) { if (wParam == VK_F12) { application->m_autoScreenshot.RequestCapture("manual_f12"); } application->QueueKeyEvent(UIInputEventType::KeyDown, wParam, lParam); return 0; } break; case WM_KEYUP: case WM_SYSKEYUP: if (application != nullptr) { application->QueueKeyEvent(UIInputEventType::KeyUp, wParam, lParam); return 0; } break; case WM_CHAR: if (application != nullptr) { application->QueueCharacterEvent(wParam, lParam); 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 RunXCUIEditorApp(HINSTANCE hInstance, int nCmdShow) { Application application; return application.Run(hInstance, nCmdShow); } } // namespace XCEngine::UI::Editor