mirror of
https://github.com/Facepunch/sbox-public.git
synced 2025-12-23 22:48:07 -05:00
958 lines
29 KiB
C#
958 lines
29 KiB
C#
#define USE_SKTEXTBLOB
|
|
// RichTextKit
|
|
// Copyright © 2019-2020 Topten Software. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
// not use this product except in compliance with the License. You may obtain
|
|
// a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
// License for the specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
using SkiaSharp;
|
|
using Topten.RichTextKit.Utils;
|
|
|
|
namespace Topten.RichTextKit
|
|
{
|
|
/// <summary>
|
|
/// Represents a font run - a physical sequence of laid glyphs all with
|
|
/// the same font and style attributes.
|
|
/// </summary>
|
|
public class FontRun
|
|
{
|
|
/// <summary>
|
|
/// The kind of font run.
|
|
/// </summary>
|
|
public FontRunKind RunKind = FontRunKind.Normal;
|
|
|
|
/// <summary>
|
|
/// The style run this typeface run was derived from.
|
|
/// </summary>
|
|
public StyleRun StyleRun;
|
|
|
|
/// <summary>
|
|
/// Get the code points of this run
|
|
/// </summary>
|
|
public Slice<int> CodePoints => CodePointBuffer.SubSlice( Start, Length );
|
|
|
|
/// <summary>
|
|
/// Code point index of the start of this run
|
|
/// </summary>
|
|
public int Start;
|
|
|
|
/// <summary>
|
|
/// The length of this run in codepoints
|
|
/// </summary>
|
|
public int Length;
|
|
|
|
/// <summary>
|
|
/// The index of the first character after this run
|
|
/// </summary>
|
|
public int End => Start + Length;
|
|
|
|
/// <summary>
|
|
/// The user supplied style for this run
|
|
/// </summary>
|
|
public IStyle Style;
|
|
|
|
/// <summary>
|
|
/// The direction of this run
|
|
/// </summary>
|
|
public TextDirection Direction;
|
|
|
|
/// <summary>
|
|
/// The typeface of this run (use this over Style.Fontface)
|
|
/// </summary>
|
|
public SKTypeface Typeface;
|
|
|
|
/// <summary>
|
|
/// The glyph indicies
|
|
/// </summary>
|
|
public Slice<ushort> Glyphs;
|
|
|
|
/// <summary>
|
|
/// The glyph positions (relative to the entire text block)
|
|
/// </summary>
|
|
public Slice<SKPoint> GlyphPositions;
|
|
|
|
/// <summary>
|
|
/// The cluster numbers for each glyph
|
|
/// </summary>
|
|
public Slice<int> Clusters;
|
|
|
|
/// <summary>
|
|
/// The x-coords of each code point, relative to this text run
|
|
/// </summary>
|
|
public Slice<float> RelativeCodePointXCoords;
|
|
|
|
/// <summary>
|
|
/// Get the x-coord of a code point
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// For LTR runs this will be the x-coordinate to the left, or RTL
|
|
/// runs it will be the x-coordinate to the right.
|
|
/// </remarks>
|
|
/// <param name="codePointIndex">The code point index (relative to the entire text block)</param>
|
|
/// <returns>The x-coord relative to the entire text block</returns>
|
|
public float GetXCoordOfCodePointIndex( int codePointIndex )
|
|
{
|
|
if ( this.RunKind == FontRunKind.Ellipsis )
|
|
codePointIndex = 0;
|
|
|
|
// Check in range
|
|
if ( codePointIndex < Start || codePointIndex > End )
|
|
throw new ArgumentOutOfRangeException( nameof( codePointIndex ) );
|
|
|
|
// End of run?
|
|
if ( codePointIndex == End )
|
|
return XCoord + (Direction == TextDirection.LTR ? Width : 0);
|
|
|
|
// Lookup
|
|
return XCoord + RelativeCodePointXCoords[codePointIndex - Start];
|
|
}
|
|
|
|
/// <summary>
|
|
/// The ascent of the font used in this run
|
|
/// </summary>
|
|
public float Ascent;
|
|
|
|
/// <summary>
|
|
/// The descent of the font used in this run
|
|
/// </summary>
|
|
public float Descent;
|
|
|
|
|
|
/// <summary>
|
|
/// The leading of the font used in this run
|
|
/// </summary>
|
|
public float Leading;
|
|
|
|
|
|
/// <summary>
|
|
/// The height of text in this run (ascent + descent)
|
|
/// </summary>
|
|
public float TextHeight => -Ascent + Descent;
|
|
|
|
/// <summary>
|
|
/// Calculate the half leading height for text in this run
|
|
/// </summary>
|
|
public float HalfLeading => (TextHeight * (Style.LineHeight - 1) + Leading) / 2;
|
|
|
|
/// <summary>
|
|
/// Width of this typeface run
|
|
/// </summary>
|
|
public float Width;
|
|
|
|
/// <summary>
|
|
/// Horizontal position of this run, relative to the left margin
|
|
/// </summary>
|
|
public float XCoord;
|
|
|
|
/// <summary>
|
|
/// The calculated width of a single space
|
|
/// </summary>
|
|
public float SpaceWidth;
|
|
|
|
/// <summary>
|
|
/// The line that owns this font run
|
|
/// </summary>
|
|
public TextLine Line { get; internal set; }
|
|
|
|
/// <summary>
|
|
/// Get the next font run from this one
|
|
/// </summary>
|
|
public FontRun NextRun
|
|
{
|
|
get
|
|
{
|
|
var allRuns = Line.TextBlock.FontRuns as List<FontRun>;
|
|
int index = allRuns.IndexOf( this );
|
|
if ( index < 0 || index + 1 >= Line.Runs.Count )
|
|
return null;
|
|
return Line.Runs[index + 1];
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the previous font run from this one
|
|
/// </summary>
|
|
public FontRun PreviousRun
|
|
{
|
|
get
|
|
{
|
|
var allRuns = Line.TextBlock.FontRuns as List<FontRun>;
|
|
int index = allRuns.IndexOf( this );
|
|
if ( index - 1 < 0 )
|
|
return null;
|
|
return Line.Runs[index - 1];
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// For debugging
|
|
/// </summary>
|
|
/// <returns>Debug string</returns>
|
|
public override string ToString()
|
|
{
|
|
switch ( RunKind )
|
|
{
|
|
case FontRunKind.Normal:
|
|
return $"{Start} - {End} @ {XCoord} - {XCoord + Width} = '{Utf32Utils.FromUtf32( CodePoints )}'";
|
|
|
|
default:
|
|
return $"{Start} - {End} @ {XCoord} - {XCoord + Width} {RunKind}'";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves all glyphs by the specified offset amount
|
|
/// </summary>
|
|
/// <param name="dx">The x-delta to move glyphs by</param>
|
|
/// <param name="dy">The y-delta to move glyphs by</param>
|
|
public void MoveGlyphs( float dx, float dy )
|
|
{
|
|
for ( int i = 0; i < GlyphPositions.Length; i++ )
|
|
{
|
|
GlyphPositions[i].X += dx;
|
|
GlyphPositions[i].Y += dy;
|
|
}
|
|
_textBlob?.Dispose();
|
|
_textBlob = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the leading width of all character from the start of the run (either
|
|
/// the left or right depending on run direction) to the specified code point
|
|
/// </summary>
|
|
/// <param name="codePoint">The code point index to measure to</param>
|
|
/// <returns>The distance from the start to the specified code point</returns>
|
|
public float LeadingWidth( int codePoint )
|
|
{
|
|
// At either end?
|
|
if ( codePoint == this.End )
|
|
return this.Width;
|
|
if ( codePoint == 0 )
|
|
return 0;
|
|
|
|
// Internal, calculate the leading width (ie from code point 0 to code point N)
|
|
int codePointIndex = codePoint - this.Start;
|
|
if ( this.Direction == TextDirection.LTR )
|
|
{
|
|
return this.RelativeCodePointXCoords[codePointIndex];
|
|
}
|
|
else
|
|
{
|
|
return this.Width - this.RelativeCodePointXCoords[codePointIndex];
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculate the position at which to break a text run
|
|
/// </summary>
|
|
/// <param name="maxWidth">The max width available</param>
|
|
/// <param name="force">Whether to force the use of at least one glyph</param>
|
|
/// <returns>The code point position to break at</returns>
|
|
internal int FindBreakPosition( float maxWidth, bool force )
|
|
{
|
|
int lastFittingCodePoint = this.Start;
|
|
int firstNonZeroWidthCodePoint = -1;
|
|
var prevWidth = 0f;
|
|
for ( int i = this.Start; i < this.End; i++ )
|
|
{
|
|
var width = this.LeadingWidth( i );
|
|
if ( prevWidth != width )
|
|
{
|
|
if ( firstNonZeroWidthCodePoint < 0 )
|
|
firstNonZeroWidthCodePoint = i;
|
|
|
|
if ( width < maxWidth )
|
|
{
|
|
lastFittingCodePoint = i;
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
prevWidth = width;
|
|
}
|
|
|
|
if ( lastFittingCodePoint > this.Start || !force )
|
|
return lastFittingCodePoint;
|
|
|
|
if ( firstNonZeroWidthCodePoint > this.Start )
|
|
return firstNonZeroWidthCodePoint;
|
|
|
|
// Split at the end
|
|
return this.End;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Split a typeface run into two separate runs, truncating this run at
|
|
/// the specified code point index and returning a new run containing the
|
|
/// split off part.
|
|
/// </summary>
|
|
/// <param name="splitAtCodePoint">The code point index to split at</param>
|
|
/// <returns>A new typeface run for the split off part</returns>
|
|
internal FontRun Split( int splitAtCodePoint )
|
|
{
|
|
if ( this.Direction == TextDirection.LTR )
|
|
{
|
|
return SplitLTR( splitAtCodePoint );
|
|
}
|
|
else
|
|
{
|
|
return SplitRTL( splitAtCodePoint );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Split a LTR typeface run into two separate runs, truncating the passed
|
|
/// run (LHS) and returning a new run containing the split off part (RHS)
|
|
/// </summary>
|
|
/// <param name="splitAtCodePoint">To code point position to split at</param>
|
|
/// <returns>The RHS run after splitting</returns>
|
|
private FontRun SplitLTR( int splitAtCodePoint )
|
|
{
|
|
// Check split point is internal to the run
|
|
System.Diagnostics.Debug.Assert( this.Direction == TextDirection.LTR );
|
|
System.Diagnostics.Debug.Assert( splitAtCodePoint > this.Start );
|
|
System.Diagnostics.Debug.Assert( splitAtCodePoint < this.End );
|
|
|
|
// Work out the split position
|
|
int codePointSplitPos = splitAtCodePoint - this.Start;
|
|
|
|
// Work out the width that we're slicing off
|
|
float sliceLeftWidth = this.RelativeCodePointXCoords[codePointSplitPos];
|
|
float sliceRightWidth = this.Width - sliceLeftWidth;
|
|
|
|
// Work out the glyph split position
|
|
int glyphSplitPos = 0;
|
|
for ( glyphSplitPos = 0; glyphSplitPos < this.Clusters.Length; glyphSplitPos++ )
|
|
{
|
|
if ( this.Clusters[glyphSplitPos] >= splitAtCodePoint )
|
|
break;
|
|
}
|
|
|
|
// Create the other run
|
|
var newRun = FontRun.Pool.Value.Get();
|
|
newRun.StyleRun = this.StyleRun;
|
|
newRun.CodePointBuffer = this.CodePointBuffer;
|
|
newRun.Direction = this.Direction;
|
|
newRun.Ascent = this.Ascent;
|
|
newRun.Descent = this.Descent;
|
|
newRun.Leading = this.Leading;
|
|
newRun.Style = this.Style;
|
|
newRun.Typeface = this.Typeface;
|
|
newRun.Start = splitAtCodePoint;
|
|
newRun.Length = this.End - splitAtCodePoint;
|
|
newRun.Width = sliceRightWidth;
|
|
newRun.RelativeCodePointXCoords = this.RelativeCodePointXCoords.SubSlice( codePointSplitPos );
|
|
newRun.GlyphPositions = this.GlyphPositions.SubSlice( glyphSplitPos );
|
|
newRun.Glyphs = this.Glyphs.SubSlice( glyphSplitPos );
|
|
newRun.Clusters = this.Clusters.SubSlice( glyphSplitPos );
|
|
newRun.SpaceWidth = this.SpaceWidth;
|
|
|
|
// Adjust code point positions
|
|
for ( int i = 0; i < newRun.RelativeCodePointXCoords.Length; i++ )
|
|
{
|
|
newRun.RelativeCodePointXCoords[i] -= sliceLeftWidth;
|
|
}
|
|
|
|
// Adjust glyph positions
|
|
for ( int i = 0; i < newRun.GlyphPositions.Length; i++ )
|
|
{
|
|
newRun.GlyphPositions[i].X -= sliceLeftWidth;
|
|
}
|
|
|
|
// Update this run
|
|
this.RelativeCodePointXCoords = this.RelativeCodePointXCoords.SubSlice( 0, codePointSplitPos );
|
|
this.Glyphs = this.Glyphs.SubSlice( 0, glyphSplitPos );
|
|
this.GlyphPositions = this.GlyphPositions.SubSlice( 0, glyphSplitPos );
|
|
this.Clusters = this.Clusters.SubSlice( 0, glyphSplitPos );
|
|
this.Width = sliceLeftWidth;
|
|
this.Length = codePointSplitPos;
|
|
this._textBlob?.Dispose();
|
|
this._textBlob = null;
|
|
|
|
// Return the new run
|
|
return newRun;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Split a RTL typeface run into two separate runs, truncating the passed
|
|
/// run (RHS) and returning a new run containing the split off part (LHS)
|
|
/// </summary>
|
|
/// <param name="splitAtCodePoint">To code point position to split at</param>
|
|
/// <returns>The LHS run after splitting</returns>
|
|
private FontRun SplitRTL( int splitAtCodePoint )
|
|
{
|
|
// Check split point is internal to the run
|
|
System.Diagnostics.Debug.Assert( this.Direction == TextDirection.RTL );
|
|
System.Diagnostics.Debug.Assert( splitAtCodePoint > this.Start );
|
|
System.Diagnostics.Debug.Assert( splitAtCodePoint < this.End );
|
|
|
|
// Work out the split position
|
|
int codePointSplitPos = splitAtCodePoint - this.Start;
|
|
|
|
// Work out the width that we're slicing off
|
|
float sliceLeftWidth = this.RelativeCodePointXCoords[codePointSplitPos];
|
|
float sliceRightWidth = this.Width - sliceLeftWidth;
|
|
|
|
// Work out the glyph split position
|
|
int glyphSplitPos = 0;
|
|
for ( glyphSplitPos = this.Clusters.Length; glyphSplitPos > 0; glyphSplitPos-- )
|
|
{
|
|
if ( this.Clusters[glyphSplitPos - 1] >= splitAtCodePoint )
|
|
break;
|
|
}
|
|
|
|
// Create the other run
|
|
var newRun = FontRun.Pool.Value.Get();
|
|
newRun.StyleRun = this.StyleRun;
|
|
newRun.CodePointBuffer = this.CodePointBuffer;
|
|
newRun.Direction = this.Direction;
|
|
newRun.Ascent = this.Ascent;
|
|
newRun.Descent = this.Descent;
|
|
newRun.Leading = this.Leading;
|
|
newRun.Style = this.Style;
|
|
newRun.Typeface = this.Typeface;
|
|
newRun.Start = splitAtCodePoint;
|
|
newRun.Length = this.End - splitAtCodePoint;
|
|
newRun.Width = sliceLeftWidth;
|
|
newRun.RelativeCodePointXCoords = this.RelativeCodePointXCoords.SubSlice( codePointSplitPos );
|
|
newRun.GlyphPositions = this.GlyphPositions.SubSlice( 0, glyphSplitPos );
|
|
newRun.Glyphs = this.Glyphs.SubSlice( 0, glyphSplitPos );
|
|
newRun.Clusters = this.Clusters.SubSlice( 0, glyphSplitPos );
|
|
newRun.SpaceWidth = this.SpaceWidth;
|
|
|
|
// Update this run
|
|
this.RelativeCodePointXCoords = this.RelativeCodePointXCoords.SubSlice( 0, codePointSplitPos );
|
|
this.Glyphs = this.Glyphs.SubSlice( glyphSplitPos );
|
|
this.GlyphPositions = this.GlyphPositions.SubSlice( glyphSplitPos );
|
|
this.Clusters = this.Clusters.SubSlice( glyphSplitPos );
|
|
this.Width = sliceRightWidth;
|
|
this.Length = codePointSplitPos;
|
|
this._textBlob?.Dispose();
|
|
this._textBlob = null;
|
|
|
|
// Adjust code point positions
|
|
for ( int i = 0; i < this.RelativeCodePointXCoords.Length; i++ )
|
|
{
|
|
this.RelativeCodePointXCoords[i] -= sliceLeftWidth;
|
|
}
|
|
|
|
// Adjust glyph positions
|
|
for ( int i = 0; i < this.GlyphPositions.Length; i++ )
|
|
{
|
|
this.GlyphPositions[i].X -= sliceLeftWidth;
|
|
}
|
|
|
|
// Return the new run
|
|
return newRun;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The global list of code points
|
|
/// </summary>
|
|
internal Buffer<int> CodePointBuffer;
|
|
|
|
/// <summary>
|
|
/// Calculate any overhang for this text line
|
|
/// </summary>
|
|
/// <param name="right"></param>
|
|
/// <param name="leftOverhang"></param>
|
|
/// <param name="rightOverhang"></param>
|
|
internal void UpdateOverhang( float right, ref float leftOverhang, ref float rightOverhang )
|
|
{
|
|
if ( RunKind == FontRunKind.TrailingWhitespace )
|
|
return;
|
|
|
|
if ( Glyphs.Length == 0 )
|
|
return;
|
|
|
|
using ( var paint = new SKPaint() )
|
|
using ( var font = new SKFont() )
|
|
{
|
|
float glyphScale = 1;
|
|
if ( Style.FontVariant == FontVariant.SuperScript )
|
|
{
|
|
glyphScale = 0.65f;
|
|
}
|
|
if ( Style.FontVariant == FontVariant.SubScript )
|
|
{
|
|
glyphScale = 0.65f;
|
|
}
|
|
|
|
font.Typeface = Typeface;
|
|
font.Size = Style.FontSize * glyphScale;
|
|
font.Subpixel = true;
|
|
paint.IsAntialias = true;
|
|
font.Edging = SKFontEdging.SubpixelAntialias;
|
|
|
|
unsafe
|
|
{
|
|
fixed ( ushort* pGlyphs = Glyphs.Underlying )
|
|
{
|
|
font.GetGlyphWidths( (IntPtr)(pGlyphs + Start), sizeof( ushort ) * Glyphs.Length, SKTextEncoding.GlyphId, out var bounds );
|
|
if ( bounds != null )
|
|
{
|
|
for ( int i = 0; i < bounds.Length; i++ )
|
|
{
|
|
float gx = GlyphPositions[i].X;
|
|
|
|
var loh = -(gx + bounds[i].Left);
|
|
if ( loh > leftOverhang )
|
|
leftOverhang = loh;
|
|
|
|
var roh = (gx + bounds[i].Right + 1) - right;
|
|
if ( roh > rightOverhang )
|
|
rightOverhang = roh;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
internal unsafe float CreateTextBlob( PaintTextContext ctx, bool withEdging = true )
|
|
{
|
|
fixed ( ushort* pGlyphs = Glyphs.Underlying )
|
|
{
|
|
float glyphScale = 1;
|
|
float glyphVOffset = 0;
|
|
if ( Style.FontVariant == FontVariant.SuperScript )
|
|
{
|
|
glyphScale = 0.65f;
|
|
glyphVOffset = -Style.FontSize * 0.35f;
|
|
}
|
|
if ( Style.FontVariant == FontVariant.SubScript )
|
|
{
|
|
glyphScale = 0.65f;
|
|
glyphVOffset = Style.FontSize * 0.1f;
|
|
}
|
|
|
|
// Create the font
|
|
if ( _font == null )
|
|
{
|
|
_font = new SKFont( this.Typeface, this.Style.FontSize * glyphScale );
|
|
}
|
|
_font.Hinting = ctx.Options.Hinting;
|
|
_font.Edging = withEdging ? ctx.Options.Edging : SKFontEdging.Antialias;
|
|
_font.Subpixel = ctx.Options.SubpixelPositioning;
|
|
|
|
// Create the SKTextBlob (if necessary)
|
|
if ( _textBlob == null )
|
|
{
|
|
_textBlob = SKTextBlob.CreatePositioned(
|
|
(IntPtr)(pGlyphs + Glyphs.Start),
|
|
Glyphs.Length * sizeof( ushort ),
|
|
SKTextEncoding.GlyphId,
|
|
_font,
|
|
GlyphPositions.AsSpan() );
|
|
}
|
|
|
|
return glyphVOffset;
|
|
}
|
|
}
|
|
|
|
internal void DrawStrokeLine( UnderlineType underlineType, SKCanvas skCanvas, SKPaint sKPaint, SKPoint startPoint, SKPoint endPoint, bool isOverline )
|
|
{
|
|
if ( underlineType == UnderlineType.Solid )
|
|
{
|
|
skCanvas.DrawLine( startPoint, endPoint, sKPaint );
|
|
}
|
|
else if ( underlineType == UnderlineType.Dashed )
|
|
{
|
|
float strokeWidth = sKPaint.StrokeWidth;
|
|
SKPathEffect previousPathEffect = sKPaint.PathEffect;
|
|
{
|
|
sKPaint.PathEffect = SKPathEffect.CreateDash( new float[] { strokeWidth * 3.0f, strokeWidth * 3.0f }, strokeWidth );
|
|
skCanvas.DrawLine( startPoint, endPoint, sKPaint );
|
|
}
|
|
sKPaint.PathEffect = previousPathEffect;
|
|
}
|
|
else if ( underlineType == UnderlineType.Dotted )
|
|
{
|
|
float strokeWidth = sKPaint.StrokeWidth;
|
|
SKStrokeCap sKStrokeCap = sKPaint.StrokeCap;
|
|
bool hasAA = sKPaint.IsAntialias;
|
|
SKPathEffect previousPathEffect = sKPaint.PathEffect;
|
|
{
|
|
sKPaint.StrokeCap = SKStrokeCap.Round;
|
|
sKPaint.IsAntialias = true;
|
|
sKPaint.PathEffect = SKPathEffect.CreateDash( new float[] { 0.0f, strokeWidth * 2.0f }, 0.0f );
|
|
skCanvas.DrawLine( startPoint, endPoint, sKPaint );
|
|
}
|
|
sKPaint.IsAntialias = hasAA;
|
|
sKPaint.StrokeCap = sKStrokeCap;
|
|
sKPaint.PathEffect = previousPathEffect;
|
|
}
|
|
else if ( underlineType == UnderlineType.Double )
|
|
{
|
|
float strokeWidth = sKPaint.StrokeWidth;
|
|
SKPathEffect previousPathEffect = sKPaint.PathEffect;
|
|
{
|
|
SKPoint skOffset = new SKPoint( 0, strokeWidth * 2.0f );
|
|
if ( isOverline )
|
|
skOffset.Y *= -1.0f;
|
|
|
|
skCanvas.DrawLine( startPoint, endPoint, sKPaint );
|
|
skCanvas.DrawLine( startPoint + skOffset, endPoint + skOffset, sKPaint );
|
|
}
|
|
sKPaint.PathEffect = previousPathEffect;
|
|
}
|
|
else if ( underlineType == UnderlineType.Wavy )
|
|
{
|
|
// Since skia doesn't have this, we gotta make it ourselves
|
|
using ( SKPath path = new SKPath() )
|
|
{
|
|
float totalWidth = endPoint.X - startPoint.X;
|
|
path.MoveTo( startPoint );
|
|
for ( float i = 0; i < totalWidth; i++ )
|
|
{
|
|
path.LineTo( startPoint.X + i, startPoint.Y + (float)(Math.Sin( i * 0.25f ) * 1.25f) );
|
|
}
|
|
|
|
bool hasAA = sKPaint.IsAntialias;
|
|
SKPaintStyle sKPaintStyle = sKPaint.Style;
|
|
SKStrokeCap sKStrokeCap = sKPaint.StrokeCap;
|
|
{
|
|
sKPaint.IsAntialias = true;
|
|
sKPaint.StrokeCap = SKStrokeCap.Round;
|
|
sKPaint.Style = SKPaintStyle.Stroke;
|
|
skCanvas.DrawPath( path, sKPaint );
|
|
}
|
|
sKPaint.IsAntialias = hasAA;
|
|
sKPaint.StrokeCap = sKStrokeCap;
|
|
sKPaint.Style = sKPaintStyle;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Paint this font run
|
|
/// </summary>
|
|
/// <param name="ctx"></param>
|
|
internal void Paint( PaintTextContext ctx )
|
|
{
|
|
// Paint selection?
|
|
if ( ctx.PaintSelectionBackground != null && RunKind != FontRunKind.Ellipsis )
|
|
{
|
|
bool paintStartHandle = false;
|
|
bool paintEndHandle = false;
|
|
|
|
float selStartXCoord;
|
|
if ( ctx.SelectionStart < Start )
|
|
selStartXCoord = Direction == TextDirection.LTR ? 0 : Width;
|
|
else if ( ctx.SelectionStart >= End )
|
|
selStartXCoord = Direction == TextDirection.LTR ? Width : 0;
|
|
else
|
|
{
|
|
paintStartHandle = true;
|
|
selStartXCoord = RelativeCodePointXCoords[ctx.SelectionStart - this.Start];
|
|
}
|
|
|
|
float selEndXCoord;
|
|
if ( ctx.SelectionEnd < Start )
|
|
selEndXCoord = Direction == TextDirection.LTR ? 0 : Width;
|
|
else if ( ctx.SelectionEnd >= End )
|
|
{
|
|
selEndXCoord = Direction == TextDirection.LTR ? Width : 0;
|
|
paintEndHandle = ctx.SelectionEnd == End;
|
|
}
|
|
else
|
|
{
|
|
selEndXCoord = RelativeCodePointXCoords[ctx.SelectionEnd - this.Start];
|
|
paintEndHandle = true;
|
|
}
|
|
|
|
if ( selStartXCoord != selEndXCoord )
|
|
{
|
|
var tl = new SKPoint( selStartXCoord + this.XCoord, Line.YCoord );
|
|
var br = new SKPoint( selEndXCoord + this.XCoord, Line.YCoord + Line.Height );
|
|
|
|
// Align coords to pixel boundaries
|
|
// Not needed - disabled antialias on SKPaint instead
|
|
/*
|
|
if (ctx.Canvas.TotalMatrix.TryInvert(out var inverse))
|
|
{
|
|
tl = ctx.Canvas.TotalMatrix.MapPoint(tl);
|
|
br = ctx.Canvas.TotalMatrix.MapPoint(br);
|
|
tl = new SKPoint((float)Math.Round(tl.X), (float)Math.Round(tl.Y));
|
|
br = new SKPoint((float)Math.Round(br.X), (float)Math.Round(br.Y));
|
|
tl = inverse.MapPoint(tl);
|
|
br = inverse.MapPoint(br);
|
|
}
|
|
*/
|
|
|
|
var rect = new SKRect( tl.X, tl.Y, br.X, br.Y );
|
|
ctx.Canvas.DrawRect( rect, ctx.PaintSelectionBackground );
|
|
|
|
// Paint selection handles?
|
|
if ( ctx.PaintSelectionHandle != null )
|
|
{
|
|
if ( paintStartHandle )
|
|
{
|
|
rect = new SKRect( tl.X - 1 * ctx.SelectionHandleScale, tl.Y, tl.X + 1 * ctx.SelectionHandleScale, br.Y );
|
|
ctx.Canvas.DrawRect( rect, ctx.PaintSelectionHandle );
|
|
ctx.Canvas.DrawCircle( new SKPoint( tl.X, tl.Y ), 5 * ctx.SelectionHandleScale, ctx.PaintSelectionHandle );
|
|
}
|
|
if ( paintEndHandle )
|
|
{
|
|
rect = new SKRect( br.X - 1 * ctx.SelectionHandleScale, tl.Y, br.X + 1 * ctx.SelectionHandleScale, br.Y );
|
|
ctx.Canvas.DrawRect( rect, ctx.PaintSelectionHandle );
|
|
ctx.Canvas.DrawCircle( new SKPoint( br.X, br.Y ), 5 * ctx.SelectionHandleScale, ctx.PaintSelectionHandle );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( RunKind == FontRunKind.Tabs )
|
|
return;
|
|
|
|
if ( RunKind == FontRunKind.TrailingWhitespace )
|
|
return;
|
|
|
|
_textBlob = null;
|
|
var glyphVOffset = CreateTextBlob( ctx );
|
|
|
|
using var paint = new SKPaint();
|
|
|
|
paint.Color = Style.TextColor;
|
|
paint.Shader = ctx.Shader;
|
|
|
|
ctx.Canvas.DrawText( _textBlob, 0, 0, paint );
|
|
|
|
PaintUnderline( ctx, paint );
|
|
PaintStrikeThrough( ctx, paint, glyphVOffset );
|
|
}
|
|
|
|
internal void PaintStrikeThrough( PaintTextContext ctx, SKPaint paint, float glyphVOffset )
|
|
{
|
|
var strokeWidth = Style.StrokeThickness ?? _font.Metrics.StrikeoutThickness ?? 0;
|
|
|
|
if ( strokeWidth <= 0 ) return;
|
|
if ( Style.StrikeThrough == StrikeThroughStyle.None ) return;
|
|
if ( RunKind != FontRunKind.Normal ) return;
|
|
|
|
paint.Color = Style.UnderlineColor ?? Style.TextColor;
|
|
paint.StrokeWidth = MathF.Max( strokeWidth, 1 );
|
|
|
|
var strikeYPos = Line.YCoord + Line.BaseLine + (_font.Metrics.StrikeoutPosition ?? 0) + glyphVOffset + Style.StrikeThroughOffset;
|
|
DrawStrokeLine( Style.UnderlineStrokeType, ctx.Canvas, paint, new SKPoint( XCoord, strikeYPos ), new SKPoint( XCoord + Width, strikeYPos ), false );
|
|
}
|
|
|
|
internal void PaintUnderline( PaintTextContext ctx, SKPaint paint )
|
|
{
|
|
var strokeWidth = Style.StrokeThickness ?? _font.Metrics.UnderlineThickness ?? 0;
|
|
|
|
if ( strokeWidth <= 0 ) return;
|
|
if ( Style.Underline == UnderlineStyle.None || RunKind != FontRunKind.Normal ) return;
|
|
|
|
paint.StrokeWidth = MathF.Max( strokeWidth, 1 );
|
|
paint.Color = Style.UnderlineColor ?? Style.TextColor;
|
|
|
|
var underlineYPos = Line.YCoord + Line.BaseLine + (_font.Metrics.UnderlinePosition ?? 0);
|
|
var bHasUnderline = false;
|
|
|
|
if ( (Style.Underline & UnderlineStyle.Gapped) != 0 )
|
|
{
|
|
var flUnderlineOffset = underlineYPos + Style.UnderlineOffset;
|
|
var interceptPositions = _textBlob.GetIntercepts( flUnderlineOffset - paint.StrokeWidth / 2, flUnderlineOffset + paint.StrokeWidth );
|
|
var x = XCoord;
|
|
|
|
if ( Style.StrokeInkSkip )
|
|
{
|
|
for ( int i = 0; i < interceptPositions.Length; i += 2 )
|
|
{
|
|
var b = interceptPositions[i] - paint.StrokeWidth;
|
|
if ( x < b )
|
|
{
|
|
DrawStrokeLine( Style.UnderlineStrokeType, ctx.Canvas, paint, new SKPoint( x, flUnderlineOffset ), new SKPoint( b, flUnderlineOffset ), false );
|
|
}
|
|
x = interceptPositions[i + 1] + paint.StrokeWidth;
|
|
}
|
|
}
|
|
if ( x < XCoord + Width )
|
|
{
|
|
DrawStrokeLine( Style.UnderlineStrokeType, ctx.Canvas, paint, new SKPoint( x, flUnderlineOffset ), new SKPoint( XCoord + Width, flUnderlineOffset ), false );
|
|
}
|
|
|
|
bHasUnderline = true;
|
|
}
|
|
|
|
if ( (Style.Underline & UnderlineStyle.Overline) != 0 )
|
|
{
|
|
var flOverlineOffset = Line.YCoord + Style.OverlineOffset;
|
|
var interceptPositions = _textBlob.GetIntercepts( flOverlineOffset - paint.StrokeWidth / 2, flOverlineOffset + paint.StrokeWidth );
|
|
var x = XCoord;
|
|
if ( Style.StrokeInkSkip )
|
|
{
|
|
for ( int i = 0; i < interceptPositions.Length; i += 2 )
|
|
{
|
|
float b = interceptPositions[i] - paint.StrokeWidth;
|
|
if ( x < b )
|
|
{
|
|
DrawStrokeLine( Style.UnderlineStrokeType, ctx.Canvas, paint, new SKPoint( x, flOverlineOffset ), new SKPoint( b, flOverlineOffset ), true );
|
|
}
|
|
x = interceptPositions[i + 1] + paint.StrokeWidth;
|
|
}
|
|
}
|
|
|
|
if ( x < XCoord + Width )
|
|
{
|
|
DrawStrokeLine( Style.UnderlineStrokeType, ctx.Canvas, paint, new SKPoint( x, flOverlineOffset ), new SKPoint( x + Width, flOverlineOffset ), true );
|
|
}
|
|
|
|
bHasUnderline = true;
|
|
}
|
|
|
|
if ( !bHasUnderline || (Style.Underline & UnderlineStyle.Solid) != 0 )
|
|
{
|
|
float flUnderlineOffset = underlineYPos + Style.UnderlineOffset;
|
|
if ( (Style.Underline & UnderlineStyle.ImeInput) != 0 )
|
|
{
|
|
paint.PathEffect = SKPathEffect.CreateDash( new float[] { paint.StrokeWidth, paint.StrokeWidth }, paint.StrokeWidth );
|
|
}
|
|
if ( (Style.Underline & UnderlineStyle.ImeConverted) != 0 )
|
|
{
|
|
paint.PathEffect = SKPathEffect.CreateDash( new float[] { paint.StrokeWidth, paint.StrokeWidth }, paint.StrokeWidth );
|
|
}
|
|
if ( (Style.Underline & UnderlineStyle.ImeConverted) != 0 )
|
|
{
|
|
paint.StrokeWidth *= 2;
|
|
}
|
|
DrawStrokeLine( Style.UnderlineStrokeType, ctx.Canvas, paint, new SKPoint( XCoord, flUnderlineOffset ), new SKPoint( XCoord + Width, flUnderlineOffset ), false );
|
|
paint.PathEffect = null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Paint background of this font run
|
|
/// </summary>
|
|
/// <param name="ctx"></param>
|
|
internal void PaintBackground( PaintTextContext ctx )
|
|
{
|
|
if ( RunKind == FontRunKind.TrailingWhitespace ) return;
|
|
|
|
if ( Style.BackgroundColor != SKColor.Empty && RunKind == FontRunKind.Normal )
|
|
{
|
|
var rect = new SKRect( XCoord, Line.YCoord,
|
|
XCoord + Width, Line.YCoord + Line.Height );
|
|
using ( var skPaint = new SKPaint { Style = SKPaintStyle.Fill, Color = Style.BackgroundColor } )
|
|
{
|
|
ctx.Canvas.DrawRect( rect, skPaint );
|
|
}
|
|
}
|
|
|
|
if ( Style.TextEffects == null || Style.TextEffects.Count() == 0 )
|
|
return;
|
|
|
|
//
|
|
// alex: Override text blob with one that does not have aliasing
|
|
// otherwise we get issues with aliased fonts and things like
|
|
// text strokes.
|
|
//
|
|
_textBlob = null;
|
|
var glyphVOffset = CreateTextBlob( ctx, false );
|
|
|
|
if ( _textBlob == null )
|
|
return;
|
|
|
|
using var effectPaint = new SKPaint();
|
|
foreach ( var effect in Style.TextEffects )
|
|
{
|
|
// Aliased 1 pixel outline needs to be rendered in a different way to look good.
|
|
if ( ctx.Options.Edging == SKFontEdging.Alias &&
|
|
effect.PaintStyle == SKPaintStyle.StrokeAndFill &&
|
|
effect.Width == 1 )
|
|
{
|
|
PaintPixelOutline( ctx, effect.Color );
|
|
|
|
continue;
|
|
}
|
|
|
|
effectPaint.Style = effect.PaintStyle;
|
|
effectPaint.StrokeWidth = effect.Width;
|
|
effectPaint.StrokeJoin = effect.StrokeJoin;
|
|
effectPaint.StrokeMiter = effect.StrokeMiter;
|
|
effectPaint.Color = effect.Color;
|
|
effectPaint.ImageFilter = effect.BlurSize > 0 ? SKImageFilter.CreateDropShadow( effect.Offset.X, effect.Offset.Y, effect.BlurSize, effect.BlurSize, effect.Color ) : null;
|
|
|
|
ctx.Canvas.DrawText( _textBlob, 0, 0, effectPaint );
|
|
PaintUnderline( ctx, effectPaint );
|
|
PaintStrikeThrough( ctx, effectPaint, glyphVOffset );
|
|
}
|
|
}
|
|
|
|
static readonly SKPoint[] PixelOutlineOffsets =
|
|
{
|
|
new( -1, 0 ),
|
|
new( 1, 0 ),
|
|
new( 0, -1 ),
|
|
new( 0, 1 ),
|
|
new( -1, -1 ),
|
|
new( -1, 1 ),
|
|
new( 1, -1 ),
|
|
new( 1, 1 ),
|
|
};
|
|
|
|
unsafe void PaintPixelOutline( PaintTextContext ctx, SKColor color )
|
|
{
|
|
using var pixelPaint = new SKPaint
|
|
{
|
|
Color = color,
|
|
IsAntialias = false,
|
|
};
|
|
|
|
// Override font edging.
|
|
var edging = _font.Edging;
|
|
_font.Edging = SKFontEdging.Alias;
|
|
|
|
fixed ( ushort* pGlyphs = Glyphs.Underlying )
|
|
{
|
|
var textBlob = SKTextBlob.CreatePositioned(
|
|
(IntPtr)(pGlyphs + Glyphs.Start),
|
|
Glyphs.Length * sizeof( ushort ),
|
|
SKTextEncoding.GlyphId,
|
|
_font,
|
|
GlyphPositions.AsSpan() );
|
|
|
|
foreach ( var o in PixelOutlineOffsets )
|
|
{
|
|
ctx.Canvas.DrawText( textBlob, o.X, o.Y, pixelPaint );
|
|
}
|
|
}
|
|
|
|
// Restore font edging.
|
|
_font.Edging = edging;
|
|
}
|
|
|
|
SKTextBlob _textBlob;
|
|
SKFont _font;
|
|
|
|
void Reset()
|
|
{
|
|
RunKind = FontRunKind.Normal;
|
|
CodePointBuffer = null;
|
|
Style = null;
|
|
Typeface = null;
|
|
Line = null;
|
|
_textBlob = null;
|
|
_font = null;
|
|
}
|
|
|
|
internal static ThreadLocal<ObjectPool<FontRun>> Pool = new ThreadLocal<ObjectPool<FontRun>>( () => new ObjectPool<FontRun>()
|
|
{
|
|
Cleaner = ( r ) => r.Reset()
|
|
} );
|
|
}
|
|
}
|