Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle TabStopps #7044

Merged
merged 10 commits into from
Dec 6, 2021
169 changes: 130 additions & 39 deletions src/Skia/Avalonia.Skia/FormattedTextImpl.cs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -13,6 +14,25 @@ namespace Avalonia.Skia
/// </summary>
internal class FormattedTextImpl : IFormattedTextImpl
{
private static readonly ThreadLocal<SKTextBlobBuilder> t_builder = new ThreadLocal<SKTextBlobBuilder>(() => new SKTextBlobBuilder());

private const float MAX_LINE_WIDTH = 10000;

private readonly List<KeyValuePair<FBrushRange, IBrush>> _foregroundBrushes =
new List<KeyValuePair<FBrushRange, IBrush>>();
private readonly List<FormattedTextLine> _lines = new List<FormattedTextLine>();
private readonly SKPaint _paint;
private readonly List<Rect> _rects = new List<Rect>();
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<AvaloniaFormattedTextLine> _skiaLines;
private ReadOnlySlice<ushort> _glyphs;
private ReadOnlySlice<float> _advances;

public FormattedTextImpl(
string text,
Typeface typeface,
Expand All @@ -23,12 +43,9 @@ public FormattedTextImpl(
IReadOnlyList<FormattedTextStyleSpan> 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,
Expand All @@ -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()
};
Expand Down Expand Up @@ -195,6 +212,40 @@ public override string ToString()
return Text;
}

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;

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();

if(blob != null)
{
canvas.DrawText(blob, x, y, paint);
}
}

internal void Draw(DrawingContextImpl context,
SKCanvas canvas,
SKPoint origin,
Expand Down Expand Up @@ -244,16 +295,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:
Expand All @@ -269,8 +319,7 @@ internal void Draw(DrawingContextImpl context,
throw new ArgumentOutOfRangeException();
}

var textLine = Text.Substring(line.Start, line.Length);
currX -= textLine.Length == 0 ? 0 : paint.MeasureText(textLine) * factor;
currX -= line.Length == 0 ? 0 : MeasureText(line.Start, line.Length) * factor;

for (int i = line.Start; i < line.Start + line.Length;)
{
Expand All @@ -288,13 +337,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);
Expand All @@ -310,21 +358,6 @@ internal void Draw(DrawingContextImpl context,
}
}

private const float MAX_LINE_WIDTH = 10000;

private readonly List<KeyValuePair<FBrushRange, IBrush>> _foregroundBrushes =
new List<KeyValuePair<FBrushRange, IBrush>>();
private readonly List<FormattedTextLine> _lines = new List<FormattedTextLine>();
private readonly SKPaint _paint;
private readonly List<Rect> _rects = new List<Rect>();
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<AvaloniaFormattedTextLine> _skiaLines;

private static void ApplyWrapperTo(ref SKPaint current, DrawingContextImpl.PaintWrapper wrapper,
ref IDisposable curr, SKPaint paint, bool canUseLcdRendering)
{
Expand Down Expand Up @@ -352,9 +385,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
Expand Down Expand Up @@ -468,8 +500,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,
Expand Down Expand Up @@ -554,7 +585,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);
Expand Down Expand Up @@ -585,7 +616,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;
Expand All @@ -608,8 +639,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;
Expand Down Expand Up @@ -668,6 +698,67 @@ private void Rebuild()
}
}

private float MeasureText(int start, int length)
{
var width = 0f;

for (int i = start; i < start + 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest making the tab width configurable like in .editorconfig .

break;
}
default:
{
glyph = glyphTypeface.GetGlyph(c);
advance = glyphTypeface.GetGlyphAdvance(glyph) * scale;
break;
}
}

glyphs[i] = glyph;
advances[i] = advance;

width += advance;
}

_glyphs = new ReadOnlySlice<ushort>(glyphs);
_advances = new ReadOnlySlice<float>(advances);
}

private float TransformX(float originX, float lineWidth, SKTextAlign align)
{
float x = 0;
Expand Down