Browse Source

Initial implementation of TextAlignment.Justify

Benedikt Stebner 3 years ago
parent
commit
a794d1765a

+ 3 - 4
src/Avalonia.Base/Media/GlyphRun.cs

@@ -740,10 +740,9 @@ namespace Avalonia.Media
 
         private void Set<T>(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;
 

+ 5 - 0
src/Avalonia.Base/Media/TextAlignment.cs

@@ -19,5 +19,10 @@ namespace Avalonia.Media
         /// The text is right-aligned.
         /// </summary>
         Right,
+
+        /// <summary>
+        /// The text is layed out so each line is stretched to an equal width.
+        /// </summary>
+        Justify
     }
 }

+ 2 - 2
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;
                         }
                 }

+ 5 - 15
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;
         }
     }
 }

+ 111 - 0
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<int>();
+
+            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)

+ 2 - 2
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);