799 строки
32 KiB
C#
799 строки
32 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 HarfBuzzSharp;
|
|
using SkiaSharp;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
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 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);
|
|
|
|
// 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);
|
|
|
|
// 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())
|
|
{
|
|
float glyphScale = 1;
|
|
if (Style.FontVariant == FontVariant.SuperScript)
|
|
{
|
|
glyphScale = 0.65f;
|
|
}
|
|
if (Style.FontVariant == FontVariant.SubScript)
|
|
{
|
|
glyphScale = 0.65f;
|
|
}
|
|
|
|
paint.TextEncoding = SKTextEncoding.GlyphId;
|
|
paint.Typeface = Typeface;
|
|
paint.TextSize = Style.FontSize * glyphScale;
|
|
paint.SubpixelText = true;
|
|
paint.IsAntialias = true;
|
|
paint.LcdRenderText = false;
|
|
|
|
unsafe
|
|
{
|
|
fixed (ushort* pGlyphs = Glyphs.Underlying)
|
|
{
|
|
paint.GetGlyphWidths((IntPtr)(pGlyphs + Start), sizeof(ushort) * Glyphs.Length, 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Don't paint trailing whitespace runs
|
|
if (RunKind == FontRunKind.TrailingWhitespace)
|
|
return;
|
|
|
|
// Text
|
|
using (var paint = new SKPaint())
|
|
using (var paintHalo = new SKPaint())
|
|
{
|
|
// Work out font variant adjustments
|
|
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;
|
|
}
|
|
|
|
// Setup SKPaint
|
|
paint.Color = Style.TextColor;
|
|
|
|
if (Style.HaloColor != SKColor.Empty)
|
|
{
|
|
paintHalo.Color = Style.HaloColor;
|
|
paintHalo.Style = SKPaintStyle.Stroke;
|
|
paintHalo.StrokeWidth = Style.HaloWidth;
|
|
paintHalo.StrokeCap = SKStrokeCap.Square;
|
|
if (Style.HaloBlur > 0)
|
|
paintHalo.MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, Style.HaloBlur);
|
|
}
|
|
|
|
unsafe
|
|
{
|
|
fixed (ushort* pGlyphs = Glyphs.Underlying)
|
|
{
|
|
// Get glyph positions
|
|
var glyphPositions = GlyphPositions.ToArray();
|
|
|
|
// Create the font
|
|
if (_font == null)
|
|
{
|
|
_font = new SKFont(this.Typeface, this.Style.FontSize * glyphScale);
|
|
}
|
|
_font.Hinting = ctx.Options.Hinting;
|
|
_font.Edging = ctx.Options.Edging;
|
|
_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());
|
|
|
|
if (_textBlob == null)
|
|
return;
|
|
}
|
|
|
|
// Paint underline
|
|
if (Style.Underline != UnderlineStyle.None && RunKind == FontRunKind.Normal)
|
|
{
|
|
// Work out underline metrics
|
|
float underlineYPos = Line.YCoord + Line.BaseLine + (_font.Metrics.UnderlinePosition ?? 0);
|
|
if (underlineYPos < Line.YCoord + Line.BaseLine + 1)
|
|
underlineYPos = Line.YCoord + Line.BaseLine + 1;
|
|
paint.StrokeWidth = _font.Metrics.UnderlineThickness ?? 1;
|
|
if (paint.StrokeWidth < 1)
|
|
paint.StrokeWidth = 1;
|
|
paintHalo.StrokeWidth = paint.StrokeWidth + Style.HaloWidth;
|
|
|
|
if (Style.Underline == UnderlineStyle.Gapped)
|
|
{
|
|
// Get intercept positions
|
|
var interceptPositions = _textBlob.GetIntercepts(underlineYPos - paint.StrokeWidth / 2, underlineYPos + paint.StrokeWidth);
|
|
|
|
// Paint gapped underlinline
|
|
float x = XCoord;
|
|
for (int i = 0; i < interceptPositions.Length; i += 2)
|
|
{
|
|
float b = interceptPositions[i] - paint.StrokeWidth;
|
|
if (x < b)
|
|
{
|
|
if (Style.HaloColor != SKColor.Empty)
|
|
ctx.Canvas.DrawLine(new SKPoint(x, underlineYPos), new SKPoint(b, underlineYPos), paintHalo);
|
|
ctx.Canvas.DrawLine(new SKPoint(x, underlineYPos), new SKPoint(b, underlineYPos), paint);
|
|
}
|
|
x = interceptPositions[i + 1] + paint.StrokeWidth;
|
|
}
|
|
if (x < XCoord + Width)
|
|
{
|
|
if (Style.HaloColor != SKColor.Empty)
|
|
ctx.Canvas.DrawLine(new SKPoint(x, underlineYPos), new SKPoint(XCoord + Width, underlineYPos), paintHalo);
|
|
ctx.Canvas.DrawLine(new SKPoint(x, underlineYPos), new SKPoint(XCoord + Width, underlineYPos), paint);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
switch (Style.Underline)
|
|
{
|
|
case UnderlineStyle.ImeInput:
|
|
paint.PathEffect = SKPathEffect.CreateDash(new float[] { paint.StrokeWidth, paint.StrokeWidth }, paint.StrokeWidth);
|
|
paintHalo.PathEffect = SKPathEffect.CreateDash(new float[] { paintHalo.StrokeWidth, paintHalo.StrokeWidth }, paintHalo.StrokeWidth);
|
|
break;
|
|
|
|
case UnderlineStyle.ImeConverted:
|
|
paint.PathEffect = SKPathEffect.CreateDash(new float[] { paint.StrokeWidth, paint.StrokeWidth }, paint.StrokeWidth);
|
|
paintHalo.PathEffect = SKPathEffect.CreateDash(new float[] { paintHalo.StrokeWidth, paintHalo.StrokeWidth }, paintHalo.StrokeWidth);
|
|
break;
|
|
|
|
case UnderlineStyle.ImeTargetConverted:
|
|
paint.StrokeWidth *= 2;
|
|
paintHalo.StrokeWidth *= 2;
|
|
break;
|
|
|
|
case UnderlineStyle.ImeTargetNonConverted:
|
|
break;
|
|
}
|
|
// Paint solid underline
|
|
if (Style.HaloColor != SKColor.Empty)
|
|
ctx.Canvas.DrawLine(new SKPoint(XCoord, underlineYPos), new SKPoint(XCoord + Width, underlineYPos), paintHalo);
|
|
ctx.Canvas.DrawLine(new SKPoint(XCoord, underlineYPos), new SKPoint(XCoord + Width, underlineYPos), paint);
|
|
paint.PathEffect = null;
|
|
paintHalo.PathEffect = null;
|
|
}
|
|
}
|
|
|
|
if (Style.HaloColor != SKColor.Empty)
|
|
{
|
|
// Paint strikethrough
|
|
if (Style.StrikeThrough != StrikeThroughStyle.None && RunKind == FontRunKind.Normal)
|
|
{
|
|
paint.StrokeWidth = _font.Metrics.StrikeoutThickness ?? 1;
|
|
if (paint.StrokeWidth < 1)
|
|
paint.StrokeWidth = 1; ;
|
|
float strikeYPos = Line.YCoord + Line.BaseLine + (_font.Metrics.StrikeoutPosition ?? 0) + glyphVOffset;
|
|
ctx.Canvas.DrawLine(new SKPoint(XCoord, strikeYPos), new SKPoint(XCoord + Width, strikeYPos), paintHalo);
|
|
}
|
|
ctx.Canvas.DrawText(_textBlob, 0, 0, paintHalo);
|
|
}
|
|
|
|
ctx.Canvas.DrawText(_textBlob, 0, 0, paint);
|
|
}
|
|
}
|
|
|
|
// Paint strikethrough
|
|
if (Style.StrikeThrough != StrikeThroughStyle.None && RunKind == FontRunKind.Normal)
|
|
{
|
|
paint.StrokeWidth = _font.Metrics.StrikeoutThickness ?? 1;
|
|
if (paint.StrokeWidth < 1)
|
|
paint.StrokeWidth = 1; ;
|
|
float strikeYPos = Line.YCoord + Line.BaseLine + (_font.Metrics.StrikeoutPosition ?? 0) + glyphVOffset;
|
|
ctx.Canvas.DrawLine(new SKPoint(XCoord, strikeYPos), new SKPoint(XCoord + Width, strikeYPos), paint);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Paint background of this font run
|
|
/// </summary>
|
|
/// <param name="ctx"></param>
|
|
internal void PaintBackground(PaintTextContext ctx)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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()
|
|
});
|
|
}
|
|
}
|