#include "Platform/Win32/EditorWindowChromeController.h" #include "Platform/Win32/EditorWindow.h" #include "Platform/Win32/EditorWindowRuntimeController.h" #include "Platform/Win32/EditorWindowState.h" #include "Platform/Win32/EditorWindowSupport.h" #include #include #include #include #include #include #include #include namespace XCEngine::UI::Editor::App { using namespace EditorWindowSupport; 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; constexpr float kTitleBarFrameStatsInsetRight = 12.0f; bool IsRootPanelVisible( const UIEditorWorkspaceController& controller, std::string_view panelId) { const UIEditorPanelSessionState* panelState = FindUIEditorPanelSessionState(controller.GetSession(), panelId); return panelState != nullptr && panelState->open && panelState->visible; } 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"); } const UIEditorPanelDescriptor* ResolveSingleVisibleRootPanelDescriptor( const EditorWindow& window) { const UIEditorWorkspaceController& workspaceController = window.GetWorkspaceController(); const UIEditorWorkspaceNode& root = workspaceController.GetWorkspace().root; if (root.kind != UIEditorWorkspaceNodeKind::TabStack) { return nullptr; } const UIEditorPanelRegistry& panelRegistry = workspaceController.GetPanelRegistry(); const UIEditorPanelDescriptor* visibleDescriptor = nullptr; std::size_t visibleCount = 0u; for (const UIEditorWorkspaceNode& child : root.children) { if (child.kind != UIEditorWorkspaceNodeKind::Panel || !IsRootPanelVisible(workspaceController, child.panel.panelId)) { continue; } const UIEditorPanelDescriptor* descriptor = FindUIEditorPanelDescriptor(panelRegistry, child.panel.panelId); if (descriptor == nullptr) { return nullptr; } ++visibleCount; visibleDescriptor = descriptor; if (visibleCount > 1u) { return nullptr; } } return visibleCount == 1u ? visibleDescriptor : nullptr; } bool IsToolWindow(const EditorWindow& window) { if (window.IsPrimary()) { return false; } const UIEditorPanelDescriptor* descriptor = ResolveSingleVisibleRootPanelDescriptor(window); return descriptor != nullptr && descriptor->toolWindow; } ::XCEngine::UI::UISize ResolveMinimumOuterSize(const EditorWindow& window) { if (const UIEditorPanelDescriptor* descriptor = ResolveSingleVisibleRootPanelDescriptor(window); descriptor != nullptr && descriptor->minimumDetachedWindowSize.width > 0.0f && descriptor->minimumDetachedWindowSize.height > 0.0f) { return descriptor->minimumDetachedWindowSize; } return ::XCEngine::UI::UISize(640.0f, 360.0f); } 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 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 { const ::XCEngine::UI::UISize minimumOuterSize = ResolveMinimumOuterSize(window); return Host::HandleBorderlessWindowGetMinMaxInfo( window.m_state->window.hwnd, lParam, static_cast(minimumOuterSize.width), static_cast(minimumOuterSize.height)); } 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; } const ::XCEngine::UI::UISize minimumOuterSize = ResolveMinimumOuterSize(window); RECT targetRect = Host::ComputeBorderlessWindowResizeRect( GetBorderlessResizeInitialWindowRect(), GetBorderlessResizeInitialScreenPoint(), currentScreenPoint, GetBorderlessResizeEdge(), static_cast(minimumOuterSize.width), static_cast(minimumOuterSize.height)); const int width = targetRect.right - targetRect.left; const int height = targetRect.bottom - targetRect.top; if (width <= 0 || height <= 0) { return true; } SetPredictedClientPixelSize( static_cast(width), static_cast(height)); window.ApplyWindowResize(static_cast(width), static_cast(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((std::max)(clientRect.right - clientRect.left, 1L))); const float clientHeightDips = window.PixelsToDips(static_cast((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(currentScreenPoint.x - currentRect.left) / static_cast(currentWidth); const float clampedPointerRatio = (std::clamp)(pointerRatio, 0.0f, 1.0f); const int newLeft = (std::clamp)( currentScreenPoint.x - static_cast(clampedPointerRatio * static_cast(restoreWidth)), workAreaRect.left, workAreaRect.right - restoreWidth); const int titleBarHeightPixels = static_cast(kBorderlessTitleBarHeightDips * 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((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 && !IsToolWindow(window) && 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 (useDetachedTitleBarTabStrip) { 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)); } 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)), IsToolWindow(window) ? ResolveDetachedTitleTabText(window) : (window.m_state->window.titleText.empty() ? std::string("XCEngine Editor") : window.m_state->window.titleText), kShellTextColor, kBorderlessTitleBarFontSize); } } 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; const std::string frameRateText = window.m_runtime->BuildFrameRateText(); 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); if (!frameRateText.empty()) { const float frameRateTextWidth = window.m_runtime->GetRenderer().MeasureTextWidth( UIEditorTextMeasureRequest{ frameRateText, kBorderlessTitleBarFontSize }); const float frameRateX = layout.dragRect.x + layout.dragRect.width - kTitleBarFrameStatsInsetRight - frameRateTextWidth; drawList.AddText( UIPoint( (std::max)(frameRateX, layout.dragRect.x + kTitleBarLogoInsetLeft), layout.titleBarRect.y + (std::max)( 0.0f, (layout.titleBarRect.height - kBorderlessTitleBarFontSize) * 0.5f - 1.0f)), frameRateText, kShellMutedTextColor, 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(width), static_cast(height)); window.ApplyWindowResize(static_cast(width), static_cast(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