Browse Source

Merge branch 'master' into bugfix-window-margin

pull/8393/head
Max Katz 4 years ago
committed by GitHub
parent
commit
f545205234
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      src/Avalonia.Base/Media/GlyphRun.cs
  2. 23
      src/Avalonia.Base/Media/TextAlignment.cs
  3. 109
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  4. 16
      src/Avalonia.Base/Media/TextFormatting/JustificationProperties.cs
  5. 30
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  6. 31
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  7. 66
      src/Avalonia.Base/Media/TextFormatting/TextLine.cs
  8. 77
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  9. 8
      src/Avalonia.Controls/TextBlock.cs
  10. 97
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

7
src/Avalonia.Base/Media/GlyphRun.cs

@ -734,10 +734,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;

23
src/Avalonia.Base/Media/TextAlignment.cs

@ -19,5 +19,28 @@ namespace Avalonia.Media
/// The text is right-aligned.
/// </summary>
Right,
/// <summary>
/// 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
}
}

109
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<int>();
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;
}
}
}
}
}

16
src/Avalonia.Base/Media/TextFormatting/JustificationProperties.cs

@ -0,0 +1,16 @@
namespace Avalonia.Media.TextFormatting
{
public abstract class JustificationProperties
{
/// <summary>
/// Gets the width in which the range is justified.
/// </summary>
public abstract double Width { get; }
/// <summary>
/// Justifies given text line.
/// </summary>
/// <param name="textLine">Text line to collapse.</param>
public abstract void Justify(TextLine textLine);
}
}

30
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,9 +404,9 @@ namespace Avalonia.Media.TextFormatting
{
endOfLine = textEndOfLine;
textRuns.Add(textRun);
textSourceLength += textEndOfLine.TextSourceLength;
textSourceLength += textRun.TextSourceLength;
textRuns.Add(textRun);
break;
}
@ -431,9 +431,9 @@ namespace Avalonia.Media.TextFormatting
break;
}
case DrawableTextRun drawableTextRun:
default:
{
textRuns.Add(drawableTextRun);
textRuns.Add(textRun);
break;
}
}
@ -552,11 +552,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 +684,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();

31
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -439,7 +439,7 @@ namespace Avalonia.Media.TextFormatting
var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth,
_paragraphProperties, previousLine?.TextLineBreak);
if(textLine == null || textLine.Length == 0)
if(textLine == null || textLine.Length == 0 || textLine.TextRuns.Count == 0 && textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph)
{
if(previousLine != null && previousLine.NewLineLength > 0)
{
@ -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;
}

66
src/Avalonia.Base/Media/TextFormatting/TextLine.cs

@ -15,9 +15,15 @@ namespace Avalonia.Media.TextFormatting
/// The contained text runs.
/// </value>
public abstract IReadOnlyList<TextRun> TextRuns { get; }
/// <summary>
/// Gets the first TextSource position of the current line.
/// </summary>
public abstract int FirstTextSourceIndex { get; }
/// <summary>
/// Gets the total number of TextSource positions of the current line.
/// </summary>
public abstract int Length { get; }
/// <summary>
@ -56,7 +62,7 @@ namespace Avalonia.Media.TextFormatting
/// Gets a value that indicates whether content of the line overflows the specified paragraph width.
/// </summary>
/// <returns>
/// <c>true</c>, it the line overflows the specified paragraph width; otherwise, <c>false</c>.
/// <c>true</c>, the line overflows the specified paragraph width; otherwise, <c>false</c>.
/// </returns>
public abstract bool HasOverflowed { get; }
@ -75,7 +81,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>
@ -149,6 +155,15 @@ namespace Avalonia.Media.TextFormatting
/// </returns>
public abstract TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList);
/// <summary>
/// Create a justified line based on justification text properties.
/// </summary>
/// <param name="justificationProperties">An object that represent the justification text properties.</param>
/// <returns>
/// A <see cref="TextLine"/> value that represents a justified line that can be displayed.
/// </returns>
public abstract void Justify(JustificationProperties justificationProperties);
/// <summary>
/// Gets the character hit corresponding to the specified distance from the beginning of the line.
/// </summary>
@ -192,50 +207,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 Math.Max(0, paragraphWidth - widthIncludingTrailingWhitespace);
default:
return 0;
}
}
switch (textAlignment)
{
case TextAlignment.Center:
return Math.Max(0, (paragraphWidth - width) / 2);
case TextAlignment.Right:
return 0;
default:
return Math.Max(0, paragraphWidth - widthIncludingTrailingWhitespace);
}
}
}
}

77
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -10,10 +10,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;
@ -25,7 +25,7 @@ namespace Avalonia.Media.TextFormatting
_paragraphWidth = paragraphWidth;
_paragraphProperties = paragraphProperties;
_flowDirection = flowDirection;
_resolvedFlowDirection = resolvedFlowDirection;
}
/// <inheritdoc/>
@ -136,7 +136,7 @@ namespace Avalonia.Media.TextFormatting
}
var collapsedLine = new TextLineImpl(collapsedRuns, FirstTextSourceIndex, Length, _paragraphWidth, _paragraphProperties,
_flowDirection, TextLineBreak, true);
_resolvedFlowDirection, TextLineBreak, true);
if (collapsedRuns.Count > 0)
{
@ -144,7 +144,14 @@ namespace Avalonia.Media.TextFormatting
}
return collapsedLine;
}
/// <inheritdoc/>
public override void Justify(JustificationProperties justificationProperties)
{
justificationProperties.Justify(this);
_textLineMetrics = CreateLineMetrics();
}
/// <inheritdoc/>
@ -167,7 +174,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);
}
@ -260,7 +267,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)
{
@ -735,7 +742,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;
@ -743,7 +750,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;
}
@ -762,7 +769,7 @@ namespace Avalonia.Media.TextFormatting
{
var currentRun = _textRuns[i];
var level = GetRunBidiLevel(currentRun, _flowDirection);
var level = GetRunBidiLevel(currentRun, _resolvedFlowDirection);
if (level > max)
{
@ -1242,8 +1249,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))
{
@ -1257,6 +1263,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)

8
src/Avalonia.Controls/TextBlock.cs

@ -113,7 +113,9 @@ namespace Avalonia.Controls
/// Defines the <see cref="TextAlignment"/> property.
/// </summary>
public static readonly AttachedProperty<TextAlignment> TextAlignmentProperty =
AvaloniaProperty.RegisterAttached<TextBlock, Control, TextAlignment>(nameof(TextAlignment),
AvaloniaProperty.RegisterAttached<TextBlock, Control, TextAlignment>(
nameof(TextAlignment),
defaultValue: TextAlignment.Start,
inherits: true);
/// <summary>
@ -748,14 +750,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);

97
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

@ -134,7 +134,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
const string text = "👍 👍 👍 👍";
var textSource = new SingleBufferTextSource(text, defaultProperties);
var formatter = new TextFormatterImpl();
@ -144,7 +144,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
new GenericTextParagraphProperties(defaultProperties));
Assert.Equal(1, textLine.TextRuns.Count);
}
}
}
[Fact]
@ -163,9 +163,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
var firstRun = textLine.TextRuns[0];
Assert.Equal(4, firstRun.Text.Length);
}
}
@ -191,7 +191,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
var textLine =
formatter.FormatLine(textSource, currentPosition, 1,
new GenericTextParagraphProperties(defaultProperties, textWrap : TextWrapping.WrapWithOverflow));
new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.WrapWithOverflow));
if (text.Length - currentPosition > expectedCharactersPerLine)
{
@ -347,8 +347,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
[InlineData("Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor",
new []{ "Lorem ipsum ", "dolor sit amet, ", "consectetur ", "adipisicing elit, ", "sed do eiusmod "})]
[InlineData("Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor",
new[] { "Lorem ipsum ", "dolor sit amet, ", "consectetur ", "adipisicing elit, ", "sed do eiusmod " })]
[Theory]
public void Should_Produce_Wrapped_And_Trimmed_Lines(string text, string[] expectedLines)
@ -368,7 +368,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
new ValueSpan<TextRunProperties>(28, 28,
new GenericTextRunProperties(new Typeface("Verdana", FontStyle.Italic),32))
};
var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, styleSpans);
var formatter = new TextFormatterImpl();
@ -389,19 +389,19 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
if (textLine.Width > 300 || currentHeight + textLine.Height > 240)
{
textLine = textLine.Collapse(new TextTrailingWordEllipsis(new ReadOnlySlice<char>(new[] {TextTrimming.s_defaultEllipsisChar}), 300, defaultProperties));
textLine = textLine.Collapse(new TextTrailingWordEllipsis(new ReadOnlySlice<char>(new[] { TextTrimming.s_defaultEllipsisChar }), 300, defaultProperties));
}
currentHeight += textLine.Height;
var currentText = text.Substring(textLine.FirstTextSourceIndex, textLine.Length);
Assert.Equal(expectedLines[currentLineIndex], currentText);
currentLineIndex++;
}
Assert.Equal(expectedLines.Length,currentLineIndex);
Assert.Equal(expectedLines.Length, currentLineIndex);
}
}
@ -412,11 +412,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
[InlineData("0123456789", TextAlignment.Left, FlowDirection.RightToLeft)]
[InlineData("0123456789", TextAlignment.Center, FlowDirection.RightToLeft)]
[InlineData("0123456789", TextAlignment.Right, FlowDirection.RightToLeft)]
[InlineData("שנבגק", TextAlignment.Left, FlowDirection.RightToLeft)]
[InlineData("שנבגק", TextAlignment.Center, FlowDirection.RightToLeft)]
[InlineData("שנבגק", TextAlignment.Right, FlowDirection.RightToLeft)]
[Theory]
public void Should_Align_TextLine(string text, TextAlignment textAlignment, FlowDirection flowDirection)
{
@ -426,44 +426,29 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var paragraphProperties = new GenericTextParagraphProperties(flowDirection, textAlignment, true, true,
defaultProperties, TextWrapping.NoWrap, 0, 0);
var textSource = new SingleBufferTextSource(text, defaultProperties);
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, 100, paragraphProperties);
var expectedOffset = 0d;
if (flowDirection == FlowDirection.LeftToRight)
switch (textAlignment)
{
switch (textAlignment)
{
case TextAlignment.Center:
expectedOffset = 50 - textLine.Width / 2;
break;
case TextAlignment.Right:
expectedOffset = 100 - textLine.WidthIncludingTrailingWhitespace;
break;
}
}
else
{
switch (textAlignment)
{
case TextAlignment.Left:
expectedOffset = 100 - textLine.WidthIncludingTrailingWhitespace;
break;
case TextAlignment.Center:
expectedOffset = 50 - textLine.Width / 2;
break;
}
case TextAlignment.Center:
expectedOffset = 50 - textLine.Width / 2;
break;
case TextAlignment.Right:
expectedOffset = 100 - textLine.WidthIncludingTrailingWhitespace;
break;
}
Assert.Equal(expectedOffset, textLine.Start);
}
}
[Fact]
public void Should_Wrap_Syriac()
{
@ -488,7 +473,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
formatter.FormatLine(textSource, textPosition, 50, paragraphProperties, lastBreak);
Assert.Equal(textLine.Length, textLine.TextRuns.Sum(x => x.TextSourceLength));
textPosition += textLine.Length;
lastBreak = textLine.TextLineBreak;
@ -503,13 +488,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
var textSource = new SingleBufferTextSource("0123456789_0123456789_0123456789_0123456789", defaultProperties);
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, 33, paragraphProperties);
Assert.NotNull(textLine.TextLineBreak?.RemainingRuns);
}
}
@ -524,12 +509,12 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
using (Start())
{
var formatter = new TextFormatterImpl();
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var paragraphProperties =
new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.NoWrap);
var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
var expectedTextLine = formatter.FormatLine(new SingleBufferTextSource(text, defaultProperties),
@ -548,16 +533,16 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
new ValueSpan<TextRunProperties>(i, j,
new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
};
var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, spans);
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties);
var shapedRuns = textLine.TextRuns.Cast<ShapedTextCharacters>().ToList();
var actualGlyphs = shapedRuns.SelectMany(x => x.GlyphRun.GlyphIndices).ToList();
Assert.Equal(expectedGlyphs, actualGlyphs);
}
}
@ -575,9 +560,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
var textLine =
TextFormatter.Current.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties);
Assert.Equal(3, textLine.TextRuns.Count);
Assert.True(textLine.TextRuns[1] is RectangleRun);
}
}
@ -590,12 +575,12 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var defaultRunProperties = new GenericTextRunProperties(Typeface.Default);
var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties);
var textSource = new EndOfLineTextSource();
var textLine =
TextFormatter.Current.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties);
Assert.NotNull(textLine.TextLineBreak);
Assert.Equal(TextRun.DefaultTextSourceLength, textLine.Length);
}
}
@ -616,7 +601,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
_text = text;
}
public TextRun GetTextRun(int textSourceIndex)
{
if (textSourceIndex >= _text.Length + TextRun.DefaultTextSourceLength + _text.Length)

Loading…
Cancel
Save