From a794d1765a7e1d0f19ad6903fa4c3a1ed6f27299 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 15 Jun 2022 18:46:51 +0200 Subject: [PATCH] Initial implementation of TextAlignment.Justify --- src/Avalonia.Base/Media/GlyphRun.cs | 7 +- src/Avalonia.Base/Media/TextAlignment.cs | 5 + .../Media/TextFormatting/TextFormatterImpl.cs | 4 +- .../Media/TextFormatting/TextLine.cs | 20 +--- .../Media/TextFormatting/TextLineImpl.cs | 111 ++++++++++++++++++ src/Avalonia.Controls/TextBlock.cs | 4 +- 6 files changed, 128 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 25c35a28e5..d6f9043455 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -740,10 +740,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..fdcaea2d46 100644 --- a/src/Avalonia.Base/Media/TextAlignment.cs +++ b/src/Avalonia.Base/Media/TextAlignment.cs @@ -19,5 +19,10 @@ namespace Avalonia.Media /// The text is right-aligned. /// Right, + + /// + /// The text is layed out so each line is stretched to an equal width. + /// + Justify } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 4205268bc6..f6c9c85889 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -431,9 +431,9 @@ namespace Avalonia.Media.TextFormatting break; } - case DrawableTextRun drawableTextRun: + default: { - textRuns.Add(drawableTextRun); + textRuns.Add(textRun); break; } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLine.cs b/src/Avalonia.Base/Media/TextFormatting/TextLine.cs index 1f69c15acc..afd0516f56 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLine.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLine.cs @@ -218,24 +218,14 @@ namespace Avalonia.Media.TextFormatting return Math.Max(0, (paragraphWidth - width) / 2); case TextAlignment.Right: - return Math.Max(0, paragraphWidth - widthIncludingTrailingWhitespace); + return flowDirection == FlowDirection.LeftToRight ? Math.Max(0, paragraphWidth - widthIncludingTrailingWhitespace) : 0; - default: - return 0; + case TextAlignment.Left: + return flowDirection == FlowDirection.LeftToRight ? 0 : Math.Max(0, paragraphWidth - widthIncludingTrailingWhitespace); } } - - switch (textAlignment) - { - case TextAlignment.Center: - return Math.Max(0, (paragraphWidth - width) / 2); - - case TextAlignment.Right: - return 0; - - default: - return Math.Max(0, paragraphWidth - widthIncludingTrailingWhitespace); - } + + return 0; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 8b5e2cc2ce..ff9d2cc868 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting @@ -708,11 +709,121 @@ namespace Avalonia.Media.TextFormatting { _textLineMetrics = CreateLineMetrics(); + Justify(); + BidiReorder(); return this; } + private void Justify() + { + if (_paragraphProperties.TextAlignment != TextAlignment.Justify) + { + return; + } + + var paragraphWidth = _paragraphWidth; + + if (double.IsInfinity(paragraphWidth)) + { + return; + } + + if(_textLineMetrics.NewLineLength > 0) + { + return; + } + + 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 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 - WidthIncludingTrailingWhitespace); + var spacing = remainingSpace / breakOportunities.Count; + + foreach (var textRun in 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; + } + } + + var trailingWhitespaceWidth = _textLineMetrics.WidthIncludingTrailingWhitespace - _textLineMetrics.Width; + + _textLineMetrics = new TextLineMetrics( + _textLineMetrics.HasOverflowed, + _textLineMetrics.Height, + _textLineMetrics.NewLineLength, + _textLineMetrics.Start, + _textLineMetrics.TextBaseline, + _textLineMetrics.TrailingWhitespaceLength, + paragraphWidth - trailingWhitespaceWidth, + paragraphWidth); + } + private static sbyte GetRunBidiLevel(DrawableTextRun run, FlowDirection flowDirection) { if (run is ShapedTextCharacters shapedTextCharacters) diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 1a69d1218c..c5893167b3 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -748,14 +748,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);