#include "Application.h" #include "EditorResources.h" #include #include #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 const char* kWindowTitleText = "Main Scene * - Main.xx - XCEngine Editor"; constexpr UINT kDefaultDpi = 96u; constexpr float kBaseDpiScale = 96.0f; constexpr float kBorderlessTitleBarHeightDips = 28.0f; constexpr float kBorderlessTitleBarFontSize = 12.0f; constexpr LONG kDetachedWindowDragOffsetXPixels = 40; constexpr LONG kDetachedWindowDragOffsetYPixels = 12; const UIColor kShellSurfaceColor(0.10f, 0.10f, 0.10f, 1.0f); const UIColor kShellBorderColor(0.15f, 0.15f, 0.15f, 1.0f); const UIColor kShellTextColor(0.92f, 0.92f, 0.92f, 1.0f); const UIColor kShellMutedTextColor(0.70f, 0.70f, 0.70f, 1.0f); constexpr DWORD kBorderlessWindowStyle = WS_POPUP | WS_THICKFRAME; 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'; } bool LoadEmbeddedPngBytes( UINT resourceId, const std::uint8_t*& outData, std::size_t& outSize, std::string& outError) { outData = nullptr; outSize = 0u; outError.clear(); HMODULE module = GetModuleHandleW(nullptr); if (module == nullptr) { outError = "GetModuleHandleW(nullptr) returned null."; return false; } HRSRC resource = FindResourceW(module, MAKEINTRESOURCEW(resourceId), L"PNG"); if (resource == nullptr) { outError = "FindResourceW failed."; return false; } HGLOBAL resourceData = LoadResource(module, resource); if (resourceData == nullptr) { outError = "LoadResource failed."; return false; } const DWORD resourceSize = SizeofResource(module, resource); if (resourceSize == 0u) { outError = "SizeofResource returned zero."; return false; } const void* lockedBytes = LockResource(resourceData); if (lockedBytes == nullptr) { outError = "LockResource failed."; return false; } outData = reinterpret_cast(lockedBytes); outSize = static_cast(resourceSize); return true; } bool LoadEmbeddedPngTexture( Host::NativeRenderer& renderer, UINT resourceId, ::XCEngine::UI::UITextureHandle& outTexture, std::string& outError) { const std::uint8_t* bytes = nullptr; std::size_t byteCount = 0u; if (!LoadEmbeddedPngBytes(resourceId, bytes, byteCount, outError)) { return false; } return renderer.LoadTextureFromMemory(bytes, byteCount, outTexture, outError); } 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(); } struct CrossWindowDockDropTarget { bool valid = false; std::string nodeId = {}; UIEditorWorkspaceDockPlacement placement = UIEditorWorkspaceDockPlacement::Center; std::size_t insertionIndex = Widgets::UIEditorTabStripInvalidIndex; }; bool IsPointInsideRect( const UIRect& rect, const UIPoint& point) { return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height; } const Widgets::UIEditorDockHostTabStackLayout* FindDockHostTabStackLayoutByNodeId( const Widgets::UIEditorDockHostLayout& layout, std::string_view nodeId) { for (const Widgets::UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) { if (tabStack.nodeId == nodeId) { return &tabStack; } } return nullptr; } std::size_t ResolveCrossWindowDropInsertionIndex( const Widgets::UIEditorDockHostTabStackLayout& tabStack, const UIPoint& point) { if (!IsPointInsideRect(tabStack.tabStripLayout.headerRect, point)) { return Widgets::UIEditorTabStripInvalidIndex; } std::size_t insertionIndex = 0u; for (const UIRect& rect : tabStack.tabStripLayout.tabHeaderRects) { const float midpoint = rect.x + rect.width * 0.5f; if (point.x > midpoint) { ++insertionIndex; } } return insertionIndex; } UIEditorWorkspaceDockPlacement ResolveCrossWindowDockPlacement( const Widgets::UIEditorDockHostTabStackLayout& tabStack, const UIPoint& point) { if (IsPointInsideRect(tabStack.tabStripLayout.headerRect, point)) { return UIEditorWorkspaceDockPlacement::Center; } const float leftDistance = point.x - tabStack.bounds.x; const float rightDistance = tabStack.bounds.x + tabStack.bounds.width - point.x; const float topDistance = point.y - tabStack.bounds.y; const float bottomDistance = tabStack.bounds.y + tabStack.bounds.height - point.y; const float minHorizontalThreshold = tabStack.bounds.width * 0.25f; const float minVerticalThreshold = tabStack.bounds.height * 0.25f; const float nearestEdge = (std::min)((std::min)(leftDistance, rightDistance), (std::min)(topDistance, bottomDistance)); if (nearestEdge == leftDistance && leftDistance <= minHorizontalThreshold) { return UIEditorWorkspaceDockPlacement::Left; } if (nearestEdge == rightDistance && rightDistance <= minHorizontalThreshold) { return UIEditorWorkspaceDockPlacement::Right; } if (nearestEdge == topDistance && topDistance <= minVerticalThreshold) { return UIEditorWorkspaceDockPlacement::Top; } if (nearestEdge == bottomDistance && bottomDistance <= minVerticalThreshold) { return UIEditorWorkspaceDockPlacement::Bottom; } return UIEditorWorkspaceDockPlacement::Center; } bool TryResolveCrossWindowDockDropTarget( const Widgets::UIEditorDockHostLayout& layout, const UIPoint& point, CrossWindowDockDropTarget& outTarget) { outTarget = {}; if (!IsPointInsideRect(layout.bounds, point)) { return false; } const Widgets::UIEditorDockHostHitTarget hitTarget = Widgets::HitTestUIEditorDockHost(layout, point); for (const Widgets::UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) { if ((!hitTarget.nodeId.empty() && tabStack.nodeId != hitTarget.nodeId) || !IsPointInsideRect(tabStack.bounds, point)) { continue; } outTarget.valid = true; outTarget.nodeId = tabStack.nodeId; outTarget.placement = ResolveCrossWindowDockPlacement(tabStack, point); if (outTarget.placement == UIEditorWorkspaceDockPlacement::Center) { outTarget.insertionIndex = ResolveCrossWindowDropInsertionIndex(tabStack, point); if (outTarget.insertionIndex == Widgets::UIEditorTabStripInvalidIndex) { outTarget.insertionIndex = tabStack.items.size(); } } return true; } for (const Widgets::UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) { if (!IsPointInsideRect(tabStack.bounds, point)) { continue; } outTarget.valid = true; outTarget.nodeId = tabStack.nodeId; outTarget.placement = ResolveCrossWindowDockPlacement(tabStack, point); if (outTarget.placement == UIEditorWorkspaceDockPlacement::Center) { outTarget.insertionIndex = tabStack.items.size(); } return true; } return false; } } // namespace Application::Application() = default; Application::~Application() = default; Application::ManagedWindowState* Application::FindWindowState(HWND hwnd) { if (hwnd == nullptr) { return nullptr; } for (const std::unique_ptr& windowState : m_windows) { if (windowState != nullptr && windowState->hwnd == hwnd) { return windowState.get(); } } return nullptr; } const Application::ManagedWindowState* Application::FindWindowState(HWND hwnd) const { if (hwnd == nullptr) { return nullptr; } for (const std::unique_ptr& windowState : m_windows) { if (windowState != nullptr && windowState->hwnd == hwnd) { return windowState.get(); } } return nullptr; } Application::ManagedWindowState* Application::FindWindowState(std::string_view windowId) { if (windowId.empty()) { return nullptr; } for (const std::unique_ptr& windowState : m_windows) { if (windowState != nullptr && windowState->windowId == windowId) { return windowState.get(); } } return nullptr; } const Application::ManagedWindowState* Application::FindWindowState(std::string_view windowId) const { if (windowId.empty()) { return nullptr; } for (const std::unique_ptr& windowState : m_windows) { if (windowState != nullptr && windowState->windowId == windowId) { return windowState.get(); } } return nullptr; } Application::ManagedWindowState* Application::FindPrimaryWindowState() { for (const std::unique_ptr& windowState : m_windows) { if (windowState != nullptr && windowState->primary) { return windowState.get(); } } return nullptr; } const Application::ManagedWindowState* Application::FindPrimaryWindowState() const { for (const std::unique_ptr& windowState : m_windows) { if (windowState != nullptr && windowState->primary) { return windowState.get(); } } return nullptr; } Application::ManagedWindowState& Application::RequireCurrentWindowState() { assert(m_currentWindowState != nullptr); return *m_currentWindowState; } const Application::ManagedWindowState& Application::RequireCurrentWindowState() const { assert(m_currentWindowState != nullptr); return *m_currentWindowState; } #define m_hwnd RequireCurrentWindowState().hwnd #define m_renderer RequireCurrentWindowState().renderer #define m_windowRenderer RequireCurrentWindowState().windowRenderer #define m_windowRenderLoop RequireCurrentWindowState().windowRenderLoop #define m_autoScreenshot RequireCurrentWindowState().autoScreenshot #define m_inputModifierTracker RequireCurrentWindowState().inputModifierTracker #define m_workspaceController RequireCurrentWindowState().workspaceController #define m_editorWorkspace RequireCurrentWindowState().editorWorkspace #define m_pendingInputEvents RequireCurrentWindowState().pendingInputEvents #define m_trackingMouseLeave RequireCurrentWindowState().trackingMouseLeave #define m_renderReady RequireCurrentWindowState().renderReady #define m_titleBarLogoIcon RequireCurrentWindowState().titleBarLogoIcon #define m_borderlessWindowChromeState RequireCurrentWindowState().borderlessWindowChromeState #define m_hostRuntime RequireCurrentWindowState().hostRuntime int Application::Run(HINSTANCE hInstance, int nCmdShow) { if (!Initialize(hInstance, nCmdShow)) { Shutdown(); return 1; } MSG message = {}; while (true) { while (PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) { if (message.message == WM_QUIT) { Shutdown(); return static_cast(message.wParam); } TranslateMessage(&message); DispatchMessageW(&message); } DestroyClosedWindows(); ProcessPendingGlobalTabDragStarts(); ProcessPendingDetachRequests(); DestroyClosedWindows(); if (m_windows.empty()) { break; } RenderAllWindows(); } Shutdown(); return 0; } bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) { m_hInstance = hInstance; m_repoRoot = ResolveRepoRootPath(); m_shutdownRequested = false; EnableDpiAwareness(); const std::filesystem::path logRoot = GetExecutableDirectory() / "logs"; InitializeUIEditorRuntimeTrace(logRoot); SetUnhandledExceptionFilter(&Application::HandleUnhandledException); LogRuntimeTrace("app", "initialize begin"); if (!m_editorContext.Initialize(m_repoRoot)) { LogRuntimeTrace( "app", "shell asset validation failed: " + m_editorContext.GetValidationMessage()); return false; } if (!RegisterWindowClass()) { return false; } m_editorContext.SetExitRequestHandler([this]() { if (ManagedWindowState* primaryWindowState = FindPrimaryWindowState(); primaryWindowState != nullptr && primaryWindowState->hwnd != nullptr) { PostMessageW(primaryWindowState->hwnd, WM_CLOSE, 0, 0); } }); ManagedWindowCreateParams primaryParams = {}; primaryParams.windowId = "main-window"; primaryParams.title = std::wstring(kWindowTitle); primaryParams.showCommand = nCmdShow; primaryParams.primary = true; primaryParams.autoCaptureOnStartup = true; if (CreateManagedWindow(m_editorContext.BuildWorkspaceController(), primaryParams) == nullptr) { LogRuntimeTrace("app", "primary window creation failed"); return false; } LogRuntimeTrace("app", "initialize completed"); return true; } bool Application::RegisterWindowClass() { if (m_windowClassAtom != 0) { return true; } WNDCLASSEXW windowClass = {}; windowClass.cbSize = sizeof(windowClass); windowClass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS; windowClass.lpfnWndProc = &Application::WndProc; windowClass.hInstance = m_hInstance; windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW); windowClass.hIcon = static_cast( LoadImageW( m_hInstance, MAKEINTRESOURCEW(IDI_APP_ICON), IMAGE_ICON, 0, 0, LR_DEFAULTSIZE)); windowClass.hIconSm = static_cast( LoadImageW( m_hInstance, MAKEINTRESOURCEW(IDI_APP_ICON_SMALL), IMAGE_ICON, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), LR_DEFAULTCOLOR)); windowClass.lpszClassName = kWindowClassName; m_windowClassAtom = RegisterClassExW(&windowClass); if (m_windowClassAtom == 0) { LogRuntimeTrace("app", "window class registration failed"); return false; } return true; } Application::ManagedWindowState* Application::CreateManagedWindow( UIEditorWorkspaceController workspaceController, const ManagedWindowCreateParams& params) { auto windowState = std::make_unique(); windowState->windowId = params.windowId; windowState->title = params.title.empty() ? std::wstring(L"XCEngine Editor") : params.title; windowState->primary = params.primary; windowState->workspaceController = std::move(workspaceController); ManagedWindowState* const rawWindowState = windowState.get(); m_windows.push_back(std::move(windowState)); const auto eraseRawWindowState = [this, rawWindowState]() { const auto it = std::find_if( m_windows.begin(), m_windows.end(), [rawWindowState](const std::unique_ptr& candidate) { return candidate.get() == rawWindowState; }); if (it != m_windows.end()) { m_windows.erase(it); } }; ManagedWindowState* const previousWindowState = m_currentWindowState; m_currentWindowState = rawWindowState; m_pendingCreateWindowState = rawWindowState; rawWindowState->hwnd = CreateWindowExW( WS_EX_APPWINDOW, kWindowClassName, rawWindowState->title.c_str(), kBorderlessWindowStyle, params.initialX, params.initialY, params.initialWidth, params.initialHeight, nullptr, nullptr, m_hInstance, this); m_pendingCreateWindowState = nullptr; if (rawWindowState->hwnd == nullptr) { m_currentWindowState = previousWindowState; eraseRawWindowState(); return nullptr; } auto failWindowInitialization = [&](std::string_view message) { LogRuntimeTrace("app", std::string(message)); DestroyManagedWindow(*rawWindowState); m_currentWindowState = previousWindowState; eraseRawWindowState(); return static_cast(nullptr); }; const HICON bigIcon = static_cast( LoadImageW( m_hInstance, MAKEINTRESOURCEW(IDI_APP_ICON), IMAGE_ICON, 0, 0, LR_DEFAULTSIZE)); const HICON smallIcon = static_cast( LoadImageW( m_hInstance, MAKEINTRESOURCEW(IDI_APP_ICON_SMALL), IMAGE_ICON, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), LR_DEFAULTCOLOR)); if (bigIcon != nullptr) { SendMessageW(m_hwnd, WM_SETICON, ICON_BIG, reinterpret_cast(bigIcon)); } if (smallIcon != nullptr) { SendMessageW(m_hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast(smallIcon)); } Host::RefreshBorderlessWindowDwmDecorations(m_hwnd); m_hostRuntime.Reset(); m_hostRuntime.SetWindowDpi(QueryWindowDpi(m_hwnd)); m_renderer.SetDpiScale(GetDpiScale()); std::ostringstream dpiTrace = {}; dpiTrace << "initial dpi=" << m_hostRuntime.GetWindowDpi() << " scale=" << GetDpiScale(); LogRuntimeTrace("window", dpiTrace.str()); if (!m_renderer.Initialize(m_hwnd)) { return failWindowInitialization("renderer initialization failed"); } 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)) { return failWindowInitialization("d3d12 window renderer initialization failed"); } 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(m_repoRoot, m_renderer); m_editorWorkspace.AttachViewportWindowRenderer(m_windowRenderer); m_editorWorkspace.SetViewportSurfacePresentationEnabled( attachResult.hasViewportSurfacePresentation); std::string titleBarLogoError = {}; if (!LoadEmbeddedPngTexture(m_renderer, IDR_PNG_LOGO_ICON, m_titleBarLogoIcon, titleBarLogoError)) { LogRuntimeTrace("icons", "titlebar logo_icon.png: " + titleBarLogoError); } if (!m_editorWorkspace.GetBuiltInIconError().empty()) { LogRuntimeTrace("icons", m_editorWorkspace.GetBuiltInIconError()); } LogRuntimeTrace( "app", "workspace initialized: " + m_editorContext.DescribeWorkspaceState( m_workspaceController, m_editorWorkspace.GetShellInteractionState())); m_renderReady = true; ShowWindow(m_hwnd, params.showCommand); UpdateWindow(m_hwnd); m_autoScreenshot.Initialize(m_editorContext.GetShellAsset().captureRootPath); if (params.autoCaptureOnStartup && IsAutoCaptureOnStartupEnabled()) { m_autoScreenshot.RequestCapture("startup"); m_editorContext.SetStatus("Capture", "Startup capture requested."); } m_currentWindowState = previousWindowState; return rawWindowState; } void Application::DestroyManagedWindow(ManagedWindowState& windowState) { if (GetCapture() == windowState.hwnd) { ReleaseCapture(); } windowState.renderReady = false; windowState.autoScreenshot.Shutdown(); windowState.editorWorkspace.Shutdown(); windowState.renderer.ReleaseTexture(windowState.titleBarLogoIcon); windowState.windowRenderLoop.Detach(); windowState.windowRenderer.Shutdown(); windowState.renderer.Shutdown(); if (windowState.hwnd != nullptr && IsWindow(windowState.hwnd)) { DestroyWindow(windowState.hwnd); } windowState.hwnd = nullptr; } void Application::DestroyClosedWindows() { for (auto it = m_windows.begin(); it != m_windows.end();) { ManagedWindowState* const windowState = it->get(); if (windowState == nullptr || windowState->hwnd != nullptr) { ++it; continue; } if (m_currentWindowState == windowState) { m_currentWindowState = nullptr; } if (m_pendingCreateWindowState == windowState) { m_pendingCreateWindowState = nullptr; } DestroyManagedWindow(*windowState); it = m_windows.erase(it); } } void Application::RenderAllWindows() { for (const std::unique_ptr& windowState : m_windows) { if (windowState == nullptr || windowState->hwnd == nullptr) { continue; } ManagedWindowState* const previousWindowState = m_currentWindowState; m_currentWindowState = windowState.get(); RenderFrame(); m_currentWindowState = previousWindowState; } } UIEditorWindowWorkspaceSet Application::BuildWindowWorkspaceSet(std::string_view activeWindowId) const { UIEditorWindowWorkspaceSet windowSet = {}; if (const ManagedWindowState* primaryWindowState = FindPrimaryWindowState(); primaryWindowState != nullptr) { windowSet.primaryWindowId = primaryWindowState->windowId; } for (const std::unique_ptr& windowState : m_windows) { if (windowState == nullptr || windowState->hwnd == nullptr) { continue; } UIEditorWindowWorkspaceState entry = {}; entry.windowId = windowState->windowId; entry.workspace = windowState->workspaceController.GetWorkspace(); entry.session = windowState->workspaceController.GetSession(); windowSet.windows.push_back(std::move(entry)); } if (!activeWindowId.empty() && FindWindowState(activeWindowId) != nullptr) { windowSet.activeWindowId = std::string(activeWindowId); } else if (m_currentWindowState != nullptr && m_currentWindowState->hwnd != nullptr) { windowSet.activeWindowId = m_currentWindowState->windowId; } else { windowSet.activeWindowId = windowSet.primaryWindowId; } return windowSet; } UIEditorWorkspaceController Application::BuildWorkspaceControllerForWindow( const UIEditorWindowWorkspaceState& windowState) const { return UIEditorWorkspaceController( m_editorContext.GetShellAsset().panelRegistry, windowState.workspace, windowState.session); } std::wstring Application::BuildManagedWindowTitle( const UIEditorWorkspaceController& workspaceController) const { const std::string& activePanelId = workspaceController.GetWorkspace().activePanelId; if (const UIEditorPanelDescriptor* descriptor = FindUIEditorPanelDescriptor( workspaceController.GetPanelRegistry(), activePanelId); descriptor != nullptr && !descriptor->defaultTitle.empty()) { const std::string titleText = descriptor->defaultTitle + " - XCEngine Editor"; return std::wstring(titleText.begin(), titleText.end()); } return std::wstring(L"XCEngine Editor"); } RECT Application::BuildDetachedWindowRect(const POINT& screenPoint) const { RECT rect = { screenPoint.x - kDetachedWindowDragOffsetXPixels, screenPoint.y - kDetachedWindowDragOffsetYPixels, screenPoint.x - kDetachedWindowDragOffsetXPixels + 960, screenPoint.y - kDetachedWindowDragOffsetYPixels + 720 }; const HMONITOR monitor = MonitorFromPoint(screenPoint, MONITOR_DEFAULTTONEAREST); MONITORINFO monitorInfo = {}; monitorInfo.cbSize = sizeof(monitorInfo); if (monitor != nullptr && GetMonitorInfoW(monitor, &monitorInfo)) { const RECT& workArea = monitorInfo.rcWork; const LONG width = rect.right - rect.left; const LONG height = rect.bottom - rect.top; rect.left = (std::max)(workArea.left, (std::min)(rect.left, workArea.right - width)); rect.top = (std::max)(workArea.top, (std::min)(rect.top, workArea.bottom - height)); rect.right = rect.left + width; rect.bottom = rect.top + height; } return rect; } void Application::ResetManagedWindowInteractionState(ManagedWindowState& windowState) { if (GetCapture() == windowState.hwnd) { ReleaseCapture(); } windowState.pendingInputEvents.clear(); windowState.trackingMouseLeave = false; windowState.inputModifierTracker.Reset(); windowState.editorWorkspace.ResetInteractionState(); windowState.borderlessWindowChromeState = {}; windowState.hostRuntime.EndBorderlessResize(); windowState.hostRuntime.EndBorderlessWindowDragRestore(); windowState.hostRuntime.EndInteractiveResize(); windowState.hostRuntime.SetHoveredBorderlessResizeEdge( Host::BorderlessWindowResizeEdge::None); windowState.hostRuntime.ClearPredictedClientPixelSize(); windowState.detachRequested = false; windowState.detachedNodeId.clear(); windowState.detachedPanelId.clear(); windowState.detachScreenPoint = {}; windowState.pendingGlobalTabDragStart = false; windowState.pendingGlobalTabDragNodeId.clear(); windowState.pendingGlobalTabDragPanelId.clear(); windowState.pendingGlobalTabDragScreenPoint = {}; windowState.pendingGlobalTabDragWindowOffset = {}; } bool Application::SynchronizeManagedWindowsFromWindowSet( const UIEditorWindowWorkspaceSet& windowSet, std::string_view preferredNewWindowId, const POINT& preferredScreenPoint) { std::vector windowIdsInSet = {}; windowIdsInSet.reserve(windowSet.windows.size()); for (const UIEditorWindowWorkspaceState& entry : windowSet.windows) { windowIdsInSet.push_back(entry.windowId); if (ManagedWindowState* existingWindowState = FindWindowState(entry.windowId); existingWindowState != nullptr) { existingWindowState->workspaceController = BuildWorkspaceControllerForWindow(entry); ResetManagedWindowInteractionState(*existingWindowState); if (!existingWindowState->primary) { existingWindowState->title = BuildManagedWindowTitle(existingWindowState->workspaceController); if (existingWindowState->hwnd != nullptr) { SetWindowTextW(existingWindowState->hwnd, existingWindowState->title.c_str()); } } continue; } ManagedWindowCreateParams createParams = {}; createParams.windowId = entry.windowId; createParams.primary = entry.windowId == windowSet.primaryWindowId; createParams.title = createParams.primary ? std::wstring(kWindowTitle) : BuildManagedWindowTitle(BuildWorkspaceControllerForWindow(entry)); if (entry.windowId == preferredNewWindowId) { const RECT detachedRect = BuildDetachedWindowRect(preferredScreenPoint); createParams.initialX = detachedRect.left; createParams.initialY = detachedRect.top; createParams.initialWidth = detachedRect.right - detachedRect.left; createParams.initialHeight = detachedRect.bottom - detachedRect.top; } if (CreateManagedWindow(BuildWorkspaceControllerForWindow(entry), createParams) == nullptr) { return false; } } for (const std::unique_ptr& windowState : m_windows) { if (windowState == nullptr || windowState->hwnd == nullptr || windowState->primary) { continue; } const bool existsInWindowSet = std::find(windowIdsInSet.begin(), windowIdsInSet.end(), windowState->windowId) != windowIdsInSet.end(); if (!existsInWindowSet) { PostMessageW(windowState->hwnd, WM_CLOSE, 0, 0); } } return true; } void Application::BeginGlobalTabDragSession( std::string_view panelWindowId, std::string_view sourceNodeId, std::string_view panelId, const POINT& screenPoint, const POINT& windowDragOffset) { m_globalTabDragSession.active = true; m_globalTabDragSession.panelWindowId = std::string(panelWindowId); m_globalTabDragSession.sourceNodeId = std::string(sourceNodeId); m_globalTabDragSession.panelId = std::string(panelId); m_globalTabDragSession.screenPoint = screenPoint; m_globalTabDragSession.windowDragOffset = windowDragOffset; } void Application::EndGlobalTabDragSession() { if (!m_globalTabDragSession.active) { return; } if (ManagedWindowState* ownerWindowState = FindWindowState(m_globalTabDragSession.panelWindowId); ownerWindowState != nullptr && ownerWindowState->hwnd != nullptr && GetCapture() == ownerWindowState->hwnd) { ReleaseCapture(); } m_globalTabDragSession = {}; } Application::ManagedWindowState* Application::FindTopmostWindowStateAtScreenPoint( const POINT& screenPoint, std::string_view excludedWindowId) { if (const HWND hitWindow = WindowFromPoint(screenPoint); hitWindow != nullptr) { const HWND rootWindow = GetAncestor(hitWindow, GA_ROOT); if (ManagedWindowState* windowState = FindWindowState(rootWindow); windowState != nullptr && windowState->windowId != excludedWindowId) { return windowState; } } for (auto it = m_windows.rbegin(); it != m_windows.rend(); ++it) { ManagedWindowState* const windowState = it->get(); if (windowState == nullptr || windowState->hwnd == nullptr || windowState->windowId == excludedWindowId) { continue; } RECT windowRect = {}; if (GetWindowRect(windowState->hwnd, &windowRect) && screenPoint.x >= windowRect.left && screenPoint.x < windowRect.right && screenPoint.y >= windowRect.top && screenPoint.y < windowRect.bottom) { return windowState; } } return nullptr; } const Application::ManagedWindowState* Application::FindTopmostWindowStateAtScreenPoint( const POINT& screenPoint, std::string_view excludedWindowId) const { return const_cast(this)->FindTopmostWindowStateAtScreenPoint( screenPoint, excludedWindowId); } POINT Application::ConvertClientDipsToScreenPixels( const ManagedWindowState& windowState, const UIPoint& point) const { const float dpiScale = windowState.hostRuntime.GetDpiScale(kBaseDpiScale); POINT clientPoint = { static_cast(point.x * dpiScale), static_cast(point.y * dpiScale) }; if (windowState.hwnd != nullptr) { ClientToScreen(windowState.hwnd, &clientPoint); } return clientPoint; } bool Application::TryResolveDraggedTabScreenRect( const ManagedWindowState& windowState, std::string_view nodeId, std::string_view panelId, RECT& outRect) const { outRect = {}; const Widgets::UIEditorDockHostLayout& layout = windowState.editorWorkspace .GetShellFrame() .workspaceInteractionFrame .dockHostFrame .layout; const Widgets::UIEditorDockHostTabStackLayout* tabStack = FindDockHostTabStackLayoutByNodeId(layout, nodeId); if (tabStack == nullptr) { return false; } std::size_t tabIndex = Widgets::UIEditorTabStripInvalidIndex; for (std::size_t index = 0; index < tabStack->items.size(); ++index) { if (tabStack->items[index].panelId == panelId) { tabIndex = index; break; } } if (tabIndex == Widgets::UIEditorTabStripInvalidIndex || tabIndex >= tabStack->tabStripLayout.tabHeaderRects.size()) { return false; } const UIRect& tabRect = tabStack->tabStripLayout.tabHeaderRects[tabIndex]; const POINT topLeft = ConvertClientDipsToScreenPixels( windowState, UIPoint(tabRect.x, tabRect.y)); const POINT bottomRight = ConvertClientDipsToScreenPixels( windowState, UIPoint(tabRect.x + tabRect.width, tabRect.y + tabRect.height)); outRect.left = topLeft.x; outRect.top = topLeft.y; outRect.right = bottomRight.x; outRect.bottom = bottomRight.y; return outRect.right > outRect.left && outRect.bottom > outRect.top; } POINT Application::ResolveGlobalTabDragWindowOffset( const ManagedWindowState& windowState, std::string_view nodeId, std::string_view panelId, const POINT& screenPoint) const { RECT tabScreenRect = {}; if (TryResolveDraggedTabScreenRect(windowState, nodeId, panelId, tabScreenRect)) { const LONG offsetX = (std::clamp)(screenPoint.x - tabScreenRect.left, 0L, tabScreenRect.right - tabScreenRect.left); const LONG offsetY = (std::clamp)(screenPoint.y - tabScreenRect.top, 0L, tabScreenRect.bottom - tabScreenRect.top); return POINT { offsetX, offsetY }; } const float dpiScale = windowState.hostRuntime.GetDpiScale(kBaseDpiScale); return POINT { static_cast(static_cast(kDetachedWindowDragOffsetXPixels) * dpiScale), static_cast(static_cast(kDetachedWindowDragOffsetYPixels) * dpiScale) }; } void Application::MoveGlobalTabDragWindow( ManagedWindowState& windowState, const POINT& screenPoint) const { if (windowState.hwnd == nullptr) { return; } RECT currentRect = {}; if (!GetWindowRect(windowState.hwnd, ¤tRect)) { return; } const LONG width = currentRect.right - currentRect.left; const LONG height = currentRect.bottom - currentRect.top; const LONG left = screenPoint.x - m_globalTabDragSession.windowDragOffset.x; const LONG top = screenPoint.y - m_globalTabDragSession.windowDragOffset.y; SetWindowPos( windowState.hwnd, nullptr, left, top, width, height, SWP_NOZORDER | SWP_NOACTIVATE); } bool Application::TryStartGlobalTabDrag(ManagedWindowState& sourceWindowState) { if (!sourceWindowState.pendingGlobalTabDragStart || sourceWindowState.pendingGlobalTabDragNodeId.empty() || sourceWindowState.pendingGlobalTabDragPanelId.empty()) { return false; } const std::string sourceNodeId = sourceWindowState.pendingGlobalTabDragNodeId; const std::string panelId = sourceWindowState.pendingGlobalTabDragPanelId; const POINT screenPoint = sourceWindowState.pendingGlobalTabDragScreenPoint; const POINT windowDragOffset = sourceWindowState.pendingGlobalTabDragWindowOffset; sourceWindowState.pendingGlobalTabDragStart = false; sourceWindowState.pendingGlobalTabDragNodeId.clear(); sourceWindowState.pendingGlobalTabDragPanelId.clear(); sourceWindowState.pendingGlobalTabDragScreenPoint = {}; sourceWindowState.pendingGlobalTabDragWindowOffset = {}; if (sourceWindowState.primary) { UIEditorWindowWorkspaceController windowWorkspaceController( m_editorContext.GetShellAsset().panelRegistry, BuildWindowWorkspaceSet(sourceWindowState.windowId)); const UIEditorWindowWorkspaceOperationResult result = windowWorkspaceController.DetachPanelToNewWindow( sourceWindowState.windowId, sourceNodeId, panelId); if (result.status != UIEditorWindowWorkspaceOperationStatus::Changed) { LogRuntimeTrace( "drag", "failed to start global tab drag from primary window: " + result.message); return false; } if (!SynchronizeManagedWindowsFromWindowSet( windowWorkspaceController.GetWindowSet(), result.targetWindowId, screenPoint)) { LogRuntimeTrace("drag", "failed to synchronize detached drag window state"); return false; } ManagedWindowState* detachedWindowState = FindWindowState(result.targetWindowId); if (detachedWindowState == nullptr || detachedWindowState->hwnd == nullptr) { LogRuntimeTrace("drag", "detached drag window was not created."); return false; } BeginGlobalTabDragSession( detachedWindowState->windowId, detachedWindowState->workspaceController.GetWorkspace().root.nodeId, panelId, screenPoint, windowDragOffset); MoveGlobalTabDragWindow(*detachedWindowState, screenPoint); SetCapture(detachedWindowState->hwnd); SetForegroundWindow(detachedWindowState->hwnd); LogRuntimeTrace( "drag", "started global tab drag by detaching panel '" + panelId + "' into window '" + detachedWindowState->windowId + "'"); return true; } ResetManagedWindowInteractionState(sourceWindowState); BeginGlobalTabDragSession( sourceWindowState.windowId, sourceNodeId, panelId, screenPoint, windowDragOffset); MoveGlobalTabDragWindow(sourceWindowState, screenPoint); if (sourceWindowState.hwnd != nullptr) { SetCapture(sourceWindowState.hwnd); } LogRuntimeTrace( "drag", "started global tab drag from detached window '" + sourceWindowState.windowId + "' panel '" + panelId + "'"); return true; } void Application::ProcessPendingGlobalTabDragStarts() { if (m_globalTabDragSession.active) { return; } for (const std::unique_ptr& windowState : m_windows) { if (windowState == nullptr || windowState->hwnd == nullptr) { continue; } if (TryStartGlobalTabDrag(*windowState)) { return; } } } bool Application::TryProcessDetachRequest(ManagedWindowState& sourceWindowState) { if (!sourceWindowState.detachRequested || sourceWindowState.detachedNodeId.empty() || sourceWindowState.detachedPanelId.empty()) { return false; } const std::string sourceWindowId = sourceWindowState.windowId; const std::string sourceNodeId = sourceWindowState.detachedNodeId; const std::string panelId = sourceWindowState.detachedPanelId; const POINT screenPoint = sourceWindowState.detachScreenPoint; sourceWindowState.detachRequested = false; sourceWindowState.detachedNodeId.clear(); sourceWindowState.detachedPanelId.clear(); sourceWindowState.detachScreenPoint = {}; UIEditorWindowWorkspaceController windowWorkspaceController( m_editorContext.GetShellAsset().panelRegistry, BuildWindowWorkspaceSet(sourceWindowId)); const UIEditorWindowWorkspaceOperationResult result = windowWorkspaceController.DetachPanelToNewWindow( sourceWindowId, sourceNodeId, panelId); if (result.status != UIEditorWindowWorkspaceOperationStatus::Changed) { LogRuntimeTrace("detach", "detach request rejected: " + result.message); return false; } if (!SynchronizeManagedWindowsFromWindowSet( windowWorkspaceController.GetWindowSet(), result.targetWindowId, screenPoint)) { LogRuntimeTrace("detach", "failed to synchronize detached window state"); return false; } if (ManagedWindowState* detachedWindowState = FindWindowState(result.targetWindowId); detachedWindowState != nullptr && detachedWindowState->hwnd != nullptr) { SetForegroundWindow(detachedWindowState->hwnd); } LogRuntimeTrace( "detach", "detached panel '" + panelId + "' from window '" + sourceWindowId + "' to window '" + result.targetWindowId + "'"); return true; } void Application::ProcessPendingDetachRequests() { if (m_globalTabDragSession.active) { return; } std::vector windowsToProcess = {}; for (const std::unique_ptr& windowState : m_windows) { if (windowState != nullptr && windowState->hwnd != nullptr && windowState->detachRequested) { windowsToProcess.push_back(windowState.get()); } } for (ManagedWindowState* windowState : windowsToProcess) { if (windowState != nullptr) { TryProcessDetachRequest(*windowState); } } } bool Application::HandleGlobalTabDragPointerMove(HWND hwnd) { if (!m_globalTabDragSession.active) { return false; } ManagedWindowState* ownerWindowState = FindWindowState(m_globalTabDragSession.panelWindowId); if (ownerWindowState == nullptr || ownerWindowState->hwnd != hwnd) { return false; } POINT screenPoint = {}; if (GetCursorPos(&screenPoint)) { m_globalTabDragSession.screenPoint = screenPoint; MoveGlobalTabDragWindow(*ownerWindowState, screenPoint); } return true; } bool Application::HandleGlobalTabDragPointerButtonUp(HWND hwnd) { if (!m_globalTabDragSession.active) { return false; } const ManagedWindowState* ownerWindowState = FindWindowState(m_globalTabDragSession.panelWindowId); if (ownerWindowState == nullptr || ownerWindowState->hwnd != hwnd) { return false; } POINT screenPoint = m_globalTabDragSession.screenPoint; GetCursorPos(&screenPoint); const std::string panelWindowId = m_globalTabDragSession.panelWindowId; const std::string sourceNodeId = m_globalTabDragSession.sourceNodeId; const std::string panelId = m_globalTabDragSession.panelId; EndGlobalTabDragSession(); ManagedWindowState* targetWindowState = FindTopmostWindowStateAtScreenPoint(screenPoint, panelWindowId); if (targetWindowState == nullptr || targetWindowState->hwnd == nullptr) { return true; } const UIPoint targetPoint = ConvertScreenPixelsToClientDips(*targetWindowState, screenPoint); const Widgets::UIEditorDockHostLayout& targetLayout = targetWindowState->editorWorkspace .GetShellFrame() .workspaceInteractionFrame .dockHostFrame .layout; CrossWindowDockDropTarget dropTarget = {}; if (!TryResolveCrossWindowDockDropTarget(targetLayout, targetPoint, dropTarget)) { return true; } UIEditorWindowWorkspaceController windowWorkspaceController( m_editorContext.GetShellAsset().panelRegistry, BuildWindowWorkspaceSet(targetWindowState->windowId)); const UIEditorWindowWorkspaceOperationResult result = dropTarget.placement == UIEditorWorkspaceDockPlacement::Center ? windowWorkspaceController.MovePanelToStack( panelWindowId, sourceNodeId, panelId, targetWindowState->windowId, dropTarget.nodeId, dropTarget.insertionIndex) : windowWorkspaceController.DockPanelRelative( panelWindowId, sourceNodeId, panelId, targetWindowState->windowId, dropTarget.nodeId, dropTarget.placement); if (result.status != UIEditorWindowWorkspaceOperationStatus::Changed) { LogRuntimeTrace("drag", "cross-window drop rejected: " + result.message); return true; } if (!SynchronizeManagedWindowsFromWindowSet( windowWorkspaceController.GetWindowSet(), {}, screenPoint)) { LogRuntimeTrace("drag", "failed to synchronize windows after cross-window drop"); return true; } if (targetWindowState->hwnd != nullptr) { SetForegroundWindow(targetWindowState->hwnd); } LogRuntimeTrace( "drag", "committed cross-window drop panel '" + panelId + "' into window '" + targetWindowState->windowId + "'"); return true; } void Application::AppendGlobalTabDragDropPreview(UIDrawList& drawList) const { if (!m_globalTabDragSession.active || m_currentWindowState == nullptr) { return; } if (m_currentWindowState->windowId == m_globalTabDragSession.panelWindowId) { return; } const ManagedWindowState* targetWindowState = FindTopmostWindowStateAtScreenPoint( m_globalTabDragSession.screenPoint, m_globalTabDragSession.panelWindowId); if (targetWindowState == nullptr || targetWindowState != m_currentWindowState) { return; } const UIPoint targetPoint = ConvertScreenPixelsToClientDips(*targetWindowState, m_globalTabDragSession.screenPoint); const Widgets::UIEditorDockHostLayout& targetLayout = targetWindowState->editorWorkspace .GetShellFrame() .workspaceInteractionFrame .dockHostFrame .layout; CrossWindowDockDropTarget dropTarget = {}; if (!TryResolveCrossWindowDockDropTarget(targetLayout, targetPoint, dropTarget)) { return; } Widgets::UIEditorDockHostDropPreviewState previewState = {}; previewState.visible = true; previewState.sourceNodeId = m_globalTabDragSession.sourceNodeId; previewState.sourcePanelId = m_globalTabDragSession.panelId; previewState.targetNodeId = dropTarget.nodeId; previewState.placement = dropTarget.placement; previewState.insertionIndex = dropTarget.insertionIndex; const Widgets::UIEditorDockHostDropPreviewLayout previewLayout = Widgets::ResolveUIEditorDockHostDropPreviewLayout(targetLayout, previewState); if (!previewLayout.visible) { return; } const Widgets::UIEditorDockHostPalette& dockPalette = ResolveUIEditorShellInteractionPalette().shellPalette.dockHostPalette; drawList.AddFilledRect( previewLayout.previewRect, dockPalette.dropPreviewFillColor); drawList.AddRectOutline( previewLayout.previewRect, dockPalette.dropPreviewBorderColor, 1.0f); } void Application::HandleDestroyedWindow(HWND hwnd) { if (ManagedWindowState* windowState = FindWindowState(hwnd); windowState != nullptr) { windowState->hwnd = nullptr; if (windowState->primary) { m_shutdownRequested = true; for (const std::unique_ptr& otherWindowState : m_windows) { if (otherWindowState != nullptr && otherWindowState.get() != windowState && otherWindowState->hwnd != nullptr) { PostMessageW(otherWindowState->hwnd, WM_CLOSE, 0, 0); } } } } } void Application::Shutdown() { LogRuntimeTrace("app", "shutdown begin"); m_shutdownRequested = true; for (const std::unique_ptr& windowState : m_windows) { if (windowState == nullptr) { continue; } ManagedWindowState* const previousWindowState = m_currentWindowState; m_currentWindowState = windowState.get(); DestroyManagedWindow(*windowState); m_currentWindowState = previousWindowState; } m_windows.clear(); m_currentWindowState = nullptr; m_pendingCreateWindowState = 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; } UINT pixelWidth = 0u; UINT pixelHeight = 0u; if (!ResolveRenderClientPixelSize(pixelWidth, pixelHeight)) { return; } const float width = PixelsToDips(static_cast(pixelWidth)); const float height = PixelsToDips(static_cast(pixelHeight)); const UIRect workspaceBounds = ResolveWorkspaceBounds(width, height); UIDrawData drawData = {}; UIDrawList& drawList = drawData.EmplaceDrawList("XCEditorShell"); drawList.AddFilledRect( UIRect(0.0f, 0.0f, width, height), kShellSurfaceColor); 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_workspaceController, m_editorWorkspace.GetShellInteractionState())); } const Host::D3D12WindowRenderLoopFrameContext frameContext = m_windowRenderLoop.BeginFrame(); if (!frameContext.warning.empty()) { LogRuntimeTrace("viewport", frameContext.warning); } m_editorContext.AttachTextMeasurer(m_renderer); m_editorWorkspace.Update( m_editorContext, m_workspaceController, workspaceBounds, frameEvents, BuildCaptureStatusText(), RequireCurrentWindowState().primary ? App::ProductEditorShellVariant::Primary : App::ProductEditorShellVariant::DetachedWindow); const UIEditorShellInteractionFrame& shellFrame = m_editorWorkspace.GetShellFrame(); const UIEditorDockHostInteractionState& dockHostInteractionState = m_editorWorkspace .GetShellInteractionState() .workspaceInteractionState .dockHostInteractionState; 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_workspaceController.GetWorkspace().activePanelId << " message=" << shellFrame.result.workspaceResult.dockHostResult.layoutResult.message; LogRuntimeTrace( "frame", frameTrace.str()); } if (!m_globalTabDragSession.active && !dockHostInteractionState.activeTabDragNodeId.empty() && !dockHostInteractionState.activeTabDragPanelId.empty()) { POINT screenPoint = {}; GetCursorPos(&screenPoint); RequireCurrentWindowState().pendingGlobalTabDragStart = true; RequireCurrentWindowState().pendingGlobalTabDragNodeId = dockHostInteractionState.activeTabDragNodeId; RequireCurrentWindowState().pendingGlobalTabDragPanelId = dockHostInteractionState.activeTabDragPanelId; RequireCurrentWindowState().pendingGlobalTabDragScreenPoint = screenPoint; RequireCurrentWindowState().pendingGlobalTabDragWindowOffset = ResolveGlobalTabDragWindowOffset( RequireCurrentWindowState(), dockHostInteractionState.activeTabDragNodeId, dockHostInteractionState.activeTabDragPanelId, screenPoint); } if (shellFrame.result.workspaceResult.dockHostResult.detachRequested) { POINT screenPoint = {}; GetCursorPos(&screenPoint); RequireCurrentWindowState().detachRequested = true; RequireCurrentWindowState().detachedNodeId = shellFrame.result.workspaceResult.dockHostResult.detachedNodeId; RequireCurrentWindowState().detachedPanelId = shellFrame.result.workspaceResult.dockHostResult.detachedPanelId; RequireCurrentWindowState().detachScreenPoint = screenPoint; } ApplyHostCaptureRequests(shellFrame.result); for (const App::ProductEditorWorkspaceTraceEntry& entry : m_editorWorkspace.GetTraceEntries()) { LogRuntimeTrace(entry.channel, entry.message); } ApplyHostedContentCaptureRequests(); ApplyCurrentCursor(); m_editorWorkspace.Append(drawList); AppendGlobalTabDragDropPreview(drawList); if (frameContext.canRenderViewports) { m_editorWorkspace.RenderRequestedViewports(frameContext.renderContext); } } else { drawList.AddText( UIPoint(28.0f, 28.0f), "Editor shell asset invalid.", kShellTextColor, 16.0f); drawList.AddText( UIPoint(28.0f, 54.0f), m_editorContext.GetValidationMessage().empty() ? std::string("Unknown validation error.") : m_editorContext.GetValidationMessage(), kShellMutedTextColor, 12.0f); } AppendBorderlessWindowChrome(drawList, width); 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); } bool Application::IsBorderlessWindowEnabled() const { return true; } bool Application::HasBorderlessWindowChrome() const { return m_currentWindowState != nullptr && m_currentWindowState->primary; } bool Application::IsBorderlessWindowMaximized() const { return m_hostRuntime.IsBorderlessWindowMaximized(); } bool Application::HandleBorderlessWindowSystemCommand(WPARAM wParam) { if (!IsBorderlessWindowEnabled()) { return false; } switch (wParam & 0xFFF0u) { case SC_MAXIMIZE: ToggleBorderlessWindowMaximizeRestore(); return true; case SC_RESTORE: if (!IsIconic(m_hwnd)) { ToggleBorderlessWindowMaximizeRestore(); return true; } return false; default: return false; } } bool Application::HandleBorderlessWindowGetMinMaxInfo(LPARAM lParam) const { return Host::HandleBorderlessWindowGetMinMaxInfo(m_hwnd, lParam); } LRESULT Application::HandleBorderlessWindowNcCalcSize(WPARAM wParam, LPARAM lParam) const { return Host::HandleBorderlessWindowNcCalcSize( m_hwnd, wParam, lParam, m_hostRuntime.GetWindowDpi()); } 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 { const Host::BorderlessWindowResizeEdge borderlessResizeEdge = m_hostRuntime.IsBorderlessResizeActive() ? m_hostRuntime.GetBorderlessResizeEdge() : m_hostRuntime.GetHoveredBorderlessResizeEdge(); if (borderlessResizeEdge != Host::BorderlessWindowResizeEdge::None) { return Host::ResolveBorderlessWindowResizeCursor(borderlessResizeEdge); } 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; } Host::BorderlessWindowResizeEdge Application::HitTestBorderlessWindowResizeEdge(LPARAM lParam) const { if (!IsBorderlessWindowEnabled() || m_hwnd == nullptr || IsBorderlessWindowMaximized()) { return Host::BorderlessWindowResizeEdge::None; } RECT clientRect = {}; if (!GetClientRect(m_hwnd, &clientRect)) { return Host::BorderlessWindowResizeEdge::None; } const float clientWidthDips = PixelsToDips(static_cast((std::max)(clientRect.right - clientRect.left, 1L))); const float clientHeightDips = PixelsToDips(static_cast((std::max)(clientRect.bottom - clientRect.top, 1L))); return Host::HitTestBorderlessWindowResizeEdge( ::XCEngine::UI::UIRect(0.0f, 0.0f, clientWidthDips, clientHeightDips), ConvertClientPixelsToDips(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam))); } bool Application::UpdateBorderlessWindowResizeHover(LPARAM lParam) { const Host::BorderlessWindowResizeEdge hoveredEdge = HitTestBorderlessWindowResizeEdge(lParam); if (m_hostRuntime.GetHoveredBorderlessResizeEdge() == hoveredEdge) { return false; } m_hostRuntime.SetHoveredBorderlessResizeEdge(hoveredEdge); ApplyBorderlessWindowResizeCursorHoverPriority(); return true; } bool Application::HandleBorderlessWindowResizeButtonDown(LPARAM lParam) { const Host::BorderlessWindowResizeEdge edge = HitTestBorderlessWindowResizeEdge(lParam); if (edge == Host::BorderlessWindowResizeEdge::None || m_hwnd == nullptr) { return false; } POINT screenPoint = {}; if (!GetCursorPos(&screenPoint)) { return false; } RECT windowRect = {}; if (!GetWindowRect(m_hwnd, &windowRect)) { return false; } m_hostRuntime.BeginBorderlessResize(edge, screenPoint, windowRect); SetCapture(m_hwnd); InvalidateHostWindow(); return true; } bool Application::HandleBorderlessWindowResizeButtonUp() { if (!m_hostRuntime.IsBorderlessResizeActive()) { return false; } m_hostRuntime.EndBorderlessResize(); if (GetCapture() == m_hwnd) { ReleaseCapture(); } InvalidateHostWindow(); return true; } bool Application::HandleBorderlessWindowResizePointerMove() { if (!m_hostRuntime.IsBorderlessResizeActive() || m_hwnd == nullptr) { return false; } POINT currentScreenPoint = {}; if (!GetCursorPos(¤tScreenPoint)) { return false; } RECT targetRect = Host::ComputeBorderlessWindowResizeRect( m_hostRuntime.GetBorderlessResizeInitialWindowRect(), m_hostRuntime.GetBorderlessResizeInitialScreenPoint(), currentScreenPoint, m_hostRuntime.GetBorderlessResizeEdge(), 640, 360); const int width = targetRect.right - targetRect.left; const int height = targetRect.bottom - targetRect.top; if (width <= 0 || height <= 0) { return true; } m_hostRuntime.SetPredictedClientPixelSize( static_cast(width), static_cast(height)); ApplyWindowResize( static_cast(width), static_cast(height)); RenderFrame(); SetWindowPos( m_hwnd, nullptr, targetRect.left, targetRect.top, width, height, SWP_NOZORDER | SWP_NOACTIVATE); return true; } void Application::ClearBorderlessWindowResizeState() { if (m_hostRuntime.IsBorderlessResizeActive()) { return; } if (m_hostRuntime.GetHoveredBorderlessResizeEdge() == Host::BorderlessWindowResizeEdge::None) { return; } m_hostRuntime.SetHoveredBorderlessResizeEdge(Host::BorderlessWindowResizeEdge::None); InvalidateHostWindow(); } void Application::ForceClearBorderlessWindowResizeState() { if (m_hostRuntime.GetHoveredBorderlessResizeEdge() == Host::BorderlessWindowResizeEdge::None && !m_hostRuntime.IsBorderlessResizeActive()) { return; } m_hostRuntime.SetHoveredBorderlessResizeEdge(Host::BorderlessWindowResizeEdge::None); m_hostRuntime.EndBorderlessResize(); if (GetCapture() == m_hwnd) { ReleaseCapture(); } InvalidateHostWindow(); } void Application::ApplyBorderlessWindowResizeCursorHoverPriority() { if (m_hostRuntime.GetHoveredBorderlessResizeEdge() != Host::BorderlessWindowResizeEdge::None || m_hostRuntime.IsBorderlessResizeActive()) { m_borderlessWindowChromeState.hoveredTarget = Host::BorderlessWindowChromeHitTarget::None; } } Host::BorderlessWindowChromeLayout Application::ResolveBorderlessWindowChromeLayout( float clientWidthDips) const { return Host::BuildBorderlessWindowChromeLayout( ::XCEngine::UI::UIRect(0.0f, 0.0f, clientWidthDips, kBorderlessTitleBarHeightDips), 0.0f); } Host::BorderlessWindowChromeHitTarget Application::HitTestBorderlessWindowChrome(LPARAM lParam) const { if (!HasBorderlessWindowChrome() || m_hwnd == nullptr) { return Host::BorderlessWindowChromeHitTarget::None; } RECT clientRect = {}; if (!GetClientRect(m_hwnd, &clientRect)) { return Host::BorderlessWindowChromeHitTarget::None; } const float clientWidthDips = PixelsToDips( static_cast((std::max)(clientRect.right - clientRect.left, 1L))); const Host::BorderlessWindowChromeLayout layout = ResolveBorderlessWindowChromeLayout(clientWidthDips); return Host::HitTestBorderlessWindowChrome( layout, ConvertClientPixelsToDips(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam))); } bool Application::UpdateBorderlessWindowChromeHover(LPARAM lParam) { if (m_hostRuntime.GetHoveredBorderlessResizeEdge() != Host::BorderlessWindowResizeEdge::None || m_hostRuntime.IsBorderlessResizeActive()) { const bool changed = m_borderlessWindowChromeState.hoveredTarget != Host::BorderlessWindowChromeHitTarget::None; m_borderlessWindowChromeState.hoveredTarget = Host::BorderlessWindowChromeHitTarget::None; return changed; } const Host::BorderlessWindowChromeHitTarget hitTarget = HitTestBorderlessWindowChrome(lParam); const Host::BorderlessWindowChromeHitTarget buttonTarget = hitTarget == Host::BorderlessWindowChromeHitTarget::MinimizeButton || hitTarget == Host::BorderlessWindowChromeHitTarget::MaximizeRestoreButton || hitTarget == Host::BorderlessWindowChromeHitTarget::CloseButton ? hitTarget : Host::BorderlessWindowChromeHitTarget::None; if (m_borderlessWindowChromeState.hoveredTarget == buttonTarget) { return false; } m_borderlessWindowChromeState.hoveredTarget = buttonTarget; return true; } bool Application::HandleBorderlessWindowChromeButtonDown(LPARAM lParam) { if (!HasBorderlessWindowChrome()) { return false; } if (m_hostRuntime.GetHoveredBorderlessResizeEdge() != Host::BorderlessWindowResizeEdge::None || m_hostRuntime.IsBorderlessResizeActive()) { return false; } const Host::BorderlessWindowChromeHitTarget hitTarget = HitTestBorderlessWindowChrome(lParam); switch (hitTarget) { case Host::BorderlessWindowChromeHitTarget::MinimizeButton: case Host::BorderlessWindowChromeHitTarget::MaximizeRestoreButton: case Host::BorderlessWindowChromeHitTarget::CloseButton: m_borderlessWindowChromeState.pressedTarget = hitTarget; if (m_hwnd != nullptr) { SetCapture(m_hwnd); } InvalidateHostWindow(); return true; case Host::BorderlessWindowChromeHitTarget::DragRegion: if (m_hwnd != nullptr) { if (IsBorderlessWindowMaximized()) { POINT screenPoint = {}; if (GetCursorPos(&screenPoint)) { m_hostRuntime.BeginBorderlessWindowDragRestore(screenPoint); SetCapture(m_hwnd); return true; } } ReleaseCapture(); SendMessageW(m_hwnd, WM_NCLBUTTONDOWN, HTCAPTION, 0); } return true; case Host::BorderlessWindowChromeHitTarget::None: default: return false; } } bool Application::HandleBorderlessWindowChromeButtonUp(LPARAM lParam) { if (!HasBorderlessWindowChrome()) { return false; } if (m_hostRuntime.IsBorderlessWindowDragRestoreArmed()) { ClearBorderlessWindowChromeDragRestoreState(); return true; } const Host::BorderlessWindowChromeHitTarget pressedTarget = m_borderlessWindowChromeState.pressedTarget; if (pressedTarget != Host::BorderlessWindowChromeHitTarget::MinimizeButton && pressedTarget != Host::BorderlessWindowChromeHitTarget::MaximizeRestoreButton && pressedTarget != Host::BorderlessWindowChromeHitTarget::CloseButton) { return false; } const Host::BorderlessWindowChromeHitTarget releasedTarget = HitTestBorderlessWindowChrome(lParam); m_borderlessWindowChromeState.pressedTarget = Host::BorderlessWindowChromeHitTarget::None; if (GetCapture() == m_hwnd) { ReleaseCapture(); } InvalidateHostWindow(); if (pressedTarget == releasedTarget) { ExecuteBorderlessWindowChromeAction(pressedTarget); } return true; } bool Application::HandleBorderlessWindowChromeDoubleClick(LPARAM lParam) { if (!HasBorderlessWindowChrome()) { return false; } if (m_hostRuntime.IsBorderlessWindowDragRestoreArmed()) { ClearBorderlessWindowChromeDragRestoreState(); } if (HitTestBorderlessWindowChrome(lParam) != Host::BorderlessWindowChromeHitTarget::DragRegion) { return false; } ExecuteBorderlessWindowChromeAction(Host::BorderlessWindowChromeHitTarget::MaximizeRestoreButton); return true; } bool Application::HandleBorderlessWindowChromeDragRestorePointerMove() { if (!HasBorderlessWindowChrome()) { return false; } if (!m_hostRuntime.IsBorderlessWindowDragRestoreArmed() || m_hwnd == nullptr) { return false; } POINT currentScreenPoint = {}; if (!GetCursorPos(¤tScreenPoint)) { return true; } const POINT initialScreenPoint = m_hostRuntime.GetBorderlessWindowDragRestoreInitialScreenPoint(); const int dragThresholdX = (std::max)(GetSystemMetrics(SM_CXDRAG), 1); const int dragThresholdY = (std::max)(GetSystemMetrics(SM_CYDRAG), 1); const LONG deltaX = currentScreenPoint.x - initialScreenPoint.x; const LONG deltaY = currentScreenPoint.y - initialScreenPoint.y; if (std::abs(deltaX) < dragThresholdX && std::abs(deltaY) < dragThresholdY) { return true; } RECT restoreRect = {}; RECT currentRect = {}; RECT workAreaRect = {}; if (!m_hostRuntime.TryGetBorderlessWindowRestoreRect(restoreRect) || !QueryCurrentWindowRect(currentRect) || !QueryBorderlessWindowWorkAreaRect(workAreaRect)) { ClearBorderlessWindowChromeDragRestoreState(); return true; } const int restoreWidth = restoreRect.right - restoreRect.left; const int restoreHeight = restoreRect.bottom - restoreRect.top; const int currentWidth = currentRect.right - currentRect.left; if (restoreWidth <= 0 || restoreHeight <= 0 || currentWidth <= 0) { ClearBorderlessWindowChromeDragRestoreState(); return true; } const float pointerRatio = static_cast(currentScreenPoint.x - currentRect.left) / static_cast(currentWidth); const float clampedPointerRatio = (std::clamp)(pointerRatio, 0.0f, 1.0f); const int newLeft = (std::clamp)( currentScreenPoint.x - static_cast(clampedPointerRatio * static_cast(restoreWidth)), workAreaRect.left, workAreaRect.right - restoreWidth); const int titleBarHeightPixels = static_cast(kBorderlessTitleBarHeightDips * GetDpiScale()); const int newTop = (std::clamp)( currentScreenPoint.y - (std::max)(titleBarHeightPixels / 2, 1), workAreaRect.top, workAreaRect.bottom - restoreHeight); const RECT targetRect = { newLeft, newTop, newLeft + restoreWidth, newTop + restoreHeight }; m_hostRuntime.SetBorderlessWindowMaximized(false); ApplyPredictedWindowRectTransition(targetRect); ClearBorderlessWindowChromeDragRestoreState(); ReleaseCapture(); SendMessageW(m_hwnd, WM_NCLBUTTONDOWN, HTCAPTION, 0); return true; } void Application::ClearBorderlessWindowChromeDragRestoreState() { if (!m_hostRuntime.IsBorderlessWindowDragRestoreArmed()) { return; } m_hostRuntime.EndBorderlessWindowDragRestore(); if (GetCapture() == m_hwnd) { ReleaseCapture(); } } void Application::ClearBorderlessWindowChromeState() { if (m_borderlessWindowChromeState.hoveredTarget == Host::BorderlessWindowChromeHitTarget::None && m_borderlessWindowChromeState.pressedTarget == Host::BorderlessWindowChromeHitTarget::None) { return; } m_borderlessWindowChromeState = {}; InvalidateHostWindow(); } void Application::AppendBorderlessWindowChrome( ::XCEngine::UI::UIDrawList& drawList, float clientWidthDips) const { if (!HasBorderlessWindowChrome()) { return; } const Host::BorderlessWindowChromeLayout layout = ResolveBorderlessWindowChromeLayout(clientWidthDips); const float iconExtent = 16.0f; const float iconInsetLeft = 8.0f; const float iconTextGap = 8.0f; const float iconX = layout.titleBarRect.x + iconInsetLeft; const float iconY = layout.titleBarRect.y + (std::max)(0.0f, (layout.titleBarRect.height - iconExtent) * 0.5f); drawList.AddFilledRect( layout.titleBarRect, kShellSurfaceColor); drawList.AddLine( UIPoint(layout.titleBarRect.x, layout.titleBarRect.y + layout.titleBarRect.height), UIPoint( layout.titleBarRect.x + layout.titleBarRect.width, layout.titleBarRect.y + layout.titleBarRect.height), kShellBorderColor, 1.0f); if (m_titleBarLogoIcon.IsValid()) { drawList.AddImage( UIRect(iconX, iconY, iconExtent, iconExtent), m_titleBarLogoIcon, UIColor(1.0f, 1.0f, 1.0f, 1.0f)); } drawList.AddText( UIPoint( iconX + (m_titleBarLogoIcon.IsValid() ? (iconExtent + iconTextGap) : 4.0f), layout.titleBarRect.y + (std::max)(0.0f, (layout.titleBarRect.height - kBorderlessTitleBarFontSize) * 0.5f - 1.0f)), kWindowTitleText, kShellTextColor, kBorderlessTitleBarFontSize); Host::AppendBorderlessWindowChrome( drawList, layout, m_borderlessWindowChromeState, IsBorderlessWindowMaximized()); } void Application::ExecuteBorderlessWindowChromeAction( Host::BorderlessWindowChromeHitTarget target) { if (m_hwnd == nullptr) { return; } switch (target) { case Host::BorderlessWindowChromeHitTarget::MinimizeButton: ShowWindow(m_hwnd, SW_MINIMIZE); break; case Host::BorderlessWindowChromeHitTarget::MaximizeRestoreButton: ToggleBorderlessWindowMaximizeRestore(); break; case Host::BorderlessWindowChromeHitTarget::CloseButton: PostMessageW(m_hwnd, WM_CLOSE, 0, 0); break; case Host::BorderlessWindowChromeHitTarget::DragRegion: case Host::BorderlessWindowChromeHitTarget::None: default: break; } InvalidateHostWindow(); } bool Application::QueryCurrentWindowRect(RECT& outRect) const { outRect = {}; return m_hwnd != nullptr && GetWindowRect(m_hwnd, &outRect) != FALSE; } bool Application::QueryBorderlessWindowWorkAreaRect(RECT& outRect) const { outRect = {}; if (m_hwnd == nullptr) { return false; } const HMONITOR monitor = MonitorFromWindow(m_hwnd, MONITOR_DEFAULTTONEAREST); if (monitor == nullptr) { return false; } MONITORINFO monitorInfo = {}; monitorInfo.cbSize = sizeof(monitorInfo); if (!GetMonitorInfoW(monitor, &monitorInfo)) { return false; } outRect = monitorInfo.rcWork; return true; } bool Application::ApplyPredictedWindowRectTransition(const RECT& targetRect) { if (m_hwnd == nullptr) { return false; } const int width = targetRect.right - targetRect.left; const int height = targetRect.bottom - targetRect.top; if (width <= 0 || height <= 0) { return false; } m_hostRuntime.SetPredictedClientPixelSize( static_cast(width), static_cast(height)); ApplyWindowResize( static_cast(width), static_cast(height)); RenderFrame(); SetWindowPos( m_hwnd, nullptr, targetRect.left, targetRect.top, width, height, SWP_NOZORDER | SWP_NOACTIVATE); InvalidateHostWindow(); return true; } void Application::ToggleBorderlessWindowMaximizeRestore() { if (m_hwnd == nullptr) { return; } if (!IsBorderlessWindowMaximized()) { RECT currentRect = {}; RECT workAreaRect = {}; if (!QueryCurrentWindowRect(currentRect) || !QueryBorderlessWindowWorkAreaRect(workAreaRect)) { return; } m_hostRuntime.SetBorderlessWindowRestoreRect(currentRect); m_hostRuntime.SetBorderlessWindowMaximized(true); ApplyPredictedWindowRectTransition(workAreaRect); return; } RECT restoreRect = {}; if (!m_hostRuntime.TryGetBorderlessWindowRestoreRect(restoreRect)) { return; } m_hostRuntime.SetBorderlessWindowMaximized(false); ApplyPredictedWindowRectTransition(restoreRect); } void Application::InvalidateHostWindow() const { if (m_hwnd != nullptr && IsWindow(m_hwnd)) { InvalidateRect(m_hwnd, nullptr, FALSE); } } UIPoint Application::ConvertClientPixelsToDips(LONG x, LONG y) const { return UIPoint( PixelsToDips(static_cast(x)), PixelsToDips(static_cast(y))); } UIPoint Application::ConvertScreenPixelsToClientDips( const ManagedWindowState& windowState, const POINT& screenPoint) const { POINT clientPoint = screenPoint; if (windowState.hwnd != nullptr) { ScreenToClient(windowState.hwnd, &clientPoint); } const float dpiScale = windowState.hostRuntime.GetDpiScale(kBaseDpiScale); return UIPoint( dpiScale > 0.0f ? static_cast(clientPoint.x) / dpiScale : static_cast(clientPoint.x), dpiScale > 0.0f ? static_cast(clientPoint.y) / dpiScale : static_cast(clientPoint.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) { bool matchesPredictedClientSize = false; UINT predictedWidth = 0u; UINT predictedHeight = 0u; if (m_hostRuntime.TryGetPredictedClientPixelSize(predictedWidth, predictedHeight)) { matchesPredictedClientSize = predictedWidth == width && predictedHeight == height; } m_hostRuntime.ClearPredictedClientPixelSize(); if (IsBorderlessWindowEnabled() && m_hwnd != nullptr) { Host::RefreshBorderlessWindowDwmDecorations(m_hwnd); } if (!matchesPredictedClientSize) { ApplyWindowResize(width, height); } } void Application::OnEnterSizeMove() { m_hostRuntime.BeginInteractiveResize(); } void Application::OnExitSizeMove() { m_hostRuntime.EndInteractiveResize(); m_hostRuntime.ClearPredictedClientPixelSize(); 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; } bool Application::ResolveRenderClientPixelSize(UINT& outWidth, UINT& outHeight) const { if (m_hostRuntime.TryGetPredictedClientPixelSize(outWidth, outHeight)) { return true; } return QueryCurrentClientPixelSize(outWidth, outHeight); } UIRect Application::ResolveWorkspaceBounds(float clientWidthDips, float clientHeightDips) const { if (!HasBorderlessWindowChrome()) { return UIRect(0.0f, 0.0f, clientWidthDips, clientHeightDips); } const float titleBarHeight = (std::min)(kBorderlessTitleBarHeightDips, clientHeightDips); return UIRect( 0.0f, titleBarHeight, clientWidthDips, (std::max)(0.0f, clientHeightDips - titleBarHeight)); } 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); } Host::RefreshBorderlessWindowDwmDecorations(m_hwnd); } 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() || m_hostRuntime.IsBorderlessWindowDragRestoreArmed(); } 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)) { const auto* createStruct = reinterpret_cast(lParam); if (createStruct != nullptr) { if (Application* application = reinterpret_cast(createStruct->lpCreateParams); application != nullptr && application->m_pendingCreateWindowState != nullptr && application->m_pendingCreateWindowState->hwnd == nullptr) { application->m_pendingCreateWindowState->hwnd = hwnd; } } return dispatcherResult; } Application* application = Host::WindowMessageDispatcher::GetApplicationFromWindow(hwnd); if (application != nullptr) { application->m_currentWindowState = application->FindWindowState(hwnd); } if (application != nullptr && Host::WindowMessageDispatcher::TryDispatch( hwnd, *application, message, wParam, lParam, dispatcherResult)) { return dispatcherResult; } switch (message) { case WM_MOUSEMOVE: if (application != nullptr) { if (application->HandleGlobalTabDragPointerMove(hwnd)) { return 0; } if (application->HandleBorderlessWindowResizePointerMove()) { return 0; } if (application->HandleBorderlessWindowChromeDragRestorePointerMove()) { return 0; } const bool resizeHoverChanged = application->UpdateBorderlessWindowResizeHover(lParam); if (application->UpdateBorderlessWindowChromeHover(lParam)) { application->InvalidateHostWindow(); } if (resizeHoverChanged) { application->InvalidateHostWindow(); } if (!application->m_trackingMouseLeave) { TRACKMOUSEEVENT trackMouseEvent = {}; trackMouseEvent.cbSize = sizeof(trackMouseEvent); trackMouseEvent.dwFlags = TME_LEAVE; trackMouseEvent.hwndTrack = hwnd; if (TrackMouseEvent(&trackMouseEvent)) { application->m_trackingMouseLeave = true; } } if (application->m_hostRuntime.GetHoveredBorderlessResizeEdge() != Host::BorderlessWindowResizeEdge::None) { return 0; } const Host::BorderlessWindowChromeHitTarget chromeHitTarget = application->HitTestBorderlessWindowChrome(lParam); if (chromeHitTarget == Host::BorderlessWindowChromeHitTarget::MinimizeButton || chromeHitTarget == Host::BorderlessWindowChromeHitTarget::MaximizeRestoreButton || chromeHitTarget == Host::BorderlessWindowChromeHitTarget::CloseButton) { return 0; } application->QueuePointerEvent( UIInputEventType::PointerMove, UIPointerButton::None, wParam, lParam); return 0; } break; case WM_MOUSELEAVE: if (application != nullptr) { application->m_trackingMouseLeave = false; application->ClearBorderlessWindowResizeState(); application->ClearBorderlessWindowChromeDragRestoreState(); application->ClearBorderlessWindowChromeState(); application->QueuePointerLeaveEvent(); return 0; } break; case WM_LBUTTONDOWN: if (application != nullptr) { if (application->HandleBorderlessWindowResizeButtonDown(lParam)) { return 0; } if (application->HandleBorderlessWindowChromeButtonDown(lParam)) { return 0; } SetFocus(hwnd); application->QueuePointerEvent( UIInputEventType::PointerButtonDown, UIPointerButton::Left, wParam, lParam); return 0; } break; case WM_LBUTTONUP: if (application != nullptr) { if (application->HandleGlobalTabDragPointerButtonUp(hwnd)) { return 0; } if (application->HandleBorderlessWindowResizeButtonUp()) { return 0; } if (application->HandleBorderlessWindowChromeButtonUp(lParam)) { return 0; } application->QueuePointerEvent( UIInputEventType::PointerButtonUp, UIPointerButton::Left, wParam, lParam); return 0; } break; case WM_LBUTTONDBLCLK: if (application != nullptr && application->HandleBorderlessWindowChromeDoubleClick(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 && application->m_globalTabDragSession.active && application->m_globalTabDragSession.panelWindowId == (application->m_currentWindowState != nullptr ? application->m_currentWindowState->windowId : std::string()) && reinterpret_cast(lParam) != hwnd) { application->EndGlobalTabDragSession(); return 0; } if (application != nullptr && reinterpret_cast(lParam) != hwnd && application->HasInteractiveCaptureState()) { application->QueueWindowFocusEvent(UIInputEventType::FocusLost); application->ForceClearBorderlessWindowResizeState(); application->ClearBorderlessWindowChromeDragRestoreState(); application->ClearBorderlessWindowChromeState(); return 0; } if (application != nullptr && reinterpret_cast(lParam) != hwnd) { application->ForceClearBorderlessWindowResizeState(); application->ClearBorderlessWindowChromeDragRestoreState(); application->ClearBorderlessWindowChromeState(); } 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) { if (application->m_globalTabDragSession.active && application->m_globalTabDragSession.panelWindowId == (application->m_currentWindowState != nullptr ? application->m_currentWindowState->windowId : std::string())) { application->EndGlobalTabDragSession(); } application->HandleDestroyedWindow(hwnd); } return 0; default: break; } return DefWindowProcW(hwnd, message, wParam, lParam); } #undef m_hwnd #undef m_renderer #undef m_windowRenderer #undef m_windowRenderLoop #undef m_autoScreenshot #undef m_inputModifierTracker #undef m_workspaceController #undef m_editorWorkspace #undef m_pendingInputEvents #undef m_trackingMouseLeave #undef m_renderReady #undef m_titleBarLogoIcon #undef m_borderlessWindowChromeState #undef m_hostRuntime int RunXCUIEditorApp(HINSTANCE hInstance, int nCmdShow) { Application application; return application.Run(hInstance, nCmdShow); } } // namespace XCEngine::UI::Editor