Files
XCEngine/new_editor/app/Application.cpp

1735 lines
58 KiB
C++

#include "Application.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 <cctype>
#include <cstdlib>
#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 DWORD kBorderlessWindowStyle =
WS_POPUP | WS_THICKFRAME | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX;
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';
}
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();
}
} // namespace
int Application::Run(HINSTANCE hInstance, int nCmdShow) {
if (!Initialize(hInstance, nCmdShow)) {
Shutdown();
return 1;
}
MSG message = {};
while (message.message != WM_QUIT) {
if (PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) {
TranslateMessage(&message);
DispatchMessageW(&message);
continue;
}
RenderFrame();
}
Shutdown();
return static_cast<int>(message.wParam);
}
bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
m_hInstance = hInstance;
EnableDpiAwareness();
const std::filesystem::path repoRoot = ResolveRepoRootPath();
const std::filesystem::path logRoot =
GetExecutableDirectory() / "logs";
InitializeUIEditorRuntimeTrace(logRoot);
SetUnhandledExceptionFilter(&Application::HandleUnhandledException);
LogRuntimeTrace("app", "initialize begin");
if (!m_editorContext.Initialize(repoRoot)) {
LogRuntimeTrace(
"app",
"shell asset validation failed: " + m_editorContext.GetValidationMessage());
return false;
}
WNDCLASSEXW windowClass = {};
windowClass.cbSize = sizeof(windowClass);
windowClass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
windowClass.lpfnWndProc = &Application::WndProc;
windowClass.hInstance = hInstance;
windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW);
windowClass.lpszClassName = kWindowClassName;
m_windowClassAtom = RegisterClassExW(&windowClass);
if (m_windowClassAtom == 0) {
LogRuntimeTrace("app", "window class registration failed");
return false;
}
m_hwnd = CreateWindowExW(
WS_EX_APPWINDOW,
kWindowClassName,
kWindowTitle,
kBorderlessWindowStyle,
CW_USEDEFAULT,
CW_USEDEFAULT,
1540,
940,
nullptr,
nullptr,
hInstance,
this);
if (m_hwnd == nullptr) {
LogRuntimeTrace("app", "window creation failed");
return false;
}
Host::RefreshBorderlessWindowDwmDecorations(m_hwnd);
m_hostRuntime.Reset();
m_hostRuntime.SetWindowDpi(QueryWindowDpi(m_hwnd));
m_renderer.SetDpiScale(GetDpiScale());
m_editorContext.SetExitRequestHandler([this]() {
if (m_hwnd != nullptr) {
PostMessageW(m_hwnd, WM_CLOSE, 0, 0);
}
});
std::ostringstream dpiTrace = {};
dpiTrace << "initial dpi=" << m_hostRuntime.GetWindowDpi() << " scale=" << GetDpiScale();
LogRuntimeTrace("window", dpiTrace.str());
if (!m_renderer.Initialize(m_hwnd)) {
LogRuntimeTrace("app", "renderer initialization failed");
return false;
}
RECT clientRect = {};
GetClientRect(m_hwnd, &clientRect);
const int clientWidth = (std::max)(clientRect.right - clientRect.left, 1L);
const int clientHeight = (std::max)(clientRect.bottom - clientRect.top, 1L);
if (!m_windowRenderer.Initialize(m_hwnd, clientWidth, clientHeight)) {
LogRuntimeTrace("app", "d3d12 window renderer initialization failed");
return false;
}
const Host::D3D12WindowRenderLoopAttachResult attachResult =
m_windowRenderLoop.Attach(m_renderer, m_windowRenderer);
if (!attachResult.interopWarning.empty()) {
LogRuntimeTrace(
"app",
attachResult.interopWarning);
}
m_editorContext.AttachTextMeasurer(m_renderer);
m_editorWorkspace.Initialize(repoRoot, m_renderer);
m_editorWorkspace.AttachViewportWindowRenderer(m_windowRenderer);
m_editorWorkspace.SetViewportSurfacePresentationEnabled(
attachResult.hasViewportSurfacePresentation);
if (!m_editorWorkspace.GetBuiltInIconError().empty()) {
LogRuntimeTrace("icons", m_editorWorkspace.GetBuiltInIconError());
}
LogRuntimeTrace(
"app",
"workspace initialized: " +
m_editorContext.DescribeWorkspaceState(m_editorWorkspace.GetShellInteractionState()));
m_renderReady = true;
ShowWindow(m_hwnd, nCmdShow);
UpdateWindow(m_hwnd);
m_autoScreenshot.Initialize(m_editorContext.GetShellAsset().captureRootPath);
if (IsAutoCaptureOnStartupEnabled()) {
m_autoScreenshot.RequestCapture("startup");
m_editorContext.SetStatus("Capture", "Startup capture requested.");
}
LogRuntimeTrace("app", "initialize completed");
return true;
}
void Application::Shutdown() {
LogRuntimeTrace("app", "shutdown begin");
m_renderReady = false;
if (GetCapture() == m_hwnd) {
ReleaseCapture();
}
m_autoScreenshot.Shutdown();
m_editorWorkspace.Shutdown();
m_windowRenderLoop.Detach();
m_windowRenderer.Shutdown();
m_renderer.Shutdown();
if (m_hwnd != nullptr && IsWindow(m_hwnd)) {
DestroyWindow(m_hwnd);
}
m_hwnd = nullptr;
if (m_windowClassAtom != 0 && m_hInstance != nullptr) {
UnregisterClassW(kWindowClassName, m_hInstance);
m_windowClassAtom = 0;
}
LogRuntimeTrace("app", "shutdown end");
ShutdownUIEditorRuntimeTrace();
}
void Application::RenderFrame() {
if (!m_renderReady || m_hwnd == nullptr) {
return;
}
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),
UIColor(0.10f, 0.10f, 0.10f, 1.0f));
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_editorWorkspace.GetShellInteractionState()));
}
const Host::D3D12WindowRenderLoopFrameContext frameContext =
m_windowRenderLoop.BeginFrame();
if (!frameContext.warning.empty()) {
LogRuntimeTrace("viewport", frameContext.warning);
}
m_editorWorkspace.Update(
m_editorContext,
workspaceBounds,
frameEvents,
BuildCaptureStatusText());
const UIEditorShellInteractionFrame& shellFrame =
m_editorWorkspace.GetShellFrame();
if (IsVerboseRuntimeTraceEnabled() &&
(!frameEvents.empty() ||
shellFrame.result.workspaceResult.dockHostResult.layoutChanged ||
shellFrame.result.workspaceResult.dockHostResult.commandExecuted)) {
std::ostringstream frameTrace = {};
frameTrace << "result consumed="
<< (shellFrame.result.consumed ? "true" : "false")
<< " layoutChanged="
<< (shellFrame.result.workspaceResult.dockHostResult.layoutChanged ? "true" : "false")
<< " commandExecuted="
<< (shellFrame.result.workspaceResult.dockHostResult.commandExecuted ? "true" : "false")
<< " active="
<< m_editorContext.GetWorkspaceController().GetWorkspace().activePanelId
<< " message="
<< shellFrame.result.workspaceResult.dockHostResult.layoutResult.message;
LogRuntimeTrace(
"frame",
frameTrace.str());
}
ApplyHostCaptureRequests(shellFrame.result);
for (const App::ProductEditorWorkspaceTraceEntry& entry : m_editorWorkspace.GetTraceEntries()) {
LogRuntimeTrace(entry.channel, entry.message);
}
ApplyHostedContentCaptureRequests();
ApplyCurrentCursor();
m_editorWorkspace.Append(drawList);
if (frameContext.canRenderViewports) {
m_editorWorkspace.RenderRequestedViewports(frameContext.renderContext);
}
} else {
drawList.AddText(
UIPoint(28.0f, 28.0f),
"Editor shell asset invalid.",
UIColor(0.92f, 0.92f, 0.92f, 1.0f),
16.0f);
drawList.AddText(
UIPoint(28.0f, 54.0f),
m_editorContext.GetValidationMessage().empty()
? std::string("Unknown validation error.")
: m_editorContext.GetValidationMessage(),
UIColor(0.72f, 0.72f, 0.72f, 1.0f),
12.0f);
}
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::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 (!IsBorderlessWindowEnabled() || 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 (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 (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 (m_hostRuntime.IsBorderlessWindowDragRestoreArmed()) {
ClearBorderlessWindowChromeDragRestoreState();
}
if (HitTestBorderlessWindowChrome(lParam) != Host::BorderlessWindowChromeHitTarget::DragRegion) {
return false;
}
ExecuteBorderlessWindowChromeAction(Host::BorderlessWindowChromeHitTarget::MaximizeRestoreButton);
return true;
}
bool Application::HandleBorderlessWindowChromeDragRestorePointerMove() {
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 (!IsBorderlessWindowEnabled()) {
return;
}
const Host::BorderlessWindowChromeLayout layout =
ResolveBorderlessWindowChromeLayout(clientWidthDips);
drawList.AddFilledRect(
layout.titleBarRect,
UIColor(0.97f, 0.97f, 0.97f, 1.0f));
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),
UIColor(0.78f, 0.78f, 0.78f, 1.0f),
1.0f);
drawList.AddText(
UIPoint(
12.0f,
layout.titleBarRect.y +
(std::max)(0.0f, (layout.titleBarRect.height - kBorderlessTitleBarFontSize) * 0.5f - 1.0f)),
kWindowTitleText,
UIColor(0.12f, 0.12f, 0.12f, 1.0f),
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)));
}
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) {
m_hostRuntime.ClearPredictedClientPixelSize();
if (IsBorderlessWindowEnabled() && m_hwnd != nullptr) {
Host::RefreshBorderlessWindowDwmDecorations(m_hwnd);
}
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 (!IsBorderlessWindowEnabled()) {
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)) {
return dispatcherResult;
}
Application* application = Host::WindowMessageDispatcher::GetApplicationFromWindow(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->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->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 &&
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) {
application->m_hwnd = nullptr;
}
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
int RunXCUIEditorApp(HINSTANCE hInstance, int nCmdShow) {
Application application;
return application.Run(hInstance, nCmdShow);
}
} // namespace XCEngine::UI::Editor