diff --git a/src/SixLabors.Fonts/TextJustification.cs b/src/SixLabors.Fonts/TextJustification.cs new file mode 100644 index 00000000..d9ea1be9 --- /dev/null +++ b/src/SixLabors.Fonts/TextJustification.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.Fonts +{ + /// + /// Text justification modes. + /// + public enum TextJustification + { + /// + /// No justification + /// + None = 0, + + /// + /// The text is justified by adding space between words (effectively varying word-spacing), + /// which is most appropriate for languages that separate words using spaces, like English or Korean. + /// + InterWord, + + /// + /// The text is justified by adding space between characters (effectively varying letter-spacing), + /// which is most appropriate for languages like Japanese. + /// + InterCharacter + } +} diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 42f7c0e5..2b3affbf 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -729,10 +729,7 @@ private static TextBox BreakLines( continue; } - // Calculate the advance for the current codepoint. CodePoint codePoint = codePointEnumerator.Current; - GlyphMetrics glyph = metrics[0]; - float glyphAdvance = isHorizontal ? glyph.AdvanceWidth : glyph.AdvanceHeight; if (CodePoint.IsVariationSelector(codePoint)) { codePointIndex++; @@ -740,6 +737,9 @@ private static TextBox BreakLines( continue; } + // Calculate the advance for the current codepoint. + GlyphMetrics glyph = metrics[0]; + float glyphAdvance = isHorizontal ? glyph.AdvanceWidth : glyph.AdvanceHeight; if (CodePoint.IsTabulation(codePoint)) { glyphAdvance *= options.TabWidth; @@ -797,7 +797,7 @@ private static TextBox BreakLines( // Mandatory wrap at index. if (currentLineBreak.PositionWrap == codePointIndex && currentLineBreak.Required) { - textLines.Add(textLine.BidiReOrder()); + textLines.Add(textLine.Finalize()); glyphCount += textLine.Count; textLine = new(); lineAdvance = 0; @@ -808,7 +808,7 @@ private static TextBox BreakLines( // Forced wordbreak if (breakAll) { - textLines.Add(textLine.BidiReOrder()); + textLines.Add(textLine.Finalize()); glyphCount += textLine.Count; textLine = new(); lineAdvance = 0; @@ -821,14 +821,14 @@ private static TextBox BreakLines( TextLine split = textLine.SplitAt(lastLineBreak, keepAll); if (split != textLine) { - textLines.Add(textLine.BidiReOrder()); + textLines.Add(textLine.Finalize()); textLine = split; lineAdvance = split.ScaledLineAdvance; } } else { - textLines.Add(textLine.BidiReOrder()); + textLines.Add(textLine.Finalize()); glyphCount += textLine.Count; textLine = new(); lineAdvance = 0; @@ -840,7 +840,7 @@ private static TextBox BreakLines( TextLine split = textLine.SplitAt(currentLineBreak, keepAll); if (split != textLine) { - textLines.Add(textLine.BidiReOrder()); + textLines.Add(textLine.Finalize()); textLine = split; lineAdvance = split.ScaledLineAdvance; } @@ -851,7 +851,7 @@ private static TextBox BreakLines( TextLine split = textLine.SplitAt(lastLineBreak, keepAll); if (split != textLine) { - textLines.Add(textLine.BidiReOrder()); + textLines.Add(textLine.Finalize()); textLine = split; lineAdvance = split.ScaledLineAdvance; } @@ -931,15 +931,22 @@ private static TextBox BreakLines( // Add the final line. if (textLine.Count > 0) { - textLines.Add(textLine.BidiReOrder()); + textLines.Add(textLine.Finalize()); } - return new TextBox(textLines); + return new TextBox(options, textLines); } internal sealed class TextBox { - public TextBox(IReadOnlyList textLines) => this.TextLines = textLines; + public TextBox(TextOptions options, IReadOnlyList textLines) + { + this.TextLines = textLines; + for (int i = 0; i < this.TextLines.Count - 1; i++) + { + this.TextLines[i].Justify(options); + } + } public IReadOnlyList TextLines { get; } @@ -1090,7 +1097,81 @@ public TextLine SplitAt(LineBreak lineBreak, bool keepAll) return result; } - public TextLine BidiReOrder() + public TextLine Finalize() => this.BidiReOrder(); + + public void Justify(TextOptions options) + { + if (options.WrappingLength == -1F || options.TextJustification == TextJustification.None) + { + return; + } + + if (this.ScaledLineAdvance == 0) + { + return; + } + + float delta = (options.WrappingLength / options.Dpi) - this.ScaledLineAdvance; + if (delta <= 0) + { + return; + } + + // Increase the advance for all non zero-width glyphs but the last. + if (options.TextJustification == TextJustification.InterCharacter) + { + int nonZeroCount = 0; + for (int i = 0; i < this.data.Count - 1; i++) + { + GlyphLayoutData glyph = this.data[i]; + if (!CodePoint.IsZeroWidthJoiner(glyph.CodePoint) && !CodePoint.IsZeroWidthNonJoiner(glyph.CodePoint)) + { + nonZeroCount++; + } + } + + float padding = delta / nonZeroCount; + for (int i = 0; i < this.data.Count - 1; i++) + { + GlyphLayoutData glyph = this.data[i]; + if (!CodePoint.IsZeroWidthJoiner(glyph.CodePoint) && !CodePoint.IsZeroWidthNonJoiner(glyph.CodePoint)) + { + glyph.ScaledAdvance += padding; + this.data[i] = glyph; + } + } + + return; + } + + // Increase the advance for all spaces but the last. + if (options.TextJustification == TextJustification.InterWord) + { + // Count all the whitespace characters. + int whiteSpaceCount = 0; + for (int i = 0; i < this.data.Count - 1; i++) + { + GlyphLayoutData glyph = this.data[i]; + if (CodePoint.IsWhiteSpace(glyph.CodePoint)) + { + whiteSpaceCount++; + } + } + + float padding = delta / whiteSpaceCount; + for (int i = 0; i < this.data.Count - 1; i++) + { + GlyphLayoutData glyph = this.data[i]; + if (CodePoint.IsWhiteSpace(glyph.CodePoint)) + { + glyph.ScaledAdvance += padding; + this.data[i] = glyph; + } + } + } + } + + private TextLine BidiReOrder() { // Build up the collection of ordered runs. BidiRun run = this.data[0].BidiRun; @@ -1234,7 +1315,7 @@ private static OrderedBidiRun LinearReOrder(OrderedBidiRun? line) } [DebuggerDisplay("{DebuggerDisplay,nq}")] - internal readonly struct GlyphLayoutData + internal struct GlyphLayoutData { public GlyphLayoutData( IReadOnlyList metrics, @@ -1266,7 +1347,7 @@ public GlyphLayoutData( public float PointSize { get; } - public float ScaledAdvance { get; } + public float ScaledAdvance { get; set; } public float ScaledLineHeight { get; } diff --git a/src/SixLabors.Fonts/TextMeasurer.cs b/src/SixLabors.Fonts/TextMeasurer.cs index 590119ca..c5b79264 100644 --- a/src/SixLabors.Fonts/TextMeasurer.cs +++ b/src/SixLabors.Fonts/TextMeasurer.cs @@ -14,7 +14,7 @@ namespace SixLabors.Fonts public static class TextMeasurer { /// - /// Measures the text. + /// Measures the size of the text in pixel units. /// /// The text. /// The text shaping options. @@ -23,7 +23,7 @@ public static FontRectangle Measure(string text, TextOptions options) => Measure(text.AsSpan(), options); /// - /// Measures the text. + /// Measures the size of the text in pixel units. /// /// The text. /// The text shaping options. @@ -32,7 +32,7 @@ public static FontRectangle Measure(ReadOnlySpan text, TextOptions options => GetSize(TextLayout.GenerateLayout(text, options), options.Dpi); /// - /// Measures the text. + /// Measures the text bounds in pixel units. /// /// The text. /// The text shaping options. @@ -41,7 +41,7 @@ public static FontRectangle MeasureBounds(string text, TextOptions options) => MeasureBounds(text.AsSpan(), options); /// - /// Measures the text. + /// Measures the text bounds in pixel units. /// /// The text. /// The text shaping options. @@ -50,7 +50,27 @@ public static FontRectangle MeasureBounds(ReadOnlySpan text, TextOptions o => GetBounds(TextLayout.GenerateLayout(text, options), options.Dpi); /// - /// Measures the character bounds of the text. For each control character the list contains a null element. + /// Measures the size of each character of the text in pixel units. + /// + /// The text. + /// The text shaping options. + /// The list of character dimensions of the text if it was to be rendered. + /// Whether any of the characters had non-empty dimensions. + public static bool TryMeasureCharacterDimensions(string text, TextOptions options, out GlyphBounds[] characterBounds) + => TryMeasureCharacterDimensions(text.AsSpan(), options, out characterBounds); + + /// + /// Measures the size of each character of the text in pixel units. + /// + /// The text. + /// The text shaping options. + /// The list of character dimensions of the text if it was to be rendered. + /// Whether any of the characters had non-empty dimensions. + public static bool TryMeasureCharacterDimensions(ReadOnlySpan text, TextOptions options, out GlyphBounds[] characterBounds) + => TryGetCharacterDimensions(TextLayout.GenerateLayout(text, options), options.Dpi, out characterBounds); + + /// + /// Measures the character bounds of the text in pixel units. /// /// The text. /// The text shaping options. @@ -60,7 +80,7 @@ public static bool TryMeasureCharacterBounds(string text, TextOptions options, o => TryMeasureCharacterBounds(text.AsSpan(), options, out characterBounds); /// - /// Measures the character bounds of the text. For each control character the list contains a null element. + /// Measures the character bounds of the text in pixel units. /// /// The text. /// The text shaping options. @@ -178,6 +198,28 @@ internal static FontRectangle GetBounds(IReadOnlyList glyphLayouts, return FontRectangle.FromLTRB(left, top, right, bottom); } + internal static bool TryGetCharacterDimensions(IReadOnlyList glyphLayouts, float dpi, out GlyphBounds[] characterBounds) + { + bool hasSize = false; + if (glyphLayouts.Count == 0) + { + characterBounds = Array.Empty(); + return hasSize; + } + + var characterBoundsList = new GlyphBounds[glyphLayouts.Count]; + for (int i = 0; i < glyphLayouts.Count; i++) + { + GlyphLayout glyph = glyphLayouts[i]; + FontRectangle bounds = new(0, 0, glyph.Width * dpi, glyph.Height * dpi); + hasSize |= bounds.Width > 0 || bounds.Height > 0; + characterBoundsList[i] = new GlyphBounds(glyph.Glyph.GlyphMetrics.CodePoint, bounds); + } + + characterBounds = characterBoundsList; + return hasSize; + } + internal static bool TryGetCharacterBounds(IReadOnlyList glyphLayouts, float dpi, out GlyphBounds[] characterBounds) { bool hasSize = false; @@ -191,8 +233,9 @@ internal static bool TryGetCharacterBounds(IReadOnlyList glyphLayou for (int i = 0; i < glyphLayouts.Count; i++) { GlyphLayout g = glyphLayouts[i]; - hasSize |= !g.IsStartOfLine; - characterBoundsList[i] = new GlyphBounds(g.Glyph.GlyphMetrics.CodePoint, g.BoundingBox(dpi)); + FontRectangle bounds = g.BoundingBox(dpi); + hasSize |= bounds.Width > 0 || bounds.Height > 0; + characterBoundsList[i] = new GlyphBounds(g.Glyph.GlyphMetrics.CodePoint, bounds); } characterBounds = characterBoundsList; diff --git a/src/SixLabors.Fonts/TextOptions.cs b/src/SixLabors.Fonts/TextOptions.cs index a46b01fe..f11ad043 100644 --- a/src/SixLabors.Fonts/TextOptions.cs +++ b/src/SixLabors.Fonts/TextOptions.cs @@ -42,6 +42,7 @@ public TextOptions(TextOptions options) this.WordBreaking = options.WordBreaking; this.TextDirection = options.TextDirection; this.TextAlignment = options.TextAlignment; + this.TextJustification = options.TextJustification; this.HorizontalAlignment = options.HorizontalAlignment; this.VerticalAlignment = options.VerticalAlignment; this.LayoutMode = options.LayoutMode; @@ -152,6 +153,11 @@ public float LineSpacing /// public TextAlignment TextAlignment { get; set; } + /// + /// Gets or sets the justification of the text within the box. + /// + public TextJustification TextJustification { get; set; } + /// /// Gets or sets the horizontal alignment of the text box. /// diff --git a/src/SixLabors.Fonts/Unicode/UnicodeTrie.cs b/src/SixLabors.Fonts/Unicode/UnicodeTrie.cs index ceabc0f7..21342ecd 100644 --- a/src/SixLabors.Fonts/Unicode/UnicodeTrie.cs +++ b/src/SixLabors.Fonts/Unicode/UnicodeTrie.cs @@ -24,7 +24,7 @@ internal sealed class UnicodeTrie public UnicodeTrie(ReadOnlySpan rawData) { - var header = MemoryMarshal.Read(rawData); + UnicodeTrieHeader header = MemoryMarshal.Read(rawData); if (!BitConverter.IsLittleEndian) { diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs index dac4ea1b..838d59dc 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs @@ -476,6 +476,204 @@ public void BuildTextRuns_PreventsOverlappingRun() Assert.Equal(76, runs[1].End); } + [Theory] + [InlineData(TextDirection.LeftToRight)] + [InlineData(TextDirection.RightToLeft)] + public void TextJustification_InterCharacter_Horizontal(TextDirection direction) + { + const string text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc ornare maximus vehicula. Duis nisi velit, dictum id mauris vitae, lobortis pretium quam. Quisque sed nisi pulvinar, consequat justo id, feugiat leo. Cras eu elementum dui."; + const float wrappingLength = 400; + const float pointSize = 20; + Font font = CreateFont(text, pointSize); + TextOptions options = new(font) + { + TextDirection = direction, + WrappingLength = wrappingLength, + TextJustification = TextJustification.InterCharacter + }; + + // Collect the first line so we can compare it to the target wrapping length. + IReadOnlyList justifiedGlyphs = TextLayout.GenerateLayout(text.AsSpan(), options); + IReadOnlyList justifiedLine = CollectFirstLine(justifiedGlyphs); + TextMeasurer.TryGetCharacterDimensions(justifiedLine, options.Dpi, out GlyphBounds[] justifiedCharacterBounds); + + Assert.Equal(wrappingLength, justifiedCharacterBounds.Sum(x => x.Bounds.Width), 4); + + // Now compare character widths. + options.TextJustification = TextJustification.None; + IReadOnlyList glyphs = TextLayout.GenerateLayout(text.AsSpan(), options); + IReadOnlyList line = CollectFirstLine(glyphs); + TextMeasurer.TryGetCharacterDimensions(line, options.Dpi, out GlyphBounds[] characterBounds); + + // All but the last justified character advance should be greater than the + // corresponding character advance. + for (int i = 0; i < characterBounds.Length; i++) + { + if (i == characterBounds.Length - 1) + { + Assert.Equal(justifiedCharacterBounds[i].Bounds.Width, characterBounds[i].Bounds.Width); + } + else + { + Assert.True(justifiedCharacterBounds[i].Bounds.Width > characterBounds[i].Bounds.Width); + } + } + } + + [Theory] + [InlineData(TextDirection.LeftToRight)] + [InlineData(TextDirection.RightToLeft)] + public void TextJustification_InterWord_Horizontal(TextDirection direction) + { + const string text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc ornare maximus vehicula. Duis nisi velit, dictum id mauris vitae, lobortis pretium quam. Quisque sed nisi pulvinar, consequat justo id, feugiat leo. Cras eu elementum dui."; + const float wrappingLength = 400; + const float pointSize = 20; + Font font = CreateFont(text, pointSize); + TextOptions options = new(font) + { + TextDirection = direction, + WrappingLength = wrappingLength, + TextJustification = TextJustification.InterWord + }; + + // Collect the first line so we can compare it to the target wrapping length. + IReadOnlyList justifiedGlyphs = TextLayout.GenerateLayout(text.AsSpan(), options); + IReadOnlyList justifiedLine = CollectFirstLine(justifiedGlyphs); + TextMeasurer.TryGetCharacterDimensions(justifiedLine, options.Dpi, out GlyphBounds[] justifiedCharacterBounds); + + Assert.Equal(wrappingLength, justifiedCharacterBounds.Sum(x => x.Bounds.Width), 4); + + // Now compare character widths. + options.TextJustification = TextJustification.None; + IReadOnlyList glyphs = TextLayout.GenerateLayout(text.AsSpan(), options); + IReadOnlyList line = CollectFirstLine(glyphs); + TextMeasurer.TryGetCharacterDimensions(line, options.Dpi, out GlyphBounds[] characterBounds); + + // All but the last justified whitespace character advance should be greater than the + // corresponding character advance. + for (int i = 0; i < characterBounds.Length; i++) + { + if (CodePoint.IsWhiteSpace(characterBounds[i].Codepoint) && i != characterBounds.Length - 1) + { + Assert.True(justifiedCharacterBounds[i].Bounds.Width > characterBounds[i].Bounds.Width); + } + else + { + Assert.Equal(justifiedCharacterBounds[i].Bounds.Width, characterBounds[i].Bounds.Width); + } + } + } + + [Theory] + [InlineData(TextDirection.LeftToRight)] + [InlineData(TextDirection.RightToLeft)] + public void TextJustification_InterCharacter_Vertical(TextDirection direction) + { + const string text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc ornare maximus vehicula. Duis nisi velit, dictum id mauris vitae, lobortis pretium quam. Quisque sed nisi pulvinar, consequat justo id, feugiat leo. Cras eu elementum dui."; + const float wrappingLength = 400; + const float pointSize = 20; + Font font = CreateFont(text, pointSize); + TextOptions options = new(font) + { + LayoutMode = LayoutMode.VerticalLeftRight, + TextDirection = direction, + WrappingLength = wrappingLength, + TextJustification = TextJustification.InterCharacter + }; + + // Collect the first line so we can compare it to the target wrapping length. + IReadOnlyList justifiedGlyphs = TextLayout.GenerateLayout(text.AsSpan(), options); + IReadOnlyList justifiedLine = CollectFirstLine(justifiedGlyphs); + TextMeasurer.TryGetCharacterDimensions(justifiedLine, options.Dpi, out GlyphBounds[] justifiedCharacterBounds); + + Assert.Equal(wrappingLength, justifiedCharacterBounds.Sum(x => x.Bounds.Height), 4); + + // Now compare character widths. + options.TextJustification = TextJustification.None; + IReadOnlyList glyphs = TextLayout.GenerateLayout(text.AsSpan(), options); + IReadOnlyList line = CollectFirstLine(glyphs); + TextMeasurer.TryGetCharacterDimensions(line, options.Dpi, out GlyphBounds[] characterBounds); + + // All but the last justified character advance should be greater than the + // corresponding character advance. + for (int i = 0; i < characterBounds.Length; i++) + { + if (i == characterBounds.Length - 1) + { + Assert.Equal(justifiedCharacterBounds[i].Bounds.Height, characterBounds[i].Bounds.Height); + } + else + { + Assert.True(justifiedCharacterBounds[i].Bounds.Height > characterBounds[i].Bounds.Height); + } + } + } + + [Theory] + [InlineData(TextDirection.LeftToRight)] + [InlineData(TextDirection.RightToLeft)] + public void TextJustification_InterWord_Vertical(TextDirection direction) + { + const string text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc ornare maximus vehicula. Duis nisi velit, dictum id mauris vitae, lobortis pretium quam. Quisque sed nisi pulvinar, consequat justo id, feugiat leo. Cras eu elementum dui."; + const float wrappingLength = 400; + const float pointSize = 20; + Font font = CreateFont(text, pointSize); + TextOptions options = new(font) + { + LayoutMode = LayoutMode.VerticalLeftRight, + TextDirection = direction, + WrappingLength = wrappingLength, + TextJustification = TextJustification.InterWord + }; + + // Collect the first line so we can compare it to the target wrapping length. + IReadOnlyList justifiedGlyphs = TextLayout.GenerateLayout(text.AsSpan(), options); + IReadOnlyList justifiedLine = CollectFirstLine(justifiedGlyphs); + TextMeasurer.TryGetCharacterDimensions(justifiedLine, options.Dpi, out GlyphBounds[] justifiedCharacterBounds); + + Assert.Equal(wrappingLength, justifiedCharacterBounds.Sum(x => x.Bounds.Height), 4); + + // Now compare character widths. + options.TextJustification = TextJustification.None; + IReadOnlyList glyphs = TextLayout.GenerateLayout(text.AsSpan(), options); + IReadOnlyList line = CollectFirstLine(glyphs); + TextMeasurer.TryGetCharacterDimensions(line, options.Dpi, out GlyphBounds[] characterBounds); + + // All but the last justified whitespace character advance should be greater than the + // corresponding character advance. + for (int i = 0; i < characterBounds.Length; i++) + { + if (CodePoint.IsWhiteSpace(characterBounds[i].Codepoint) && i != characterBounds.Length - 1) + { + Assert.True(justifiedCharacterBounds[i].Bounds.Height > characterBounds[i].Bounds.Height); + } + else + { + Assert.Equal(justifiedCharacterBounds[i].Bounds.Height, characterBounds[i].Bounds.Height); + } + } + } + + private static IReadOnlyList CollectFirstLine(IReadOnlyList glyphs) + { + List line = new() + { + glyphs[0] + }; + + for (int i = 1; i < glyphs.Count; i++) + { + if (glyphs[i].IsStartOfLine) + { + break; + } + + line.Add(glyphs[i]); + } + + return line; + } + #if OS_WINDOWS [Fact] public FontRectangle BenchmarkTest() diff --git a/tests/SixLabors.Fonts.Tests/TextOptionsTests.cs b/tests/SixLabors.Fonts.Tests/TextOptionsTests.cs index de15425c..1e32b603 100644 --- a/tests/SixLabors.Fonts.Tests/TextOptionsTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextOptionsTests.cs @@ -293,6 +293,14 @@ public void DefaultTextOptionsLineSpacing() Assert.Equal(expected, this.clonedTextOptions.LineSpacing); } + [Fact] + public void DefaultTextOptionsTextJustification() + { + const TextJustification expected = TextJustification.None; + Assert.Equal(expected, this.newTextOptions.TextJustification); + Assert.Equal(expected, this.clonedTextOptions.TextJustification); + } + [Fact] public void NonDefaultClone() { @@ -324,15 +332,17 @@ public void NonDefaultClone() public void CloneIsDeep() { var expected = new TextOptions(this.fakeFont); - TextOptions actual = new(expected); - - actual.KerningMode = KerningMode.None; - actual.Dpi = 46F; - actual.HorizontalAlignment = HorizontalAlignment.Center; - actual.TabWidth = 3F; - actual.LineSpacing = 2F; - actual.VerticalAlignment = VerticalAlignment.Bottom; - actual.WrappingLength = 42F; + TextOptions actual = new(expected) + { + KerningMode = KerningMode.None, + Dpi = 46F, + HorizontalAlignment = HorizontalAlignment.Center, + TabWidth = 3F, + LineSpacing = 2F, + VerticalAlignment = VerticalAlignment.Bottom, + TextJustification = TextJustification.InterCharacter, + WrappingLength = 42F + }; Assert.NotEqual(expected.KerningMode, actual.KerningMode); Assert.NotEqual(expected.Dpi, actual.Dpi); @@ -341,6 +351,7 @@ public void CloneIsDeep() Assert.NotEqual(expected.TabWidth, actual.TabWidth); Assert.NotEqual(expected.VerticalAlignment, actual.VerticalAlignment); Assert.NotEqual(expected.WrappingLength, actual.WrappingLength); + Assert.NotEqual(expected.TextJustification, actual.TextJustification); } private static void VerifyPropertyDefault(TextOptions options) @@ -351,6 +362,7 @@ private static void VerifyPropertyDefault(TextOptions options) Assert.Equal(HorizontalAlignment.Left, options.HorizontalAlignment); Assert.Equal(VerticalAlignment.Top, options.VerticalAlignment); Assert.Equal(TextAlignment.Start, options.TextAlignment); + Assert.Equal(TextJustification.None, options.TextJustification); Assert.Equal(TextDirection.Auto, options.TextDirection); Assert.Equal(LayoutMode.HorizontalTopBottom, options.LayoutMode); Assert.Equal(1, options.LineSpacing);