Browse Source

Merge branch 'master' into osx-dialog-flag

pull/3867/head
Dariusz Komosiński 6 years ago
committed by GitHub
parent
commit
d240d78bf7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 26
      src/Avalonia.Visuals/Media/FontManager.cs
  2. 3
      src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
  3. 16
      src/Avalonia.Visuals/Media/Fonts/FontKey.cs
  4. 2
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs
  5. 2
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs
  6. 44
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs
  7. 67
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  8. 46
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  9. 3
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  10. 9
      src/Skia/Avalonia.Skia/GlyphRunImpl.cs
  11. 116
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  12. 4
      src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
  13. 2
      src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
  14. 2
      src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
  15. 48
      tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs
  16. 13
      tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs
  17. 24
      tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs
  18. 11
      tests/Avalonia.UnitTests/MockFontManagerImpl.cs
  19. 12
      tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs

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

@ -23,6 +23,11 @@ namespace Avalonia.Media
DefaultFontFamilyName = PlatformImpl.GetDefaultFontFamilyName(); DefaultFontFamilyName = PlatformImpl.GetDefaultFontFamilyName();
if (string.IsNullOrEmpty(DefaultFontFamilyName))
{
throw new InvalidOperationException("Default font family name can't be null or empty.");
}
_defaultFontFamily = new FontFamily(DefaultFontFamilyName); _defaultFontFamily = new FontFamily(DefaultFontFamilyName);
} }
@ -39,7 +44,8 @@ namespace Avalonia.Media
var fontManagerImpl = AvaloniaLocator.Current.GetService<IFontManagerImpl>(); 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); current = new FontManager(fontManagerImpl);
@ -87,7 +93,7 @@ namespace Avalonia.Media
fontFamily = _defaultFontFamily; fontFamily = _defaultFontFamily;
} }
var key = new FontKey(fontFamily, fontWeight, fontStyle); var key = new FontKey(fontFamily.Name, fontWeight, fontStyle);
if (_typefaceCache.TryGetValue(key, out var typeface)) if (_typefaceCache.TryGetValue(key, out var typeface))
{ {
@ -126,9 +132,21 @@ namespace Avalonia.Media
FontStyle fontStyle = FontStyle.Normal, FontStyle fontStyle = FontStyle.Normal,
FontFamily fontFamily = null, CultureInfo culture = null) FontFamily fontFamily = null, CultureInfo culture = null)
{ {
return PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ? foreach (var cachedTypeface in _typefaceCache.Values)
_typefaceCache.GetOrAdd(key, new Typeface(key.FontFamily, key.Weight, key.Style)) : {
// 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; null;
return matchedTypeface;
} }
} }
} }

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

@ -1,7 +1,6 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Text; using System.Text;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -21,7 +20,7 @@ namespace Avalonia.Media.Fonts
throw new ArgumentNullException(nameof(familyNames)); throw new ArgumentNullException(nameof(familyNames));
} }
Names = familyNames.Split(',').Select(x => x.Trim()).ToArray(); Names = Array.ConvertAll(familyNames.Split(','), p => p.Trim());
PrimaryFamilyName = Names[0]; 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 struct FontKey : IEquatable<FontKey>
{ {
public readonly FontFamily FontFamily; public FontKey(string familyName, FontWeight weight, FontStyle style)
public readonly FontStyle Style;
public readonly FontWeight Weight;
public FontKey(FontFamily fontFamily, FontWeight weight, FontStyle style)
{ {
FontFamily = fontFamily; FamilyName = familyName;
Style = style; Style = style;
Weight = weight; Weight = weight;
} }
public string FamilyName { get; }
public FontStyle Style { get; }
public FontWeight Weight { get; }
public override int GetHashCode() public override int GetHashCode()
{ {
var hash = FontFamily.GetHashCode(); var hash = FamilyName.GetHashCode();
hash = hash * 31 + (int)Style; hash = hash * 31 + (int)Style;
hash = hash * 31 + (int)Weight; hash = hash * 31 + (int)Weight;
@ -32,7 +32,7 @@ namespace Avalonia.Media.Fonts
public bool Equals(FontKey other) public bool Equals(FontKey other)
{ {
return FontFamily == other.FontFamily && return FamilyName == other.FamilyName &&
Style == other.Style && Style == other.Style &&
Weight == other.Weight; Weight == other.Weight;
} }

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

@ -66,7 +66,7 @@ namespace Avalonia.Media.TextFormatting
//ToDo: Fix FontFamily fallback //ToDo: Fix FontFamily fallback
currentTypeface = 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)) 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 namespace Avalonia.Media.TextFormatting.Unicode
{ {
internal ref struct CodepointEnumerator public ref struct CodepointEnumerator
{ {
private ReadOnlySlice<char> _text; 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 Matrix _currentTransform;
private GRContext _grContext; private GRContext _grContext;
private bool _disposed; private bool _disposed;
private readonly SKPaint _strokePaint = new SKPaint();
private readonly SKPaint _fillPaint = new SKPaint();
/// <summary> /// <summary>
/// Context create info. /// Context create info.
/// </summary> /// </summary>
@ -153,7 +157,7 @@ namespace Avalonia.Skia
/// <inheritdoc /> /// <inheritdoc />
public void DrawLine(IPen pen, Point p1, Point p2) 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); 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 impl = (GeometryImpl) geometry;
var size = geometry.Bounds.Size; var size = geometry.Bounds.Size;
using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper)) using (var fill = brush != null ? CreatePaint(_fillPaint, brush, size) : default(PaintWrapper))
using (var stroke = pen?.Brush != null ? CreatePaint(pen, size) : default(PaintWrapper)) using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, size) : default(PaintWrapper))
{ {
if (fill.Paint != null) if (fill.Paint != null)
{ {
@ -188,7 +192,7 @@ namespace Avalonia.Skia
if (brush != null) if (brush != null)
{ {
using (var paint = CreatePaint(brush, rect.Size)) using (var paint = CreatePaint(_fillPaint, brush, rect.Size))
{ {
if (isRounded) if (isRounded)
{ {
@ -204,7 +208,7 @@ namespace Avalonia.Skia
if (pen?.Brush != null) if (pen?.Brush != null)
{ {
using (var paint = CreatePaint(pen, rect.Size)) using (var paint = CreatePaint(_strokePaint, pen, rect.Size))
{ {
if (isRounded) if (isRounded)
{ {
@ -222,7 +226,7 @@ namespace Avalonia.Skia
/// <inheritdoc /> /// <inheritdoc />
public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) 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; var textImpl = (FormattedTextImpl) text;
textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering); textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering);
@ -232,14 +236,14 @@ namespace Avalonia.Skia
/// <inheritdoc /> /// <inheritdoc />
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) 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; var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
paint.ApplyTo(glyphRunImpl.Paint); ConfigureTextRendering(paintWrapper);
Canvas.DrawText(glyphRunImpl.TextBlob, (float)baselineOrigin.X, 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(); var paint = new SKPaint();
Canvas.SaveLayer(paint); Canvas.SaveLayer(paint);
_maskStack.Push(CreatePaint(mask, bounds.Size)); _maskStack.Push(CreatePaint(paint, mask, bounds.Size, true));
} }
/// <inheritdoc /> /// <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> /// <summary>
/// Configure paint wrapper for using gradient brush. /// Configure paint wrapper for using gradient brush.
/// </summary> /// </summary>
@ -514,17 +527,16 @@ namespace Avalonia.Skia
/// <summary> /// <summary>
/// Creates paint wrapper for given brush. /// Creates paint wrapper for given brush.
/// </summary> /// </summary>
/// <param name="paint">The paint to wrap.</param>
/// <param name="brush">Source brush.</param> /// <param name="brush">Source brush.</param>
/// <param name="targetSize">Target size.</param> /// <param name="targetSize">Target size.</param>
/// <param name="disposePaint">Optional dispose of the supplied paint.</param>
/// <returns>Paint wrapper for given brush.</returns> /// <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 var paintWrapper = new PaintWrapper(paint, disposePaint);
{
IsAntialias = true
};
var paintWrapper = new PaintWrapper(paint); paint.IsAntialias = true;
double opacity = brush.Opacity * _currentOpacity; double opacity = brush.Opacity * _currentOpacity;
@ -572,10 +584,12 @@ namespace Avalonia.Skia
/// <summary> /// <summary>
/// Creates paint wrapper for given pen. /// Creates paint wrapper for given pen.
/// </summary> /// </summary>
/// <param name="paint">The paint to wrap.</param>
/// <param name="pen">Source pen.</param> /// <param name="pen">Source pen.</param>
/// <param name="targetSize">Target size.</param> /// <param name="targetSize">Target size.</param>
/// <param name="disposePaint">Optional dispose of the supplied paint.</param>
/// <returns></returns> /// <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 // In Skia 0 thickness means - use hairline rendering
// and for us it means - there is nothing rendered. // and for us it means - there is nothing rendered.
@ -584,8 +598,7 @@ namespace Avalonia.Skia
return default; return default;
} }
var rv = CreatePaint(pen.Brush, targetSize); var rv = CreatePaint(paint, pen.Brush, targetSize, disposePaint);
var paint = rv.Paint;
paint.IsStroke = true; paint.IsStroke = true;
paint.StrokeWidth = (float) pen.Thickness; paint.StrokeWidth = (float) pen.Thickness;
@ -668,7 +681,7 @@ namespace Avalonia.Skia
/// <summary> /// <summary>
/// Skia cached paint state. /// Skia cached paint state.
/// </summary> /// </summary>
private struct PaintState : IDisposable private readonly struct PaintState : IDisposable
{ {
private readonly SKColor _color; private readonly SKColor _color;
private readonly SKShader _shader; private readonly SKShader _shader;
@ -696,14 +709,16 @@ namespace Avalonia.Skia
{ {
//We are saving memory allocations there //We are saving memory allocations there
public readonly SKPaint Paint; public readonly SKPaint Paint;
private readonly bool _disposePaint;
private IDisposable _disposable1; private IDisposable _disposable1;
private IDisposable _disposable2; private IDisposable _disposable2;
private IDisposable _disposable3; private IDisposable _disposable3;
public PaintWrapper(SKPaint paint) public PaintWrapper(SKPaint paint, bool disposePaint)
{ {
Paint = paint; Paint = paint;
_disposePaint = disposePaint;
_disposable1 = null; _disposable1 = null;
_disposable2 = null; _disposable2 = null;
@ -751,7 +766,15 @@ namespace Avalonia.Skia
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() public void Dispose()
{ {
Paint?.Dispose(); if (_disposePaint)
{
Paint?.Dispose();
}
else
{
Paint?.Reset();
}
_disposable1?.Dispose(); _disposable1?.Dispose();
_disposable2?.Dispose(); _disposable2?.Dispose();
_disposable3?.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, public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle,
FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) 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) if (culture == null)
{ {
culture = CultureInfo.CurrentUICulture; culture = CultureInfo.CurrentUICulture;
@ -45,31 +66,32 @@ namespace Avalonia.Skia
t_languageTagBuffer[0] = culture.TwoLetterISOLanguageName; t_languageTagBuffer[0] = culture.TwoLetterISOLanguageName;
t_languageTagBuffer[1] = culture.ThreeLetterISOLanguageName; 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, var skTypeface =
SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint); _skFontManager.MatchCharacter(familyNames[i], skFontStyle, t_languageTagBuffer, codepoint);
if (skTypeface == null) if (skTypeface == null)
{ {
continue; continue;
} }
fontKey = new FontKey(new FontFamily(familyName), fontWeight, fontStyle); fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle);
return true; return true;
} }
} }
else else
{ {
var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight, var skTypeface = _skFontManager.MatchCharacter(null, skFontStyle, t_languageTagBuffer, codepoint);
SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint);
if (skTypeface != null) if (skTypeface != null)
{ {
fontKey = new FontKey(new FontFamily(skTypeface.FamilyName), fontWeight, fontStyle); fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle);
return true; return true;
} }
@ -82,7 +104,7 @@ namespace Avalonia.Skia
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{ {
var skTypeface = SKTypeface.Default; SKTypeface skTypeface = null;
if (typeface.FontFamily.Key == null) if (typeface.FontFamily.Key == null)
{ {
@ -109,6 +131,12 @@ namespace Avalonia.Skia
skTypeface = fontCollection.Get(typeface); skTypeface = fontCollection.Get(typeface);
} }
if (skTypeface == null)
{
throw new InvalidOperationException(
$"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
}
return new GlyphTypefaceImpl(skTypeface); return new GlyphTypefaceImpl(skTypeface);
} }
} }

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

@ -266,7 +266,8 @@ namespace Avalonia.Skia
if (fb != null) if (fb != null)
{ {
//TODO: figure out how to get the brush size //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 else
{ {

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

@ -7,17 +7,11 @@ namespace Avalonia.Skia
/// <inheritdoc /> /// <inheritdoc />
public class GlyphRunImpl : IGlyphRunImpl public class GlyphRunImpl : IGlyphRunImpl
{ {
public GlyphRunImpl(SKPaint paint, SKTextBlob textBlob) public GlyphRunImpl(SKTextBlob textBlob)
{ {
Paint = paint;
TextBlob = textBlob; TextBlob = textBlob;
} }
/// <summary>
/// Gets the paint to draw with.
/// </summary>
public SKPaint Paint { get; }
/// <summary> /// <summary>
/// Gets the text blob to draw. /// Gets the text blob to draw.
/// </summary> /// </summary>
@ -26,7 +20,6 @@ namespace Avalonia.Skia
void IDisposable.Dispose() void IDisposable.Dispose()
{ {
TextBlob.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); 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 /> /// <inheritdoc />
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
{ {
@ -158,92 +168,84 @@ namespace Avalonia.Skia
var typeface = glyphTypeface.Typeface; var typeface = glyphTypeface.Typeface;
var paint = new SKPaint s_paint.TextSize = (float)glyphRun.FontRenderingEmSize;
{ s_paint.Typeface = typeface;
TextSize = (float)glyphRun.FontRenderingEmSize,
Typeface = typeface,
TextEncoding = SKTextEncoding.GlyphId,
IsAntialias = true,
IsStroke = false,
SubpixelText = true
};
using (var textBlobBuilder = new SKTextBlobBuilder())
{
SKTextBlob textBlob;
width = 0;
var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight); SKTextBlob textBlob;
if (glyphRun.GlyphOffsets.IsEmpty) width = 0;
{
if (glyphTypeface.IsFixedPitch)
{
textBlobBuilder.AddRun(paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span);
textBlob = textBlobBuilder.Build(); var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length; if (glyphRun.GlyphOffsets.IsEmpty)
} {
else if (glyphTypeface.IsFixedPitch)
{ {
var buffer = textBlobBuilder.AllocateHorizontalRun(paint, count, 0); s_textBlobBuilder.AddRun(s_paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span);
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];
}
}
buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span); textBlob = s_textBlobBuilder.Build();
textBlob = textBlobBuilder.Build(); width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length;
}
} }
else else
{ {
var buffer = textBlobBuilder.AllocatePositionedRun(paint, count); var buffer = s_textBlobBuilder.AllocateHorizontalRun(s_paint, count, 0);
var glyphPositions = buffer.GetPositionSpan(); var positions = buffer.GetPositionSpan();
var currentX = 0.0;
for (var i = 0; i < count; i++) for (var i = 0; i < count; i++)
{ {
var glyphOffset = glyphRun.GlyphOffsets[i]; positions[i] = (float)width;
glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y);
if (glyphRun.GlyphAdvances.IsEmpty) if (glyphRun.GlyphAdvances.IsEmpty)
{ {
currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale; width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
} }
else else
{ {
currentX += glyphRun.GlyphAdvances[i]; width += glyphRun.GlyphAdvances[i];
} }
} }
buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span); 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) 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); return GetNearestMatch(_typefaces, key);
} }
@ -49,7 +49,7 @@ namespace Avalonia.Skia
if (keys.Length == 0) if (keys.Length == 0)
{ {
return SKTypeface.Default; return null;
} }
key = keys[0]; key = keys[0];

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

@ -54,7 +54,7 @@ namespace Avalonia.Skia
continue; 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); 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); var fontFamilyName = font.FontFamily.FamilyNames.GetString(0);
fontKey = new FontKey(new FontFamily(fontFamilyName), fontWeight, fontStyle); fontKey = new FontKey(fontFamilyName, fontWeight, fontStyle);
return true; return true;
} }

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

@ -11,10 +11,11 @@ namespace Avalonia.Skia.UnitTests
public class CustomFontManagerImpl : IFontManagerImpl public class CustomFontManagerImpl : IFontManagerImpl
{ {
private readonly Typeface[] _customTypefaces; private readonly Typeface[] _customTypefaces;
private readonly string _defaultFamilyName;
private readonly Typeface _defaultTypeface = private readonly Typeface _defaultTypeface =
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"); 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"); new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans");
private readonly Typeface _emojiTypeface = private readonly Typeface _emojiTypeface =
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Twitter Color Emoji"); new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Twitter Color Emoji");
@ -22,11 +23,12 @@ namespace Avalonia.Skia.UnitTests
public CustomFontManagerImpl() public CustomFontManagerImpl()
{ {
_customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface }; _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface };
_defaultFamilyName = _defaultTypeface.FontFamily.FamilyNames.PrimaryFamilyName;
} }
public string GetDefaultFontFamilyName() public string GetDefaultFontFamilyName()
{ {
return _defaultTypeface.FontFamily.ToString(); return _defaultFamilyName;
} }
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
@ -34,39 +36,65 @@ namespace Avalonia.Skia.UnitTests
return _customTypefaces.Select(x => x.FontFamily.Name); 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, public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily,
CultureInfo culture, out FontKey fontKey) CultureInfo culture, out FontKey fontKey)
{ {
foreach (var customTypeface in _customTypefaces) foreach (var customTypeface in _customTypefaces)
{ {
if (customTypeface.GlyphTypeface.GetGlyph((uint)codepoint) == 0) if (customTypeface.GlyphTypeface.GetGlyph((uint)codepoint) == 0)
{
continue; continue;
fontKey = new FontKey(customTypeface.FontFamily, fontWeight, fontStyle); }
fontKey = new FontKey(customTypeface.FontFamily.Name, fontWeight, fontStyle);
return true; 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; return true;
} }
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{ {
SKTypeface skTypeface;
switch (typeface.FontFamily.Name) switch (typeface.FontFamily.Name)
{ {
case "Twitter Color Emoji": case "Twitter Color Emoji":
{
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_emojiTypeface.FontFamily);
skTypeface = typefaceCollection.Get(typeface);
break;
}
case "Noto Sans": case "Noto Sans":
{
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_italicTypeface.FontFamily);
skTypeface = typefaceCollection.Get(typeface);
break;
}
case "Noto Mono": case "Noto Mono":
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily); {
var skTypeface = typefaceCollection.Get(typeface); var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily);
return new GlyphTypefaceImpl(skTypeface); skTypeface = typefaceCollection.Get(typeface);
break;
}
default: 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); 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, Typeface.Default,
12.0f, 12.0f,
Brushes.Black.ToImmutable(), Brushes.Black.ToImmutable(),
maxWidth : 200, maxWidth : 200,
maxHeight : 125, maxHeight : 125,
textStyleOverrides: spans); 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() public static IDisposable Start()
{ {
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
.With(renderInterface: new PlatformRenderInterface(null), .With(renderInterface: new PlatformRenderInterface(null),
textShaperImpl: new TextShaperImpl(), textShaperImpl: new TextShaperImpl(),
fontManagerImpl : new CustomFontManagerImpl())); fontManagerImpl : new CustomFontManagerImpl()));

11
tests/Avalonia.UnitTests/MockFontManagerImpl.cs

@ -8,14 +8,21 @@ namespace Avalonia.UnitTests
{ {
public class MockFontManagerImpl : IFontManagerImpl public class MockFontManagerImpl : IFontManagerImpl
{ {
private readonly string _defaultFamilyName;
public MockFontManagerImpl(string defaultFamilyName = "Default")
{
_defaultFamilyName = defaultFamilyName;
}
public string GetDefaultFontFamilyName() public string GetDefaultFontFamilyName()
{ {
return "Default"; return _defaultFamilyName;
} }
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
{ {
return new[] { "Default" }; return new[] { _defaultFamilyName };
} }
public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily, 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.Platform;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Xunit; using Xunit;
@ -19,5 +20,14 @@ namespace Avalonia.Visuals.UnitTests.Media
Assert.Same(typeface, FontManager.Current.GetOrAddTypeface(fontFamily)); 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