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/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 85d035c446..f3af240c58 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -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 88a8d9d985..c8a23097db 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLine.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLine.cs @@ -16,8 +16,14 @@ namespace Avalonia.Media.TextFormatting /// 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; } @@ -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. /// diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index ce7edaa5da..7c686358e2 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting @@ -145,7 +144,14 @@ namespace Avalonia.Media.TextFormatting } return collapsedLine; + } + /// + public override void Justify(JustificationProperties justificationProperties) + { + justificationProperties.Justify(this); + + _textLineMetrics = CreateLineMetrics(); } /// @@ -709,121 +715,11 @@ 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)