#include "Application.h" #include #include #include #include #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; constexpr const wchar_t* kWindowClassName = L"XCEditorShellHost"; constexpr const wchar_t* kWindowTitle = L"Main Scene * - Main.xx - XCEngine Editor"; constexpr UINT kDefaultDpi = 96u; constexpr float kBaseDpiScale = 96.0f; bool ResolveVerboseRuntimeTraceEnabled() { wchar_t buffer[8] = {}; const DWORD length = GetEnvironmentVariableW( L"XCUIEDITOR_VERBOSE_TRACE", buffer, static_cast(std::size(buffer))); return length > 0u && buffer[0] != L'0'; } UINT QuerySystemDpi() { HDC screenDc = GetDC(nullptr); if (screenDc == nullptr) { return kDefaultDpi; } const int dpiX = GetDeviceCaps(screenDc, LOGPIXELSX); ReleaseDC(nullptr, screenDc); return dpiX > 0 ? static_cast(dpiX) : kDefaultDpi; } UINT QueryWindowDpi(HWND hwnd) { if (hwnd != nullptr) { const HMODULE user32 = GetModuleHandleW(L"user32.dll"); if (user32 != nullptr) { using GetDpiForWindowFn = UINT(WINAPI*)(HWND); const auto getDpiForWindow = reinterpret_cast(GetProcAddress(user32, "GetDpiForWindow")); if (getDpiForWindow != nullptr) { const UINT dpi = getDpiForWindow(hwnd); if (dpi != 0u) { return dpi; } } } } return QuerySystemDpi(); } void EnableDpiAwareness() { const HMODULE user32 = GetModuleHandleW(L"user32.dll"); if (user32 != nullptr) { using SetProcessDpiAwarenessContextFn = BOOL(WINAPI*)(DPI_AWARENESS_CONTEXT); const auto setProcessDpiAwarenessContext = reinterpret_cast( GetProcAddress(user32, "SetProcessDpiAwarenessContext")); if (setProcessDpiAwarenessContext != nullptr) { if (setProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)) { return; } if (GetLastError() == ERROR_ACCESS_DENIED) { return; } if (setProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE)) { return; } if (GetLastError() == ERROR_ACCESS_DENIED) { return; } } } const HMODULE shcore = LoadLibraryW(L"shcore.dll"); if (shcore != nullptr) { using SetProcessDpiAwarenessFn = HRESULT(WINAPI*)(PROCESS_DPI_AWARENESS); const auto setProcessDpiAwareness = reinterpret_cast( GetProcAddress(shcore, "SetProcessDpiAwareness")); if (setProcessDpiAwareness != nullptr) { const HRESULT hr = setProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE); FreeLibrary(shcore); if (SUCCEEDED(hr) || hr == E_ACCESSDENIED) { return; } } else { FreeLibrary(shcore); } } if (user32 != nullptr) { using SetProcessDPIAwareFn = BOOL(WINAPI*)(); const auto setProcessDPIAware = reinterpret_cast(GetProcAddress(user32, "SetProcessDPIAware")); if (setProcessDPIAware != nullptr) { setProcessDPIAware(); } } } 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; } std::filesystem::path GetExecutableDirectory() { std::vector buffer(MAX_PATH); while (true) { const DWORD copied = ::GetModuleFileNameW( nullptr, buffer.data(), static_cast(buffer.size())); if (copied == 0u) { return std::filesystem::current_path().lexically_normal(); } if (copied < buffer.size() - 1u) { return std::filesystem::path(std::wstring(buffer.data(), copied)) .parent_path() .lexically_normal(); } buffer.resize(buffer.size() * 2u); } } std::string DescribeInputEventType(const UIInputEvent& event) { switch (event.type) { case UIInputEventType::PointerMove: return "PointerMove"; case UIInputEventType::PointerEnter: return "PointerEnter"; case UIInputEventType::PointerLeave: return "PointerLeave"; case UIInputEventType::PointerButtonDown: return "PointerDown"; case UIInputEventType::PointerButtonUp: return "PointerUp"; case UIInputEventType::PointerWheel: return "PointerWheel"; case UIInputEventType::KeyDown: return "KeyDown"; case UIInputEventType::KeyUp: return "KeyUp"; case UIInputEventType::Character: return "Character"; case UIInputEventType::FocusGained: return "FocusGained"; case UIInputEventType::FocusLost: return "FocusLost"; default: return "Unknown"; } } std::string DescribeProjectPanelEvent(const App::ProductProjectPanel::Event& event) { std::ostringstream stream = {}; switch (event.kind) { case App::ProductProjectPanel::EventKind::AssetSelected: stream << "AssetSelected"; break; case App::ProductProjectPanel::EventKind::AssetSelectionCleared: stream << "AssetSelectionCleared"; break; case App::ProductProjectPanel::EventKind::FolderNavigated: stream << "FolderNavigated"; break; case App::ProductProjectPanel::EventKind::AssetOpened: stream << "AssetOpened"; break; case App::ProductProjectPanel::EventKind::ContextMenuRequested: stream << "ContextMenuRequested"; break; case App::ProductProjectPanel::EventKind::None: default: stream << "None"; break; } stream << " source="; switch (event.source) { case App::ProductProjectPanel::EventSource::Tree: stream << "Tree"; break; case App::ProductProjectPanel::EventSource::Breadcrumb: stream << "Breadcrumb"; break; case App::ProductProjectPanel::EventSource::GridPrimary: stream << "GridPrimary"; break; case App::ProductProjectPanel::EventSource::GridDoubleClick: stream << "GridDoubleClick"; break; case App::ProductProjectPanel::EventSource::GridSecondary: stream << "GridSecondary"; break; case App::ProductProjectPanel::EventSource::Background: stream << "Background"; break; case App::ProductProjectPanel::EventSource::None: default: stream << "None"; break; } if (!event.itemId.empty()) { stream << " item=" << event.itemId; } if (!event.displayName.empty()) { stream << " label=" << event.displayName; } return stream.str(); } std::string DescribeHierarchyPanelEvent(const App::ProductHierarchyPanel::Event& event) { std::ostringstream stream = {}; switch (event.kind) { case App::ProductHierarchyPanel::EventKind::SelectionChanged: stream << "SelectionChanged"; break; case App::ProductHierarchyPanel::EventKind::Reparented: stream << "Reparented"; break; case App::ProductHierarchyPanel::EventKind::MovedToRoot: stream << "MovedToRoot"; break; case App::ProductHierarchyPanel::EventKind::RenameRequested: stream << "RenameRequested"; break; case App::ProductHierarchyPanel::EventKind::None: default: stream << "None"; break; } if (!event.itemId.empty()) { stream << " item=" << event.itemId; } if (!event.targetItemId.empty()) { stream << " target=" << event.targetItemId; } if (!event.label.empty()) { stream << " label=" << event.label; } return stream.str(); } } // 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(); } Shutdown(); return static_cast(message.wParam); } bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) { m_hInstance = hInstance; EnableDpiAwareness(); const std::filesystem::path repoRoot = ResolveRepoRootPath(); const std::filesystem::path logRoot = GetExecutableDirectory() / "logs"; InitializeUIEditorRuntimeTrace(logRoot); SetUnhandledExceptionFilter(&Application::HandleUnhandledException); LogRuntimeTrace("app", "initialize begin"); if (!m_editorContext.Initialize(repoRoot)) { LogRuntimeTrace( "app", "shell asset validation failed: " + m_editorContext.GetValidationMessage()); return false; } 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) { LogRuntimeTrace("app", "window class registration failed"); return false; } m_hwnd = CreateWindowExW( 0, kWindowClassName, kWindowTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 1540, 940, nullptr, nullptr, hInstance, this); if (m_hwnd == nullptr) { LogRuntimeTrace("app", "window creation failed"); return false; } m_hostRuntime.Reset(); m_hostRuntime.SetWindowDpi(QueryWindowDpi(m_hwnd)); m_renderer.SetDpiScale(GetDpiScale()); m_editorContext.SetExitRequestHandler([this]() { if (m_hwnd != nullptr) { PostMessageW(m_hwnd, WM_CLOSE, 0, 0); } }); std::ostringstream dpiTrace = {}; dpiTrace << "initial dpi=" << m_hostRuntime.GetWindowDpi() << " scale=" << GetDpiScale(); LogRuntimeTrace("window", dpiTrace.str()); if (!m_renderer.Initialize(m_hwnd)) { LogRuntimeTrace("app", "renderer initialization failed"); return false; } RECT clientRect = {}; GetClientRect(m_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(m_hwnd, clientWidth, clientHeight)) { LogRuntimeTrace("app", "d3d12 window renderer initialization failed"); return false; } const Host::D3D12WindowRenderLoopAttachResult attachResult = m_windowRenderLoop.Attach(m_renderer, m_windowRenderer); if (!attachResult.interopWarning.empty()) { LogRuntimeTrace( "app", attachResult.interopWarning); } m_editorContext.AttachTextMeasurer(m_renderer); m_editorWorkspace.Initialize(repoRoot, m_renderer); m_editorWorkspace.AttachViewportWindowRenderer(m_windowRenderer); m_editorWorkspace.SetViewportSurfacePresentationEnabled( attachResult.hasViewportSurfacePresentation); if (!m_editorWorkspace.GetBuiltInIconError().empty()) { LogRuntimeTrace("icons", m_editorWorkspace.GetBuiltInIconError()); } LogRuntimeTrace( "app", "workspace initialized: " + m_editorContext.DescribeWorkspaceState(m_editorWorkspace.GetShellInteractionState())); m_renderReady = true; ShowWindow(m_hwnd, nCmdShow); UpdateWindow(m_hwnd); m_autoScreenshot.Initialize(m_editorContext.GetShellAsset().captureRootPath); if (IsAutoCaptureOnStartupEnabled()) { m_autoScreenshot.RequestCapture("startup"); m_editorContext.SetStatus("Capture", "Startup capture requested."); } LogRuntimeTrace("app", "initialize completed"); return true; } void Application::Shutdown() { LogRuntimeTrace("app", "shutdown begin"); m_renderReady = false; if (GetCapture() == m_hwnd) { ReleaseCapture(); } m_autoScreenshot.Shutdown(); m_editorWorkspace.Shutdown(); m_windowRenderLoop.Detach(); m_windowRenderer.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; } LogRuntimeTrace("app", "shutdown end"); ShutdownUIEditorRuntimeTrace(); } void Application::RenderFrame() { if (!m_renderReady || m_hwnd == nullptr) { return; } RECT clientRect = {}; GetClientRect(m_hwnd, &clientRect); const unsigned int pixelWidth = static_cast((std::max)(clientRect.right - clientRect.left, 1L)); const unsigned int pixelHeight = static_cast((std::max)(clientRect.bottom - clientRect.top, 1L)); const float width = PixelsToDips(static_cast(pixelWidth)); const float height = PixelsToDips(static_cast(pixelHeight)); 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_editorContext.IsValid()) { std::vector frameEvents = std::move(m_pendingInputEvents); m_pendingInputEvents.clear(); if (!frameEvents.empty() && IsVerboseRuntimeTraceEnabled()) { LogRuntimeTrace( "input", DescribeInputEvents(frameEvents) + " | " + m_editorContext.DescribeWorkspaceState( m_editorWorkspace.GetShellInteractionState())); } const Host::D3D12WindowRenderLoopFrameContext frameContext = m_windowRenderLoop.BeginFrame(); if (!frameContext.warning.empty()) { LogRuntimeTrace("viewport", frameContext.warning); } m_editorWorkspace.Update( m_editorContext, UIRect(0.0f, 0.0f, width, height), frameEvents, BuildCaptureStatusText()); const UIEditorShellInteractionFrame& shellFrame = m_editorWorkspace.GetShellFrame(); if (IsVerboseRuntimeTraceEnabled() && (!frameEvents.empty() || shellFrame.result.workspaceResult.dockHostResult.layoutChanged || shellFrame.result.workspaceResult.dockHostResult.commandExecuted)) { std::ostringstream frameTrace = {}; frameTrace << "result consumed=" << (shellFrame.result.consumed ? "true" : "false") << " layoutChanged=" << (shellFrame.result.workspaceResult.dockHostResult.layoutChanged ? "true" : "false") << " commandExecuted=" << (shellFrame.result.workspaceResult.dockHostResult.commandExecuted ? "true" : "false") << " active=" << m_editorContext.GetWorkspaceController().GetWorkspace().activePanelId << " message=" << shellFrame.result.workspaceResult.dockHostResult.layoutResult.message; LogRuntimeTrace( "frame", frameTrace.str()); } ApplyHostCaptureRequests(shellFrame.result); for (const App::ProductEditorWorkspaceTraceEntry& entry : m_editorWorkspace.GetTraceEntries()) { LogRuntimeTrace(entry.channel, entry.message); } ApplyHostedContentCaptureRequests(); ApplyCurrentCursor(); m_editorWorkspace.Append(drawList); if (frameContext.canRenderViewports) { m_editorWorkspace.RenderRequestedViewports(frameContext.renderContext); } } 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_editorContext.GetValidationMessage().empty() ? std::string("Unknown validation error.") : m_editorContext.GetValidationMessage(), UIColor(0.72f, 0.72f, 0.72f, 1.0f), 12.0f); } const Host::D3D12WindowRenderLoopPresentResult presentResult = m_windowRenderLoop.Present(drawData); if (!presentResult.warning.empty()) { LogRuntimeTrace("present", presentResult.warning); } m_autoScreenshot.CaptureIfRequested( m_renderer, drawData, pixelWidth, pixelHeight, presentResult.framePresented); } void Application::OnPaintMessage() { if (!m_renderReady || m_hwnd == nullptr) { return; } PAINTSTRUCT paintStruct = {}; BeginPaint(m_hwnd, &paintStruct); RenderFrame(); EndPaint(m_hwnd, &paintStruct); } float Application::GetDpiScale() const { return m_hostRuntime.GetDpiScale(kBaseDpiScale); } float Application::PixelsToDips(float pixels) const { const float dpiScale = GetDpiScale(); return dpiScale > 0.0f ? pixels / dpiScale : pixels; } bool Application::IsPointerInsideClientArea() const { if (m_hwnd == nullptr || !IsWindow(m_hwnd)) { return false; } POINT screenPoint = {}; if (!GetCursorPos(&screenPoint)) { return false; } const LPARAM pointParam = MAKELPARAM( static_cast(screenPoint.x), static_cast(screenPoint.y)); return SendMessageW(m_hwnd, WM_NCHITTEST, 0, pointParam) == HTCLIENT; } LPCWSTR Application::ResolveCurrentCursorResource() const { switch (m_editorWorkspace.GetHostedContentCursorKind()) { case App::ProductProjectPanel::CursorKind::ResizeEW: return IDC_SIZEWE; case App::ProductProjectPanel::CursorKind::Arrow: default: break; } switch (m_editorWorkspace.GetDockCursorKind()) { case Widgets::UIEditorDockHostCursorKind::ResizeEW: return IDC_SIZEWE; case Widgets::UIEditorDockHostCursorKind::ResizeNS: return IDC_SIZENS; case Widgets::UIEditorDockHostCursorKind::Arrow: default: return IDC_ARROW; } } bool Application::ApplyCurrentCursor() const { if (!HasInteractiveCaptureState() && !IsPointerInsideClientArea()) { return false; } const HCURSOR cursor = LoadCursorW(nullptr, ResolveCurrentCursorResource()); if (cursor == nullptr) { return false; } SetCursor(cursor); return true; } UIPoint Application::ConvertClientPixelsToDips(LONG x, LONG y) const { return UIPoint( PixelsToDips(static_cast(x)), PixelsToDips(static_cast(y))); } std::string Application::BuildCaptureStatusText() const { if (m_autoScreenshot.HasPendingCapture()) { return "Shot pending..."; } if (!m_autoScreenshot.GetLastCaptureError().empty()) { return TruncateText(m_autoScreenshot.GetLastCaptureError(), 38u); } if (!m_autoScreenshot.GetLastCaptureSummary().empty()) { return TruncateText(m_autoScreenshot.GetLastCaptureSummary(), 38u); } return {}; } void Application::LogRuntimeTrace( std::string_view channel, std::string_view message) const { AppendUIEditorRuntimeTrace(channel, message); } bool Application::IsVerboseRuntimeTraceEnabled() { static const bool s_enabled = ResolveVerboseRuntimeTraceEnabled(); return s_enabled; } std::string Application::DescribeInputEvents( const std::vector& events) const { std::ostringstream stream = {}; stream << "events=["; for (std::size_t index = 0; index < events.size(); ++index) { if (index > 0u) { stream << " | "; } const UIInputEvent& event = events[index]; stream << DescribeInputEventType(event) << '@' << static_cast(event.position.x) << ',' << static_cast(event.position.y); } stream << ']'; return stream.str(); } void Application::OnResize(UINT width, UINT height) { ApplyWindowResize(width, height); } void Application::OnEnterSizeMove() { m_hostRuntime.BeginInteractiveResize(); } void Application::OnExitSizeMove() { m_hostRuntime.EndInteractiveResize(); UINT width = 0u; UINT height = 0u; if (QueryCurrentClientPixelSize(width, height)) { ApplyWindowResize(width, height); } } bool Application::ApplyWindowResize(UINT width, UINT height) { if (!m_renderReady || width == 0u || height == 0u) { return false; } const Host::D3D12WindowRenderLoopResizeResult resizeResult = m_windowRenderLoop.ApplyResize(width, height); m_editorWorkspace.SetViewportSurfacePresentationEnabled( resizeResult.hasViewportSurfacePresentation); if (!resizeResult.windowRendererWarning.empty()) { LogRuntimeTrace("present", resizeResult.windowRendererWarning); } if (!resizeResult.interopWarning.empty()) { LogRuntimeTrace("present", resizeResult.interopWarning); } return resizeResult.hasViewportSurfacePresentation; } bool Application::QueryCurrentClientPixelSize(UINT& outWidth, UINT& outHeight) const { outWidth = 0u; outHeight = 0u; if (m_hwnd == nullptr || !IsWindow(m_hwnd)) { return false; } RECT clientRect = {}; if (!GetClientRect(m_hwnd, &clientRect)) { return false; } const LONG width = clientRect.right - clientRect.left; const LONG height = clientRect.bottom - clientRect.top; if (width <= 0 || height <= 0) { return false; } outWidth = static_cast(width); outHeight = static_cast(height); return true; } void Application::OnDpiChanged(UINT dpi, const RECT& suggestedRect) { m_hostRuntime.SetWindowDpi(dpi == 0u ? kDefaultDpi : dpi); m_renderer.SetDpiScale(GetDpiScale()); if (m_hwnd != nullptr) { const LONG windowWidth = suggestedRect.right - suggestedRect.left; const LONG windowHeight = suggestedRect.bottom - suggestedRect.top; SetWindowPos( m_hwnd, nullptr, suggestedRect.left, suggestedRect.top, windowWidth, windowHeight, SWP_NOZORDER | SWP_NOACTIVATE); UINT clientWidth = 0u; UINT clientHeight = 0u; if (QueryCurrentClientPixelSize(clientWidth, clientHeight)) { ApplyWindowResize(clientWidth, clientHeight); } } std::ostringstream trace = {}; trace << "dpi changed to " << m_hostRuntime.GetWindowDpi() << " scale=" << GetDpiScale(); LogRuntimeTrace("window", trace.str()); } void Application::ApplyHostCaptureRequests(const UIEditorShellInteractionResult& result) { if (result.requestPointerCapture && GetCapture() != m_hwnd) { SetCapture(m_hwnd); } if (result.releasePointerCapture && GetCapture() == m_hwnd) { ReleaseCapture(); } } void Application::ApplyHostedContentCaptureRequests() { if (m_editorWorkspace.WantsHostPointerCapture() && GetCapture() != m_hwnd) { SetCapture(m_hwnd); } if (m_editorWorkspace.WantsHostPointerRelease() && GetCapture() == m_hwnd && !m_editorWorkspace.HasShellInteractiveCapture()) { ReleaseCapture(); } } bool Application::HasInteractiveCaptureState() const { return m_editorWorkspace.HasInteractiveCapture(); } void Application::QueuePointerEvent( UIInputEventType type, UIPointerButton button, WPARAM wParam, LPARAM lParam) { UIInputEvent event = {}; event.type = type; event.pointerButton = button; event.position = ConvertClientPixelsToDips( GET_X_LPARAM(lParam), 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 = ConvertClientPixelsToDips(clientPoint.x, 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 = ConvertClientPixelsToDips(screenPoint.x, 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(); } LONG WINAPI Application::HandleUnhandledException(EXCEPTION_POINTERS* exceptionInfo) { if (exceptionInfo != nullptr && exceptionInfo->ExceptionRecord != nullptr) { AppendUIEditorCrashTrace( exceptionInfo->ExceptionRecord->ExceptionCode, exceptionInfo->ExceptionRecord->ExceptionAddress); } else { AppendUIEditorCrashTrace(0u, nullptr); } return EXCEPTION_EXECUTE_HANDLER; } LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { LRESULT dispatcherResult = 0; if (Host::WindowMessageDispatcher::TryHandleNonClientCreate( hwnd, message, lParam, dispatcherResult)) { return dispatcherResult; } Application* application = Host::WindowMessageDispatcher::GetApplicationFromWindow(hwnd); if (application != nullptr && Host::WindowMessageDispatcher::TryDispatch( *application, message, wParam, lParam, dispatcherResult)) { return dispatcherResult; } switch (message) { 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