diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index 08a9aa3ceb..4f69f39e02 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,6 +1,6 @@  - - + + diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index 1ce9f5db06..ca1d97290e 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/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)); diff --git a/src/Avalonia.Animation/AnimatorKeyFrame.cs b/src/Avalonia.Animation/AnimatorKeyFrame.cs index 36d15e518e..f6a0c12be4 100644 --- a/src/Avalonia.Animation/AnimatorKeyFrame.cs +++ b/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; diff --git a/src/Avalonia.Animation/Animators/Animator`1.cs b/src/Avalonia.Animation/Animators/Animator`1.cs index aa5e6aaf14..0660440e30 100644 --- a/src/Avalonia.Animation/Animators/Animator`1.cs +++ b/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); } diff --git a/src/Avalonia.Animation/KeyFrame.cs b/src/Avalonia.Animation/KeyFrame.cs index ec59586584..c2cc1aa051 100644 --- a/src/Avalonia.Animation/KeyFrame.cs +++ b/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 } } + /// + /// Gets or sets the KeySpline of this . + /// + /// The key spline. + 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."); + } + } + } } diff --git a/src/Avalonia.Animation/KeySpline.cs b/src/Avalonia.Animation/KeySpline.cs new file mode 100644 index 0000000000..5a4f7a15a3 --- /dev/null +++ b/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 +{ + /// + /// 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 + /// + [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 + + /// + /// Create a with X1 = Y1 = 0 and X2 = Y2 = 1. + /// + public KeySpline() + { + _controlPointX1 = 0.0; + _controlPointY1 = 0.0; + _controlPointX2 = 1.0; + _controlPointY2 = 1.0; + _isDirty = true; + } + + /// + /// Create a with the given parameters + /// + /// X coordinate for the first control point + /// Y coordinate for the first control point + /// X coordinate for the second control point + /// Y coordinate for the second control point + public KeySpline(double x1, double y1, double x2, double y2) + { + _controlPointX1 = x1; + _controlPointY1 = y1; + _controlPointX2 = x2; + _controlPointY2 = y2; + _isDirty = true; + } + + /// + /// Parse a from a string. The string + /// needs to contain 4 values in it for the 2 control points. + /// + /// string with 4 values in it + /// culture of the string + /// Thrown if the string does not have 4 values + /// A with the appropriate values set + 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()); + } + } + + /// + /// X coordinate of the first control point + /// + 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."); + } + } + } + + /// + /// Y coordinate of the first control point + /// + public double ControlPointY1 + { + get => _controlPointY1; + set => _controlPointY1 = value; + } + + /// + /// X coordinate of the second control point + /// + 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."); + } + } + } + + /// + /// Y coordinate of the second control point + /// + public double ControlPointY2 + { + get => _controlPointY2; + set => _controlPointY2 = value; + } + + /// + /// Calculates spline progress from a linear progress. + /// + /// the linear progress + /// the spline progress + public double GetSplineProgress(double linearProgress) + { + if (_isDirty) + { + Build(); + } + + if (!_isSpecified) + { + return linearProgress; + } + else + { + SetParameterFromX(linearProgress); + + return GetBezierValue(_By, _Cy, _parameter); + } + } + + /// + /// Check to see whether the is valid by looking + /// at its X values. + /// + /// true if the X values for this fall in + /// acceptable range; false otherwise. + public bool IsValid() + { + return IsValidXValue(_controlPointX1) && IsValidXValue(_controlPointX2); + } + + /// + /// + /// + /// + /// + private bool IsValidXValue(double value) + { + return value >= 0.0 && value <= 1.0; + } + + /// + /// Compute cached coefficients. + /// + 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; + } + + /// + /// Get an X or Y value with the Bezier formula. + /// + /// the second Bezier coefficient + /// the third Bezier coefficient + /// the parameter value to evaluate at + /// the value of the Bezier function at the given parameter + 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; + } + + /// + /// Get X and dX/dt at a given parameter + /// + /// the parameter value to evaluate at + /// the value of x there + /// the value of dx/dt there + 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; + } + + /// + /// 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. + /// + /// the time, scaled to fit in [0,1] + 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; + } + } + } + } + } + + /// + /// Converts string values to values + /// + 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); + } + } +} diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs index 2de629432c..f9410afe6a 100644 --- a/src/Avalonia.Visuals/Media/FontManager.cs +++ b/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(); - 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; } } } diff --git a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs index a9ea322d76..96312a5466 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs +++ b/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]; diff --git a/src/Avalonia.Visuals/Media/Fonts/FontKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs index 1f1e9b067d..a8d81648ba 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FontKey.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs @@ -4,20 +4,20 @@ namespace Avalonia.Media.Fonts { public readonly struct FontKey : IEquatable { - 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; } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs index 7956c5f260..7da39dc5dc 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs +++ b/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)) { diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs index 9c20efd867..2ff4952cab 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs +++ b/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 _text; diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs deleted file mode 100644 index 3385116f26..0000000000 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs +++ /dev/null @@ -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 - } -} diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 4bec7b3f56..9f99ed3cef 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/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(); + /// /// Context create info. /// @@ -153,7 +157,7 @@ namespace Avalonia.Skia /// 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 /// 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 /// 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)); } /// @@ -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; + } + /// /// Configure paint wrapper for using gradient brush. /// @@ -514,17 +527,16 @@ namespace Avalonia.Skia /// /// Creates paint wrapper for given brush. /// + /// The paint to wrap. /// Source brush. /// Target size. + /// Optional dispose of the supplied paint. /// Paint wrapper for given brush. - 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 /// /// Creates paint wrapper for given pen. /// + /// The paint to wrap. /// Source pen. /// Target size. + /// Optional dispose of the supplied paint. /// - 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 /// /// Skia cached paint state. /// - 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 /// public void Dispose() { - Paint?.Dispose(); + if (_disposePaint) + { + Paint?.Dispose(); + } + else + { + Paint?.Reset(); + } + _disposable1?.Dispose(); _disposable2?.Dispose(); _disposable3?.Dispose(); diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 53aa6a147c..415a89e1c1 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/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); } } diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index d0157815a9..6022e7a552 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/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 { diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs index 7c3f718f9e..0fdea5ed40 100644 --- a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -7,17 +7,11 @@ namespace Avalonia.Skia /// public class GlyphRunImpl : IGlyphRunImpl { - public GlyphRunImpl(SKPaint paint, SKTextBlob textBlob) + public GlyphRunImpl(SKTextBlob textBlob) { - Paint = paint; TextBlob = textBlob; } - /// - /// Gets the paint to draw with. - /// - public SKPaint Paint { get; } - /// /// Gets the text blob to draw. /// @@ -26,7 +20,6 @@ namespace Avalonia.Skia void IDisposable.Dispose() { TextBlob.Dispose(); - Paint.Dispose(); } } } diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index f1df6a804e..46872f903e 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/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(); + /// 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); + } } } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index ce82835968..7aea90e61e 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/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]; diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index 4f04d25dee..a9aed80a04 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/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); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index 8ba46dcbce..253a373106 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/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; } diff --git a/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs new file mode 100644 index 0000000000..df7c0693e1 --- /dev/null +++ b/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(() => keySpline.ControlPointX1 = input); + Assert.Throws(() => 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: +