Files
UnrealEngine/Engine/Source/Runtime/Slate/Private/Widgets/Input/SSlider.cpp
2025-05-18 13:04:45 +08:00

606 lines
18 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Widgets/Input/SSlider.h"
#include "Rendering/DrawElements.h"
#include "Framework/Application/SlateApplication.h"
#if WITH_ACCESSIBILITY
#include "Widgets/Accessibility/SlateAccessibleWidgets.h"
#endif
SLATE_IMPLEMENT_WIDGET(SSlider)
void SSlider::PrivateRegisterAttributes(FSlateAttributeInitializer& AttributeInitializer)
{
SLATE_ADD_MEMBER_ATTRIBUTE_DEFINITION_WITH_NAME(AttributeInitializer, "Value", ValueSlateAttribute, EInvalidateWidgetReason::Paint);
SLATE_ADD_MEMBER_ATTRIBUTE_DEFINITION_WITH_NAME(AttributeInitializer, "IndentHandle", IndentHandleSlateAttribute, EInvalidateWidgetReason::Paint);
SLATE_ADD_MEMBER_ATTRIBUTE_DEFINITION_WITH_NAME(AttributeInitializer, "Locked", LockedSlateAttribute, EInvalidateWidgetReason::Paint);
SLATE_ADD_MEMBER_ATTRIBUTE_DEFINITION_WITH_NAME(AttributeInitializer, "SliderBarColor", SliderBarColorSlateAttribute, EInvalidateWidgetReason::Paint);
SLATE_ADD_MEMBER_ATTRIBUTE_DEFINITION_WITH_NAME(AttributeInitializer, "SliderHandleColor", SliderHandleColorSlateAttribute, EInvalidateWidgetReason::Paint);
AttributeInitializer.OverrideInvalidationReason("EnabledState", FSlateAttributeDescriptor::FInvalidateWidgetReasonAttribute{EInvalidateWidgetReason::Paint});
AttributeInitializer.OverrideInvalidationReason("Hovered", FSlateAttributeDescriptor::FInvalidateWidgetReasonAttribute{EInvalidateWidgetReason::Paint});
}
SSlider::SSlider()
: Style(nullptr)
, PressedScreenSpaceTouchDownPosition(FVector2f(0, 0))
, ValueSlateAttribute(*this, 1.f)
, IndentHandleSlateAttribute(*this, true)
, LockedSlateAttribute(*this, false)
, SliderBarColorSlateAttribute(*this, FLinearColor::White)
, SliderHandleColorSlateAttribute(*this, FLinearColor::White)
{
#if WITH_ACCESSIBILITY
AccessibleBehavior = EAccessibleBehavior::Summary;
bCanChildrenBeAccessible = false;
#endif
}
SSlider::~SSlider() = default;
void SSlider::Construct( const SSlider::FArguments& InDeclaration )
{
check(InDeclaration._Style);
Style = InDeclaration._Style;
IndentHandleSlateAttribute.Assign(*this, InDeclaration._IndentHandle);
bMouseUsesStep = InDeclaration._MouseUsesStep;
bRequiresControllerLock = InDeclaration._RequiresControllerLock;
LockedSlateAttribute.Assign(*this, InDeclaration._Locked);
Orientation = InDeclaration._Orientation;
StepSize = InDeclaration._StepSize;
ValueSlateAttribute.Assign(*this, InDeclaration._Value);
MinValue = InDeclaration._MinValue;
MaxValue = InDeclaration._MaxValue;
SliderBarColorSlateAttribute.Assign(*this, InDeclaration._SliderBarColor);
SliderHandleColorSlateAttribute.Assign(*this, InDeclaration._SliderHandleColor);
bIsFocusable = InDeclaration._IsFocusable;
OnMouseCaptureBegin = InDeclaration._OnMouseCaptureBegin;
OnMouseCaptureEnd = InDeclaration._OnMouseCaptureEnd;
OnControllerCaptureBegin = InDeclaration._OnControllerCaptureBegin;
OnControllerCaptureEnd = InDeclaration._OnControllerCaptureEnd;
OnValueChanged = InDeclaration._OnValueChanged;
bPreventThrottling = InDeclaration._PreventThrottling;
bControllerInputCaptured = false;
}
int32 SSlider::OnPaint( const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled ) const
{
// we draw the slider like a horizontal slider regardless of the orientation, and apply a render transform to make it display correctly.
// However, the AllottedGeometry is computed as it will be rendered, so we have to use the "horizontal orientation" when doing drawing computations.
const float AllottedWidth = Orientation == Orient_Horizontal ? AllottedGeometry.GetLocalSize().X : AllottedGeometry.GetLocalSize().Y;
const float AllottedHeight = Orientation == Orient_Horizontal ? AllottedGeometry.GetLocalSize().Y : AllottedGeometry.GetLocalSize().X;
float HandleRotation;
FVector2f HandleTopLeftPoint;
FVector2f SliderStartPoint;
FVector2f SliderEndPoint;
// calculate slider geometry as if it's a horizontal slider (we'll rotate it later if it's vertical)
const FVector2f HandleSize = GetThumbImage()->ImageSize;
const FVector2f HalfHandleSize = 0.5f * HandleSize;
const float Indentation = IndentHandleSlateAttribute.Get() ? HandleSize.X : 0.0f;
// We clamp to make sure that the slider cannot go out of the slider Length.
const float SliderPercent = FMath::Clamp(GetNormalizedValue(), 0.0f, 1.0f);
const float SliderLength = AllottedWidth - (Indentation + HandleSize.X);
const float SliderHandleOffset = SliderPercent * SliderLength;
const float SliderY = 0.5f * AllottedHeight;
HandleRotation = 0.0f;
HandleTopLeftPoint = FVector2f(SliderHandleOffset + (0.5f * Indentation), SliderY - HalfHandleSize.Y);
SliderStartPoint = FVector2f(HalfHandleSize.X, SliderY);
SliderEndPoint = FVector2f(AllottedWidth - HalfHandleSize.X, SliderY);
FGeometry SliderGeometry = AllottedGeometry;
// rotate the slider 90deg if it's vertical. The 0 side goes on the bottom, the 1 side on the top.
if (Orientation == Orient_Vertical)
{
// Do this by translating along -X by the width of the geometry, then rotating 90 degreess CCW (left-hand coords)
FSlateRenderTransform SlateRenderTransform = TransformCast<FSlateRenderTransform>(Concatenate(Inverse(FVector2f(AllottedWidth, 0)), FQuat2D(FMath::DegreesToRadians(-90.0f))));
// create a child geometry matching this one, but with the render transform.
SliderGeometry = AllottedGeometry.MakeChild(
FVector2f(AllottedWidth, AllottedHeight),
FSlateLayoutTransform(),
SlateRenderTransform, FVector2f::ZeroVector);
}
const bool bEnabled = ShouldBeEnabled(bParentEnabled);
const ESlateDrawEffect DrawEffects = bEnabled ? ESlateDrawEffect::None : ESlateDrawEffect::DisabledEffect;
// draw slider bar
auto BarTopLeft = FVector2f(SliderStartPoint.X, SliderStartPoint.Y - Style->BarThickness * 0.5f);
auto BarSize = FVector2f(SliderEndPoint.X - SliderStartPoint.X, Style->BarThickness);
auto BarImage = GetBarImage();
auto ThumbImage = GetThumbImage();
FSlateDrawElement::MakeBox(
OutDrawElements,
LayerId,
SliderGeometry.ToPaintGeometry(BarSize, FSlateLayoutTransform(BarTopLeft)),
BarImage,
DrawEffects,
BarImage->GetTint(InWidgetStyle) * SliderBarColorSlateAttribute.Get().GetColor(InWidgetStyle) * InWidgetStyle.GetColorAndOpacityTint()
);
++LayerId;
// draw slider thumb
FSlateDrawElement::MakeBox(
OutDrawElements,
LayerId,
SliderGeometry.ToPaintGeometry(GetThumbImage()->ImageSize, FSlateLayoutTransform(HandleTopLeftPoint)),
ThumbImage,
DrawEffects,
ThumbImage->GetTint(InWidgetStyle) * SliderHandleColorSlateAttribute.Get().GetColor(InWidgetStyle) * InWidgetStyle.GetColorAndOpacityTint()
);
return LayerId;
}
FVector2D SSlider::ComputeDesiredSize( float ) const
{
static const FVector2D SSliderDesiredSize(16.0f, 16.0f);
if ( Style == nullptr )
{
return SSliderDesiredSize;
}
const float Thickness = FMath::Max(Style->BarThickness,
FMath::Max(Style->NormalThumbImage.ImageSize.Y, Style->HoveredThumbImage.ImageSize.Y));
if (Orientation == Orient_Vertical)
{
return FVector2D(Thickness, SSliderDesiredSize.Y);
}
return FVector2D(SSliderDesiredSize.X, Thickness);
}
void SSlider::SetStyle(const FSliderStyle* InStyle)
{
Style = InStyle;
Invalidate(EInvalidateWidgetReason::Layout);
}
bool SSlider::IsLocked() const
{
return LockedSlateAttribute.Get();
}
bool SSlider::IsInteractable() const
{
return IsEnabled() && !IsLocked() && SupportsKeyboardFocus();
}
bool SSlider::SupportsKeyboardFocus() const
{
return bIsFocusable;
}
void SSlider::ResetControllerState()
{
if (bControllerInputCaptured)
{
OnControllerCaptureEnd.ExecuteIfBound();
bControllerInputCaptured = false;
}
}
FNavigationReply SSlider::OnNavigation(const FGeometry& MyGeometry, const FNavigationEvent& InNavigationEvent)
{
if (bControllerInputCaptured || !bRequiresControllerLock)
{
FNavigationReply Reply = FNavigationReply::Escape();
float NewValue = ValueSlateAttribute.Get();
if (Orientation == EOrientation::Orient_Horizontal)
{
if (InNavigationEvent.GetNavigationType() == EUINavigation::Left)
{
NewValue -= StepSize.Get();
Reply = FNavigationReply::Stop();
}
else if (InNavigationEvent.GetNavigationType() == EUINavigation::Right)
{
NewValue += StepSize.Get();
Reply = FNavigationReply::Stop();
}
}
else
{
if (InNavigationEvent.GetNavigationType() == EUINavigation::Down)
{
NewValue -= StepSize.Get();
Reply = FNavigationReply::Stop();
}
else if (InNavigationEvent.GetNavigationType() == EUINavigation::Up)
{
NewValue += StepSize.Get();
Reply = FNavigationReply::Stop();
}
}
if (ValueSlateAttribute.Get() != NewValue)
{
CommitValue(FMath::Clamp(NewValue, MinValue, MaxValue));
return Reply;
}
}
return SLeafWidget::OnNavigation(MyGeometry, InNavigationEvent);
}
FReply SSlider::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent)
{
FReply Reply = FReply::Unhandled();
if (IsInteractable())
{
// The controller's bottom face button must be pressed once to begin manipulating the slider's value.
// Navigation away from the widget is prevented until the button has been pressed again or focus is lost.
// The value can be manipulated by using the game pad's directional arrows ( relative to slider orientation ).
if (FSlateApplication::Get().GetNavigationActionFromKey(InKeyEvent) == EUINavigationAction::Accept && bRequiresControllerLock)
{
if (bControllerInputCaptured == false)
{
// Begin capturing controller input and allow user to modify the slider's value.
bControllerInputCaptured = true;
OnControllerCaptureBegin.ExecuteIfBound();
Reply = FReply::Handled();
}
else
{
ResetControllerState();
Reply = FReply::Handled();
}
}
else
{
Reply = SLeafWidget::OnKeyDown(MyGeometry, InKeyEvent);
}
}
else
{
Reply = SLeafWidget::OnKeyDown(MyGeometry, InKeyEvent);
}
return Reply;
}
FReply SSlider::OnKeyUp(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent)
{
FReply Reply = FReply::Unhandled();
if (bControllerInputCaptured)
{
Reply = FReply::Handled();
}
return Reply;
}
void SSlider::OnFocusLost(const FFocusEvent& InFocusEvent)
{
if (bControllerInputCaptured)
{
// Commit and reset state
CommitValue(ValueSlateAttribute.Get());
ResetControllerState();
}
}
FReply SSlider::OnMouseButtonDown( const FGeometry& MyGeometry, const FPointerEvent& MouseEvent )
{
if ((MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton) && !IsLocked())
{
CachedCursor = GetCursor().Get(EMouseCursor::Default);
OnMouseCaptureBegin.ExecuteIfBound();
CommitValue(PositionToValue(MyGeometry, MouseEvent.GetScreenSpacePosition()));
// Release capture for controller/keyboard when switching to mouse.
ResetControllerState();
FReply ReturnReply = FReply::Handled().CaptureMouse(SharedThis(this));
if (bPreventThrottling)
{
ReturnReply.PreventThrottling();
}
return ReturnReply;
}
return FReply::Unhandled();
}
FReply SSlider::OnMouseButtonUp( const FGeometry& MyGeometry, const FPointerEvent& MouseEvent )
{
if ((MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton) && HasMouseCaptureByUser(MouseEvent.GetUserIndex(), MouseEvent.GetPointerIndex()))
{
SetCursor(CachedCursor);
// Release capture for controller/keyboard when switching to mouse.
ResetControllerState();
return FReply::Handled().ReleaseMouseCapture();
}
return FReply::Unhandled();
}
void SSlider::OnMouseCaptureLost(const FCaptureLostEvent& CaptureLostEvent)
{
OnMouseCaptureEnd.ExecuteIfBound();
SLeafWidget::OnMouseCaptureLost(CaptureLostEvent);
}
FReply SSlider::OnMouseMove( const FGeometry& MyGeometry, const FPointerEvent& MouseEvent )
{
if (HasMouseCaptureByUser(MouseEvent.GetUserIndex(), MouseEvent.GetPointerIndex()) && !IsLocked())
{
SetCursor((Orientation == Orient_Horizontal) ? EMouseCursor::ResizeLeftRight : EMouseCursor::ResizeUpDown);
CommitValue(PositionToValue(MyGeometry, MouseEvent.GetScreenSpacePosition()));
// Release capture for controller/keyboard when switching to mouse
ResetControllerState();
return FReply::Handled();
}
return FReply::Unhandled();
}
FReply SSlider::OnTouchStarted(const FGeometry& MyGeometry, const FPointerEvent& InTouchEvent)
{
if (!IsLocked())
{
// Release capture for controller/keyboard when switching to mouse.
ResetControllerState();
PressedScreenSpaceTouchDownPosition = InTouchEvent.GetScreenSpacePosition();
return FReply::Handled();
}
return FReply::Unhandled();
}
FReply SSlider::OnTouchMoved(const FGeometry& MyGeometry, const FPointerEvent& InTouchEvent)
{
if (HasMouseCaptureByUser(InTouchEvent.GetUserIndex(), InTouchEvent.GetPointerIndex()))
{
CommitValue(PositionToValue(MyGeometry, InTouchEvent.GetScreenSpacePosition()));
// Release capture for controller/keyboard when switching to mouse
ResetControllerState();
return FReply::Handled();
}
else if (!HasMouseCapture())
{
if (FSlateApplication::Get().HasTraveledFarEnoughToTriggerDrag(InTouchEvent, PressedScreenSpaceTouchDownPosition, Orientation))
{
CachedCursor = GetCursor().Get(EMouseCursor::Default);
OnMouseCaptureBegin.ExecuteIfBound();
CommitValue(PositionToValue(MyGeometry, InTouchEvent.GetScreenSpacePosition()));
// Release capture for controller/keyboard when switching to mouse
ResetControllerState();
return FReply::Handled().CaptureMouse(SharedThis(this));
}
}
return FReply::Unhandled();
}
FReply SSlider::OnTouchEnded(const FGeometry& MyGeometry, const FPointerEvent& InTouchEvent)
{
if (HasMouseCaptureByUser(InTouchEvent.GetUserIndex(), InTouchEvent.GetPointerIndex()))
{
SetCursor(CachedCursor);
OnMouseCaptureEnd.ExecuteIfBound();
CommitValue(PositionToValue(MyGeometry, InTouchEvent.GetScreenSpacePosition()));
// Release capture for controller/keyboard when switching to mouse.
ResetControllerState();
return FReply::Handled().ReleaseMouseCapture();
}
return FReply::Unhandled();
}
void SSlider::CommitValue(float NewValue)
{
const float OldValue = GetValue();
if (NewValue != OldValue)
{
if (!ValueSlateAttribute.IsBound(*this))
{
ValueSlateAttribute.Assign(*this, NewValue);
}
Invalidate(EInvalidateWidgetReason::Paint);
OnValueChanged.ExecuteIfBound(NewValue);
}
}
float SSlider::PositionToValue( const FGeometry& MyGeometry, const UE::Slate::FDeprecateVector2DParameter& AbsolutePosition )
{
const FVector2f LocalPosition = MyGeometry.AbsoluteToLocal(AbsolutePosition);
float RelativeValue;
float Denominator;
// Only need X as we rotate the thumb image when rendering vertically
const float Indentation = GetThumbImage()->ImageSize.X * (IndentHandleSlateAttribute.Get() ? 2.f : 1.f);
const float HalfIndentation = 0.5f * Indentation;
if (Orientation == Orient_Horizontal)
{
Denominator = MyGeometry.Size.X - Indentation;
RelativeValue = (Denominator != 0.f) ? (LocalPosition.X - HalfIndentation) / Denominator : 0.f;
}
else
{
Denominator = MyGeometry.Size.Y - Indentation;
// Inverse the calculation as top is 0 and bottom is 1
RelativeValue = (Denominator != 0.f) ? ((MyGeometry.Size.Y - LocalPosition.Y) - HalfIndentation) / Denominator : 0.f;
}
RelativeValue = FMath::Clamp(RelativeValue, 0.0f, 1.0f) * (MaxValue - MinValue) + MinValue;
if (bMouseUsesStep)
{
float direction = ValueSlateAttribute.Get() - RelativeValue;
float CurrentStepSize = StepSize.Get();
if (CurrentStepSize <= 0)
{
// Invalid step size, keep current value
return ValueSlateAttribute.Get();
}
float Steps = FMath::Abs(direction) / CurrentStepSize;
Steps = FMath::RoundHalfFromZero(Steps);
const float ClampedDist = Steps * CurrentStepSize;
if (direction > CurrentStepSize / 2.0f)
{
return FMath::Clamp(ValueSlateAttribute.Get() - ClampedDist, MinValue, MaxValue);
}
else if (direction < CurrentStepSize / -2.0f)
{
return FMath::Clamp(ValueSlateAttribute.Get() + ClampedDist, MinValue, MaxValue);
}
else
{
return ValueSlateAttribute.Get();
}
}
return RelativeValue;
}
const FSlateBrush* SSlider::GetBarImage() const
{
if (!IsEnabled() || LockedSlateAttribute.Get())
{
return &Style->DisabledBarImage;
}
else if (IsHovered())
{
return &Style->HoveredBarImage;
}
else
{
return &Style->NormalBarImage;
}
}
const FSlateBrush* SSlider::GetThumbImage() const
{
if (!IsEnabled() || LockedSlateAttribute.Get())
{
return &Style->DisabledThumbImage;
}
else if (IsHovered())
{
return &Style->HoveredThumbImage;
}
else
{
return &Style->NormalThumbImage;
}
}
float SSlider::GetValue() const
{
return ValueSlateAttribute.Get();
}
float SSlider::GetNormalizedValue() const
{
if (MaxValue == MinValue)
{
return 1.0f;
}
else
{
return (ValueSlateAttribute.Get() - MinValue) / (MaxValue - MinValue);
}
}
void SSlider::SetValue(TAttribute<float> InValueAttribute)
{
ValueSlateAttribute.Assign(*this, MoveTemp(InValueAttribute));
}
void SSlider::SetMinAndMaxValues(float InMinValue, float InMaxValue)
{
if (MinValue != InMinValue || MaxValue != InMaxValue)
{
MinValue = InMinValue;
MaxValue = InMaxValue;
if (MinValue > MaxValue)
{
MaxValue = MinValue;
}
Invalidate(EInvalidateWidgetReason::Paint);
}
}
void SSlider::SetIndentHandle(TAttribute<bool> InIndentHandle)
{
IndentHandleSlateAttribute.Assign(*this, MoveTemp(InIndentHandle));
}
void SSlider::SetLocked(TAttribute<bool> InLocked)
{
LockedSlateAttribute.Assign(*this, MoveTemp(InLocked));
}
void SSlider::SetOrientation(EOrientation InOrientation)
{
if (Orientation != InOrientation)
{
Orientation = InOrientation;
Invalidate(EInvalidateWidgetReason::Layout);
}
}
void SSlider::SetSliderBarColor(TAttribute<FSlateColor> InSliderBarColor)
{
SliderBarColorSlateAttribute.Assign(*this, MoveTemp(InSliderBarColor));
}
void SSlider::SetSliderHandleColor(TAttribute<FSlateColor> InSliderHandleColor)
{
SliderHandleColorSlateAttribute.Assign(*this, MoveTemp(InSliderHandleColor));
}
float SSlider::GetStepSize() const
{
return StepSize.Get();
}
void SSlider::SetStepSize(TAttribute<float> InStepSize)
{
StepSize = MoveTemp(InStepSize);
}
void SSlider::SetMouseUsesStep(bool MouseUsesStep)
{
bMouseUsesStep = MouseUsesStep;
}
void SSlider::SetRequiresControllerLock(bool RequiresControllerLock)
{
bRequiresControllerLock = RequiresControllerLock;
}
#if WITH_ACCESSIBILITY
TSharedRef<FSlateAccessibleWidget> SSlider::CreateAccessibleWidget()
{
return MakeShareable<FSlateAccessibleWidget>(new FSlateAccessibleSlider(SharedThis(this)));
}
#endif