From 517d7eba712dc4aed2d57c7ff402325ccd8d2d89 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 30 Nov 2021 14:54:16 +0100 Subject: [PATCH 1/4] Handle TabStopps --- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 160 +++++++++++++++----- 1 file changed, 122 insertions(+), 38 deletions(-) diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 5f4980e4619..60f8311441b 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; +using System.Threading; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Utilities; using SkiaSharp; namespace Avalonia.Skia @@ -13,6 +14,25 @@ namespace Avalonia.Skia /// internal class FormattedTextImpl : IFormattedTextImpl { + private static readonly ThreadLocal t_builder = new ThreadLocal(() => new SKTextBlobBuilder()); + + private const float MAX_LINE_WIDTH = 10000; + + private readonly List> _foregroundBrushes = + new List>(); + private readonly List _lines = new List(); + private readonly SKPaint _paint; + private readonly List _rects = new List(); + public string Text { get; } + private readonly TextWrapping _wrapping; + private Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity); + private float _lineHeight = 0; + private float _lineOffset = 0; + private Rect _bounds; + private List _skiaLines; + private ReadOnlySlice _glyphs; + private ReadOnlySlice _advances; + public FormattedTextImpl( string text, Typeface typeface, @@ -23,12 +43,9 @@ public FormattedTextImpl( IReadOnlyList spans) { Text = text ?? string.Empty; - - // Replace 0 characters with zero-width spaces (200B) - Text = Text.Replace((char)0, (char)0x200B); - - var glyphTypeface = (GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl; + UpdateGlyphInfo(Text, typeface.GlyphTypeface, (float)fontSize); + _paint = new SKPaint { TextEncoding = SKTextEncoding.Utf16, @@ -37,7 +54,7 @@ public FormattedTextImpl( LcdRenderText = true, SubpixelText = true, IsLinearText = true, - Typeface = glyphTypeface.Typeface, + Typeface = ((GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl).Typeface, TextSize = (float)fontSize, TextAlign = textAlignment.ToSKTextAlign() }; @@ -195,6 +212,32 @@ public override string ToString() return Text; } + private void DrawTextBlob(int start, int length, float x, float y, SKCanvas canvas, SKPaint paint) + { + var glyphs = _glyphs.Buffer.Span.Slice(start, length); + var advances = _advances.Buffer.Span.Slice(start, length); + var builder = t_builder.Value; + + var buffer = builder.AllocateHorizontalRun(_paint.ToFont(), length, 0); + + buffer.SetGlyphs(glyphs); + + var positions = buffer.GetPositionSpan(); + + var pos = 0f; + + for (int i = 0; i < advances.Length; i++) + { + positions[i] = pos; + + pos += advances[i]; + } + + var blob = builder.Build(); + + canvas.DrawText(blob, x, y, paint); + } + internal void Draw(DrawingContextImpl context, SKCanvas canvas, SKPoint origin, @@ -244,16 +287,15 @@ internal void Draw(DrawingContextImpl context, if (!hasCusomFGBrushes) { - var subString = Text.Substring(line.Start, line.Length); - canvas.DrawText(subString, x, origin.Y + line.Top + _lineOffset, paint); + DrawTextBlob(line.Start, line.Length, x, origin.Y + line.Top + _lineOffset, canvas, paint); } else { float currX = x; - string subStr; float measure; int len; float factor; + switch (paint.TextAlign) { case SKTextAlign.Left: @@ -270,7 +312,7 @@ internal void Draw(DrawingContextImpl context, } var textLine = Text.Substring(line.Start, line.Length); - currX -= textLine.Length == 0 ? 0 : paint.MeasureText(textLine) * factor; + currX -= textLine.Length == 0 ? 0 : MeasureText(line.Start, line.Length) * factor; for (int i = line.Start; i < line.Start + line.Length;) { @@ -288,13 +330,12 @@ internal void Draw(DrawingContextImpl context, currentWrapper = foreground; } - subStr = Text.Substring(i, len); - measure = paint.MeasureText(subStr); + measure = MeasureText(i, len); currX += measure * factor; - ApplyWrapperTo(ref currentPaint, currentWrapper, ref currd, paint, canUseLcdRendering); + ApplyWrapperTo(ref currentPaint, currentWrapper, ref currd, paint, canUseLcdRendering); - canvas.DrawText(subStr, currX, origin.Y + line.Top + _lineOffset, paint); + DrawTextBlob(i, len, currX, origin.Y + line.Top + _lineOffset, canvas, paint); i += len; currX += measure * (1 - factor); @@ -310,21 +351,6 @@ internal void Draw(DrawingContextImpl context, } } - private const float MAX_LINE_WIDTH = 10000; - - private readonly List> _foregroundBrushes = - new List>(); - private readonly List _lines = new List(); - private readonly SKPaint _paint; - private readonly List _rects = new List(); - public string Text { get; } - private readonly TextWrapping _wrapping; - private Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity); - private float _lineHeight = 0; - private float _lineOffset = 0; - private Rect _bounds; - private List _skiaLines; - private static void ApplyWrapperTo(ref SKPaint current, DrawingContextImpl.PaintWrapper wrapper, ref IDisposable curr, SKPaint paint, bool canUseLcdRendering) { @@ -352,9 +378,8 @@ private static int LineBreak(string textInput, int textIndex, int stop, } else { - float measuredWidth; string subText = textInput.Substring(textIndex, stop - textIndex); - lengthBreak = (int)paint.BreakText(subText, maxWidth, out measuredWidth); + lengthBreak = (int)paint.BreakText(subText, maxWidth, out _); } //Check for white space or line breakers before the lengthBreak @@ -468,8 +493,7 @@ private void BuildRects() for (int i = line.Start; i < line.Start + line.TextLength; i++) { - var c = Text[i]; - var w = line.IsEmptyTrailingLine ? 0 :_paint.MeasureText(Text[i].ToString()); + var w = line.IsEmptyTrailingLine ? 0 : _advances[i]; _rects.Add(new Rect( prevRight, @@ -554,7 +578,7 @@ private void Rebuild() // This seems like the best measure of full vertical extent // matches Direct2D line height - _lineHeight = mDescent - mAscent; + _lineHeight = mDescent - mAscent + metrics.Leading; // Rendering is relative to baseline _lineOffset = (-metrics.Ascent); @@ -585,7 +609,7 @@ private void Rebuild() line.Start = curOff; line.TextLength = measured; subString = Text.Substring(line.Start, line.TextLength); - lineWidth = _paint.MeasureText(subString); + lineWidth = MeasureText(line.Start, line.TextLength); line.Length = measured - trailingnumber; line.Width = lineWidth; line.Height = _lineHeight; @@ -608,8 +632,7 @@ private void Rebuild() AvaloniaFormattedTextLine lastLine = new AvaloniaFormattedTextLine(); lastLine.TextLength = lengthDiff; lastLine.Start = curOff - lengthDiff; - var lastLineSubString = Text.Substring(line.Start, line.TextLength); - var lastLineWidth = _paint.MeasureText(lastLineSubString); + var lastLineWidth = MeasureText(line.Start, line.TextLength); lastLine.Length = 0; lastLine.Width = lastLineWidth; lastLine.Height = _lineHeight; @@ -668,6 +691,67 @@ private void Rebuild() } } + private float MeasureText(int start, int length) + { + var width = 0f; + + for (int i = start; i < length; i++) + { + var advance = _advances[i]; + + width += advance; + } + + return width; + } + + private void UpdateGlyphInfo(string text, GlyphTypeface glyphTypeface, float fontSize) + { + var glyphs = new ushort[text.Length]; + var advances = new float[text.Length]; + + var scale = fontSize / glyphTypeface.DesignEmHeight; + var width = 0f; + var characters = text.AsSpan(); + + for (int i = 0; i < characters.Length; i++) + { + var c = characters[i]; + float advance; + ushort glyph; + + switch (c) + { + case (char)0: + { + glyph = glyphTypeface.GetGlyph(0x200B); + advance = 0; + break; + } + case '\t': + { + glyph = glyphTypeface.GetGlyph(' '); + advance = glyphTypeface.GetGlyphAdvance(glyph) * scale * 4; + break; + } + default: + { + glyph = glyphTypeface.GetGlyph(c); + advance = glyphTypeface.GetGlyphAdvance(glyph) * scale; + break; + } + } + + glyphs[i] = glyph; + advances[i] = advance; + + width += advance; + } + + _glyphs = new ReadOnlySlice(glyphs); + _advances = new ReadOnlySlice(advances); + } + private float TransformX(float originX, float lineWidth, SKTextAlign align) { float x = 0; From 03396dfa680d3b62ce1e6b17dabb342a14de6c47 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 1 Dec 2021 12:43:40 +0100 Subject: [PATCH 2/4] Remove redundant SubString usage --- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 60f8311441b..a632376637c 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -311,8 +311,7 @@ internal void Draw(DrawingContextImpl context, throw new ArgumentOutOfRangeException(); } - var textLine = Text.Substring(line.Start, line.Length); - currX -= textLine.Length == 0 ? 0 : MeasureText(line.Start, line.Length) * factor; + currX -= line.Length == 0 ? 0 : MeasureText(line.Start, line.Length) * factor; for (int i = line.Start; i < line.Start + line.Length;) { From 2cfd084db32d672f9776c3e35aa514eed35131bc Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 1 Dec 2021 19:43:53 +0100 Subject: [PATCH 3/4] Fix empty text render --- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index a632376637c..2be8caaa293 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -214,6 +214,11 @@ public override string ToString() private void DrawTextBlob(int start, int length, float x, float y, SKCanvas canvas, SKPaint paint) { + if(length == 0) + { + return; + } + var glyphs = _glyphs.Buffer.Span.Slice(start, length); var advances = _advances.Buffer.Span.Slice(start, length); var builder = t_builder.Value; @@ -235,7 +240,10 @@ private void DrawTextBlob(int start, int length, float x, float y, SKCanvas canv var blob = builder.Build(); - canvas.DrawText(blob, x, y, paint); + if(blob != null) + { + canvas.DrawText(blob, x, y, paint); + } } internal void Draw(DrawingContextImpl context, From 58e744050cf7ed1e90527df38a5915e015dc487c Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 1 Dec 2021 20:06:23 +0100 Subject: [PATCH 4/4] Fix MeasureText --- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 2be8caaa293..81a79201d0d 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -702,7 +702,7 @@ private float MeasureText(int start, int length) { var width = 0f; - for (int i = start; i < length; i++) + for (int i = start; i < start + length; i++) { var advance = _advances[i];