Browse Source

Refactor text trimming and implement prefix trimming.

pull/7322/head
Dariusz Komosinski 5 years ago
parent
commit
6bcf9c2973
  1. 2
      src/Avalonia.Controls/TextBlock.cs
  2. 19
      src/Avalonia.Visuals/Media/FormattedText.cs
  3. 16
      src/Avalonia.Visuals/Media/TextCollapsingCreateInfo.cs
  4. 23
      src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
  5. 9
      src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs
  6. 18
      src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingStyle.cs
  7. 99
      src/Avalonia.Visuals/Media/TextFormatting/TextEllipsisHelper.cs
  8. 13
      src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
  9. 126
      src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  10. 79
      src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
  11. 17
      src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
  12. 15
      src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs
  13. 31
      src/Avalonia.Visuals/Media/TextLeadingPrefixTrimming.cs
  14. 18
      src/Avalonia.Visuals/Media/TextNoneTrimming.cs
  15. 36
      src/Avalonia.Visuals/Media/TextTrailingTrimming.cs
  16. 58
      src/Avalonia.Visuals/Media/TextTrimming.cs
  17. 26
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
  18. 6
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  19. 2
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  20. 38
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

2
src/Avalonia.Controls/TextBlock.cs

@ -134,7 +134,7 @@ namespace Avalonia.Controls
/// Defines the <see cref="TextTrimming"/> property.
/// </summary>
public static readonly StyledProperty<TextTrimming> TextTrimmingProperty =
AvaloniaProperty.Register<TextBlock, TextTrimming>(nameof(TextTrimming));
AvaloniaProperty.Register<TextBlock, TextTrimming>(nameof(TextTrimming), defaultValue: TextTrimming.None);
/// <summary>
/// Defines the <see cref="TextDecorations"/> property.

19
src/Avalonia.Visuals/Media/FormattedText.cs

@ -854,19 +854,9 @@ namespace Avalonia.Media
var lastRunProps = (GenericTextRunProperties)thatFormatRider.CurrentElement!;
TextCollapsingProperties trailingEllipsis;
TextCollapsingProperties collapsingProperties = _that._trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(maxLineLength, lastRunProps));
if (_that._trimming == TextTrimming.CharacterEllipsis)
{
trailingEllipsis = new TextTrailingCharacterEllipsis(maxLineLength, lastRunProps);
}
else
{
Debug.Assert(_that._trimming == TextTrimming.WordEllipsis);
trailingEllipsis = new TextTrailingWordEllipsis(maxLineLength, lastRunProps);
}
var collapsedLine = line.Collapse(trailingEllipsis);
var collapsedLine = line.Collapse(collapsingProperties);
line = collapsedLine;
}
@ -1121,11 +1111,6 @@ namespace Avalonia.Media
{
set
{
if ((int)value < 0 || (int)value > (int)TextTrimming.WordEllipsis)
{
throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(TextTrimming));
}
_trimming = value;
_defaultParaProps.SetTextWrapping(_trimming == TextTrimming.None ?

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

@ -0,0 +1,16 @@
using Avalonia.Media.TextFormatting;
namespace Avalonia.Media
{
public readonly struct TextCollapsingCreateInfo
{
public readonly double Width;
public readonly TextRunProperties TextRunProperties;
public TextCollapsingCreateInfo(double width, TextRunProperties textRunProperties)
{
Width = width;
TextRunProperties = textRunProperties;
}
}
}

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

@ -132,6 +132,29 @@ namespace Avalonia.Media.TextFormatting
return length > 0;
}
internal bool TryMeasureCharactersBackwards(double availableWidth, out int length, out double width)
{
length = 0;
width = 0;
for (var i = ShapedBuffer.Length - 1; i >= 0; i--)
{
var advance = ShapedBuffer.GlyphAdvances[i];
if (width + advance > availableWidth)
{
break;
}
Codepoint.ReadAt(GlyphRun.Characters, length, out var count);
length += count;
width += advance;
}
return length > 0;
}
internal SplitResult<ShapedTextCharacters> Split(int length)
{
if (IsReversed)

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

@ -1,4 +1,6 @@
namespace Avalonia.Media.TextFormatting
using System.Collections.Generic;
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Properties of text collapsing
@ -16,8 +18,9 @@
public abstract TextRun Symbol { get; }
/// <summary>
/// Gets the style of collapsing
/// Collapses given text line.
/// </summary>
public abstract TextCollapsingStyle Style { get; }
/// <param name="textLine">Text line to collapse.</param>
public abstract IReadOnlyList<TextRun>? Collapse(TextLine textLine);
}
}

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

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

99
src/Avalonia.Visuals/Media/TextFormatting/TextEllipsisHelper.cs

@ -0,0 +1,99 @@
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode;
namespace Avalonia.Media.TextFormatting
{
internal class TextEllipsisHelper
{
public static List<ShapedTextCharacters>? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis)
{
var shapedTextRuns = textLine.TextRuns as List<ShapedTextCharacters>;
if (shapedTextRuns is null)
{
return null;
}
var runIndex = 0;
var currentWidth = 0.0;
var collapsedLength = 0;
var textRange = textLine.TextRange;
// TODO: From where this is supposed to come now?
var flowDirection = FlowDirection.LeftToRight;
var shapedSymbol = TextFormatterImpl.CreateSymbol(properties.Symbol, flowDirection);
if (properties.Width < shapedSymbol.GlyphRun.Size.Width)
{
return new List<ShapedTextCharacters>(0);
}
var availableWidth = properties.Width - shapedSymbol.Size.Width;
while (runIndex < shapedTextRuns.Count)
{
var currentRun = shapedTextRuns[runIndex];
currentWidth += currentRun.Size.Width;
if (currentWidth > availableWidth)
{
if (currentRun.TryMeasureCharacters(availableWidth, out var measuredLength))
{
if (isWordEllipsis && measuredLength < textRange.End)
{
var currentBreakPosition = 0;
var lineBreaker = new LineBreakEnumerator(currentRun.Text);
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
{
var nextBreakPosition = lineBreaker.Current.PositionMeasure;
if (nextBreakPosition == 0)
{
break;
}
if (nextBreakPosition >= measuredLength)
{
break;
}
currentBreakPosition = nextBreakPosition;
}
measuredLength = currentBreakPosition;
}
}
collapsedLength += measuredLength;
var shapedTextCharacters = new List<ShapedTextCharacters>(shapedTextRuns.Count);
if (collapsedLength > 0)
{
var splitResult = TextFormatterImpl.SplitShapedRuns(shapedTextRuns, collapsedLength);
shapedTextCharacters.AddRange(splitResult.First);
TextLineImpl.SortRuns(shapedTextCharacters);
}
shapedTextCharacters.Add(shapedSymbol);
return shapedTextCharacters;
}
availableWidth -= currentRun.Size.Width;
collapsedLength += currentRun.GlyphRun.Characters.Length;
runIndex++;
}
return null;
}
}
}

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

@ -41,7 +41,7 @@ namespace Avalonia.Media.TextFormatting
IBrush? foreground,
TextAlignment textAlignment = TextAlignment.Left,
TextWrapping textWrapping = TextWrapping.NoWrap,
TextTrimming textTrimming = TextTrimming.None,
TextTrimming? textTrimming = null,
TextDecorationCollection? textDecorations = null,
FlowDirection flowDirection = FlowDirection.LeftToRight,
double maxWidth = double.PositiveInfinity,
@ -58,7 +58,7 @@ namespace Avalonia.Media.TextFormatting
CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping,
textDecorations, flowDirection, lineHeight);
_textTrimming = textTrimming;
_textTrimming = textTrimming ?? TextTrimming.None;
_textStyleOverrides = textStyleOverrides;
@ -641,14 +641,7 @@ namespace Avalonia.Media.TextFormatting
/// <returns>The <see cref="TextCollapsingProperties"/>.</returns>
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(),
};
return _textTrimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties));
}
}
}

126
src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs

@ -0,0 +1,126 @@
using System;
using System.Collections.Generic;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Ellipsis based on a fixed length leading prefix and suffix growing from the end.
/// </summary>
public class TextLeadingPrefixEllipsis : TextCollapsingProperties
{
private readonly int _prefixLength;
/// <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 TextLeadingPrefixEllipsis(
ReadOnlySlice<char> ellipsis,
int prefixLength,
double width,
TextRunProperties textRunProperties
)
{
_prefixLength = prefixLength;
Width = width;
Symbol = new TextCharacters(ellipsis, textRunProperties);
}
/// <inheritdoc/>
public sealed override double Width { get; }
/// <inheritdoc/>
public sealed override TextRun Symbol { get; }
public override IReadOnlyList<TextRun>? Collapse(TextLine textLine)
{
var shapedTextRuns = textLine.TextRuns as List<ShapedTextCharacters>;
if (shapedTextRuns is null)
{
return null;
}
var runIndex = 0;
var currentWidth = 0.0;
// TODO: From where this is supposed to come now?
var flowDirection = FlowDirection.LeftToRight;
var shapedSymbol = TextFormatterImpl.CreateSymbol(Symbol, flowDirection);
if (Width < shapedSymbol.GlyphRun.Size.Width)
{
return new List<ShapedTextCharacters>(0);
}
var availableWidth = Width - shapedSymbol.Size.Width;
while (runIndex < shapedTextRuns.Count)
{
var currentRun = shapedTextRuns[runIndex];
currentWidth += currentRun.Size.Width;
if (currentWidth > availableWidth)
{
currentRun.TryMeasureCharacters(availableWidth, out var measuredLength);
var shapedTextCharacters = new List<ShapedTextCharacters>(shapedTextRuns.Count);
if (measuredLength > 0)
{
var splitResult = TextFormatterImpl.SplitShapedRuns(shapedTextRuns, Math.Min(_prefixLength, measuredLength));
shapedTextCharacters.AddRange(splitResult.First);
TextLineImpl.SortRuns(shapedTextCharacters);
shapedTextCharacters.Add(shapedSymbol);
if (measuredLength > _prefixLength && splitResult.Second is not null)
{
var availableSuffixWidth = availableWidth;
foreach (var run in splitResult.First)
{
availableSuffixWidth -= run.Size.Width;
}
for (int i = splitResult.Second.Count - 1; i >= 0; i--)
{
var run = splitResult.Second[i];
if (run.TryMeasureCharactersBackwards(availableSuffixWidth, out int suffixCount, out double suffixWidth))
{
availableSuffixWidth -= suffixWidth;
if (suffixCount > 0)
{
var splitSuffix = run.Split(run.TextSourceLength - suffixCount);
shapedTextCharacters.Add(splitSuffix.Second!);
}
}
}
}
}
else
{
shapedTextCharacters.Add(shapedSymbol);
}
return shapedTextCharacters;
}
availableWidth -= currentRun.Size.Width;
runIndex++;
}
return null;
}
}
}

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

@ -106,85 +106,18 @@ namespace Avalonia.Media.TextFormatting
var collapsingProperties = collapsingPropertiesList[0];
var runIndex = 0;
var currentWidth = 0.0;
var textRange = TextRange;
var collapsedLength = 0;
var collapsedRuns = collapsingProperties.Collapse(this);
var shapedSymbol = TextFormatterImpl.CreateSymbol(collapsingProperties.Symbol, _paragraphProperties.FlowDirection);
if (collapsingProperties.Width < shapedSymbol.GlyphRun.Size.Width)
if (collapsedRuns is List<ShapedTextCharacters> shapedRuns)
{
return new TextLineImpl(new List<ShapedTextCharacters>(0), textRange, _paragraphWidth, _paragraphProperties,
_flowDirection, TextLineBreak, true);
}
var availableWidth = collapsingProperties.Width - shapedSymbol.GlyphRun.Size.Width;
while (runIndex < _textRuns.Count)
{
var currentRun = _textRuns[runIndex];
currentWidth += currentRun.Size.Width;
var collapsedLine = new TextLineImpl(shapedRuns, TextRange, _paragraphWidth, _paragraphProperties, _flowDirection, TextLineBreak, true);
if (currentWidth > availableWidth)
if (shapedRuns.Count > 0)
{
if (currentRun.TryMeasureCharacters(availableWidth, out var measuredLength))
{
if (collapsingProperties.Style == TextCollapsingStyle.TrailingWord &&
measuredLength < textRange.End)
{
var currentBreakPosition = 0;
var lineBreaker = new LineBreakEnumerator(currentRun.Text);
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
{
var nextBreakPosition = lineBreaker.Current.PositionMeasure;
if (nextBreakPosition == 0)
{
break;
}
if (nextBreakPosition >= measuredLength)
{
break;
}
currentBreakPosition = nextBreakPosition;
}
measuredLength = currentBreakPosition;
}
}
collapsedLength += measuredLength;
var shapedTextCharacters = new List<ShapedTextCharacters>(_textRuns.Count);
if (collapsedLength > 0)
{
var splitResult = TextFormatterImpl.SplitShapedRuns(_textRuns, collapsedLength);
shapedTextCharacters.AddRange(splitResult.First);
SortRuns(shapedTextCharacters);
}
shapedTextCharacters.Add(shapedSymbol);
var textLine = new TextLineImpl(shapedTextCharacters, textRange, _paragraphWidth, _paragraphProperties,
_flowDirection, TextLineBreak, true);
return textLine.FinalizeLine();
collapsedLine.FinalizeLine();
}
availableWidth -= currentRun.Size.Width;
collapsedLength += currentRun.GlyphRun.Characters.Length;
runIndex++;
return collapsedLine;
}
return this;

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

@ -1,24 +1,23 @@
using Avalonia.Utilities;
using System.Collections.Generic;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// a collapsing properties to collapse whole line toward the end
/// 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)
public TextTrailingCharacterEllipsis(ReadOnlySlice<char> ellipsis, double width, TextRunProperties textRunProperties)
{
Width = width;
Symbol = new TextCharacters(s_ellipsis, textRunProperties);
Symbol = new TextCharacters(ellipsis, textRunProperties);
}
/// <inheritdoc/>
@ -27,7 +26,9 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/>
public sealed override TextRun Symbol { get; }
/// <inheritdoc/>
public sealed override TextCollapsingStyle Style { get; } = TextCollapsingStyle.TrailingCharacter;
public override IReadOnlyList<TextRun>? Collapse(TextLine textLine)
{
return TextEllipsisHelper.Collapse(textLine, this, false);
}
}
}

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

@ -1,4 +1,5 @@
using Avalonia.Utilities;
using System.Collections.Generic;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
@ -8,30 +9,30 @@ namespace Avalonia.Media.TextFormatting
/// </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(
ReadOnlySlice<char> ellipsis,
double width,
TextRunProperties textRunProperties
)
{
Width = width;
Symbol = new TextCharacters(s_ellipsis, textRunProperties);
Symbol = new TextCharacters(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;
public override IReadOnlyList<TextRun>? Collapse(TextLine textLine)
{
return TextEllipsisHelper.Collapse(textLine, this, true);
}
}
}

31
src/Avalonia.Visuals/Media/TextLeadingPrefixTrimming.cs

@ -0,0 +1,31 @@
using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;
namespace Avalonia.Media
{
public class TextLeadingPrefixTrimming : TextTrimming
{
private readonly ReadOnlySlice<char> _ellipsis;
private readonly int _prefixLength;
public TextLeadingPrefixTrimming(char ellipsis, int prefixLength) : this(new[] { ellipsis }, prefixLength)
{
}
public TextLeadingPrefixTrimming(char[] ellipsis, int prefixLength)
{
_prefixLength = prefixLength;
_ellipsis = new ReadOnlySlice<char>(ellipsis);
}
public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo)
{
return new TextLeadingPrefixEllipsis(_ellipsis, _prefixLength, createInfo.Width, createInfo.TextRunProperties);
}
public override string ToString()
{
return nameof(PrefixEllipsis);
}
}
}

18
src/Avalonia.Visuals/Media/TextNoneTrimming.cs

@ -0,0 +1,18 @@
using System;
using Avalonia.Media.TextFormatting;
namespace Avalonia.Media
{
internal class TextNoneTrimming : TextTrimming
{
public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo)
{
throw new NotSupportedException();
}
public override string ToString()
{
return nameof(None);
}
}
}

36
src/Avalonia.Visuals/Media/TextTrailingTrimming.cs

@ -0,0 +1,36 @@
using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;
namespace Avalonia.Media
{
public class TextTrailingTrimming : TextTrimming
{
private readonly ReadOnlySlice<char> _ellipsis;
private readonly bool _isWordBased;
public TextTrailingTrimming(char ellipsis, bool isWordBased) : this(new[] {ellipsis}, isWordBased)
{
}
public TextTrailingTrimming(char[] ellipsis, bool isWordBased)
{
_isWordBased = isWordBased;
_ellipsis = new ReadOnlySlice<char>(ellipsis);
}
public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo)
{
if (_isWordBased)
{
return new TextTrailingWordEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties);
}
return new TextTrailingCharacterEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties);
}
public override string ToString()
{
return _isWordBased ? nameof(WordEllipsis) : nameof(CharacterEllipsis);
}
}
}

58
src/Avalonia.Visuals/Media/TextTrimming.cs

@ -1,23 +1,71 @@
namespace Avalonia.Media
using System;
using Avalonia.Media.TextFormatting;
namespace Avalonia.Media
{
/// <summary>
/// Describes how text is trimmed when it overflows.
/// </summary>
public enum TextTrimming
public abstract class TextTrimming
{
public static char s_defaultEllipsisChar = '\u2026';
/// <summary>
/// Text is not trimmed.
/// </summary>
None,
public static TextTrimming None { get; } = new TextNoneTrimming();
/// <summary>
/// Text is trimmed at a character boundary. An ellipsis (...) is drawn in place of remaining text.
/// </summary>
CharacterEllipsis,
public static TextTrimming CharacterEllipsis { get; } = new TextTrailingTrimming(s_defaultEllipsisChar, false);
/// <summary>
/// Text is trimmed at a word boundary. An ellipsis (...) is drawn in place of remaining text.
/// </summary>
WordEllipsis
public static TextTrimming WordEllipsis { get; } = new TextTrailingTrimming(s_defaultEllipsisChar, true);
/// <summary>
/// Text is trimmed after a given prefix length. An ellipsis (...) is drawn in between prefix and suffix and represents remaining text.
/// </summary>
public static TextTrimming PrefixEllipsis { get; } = new TextLeadingPrefixTrimming(s_defaultEllipsisChar, 8);
/// <summary>
/// Creates properties that will be used for collapsing lines of text.
/// </summary>
/// <param name="createInfo">Contextual info about text that will be collapsed.</param>
public abstract TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo);
/// <summary>
/// Parses a text trimming string. Names must match static properties defined in this class.
/// </summary>
/// <param name="s">The text trimming string.</param>
/// <returns>The <see cref="TextTrimming"/>.</returns>
public static TextTrimming Parse(string s)
{
bool Matches(string name)
{
return name.Equals(s, StringComparison.OrdinalIgnoreCase);
}
if (Matches(nameof(None)))
{
return None;
}
if (Matches(nameof(CharacterEllipsis)))
{
return CharacterEllipsis;
}
else if (Matches(nameof(WordEllipsis)))
{
return WordEllipsis;
}
else if (Matches(nameof(PrefixEllipsis)))
{
return PrefixEllipsis;
}
throw new FormatException($"Invalid text trimming string: '{s}'.");
}
}
}

26
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs

@ -221,6 +221,32 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
}
}
if (type.Equals(types.TextTrimming))
{
foreach (var property in types.TextTrimming.Properties)
{
if (property.PropertyType == types.TextTrimming && property.Name.Equals(text, StringComparison.OrdinalIgnoreCase))
{
result = new XamlStaticOrTargetedReturnMethodCallNode(node, property.Getter, Enumerable.Empty<IXamlAstValueNode>());
return true;
}
}
}
if (type.Equals(types.TextDecorationCollection))
{
foreach (var property in types.TextDecorations.Properties)
{
if (property.PropertyType == types.TextDecorationCollection && property.Name.Equals(text, StringComparison.OrdinalIgnoreCase))
{
result = new XamlStaticOrTargetedReturnMethodCallNode(node, property.Getter, Enumerable.Empty<IXamlAstValueNode>());
return true;
}
}
}
result = null;
return false;
}

6
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@ -88,6 +88,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType ImmutableSolidColorBrush { get; }
public IXamlConstructor ImmutableSolidColorBrushConstructorColor { get; }
public IXamlType TypeUtilities { get; }
public IXamlType TextDecorationCollection { get; }
public IXamlType TextDecorations { get; }
public IXamlType TextTrimming { get; }
public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg)
{
@ -193,6 +196,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
ImmutableSolidColorBrush = cfg.TypeSystem.GetType("Avalonia.Media.Immutable.ImmutableSolidColorBrush");
ImmutableSolidColorBrushConstructorColor = ImmutableSolidColorBrush.GetConstructor(new List<IXamlType> { UInt });
TypeUtilities = cfg.TypeSystem.GetType("Avalonia.Utilities.TypeUtilities");
TextDecorationCollection = cfg.TypeSystem.GetType("Avalonia.Media.TextDecorationCollection");
TextDecorations = cfg.TypeSystem.GetType("Avalonia.Media.TextDecorations");
TextTrimming = cfg.TypeSystem.GetType("Avalonia.Media.TextTrimming");
}
}

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

@ -387,7 +387,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
if (textLine.Width > 300 || currentHeight + textLine.Height > 240)
{
textLine = textLine.Collapse(new TextTrailingWordEllipsis(300, defaultProperties));
textLine = textLine.Collapse(new TextTrailingWordEllipsis(new ReadOnlySlice<char>(new[] {TextTrimming.s_defaultEllipsisChar}), 300, defaultProperties));
}
currentHeight += textLine.Height;

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

@ -360,12 +360,29 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
[InlineData("01234 01234", 58, TextCollapsingStyle.TrailingCharacter, "01234 0\u2026")]
[InlineData("01234 01234", 58, TextCollapsingStyle.TrailingWord, "01234\u2026")]
[InlineData("01234", 9, TextCollapsingStyle.TrailingCharacter, "\u2026")]
[InlineData("01234", 2, TextCollapsingStyle.TrailingCharacter, "")]
public static IEnumerable<object[]> CollapsingData
{
get
{
yield return CreateData("01234 01234 01234", 120, TextTrimming.PrefixEllipsis, "01234 01\u20264 01234");
yield return CreateData("01234 01234", 58, TextTrimming.CharacterEllipsis, "01234 0\u2026");
yield return CreateData("01234 01234", 58, TextTrimming.WordEllipsis, "01234\u2026");
yield return CreateData("01234", 9, TextTrimming.CharacterEllipsis, "\u2026");
yield return CreateData("01234", 2, TextTrimming.CharacterEllipsis, "");
object[] CreateData(string text, double width, TextTrimming mode, string expected)
{
return new object[]
{
text, width, mode, expected
};
}
}
}
[MemberData(nameof(CollapsingData))]
[Theory]
public void Should_Collapse_Line(string text, double width, TextCollapsingStyle style, string expected)
public void Should_Collapse_Line(string text, double width, TextTrimming trimming, string expected)
{
using (Start())
{
@ -381,16 +398,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.False(textLine.HasCollapsed);
TextCollapsingProperties collapsingProperties;
if (style == TextCollapsingStyle.TrailingCharacter)
{
collapsingProperties = new TextTrailingCharacterEllipsis(width, defaultProperties);
}
else
{
collapsingProperties = new TextTrailingWordEllipsis(width, defaultProperties);
}
TextCollapsingProperties collapsingProperties = trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, defaultProperties));
var collapsedLine = textLine.Collapse(collapsingProperties);

Loading…
Cancel
Save