using Sandbox.UI; using SkiaSharp; namespace Sandbox; public static partial class TextRendering { /// /// We'll expose this at some point, but will probably be as Sandbox.TextBlock - and then need to think about ownership and caching /// internal class TextBlock : IDisposable { public Texture Texture; public TextFlag Flags; public Vector2 Clip; public FontSmooth Smooth; public bool IsEmpty; public RealTimeSince TimeSinceUsed; TextRendering.Scope Scope; Margin _effectMargin = default; public TextBlock( string text, Color color, string font, float size, int fontWeight, Vector2 clip, TextFlag flag, FontSmooth? smooth = FontSmooth.Auto ) { Assert.False( Application.IsHeadless ); Flags = flag; Clip = clip; Smooth = smooth ?? FontSmooth.Auto; Initialize( new TextRendering.Scope { Text = text, TextColor = color, FontName = font, FontSize = size, FontWeight = fontWeight } ); } public TextBlock( Scope scope, Vector2 clip, TextFlag flag, FontSmooth? smooth = FontSmooth.Auto ) { Assert.False( Application.IsHeadless ); Flags = flag; Clip = clip; Smooth = smooth ?? FontSmooth.Auto; Initialize( scope ); } public TextBlock() { } internal void Initialize( TextRendering.Scope scope ) { Scope = scope; IsEmpty = string.IsNullOrEmpty( Scope.Text ); _effectMargin = default; if ( scope.Outline.Enabled && scope.Outline.Size > 0 ) { _effectMargin.Left = MathF.Max( _effectMargin.Left, scope.Outline.Size ).CeilToInt(); _effectMargin.Right = MathF.Max( _effectMargin.Left, scope.Outline.Size ).CeilToInt(); _effectMargin.Top = MathF.Max( _effectMargin.Left, scope.Outline.Size ).CeilToInt(); _effectMargin.Bottom = MathF.Max( _effectMargin.Left, scope.Outline.Size ).CeilToInt(); } if ( scope.OutlineUnder.Enabled && scope.OutlineUnder.Size > 0 ) { _effectMargin.Left = MathF.Max( _effectMargin.Left, scope.OutlineUnder.Size ).CeilToInt(); _effectMargin.Right = MathF.Max( _effectMargin.Left, scope.OutlineUnder.Size ).CeilToInt(); _effectMargin.Top = MathF.Max( _effectMargin.Left, scope.OutlineUnder.Size ).CeilToInt(); _effectMargin.Bottom = MathF.Max( _effectMargin.Left, scope.OutlineUnder.Size ).CeilToInt(); } if ( scope.Shadow.Enabled ) { _effectMargin.Left = MathF.Max( _effectMargin.Left, scope.Shadow.Size + -scope.Shadow.Offset.x ).CeilToInt(); _effectMargin.Right = MathF.Max( _effectMargin.Right, scope.Shadow.Size + scope.Shadow.Offset.x ).CeilToInt(); _effectMargin.Top = MathF.Max( _effectMargin.Top, scope.Shadow.Size + -scope.Shadow.Offset.y ).CeilToInt(); _effectMargin.Bottom = MathF.Max( _effectMargin.Bottom, scope.Shadow.Size + scope.Shadow.Offset.y ).CeilToInt(); } if ( scope.ShadowUnder.Enabled ) { _effectMargin.Left = MathF.Max( _effectMargin.Left, scope.ShadowUnder.Size + -scope.ShadowUnder.Offset.x ).CeilToInt(); _effectMargin.Right = MathF.Max( _effectMargin.Right, scope.ShadowUnder.Size + scope.ShadowUnder.Offset.x ).CeilToInt(); _effectMargin.Top = MathF.Max( _effectMargin.Top, scope.ShadowUnder.Size + -scope.ShadowUnder.Offset.y ).CeilToInt(); _effectMargin.Bottom = MathF.Max( _effectMargin.Bottom, scope.ShadowUnder.Size + scope.ShadowUnder.Offset.y ).CeilToInt(); } // don't let shit get crazy _effectMargin.Left = MathF.Min( _effectMargin.Left, 512 ); _effectMargin.Right = MathF.Min( _effectMargin.Right, 512 ); _effectMargin.Top = MathF.Min( _effectMargin.Top, 512 ); _effectMargin.Bottom = MathF.Min( _effectMargin.Bottom, 512 ); } public virtual void Dispose() { Texture?.Dispose(); Texture = null; } Topten.RichTextKit.TextAlignment GetAlignment() { if ( Flags.Contains( TextFlag.Left ) ) return Topten.RichTextKit.TextAlignment.Left; if ( Flags.Contains( TextFlag.CenterHorizontally ) ) return Topten.RichTextKit.TextAlignment.Center; if ( Flags.Contains( TextFlag.Right ) ) return Topten.RichTextKit.TextAlignment.Right; return Topten.RichTextKit.TextAlignment.Left; } public void Render( SKCanvas canvas, Rect targetRect ) { if ( IsEmpty ) return; var block = new Topten.RichTextKit.TextBlock(); block.FontMapper = FontManager.Instance; block.Alignment = GetAlignment(); if ( Flags.Contains( TextFlag.SingleLine ) ) // should we remove any newlines? { block.MaxLines = 1; } if ( !Flags.Contains( TextFlag.DontClip ) ) { block.MaxWidth = targetRect.Width; block.MaxHeight = targetRect.Height; } var style = new Topten.RichTextKit.Style(); Scope.ToStyle( style ); block.AddText( Scope.Text, style ); var o = new Topten.RichTextKit.TextPaintOptions { Edging = Smooth switch { FontSmooth.Never => SKFontEdging.Alias, _ => SKFontEdging.Antialias, }, Hinting = SKFontHinting.Full }; if ( !IsEmpty ) { var rect = new Rect( 0, 0, block.MeasuredWidth, block.MeasuredHeight ); rect = targetRect.Align( rect.Size, Flags ); SKPoint drawPosition = new SKPoint( rect.Left, rect.Top ); block.Paint( canvas, drawPosition, o ); } } public void MakeReady() { TimeSinceUsed = 0; if ( Texture != null ) return; // todo - we could probably expose shadows and outlines.. but later down the road. var block = new Topten.RichTextKit.TextBlock(); block.FontMapper = FontManager.Instance; block.MaxWidth = Clip.x; block.MaxHeight = Clip.y; block.Alignment = GetAlignment(); if ( Flags.Contains( TextFlag.SingleLine ) ) // should we remove any newlines? { block.MaxLines = 1; } if ( Flags.Contains( TextFlag.DontClip ) ) { block.MaxWidth = null; block.MaxHeight = null; } // // Build text block // var style = new Topten.RichTextKit.Style(); Scope.ToStyle( style ); block.AddText( IsEmpty ? "." : Scope.Text, style ); // // Build Text // var pad = block.MeasuredPadding; int width = block.MeasuredWidth.CeilToInt().Clamp( 2, 4096 ); int height = block.MeasuredHeight.CeilToInt().Clamp( 2, 4096 ); if ( style.LetterSpacing < 0 ) width += Math.Abs( (int)MathF.Floor( style.LetterSpacing ) ); var marginEdge = _effectMargin.EdgeSize; width += marginEdge.x.CeilToInt(); height += marginEdge.y.CeilToInt(); using ( var bitmap = new SkiaSharp.SKBitmap( width, height, SkiaSharp.SKColorType.Rgba8888, SkiaSharp.SKAlphaType.Unpremul ) ) using ( var canvas = new SkiaSharp.SKCanvas( bitmap ) ) { var o = new Topten.RichTextKit.TextPaintOptions { Edging = Smooth switch { FontSmooth.Never => SKFontEdging.Alias, _ => SKFontEdging.Antialias, }, Hinting = SKFontHinting.Full }; // trying to prevent premultiply changing this to 0,0,0,0 SKColor clearColor = new( style.TextColor.Red, style.TextColor.Green, style.TextColor.Blue, 0 ); bitmap.Erase( clearColor ); if ( !IsEmpty ) { SKPoint drawPosition = new SKPoint( _effectMargin.Left - pad.Left, _effectMargin.Top - pad.Top ); block.Paint( canvas, drawPosition, o ); } // Always use the max number of mips var mips = (int)MathF.Log2( MathF.Min( width, height ) ) + 1; mips = mips.Clamp( 1, 8 ); Texture = Texture.Create( width, height, ImageFormat.RGBA8888 ) .WithName( "textblock" ) .WithData( bitmap.GetPixels(), width * height * bitmap.BytesPerPixel ) .WithDynamicUsage() .WithMips( mips ) .Finish(); } } } }