Files
XCEngine/new_editor/app/Application.cpp

3027 lines
105 KiB
C++

#include "Application.h"
#include "EditorResources.h"
#include <Host/WindowMessageDispatcher.h>
#include <XCEditor/Foundation/UIEditorRuntimeTrace.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include <XCEngine/Input/InputTypes.h>
#include <XCEngine/UI/DrawData.h>
#include <algorithm>
#include <cassert>
#include <cctype>
#include <cstdlib>
#include <memory>
#include <sstream>
#include <string>
#include <shellscalingapi.h>
#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<DWORD>(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<const std::uint8_t*>(lockedBytes);
outSize = static_cast<std::size_t>(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<UINT>(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<GetDpiForWindowFn>(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<SetProcessDpiAwarenessContextFn>(
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<SetProcessDpiAwarenessFn>(
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<SetProcessDPIAwareFn>(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<char>(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<std::int32_t>(KeyCode::A);
case 'B': return static_cast<std::int32_t>(KeyCode::B);
case 'C': return static_cast<std::int32_t>(KeyCode::C);
case 'D': return static_cast<std::int32_t>(KeyCode::D);
case 'E': return static_cast<std::int32_t>(KeyCode::E);
case 'F': return static_cast<std::int32_t>(KeyCode::F);
case 'G': return static_cast<std::int32_t>(KeyCode::G);
case 'H': return static_cast<std::int32_t>(KeyCode::H);
case 'I': return static_cast<std::int32_t>(KeyCode::I);
case 'J': return static_cast<std::int32_t>(KeyCode::J);
case 'K': return static_cast<std::int32_t>(KeyCode::K);
case 'L': return static_cast<std::int32_t>(KeyCode::L);
case 'M': return static_cast<std::int32_t>(KeyCode::M);
case 'N': return static_cast<std::int32_t>(KeyCode::N);
case 'O': return static_cast<std::int32_t>(KeyCode::O);
case 'P': return static_cast<std::int32_t>(KeyCode::P);
case 'Q': return static_cast<std::int32_t>(KeyCode::Q);
case 'R': return static_cast<std::int32_t>(KeyCode::R);
case 'S': return static_cast<std::int32_t>(KeyCode::S);
case 'T': return static_cast<std::int32_t>(KeyCode::T);
case 'U': return static_cast<std::int32_t>(KeyCode::U);
case 'V': return static_cast<std::int32_t>(KeyCode::V);
case 'W': return static_cast<std::int32_t>(KeyCode::W);
case 'X': return static_cast<std::int32_t>(KeyCode::X);
case 'Y': return static_cast<std::int32_t>(KeyCode::Y);
case 'Z': return static_cast<std::int32_t>(KeyCode::Z);
case '0': return static_cast<std::int32_t>(KeyCode::Zero);
case '1': return static_cast<std::int32_t>(KeyCode::One);
case '2': return static_cast<std::int32_t>(KeyCode::Two);
case '3': return static_cast<std::int32_t>(KeyCode::Three);
case '4': return static_cast<std::int32_t>(KeyCode::Four);
case '5': return static_cast<std::int32_t>(KeyCode::Five);
case '6': return static_cast<std::int32_t>(KeyCode::Six);
case '7': return static_cast<std::int32_t>(KeyCode::Seven);
case '8': return static_cast<std::int32_t>(KeyCode::Eight);
case '9': return static_cast<std::int32_t>(KeyCode::Nine);
case VK_SPACE: return static_cast<std::int32_t>(KeyCode::Space);
case VK_TAB: return static_cast<std::int32_t>(KeyCode::Tab);
case VK_RETURN: return static_cast<std::int32_t>(KeyCode::Enter);
case VK_ESCAPE: return static_cast<std::int32_t>(KeyCode::Escape);
case VK_SHIFT: return static_cast<std::int32_t>(KeyCode::LeftShift);
case VK_CONTROL: return static_cast<std::int32_t>(KeyCode::LeftCtrl);
case VK_MENU: return static_cast<std::int32_t>(KeyCode::LeftAlt);
case VK_UP: return static_cast<std::int32_t>(KeyCode::Up);
case VK_DOWN: return static_cast<std::int32_t>(KeyCode::Down);
case VK_LEFT: return static_cast<std::int32_t>(KeyCode::Left);
case VK_RIGHT: return static_cast<std::int32_t>(KeyCode::Right);
case VK_HOME: return static_cast<std::int32_t>(KeyCode::Home);
case VK_END: return static_cast<std::int32_t>(KeyCode::End);
case VK_PRIOR: return static_cast<std::int32_t>(KeyCode::PageUp);
case VK_NEXT: return static_cast<std::int32_t>(KeyCode::PageDown);
case VK_DELETE: return static_cast<std::int32_t>(KeyCode::Delete);
case VK_BACK: return static_cast<std::int32_t>(KeyCode::Backspace);
case VK_F1: return static_cast<std::int32_t>(KeyCode::F1);
case VK_F2: return static_cast<std::int32_t>(KeyCode::F2);
case VK_F3: return static_cast<std::int32_t>(KeyCode::F3);
case VK_F4: return static_cast<std::int32_t>(KeyCode::F4);
case VK_F5: return static_cast<std::int32_t>(KeyCode::F5);
case VK_F6: return static_cast<std::int32_t>(KeyCode::F6);
case VK_F7: return static_cast<std::int32_t>(KeyCode::F7);
case VK_F8: return static_cast<std::int32_t>(KeyCode::F8);
case VK_F9: return static_cast<std::int32_t>(KeyCode::F9);
case VK_F10: return static_cast<std::int32_t>(KeyCode::F10);
case VK_F11: return static_cast<std::int32_t>(KeyCode::F11);
case VK_F12: return static_cast<std::int32_t>(KeyCode::F12);
default: return static_cast<std::int32_t>(KeyCode::None);
}
}
bool IsRepeatKeyMessage(LPARAM lParam) {
return (static_cast<unsigned long>(lParam) & (1ul << 30)) != 0ul;
}
std::filesystem::path GetExecutableDirectory() {
std::vector<wchar_t> buffer(MAX_PATH);
while (true) {
const DWORD copied = ::GetModuleFileNameW(
nullptr,
buffer.data(),
static_cast<DWORD>(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<ManagedWindowState>& 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<ManagedWindowState>& 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<ManagedWindowState>& 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<ManagedWindowState>& windowState : m_windows) {
if (windowState != nullptr && windowState->windowId == windowId) {
return windowState.get();
}
}
return nullptr;
}
Application::ManagedWindowState* Application::FindPrimaryWindowState() {
for (const std::unique_ptr<ManagedWindowState>& windowState : m_windows) {
if (windowState != nullptr && windowState->primary) {
return windowState.get();
}
}
return nullptr;
}
const Application::ManagedWindowState* Application::FindPrimaryWindowState() const {
for (const std::unique_ptr<ManagedWindowState>& 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<int>(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<HICON>(
LoadImageW(
m_hInstance,
MAKEINTRESOURCEW(IDI_APP_ICON),
IMAGE_ICON,
0,
0,
LR_DEFAULTSIZE));
windowClass.hIconSm = static_cast<HICON>(
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<ManagedWindowState>();
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<ManagedWindowState>& 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<ManagedWindowState*>(nullptr);
};
const HICON bigIcon = static_cast<HICON>(
LoadImageW(
m_hInstance,
MAKEINTRESOURCEW(IDI_APP_ICON),
IMAGE_ICON,
0,
0,
LR_DEFAULTSIZE));
const HICON smallIcon = static_cast<HICON>(
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<LPARAM>(bigIcon));
}
if (smallIcon != nullptr) {
SendMessageW(m_hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast<LPARAM>(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<ManagedWindowState>& 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<ManagedWindowState>& 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<std::string> 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<ManagedWindowState>& 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<Application*>(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<LONG>(point.x * dpiScale),
static_cast<LONG>(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<LONG>(static_cast<float>(kDetachedWindowDragOffsetXPixels) * dpiScale),
static_cast<LONG>(static_cast<float>(kDetachedWindowDragOffsetYPixels) * dpiScale)
};
}
void Application::MoveGlobalTabDragWindow(
ManagedWindowState& windowState,
const POINT& screenPoint) const {
if (windowState.hwnd == nullptr) {
return;
}
RECT currentRect = {};
if (!GetWindowRect(windowState.hwnd, &currentRect)) {
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<ManagedWindowState>& 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<ManagedWindowState*> windowsToProcess = {};
for (const std::unique_ptr<ManagedWindowState>& 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<ManagedWindowState>& 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<ManagedWindowState>& 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<float>(pixelWidth));
const float height = PixelsToDips(static_cast<float>(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<UIInputEvent> 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<SHORT>(screenPoint.x),
static_cast<SHORT>(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<float>((std::max)(clientRect.right - clientRect.left, 1L)));
const float clientHeightDips =
PixelsToDips(static_cast<float>((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(&currentScreenPoint)) {
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<UINT>(width),
static_cast<UINT>(height));
ApplyWindowResize(
static_cast<UINT>(width),
static_cast<UINT>(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<float>((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(&currentScreenPoint)) {
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<float>(currentScreenPoint.x - currentRect.left) /
static_cast<float>(currentWidth);
const float clampedPointerRatio = (std::clamp)(pointerRatio, 0.0f, 1.0f);
const int newLeft =
(std::clamp)(
currentScreenPoint.x - static_cast<int>(clampedPointerRatio * static_cast<float>(restoreWidth)),
workAreaRect.left,
workAreaRect.right - restoreWidth);
const int titleBarHeightPixels =
static_cast<int>(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<UINT>(width),
static_cast<UINT>(height));
ApplyWindowResize(
static_cast<UINT>(width),
static_cast<UINT>(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<float>(x)),
PixelsToDips(static_cast<float>(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<float>(clientPoint.x) / dpiScale : static_cast<float>(clientPoint.x),
dpiScale > 0.0f ? static_cast<float>(clientPoint.y) / dpiScale : static_cast<float>(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<UIInputEvent>& 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<int>(event.position.x)
<< ','
<< static_cast<int>(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<UINT>(width);
outHeight = static_cast<UINT>(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<std::size_t>(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<float>(wheelDelta);
event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast<std::size_t>(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<std::uint32_t>(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<const CREATESTRUCTW*>(lParam);
if (createStruct != nullptr) {
if (Application* application =
reinterpret_cast<Application*>(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<HWND>(lParam) != hwnd) {
application->EndGlobalTabDragSession();
return 0;
}
if (application != nullptr &&
reinterpret_cast<HWND>(lParam) != hwnd &&
application->HasInteractiveCaptureState()) {
application->QueueWindowFocusEvent(UIInputEventType::FocusLost);
application->ForceClearBorderlessWindowResizeState();
application->ClearBorderlessWindowChromeDragRestoreState();
application->ClearBorderlessWindowChromeState();
return 0;
}
if (application != nullptr &&
reinterpret_cast<HWND>(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