UE4 SlateUI事件机制
最近开发过程中,碰到一个比较奇怪的Bug,同事在场景中创建了个3D UI,使用的是WidgetComponent组件,然后动态设置widget实例,第一次创建的3D UI可以正常接收到鼠标事件,通过3D UI进入战斗场景后,第二场战斗的3D UI界面没法相应事件了,然后我就接住这口锅了。
wbp_path = '/Game/test_3d_ui.test_3d_ui'
# game.ui: 全局ui管理器
# create_3d_ui:加载WidgetBlueprint,并打开
widget = game.ui.create_3d_ui(wbp_path)
widget_comp.set_widget(widget)
PyObject *py_ue_set_widget(ue_PyUObject * self, PyObject * args)
{
ue_py_check(self);
PyObject *widget;
if (!PyArg_ParseTuple(args, "O", &widget))
return nullptr;
UWidgetComponent *widget_component = ue_py_check_type<UWidgetComponent>(self);
if (!widget_component)
return PyErr_Format(PyExc_Exception, "uobject is not a UWidgetComponent");
UUserWidget *uwidget = ue_py_check_type<UUserWidget>(widget);
if (!uwidget)
return PyErr_Format(PyExc_Exception, "argument2 is not a APlayerController");
widget_component->SetWidget(uwidget);
Py_RETURN_NONE;
}
然后开始看UE4源码,研究下UE4 SlateUI事件机制
按钮事件调用栈
下图是从Launch.cpp里里的GEngineLoop Tick调用Windows平台处理事件的代码,最终进入Button代码,响应OnClicked回调的调用栈。
事件来源
在Windows平台上,鼠标点击,键盘事件都是调用Windows的API,从Windows事件列表中获取的。
/**
* Ticks the engine loop
* Engine\Source\Runtime\Launch\Private\Launch.cpp
*/
void EngineTick( void )
{
//** line:62 **//
GEngineLoop.Tick();
}
/**
* Engine\Source\Runtime\ApplicationCore\Private\Windows\WindowsPlatformApplicationMisc.cpp
* windows 消息处理
*/
static void WinPumpMessages()
{
{
MSG Msg;
while( PeekMessage(&Msg,NULL,0,0,PM_REMOVE) )
{
TranslateMessage( &Msg );
//* line:108 *//
DispatchMessage( &Msg );
}
}
}
/**
* Engine\Source\Runtime\ApplicationCore\Private\Windows\WindowsApplication.cpp
*/
int32 FWindowsApplication::ProcessMessage( HWND hwnd, uint32 msg, WPARAM wParam, LPARAM lParam )
{
TSharedPtr< FWindowsWindow > CurrentNativeEventWindowPtr = FindWindowByHWND( Windows, hwnd );
if( Windows.Num() && CurrentNativeEventWindowPtr.IsValid() )
{
// .....
switch(msg)
{
case WM_KEYDOWN:
case WM_SYSKEYUP:
case WM_KEYUP:
case WM_LBUTTONDBLCLK:
case WM_LBUTTONDOWN:
case WM_MBUTTONDBLCLK:
case WM_MBUTTONDOWN:
case WM_RBUTTONDBLCLK:
case WM_RBUTTONDOWN:
case WM_XBUTTONDBLCLK:
case WM_XBUTTONDOWN:
case WM_XBUTTONUP:
case WM_LBUTTONUP:
case WM_MBUTTONUP:
case WM_RBUTTONUP:
case WM_NCMOUSEMOVE:
case WM_MOUSEMOVE:
case WM_MOUSEWHEEL:
#if WINVER >= 0x0601
case WM_TOUCH:
#endif
{
//** line:1042 **//
DeferMessage( CurrentNativeEventWindowPtr, hwnd, msg, wParam, lParam );
// Handled
return 0;
}
break;
}
}
}
/**
* Engine\Source\Runtime\ApplicationCore\Private\Windows\WindowsApplication.cpp
*/
int32 FWindowsApplication::ProcessDeferredMessage( const FDeferredWindowsMessage& DeferredMessage )
{
if ( Windows.Num() && DeferredMessage.NativeWindow.IsValid() )
{
HWND hwnd = DeferredMessage.hWND;
uint32 msg = DeferredMessage.Message;
WPARAM wParam = DeferredMessage.wParam;
LPARAM lParam = DeferredMessage.lParam;
switch(msg)
{
case WM_LBUTTONDBLCLK:
case WM_LBUTTONDOWN:
case WM_MBUTTONDBLCLK:
case WM_MBUTTONDOWN:
case WM_RBUTTONDBLCLK:
case WM_RBUTTONDOWN:
case WM_XBUTTONDBLCLK:
case WM_XBUTTONDOWN:
case WM_LBUTTONUP:
case WM_MBUTTONUP:
case WM_RBUTTONUP:
case WM_XBUTTONUP:
{
POINT CursorPoint;
CursorPoint.x = GET_X_LPARAM(lParam);
CursorPoint.y = GET_Y_LPARAM(lParam);
ClientToScreen(hwnd, &CursorPoint);
const FVector2D CursorPos(CursorPoint.x, CursorPoint.y);
EMouseButtons::Type MouseButton = EMouseButtons::Invalid;
bool bDoubleClick = false;
bool bMouseUp = false;
switch(msg)
{
case WM_LBUTTONDBLCLK:
bDoubleClick = true;
MouseButton = EMouseButtons::Left;
break;
case WM_LBUTTONUP:
bMouseUp = true;
MouseButton = EMouseButtons::Left;
break;
case WM_LBUTTONDOWN:
MouseButton = EMouseButtons::Left;
break;
// ...
default:
check(0);
}
if (bMouseUp)
{
//** line:2183 **//
return MessageHandler->OnMouseUp( MouseButton, CursorPos ) ? 0 : 1;
}
else if (bDoubleClick)
{
MessageHandler->OnMouseDoubleClick( CurrentNativeEventWindowPtr, MouseButton, CursorPos );
}
else
{
MessageHandler->OnMouseDown( CurrentNativeEventWindowPtr, MouseButton, CursorPos );
}
return 0;
}
break;
}
}
}
}
随后代码进入SlateApplication中,对事件进行封装,然后开始找到响应的Widget,调用对应的响应函数,并最终响应事件。
/* ==================================
前面都是从Windows事件队列获取消息
并对消息进行处理,后面开始进入最难
的地方了
==================================
*/
/**
* Engine\Source\Runtime\Slate\Private\Framework\Application\SlateApplication.cpp
*/
bool FSlateApplication::OnMouseUp( const EMouseButtons::Type Button, const FVector2D CursorPos )
{
// convert left mouse click to touch event if we are faking it
if (IsFakingTouchEvents() && Button == EMouseButtons::Left)
{
bIsFakingTouched = false;
//** line:5305 **//
return OnTouchEnded(PlatformApplication->Cursor->GetPosition(), 0, 0);
}
FKey Key = TranslateMouseButtonToKey( Button );
FPointerEvent MouseEvent(
GetUserIndexForMouse(),
CursorPointerIndex,
CursorPos,
GetLastCursorPos(),
PressedMouseButtons,
Key,
0,
PlatformApplication->GetModifierKeys()
);
return ProcessMouseButtonUpEvent( MouseEvent );
}
/**
* Engine\Source\Runtime\Slate\Private\Framework\Application\SlateApplication.cpp
*/
bool FSlateApplication::OnTouchEnded( const FVector2D& Location, int32 TouchIndex, int32 ControllerId )
{
TSharedRef<FSlateUser> User = GetOrCreateUser(ControllerId);
if (User->IsTouchPointerActive(TouchIndex))
{
FPointerEvent PointerEvent(
ControllerId,
TouchIndex,
Location,
Location,
0.0f,
true);
//** line:5912 **//
ProcessTouchEndedEvent(PointerEvent);
#if WITH_SLATE_DEBUGGING
ensure(!User->IsTouchPointerActive(TouchIndex));
#endif
return true;
}
return false;
}
/**
* Engine\Source\Runtime\Slate\Private\Framework\Application\SlateApplication.cpp
*/
bool FSlateApplication::ProcessMouseButtonUpEvent( const FPointerEvent& MouseEvent )
{
// ...
// An empty widget path is passed in. As an optimization, one will be generated only if a captured mouse event isn't routed
FWidgetPath EmptyPath;
//** line:5356 **//
const bool bHandled = RoutePointerUpEvent( EmptyPath, MouseEvent ).IsEventHandled();
if ( bIsCursorUser && PressedMouseButtons.Num() == 0 )
{
PlatformApplication->SetCapture( nullptr );
}
return bHandled;
}
/**
* Engine\Source\Runtime\Slate\Private\Framework\Application\SlateApplication.cpp
*/
FReply FSlateApplication::RoutePointerUpEvent(const FWidgetPath& WidgetsUnderPointer, const FPointerEvent& PointerEvent)
{
TScopeCounter<int32> BeginInput(ProcessingInput);
FReply Reply = FReply::Unhandled();
TSharedRef<FSlateUser> SlateUser = GetOrCreateUser(PointerEvent);
TSharedPtr<FDragDropOperation> LocalDragDropContent;
if (SlateUser->HasCapture(PointerEvent.GetPointerIndex()))
{
FWidgetPath MouseCaptorPath = SlateUser->GetCaptorPath(PointerEvent.GetPointerIndex(),
FWeakWidgetPath::EInterruptedPathHandling::Truncate, &PointerEvent);
if ( ensureMsgf(MouseCaptorPath.Widgets.Num() > 0, TEXT("A window had a widget with mouse capture.
That entire window has been dismissed before the mouse up could be processed.")) )
{
// Switch worlds widgets in the current path
FScopedSwitchWorldHack SwitchWorld( MouseCaptorPath );
//** line:4815 **//
Reply =
FEventRouter::Route<FReply>( this, FEventRouter::FToLeafmostPolicy(MouseCaptorPath), PointerEvent,
[this]( const FArrangedWidget& TargetWidget, const FPointerEvent& Event )
{
FReply TempReply = FReply::Unhandled();
if (Event.IsTouchEvent())
{
TempReply = TargetWidget.Widget->OnTouchEnded(TargetWidget.Geometry, Event);
}
if (!Event.IsTouchEvent() || (!TempReply.IsEventHandled() && this->bTouchFallbackToMouse))
{
TempReply = TargetWidget.Widget->OnMouseButtonUp( TargetWidget.Geometry, Event );
}
if ( Event.IsTouchEvent() && !IsFakingTouchEvents() )
{
// Generate a Leave event when a touch ends as well, since a
// touch can enter a widget and then end inside it
TargetWidget.Widget->OnMouseLeave(Event);
}
return TempReply;
}, ESlateDebuggingInputEvent::MouseButtonUp);
}
}
else
{
if (!LocalWidgetsUnderPointer.IsValid())
{
// 更新屏幕坐标区域中的widget
LocalWidgetsUnderPointer = LocateWindowUnderMouse(PointerEvent.GetScreenSpacePosition(),
GetInteractiveTopLevelWindows(), false, SlateUser->GetUserIndex());
}
}
}
/**
* Route an event based on the Routing Policy.
* Engine\Source\Runtime\Slate\Private\Framework\Application\SlateApplication.cpp
*/
template< typename ReplyType, typename RoutingPolicyType, typename EventType, typename FuncType >
static ReplyType Route( FSlateApplication* ThisApplication, RoutingPolicyType RoutingPolicy, EventType EventCopy,
const FuncType& Lambda, ESlateDebuggingInputEvent DebuggingInputEvent)
{
ReplyType Reply = ReplyType::Unhandled();
const FWidgetPath& RoutingPath = RoutingPolicy.GetRoutingPath();
const FWidgetPath* WidgetsUnderCursor = RoutingPolicy.GetWidgetsUnderCursor();
EventCopy.SetEventPath( RoutingPath );
for (; !Reply.IsEventHandled() && RoutingPolicy.ShouldKeepGoing(); RoutingPolicy.Next())
{
const FWidgetAndPointer& ArrangedWidget = RoutingPolicy.GetWidget();
#if PLATFORM_COMPILER_HAS_IF_CONSTEXPR
if constexpr (Translate<EventType>::TranslationNeeded())
{
const EventType TranslatedEvent = Translate<EventType>::PointerEvent(ArrangedWidget.PointerPosition, EventCopy);
//** line:378 **//
Reply = Lambda(ArrangedWidget, TranslatedEvent).SetHandler(ArrangedWidget.Widget);
ProcessReply(ThisApplication, RoutingPath, Reply, WidgetsUnderCursor, &TranslatedEvent);
}
else
{
Reply = Lambda(ArrangedWidget, EventCopy).SetHandler(ArrangedWidget.Widget);
ProcessReply(ThisApplication, RoutingPath, Reply, WidgetsUnderCursor, &EventCopy);
}
#else
const EventType TranslatedEvent = Translate<EventType>::PointerEvent(ArrangedWidget.PointerPosition, EventCopy);
Reply = Lambda(ArrangedWidget, TranslatedEvent).SetHandler(ArrangedWidget.Widget);
ProcessReply(ThisApplication, RoutingPath, Reply, WidgetsUnderCursor, &TranslatedEvent);
#endif
}
return Reply;
}
/**
* Engine\Source\Runtime\Slate\Private\Framework\Application\SlateApplication.cpp
*/
FReply FSlateApplication::RoutePointerUpEvent(const FWidgetPath& WidgetsUnderPointer, const FPointerEvent& PointerEvent)
{
// ...
// Switch worlds widgets in the current path
FScopedSwitchWorldHack SwitchWorld( MouseCaptorPath );
//** line:4815 **//
Reply = FEventRouter::Route<FReply>( this, FEventRouter::FToLeafmostPolicy(MouseCaptorPath), PointerEvent,
[this]( const FArrangedWidget& TargetWidget, const FPointerEvent& Event )
{
FReply TempReply = FReply::Unhandled();
if (Event.IsTouchEvent())
{
TempReply = TargetWidget.Widget->OnTouchEnded(TargetWidget.Geometry, Event);
}
if (!Event.IsTouchEvent() || (!TempReply.IsEventHandled() && this->bTouchFallbackToMouse))
{
//** line:4829 **//
TempReply = TargetWidget.Widget->OnMouseButtonUp( TargetWidget.Geometry, Event );
}
if ( Event.IsTouchEvent() && !IsFakingTouchEvents() )
{
// Generate a Leave event when a touch ends as well, since a
// touch can enter a widget and then end inside it
TargetWidget.Widget->OnMouseLeave(Event);
}
return TempReply;
}, ESlateDebuggingInputEvent::MouseButtonUp);
}
/**
* Engine\Source\Runtime\Slate\Private\Widgets\Input\SButton.cpp
*/
FReply SButton::OnMouseButtonUp( const FGeometry& MyGeometry, const FPointerEvent& MouseEvent )
{
FReply Reply = FReply::Unhandled();
// ...
//** line:304 **//
Reply = ExecuteOnClick();
}
/**
* Engine\Source\Runtime\Slate\Private\Widgets\Input\SButton.cpp
*/
FReply SButton::ExecuteOnClick()
{
if (OnClicked.IsBound())
{
//** line:385 **//
FReply Reply = OnClicked.Execute();
return Reply;
}
else
{
return FReply::Handled();
}
}
/**
* Engine\Source\Runtime\UMG\Private\Components\Button.cpp
*/
FReply UButton::SlateHandleClicked()
{
//** line:203 **//
OnClicked.Broadcast();
return FReply::Handled();
}
获取响应控件
UE4中,为了方便获取鼠标响应控件,会将屏幕区域划分成一个一个区域,然后按照区域,将控件划分到对应的区域中管理,一个控件可能会被划分到多个区域中。例如:1920 * 1080 分辨率会被划分成 15 * 9 个Cell。详细代码参见如下:
/**
* Engine\Source\Runtime\SlateCore\Private\Input\HittestGrid.cpp
*/
// 屏幕分区大小
const FVector2D CellSize(128.0f, 128.0f);
// 计算屏幕分区个数
bool FHittestGrid::SetHittestArea(const FVector2D& HittestPositionInDesktop, const FVector2D& HittestDimensions,
const FVector2D& HitestOffsetInWindow)
{
bool bWasCleared = false;
// If the size of the hit test area changes we need to clear it out
if (GridSize != HittestDimensions)
{
GridSize = HittestDimensions;
NumCells = FIntPoint(FMath::CeilToInt(GridSize.X / CellSize.X), FMath::CeilToInt(GridSize.Y / CellSize.Y));
const int32 NewTotalCells = NumCells.X * NumCells.Y;
ClearInternal(NewTotalCells);
bWasCleared = true;
}
GridOrigin = HittestPositionInDesktop;
GridWindowOrigin = HitestOffsetInWindow;
return bWasCleared;
}
// 通过屏幕坐标获取对应分割区Cell坐标
FIntPoint FHittestGrid::GetCellCoordinate(FVector2D Position) const
{
return FIntPoint(
FMath::Min(FMath::Max(FMath::FloorToInt(Position.X / CellSize.X), 0), NumCells.X - 1),
FMath::Min(FMath::Max(FMath::FloorToInt(Position.Y / CellSize.Y), 0), NumCells.Y - 1));
}
HittestGrid 每帧都会刷新,刷新堆栈如下:
每帧从SWindow根节点开始绘制,调用SetHittestArea函数,刷新HittestGrid:
int32 SWindow::PaintWindow( double CurrentTime, float DeltaTime, FSlateWindowElementList& OutDrawElements,
const FWidgetStyle& InWidgetStyle, bool bParentEnabled )
{
// 更新HittestArea屏幕大小
const bool HittestCleared = HittestGrid->SetHittestArea(GetPositionInScreen(), GetViewportSize());
FPaintArgs PaintArgs(nullptr, GetHittestGrid(), GetPositionInScreen(), CurrentTime, DeltaTime);
FSlateInvalidationContext Context(OutDrawElements, InWidgetStyle);
Context.bParentEnabled = bParentEnabled;
Context.PaintArgs = &PaintArgs;
// 开始绘制窗口界面
FSlateInvalidationResult Result = PaintInvalidationRoot(Context);
}
根节点开始Paint后,会以深度优先方式遍历所有子节点,并调用子节点的Paint函数
/**
* Engine\Source\Runtime\SlateCore\Private\Widgets\SWidget.cpp
*/
int32 SWidget::Paint(const FPaintArgs& Args, const FGeometry& AllottedGeometry,
const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements,
int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
// ...
OutDrawElements.PushPaintingWidget(*this, LayerId, PersistentState.CachedElementHandle);
if (bOutgoingHittestability)
{
//** line:1344 **//
Args.GetHittestGrid().AddWidget(MutableThis, 0, LayerId, FastPathProxyHandle.GetIndex());
}
// ...
// Paint the geometry of this widget.
int32 NewLayerId = OnPaint(UpdatedArgs, AllottedGeometry, CullingBounds, OutDrawElements, LayerId,
ContentWidgetStyle, bParentEnabled);
}
然后再调用FHittestGrid::AddWidget函数,对每个Widget进行区域划分,将Widget加入对应的Cell中。
void FHittestGrid::AddWidget(const TSharedRef<SWidget>& InWidget, int32 InBatchPriorityGroup,
int32 InLayerId, int32 InSecondarySort)
{
// Widget不可见,直接返回
if (!InWidget->GetVisibility().IsHitTestVisible())
{
return;
}
FGeometry GridSpaceGeometry = InWidget->GetPaintSpaceGeometry();
GridSpaceGeometry.AppendTransform(FSlateLayoutTransform(-GridWindowOrigin));
const FSlateRect BoundingRect = GridSpaceGeometry.GetRenderBoundingRect();
// 获取Widget最左上角跟最右下角的Cell Index
// 后面循环将Widget加入到对应的Cell区域
const FIntPoint UpperLeftCell = GetCellCoordinate(BoundingRect.GetTopLeft());
const FIntPoint LowerRightCell = GetCellCoordinate(BoundingRect.GetBottomRight());
if (bAddWidget)
{
int32& WidgetIndex = WidgetMap.Add(&*InWidget);
for (int32 XIndex = UpperLeftCell.X; XIndex <= LowerRightCell.X; ++XIndex)
{
for (int32 YIndex = UpperLeftCell.Y; YIndex <= LowerRightCell.Y; ++YIndex)
{
if (IsValidCellCoord(XIndex, YIndex))
{
CellAt(XIndex, YIndex).AddIndex(WidgetIndex);
}
}
}
}
}
TODO:
FWidgetPath FSlateApplication::LocateWidgetInWindow(FVector2D ScreenspaceMouseCoordinate, const TSharedRef<SWindow>& Window, bool bIgnoreEnabledStatus, int32 UserIndex) const
{
const bool bAcceptsInput = Window->IsVisible() && (Window->AcceptsInput() || IsWindowHousingInteractiveTooltip(Window));
if (bAcceptsInput && Window->IsScreenspaceMouseWithin(ScreenspaceMouseCoordinate))
{
TArray<FWidgetAndPointer> WidgetsAndCursors = Window->GetHittestGrid().GetBubblePath(ScreenspaceMouseCoordinate, GetCursorRadius(), bIgnoreEnabledStatus, UserIndex);
return FWidgetPath(MoveTemp(WidgetsAndCursors));
}
else
{
return FWidgetPath();
}
}
TArray<FWidgetAndPointer> FHittestGrid::GetBubblePath(FVector2D DesktopSpaceCoordinate, float CursorRadius, bool bIgnoreEnabledStatus, int32 UserIndex)
{
checkSlow(IsInGameThread());
const FVector2D CursorPositionInGrid = DesktopSpaceCoordinate - GridOrigin;
if (WidgetArray.Num() > 0 && Cells.Num() > 0)
{
FGridTestingParams TestingParams;
TestingParams.CursorPositionInGrid = CursorPositionInGrid;
TestingParams.CellCoord = GetCellCoordinate(CursorPositionInGrid);
TestingParams.Radius = 0.0f;
TestingParams.bTestWidgetIsInteractive = false;
// First add the exact point test results
const FIndexAndDistance BestHit = GetHitIndexFromCellIndex(TestingParams);
if (BestHit.IsValid())
{
const FWidgetData& BestHitWidgetData = BestHit.GetWidgetData();
const TSharedPtr<SWidget> FirstHitWidget = BestHitWidgetData.GetWidget();
// Make Sure we landed on a valid widget
if (FirstHitWidget.IsValid() && IsCompatibleUserIndex(UserIndex, BestHitWidgetData.UserIndex))
{
TArray<FWidgetAndPointer> Path;
TSharedPtr<SWidget> CurWidget = FirstHitWidget;
while (CurWidget.IsValid())
{
FGeometry DesktopSpaceGeometry = CurWidget->GetPaintSpaceGeometry();
DesktopSpaceGeometry.AppendTransform(FSlateLayoutTransform(GridOrigin - GridWindowOrigin));
Path.Emplace(FArrangedWidget(CurWidget.ToSharedRef(), DesktopSpaceGeometry), TSharedPtr<FVirtualPointerPosition>());
CurWidget = CurWidget->Advanced_GetPaintParentWidget();
}
if (!Path.Last().Widget->Advanced_IsWindow())
{
return TArray<FWidgetAndPointer>();
}
Algo::Reverse(Path);
bool bRemovedDisabledWidgets = false;
if (!bIgnoreEnabledStatus)
{
// @todo It might be more correct to remove all disabled widgets and non-hit testable widgets. It doesn't make sense to have a hit test invisible widget as a leaf in the path
// and that can happen if we remove a disabled widget. Furthermore if we did this we could then append custom paths in all cases since the leaf most widget would be hit testable
// For backwards compatibility changing this could be risky
const int32 DisabledWidgetIndex = Path.IndexOfByPredicate([](const FArrangedWidget& SomeWidget) { return !SomeWidget.Widget->IsEnabled(); });
if (DisabledWidgetIndex != INDEX_NONE)
{
bRemovedDisabledWidgets = true;
Path.RemoveAt(DisabledWidgetIndex, Path.Num() - DisabledWidgetIndex);
}
}
if (!bRemovedDisabledWidgets && Path.Num() > 0)
{
if (BestHitWidgetData.CustomPath.IsValid())
{
const TArray<FWidgetAndPointer> BubblePathExtension = BestHitWidgetData.CustomPath.Pin()->GetBubblePathAndVirtualCursors(FirstHitWidget->GetTickSpaceGeometry(), DesktopSpaceCoordinate, bIgnoreEnabledStatus);
Path.Append(BubblePathExtension);
}
}
return Path;
}
}
}
return TArray<FWidgetAndPointer>();
}
FHittestGrid::FIndexAndDistance FHittestGrid::GetHitIndexFromCellIndex(const FGridTestingParams& Params) const
{
//check if the cell coord
if (IsValidCellCoord(Params.CellCoord))
{
// Get the cell and sort it
FCollapsedWidgetsArray WidgetIndexes;
GetCollapsedWidgets(WidgetIndexes, Params.CellCoord.X, Params.CellCoord.Y);
// Consider front-most widgets first for hittesting.
for (int32 i = WidgetIndexes.Num() - 1; i >= 0; --i)
{
check(WidgetIndexes[i].IsValid());
const FWidgetData& TestCandidate = WidgetIndexes[i].GetWidgetData();
const TSharedPtr<SWidget> TestWidget = TestCandidate.GetWidget();
// When performing a point hittest, accept all hittestable widgets.
// When performing a hittest with a radius, only grab interactive widgets.
const bool bIsValidWidget = TestWidget.IsValid() && (!Params.bTestWidgetIsInteractive || TestWidget->IsInteractable());
if (bIsValidWidget)
{
const FVector2D WindowSpaceCoordinate = Params.CursorPositionInGrid + GridWindowOrigin;
const FGeometry& TestGeometry = TestWidget->GetPaintSpaceGeometry();
bool bPointInsideClipMasks = true;
if (WidgetIndexes[i].GetCullingRect().IsValid())
{
bPointInsideClipMasks = WidgetIndexes[i].GetCullingRect().ContainsPoint(WindowSpaceCoordinate);
}
if (bPointInsideClipMasks)
{
const TOptional<FSlateClippingState>& WidgetClippingState = TestWidget->GetCurrentClippingState();
if (WidgetClippingState.IsSet())
{
// TODO: Solve non-zero radius cursors?
bPointInsideClipMasks = WidgetClippingState->IsPointInside(WindowSpaceCoordinate);
}
}
if (bPointInsideClipMasks)
{
// Compute the render space clipping rect (FGeometry exposes a layout space clipping rect).
const FSlateRotatedRect WindowOrientedClipRect = TransformRect(
Concatenate(
Inverse(TestGeometry.GetAccumulatedLayoutTransform()),
TestGeometry.GetAccumulatedRenderTransform()),
FSlateRotatedRect(TestGeometry.GetLayoutBoundingRect())
);
if (IsOverlappingSlateRotatedRect(WindowSpaceCoordinate, Params.Radius, WindowOrientedClipRect))
{
// For non-0 radii also record the distance to cursor's center so that we can pick the closest hit from the results.
const bool bNeedsDistanceSearch = Params.Radius > 0.0f;
const float DistSq = (bNeedsDistanceSearch) ? DistanceSqToSlateRotatedRect(WindowSpaceCoordinate, WindowOrientedClipRect) : 0.0f;
return FIndexAndDistance(WidgetIndexes[i], DistSq);
}
}
}
}
}
return FIndexAndDistance();
}