From fd364c349078a42dfd7f8cf60b20309337a5471e Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 6 Jan 2025 08:38:54 +0100 Subject: [PATCH 1/2] Properly handle mutliple runs during justification --- .../TextFormatting/InterWordJustification.cs | 57 +++++++++++++++---- .../Presenters/TextPresenter.cs | 4 +- 2 files changed, 48 insertions(+), 13 deletions(-) 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/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 622727ccbe..6e59f1e2fc 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -627,14 +627,14 @@ namespace Avalonia.Controls.Presenters InvalidateArrange(); - var textWidth = TextLayout.OverhangLeading + TextLayout.WidthIncludingTrailingWhitespace + TextLayout.OverhangTrailing; + var textWidth = Math.Ceiling(TextLayout.MinTextWidth); return new Size(textWidth, TextLayout.Height); } protected override Size ArrangeOverride(Size finalSize) { - var textWidth = Math.Ceiling(TextLayout.OverhangLeading + TextLayout.WidthIncludingTrailingWhitespace + TextLayout.OverhangTrailing); + var textWidth = Math.Ceiling(TextLayout.MinTextWidth); if (finalSize.Width < textWidth) { From 1f8ff62f30e4fd80bdfa264b1e82b57c51418f1a Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 8 Dec 2025 12:30:11 +0100 Subject: [PATCH 2/2] Add some render test --- .../Controls/TextBlockTests.cs | 25 ++++++++++++++++++ .../Should_Justify_With_Spaces.expected.png | Bin 0 -> 10309 bytes 2 files changed, 25 insertions(+) create mode 100644 tests/TestFiles/Skia/Controls/TextBlock/Should_Justify_With_Spaces.expected.png diff --git a/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs b/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs index 45b8c5d2ce..ca0061d2a6 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 { @@ -386,5 +387,29 @@ namespace Avalonia.Skia.RenderTests }; } + [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 0000000000000000000000000000000000000000..49ff4430a7bea8dadb4b4fd19ff62f977843a7d3 GIT binary patch literal 10309 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 literal 0 HcmV?d00001