#include "UI/NeuStyle.h" #include "Core/Tuning/DescentTune_UI.h" #include "TheDescent.h" #include "Engine/Texture2D.h" #include "Fonts/FontMeasure.h" #include "Framework/Application/SlateApplication.h" #include "Brushes/SlateColorBrush.h" #include "Brushes/SlateRoundedBoxBrush.h" #include "Misc/CoreDelegates.h" #include "Rendering/SlateRenderer.h" #include "Styling/CoreStyle.h" namespace UiTune = Descent::Tune::UI; // --------------------------------------------------------------------------------------------------------- // Tokens // --------------------------------------------------------------------------------------------------------- namespace Neu { FLinearColor Hex(uint32 Rgb, float Alpha) { const FColor C(static_cast((Rgb >> 16) & 0xFF), static_cast((Rgb >> 8) & 0xFF), static_cast(Rgb & 0xFF), 255); FLinearColor L = FLinearColor::FromSRGBColor(C); L.A = Alpha; return L; } #define NEU_TOKEN(Name, Hex6) \ const FLinearColor& Name() { static const FLinearColor C = Hex(Hex6); return C; } NEU_TOKEN(Bg, 0x1a1f26) NEU_TOKEN(Bg2, 0x1d232b) NEU_TOKEN(Hi, 0x262e39) NEU_TOKEN(Lo, 0x0e1216) NEU_TOKEN(Txt, 0xcfe3ec) NEU_TOKEN(Dim, 0x7f95a3) NEU_TOKEN(Cyan, 0x62d9e6) NEU_TOKEN(Amber, 0xf5b95a) NEU_TOKEN(Red, 0xff6b57) NEU_TOKEN(Green, 0x72e3a5) NEU_TOKEN(Violet, 0xa58cff) #undef NEU_TOKEN FLinearColor WithAlpha(const FLinearColor& C, float A) { return FLinearColor(C.R, C.G, C.B, A); } FLinearColor Lerp(const FLinearColor& A, const FLinearColor& B, float T) { return FLinearColor(FMath::Lerp(A.R, B.R, T), FMath::Lerp(A.G, B.G, T), FMath::Lerp(A.B, B.B, T), FMath::Lerp(A.A, B.A, T)); } // ---- shadow parameters ------------------------------------------------------------------------------ float ShadowSigma(ENeuShadowScale Scale) { return Scale == ENeuShadowScale::Soft ? UiTune::ShadowSoftSigmaPx : UiTune::ShadowSmallSigmaPx; } float ShadowDarkOffset(ENeuShadowScale Scale) { return Scale == ENeuShadowScale::Soft ? UiTune::ShadowSoftDarkOffsetPx : UiTune::ShadowSmallDarkOffsetPx; } float ShadowLightOffset(ENeuShadowScale Scale) { return Scale == ENeuShadowScale::Soft ? UiTune::ShadowSoftLightOffsetPx : UiTune::ShadowSmallLightOffsetPx; } int32 ShadowReach(ENeuShadowScale Scale) { return FMath::CeilToInt(UiTune::ShadowReachSigmas * ShadowSigma(Scale)); } namespace { /** Signed distance to a rounded rectangle centred at (Cx, Cy) with half sizes (Hx, Hy) and corner radius R. Negative inside. */ float SdRoundRect(float Px, float Py, float Cx, float Cy, float Hx, float Hy, float R) { const float Qx = FMath::Abs(Px - Cx) - (Hx - R); const float Qy = FMath::Abs(Py - Cy) - (Hy - R); const float Ox = FMath::Max(Qx, 0.f); const float Oy = FMath::Max(Qy, 0.f); return FMath::Sqrt(Ox * Ox + Oy * Oy) + FMath::Min(FMath::Max(Qx, Qy), 0.f) - R; } /** The normal distribution function, as the logistic curve that follows it to within one percent. */ float Phi(float Z) { return 1.f / (1.f + FMath::Exp(-1.702f * Z)); } } FNeuShadowImage MakeShadowImage(ENeuShadowKind Kind, ENeuShadowScale Scale, float BodyW, float BodyH, float Radius) { FNeuShadowImage Img; const float W = FMath::Max(1.f, FMath::RoundToFloat(BodyW)); const float H = FMath::Max(1.f, FMath::RoundToFloat(BodyH)); const float R = FMath::Clamp(Radius, 0.f, FMath::Min(W, H) * 0.5f); const float Sigma = ShadowSigma(Scale); const int32 Reach = Kind == ENeuShadowKind::Outer ? ShadowReach(Scale) : 0; Img.Reach = Reach; Img.Width = static_cast(W) + 2 * Reach; Img.Height = static_cast(H) + 2 * Reach; Img.Pixels.SetNumUninitialized(Img.Width * Img.Height); const float Cx = Reach + W * 0.5f; const float Cy = Reach + H * 0.5f; float ShiftX = 0.f, ShiftY = 0.f; if (Kind == ENeuShadowKind::InnerDark) { ShiftX = ShiftY = ShadowDarkOffset(Scale); } else if (Kind == ENeuShadowKind::InnerLight) { ShiftX = ShiftY = -ShadowLightOffset(Scale); } for (int32 Y = 0; Y < Img.Height; ++Y) { for (int32 X = 0; X < Img.Width; ++X) { const float Px = X + 0.5f; const float Py = Y + 0.5f; float A; if (Kind == ENeuShadowKind::Outer) { const float Sd = SdRoundRect(Px, Py, Cx, Cy, W * 0.5f, H * 0.5f, R); A = Phi(-Sd / Sigma); A = FMath::Max(0.f, A - 0.01f) / 0.99f; } else { const float Mask = FMath::Clamp(0.5f - SdRoundRect(Px, Py, Cx, Cy, W * 0.5f, H * 0.5f, R), 0.f, 1.f); const float SdShifted = SdRoundRect(Px - ShiftX, Py - ShiftY, Cx, Cy, W * 0.5f, H * 0.5f, R); A = Mask * (1.f - Phi(-SdShifted / Sigma)); } Img.Pixels[Y * Img.Width + X] = FColor(255, 255, 255, static_cast(FMath::Clamp(FMath::RoundToInt(A * 255.f), 0, 255))); } } return Img; } float NineSliceMinBody(ENeuShadowKind Kind, ENeuShadowScale Scale, float Radius) { if (Kind == ENeuShadowKind::Outer) { return 2.f * Radius; } return 2.f * (Radius + FMath::Max(ShadowDarkOffset(Scale), ShadowLightOffset(Scale)) + FMath::CeilToFloat(2.f * ShadowSigma(Scale))); } // ---- text ---------------------------------------------------------------------------------------------- FSlateFontInfo Font(ENeuFont Weight, float Px) { const TCHAR* Face = Weight == ENeuFont::Bold ? TEXT("Bold") : (Weight == ENeuFont::Mono ? TEXT("Mono") : TEXT("Regular")); return FCoreStyle::GetDefaultFontStyle(Face, FMath::Max(1.f, Px * 0.75f)); } FVector2f MeasureText(const FString& Text, ENeuFont Weight, float Px) { if (Text.IsEmpty()) { return FVector2f(0.f, Px * 1.2f); } if (FSlateApplication::IsInitialized() && FSlateApplication::Get().GetRenderer()) { const TSharedRef M = FSlateApplication::Get().GetRenderer()->GetFontMeasureService(); const FVector2f Size = M->Measure(Text, Font(Weight, Px)); return Size; } // No renderer (headless tests): an estimate close enough for layout decisions. return FVector2f(Text.Len() * Px * (Weight == ENeuFont::Bold ? 0.58f : 0.54f), Px * 1.2f); } float FitTextPx(const FString& Text, ENeuFont Weight, float WantPx, float MinPx, float Width) { float Px = WantPx; while (Px > MinPx && MeasureText(Text, Weight, Px).X > Width) { Px = FMath::Max(MinPx, Px - 1.f); } return Px; } FString ElideText(const FString& Text, ENeuFont Weight, float Px, float Width) { if (MeasureText(Text, Weight, Px).X <= Width) { return Text; } FString Cut = Text; while (Cut.Len() > 1) { Cut.LeftChopInline(1, EAllowShrinking::No); const FString Try = Cut.TrimEnd() + TEXT("..."); if (MeasureText(Try, Weight, Px).X <= Width) { return Try; } } return TEXT("..."); } void WrapText(const FString& Text, ENeuFont Weight, float Px, float Width, int32 MaxLines, TArray& OutLines) { OutLines.Reset(); TArray Words; Text.ParseIntoArray(Words, TEXT(" "), true); FString Line; for (int32 I = 0; I < Words.Num(); ++I) { const FString Try = Line.IsEmpty() ? Words[I] : Line + TEXT(" ") + Words[I]; if (Line.IsEmpty() || MeasureText(Try, Weight, Px).X <= Width) { Line = Try; continue; } if (OutLines.Num() == MaxLines - 1) { // Last allowed line: everything left goes into it and is elided. FString Rest = Line; for (int32 J = I; J < Words.Num(); ++J) { Rest += TEXT(" ") + Words[J]; } OutLines.Add(ElideText(Rest, Weight, Px, Width)); return; } OutLines.Add(Line); Line = Words[I]; } if (!Line.IsEmpty()) { OutLines.Add(ElideText(Line, Weight, Px, Width)); } } } // --------------------------------------------------------------------------------------------------------- // FNeuStyle: textures and brushes // --------------------------------------------------------------------------------------------------------- namespace { FNeuStyle* GNeuStyle = nullptr; uint64 NeuShadowKey(ENeuShadowKind Kind, ENeuShadowScale Scale, float Radius, bool bNineSlice, int32 W, int32 H) { uint64 Key = static_cast(Kind); Key |= static_cast(Scale) << 2; Key |= static_cast(bNineSlice ? 1 : 0) << 3; Key |= static_cast(FMath::Clamp(FMath::RoundToInt(Radius * 2.f), 0, 1023)) << 4; Key |= static_cast(FMath::Clamp(W, 0, 16383)) << 14; Key |= static_cast(FMath::Clamp(H, 0, 16383)) << 28; return Key; } } FNeuStyle& FNeuStyle::Get() { if (!GNeuStyle) { GNeuStyle = new FNeuStyle(); GNeuStyle->bExitHooked = true; FCoreDelegates::OnEnginePreExit.AddLambda([]() { FNeuStyle::Shutdown(); }); } return *GNeuStyle; } void FNeuStyle::Shutdown() { delete GNeuStyle; GNeuStyle = nullptr; } void FNeuStyle::AddReferencedObjects(FReferenceCollector& Collector) { Collector.AddReferencedObjects(Textures); } UTexture2D* FNeuStyle::MakeTexture(const FNeuShadowImage& Image) { const TConstArrayView64 Bytes(reinterpret_cast(Image.Pixels.GetData()), static_cast(Image.Pixels.Num()) * 4); UTexture2D* Tex = UTexture2D::CreateTransient(Image.Width, Image.Height, PF_B8G8R8A8, NAME_None, Bytes); if (!Tex) { return nullptr; } Tex->SRGB = false; Tex->Filter = TF_Bilinear; Tex->AddressX = TA_Clamp; Tex->AddressY = TA_Clamp; Tex->NeverStream = true; Tex->UpdateResource(); Textures.Add(Tex); return Tex; } FNeuStyle::FShadowDraw FNeuStyle::GetShadow(ENeuShadowKind Kind, ENeuShadowScale Scale, FVector2f BodySize, float Radius) { FShadowDraw Draw; const float R = FMath::Clamp(FMath::RoundToFloat(Radius), 0.f, FMath::Min(BodySize.X, BodySize.Y) * 0.5f); const int32 Reach = Kind == ENeuShadowKind::Outer ? Neu::ShadowReach(Scale) : 0; const float MinBody = Neu::NineSliceMinBody(Kind, Scale, R); const bool bNine = BodySize.X >= MinBody && BodySize.Y >= MinBody; // Outer shadows are drawn bigger than the body by Reach on every side; inner ones exactly over it. Draw.Offset = FVector2f(-static_cast(Reach), -static_cast(Reach)); Draw.Size = BodySize + FVector2f(2.f * Reach, 2.f * Reach); uint64 Key; FNeuShadowImage Image; float CanonBody = 0.f; if (bNine) { // Canonical body: two corners and a two-pixel middle strip, so the strip is a straight stretch of edge. const float Corner = Kind == ENeuShadowKind::Outer ? R : MinBody * 0.5f; CanonBody = 2.f * Corner + 2.f; Key = NeuShadowKey(Kind, Scale, R, true, 0, 0); } else { Key = NeuShadowKey(Kind, Scale, R, false, FMath::RoundToInt(BodySize.X), FMath::RoundToInt(BodySize.Y)); } if (FEntry* Found = Shadows.Find(Key)) { Draw.Brush = Found->Brush.Get(); return Draw; } if (!bNine && Shadows.Num() >= UiTune::ShadowCacheLimit * 2) { return Draw; // cache full of exact-size textures: the caller draws without this shadow } Image = bNine ? Neu::MakeShadowImage(Kind, Scale, CanonBody, CanonBody, R) : Neu::MakeShadowImage(Kind, Scale, BodySize.X, BodySize.Y, R); UTexture2D* Tex = MakeTexture(Image); if (!Tex) { return Draw; } FEntry Entry; Entry.Brush = MakeShared(); FSlateBrush& B = *Entry.Brush; B.SetResourceObject(Tex); B.ImageSize = FVector2f(static_cast(Image.Width), static_cast(Image.Height)); B.TintColor = FLinearColor::White; if (bNine) { B.DrawAs = ESlateBrushDrawType::Box; const float Corner = static_cast(Reach) + (Kind == ENeuShadowKind::Outer ? R : MinBody * 0.5f); B.Margin = FMargin(Corner / Image.Width, Corner / Image.Height, Corner / Image.Width, Corner / Image.Height); } else { B.DrawAs = ESlateBrushDrawType::Image; } Draw.Brush = Entry.Brush.Get(); Shadows.Add(Key, MoveTemp(Entry)); return Draw; } const FSlateBrush* FNeuStyle::RoundBrush(float Radius) { const uint32 Key = static_cast(FMath::Clamp(FMath::RoundToInt(Radius * 4.f), 0, 100000)); if (TSharedPtr* Found = Rounds.Find(Key)) { return Found->Get(); } TSharedPtr Brush = MakeShared(FLinearColor::White, Key / 4.f); const FSlateBrush* Ptr = Brush.Get(); Rounds.Add(Key, MoveTemp(Brush)); return Ptr; } const FSlateBrush* FNeuStyle::OutlineBrush(float Radius, float Width, const FLinearColor& Color) { const FColor Packed = Color.ToFColor(true); const uint64 Key = static_cast(FMath::Clamp(FMath::RoundToInt(Radius * 4.f), 0, 65535)) | (static_cast(FMath::Clamp(FMath::RoundToInt(Width * 4.f), 0, 255)) << 16) | (static_cast(Packed.DWColor()) << 24); if (TSharedPtr* Found = Outlines.Find(Key)) { return Found->Get(); } TSharedPtr Brush = MakeShared(FLinearColor::Transparent, Radius, Color, Width); const FSlateBrush* Ptr = Brush.Get(); Outlines.Add(Key, MoveTemp(Brush)); return Ptr; } // --------------------------------------------------------------------------------------------------------- // FNeuPainter // --------------------------------------------------------------------------------------------------------- FNeuPainter::FNeuPainter(FSlateWindowElementList& InOut, const FGeometry& InGeo, int32 InLayer, float InAlpha) : Out(InOut), Geo(InGeo), Layer(InLayer), Alpha(InAlpha), Style(FNeuStyle::Get()) { } FPaintGeometry FNeuPainter::PG(FVector2f Pos, FVector2f Size) const { return Geo.ToPaintGeometry(Size, FSlateLayoutTransform(Pos)); } void FNeuPainter::RoundRect(FVector2f Pos, FVector2f Size, float Radius, const FLinearColor& Color) { if (Size.X <= 0.f || Size.Y <= 0.f) { return; } const float R = FMath::Clamp(Radius, 0.f, FMath::Min(Size.X, Size.Y) * 0.5f); FSlateDrawElement::MakeBox(Out, Layer++, PG(Pos, Size), Style.RoundBrush(R), ESlateDrawEffect::None, Mul(Color)); } void FNeuPainter::RoundRectGradient(FVector2f Pos, FVector2f Size, float Radius, const FLinearColor& Top, const FLinearColor& Bottom) { if (Size.X <= 0.f || Size.Y <= 0.f) { return; } const float R = FMath::Clamp(Radius, 0.f, FMath::Min(Size.X, Size.Y) * 0.5f); TArray Stops; Stops.Add(FSlateGradientStop(FVector2f(0.f, 0.f), Mul(Top))); Stops.Add(FSlateGradientStop(FVector2f(0.f, Size.Y), Mul(Bottom))); // Orient_Horizontal is the one whose colour changes from top to bottom (the stops are spread along Y). FSlateDrawElement::MakeGradient(Out, Layer++, PG(Pos, Size), MoveTemp(Stops), Orient_Horizontal, ESlateDrawEffect::None, FVector4f(R, R, R, R)); } void FNeuPainter::RoundRectOutline(FVector2f Pos, FVector2f Size, float Radius, float Width, const FLinearColor& Color) { if (Size.X <= 0.f || Size.Y <= 0.f) { return; } const float R = FMath::Clamp(Radius, 0.f, FMath::Min(Size.X, Size.Y) * 0.5f); FSlateDrawElement::MakeBox(Out, Layer++, PG(Pos, Size), Style.OutlineBrush(R, Width, Mul(Color)), ESlateDrawEffect::None, FLinearColor::Transparent); } void FNeuPainter::Disc(FVector2f Center, float Radius, const FLinearColor& Color) { RoundRect(Center - FVector2f(Radius, Radius), FVector2f(2.f * Radius, 2.f * Radius), Radius, Color); } void FNeuPainter::Extruded(FVector2f Pos, FVector2f Size, float Radius, bool bSmall, const FLinearColor* Body) { const ENeuShadowScale Scale = bSmall ? ENeuShadowScale::Small : ENeuShadowScale::Soft; const float R = FMath::Clamp(Radius, 0.f, FMath::Min(Size.X, Size.Y) * 0.5f); const FNeuStyle::FShadowDraw Shadow = Style.GetShadow(ENeuShadowKind::Outer, Scale, Size, R); if (Shadow.Brush) { const float Dark = Neu::ShadowDarkOffset(Scale); const float Light = Neu::ShadowLightOffset(Scale); FSlateDrawElement::MakeBox(Out, Layer++, PG(Pos + Shadow.Offset + FVector2f(Dark, Dark), Shadow.Size), Shadow.Brush, ESlateDrawEffect::None, Mul(Neu::WithAlpha(Neu::Lo(), UiTune::ShadowDarkStrength))); FSlateDrawElement::MakeBox(Out, Layer++, PG(Pos + Shadow.Offset - FVector2f(Light, Light), Shadow.Size), Shadow.Brush, ESlateDrawEffect::None, Mul(Neu::WithAlpha(Neu::Hi(), UiTune::ShadowLightStrength))); } const FLinearColor Base = Body ? *Body : Neu::Bg(); const FLinearColor Top(Base.R * (1.f + UiTune::BodyGradient), Base.G * (1.f + UiTune::BodyGradient), Base.B * (1.f + UiTune::BodyGradient), Base.A); const FLinearColor Bottom(Base.R * (1.f - UiTune::BodyGradient), Base.G * (1.f - UiTune::BodyGradient), Base.B * (1.f - UiTune::BodyGradient), Base.A); RoundRectGradient(Pos, Size, R, Top, Bottom); } void FNeuPainter::Inset(FVector2f Pos, FVector2f Size, float Radius, bool bSmall, const FLinearColor* Body) { const ENeuShadowScale Scale = bSmall ? ENeuShadowScale::Small : ENeuShadowScale::Soft; const float R = FMath::Clamp(Radius, 0.f, FMath::Min(Size.X, Size.Y) * 0.5f); RoundRect(Pos, Size, R, Body ? *Body : Neu::Bg2()); const FNeuStyle::FShadowDraw DarkSh = Style.GetShadow(ENeuShadowKind::InnerDark, Scale, Size, R); if (DarkSh.Brush) { FSlateDrawElement::MakeBox(Out, Layer++, PG(Pos, Size), DarkSh.Brush, ESlateDrawEffect::None, Mul(Neu::WithAlpha(Neu::Lo(), UiTune::ShadowDarkStrength))); } const FNeuStyle::FShadowDraw LightSh = Style.GetShadow(ENeuShadowKind::InnerLight, Scale, Size, R); if (LightSh.Brush) { FSlateDrawElement::MakeBox(Out, Layer++, PG(Pos, Size), LightSh.Brush, ESlateDrawEffect::None, Mul(Neu::WithAlpha(Neu::Hi(), UiTune::ShadowLightStrength))); } } void FNeuPainter::ExtrudedDisc(FVector2f Center, float Diameter, bool bSmall, const FLinearColor* Body) { Extruded(Center - FVector2f(Diameter * 0.5f, Diameter * 0.5f), FVector2f(Diameter, Diameter), Diameter * 0.5f, bSmall, Body); } void FNeuPainter::InsetDisc(FVector2f Center, float Diameter, bool bSmall, const FLinearColor* Body) { Inset(Center - FVector2f(Diameter * 0.5f, Diameter * 0.5f), FVector2f(Diameter, Diameter), Diameter * 0.5f, bSmall, Body); } void FNeuPainter::Line(FVector2f A, FVector2f B, float Thickness, const FLinearColor& Color) { TArray Pts; Pts.Add(A); Pts.Add(B); FSlateDrawElement::MakeLines(Out, Layer++, Geo.ToPaintGeometry(), MoveTemp(Pts), ESlateDrawEffect::None, Mul(Color), true, Thickness); } void FNeuPainter::Polyline(const TArray& Points, float Thickness, const FLinearColor& Color, bool bClosed) { if (Points.Num() < 2) { return; } TArray Pts = Points; if (bClosed) { Pts.Add(Points[0]); } FSlateDrawElement::MakeLines(Out, Layer++, Geo.ToPaintGeometry(), MoveTemp(Pts), ESlateDrawEffect::None, Mul(Color), true, Thickness); } void FNeuPainter::Polygon(const TArray& Points, const FLinearColor& Color) { if (Points.Num() < 3) { return; } const FLinearColor C = Mul(Color); const FColor Packed = C.ToFColor(true); const FSlateRenderTransform RenderTransform = Geo.GetAccumulatedRenderTransform(); TArray Verts; TArray Indices; for (const FVector2f& P : Points) { Verts.Add(FSlateVertex::Make(RenderTransform, P, FVector2f(0.f, 0.f), Packed)); } for (int32 I = 1; I + 1 < Points.Num(); ++I) { Indices.Add(0); Indices.Add(static_cast(I)); Indices.Add(static_cast(I + 1)); } FSlateDrawElement::MakeCustomVerts(Out, Layer++, FSlateResourceHandle(), Verts, Indices, nullptr, 0, 0); // A one-unit anti-aliased rim of the same colour smooths the stair-stepped edge of the fill. Polyline(Points, 1.f, Color, true); } void FNeuPainter::Arc(FVector2f Center, float Radius, float StartDeg, float SweepDeg, float Thickness, const FLinearColor& Color, bool bRoundCaps) { if (SweepDeg <= 0.f || Radius <= 0.f) { return; } const int32 Steps = FMath::Clamp(FMath::CeilToInt(SweepDeg / 2.f), 2, 200); TArray Pts; Pts.Reserve(Steps + 1); for (int32 I = 0; I <= Steps; ++I) { const float A = FMath::DegreesToRadians(StartDeg + SweepDeg * I / Steps); Pts.Add(Center + FVector2f(FMath::Cos(A), FMath::Sin(A)) * Radius); } const FVector2f First = Pts[0]; const FVector2f Last = Pts.Last(); FSlateDrawElement::MakeLines(Out, Layer++, Geo.ToPaintGeometry(), MoveTemp(Pts), ESlateDrawEffect::None, Mul(Color), true, Thickness); if (bRoundCaps) { Disc(First, Thickness * 0.5f, Color); Disc(Last, Thickness * 0.5f, Color); } } void FNeuPainter::ArcGlow(FVector2f Center, float Radius, float StartDeg, float SweepDeg, float Thickness, const FLinearColor& Color) { for (int32 I = 3; I >= 1; --I) { Arc(Center, Radius, StartDeg, SweepDeg, Thickness + I * 6.f, Neu::WithAlpha(Color, Color.A * 0.07f), true); } } void FNeuPainter::Text(const FString& Str, FVector2f Pos, ENeuFont Weight, float Px, const FLinearColor& Color) { if (Str.IsEmpty()) { return; } const FVector2f Ext = Neu::MeasureText(Str, Weight, Px); FSlateDrawElement::MakeText(Out, Layer++, PG(Pos, Ext), Str, Neu::Font(Weight, Px), ESlateDrawEffect::None, Mul(Color)); } void FNeuPainter::TextBox(const FString& Str, FVector2f Pos, FVector2f Size, ENeuFont Weight, float Px, const FLinearColor& Color, ENeuAlign HAlign, ENeuVAlign VAlign, float MinPx) { if (Str.IsEmpty() || Size.X <= 0.f) { return; } float UsePx = Px; if (MinPx > 0.f) { UsePx = Neu::FitTextPx(Str, Weight, Px, MinPx, Size.X); } FString Shown = Str; if (Neu::MeasureText(Shown, Weight, UsePx).X > Size.X + 0.5f) { Shown = Neu::ElideText(Shown, Weight, UsePx, Size.X); } const FVector2f Ext = Neu::MeasureText(Shown, Weight, UsePx); float X = Pos.X; if (HAlign == ENeuAlign::Center) { X += (Size.X - Ext.X) * 0.5f; } else if (HAlign == ENeuAlign::Right) { X += Size.X - Ext.X; } float Y = Pos.Y; if (VAlign == ENeuVAlign::Center) { Y += (Size.Y - Ext.Y) * 0.5f; } else if (VAlign == ENeuVAlign::Bottom) { Y += Size.Y - Ext.Y; } Text(Shown, FVector2f(FMath::RoundToFloat(X), FMath::RoundToFloat(Y)), Weight, UsePx, Color); }