Browse Source

Ability to configure font features (#14157)

* Ability to configure font features

* Minor adjustments

---------

Co-authored-by: Herman Kirshin <herman.kirshin@jetbrains.com>
Co-authored-by: Benedikt Stebner <Gillibald@users.noreply.github.com>
pull/14430/head
Herman K 3 years ago
committed by GitHub
parent
commit
88967de49e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 7
      samples/ControlCatalog/Pages/TextBlockPage.xaml
  2. 153
      src/Avalonia.Base/Media/FontFeature.cs
  3. 10
      src/Avalonia.Base/Media/FontFeatureCollection.cs
  4. 66
      src/Avalonia.Base/Media/FormattedText.cs
  5. 24
      src/Avalonia.Base/Media/TextFormatting/GenericTextRunProperties.cs
  6. 5
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  7. 50
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  8. 10
      src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs
  9. 22
      src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs
  10. 10
      src/Avalonia.Controls/Documents/Inline.cs
  11. 37
      src/Avalonia.Controls/Documents/TextElement.cs
  12. 15
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  13. 15
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  14. 3
      src/Avalonia.Controls/SelectableTextBlock.cs
  15. 17
      src/Avalonia.Controls/TextBlock.cs
  16. 4
      src/Avalonia.Controls/TextBox.cs
  17. 25
      src/Skia/Avalonia.Skia/TextShaperImpl.cs
  18. 25
      src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs

7
samples/ControlCatalog/Pages/TextBlockPage.xaml

@ -128,6 +128,13 @@
</Span>. </Span>.
</SelectableTextBlock> </SelectableTextBlock>
</Border> </Border>
<Border>
<TextBlock FontFamily="Times New Roman">
<Run Text="ABC" FontFeatures="+c2sc, +smcp"/>
<Run Text="DEF"/>
<Run Text="0123" FontFeatures="frac"/>
</TextBlock>
</Border>
</WrapPanel> </WrapPanel>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

153
src/Avalonia.Base/Media/FontFeature.cs

@ -0,0 +1,153 @@
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
namespace Avalonia.Media;
/// <summary>
/// Font feature
/// </summary>
public record FontFeature
{
private const int DefaultValue = 1;
private const int InfinityEnd = -1;
private static readonly Regex s_featureRegex = new Regex(
@"^\s*(?<Value>[+-])?\s*(?<Tag>\w{4})\s*(\[\s*(?<Start>\d+)?(\s*(?<Separator>:)\s*)?(?<End>\d+)?\s*\])?\s*(?(Value)()|(=\s*(?<Value>\d+|on|off)))?\s*$",
RegexOptions.Compiled | RegexOptions.ExplicitCapture);
/// <summary>Gets or sets the tag.</summary>
public string Tag
{
get;
init;
}
/// <summary>Gets or sets the value.</summary>
public int Value
{
get;
init;
}
/// <summary>Gets or sets the start.</summary>
public int Start
{
get;
init;
}
/// <summary>Gets or sets the end.</summary>
public int End
{
get;
init;
}
/// <summary>
/// Creates an instance of FontFeature.
/// </summary>
public FontFeature()
{
Tag = string.Empty;
Value = DefaultValue;
Start = 0;
End = InfinityEnd;
}
/// <summary>
/// Parses a string to return a <see cref="FontFeature"/>.
/// 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
///
/// </summary>
/// <param name="s">The string.</param>
/// <returns>The <see cref="FontFeature"/>.</returns>
// 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;
}
/// <summary>
/// Gets a string representation of the <see cref="FontFeature"/>.
/// </summary>
/// <returns>The string representation.</returns>
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();
}
}

10
src/Avalonia.Base/Media/FontFeatureCollection.cs

@ -0,0 +1,10 @@
using Avalonia.Collections;
namespace Avalonia.Media;
/// <summary>
/// List of font feature settings
/// </summary>
public class FontFeatureCollection : AvaloniaList<FontFeature>
{
}

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

@ -3,6 +3,7 @@ using System.Collections;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Linq;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -50,6 +51,7 @@ namespace Avalonia.Media
/// <param name="typeface">Type face used to display text.</param> /// <param name="typeface">Type face used to display text.</param>
/// <param name="emSize">Font em size in visual units (1/96 of an inch).</param> /// <param name="emSize">Font em size in visual units (1/96 of an inch).</param>
/// <param name="foreground">Foreground brush used to render text.</param> /// <param name="foreground">Foreground brush used to render text.</param>
/// <param name="features">Optional list of turned on/off features.</param>
public FormattedText( public FormattedText(
string textToFormat, string textToFormat,
CultureInfo culture, CultureInfo culture,
@ -183,6 +185,7 @@ namespace Avalonia.Media
var newProps = new GenericTextRunProperties( var newProps = new GenericTextRunProperties(
runProps.Typeface, runProps.Typeface,
runProps.FontFeatures,
runProps.FontRenderingEmSize, runProps.FontRenderingEmSize,
runProps.TextDecorations, runProps.TextDecorations,
foregroundBrush, foregroundBrush,
@ -197,6 +200,62 @@ namespace Avalonia.Media
} }
} }
/// <summary>
/// Sets or changes the font features for the text object
/// </summary>
/// <param name="fontFeatures">Feature collection</param>
public void SetFontFeatures(FontFeatureCollection? fontFeatures)
{
SetFontFeatures(fontFeatures, 0, _text.Length);
}
/// <summary>
/// Sets or changes the font features for the text object
/// </summary>
/// <param name="fontFeatures">Feature collection</param>
/// <param name="startIndex">The start index of initial character to apply the change to.</param>
/// <param name="count">The number of characters the change should be applied to.</param>
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);
}
}
/// <summary> /// <summary>
/// Sets or changes the font family for the text object /// Sets or changes the font family for the text object
/// </summary> /// </summary>
@ -270,6 +329,7 @@ namespace Avalonia.Media
var newProps = new GenericTextRunProperties( var newProps = new GenericTextRunProperties(
new Typeface(fontFamily, oldTypeface.Style, oldTypeface.Weight), new Typeface(fontFamily, oldTypeface.Style, oldTypeface.Weight),
runProps.FontFeatures,
runProps.FontRenderingEmSize, runProps.FontRenderingEmSize,
runProps.TextDecorations, runProps.TextDecorations,
runProps.ForegroundBrush, runProps.ForegroundBrush,
@ -329,6 +389,7 @@ namespace Avalonia.Media
var newProps = new GenericTextRunProperties( var newProps = new GenericTextRunProperties(
runProps.Typeface, runProps.Typeface,
runProps.FontFeatures,
emSize, emSize,
runProps.TextDecorations, runProps.TextDecorations,
runProps.ForegroundBrush, runProps.ForegroundBrush,
@ -391,6 +452,7 @@ namespace Avalonia.Media
var newProps = new GenericTextRunProperties( var newProps = new GenericTextRunProperties(
runProps.Typeface, runProps.Typeface,
runProps.FontFeatures,
runProps.FontRenderingEmSize, runProps.FontRenderingEmSize,
runProps.TextDecorations, runProps.TextDecorations,
runProps.ForegroundBrush, runProps.ForegroundBrush,
@ -450,6 +512,7 @@ namespace Avalonia.Media
var newProps = new GenericTextRunProperties( var newProps = new GenericTextRunProperties(
new Typeface(oldTypeface.FontFamily, oldTypeface.Style, weight), new Typeface(oldTypeface.FontFamily, oldTypeface.Style, weight),
runProps.FontFeatures,
runProps.FontRenderingEmSize, runProps.FontRenderingEmSize,
runProps.TextDecorations, runProps.TextDecorations,
runProps.ForegroundBrush, runProps.ForegroundBrush,
@ -506,6 +569,7 @@ namespace Avalonia.Media
var newProps = new GenericTextRunProperties( var newProps = new GenericTextRunProperties(
new Typeface(oldTypeface.FontFamily, style, oldTypeface.Weight), new Typeface(oldTypeface.FontFamily, style, oldTypeface.Weight),
runProps.FontFeatures,
runProps.FontRenderingEmSize, runProps.FontRenderingEmSize,
runProps.TextDecorations, runProps.TextDecorations,
runProps.ForegroundBrush, runProps.ForegroundBrush,
@ -562,6 +626,7 @@ namespace Avalonia.Media
var newProps = new GenericTextRunProperties( var newProps = new GenericTextRunProperties(
typeface, typeface,
runProps.FontFeatures,
runProps.FontRenderingEmSize, runProps.FontRenderingEmSize,
runProps.TextDecorations, runProps.TextDecorations,
runProps.ForegroundBrush, runProps.ForegroundBrush,
@ -619,6 +684,7 @@ namespace Avalonia.Media
var newProps = new GenericTextRunProperties( var newProps = new GenericTextRunProperties(
runProps.Typeface, runProps.Typeface,
runProps.FontFeatures,
runProps.FontRenderingEmSize, runProps.FontRenderingEmSize,
textDecorations, textDecorations,
runProps.ForegroundBrush, runProps.ForegroundBrush,

24
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 namespace Avalonia.Media.TextFormatting
{ {
@ -9,9 +11,25 @@ namespace Avalonia.Media.TextFormatting
{ {
private const double DefaultFontRenderingEmSize = 12; 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, public GenericTextRunProperties(Typeface typeface, double fontRenderingEmSize = DefaultFontRenderingEmSize,
TextDecorationCollection? textDecorations = null, IBrush? foregroundBrush = null, TextDecorationCollection? textDecorations = null, IBrush? foregroundBrush = null,
IBrush? backgroundBrush = null, BaselineAlignment baselineAlignment = BaselineAlignment.Baseline, 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) CultureInfo? cultureInfo = null)
{ {
Typeface = typeface; Typeface = typeface;
@ -21,6 +39,7 @@ namespace Avalonia.Media.TextFormatting
BackgroundBrush = backgroundBrush; BackgroundBrush = backgroundBrush;
BaselineAlignment = baselineAlignment; BaselineAlignment = baselineAlignment;
CultureInfo = cultureInfo; CultureInfo = cultureInfo;
FontFeatures = fontFeatures;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -38,6 +57,9 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc /> /// <inheritdoc />
public override IBrush? BackgroundBrush { get; } public override IBrush? BackgroundBrush { get; }
/// <inheritdoc />
public override FontFeatureCollection? FontFeatures { get; }
/// <inheritdoc /> /// <inheritdoc />
public override BaselineAlignment BaselineAlignment { get; } public override BaselineAlignment BaselineAlignment { get; }

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

@ -272,7 +272,7 @@ namespace Avalonia.Media.TextFormatting
} }
var shaperOptions = new TextShaperOptions( var shaperOptions = new TextShaperOptions(
properties.CachedGlyphTypeface, properties.CachedGlyphTypeface, properties.FontFeatures,
properties.FontRenderingEmSize, shapeableRun.BidiLevel, properties.CultureInfo, properties.FontRenderingEmSize, shapeableRun.BidiLevel, properties.CultureInfo,
paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
@ -976,7 +976,8 @@ namespace Avalonia.Media.TextFormatting
var cultureInfo = textRun.Properties.CultureInfo; 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); var shapedBuffer = textShaper.ShapeText(textRun.Text, shaperOptions);

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

@ -17,6 +17,7 @@ namespace Avalonia.Media.TextFormatting
private int _textSourceLength; private int _textSourceLength;
// TODO12: Remove in 12.0.0 and make fontFeatures parameter in main ctor optional
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TextLayout" /> class. /// Initializes a new instance of the <see cref="TextLayout" /> class.
/// </summary> /// </summary>
@ -51,10 +52,52 @@ namespace Avalonia.Media.TextFormatting
double letterSpacing = 0, double letterSpacing = 0,
int maxLines = 0, int maxLines = 0,
IReadOnlyList<ValueSpan<TextRunProperties>>? textStyleOverrides = null) IReadOnlyList<ValueSpan<TextRunProperties>>? 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
/// <summary>
/// Initializes a new instance of the <see cref="TextLayout" /> class.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="typeface">The typeface.</param>
/// <param name="fontSize">Size of the font.</param>
/// <param name="foreground">The foreground.</param>
/// <param name="textAlignment">The text alignment.</param>
/// <param name="textWrapping">The text wrapping.</param>
/// <param name="textTrimming">The text trimming.</param>
/// <param name="textDecorations">The text decorations.</param>
/// <param name="flowDirection">The text flow direction.</param>
/// <param name="maxWidth">The maximum width.</param>
/// <param name="maxHeight">The maximum height.</param>
/// <param name="lineHeight">The height of each line of text.</param>
/// <param name="letterSpacing">The letter spacing that is applied to rendered glyphs.</param>
/// <param name="maxLines">The maximum number of text lines.</param>
/// <param name="textStyleOverrides">The text style overrides.</param>
/// <param name="fontFeatures">Optional list of turned on/off features.</param>
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<ValueSpan<TextRunProperties>>? textStyleOverrides = null)
{ {
_paragraphProperties = _paragraphProperties =
CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping, CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping,
textDecorations, flowDirection, lineHeight, letterSpacing); textDecorations, flowDirection, lineHeight, letterSpacing, fontFeatures);
_textSource = new FormattedTextSource(text ?? "", _paragraphProperties.DefaultTextRunProperties, textStyleOverrides); _textSource = new FormattedTextSource(text ?? "", _paragraphProperties.DefaultTextRunProperties, textStyleOverrides);
@ -484,13 +527,14 @@ namespace Avalonia.Media.TextFormatting
/// <param name="flowDirection">The text flow direction.</param> /// <param name="flowDirection">The text flow direction.</param>
/// <param name="lineHeight">The height of each line of text.</param> /// <param name="lineHeight">The height of each line of text.</param>
/// <param name="letterSpacing">The letter spacing that is applied to rendered glyphs.</param> /// <param name="letterSpacing">The letter spacing that is applied to rendered glyphs.</param>
/// <param name="features">Optional list of turned on/off features.</param>
/// <returns></returns> /// <returns></returns>
internal static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize, internal static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize,
IBrush? foreground, TextAlignment textAlignment, TextWrapping textWrapping, IBrush? foreground, TextAlignment textAlignment, TextWrapping textWrapping,
TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight, 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, return new GenericTextParagraphProperties(flowDirection, textAlignment, true, false,
textRunStyle, textWrapping, lineHeight, 0, letterSpacing); textRunStyle, textWrapping, lineHeight, 0, letterSpacing);

10
src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs

@ -44,6 +44,11 @@ namespace Avalonia.Media.TextFormatting
/// </summary> /// </summary>
public abstract CultureInfo? CultureInfo { get; } public abstract CultureInfo? CultureInfo { get; }
/// <summary>
/// Optional features of used font.
/// </summary>
public virtual FontFeatureCollection? FontFeatures => null;
/// <summary> /// <summary>
/// Run vertical box alignment /// Run vertical box alignment
/// </summary> /// </summary>
@ -64,7 +69,8 @@ namespace Avalonia.Media.TextFormatting
&& Equals(TextDecorations, other.TextDecorations) && && Equals(TextDecorations, other.TextDecorations) &&
Equals(ForegroundBrush, other.ForegroundBrush) && Equals(ForegroundBrush, other.ForegroundBrush) &&
Equals(BackgroundBrush, other.BackgroundBrush) && Equals(BackgroundBrush, other.BackgroundBrush) &&
Equals(CultureInfo, other.CultureInfo); Equals(CultureInfo, other.CultureInfo) &&
Equals(FontFeatures, other.FontFeatures);
} }
public override bool Equals(object? obj) public override bool Equals(object? obj)
@ -101,7 +107,7 @@ namespace Avalonia.Media.TextFormatting
if (this is GenericTextRunProperties other && other.Typeface == typeface) if (this is GenericTextRunProperties other && other.Typeface == typeface)
return this; return this;
return new GenericTextRunProperties(typeface, FontRenderingEmSize, return new GenericTextRunProperties(typeface, FontFeatures, FontRenderingEmSize,
TextDecorations, ForegroundBrush, BackgroundBrush, BaselineAlignment); TextDecorations, ForegroundBrush, BackgroundBrush, BaselineAlignment);
} }
} }

22
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 namespace Avalonia.Media.TextFormatting
{ {
@ -7,8 +8,22 @@ namespace Avalonia.Media.TextFormatting
/// </summary> /// </summary>
public readonly record struct TextShaperOptions 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( public TextShaperOptions(
IGlyphTypeface typeface, IGlyphTypeface typeface,
IReadOnlyList<FontFeature>? fontFeatures,
double fontRenderingEmSize = 12, double fontRenderingEmSize = 12,
sbyte bidiLevel = 0, sbyte bidiLevel = 0,
CultureInfo? culture = null, CultureInfo? culture = null,
@ -21,6 +36,7 @@ namespace Avalonia.Media.TextFormatting
Culture = culture; Culture = culture;
IncrementalTabWidth = incrementalTabWidth; IncrementalTabWidth = incrementalTabWidth;
LetterSpacing = letterSpacing; LetterSpacing = letterSpacing;
FontFeatures = fontFeatures;
} }
/// <summary> /// <summary>
@ -52,5 +68,9 @@ namespace Avalonia.Media.TextFormatting
/// </summary> /// </summary>
public double LetterSpacing { get; } public double LetterSpacing { get; }
/// <summary>
/// Get features.
/// </summary>
public IReadOnlyList<FontFeature>? FontFeatures { get; }
} }
} }

10
src/Avalonia.Controls/Documents/Inline.cs

@ -102,8 +102,14 @@ namespace Avalonia.Controls.Documents
fontWeight = FontWeight.Bold; fontWeight = FontWeight.Bold;
} }
return new GenericTextRunProperties(new Typeface(FontFamily, fontStyle, fontWeight), FontSize, return new GenericTextRunProperties(
textDecorations, Foreground, background, BaselineAlignment); new Typeface(FontFamily, fontStyle, fontWeight),
FontFeatures,
FontSize,
textDecorations,
Foreground,
background,
BaselineAlignment);
} }
/// <inheritdoc /> /// <inheritdoc />

37
src/Avalonia.Controls/Documents/TextElement.cs

@ -23,6 +23,14 @@ namespace Avalonia.Controls.Documents
defaultValue: FontFamily.Default, defaultValue: FontFamily.Default,
inherits: true); inherits: true);
/// <summary>
/// Defines the <see cref="FontFeatures"/> property.
/// </summary>
public static readonly AttachedProperty<FontFeatureCollection?> FontFeaturesProperty =
AvaloniaProperty.RegisterAttached<TextElement, TextElement, FontFeatureCollection?>(
nameof(FontFeatures),
inherits: true);
/// <summary> /// <summary>
/// Defines the <see cref="FontSize"/> property. /// Defines the <see cref="FontSize"/> property.
/// </summary> /// </summary>
@ -87,6 +95,15 @@ namespace Avalonia.Controls.Documents
set => SetValue(FontFamilyProperty, value); set => SetValue(FontFamilyProperty, value);
} }
/// <summary>
/// Gets or sets the font features.
/// </summary>
public FontFeatureCollection? FontFeatures
{
get => GetValue(FontFeaturesProperty);
set => SetValue(FontFeaturesProperty, value);
}
/// <summary> /// <summary>
/// Gets or sets the font size. /// Gets or sets the font size.
/// </summary> /// </summary>
@ -152,6 +169,26 @@ namespace Avalonia.Controls.Documents
control.SetValue(FontFamilyProperty, value); control.SetValue(FontFamilyProperty, value);
} }
/// <summary>
/// Gets the value of the attached <see cref="FontFeaturesProperty"/> on a control.
/// </summary>
/// <param name="control">The control.</param>
/// <returns>The font family.</returns>
public static FontFeatureCollection? GetFontFeatures(Control control)
{
return control.GetValue(FontFeaturesProperty);
}
/// <summary>
/// Sets the value of the attached <see cref="FontFeaturesProperty"/> on a control.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="value">The property value to set.</param>
public static void SetFontFeatures(Control control, FontFeatureCollection? value)
{
control.SetValue(FontFeaturesProperty, value);
}
/// <summary> /// <summary>
/// Gets the value of the attached <see cref="FontSizeProperty"/> on a control. /// Gets the value of the attached <see cref="FontSizeProperty"/> on a control.
/// </summary> /// </summary>

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

@ -152,6 +152,15 @@ namespace Avalonia.Controls.Presenters
set => TextElement.SetFontFamily(this, value); set => TextElement.SetFontFamily(this, value);
} }
/// <summary>
/// Gets or sets the font family.
/// </summary>
public FontFeatureCollection? FontFeatures
{
get => TextElement.GetFontFeatures(this);
set => TextElement.SetFontFeatures(this, value);
}
/// <summary> /// <summary>
/// Gets or sets the font size. /// Gets or sets the font size.
/// </summary> /// </summary>
@ -329,7 +338,7 @@ namespace Avalonia.Controls.Presenters
var maxWidth = MathUtilities.IsZero(constraint.Width) ? double.PositiveInfinity : constraint.Width; var maxWidth = MathUtilities.IsZero(constraint.Width) ? double.PositiveInfinity : constraint.Width;
var maxHeight = MathUtilities.IsZero(constraint.Height) ? double.PositiveInfinity : constraint.Height; 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, TextWrapping, maxWidth: maxWidth, maxHeight: maxHeight, textStyleOverrides: textStyleOverrides,
flowDirection: FlowDirection, lineHeight: LineHeight, letterSpacing: LetterSpacing); flowDirection: FlowDirection, lineHeight: LineHeight, letterSpacing: LetterSpacing);
@ -531,7 +540,7 @@ namespace Avalonia.Controls.Presenters
if (!string.IsNullOrEmpty(preeditText)) if (!string.IsNullOrEmpty(preeditText))
{ {
var preeditHighlight = new ValueSpan<TextRunProperties>(caretIndex, preeditText.Length, var preeditHighlight = new ValueSpan<TextRunProperties>(caretIndex, preeditText.Length,
new GenericTextRunProperties(typeface, FontSize, new GenericTextRunProperties(typeface, FontFeatures, FontSize,
foregroundBrush: foreground, foregroundBrush: foreground,
textDecorations: TextDecorations.Underline)); textDecorations: TextDecorations.Underline));
@ -547,7 +556,7 @@ namespace Avalonia.Controls.Presenters
textStyleOverrides = new[] textStyleOverrides = new[]
{ {
new ValueSpan<TextRunProperties>(start, length, new ValueSpan<TextRunProperties>(start, length,
new GenericTextRunProperties(typeface, FontSize, new GenericTextRunProperties(typeface, FontFeatures, FontSize,
foregroundBrush: SelectionForegroundBrush)) foregroundBrush: SelectionForegroundBrush))
}; };
} }

15
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@ -52,6 +52,12 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty<FontFamily> FontFamilyProperty = public static readonly StyledProperty<FontFamily> FontFamilyProperty =
TextElement.FontFamilyProperty.AddOwner<TemplatedControl>(); TextElement.FontFamilyProperty.AddOwner<TemplatedControl>();
/// <summary>
/// Defines the <see cref="FontFeaturesProperty"/> property.
/// </summary>
public static readonly StyledProperty<FontFeatureCollection?> FontFeaturesProperty =
TextElement.FontFeaturesProperty.AddOwner<TemplatedControl>();
/// <summary> /// <summary>
/// Defines the <see cref="FontSize"/> property. /// Defines the <see cref="FontSize"/> property.
/// </summary> /// </summary>
@ -182,6 +188,15 @@ namespace Avalonia.Controls.Primitives
set => SetValue(FontFamilyProperty, value); set => SetValue(FontFamilyProperty, value);
} }
/// <summary>
/// Gets or sets the font features turned on/off.
/// </summary>
public FontFeatureCollection? FontFeatures
{
get => GetValue(FontFeaturesProperty);
set => SetValue(FontFeaturesProperty, value);
}
/// <summary> /// <summary>
/// Gets or sets the size of the control's text in points. /// Gets or sets the size of the control's text in points.
/// </summary> /// </summary>

3
src/Avalonia.Controls/SelectableTextBlock.cs

@ -186,6 +186,7 @@ namespace Avalonia.Controls
var defaultProperties = new GenericTextRunProperties( var defaultProperties = new GenericTextRunProperties(
typeface, typeface,
FontFeatures,
FontSize, FontSize,
TextDecorations, TextDecorations,
Foreground); Foreground);
@ -207,7 +208,7 @@ namespace Avalonia.Controls
textStyleOverrides = new[] textStyleOverrides = new[]
{ {
new ValueSpan<TextRunProperties>(start, length, new ValueSpan<TextRunProperties>(start, length,
new GenericTextRunProperties(typeface, FontSize, new GenericTextRunProperties(typeface, FontFeatures, FontSize,
foregroundBrush: SelectionForegroundBrush)) foregroundBrush: SelectionForegroundBrush))
}; };
} }

17
src/Avalonia.Controls/TextBlock.cs

@ -148,6 +148,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<TextDecorationCollection?> TextDecorationsProperty = public static readonly StyledProperty<TextDecorationCollection?> TextDecorationsProperty =
Inline.TextDecorationsProperty.AddOwner<TextBlock>(); Inline.TextDecorationsProperty.AddOwner<TextBlock>();
/// <summary>
/// Defines the <see cref="FontFeatures"/> property.
/// </summary>
public static readonly StyledProperty<FontFeatureCollection?> FontFeaturesProperty =
TextElement.FontFeaturesProperty.AddOwner<TextBlock>();
/// <summary> /// <summary>
/// Defines the <see cref="Inlines"/> property. /// Defines the <see cref="Inlines"/> property.
/// </summary> /// </summary>
@ -339,6 +345,15 @@ namespace Avalonia.Controls
set => SetValue(TextDecorationsProperty, value); set => SetValue(TextDecorationsProperty, value);
} }
/// <summary>
/// Gets or sets the font features.
/// </summary>
public FontFeatureCollection? FontFeatures
{
get => GetValue(FontFeaturesProperty);
set => SetValue(FontFeaturesProperty, value);
}
/// <summary> /// <summary>
/// Gets or sets the inlines. /// Gets or sets the inlines.
/// </summary> /// </summary>
@ -635,6 +650,7 @@ namespace Avalonia.Controls
var defaultProperties = new GenericTextRunProperties( var defaultProperties = new GenericTextRunProperties(
typeface, typeface,
FontFeatures,
FontSize, FontSize,
TextDecorations, TextDecorations,
Foreground); Foreground);
@ -806,6 +822,7 @@ namespace Avalonia.Controls
case nameof(Text): case nameof(Text):
case nameof(TextDecorations): case nameof(TextDecorations):
case nameof(FontFeatures):
case nameof(Foreground): case nameof(Foreground):
{ {
InvalidateTextLayout(); InvalidateTextLayout();

4
src/Avalonia.Controls/TextBox.cs

@ -2221,7 +2221,7 @@ namespace Avalonia.Controls
{ {
var fontSize = FontSize; var fontSize = FontSize;
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); 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 textLayout = new TextLayout(new LineTextSource(MaxLines), paragraphProperties);
var verticalSpace = GetVerticalSpaceBetweenScrollViewerAndPresenter(); var verticalSpace = GetVerticalSpaceBetweenScrollViewerAndPresenter();
@ -2237,7 +2237,7 @@ namespace Avalonia.Controls
{ {
var fontSize = FontSize; var fontSize = FontSize;
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); 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 textLayout = new TextLayout(new LineTextSource(MinLines), paragraphProperties);
var verticalSpace = GetVerticalSpaceBetweenScrollViewerAndPresenter(); var verticalSpace = GetVerticalSpaceBetweenScrollViewerAndPresenter();

25
src/Skia/Avalonia.Skia/TextShaperImpl.cs

@ -41,7 +41,7 @@ namespace Avalonia.Skia
var font = ((GlyphTypefaceImpl)typeface).Font; var font = ((GlyphTypefaceImpl)typeface).Font;
font.Shape(buffer); font.Shape(buffer, GetFeatures(options));
if (buffer.Direction == Direction.RightToLeft) if (buffer.Direction == Direction.RightToLeft)
{ {
@ -176,5 +176,28 @@ namespace Avalonia.Skia
// should never happen // should never happen
throw new InvalidOperationException("Memory not backed by string, array or manager"); 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<Feature>();
}
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;
}
} }
} }

25
src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs

@ -42,7 +42,7 @@ namespace Avalonia.Direct2D1.Media
var font = ((GlyphTypefaceImpl)typeface).Font; var font = ((GlyphTypefaceImpl)typeface).Font;
font.Shape(buffer); font.Shape(buffer, GetFeatures(options));
if (buffer.Direction == Direction.RightToLeft) if (buffer.Direction == Direction.RightToLeft)
{ {
@ -177,5 +177,28 @@ namespace Avalonia.Direct2D1.Media
// should never happen // should never happen
throw new InvalidOperationException("Memory not backed by string, array or manager"); 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<Feature>();
}
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;
}
} }
} }

Loading…
Cancel
Save