Browse Source

Merge branch 'master' into feature/windowstate-fullscreen

pull/3849/head
danwalmsley 6 years ago
committed by GitHub
parent
commit
30fb4c2d12
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      build/SkiaSharp.props
  2. 2
      src/Avalonia.Animation/Animation.cs
  3. 9
      src/Avalonia.Animation/AnimatorKeyFrame.cs
  4. 3
      src/Avalonia.Animation/Animators/Animator`1.cs
  5. 20
      src/Avalonia.Animation/KeyFrame.cs
  6. 349
      src/Avalonia.Animation/KeySpline.cs
  7. 26
      src/Avalonia.Visuals/Media/FontManager.cs
  8. 3
      src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
  9. 16
      src/Avalonia.Visuals/Media/Fonts/FontKey.cs
  10. 2
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs
  11. 2
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs
  12. 44
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs
  13. 67
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  14. 46
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  15. 3
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  16. 9
      src/Skia/Avalonia.Skia/GlyphRunImpl.cs
  17. 116
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  18. 4
      src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
  19. 2
      src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
  20. 2
      src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
  21. 145
      tests/Avalonia.Animation.UnitTests/KeySplineTests.cs
  22. 48
      tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs
  23. 13
      tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs
  24. 24
      tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs
  25. 11
      tests/Avalonia.UnitTests/MockFontManagerImpl.cs
  26. 12
      tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs

4
build/SkiaSharp.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="1.68.1" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.1" />
<PackageReference Include="SkiaSharp" Version="1.68.2" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.2" />
</ItemGroup>
</Project>

2
src/Avalonia.Animation/Animation.cs

@ -254,7 +254,7 @@ namespace Avalonia.Animation
cue = new Cue(keyframe.KeyTime.TotalSeconds / Duration.TotalSeconds);
}
var newKF = new AnimatorKeyFrame(handler, cue);
var newKF = new AnimatorKeyFrame(handler, cue, keyframe.KeySpline);
subscriptions.Add(newKF.BindSetter(setter, control));

9
src/Avalonia.Animation/AnimatorKeyFrame.cs

@ -24,11 +24,20 @@ namespace Avalonia.Animation
{
AnimatorType = animatorType;
Cue = cue;
KeySpline = null;
}
public AnimatorKeyFrame(Type animatorType, Cue cue, KeySpline keySpline)
{
AnimatorType = animatorType;
Cue = cue;
KeySpline = keySpline;
}
internal bool isNeutral;
public Type AnimatorType { get; }
public Cue Cue { get; }
public KeySpline KeySpline { get; }
public AvaloniaProperty Property { get; private set; }
private object _value;

3
src/Avalonia.Animation/Animators/Animator`1.cs

@ -89,6 +89,9 @@ namespace Avalonia.Animation.Animators
else
newValue = (T)lastKeyframe.Value;
if (lastKeyframe.KeySpline != null)
progress = lastKeyframe.KeySpline.GetSplineProgress(progress);
return Interpolate(progress, oldValue, newValue);
}

20
src/Avalonia.Animation/KeyFrame.cs

@ -19,6 +19,7 @@ namespace Avalonia.Animation
{
private TimeSpan _ktimeSpan;
private Cue _kCue;
private KeySpline _kKeySpline;
public KeyFrame()
{
@ -74,6 +75,25 @@ namespace Avalonia.Animation
}
}
/// <summary>
/// Gets or sets the KeySpline of this <see cref="KeyFrame"/>.
/// </summary>
/// <value>The key spline.</value>
public KeySpline KeySpline
{
get
{
return _kKeySpline;
}
set
{
_kKeySpline = value;
if (value != null && !value.IsValid())
{
throw new ArgumentException($"{nameof(KeySpline)} must have X coordinates >= 0.0 and <= 1.0.");
}
}
}
}

349
src/Avalonia.Animation/KeySpline.cs

@ -0,0 +1,349 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Text;
using Avalonia;
using Avalonia.Utilities;
// Ported from WPF open-source code.
// https://github.com/dotnet/wpf/blob/ae1790531c3b993b56eba8b1f0dd395a3ed7de75/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Animation/KeySpline.cs
namespace Avalonia.Animation
{
/// <summary>
/// Determines how an animation is used based on a cubic bezier curve.
/// X1 and X2 must be between 0.0 and 1.0, inclusive.
/// See https://docs.microsoft.com/en-us/dotnet/api/system.windows.media.animation.keyspline
/// </summary>
[TypeConverter(typeof(KeySplineTypeConverter))]
public class KeySpline : AvaloniaObject
{
// Control points
private double _controlPointX1;
private double _controlPointY1;
private double _controlPointX2;
private double _controlPointY2;
private bool _isSpecified;
private bool _isDirty;
// The parameter that corresponds to the most recent time
private double _parameter;
// Cached coefficients
private double _Bx; // 3*points[0].X
private double _Cx; // 3*points[1].X
private double _Cx_Bx; // 2*(Cx - Bx)
private double _three_Cx; // 3 - Cx
private double _By; // 3*points[0].Y
private double _Cy; // 3*points[1].Y
// constants
private const double _accuracy = .001; // 1/3 the desired accuracy in X
private const double _fuzz = .000001; // computational zero
/// <summary>
/// Create a <see cref="KeySpline"/> with X1 = Y1 = 0 and X2 = Y2 = 1.
/// </summary>
public KeySpline()
{
_controlPointX1 = 0.0;
_controlPointY1 = 0.0;
_controlPointX2 = 1.0;
_controlPointY2 = 1.0;
_isDirty = true;
}
/// <summary>
/// Create a <see cref="KeySpline"/> with the given parameters
/// </summary>
/// <param name="x1">X coordinate for the first control point</param>
/// <param name="y1">Y coordinate for the first control point</param>
/// <param name="x2">X coordinate for the second control point</param>
/// <param name="y2">Y coordinate for the second control point</param>
public KeySpline(double x1, double y1, double x2, double y2)
{
_controlPointX1 = x1;
_controlPointY1 = y1;
_controlPointX2 = x2;
_controlPointY2 = y2;
_isDirty = true;
}
/// <summary>
/// Parse a <see cref="KeySpline"/> from a string. The string
/// needs to contain 4 values in it for the 2 control points.
/// </summary>
/// <param name="value">string with 4 values in it</param>
/// <param name="culture">culture of the string</param>
/// <exception cref="FormatException">Thrown if the string does not have 4 values</exception>
/// <returns>A <see cref="KeySpline"/> with the appropriate values set</returns>
public static KeySpline Parse(string value, CultureInfo culture)
{
using (var tokenizer = new StringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid KeySpline."))
{
return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble());
}
}
/// <summary>
/// X coordinate of the first control point
/// </summary>
public double ControlPointX1
{
get => _controlPointX1;
set
{
if (IsValidXValue(value))
{
_controlPointX1 = value;
}
else
{
throw new ArgumentException("Invalid KeySpline X1 value. Must be >= 0.0 and <= 1.0.");
}
}
}
/// <summary>
/// Y coordinate of the first control point
/// </summary>
public double ControlPointY1
{
get => _controlPointY1;
set => _controlPointY1 = value;
}
/// <summary>
/// X coordinate of the second control point
/// </summary>
public double ControlPointX2
{
get => _controlPointX2;
set
{
if (IsValidXValue(value))
{
_controlPointX2 = value;
}
else
{
throw new ArgumentException("Invalid KeySpline X2 value. Must be >= 0.0 and <= 1.0.");
}
}
}
/// <summary>
/// Y coordinate of the second control point
/// </summary>
public double ControlPointY2
{
get => _controlPointY2;
set => _controlPointY2 = value;
}
/// <summary>
/// Calculates spline progress from a linear progress.
/// </summary>
/// <param name="linearProgress">the linear progress</param>
/// <returns>the spline progress</returns>
public double GetSplineProgress(double linearProgress)
{
if (_isDirty)
{
Build();
}
if (!_isSpecified)
{
return linearProgress;
}
else
{
SetParameterFromX(linearProgress);
return GetBezierValue(_By, _Cy, _parameter);
}
}
/// <summary>
/// Check to see whether the <see cref="KeySpline"/> is valid by looking
/// at its X values.
/// </summary>
/// <returns>true if the X values for this <see cref="KeySpline"/> fall in
/// acceptable range; false otherwise.</returns>
public bool IsValid()
{
return IsValidXValue(_controlPointX1) && IsValidXValue(_controlPointX2);
}
/// <summary>
///
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
private bool IsValidXValue(double value)
{
return value >= 0.0 && value <= 1.0;
}
/// <summary>
/// Compute cached coefficients.
/// </summary>
private void Build()
{
if (_controlPointX1 == 0 && _controlPointY1 == 0 && _controlPointX2 == 1 && _controlPointY2 == 1)
{
// This KeySpline would have no effect on the progress.
_isSpecified = false;
}
else
{
_isSpecified = true;
_parameter = 0;
// X coefficients
_Bx = 3 * _controlPointX1;
_Cx = 3 * _controlPointX2;
_Cx_Bx = 2 * (_Cx - _Bx);
_three_Cx = 3 - _Cx;
// Y coefficients
_By = 3 * _controlPointY1;
_Cy = 3 * _controlPointY2;
}
_isDirty = false;
}
/// <summary>
/// Get an X or Y value with the Bezier formula.
/// </summary>
/// <param name="b">the second Bezier coefficient</param>
/// <param name="c">the third Bezier coefficient</param>
/// <param name="t">the parameter value to evaluate at</param>
/// <returns>the value of the Bezier function at the given parameter</returns>
static private double GetBezierValue(double b, double c, double t)
{
double s = 1.0 - t;
double t2 = t * t;
return b * t * s * s + c * t2 * s + t2 * t;
}
/// <summary>
/// Get X and dX/dt at a given parameter
/// </summary>
/// <param name="t">the parameter value to evaluate at</param>
/// <param name="x">the value of x there</param>
/// <param name="dx">the value of dx/dt there</param>
private void GetXAndDx(double t, out double x, out double dx)
{
double s = 1.0 - t;
double t2 = t * t;
double s2 = s * s;
x = _Bx * t * s2 + _Cx * t2 * s + t2 * t;
dx = _Bx * s2 + _Cx_Bx * s * t + _three_Cx * t2;
}
/// <summary>
/// Compute the parameter value that corresponds to a given X value, using a modified
/// clamped Newton-Raphson algorithm to solve the equation X(t) - time = 0. We make
/// use of some known properties of this particular function:
/// * We are only interested in solutions in the interval [0,1]
/// * X(t) is increasing, so we can assume that if X(t) > time t > solution. We use
/// that to clamp down the search interval with every probe.
/// * The derivative of X and Y are between 0 and 3.
/// </summary>
/// <param name="time">the time, scaled to fit in [0,1]</param>
private void SetParameterFromX(double time)
{
// Dynamic search interval to clamp with
double bottom = 0;
double top = 1;
if (time == 0)
{
_parameter = 0;
}
else if (time == 1)
{
_parameter = 1;
}
else
{
// Loop while improving the guess
while (top - bottom > _fuzz)
{
double x, dx, absdx;
// Get x and dx/dt at the current parameter
GetXAndDx(_parameter, out x, out dx);
absdx = Math.Abs(dx);
// Clamp down the search interval, relying on the monotonicity of X(t)
if (x > time)
{
top = _parameter; // because parameter > solution
}
else
{
bottom = _parameter; // because parameter < solution
}
// The desired accuracy is in ultimately in y, not in x, so the
// accuracy needs to be multiplied by dx/dy = (dx/dt) / (dy/dt).
// But dy/dt <=3, so we omit that
if (Math.Abs(x - time) < _accuracy * absdx)
{
break; // We're there
}
if (absdx > _fuzz)
{
// Nonzero derivative, use Newton-Raphson to obtain the next guess
double next = _parameter - (x - time) / dx;
// If next guess is out of the search interval then clamp it in
if (next >= top)
{
_parameter = (_parameter + top) / 2;
}
else if (next <= bottom)
{
_parameter = (_parameter + bottom) / 2;
}
else
{
// Next guess is inside the search interval, accept it
_parameter = next;
}
}
else // Zero derivative, halve the search interval
{
_parameter = (bottom + top) / 2;
}
}
}
}
}
/// <summary>
/// Converts string values to <see cref="KeySpline"/> values
/// </summary>
public class KeySplineTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
return KeySpline.Parse((string)value, culture);
}
}
}

26
src/Avalonia.Visuals/Media/FontManager.cs

@ -23,6 +23,11 @@ namespace Avalonia.Media
DefaultFontFamilyName = PlatformImpl.GetDefaultFontFamilyName();
if (string.IsNullOrEmpty(DefaultFontFamilyName))
{
throw new InvalidOperationException("Default font family name can't be null or empty.");
}
_defaultFontFamily = new FontFamily(DefaultFontFamilyName);
}
@ -39,7 +44,8 @@ namespace Avalonia.Media
var fontManagerImpl = AvaloniaLocator.Current.GetService<IFontManagerImpl>();
if (fontManagerImpl == null) throw new InvalidOperationException("No font manager implementation was registered.");
if (fontManagerImpl == null)
throw new InvalidOperationException("No font manager implementation was registered.");
current = new FontManager(fontManagerImpl);
@ -87,7 +93,7 @@ namespace Avalonia.Media
fontFamily = _defaultFontFamily;
}
var key = new FontKey(fontFamily, fontWeight, fontStyle);
var key = new FontKey(fontFamily.Name, fontWeight, fontStyle);
if (_typefaceCache.TryGetValue(key, out var typeface))
{
@ -126,9 +132,21 @@ namespace Avalonia.Media
FontStyle fontStyle = FontStyle.Normal,
FontFamily fontFamily = null, CultureInfo culture = null)
{
return PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ?
_typefaceCache.GetOrAdd(key, new Typeface(key.FontFamily, key.Weight, key.Style)) :
foreach (var cachedTypeface in _typefaceCache.Values)
{
// First try to find a cached typeface by style and weight to avoid redundant glyph index lookup.
if (cachedTypeface.Style == fontStyle && cachedTypeface.Weight == fontWeight
&& cachedTypeface.GlyphTypeface.GetGlyph((uint)codepoint) != 0)
{
return cachedTypeface;
}
}
var matchedTypeface = PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ?
_typefaceCache.GetOrAdd(key, new Typeface(key.FamilyName, key.Weight, key.Style)) :
null;
return matchedTypeface;
}
}
}

3
src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs

@ -1,7 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Avalonia.Utilities;
@ -21,7 +20,7 @@ namespace Avalonia.Media.Fonts
throw new ArgumentNullException(nameof(familyNames));
}
Names = familyNames.Split(',').Select(x => x.Trim()).ToArray();
Names = Array.ConvertAll(familyNames.Split(','), p => p.Trim());
PrimaryFamilyName = Names[0];

16
src/Avalonia.Visuals/Media/Fonts/FontKey.cs

@ -4,20 +4,20 @@ namespace Avalonia.Media.Fonts
{
public readonly struct FontKey : IEquatable<FontKey>
{
public readonly FontFamily FontFamily;
public readonly FontStyle Style;
public readonly FontWeight Weight;
public FontKey(FontFamily fontFamily, FontWeight weight, FontStyle style)
public FontKey(string familyName, FontWeight weight, FontStyle style)
{
FontFamily = fontFamily;
FamilyName = familyName;
Style = style;
Weight = weight;
}
public string FamilyName { get; }
public FontStyle Style { get; }
public FontWeight Weight { get; }
public override int GetHashCode()
{
var hash = FontFamily.GetHashCode();
var hash = FamilyName.GetHashCode();
hash = hash * 31 + (int)Style;
hash = hash * 31 + (int)Weight;
@ -32,7 +32,7 @@ namespace Avalonia.Media.Fonts
public bool Equals(FontKey other)
{
return FontFamily == other.FontFamily &&
return FamilyName == other.FamilyName &&
Style == other.Style &&
Weight == other.Weight;
}

2
src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs

@ -66,7 +66,7 @@ namespace Avalonia.Media.TextFormatting
//ToDo: Fix FontFamily fallback
currentTypeface =
FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style);
FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style, defaultStyle.TextFormat.Typeface.FontFamily);
if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count))
{

2
src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs

@ -2,7 +2,7 @@
namespace Avalonia.Media.TextFormatting.Unicode
{
internal ref struct CodepointEnumerator
public ref struct CodepointEnumerator
{
private ReadOnlySlice<char> _text;

44
src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs

@ -1,44 +0,0 @@
namespace Avalonia.Media.TextFormatting.Unicode
{
public enum UnicodeGeneralCategory : byte
{
Other, //C# Cc | Cf | Cn | Co | Cs
Control, //Cc
Format, //Cf
Unassigned, //Cn
PrivateUse, //Co
Surrogate, //Cs
Letter, //L# Ll | Lm | Lo | Lt | Lu
CasedLetter, //LC# Ll | Lt | Lu
LowercaseLetter, //Ll
ModifierLetter, //Lm
OtherLetter, //Lo
TitlecaseLetter, //Lt
UppercaseLetter, //Lu
Mark, //M
SpacingMark, //Mc
EnclosingMark, //Me
NonspacingMark, //Mn
Number, //N# Nd | Nl | No
DecimalNumber, //Nd
LetterNumber, //Nl
OtherNumber, //No
Punctuation, //P
ConnectorPunctuation, //Pc
DashPunctuation, //Pd
ClosePunctuation, //Pe
FinalPunctuation, //Pf
InitialPunctuation, //Pi
OtherPunctuation, //Po
OpenPunctuation, //Ps
Symbol, //S# Sc | Sk | Sm | So
CurrencySymbol, //Sc
ModifierSymbol, //Sk
MathSymbol, //Sm
OtherSymbol, //So
Separator, //Z# Zl | Zp | Zs
LineSeparator, //Zl
ParagraphSeparator, //Zp
SpaceSeparator, //Zs
}
}

67
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -30,6 +30,10 @@ namespace Avalonia.Skia
private Matrix _currentTransform;
private GRContext _grContext;
private bool _disposed;
private readonly SKPaint _strokePaint = new SKPaint();
private readonly SKPaint _fillPaint = new SKPaint();
/// <summary>
/// Context create info.
/// </summary>
@ -153,7 +157,7 @@ namespace Avalonia.Skia
/// <inheritdoc />
public void DrawLine(IPen pen, Point p1, Point p2)
{
using (var paint = CreatePaint(pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))))
using (var paint = CreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))))
{
Canvas.DrawLine((float) p1.X, (float) p1.Y, (float) p2.X, (float) p2.Y, paint.Paint);
}
@ -165,8 +169,8 @@ namespace Avalonia.Skia
var impl = (GeometryImpl) geometry;
var size = geometry.Bounds.Size;
using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper))
using (var stroke = pen?.Brush != null ? CreatePaint(pen, size) : default(PaintWrapper))
using (var fill = brush != null ? CreatePaint(_fillPaint, brush, size) : default(PaintWrapper))
using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, size) : default(PaintWrapper))
{
if (fill.Paint != null)
{
@ -188,7 +192,7 @@ namespace Avalonia.Skia
if (brush != null)
{
using (var paint = CreatePaint(brush, rect.Size))
using (var paint = CreatePaint(_fillPaint, brush, rect.Size))
{
if (isRounded)
{
@ -204,7 +208,7 @@ namespace Avalonia.Skia
if (pen?.Brush != null)
{
using (var paint = CreatePaint(pen, rect.Size))
using (var paint = CreatePaint(_strokePaint, pen, rect.Size))
{
if (isRounded)
{
@ -222,7 +226,7 @@ namespace Avalonia.Skia
/// <inheritdoc />
public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
{
using (var paint = CreatePaint(foreground, text.Bounds.Size))
using (var paint = CreatePaint(_fillPaint, foreground, text.Bounds.Size))
{
var textImpl = (FormattedTextImpl) text;
textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering);
@ -232,14 +236,14 @@ namespace Avalonia.Skia
/// <inheritdoc />
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
{
using (var paint = CreatePaint(foreground, glyphRun.Bounds.Size))
using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Bounds.Size))
{
var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
paint.ApplyTo(glyphRunImpl.Paint);
ConfigureTextRendering(paintWrapper);
Canvas.DrawText(glyphRunImpl.TextBlob, (float)baselineOrigin.X,
(float)baselineOrigin.Y, glyphRunImpl.Paint);
(float)baselineOrigin.Y, paintWrapper.Paint);
}
}
@ -323,7 +327,7 @@ namespace Avalonia.Skia
var paint = new SKPaint();
Canvas.SaveLayer(paint);
_maskStack.Push(CreatePaint(mask, bounds.Size));
_maskStack.Push(CreatePaint(paint, mask, bounds.Size, true));
}
/// <inheritdoc />
@ -364,6 +368,15 @@ namespace Avalonia.Skia
}
}
internal void ConfigureTextRendering(PaintWrapper wrapper)
{
var paint = wrapper.Paint;
paint.IsEmbeddedBitmapText = true;
paint.SubpixelText = true;
paint.LcdRenderText = _canTextUseLcdRendering;
}
/// <summary>
/// Configure paint wrapper for using gradient brush.
/// </summary>
@ -514,17 +527,16 @@ namespace Avalonia.Skia
/// <summary>
/// Creates paint wrapper for given brush.
/// </summary>
/// <param name="paint">The paint to wrap.</param>
/// <param name="brush">Source brush.</param>
/// <param name="targetSize">Target size.</param>
/// <param name="disposePaint">Optional dispose of the supplied paint.</param>
/// <returns>Paint wrapper for given brush.</returns>
internal PaintWrapper CreatePaint(IBrush brush, Size targetSize)
internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Size targetSize, bool disposePaint = false)
{
var paint = new SKPaint
{
IsAntialias = true
};
var paintWrapper = new PaintWrapper(paint, disposePaint);
var paintWrapper = new PaintWrapper(paint);
paint.IsAntialias = true;
double opacity = brush.Opacity * _currentOpacity;
@ -572,10 +584,12 @@ namespace Avalonia.Skia
/// <summary>
/// Creates paint wrapper for given pen.
/// </summary>
/// <param name="paint">The paint to wrap.</param>
/// <param name="pen">Source pen.</param>
/// <param name="targetSize">Target size.</param>
/// <param name="disposePaint">Optional dispose of the supplied paint.</param>
/// <returns></returns>
private PaintWrapper CreatePaint(IPen pen, Size targetSize)
private PaintWrapper CreatePaint(SKPaint paint, IPen pen, Size targetSize, bool disposePaint = false)
{
// In Skia 0 thickness means - use hairline rendering
// and for us it means - there is nothing rendered.
@ -584,8 +598,7 @@ namespace Avalonia.Skia
return default;
}
var rv = CreatePaint(pen.Brush, targetSize);
var paint = rv.Paint;
var rv = CreatePaint(paint, pen.Brush, targetSize, disposePaint);
paint.IsStroke = true;
paint.StrokeWidth = (float) pen.Thickness;
@ -668,7 +681,7 @@ namespace Avalonia.Skia
/// <summary>
/// Skia cached paint state.
/// </summary>
private struct PaintState : IDisposable
private readonly struct PaintState : IDisposable
{
private readonly SKColor _color;
private readonly SKShader _shader;
@ -696,14 +709,16 @@ namespace Avalonia.Skia
{
//We are saving memory allocations there
public readonly SKPaint Paint;
private readonly bool _disposePaint;
private IDisposable _disposable1;
private IDisposable _disposable2;
private IDisposable _disposable3;
public PaintWrapper(SKPaint paint)
public PaintWrapper(SKPaint paint, bool disposePaint)
{
Paint = paint;
_disposePaint = disposePaint;
_disposable1 = null;
_disposable2 = null;
@ -751,7 +766,15 @@ namespace Avalonia.Skia
/// <inheritdoc />
public void Dispose()
{
Paint?.Dispose();
if (_disposePaint)
{
Paint?.Dispose();
}
else
{
Paint?.Reset();
}
_disposable1?.Dispose();
_disposable2?.Dispose();
_disposable3?.Dispose();

46
src/Skia/Avalonia.Skia/FontManagerImpl.cs

@ -32,6 +32,27 @@ namespace Avalonia.Skia
public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle,
FontFamily fontFamily, CultureInfo culture, out FontKey fontKey)
{
SKFontStyle skFontStyle;
switch (fontWeight)
{
case FontWeight.Normal when fontStyle == FontStyle.Normal:
skFontStyle = SKFontStyle.Normal;
break;
case FontWeight.Normal when fontStyle == FontStyle.Italic:
skFontStyle = SKFontStyle.Italic;
break;
case FontWeight.Bold when fontStyle == FontStyle.Normal:
skFontStyle = SKFontStyle.Bold;
break;
case FontWeight.Bold when fontStyle == FontStyle.Italic:
skFontStyle = SKFontStyle.BoldItalic;
break;
default:
skFontStyle = new SKFontStyle((SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle);
break;
}
if (culture == null)
{
culture = CultureInfo.CurrentUICulture;
@ -45,31 +66,32 @@ namespace Avalonia.Skia
t_languageTagBuffer[0] = culture.TwoLetterISOLanguageName;
t_languageTagBuffer[1] = culture.ThreeLetterISOLanguageName;
if (fontFamily != null)
if (fontFamily != null && fontFamily.FamilyNames.HasFallbacks)
{
foreach (var familyName in fontFamily.FamilyNames)
var familyNames = fontFamily.FamilyNames;
for (var i = 1; i < familyNames.Count; i++)
{
var skTypeface = _skFontManager.MatchCharacter(familyName, (SKFontStyleWeight)fontWeight,
SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint);
var skTypeface =
_skFontManager.MatchCharacter(familyNames[i], skFontStyle, t_languageTagBuffer, codepoint);
if (skTypeface == null)
{
continue;
}
fontKey = new FontKey(new FontFamily(familyName), fontWeight, fontStyle);
fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle);
return true;
}
}
else
{
var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight,
SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint);
var skTypeface = _skFontManager.MatchCharacter(null, skFontStyle, t_languageTagBuffer, codepoint);
if (skTypeface != null)
{
fontKey = new FontKey(new FontFamily(skTypeface.FamilyName), fontWeight, fontStyle);
fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle);
return true;
}
@ -82,7 +104,7 @@ namespace Avalonia.Skia
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{
var skTypeface = SKTypeface.Default;
SKTypeface skTypeface = null;
if (typeface.FontFamily.Key == null)
{
@ -109,6 +131,12 @@ namespace Avalonia.Skia
skTypeface = fontCollection.Get(typeface);
}
if (skTypeface == null)
{
throw new InvalidOperationException(
$"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
}
return new GlyphTypefaceImpl(skTypeface);
}
}

3
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@ -266,7 +266,8 @@ namespace Avalonia.Skia
if (fb != null)
{
//TODO: figure out how to get the brush size
currentWrapper = context.CreatePaint(fb, new Size());
currentWrapper = context.CreatePaint(new SKPaint { IsAntialias = true }, fb,
new Size());
}
else
{

9
src/Skia/Avalonia.Skia/GlyphRunImpl.cs

@ -7,17 +7,11 @@ namespace Avalonia.Skia
/// <inheritdoc />
public class GlyphRunImpl : IGlyphRunImpl
{
public GlyphRunImpl(SKPaint paint, SKTextBlob textBlob)
public GlyphRunImpl(SKTextBlob textBlob)
{
Paint = paint;
TextBlob = textBlob;
}
/// <summary>
/// Gets the paint to draw with.
/// </summary>
public SKPaint Paint { get; }
/// <summary>
/// Gets the text blob to draw.
/// </summary>
@ -26,7 +20,6 @@ namespace Avalonia.Skia
void IDisposable.Dispose()
{
TextBlob.Dispose();
Paint.Dispose();
}
}
}

116
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -149,6 +149,16 @@ namespace Avalonia.Skia
return new WriteableBitmapImpl(size, dpi, format);
}
private static readonly SKPaint s_paint = new SKPaint
{
TextEncoding = SKTextEncoding.GlyphId,
IsAntialias = true,
IsStroke = false,
SubpixelText = true
};
private static readonly SKTextBlobBuilder s_textBlobBuilder = new SKTextBlobBuilder();
/// <inheritdoc />
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
{
@ -158,92 +168,84 @@ namespace Avalonia.Skia
var typeface = glyphTypeface.Typeface;
var paint = new SKPaint
{
TextSize = (float)glyphRun.FontRenderingEmSize,
Typeface = typeface,
TextEncoding = SKTextEncoding.GlyphId,
IsAntialias = true,
IsStroke = false,
SubpixelText = true
};
using (var textBlobBuilder = new SKTextBlobBuilder())
{
SKTextBlob textBlob;
s_paint.TextSize = (float)glyphRun.FontRenderingEmSize;
s_paint.Typeface = typeface;
width = 0;
var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
SKTextBlob textBlob;
if (glyphRun.GlyphOffsets.IsEmpty)
{
if (glyphTypeface.IsFixedPitch)
{
textBlobBuilder.AddRun(paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span);
width = 0;
textBlob = textBlobBuilder.Build();
var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length;
}
else
{
var buffer = textBlobBuilder.AllocateHorizontalRun(paint, count, 0);
var positions = buffer.GetPositionSpan();
for (var i = 0; i < count; i++)
{
positions[i] = (float)width;
if (glyphRun.GlyphAdvances.IsEmpty)
{
width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
}
else
{
width += glyphRun.GlyphAdvances[i];
}
}
if (glyphRun.GlyphOffsets.IsEmpty)
{
if (glyphTypeface.IsFixedPitch)
{
s_textBlobBuilder.AddRun(s_paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span);
buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
textBlob = s_textBlobBuilder.Build();
textBlob = textBlobBuilder.Build();
}
width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length;
}
else
{
var buffer = textBlobBuilder.AllocatePositionedRun(paint, count);
var buffer = s_textBlobBuilder.AllocateHorizontalRun(s_paint, count, 0);
var glyphPositions = buffer.GetPositionSpan();
var currentX = 0.0;
var positions = buffer.GetPositionSpan();
for (var i = 0; i < count; i++)
{
var glyphOffset = glyphRun.GlyphOffsets[i];
glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y);
positions[i] = (float)width;
if (glyphRun.GlyphAdvances.IsEmpty)
{
currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
}
else
{
currentX += glyphRun.GlyphAdvances[i];
width += glyphRun.GlyphAdvances[i];
}
}
buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
width = currentX;
textBlob = s_textBlobBuilder.Build();
}
}
else
{
var buffer = s_textBlobBuilder.AllocatePositionedRun(s_paint, count);
var glyphPositions = buffer.GetPositionSpan();
var currentX = 0.0;
for (var i = 0; i < count; i++)
{
var glyphOffset = glyphRun.GlyphOffsets[i];
glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y);
textBlob = textBlobBuilder.Build();
if (glyphRun.GlyphAdvances.IsEmpty)
{
currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
}
else
{
currentX += glyphRun.GlyphAdvances[i];
}
}
return new GlyphRunImpl(paint, textBlob);
buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
width = currentX;
textBlob = s_textBlobBuilder.Build();
}
return new GlyphRunImpl(textBlob);
}
}
}

4
src/Skia/Avalonia.Skia/SKTypefaceCollection.cs

@ -19,7 +19,7 @@ namespace Avalonia.Skia
public SKTypeface Get(Typeface typeface)
{
var key = new FontKey(typeface.FontFamily, typeface.Weight, typeface.Style);
var key = new FontKey(typeface.FontFamily.Name, typeface.Weight, typeface.Style);
return GetNearestMatch(_typefaces, key);
}
@ -49,7 +49,7 @@ namespace Avalonia.Skia
if (keys.Length == 0)
{
return SKTypeface.Default;
return null;
}
key = keys[0];

2
src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs

@ -54,7 +54,7 @@ namespace Avalonia.Skia
continue;
}
var key = new FontKey(fontFamily, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant);
var key = new FontKey(fontFamily.Name, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant);
typeFaceCollection.AddTypeface(key, typeface);
}

2
src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs

@ -50,7 +50,7 @@ namespace Avalonia.Direct2D1.Media
var fontFamilyName = font.FontFamily.FamilyNames.GetString(0);
fontKey = new FontKey(new FontFamily(fontFamilyName), fontWeight, fontStyle);
fontKey = new FontKey(fontFamilyName, fontWeight, fontStyle);
return true;
}

145
tests/Avalonia.Animation.UnitTests/KeySplineTests.cs

@ -0,0 +1,145 @@
using System;
using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.Styling;
using Xunit;
namespace Avalonia.Animation.UnitTests
{
public class KeySplineTests
{
[Theory]
[InlineData("1,2 3,4")]
[InlineData("1 2 3 4")]
[InlineData("1 2,3 4")]
[InlineData("1,2,3,4")]
public void Can_Parse_KeySpline_Via_TypeConverter(string input)
{
var conv = new KeySplineTypeConverter();
var keySpline = (KeySpline)conv.ConvertFrom(input);
Assert.Equal(1, keySpline.ControlPointX1);
Assert.Equal(2, keySpline.ControlPointY1);
Assert.Equal(3, keySpline.ControlPointX2);
Assert.Equal(4, keySpline.ControlPointY2);
}
[Theory]
[InlineData(0.00)]
[InlineData(0.50)]
[InlineData(1.00)]
public void KeySpline_X_Values_In_Range_Do_Not_Throw(double input)
{
var keySpline = new KeySpline();
keySpline.ControlPointX1 = input; // no exception will be thrown -- test will fail if exception thrown
keySpline.ControlPointX2 = input; // no exception will be thrown -- test will fail if exception thrown
}
[Theory]
[InlineData(-0.01)]
[InlineData(1.01)]
public void KeySpline_X_Values_Cannot_Be_Out_Of_Range(double input)
{
var keySpline = new KeySpline();
Assert.Throws<ArgumentException>(() => keySpline.ControlPointX1 = input);
Assert.Throws<ArgumentException>(() => keySpline.ControlPointX2 = input);
}
/*
To get the test values for the KeySpline test, you can:
1) Grab the WPF sample for KeySpline animations from https://github.com/microsoft/WPF-Samples/tree/master/Animation/KeySplineAnimations
2) Add the following xaml somewhere:
<Button Content="Capture"
Click="Button_Click"/>
<ScrollViewer VerticalScrollBarVisibility="Visible">
<TextBlock Name="CaptureData"
Text="---"
TextWrapping="Wrap" />
</ScrollViewer>
3) Add the following code to the code behind:
private void Button_Click(object sender, RoutedEventArgs e)
{
CaptureData.Text += string.Format("\n{0} | {1}", myTranslateTransform3D.OffsetX, (TimeSpan)ExampleStoryboard.GetCurrentTime(this));
CaptureData.Text +=
"\nKeySpline=\"" + mySplineKeyFrame.KeySpline.ControlPoint1.X.ToString() + "," +
mySplineKeyFrame.KeySpline.ControlPoint1.Y.ToString() + " " +
mySplineKeyFrame.KeySpline.ControlPoint2.X.ToString() + "," +
mySplineKeyFrame.KeySpline.ControlPoint2.Y.ToString() + "\"";
CaptureData.Text += "\n-----";
}
4) Run the app, mess with the slider values, then click the button to capture output values
**/
[Fact]
public void Check_KeySpline_Handled_properly()
{
var keyframe1 = new KeyFrame()
{
Setters =
{
new Setter(RotateTransform.AngleProperty, -2.5d),
},
KeyTime = TimeSpan.FromSeconds(0)
};
var keyframe2 = new KeyFrame()
{
Setters =
{
new Setter(RotateTransform.AngleProperty, 2.5d),
},
KeyTime = TimeSpan.FromSeconds(5),
KeySpline = new KeySpline(0.1123555056179775,
0.657303370786517,
0.8370786516853934,
0.499999999999999999)
};
var animation = new Animation()
{
Duration = TimeSpan.FromSeconds(5),
Children =
{
keyframe1,
keyframe2
},
IterationCount = new IterationCount(5),
PlaybackDirection = PlaybackDirection.Alternate
};
var rotateTransform = new RotateTransform(-2.5);
var rect = new Rectangle()
{
RenderTransform = rotateTransform
};
var clock = new TestClock();
var animationRun = animation.RunAsync(rect, clock);
// position is what you'd expect at end and beginning
clock.Step(TimeSpan.Zero);
Assert.Equal(rotateTransform.Angle, -2.5);
clock.Step(TimeSpan.FromSeconds(5));
Assert.Equal(rotateTransform.Angle, 2.5);
// test some points in between end and beginning
var tolerance = 0.01;
clock.Step(TimeSpan.Parse("00:00:10.0153932"));
var expected = -2.4122350198982545;
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
clock.Step(TimeSpan.Parse("00:00:11.2655407"));
expected = -0.37153223002125113;
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
clock.Step(TimeSpan.Parse("00:00:12.6158773"));
expected = 0.3967885416786294;
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
clock.Step(TimeSpan.Parse("00:00:14.6495256"));
expected = 1.8016358493761722;
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
}
}
}

48
tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs

@ -11,10 +11,11 @@ namespace Avalonia.Skia.UnitTests
public class CustomFontManagerImpl : IFontManagerImpl
{
private readonly Typeface[] _customTypefaces;
private readonly string _defaultFamilyName;
private readonly Typeface _defaultTypeface =
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono");
private readonly Typeface _italicTypeface =
private readonly Typeface _italicTypeface =
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans");
private readonly Typeface _emojiTypeface =
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Twitter Color Emoji");
@ -22,11 +23,12 @@ namespace Avalonia.Skia.UnitTests
public CustomFontManagerImpl()
{
_customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface };
_defaultFamilyName = _defaultTypeface.FontFamily.FamilyNames.PrimaryFamilyName;
}
public string GetDefaultFontFamilyName()
{
return _defaultTypeface.FontFamily.ToString();
return _defaultFamilyName;
}
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
@ -34,39 +36,65 @@ namespace Avalonia.Skia.UnitTests
return _customTypefaces.Select(x => x.FontFamily.Name);
}
private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName };
public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily,
CultureInfo culture, out FontKey fontKey)
{
foreach (var customTypeface in _customTypefaces)
{
if (customTypeface.GlyphTypeface.GetGlyph((uint)codepoint) == 0)
{
continue;
fontKey = new FontKey(customTypeface.FontFamily, fontWeight, fontStyle);
}
fontKey = new FontKey(customTypeface.FontFamily.Name, fontWeight, fontStyle);
return true;
}
var fallback = SKFontManager.Default.MatchCharacter(codepoint);
var fallback = SKFontManager.Default.MatchCharacter(fontFamily?.Name, (SKFontStyleWeight)fontWeight,
SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, _bcp47, codepoint);
fontKey = new FontKey(fallback?.FamilyName ?? SKTypeface.Default.FamilyName, fontWeight, fontStyle);
fontKey = new FontKey(fallback?.FamilyName ?? _defaultFamilyName, fontWeight, fontStyle);
return true;
}
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{
SKTypeface skTypeface;
switch (typeface.FontFamily.Name)
{
case "Twitter Color Emoji":
{
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_emojiTypeface.FontFamily);
skTypeface = typefaceCollection.Get(typeface);
break;
}
case "Noto Sans":
{
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_italicTypeface.FontFamily);
skTypeface = typefaceCollection.Get(typeface);
break;
}
case "Noto Mono":
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily);
var skTypeface = typefaceCollection.Get(typeface);
return new GlyphTypefaceImpl(skTypeface);
{
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily);
skTypeface = typefaceCollection.Get(typeface);
break;
}
default:
return new GlyphTypefaceImpl(SKTypeface.FromFamilyName(typeface.FontFamily.Name,
(SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style));
{
skTypeface = SKTypeface.FromFamilyName(typeface.FontFamily.Name,
(SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style);
break;
}
}
return new GlyphTypefaceImpl(skTypeface);
}
}
}

13
tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs

@ -95,5 +95,18 @@ namespace Avalonia.Skia.UnitTests
Assert.Equal("Noto Mono", skTypeface.FamilyName);
}
}
[Fact]
public void Should_Throw_For_Invalid_Custom_Font()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var fontManager = new FontManagerImpl();
Assert.Throws<InvalidOperationException>(() =>
fontManager.CreateGlyphTypeface(
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Unknown")));
}
}
}
}

24
tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs

@ -332,7 +332,7 @@ namespace Avalonia.Skia.UnitTests
Typeface.Default,
12.0f,
Brushes.Black.ToImmutable(),
maxWidth : 200,
maxWidth : 200,
maxHeight : 125,
textStyleOverrides: spans);
@ -506,10 +506,30 @@ namespace Avalonia.Skia.UnitTests
}
}
private const string Text = "日本でTest一番読まれている英字新聞・ジャパンタイムズが発信する国内外ニュースと、様々なジャンルの特集記事。";
[Fact]
public void Should_Wrap()
{
using (Start())
{
for (var i = 0; i < 2000; i++)
{
var layout = new TextLayout(
Text,
Typeface.Default,
12,
Brushes.Black,
textWrapping: TextWrapping.Wrap,
maxWidth: 50);
}
}
}
public static IDisposable Start()
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
.With(renderInterface: new PlatformRenderInterface(null),
.With(renderInterface: new PlatformRenderInterface(null),
textShaperImpl: new TextShaperImpl(),
fontManagerImpl : new CustomFontManagerImpl()));

11
tests/Avalonia.UnitTests/MockFontManagerImpl.cs

@ -8,14 +8,21 @@ namespace Avalonia.UnitTests
{
public class MockFontManagerImpl : IFontManagerImpl
{
private readonly string _defaultFamilyName;
public MockFontManagerImpl(string defaultFamilyName = "Default")
{
_defaultFamilyName = defaultFamilyName;
}
public string GetDefaultFontFamilyName()
{
return "Default";
return _defaultFamilyName;
}
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
return new[] { "Default" };
return new[] { _defaultFamilyName };
}
public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily,

12
tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs

@ -1,4 +1,5 @@
using Avalonia.Media;
using System;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Xunit;
@ -19,5 +20,14 @@ namespace Avalonia.Visuals.UnitTests.Media
Assert.Same(typeface, FontManager.Current.GetOrAddTypeface(fontFamily));
}
}
[Fact]
public void Should_Throw_When_Default_FamilyName_Is_Null()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new MockFontManagerImpl(null))))
{
Assert.Throws<InvalidOperationException>(() => FontManager.Current);
}
}
}
}

Loading…
Cancel
Save