Skip to content

Instantly share code, notes, and snippets.

@rtm223
Last active January 16, 2026 18:29
Show Gist options
  • Select an option

  • Save rtm223/a43ca4d9003d7c0470d3f886d30a4d7d to your computer and use it in GitHub Desktop.

Select an option

Save rtm223/a43ca4d9003d7c0470d3f886d30a4d7d to your computer and use it in GitHub Desktop.
Cursor Receiver Widget
// The MIT License
// Copyright (c) Richard Meredith AB
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#include "UI/UMG/RTMCursorReceiverWidget.h"
#include "UI/Slate/SRTMCursorReceiver.h"
#define LOCTEXT_NAMESPACE "RTM"
TSharedRef<SWidget> URTMCursorReceiverWidget::RebuildWidget()
{
Receiver = SNew(SRTMCursorReceiver)
.bDetectDragEvents(bDetectDragEvents)
.OnMoved(BIND_UOBJECT_DELEGATE(FRTMCursorReceiverMoveDelegate, SlateHandleMoved))
.OnClicked(BIND_UOBJECT_DELEGATE(FRTMCursorReceiverClickDelegate, SlateHandleClicked))
.OnPressed(BIND_UOBJECT_DELEGATE(FRTMCursorReceiverDelegate, SlateHandlePressed))
.OnReleased(BIND_UOBJECT_DELEGATE(FRTMCursorReceiverDelegate, SlateHandleReleased))
.OnDragMoved(BIND_UOBJECT_DELEGATE(FRTMCursorReceiverMoveDelegate, SlateHandleDragMoved))
.OnWheelChanged(BIND_UOBJECT_DELEGATE(FRTMCursorReceiverWheelDelegate, SlateHandleWheelChanged))
.OnHovered_UObject(this, &ThisClass::SlateHandleHovered)
.OnUnhovered_UObject(this, &ThisClass::SlateHandleUnhovered)
.OnDragStarted_UObject(this, &ThisClass::SlateHandleDragStarted)
.OnDragEnded_UObject(this, &ThisClass::SlateHandleDragEnded);
return Receiver.ToSharedRef();
}
void URTMCursorReceiverWidget::ReleaseSlateResources(bool bReleaseChildren)
{
Super::ReleaseSlateResources(bReleaseChildren);
Receiver.Reset();
}
#if WITH_EDITOR
TSharedRef<SWidget> URTMCursorReceiverWidget::RebuildDesignWidget(TSharedRef<SWidget> Content)
{
return Content;
}
const FText URTMCursorReceiverWidget::GetPaletteCategory()
{
return LOCTEXT("WIDGETS_CATEGORY", "RTM");
}
#endif
void URTMCursorReceiverWidget::SlateHandleHovered() { OnHovered.Broadcast(); }
void URTMCursorReceiverWidget::SlateHandleUnhovered() { OnUnhovered.Broadcast(); }
void URTMCursorReceiverWidget::SlateHandleMoved(const FVector2D& delta) {OnCursorMoved.Broadcast(delta); }
void URTMCursorReceiverWidget::SlateHandlePressed(const FKey& key) { OnPressed.Broadcast(key); }
void URTMCursorReceiverWidget::SlateHandleReleased(const FKey& key) { OnReleased.Broadcast(key); }
void URTMCursorReceiverWidget::SlateHandleDragStarted(const FKey& key) { OnDragStarted.Broadcast(key); }
void URTMCursorReceiverWidget::SlateHandleDragEnded(const FKey& key) { OnDragEnded.Broadcast(key); }
void URTMCursorReceiverWidget::SlateHandleDragMoved(const FVector2D& positionDelta) { OnDragMoved.Broadcast(positionDelta); }
void URTMCursorReceiverWidget::SlateHandleWheelChanged(float wheelDelta) { OnWheelChanged.Broadcast(wheelDelta); }
FReply URTMCursorReceiverWidget::SlateHandleClicked(const FKey& key)
{
OnClicked.Broadcast(key);
return FReply::Handled();
}
// The MIT License
// Copyright (c) Richard Meredith AB
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#pragma once
#include "CoreMinimal.h"
#include "Components/Widget.h"
#include "RTMCursorReceiverWidget.generated.h"
class SRTMCursorReceiver;
UCLASS()
class RTMCOMMON_API URTMCursorReceiverWidget : public UWidget
{
GENERATED_BODY()
public:
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FEvent);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FButtonEvent, const FKey&, key);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMoveEvent, const FVector2D&, positionDelta);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FWheelEvent, float, wheelDelta);
UPROPERTY(BlueprintAssignable, Category="Cursor|Events")
FEvent OnHovered;
UPROPERTY(BlueprintAssignable, Category="Cursor|Events")
FEvent OnUnhovered;
UPROPERTY(BlueprintAssignable, Category="Cursor|Events")
FMoveEvent OnCursorMoved;
UPROPERTY(BlueprintAssignable, Category="Cursor|Events")
FButtonEvent OnClicked;
UPROPERTY(BlueprintAssignable, Category="Cursor|Events")
FButtonEvent OnPressed;
UPROPERTY(BlueprintAssignable, Category="Cursor|Events")
FButtonEvent OnReleased;
UPROPERTY(BlueprintAssignable, Category="Cursor|Events")
FButtonEvent OnDragStarted;
UPROPERTY(BlueprintAssignable, Category="Cursor|Events")
FButtonEvent OnDragEnded;
UPROPERTY(BlueprintAssignable, Category="Cursor|Events")
FMoveEvent OnDragMoved;
UPROPERTY(BlueprintAssignable, Category="Cursor|Events")
FWheelEvent OnWheelChanged;
protected:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Cursor")
bool bDetectDragEvents = false;
virtual TSharedRef<SWidget> RebuildWidget() override;
virtual void ReleaseSlateResources(bool bReleaseChildren) override;
#if WITH_EDITOR
virtual TSharedRef<SWidget> RebuildDesignWidget(TSharedRef<SWidget> Content) override;
virtual const FText GetPaletteCategory() override;
#endif
//#if WITH_ACCESSIBILITY
// virtual TSharedPtr<SWidget> GetAccessibleWidget() const override;
//#endif
private:
TSharedPtr<SRTMCursorReceiver> Receiver;
void SlateHandleHovered();
void SlateHandleUnhovered();
void SlateHandleMoved(const FVector2D& delta);
void SlateHandlePressed(const FKey& key);
void SlateHandleReleased(const FKey& key);
FReply SlateHandleClicked(const FKey& key);
void SlateHandleDragStarted(const FKey& key);
void SlateHandleDragEnded(const FKey& key);
void SlateHandleDragMoved(const FVector2D& positionDelta);
void SlateHandleWheelChanged(float wheelDelta);
};
// The MIT License
// Copyright (c) Richard Meredith AB
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#include "UI/Slate/SRTMCursorReceiver.h"
#include "SlateOptMacros.h"
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SRTMCursorReceiver::Construct(const FArguments& InArgs)
{
SetCanTick(false);
OnHovered = InArgs._OnHovered;
OnUnhovered = InArgs._OnUnhovered;
OnMoved = InArgs._OnMoved;
OnPressed = InArgs._OnPressed;
OnReleased = InArgs._OnReleased;
OnClicked = InArgs._OnClicked;
OnDragStarted = InArgs._OnDragStarted;
OnDragEnded = InArgs._OnDragEnded;
OnDragMoved = InArgs._OnDragMoved;
OnWheelChanged = InArgs._OnWheelChanged;
bDetectDragEvents = InArgs._bDetectDragEvents;
ActiveKey = EKeys::Invalid;
PressedCursorPosition = {0.0f, 0.0f};
}
bool SRTMCursorReceiver::ComputeVolatility() const
{
#if ENGINE_MAJOR_VERSION >= 5
return GetContentScaleAttribute().IsBound();
#else
return ContentScale.IsBound();
#endif
}
void SRTMCursorReceiver::OnMouseEnter(const FGeometry& myGeometry, const FPointerEvent& mouseEvent)
{
Super::OnMouseEnter(myGeometry, mouseEvent);
OnHovered.ExecuteIfBound();
Invalidate(EInvalidateWidgetReason::Layout);
}
void SRTMCursorReceiver::OnMouseLeave(const FPointerEvent& MouseEvent)
{
const bool wasHovered = IsHovered();
Super::OnMouseLeave(MouseEvent);
MaybeRelease(EKeys::AnyKey);
if(wasHovered)
OnUnhovered.ExecuteIfBound();
Invalidate(EInvalidateWidgetReason::Layout);
}
FReply SRTMCursorReceiver::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& mouseEvent)
{
FReply reply = FReply::Unhandled();
if(IsEnabled() && IsInterestingCursorButton(mouseEvent))
{
MaybePress(mouseEvent.GetEffectingButton());
PressedCursorPosition = mouseEvent.GetScreenSpacePosition();
reply = FReply::Handled().CaptureMouse(AsShared());
if(bDetectDragEvents)
reply.DetectDrag(this->AsShared(), mouseEvent.GetEffectingButton());
}
Invalidate(EInvalidateWidgetReason::Layout);
return reply;
}
FReply SRTMCursorReceiver::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& mouseEvent)
{
FReply reply = FReply::Unhandled();
if(IsEnabled() && IsInterestingCursorButton(mouseEvent))
{
reply = FReply::Handled();
const FKey effectingButton = GetEffectiveCursorButton(mouseEvent);
if(ActiveKey == effectingButton)
reply = ExecuteOnClick(effectingButton);
else
reply = FReply::Handled();
if(DragKey == effectingButton)
{
OnDragEnded.ExecuteIfBound(DragKey);
DragKey = EKeys::Invalid;
}
MaybeRelease(effectingButton);
}
if(reply.GetMouseCaptor().IsValid() == false && HasMouseCapture())
reply.ReleaseMouseCapture();
Invalidate(EInvalidateWidgetReason::Layout);
return reply;
}
FReply SRTMCursorReceiver::OnMouseButtonDoubleClick(const FGeometry& myGeometry, const FPointerEvent& mouseEvent)
{
return OnMouseButtonDown(myGeometry, mouseEvent);
}
FReply SRTMCursorReceiver::OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& mouseEvent)
{
if(bDetectDragEvents && FSlateApplication::Get().HasTraveledFarEnoughToTriggerDrag(mouseEvent, PressedCursorPosition))
MaybeRelease(EKeys::AnyKey);
auto delta = mouseEvent.GetCursorDelta();
if(!delta.IsNearlyZero())
(IsDragging() ? OnDragMoved : OnMoved).ExecuteIfBound(delta);
return FReply::Unhandled();
}
void SRTMCursorReceiver::OnMouseCaptureLost(const FCaptureLostEvent& captureLostEvent)
{
if(HasMouseCapture())
MaybeRelease(EKeys::AnyKey);
}
FReply SRTMCursorReceiver::OnMouseWheel(const FGeometry& myGeometry, const FPointerEvent& mouseEvent)
{
OnWheelChanged.ExecuteIfBound(mouseEvent.GetWheelDelta());
return FReply::Handled();
}
FReply SRTMCursorReceiver::OnDragDetected(const FGeometry& myGeometry, const FPointerEvent& mouseEvent)
{
Super::OnDragDetected(myGeometry, mouseEvent);
DragKey = ActiveKey;
MaybeRelease(ActiveKey);
OnDragStarted.ExecuteIfBound(DragKey);
return FReply::Handled();
}
FReply SRTMCursorReceiver::ExecuteOnClick(const FKey& key)
{
if(OnClicked.IsBound())
{
FReply reply = OnClicked.Execute(key);
//#if WITH_ACCESSIBILITY
// FSlateApplicationBase::Get().GetAccessibleMessageHandler()->OnWidgetEventRaised(AsShared(), EAccessibleEvent::Activate);
//#endif
return reply;
}
return FReply::Handled();
}
void SRTMCursorReceiver::MaybePress(FKey key)
{
if(ActiveKey == EKeys::Invalid)
{
ActiveKey = key;
OnPressed.ExecuteIfBound(key);
}
}
void SRTMCursorReceiver::MaybeRelease(FKey key)
{
if(ActiveKey == key)
{
ActiveKey = EKeys::Invalid;
OnReleased.ExecuteIfBound(key);
}
}
FKey SRTMCursorReceiver::GetEffectiveCursorButton(const FPointerEvent& mouseEvent)
{
if(mouseEvent.IsTouchEvent())
return mouseEvent.GetPointerIndex() == 0 ? EKeys::LeftMouseButton : EKeys::Invalid;
return mouseEvent.GetEffectingButton();
}
bool SRTMCursorReceiver::IsInterestingCursorButton(const FPointerEvent& mouseEvent)
{
const FKey effectingButton = GetEffectiveCursorButton(mouseEvent);
return effectingButton == EKeys::LeftMouseButton || effectingButton == EKeys::RightMouseButton;
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
// The MIT License
// Copyright (c) Richard Meredith AB
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#pragma once
#include "CoreMinimal.h"
#include "Widgets/SCompoundWidget.h"
DECLARE_DELEGATE_OneParam(FRTMCursorReceiverDelegate, const FKey&)
DECLARE_DELEGATE_OneParam(FRTMCursorReceiverMoveDelegate, const FVector2D&)
DECLARE_DELEGATE_OneParam(FRTMCursorReceiverWheelDelegate, float)
DECLARE_DELEGATE_RetVal_OneParam(FReply, FRTMCursorReceiverClickDelegate, const FKey&)
class RTMCOMMON_API SRTMCursorReceiver : public SCompoundWidget
{
private:
using Super = SCompoundWidget;
public:
SLATE_BEGIN_ARGS(SRTMCursorReceiver) {}
SLATE_ARGUMENT(bool, bDetectDragEvents)
SLATE_EVENT(FSimpleDelegate, OnHovered)
SLATE_EVENT(FSimpleDelegate, OnUnhovered)
SLATE_EVENT(FRTMCursorReceiverMoveDelegate, OnMoved)
SLATE_EVENT(FRTMCursorReceiverDelegate, OnPressed)
SLATE_EVENT(FRTMCursorReceiverDelegate, OnReleased)
SLATE_EVENT(FRTMCursorReceiverClickDelegate, OnClicked)
SLATE_EVENT(FRTMCursorReceiverDelegate, OnDragStarted)
SLATE_EVENT(FRTMCursorReceiverDelegate, OnDragEnded)
SLATE_EVENT(FRTMCursorReceiverMoveDelegate, OnDragMoved)
SLATE_EVENT(FRTMCursorReceiverWheelDelegate, OnWheelChanged)
SLATE_DEFAULT_SLOT(FArguments, Content)
SLATE_END_ARGS()
void Construct(const FArguments& InArgs);
FORCEINLINE bool IsButtonPressed() const { return ActiveKey != EKeys::Invalid; }
FORCEINLINE bool IsDragging() const { return DragKey != EKeys::Invalid; }
protected:
virtual bool SupportsKeyboardFocus() const override { return false; }
virtual bool IsInteractable() const override { return IsEnabled(); }
virtual bool ComputeVolatility() const override;
virtual void OnMouseEnter(const FGeometry& myGeometry, const FPointerEvent& mouseEvent) override;
virtual void OnMouseLeave(const FPointerEvent& MouseEvent) override;
virtual FReply OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& mouseEvent) override;
virtual FReply OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& mouseEvent) override;
virtual FReply OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& mouseEvent) override;
virtual FReply OnMouseButtonDoubleClick(const FGeometry& myGeometry, const FPointerEvent& mouseEvent) override;
virtual void OnMouseCaptureLost(const FCaptureLostEvent& captureLostEvent) override;
virtual FReply OnMouseWheel(const FGeometry& myGeometry, const FPointerEvent& mouseEvent) override;
virtual FReply OnDragDetected(const FGeometry& myGeometry, const FPointerEvent& mouseEvent) override;
private:
bool bDetectDragEvents = true;
FVector2D PressedCursorPosition = FVector2D::ZeroVector;
FKey ActiveKey = EKeys::Invalid;
FKey DragKey = EKeys::Invalid;
FReply ExecuteOnClick(const FKey& key);
void MaybePress(FKey key);
void MaybeRelease(FKey key);
FSimpleDelegate OnHovered;
FSimpleDelegate OnUnhovered;
FRTMCursorReceiverMoveDelegate OnMoved;
FRTMCursorReceiverDelegate OnPressed;
FRTMCursorReceiverDelegate OnReleased;
FRTMCursorReceiverClickDelegate OnClicked;
FRTMCursorReceiverDelegate OnDragStarted;
FRTMCursorReceiverDelegate OnDragEnded;
FRTMCursorReceiverMoveDelegate OnDragMoved;
FRTMCursorReceiverWheelDelegate OnWheelChanged;
static FKey GetEffectiveCursorButton(const FPointerEvent& mouseEvent);
bool IsInterestingCursorButton(const FPointerEvent& mouseEvent);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment