diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs
index 27d99bdc10..90b9755493 100644
--- a/src/Avalonia.Base/Media/FormattedText.cs
+++ b/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();
diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs
index a1cb00e209..d93a68e78b 100644
--- a/src/Avalonia.Base/Media/GlyphRun.cs
+++ b/src/Avalonia.Base/Media/GlyphRun.cs
@@ -170,7 +170,7 @@ namespace Avalonia.Media
}
///
- /// Gets the scale of the current
+ /// Gets the scale of the current
///
internal double Scale => FontRenderingEmSize / GlyphTypeface.Metrics.DesignEmHeight;
@@ -860,82 +860,9 @@ namespace Avalonia.Media
private IGlyphRunImpl CreateGlyphRunImpl()
{
- IGlyphRunImpl glyphRunImpl;
-
var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService();
- 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()
diff --git a/src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs b/src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs
index dccad1e647..b9ed31523e 100644
--- a/src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs
@@ -17,15 +17,18 @@
/// logical horizontal alignment
/// text wrap option
/// Paragraph line height
+ /// letter spacing
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;
}
///
@@ -39,6 +42,7 @@
/// text wrap option
/// Paragraph line height
/// line indentation
+ /// letter spacing
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 @@
///
public override double Indent { get; }
+ ///
+ /// The letter spacing
+ ///
+ public override double LetterSpacing { get; }
+
///
/// Set text flow direction
///
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
index 145c99cadc..7bad95c4a2 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
+++ b/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));
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
index 0828b6518a..dc79e61333 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
@@ -31,6 +31,7 @@ namespace Avalonia.Media.TextFormatting
/// The maximum width.
/// The maximum height.
/// The height of each line of text.
+ /// The letter spacing that is applied to rendered glyphs.
/// The maximum number of text lines.
/// The text style overrides.
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>? 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
/// The maximum width.
/// The maximum height.
/// The height of each line of text.
+ /// The letter spacing that is applied to rendered glyphs.
/// The maximum number of text lines.
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
///
public int MaxLines { get; }
+ ///
+ /// Gets the text spacing.
+ ///
+ public double LetterSpacing { get; }
+
///
/// Gets the text lines.
///
@@ -374,15 +387,17 @@ namespace Avalonia.Media.TextFormatting
/// The text decorations.
/// The text flow direction.
/// The height of each line of text.
+ /// The letter spacing that is applied to rendered glyphs.
///
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);
}
///
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs
index 82a0ba14d8..5691dd8ad0 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs
@@ -57,7 +57,7 @@
public abstract double Indent { get; }
///
- /// Paragraph indentation
+ /// Get the paragraph indentation.
///
public virtual double ParagraphIndent
{
@@ -65,11 +65,16 @@
}
///
- /// Default Incremental Tab
+ /// Gets the default incremental tab width.
///
public virtual double DefaultIncrementalTab
{
get { return 4 * DefaultTextRunProperties.FontRenderingEmSize; }
}
+
+ ///
+ /// Gets the letter spacing.
+ ///
+ public virtual double LetterSpacing { get; }
}
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs b/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs
index 0d00bed51e..80bbbcdbfe 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs
+++ b/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;
}
///
@@ -45,5 +47,10 @@ namespace Avalonia.Media.TextFormatting
///
public double IncrementalTabWidth { get; }
+ ///
+ /// Get the letter spacing.
+ ///
+ public double LetterSpacing { get; }
+
}
}
diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
index 9d0d7974b4..518c5f37b8 100644
--- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
+++ b/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);
///
- /// Allocates a platform glyph run buffer.
+ /// Creates a platform implementation of a glyph run.
///
/// The glyph typeface.
/// The font rendering em size.
- /// The length.
- /// An .
- ///
- /// This buffer only holds glyph indices.
- ///
- IGlyphRunBuffer AllocateGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length);
-
- ///
- /// Allocates a horizontal platform glyph run buffer.
- ///
- /// The glyph typeface.
- /// The font rendering em size.
- /// The length.
- /// An .
- ///
- /// This buffer holds glyph indices and glyph advances.
- ///
- IHorizontalGlyphRunBuffer AllocateHorizontalGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length);
-
- ///
- /// Allocates a positioned platform glyph run buffer.
- ///
- /// The glyph typeface.
- /// The font rendering em size.
- /// The length.
- /// An .
- ///
- /// This buffer holds glyph indices, glyph advances and glyph positions.
- ///
- IPositionedGlyphRunBuffer AllocatePositionedGlyphRun(IGlyphTypeface glyphTypeface, float fontRenderingEmSize, int length);
+ /// The glyph indices.
+ /// The glyph advances.
+ /// The glyph offsets.
+ ///
+ IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList? glyphAdvances, IReadOnlyList? glyphOffsets);
///
/// Gets a value indicating whether the platform directly supports rectangles with rounded corners.
diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs
index a9bb16c7df..adf0569551 100644
--- a/src/Avalonia.Controls/Presenters/TextPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs
@@ -80,6 +80,12 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty LineHeightProperty =
TextBlock.LineHeightProperty.AddOwner();
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty LetterSpacingProperty =
+ TextBlock.LetterSpacingProperty.AddOwner();
+
///
/// Defines the property.
///
@@ -212,6 +218,15 @@ namespace Avalonia.Controls.Presenters
set => SetValue(LineHeightProperty, value);
}
+ ///
+ /// Gets or sets the letter spacing.
+ ///
+ public double LetterSpacing
+ {
+ get => GetValue(LetterSpacingProperty);
+ set => SetValue(LetterSpacingProperty, value);
+ }
+
///
/// Gets or sets the text alignment.
///
@@ -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):
diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index f79d3f8296..0492c2c1e3 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/src/Avalonia.Controls/TextBlock.cs
@@ -82,6 +82,15 @@ namespace Avalonia.Controls
validate: IsValidLineHeight,
inherits: true);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly AttachedProperty LetterSpacingProperty =
+ AvaloniaProperty.RegisterAttached(
+ nameof(LetterSpacing),
+ 0,
+ inherits: true);
+
///
/// Defines the property.
///
@@ -262,6 +271,15 @@ namespace Avalonia.Controls
set => SetValue(LineHeightProperty, value);
}
+ ///
+ /// Gets or sets the letter spacing.
+ ///
+ public double LetterSpacing
+ {
+ get => GetValue(LetterSpacingProperty);
+ set => SetValue(LetterSpacingProperty, value);
+ }
+
///
/// Gets or sets the maximum number of text lines.
///
@@ -475,6 +493,35 @@ namespace Avalonia.Controls
control.SetValue(LineHeightProperty, height);
}
+ ///
+ /// Reads the attached property from the given element
+ ///
+ /// The element to which to read the attached property.
+ public static double GetLetterSpacing(Control control)
+ {
+ if (control == null)
+ {
+ throw new ArgumentNullException(nameof(control));
+ }
+
+ return control.GetValue(LetterSpacingProperty);
+ }
+
+ ///
+ /// Writes the attached property LetterSpacing to the given element.
+ ///
+ /// The element to which to write the attached property.
+ /// The property value to set
+ public static void SetLetterSpacing(Control control, double letterSpacing)
+ {
+ if (control == null)
+ {
+ throw new ArgumentNullException(nameof(control));
+ }
+
+ control.SetValue(LetterSpacingProperty, letterSpacing);
+ }
+
///
/// Reads the attached property from the given element
///
@@ -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):
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index da4e90fb66..85c1c9a9d1 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/src/Avalonia.Controls/TextBox.cs
@@ -114,6 +114,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty LineHeightProperty =
TextBlock.LineHeightProperty.AddOwner();
+ ///
+ /// Defines see property.
+ ///
+ public static readonly StyledProperty LetterSpacingProperty =
+ TextBlock.LetterSpacingProperty.AddOwner();
+
public static readonly StyledProperty WatermarkProperty =
AvaloniaProperty.Register(nameof(Watermark));
@@ -378,6 +384,12 @@ namespace Avalonia.Controls
set => SetValue(MaxLinesProperty, value);
}
+ public double LetterSpacing
+ {
+ get => GetValue(LetterSpacingProperty);
+ set => SetValue(LetterSpacingProperty, value);
+ }
+
///
/// Gets or sets the line height.
///
diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
index fcd7f1e31f..501d239cee 100644
--- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
+++ b/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 glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList 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 GlyphIndices => Span.Empty;
-
- public IGlyphRunImpl Build()
- {
- return new HeadlessGlyphRunStub();
- }
- }
-
- class HeadlessHorizontalGlyphRunBufferStub : HeadlessGlyphRunBufferStub, IHorizontalGlyphRunBuffer
- {
- public Span GlyphPositions => Span.Empty;
- }
-
- class HeadlessPositionedGlyphRunBufferStub : HeadlessGlyphRunBufferStub, IPositionedGlyphRunBuffer
- {
- public Span GlyphPositions => Span.Empty;
- }
-
- class HeadlessGlyphRunStub : IGlyphRunImpl
- {
- public void Dispose()
- {
- }
- }
-
class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl
{
public HeadlessStreamingGeometryStub() : base(Rect.Empty)
diff --git a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
index 17c69da8fd..db487ef76b 100644
--- a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
+++ b/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}"
diff --git a/src/Avalonia.Themes.Simple/Controls/TextBox.xaml b/src/Avalonia.Themes.Simple/Controls/TextBox.xaml
index 5fa6412688..0bcb425ca9 100644
--- a/src/Avalonia.Themes.Simple/Controls/TextBox.xaml
+++ b/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}" />
diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
index d11f4aa7d3..71bdc1bd6b 100644
--- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
+++ b/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;
diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
index a9696efbd4..dd3badb2d8 100644
--- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
+++ b/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 glyphIndices,
+ IReadOnlyList glyphAdvances, IReadOnlyList 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 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 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 GlyphIndices => _buffer.GetGlyphSpan();
+ positionSpan[i] = (float)currentX;
- public Span 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 GlyphIndices => _buffer.GetGlyphSpan();
-
- public Span GlyphPositions => MemoryMarshal.Cast(_buffer.GetPositionSpan());
+ return new GlyphRunImpl(builder.Build());
}
}
}
diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs
index d6bb37a06a..eaf588c27d 100644
--- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs
+++ b/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);
diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
index 8103f89dad..4d307c9762 100644
--- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
+++ b/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 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 glyphIndices,
+ IReadOnlyList glyphAdvances, IReadOnlyList 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 GlyphIndices => MemoryMarshal.Cast(_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 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 GlyphPositions => MemoryMarshal.Cast(_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;
diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
index 5f8eb45f71..10db08f302 100644
--- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
+++ b/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 glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList 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();
diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
index 34f0dfef11..4170de71e6 100644
--- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
+++ b/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 glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList 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;
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
index a315158e1b..316926b00c 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
+++ b/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();
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
index 33c18c5064..87de9ed11f 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
+++ b/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);
diff --git a/tests/Avalonia.UnitTests/MockGlyphRun.cs b/tests/Avalonia.UnitTests/MockGlyphRun.cs
new file mode 100644
index 0000000000..24948aff01
--- /dev/null
+++ b/tests/Avalonia.UnitTests/MockGlyphRun.cs
@@ -0,0 +1,12 @@
+using Avalonia.Platform;
+
+namespace Avalonia.UnitTests
+{
+ public class MockGlyphRun : IGlyphRunImpl
+ {
+ public void Dispose()
+ {
+
+ }
+ }
+}
diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
index 586436ef7f..0f951ed867 100644
--- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
+++ b/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 glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets)
{
return Mock.Of();
}