From 17b2834d21f7e7acd63586b69f8ece4d7099f131 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 2 Nov 2022 10:56:06 +0100 Subject: [PATCH] Implement letter spacing --- src/Avalonia.Base/Media/FormattedText.cs | 3 +- src/Avalonia.Base/Media/GlyphRun.cs | 77 +--------- .../GenericTextParagraphProperties.cs | 19 ++- .../Media/TextFormatting/TextFormatterImpl.cs | 3 +- .../Media/TextFormatting/TextLayout.cs | 21 ++- .../TextFormatting/TextParagraphProperties.cs | 9 +- .../Media/TextFormatting/TextShaperOptions.cs | 9 +- .../Platform/IPlatformRenderInterface.cs | 37 +---- .../Presenters/TextPresenter.cs | 20 ++- src/Avalonia.Controls/TextBlock.cs | 56 +++++++- src/Avalonia.Controls/TextBox.cs | 12 ++ .../HeadlessPlatformRenderInterface.cs | 42 +----- .../Controls/TextBox.xaml | 1 + .../Controls/TextBox.xaml | 4 +- src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs | 4 - .../Avalonia.Skia/PlatformRenderInterface.cs | 126 +++++++++-------- src/Skia/Avalonia.Skia/TextShaperImpl.cs | 2 +- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 131 +++++++++--------- .../VisualTree/MockRenderInterface.cs | 17 +-- .../NullRenderingPlatform.cs | 15 +- .../TextFormatting/TextFormatterTests.cs | 2 +- .../Media/TextFormatting/TextLineTests.cs | 6 +- tests/Avalonia.UnitTests/MockGlyphRun.cs | 12 ++ .../MockPlatformRenderInterface.cs | 2 +- 24 files changed, 306 insertions(+), 324 deletions(-) create mode 100644 tests/Avalonia.UnitTests/MockGlyphRun.cs 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(); }