diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index 75249ff7e7..bcc83da06e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -31,6 +31,8 @@ namespace Avalonia.Media.TextFormatting var currentPosition = textLine.FirstTextSourceIndex; + var whiteSpaceWidth = 0.0; + for (var i = 0; i < lineImpl.TextRuns.Count; ++i) { var textRun = lineImpl.TextRuns[i]; @@ -41,15 +43,38 @@ namespace Avalonia.Media.TextFormatting continue; } - var lineBreakEnumerator = new LineBreakEnumerator(text.Span); - - while (lineBreakEnumerator.MoveNext(out var currentBreak)) + if (textRun is ShapedTextRun shapedText) { - if (!currentBreak.Required && currentBreak.PositionWrap != textRun.Length) + var glyphRun = shapedText.GlyphRun; + var shapedBuffer = shapedText.ShapedBuffer; + + var lineBreakEnumerator = new LineBreakEnumerator(text.Span); + + while (lineBreakEnumerator.MoveNext(out var currentBreak)) { - breakOportunities.Enqueue(currentPosition + currentBreak.PositionMeasure); + //Ignore the break at the end + if(currentPosition + currentBreak.PositionWrap == textLine.Length - TextRun.DefaultTextSourceLength) + { + break; + } + + if (!currentBreak.Required) + { + breakOportunities.Enqueue(currentPosition + currentBreak.PositionWrap); + + var offset = Math.Max(0, currentPosition - glyphRun.Metrics.FirstCluster); + + var characterIndex = currentPosition - offset + currentBreak.PositionWrap - 1; + var glyphIndex = glyphRun.FindGlyphIndex(characterIndex); + var glyphInfo = shapedBuffer[glyphIndex]; + + if (Codepoint.ReadAt(text.Span, currentBreak.PositionWrap - 1, out _).IsWhiteSpace) + { + whiteSpaceWidth += glyphInfo.GlyphAdvance; + } + } } - } + } currentPosition += textRun.Length; } @@ -59,7 +84,9 @@ namespace Avalonia.Media.TextFormatting return; } - var remainingSpace = Math.Max(0, paragraphWidth - lineImpl.WidthIncludingTrailingWhitespace); + //Adjust remaining space by whiteSpace width + var remainingSpace = Math.Max(0, paragraphWidth - lineImpl.Width) + whiteSpaceWidth; + var spacing = remainingSpace / breakOportunities.Count; currentPosition = textLine.FirstTextSourceIndex; @@ -82,17 +109,25 @@ namespace Avalonia.Media.TextFormatting { var characterIndex = breakOportunities.Dequeue(); - if (characterIndex < currentPosition) + var offset = Math.Max(0, currentPosition - glyphRun.Metrics.FirstCluster); + + if (characterIndex + offset < currentPosition) { continue; } - var offset = Math.Max(0, currentPosition - glyphRun.Metrics.FirstCluster); - var glyphIndex = glyphRun.FindGlyphIndex(characterIndex - offset); + var glyphIndex = glyphRun.FindGlyphIndex(characterIndex - offset - 1); var glyphInfo = shapedBuffer[glyphIndex]; + var isWhitespace = Codepoint.ReadAt(text.Span, characterIndex - 1 - currentPosition, out _).IsWhiteSpace; + shapedBuffer[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, - glyphInfo.GlyphCluster, glyphInfo.GlyphAdvance + spacing); + glyphInfo.GlyphCluster, isWhitespace ? spacing : glyphInfo.GlyphAdvance + spacing); + + if (glyphIndex == shapedBuffer.Length - 1) + { + break; + } } glyphRun.GlyphInfos = shapedBuffer; diff --git a/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs b/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs index deecc9eae3..863c7d2872 100644 --- a/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs +++ b/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs @@ -7,6 +7,7 @@ using Avalonia.Controls.Documents; using Avalonia.Layout; using Avalonia.Media; using Xunit; +using static System.Net.Mime.MediaTypeNames; namespace Avalonia.Skia.RenderTests { @@ -424,5 +425,29 @@ namespace Avalonia.Skia.RenderTests CompareImages(testName); } + [Win32Fact("Has text")] + public async Task Should_Justify_With_Spaces() + { + var target = new StackPanel + { + Width = 300, + Height = 400, + Background = new SolidColorBrush(Colors.White), // Required antialiasing to work for Overhang + }; + + target.Children.Add(CreateText("今天的晚饭很好")); + target.Children.Add(CreateText("今 天 的 晚 饭 很 好")); + + await RenderToFile(target); + + CompareImages(); + + static TextBlock CreateText(string text) => new TextBlock + { + TextAlignment = TextAlignment.Justify, + FontSize = 28, + Text = text + }; + } } } diff --git a/tests/TestFiles/Skia/Controls/TextBlock/Should_Justify_With_Spaces.expected.png b/tests/TestFiles/Skia/Controls/TextBlock/Should_Justify_With_Spaces.expected.png new file mode 100644 index 0000000000..49ff4430a7 Binary files /dev/null and b/tests/TestFiles/Skia/Controls/TextBlock/Should_Justify_With_Spaces.expected.png differ