#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. // Modifications by Alexander Tsyganeko: marked by ATZ using HarfBuzzSharp; using SkiaSharp; using System; using System.Collections.Generic; using System.Threading; using Topten.RichTextKit.Utils; namespace Topten.RichTextKit { /// /// Represents a font run - a physical sequence of laid glyphs all with /// the same font and style attributes. /// public class FontRun { /// /// The kind of font run. /// public FontRunKind RunKind = FontRunKind.Normal; /// /// The style run this typeface run was derived from. /// public StyleRun StyleRun; /// /// Get the code points of this run /// public Slice CodePoints => CodePointBuffer.SubSlice(Start, Length); /// /// Code point index of the start of this run /// public int Start; /// /// The length of this run in codepoints /// public int Length; /// /// The index of the first character after this run /// public int End => Start + Length; /// /// The user supplied style for this run /// public IStyle Style; /// /// The direction of this run /// public TextDirection Direction; /// /// The typeface of this run (use this over Style.Fontface) /// public SKTypeface Typeface; /// /// The glyph indicies /// public Slice Glyphs; /// /// The glyph positions (relative to the entire text block) /// public Slice GlyphPositions; /// /// The cluster numbers for each glyph /// public Slice Clusters; /// /// The x-coords of each code point, relative to this text run /// public Slice RelativeCodePointXCoords; /// /// Get the x-coord of a code point /// /// /// For LTR runs this will be the x-coordinate to the left, or RTL /// runs it will be the x-coordinate to the right. /// /// The code point index (relative to the entire text block) /// The x-coord relative to the entire text block 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]; } /// /// The ascent of the font used in this run /// public float Ascent; /// /// The descent of the font used in this run /// public float Descent; /// /// The leading of the font used in this run /// public float Leading; /// /// The height of text in this run (ascent + descent) /// public float TextHeight => -Ascent + Descent; /// /// Calculate the half leading height for text in this run /// public float HalfLeading => (TextHeight * (Style.LineHeight - 1) + Leading) / 2; /// /// Width of this typeface run /// public float Width; /// /// Horizontal position of this run, relative to the left margin /// public float XCoord; /// /// The line that owns this font run /// public TextLine Line { get; internal set; } /// /// Get the next font run from this one /// public FontRun NextRun { get { var allRuns = Line.TextBlock.FontRuns as List; int index = allRuns.IndexOf(this); if (index < 0 || index + 1 >= Line.Runs.Count) return null; return Line.Runs[index + 1]; } } /// /// Get the previous font run from this one /// public FontRun PreviousRun { get { var allRuns = Line.TextBlock.FontRuns as List; int index = allRuns.IndexOf(this); if (index - 1 < 0) return null; return Line.Runs[index - 1]; } } /// /// For debugging /// /// Debug string 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}'"; } } /// /// Moves all glyphs by the specified offset amount /// /// The x-delta to move glyphs by /// The y-delta to move glyphs by 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; } /// /// 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 /// /// The code point index to measure to /// The distance from the start to the specified code point 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]; } } /// /// Calculate the position at which to break a text run /// /// The max width available /// Whether to force the use of at least one glyph /// The code point position to break at 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; } /// /// 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. /// /// The code point index to split at /// A new typeface run for the split off part internal FontRun Split(int splitAtCodePoint) { if (this.Direction == TextDirection.LTR) { return SplitLTR(splitAtCodePoint); } else { return SplitRTL(splitAtCodePoint); } } /// /// 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) /// /// To code point position to split at /// The RHS run after splitting 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; } // ATZ: remove \r symbol if (codePointSplitPos > 0 && this.CodePoints[codePointSplitPos - 1] == '\r') { codePointSplitPos--; glyphSplitPos--; sliceLeftWidth = this.RelativeCodePointXCoords[codePointSplitPos]; if (codePointSplitPos == 0 || glyphSplitPos == 0) this.RunKind = FontRunKind.TrailingWhitespace; } // 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; } /// /// 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) /// /// To code point position to split at /// The LHS run after splitting 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); // ATZ: remove \r symbol if (codePointSplitPos > 0 && this.CodePoints[codePointSplitPos - 1] == '\r') { codePointSplitPos++; glyphSplitPos++; sliceLeftWidth = this.RelativeCodePointXCoords[codePointSplitPos]; sliceRightWidth = this.Width - sliceLeftWidth; if (codePointSplitPos >= this.CodePoints.Length || glyphSplitPos >= this.Glyphs.Length) this.RunKind = FontRunKind.TrailingWhitespace; } // 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; } /// /// The global list of code points /// internal Buffer CodePointBuffer; /// /// Calculate any overhang for this text line /// /// /// /// 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) { // ATZ: should it be Glyphs.Start here? VVVVV 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; } } } } } } /// /// Paint this font run /// /// 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()); } // Paint underline if (Style.Underline != UnderlineStyle.None && RunKind == FontRunKind.Normal) { // Work out underline metrics float underlineYPos = Line.YCoord + Line.BaseLine + (_font.Metrics.UnderlinePosition ?? 0); paint.StrokeWidth = _font.Metrics.UnderlineThickness ?? 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); // ATZ : draw to path if (ctx.Options.DrawToPath != null) ctx.Options.DrawToPath.AddRect(new SKRect(x, underlineYPos, b, underlineYPos + 1)); else // 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); // ATZ : draw to path if (ctx.Options.DrawToPath != null) ctx.Options.DrawToPath.AddRect(new SKRect(x, underlineYPos, XCoord + Width, underlineYPos + paint.StrokeWidth)); else // 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); // ATZ : draw to path if (ctx.Options.DrawToPath != null) ctx.Options.DrawToPath.AddRect(new SKRect(XCoord, underlineYPos, XCoord + Width, underlineYPos + paint.StrokeWidth)); else // 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 ?? 0; 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); } // ATZ : draw to path if (ctx.Options.DrawToPath != null) { paint.Typeface = _font.Typeface; paint.TextSize = _font.Size; paint.TextEncoding = SKTextEncoding.GlyphId; ctx.Options.DrawToPath.AddPath( paint.GetTextPath((IntPtr)(pGlyphs + Glyphs.Start), Glyphs.Length * sizeof(ushort), GlyphPositions.AsSpan())); } else // ctx.Canvas.DrawText(_textBlob, 0, 0, paint); } } // Paint strikethrough if (Style.StrikeThrough != StrikeThroughStyle.None && RunKind == FontRunKind.Normal) { paint.StrokeWidth = _font.Metrics.StrikeoutThickness ?? 0; float strikeYPos = Line.YCoord + Line.BaseLine + (_font.Metrics.StrikeoutPosition ?? 0) + glyphVOffset; // ATZ : draw to path if (ctx.Options.DrawToPath != null) ctx.Options.DrawToPath.AddRect(new SKRect(XCoord, strikeYPos, XCoord + Width, strikeYPos + paint.StrokeWidth)); else // ctx.Canvas.DrawLine(new SKPoint(XCoord, strikeYPos), new SKPoint(XCoord + Width, strikeYPos), paint); } } } /// /// Paint background of this font run /// /// 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> Pool = new ThreadLocal>(() => new ObjectPool() { Cleaner = (r) => r.Reset() }); } }