diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 6f1fa03990..ac87d521a5 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -734,10 +734,9 @@ namespace Avalonia.Media private void Set(ref T field, T value) { - if (_glyphRunImpl != null) - { - throw new InvalidOperationException("GlyphRun can't be changed after it has been initialized.'"); - } + _glyphRunImpl?.Dispose(); + + _glyphRunImpl = null; _glyphRunMetrics = null; diff --git a/src/Avalonia.Base/Media/TextAlignment.cs b/src/Avalonia.Base/Media/TextAlignment.cs index b1a394e157..94416ccde2 100644 --- a/src/Avalonia.Base/Media/TextAlignment.cs +++ b/src/Avalonia.Base/Media/TextAlignment.cs @@ -19,5 +19,28 @@ namespace Avalonia.Media /// The text is right-aligned. /// Right, + + /// + /// The beginning of the text is aligned to the edge of the available space. + /// + Start, + + /// + /// The end of the text is aligned to the edge of the available space. + /// + End, + + /// + /// Text alignment is inferred from the text content. + /// + /// + /// When the TextAlignment property is set to DetectFromContent, alignment is inferred from the text content of the control. For example, English text is left aligned, and Arabic text is right aligned. + /// + DetectFromContent, + + /// + /// Text is justified within the available space. + /// + Justify } } diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs new file mode 100644 index 0000000000..df83ada34a --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using Avalonia.Media.TextFormatting.Unicode; + +namespace Avalonia.Media.TextFormatting +{ + internal class InterWordJustification : JustificationProperties + { + public InterWordJustification(double width) + { + Width = width; + } + + public override double Width { get; } + + public override void Justify(TextLine textLine) + { + var paragraphWidth = Width; + + if (double.IsInfinity(paragraphWidth)) + { + return; + } + + if (textLine.NewLineLength > 0) + { + return; + } + + var textLineBreak = textLine.TextLineBreak; + + if (textLineBreak is not null && textLineBreak.TextEndOfLine is not null) + { + if (textLineBreak.RemainingRuns is null || textLineBreak.RemainingRuns.Count == 0) + { + return; + } + } + + var breakOportunities = new Queue(); + + foreach (var textRun in textLine.TextRuns) + { + var text = textRun.Text; + + if (text.IsEmpty) + { + continue; + } + + var start = text.Start; + + var lineBreakEnumerator = new LineBreakEnumerator(text); + + while (lineBreakEnumerator.MoveNext()) + { + var currentBreak = lineBreakEnumerator.Current; + + if (!currentBreak.Required && currentBreak.PositionWrap != text.Length) + { + breakOportunities.Enqueue(start + currentBreak.PositionMeasure); + } + } + } + + if (breakOportunities.Count == 0) + { + return; + } + + var remainingSpace = Math.Max(0, paragraphWidth - textLine.WidthIncludingTrailingWhitespace); + var spacing = remainingSpace / breakOportunities.Count; + + foreach (var textRun in textLine.TextRuns) + { + var text = textRun.Text; + + if (text.IsEmpty) + { + continue; + } + + if (textRun is ShapedTextCharacters shapedText) + { + var glyphRun = shapedText.GlyphRun; + var shapedBuffer = shapedText.ShapedBuffer; + var currentPosition = text.Start; + + while (breakOportunities.Count > 0) + { + var characterIndex = breakOportunities.Dequeue(); + + if (characterIndex < currentPosition) + { + continue; + } + + var glyphIndex = glyphRun.FindGlyphIndex(characterIndex); + var glyphInfo = shapedBuffer.GlyphInfos[glyphIndex]; + + shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, glyphInfo.GlyphCluster, glyphInfo.GlyphAdvance + spacing); + } + + glyphRun.GlyphAdvances = shapedBuffer.GlyphAdvances; + } + } + } + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/JustificationProperties.cs b/src/Avalonia.Base/Media/TextFormatting/JustificationProperties.cs new file mode 100644 index 0000000000..620ad17189 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/JustificationProperties.cs @@ -0,0 +1,16 @@ +namespace Avalonia.Media.TextFormatting +{ + public abstract class JustificationProperties + { + /// + /// Gets the width in which the range is justified. + /// + public abstract double Width { get; } + + /// + /// Justifies given text line. + /// + /// Text line to collapse. + public abstract void Justify(TextLine textLine); + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 4205268bc6..16caadb0dd 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -15,7 +15,7 @@ namespace Avalonia.Media.TextFormatting TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null) { var textWrapping = paragraphProperties.TextWrapping; - FlowDirection flowDirection; + FlowDirection resolvedFlowDirection; TextLineBreak? nextLineBreak = null; List drawableTextRuns; @@ -24,17 +24,17 @@ namespace Avalonia.Media.TextFormatting if (previousLineBreak?.RemainingRuns != null) { - flowDirection = previousLineBreak.FlowDirection; + resolvedFlowDirection = previousLineBreak.FlowDirection; drawableTextRuns = previousLineBreak.RemainingRuns.ToList(); nextLineBreak = previousLineBreak; } else { - drawableTextRuns = ShapeTextRuns(textRuns, paragraphProperties, out flowDirection); + drawableTextRuns = ShapeTextRuns(textRuns, paragraphProperties, out resolvedFlowDirection); if (nextLineBreak == null && textEndOfLine != null) { - nextLineBreak = new TextLineBreak(textEndOfLine, flowDirection); + nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection); } } @@ -45,7 +45,7 @@ namespace Avalonia.Media.TextFormatting case TextWrapping.NoWrap: { textLine = new TextLineImpl(drawableTextRuns, firstTextSourceIndex, textSourceLength, - paragraphWidth, paragraphProperties, flowDirection, nextLineBreak); + paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); textLine.FinalizeLine(); @@ -55,7 +55,7 @@ namespace Avalonia.Media.TextFormatting case TextWrapping.Wrap: { textLine = PerformTextWrapping(drawableTextRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties, - flowDirection, nextLineBreak); + resolvedFlowDirection, nextLineBreak); break; } default: @@ -404,9 +404,9 @@ namespace Avalonia.Media.TextFormatting { endOfLine = textEndOfLine; - textRuns.Add(textRun); + textSourceLength += textEndOfLine.TextSourceLength; - textSourceLength += textRun.TextSourceLength; + textRuns.Add(textRun); break; } @@ -431,9 +431,9 @@ namespace Avalonia.Media.TextFormatting break; } - case DrawableTextRun drawableTextRun: + default: { - textRuns.Add(drawableTextRun); + textRuns.Add(textRun); break; } } @@ -552,11 +552,11 @@ namespace Avalonia.Media.TextFormatting /// The first text source index. /// The paragraph width. /// The text paragraph properties. - /// + /// /// The current line break if the line was explicitly broken. /// The wrapped text line. private static TextLineImpl PerformTextWrapping(List textRuns, int firstTextSourceIndex, - double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection flowDirection, + double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection, TextLineBreak? currentLineBreak) { if(textRuns.Count == 0) @@ -684,16 +684,16 @@ namespace Avalonia.Media.TextFormatting var remainingCharacters = splitResult.Second; var lineBreak = remainingCharacters?.Count > 0 ? - new TextLineBreak(currentLineBreak?.TextEndOfLine, flowDirection, remainingCharacters) : + new TextLineBreak(currentLineBreak?.TextEndOfLine, resolvedFlowDirection, remainingCharacters) : null; if (lineBreak is null && currentLineBreak?.TextEndOfLine != null) { - lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, flowDirection); + lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection); } var textLine = new TextLineImpl(splitResult.First, firstTextSourceIndex, measuredLength, - paragraphWidth, paragraphProperties, flowDirection, + paragraphWidth, paragraphProperties, resolvedFlowDirection, lineBreak); return textLine.FinalizeLine(); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 4f7c43a6d1..f3af240c58 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -439,7 +439,7 @@ namespace Avalonia.Media.TextFormatting var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth, _paragraphProperties, previousLine?.TextLineBreak); - if(textLine == null || textLine.Length == 0) + if(textLine == null || textLine.Length == 0 || textLine.TextRuns.Count == 0 && textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph) { if(previousLine != null && previousLine.NewLineLength > 0) { @@ -501,6 +501,35 @@ namespace Avalonia.Media.TextFormatting Bounds = new Rect(left, 0, width, height); + if(_paragraphProperties.TextAlignment == TextAlignment.Justify) + { + var whitespaceWidth = 0d; + + foreach (var line in textLines) + { + var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace; + + if(lineWhitespaceWidth > whitespaceWidth) + { + whitespaceWidth = lineWhitespaceWidth; + } + } + + var justificationWidth = width - whitespaceWidth; + + if(justificationWidth > 0) + { + var justificationProperties = new InterWordJustification(justificationWidth); + + for (var i = 0; i < textLines.Count - 1; i++) + { + var line = textLines[i]; + + line.Justify(justificationProperties); + } + } + } + return textLines; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLine.cs b/src/Avalonia.Base/Media/TextFormatting/TextLine.cs index 1f69c15acc..c8a23097db 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLine.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLine.cs @@ -15,9 +15,15 @@ namespace Avalonia.Media.TextFormatting /// The contained text runs. /// public abstract IReadOnlyList TextRuns { get; } - + + /// + /// Gets the first TextSource position of the current line. + /// public abstract int FirstTextSourceIndex { get; } + /// + /// Gets the total number of TextSource positions of the current line. + /// public abstract int Length { get; } /// @@ -56,7 +62,7 @@ namespace Avalonia.Media.TextFormatting /// Gets a value that indicates whether content of the line overflows the specified paragraph width. /// /// - /// true, it the line overflows the specified paragraph width; otherwise, false. + /// true, the line overflows the specified paragraph width; otherwise, false. /// public abstract bool HasOverflowed { get; } @@ -75,7 +81,7 @@ namespace Avalonia.Media.TextFormatting /// The number of newline characters. /// public abstract int NewLineLength { get; } - + /// /// Gets the distance that black pixels extend beyond the bottom alignment edge of a line. /// @@ -149,6 +155,15 @@ namespace Avalonia.Media.TextFormatting /// public abstract TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList); + /// + /// Create a justified line based on justification text properties. + /// + /// An object that represent the justification text properties. + /// + /// A value that represents a justified line that can be displayed. + /// + public abstract void Justify(JustificationProperties justificationProperties); + /// /// Gets the character hit corresponding to the specified distance from the beginning of the line. /// @@ -192,50 +207,5 @@ namespace Avalonia.Media.TextFormatting /// number of characters of the specified range /// an array of bounding rectangles. public abstract IReadOnlyList GetTextBounds(int firstTextSourceCharacterIndex, int textLength); - - /// - /// Gets the text line offset x. - /// - /// The line width. - /// The paragraph width including whitespace. - /// The paragraph width. - /// The text alignment. - /// The flow direction of the line. - /// The paragraph offset. - internal static double GetParagraphOffsetX(double width, double widthIncludingTrailingWhitespace, - double paragraphWidth, TextAlignment textAlignment, FlowDirection flowDirection) - { - if (double.IsPositiveInfinity(paragraphWidth)) - { - return 0; - } - - if (flowDirection == FlowDirection.LeftToRight) - { - switch (textAlignment) - { - case TextAlignment.Center: - return Math.Max(0, (paragraphWidth - width) / 2); - - case TextAlignment.Right: - return Math.Max(0, paragraphWidth - widthIncludingTrailingWhitespace); - - default: - return 0; - } - } - - switch (textAlignment) - { - case TextAlignment.Center: - return Math.Max(0, (paragraphWidth - width) / 2); - - case TextAlignment.Right: - return 0; - - default: - return Math.Max(0, paragraphWidth - widthIncludingTrailingWhitespace); - } - } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 8b5e2cc2ce..7c686358e2 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -10,10 +10,10 @@ namespace Avalonia.Media.TextFormatting private readonly double _paragraphWidth; private readonly TextParagraphProperties _paragraphProperties; private TextLineMetrics _textLineMetrics; - private readonly FlowDirection _flowDirection; + private readonly FlowDirection _resolvedFlowDirection; public TextLineImpl(List textRuns, int firstTextSourceIndex, int length, double paragraphWidth, - TextParagraphProperties paragraphProperties, FlowDirection flowDirection = FlowDirection.LeftToRight, + TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection = FlowDirection.LeftToRight, TextLineBreak? lineBreak = null, bool hasCollapsed = false) { FirstTextSourceIndex = firstTextSourceIndex; @@ -25,7 +25,7 @@ namespace Avalonia.Media.TextFormatting _paragraphWidth = paragraphWidth; _paragraphProperties = paragraphProperties; - _flowDirection = flowDirection; + _resolvedFlowDirection = resolvedFlowDirection; } /// @@ -136,7 +136,7 @@ namespace Avalonia.Media.TextFormatting } var collapsedLine = new TextLineImpl(collapsedRuns, FirstTextSourceIndex, Length, _paragraphWidth, _paragraphProperties, - _flowDirection, TextLineBreak, true); + _resolvedFlowDirection, TextLineBreak, true); if (collapsedRuns.Count > 0) { @@ -144,7 +144,14 @@ namespace Avalonia.Media.TextFormatting } return collapsedLine; + } + /// + public override void Justify(JustificationProperties justificationProperties) + { + justificationProperties.Justify(this); + + _textLineMetrics = CreateLineMetrics(); } /// @@ -167,7 +174,7 @@ namespace Avalonia.Media.TextFormatting return shapedTextCharacters.GlyphRun.GetCharacterHitFromDistance(distance, out _); } - return _flowDirection == FlowDirection.LeftToRight ? + return _resolvedFlowDirection == FlowDirection.LeftToRight ? new CharacterHit(FirstTextSourceIndex) : new CharacterHit(FirstTextSourceIndex + Length); } @@ -260,7 +267,7 @@ namespace Avalonia.Media.TextFormatting //Look at the left and right edge of the current run if (currentRun.IsLeftToRight) { - if (_flowDirection == FlowDirection.LeftToRight && (lastRun == null || lastRun.IsLeftToRight)) + if (_resolvedFlowDirection == FlowDirection.LeftToRight && (lastRun == null || lastRun.IsLeftToRight)) { if (characterIndex <= currentPosition) { @@ -735,7 +742,7 @@ namespace Avalonia.Media.TextFormatting // Build up the collection of ordered runs. var run = _textRuns[0]; - OrderedBidiRun orderedRun = new(run, GetRunBidiLevel(run, _flowDirection)); + OrderedBidiRun orderedRun = new(run, GetRunBidiLevel(run, _resolvedFlowDirection)); var current = orderedRun; @@ -743,7 +750,7 @@ namespace Avalonia.Media.TextFormatting { run = _textRuns[i]; - current.Next = new OrderedBidiRun(run, GetRunBidiLevel(run, _flowDirection)); + current.Next = new OrderedBidiRun(run, GetRunBidiLevel(run, _resolvedFlowDirection)); current = current.Next; } @@ -762,7 +769,7 @@ namespace Avalonia.Media.TextFormatting { var currentRun = _textRuns[i]; - var level = GetRunBidiLevel(currentRun, _flowDirection); + var level = GetRunBidiLevel(currentRun, _resolvedFlowDirection); if (level > max) { @@ -1242,8 +1249,7 @@ namespace Avalonia.Media.TextFormatting } } - var start = GetParagraphOffsetX(width, widthIncludingWhitespace, _paragraphWidth, - _paragraphProperties.TextAlignment, _paragraphProperties.FlowDirection); + var start = GetParagraphOffsetX(width, widthIncludingWhitespace); if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight)) { @@ -1257,6 +1263,55 @@ namespace Avalonia.Media.TextFormatting -ascent, trailingWhitespaceLength, width, widthIncludingWhitespace); } + /// + /// Gets the text line offset x. + /// + /// The line width. + /// The paragraph width including whitespace. + + /// The paragraph offset. + private double GetParagraphOffsetX(double width, double widthIncludingTrailingWhitespace) + { + if (double.IsPositiveInfinity(_paragraphWidth)) + { + return 0; + } + + var textAlignment = _paragraphProperties.TextAlignment; + var paragraphFlowDirection = _paragraphProperties.FlowDirection; + + switch (textAlignment) + { + case TextAlignment.Start: + { + textAlignment = paragraphFlowDirection == FlowDirection.LeftToRight ? TextAlignment.Left : TextAlignment.Right; + break; + } + case TextAlignment.End: + { + textAlignment = paragraphFlowDirection == FlowDirection.RightToLeft ? TextAlignment.Left : TextAlignment.Right; + break; + } + case TextAlignment.DetectFromContent: + { + textAlignment = _resolvedFlowDirection == FlowDirection.LeftToRight ? TextAlignment.Left : TextAlignment.Right; + break; + } + } + + switch (textAlignment) + { + case TextAlignment.Center: + return Math.Max(0, (_paragraphWidth - width) / 2); + + case TextAlignment.Right: + return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace); + + default: + return 0; + } + } + private sealed class OrderedBidiRun { public OrderedBidiRun(DrawableTextRun run, sbyte level) diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 1a69d1218c..db315d3aaf 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -113,7 +113,9 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly AttachedProperty TextAlignmentProperty = - AvaloniaProperty.RegisterAttached(nameof(TextAlignment), + AvaloniaProperty.RegisterAttached( + nameof(TextAlignment), + defaultValue: TextAlignment.Start, inherits: true); /// @@ -748,14 +750,14 @@ namespace Avalonia.Controls { if (textSourceIndex > _text.Length) { - return null; + return new TextEndOfParagraph(); } var runText = _text.Skip(textSourceIndex); if (runText.IsEmpty) { - return null; + return new TextEndOfParagraph(); } return new TextCharacters(runText, _defaultProperties); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index d395f68f96..48dbfa5985 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -134,7 +134,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var defaultProperties = new GenericTextRunProperties(Typeface.Default); const string text = "👍 👍 👍 👍"; - + var textSource = new SingleBufferTextSource(text, defaultProperties); var formatter = new TextFormatterImpl(); @@ -144,7 +144,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting new GenericTextParagraphProperties(defaultProperties)); Assert.Equal(1, textLine.TextRuns.Count); - } + } } [Fact] @@ -163,9 +163,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); - + var firstRun = textLine.TextRuns[0]; - + Assert.Equal(4, firstRun.Text.Length); } } @@ -191,7 +191,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var textLine = formatter.FormatLine(textSource, currentPosition, 1, - new GenericTextParagraphProperties(defaultProperties, textWrap : TextWrapping.WrapWithOverflow)); + new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.WrapWithOverflow)); if (text.Length - currentPosition > expectedCharactersPerLine) { @@ -347,8 +347,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } - [InlineData("Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor", - new []{ "Lorem ipsum ", "dolor sit amet, ", "consectetur ", "adipisicing elit, ", "sed do eiusmod "})] + [InlineData("Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor", + new[] { "Lorem ipsum ", "dolor sit amet, ", "consectetur ", "adipisicing elit, ", "sed do eiusmod " })] [Theory] public void Should_Produce_Wrapped_And_Trimmed_Lines(string text, string[] expectedLines) @@ -368,7 +368,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting new ValueSpan(28, 28, new GenericTextRunProperties(new Typeface("Verdana", FontStyle.Italic),32)) }; - + var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, styleSpans); var formatter = new TextFormatterImpl(); @@ -389,19 +389,19 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting if (textLine.Width > 300 || currentHeight + textLine.Height > 240) { - textLine = textLine.Collapse(new TextTrailingWordEllipsis(new ReadOnlySlice(new[] {TextTrimming.s_defaultEllipsisChar}), 300, defaultProperties)); + textLine = textLine.Collapse(new TextTrailingWordEllipsis(new ReadOnlySlice(new[] { TextTrimming.s_defaultEllipsisChar }), 300, defaultProperties)); } - + currentHeight += textLine.Height; var currentText = text.Substring(textLine.FirstTextSourceIndex, textLine.Length); - + Assert.Equal(expectedLines[currentLineIndex], currentText); currentLineIndex++; } - - Assert.Equal(expectedLines.Length,currentLineIndex); + + Assert.Equal(expectedLines.Length, currentLineIndex); } } @@ -412,11 +412,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting [InlineData("0123456789", TextAlignment.Left, FlowDirection.RightToLeft)] [InlineData("0123456789", TextAlignment.Center, FlowDirection.RightToLeft)] [InlineData("0123456789", TextAlignment.Right, FlowDirection.RightToLeft)] - + [InlineData("שנבגק", TextAlignment.Left, FlowDirection.RightToLeft)] [InlineData("שנבגק", TextAlignment.Center, FlowDirection.RightToLeft)] [InlineData("שנבגק", TextAlignment.Right, FlowDirection.RightToLeft)] - + [Theory] public void Should_Align_TextLine(string text, TextAlignment textAlignment, FlowDirection flowDirection) { @@ -426,44 +426,29 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var paragraphProperties = new GenericTextParagraphProperties(flowDirection, textAlignment, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0); - + var textSource = new SingleBufferTextSource(text, defaultProperties); var formatter = new TextFormatterImpl(); - + var textLine = formatter.FormatLine(textSource, 0, 100, paragraphProperties); var expectedOffset = 0d; - if (flowDirection == FlowDirection.LeftToRight) + switch (textAlignment) { - switch (textAlignment) - { - case TextAlignment.Center: - expectedOffset = 50 - textLine.Width / 2; - break; - case TextAlignment.Right: - expectedOffset = 100 - textLine.WidthIncludingTrailingWhitespace; - break; - } - } - else - { - switch (textAlignment) - { - case TextAlignment.Left: - expectedOffset = 100 - textLine.WidthIncludingTrailingWhitespace; - break; - case TextAlignment.Center: - expectedOffset = 50 - textLine.Width / 2; - break; - } + case TextAlignment.Center: + expectedOffset = 50 - textLine.Width / 2; + break; + case TextAlignment.Right: + expectedOffset = 100 - textLine.WidthIncludingTrailingWhitespace; + break; } Assert.Equal(expectedOffset, textLine.Start); } } - + [Fact] public void Should_Wrap_Syriac() { @@ -488,7 +473,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting formatter.FormatLine(textSource, textPosition, 50, paragraphProperties, lastBreak); Assert.Equal(textLine.Length, textLine.TextRuns.Sum(x => x.TextSourceLength)); - + textPosition += textLine.Length; lastBreak = textLine.TextLineBreak; @@ -503,13 +488,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap); - + var textSource = new SingleBufferTextSource("0123456789_0123456789_0123456789_0123456789", defaultProperties); var formatter = new TextFormatterImpl(); - + var textLine = formatter.FormatLine(textSource, 0, 33, paragraphProperties); - + Assert.NotNull(textLine.TextLineBreak?.RemainingRuns); } } @@ -524,12 +509,12 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting using (Start()) { var formatter = new TextFormatterImpl(); - + var defaultProperties = new GenericTextRunProperties(Typeface.Default); var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.NoWrap); - + var foreground = new SolidColorBrush(Colors.Red).ToImmutable(); var expectedTextLine = formatter.FormatLine(new SingleBufferTextSource(text, defaultProperties), @@ -548,16 +533,16 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting new ValueSpan(i, j, new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground)) }; - + var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, spans); - + var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties); - + var shapedRuns = textLine.TextRuns.Cast().ToList(); var actualGlyphs = shapedRuns.SelectMany(x => x.GlyphRun.GlyphIndices).ToList(); - + Assert.Equal(expectedGlyphs, actualGlyphs); } } @@ -575,9 +560,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var textLine = TextFormatter.Current.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties); - + Assert.Equal(3, textLine.TextRuns.Count); - + Assert.True(textLine.TextRuns[1] is RectangleRun); } } @@ -590,12 +575,12 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var defaultRunProperties = new GenericTextRunProperties(Typeface.Default); var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties); var textSource = new EndOfLineTextSource(); - + var textLine = TextFormatter.Current.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties); - + Assert.NotNull(textLine.TextLineBreak); - + Assert.Equal(TextRun.DefaultTextSourceLength, textLine.Length); } } @@ -616,7 +601,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { _text = text; } - + public TextRun GetTextRun(int textSourceIndex) { if (textSourceIndex >= _text.Length + TextRun.DefaultTextSourceLength + _text.Length)