From 88967de49e083992e9ee748d7767d7a999e42f41 Mon Sep 17 00:00:00 2001 From: Herman K Date: Thu, 1 Feb 2024 07:05:10 +0200 Subject: [PATCH] Ability to configure font features (#14157) * Ability to configure font features * Minor adjustments --------- Co-authored-by: Herman Kirshin Co-authored-by: Benedikt Stebner --- .../ControlCatalog/Pages/TextBlockPage.xaml | 7 + src/Avalonia.Base/Media/FontFeature.cs | 153 ++++++++++++++++++ .../Media/FontFeatureCollection.cs | 10 ++ src/Avalonia.Base/Media/FormattedText.cs | 66 ++++++++ .../GenericTextRunProperties.cs | 24 ++- .../Media/TextFormatting/TextFormatterImpl.cs | 5 +- .../Media/TextFormatting/TextLayout.cs | 50 +++++- .../Media/TextFormatting/TextRunProperties.cs | 10 +- .../Media/TextFormatting/TextShaperOptions.cs | 22 ++- src/Avalonia.Controls/Documents/Inline.cs | 10 +- .../Documents/TextElement.cs | 37 +++++ .../Presenters/TextPresenter.cs | 15 +- .../Primitives/TemplatedControl.cs | 15 ++ src/Avalonia.Controls/SelectableTextBlock.cs | 3 +- src/Avalonia.Controls/TextBlock.cs | 17 ++ src/Avalonia.Controls/TextBox.cs | 4 +- src/Skia/Avalonia.Skia/TextShaperImpl.cs | 25 ++- .../Media/TextShaperImpl.cs | 25 ++- 18 files changed, 479 insertions(+), 19 deletions(-) create mode 100644 src/Avalonia.Base/Media/FontFeature.cs create mode 100644 src/Avalonia.Base/Media/FontFeatureCollection.cs diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml index 70f83f6799..7c0300d318 100644 --- a/samples/ControlCatalog/Pages/TextBlockPage.xaml +++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml @@ -128,6 +128,13 @@ . + + + + + + + diff --git a/src/Avalonia.Base/Media/FontFeature.cs b/src/Avalonia.Base/Media/FontFeature.cs new file mode 100644 index 0000000000..7e843c84d1 --- /dev/null +++ b/src/Avalonia.Base/Media/FontFeature.cs @@ -0,0 +1,153 @@ +using System.Globalization; +using System.Text; +using System.Text.RegularExpressions; + +namespace Avalonia.Media; + +/// +/// Font feature +/// +public record FontFeature +{ + private const int DefaultValue = 1; + private const int InfinityEnd = -1; + + private static readonly Regex s_featureRegex = new Regex( + @"^\s*(?[+-])?\s*(?\w{4})\s*(\[\s*(?\d+)?(\s*(?:)\s*)?(?\d+)?\s*\])?\s*(?(Value)()|(=\s*(?\d+|on|off)))?\s*$", + RegexOptions.Compiled | RegexOptions.ExplicitCapture); + + /// Gets or sets the tag. + public string Tag + { + get; + init; + } + + /// Gets or sets the value. + public int Value + { + get; + init; + } + + /// Gets or sets the start. + public int Start + { + get; + init; + } + + /// Gets or sets the end. + public int End + { + get; + init; + } + + /// + /// Creates an instance of FontFeature. + /// + public FontFeature() + { + Tag = string.Empty; + Value = DefaultValue; + Start = 0; + End = InfinityEnd; + } + + /// + /// Parses a string to return a . + /// Syntax is the following: + /// + /// Syntax Value Start End + /// Setting value: + /// kern 1 0 ∞ Turn feature on + /// +kern 1 0 ∞ Turn feature on + /// -kern 0 0 ∞ Turn feature off + /// kern=0 0 0 ∞ Turn feature off + /// kern=1 1 0 ∞ Turn feature on + /// aalt=2 2 0 ∞ Choose 2nd alternate + /// Setting index: + /// kern[] 1 0 ∞ Turn feature on + /// kern[:] 1 0 ∞ Turn feature on + /// kern[5:] 1 5 ∞ Turn feature on, partial + /// kern[:5] 1 0 5 Turn feature on, partial + /// kern[3:5] 1 3 5 Turn feature on, range + /// kern[3] 1 3 3+1 Turn feature on, single char + /// Mixing it all: + /// aalt[3:5]=2 2 3 5 Turn 2nd alternate on for range + /// + /// + /// The string. + /// The . + // ReSharper disable once UnusedMember.Global + public static FontFeature Parse(string s) + { + var match = s_featureRegex.Match(s); + + if (!match.Success) + { + return new FontFeature(); + } + + var hasSeparator = match.Groups["Separator"].Value == ":"; + var hasStart = int.TryParse(match.Groups["Start"].Value, NumberStyles.None, CultureInfo.InvariantCulture, out var start); + var hasEnd = int.TryParse(match.Groups["End"].Value, NumberStyles.None, CultureInfo.InvariantCulture, out var end); + + var stringValue = match.Groups["Value"].Value; + if (stringValue == "-" || stringValue.ToUpperInvariant() == "OFF") + stringValue = "0"; + if (stringValue == "+" || stringValue.ToUpperInvariant() == "ON") + stringValue = "1"; + + var result = new FontFeature + { + Tag = match.Groups["Tag"].Value, + Start = hasStart ? start : 0, + End = hasEnd ? end : hasStart && !hasSeparator ? (start + 1) : InfinityEnd, + Value = int.TryParse(stringValue, NumberStyles.None, CultureInfo.InvariantCulture, out var value) ? value : DefaultValue, + }; + + return result; + } + + /// + /// Gets a string representation of the . + /// + /// The string representation. + public override string ToString() + { + var result = new StringBuilder(128); + + if (Value == 0) + result.Append('-'); + result.Append(Tag ?? string.Empty); + + if (Start != 0 || End != InfinityEnd) + { + result.Append('['); + + if (Start > 0) + result.Append(Start.ToString(CultureInfo.InvariantCulture)); + + if (End != Start + 1) + { + result.Append(':'); + if (End != InfinityEnd) + result.Append(End.ToString(CultureInfo.InvariantCulture)); + } + + result.Append(']'); + } + + if (Value is DefaultValue or 0) + { + return result.ToString(); + } + + result.Append('='); + result.Append(Value.ToString(CultureInfo.InvariantCulture)); + + return result.ToString(); + } +} diff --git a/src/Avalonia.Base/Media/FontFeatureCollection.cs b/src/Avalonia.Base/Media/FontFeatureCollection.cs new file mode 100644 index 0000000000..f05a490a64 --- /dev/null +++ b/src/Avalonia.Base/Media/FontFeatureCollection.cs @@ -0,0 +1,10 @@ +using Avalonia.Collections; + +namespace Avalonia.Media; + +/// +/// List of font feature settings +/// +public class FontFeatureCollection : AvaloniaList +{ +} diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs index 8ad2c0d9e9..2029cbf7cd 100644 --- a/src/Avalonia.Base/Media/FormattedText.cs +++ b/src/Avalonia.Base/Media/FormattedText.cs @@ -3,6 +3,7 @@ using System.Collections; using System.ComponentModel; using System.Diagnostics; using System.Globalization; +using System.Linq; using Avalonia.Media.TextFormatting; using Avalonia.Utilities; @@ -50,6 +51,7 @@ namespace Avalonia.Media /// Type face used to display text. /// Font em size in visual units (1/96 of an inch). /// Foreground brush used to render text. + /// Optional list of turned on/off features. public FormattedText( string textToFormat, CultureInfo culture, @@ -183,6 +185,7 @@ namespace Avalonia.Media var newProps = new GenericTextRunProperties( runProps.Typeface, + runProps.FontFeatures, runProps.FontRenderingEmSize, runProps.TextDecorations, foregroundBrush, @@ -197,6 +200,62 @@ namespace Avalonia.Media } } + /// + /// Sets or changes the font features for the text object + /// + /// Feature collection + public void SetFontFeatures(FontFeatureCollection? fontFeatures) + { + SetFontFeatures(fontFeatures, 0, _text.Length); + } + + /// + /// Sets or changes the font features for the text object + /// + /// Feature collection + /// The start index of initial character to apply the change to. + /// The number of characters the change should be applied to. + public void SetFontFeatures(FontFeatureCollection? fontFeatures, int startIndex, int count) + { + var limit = ValidateRange(startIndex, count); + for (var i = startIndex; i < limit;) + { + var formatRider = new SpanRider(_formatRuns, _latestPosition, i); + i = Math.Min(limit, i + formatRider.Length); + +#pragma warning disable 6506 + // Presharp warns that runProps is not validated, but it can never be null + // because the rider is already checked to be in range + + if (!(formatRider.CurrentElement is GenericTextRunProperties runProps)) + { + throw new NotSupportedException($"{nameof(runProps)} can not be null."); + } + + if ((fontFeatures == null && runProps.FontFeatures == null) || + (fontFeatures != null && runProps.FontFeatures != null && + fontFeatures.SequenceEqual(runProps.FontFeatures))) + { + continue; + } + + var newProps = new GenericTextRunProperties( + runProps.Typeface, + fontFeatures, + runProps.FontRenderingEmSize, + runProps.TextDecorations, + runProps.ForegroundBrush, + runProps.BackgroundBrush, + runProps.BaselineAlignment, + runProps.CultureInfo + ); + +#pragma warning restore 6506 + _latestPosition = _formatRuns.SetValue(formatRider.CurrentPosition, i - formatRider.CurrentPosition, + newProps, formatRider.SpanPosition); + } + } + /// /// Sets or changes the font family for the text object /// @@ -270,6 +329,7 @@ namespace Avalonia.Media var newProps = new GenericTextRunProperties( new Typeface(fontFamily, oldTypeface.Style, oldTypeface.Weight), + runProps.FontFeatures, runProps.FontRenderingEmSize, runProps.TextDecorations, runProps.ForegroundBrush, @@ -329,6 +389,7 @@ namespace Avalonia.Media var newProps = new GenericTextRunProperties( runProps.Typeface, + runProps.FontFeatures, emSize, runProps.TextDecorations, runProps.ForegroundBrush, @@ -391,6 +452,7 @@ namespace Avalonia.Media var newProps = new GenericTextRunProperties( runProps.Typeface, + runProps.FontFeatures, runProps.FontRenderingEmSize, runProps.TextDecorations, runProps.ForegroundBrush, @@ -450,6 +512,7 @@ namespace Avalonia.Media var newProps = new GenericTextRunProperties( new Typeface(oldTypeface.FontFamily, oldTypeface.Style, weight), + runProps.FontFeatures, runProps.FontRenderingEmSize, runProps.TextDecorations, runProps.ForegroundBrush, @@ -506,6 +569,7 @@ namespace Avalonia.Media var newProps = new GenericTextRunProperties( new Typeface(oldTypeface.FontFamily, style, oldTypeface.Weight), + runProps.FontFeatures, runProps.FontRenderingEmSize, runProps.TextDecorations, runProps.ForegroundBrush, @@ -562,6 +626,7 @@ namespace Avalonia.Media var newProps = new GenericTextRunProperties( typeface, + runProps.FontFeatures, runProps.FontRenderingEmSize, runProps.TextDecorations, runProps.ForegroundBrush, @@ -619,6 +684,7 @@ namespace Avalonia.Media var newProps = new GenericTextRunProperties( runProps.Typeface, + runProps.FontFeatures, runProps.FontRenderingEmSize, textDecorations, runProps.ForegroundBrush, diff --git a/src/Avalonia.Base/Media/TextFormatting/GenericTextRunProperties.cs b/src/Avalonia.Base/Media/TextFormatting/GenericTextRunProperties.cs index 13d7b11645..0a8ab72a25 100644 --- a/src/Avalonia.Base/Media/TextFormatting/GenericTextRunProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/GenericTextRunProperties.cs @@ -1,4 +1,6 @@ -using System.Globalization; +using System; +using System.Collections.Generic; +using System.Globalization; namespace Avalonia.Media.TextFormatting { @@ -9,9 +11,25 @@ namespace Avalonia.Media.TextFormatting { private const double DefaultFontRenderingEmSize = 12; + // TODO12: Remove in 12.0.0 and make fontFeatures parameter in main ctor optional public GenericTextRunProperties(Typeface typeface, double fontRenderingEmSize = DefaultFontRenderingEmSize, TextDecorationCollection? textDecorations = null, IBrush? foregroundBrush = null, IBrush? backgroundBrush = null, BaselineAlignment baselineAlignment = BaselineAlignment.Baseline, + CultureInfo? cultureInfo = null) : + this(typeface, null, fontRenderingEmSize, textDecorations, foregroundBrush, + backgroundBrush, baselineAlignment, cultureInfo) + { + } + + // TODO12:Change signature in 12.0.0 + public GenericTextRunProperties( + Typeface typeface, + FontFeatureCollection? fontFeatures, + double fontRenderingEmSize = DefaultFontRenderingEmSize, + TextDecorationCollection? textDecorations = null, + IBrush? foregroundBrush = null, + IBrush? backgroundBrush = null, + BaselineAlignment baselineAlignment = BaselineAlignment.Baseline, CultureInfo? cultureInfo = null) { Typeface = typeface; @@ -21,6 +39,7 @@ namespace Avalonia.Media.TextFormatting BackgroundBrush = backgroundBrush; BaselineAlignment = baselineAlignment; CultureInfo = cultureInfo; + FontFeatures = fontFeatures; } /// @@ -38,6 +57,9 @@ namespace Avalonia.Media.TextFormatting /// public override IBrush? BackgroundBrush { get; } + /// + public override FontFeatureCollection? FontFeatures { get; } + /// public override BaselineAlignment BaselineAlignment { get; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 1b477db1a8..5aeff0fba2 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -272,7 +272,7 @@ namespace Avalonia.Media.TextFormatting } var shaperOptions = new TextShaperOptions( - properties.CachedGlyphTypeface, + properties.CachedGlyphTypeface, properties.FontFeatures, properties.FontRenderingEmSize, shapeableRun.BidiLevel, properties.CultureInfo, paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); @@ -976,7 +976,8 @@ namespace Avalonia.Media.TextFormatting var cultureInfo = textRun.Properties.CultureInfo; - var shaperOptions = new TextShaperOptions(glyphTypeface, fontRenderingEmSize, (sbyte)flowDirection, cultureInfo); + var shaperOptions = new TextShaperOptions(glyphTypeface, textRun.Properties.FontFeatures, + fontRenderingEmSize, (sbyte)flowDirection, cultureInfo); var shapedBuffer = textShaper.ShapeText(textRun.Text, shaperOptions); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index b6b6d11a49..485df1ef1b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -17,6 +17,7 @@ namespace Avalonia.Media.TextFormatting private int _textSourceLength; + // TODO12: Remove in 12.0.0 and make fontFeatures parameter in main ctor optional /// /// Initializes a new instance of the class. /// @@ -51,10 +52,52 @@ namespace Avalonia.Media.TextFormatting double letterSpacing = 0, int maxLines = 0, IReadOnlyList>? textStyleOverrides = null) + : this(text, typeface, null, fontSize, foreground, textAlignment, textWrapping, textTrimming, textDecorations, + flowDirection, maxWidth, maxHeight, lineHeight, letterSpacing, maxLines, textStyleOverrides) + { + } + + // TODO12:Change signature in 12.0.0 + /// + /// Initializes a new instance of the class. + /// + /// The text. + /// The typeface. + /// Size of the font. + /// The foreground. + /// The text alignment. + /// The text wrapping. + /// The text trimming. + /// The text decorations. + /// The text flow direction. + /// 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. + /// Optional list of turned on/off features. + public TextLayout( + string? text, + Typeface typeface, + FontFeatureCollection? fontFeatures, + double fontSize, + IBrush? foreground, + TextAlignment textAlignment = TextAlignment.Left, + TextWrapping textWrapping = TextWrapping.NoWrap, + TextTrimming? textTrimming = null, + TextDecorationCollection? textDecorations = null, + FlowDirection flowDirection = FlowDirection.LeftToRight, + 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, letterSpacing); + textDecorations, flowDirection, lineHeight, letterSpacing, fontFeatures); _textSource = new FormattedTextSource(text ?? "", _paragraphProperties.DefaultTextRunProperties, textStyleOverrides); @@ -484,13 +527,14 @@ namespace Avalonia.Media.TextFormatting /// The text flow direction. /// The height of each line of text. /// The letter spacing that is applied to rendered glyphs. + /// Optional list of turned on/off features. /// internal static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize, IBrush? foreground, TextAlignment textAlignment, TextWrapping textWrapping, TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight, - double letterSpacing) + double letterSpacing, FontFeatureCollection? features) { - var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground); + var textRunStyle = new GenericTextRunProperties(typeface, features, fontSize, textDecorations, foreground); return new GenericTextParagraphProperties(flowDirection, textAlignment, true, false, textRunStyle, textWrapping, lineHeight, 0, letterSpacing); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs index 1622bc3b6d..11db49aaf1 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs @@ -44,6 +44,11 @@ namespace Avalonia.Media.TextFormatting /// public abstract CultureInfo? CultureInfo { get; } + /// + /// Optional features of used font. + /// + public virtual FontFeatureCollection? FontFeatures => null; + /// /// Run vertical box alignment /// @@ -64,7 +69,8 @@ namespace Avalonia.Media.TextFormatting && Equals(TextDecorations, other.TextDecorations) && Equals(ForegroundBrush, other.ForegroundBrush) && Equals(BackgroundBrush, other.BackgroundBrush) && - Equals(CultureInfo, other.CultureInfo); + Equals(CultureInfo, other.CultureInfo) && + Equals(FontFeatures, other.FontFeatures); } public override bool Equals(object? obj) @@ -101,7 +107,7 @@ namespace Avalonia.Media.TextFormatting if (this is GenericTextRunProperties other && other.Typeface == typeface) return this; - return new GenericTextRunProperties(typeface, FontRenderingEmSize, + return new GenericTextRunProperties(typeface, FontFeatures, FontRenderingEmSize, TextDecorations, ForegroundBrush, BackgroundBrush, BaselineAlignment); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs b/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs index 610fc3dbc9..7fc9398732 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs @@ -1,4 +1,5 @@ -using System.Globalization; +using System.Collections.Generic; +using System.Globalization; namespace Avalonia.Media.TextFormatting { @@ -7,8 +8,22 @@ namespace Avalonia.Media.TextFormatting /// public readonly record struct TextShaperOptions { + // TODO12: Remove in 12.0.0 and make fontFeatures parameter in main ctor optional + public TextShaperOptions( + IGlyphTypeface typeface, + double fontRenderingEmSize = 12, + sbyte bidiLevel = 0, + CultureInfo? culture = null, + double incrementalTabWidth = 0, + double letterSpacing = 0) + : this(typeface, null, fontRenderingEmSize, bidiLevel, culture, incrementalTabWidth, letterSpacing) + { + } + + // TODO12:Change signature in 12.0.0 public TextShaperOptions( IGlyphTypeface typeface, + IReadOnlyList? fontFeatures, double fontRenderingEmSize = 12, sbyte bidiLevel = 0, CultureInfo? culture = null, @@ -21,6 +36,7 @@ namespace Avalonia.Media.TextFormatting Culture = culture; IncrementalTabWidth = incrementalTabWidth; LetterSpacing = letterSpacing; + FontFeatures = fontFeatures; } /// @@ -52,5 +68,9 @@ namespace Avalonia.Media.TextFormatting /// public double LetterSpacing { get; } + /// + /// Get features. + /// + public IReadOnlyList? FontFeatures { get; } } } diff --git a/src/Avalonia.Controls/Documents/Inline.cs b/src/Avalonia.Controls/Documents/Inline.cs index f18c60382f..ffa584f055 100644 --- a/src/Avalonia.Controls/Documents/Inline.cs +++ b/src/Avalonia.Controls/Documents/Inline.cs @@ -102,8 +102,14 @@ namespace Avalonia.Controls.Documents fontWeight = FontWeight.Bold; } - return new GenericTextRunProperties(new Typeface(FontFamily, fontStyle, fontWeight), FontSize, - textDecorations, Foreground, background, BaselineAlignment); + return new GenericTextRunProperties( + new Typeface(FontFamily, fontStyle, fontWeight), + FontFeatures, + FontSize, + textDecorations, + Foreground, + background, + BaselineAlignment); } /// diff --git a/src/Avalonia.Controls/Documents/TextElement.cs b/src/Avalonia.Controls/Documents/TextElement.cs index cae7a1d751..e4e5fb8983 100644 --- a/src/Avalonia.Controls/Documents/TextElement.cs +++ b/src/Avalonia.Controls/Documents/TextElement.cs @@ -23,6 +23,14 @@ namespace Avalonia.Controls.Documents defaultValue: FontFamily.Default, inherits: true); + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontFeaturesProperty = + AvaloniaProperty.RegisterAttached( + nameof(FontFeatures), + inherits: true); + /// /// Defines the property. /// @@ -87,6 +95,15 @@ namespace Avalonia.Controls.Documents set => SetValue(FontFamilyProperty, value); } + /// + /// Gets or sets the font features. + /// + public FontFeatureCollection? FontFeatures + { + get => GetValue(FontFeaturesProperty); + set => SetValue(FontFeaturesProperty, value); + } + /// /// Gets or sets the font size. /// @@ -152,6 +169,26 @@ namespace Avalonia.Controls.Documents control.SetValue(FontFamilyProperty, value); } + /// + /// Gets the value of the attached on a control. + /// + /// The control. + /// The font family. + public static FontFeatureCollection? GetFontFeatures(Control control) + { + return control.GetValue(FontFeaturesProperty); + } + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetFontFeatures(Control control, FontFeatureCollection? value) + { + control.SetValue(FontFeaturesProperty, value); + } + /// /// Gets the value of the attached on a control. /// diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 37fbbaa2e9..fc7e1db1df 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -152,6 +152,15 @@ namespace Avalonia.Controls.Presenters set => TextElement.SetFontFamily(this, value); } + /// + /// Gets or sets the font family. + /// + public FontFeatureCollection? FontFeatures + { + get => TextElement.GetFontFeatures(this); + set => TextElement.SetFontFeatures(this, value); + } + /// /// Gets or sets the font size. /// @@ -329,7 +338,7 @@ namespace Avalonia.Controls.Presenters var maxWidth = MathUtilities.IsZero(constraint.Width) ? double.PositiveInfinity : constraint.Width; var maxHeight = MathUtilities.IsZero(constraint.Height) ? double.PositiveInfinity : constraint.Height; - var textLayout = new TextLayout(text, typeface, FontSize, foreground, TextAlignment, + var textLayout = new TextLayout(text, typeface, FontFeatures, FontSize, foreground, TextAlignment, TextWrapping, maxWidth: maxWidth, maxHeight: maxHeight, textStyleOverrides: textStyleOverrides, flowDirection: FlowDirection, lineHeight: LineHeight, letterSpacing: LetterSpacing); @@ -531,7 +540,7 @@ namespace Avalonia.Controls.Presenters if (!string.IsNullOrEmpty(preeditText)) { var preeditHighlight = new ValueSpan(caretIndex, preeditText.Length, - new GenericTextRunProperties(typeface, FontSize, + new GenericTextRunProperties(typeface, FontFeatures, FontSize, foregroundBrush: foreground, textDecorations: TextDecorations.Underline)); @@ -547,7 +556,7 @@ namespace Avalonia.Controls.Presenters textStyleOverrides = new[] { new ValueSpan(start, length, - new GenericTextRunProperties(typeface, FontSize, + new GenericTextRunProperties(typeface, FontFeatures, FontSize, foregroundBrush: SelectionForegroundBrush)) }; } diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 1fef382159..12b631aeb1 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -52,6 +52,12 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty FontFamilyProperty = TextElement.FontFamilyProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty FontFeaturesProperty = + TextElement.FontFeaturesProperty.AddOwner(); + /// /// Defines the property. /// @@ -182,6 +188,15 @@ namespace Avalonia.Controls.Primitives set => SetValue(FontFamilyProperty, value); } + /// + /// Gets or sets the font features turned on/off. + /// + public FontFeatureCollection? FontFeatures + { + get => GetValue(FontFeaturesProperty); + set => SetValue(FontFeaturesProperty, value); + } + /// /// Gets or sets the size of the control's text in points. /// diff --git a/src/Avalonia.Controls/SelectableTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs index 143ac29c02..269f7b23e9 100644 --- a/src/Avalonia.Controls/SelectableTextBlock.cs +++ b/src/Avalonia.Controls/SelectableTextBlock.cs @@ -186,6 +186,7 @@ namespace Avalonia.Controls var defaultProperties = new GenericTextRunProperties( typeface, + FontFeatures, FontSize, TextDecorations, Foreground); @@ -207,7 +208,7 @@ namespace Avalonia.Controls textStyleOverrides = new[] { new ValueSpan(start, length, - new GenericTextRunProperties(typeface, FontSize, + new GenericTextRunProperties(typeface, FontFeatures, FontSize, foregroundBrush: SelectionForegroundBrush)) }; } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 01d67582d5..db144df4f8 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -148,6 +148,12 @@ namespace Avalonia.Controls public static readonly StyledProperty TextDecorationsProperty = Inline.TextDecorationsProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty FontFeaturesProperty = + TextElement.FontFeaturesProperty.AddOwner(); + /// /// Defines the property. /// @@ -339,6 +345,15 @@ namespace Avalonia.Controls set => SetValue(TextDecorationsProperty, value); } + /// + /// Gets or sets the font features. + /// + public FontFeatureCollection? FontFeatures + { + get => GetValue(FontFeaturesProperty); + set => SetValue(FontFeaturesProperty, value); + } + /// /// Gets or sets the inlines. /// @@ -635,6 +650,7 @@ namespace Avalonia.Controls var defaultProperties = new GenericTextRunProperties( typeface, + FontFeatures, FontSize, TextDecorations, Foreground); @@ -806,6 +822,7 @@ namespace Avalonia.Controls case nameof(Text): case nameof(TextDecorations): + case nameof(FontFeatures): case nameof(Foreground): { InvalidateTextLayout(); diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 8009492d77..cdc0fb8cf4 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -2221,7 +2221,7 @@ namespace Avalonia.Controls { var fontSize = FontSize; var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); - var paragraphProperties = TextLayout.CreateTextParagraphProperties(typeface, fontSize, null, default, default, null, default, LineHeight, default); + var paragraphProperties = TextLayout.CreateTextParagraphProperties(typeface, fontSize, null, default, default, null, default, LineHeight, default, FontFeatures); var textLayout = new TextLayout(new LineTextSource(MaxLines), paragraphProperties); var verticalSpace = GetVerticalSpaceBetweenScrollViewerAndPresenter(); @@ -2237,7 +2237,7 @@ namespace Avalonia.Controls { var fontSize = FontSize; var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); - var paragraphProperties = TextLayout.CreateTextParagraphProperties(typeface, fontSize, null, default, default, null, default, LineHeight, default); + var paragraphProperties = TextLayout.CreateTextParagraphProperties(typeface, fontSize, null, default, default, null, default, LineHeight, default, FontFeatures); var textLayout = new TextLayout(new LineTextSource(MinLines), paragraphProperties); var verticalSpace = GetVerticalSpaceBetweenScrollViewerAndPresenter(); diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index aeb9025ade..6744727c24 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -41,7 +41,7 @@ namespace Avalonia.Skia var font = ((GlyphTypefaceImpl)typeface).Font; - font.Shape(buffer); + font.Shape(buffer, GetFeatures(options)); if (buffer.Direction == Direction.RightToLeft) { @@ -176,5 +176,28 @@ namespace Avalonia.Skia // should never happen throw new InvalidOperationException("Memory not backed by string, array or manager"); } + + private static Feature[] GetFeatures(TextShaperOptions options) + { + if (options.FontFeatures is null || options.FontFeatures.Count == 0) + { + return Array.Empty(); + } + + var features = new Feature[options.FontFeatures.Count]; + + for (var i = 0; i < options.FontFeatures.Count; i++) + { + var fontFeature = options.FontFeatures[i]; + + features[i] = new Feature( + Tag.Parse(fontFeature.Tag), + (uint)fontFeature.Value, + (uint)fontFeature.Start, + (uint)fontFeature.End); + } + + return features; + } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index b881129488..630696e760 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs @@ -42,7 +42,7 @@ namespace Avalonia.Direct2D1.Media var font = ((GlyphTypefaceImpl)typeface).Font; - font.Shape(buffer); + font.Shape(buffer, GetFeatures(options)); if (buffer.Direction == Direction.RightToLeft) { @@ -177,5 +177,28 @@ namespace Avalonia.Direct2D1.Media // should never happen throw new InvalidOperationException("Memory not backed by string, array or manager"); } + + private static Feature[] GetFeatures(TextShaperOptions options) + { + if (options.FontFeatures is null || options.FontFeatures.Count == 0) + { + return Array.Empty(); + } + + var features = new Feature[options.FontFeatures.Count]; + + for (var i = 0; i < options.FontFeatures.Count; i++) + { + var fontFeature = options.FontFeatures[i]; + + features[i] = new Feature( + Tag.Parse(fontFeature.Tag), + (uint)fontFeature.Value, + (uint)fontFeature.Start, + (uint)fontFeature.End); + } + + return features; + } } }