mirror of
https://github.com/Facepunch/sbox-public.git
synced 2025-12-23 14:38:13 -05:00
* Add TextureFlags * Add TextureFlags.PremultipliedAlpha in text block and webpanel textures * Add BlendMode.PremultipliedAlpha * Add panel to PauseModal
709 lines
20 KiB
C#
709 lines
20 KiB
C#
using Sandbox.Html;
|
|
using Sandbox.Rendering;
|
|
using SkiaSharp;
|
|
using System.Buffers;
|
|
using Topten.RichTextKit;
|
|
|
|
namespace Sandbox.UI;
|
|
|
|
internal sealed class TextBlock : IDisposable
|
|
{
|
|
[ConVar( ConVarFlags.Protected, Help = "Enable rendering text to textures" )]
|
|
public static bool ui_rendertext { get; set; } = true;
|
|
|
|
public string Text { get; internal set; }
|
|
|
|
public bool ShouldDrawSelection = false;
|
|
public int SelectionStart { get; internal set; } = 0;
|
|
public int SelectionEnd { get; internal set; } = 0;
|
|
public Color SelectionColor { get; set; } = Color.Cyan.WithAlpha( 0.39f );
|
|
public bool IsTruncated { get; internal set; }
|
|
public bool NoWrap { get; internal set; }
|
|
public bool IsHtml { get; internal set; }
|
|
|
|
public Func<INode, Styles> LookupStyles { get; set; }
|
|
|
|
public Vector2 BlockSize;
|
|
|
|
Texture Texture;
|
|
|
|
// we keep the last texture around incase we can re-use it
|
|
Texture LastTexture;
|
|
|
|
internal void SetText( string text )
|
|
{
|
|
Text = text;
|
|
IsHtml = false;
|
|
}
|
|
|
|
Sandbox.Html.Node htmlNode;
|
|
|
|
internal void SetHtml( string text )
|
|
{
|
|
if ( IsHtml && Text == text ) return;
|
|
|
|
Text = text;
|
|
IsHtml = true;
|
|
|
|
try
|
|
{
|
|
htmlNode = default;
|
|
htmlNode = Sandbox.Html.Node.Parse( Text );
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
public void Dirty()
|
|
{
|
|
FontHash = default;
|
|
}
|
|
|
|
Topten.RichTextKit.TextBlock Block;
|
|
Topten.RichTextKit.Style Style;
|
|
Topten.RichTextKit.TextGradient Gradient;
|
|
|
|
int FontHash;
|
|
//int ParentHash;
|
|
|
|
float FontSize;
|
|
int? FontWeight;
|
|
TextAlign TextAlign;
|
|
TextOverflow TextOverflow;
|
|
FilterMode TextFilter;
|
|
TextDecoration TextDecoration;
|
|
FontStyle FontStyle;
|
|
WordBreak WordBreak;
|
|
TextTransform? TextTransform;
|
|
Length? LetterSpacing;
|
|
Length? WordSpacing;
|
|
Length? LineHeight;
|
|
Align AlignItems;
|
|
WhiteSpace? WhiteSpace;
|
|
GradientInfo GradientInfo;
|
|
FontSmooth Smooth;
|
|
|
|
Margin EffectMargin;
|
|
|
|
|
|
Dictionary<int, Vector2> SizeCache = new Dictionary<int, Vector2>();
|
|
|
|
public Vector2 Measure( float width, float height )
|
|
{
|
|
if ( !float.IsNaN( width ) ) width = width.CeilToInt();
|
|
if ( !float.IsNaN( height ) ) height = height.CeilToInt();
|
|
|
|
var hash = (int)width;
|
|
if ( SizeCache.TryGetValue( hash, out var size ) )
|
|
return size;
|
|
|
|
Block.MaxWidth = float.IsNaN( width ) ? null : (width + 1);
|
|
|
|
if ( TextOverflow != TextOverflow.None )
|
|
{
|
|
Block.MaxHeight = float.IsNaN( height ) ? null : (height + 1);
|
|
}
|
|
|
|
var s = new Vector2( Block.MeasuredWidth.CeilToInt(), Block.MeasuredHeight.CeilToInt() );
|
|
|
|
SizeCache[hash] = s;
|
|
|
|
return s;
|
|
}
|
|
|
|
void WaitTextureReady()
|
|
{
|
|
if ( TextureRebuild == null ) return;
|
|
using var perfScope = Performance.Scope( "TextBlock.WaitRebuild" );
|
|
TextureRebuild.Wait();
|
|
TextureRebuild = null;
|
|
}
|
|
|
|
RenderAttributes textAttr = new RenderAttributes();
|
|
|
|
/// <summary>
|
|
/// Proper Rendering
|
|
/// </summary>
|
|
internal void Render( PanelRenderer renderer, ref RenderState state, Styles currentStyle, Rect textrect, float opacity )
|
|
{
|
|
WaitTextureReady();
|
|
|
|
if ( Texture is null ) return;
|
|
if ( BlockSize == 0 ) return;
|
|
if ( !PanelRenderer.ui_drawtext ) return;
|
|
|
|
if ( currentStyle.TextAlign == TextAlign.Center )
|
|
{
|
|
textrect.Left += (textrect.Width - BlockSize.x) * 0.5f;
|
|
}
|
|
else if ( currentStyle.TextAlign == TextAlign.Right )
|
|
{
|
|
textrect.Left = textrect.Right - BlockSize.x;
|
|
}
|
|
|
|
if ( currentStyle.AlignItems == Align.Center )
|
|
{
|
|
textrect.Top += (textrect.Height - BlockSize.y) * 0.5f;
|
|
}
|
|
else if ( currentStyle.AlignItems == Align.FlexEnd )
|
|
{
|
|
textrect.Top = textrect.Bottom - BlockSize.y;
|
|
}
|
|
|
|
textrect.Size = Texture.Size;
|
|
textrect.Position -= EffectMargin.Position;
|
|
|
|
var color = Color.White;
|
|
color.a *= opacity;
|
|
|
|
if ( color.a <= 0 ) return;
|
|
|
|
var bm = renderer.OverrideBlendMode;
|
|
|
|
if ( bm == BlendMode.Normal && Texture.Flags.Contains( TextureFlags.PremultipliedAlpha ) )
|
|
bm = BlendMode.PremultipliedAlpha;
|
|
|
|
textAttr.Set( "BoxPosition", textrect.Position );
|
|
textAttr.Set( "BoxSize", textrect.Size );
|
|
|
|
textAttr.Set( "TextureIndex", Texture.Index );
|
|
textAttr.Set( "SamplerIndex", SamplerState.GetBindlessIndex( new SamplerState() { Filter = TextFilter } ) );
|
|
textAttr.SetComboEnum( "D_BLENDMODE", bm );
|
|
|
|
Graphics.DrawQuad( textrect.Floor(), Material.UI.Text, color, textAttr );
|
|
}
|
|
|
|
|
|
public Rect CaretRect( int caretPosition )
|
|
{
|
|
var codepoint = CaretToCodePointIndex( caretPosition );
|
|
|
|
// Skias caret includes newlines however for rendering, we don't want this
|
|
// It also appears AltPosition is absolutely fucked and changes nothing
|
|
var cp = new CaretPosition { AltPosition = false, CodePointIndex = codepoint };
|
|
var pos = Block.GetCaretInfo( cp );
|
|
|
|
float xPosition = pos.CaretRectangle.Left;
|
|
float yPosition = pos.CaretRectangle.Top;
|
|
|
|
if ( codepoint > 0 && codepoint == Block.Length && Text.Length > 0 && Text[^1] == '\n' )
|
|
{
|
|
xPosition = 0;
|
|
yPosition += Block.Lines[pos.LineIndex].Height;
|
|
}
|
|
|
|
return new Rect( xPosition, yPosition, pos.CaretRectangle.Width, pos.CaretRectangle.Height );
|
|
}
|
|
|
|
public int GetLetterAt( Vector2 pos )
|
|
{
|
|
if ( Block == null ) return -1;
|
|
|
|
var result = Block.HitTest( pos.x, pos.y );
|
|
|
|
return Block.LookupCaretIndex( result.ClosestCodePointIndex );
|
|
}
|
|
|
|
public HtmlSpan GetSpanAt( Vector2 pos )
|
|
{
|
|
if ( Block == null ) return default;
|
|
if ( HtmlSpans is null ) return default;
|
|
|
|
var result = Block.HitTest( pos.x, pos.y );
|
|
return HtmlSpans.Where( x => x.from <= result.OverCodePointIndex && x.to > result.OverCodePointIndex ).FirstOrDefault();
|
|
}
|
|
|
|
public bool UpdateStyles( Styles style )
|
|
{
|
|
var fontFamily = style.FontFamily ?? "Arial";
|
|
var fontColor = style.FontColor ?? Color.Black;
|
|
var fontSize = style.FontSize ?? Length.Pixels( 13 ).Value;
|
|
|
|
FontSize = fontSize.GetPixels( 100 ); // this should probably be screen height for font length?
|
|
FontSize = MathF.Round( FontSize * 32.0f ) / 32.0f; // round the font size so we're not redrawing for no reason on font size lerp
|
|
FontWeight = style.FontWeight;
|
|
TextAlign = style.TextAlign.Value;
|
|
TextDecoration = style.TextDecorationLine.Value;
|
|
FontStyle = style.FontStyle.Value;
|
|
AlignItems = style.AlignItems.Value;
|
|
LetterSpacing = style.LetterSpacing;
|
|
WordSpacing = style.WordSpacing;
|
|
LineHeight = style.LineHeight;
|
|
WhiteSpace = style.WhiteSpace;
|
|
TextTransform = style.TextTransform;
|
|
GradientInfo = style.TextGradient;
|
|
TextOverflow = style.TextOverflow.Value;
|
|
TextFilter = style.TextFilter.Value;
|
|
WordBreak = style.WordBreak.Value;
|
|
Smooth = style.FontSmooth.Value;
|
|
|
|
var hash = HashCode.Combine( FontSize, fontColor, fontFamily, FontWeight, TextAlign, WhiteSpace, TextDecoration, FontStyle );
|
|
hash = HashCode.Combine( hash, LetterSpacing, TextTransform, Text, SelectionStart, SelectionEnd, ShouldDrawSelection, style.TextShadow );
|
|
hash = HashCode.Combine( hash, style.TextStrokeWidth, style.TextStrokeColor, style.TextDecorationColor, style.TextDecorationThickness, style.TextDecorationSkipInk, style.TextDecorationStyle );
|
|
hash = HashCode.Combine( hash, style.TextUnderlineOffset, style.TextOverlineOffset, style.TextLineThroughOffset, style.TextGradient, style.TextOverflow, style.WordBreak, style.LineHeight );
|
|
hash = HashCode.Combine( hash, style.WordSpacing );
|
|
hash = HashCode.Combine( hash, Smooth );
|
|
|
|
if ( FontHash == hash && Block != null )
|
|
return false;
|
|
|
|
//
|
|
// Create a hash of things on the font that could change
|
|
//
|
|
|
|
FontHash = hash;
|
|
|
|
Style ??= new Style();
|
|
|
|
Style.FontFamily = fontFamily;
|
|
Style.FontSize = FontSize;
|
|
Style.FontWeight = FontWeight ?? 400;
|
|
Style.FontItalic = FontStyle != FontStyle.None;
|
|
Style.TextColor = fontColor.ToSk();
|
|
Style.Underline = UnderlineStyle.None;
|
|
Style.StrokeInkSkip = style.TextDecorationSkipInk == TextSkipInk.All;
|
|
Style.UnderlineOffset = style.TextUnderlineOffset.Value.GetPixels( 100 );
|
|
Style.OverlineOffset = style.TextOverlineOffset.Value.GetPixels( 100 );
|
|
Style.StrikeThroughOffset = style.TextLineThroughOffset.Value.GetPixels( 100 );
|
|
|
|
switch ( style.TextDecorationStyle )
|
|
{
|
|
case TextDecorationStyle.Solid:
|
|
Style.UnderlineStrokeType = UnderlineType.Solid;
|
|
break;
|
|
case TextDecorationStyle.Double:
|
|
Style.UnderlineStrokeType = UnderlineType.Double;
|
|
break;
|
|
case TextDecorationStyle.Dotted:
|
|
Style.UnderlineStrokeType = UnderlineType.Dotted;
|
|
break;
|
|
case TextDecorationStyle.Dashed:
|
|
Style.UnderlineStrokeType = UnderlineType.Dashed;
|
|
break;
|
|
case TextDecorationStyle.Wavy:
|
|
Style.UnderlineStrokeType = UnderlineType.Wavy;
|
|
break;
|
|
default:
|
|
Style.UnderlineStrokeType = UnderlineType.Solid;
|
|
break;
|
|
}
|
|
|
|
Style.UnderlineColor = (style.TextDecorationColor ?? fontColor).ToSk();
|
|
Style.StrokeThickness = style.TextDecorationThickness?.GetPixels( 100.0f );
|
|
Style.Underline |= (TextDecoration & UI.TextDecoration.Underline) != 0 ? UnderlineStyle.Gapped : UnderlineStyle.None;
|
|
Style.Underline |= (TextDecoration & UI.TextDecoration.Overline) != 0 ? UnderlineStyle.Overline : UnderlineStyle.None;
|
|
Style.StrikeThrough = (TextDecoration & UI.TextDecoration.LineThrough) != 0 ? StrikeThroughStyle.Solid : StrikeThroughStyle.None;
|
|
Style.LetterSpacing = LetterSpacing.Value.GetPixels( 1000.0f );
|
|
Style.WordSpacing = WordSpacing.Value.GetPixels( 1000.0f );
|
|
Style.LineHeight = GetLineHeightMultiplier();
|
|
|
|
Style.ClearEffects();
|
|
Gradient = null;
|
|
|
|
EffectMargin = default;
|
|
|
|
if ( !style.TextGradient.ColorOffsets.IsDefaultOrEmpty )
|
|
{
|
|
var colors = style.TextGradient.ColorOffsets.Select( x => SkiaCompat.ToSk( x.color ) ).ToArray();
|
|
var stops = style.TextGradient.ColorOffsets.Select( x => x.offset.Value ).ToArray();
|
|
|
|
if ( style.TextGradient.GradientType == GradientInfo.GradientTypes.Linear )
|
|
{
|
|
Gradient = TextGradient.Linear( colors, stops, style.TextGradient.Angle );
|
|
}
|
|
|
|
if ( style.TextGradient.GradientType == GradientInfo.GradientTypes.Radial )
|
|
{
|
|
Gradient = TextGradient.Radial( colors, stops, 0, new SKPoint( 0.5f, 0.5f ), (RadialSizeMode)style.TextGradient.SizeMode );
|
|
}
|
|
}
|
|
|
|
if ( style.TextShadow != null && !style.TextShadow.IsNone )
|
|
{
|
|
foreach ( var shadow in style.TextShadow )
|
|
{
|
|
var effect = TextEffect.DropShadow( shadow.Color.ToSk(), shadow.OffsetX, shadow.OffsetY, shadow.Blur );
|
|
effect.Width = 0;
|
|
effect.BlurSize = MathF.Max( effect.BlurSize, 0.01f );
|
|
Style.AddEffect( effect );
|
|
|
|
var shadowSize = (effect.Width + shadow.Blur) * 2.0f;
|
|
|
|
EffectMargin.Left = MathF.Max( EffectMargin.Left, shadowSize + -shadow.OffsetX ).CeilToInt();
|
|
EffectMargin.Right = MathF.Max( EffectMargin.Right, shadowSize + shadow.OffsetX ).CeilToInt();
|
|
EffectMargin.Top = MathF.Max( EffectMargin.Top, shadowSize + -shadow.OffsetY ).CeilToInt();
|
|
EffectMargin.Bottom = MathF.Max( EffectMargin.Bottom, shadowSize + shadow.OffsetY ).CeilToInt();
|
|
}
|
|
}
|
|
|
|
if ( style.TextStrokeWidth.Value.Value > 0.0f )
|
|
{
|
|
var color = style.TextStrokeColor ?? style.FontColor ?? Color.Black;
|
|
|
|
var size = style.TextStrokeWidth.Value.GetPixels( 1.0f );
|
|
var effect = TextEffect.Outline( color.ToSk(), size );
|
|
effect.StrokeMiter = 2.0f;
|
|
effect.StrokeJoin = SKStrokeJoin.Round;
|
|
Style.AddEffect( effect );
|
|
|
|
EffectMargin.Left = MathF.Max( EffectMargin.Left, size ).CeilToInt();
|
|
EffectMargin.Right = MathF.Max( EffectMargin.Right, size ).CeilToInt();
|
|
EffectMargin.Top = MathF.Max( EffectMargin.Top, size ).CeilToInt();
|
|
EffectMargin.Bottom = MathF.Max( EffectMargin.Bottom, size ).CeilToInt();
|
|
}
|
|
|
|
if ( Block == null )
|
|
{
|
|
Block = new Topten.RichTextKit.TextBlock();
|
|
Block.FontMapper = FontManager.Instance;
|
|
}
|
|
|
|
Block.Clear();
|
|
Block.Alignment = (Topten.RichTextKit.TextAlignment)TextAlign;
|
|
Block.Overflow = (Topten.RichTextKit.TextOverflow)TextOverflow;
|
|
Block.WordBreak = (Topten.RichTextKit.WordBreakMode)WordBreak;
|
|
Block.NoWrap = NoWrap || WhiteSpace == UI.WhiteSpace.NoWrap;
|
|
|
|
if ( IsHtml && !string.IsNullOrWhiteSpace( Text ) )
|
|
{
|
|
try
|
|
{
|
|
HtmlSpans = new List<HtmlSpan>();
|
|
|
|
var html = htmlNode;
|
|
if ( html is not null )
|
|
{
|
|
BuildBlockFromHtml( Block, html, Style );
|
|
}
|
|
|
|
if ( LookupStyles is not null )
|
|
{
|
|
foreach ( var span in HtmlSpans )
|
|
{
|
|
var s = LookupStyles( span.node );
|
|
if ( s is null ) continue;
|
|
|
|
var sty = Style.Copy();
|
|
|
|
sty.FontSize = (style.FontSize ?? Length.Pixels( 13 ).Value).GetPixels( 100 );
|
|
sty.FontSize = MathF.Round( FontSize * 32.0f ) / 32.0f;
|
|
sty.FontFamily = s.FontFamily;
|
|
sty.TextColor = s.FontColor?.ToSk() ?? sty.TextColor;
|
|
sty.BackgroundColor = s.BackgroundColor?.ToSk() ?? sty.BackgroundColor;
|
|
sty.FontWeight = s.FontWeight ?? sty.FontWeight;
|
|
sty.FontItalic = s.FontStyle == FontStyle.Italic;
|
|
sty.Underline = s.TextDecorationLine == UI.TextDecoration.Underline ? UnderlineStyle.Solid : UnderlineStyle.None;
|
|
sty.UnderlineColor = sty.TextColor;
|
|
sty.LetterSpacing = s.LetterSpacing?.GetPixels( 1000.0f ) ?? sty.LetterSpacing;
|
|
sty.WordSpacing = s.WordSpacing?.GetPixels( 1000.0f ) ?? sty.WordSpacing;
|
|
sty.StrikeThrough = (s.TextDecorationLine?.Contains( UI.TextDecoration.LineThrough ) ?? false) ? StrikeThroughStyle.Solid : sty.StrikeThrough;
|
|
|
|
Block.ApplyStyle( span.from, span.to - span.from, sty );
|
|
}
|
|
}
|
|
}
|
|
catch ( System.Exception e )
|
|
{
|
|
Log.Warning( e );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Block.AddText( FixedText( Text ), Style );
|
|
}
|
|
|
|
|
|
SizeCache.Clear();
|
|
ReleaseTexture();
|
|
|
|
return true;
|
|
}
|
|
|
|
public record class HtmlSpan( INode node, int from, int to );
|
|
public List<HtmlSpan> HtmlSpans;
|
|
|
|
private void BuildBlockFromHtml( Topten.RichTextKit.TextBlock block, Node node, Style style )
|
|
{
|
|
if ( node.IsComment )
|
|
return;
|
|
|
|
if ( node.IsText )
|
|
{
|
|
var startText = block.Length;
|
|
block.AddText( node.InnerHtml, style );
|
|
var endText = block.Length;
|
|
|
|
var span = new HtmlSpan( node?.ParentNode, startText, endText );
|
|
HtmlSpans.Add( span );
|
|
}
|
|
|
|
if ( node.Name == "br" )
|
|
{
|
|
block.AddText( "\n", style );
|
|
return;
|
|
}
|
|
|
|
foreach ( var c in node.ChildNodes )
|
|
{
|
|
BuildBlockFromHtml( block, c, style );
|
|
}
|
|
}
|
|
|
|
void ReleaseTexture()
|
|
{
|
|
WaitTextureReady();
|
|
|
|
if ( Texture == null )
|
|
return;
|
|
|
|
LastTexture?.Dispose();
|
|
LastTexture = Texture;
|
|
|
|
Texture = null;
|
|
}
|
|
|
|
int lastSizeHash = 0;
|
|
|
|
/// <summary>
|
|
/// Called on layout. We should decide here if we actually need to rebuild
|
|
/// </summary>
|
|
public void SizeFinalized( float width, float height )
|
|
{
|
|
WaitTextureReady();
|
|
|
|
width = width.CeilToInt();
|
|
height = height.CeilToInt();
|
|
|
|
int sizeHash = new Vector2( width, height ).GetHashCode();
|
|
|
|
if ( lastSizeHash != sizeHash )
|
|
{
|
|
ReleaseTexture();
|
|
lastSizeHash = sizeHash;
|
|
|
|
if ( Text.Length == 0 )
|
|
{
|
|
BlockSize = new Vector2( Block.MeasuredWidth.CeilToInt().Clamp( 2, 4096 ), Block.MeasuredHeight.CeilToInt().Clamp( 2, 4096 ) );
|
|
}
|
|
}
|
|
|
|
if ( Text.Length == 0 )
|
|
return;
|
|
|
|
if ( Texture == null )
|
|
{
|
|
// threaded
|
|
// TextureRebuild = Task.Run( () => RebuildTexture( width, height ) );
|
|
|
|
// blocking
|
|
RebuildTexture( width, height );
|
|
}
|
|
}
|
|
|
|
Task TextureRebuild;
|
|
|
|
/// <summary>
|
|
/// Actually recreate the texture
|
|
/// </summary>
|
|
unsafe void RebuildTexture( float maxwidth, float maxheight )
|
|
{
|
|
if ( !ui_rendertext )
|
|
return;
|
|
|
|
//Log.Info( $"RenderText: {Text}" );
|
|
|
|
bool isEmpty = Text.Length == 0;
|
|
|
|
if ( TextOverflow != TextOverflow.None )
|
|
{
|
|
Block.MaxWidth = maxwidth;
|
|
Block.MaxHeight = maxheight;
|
|
}
|
|
else
|
|
{
|
|
Block.MaxWidth = WhiteSpace == UI.WhiteSpace.NoWrap ? null : (maxwidth.CeilToInt() + 1);
|
|
}
|
|
|
|
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 ) );
|
|
|
|
BlockSize = new Vector2( width, height );
|
|
IsTruncated = Block.Truncated;
|
|
|
|
var marginEdge = EffectMargin.EdgeSize;
|
|
width += marginEdge.x.CeilToInt();
|
|
height += marginEdge.y.CeilToInt();
|
|
|
|
if ( isEmpty )
|
|
return;
|
|
|
|
if ( Gradient != null && Gradient.GradientType == Topten.RichTextKit.GradientType.Radial )
|
|
{
|
|
var centerX = GradientInfo.OffsetX.GetPixels( width ) / width;
|
|
var centerY = GradientInfo.OffsetY.GetPixels( height ) / height;
|
|
Gradient.Center = new SKPoint( centerX, centerY );
|
|
}
|
|
|
|
using var perfScope = Performance.Scope( "TextBlock.RebuildTexture" );
|
|
|
|
using ( var bitmap = new SKBitmap( width, height, SKColorType.Bgra8888, SKAlphaType.Premul ) )
|
|
using ( var canvas = new SKCanvas( bitmap ) )
|
|
{
|
|
var o = new Topten.RichTextKit.TextPaintOptions
|
|
{
|
|
Edging = Smooth switch
|
|
{
|
|
FontSmooth.Never => SKFontEdging.Alias,
|
|
_ => SKFontEdging.Antialias,
|
|
},
|
|
|
|
Hinting = SKFontHinting.Full,
|
|
TextGradient = Gradient
|
|
};
|
|
|
|
canvas.Clear( Style.TextColor.WithAlpha( 0 ) );
|
|
|
|
if ( ShouldDrawSelection && (SelectionStart > 0 || SelectionEnd > 0) )
|
|
{
|
|
o.Selection = new TextRange( CaretToCodePointIndex( SelectionStart ), CaretToCodePointIndex( SelectionEnd ) );
|
|
o.SelectionColor = SelectionColor.ToSk();
|
|
}
|
|
|
|
Block.Paint( canvas, new SKPoint( EffectMargin.Left - Block.MeasuredPadding.Left, EffectMargin.Top ), o );
|
|
|
|
var debugName = Text;
|
|
if ( debugName.Length > 10 ) debugName = $"{debugName.Substring( 0, 8 )}..";
|
|
if ( debugName.Contains( ':' ) ) debugName = debugName.Replace( ':', '-' );
|
|
|
|
//
|
|
// Make a texture that big
|
|
//
|
|
int numMips = (int)Math.Log2( Math.Min( width, height ) ) + 1;
|
|
|
|
if ( LastTexture != null )
|
|
{
|
|
// we already have a texture that is the right size, lets just use that
|
|
if ( LastTexture.Size == new Vector2( width, height ) )
|
|
{
|
|
var span = new Span<byte>( bitmap.GetPixels().ToPointer(), width * height * bitmap.BytesPerPixel );
|
|
LastTexture.Update( span, 0, 0, width, height );
|
|
Texture = LastTexture;
|
|
LastTexture = null;
|
|
return;
|
|
}
|
|
|
|
LastTexture?.Dispose();
|
|
LastTexture = null;
|
|
}
|
|
|
|
Texture = Texture.Create( width, height, ImageFormat.BGRA8888 )
|
|
.WithName( $"skiatextblock[{debugName}]" )
|
|
.WithMips( numMips )
|
|
.WithData( bitmap.GetPixels(), width * height * bitmap.BytesPerPixel )
|
|
.WithDynamicUsage()
|
|
.Finish();
|
|
|
|
Texture.Flags |= TextureFlags.PremultipliedAlpha;
|
|
}
|
|
}
|
|
|
|
int CaretToCodePointIndex( int caretPos )
|
|
{
|
|
if ( caretPos < 0 || caretPos > Block.CaretIndicies.Count - 1 )
|
|
return caretPos;
|
|
|
|
return Block.CaretIndicies[caretPos];
|
|
}
|
|
|
|
string FixedText( string text )
|
|
{
|
|
if ( string.IsNullOrEmpty( text ) ) return ".";
|
|
|
|
// TODO - if starts with #, look up localized string
|
|
|
|
//text = text.Replace( "\r", "" );
|
|
text = text.Replace( "\r\n", new string( (char)0x2029, 1 ) );
|
|
text = text.Replace( '\n', (char)0x2029 ); // replace newlines with paragraph, makes them not render as a square with a cross in it
|
|
|
|
if ( TextTransform.HasValue )
|
|
{
|
|
switch ( TextTransform.Value )
|
|
{
|
|
case UI.TextTransform.Uppercase:
|
|
text = text.ToUpperInvariant();
|
|
break;
|
|
|
|
case UI.TextTransform.Lowercase:
|
|
text = text.ToLowerInvariant();
|
|
break;
|
|
|
|
case UI.TextTransform.Capitalize:
|
|
{
|
|
text = System.Threading.Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase( text );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
text = WhiteSpace switch
|
|
{
|
|
UI.WhiteSpace.Normal or UI.WhiteSpace.NoWrap => text.CollapseWhiteSpace(),
|
|
UI.WhiteSpace.PreLine => text.CollapseSpacesAndPreserveLines(),
|
|
UI.WhiteSpace.Pre => text,
|
|
_ => throw new Exception( $"Unknown white-space value {WhiteSpace}" ),
|
|
};
|
|
|
|
return text;
|
|
}
|
|
|
|
float GetLineHeightMultiplier()
|
|
{
|
|
var lineHeight = LineHeight.Value;
|
|
|
|
if ( lineHeight.Unit == LengthUnit.Percentage )
|
|
return lineHeight.GetFraction();
|
|
|
|
if ( lineHeight.Unit == LengthUnit.Pixels )
|
|
return lineHeight.Value / Math.Max( FontSize, 1.0f );
|
|
|
|
return 1.0f;
|
|
}
|
|
|
|
internal void ScrollToCaret( int caretPosition, ref Vector2 scroll, Vector2 visibleBounds )
|
|
{
|
|
Rect caretRect = CaretRect( caretPosition - 1 );
|
|
|
|
if ( caretRect.Left < scroll.x )
|
|
{
|
|
scroll.x = caretRect.Left;
|
|
}
|
|
else if ( caretRect.Right > scroll.x + visibleBounds.x )
|
|
{
|
|
scroll.x = caretRect.Right - visibleBounds.x + caretRect.Width;
|
|
}
|
|
|
|
if ( caretRect.Top < scroll.y )
|
|
{
|
|
scroll.y = caretRect.Top;
|
|
}
|
|
else if ( caretRect.Bottom > scroll.y + visibleBounds.y )
|
|
{
|
|
scroll.y = caretRect.Bottom - visibleBounds.y + caretRect.Height;
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
ReleaseTexture();
|
|
|
|
LastTexture?.Dispose();
|
|
LastTexture = null;
|
|
|
|
Block = null;
|
|
Style = null;
|
|
SizeCache = null;
|
|
}
|
|
}
|