Browse Source

Add more alignment options

Benedikt Stebner 3 years ago
parent
commit
d26a989406

+ 19 - 1
src/Avalonia.Base/Media/TextAlignment.cs

@@ -21,7 +21,25 @@ namespace Avalonia.Media
         Right,
 
         /// <summary>
-        /// The text is layed out so each line is stretched to an equal width.
+        /// The beginning of the text is aligned to the edge of the available space.
+        /// </summary>
+        Start,
+
+        /// <summary>
+        /// The end of the text is aligned to the edge of the available space.
+        /// </summary>
+        End,
+
+        /// <summary>
+        /// Text alignment is inferred from the text content.
+        /// </summary>
+        /// <remarks>
+        /// When the TextAlignment property is set to DetectFromContent, alignment is inferred from the text content of the control. For example, English text is left aligned, and Arabic text is right aligned.
+        /// </remarks>
+        DetectFromContent,
+
+        /// <summary>
+        /// Text is justified within the available space.
         /// </summary>
         Justify
     }

+ 11 - 15
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@@ -15,7 +15,7 @@ namespace Avalonia.Media.TextFormatting
             TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null)
         {
             var textWrapping = paragraphProperties.TextWrapping;
-            FlowDirection flowDirection;
+            FlowDirection resolvedFlowDirection;
             TextLineBreak? nextLineBreak = null;
             List<DrawableTextRun> drawableTextRuns;
 
@@ -24,17 +24,17 @@ namespace Avalonia.Media.TextFormatting
 
             if (previousLineBreak?.RemainingRuns != null)
             {
-                flowDirection = previousLineBreak.FlowDirection;
+                resolvedFlowDirection = previousLineBreak.FlowDirection;
                 drawableTextRuns = previousLineBreak.RemainingRuns.ToList();
                 nextLineBreak = previousLineBreak;
             }
             else
             {
-                drawableTextRuns = ShapeTextRuns(textRuns, paragraphProperties, out flowDirection);
+                drawableTextRuns = ShapeTextRuns(textRuns, paragraphProperties, out resolvedFlowDirection);
 
                 if (nextLineBreak == null && textEndOfLine != null)
                 {
-                    nextLineBreak = new TextLineBreak(textEndOfLine, flowDirection);
+                    nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection);
                 }
             }
 
@@ -45,7 +45,7 @@ namespace Avalonia.Media.TextFormatting
                 case TextWrapping.NoWrap:
                     {
                         textLine = new TextLineImpl(drawableTextRuns, firstTextSourceIndex, textSourceLength,
-                            paragraphWidth, paragraphProperties, flowDirection, nextLineBreak);
+                            paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak);
 
                         textLine.FinalizeLine();
 
@@ -55,7 +55,7 @@ namespace Avalonia.Media.TextFormatting
                 case TextWrapping.Wrap:
                     {
                         textLine = PerformTextWrapping(drawableTextRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties,
-                            flowDirection, nextLineBreak);
+                            resolvedFlowDirection, nextLineBreak);
                         break;
                     }
                 default:
@@ -404,10 +404,6 @@ namespace Avalonia.Media.TextFormatting
                 {
                     endOfLine = textEndOfLine;
 
-                    textRuns.Add(textRun);
-
-                    textSourceLength += textRun.TextSourceLength;
-
                     break;
                 }
 
@@ -552,11 +548,11 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="firstTextSourceIndex">The first text source index.</param>
         /// <param name="paragraphWidth">The paragraph width.</param>
         /// <param name="paragraphProperties">The text paragraph properties.</param>
-        /// <param name="flowDirection"></param>
+        /// <param name="resolvedFlowDirection"></param>
         /// <param name="currentLineBreak">The current line break if the line was explicitly broken.</param>
         /// <returns>The wrapped text line.</returns>
         private static TextLineImpl PerformTextWrapping(List<DrawableTextRun> textRuns, int firstTextSourceIndex,
-            double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection flowDirection,
+            double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection,
             TextLineBreak? currentLineBreak)
         {
             if(textRuns.Count == 0)
@@ -684,16 +680,16 @@ namespace Avalonia.Media.TextFormatting
             var remainingCharacters = splitResult.Second;
 
             var lineBreak = remainingCharacters?.Count > 0 ?
-                new TextLineBreak(currentLineBreak?.TextEndOfLine, flowDirection, remainingCharacters) :
+                new TextLineBreak(currentLineBreak?.TextEndOfLine, resolvedFlowDirection, remainingCharacters) :
                 null;
 
             if (lineBreak is null && currentLineBreak?.TextEndOfLine != null)
             {
-                lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, flowDirection);
+                lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection);
             }
 
             var textLine = new TextLineImpl(splitResult.First, firstTextSourceIndex, measuredLength,
-                paragraphWidth, paragraphProperties, flowDirection,
+                paragraphWidth, paragraphProperties, resolvedFlowDirection,
                 lineBreak);
 
             return textLine.FinalizeLine();

+ 2 - 37
src/Avalonia.Base/Media/TextFormatting/TextLine.cs

@@ -15,7 +15,7 @@ namespace Avalonia.Media.TextFormatting
         /// The contained text runs.
         /// </value>
         public abstract IReadOnlyList<TextRun> TextRuns { get; }
-        
+
         public abstract int FirstTextSourceIndex { get; }
 
         public abstract int Length { get; }
@@ -75,7 +75,7 @@ namespace Avalonia.Media.TextFormatting
         /// The number of newline characters.
         /// </returns>
         public abstract int NewLineLength { get; }
-        
+
         /// <summary>
         /// Gets the distance that black pixels extend beyond the bottom alignment edge of a line.
         /// </summary>
@@ -192,40 +192,5 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="textLength">number of characters of the specified range</param>
         /// <returns>an array of bounding rectangles.</returns>
         public abstract IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceCharacterIndex, int textLength);
-        
-        /// <summary>
-        /// Gets the text line offset x.
-        /// </summary>
-        /// <param name="width">The line width.</param>
-        /// <param name="widthIncludingTrailingWhitespace">The paragraph width including whitespace.</param>
-        /// <param name="paragraphWidth">The paragraph width.</param>
-        /// <param name="textAlignment">The text alignment.</param>
-        /// <param name="flowDirection">The flow direction of the line.</param>
-        /// <returns>The paragraph offset.</returns>
-        internal static double GetParagraphOffsetX(double width, double widthIncludingTrailingWhitespace,
-            double paragraphWidth, TextAlignment textAlignment, FlowDirection flowDirection)
-        {
-            if (double.IsPositiveInfinity(paragraphWidth))
-            {
-                return 0;
-            }
-
-            if (flowDirection == FlowDirection.LeftToRight)
-            {
-                switch (textAlignment)
-                {
-                    case TextAlignment.Center:
-                        return Math.Max(0, (paragraphWidth - width) / 2);
-
-                    case TextAlignment.Right:
-                        return flowDirection == FlowDirection.LeftToRight ? Math.Max(0, paragraphWidth - widthIncludingTrailingWhitespace) : 0;
-
-                    case TextAlignment.Left:
-                        return flowDirection == FlowDirection.LeftToRight ? 0 : Math.Max(0, paragraphWidth - widthIncludingTrailingWhitespace);
-                }
-            }
-   
-            return 0;
-        }
     }
 }

+ 59 - 11
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@@ -11,10 +11,10 @@ namespace Avalonia.Media.TextFormatting
         private readonly double _paragraphWidth;
         private readonly TextParagraphProperties _paragraphProperties;
         private TextLineMetrics _textLineMetrics;
-        private readonly FlowDirection _flowDirection;
+        private readonly FlowDirection _resolvedFlowDirection;
 
         public TextLineImpl(List<DrawableTextRun> textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
-            TextParagraphProperties paragraphProperties, FlowDirection flowDirection = FlowDirection.LeftToRight,
+            TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection = FlowDirection.LeftToRight,
             TextLineBreak? lineBreak = null, bool hasCollapsed = false)
         {
             FirstTextSourceIndex = firstTextSourceIndex;
@@ -26,7 +26,7 @@ namespace Avalonia.Media.TextFormatting
             _paragraphWidth = paragraphWidth;
             _paragraphProperties = paragraphProperties;
 
-            _flowDirection = flowDirection;
+            _resolvedFlowDirection = resolvedFlowDirection;
         }
 
         /// <inheritdoc/>
@@ -137,7 +137,7 @@ namespace Avalonia.Media.TextFormatting
             }
 
             var collapsedLine = new TextLineImpl(collapsedRuns, FirstTextSourceIndex, Length, _paragraphWidth, _paragraphProperties,
-                _flowDirection, TextLineBreak, true);
+                _resolvedFlowDirection, TextLineBreak, true);
 
             if (collapsedRuns.Count > 0)
             {
@@ -168,7 +168,7 @@ namespace Avalonia.Media.TextFormatting
                     return shapedTextCharacters.GlyphRun.GetCharacterHitFromDistance(distance, out _);
                 }
 
-                return _flowDirection == FlowDirection.LeftToRight ?
+                return _resolvedFlowDirection == FlowDirection.LeftToRight ?
                     new CharacterHit(FirstTextSourceIndex) :
                     new CharacterHit(FirstTextSourceIndex + Length);
             }
@@ -261,7 +261,7 @@ namespace Avalonia.Media.TextFormatting
                             //Look at the left and right edge of the current run
                             if (currentRun.IsLeftToRight)
                             {
-                                if (_flowDirection == FlowDirection.LeftToRight && (lastRun == null || lastRun.IsLeftToRight))
+                                if (_resolvedFlowDirection == FlowDirection.LeftToRight && (lastRun == null || lastRun.IsLeftToRight))
                                 {
                                     if (characterIndex <= currentPosition)
                                     {
@@ -846,7 +846,7 @@ namespace Avalonia.Media.TextFormatting
             // Build up the collection of ordered runs.
             var run = _textRuns[0];
 
-            OrderedBidiRun orderedRun = new(run, GetRunBidiLevel(run, _flowDirection));
+            OrderedBidiRun orderedRun = new(run, GetRunBidiLevel(run, _resolvedFlowDirection));
 
             var current = orderedRun;
 
@@ -854,7 +854,7 @@ namespace Avalonia.Media.TextFormatting
             {
                 run = _textRuns[i];
 
-                current.Next = new OrderedBidiRun(run, GetRunBidiLevel(run, _flowDirection));
+                current.Next = new OrderedBidiRun(run, GetRunBidiLevel(run, _resolvedFlowDirection));
 
                 current = current.Next;
             }
@@ -873,7 +873,7 @@ namespace Avalonia.Media.TextFormatting
             {
                 var currentRun = _textRuns[i];
 
-                var level = GetRunBidiLevel(currentRun, _flowDirection);
+                var level = GetRunBidiLevel(currentRun, _resolvedFlowDirection);
 
                 if (level > max)
                 {
@@ -1353,8 +1353,7 @@ namespace Avalonia.Media.TextFormatting
                 }
             }
 
-            var start = GetParagraphOffsetX(width, widthIncludingWhitespace, _paragraphWidth,
-                _paragraphProperties.TextAlignment, _paragraphProperties.FlowDirection);
+            var start = GetParagraphOffsetX(width, widthIncludingWhitespace);
 
             if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
             {
@@ -1368,6 +1367,55 @@ namespace Avalonia.Media.TextFormatting
                 -ascent, trailingWhitespaceLength, width, widthIncludingWhitespace);
         }
 
+        /// <summary>
+        /// Gets the text line offset x.
+        /// </summary>
+        /// <param name="width">The line width.</param>
+        /// <param name="widthIncludingTrailingWhitespace">The paragraph width including whitespace.</param>
+
+        /// <returns>The paragraph offset.</returns>
+        private double GetParagraphOffsetX(double width, double widthIncludingTrailingWhitespace)
+        {
+            if (double.IsPositiveInfinity(_paragraphWidth))
+            {
+                return 0;
+            }
+
+            var textAlignment = _paragraphProperties.TextAlignment;
+            var paragraphFlowDirection = _paragraphProperties.FlowDirection;
+
+            switch (textAlignment)
+            {
+                case TextAlignment.Start:
+                    {
+                        textAlignment = paragraphFlowDirection == FlowDirection.LeftToRight ? TextAlignment.Left : TextAlignment.Right;
+                        break;
+                    }
+                case TextAlignment.End:
+                    {
+                        textAlignment = paragraphFlowDirection == FlowDirection.RightToLeft ? TextAlignment.Left : TextAlignment.Right;
+                        break;
+                    }
+                case TextAlignment.DetectFromContent:
+                    {
+                        textAlignment = _resolvedFlowDirection == FlowDirection.LeftToRight ? TextAlignment.Left : TextAlignment.Right;
+                        break;
+                    }
+            }
+
+            switch (textAlignment)
+            {
+                case TextAlignment.Center:
+                    return Math.Max(0, (_paragraphWidth - width) / 2);
+
+                case TextAlignment.Right:
+                    return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace);
+
+                default:
+                    return 0;
+            }
+        }
+
         private sealed class OrderedBidiRun
         {
             public OrderedBidiRun(DrawableTextRun run, sbyte level)