Browse Source

Implement letter spacing

pull/9299/head
Benedikt Stebner 3 years ago
parent
commit
17b2834d21
  1. 3
      src/Avalonia.Base/Media/FormattedText.cs
  2. 77
      src/Avalonia.Base/Media/GlyphRun.cs
  3. 19
      src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs
  4. 3
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  5. 21
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  6. 9
      src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs
  7. 9
      src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs
  8. 37
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  9. 20
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  10. 56
      src/Avalonia.Controls/TextBlock.cs
  11. 12
      src/Avalonia.Controls/TextBox.cs
  12. 42
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  13. 1
      src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
  14. 4
      src/Avalonia.Themes.Simple/Controls/TextBox.xaml
  15. 4
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  16. 126
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  17. 2
      src/Skia/Avalonia.Skia/TextShaperImpl.cs
  18. 131
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  19. 17
      tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
  20. 15
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  21. 2
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  22. 6
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
  23. 12
      tests/Avalonia.UnitTests/MockGlyphRun.cs
  24. 2
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

3
src/Avalonia.Base/Media/FormattedText.cs

@ -93,7 +93,8 @@ namespace Avalonia.Media
runProps,
TextWrapping.WrapWithOverflow,
0, // line height not specified
0 // indentation not specified
0, // indentation not specified
0
);
InvalidateMetrics();

77
src/Avalonia.Base/Media/GlyphRun.cs

@ -170,7 +170,7 @@ namespace Avalonia.Media
}
/// <summary>
/// Gets the scale of the current <see cref="Media.GlyphTypeface"/>
/// Gets the scale of the current <see cref="IGlyphTypeface"/>
/// </summary>
internal double Scale => FontRenderingEmSize / GlyphTypeface.Metrics.DesignEmHeight;
@ -860,82 +860,9 @@ namespace Avalonia.Media
private IGlyphRunImpl CreateGlyphRunImpl()
{
IGlyphRunImpl glyphRunImpl;
var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
var count = GlyphIndices.Count;
var scale = (float)(FontRenderingEmSize / GlyphTypeface.Metrics.DesignEmHeight);
if (GlyphOffsets == null)
{
if (GlyphTypeface.Metrics.IsFixedPitch)
{
var buffer = platformRenderInterface.AllocateGlyphRun(GlyphTypeface, (float)FontRenderingEmSize, count);
var glyphs = buffer.GlyphIndices;
for (int i = 0; i < glyphs.Length; i++)
{
glyphs[i] = GlyphIndices[i];
}
glyphRunImpl = buffer.Build();
}
else
{
var buffer = platformRenderInterface.AllocateHorizontalGlyphRun(GlyphTypeface, (float)FontRenderingEmSize, count);
var glyphs = buffer.GlyphIndices;
var positions = buffer.GlyphPositions;
var width = 0d;
for (var i = 0; i < count; i++)
{
positions[i] = (float)width;
if (GlyphAdvances == null)
{
width += GlyphTypeface.GetGlyphAdvance(GlyphIndices[i]) * scale;
}
else
{
width += GlyphAdvances[i];
}
glyphs[i] = GlyphIndices[i];
}
glyphRunImpl = buffer.Build();
}
}
else
{
var buffer = platformRenderInterface.AllocatePositionedGlyphRun(GlyphTypeface, (float)FontRenderingEmSize, count);
var glyphs = buffer.GlyphIndices;
var glyphPositions = buffer.GlyphPositions;
var currentX = 0.0;
for (var i = 0; i < count; i++)
{
var glyphOffset = GlyphOffsets[i];
glyphPositions[i] = new PointF((float)(currentX + glyphOffset.X), (float)glyphOffset.Y);
if (GlyphAdvances == null)
{
currentX += GlyphTypeface.GetGlyphAdvance(GlyphIndices[i]) * scale;
}
else
{
currentX += GlyphAdvances[i];
}
glyphs[i] = GlyphIndices[i];
}
glyphRunImpl = buffer.Build();
}
return glyphRunImpl;
return platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets);
}
void IDisposable.Dispose()

19
src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs

@ -17,15 +17,18 @@
/// <param name="textAlignment">logical horizontal alignment</param>
/// <param name="textWrap">text wrap option</param>
/// <param name="lineHeight">Paragraph line height</param>
/// <param name="letterSpacing">letter spacing</param>
public GenericTextParagraphProperties(TextRunProperties defaultTextRunProperties,
TextAlignment textAlignment = TextAlignment.Left,
TextWrapping textWrap = TextWrapping.NoWrap,
double lineHeight = 0)
double lineHeight = 0,
double letterSpacing = 0)
{
DefaultTextRunProperties = defaultTextRunProperties;
_textAlignment = textAlignment;
_textWrap = textWrap;
_lineHeight = lineHeight;
LetterSpacing = letterSpacing;
}
/// <summary>
@ -39,6 +42,7 @@
/// <param name="textWrap">text wrap option</param>
/// <param name="lineHeight">Paragraph line height</param>
/// <param name="indent">line indentation</param>
/// <param name="letterSpacing">letter spacing</param>
public GenericTextParagraphProperties(
FlowDirection flowDirection,
TextAlignment textAlignment,
@ -47,8 +51,8 @@
TextRunProperties defaultTextRunProperties,
TextWrapping textWrap,
double lineHeight,
double indent
)
double indent,
double letterSpacing)
{
_flowDirection = flowDirection;
_textAlignment = textAlignment;
@ -57,6 +61,7 @@
DefaultTextRunProperties = defaultTextRunProperties;
_textWrap = textWrap;
_lineHeight = lineHeight;
LetterSpacing = letterSpacing;
Indent = indent;
}
@ -72,7 +77,8 @@
textParagraphProperties.DefaultTextRunProperties,
textParagraphProperties.TextWrapping,
textParagraphProperties.LineHeight,
textParagraphProperties.Indent)
textParagraphProperties.Indent,
textParagraphProperties.LetterSpacing)
{
}
@ -131,6 +137,11 @@
/// </summary>
public override double Indent { get; }
/// <summary>
/// The letter spacing
/// </summary>
public override double LetterSpacing { get; }
/// <summary>
/// Set text flow direction
/// </summary>

3
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@ -249,7 +249,8 @@ namespace Avalonia.Media.TextFormatting
var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface,
currentRun.Properties.FontRenderingEmSize,
shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, paragraphProperties.DefaultIncrementalTab);
shapeableRun.BidiLevel, currentRun.Properties.CultureInfo,
paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
drawableTextRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions));

21
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -31,6 +31,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="maxWidth">The maximum width.</param>
/// <param name="maxHeight">The maximum height.</param>
/// <param name="lineHeight">The height of each line of text.</param>
/// <param name="letterSpacing">The letter spacing that is applied to rendered glyphs.</param>
/// <param name="maxLines">The maximum number of text lines.</param>
/// <param name="textStyleOverrides">The text style overrides.</param>
public TextLayout(
@ -46,12 +47,13 @@ namespace Avalonia.Media.TextFormatting
double maxWidth = double.PositiveInfinity,
double maxHeight = double.PositiveInfinity,
double lineHeight = double.NaN,
double letterSpacing = 0,
int maxLines = 0,
IReadOnlyList<ValueSpan<TextRunProperties>>? textStyleOverrides = null)
{
_paragraphProperties =
CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping,
textDecorations, flowDirection, lineHeight);
textDecorations, flowDirection, lineHeight, letterSpacing);
_textSource = new FormattedTextSource(text.AsMemory(), _paragraphProperties.DefaultTextRunProperties, textStyleOverrides);
@ -63,6 +65,8 @@ namespace Avalonia.Media.TextFormatting
MaxHeight = maxHeight;
LetterSpacing = letterSpacing;
MaxLines = maxLines;
TextLines = CreateTextLines();
@ -77,6 +81,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="maxWidth">The maximum width.</param>
/// <param name="maxHeight">The maximum height.</param>
/// <param name="lineHeight">The height of each line of text.</param>
/// <param name="letterSpacing">The letter spacing that is applied to rendered glyphs.</param>
/// <param name="maxLines">The maximum number of text lines.</param>
public TextLayout(
ITextSource textSource,
@ -85,6 +90,7 @@ namespace Avalonia.Media.TextFormatting
double maxWidth = double.PositiveInfinity,
double maxHeight = double.PositiveInfinity,
double lineHeight = double.NaN,
double letterSpacing = 0,
int maxLines = 0)
{
_textSource = textSource;
@ -99,6 +105,8 @@ namespace Avalonia.Media.TextFormatting
MaxHeight = maxHeight;
LetterSpacing = letterSpacing;
MaxLines = maxLines;
TextLines = CreateTextLines();
@ -128,6 +136,11 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public int MaxLines { get; }
/// <summary>
/// Gets the text spacing.
/// </summary>
public double LetterSpacing { get; }
/// <summary>
/// Gets the text lines.
/// </summary>
@ -374,15 +387,17 @@ namespace Avalonia.Media.TextFormatting
/// <param name="textDecorations">The text decorations.</param>
/// <param name="flowDirection">The text flow direction.</param>
/// <param name="lineHeight">The height of each line of text.</param>
/// <param name="letterSpacing">The letter spacing that is applied to rendered glyphs.</param>
/// <returns></returns>
private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize,
IBrush? foreground, TextAlignment textAlignment, TextWrapping textWrapping,
TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight)
TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight,
double letterSpacing)
{
var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground);
return new GenericTextParagraphProperties(flowDirection, textAlignment, true, false,
textRunStyle, textWrapping, lineHeight, 0);
textRunStyle, textWrapping, lineHeight, 0, letterSpacing);
}
/// <summary>

9
src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs

@ -57,7 +57,7 @@
public abstract double Indent { get; }
/// <summary>
/// Paragraph indentation
/// Get the paragraph indentation.
/// </summary>
public virtual double ParagraphIndent
{
@ -65,11 +65,16 @@
}
/// <summary>
/// Default Incremental Tab
/// Gets the default incremental tab width.
/// </summary>
public virtual double DefaultIncrementalTab
{
get { return 4 * DefaultTextRunProperties.FontRenderingEmSize; }
}
/// <summary>
/// Gets the letter spacing.
/// </summary>
public virtual double LetterSpacing { get; }
}
}

9
src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs

@ -12,13 +12,15 @@ namespace Avalonia.Media.TextFormatting
double fontRenderingEmSize = 12,
sbyte bidiLevel = 0,
CultureInfo? culture = null,
double incrementalTabWidth = 0)
double incrementalTabWidth = 0,
double letterSpacing = 0)
{
Typeface = typeface;
FontRenderingEmSize = fontRenderingEmSize;
BidiLevel = bidiLevel;
Culture = culture;
IncrementalTabWidth = incrementalTabWidth;
LetterSpacing = letterSpacing;
}
/// <summary>
@ -45,5 +47,10 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public double IncrementalTabWidth { get; }
/// <summary>
/// Get the letter spacing.
/// </summary>
public double LetterSpacing { get; }
}
}

37
src/Avalonia.Base/Platform/IPlatformRenderInterface.cs

@ -171,40 +171,15 @@ namespace Avalonia.Platform
IBitmapImpl LoadBitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, Vector dpi, int stride);
/// <summary>
/// Allocates a platform glyph run buffer.
/// Creates a platform implementation of a glyph run.
/// </summary>
/// <param name="glyphTypeface">The glyph typeface.</param>
/// <param name="fontRenderingEmSize">The font rendering em size.</param>
/// <param name="length">The length.</param>
/// <returns>An <see cref="IGlyphRunBuffer"/>.</returns>
/// <remarks>
/// This buffer only holds glyph indices.
/// </remarks>
IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length);
/// <summary>
/// Allocates a horizontal platform glyph run buffer.
/// </summary>
/// <param name="glyphTypeface">The glyph typeface.</param>
/// <param name="fontRenderingEmSize">The font rendering em size.</param>
/// <param name="length">The length.</param>
/// <returns>An <see cref="IGlyphRunBuffer"/>.</returns>
/// <remarks>
/// This buffer holds glyph indices and glyph advances.
/// </remarks>
IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length);
/// <summary>
/// Allocates a positioned platform glyph run buffer.
/// </summary>
/// <param name="glyphTypeface">The glyph typeface.</param>
/// <param name="fontRenderingEmSize">The font rendering em size.</param>
/// <param name="length">The length.</param>
/// <returns>An <see cref="IGlyphRunBuffer"/>.</returns>
/// <remarks>
/// This buffer holds glyph indices, glyph advances and glyph positions.
/// </remarks>
IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length);
/// <param name="glyphIndices">The glyph indices.</param>
/// <param name="glyphAdvances">The glyph advances.</param>
/// <param name="glyphOffsets">The glyph offsets.</param>
/// <returns></returns>
IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double>? glyphAdvances, IReadOnlyList<Vector>? glyphOffsets);
/// <summary>
/// Gets a value indicating whether the platform directly supports rectangles with rounded corners.

20
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -80,6 +80,12 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty<double> LineHeightProperty =
TextBlock.LineHeightProperty.AddOwner<TextPresenter>();
/// <summary>
/// Defines the <see cref="LetterSpacing"/> property.
/// </summary>
public static readonly StyledProperty<double> LetterSpacingProperty =
TextBlock.LetterSpacingProperty.AddOwner<TextPresenter>();
/// <summary>
/// Defines the <see cref="Background"/> property.
/// </summary>
@ -212,6 +218,15 @@ namespace Avalonia.Controls.Presenters
set => SetValue(LineHeightProperty, value);
}
/// <summary>
/// Gets or sets the letter spacing.
/// </summary>
public double LetterSpacing
{
get => GetValue(LetterSpacingProperty);
set => SetValue(LetterSpacingProperty, value);
}
/// <summary>
/// Gets or sets the text alignment.
/// </summary>
@ -333,7 +348,7 @@ namespace Avalonia.Controls.Presenters
var textLayout = new TextLayout(text, typeface, FontSize, foreground, TextAlignment,
TextWrapping, maxWidth: maxWidth, maxHeight: maxHeight, textStyleOverrides: textStyleOverrides,
flowDirection: FlowDirection, lineHeight: LineHeight);
flowDirection: FlowDirection, lineHeight: LineHeight, letterSpacing: LetterSpacing);
return textLayout;
}
@ -916,6 +931,9 @@ namespace Avalonia.Controls.Presenters
case nameof(TextAlignment):
case nameof(TextWrapping):
case nameof(LineHeight):
case nameof(LetterSpacing):
case nameof(SelectionStart):
case nameof(SelectionEnd):
case nameof(SelectionForegroundBrush):

56
src/Avalonia.Controls/TextBlock.cs

@ -82,6 +82,15 @@ namespace Avalonia.Controls
validate: IsValidLineHeight,
inherits: true);
/// <summary>
/// Defines the <see cref="LetterSpacing"/> property.
/// </summary>
public static readonly AttachedProperty<double> LetterSpacingProperty =
AvaloniaProperty.RegisterAttached<TextBlock, Control, double>(
nameof(LetterSpacing),
0,
inherits: true);
/// <summary>
/// Defines the <see cref="MaxLines"/> property.
/// </summary>
@ -262,6 +271,15 @@ namespace Avalonia.Controls
set => SetValue(LineHeightProperty, value);
}
/// <summary>
/// Gets or sets the letter spacing.
/// </summary>
public double LetterSpacing
{
get => GetValue(LetterSpacingProperty);
set => SetValue(LetterSpacingProperty, value);
}
/// <summary>
/// Gets or sets the maximum number of text lines.
/// </summary>
@ -475,6 +493,35 @@ namespace Avalonia.Controls
control.SetValue(LineHeightProperty, height);
}
/// <summary>
/// Reads the attached property from the given element
/// </summary>
/// <param name="control">The element to which to read the attached property.</param>
public static double GetLetterSpacing(Control control)
{
if (control == null)
{
throw new ArgumentNullException(nameof(control));
}
return control.GetValue(LetterSpacingProperty);
}
/// <summary>
/// Writes the attached property LetterSpacing to the given element.
/// </summary>
/// <param name="control">The element to which to write the attached property.</param>
/// <param name="letterSpacing">The property value to set</param>
public static void SetLetterSpacing(Control control, double letterSpacing)
{
if (control == null)
{
throw new ArgumentNullException(nameof(control));
}
control.SetValue(LetterSpacingProperty, letterSpacing);
}
/// <summary>
/// Reads the attached property from the given element
/// </summary>
@ -584,7 +631,7 @@ namespace Avalonia.Controls
Foreground);
var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
defaultProperties, TextWrapping, LineHeight, 0);
defaultProperties, TextWrapping, LineHeight, 0, LetterSpacing);
ITextSource textSource;
@ -744,9 +791,10 @@ namespace Avalonia.Controls
case nameof(FlowDirection):
case nameof(Padding):
case nameof(LineHeight):
case nameof(MaxLines):
case nameof (Padding):
case nameof (LineHeight):
case nameof (LetterSpacing):
case nameof (MaxLines):
case nameof(Text):
case nameof(TextDecorations):

12
src/Avalonia.Controls/TextBox.cs

@ -114,6 +114,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<double> LineHeightProperty =
TextBlock.LineHeightProperty.AddOwner<TextBox>();
/// <summary>
/// Defines see <see cref="TextBlock.LetterSpacing"/> property.
/// </summary>
public static readonly StyledProperty<double> LetterSpacingProperty =
TextBlock.LetterSpacingProperty.AddOwner<TextBox>();
public static readonly StyledProperty<string?> WatermarkProperty =
AvaloniaProperty.Register<TextBox, string?>(nameof(Watermark));
@ -378,6 +384,12 @@ namespace Avalonia.Controls
set => SetValue(MaxLinesProperty, value);
}
public double LetterSpacing
{
get => GetValue(LetterSpacingProperty);
set => SetValue(LetterSpacingProperty, value);
}
/// <summary>
/// Gets or sets the line height.
/// </summary>

42
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -115,19 +115,16 @@ namespace Avalonia.Headless
return new HeadlessGeometryStub(new Rect(glyphRun.Size));
}
public IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double> glyphAdvances, IReadOnlyList<Vector> glyphOffsets)
{
return new HeadlessGlyphRunBufferStub();
return new HeadlessGlyphRunStub();
}
public IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
{
return new HeadlessHorizontalGlyphRunBufferStub();
}
public IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
class HeadlessGlyphRunStub : IGlyphRunImpl
{
return new HeadlessPositionedGlyphRunBufferStub();
public void Dispose()
{
}
}
class HeadlessGeometryStub : IGeometryImpl
@ -213,33 +210,6 @@ namespace Avalonia.Headless
public Matrix Transform { get; }
}
class HeadlessGlyphRunBufferStub : IGlyphRunBuffer
{
public Span<ushort> GlyphIndices => Span<ushort>.Empty;
public IGlyphRunImpl Build()
{
return new HeadlessGlyphRunStub();
}
}
class HeadlessHorizontalGlyphRunBufferStub : HeadlessGlyphRunBufferStub, IHorizontalGlyphRunBuffer
{
public Span<float> GlyphPositions => Span<float>.Empty;
}
class HeadlessPositionedGlyphRunBufferStub : HeadlessGlyphRunBufferStub, IPositionedGlyphRunBuffer
{
public Span<System.Drawing.PointF> GlyphPositions => Span<System.Drawing.PointF>.Empty;
}
class HeadlessGlyphRunStub : IGlyphRunImpl
{
public void Dispose()
{
}
}
class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl
{
public HeadlessStreamingGeometryStub() : base(Rect.Empty)

1
src/Avalonia.Themes.Fluent/Controls/TextBox.xaml

@ -161,6 +161,7 @@
TextAlignment="{TemplateBinding TextAlignment}"
TextWrapping="{TemplateBinding TextWrapping}"
LineHeight="{TemplateBinding LineHeight}"
LetterSpacing="{TemplateBinding LetterSpacing}"
PasswordChar="{TemplateBinding PasswordChar}"
RevealPassword="{TemplateBinding RevealPassword}"
SelectionBrush="{TemplateBinding SelectionBrush}"

4
src/Avalonia.Themes.Simple/Controls/TextBox.xaml

@ -149,14 +149,14 @@
CaretBrush="{TemplateBinding CaretBrush}"
CaretIndex="{TemplateBinding CaretIndex}"
LineHeight="{TemplateBinding LineHeight}"
LetterSpacing="{TemplateBinding LetterSpacing}"
PasswordChar="{TemplateBinding PasswordChar}"
RevealPassword="{TemplateBinding RevealPassword}"
SelectionBrush="{TemplateBinding SelectionBrush}"
SelectionEnd="{TemplateBinding SelectionEnd}"
SelectionForegroundBrush="{TemplateBinding SelectionForegroundBrush}"
SelectionStart="{TemplateBinding SelectionStart}"
Text="{TemplateBinding Text,
Mode=TwoWay}"
Text="{TemplateBinding Text,Mode=TwoWay}"
TextAlignment="{TemplateBinding TextAlignment}"
TextWrapping="{TemplateBinding TextWrapping}" />
</Panel>

4
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@ -69,10 +69,6 @@ namespace Avalonia.Skia
public int GlyphCount { get; }
public bool IsFakeBold { get; }
public bool IsFakeItalic { get; }
public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics)
{
metrics = default;

126
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -12,8 +12,6 @@ using Avalonia.OpenGL.Imaging;
using Avalonia.Platform;
using Avalonia.Media.Imaging;
using SkiaSharp;
using System.Runtime.InteropServices;
using System.Drawing;
namespace Avalonia.Skia
{
@ -79,7 +77,7 @@ namespace Avalonia.Skia
var skFont = new SKFont(glyphTypeface.Typeface, fontRenderingEmSize)
{
Size = fontRenderingEmSize,
Edging = SKFontEdging.Antialias,
Edging = SKFontEdging.Alias,
Hinting = SKFontHinting.None,
LinearMetrics = true
};
@ -244,85 +242,91 @@ namespace Avalonia.Skia
"Current GPU acceleration backend does not support OpenGL integration");
}
public IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
=> new SKGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length);
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices,
IReadOnlyList<double> glyphAdvances, IReadOnlyList<Vector> glyphOffsets)
{
if (glyphTypeface == null)
{
throw new ArgumentNullException(nameof(glyphTypeface));
}
public IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
=> new SKHorizontalGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length);
if (glyphIndices == null)
{
throw new ArgumentNullException(nameof(glyphIndices));
}
public IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
=> new SKPositionedGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length);
var glyphTypefaceImpl = glyphTypeface as GlyphTypefaceImpl;
private abstract class SKGlyphRunBufferBase : IGlyphRunBuffer
{
protected readonly SKTextBlobBuilder _builder;
protected readonly SKFont _font;
var font = new SKFont
{
LinearMetrics = true,
Subpixel = true,
Edging = SKFontEdging.SubpixelAntialias,
Hinting = SKFontHinting.Full,
Size = (float)fontRenderingEmSize,
Typeface = glyphTypefaceImpl.Typeface,
Embolden = (glyphTypefaceImpl.FontSimulations & FontSimulations.Bold) != 0,
SkewX = (glyphTypefaceImpl.FontSimulations & FontSimulations.Oblique) != 0 ? -0.2f : 0
};
var builder = new SKTextBlobBuilder();
public SKGlyphRunBufferBase(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
var count = glyphIndices.Count;
if(glyphOffsets != null && glyphAdvances != null)
{
_builder = new SKTextBlobBuilder();
var runBuffer = builder.AllocatePositionedRun(font, count);
var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface;
var glyphSpan = runBuffer.GetGlyphSpan();
var positionSpan = runBuffer.GetPositionSpan();
_font = new SKFont
{
Subpixel = true,
Edging = SKFontEdging.SubpixelAntialias,
Hinting = SKFontHinting.Full,
LinearMetrics = true,
Size = fontRenderingEmSize,
Typeface = glyphTypefaceImpl.Typeface,
Embolden = glyphTypefaceImpl.IsFakeBold,
SkewX = glyphTypefaceImpl.IsFakeItalic ? -0.2f : 0
};
}
var currentX = 0.0;
public abstract Span<ushort> GlyphIndices { get; }
for (int i = 0; i < glyphOffsets.Count; i++)
{
var offset = glyphOffsets[i];
public IGlyphRunImpl Build()
{
return new GlyphRunImpl(_builder.Build());
}
}
glyphSpan[i] = glyphIndices[i];
private sealed class SKGlyphRunBuffer : SKGlyphRunBufferBase
{
private readonly SKRunBuffer _buffer;
positionSpan[i] = new SKPoint((float)(currentX + offset.X), (float)offset.Y);
public SKGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) : base(glyphTypeface, fontRenderingEmSize, length)
{
_buffer = _builder.AllocateRun(_font, length, 0, 0);
currentX += glyphAdvances[i];
}
}
else
{
if(glyphAdvances != null)
{
var runBuffer = builder.AllocateHorizontalRun(font, count, 0);
public override Span<ushort> GlyphIndices => _buffer.GetGlyphSpan();
}
var glyphSpan = runBuffer.GetGlyphSpan();
var positionSpan = runBuffer.GetPositionSpan();
private sealed class SKHorizontalGlyphRunBuffer : SKGlyphRunBufferBase, IHorizontalGlyphRunBuffer
{
private readonly SKHorizontalRunBuffer _buffer;
var currentX = 0.0;
public SKHorizontalGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) : base(glyphTypeface, fontRenderingEmSize, length)
{
_buffer = _builder.AllocateHorizontalRun(_font, length, 0);
}
for (int i = 0; i < glyphOffsets.Count; i++)
{
glyphSpan[i] = glyphIndices[i];
public override Span<ushort> GlyphIndices => _buffer.GetGlyphSpan();
positionSpan[i] = (float)currentX;
public Span<float> GlyphPositions => _buffer.GetPositionSpan();
}
currentX += glyphAdvances[i];
}
}
else
{
var runBuffer = builder.AllocateRun(font, count, 0, 0);
private sealed class SKPositionedGlyphRunBuffer : SKGlyphRunBufferBase, IPositionedGlyphRunBuffer
{
private readonly SKPositionedRunBuffer _buffer;
var glyphSpan = runBuffer.GetGlyphSpan();
public SKPositionedGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length) : base(glyphTypeface, fontRenderingEmSize, length)
{
_buffer = _builder.AllocatePositionedRun(_font, length);
for (int i = 0; i < glyphOffsets.Count; i++)
{
glyphSpan[i] = glyphIndices[i];
}
}
}
public override Span<ushort> GlyphIndices => _buffer.GetGlyphSpan();
public Span<PointF> GlyphPositions => MemoryMarshal.Cast<SKPoint, PointF>(_buffer.GetPositionSpan());
return new GlyphRunImpl(builder.Build());
}
}
}

2
src/Skia/Avalonia.Skia/TextShaperImpl.cs

@ -60,7 +60,7 @@ namespace Avalonia.Skia
var glyphCluster = (int)(sourceInfo.Cluster);
var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale);
var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale) + options.LetterSpacing;
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);

131
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@ -10,10 +10,7 @@ using Avalonia.Media.Imaging;
using Avalonia.Platform;
using SharpDX.DirectWrite;
using GlyphRun = Avalonia.Media.GlyphRun;
using TextAlignment = Avalonia.Media.TextAlignment;
using SharpDX.Mathematics.Interop;
using System.Runtime.InteropServices;
using System.Drawing;
namespace Avalonia
{
@ -160,6 +157,72 @@ namespace Avalonia.Direct2D1
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children) => new GeometryGroupImpl(fillRule, children);
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2);
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices,
IReadOnlyList<double> glyphAdvances, IReadOnlyList<Vector> glyphOffsets)
{
var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface;
var glyphCount = glyphIndices.Count;
var run = new SharpDX.DirectWrite.GlyphRun
{
FontFace = glyphTypefaceImpl.FontFace,
FontSize = (float)fontRenderingEmSize
};
var indices = new short[glyphCount];
for (var i = 0; i < glyphCount; i++)
{
indices[i] = (short)glyphIndices[i];
}
run.Indices = indices;
run.Advances = new float[glyphCount];
var scale = (float)(fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight);
if (glyphAdvances == null)
{
for (var i = 0; i < glyphCount; i++)
{
var advance = glyphTypeface.GetGlyphAdvance(glyphIndices[i]) * scale;
run.Advances[i] = advance;
}
}
else
{
for (var i = 0; i < glyphCount; i++)
{
var advance = (float)glyphAdvances[i];
run.Advances[i] = advance;
}
}
if (glyphOffsets == null)
{
return new GlyphRunImpl(run);
}
run.Offsets = new GlyphOffset[glyphCount];
for (var i = 0; i < glyphCount; i++)
{
var (x, y) = glyphOffsets[i];
run.Offsets[i] = new GlyphOffset
{
AdvanceOffset = (float)x,
AscenderOffset = (float)y
};
}
return new GlyphRunImpl(run);
}
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
{
if (glyphRun.GlyphTypeface is not GlyphTypefaceImpl glyphTypeface)
@ -260,68 +323,6 @@ namespace Avalonia.Direct2D1
return new WicBitmapImpl(format, alphaFormat, data, size, dpi, stride);
}
private class DWGlyphRunBuffer : IGlyphRunBuffer
{
protected readonly SharpDX.DirectWrite.GlyphRun _dwRun;
public DWGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
{
var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface;
_dwRun = new SharpDX.DirectWrite.GlyphRun
{
FontFace = glyphTypefaceImpl.FontFace,
FontSize = fontRenderingEmSize,
Indices = new short[length]
};
}
public Span<ushort> GlyphIndices => MemoryMarshal.Cast<short, ushort>(_dwRun.Indices.AsSpan());
public IGlyphRunImpl Build()
{
return new GlyphRunImpl(_dwRun);
}
}
private class DWHorizontalGlyphRunBuffer : DWGlyphRunBuffer, IHorizontalGlyphRunBuffer
{
public DWHorizontalGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
: base(glyphTypeface, fontRenderingEmSize, length)
{
_dwRun.Advances = new float[length];
}
public Span<float> GlyphPositions => _dwRun.Advances.AsSpan();
}
private class DWPositionedGlyphRunBuffer : DWGlyphRunBuffer, IPositionedGlyphRunBuffer
{
public DWPositionedGlyphRunBuffer(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
: base(glyphTypeface, fontRenderingEmSize, length)
{
_dwRun.Advances = new float[length];
_dwRun.Offsets = new GlyphOffset[length];
}
public Span<PointF> GlyphPositions => MemoryMarshal.Cast<GlyphOffset, PointF>(_dwRun.Offsets.AsSpan());
}
public IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
{
return new DWGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length);
}
public IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
{
return new DWHorizontalGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length);
}
public IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
{
return new DWPositionedGlyphRunBuffer(glyphTypeface, fontRenderingEmSize, length);
}
public bool SupportsIndividualRoundRects => false;
public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul;

17
tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs

@ -72,7 +72,7 @@ namespace Avalonia.Base.UnitTests.VisualTree
throw new NotImplementedException();
}
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double> glyphAdvances, IReadOnlyList<Vector> glyphOffsets)
{
throw new NotImplementedException();
}
@ -126,21 +126,6 @@ namespace Avalonia.Base.UnitTests.VisualTree
throw new NotImplementedException();
}
public IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
{
throw new NotImplementedException();
}
public IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
{
throw new NotImplementedException();
}
public IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
{
throw new NotImplementedException();
}
class MockStreamGeometry : IStreamGeometryImpl
{
private MockStreamGeometryContext _impl = new MockStreamGeometryContext();

15
tests/Avalonia.Benchmarks/NullRenderingPlatform.cs

@ -5,6 +5,7 @@ using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Avalonia.Media.Imaging;
using Microsoft.Diagnostics.Runtime;
namespace Avalonia.Benchmarks
{
@ -117,19 +118,9 @@ namespace Avalonia.Benchmarks
return new MockStreamGeometryImpl();
}
public IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double> glyphAdvances, IReadOnlyList<Vector> glyphOffsets)
{
throw new NotImplementedException();
}
public IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
{
throw new NotImplementedException();
}
public IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length)
{
throw new NotImplementedException();
return new MockGlyphRun();
}
public bool SupportsIndividualRoundRects => true;

2
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

@ -425,7 +425,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var paragraphProperties = new GenericTextParagraphProperties(flowDirection, textAlignment, true, true,
defaultProperties, TextWrapping.NoWrap, 0, 0);
defaultProperties, TextWrapping.NoWrap, 0, 0, 0);
var textSource = new SingleBufferTextSource(text, defaultProperties);
var formatter = new TextFormatterImpl();

6
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@ -878,7 +878,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textLine =
formatter.FormatLine(textSource, 0, 200,
new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0));
new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left,
true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0));
var textBounds = textLine.GetTextBounds(0, 3);
@ -924,7 +925,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textLine =
formatter.FormatLine(textSource, 0, 200,
new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0));
new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Left,
true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0));
var textBounds = textLine.GetTextBounds(0, 4);

12
tests/Avalonia.UnitTests/MockGlyphRun.cs

@ -0,0 +1,12 @@
using Avalonia.Platform;
namespace Avalonia.UnitTests
{
public class MockGlyphRun : IGlyphRunImpl
{
public void Dispose()
{
}
}
}

2
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@ -142,7 +142,7 @@ namespace Avalonia.UnitTests
throw new NotImplementedException();
}
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double> glyphAdvances, IReadOnlyList<Vector> glyphOffsets)
{
return Mock.Of<IGlyphRunImpl>();
}

Loading…
Cancel
Save