Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to configure font features #14157

Merged
merged 2 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions samples/ControlCatalog/Pages/TextBlockPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,13 @@
</Span>.
</SelectableTextBlock>
</Border>
<Border>
<TextBlock FontFamily="Times New Roman">
<Run Text="ABC" FontFeatures="+c2sc, +smcp"/>
<Run Text="DEF"/>
<Run Text="0123" FontFeatures="frac"/>
</TextBlock>
</Border>
</WrapPanel>
</StackPanel>
</UserControl>
153 changes: 153 additions & 0 deletions src/Avalonia.Base/Media/FontFeature.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 10 additions & 0 deletions src/Avalonia.Base/Media/FontFeatureCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Avalonia.Collections;

namespace Avalonia.Media;

/// <summary>
/// List of font feature settings
/// </summary>
public class FontFeatureCollection : AvaloniaList<FontFeature>
{
}
66 changes: 66 additions & 0 deletions src/Avalonia.Base/Media/FormattedText.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;

Expand Down Expand Up @@ -50,6 +51,7 @@ public class FormattedText
/// <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="foreground">Foreground brush used to render text.</param>
/// <param name="features">Optional list of turned on/off features.</param>
public FormattedText(
string textToFormat,
CultureInfo culture,
Expand Down Expand Up @@ -183,6 +185,7 @@ public void SetForegroundBrush(IBrush? foregroundBrush, int startIndex, int coun

var newProps = new GenericTextRunProperties(
runProps.Typeface,
runProps.FontFeatures,
runProps.FontRenderingEmSize,
runProps.TextDecorations,
foregroundBrush,
Expand All @@ -197,6 +200,62 @@ public void SetForegroundBrush(IBrush? foregroundBrush, int startIndex, int coun
}
}

/// <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>
/// Sets or changes the font family for the text object
/// </summary>
Expand Down Expand Up @@ -270,6 +329,7 @@ public void SetFontFamily(FontFamily fontFamily, int startIndex, int count)

var newProps = new GenericTextRunProperties(
new Typeface(fontFamily, oldTypeface.Style, oldTypeface.Weight),
runProps.FontFeatures,
runProps.FontRenderingEmSize,
runProps.TextDecorations,
runProps.ForegroundBrush,
Expand Down Expand Up @@ -329,6 +389,7 @@ public void SetFontSize(double emSize, int startIndex, int count)

var newProps = new GenericTextRunProperties(
runProps.Typeface,
runProps.FontFeatures,
emSize,
runProps.TextDecorations,
runProps.ForegroundBrush,
Expand Down Expand Up @@ -391,6 +452,7 @@ public void SetCulture(CultureInfo culture, int startIndex, int count)

var newProps = new GenericTextRunProperties(
runProps.Typeface,
runProps.FontFeatures,
runProps.FontRenderingEmSize,
runProps.TextDecorations,
runProps.ForegroundBrush,
Expand Down Expand Up @@ -450,6 +512,7 @@ public void SetFontWeight(FontWeight weight, int startIndex, int count)

var newProps = new GenericTextRunProperties(
new Typeface(oldTypeface.FontFamily, oldTypeface.Style, weight),
runProps.FontFeatures,
runProps.FontRenderingEmSize,
runProps.TextDecorations,
runProps.ForegroundBrush,
Expand Down Expand Up @@ -506,6 +569,7 @@ public void SetFontStyle(FontStyle style, int startIndex, int count)

var newProps = new GenericTextRunProperties(
new Typeface(oldTypeface.FontFamily, style, oldTypeface.Weight),
runProps.FontFeatures,
runProps.FontRenderingEmSize,
runProps.TextDecorations,
runProps.ForegroundBrush,
Expand Down Expand Up @@ -562,6 +626,7 @@ public void SetFontTypeface(Typeface typeface, int startIndex, int count)

var newProps = new GenericTextRunProperties(
typeface,
runProps.FontFeatures,
runProps.FontRenderingEmSize,
runProps.TextDecorations,
runProps.ForegroundBrush,
Expand Down Expand Up @@ -619,6 +684,7 @@ public void SetTextDecorations(TextDecorationCollection textDecorations, int sta

var newProps = new GenericTextRunProperties(
runProps.Typeface,
runProps.FontFeatures,
runProps.FontRenderingEmSize,
textDecorations,
runProps.ForegroundBrush,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Globalization;
using System;
using System.Collections.Generic;
using System.Globalization;

namespace Avalonia.Media.TextFormatting
{
Expand All @@ -9,9 +11,25 @@ public class GenericTextRunProperties : TextRunProperties
{
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;
Expand All @@ -21,6 +39,7 @@ public GenericTextRunProperties(Typeface typeface, double fontRenderingEmSize =
BackgroundBrush = backgroundBrush;
BaselineAlignment = baselineAlignment;
CultureInfo = cultureInfo;
FontFeatures = fontFeatures;
}

/// <inheritdoc />
Expand All @@ -38,6 +57,9 @@ public GenericTextRunProperties(Typeface typeface, double fontRenderingEmSize =
/// <inheritdoc />
public override IBrush? BackgroundBrush { get; }

/// <inheritdoc />
public override FontFeatureCollection? FontFeatures { get; }

/// <inheritdoc />
public override BaselineAlignment BaselineAlignment { get; }

Expand Down
5 changes: 3 additions & 2 deletions src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ private static RentedList<TextRun> ShapeTextRuns(IReadOnlyList<TextRun> textRuns
}

var shaperOptions = new TextShaperOptions(
properties.CachedGlyphTypeface,
properties.CachedGlyphTypeface, properties.FontFeatures,
properties.FontRenderingEmSize, shapeableRun.BidiLevel, properties.CultureInfo,
paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);

Expand Down Expand Up @@ -976,7 +976,8 @@ internal static ShapedTextRun CreateSymbol(TextRun textRun, FlowDirection flowDi

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);

Expand Down
Loading