862 lines
29 KiB
C++
862 lines
29 KiB
C++
|
|
#include "Platform/Win32/EditorWindowChromeController.h"
|
||
|
|
|
||
|
|
#include "Platform/Win32/EditorWindow.h"
|
||
|
|
#include "Platform/Win32/EditorWindowConstants.h"
|
||
|
|
#include "Platform/Win32/EditorWindowInternalState.h"
|
||
|
|
#include "Platform/Win32/EditorWindowRuntimeController.h"
|
||
|
|
#include "Platform/Win32/EditorWindowStyle.h"
|
||
|
|
|
||
|
|
#include <XCEditor/Foundation/UIEditorTheme.h>
|
||
|
|
#include <XCEditor/Panels/UIEditorPanelRegistry.h>
|
||
|
|
#include <XCEngine/UI/DrawData.h>
|
||
|
|
#include <XCEngine/UI/Layout/UITabStripLayout.h>
|
||
|
|
|
||
|
|
#include <algorithm>
|
||
|
|
#include <cmath>
|
||
|
|
#include <string_view>
|
||
|
|
|
||
|
|
#include <windowsx.h>
|
||
|
|
|
||
|
|
namespace XCEngine::UI::Editor::App::Internal {
|
||
|
|
|
||
|
|
using namespace EditorWindowInternal;
|
||
|
|
using ::XCEngine::UI::Layout::MeasureUITabStripHeaderWidth;
|
||
|
|
using ::XCEngine::UI::UIColor;
|
||
|
|
using ::XCEngine::UI::UIDrawList;
|
||
|
|
using ::XCEngine::UI::UIPoint;
|
||
|
|
using ::XCEngine::UI::UIRect;
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
|
||
|
|
constexpr float kTitleBarLogoExtent = 16.0f;
|
||
|
|
constexpr float kTitleBarLogoInsetLeft = 8.0f;
|
||
|
|
constexpr float kTitleBarLogoTextGap = 8.0f;
|
||
|
|
|
||
|
|
std::string ResolveDetachedTitleTabText(const EditorWindow& window) {
|
||
|
|
const auto& workspaceController = window.GetWorkspaceController();
|
||
|
|
const std::string_view activePanelId = workspaceController.GetWorkspace().activePanelId;
|
||
|
|
if (!activePanelId.empty()) {
|
||
|
|
if (const UIEditorPanelDescriptor* descriptor =
|
||
|
|
FindUIEditorPanelDescriptor(
|
||
|
|
workspaceController.GetPanelRegistry(),
|
||
|
|
activePanelId);
|
||
|
|
descriptor != nullptr &&
|
||
|
|
!descriptor->defaultTitle.empty()) {
|
||
|
|
return descriptor->defaultTitle;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return std::string("Panel");
|
||
|
|
}
|
||
|
|
|
||
|
|
float ResolveDetachedTabWidth(std::string_view text) {
|
||
|
|
const Widgets::UIEditorTabStripMetrics& metrics = ResolveUIEditorTabStripMetrics();
|
||
|
|
Widgets::UIEditorTabStripItem item = {};
|
||
|
|
item.title = std::string(text);
|
||
|
|
const float desiredLabelWidth =
|
||
|
|
Widgets::ResolveUIEditorTabStripDesiredHeaderLabelWidth(item, metrics);
|
||
|
|
return MeasureUITabStripHeaderWidth(desiredLabelWidth, metrics.layoutMetrics);
|
||
|
|
}
|
||
|
|
|
||
|
|
bool IsRootPanelVisible(
|
||
|
|
const UIEditorWorkspaceController& controller,
|
||
|
|
std::string_view panelId) {
|
||
|
|
const UIEditorPanelSessionState* panelState =
|
||
|
|
FindUIEditorPanelSessionState(controller.GetSession(), panelId);
|
||
|
|
return panelState != nullptr && panelState->open && panelState->visible;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool HasSingleVisibleRootTab(const UIEditorWorkspaceController& controller) {
|
||
|
|
const UIEditorWorkspaceNode& root = controller.GetWorkspace().root;
|
||
|
|
if (root.kind != UIEditorWorkspaceNodeKind::TabStack) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::size_t visiblePanelCount = 0u;
|
||
|
|
for (const UIEditorWorkspaceNode& child : root.children) {
|
||
|
|
if (child.kind != UIEditorWorkspaceNodeKind::Panel ||
|
||
|
|
!IsRootPanelVisible(controller, child.panel.panelId)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
++visiblePanelCount;
|
||
|
|
if (visiblePanelCount > 1u) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return visiblePanelCount == 1u;
|
||
|
|
}
|
||
|
|
|
||
|
|
UIRect BuildDetachedTitleLogoRect(const Host::BorderlessWindowChromeLayout& layout) {
|
||
|
|
const float availableLeft = layout.titleBarRect.x;
|
||
|
|
const float availableRight = layout.minimizeButtonRect.x;
|
||
|
|
const float centeredX = std::floor(
|
||
|
|
layout.titleBarRect.x + layout.titleBarRect.width * 0.5f - kTitleBarLogoExtent * 0.5f);
|
||
|
|
const float clampedX = (std::max)(
|
||
|
|
availableLeft,
|
||
|
|
(std::min)(centeredX, availableRight - kTitleBarLogoExtent));
|
||
|
|
const float logoY =
|
||
|
|
layout.titleBarRect.y +
|
||
|
|
(std::max)(0.0f, (layout.titleBarRect.height - kTitleBarLogoExtent) * 0.5f);
|
||
|
|
return UIRect(clampedX, logoY, kTitleBarLogoExtent, kTitleBarLogoExtent);
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
void EditorWindowChromeController::Reset() {
|
||
|
|
m_chromeState = {};
|
||
|
|
m_runtimeState.Reset();
|
||
|
|
}
|
||
|
|
|
||
|
|
void EditorWindowChromeController::SetWindowDpi(UINT dpi) {
|
||
|
|
m_runtimeState.SetWindowDpi(dpi);
|
||
|
|
}
|
||
|
|
|
||
|
|
UINT EditorWindowChromeController::GetWindowDpi() const {
|
||
|
|
return m_runtimeState.GetWindowDpi();
|
||
|
|
}
|
||
|
|
|
||
|
|
float EditorWindowChromeController::GetDpiScale(float baseDpiScale) const {
|
||
|
|
return m_runtimeState.GetDpiScale(baseDpiScale);
|
||
|
|
}
|
||
|
|
|
||
|
|
void EditorWindowChromeController::BeginInteractiveResize() {
|
||
|
|
m_runtimeState.BeginInteractiveResize();
|
||
|
|
}
|
||
|
|
|
||
|
|
void EditorWindowChromeController::EndInteractiveResize() {
|
||
|
|
m_runtimeState.EndInteractiveResize();
|
||
|
|
}
|
||
|
|
|
||
|
|
void EditorWindowChromeController::BeginBorderlessResize(
|
||
|
|
Host::BorderlessWindowResizeEdge edge,
|
||
|
|
const POINT& initialScreenPoint,
|
||
|
|
const RECT& initialWindowRect) {
|
||
|
|
m_runtimeState.BeginBorderlessResize(edge, initialScreenPoint, initialWindowRect);
|
||
|
|
}
|
||
|
|
|
||
|
|
void EditorWindowChromeController::EndBorderlessResize() {
|
||
|
|
m_runtimeState.EndBorderlessResize();
|
||
|
|
}
|
||
|
|
|
||
|
|
bool EditorWindowChromeController::IsBorderlessResizeActive() const {
|
||
|
|
return m_runtimeState.IsBorderlessResizeActive();
|
||
|
|
}
|
||
|
|
|
||
|
|
Host::BorderlessWindowResizeEdge EditorWindowChromeController::GetBorderlessResizeEdge() const {
|
||
|
|
return m_runtimeState.GetBorderlessResizeEdge();
|
||
|
|
}
|
||
|
|
|
||
|
|
const POINT& EditorWindowChromeController::GetBorderlessResizeInitialScreenPoint() const {
|
||
|
|
return m_runtimeState.GetBorderlessResizeInitialScreenPoint();
|
||
|
|
}
|
||
|
|
|
||
|
|
const RECT& EditorWindowChromeController::GetBorderlessResizeInitialWindowRect() const {
|
||
|
|
return m_runtimeState.GetBorderlessResizeInitialWindowRect();
|
||
|
|
}
|
||
|
|
|
||
|
|
void EditorWindowChromeController::SetHoveredBorderlessResizeEdge(
|
||
|
|
Host::BorderlessWindowResizeEdge edge) {
|
||
|
|
m_runtimeState.SetHoveredBorderlessResizeEdge(edge);
|
||
|
|
}
|
||
|
|
|
||
|
|
Host::BorderlessWindowResizeEdge EditorWindowChromeController::GetHoveredBorderlessResizeEdge()
|
||
|
|
const {
|
||
|
|
return m_runtimeState.GetHoveredBorderlessResizeEdge();
|
||
|
|
}
|
||
|
|
|
||
|
|
void EditorWindowChromeController::SetPredictedClientPixelSize(UINT width, UINT height) {
|
||
|
|
m_runtimeState.SetPredictedClientPixelSize(width, height);
|
||
|
|
}
|
||
|
|
|
||
|
|
void EditorWindowChromeController::ClearPredictedClientPixelSize() {
|
||
|
|
m_runtimeState.ClearPredictedClientPixelSize();
|
||
|
|
}
|
||
|
|
|
||
|
|
bool EditorWindowChromeController::TryGetPredictedClientPixelSize(
|
||
|
|
UINT& outWidth,
|
||
|
|
UINT& outHeight) const {
|
||
|
|
return m_runtimeState.TryGetPredictedClientPixelSize(outWidth, outHeight);
|
||
|
|
}
|
||
|
|
|
||
|
|
void EditorWindowChromeController::SetBorderlessWindowMaximized(bool maximized) {
|
||
|
|
m_runtimeState.SetBorderlessWindowMaximized(maximized);
|
||
|
|
}
|
||
|
|
|
||
|
|
bool EditorWindowChromeController::IsBorderlessWindowMaximized() const {
|
||
|
|
return m_runtimeState.IsBorderlessWindowMaximized();
|
||
|
|
}
|
||
|
|
|
||
|
|
void EditorWindowChromeController::SetBorderlessWindowRestoreRect(const RECT& rect) {
|
||
|
|
m_runtimeState.SetBorderlessWindowRestoreRect(rect);
|
||
|
|
}
|
||
|
|
|
||
|
|
bool EditorWindowChromeController::TryGetBorderlessWindowRestoreRect(RECT& outRect) const {
|
||
|
|
return m_runtimeState.TryGetBorderlessWindowRestoreRect(outRect);
|
||
|
|
}
|
||
|
|
|
||
|
|
void EditorWindowChromeController::BeginBorderlessWindowDragRestore(
|
||
|
|
const POINT& initialScreenPoint) {
|
||
|
|
m_runtimeState.BeginBorderlessWindowDragRestore(initialScreenPoint);
|
||
|
|
}
|
||
|
|
|
||
|
|
void EditorWindowChromeController::EndBorderlessWindowDragRestore() {
|
||
|
|
m_runtimeState.EndBorderlessWindowDragRestore();
|
||
|
|
}
|
||
|
|
|
||
|
|
bool EditorWindowChromeController::IsBorderlessWindowDragRestoreArmed() const {
|
||
|
|
return m_runtimeState.IsBorderlessWindowDragRestoreArmed();
|
||
|
|
}
|
||
|
|
|
||
|
|
const POINT& EditorWindowChromeController::GetBorderlessWindowDragRestoreInitialScreenPoint()
|
||
|
|
const {
|
||
|
|
return m_runtimeState.GetBorderlessWindowDragRestoreInitialScreenPoint();
|
||
|
|
}
|
||
|
|
|
||
|
|
Host::BorderlessWindowChromeHitTarget EditorWindowChromeController::GetHoveredChromeTarget() const {
|
||
|
|
return m_chromeState.hoveredTarget;
|
||
|
|
}
|
||
|
|
|
||
|
|
void EditorWindowChromeController::SetHoveredChromeTarget(
|
||
|
|
Host::BorderlessWindowChromeHitTarget target) {
|
||
|
|
m_chromeState.hoveredTarget = target;
|
||
|
|
}
|
||
|
|
|
||
|
|
Host::BorderlessWindowChromeHitTarget EditorWindowChromeController::GetPressedChromeTarget() const {
|
||
|
|
return m_chromeState.pressedTarget;
|
||
|
|
}
|
||
|
|
|
||
|
|
void EditorWindowChromeController::SetPressedChromeTarget(
|
||
|
|
Host::BorderlessWindowChromeHitTarget target) {
|
||
|
|
m_chromeState.pressedTarget = target;
|
||
|
|
}
|
||
|
|
|
||
|
|
void EditorWindowChromeController::ResetChromeState() {
|
||
|
|
m_chromeState = {};
|
||
|
|
}
|
||
|
|
|
||
|
|
bool EditorWindowChromeController::IsChromeStateClear() const {
|
||
|
|
return m_chromeState.hoveredTarget == Host::BorderlessWindowChromeHitTarget::None &&
|
||
|
|
m_chromeState.pressedTarget == Host::BorderlessWindowChromeHitTarget::None;
|
||
|
|
}
|
||
|
|
|
||
|
|
const Host::BorderlessWindowChromeState& EditorWindowChromeController::GetChromeState() const {
|
||
|
|
return m_chromeState;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool EditorWindowChromeController::HandleSystemCommand(
|
||
|
|
EditorWindow& window,
|
||
|
|
EditorContext& editorContext,
|
||
|
|
bool globalTabDragActive,
|
||
|
|
WPARAM wParam) {
|
||
|
|
if (!window.IsBorderlessWindowEnabled()) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
switch (wParam & 0xFFF0u) {
|
||
|
|
case SC_MAXIMIZE:
|
||
|
|
ToggleMaximizeRestore(window, editorContext, globalTabDragActive);
|
||
|
|
return true;
|
||
|
|
case SC_RESTORE:
|
||
|
|
if (!IsIconic(window.m_state->window.hwnd)) {
|
||
|
|
ToggleMaximizeRestore(window, editorContext, globalTabDragActive);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
default:
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
bool EditorWindowChromeController::HandleGetMinMaxInfo(
|
||
|
|
const EditorWindow& window,
|
||
|
|
LPARAM lParam) const {
|
||
|
|
return Host::HandleBorderlessWindowGetMinMaxInfo(window.m_state->window.hwnd, lParam);
|
||
|
|
}
|
||
|
|
|
||
|
|
LRESULT EditorWindowChromeController::HandleNcCalcSize(
|
||
|
|
const EditorWindow& window,
|
||
|
|
WPARAM wParam,
|
||
|
|
LPARAM lParam) const {
|
||
|
|
return Host::HandleBorderlessWindowNcCalcSize(
|
||
|
|
window.m_state->window.hwnd,
|
||
|
|
wParam,
|
||
|
|
lParam,
|
||
|
|
GetWindowDpi());
|
||
|
|
}
|
||
|
|
|
||
|
|
bool EditorWindowChromeController::UpdateResizeHover(EditorWindow& window, LPARAM lParam) {
|
||
|
|
const Host::BorderlessWindowResizeEdge hoveredEdge = HitTestResizeEdge(window, lParam);
|
||
|
|
if (GetHoveredBorderlessResizeEdge() == hoveredEdge) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
SetHoveredBorderlessResizeEdge(hoveredEdge);
|
||
|
|
ApplyResizeCursorHoverPriority();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool EditorWindowChromeController::HandleResizeButtonDown(EditorWindow& window, LPARAM lParam) {
|
||
|
|
const Host::BorderlessWindowResizeEdge edge = HitTestResizeEdge(window, lParam);
|
||
|
|
if (edge == Host::BorderlessWindowResizeEdge::None ||
|
||
|
|
window.m_state->window.hwnd == nullptr) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
POINT screenPoint = {};
|
||
|
|
if (!GetCursorPos(&screenPoint)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
RECT windowRect = {};
|
||
|
|
if (!GetWindowRect(window.m_state->window.hwnd, &windowRect)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
BeginBorderlessResize(edge, screenPoint, windowRect);
|
||
|
|
window.AcquirePointerCapture(EditorWindowPointerCaptureOwner::BorderlessResize);
|
||
|
|
window.InvalidateHostWindow();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool EditorWindowChromeController::HandleResizeButtonUp(EditorWindow& window) {
|
||
|
|
if (!IsBorderlessResizeActive()) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
EndBorderlessResize();
|
||
|
|
window.ReleasePointerCapture(EditorWindowPointerCaptureOwner::BorderlessResize);
|
||
|
|
window.InvalidateHostWindow();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool EditorWindowChromeController::HandleResizePointerMove(
|
||
|
|
EditorWindow& window,
|
||
|
|
EditorContext& editorContext,
|
||
|
|
bool globalTabDragActive) {
|
||
|
|
if (!IsBorderlessResizeActive() ||
|
||
|
|
window.m_state->window.hwnd == nullptr) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
POINT currentScreenPoint = {};
|
||
|
|
if (!GetCursorPos(¤tScreenPoint)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
RECT targetRect = Host::ComputeBorderlessWindowResizeRect(
|
||
|
|
GetBorderlessResizeInitialWindowRect(),
|
||
|
|
GetBorderlessResizeInitialScreenPoint(),
|
||
|
|
currentScreenPoint,
|
||
|
|
GetBorderlessResizeEdge(),
|
||
|
|
640,
|
||
|
|
360);
|
||
|
|
const int width = targetRect.right - targetRect.left;
|
||
|
|
const int height = targetRect.bottom - targetRect.top;
|
||
|
|
if (width <= 0 || height <= 0) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
SetPredictedClientPixelSize(
|
||
|
|
static_cast<UINT>(width),
|
||
|
|
static_cast<UINT>(height));
|
||
|
|
window.ApplyWindowResize(static_cast<UINT>(width), static_cast<UINT>(height));
|
||
|
|
(void)window.RenderFrame(editorContext, globalTabDragActive);
|
||
|
|
|
||
|
|
SetWindowPos(
|
||
|
|
window.m_state->window.hwnd,
|
||
|
|
nullptr,
|
||
|
|
targetRect.left,
|
||
|
|
targetRect.top,
|
||
|
|
width,
|
||
|
|
height,
|
||
|
|
SWP_NOZORDER | SWP_NOACTIVATE);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
void EditorWindowChromeController::ClearResizeState(EditorWindow& window) {
|
||
|
|
if (IsBorderlessResizeActive()) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (GetHoveredBorderlessResizeEdge() == Host::BorderlessWindowResizeEdge::None) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
SetHoveredBorderlessResizeEdge(Host::BorderlessWindowResizeEdge::None);
|
||
|
|
window.InvalidateHostWindow();
|
||
|
|
}
|
||
|
|
|
||
|
|
void EditorWindowChromeController::ForceClearResizeState(EditorWindow& window) {
|
||
|
|
if (GetHoveredBorderlessResizeEdge() == Host::BorderlessWindowResizeEdge::None &&
|
||
|
|
!IsBorderlessResizeActive()) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
SetHoveredBorderlessResizeEdge(Host::BorderlessWindowResizeEdge::None);
|
||
|
|
EndBorderlessResize();
|
||
|
|
window.ReleasePointerCapture(EditorWindowPointerCaptureOwner::BorderlessResize);
|
||
|
|
window.InvalidateHostWindow();
|
||
|
|
}
|
||
|
|
|
||
|
|
Host::BorderlessWindowResizeEdge EditorWindowChromeController::HitTestResizeEdge(
|
||
|
|
const EditorWindow& window,
|
||
|
|
LPARAM lParam) const {
|
||
|
|
if (!window.IsBorderlessWindowEnabled() ||
|
||
|
|
window.m_state->window.hwnd == nullptr ||
|
||
|
|
window.IsBorderlessWindowMaximized()) {
|
||
|
|
return Host::BorderlessWindowResizeEdge::None;
|
||
|
|
}
|
||
|
|
|
||
|
|
RECT clientRect = {};
|
||
|
|
if (!GetClientRect(window.m_state->window.hwnd, &clientRect)) {
|
||
|
|
return Host::BorderlessWindowResizeEdge::None;
|
||
|
|
}
|
||
|
|
|
||
|
|
const float clientWidthDips =
|
||
|
|
window.PixelsToDips(static_cast<float>((std::max)(clientRect.right - clientRect.left, 1L)));
|
||
|
|
const float clientHeightDips =
|
||
|
|
window.PixelsToDips(static_cast<float>((std::max)(clientRect.bottom - clientRect.top, 1L)));
|
||
|
|
return Host::HitTestBorderlessWindowResizeEdge(
|
||
|
|
UIRect(0.0f, 0.0f, clientWidthDips, clientHeightDips),
|
||
|
|
window.ConvertClientPixelsToDips(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)));
|
||
|
|
}
|
||
|
|
|
||
|
|
bool EditorWindowChromeController::UpdateChromeHover(EditorWindow& window, LPARAM lParam) {
|
||
|
|
if (GetHoveredBorderlessResizeEdge() != Host::BorderlessWindowResizeEdge::None ||
|
||
|
|
IsBorderlessResizeActive()) {
|
||
|
|
const bool changed =
|
||
|
|
GetHoveredChromeTarget() != Host::BorderlessWindowChromeHitTarget::None;
|
||
|
|
SetHoveredChromeTarget(Host::BorderlessWindowChromeHitTarget::None);
|
||
|
|
return changed;
|
||
|
|
}
|
||
|
|
|
||
|
|
const Host::BorderlessWindowChromeHitTarget hitTarget = HitTestChrome(window, lParam);
|
||
|
|
const Host::BorderlessWindowChromeHitTarget buttonTarget =
|
||
|
|
hitTarget == Host::BorderlessWindowChromeHitTarget::MinimizeButton ||
|
||
|
|
hitTarget == Host::BorderlessWindowChromeHitTarget::MaximizeRestoreButton ||
|
||
|
|
hitTarget == Host::BorderlessWindowChromeHitTarget::CloseButton
|
||
|
|
? hitTarget
|
||
|
|
: Host::BorderlessWindowChromeHitTarget::None;
|
||
|
|
if (GetHoveredChromeTarget() == buttonTarget) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
SetHoveredChromeTarget(buttonTarget);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool EditorWindowChromeController::HandleChromeButtonDown(EditorWindow& window, LPARAM lParam) {
|
||
|
|
if (GetHoveredBorderlessResizeEdge() != Host::BorderlessWindowResizeEdge::None ||
|
||
|
|
IsBorderlessResizeActive()) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
const Host::BorderlessWindowChromeHitTarget hitTarget = HitTestChrome(window, lParam);
|
||
|
|
switch (hitTarget) {
|
||
|
|
case Host::BorderlessWindowChromeHitTarget::MinimizeButton:
|
||
|
|
case Host::BorderlessWindowChromeHitTarget::MaximizeRestoreButton:
|
||
|
|
case Host::BorderlessWindowChromeHitTarget::CloseButton:
|
||
|
|
SetPressedChromeTarget(hitTarget);
|
||
|
|
window.AcquirePointerCapture(EditorWindowPointerCaptureOwner::BorderlessChrome);
|
||
|
|
window.InvalidateHostWindow();
|
||
|
|
return true;
|
||
|
|
case Host::BorderlessWindowChromeHitTarget::DragRegion:
|
||
|
|
if (window.m_state->window.hwnd != nullptr) {
|
||
|
|
if (window.IsBorderlessWindowMaximized()) {
|
||
|
|
POINT screenPoint = {};
|
||
|
|
if (GetCursorPos(&screenPoint)) {
|
||
|
|
BeginBorderlessWindowDragRestore(screenPoint);
|
||
|
|
window.AcquirePointerCapture(EditorWindowPointerCaptureOwner::BorderlessChrome);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
window.ForceReleasePointerCapture();
|
||
|
|
SendMessageW(window.m_state->window.hwnd, WM_NCLBUTTONDOWN, HTCAPTION, 0);
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
case Host::BorderlessWindowChromeHitTarget::None:
|
||
|
|
default:
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
bool EditorWindowChromeController::HandleChromeButtonUp(
|
||
|
|
EditorWindow& window,
|
||
|
|
EditorContext& editorContext,
|
||
|
|
bool globalTabDragActive,
|
||
|
|
LPARAM lParam) {
|
||
|
|
if (IsBorderlessWindowDragRestoreArmed()) {
|
||
|
|
ClearChromeDragRestoreState(window);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
const Host::BorderlessWindowChromeHitTarget pressedTarget = GetPressedChromeTarget();
|
||
|
|
if (pressedTarget != Host::BorderlessWindowChromeHitTarget::MinimizeButton &&
|
||
|
|
pressedTarget != Host::BorderlessWindowChromeHitTarget::MaximizeRestoreButton &&
|
||
|
|
pressedTarget != Host::BorderlessWindowChromeHitTarget::CloseButton) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
const Host::BorderlessWindowChromeHitTarget releasedTarget = HitTestChrome(window, lParam);
|
||
|
|
SetPressedChromeTarget(Host::BorderlessWindowChromeHitTarget::None);
|
||
|
|
window.ReleasePointerCapture(EditorWindowPointerCaptureOwner::BorderlessChrome);
|
||
|
|
window.InvalidateHostWindow();
|
||
|
|
|
||
|
|
if (pressedTarget == releasedTarget) {
|
||
|
|
ExecuteChromeAction(window, editorContext, globalTabDragActive, pressedTarget);
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool EditorWindowChromeController::HandleChromeDoubleClick(
|
||
|
|
EditorWindow& window,
|
||
|
|
EditorContext& editorContext,
|
||
|
|
bool globalTabDragActive,
|
||
|
|
LPARAM lParam) {
|
||
|
|
if (IsBorderlessWindowDragRestoreArmed()) {
|
||
|
|
ClearChromeDragRestoreState(window);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (HitTestChrome(window, lParam) != Host::BorderlessWindowChromeHitTarget::DragRegion) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
ExecuteChromeAction(
|
||
|
|
window,
|
||
|
|
editorContext,
|
||
|
|
globalTabDragActive,
|
||
|
|
Host::BorderlessWindowChromeHitTarget::MaximizeRestoreButton);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool EditorWindowChromeController::HandleChromeDragRestorePointerMove(
|
||
|
|
EditorWindow& window,
|
||
|
|
EditorContext& editorContext,
|
||
|
|
bool globalTabDragActive) {
|
||
|
|
if (!IsBorderlessWindowDragRestoreArmed() ||
|
||
|
|
window.m_state->window.hwnd == nullptr) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
POINT currentScreenPoint = {};
|
||
|
|
if (!GetCursorPos(¤tScreenPoint)) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
const POINT initialScreenPoint = 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 (!TryGetBorderlessWindowRestoreRect(restoreRect) ||
|
||
|
|
!QueryCurrentWindowRect(window, currentRect) ||
|
||
|
|
!QueryBorderlessWindowWorkAreaRect(window, workAreaRect)) {
|
||
|
|
ClearChromeDragRestoreState(window);
|
||
|
|
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) {
|
||
|
|
ClearChromeDragRestoreState(window);
|
||
|
|
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 * window.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
|
||
|
|
};
|
||
|
|
|
||
|
|
SetBorderlessWindowMaximized(false);
|
||
|
|
ApplyPredictedWindowRectTransition(window, editorContext, globalTabDragActive, targetRect);
|
||
|
|
ClearChromeDragRestoreState(window);
|
||
|
|
SendMessageW(window.m_state->window.hwnd, WM_NCLBUTTONDOWN, HTCAPTION, 0);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
void EditorWindowChromeController::ClearChromeDragRestoreState(EditorWindow& window) {
|
||
|
|
if (!IsBorderlessWindowDragRestoreArmed()) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
EndBorderlessWindowDragRestore();
|
||
|
|
window.ReleasePointerCapture(EditorWindowPointerCaptureOwner::BorderlessChrome);
|
||
|
|
}
|
||
|
|
|
||
|
|
void EditorWindowChromeController::ClearChromeState(EditorWindow& window) {
|
||
|
|
if (IsChromeStateClear()) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
ResetChromeState();
|
||
|
|
window.InvalidateHostWindow();
|
||
|
|
}
|
||
|
|
|
||
|
|
Host::BorderlessWindowChromeHitTarget EditorWindowChromeController::HitTestChrome(
|
||
|
|
const EditorWindow& window,
|
||
|
|
LPARAM lParam) const {
|
||
|
|
if (!window.IsBorderlessWindowEnabled() || window.m_state->window.hwnd == nullptr) {
|
||
|
|
return Host::BorderlessWindowChromeHitTarget::None;
|
||
|
|
}
|
||
|
|
|
||
|
|
RECT clientRect = {};
|
||
|
|
if (!GetClientRect(window.m_state->window.hwnd, &clientRect)) {
|
||
|
|
return Host::BorderlessWindowChromeHitTarget::None;
|
||
|
|
}
|
||
|
|
|
||
|
|
const float clientWidthDips =
|
||
|
|
window.PixelsToDips(static_cast<float>((std::max)(clientRect.right - clientRect.left, 1L)));
|
||
|
|
const Host::BorderlessWindowChromeLayout layout =
|
||
|
|
ResolveChromeLayout(window, clientWidthDips);
|
||
|
|
return Host::HitTestBorderlessWindowChrome(
|
||
|
|
layout,
|
||
|
|
window.ConvertClientPixelsToDips(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)));
|
||
|
|
}
|
||
|
|
|
||
|
|
Host::BorderlessWindowChromeLayout EditorWindowChromeController::ResolveChromeLayout(
|
||
|
|
const EditorWindow& window,
|
||
|
|
float clientWidthDips) const {
|
||
|
|
float leadingOccupiedRight = 0.0f;
|
||
|
|
if (ShouldUseDetachedTitleBarTabStrip(window)) {
|
||
|
|
leadingOccupiedRight = ResolveDetachedTabWidth(ResolveDetachedTitleTabText(window));
|
||
|
|
}
|
||
|
|
|
||
|
|
return Host::BuildBorderlessWindowChromeLayout(
|
||
|
|
UIRect(0.0f, 0.0f, clientWidthDips, kBorderlessTitleBarHeightDips),
|
||
|
|
leadingOccupiedRight);
|
||
|
|
}
|
||
|
|
|
||
|
|
bool EditorWindowChromeController::ShouldUseDetachedTitleBarTabStrip(
|
||
|
|
const EditorWindow& window) const {
|
||
|
|
return !window.m_state->window.primary &&
|
||
|
|
HasSingleVisibleRootTab(window.m_runtime->GetWorkspaceController());
|
||
|
|
}
|
||
|
|
|
||
|
|
void EditorWindowChromeController::AppendChrome(
|
||
|
|
const EditorWindow& window,
|
||
|
|
UIDrawList& drawList,
|
||
|
|
float clientWidthDips) const {
|
||
|
|
if (!window.IsBorderlessWindowEnabled()) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const Host::BorderlessWindowChromeLayout layout =
|
||
|
|
ResolveChromeLayout(window, clientWidthDips);
|
||
|
|
const bool useDetachedTitleBarTabStrip = ShouldUseDetachedTitleBarTabStrip(window);
|
||
|
|
if (!useDetachedTitleBarTabStrip) {
|
||
|
|
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 (!window.m_state->window.primary) {
|
||
|
|
if (window.m_runtime->GetTitleBarLogoIcon().IsValid()) {
|
||
|
|
drawList.AddImage(
|
||
|
|
BuildDetachedTitleLogoRect(layout),
|
||
|
|
window.m_runtime->GetTitleBarLogoIcon(),
|
||
|
|
UIColor(1.0f, 1.0f, 1.0f, 1.0f));
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
const float iconX = layout.titleBarRect.x + kTitleBarLogoInsetLeft;
|
||
|
|
const float iconY =
|
||
|
|
layout.titleBarRect.y +
|
||
|
|
(std::max)(0.0f, (layout.titleBarRect.height - kTitleBarLogoExtent) * 0.5f);
|
||
|
|
if (window.m_runtime->GetTitleBarLogoIcon().IsValid()) {
|
||
|
|
drawList.AddImage(
|
||
|
|
UIRect(iconX, iconY, kTitleBarLogoExtent, kTitleBarLogoExtent),
|
||
|
|
window.m_runtime->GetTitleBarLogoIcon(),
|
||
|
|
UIColor(1.0f, 1.0f, 1.0f, 1.0f));
|
||
|
|
}
|
||
|
|
|
||
|
|
const std::string titleText =
|
||
|
|
window.m_state->window.titleText.empty()
|
||
|
|
? std::string("XCEngine Editor")
|
||
|
|
: window.m_state->window.titleText;
|
||
|
|
drawList.AddText(
|
||
|
|
UIPoint(
|
||
|
|
iconX +
|
||
|
|
(window.m_runtime->GetTitleBarLogoIcon().IsValid()
|
||
|
|
? (kTitleBarLogoExtent + kTitleBarLogoTextGap)
|
||
|
|
: 4.0f),
|
||
|
|
layout.titleBarRect.y +
|
||
|
|
(std::max)(
|
||
|
|
0.0f,
|
||
|
|
(layout.titleBarRect.height - kBorderlessTitleBarFontSize) * 0.5f - 1.0f)),
|
||
|
|
titleText,
|
||
|
|
kShellTextColor,
|
||
|
|
kBorderlessTitleBarFontSize);
|
||
|
|
}
|
||
|
|
|
||
|
|
Host::AppendBorderlessWindowChrome(
|
||
|
|
drawList,
|
||
|
|
layout,
|
||
|
|
GetChromeState(),
|
||
|
|
window.IsBorderlessWindowMaximized());
|
||
|
|
}
|
||
|
|
|
||
|
|
void EditorWindowChromeController::ApplyResizeCursorHoverPriority() {
|
||
|
|
if (GetHoveredBorderlessResizeEdge() != Host::BorderlessWindowResizeEdge::None ||
|
||
|
|
IsBorderlessResizeActive()) {
|
||
|
|
SetHoveredChromeTarget(Host::BorderlessWindowChromeHitTarget::None);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
bool EditorWindowChromeController::QueryCurrentWindowRect(
|
||
|
|
const EditorWindow& window,
|
||
|
|
RECT& outRect) const {
|
||
|
|
outRect = {};
|
||
|
|
return window.m_state->window.hwnd != nullptr &&
|
||
|
|
GetWindowRect(window.m_state->window.hwnd, &outRect) != FALSE;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool EditorWindowChromeController::QueryBorderlessWindowWorkAreaRect(
|
||
|
|
const EditorWindow& window,
|
||
|
|
RECT& outRect) const {
|
||
|
|
outRect = {};
|
||
|
|
if (window.m_state->window.hwnd == nullptr) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
const HMONITOR monitor =
|
||
|
|
MonitorFromWindow(window.m_state->window.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 EditorWindowChromeController::ApplyPredictedWindowRectTransition(
|
||
|
|
EditorWindow& window,
|
||
|
|
EditorContext& editorContext,
|
||
|
|
bool globalTabDragActive,
|
||
|
|
const RECT& targetRect) {
|
||
|
|
if (window.m_state->window.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;
|
||
|
|
}
|
||
|
|
|
||
|
|
SetPredictedClientPixelSize(static_cast<UINT>(width), static_cast<UINT>(height));
|
||
|
|
window.ApplyWindowResize(static_cast<UINT>(width), static_cast<UINT>(height));
|
||
|
|
(void)window.RenderFrame(editorContext, globalTabDragActive);
|
||
|
|
SetWindowPos(
|
||
|
|
window.m_state->window.hwnd,
|
||
|
|
nullptr,
|
||
|
|
targetRect.left,
|
||
|
|
targetRect.top,
|
||
|
|
width,
|
||
|
|
height,
|
||
|
|
SWP_NOZORDER | SWP_NOACTIVATE);
|
||
|
|
window.InvalidateHostWindow();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
void EditorWindowChromeController::ToggleMaximizeRestore(
|
||
|
|
EditorWindow& window,
|
||
|
|
EditorContext& editorContext,
|
||
|
|
bool globalTabDragActive) {
|
||
|
|
if (window.m_state->window.hwnd == nullptr) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!window.IsBorderlessWindowMaximized()) {
|
||
|
|
RECT currentRect = {};
|
||
|
|
RECT workAreaRect = {};
|
||
|
|
if (!QueryCurrentWindowRect(window, currentRect) ||
|
||
|
|
!QueryBorderlessWindowWorkAreaRect(window, workAreaRect)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
SetBorderlessWindowRestoreRect(currentRect);
|
||
|
|
SetBorderlessWindowMaximized(true);
|
||
|
|
ApplyPredictedWindowRectTransition(window, editorContext, globalTabDragActive, workAreaRect);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
RECT restoreRect = {};
|
||
|
|
if (!TryGetBorderlessWindowRestoreRect(restoreRect)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
SetBorderlessWindowMaximized(false);
|
||
|
|
ApplyPredictedWindowRectTransition(window, editorContext, globalTabDragActive, restoreRect);
|
||
|
|
}
|
||
|
|
|
||
|
|
void EditorWindowChromeController::ExecuteChromeAction(
|
||
|
|
EditorWindow& window,
|
||
|
|
EditorContext& editorContext,
|
||
|
|
bool globalTabDragActive,
|
||
|
|
Host::BorderlessWindowChromeHitTarget target) {
|
||
|
|
if (window.m_state->window.hwnd == nullptr) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
switch (target) {
|
||
|
|
case Host::BorderlessWindowChromeHitTarget::MinimizeButton:
|
||
|
|
ShowWindow(window.m_state->window.hwnd, SW_MINIMIZE);
|
||
|
|
break;
|
||
|
|
case Host::BorderlessWindowChromeHitTarget::MaximizeRestoreButton:
|
||
|
|
ToggleMaximizeRestore(window, editorContext, globalTabDragActive);
|
||
|
|
break;
|
||
|
|
case Host::BorderlessWindowChromeHitTarget::CloseButton:
|
||
|
|
PostMessageW(window.m_state->window.hwnd, WM_CLOSE, 0, 0);
|
||
|
|
break;
|
||
|
|
case Host::BorderlessWindowChromeHitTarget::DragRegion:
|
||
|
|
case Host::BorderlessWindowChromeHitTarget::None:
|
||
|
|
default:
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
window.InvalidateHostWindow();
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace XCEngine::UI::Editor::App::Internal
|