#pragma once #include "CoreMinimal.h" #include "Fonts/SlateFontInfo.h" #include "Layout/Geometry.h" #include "Rendering/DrawElements.h" #include "Styling/SlateBrush.h" #include "UObject/GCObject.h" class UTexture2D; /** * The neumorphic style kit (DESIGN_UE.md section 8.5, requirement V13): tokens, fonts, the generated shadow textures and * a painter that draws extruded and inset surfaces with dual light and dark shadows. * * Slate has no blurred box shadow, so the shadows are textures made in code at first use: a rounded rectangle blurred by * a Gaussian (evaluated analytically from the rectangle's signed distance, so there is no convolution). An extruded * surface draws a dark shadow down-right and a light shadow up-left under its body. An inset well draws two "inner * shadow" textures over its body: dark along the top-left rim, light along the bottom-right rim. * * Big rectangles use a 9-slice texture of a canonical size; rectangles too small for it (circles, slots) get a texture * made for their exact size, cached. No texture or font asset exists anywhere in the project. */ enum class ENeuFont : uint8 { Regular, Bold, Mono }; enum class ENeuAlign : uint8 { Left, Center, Right }; enum class ENeuVAlign : uint8 { Top, Center, Bottom }; /** Soft: panels and windows. Small: slots, buttons, anything under about 70 units. */ enum class ENeuShadowScale : uint8 { Soft, Small }; enum class ENeuShadowKind : uint8 { Outer, InnerDark, InnerLight }; /** A shadow image, white with the shadow amount in alpha. Pure data so tests can examine it. */ struct FNeuShadowImage { int32 Width = 0; int32 Height = 0; /** Outer: how far the image extends past the body on every side. Inner: 0 (the image is the body). */ int32 Reach = 0; TArray Pixels; // BGRA8, row-major, top row first FColor At(int32 X, int32 Y) const { return Pixels[Y * Width + X]; } }; namespace Neu { // ---- tokens (section 8.5, from v2's CSS) ----------------------------------------------------------------- /** sRGB hex (0xRRGGBB) to the linear colour Slate tints with. */ THEDESCENT_API FLinearColor Hex(uint32 Rgb, float Alpha = 1.f); THEDESCENT_API const FLinearColor& Bg(); // #1a1f26 THEDESCENT_API const FLinearColor& Bg2(); // #1d232b THEDESCENT_API const FLinearColor& Hi(); // #262e39 light shadow THEDESCENT_API const FLinearColor& Lo(); // #0e1216 dark shadow THEDESCENT_API const FLinearColor& Txt(); // #cfe3ec THEDESCENT_API const FLinearColor& Dim(); // #7f95a3 THEDESCENT_API const FLinearColor& Cyan(); // #62d9e6 THEDESCENT_API const FLinearColor& Amber(); // #f5b95a THEDESCENT_API const FLinearColor& Red(); // #ff6b57 THEDESCENT_API const FLinearColor& Green(); // #72e3a5 THEDESCENT_API const FLinearColor& Violet(); // #a58cff THEDESCENT_API FLinearColor WithAlpha(const FLinearColor& C, float A); THEDESCENT_API FLinearColor Lerp(const FLinearColor& A, const FLinearColor& B, float T); // ---- shadow images ------------------------------------------------------------------------------------- THEDESCENT_API float ShadowSigma(ENeuShadowScale Scale); THEDESCENT_API float ShadowDarkOffset(ENeuShadowScale Scale); THEDESCENT_API float ShadowLightOffset(ENeuShadowScale Scale); /** Outer shadow reach in pixels (ShadowReachSigmas * sigma, rounded up). */ THEDESCENT_API int32 ShadowReach(ENeuShadowScale Scale); /** * The shadow of a rounded rectangle of BodyW x BodyH with corner Radius. * Outer: (BodyW + 2 Reach) x (BodyH + 2 Reach), alpha = the blurred body. * InnerDark / InnerLight: BodyW x BodyH, alpha = (1 - blurred body shifted by the offset) inside the body, 0 outside. */ THEDESCENT_API FNeuShadowImage MakeShadowImage(ENeuShadowKind Kind, ENeuShadowScale Scale, float BodyW, float BodyH, float Radius); /** The smallest body (both sides) for which a 9-slice shadow of this kind and radius is exact. Smaller bodies get an exact texture. */ THEDESCENT_API float NineSliceMinBody(ENeuShadowKind Kind, ENeuShadowScale Scale, float Radius); // ---- text ------------------------------------------------------------------------------------------------ /** Font of a pixel size (Slate sizes are points at 96 dpi: px * 0.75). */ THEDESCENT_API FSlateFontInfo Font(ENeuFont Weight, float Px); /** Width and height in UI units. Falls back to an estimate when Slate has no renderer (headless tests). */ THEDESCENT_API FVector2f MeasureText(const FString& Text, ENeuFont Weight, float Px); /** Largest px (not above WantPx, not below MinPx) at which Text fits in Width, or MinPx if none does. */ THEDESCENT_API float FitTextPx(const FString& Text, ENeuFont Weight, float WantPx, float MinPx, float Width); /** Text shortened with "..." so that it fits Width at the given size. */ THEDESCENT_API FString ElideText(const FString& Text, ENeuFont Weight, float Px, float Width); /** Word-wraps Text to Width, at most MaxLines lines (the last is elided). */ THEDESCENT_API void WrapText(const FString& Text, ENeuFont Weight, float Px, float Width, int32 MaxLines, TArray& OutLines); } /** Owns the generated textures and the brushes. One per process, created at first use on the game thread. */ class THEDESCENT_API FNeuStyle : public FGCObject { public: static FNeuStyle& Get(); /** Releases the textures (called on engine exit). Safe to call twice. */ static void Shutdown(); /** Where and how big to draw a shadow so that it lines up with a body at (0,0) of the given size. */ struct FShadowDraw { const FSlateBrush* Brush = nullptr; FVector2f Offset = FVector2f::ZeroVector; // top-left of the shadow image relative to the body's top-left (before the directional shift) FVector2f Size = FVector2f::ZeroVector; }; FShadowDraw GetShadow(ENeuShadowKind Kind, ENeuShadowScale Scale, FVector2f BodySize, float Radius); /** White rounded-box brush (tint it when drawing). Radius 0 gives a plain rectangle. */ const FSlateBrush* RoundBrush(float Radius); /** Rounded box with a transparent fill and an outline of the given width and colour (the colour lives in the brush; draw it with a transparent tint). */ const FSlateBrush* OutlineBrush(float Radius, float Width, const FLinearColor& Color); int32 NumShadowTextures() const { return Textures.Num(); } // FGCObject virtual void AddReferencedObjects(FReferenceCollector& Collector) override; virtual FString GetReferencerName() const override { return TEXT("FNeuStyle"); } private: FNeuStyle() = default; struct FEntry { TSharedPtr Brush; FVector2f Offset = FVector2f::ZeroVector; FVector2f Size = FVector2f::ZeroVector; }; FEntry& FindOrMakeEntry(uint64 Key, const FNeuShadowImage& Image, bool bNineSlice, float NineMarginPx, float CornerAnchor); UTexture2D* MakeTexture(const FNeuShadowImage& Image); TMap Shadows; TArray> Textures; TMap> Rounds; TMap> Outlines; bool bExitHooked = false; }; /** * Draws in the local space of a widget's geometry. Every call takes the next layer, so drawing order is call order. * Construct one at the top of OnPaint, draw, and return Painter.Layer (+1) from OnPaint. */ struct THEDESCENT_API FNeuPainter { FNeuPainter(FSlateWindowElementList& InOut, const FGeometry& InGeo, int32 InLayer, float InAlpha = 1.f); FSlateWindowElementList& Out; const FGeometry& Geo; int32 Layer; /** Multiplies every colour's alpha (disabled widgets, fades). */ float Alpha; FNeuStyle& Style; FPaintGeometry PG(FVector2f Pos, FVector2f Size) const; FLinearColor Mul(const FLinearColor& C) const { return FLinearColor(C.R, C.G, C.B, C.A * Alpha); } // ---- the neumorphic surfaces ---- /** A raised card. Body defaults to Bg(). */ void Extruded(FVector2f Pos, FVector2f Size, float Radius = 16.f, bool bSmall = false, const FLinearColor* Body = nullptr); /** A sunken well. Body defaults to Bg2(). */ void Inset(FVector2f Pos, FVector2f Size, float Radius = 16.f, bool bSmall = false, const FLinearColor* Body = nullptr); /** A raised or sunken circle of the given diameter centred at Center. */ void ExtrudedDisc(FVector2f Center, float Diameter, bool bSmall = false, const FLinearColor* Body = nullptr); void InsetDisc(FVector2f Center, float Diameter, bool bSmall = false, const FLinearColor* Body = nullptr); // ---- primitives ---- void RoundRect(FVector2f Pos, FVector2f Size, float Radius, const FLinearColor& Color); void RoundRectGradient(FVector2f Pos, FVector2f Size, float Radius, const FLinearColor& Top, const FLinearColor& Bottom); void RoundRectOutline(FVector2f Pos, FVector2f Size, float Radius, float Width, const FLinearColor& Color); void Disc(FVector2f Center, float Radius, const FLinearColor& Color); void Line(FVector2f A, FVector2f B, float Thickness, const FLinearColor& Color); void Polyline(const TArray& Points, float Thickness, const FLinearColor& Color, bool bClosed = false); /** A convex polygon, filled, with a one-unit anti-aliased edge. */ void Polygon(const TArray& Points, const FLinearColor& Color); /** Arc from StartDeg sweeping SweepDeg clockwise on screen (0 = right, 90 = down). Round caps optional. */ void Arc(FVector2f Center, float Radius, float StartDeg, float SweepDeg, float Thickness, const FLinearColor& Color, bool bRoundCaps); /** A soft glow: the arc drawn wider and fainter, three times. */ void ArcGlow(FVector2f Center, float Radius, float StartDeg, float SweepDeg, float Thickness, const FLinearColor& Color); // ---- text ---- /** Text with its top-left at Pos. */ void Text(const FString& Str, FVector2f Pos, ENeuFont Weight, float Px, const FLinearColor& Color); /** Text in a box: aligned, shrunk down to MinPx if too wide, then elided. */ void TextBox(const FString& Str, FVector2f Pos, FVector2f Size, ENeuFont Weight, float Px, const FLinearColor& Color, ENeuAlign HAlign = ENeuAlign::Left, ENeuVAlign VAlign = ENeuVAlign::Center, float MinPx = 0.f); };