#include "Platform/Win32/Windowing/EditorWindowMessageDispatcher.h" #include "Platform/Win32/Chrome/BorderlessWindowChrome.h" #include "Platform/Win32/Chrome/EditorWindowChromeController.h" #include "Platform/Win32/Runtime/EditorWindowInputController.h" #include "Platform/Win32/Windowing/EditorWindow.h" #include "Platform/Win32/Runtime/EditorWindowFrameDriver.h" #include "Platform/Win32/Runtime/EditorWindowRuntimeController.h" #include "Platform/Win32/Windowing/EditorWindowPointerCapture.h" #include "Platform/Win32/Windowing/EditorWindowHostRuntime.h" #include "Platform/Win32/Windowing/EditorWindowLifecycleCoordinator.h" #include "Platform/Win32/Windowing/EditorUtilityWindowCoordinator.h" #include "Platform/Win32/Windowing/EditorWindowWorkspaceCoordinator.h" #include #include #include namespace XCEngine::UI::Editor::App { namespace { constexpr UINT kMessageNcUaDrawCaption = 0x00AEu; constexpr UINT kMessageNcUaDrawFrame = 0x00AFu; std::string DescribeHwnd(HWND hwnd) { std::ostringstream stream = {}; stream << "0x" << std::hex << std::uppercase << reinterpret_cast(hwnd); return stream.str(); } std::string DescribeHostWindows(const EditorWindowHostRuntime& hostRuntime) { std::ostringstream stream = {}; const auto& windows = hostRuntime.GetWindows(); stream << "count=" << windows.size() << " ["; bool first = true; for (const std::unique_ptr& window : windows) { if (!first) { stream << ", "; } first = false; if (window == nullptr) { stream << ""; continue; } stream << window->GetWindowId() << "{hwnd=" << DescribeHwnd(window->GetHwnd()) << ",primary=" << (window->IsPrimary() ? '1' : '0') << ",state=" << GetEditorWindowLifecycleStateName(window->GetLifecycleState()) << '}'; } stream << ']'; return stream.str(); } } // namespace struct EditorWindowMessageDispatcher::DispatchContext { HWND hwnd = nullptr; EditorWindowHostRuntime& hostRuntime; EditorWindowLifecycleCoordinator& lifecycleCoordinator; EditorUtilityWindowCoordinator& utilityCoordinator; EditorWindowWorkspaceCoordinator& workspaceCoordinator; EditorWindow& window; }; void EditorWindowMessageDispatcher::DispatchWindowFrameTransferRequests( const DispatchContext& context, const EditorWindowFrameTransferRequests& transferRequests) { context.workspaceCoordinator.HandleWindowFrameTransferRequests( context.window, transferRequests); context.utilityCoordinator.HandleWindowFrameTransferRequests( context.window, transferRequests); } void EditorWindowMessageDispatcher::FinalizeImmediateFrame( const DispatchContext& context, const EditorWindowFrameTransferRequests& transferRequests) { context.workspaceCoordinator.RefreshWindowPresentation(context.window); if (!transferRequests.HasPendingRequests()) { return; } DispatchWindowFrameTransferRequests(context, transferRequests); } void EditorWindowMessageDispatcher::FlushQueuedCompletedImmediateFrame( const DispatchContext& context) { if (!context.window.HasQueuedCompletedImmediateFrame()) { return; } FinalizeImmediateFrame( context, context.window.ConsumeQueuedCompletedImmediateFrameTransferRequests()); } void EditorWindowMessageDispatcher::RenderAndHandleWindowFrame(const DispatchContext& context) { FinalizeImmediateFrame( context, EditorWindowFrameDriver::DriveImmediateFrame( context.window, context.hostRuntime.GetEditorContext(), context.workspaceCoordinator.IsGlobalTabDragActive())); } bool EditorWindowMessageDispatcher::EnsureTrackingMouseLeave(const DispatchContext& context) { if (context.window.m_inputController->IsTrackingMouseLeave()) { return true; } TRACKMOUSEEVENT trackMouseEvent = {}; trackMouseEvent.cbSize = sizeof(trackMouseEvent); trackMouseEvent.dwFlags = TME_LEAVE; trackMouseEvent.hwndTrack = context.hwnd; if (!TrackMouseEvent(&trackMouseEvent)) { return false; } context.window.m_inputController->SetTrackingMouseLeave(true); return true; } bool EditorWindowMessageDispatcher::TryHandleChromeHoverConsumption( const DispatchContext& context, LPARAM lParam, LRESULT& outResult) { EditorWindowInputController& inputController = *context.window.m_inputController; EditorWindowChromeController& chromeController = *context.window.m_chromeController; const EditorWindowInputFeedbackBinding* inputFeedbackBinding = context.window.m_runtime->TryGetInputFeedbackBinding(); if (!CanConsumeEditorWindowChromeHover( inputController.GetPointerCaptureOwner(), inputFeedbackBinding != nullptr && inputFeedbackBinding->HasShellInteractiveCapture(), inputFeedbackBinding != nullptr && inputFeedbackBinding->HasHostedContentCapture())) { return false; } const bool resizeHoverChanged = chromeController.UpdateResizeHover(context.window, lParam); if (chromeController.UpdateChromeHover(context.window, lParam)) { context.window.InvalidateHostWindow(); } if (resizeHoverChanged) { context.window.InvalidateHostWindow(); } EnsureTrackingMouseLeave(context); if (chromeController.GetHoveredBorderlessResizeEdge() != Host::BorderlessWindowResizeEdge::None) { outResult = 0; return true; } const Host::BorderlessWindowChromeHitTarget chromeHitTarget = chromeController.HitTestChrome(context.window, lParam); if (chromeHitTarget == Host::BorderlessWindowChromeHitTarget::PinButton || chromeHitTarget == Host::BorderlessWindowChromeHitTarget::MinimizeButton || chromeHitTarget == Host::BorderlessWindowChromeHitTarget::MaximizeRestoreButton || chromeHitTarget == Host::BorderlessWindowChromeHitTarget::CloseButton) { outResult = 0; return true; } return false; } bool EditorWindowMessageDispatcher::TryDispatchWindowPointerMessage( const DispatchContext& context, UINT message, WPARAM wParam, LPARAM lParam, LRESULT& outResult) { EditorWindowInputController& inputController = *context.window.m_inputController; EditorWindowChromeController& chromeController = *context.window.m_chromeController; switch (message) { case WM_MOUSEMOVE: if (CanRouteEditorWindowGlobalTabDragPointerMessages( inputController.GetPointerCaptureOwner(), context.workspaceCoordinator.OwnsActiveGlobalTabDrag(context.window.GetWindowId())) && context.workspaceCoordinator.HandleGlobalTabDragPointerMove(context.hwnd)) { outResult = 0; return true; } if (CanRouteEditorWindowBorderlessResizePointerMessages( inputController.GetPointerCaptureOwner()) && chromeController.HandleResizePointerMove( context.window, context.hostRuntime.GetEditorContext(), context.workspaceCoordinator.IsGlobalTabDragActive())) { outResult = 0; return true; } if (CanRouteEditorWindowBorderlessChromePointerMessages( inputController.GetPointerCaptureOwner()) && chromeController.HandleChromeDragRestorePointerMove( context.window, context.hostRuntime.GetEditorContext(), context.workspaceCoordinator.IsGlobalTabDragActive())) { outResult = 0; return true; } if (TryHandleChromeHoverConsumption(context, lParam, outResult)) { return true; } context.window.QueuePointerEvent( ::XCEngine::UI::UIInputEventType::PointerMove, ::XCEngine::UI::UIPointerButton::None, wParam, lParam); outResult = 0; return true; case WM_MOUSELEAVE: inputController.SetTrackingMouseLeave(false); chromeController.ClearResizeState(context.window); chromeController.ClearChromeDragRestoreState(context.window); chromeController.ClearChromeState(context.window); context.window.QueuePointerLeaveEvent(); outResult = 0; return true; case WM_LBUTTONDOWN: if (context.workspaceCoordinator.OwnsActiveGlobalTabDrag(context.window.GetWindowId())) { context.workspaceCoordinator.EndGlobalTabDragSession(); } if (chromeController.HandleResizeButtonDown(context.window, lParam)) { outResult = 0; return true; } if (chromeController.HandleChromeButtonDown(context.window, lParam)) { outResult = 0; return true; } SetFocus(context.hwnd); context.window.TryStartImmediateShellPointerCapture(lParam); context.window.QueuePointerEvent( ::XCEngine::UI::UIInputEventType::PointerButtonDown, ::XCEngine::UI::UIPointerButton::Left, wParam, lParam, true); outResult = 0; return true; case WM_RBUTTONDOWN: SetFocus(context.hwnd); context.window.TryStartImmediateShellPointerCapture(lParam); context.window.QueuePointerEvent( ::XCEngine::UI::UIInputEventType::PointerButtonDown, ::XCEngine::UI::UIPointerButton::Right, wParam, lParam, true); outResult = 0; return true; case WM_MBUTTONDOWN: SetFocus(context.hwnd); context.window.TryStartImmediateShellPointerCapture(lParam); context.window.QueuePointerEvent( ::XCEngine::UI::UIInputEventType::PointerButtonDown, ::XCEngine::UI::UIPointerButton::Middle, wParam, lParam, true); outResult = 0; return true; case WM_LBUTTONUP: if (CanRouteEditorWindowGlobalTabDragPointerMessages( inputController.GetPointerCaptureOwner(), context.workspaceCoordinator.OwnsActiveGlobalTabDrag(context.window.GetWindowId())) && context.workspaceCoordinator.HandleGlobalTabDragPointerButtonUp(context.hwnd)) { outResult = 0; return true; } if (CanRouteEditorWindowBorderlessResizePointerMessages( inputController.GetPointerCaptureOwner()) && chromeController.HandleResizeButtonUp(context.window)) { outResult = 0; return true; } if (CanRouteEditorWindowBorderlessChromePointerMessages( inputController.GetPointerCaptureOwner()) && chromeController.HandleChromeButtonUp( context.window, context.hostRuntime.GetEditorContext(), context.workspaceCoordinator.IsGlobalTabDragActive(), lParam)) { outResult = 0; return true; } context.window.QueuePointerEvent( ::XCEngine::UI::UIInputEventType::PointerButtonUp, ::XCEngine::UI::UIPointerButton::Left, wParam, lParam); outResult = 0; return true; case WM_RBUTTONUP: context.window.QueuePointerEvent( ::XCEngine::UI::UIInputEventType::PointerButtonUp, ::XCEngine::UI::UIPointerButton::Right, wParam, lParam); outResult = 0; return true; case WM_MBUTTONUP: context.window.QueuePointerEvent( ::XCEngine::UI::UIInputEventType::PointerButtonUp, ::XCEngine::UI::UIPointerButton::Middle, wParam, lParam); outResult = 0; return true; case WM_LBUTTONDBLCLK: if (chromeController.HandleChromeDoubleClick( context.window, context.hostRuntime.GetEditorContext(), context.workspaceCoordinator.IsGlobalTabDragActive(), lParam)) { outResult = 0; return true; } SetFocus(context.hwnd); context.window.TryStartImmediateShellPointerCapture(lParam); context.window.QueuePointerEvent( ::XCEngine::UI::UIInputEventType::PointerButtonDown, ::XCEngine::UI::UIPointerButton::Left, wParam, lParam); outResult = 0; return true; case WM_RBUTTONDBLCLK: SetFocus(context.hwnd); context.window.TryStartImmediateShellPointerCapture(lParam); context.window.QueuePointerEvent( ::XCEngine::UI::UIInputEventType::PointerButtonDown, ::XCEngine::UI::UIPointerButton::Right, wParam, lParam); outResult = 0; return true; case WM_MBUTTONDBLCLK: SetFocus(context.hwnd); context.window.TryStartImmediateShellPointerCapture(lParam); context.window.QueuePointerEvent( ::XCEngine::UI::UIInputEventType::PointerButtonDown, ::XCEngine::UI::UIPointerButton::Middle, wParam, lParam); outResult = 0; return true; case WM_MOUSEWHEEL: context.window.QueuePointerWheelEvent(GET_WHEEL_DELTA_WPARAM(wParam), wParam, lParam); outResult = 0; return true; default: return false; } } bool EditorWindowMessageDispatcher::TryDispatchWindowInputMessage( const DispatchContext& context, UINT message, WPARAM wParam, LPARAM lParam, LRESULT& outResult) { EditorWindowInputController& inputController = *context.window.m_inputController; EditorWindowChromeController& chromeController = *context.window.m_chromeController; if (TryDispatchWindowPointerMessage(context, message, wParam, lParam, outResult)) { return true; } switch (message) { case WM_SETFOCUS: inputController.SyncInputModifiersFromSystemState(); inputController.QueueWindowFocusEvent(::XCEngine::UI::UIInputEventType::FocusGained); outResult = 0; return true; case WM_KILLFOCUS: inputController.ResetInputModifiers(); inputController.QueueWindowFocusEvent(::XCEngine::UI::UIInputEventType::FocusLost); outResult = 0; return true; case WM_CAPTURECHANGED: if (reinterpret_cast(lParam) != context.hwnd) { inputController.ClearPointerCaptureOwner(); } if (context.workspaceCoordinator.OwnsActiveGlobalTabDrag(context.window.GetWindowId()) && reinterpret_cast(lParam) != context.hwnd) { context.workspaceCoordinator.EndGlobalTabDragSession(); outResult = 0; return true; } if (reinterpret_cast(lParam) != context.hwnd && context.window.HasInteractiveCaptureState()) { inputController.QueueWindowFocusEvent(::XCEngine::UI::UIInputEventType::FocusLost); chromeController.ForceClearResizeState(context.window); chromeController.ClearChromeDragRestoreState(context.window); chromeController.ClearChromeState(context.window); outResult = 0; return true; } if (reinterpret_cast(lParam) != context.hwnd) { chromeController.ForceClearResizeState(context.window); chromeController.ClearChromeDragRestoreState(context.window); chromeController.ClearChromeState(context.window); } return false; case WM_KEYDOWN: case WM_SYSKEYDOWN: if (wParam == VK_F12) { context.window.m_runtime->RequestManualScreenshot("manual_f12"); } inputController.QueueKeyEvent(::XCEngine::UI::UIInputEventType::KeyDown, wParam, lParam); outResult = 0; return true; case WM_KEYUP: case WM_SYSKEYUP: inputController.QueueKeyEvent(::XCEngine::UI::UIInputEventType::KeyUp, wParam, lParam); outResult = 0; return true; case WM_CHAR: inputController.QueueCharacterEvent(wParam); outResult = 0; return true; default: return false; } } bool EditorWindowMessageDispatcher::TryDispatchWindowLifecycleMessage( const DispatchContext& context, UINT message, WPARAM wParam, LPARAM lParam, LRESULT& outResult) { switch (message) { case WM_DPICHANGED: if (lParam == 0) { return false; } context.window.OnDpiChanged( static_cast(LOWORD(wParam)), *reinterpret_cast(lParam)); RenderAndHandleWindowFrame(context); outResult = 0; return true; case WM_ENTERSIZEMOVE: context.window.OnEnterSizeMove(); outResult = 0; return true; case WM_EXITSIZEMOVE: if (context.window.OnExitSizeMove()) { RenderAndHandleWindowFrame(context); } outResult = 0; return true; case WM_SIZE: if (wParam != SIZE_MINIMIZED) { const bool requiresImmediateFrame = context.window.OnResize( static_cast(LOWORD(lParam)), static_cast(HIWORD(lParam))); if (requiresImmediateFrame) { RenderAndHandleWindowFrame(context); } } outResult = 0; return true; case WM_CLOSE: context.lifecycleCoordinator.ExecuteCloseRequest(context.window); outResult = 0; return true; case WM_PAINT: FinalizeImmediateFrame( context, context.window.OnPaintMessage( context.hostRuntime.GetEditorContext(), context.workspaceCoordinator.IsGlobalTabDragActive())); outResult = 0; return true; case WM_ERASEBKGND: outResult = 1; return true; case WM_DESTROY: context.lifecycleCoordinator.HandleNativeWindowDestroyed(context.window); outResult = 0; return true; default: return false; } } bool EditorWindowMessageDispatcher::TryDispatchWindowChromeMessage( const DispatchContext& context, UINT message, WPARAM wParam, LPARAM lParam, LRESULT& outResult) { EditorWindowChromeController& chromeController = *context.window.m_chromeController; switch (message) { case WM_GETMINMAXINFO: if (chromeController.HandleGetMinMaxInfo(context.window, lParam)) { outResult = 0; return true; } return false; case WM_NCCALCSIZE: outResult = chromeController.HandleNcCalcSize(context.window, wParam, lParam); return true; case WM_NCACTIVATE: outResult = TRUE; return true; case WM_NCHITTEST: outResult = HTCLIENT; return true; case WM_NCPAINT: case kMessageNcUaDrawCaption: case kMessageNcUaDrawFrame: outResult = 0; return true; case WM_SYSCOMMAND: if (chromeController.HandleSystemCommand( context.window, context.hostRuntime.GetEditorContext(), context.workspaceCoordinator.IsGlobalTabDragActive(), wParam)) { outResult = 0; return true; } return false; case WM_SETCURSOR: if (LOWORD(lParam) == HTCLIENT && context.window.ApplyCurrentCursor()) { outResult = TRUE; return true; } return false; default: return false; } } bool EditorWindowMessageDispatcher::TryDispatch( HWND hwnd, EditorWindowHostRuntime& hostRuntime, EditorWindowLifecycleCoordinator& lifecycleCoordinator, EditorUtilityWindowCoordinator& utilityCoordinator, EditorWindowWorkspaceCoordinator& workspaceCoordinator, EditorWindow& window, UINT message, WPARAM wParam, LPARAM lParam, LRESULT& outResult) { const DispatchContext context = { .hwnd = hwnd, .hostRuntime = hostRuntime, .lifecycleCoordinator = lifecycleCoordinator, .utilityCoordinator = utilityCoordinator, .workspaceCoordinator = workspaceCoordinator, .window = window, }; bool handled = false; if (TryDispatchWindowChromeMessage(context, message, wParam, lParam, outResult)) { handled = true; } else if (TryDispatchWindowLifecycleMessage(context, message, wParam, lParam, outResult)) { handled = true; } else if (TryDispatchWindowInputMessage(context, message, wParam, lParam, outResult)) { handled = true; } if (handled) { FlushQueuedCompletedImmediateFrame(context); } return handled; } } // namespace XCEngine::UI::Editor::App