Browse Source

Merge pull request #559 from donandren/skiaimprovements

Skia improvements
pull/576/head
Nikita Tsukanov 10 years ago
committed by GitHub
parent
commit
3eb6901d69
  1. 2
      nuget/template/Avalonia.Skia.Desktop.nuspec
  2. 4
      src/Skia/Avalonia.Skia.Android/Avalonia.Skia.Android.csproj
  3. 2
      src/Skia/Avalonia.Skia.Android/packages.config
  4. 8
      src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.csproj
  5. 2
      src/Skia/Avalonia.Skia.Desktop/packages.config
  6. 4
      src/Skia/Avalonia.Skia.iOS/Avalonia.Skia.iOS.csproj
  7. 2
      src/Skia/Avalonia.Skia.iOS/packages.config
  8. 4
      src/Skia/Avalonia.Skia/BitmapImpl.cs
  9. 62
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  10. 712
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  11. 2
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  12. 1
      tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems
  13. 321
      tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs

2
nuget/template/Avalonia.Skia.Desktop.nuspec

@ -13,7 +13,7 @@
<copyright>Copyright 2015</copyright>
<tags>Avalonia</tags>
<dependencies>
<dependency id="SkiaSharp" version="1.49.2.1-beta"/>
<dependency id="SkiaSharp" version="1.49.3.0-beta"/>
<dependency id="Avalonia" version="#VERSION#" />
</dependencies>
</metadata>

4
src/Skia/Avalonia.Skia.Android/Avalonia.Skia.Android.csproj

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@ -40,7 +40,7 @@
<Reference Include="Mono.Android" />
<Reference Include="mscorlib" />
<Reference Include="SkiaSharp, Version=1.49.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\..\..\packages\SkiaSharp.1.49.2.1-beta\lib\MonoAndroid\SkiaSharp.dll</HintPath>
<HintPath>..\..\..\packages\SkiaSharp.1.49.3.0-beta\lib\MonoAndroid\SkiaSharp.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />

2
src/Skia/Avalonia.Skia.Android/packages.config

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="SkiaSharp" version="1.49.2.1-beta" targetFramework="monoandroid50" />
<package id="SkiaSharp" version="1.49.3.0-beta" targetFramework="monoandroid50" />
</packages>

8
src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.csproj

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
@ -57,7 +57,7 @@
</PropertyGroup>
<ItemGroup>
<Reference Include="SkiaSharp, Version=1.49.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\..\..\packages\SkiaSharp.1.49.2.1-beta\lib\net45\SkiaSharp.dll</HintPath>
<HintPath>..\..\..\packages\SkiaSharp.1.49.3.0-beta\lib\net45\SkiaSharp.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
@ -116,12 +116,12 @@
<Import Project="..\Avalonia.Skia\Avalonia.Skia.projitems" Label="Shared" />
<Import Project="..\..\Shared\RenderHelpers\RenderHelpers.projitems" Label="Shared" />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\..\..\packages\SkiaSharp.1.49.2.1-beta\build\net45\SkiaSharp.targets" Condition="Exists('..\..\..\packages\SkiaSharp.1.49.2.1-beta\build\net45\SkiaSharp.targets')" />
<Import Project="..\..\..\packages\SkiaSharp.1.49.3.0-beta\build\net45\SkiaSharp.targets" Condition="Exists('..\..\..\packages\SkiaSharp.1.49.3.0-beta\build\net45\SkiaSharp.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\packages\SkiaSharp.1.49.2.1-beta\build\net45\SkiaSharp.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\SkiaSharp.1.49.2.1-beta\build\net45\SkiaSharp.targets'))" />
<Error Condition="!Exists('..\..\..\packages\SkiaSharp.1.49.3.0-beta\build\net45\SkiaSharp.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\SkiaSharp.1.49.3.0-beta\build\net45\SkiaSharp.targets'))" />
</Target>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.

2
src/Skia/Avalonia.Skia.Desktop/packages.config

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="SkiaSharp" version="1.49.2.1-beta" targetFramework="net45" />
<package id="SkiaSharp" version="1.49.3.0-beta" targetFramework="net45" />
</packages>

4
src/Skia/Avalonia.Skia.iOS/Avalonia.Skia.iOS.csproj

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@ -41,7 +41,7 @@
</ItemGroup>
<ItemGroup>
<Reference Include="SkiaSharp, Version=1.49.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\..\..\packages\SkiaSharp.1.49.2.1-beta\lib\XamariniOS\SkiaSharp.dll</HintPath>
<HintPath>..\..\..\packages\SkiaSharp.1.49.3.0-beta\lib\XamariniOS\SkiaSharp.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />

2
src/Skia/Avalonia.Skia.iOS/packages.config

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="SkiaSharp" version="1.49.2.1-beta" targetFramework="xamarinios10" />
<package id="SkiaSharp" version="1.49.3.0-beta" targetFramework="xamarinios10" />
</packages>

4
src/Skia/Avalonia.Skia/BitmapImpl.cs

@ -16,8 +16,8 @@ namespace Avalonia.Skia
public BitmapImpl(SKBitmap bm)
{
Bitmap = bm;
PixelHeight = bm.Width;
PixelWidth = bm.Height;
PixelHeight = bm.Height;
PixelWidth = bm.Width;
}
public BitmapImpl(int width, int height)

62
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -1,10 +1,10 @@
using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.RenderHelpers;
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.RenderHelpers;
namespace Avalonia.Skia
{
@ -23,7 +23,11 @@ namespace Avalonia.Skia
var impl = (BitmapImpl)source.PlatformImpl;
var s = sourceRect.ToSKRect();
var d = destRect.ToSKRect();
Canvas.DrawBitmap(impl.Bitmap, s, d);
using (var paint = new SKPaint()
{ Color = new SKColor(255, 255, 255, (byte)(255 * opacity)) })
{
Canvas.DrawBitmap(impl.Bitmap, s, d, paint);
}
}
public void DrawLine(Pen pen, Point p1, Point p2)
@ -53,14 +57,44 @@ namespace Avalonia.Skia
}
}
struct PaintWrapper : IDisposable
private struct PaintState : IDisposable
{
private readonly SKColor _color;
private readonly SKShader _shader;
private readonly SKPaint _paint;
public PaintState(SKPaint paint, SKColor color, SKShader shader)
{
_paint = paint;
_color = color;
_shader = shader;
}
public void Dispose()
{
_paint.Color = _color;
_paint.Shader = _shader;
}
}
internal struct PaintWrapper : IDisposable
{
//We are saving memory allocations there
//TODO: add more disposable fields if needed
public readonly SKPaint Paint;
private IDisposable _disposable1;
public IDisposable ApplyTo(SKPaint paint)
{
var state = new PaintState(paint, paint.Color, paint.Shader);
paint.Color = Paint.Color;
paint.Shader = Paint.Shader;
return state;
}
public void AddDisposable(IDisposable disposable)
{
if (_disposable1 == null)
@ -82,7 +116,7 @@ namespace Avalonia.Skia
}
}
private PaintWrapper CreatePaint(IBrush brush, Size targetSize)
internal PaintWrapper CreatePaint(IBrush brush, Size targetSize)
{
SKPaint paint = new SKPaint();
var rv = new PaintWrapper(paint);
@ -95,14 +129,16 @@ namespace Avalonia.Skia
SKColor color = new SKColor(255, 255, 255, 255);
var solid = brush as SolidColorBrush;
var solid = brush as ISolidColorBrush;
if (solid != null)
color = solid.Color.ToSKColor();
paint.Color = (new SKColor(color.Red, color.Green, color.Blue, (byte) (color.Alpha*opacity)));
paint.Color = (new SKColor(color.Red, color.Green, color.Blue, (byte)(color.Alpha * opacity)));
if (solid != null)
{
return rv;
}
var gradient = brush as GradientBrush;
if (gradient != null)
@ -150,7 +186,7 @@ namespace Avalonia.Skia
var bitmap = new BitmapImpl((int)helper.IntermediateSize.Width, (int)helper.IntermediateSize.Height);
rv.AddDisposable(bitmap);
using (var ctx = bitmap.CreateDrawingContext())
helper.DrawIntermediate(ctx);
helper.DrawIntermediate(ctx);
SKMatrix translation = SKMatrix.MakeTranslation(-(float)helper.DestinationRect.X, -(float)helper.DestinationRect.Y);
SKShaderTileMode tileX =
tileBrush.TileMode == TileMode.None
@ -255,7 +291,7 @@ namespace Avalonia.Skia
using (var paint = CreatePaint(foreground, text.Measure()))
{
var textImpl = text.PlatformImpl as FormattedTextImpl;
textImpl.Draw(this.Canvas, origin.ToSKPoint());
textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint);
}
}
@ -270,7 +306,7 @@ namespace Avalonia.Skia
Canvas.Restore();
}
double _currentOpacity = 1.0f;
private double _currentOpacity = 1.0f;
private readonly Stack<double> _opacityStack = new Stack<double>();
public void PushOpacity(double opacity)

712
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@ -1,66 +1,60 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Media;
using Avalonia.Platform;
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Avalonia.Skia
{
unsafe class FormattedTextImpl : IFormattedTextImpl
public class FormattedTextImpl : IFormattedTextImpl
{
public SKPaint Paint { get; private set; }
public FormattedTextImpl(string text)
public FormattedTextImpl(string text, string fontFamilyName, double fontSize, FontStyle fontStyle,
TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping)
{
_text = text;
Paint = new SKPaint();
Paint.TextEncoding = SKTextEncoding.Utf16;
Paint.IsStroke = false;
Paint.IsAntialias = true;
LineOffset = 0;
_text = text ?? string.Empty;
// Replace 0 characters with zero-width spaces (200B)
_text = _text.Replace((char)0, (char)0x200B);
}
public static FormattedTextImpl Create(string text, string fontFamilyName, double fontSize, FontStyle fontStyle,
TextAlignment textAlignment, FontWeight fontWeight)
{
var typeface = TypefaceCache.GetTypeface(fontFamilyName, fontStyle, fontWeight);
FormattedTextImpl instance = new FormattedTextImpl(text);
instance.Paint.Typeface = typeface;
instance.Paint.TextSize = (float)fontSize;
instance.Paint.TextAlign = textAlignment.ToSKTextAlign();
instance.Rebuild();
return instance;
}
_paint = new SKPaint();
private readonly string _text;
//currently Skia does not measure properly with Utf8 !!!
//Paint.TextEncoding = SKTextEncoding.Utf8;
_paint.TextEncoding = SKTextEncoding.Utf16;
_paint.IsStroke = false;
_paint.IsAntialias = true;
_paint.Typeface = typeface;
_paint.TextSize = (float)fontSize;
_paint.TextAlign = textAlignment.ToSKTextAlign();
readonly List<FormattedTextLine> _lines = new List<FormattedTextLine>();
readonly List<Rect> _rects = new List<Rect>();
_wrapping = wrapping;
List<AvaloniaFormattedTextLine> _skiaLines;
SKRect[] _skiaRects;
Rebuild();
}
Size _size;
public Size Constraint
{
get { return _constraint; }
set
{
if (_constraint == value)
return;
const float MAX_LINE_WIDTH = 10000;
float LineOffset;
float WidthConstraint = -1;
_constraint = value;
struct AvaloniaFormattedTextLine
Rebuild();
}
}
public void Dispose()
{
public float Top;
public int Start;
public int Length;
public float Height;
public float Width;
};
}
public IEnumerable<FormattedTextLine> GetLines()
{
@ -69,36 +63,98 @@ namespace Avalonia.Skia
public TextHitTestResult HitTestPoint(Point point)
{
for (int c = 0; c < _rects.Count; c++)
float y = (float)point.Y;
var line = _skiaLines.Find(l => l.Top <= y && (l.Top + l.Height) > y);
if (!line.Equals(default(AvaloniaFormattedTextLine)))
{
//TODO: Detect line first
var rc = _rects[c];
if (rc.Contains(point))
var rects = GetRects();
for (int c = line.Start; c < line.Start + line.TextLength; c++)
{
return new TextHitTestResult
var rc = rects[c];
if (rc.Contains(point))
{
IsInside = true,
TextPosition = c,
IsTrailing = (point.X - rc.X) > rc.Width / 2
};
return new TextHitTestResult
{
IsInside = !(line.TextLength > line.Length),
TextPosition = c,
IsTrailing = (point.X - rc.X) > rc.Width / 2
};
}
}
int offset = 0;
if (point.X >= (rects[line.Start].X + line.Width) / 2 && line.Length > 0)
{
offset = line.TextLength > line.Length ?
line.Length : (line.Length - 1);
}
return new TextHitTestResult
{
IsInside = false,
TextPosition = line.Start + offset,
IsTrailing = _text.Length == (line.Start + offset + 1)
};
}
bool end = point.X > _size.Width || point.Y > _size.Height;
return new TextHitTestResult() { IsTrailing = end, TextPosition = end ? _text.Length - 1 : 0 };
bool end = point.X > _size.Width || point.Y > _lines.Sum(l => l.Height);
return new TextHitTestResult()
{
IsInside = false,
IsTrailing = end,
TextPosition = end ? _text.Length - 1 : 0
};
}
public Rect HitTestTextPosition(int index)
{
if (index < 0 || index >= _rects.Count)
return new Rect();
return _rects[index];
var rects = GetRects();
if (index < 0 || index >= rects.Count)
{
var r = rects.LastOrDefault();
return new Rect(r.X + r.Width, r.Y, 0, _lineHeight);
}
if (rects.Count == 0)
{
return new Rect(0, 0, 1, _lineHeight);
}
if (index == rects.Count)
{
var lr = rects[rects.Count - 1];
return new Rect(new Point(lr.X + lr.Width, lr.Y), rects[index - 1].Size);
}
return rects[index];
}
public IEnumerable<Rect> HitTestTextRange(int index, int length)
{
for (var c = 0; c < length; c++)
yield return _rects[c + index];
List<Rect> result = new List<Rect>();
var rects = GetRects();
int lastIndex = index + length - 1;
foreach (var line in _skiaLines.Where(l =>
(l.Start + l.Length) > index &&
lastIndex >= l.Start))
{
int lineEndIndex = line.Start + (line.Length > 0 ? line.Length - 1 : 0);
double left = rects[line.Start > index ? line.Start : index].X;
double right = rects[lineEndIndex > lastIndex ? lastIndex : lineEndIndex].Right;
result.Add(new Rect(left, line.Top, right - left, line.Height));
}
return result;
}
public Size Measure()
@ -108,135 +164,400 @@ namespace Avalonia.Skia
public void SetForegroundBrush(IBrush brush, int startIndex, int length)
{
// TODO: we need an implementation here to properly support FormattedText
var key = new FBrushRange(startIndex, length);
if (brush == null)
{
if (_foregroundBrushes.ContainsKey(key))
_foregroundBrushes.Remove(key);
}
else
{
_foregroundBrushes[key] = brush;
}
}
void Rebuild()
public override string ToString()
{
var length = _text.Length;
return _text;
}
_lines.Clear();
internal void Draw(DrawingContextImpl context,
SKCanvas canvas, SKPoint origin,
DrawingContextImpl.PaintWrapper foreground)
{
/* TODO: This originated from Native code, it might be useful for debugging character positions as
* we improve the FormattedText support. Will need to port this to C# obviously. Rmove when
* not needed anymore.
_skiaRects = new SKRect[length];
_skiaLines = new List<AvaloniaFormattedTextLine>();
SkPaint dpaint;
ctx->Canvas->save();
ctx->Canvas->translate(origin.fX, origin.fY);
for (int c = 0; c < Lines.size(); c++)
{
dpaint.setARGB(255, 0, 0, 0);
SkRect rc;
rc.fLeft = 0;
rc.fTop = Lines[c].Top;
rc.fRight = Lines[c].Width;
rc.fBottom = rc.fTop + LineOffset;
ctx->Canvas->drawRect(rc, dpaint);
}
for (int c = 0; c < Length; c++)
{
dpaint.setARGB(255, c % 10 * 125 / 10 + 125, (c * 7) % 10 * 250 / 10, (c * 13) % 10 * 250 / 10);
dpaint.setStyle(SkPaint::kFill_Style);
ctx->Canvas->drawRect(Rects[c], dpaint);
}
ctx->Canvas->restore();
*/
SKPaint paint = _paint;
IDisposable currd = null;
var currentWrapper = foreground;
int curOff = 0;
float curY = 0;
try
{
SKPaint currFGPaint = ApplyWrapperTo(ref foreground, ref currd, paint);
bool hasCusomFGBrushes = _foregroundBrushes.Any();
var metrics = Paint.FontMetrics;
var mTop = metrics.Top; // The greatest distance above the baseline for any glyph (will be <= 0).
var mBottom = metrics.Bottom; // The greatest distance below the baseline for any glyph (will be >= 0).
var mLeading = metrics.Leading; // The recommended distance to add between lines of text (will be >= 0).
for (int c = 0; c < _skiaLines.Count; c++)
{
AvaloniaFormattedTextLine line = _skiaLines[c];
// This seems like the best measure of full vertical extent
float lineHeight = mBottom - mTop;
float x = TransformX(origin.X, 0, paint.TextAlign);
// Rendering is relative to baseline
LineOffset = -metrics.Top;
if (!hasCusomFGBrushes)
{
var subString = _text.Substring(line.Start, line.Length);
canvas.DrawText(subString, x, origin.Y + line.Top + _lineOffset, paint);
}
else
{
float currX = x;
string subStr;
int len;
string subString;
for (int i = line.Start; i < line.Start + line.Length;)
{
var fb = GetNextForegroundBrush(ref line, i, out len);
byte[] bytes;
GCHandle pinnedArray;
IntPtr pointer;
if (fb != null)
{
//TODO: figure out how to get the brush size
currentWrapper = context.CreatePaint(fb, new Size());
}
else
{
if (!currentWrapper.Equals(foreground)) currentWrapper.Dispose();
currentWrapper = foreground;
}
for (int c = 0; curOff < length; c++)
subStr = _text.Substring(i, len);
if (currFGPaint != currentWrapper.Paint)
{
currFGPaint = ApplyWrapperTo(ref currentWrapper, ref currd, paint);
}
canvas.DrawText(subStr, currX, origin.Y + line.Top + _lineOffset, paint);
i += len;
currX += paint.MeasureText(subStr);
}
}
}
}
finally
{
float lineWidth = -1;
int measured;
int extraSkip = 0;
if (WidthConstraint <= 0)
if (!currentWrapper.Equals(foreground)) currentWrapper.Dispose();
currd?.Dispose();
}
}
private const float MAX_LINE_WIDTH = 10000;
private readonly Dictionary<FBrushRange, IBrush> _foregroundBrushes =
new Dictionary<FBrushRange, IBrush>();
private readonly List<FormattedTextLine> _lines = new List<FormattedTextLine>();
private readonly SKPaint _paint;
private readonly List<Rect> _rects = new List<Rect>();
private readonly string _text;
private readonly TextWrapping _wrapping;
private Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity);
private float _lineHeight = 0;
private float _lineOffset = 0;
private Size _size;
private List<AvaloniaFormattedTextLine> _skiaLines;
private static SKPaint ApplyWrapperTo(ref DrawingContextImpl.PaintWrapper wrapper,
ref IDisposable curr, SKPaint paint)
{
curr?.Dispose();
curr = wrapper.ApplyTo(paint);
return wrapper.Paint;
}
private static bool IsBreakChar(char c)
{
//white space or zero space whitespace
return char.IsWhiteSpace(c) || c == '\u200B';
}
private static int LineBreak(string textInput, int textIndex, int stop,
SKPaint paint, float maxWidth,
out int trailingCount)
{
int lengthBreak;
if (maxWidth == -1)
{
lengthBreak = stop - textIndex;
}
else
{
float measuredWidth;
string subText = textInput.Substring(textIndex, stop - textIndex);
lengthBreak = (int)paint.BreakText(subText, maxWidth, out measuredWidth) / 2;
}
//Check for white space or line breakers before the lengthBreak
int startIndex = textIndex;
int index = textIndex;
int word_start = textIndex;
bool prevBreak = true;
trailingCount = 0;
while (index < stop)
{
int prevText = index;
char currChar = textInput[index++];
bool currBreak = IsBreakChar(currChar);
if (!currBreak && prevBreak)
{
measured = length;
word_start = prevText;
}
else
{
float constraint = WidthConstraint;
if (constraint > MAX_LINE_WIDTH)
constraint = MAX_LINE_WIDTH;
subString = _text.Substring(curOff);
prevBreak = currBreak;
if (index > startIndex + lengthBreak)
{
if (currBreak)
{
// eat the rest of the whitespace
while (index < stop && IsBreakChar(textInput[index]))
{
index++;
}
// TODO: This method is not linking into SkiaSharp so we must use the RAW buffer version for now
//measured = (int)Paint.BreakText(subString, constraint, out lineWidth) / 2;
bytes = Encoding.UTF8.GetBytes(subString);
pinnedArray = GCHandle.Alloc(bytes, GCHandleType.Pinned);
pointer = pinnedArray.AddrOfPinnedObject();
// for some reason I have to pass nBytes * 2. I assume under the hood it expects Unicode/WChar??
measured = (int)Paint.BreakText(pointer, (IntPtr)(bytes.Length * 2), constraint, out lineWidth) / 2;
pinnedArray.Free();
trailingCount = index - prevText;
}
else
{
// backup until a whitespace (or 1 char)
if (word_start == startIndex)
{
if (prevText > startIndex)
{
index = prevText;
}
}
else
{
index = word_start;
}
}
break;
}
if (measured == 0)
if ('\n' == currChar)
{
int ret = index - startIndex;
int lineBreakSize = 1;
if (index < stop)
{
measured = 1;
lineWidth = -1;
currChar = textInput[index++];
if ('\r' == currChar)
{
ret = index - startIndex;
++lineBreakSize;
}
}
char nextChar = ' ';
if (curOff + measured < length)
nextChar = _text[curOff + measured];
trailingCount = lineBreakSize;
if (nextChar != ' ')
return ret;
}
if ('\r' == currChar)
{
int ret = index - startIndex;
int lineBreakSize = 1;
if (index < stop)
{
// Perform scan for the last space and end the line there
for (int si = curOff + measured - 1; si > curOff; si--)
currChar = textInput[index++];
if ('\n' == currChar)
{
if (_text[si] == ' ')
{
measured = si - curOff;
extraSkip = 1;
break;
}
ret = index - startIndex;
++lineBreakSize;
}
}
trailingCount = lineBreakSize;
return ret;
}
}
AvaloniaFormattedTextLine line = new AvaloniaFormattedTextLine();
line.Start = curOff;
line.Length = measured;
line.Width = lineWidth;
line.Height = lineHeight;
line.Top = curY;
return index - startIndex;
}
private void BuildRects()
{
// Build character rects
var fm = _paint.FontMetrics;
SKTextAlign align = _paint.TextAlign;
for (int li = 0; li < _skiaLines.Count; li++)
{
var line = _skiaLines[li];
float prevRight = TransformX(0, line.Width, align);
double nextTop = line.Top + line.Height;
if (li + 1 < _skiaLines.Count)
{
nextTop = _skiaLines[li + 1].Top;
}
for (int i = line.Start; i < line.Start + line.TextLength; i++)
{
float w = _paint.MeasureText(_text[i].ToString());
_rects.Add(new Rect(
prevRight,
line.Top,
w,
nextTop - line.Top));
prevRight += w;
}
}
}
private IBrush GetNextForegroundBrush(ref AvaloniaFormattedTextLine line, int index, out int length)
{
IBrush result = null;
int len = length = line.Start + line.Length - index;
if (_foregroundBrushes.Any())
{
var cbi = _foregroundBrushes.FirstOrDefault(b => b.Key.Intersects(index, len));
if (cbi.Value != null)
{
var r = cbi.Key;
if (r.StartIndex > index)
{
len = r.StartIndex - index;
}
else
{
len = r.EndIndex - index + 1;
result = cbi.Value;
}
if (len > 0 && len < length)
{
length = len;
}
}
}
return result;
}
private List<Rect> GetRects()
{
if (_text.Length > _rects.Count)
{
BuildRects();
}
return _rects;
}
private void Rebuild()
{
var length = _text.Length;
_lines.Clear();
_rects.Clear();
_skiaLines = new List<AvaloniaFormattedTextLine>();
int curOff = 0;
float curY = 0;
var metrics = _paint.FontMetrics;
var mTop = metrics.Top; // The greatest distance above the baseline for any glyph (will be <= 0).
var mBottom = metrics.Bottom; // The greatest distance below the baseline for any glyph (will be >= 0).
var mLeading = metrics.Leading; // The recommended distance to add between lines of text (will be >= 0).
var mDescent = metrics.Descent; //The recommended distance below the baseline. Will be >= 0.
var mAscent = metrics.Ascent; //The recommended distance above the baseline. Will be <= 0.
var lastLineDescent = mBottom - mDescent;
// This seems like the best measure of full vertical extent
// matches Direct2D line height
_lineHeight = mDescent - mAscent;
// Rendering is relative to baseline
_lineOffset = -metrics.Top;
string subString;
float widthConstraint = (_constraint.Width != double.PositiveInfinity)
? (float)_constraint.Width
: -1;
for (int c = 0; curOff < length; c++)
{
float lineWidth = -1;
int measured;
int trailingnumber = 0;
subString = _text.Substring(curOff);
if (line.Width < 0)
line.Width = _skiaRects[line.Start + line.Length - 1].Right;
float constraint = -1;
// Build character rects
for (int i = line.Start; i < line.Start + line.Length; i++)
if (_wrapping == TextWrapping.Wrap)
{
float prevRight = 0;
if (i != line.Start)
prevRight = _skiaRects[i - 1].Right;
subString = _text.Substring(line.Start, i - line.Start + 1);
float w = Paint.MeasureText(subString);
SKRect rc;
rc.Left = prevRight;
rc.Right = w;
rc.Top = line.Top;
rc.Bottom = line.Top + line.Height;
_skiaRects[i] = rc;
constraint = widthConstraint <= 0 ? MAX_LINE_WIDTH : widthConstraint;
if (constraint > MAX_LINE_WIDTH)
constraint = MAX_LINE_WIDTH;
}
subString = _text.Substring(line.Start, line.Length);
line.Width = Paint.MeasureText(subString);
measured = LineBreak(_text, curOff, length, _paint, constraint, out trailingnumber);
AvaloniaFormattedTextLine line = new AvaloniaFormattedTextLine();
line.TextLength = measured;
subString = _text.Substring(line.Start, line.TextLength);
lineWidth = _paint.MeasureText(subString);
line.Start = curOff;
line.Length = measured - trailingnumber;
line.Width = lineWidth;
line.Height = _lineHeight;
line.Top = curY;
_skiaLines.Add(line);
curY += lineHeight;
curY += _lineHeight;
// TODO: We may want to consider adding Leading to the vertical line spacing but for now
// it appears to make no difference. Revisit as part of FormattedText improvements.
//
//curY += mLeading;
curY += mLeading;
curOff += measured + extraSkip;
curOff += measured;
}
// Now convert to Avalonia data formats
_lines.Clear();
_rects.Clear();
float maxX = 0;
for (var c = 0; c < _skiaLines.Count; c++)
@ -245,89 +566,72 @@ namespace Avalonia.Skia
if (maxX < w)
maxX = w;
_lines.Add(new FormattedTextLine(_skiaLines[c].Length, _skiaLines[c].Height));
}
for (var c = 0; c < _text.Length; c++)
{
_rects.Add(_skiaRects[c].ToAvaloniaRect());
_lines.Add(new FormattedTextLine(_skiaLines[c].TextLength, _skiaLines[c].Height));
}
if (_skiaLines.Count == 0)
{
_size = new Size();
_lines.Add(new FormattedTextLine(0, _lineHeight));
_size = new Size(0, _lineHeight + lastLineDescent);
}
else
{
var lastLine = _skiaLines[_skiaLines.Count - 1];
_size = new Size(maxX, lastLine.Top + lastLine.Height);
_size = new Size(maxX, lastLine.Top + lastLine.Height + lastLineDescent);
}
}
internal void Draw(SKCanvas canvas, SKPoint origin)
private float TransformX(float originX, float lineWidth, SKTextAlign align)
{
SKPaint paint = Paint;
float x = 0;
/* TODO: This originated from Native code, it might be useful for debugging character positions as
* we improve the FormattedText support. Will need to port this to C# obviously. Rmove when
* not needed anymore.
SkPaint dpaint;
ctx->Canvas->save();
ctx->Canvas->translate(origin.fX, origin.fY);
for (int c = 0; c < Lines.size(); c++)
{
dpaint.setARGB(255, 0, 0, 0);
SkRect rc;
rc.fLeft = 0;
rc.fTop = Lines[c].Top;
rc.fRight = Lines[c].Width;
rc.fBottom = rc.fTop + LineOffset;
ctx->Canvas->drawRect(rc, dpaint);
}
for (int c = 0; c < Length; c++)
if (align == SKTextAlign.Left)
{
x = originX;
}
else
{
double width = Constraint.Width > 0 && !double.IsPositiveInfinity(Constraint.Width) ?
Constraint.Width :
_size.Width;
switch (align)
{
dpaint.setARGB(255, c % 10 * 125 / 10 + 125, (c * 7) % 10 * 250 / 10, (c * 13) % 10 * 250 / 10);
dpaint.setStyle(SkPaint::kFill_Style);
ctx->Canvas->drawRect(Rects[c], dpaint);
case SKTextAlign.Center: x = originX + (float)(width - lineWidth) / 2; break;
case SKTextAlign.Right: x = originX + (float)(width - lineWidth); break;
}
ctx->Canvas->restore();
*/
for (int c = 0; c < _skiaLines.Count; c++)
{
AvaloniaFormattedTextLine line = _skiaLines[c];
var subString = _text.Substring(line.Start, line.Length);
canvas.DrawText(subString, origin.X, origin.Y + line.Top + LineOffset, paint);
}
return x;
}
Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity);
private struct AvaloniaFormattedTextLine
{
public float Height;
public int Length;
public int Start;
public int TextLength;
public float Top;
public float Width;
};
public Size Constraint
private struct FBrushRange
{
get { return _constraint; }
set
public FBrushRange(int startIndex, int length)
{
if (_constraint == value)
return;
StartIndex = startIndex;
Length = length;
}
_constraint = value;
WidthConstraint = (_constraint.Width != double.PositiveInfinity)
? (float)_constraint.Width
: -1;
public int EndIndex => StartIndex + Length - 1;
Rebuild();
}
}
public int Length { get; private set; }
public override string ToString()
{
return _text;
}
public int StartIndex { get; private set; }
public void Dispose()
{
public bool Intersects(int index, int len) =>
(index + len) > StartIndex &&
(StartIndex + Length) > index;
}
}
}
}

2
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -20,7 +20,7 @@ namespace Avalonia.Skia
public IFormattedTextImpl CreateFormattedText(string text, string fontFamilyName, double fontSize, FontStyle fontStyle,
TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping)
{
return FormattedTextImpl.Create(text, fontFamilyName, fontSize, fontStyle, textAlignment, fontWeight);
return new FormattedTextImpl(text, fontFamilyName, fontSize, fontStyle, textAlignment, fontWeight, wrapping);
}
public IStreamGeometryImpl CreateStreamGeometry()

1
tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems

@ -9,6 +9,7 @@
<Import_RootNamespace>Avalonia.RenderTests</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="Media\FormattedTextImplTests.cs" />
<Compile Include="Controls\ImageTests.cs" />
<Compile Include="Controls\BorderTests.cs" />
<Compile Include="Media\ImageBrushTests.cs" />

321
tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs

@ -0,0 +1,321 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Media;
using Avalonia.Platform;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using Xunit;
#if AVALONIA_CAIRO
namespace Avalonia.Cairo.RenderTests.Media
#elif AVALONIA_SKIA
namespace Avalonia.Skia.RenderTests
#else
using Avalonia.Direct2D1.RenderTests;
namespace Avalonia.Direct2D1.RenderTests.Media
#endif
{
public class FormattedTextImplTests : TestBase
{
private const string FontName = "Courier New";
private const double FontSize = 12;
private const double MediumFontSize = 18;
private const double BigFontSize = 32;
private const double FontSizeHeight = 13.594;//real value 13.59375
private const string stringword = "word";
private const string stringmiddle = "The quick brown fox jumps over the lazy dog";
private const string stringmiddle2lines = "The quick brown fox\njumps over the lazy dog";
private const string stringmiddle3lines = "01234567\n\n0123456789";
private const string stringmiddlenewlines = "012345678\r 1234567\r\n 12345678\n0123456789";
private const string stringlong =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis " +
"aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero" +
" at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus " +
"pretium ornare est.";
public FormattedTextImplTests()
: base(@"Media\FormattedText")
{
}
private IFormattedTextImpl Create(string text,
string fontFamily,
double fontSize,
FontStyle fontStyle,
TextAlignment textAlignment,
FontWeight fontWeight,
TextWrapping wrapping)
{
var r = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
return r.CreateFormattedText(text,
fontFamily,
fontSize,
fontStyle,
textAlignment,
fontWeight,
wrapping);
}
private IFormattedTextImpl Create(string text, double fontSize)
{
return Create(text, FontName, fontSize,
FontStyle.Normal, TextAlignment.Left,
FontWeight.Normal, TextWrapping.NoWrap);
}
private IFormattedTextImpl Create(string text, double fontSize, TextAlignment alignment)
{
return Create(text, FontName, fontSize,
FontStyle.Normal, alignment,
FontWeight.Normal, TextWrapping.NoWrap);
}
private IFormattedTextImpl Create(string text, double fontSize, TextWrapping wrap)
{
return Create(text, FontName, fontSize,
FontStyle.Normal, TextAlignment.Left,
FontWeight.Normal, wrap);
}
#if AVALONIA_CAIRO
[Theory(Skip = "TODO: Font scaling currently broken on cairo")]
#else
[Theory]
#endif
[InlineData("", FontSize, 0, FontSizeHeight)]
[InlineData("x", FontSize, 7.20, FontSizeHeight)]
[InlineData(stringword, FontSize, 28.80, FontSizeHeight)]
[InlineData(stringmiddle, FontSize, 309.65, FontSizeHeight)]
[InlineData(stringmiddle, MediumFontSize, 464.48, 20.391)]
[InlineData(stringmiddle, BigFontSize, 825.73, 36.25)]
[InlineData(stringmiddle2lines, FontSize, 165.63, 2 * FontSizeHeight)]
[InlineData(stringmiddle2lines, MediumFontSize, 248.44, 2 * 20.391)]
[InlineData(stringmiddle2lines, BigFontSize, 441.67, 2 * 36.25)]
[InlineData(stringlong, FontSize, 2160.35, FontSizeHeight)]
[InlineData(stringmiddlenewlines, FontSize, 72.01, 4 * FontSizeHeight)]
public void Should_Measure_String_Correctly(string input, double fontSize, double expWidth, double expHeight)
{
#if !AVALONIA_SKIA
double heightCorr = 0;
#else
//In skia there is a small descent added to last line,
//otherwise some letters are clipped at bottom
//4.55273438 for font 12 size
double heightCorr = 0.3793945*fontSize;
#endif
using (var fmt = Create(input, fontSize))
{
var size = fmt.Measure();
Assert.Equal(expWidth, size.Width, 2);
Assert.Equal(expHeight + heightCorr, size.Height, 2);
var linesHeight = fmt.GetLines().Sum(l => l.Height);
Assert.Equal(expHeight, linesHeight, 2);
}
}
#if AVALONIA_CAIRO
[Theory(Skip = "TODO: Font scaling currently broken on cairo")]
#else
[Theory]
#endif
[InlineData("", 1, -1, TextWrapping.NoWrap)]
[InlineData("x", 1, -1, TextWrapping.NoWrap)]
[InlineData(stringword, 1, -1, TextWrapping.NoWrap)]
[InlineData(stringmiddle, 1, -1, TextWrapping.NoWrap)]
[InlineData(stringmiddle, 3, 150, TextWrapping.Wrap)]
[InlineData(stringmiddle2lines, 2, -1, TextWrapping.NoWrap)]
[InlineData(stringmiddle2lines, 3, 150, TextWrapping.Wrap)]
[InlineData(stringlong, 1, -1, TextWrapping.NoWrap)]
[InlineData(stringlong, 18, 150, TextWrapping.Wrap)]
[InlineData(stringmiddlenewlines, 4, -1, TextWrapping.NoWrap)]
[InlineData(stringmiddlenewlines, 4, 150, TextWrapping.Wrap)]
public void Should_Break_Lines_String_Correctly(string input,
int linesCount,
double widthConstraint,
TextWrapping wrap)
{
using (var fmt = Create(input, FontSize, wrap))
{
if (widthConstraint != -1)
{
fmt.Constraint = new Size(widthConstraint, 10000);
}
var lines = fmt.GetLines().ToArray();
Assert.Equal(linesCount, lines.Count());
}
}
#if AVALONIA_CAIRO
[Theory(Skip = "TODO: Font scaling currently broken on cairo")]
#else
[Theory]
#endif
[InlineData("x", 0, 0, true, false, 0)]
[InlineData(stringword, -1, -1, false, false, 0)]
[InlineData(stringword, 25, 13, true, false, 3)]
[InlineData(stringword, 28.70, 13.5, true, true, 3)]
[InlineData(stringword, 30, 13, false, true, 3)]
[InlineData(stringword + "\r\n", 30, 13, false, false, 4)]
[InlineData(stringword + "\r\nnext", 30, 13, false, false, 4)]
[InlineData(stringword, 300, 13, false, true, 3)]
[InlineData(stringword + "\r\n", 300, 13, false, false, 4)]
[InlineData(stringword + "\r\nnext", 300, 13, false, false, 4)]
[InlineData(stringword, 300, 300, false, true, 3)]
//TODO: Direct2D implementation return textposition 6
//but the text is 6 length, can't find the logic for me it should be 5
//[InlineData(stringword + "\r\n", 300, 300, false, false, 6)]
[InlineData(stringword + "\r\nnext", 300, 300, false, true, 9)]
[InlineData(stringword + "\r\nnext", 300, 25, false, true, 9)]
[InlineData(stringword, 28, 15, false, true, 3)]
[InlineData(stringword, 30, 15, false, true, 3)]
[InlineData(stringmiddle3lines, 30, 15, false, false, 9)]
[InlineData(stringmiddle3lines, 500, 13, false, false, 8)]
[InlineData(stringmiddle3lines, 30, 25, false, false, 9)]
[InlineData(stringmiddle3lines, -1, 30, false, false, 10)]
public void Should_HitTestPoint_Correctly(string input,
double x, double y,
bool isInside, bool isTrailing, int pos)
{
using (var fmt = Create(input, FontSize))
{
var htRes = fmt.HitTestPoint(new Point(x, y));
Assert.Equal(pos, htRes.TextPosition);
Assert.Equal(isInside, htRes.IsInside);
Assert.Equal(isTrailing, htRes.IsTrailing);
}
}
#if AVALONIA_CAIRO
[Theory(Skip = "TODO: Font scaling currently broken on cairo")]
#else
[Theory]
#endif
[InlineData("", 0, 0, 0, 0, FontSizeHeight)]
[InlineData("x", 0, 0, 0, 7.20, FontSizeHeight)]
[InlineData("x", -1, 7.20, 0, 0, FontSizeHeight)]
[InlineData(stringword, 3, 21.60, 0, 7.20, FontSizeHeight)]
[InlineData(stringword, 4, 21.60 + 7.20, 0, 0, FontSizeHeight)]
[InlineData(stringmiddlenewlines, 10, 0, FontSizeHeight, 7.20, FontSizeHeight)]
[InlineData(stringmiddlenewlines, 15, 36.01, FontSizeHeight, 7.20, FontSizeHeight)]
[InlineData(stringmiddlenewlines, 20, 0, 2 * FontSizeHeight, 7.20, FontSizeHeight)]
[InlineData(stringmiddlenewlines, -1, 72.01, 3 * FontSizeHeight, 0, FontSizeHeight)]
public void Should_HitTestPosition_Correctly(string input,
int index, double x, double y, double width, double height)
{
using (var fmt = Create(input, FontSize))
{
var r = fmt.HitTestTextPosition(index);
Assert.Equal(x, r.X, 2);
Assert.Equal(y, r.Y, 2);
Assert.Equal(width, r.Width, 2);
Assert.Equal(height, r.Height, 2);
}
}
#if AVALONIA_CAIRO
[Theory(Skip = "TODO: Font scaling currently broken on cairo")]
#else
[Theory]
#endif
[InlineData("x", 0, 200, 200 - 7.20, 0, 7.20, FontSizeHeight)]
[InlineData(stringword, 0, 200, 171.20, 0, 7.20, FontSizeHeight)]
[InlineData(stringword, 3, 200, 200 - 7.20, 0, 7.20, FontSizeHeight)]
public void Should_HitTestPosition_RigthAlign_Correctly(
string input, int index, double widthConstraint,
double x, double y, double width, double height)
{
//parse expected
using (var fmt = Create(input, FontSize, TextAlignment.Right))
{
fmt.Constraint = new Size(widthConstraint, 100);
var r = fmt.HitTestTextPosition(index);
Assert.Equal(x, r.X, 2);
Assert.Equal(y, r.Y, 2);
Assert.Equal(width, r.Width, 2);
Assert.Equal(height, r.Height, 2);
}
}
#if AVALONIA_CAIRO
[Theory(Skip = "TODO: Font scaling currently broken on cairo")]
#else
[Theory]
#endif
[InlineData("x", 0, 200, 100 - 7.20 / 2, 0, 7.20, FontSizeHeight)]
[InlineData(stringword, 0, 200, 85.6, 0, 7.20, FontSizeHeight)]
[InlineData(stringword, 3, 200, 100 + 7.20, 0, 7.20, FontSizeHeight)]
public void Should_HitTestPosition_CenterAlign_Correctly(
string input, int index, double widthConstraint,
double x, double y, double width, double height)
{
//parse expected
using (var fmt = Create(input, FontSize, TextAlignment.Center))
{
fmt.Constraint = new Size(widthConstraint, 100);
var r = fmt.HitTestTextPosition(index);
Assert.Equal(x, r.X, 2);
Assert.Equal(y, r.Y, 2);
Assert.Equal(width, r.Width, 2);
Assert.Equal(height, r.Height, 2);
}
}
#if AVALONIA_CAIRO
[Theory(Skip = "TODO: Font scaling currently broken on cairo")]
#else
[Theory]
#endif
[InlineData("x", 0, 1, "0,0,7.20,13.59")]
[InlineData(stringword, 0, 4, "0,0,28.80,13.59")]
[InlineData(stringmiddlenewlines, 10, 10, "0,13.59,57.61,13.59")]
[InlineData(stringmiddlenewlines, 10, 20, "0,13.59,57.61,13.59;0,27.19,64.81,13.59")]
[InlineData(stringmiddlenewlines, 10, 15, "0,13.59,57.61,13.59;0,27.19,36.01,13.59")]
[InlineData(stringmiddlenewlines, 15, 15, "36.01,13.59,21.60,13.59;0,27.19,64.81,13.59")]
public void Should_HitTestRange_Correctly(string input,
int index, int length,
string expectedRects)
{
//parse expected result
var rects = expectedRects.Split(';').Select(s =>
{
double[] v = s.Split(',')
.Select(sd => double.Parse(sd, CultureInfo.InvariantCulture)).ToArray();
return new Rect(v[0], v[1], v[2], v[3]);
}).ToArray();
using (var fmt = Create(input, FontSize))
{
var htRes = fmt.HitTestTextRange(index, length).ToArray();
Assert.Equal(rects.Length, htRes.Length);
for (int i = 0; i < rects.Length; i++)
{
var exr = rects[i];
var r = htRes[i];
Assert.Equal(exr.X, r.X, 2);
Assert.Equal(exr.Y, r.Y, 2);
Assert.Equal(exr.Width, r.Width, 2);
Assert.Equal(exr.Height, r.Height, 2);
}
}
}
}
}
Loading…
Cancel
Save