Files
UnrealEngine/Engine/Source/Editor/UMGEditor/Private/Designer/SRuler.cpp
2025-05-18 13:04:45 +08:00

385 lines
12 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Designer/SRuler.h"
#include "Containers/Array.h"
#include "Containers/UnrealString.h"
#include "Fonts/FontMeasure.h"
#include "Fonts/SlateFontInfo.h"
#include "Framework/Application/SlateApplication.h"
#include "HAL/PlatformCrt.h"
#include "Layout/Geometry.h"
#include "Layout/SlateRect.h"
#include "Math/Color.h"
#include "Math/Range.h"
#include "Math/UnrealMathSSE.h"
#include "Rendering/DrawElements.h"
#include "Rendering/RenderingCommon.h"
#include "Rendering/SlateRenderer.h"
#include "Styling/CoreStyle.h"
#include "Styling/ISlateStyle.h"
#include "Templates/SharedPointer.h"
class FWidgetStyle;
struct FPointerEvent;
struct FSlateBrush;
#define LOCTEXT_NAMESPACE "UMG"
namespace ScrubConstants
{
/** The minimum amount of pixels between each major ticks on the widget */
const int32 MinPixelsPerDisplayTick = 5;
/**The smallest number of units between between major tick marks */
const float MinDisplayTickSpacing = 0.1f;
}
/** Utility struct for converting between scrub range space and local/absolute screen space */
struct SRuler::FScrubRangeToScreen
{
float RulerLengthSlateUnits;
TRange<float> ViewInput;
float ViewInputRange;
float PixelsPerInput;
FScrubRangeToScreen(TRange<float> InViewInput, const float InRulerLengthSlateUnits)
{
RulerLengthSlateUnits = InRulerLengthSlateUnits;
ViewInput = InViewInput;
ViewInputRange = ViewInput.Size<float>();
PixelsPerInput = ViewInputRange > 0 ? ( RulerLengthSlateUnits / ViewInputRange ) : 0;
// Round the float to the nearest four decimals, this keeps it stable as the user moves the ruler around
// otherwise small variations will cause the spacing to change wildly as the user pans.
PixelsPerInput = FMath::RoundToFloat(PixelsPerInput * 10000.0f) / 10000.0f;
}
/** Local Widget Space -> Curve Input domain. */
float LocalXToInput(float ScreenX) const
{
float LocalX = ScreenX;
return ( LocalX / PixelsPerInput ) + ViewInput.GetLowerBoundValue();
}
/** Curve Input domain -> local Widget Space */
float InputToLocalX(float Input) const
{
return ( Input - ViewInput.GetLowerBoundValue() ) * PixelsPerInput;
}
};
struct SRuler::FDrawTickArgs
{
/** Geometry of the area */
FGeometry AllottedGeometry;
/** Clipping rect of the area */
FSlateRect ClippingRect;
/** Color of each tick */
FLinearColor TickColor;
/** Color of each tick */
FLinearColor TextColor;
/** Offset in Y where to start the tick */
float TickOffset;
/** Height in of major ticks */
float MajorTickHeight;
/** Start layer for elements */
int32 StartLayer;
/** Draw effects to apply */
ESlateDrawEffect DrawEffects;
/** Whether or not to only draw major ticks */
bool bOnlyDrawMajorTicks;
};
/**
* Gets the the next spacing value in the series
* to determine a good spacing value
* E.g, .001,.005,.010,.050,.100,.500,1.000,etc
*/
static float GetNextSpacing( uint32 CurrentStep )
{
if(CurrentStep & 0x01)
{
// Odd numbers
return FMath::Pow( 10.f, 0.5f*((float)(CurrentStep-1)) + 1.f );
}
else
{
// Even numbers
return 0.5f * FMath::Pow( 10.f, 0.5f*((float)(CurrentStep)) + 1.f );
}
}
/////////////////////////////////////////////////////
// SRuler
void SRuler::Construct(const SRuler::FArguments& InArgs)
{
Orientation = InArgs._Orientation;
AbsoluteOrigin = FVector2D(0, 0);
SlateToUnitScale = 1;
MouseButtonDownHandler = InArgs._OnMouseButtonDown;
}
float SRuler::DetermineOptimalSpacing(float InPixelsPerInput, uint32 MinTick, float MinTickSpacing) const
{
if (InPixelsPerInput == 0.0f)
return MinTickSpacing;
uint32 CurStep = 0;
// Start with the smallest spacing
float Spacing = MinTickSpacing;
while( Spacing * InPixelsPerInput < MinTick )
{
Spacing = MinTickSpacing * GetNextSpacing( CurStep );
CurStep++;
}
return Spacing;
}
void SRuler::SetRuling(FVector2D InAbsoluteOrigin, float InSlateToUnitScale)
{
AbsoluteOrigin = InAbsoluteOrigin;
SlateToUnitScale = InSlateToUnitScale;
}
void SRuler::SetCursor(TOptional<FVector2D> InAbsoluteCursor)
{
AbsoluteCursor = InAbsoluteCursor;
}
int32 SRuler::DrawTicks( FSlateWindowElementList& OutDrawElements, const struct FScrubRangeToScreen& RangeToScreen, FDrawTickArgs& InArgs ) const
{
const float Spacing = DetermineOptimalSpacing( RangeToScreen.PixelsPerInput, ScrubConstants::MinPixelsPerDisplayTick, ScrubConstants::MinDisplayTickSpacing );
// Sub divisions
// @todo Sequencer may need more robust calculation
const int32 Divider = 10;
// For slightly larger halfway tick mark
const int32 HalfDivider = Divider / 2;
// Find out where to start from
int32 OffsetNum = FMath::FloorToInt(RangeToScreen.ViewInput.GetLowerBoundValue() / Spacing);
FSlateFontInfo SmallLayoutFont = FCoreStyle::GetDefaultFontStyle("Regular", 7);
TArray<FVector2D> LinePoints;
LinePoints.AddUninitialized(2);
// lines should not need anti-aliasing
const bool bAntiAliasLines = false;
float Number = 0;
while ( ( Number = OffsetNum*Spacing ) < RangeToScreen.ViewInput.GetUpperBoundValue() )
{
// X position local to start of the widget area
float XPos = RangeToScreen.InputToLocalX(Number);
uint32 AbsOffsetNum = FMath::Abs(OffsetNum);
if ( AbsOffsetNum % Divider == 0 )
{
FVector2D Offset = Orientation == Orient_Horizontal ? FVector2D(XPos, InArgs.TickOffset) : FVector2D(InArgs.TickOffset, XPos);
FVector2D TickSize = Orientation == Orient_Horizontal ? FVector2D(1.0f, InArgs.MajorTickHeight) : FVector2D(InArgs.MajorTickHeight, 1.0f);
LinePoints[0] = FVector2D(1.0f, 1.0f);
LinePoints[1] = TickSize;
// Draw each tick mark
FSlateDrawElement::MakeLines(
OutDrawElements,
InArgs.StartLayer,
InArgs.AllottedGeometry.ToPaintGeometry( TickSize, FSlateLayoutTransform(Offset) ),
LinePoints,
InArgs.DrawEffects,
InArgs.TickColor,
bAntiAliasLines
);
if( !InArgs.bOnlyDrawMajorTicks )
{
float TextNumber = FMath::Abs(Number);
FString FrameString = Spacing == ScrubConstants::MinDisplayTickSpacing ? FString::Printf(TEXT("%.1f"), TextNumber) : FString::Printf(TEXT("%.0f"), TextNumber);
// If the orientation is vertical break the number up over multiple lines.
if ( Orientation == Orient_Vertical )
{
for ( int32 CharIndex = 0; CharIndex < FrameString.Len() - 1; CharIndex += 2 )
{
FrameString.InsertAt(CharIndex + 1, '\n');
}
}
// Space the text between the tick mark but slightly above
const TSharedRef< FSlateFontMeasure > FontMeasureService = FSlateApplication::Get().GetRenderer()->GetFontMeasureService();
FVector2D TextSize = FontMeasureService->Measure(FrameString, SmallLayoutFont);
FVector2D TextOffset = Orientation == Orient_Horizontal ?
FVector2D(XPos - ( TextSize.X*0.5f ), FMath::Abs(InArgs.AllottedGeometry.GetLocalSize().Y - ( InArgs.MajorTickHeight + TextSize.Y )))
:
FVector2D(FMath::Abs(InArgs.AllottedGeometry.GetLocalSize().X - ( InArgs.MajorTickHeight + TextSize.X )), XPos - ( TextSize.Y*0.5f ));
FSlateDrawElement::MakeText(
OutDrawElements,
InArgs.StartLayer,
InArgs.AllottedGeometry.ToPaintGeometry( TextSize, FSlateLayoutTransform(TextOffset) ),
FrameString,
SmallLayoutFont,
InArgs.DrawEffects,
InArgs.TextColor
);
}
}
else if( !InArgs.bOnlyDrawMajorTicks )
{
// Compute the size of each tick mark. If we are half way between to visible values display a slightly larger tick mark
const float MinorTickHeight = AbsOffsetNum % HalfDivider == 0 ? 7.0f : 4.0f;
FVector2D Offset = Orientation == Orient_Horizontal ? FVector2D(XPos, FMath::Abs(InArgs.AllottedGeometry.GetLocalSize().Y - MinorTickHeight)) : FVector2D(FMath::Abs(InArgs.AllottedGeometry.GetLocalSize().X - MinorTickHeight), XPos);
FVector2D TickSize = Orientation == Orient_Horizontal ? FVector2D(1, MinorTickHeight) : FVector2D(MinorTickHeight, 1);
LinePoints[0] = FVector2D(1.0f,1.0f);
LinePoints[1] = TickSize;
// Draw each sub mark
FSlateDrawElement::MakeLines(
OutDrawElements,
InArgs.StartLayer,
InArgs.AllottedGeometry.ToPaintGeometry( TickSize, FSlateLayoutTransform(Offset) ),
LinePoints,
InArgs.DrawEffects,
InArgs.TickColor,
bAntiAliasLines
);
}
// Advance to next tick mark
++OffsetNum;
}
// Draw line that runs along the bottom of all the ticks.
{
LinePoints[0] = Orientation == Orient_Horizontal ? FVector2D(0, InArgs.AllottedGeometry.Size.Y) : FVector2D(InArgs.AllottedGeometry.Size.X, 0);
LinePoints[1] = FVector2D(InArgs.AllottedGeometry.Size.X, InArgs.AllottedGeometry.Size.Y);
// Draw each sub mark
FSlateDrawElement::MakeLines(
OutDrawElements,
InArgs.StartLayer,
InArgs.AllottedGeometry.ToPaintGeometry(),
LinePoints,
InArgs.DrawEffects,
InArgs.TickColor,
bAntiAliasLines
);
}
// Draw the line that shows where the cursor is.
if ( AbsoluteCursor.IsSet() )
{
FVector2D LocalCursor = InArgs.AllottedGeometry.AbsoluteToLocal(AbsoluteCursor.GetValue());
LinePoints[0] = Orientation == Orient_Horizontal ? FVector2D(LocalCursor.X, 0) : FVector2D(0, LocalCursor.Y);
LinePoints[1] = Orientation == Orient_Horizontal ? FVector2D(LocalCursor.X, InArgs.AllottedGeometry.Size.Y) : FVector2D(InArgs.AllottedGeometry.Size.X, LocalCursor.Y);
// Draw each sub mark
FSlateDrawElement::MakeLines(
OutDrawElements,
++InArgs.StartLayer,
InArgs.AllottedGeometry.ToPaintGeometry(),
LinePoints,
InArgs.DrawEffects,
FLinearColor(FColor(0x19, 0xD1, 0x19)),
bAntiAliasLines
);
}
return InArgs.StartLayer;
}
int32 SRuler::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
const bool bEnabled = bParentEnabled;
const ESlateDrawEffect DrawEffects = bEnabled ? ESlateDrawEffect::None : ESlateDrawEffect::DisabledEffect;
static const FSlateBrush* WhiteBrush = FCoreStyle::Get().GetBrush("GenericWhiteBox");
// Draw solid background
FSlateDrawElement::MakeBox(
OutDrawElements,
LayerId,
AllottedGeometry.ToPaintGeometry(),
WhiteBrush,
DrawEffects,
FLinearColor(FColor(48,48,48))
);
FVector2f LocalOrigin = AllottedGeometry.AbsoluteToLocal(AbsoluteOrigin);
float Scale = SlateToUnitScale < ScrubConstants::MinDisplayTickSpacing ? ScrubConstants::MinDisplayTickSpacing : SlateToUnitScale;
float Origin = Orientation == Orient_Horizontal ? LocalOrigin.X : LocalOrigin.Y;
float Min = 0 - ( Origin * SlateToUnitScale );
float Max = Orientation == Orient_Horizontal ? AllottedGeometry.Size.X : AllottedGeometry.Size.Y;
Max = (Max - Origin) * SlateToUnitScale;
TRange<float> LocalViewRange(Min, Max);
FScrubRangeToScreen RangeToScreen(LocalViewRange, Orientation == Orient_Horizontal ? AllottedGeometry.GetLocalSize().X : AllottedGeometry.GetLocalSize().Y);
const float MajorTickHeight = 9.0f;
FDrawTickArgs TickArgs;
TickArgs.AllottedGeometry = AllottedGeometry;
TickArgs.bOnlyDrawMajorTicks = false;
TickArgs.TickColor = FLinearColor(FColor(97, 97, 97));
TickArgs.TextColor = FLinearColor(FColor(165, 165, 165));
TickArgs.ClippingRect = MyCullingRect;
TickArgs.DrawEffects = DrawEffects;
TickArgs.StartLayer = LayerId + 1;
TickArgs.TickOffset = Orientation == Orient_Horizontal ? FMath::Abs(AllottedGeometry.GetLocalSize().Y - MajorTickHeight) : FMath::Abs(AllottedGeometry.GetLocalSize().X - MajorTickHeight);
TickArgs.MajorTickHeight = MajorTickHeight;
LayerId = DrawTicks(OutDrawElements, RangeToScreen, TickArgs);
return LayerId;
}
FReply SRuler::OnMouseButtonDown( const FGeometry& MyGeometry, const FPointerEvent& MouseEvent )
{
if ( MouseButtonDownHandler.IsBound() )
{
// If a handler is assigned, call it.
return MouseButtonDownHandler.Execute(MyGeometry, MouseEvent);
}
else
{
// otherwise the event is unhandled.
return FReply::Unhandled();
}
}
FReply SRuler::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
return FReply::Unhandled();
}
FReply SRuler::OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
return FReply::Unhandled();
}
FVector2D SRuler::ComputeDesiredSize( float ) const
{
return Orientation == Orient_Horizontal ? FVector2D(100, 18) : FVector2D(18, 100);
}
FReply SRuler::OnMouseWheel(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
return FReply::Unhandled();
}
#undef LOCTEXT_NAMESPACE