Browse Source

Perf: pass FontManager and typefaces around during text layout

pull/10047/head
Julien Lebosquain 4 years ago
parent
commit
63f6ef63af
  1. 2
      src/Avalonia.Base/Media/FontManager.cs
  2. 2
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  3. 54
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  4. 37
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  5. 7
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  6. 9
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  7. 5
      src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs

2
src/Avalonia.Base/Media/FontManager.cs

@ -132,7 +132,7 @@ namespace Avalonia.Media
{ {
typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch); typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch);
var glyphTypeface = typeface.GlyphTypeface; var glyphTypeface = GetOrAddGlyphTypeface(typeface);
if(glyphTypeface.TryGetGlyph((uint)codepoint, out _)){ if(glyphTypeface.TryGetGlyph((uint)codepoint, out _)){
return true; return true;

2
src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs

@ -14,7 +14,7 @@ namespace Avalonia.Media.TextFormatting
{ {
ShapedBuffer = shapedBuffer; ShapedBuffer = shapedBuffer;
Properties = properties; Properties = properties;
TextMetrics = new TextMetrics(properties.Typeface.GlyphTypeface, properties.FontRenderingEmSize); TextMetrics = new TextMetrics(properties.CachedGlyphTypeface, properties.FontRenderingEmSize);
} }
public bool IsReversed { get; private set; } public bool IsReversed { get; private set; }

54
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@ -69,6 +69,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="text">The characters to create text runs from.</param> /// <param name="text">The characters to create text runs from.</param>
/// <param name="defaultProperties">The default text run properties.</param> /// <param name="defaultProperties">The default text run properties.</param>
/// <param name="biDiLevel">The bidi level of the run.</param> /// <param name="biDiLevel">The bidi level of the run.</param>
/// <param name="fontManager">The font manager to use.</param>
/// <param name="previousProperties"></param> /// <param name="previousProperties"></param>
/// <returns>A list of shapeable text runs.</returns> /// <returns>A list of shapeable text runs.</returns>
private static UnshapedTextRun CreateShapeableRun(ReadOnlyMemory<char> text, private static UnshapedTextRun CreateShapeableRun(ReadOnlyMemory<char> text,
@ -76,31 +77,32 @@ namespace Avalonia.Media.TextFormatting
ref TextRunProperties? previousProperties) ref TextRunProperties? previousProperties)
{ {
var defaultTypeface = defaultProperties.Typeface; var defaultTypeface = defaultProperties.Typeface;
var currentTypeface = defaultTypeface; var defaultGlyphTypeface = defaultProperties.CachedGlyphTypeface;
var previousTypeface = previousProperties?.Typeface; var previousTypeface = previousProperties?.Typeface;
var previousGlyphTypeface = previousProperties?.CachedGlyphTypeface;
var textSpan = text.Span; var textSpan = text.Span;
if (TryGetShapeableLength(textSpan, currentTypeface, null, out var count, out var script)) if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out var count, out var script))
{ {
if (script == Script.Common && previousTypeface is not null) if (script == Script.Common && previousGlyphTypeface is not null)
{ {
if (TryGetShapeableLength(textSpan, previousTypeface.Value, null, out var fallbackCount, out _)) if (TryGetShapeableLength(textSpan, previousGlyphTypeface, null, out var fallbackCount, out _))
{ {
return new UnshapedTextRun(text.Slice(0, fallbackCount), return new UnshapedTextRun(text.Slice(0, fallbackCount),
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel);
} }
} }
return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface), return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(defaultTypeface),
biDiLevel); biDiLevel);
} }
if (previousTypeface is not null) if (previousGlyphTypeface is not null)
{ {
if (TryGetShapeableLength(textSpan, previousTypeface.Value, defaultTypeface, out count, out _)) if (TryGetShapeableLength(textSpan, previousGlyphTypeface, defaultGlyphTypeface, out count, out _))
{ {
return new UnshapedTextRun(text.Slice(0, count), return new UnshapedTextRun(text.Slice(0, count),
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel);
} }
} }
@ -124,25 +126,23 @@ namespace Avalonia.Media.TextFormatting
var matchFound = var matchFound =
fontManager.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, fontManager.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight,
defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo, defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo,
out currentTypeface); out var fallbackTypeface);
if (matchFound && TryGetShapeableLength(textSpan, currentTypeface, defaultTypeface, out count, out _)) var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface);
if (matchFound && TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count, out _))
{ {
//Fallback found //Fallback found
return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface), return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface),
biDiLevel); biDiLevel);
} }
// no fallback found // no fallback found
currentTypeface = defaultTypeface;
var glyphTypeface = currentTypeface.GlyphTypeface;
var enumerator = new GraphemeEnumerator(textSpan); var enumerator = new GraphemeEnumerator(textSpan);
while (enumerator.MoveNext(out var grapheme)) while (enumerator.MoveNext(out var grapheme))
{ {
if (!grapheme.FirstCodepoint.IsWhiteSpace && glyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _)) if (!grapheme.FirstCodepoint.IsWhiteSpace && defaultGlyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _))
{ {
break; break;
} }
@ -157,15 +157,15 @@ namespace Avalonia.Media.TextFormatting
/// Tries to get a shapeable length that is supported by the specified typeface. /// Tries to get a shapeable length that is supported by the specified typeface.
/// </summary> /// </summary>
/// <param name="text">The characters to shape.</param> /// <param name="text">The characters to shape.</param>
/// <param name="typeface">The typeface that is used to find matching characters.</param> /// <param name="glyphTypeface">The typeface that is used to find matching characters.</param>
/// <param name="defaultTypeface"></param> /// <param name="defaultGlyphTypeface">The default typeface.</param>
/// <param name="length">The shapeable length.</param> /// <param name="length">The shapeable length.</param>
/// <param name="script"></param> /// <param name="script"></param>
/// <returns></returns> /// <returns></returns>
internal static bool TryGetShapeableLength( internal static bool TryGetShapeableLength(
ReadOnlySpan<char> text, ReadOnlySpan<char> text,
Typeface typeface, IGlyphTypeface glyphTypeface,
Typeface? defaultTypeface, IGlyphTypeface? defaultGlyphTypeface,
out int length, out int length,
out Script script) out Script script)
{ {
@ -177,22 +177,22 @@ namespace Avalonia.Media.TextFormatting
return false; return false;
} }
var font = typeface.GlyphTypeface;
var defaultFont = defaultTypeface?.GlyphTypeface;
var enumerator = new GraphemeEnumerator(text); var enumerator = new GraphemeEnumerator(text);
while (enumerator.MoveNext(out var currentGrapheme)) while (enumerator.MoveNext(out var currentGrapheme))
{ {
var currentScript = currentGrapheme.FirstCodepoint.Script; var currentCodepoint = currentGrapheme.FirstCodepoint;
var currentScript = currentCodepoint.Script;
if (!currentGrapheme.FirstCodepoint.IsWhiteSpace && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) if (!currentCodepoint.IsWhiteSpace
&& defaultGlyphTypeface != null
&& defaultGlyphTypeface.TryGetGlyph(currentCodepoint, out _))
{ {
break; break;
} }
//Stop at the first missing glyph //Stop at the first missing glyph
if (!currentGrapheme.FirstCodepoint.IsBreakChar && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) if (!currentCodepoint.IsBreakChar && !glyphTypeface.TryGetGlyph(currentCodepoint, out _))
{ {
break; break;
} }

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

@ -27,6 +27,7 @@ namespace Avalonia.Media.TextFormatting
TextLineBreak? nextLineBreak = null; TextLineBreak? nextLineBreak = null;
IReadOnlyList<TextRun>? textRuns; IReadOnlyList<TextRun>? textRuns;
var objectPool = FormattingObjectPool.Instance; var objectPool = FormattingObjectPool.Instance;
var fontManager = FontManager.Current;
var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool,
out var textEndOfLine, out var textSourceLength); out var textEndOfLine, out var textSourceLength);
@ -42,7 +43,7 @@ namespace Avalonia.Media.TextFormatting
} }
else else
{ {
shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, out resolvedFlowDirection); shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager, out resolvedFlowDirection);
textRuns = shapedTextRuns; textRuns = shapedTextRuns;
if (nextLineBreak == null && textEndOfLine != null) if (nextLineBreak == null && textEndOfLine != null)
@ -72,7 +73,7 @@ namespace Avalonia.Media.TextFormatting
case TextWrapping.Wrap: case TextWrapping.Wrap:
{ {
textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth,
paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool); paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool, fontManager);
break; break;
} }
default: default:
@ -178,12 +179,13 @@ namespace Avalonia.Media.TextFormatting
/// <param name="paragraphProperties">The default paragraph properties.</param> /// <param name="paragraphProperties">The default paragraph properties.</param>
/// <param name="resolvedFlowDirection">The resolved flow direction.</param> /// <param name="resolvedFlowDirection">The resolved flow direction.</param>
/// <param name="objectPool">A pool used to get reusable formatting objects.</param> /// <param name="objectPool">A pool used to get reusable formatting objects.</param>
/// <param name="fontManager">The font manager to use.</param>
/// <returns> /// <returns>
/// A list of shaped text characters. /// A list of shaped text characters.
/// </returns> /// </returns>
private static RentedList<TextRun> ShapeTextRuns(IReadOnlyList<TextRun> textRuns, private static RentedList<TextRun> ShapeTextRuns(IReadOnlyList<TextRun> textRuns,
TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool, TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool,
out FlowDirection resolvedFlowDirection) FontManager fontManager, out FlowDirection resolvedFlowDirection)
{ {
var flowDirection = paragraphProperties.FlowDirection; var flowDirection = paragraphProperties.FlowDirection;
var shapedRuns = objectPool.TextRunLists.Rent(); var shapedRuns = objectPool.TextRunLists.Rent();
@ -223,7 +225,7 @@ namespace Avalonia.Media.TextFormatting
var processedRuns = objectPool.TextRunLists.Rent(); var processedRuns = objectPool.TextRunLists.Rent();
CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, processedRuns); CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, fontManager, processedRuns);
bidiData.Reset(); bidiData.Reset();
bidiAlgorithm.Reset(); bidiAlgorithm.Reset();
@ -240,7 +242,9 @@ namespace Avalonia.Media.TextFormatting
{ {
groupedRuns.Clear(); groupedRuns.Clear();
groupedRuns.Add(shapeableRun); groupedRuns.Add(shapeableRun);
var text = shapeableRun.Text; var text = shapeableRun.Text;
var properties = shapeableRun.Properties;
while (index + 1 < processedRuns.Count) while (index + 1 < processedRuns.Count)
{ {
@ -251,7 +255,7 @@ namespace Avalonia.Media.TextFormatting
if (shapeableRun.BidiLevel == nextRun.BidiLevel if (shapeableRun.BidiLevel == nextRun.BidiLevel
&& TryJoinContiguousMemories(text, nextRun.Text, out var joinedText) && TryJoinContiguousMemories(text, nextRun.Text, out var joinedText)
&& CanShapeTogether(shapeableRun.Properties, nextRun.Properties)) && CanShapeTogether(properties, nextRun.Properties))
{ {
groupedRuns.Add(nextRun); groupedRuns.Add(nextRun);
index++; index++;
@ -263,10 +267,10 @@ namespace Avalonia.Media.TextFormatting
break; break;
} }
var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface, var shaperOptions = new TextShaperOptions(
currentRun.Properties.FontRenderingEmSize, properties.CachedGlyphTypeface,
shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, properties.FontRenderingEmSize, shapeableRun.BidiLevel, properties.CultureInfo,
paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
ShapeTogether(groupedRuns, text, shaperOptions, shapedRuns); ShapeTogether(groupedRuns, text, shaperOptions, shapedRuns);
@ -377,10 +381,11 @@ namespace Avalonia.Media.TextFormatting
/// </summary> /// </summary>
/// <param name="textCharacters">The text characters to form <see cref="UnshapedTextRun"/> from.</param> /// <param name="textCharacters">The text characters to form <see cref="UnshapedTextRun"/> from.</param>
/// <param name="levels">The bidi levels.</param> /// <param name="levels">The bidi levels.</param>
/// <param name="fontManager">The font manager to use.</param>
/// <param name="processedRuns">A list that will be filled with the processed runs.</param> /// <param name="processedRuns">A list that will be filled with the processed runs.</param>
/// <returns></returns> /// <returns></returns>
private static void CoalesceLevels(IReadOnlyList<TextRun> textCharacters, ReadOnlySpan<sbyte> levels, private static void CoalesceLevels(IReadOnlyList<TextRun> textCharacters, ReadOnlySpan<sbyte> levels,
RentedList<TextRun> processedRuns) FontManager fontManager, RentedList<TextRun> processedRuns)
{ {
if (levels.Length == 0) if (levels.Length == 0)
{ {
@ -393,7 +398,6 @@ namespace Avalonia.Media.TextFormatting
TextRunProperties? previousProperties = null; TextRunProperties? previousProperties = null;
TextCharacters? currentRun = null; TextCharacters? currentRun = null;
ReadOnlyMemory<char> runText = default; ReadOnlyMemory<char> runText = default;
var fontManager = FontManager.Current;
for (var i = 0; i < textCharacters.Count; i++) for (var i = 0; i < textCharacters.Count; i++)
{ {
@ -638,11 +642,11 @@ namespace Avalonia.Media.TextFormatting
/// </summary> /// </summary>
/// <returns>The empty text line.</returns> /// <returns>The empty text line.</returns>
public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth,
TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool) TextParagraphProperties paragraphProperties, FontManager fontManager)
{ {
var flowDirection = paragraphProperties.FlowDirection; var flowDirection = paragraphProperties.FlowDirection;
var properties = paragraphProperties.DefaultTextRunProperties; var properties = paragraphProperties.DefaultTextRunProperties;
var glyphTypeface = properties.Typeface.GlyphTypeface; var glyphTypeface = properties.CachedGlyphTypeface;
var glyph = glyphTypeface.GetGlyph(s_empty[0]); var glyph = glyphTypeface.GetGlyph(s_empty[0]);
var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex, 0.0) }; var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex, 0.0) };
@ -666,14 +670,15 @@ namespace Avalonia.Media.TextFormatting
/// <param name="resolvedFlowDirection"></param> /// <param name="resolvedFlowDirection"></param>
/// <param name="currentLineBreak">The current line break if the line was explicitly broken.</param> /// <param name="currentLineBreak">The current line break if the line was explicitly broken.</param>
/// <param name="objectPool">A pool used to get reusable formatting objects.</param> /// <param name="objectPool">A pool used to get reusable formatting objects.</param>
/// <param name="fontManager">The font manager to use.</param>
/// <returns>The wrapped text line.</returns> /// <returns>The wrapped text line.</returns>
private static TextLineImpl PerformTextWrapping(IReadOnlyList<TextRun> textRuns, int firstTextSourceIndex, private static TextLineImpl PerformTextWrapping(IReadOnlyList<TextRun> textRuns, int firstTextSourceIndex,
double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection, double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection,
TextLineBreak? currentLineBreak, FormattingObjectPool objectPool) TextLineBreak? currentLineBreak, FormattingObjectPool objectPool, FontManager fontManager)
{ {
if (textRuns.Count == 0) if (textRuns.Count == 0)
{ {
return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties, objectPool); return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties, fontManager);
} }
if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength))
@ -869,7 +874,7 @@ namespace Avalonia.Media.TextFormatting
{ {
var textShaper = TextShaper.Current; var textShaper = TextShaper.Current;
var glyphTypeface = textRun.Properties!.Typeface.GlyphTypeface; var glyphTypeface = textRun.Properties!.CachedGlyphTypeface;
var fontRenderingEmSize = textRun.Properties.FontRenderingEmSize; var fontRenderingEmSize = textRun.Properties.FontRenderingEmSize;

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

@ -427,11 +427,12 @@ namespace Avalonia.Media.TextFormatting
private TextLine[] CreateTextLines() private TextLine[] CreateTextLines()
{ {
var objectPool = FormattingObjectPool.Instance; var objectPool = FormattingObjectPool.Instance;
var fontManager = FontManager.Current;
if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
{ {
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties, var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties,
FormattingObjectPool.Instance); fontManager);
Bounds = new Rect(0, 0, 0, textLine.Height); Bounds = new Rect(0, 0, 0, textLine.Height);
@ -458,7 +459,7 @@ namespace Avalonia.Media.TextFormatting
if (previousLine != null && previousLine.NewLineLength > 0) if (previousLine != null && previousLine.NewLineLength > 0)
{ {
var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth,
_paragraphProperties, objectPool); _paragraphProperties, fontManager);
textLines.Add(emptyTextLine); textLines.Add(emptyTextLine);
@ -517,7 +518,7 @@ namespace Avalonia.Media.TextFormatting
//Make sure the TextLayout always contains at least on empty line //Make sure the TextLayout always contains at least on empty line
if (textLines.Count == 0) if (textLines.Count == 0)
{ {
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, objectPool); var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, fontManager);
textLines.Add(textLine); textLines.Add(textLine);

9
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -1256,7 +1256,7 @@ namespace Avalonia.Media.TextFormatting
private TextLineMetrics CreateLineMetrics() private TextLineMetrics CreateLineMetrics()
{ {
var fontMetrics = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface.Metrics; var fontMetrics = _paragraphProperties.DefaultTextRunProperties.CachedGlyphTypeface.Metrics;
var fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize; var fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize;
var scale = fontRenderingEmSize / fontMetrics.DesignEmHeight; var scale = fontRenderingEmSize / fontMetrics.DesignEmHeight;
@ -1285,12 +1285,13 @@ namespace Avalonia.Media.TextFormatting
{ {
case ShapedTextRun textRun: case ShapedTextRun textRun:
{ {
var properties = textRun.Properties;
var textMetrics = var textMetrics =
new TextMetrics(textRun.Properties.Typeface.GlyphTypeface, textRun.Properties.FontRenderingEmSize); new TextMetrics(properties.CachedGlyphTypeface, properties.FontRenderingEmSize);
if (fontRenderingEmSize < textRun.Properties.FontRenderingEmSize) if (fontRenderingEmSize < properties.FontRenderingEmSize)
{ {
fontRenderingEmSize = textRun.Properties.FontRenderingEmSize; fontRenderingEmSize = properties.FontRenderingEmSize;
if (ascent > textMetrics.Ascent) if (ascent > textMetrics.Ascent)
{ {

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

@ -12,6 +12,8 @@ namespace Avalonia.Media.TextFormatting
/// </remarks> /// </remarks>
public abstract class TextRunProperties : IEquatable<TextRunProperties> public abstract class TextRunProperties : IEquatable<TextRunProperties>
{ {
private IGlyphTypeface? _cachedGlyphTypeFace;
/// <summary> /// <summary>
/// Run typeface /// Run typeface
/// </summary> /// </summary>
@ -47,6 +49,9 @@ namespace Avalonia.Media.TextFormatting
/// </summary> /// </summary>
public virtual BaselineAlignment BaselineAlignment => BaselineAlignment.Baseline; public virtual BaselineAlignment BaselineAlignment => BaselineAlignment.Baseline;
internal IGlyphTypeface CachedGlyphTypeface
=> _cachedGlyphTypeFace ??= Typeface.GlyphTypeface;
public bool Equals(TextRunProperties? other) public bool Equals(TextRunProperties? other)
{ {
if (ReferenceEquals(null, other)) if (ReferenceEquals(null, other))

Loading…
Cancel
Save