Browse Source

Allow combining TextTrimming and TextWrapping

pull/4260/head
Benedikt Schroeder 6 years ago
parent
commit
e87697f901
  1. 15
      src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs
  2. 3
      src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
  3. 23
      src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs
  4. 18
      src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingStyle.cs
  5. 221
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
  6. 62
      src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
  7. 11
      src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
  8. 136
      src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
  9. 11
      src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs
  10. 5
      src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
  11. 33
      src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
  12. 37
      src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs
  13. 1
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs
  14. 16
      src/Avalonia.Visuals/Media/TextWrapping.cs
  15. 12
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
  16. 58
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

15
src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs

@ -4,14 +4,12 @@
{
private TextAlignment _textAlignment;
private TextWrapping _textWrapping;
private TextTrimming _textTrimming;
private double _lineHeight;
public GenericTextParagraphProperties(
TextRunProperties defaultTextRunProperties,
TextAlignment textAlignment = TextAlignment.Left,
TextWrapping textWrapping = TextWrapping.WrapWithOverflow,
TextTrimming textTrimming = TextTrimming.None,
TextWrapping textWrapping = TextWrapping.NoWrap,
double lineHeight = 0)
{
DefaultTextRunProperties = defaultTextRunProperties;
@ -20,8 +18,6 @@
_textWrapping = textWrapping;
_textTrimming = textTrimming;
_lineHeight = lineHeight;
}
@ -31,8 +27,6 @@
public override TextWrapping TextWrapping => _textWrapping;
public override TextTrimming TextTrimming => _textTrimming;
public override double LineHeight => _lineHeight;
/// <summary>
@ -50,13 +44,6 @@
{
_textWrapping = textWrapping;
}
/// <summary>
/// Set text trimming
/// </summary>
internal void SetTextTrimming(TextTrimming textTrimming)
{
_textTrimming = textTrimming;
}
/// <summary>
/// Set line height

3
src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs

@ -1,5 +1,4 @@
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{

23
src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs

@ -0,0 +1,23 @@
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Properties of text collapsing
/// </summary>
public abstract class TextCollapsingProperties
{
/// <summary>
/// Gets the width in which the collapsible range is constrained to
/// </summary>
public abstract double Width { get; }
/// <summary>
/// Gets the text run that is used as collapsing symbol
/// </summary>
public abstract TextRun Symbol { get; }
/// <summary>
/// Gets the style of collapsing
/// </summary>
public abstract TextCollapsingStyle Style { get; }
}
}

18
src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingStyle.cs

@ -0,0 +1,18 @@
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Text collapsing style
/// </summary>
public enum TextCollapsingStyle
{
/// <summary>
/// Collapse trailing characters
/// </summary>
TrailingCharacter,
/// <summary>
/// Collapse trailing words
/// </summary>
TrailingWord,
}
}

221
src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs

@ -1,49 +1,41 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
internal class TextFormatterImpl : TextFormatter
{
private static readonly ReadOnlySlice<char> s_ellipsis = new ReadOnlySlice<char>(new[] { '\u2026' });
/// <inheritdoc cref="TextFormatter.FormatLine"/>
public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
TextParagraphProperties paragraphProperties, TextLineBreak previousLineBreak = null)
{
var textTrimming = paragraphProperties.TextTrimming;
var textWrapping = paragraphProperties.TextWrapping;
TextLine textLine = null;
var textRuns = FetchTextRuns(textSource, firstTextSourceIndex, previousLineBreak, out var nextLineBreak);
var textRange = GetTextRange(textRuns);
if (textTrimming != TextTrimming.None)
{
textLine = PerformTextTrimming(textRuns, textRange, paragraphWidth, paragraphProperties);
}
else
TextLine textLine;
switch (textWrapping)
{
switch (textWrapping)
{
case TextWrapping.NoWrap:
{
var textLineMetrics =
TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties);
case TextWrapping.NoWrap:
{
var textLineMetrics =
TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties);
textLine = new TextLineImpl(textRuns, textLineMetrics, nextLineBreak);
break;
}
case TextWrapping.WrapWithOverflow:
case TextWrapping.Wrap:
{
textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties);
break;
}
}
textLine = new TextLineImpl(textRuns, textLineMetrics, nextLineBreak);
break;
}
case TextWrapping.WrapWithOverflow:
case TextWrapping.Wrap:
{
textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties);
break;
}
default:
throw new ArgumentOutOfRangeException();
}
return textLine;
@ -174,87 +166,6 @@ namespace Avalonia.Media.TextFormatting
return false;
}
/// <summary>
/// Performs text trimming and returns a trimmed line.
/// </summary>
/// <param name="textRuns">The text runs to perform the trimming on.</param>
/// <param name="textRange">The text range that is covered by the text runs.</param>
/// <param name="paragraphWidth">A <see cref="double"/> value that specifies the width of the paragraph that the line fills.</param>
/// <param name="paragraphProperties">A <see cref="TextParagraphProperties"/> value that represents paragraph properties,
/// such as TextWrapping, TextAlignment, or TextStyle.</param>
/// <returns></returns>
private static TextLine PerformTextTrimming(IReadOnlyList<ShapedTextCharacters> textRuns, TextRange textRange,
double paragraphWidth, TextParagraphProperties paragraphProperties)
{
var textTrimming = paragraphProperties.TextTrimming;
var availableWidth = paragraphWidth;
var currentWidth = 0.0;
var runIndex = 0;
while (runIndex < textRuns.Count)
{
var currentRun = textRuns[runIndex];
currentWidth += currentRun.GlyphRun.Bounds.Width;
if (currentWidth > availableWidth)
{
var ellipsisRun = CreateEllipsisRun(currentRun.Properties);
var measuredLength = MeasureText(currentRun, availableWidth - ellipsisRun.GlyphRun.Bounds.Width);
if (textTrimming == TextTrimming.WordEllipsis)
{
if (measuredLength < textRange.End)
{
var currentBreakPosition = 0;
var lineBreaker = new LineBreakEnumerator(currentRun.Text);
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
{
var nextBreakPosition = lineBreaker.Current.PositionWrap;
if (nextBreakPosition == 0)
{
break;
}
if (nextBreakPosition > measuredLength)
{
break;
}
currentBreakPosition = nextBreakPosition;
}
measuredLength = currentBreakPosition;
}
}
var splitResult = SplitTextRuns(textRuns, measuredLength);
var trimmedRuns = new List<ShapedTextCharacters>(splitResult.First.Count + 1);
trimmedRuns.AddRange(splitResult.First);
trimmedRuns.Add(ellipsisRun);
var textLineMetrics =
TextLineMetrics.Create(trimmedRuns, textRange, paragraphWidth, paragraphProperties);
return new TextLineImpl(trimmedRuns, textLineMetrics);
}
availableWidth -= currentRun.GlyphRun.Bounds.Width;
runIndex++;
}
return new TextLineImpl(textRuns,
TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties));
}
/// <summary>
/// Performs text wrapping returns a list of text lines.
/// </summary>
@ -269,7 +180,7 @@ namespace Avalonia.Media.TextFormatting
var availableWidth = paragraphWidth;
var currentWidth = 0.0;
var runIndex = 0;
var length = 0;
var currentLength = 0;
while (runIndex < textRuns.Count)
{
@ -279,58 +190,53 @@ namespace Avalonia.Media.TextFormatting
{
var measuredLength = MeasureText(currentRun, paragraphWidth - currentWidth);
var breakFound = false;
var currentBreakPosition = 0;
if (measuredLength < currentRun.Text.Length)
{
if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow)
{
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Skip(measuredLength));
var lineBreaker = new LineBreakEnumerator(currentRun.Text);
if (lineBreaker.MoveNext())
{
measuredLength += lineBreaker.Current.PositionWrap;
}
else
{
measuredLength = currentRun.Text.Length;
}
}
else
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
{
var currentBreakPosition = -1;
var nextBreakPosition = lineBreaker.Current.PositionWrap;
var lineBreaker = new LineBreakEnumerator(currentRun.Text);
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
if (nextBreakPosition == 0 || nextBreakPosition > measuredLength)
{
var nextBreakPosition = lineBreaker.Current.PositionWrap;
break;
}
if (nextBreakPosition == 0)
{
break;
}
breakFound = lineBreaker.Current.Required ||
lineBreaker.Current.PositionWrap != currentRun.Text.Length;
if (nextBreakPosition > measuredLength)
{
break;
}
currentBreakPosition = nextBreakPosition;
}
}
currentBreakPosition = nextBreakPosition;
}
if (breakFound)
{
measuredLength = currentBreakPosition;
}
else
{
if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow)
{
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Skip(currentBreakPosition));
if (currentBreakPosition != -1)
if (lineBreaker.MoveNext())
{
measuredLength = currentBreakPosition;
measuredLength = currentBreakPosition + lineBreaker.Current.PositionWrap;
}
}
}
length += measuredLength;
currentLength += measuredLength;
var splitResult = SplitTextRuns(textRuns, length);
var splitResult = SplitTextRuns(textRuns, currentLength);
var textLineMetrics = TextLineMetrics.Create(splitResult.First,
new TextRange(textRange.Start, length), paragraphWidth, paragraphProperties);
new TextRange(textRange.Start, currentLength), paragraphWidth, paragraphProperties);
var lineBreak = splitResult.Second != null && splitResult.Second.Count > 0 ?
new TextLineBreak(splitResult.Second) :
@ -341,7 +247,7 @@ namespace Avalonia.Media.TextFormatting
currentWidth += currentRun.GlyphRun.Bounds.Width;
length += currentRun.GlyphRun.Characters.Length;
currentLength += currentRun.GlyphRun.Characters.Length;
runIndex++;
}
@ -356,7 +262,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="textCharacters">The text run.</param>
/// <param name="availableWidth">The available width.</param>
/// <returns></returns>
private static int MeasureText(ShapedTextCharacters textCharacters, double availableWidth)
internal static int MeasureText(ShapedTextCharacters textCharacters, double availableWidth)
{
var glyphRun = textCharacters.GlyphRun;
@ -391,10 +297,8 @@ namespace Avalonia.Media.TextFormatting
}
else
{
for (var i = 0; i < glyphRun.GlyphAdvances.Length; i++)
foreach (var advance in glyphRun.GlyphAdvances)
{
var advance = glyphRun.GlyphAdvances[i];
if (currentWidth + advance > availableWidth)
{
break;
@ -423,21 +327,6 @@ namespace Avalonia.Media.TextFormatting
return lastCluster - firstCluster;
}
/// <summary>
/// Creates an ellipsis.
/// </summary>
/// <param name="properties">The text run properties.</param>
/// <returns></returns>
private static ShapedTextCharacters CreateEllipsisRun(TextRunProperties properties)
{
var formatterImpl = AvaloniaLocator.Current.GetService<ITextShaperImpl>();
var glyphRun = formatterImpl.ShapeText(s_ellipsis, properties.Typeface, properties.FontRenderingEmSize,
properties.CultureInfo);
return new ShapedTextCharacters(glyphRun, properties);
}
/// <summary>
/// Gets the text range that is covered by the text runs.
/// </summary>
@ -470,7 +359,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="textRuns">The text run's.</param>
/// <param name="length">The length to split at.</param>
/// <returns>The split text runs.</returns>
private static SplitTextRunsResult SplitTextRuns(IReadOnlyList<ShapedTextCharacters> textRuns, int length)
internal static SplitTextRunsResult SplitTextRuns(IReadOnlyList<ShapedTextCharacters> textRuns, int length)
{
var currentLength = 0;
@ -543,7 +432,7 @@ namespace Avalonia.Media.TextFormatting
return new SplitTextRunsResult(textRuns, null);
}
private readonly struct SplitTextRunsResult
internal readonly struct SplitTextRunsResult
{
public SplitTextRunsResult(IReadOnlyList<ShapedTextCharacters> first, IReadOnlyList<ShapedTextCharacters> second)
{

62
src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs

@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
using Avalonia.Platform;
namespace Avalonia.Media.TextFormatting
{
@ -17,6 +15,7 @@ namespace Avalonia.Media.TextFormatting
private readonly ReadOnlySlice<char> _text;
private readonly TextParagraphProperties _paragraphProperties;
private readonly IReadOnlyList<ValueSpan<TextRunProperties>> _textStyleOverrides;
private readonly TextTrimming _textTrimming;
/// <summary>
/// Initializes a new instance of the <see cref="TextLayout" /> class.
@ -54,9 +53,11 @@ namespace Avalonia.Media.TextFormatting
new ReadOnlySlice<char>(text.AsMemory());
_paragraphProperties =
CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping, textTrimming,
CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping,
textDecorations, lineHeight);
_textTrimming = textTrimming;
_textStyleOverrides = textStyleOverrides;
LineHeight = lineHeight;
@ -143,18 +144,16 @@ namespace Avalonia.Media.TextFormatting
/// <param name="foreground">The foreground.</param>
/// <param name="textAlignment">The text alignment.</param>
/// <param name="textWrapping">The text wrapping.</param>
/// <param name="textTrimming">The text trimming.</param>
/// <param name="textDecorations">The text decorations.</param>
/// <param name="lineHeight">The height of each line of text.</param>
/// <returns></returns>
private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize,
IBrush foreground, TextAlignment textAlignment, TextWrapping textWrapping, TextTrimming textTrimming,
IBrush foreground, TextAlignment textAlignment, TextWrapping textWrapping,
TextDecorationCollection textDecorations, double lineHeight)
{
var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground);
return new GenericTextParagraphProperties(textRunStyle, textAlignment, textWrapping, textTrimming,
lineHeight);
return new GenericTextParagraphProperties(textRunStyle, textAlignment, textWrapping, lineHeight);
}
/// <summary>
@ -214,25 +213,44 @@ namespace Avalonia.Media.TextFormatting
var textSource = new FormattedTextSource(_text,
_paragraphProperties.DefaultTextRunProperties, _textStyleOverrides);
TextLineBreak previousLineBreak = null;
TextLine previousLine = null;
while (currentPosition < _text.Length && (MaxLines == 0 || textLines.Count < MaxLines))
while (currentPosition < _text.Length)
{
var textLine = TextFormatter.Current.FormatLine(textSource, currentPosition, MaxWidth,
_paragraphProperties, previousLineBreak);
_paragraphProperties, previousLine?.LineBreak);
previousLineBreak = textLine.LineBreak;
currentPosition += textLine.TextRange.Length;
textLines.Add(textLine);
if (textLines.Count > 0)
{
if (textLines.Count == MaxLines || !double.IsPositiveInfinity(MaxHeight) &&
height + textLine.LineMetrics.Size.Height > MaxHeight)
{
if (previousLine?.LineBreak != null && _textTrimming != TextTrimming.None)
{
var collapsedLine =
previousLine.Collapse(GetCollapsingProperties(MaxWidth));
UpdateBounds(textLine, ref width, ref height);
textLines[textLines.Count - 1] = collapsedLine;
}
break;
}
}
if (!double.IsPositiveInfinity(MaxHeight) && height > MaxHeight)
var hasOverflowed = textLine.LineMetrics.HasOverflowed;
if (hasOverflowed && _textTrimming != TextTrimming.None)
{
break;
textLine = textLine.Collapse(GetCollapsingProperties(MaxWidth));
}
currentPosition += textLine.TextRange.Length;
textLines.Add(textLine);
UpdateBounds(textLine, ref width, ref height);
previousLine = textLine;
if (currentPosition != _text.Length || textLine.LineBreak == null)
{
@ -250,6 +268,18 @@ namespace Avalonia.Media.TextFormatting
}
}
private TextCollapsingProperties GetCollapsingProperties(double width)
{
return _textTrimming switch
{
TextTrimming.CharacterEllipsis => new TextTrailingCharacterEllipsis(width,
_paragraphProperties.DefaultTextRunProperties),
TextTrimming.WordEllipsis => new TextTrailingWordEllipsis(width,
_paragraphProperties.DefaultTextRunProperties),
_ => throw new ArgumentOutOfRangeException(),
};
}
private readonly struct FormattedTextSource : ITextSource
{
private readonly ReadOnlySlice<char> _text;

11
src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs

@ -39,6 +39,11 @@ namespace Avalonia.Media.TextFormatting
/// </returns>
public abstract TextLineBreak LineBreak { get; }
/// <summary>
/// Client to get a boolean value indicates whether a line has been collapsed
/// </summary>
public abstract bool HasCollapsed { get; }
/// <summary>
/// Draws the <see cref="TextLine"/> at the given origin.
/// </summary>
@ -46,6 +51,12 @@ namespace Avalonia.Media.TextFormatting
/// <param name="origin">The origin.</param>
public abstract void Draw(DrawingContext drawingContext, Point origin);
/// <summary>
/// Client to collapse the line and get a collapsed line that fits for display
/// </summary>
/// <param name="collapsingPropertiesList">a list of collapsing properties</param>
public abstract TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList);
/// <summary>
/// Client to get the character hit corresponding to the specified
/// distance from the beginning of the line.

136
src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs

@ -1,4 +1,6 @@
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
namespace Avalonia.Media.TextFormatting
{
@ -7,11 +9,12 @@ namespace Avalonia.Media.TextFormatting
private readonly IReadOnlyList<ShapedTextCharacters> _textRuns;
public TextLineImpl(IReadOnlyList<ShapedTextCharacters> textRuns, TextLineMetrics lineMetrics,
TextLineBreak lineBreak = null)
TextLineBreak lineBreak = null, bool hasCollapsed = false)
{
_textRuns = textRuns;
LineMetrics = lineMetrics;
LineBreak = lineBreak;
HasCollapsed = hasCollapsed;
}
/// <inheritdoc/>
@ -26,6 +29,9 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/>
public override TextLineBreak LineBreak { get; }
/// <inheritdoc/>
public override bool HasCollapsed { get; }
/// <inheritdoc/>
public override void Draw(DrawingContext drawingContext, Point origin)
{
@ -41,6 +47,98 @@ namespace Avalonia.Media.TextFormatting
}
}
public override TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList)
{
if (collapsingPropertiesList == null || collapsingPropertiesList.Length == 0)
{
return this;
}
var collapsingProperties = collapsingPropertiesList[0];
var runIndex = 0;
var currentWidth = 0.0;
var textRange = TextRange;
var collapsedLength = 0;
TextLineMetrics textLineMetrics;
var shapedSymbol = CreateShapedSymbol(collapsingProperties.Symbol);
var availableWidth = collapsingProperties.Width - shapedSymbol.Bounds.Width;
while (runIndex < _textRuns.Count)
{
var currentRun = _textRuns[runIndex];
currentWidth += currentRun.GlyphRun.Bounds.Width;
if (currentWidth > availableWidth)
{
var measuredLength = TextFormatterImpl.MeasureText(currentRun, availableWidth);
var currentBreakPosition = 0;
if (measuredLength < textRange.End)
{
var lineBreaker = new LineBreakEnumerator(currentRun.Text);
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
{
var nextBreakPosition = lineBreaker.Current.PositionWrap;
if (nextBreakPosition == 0)
{
break;
}
if (nextBreakPosition > measuredLength)
{
break;
}
currentBreakPosition = nextBreakPosition;
}
}
if (collapsingProperties.Style == TextCollapsingStyle.TrailingWord)
{
measuredLength = currentBreakPosition;
}
collapsedLength += measuredLength;
var splitResult = TextFormatterImpl.SplitTextRuns(_textRuns, collapsedLength);
var shapedTextCharacters = new List<ShapedTextCharacters>(splitResult.First.Count + 1);
shapedTextCharacters.AddRange(splitResult.First);
shapedTextCharacters.Add(shapedSymbol);
textRange = new TextRange(textRange.Start, collapsedLength);
var shapedWidth = GetShapedWidth(shapedTextCharacters);
textLineMetrics = new TextLineMetrics(new Size(shapedWidth, LineMetrics.Size.Height),
LineMetrics.TextBaseline, textRange, false);
return new TextLineImpl(shapedTextCharacters, textLineMetrics, LineBreak, true);
}
availableWidth -= currentRun.GlyphRun.Bounds.Width;
collapsedLength += currentRun.GlyphRun.Characters.Length;
runIndex++;
}
textLineMetrics =
new TextLineMetrics(LineMetrics.Size.WithWidth(LineMetrics.Size.Width + shapedSymbol.Bounds.Width),
LineMetrics.TextBaseline, TextRange, LineMetrics.HasOverflowed);
return new TextLineImpl(new List<ShapedTextCharacters>(_textRuns) { shapedSymbol }, textLineMetrics, null,
true);
}
/// <inheritdoc/>
public override CharacterHit GetCharacterHitFromDistance(double distance)
{
@ -230,5 +328,41 @@ namespace Avalonia.Media.TextFormatting
return runIndex;
}
/// <summary>
/// Creates a shaped symbol.
/// </summary>
/// <param name="textRun">The symbol run to shape.</param>
/// <returns>
/// The shaped symbol.
/// </returns>
internal static ShapedTextCharacters CreateShapedSymbol(TextRun textRun)
{
var formatterImpl = AvaloniaLocator.Current.GetService<ITextShaperImpl>();
var glyphRun = formatterImpl.ShapeText(textRun.Text, textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize,
textRun.Properties.CultureInfo);
return new ShapedTextCharacters(glyphRun, textRun.Properties);
}
/// <summary>
/// Gets the shaped width of specified shaped text characters.
/// </summary>
/// <param name="shapedTextCharacters">The shaped text characters.</param>
/// <returns>
/// The shaped width.
/// </returns>
private static double GetShapedWidth(IReadOnlyList<ShapedTextCharacters> shapedTextCharacters)
{
var shapedWidth = 0.0;
for (var i = 0; i < shapedTextCharacters.Count; i++)
{
shapedWidth += shapedTextCharacters[i].Bounds.Width;
}
return shapedWidth;
}
}
}

11
src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs

@ -9,11 +9,12 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public readonly struct TextLineMetrics
{
public TextLineMetrics(Size size, double textBaseline, TextRange textRange)
public TextLineMetrics(Size size, double textBaseline, TextRange textRange, bool hasOverflowed)
{
Size = size;
TextBaseline = textBaseline;
TextRange = textRange;
HasOverflowed = hasOverflowed;
}
/// <summary>
@ -37,6 +38,12 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public double TextBaseline { get; }
/// <summary>
/// Gets a boolean value that indicates whether content of the line overflows
/// the specified paragraph width.
/// </summary>
public bool HasOverflowed { get; }
/// <summary>
/// Creates the text line metrics.
/// </summary>
@ -83,7 +90,7 @@ namespace Avalonia.Media.TextFormatting
descent - ascent + lineGap :
paragraphProperties.LineHeight);
return new TextLineMetrics(size, -ascent, textRange);
return new TextLineMetrics(size, -ascent, textRange, size.Width > paragraphWidth);
}
}
}

5
src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs

@ -26,11 +26,6 @@
/// </summary>
public abstract TextWrapping TextWrapping { get; }
/// <summary>
/// Gets the text trimming.
/// </summary>
public abstract TextTrimming TextTrimming { get; }
/// <summary>
/// Paragraph's line height
/// </summary>

33
src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs

@ -0,0 +1,33 @@
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// a collapsing properties to collapse whole line toward the end
/// at character granularity and with ellipsis being the collapsing symbol
/// </summary>
public class TextTrailingCharacterEllipsis : TextCollapsingProperties
{
private static readonly ReadOnlySlice<char> s_ellipsis = new ReadOnlySlice<char>(new[] { '\u2026' });
/// <summary>
/// Construct a text trailing character ellipsis collapsing properties
/// </summary>
/// <param name="width">width in which collapsing is constrained to</param>
/// <param name="textRunProperties">text run properties of ellispis symbol</param>
public TextTrailingCharacterEllipsis(double width, TextRunProperties textRunProperties)
{
Width = width;
Symbol = new TextCharacters(s_ellipsis, textRunProperties);
}
/// <inheritdoc/>
public sealed override double Width { get; }
/// <inheritdoc/>
public sealed override TextRun Symbol { get; }
/// <inheritdoc/>
public sealed override TextCollapsingStyle Style { get; } = TextCollapsingStyle.TrailingCharacter;
}
}

37
src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs

@ -0,0 +1,37 @@
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// a collapsing properties to collapse whole line toward the end
/// at word granularity and with ellipsis being the collapsing symbol
/// </summary>
public class TextTrailingWordEllipsis : TextCollapsingProperties
{
private static readonly ReadOnlySlice<char> s_ellipsis = new ReadOnlySlice<char>(new[] { '\u2026' });
/// <summary>
/// Construct a text trailing word ellipsis collapsing properties
/// </summary>
/// <param name="width">width in which collapsing is constrained to</param>
/// <param name="textRunProperties">text run properties of ellispis symbol</param>
public TextTrailingWordEllipsis(
double width,
TextRunProperties textRunProperties
)
{
Width = width;
Symbol = new TextCharacters(s_ellipsis, textRunProperties);
}
/// <inheritdoc/>
public sealed override double Width { get; }
/// <inheritdoc/>
public sealed override TextRun Symbol { get; }
/// <inheritdoc/>
public sealed override TextCollapsingStyle Style { get; } = TextCollapsingStyle.TrailingWord;
}
}

1
src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs

@ -109,7 +109,6 @@ namespace Avalonia.Media.TextFormatting.Unicode
{
case PairBreakType.DI: // Direct break
shouldBreak = true;
_lastPos = _pos;
break;
case PairBreakType.IN: // possible indirect break

16
src/Avalonia.Visuals/Media/TextWrapping.cs

@ -5,13 +5,6 @@ namespace Avalonia.Media
/// </summary>
public enum TextWrapping
{
/// <summary>
/// Line-breaking occurs if the line overflows the available block width.
/// However, a line may overflow the block width if the line breaking algorithm
/// cannot determine a break opportunity, as in the case of a very long word.
/// </summary>
WrapWithOverflow,
/// <summary>
/// Text should not wrap.
/// </summary>
@ -20,6 +13,13 @@ namespace Avalonia.Media
/// <summary>
/// Text can wrap.
/// </summary>
Wrap
Wrap,
/// <summary>
/// Line-breaking occurs if the line overflows the available block width.
/// However, a line may overflow the block width if the line breaking algorithm
/// cannot determine a break opportunity, as in the case of a very long word.
/// </summary>
WrapWithOverflow
}
}

12
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

@ -490,10 +490,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
[InlineData("0123456789\r0123456789", 2)]
[InlineData("0123456789", 1)]
[InlineData("0123456789\r0123456789")]
[InlineData("0123456789")]
[Theory]
public void Should_Include_Last_Line_When_Constraint_Is_Surpassed(string text, int numberOfLines)
public void Should_Include_First_Line_When_Constraint_Is_Surpassed(string text)
{
using (Start())
{
@ -508,11 +508,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Typeface.Default,
12,
Brushes.Black.ToImmutable(),
maxHeight: lineHeight * numberOfLines - lineHeight * 0.5);
maxHeight: lineHeight - lineHeight * 0.5);
Assert.Equal(numberOfLines, layout.TextLines.Count);
Assert.Equal(1, layout.TextLines.Count);
Assert.Equal(numberOfLines * lineHeight, layout.Size.Height);
Assert.Equal(lineHeight, layout.Size.Height);
}
}

58
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@ -162,6 +162,64 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
[InlineData("01234 01234", 8, TextCollapsingStyle.TrailingCharacter, "01234 0\u2026")]
[InlineData("01234 01234", 8, TextCollapsingStyle.TrailingWord, "01234 \u2026")]
[Theory]
public void Should_Collapse_Line(string text, int numberOfCharacters, TextCollapsingStyle style, string expected)
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var textSource = new SingleBufferTextSource(text, defaultProperties);
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
Assert.False(textLine.HasCollapsed);
var glyphTypeface = Typeface.Default.GlyphTypeface;
var scale = defaultProperties.FontRenderingEmSize / glyphTypeface.DesignEmHeight;
var width = 1.0;
for (var i = 0; i < numberOfCharacters; i++)
{
var glyph = glyphTypeface.GetGlyph(text[i]);
width += glyphTypeface.GetGlyphAdvance(glyph) * scale;
}
TextCollapsingProperties collapsingProperties;
if (style == TextCollapsingStyle.TrailingCharacter)
{
collapsingProperties = new TextTrailingCharacterEllipsis(width, defaultProperties);
}
else
{
collapsingProperties = new TextTrailingWordEllipsis(width, defaultProperties);
}
var collapsedLine = textLine.Collapse(collapsingProperties);
Assert.True(collapsedLine.HasCollapsed);
var trimmedText = collapsedLine.TextRuns.SelectMany(x => x.Text).ToArray();
Assert.Equal(expected.Length, trimmedText.Length);
for (var i = 0; i < expected.Length; i++)
{
Assert.Equal(expected[i], trimmedText[i]);
}
}
}
private static IDisposable Start()
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface

Loading…
Cancel
Save