3027 lines
105 KiB
C++
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, ¤tRect)) {
|
|
return;
|
|
}
|
|
|
|
const LONG width = currentRect.right - currentRect.left;
|
|
const LONG height = currentRect.bottom - currentRect.top;
|
|
const LONG left = screenPoint.x - m_globalTabDragSession.windowDragOffset.x;
|
|
const LONG top = screenPoint.y - m_globalTabDragSession.windowDragOffset.y;
|
|
SetWindowPos(
|
|
windowState.hwnd,
|
|
nullptr,
|
|
left,
|
|
top,
|
|
width,
|
|
height,
|
|
SWP_NOZORDER | SWP_NOACTIVATE);
|
|
}
|
|
|
|
bool Application::TryStartGlobalTabDrag(ManagedWindowState& sourceWindowState) {
|
|
if (!sourceWindowState.pendingGlobalTabDragStart ||
|
|
sourceWindowState.pendingGlobalTabDragNodeId.empty() ||
|
|
sourceWindowState.pendingGlobalTabDragPanelId.empty()) {
|
|
return false;
|
|
}
|
|
|
|
const std::string sourceNodeId = sourceWindowState.pendingGlobalTabDragNodeId;
|
|
const std::string panelId = sourceWindowState.pendingGlobalTabDragPanelId;
|
|
const POINT screenPoint = sourceWindowState.pendingGlobalTabDragScreenPoint;
|
|
const POINT windowDragOffset = sourceWindowState.pendingGlobalTabDragWindowOffset;
|
|
sourceWindowState.pendingGlobalTabDragStart = false;
|
|
sourceWindowState.pendingGlobalTabDragNodeId.clear();
|
|
sourceWindowState.pendingGlobalTabDragPanelId.clear();
|
|
sourceWindowState.pendingGlobalTabDragScreenPoint = {};
|
|
sourceWindowState.pendingGlobalTabDragWindowOffset = {};
|
|
|
|
if (sourceWindowState.primary) {
|
|
UIEditorWindowWorkspaceController windowWorkspaceController(
|
|
m_editorContext.GetShellAsset().panelRegistry,
|
|
BuildWindowWorkspaceSet(sourceWindowState.windowId));
|
|
const UIEditorWindowWorkspaceOperationResult result =
|
|
windowWorkspaceController.DetachPanelToNewWindow(
|
|
sourceWindowState.windowId,
|
|
sourceNodeId,
|
|
panelId);
|
|
if (result.status != UIEditorWindowWorkspaceOperationStatus::Changed) {
|
|
LogRuntimeTrace(
|
|
"drag",
|
|
"failed to start global tab drag from primary window: " + result.message);
|
|
return false;
|
|
}
|
|
|
|
if (!SynchronizeManagedWindowsFromWindowSet(
|
|
windowWorkspaceController.GetWindowSet(),
|
|
result.targetWindowId,
|
|
screenPoint)) {
|
|
LogRuntimeTrace("drag", "failed to synchronize detached drag window state");
|
|
return false;
|
|
}
|
|
|
|
ManagedWindowState* detachedWindowState = FindWindowState(result.targetWindowId);
|
|
if (detachedWindowState == nullptr || detachedWindowState->hwnd == nullptr) {
|
|
LogRuntimeTrace("drag", "detached drag window was not created.");
|
|
return false;
|
|
}
|
|
|
|
BeginGlobalTabDragSession(
|
|
detachedWindowState->windowId,
|
|
detachedWindowState->workspaceController.GetWorkspace().root.nodeId,
|
|
panelId,
|
|
screenPoint,
|
|
windowDragOffset);
|
|
MoveGlobalTabDragWindow(*detachedWindowState, screenPoint);
|
|
SetCapture(detachedWindowState->hwnd);
|
|
SetForegroundWindow(detachedWindowState->hwnd);
|
|
LogRuntimeTrace(
|
|
"drag",
|
|
"started global tab drag by detaching panel '" + panelId +
|
|
"' into window '" + detachedWindowState->windowId + "'");
|
|
return true;
|
|
}
|
|
|
|
ResetManagedWindowInteractionState(sourceWindowState);
|
|
BeginGlobalTabDragSession(
|
|
sourceWindowState.windowId,
|
|
sourceNodeId,
|
|
panelId,
|
|
screenPoint,
|
|
windowDragOffset);
|
|
MoveGlobalTabDragWindow(sourceWindowState, screenPoint);
|
|
if (sourceWindowState.hwnd != nullptr) {
|
|
SetCapture(sourceWindowState.hwnd);
|
|
}
|
|
LogRuntimeTrace(
|
|
"drag",
|
|
"started global tab drag from detached window '" + sourceWindowState.windowId +
|
|
"' panel '" + panelId + "'");
|
|
return true;
|
|
}
|
|
|
|
void Application::ProcessPendingGlobalTabDragStarts() {
|
|
if (m_globalTabDragSession.active) {
|
|
return;
|
|
}
|
|
|
|
for (const std::unique_ptr<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(¤tScreenPoint)) {
|
|
return false;
|
|
}
|
|
|
|
RECT targetRect = Host::ComputeBorderlessWindowResizeRect(
|
|
m_hostRuntime.GetBorderlessResizeInitialWindowRect(),
|
|
m_hostRuntime.GetBorderlessResizeInitialScreenPoint(),
|
|
currentScreenPoint,
|
|
m_hostRuntime.GetBorderlessResizeEdge(),
|
|
640,
|
|
360);
|
|
const int width = targetRect.right - targetRect.left;
|
|
const int height = targetRect.bottom - targetRect.top;
|
|
if (width <= 0 || height <= 0) {
|
|
return true;
|
|
}
|
|
|
|
m_hostRuntime.SetPredictedClientPixelSize(
|
|
static_cast<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(¤tScreenPoint)) {
|
|
return true;
|
|
}
|
|
|
|
const POINT initialScreenPoint = m_hostRuntime.GetBorderlessWindowDragRestoreInitialScreenPoint();
|
|
const int dragThresholdX = (std::max)(GetSystemMetrics(SM_CXDRAG), 1);
|
|
const int dragThresholdY = (std::max)(GetSystemMetrics(SM_CYDRAG), 1);
|
|
const LONG deltaX = currentScreenPoint.x - initialScreenPoint.x;
|
|
const LONG deltaY = currentScreenPoint.y - initialScreenPoint.y;
|
|
if (std::abs(deltaX) < dragThresholdX && std::abs(deltaY) < dragThresholdY) {
|
|
return true;
|
|
}
|
|
|
|
RECT restoreRect = {};
|
|
RECT currentRect = {};
|
|
RECT workAreaRect = {};
|
|
if (!m_hostRuntime.TryGetBorderlessWindowRestoreRect(restoreRect) ||
|
|
!QueryCurrentWindowRect(currentRect) ||
|
|
!QueryBorderlessWindowWorkAreaRect(workAreaRect)) {
|
|
ClearBorderlessWindowChromeDragRestoreState();
|
|
return true;
|
|
}
|
|
|
|
const int restoreWidth = restoreRect.right - restoreRect.left;
|
|
const int restoreHeight = restoreRect.bottom - restoreRect.top;
|
|
const int currentWidth = currentRect.right - currentRect.left;
|
|
if (restoreWidth <= 0 || restoreHeight <= 0 || currentWidth <= 0) {
|
|
ClearBorderlessWindowChromeDragRestoreState();
|
|
return true;
|
|
}
|
|
|
|
const float pointerRatio =
|
|
static_cast<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
|