diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 09f22612de..69b55b7222 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -134,7 +134,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty TextTrimmingProperty = - AvaloniaProperty.Register(nameof(TextTrimming)); + AvaloniaProperty.Register(nameof(TextTrimming), defaultValue: TextTrimming.None); /// /// Defines the property. diff --git a/src/Avalonia.Visuals/Media/FormattedText.cs b/src/Avalonia.Visuals/Media/FormattedText.cs index 12c40e4d59..1cac3243e3 100644 --- a/src/Avalonia.Visuals/Media/FormattedText.cs +++ b/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 ? diff --git a/src/Avalonia.Visuals/Media/TextCollapsingCreateInfo.cs b/src/Avalonia.Visuals/Media/TextCollapsingCreateInfo.cs new file mode 100644 index 0000000000..78f15b724a --- /dev/null +++ b/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; + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs index 88ca596d2d..fb85766003 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs +++ b/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 Split(int length) { if (IsReversed) diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs index ffd65423a3..0d6ae84493 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Media.TextFormatting +using System.Collections.Generic; + +namespace Avalonia.Media.TextFormatting { /// /// Properties of text collapsing @@ -16,8 +18,9 @@ public abstract TextRun Symbol { get; } /// - /// Gets the style of collapsing + /// Collapses given text line. /// - public abstract TextCollapsingStyle Style { get; } + /// Text line to collapse. + public abstract IReadOnlyList? Collapse(TextLine textLine); } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingStyle.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingStyle.cs deleted file mode 100644 index 1523cc4d9a..0000000000 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingStyle.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Avalonia.Media.TextFormatting -{ - /// - /// Text collapsing style - /// - public enum TextCollapsingStyle - { - /// - /// Collapse trailing characters - /// - TrailingCharacter, - - /// - /// Collapse trailing words - /// - TrailingWord, - } -} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextEllipsisHelper.cs new file mode 100644 index 0000000000..6ca93a6116 --- /dev/null +++ b/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? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis) + { + var shapedTextRuns = textLine.TextRuns as List; + + 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(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(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; + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index 4512890f08..0ff127694b 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/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 /// The . 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)); } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs new file mode 100644 index 0000000000..f0f212b0fe --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using Avalonia.Utilities; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Ellipsis based on a fixed length leading prefix and suffix growing from the end. + /// + public class TextLeadingPrefixEllipsis : TextCollapsingProperties + { + private readonly int _prefixLength; + + /// + /// Construct a text trailing word ellipsis collapsing properties + /// + /// width in which collapsing is constrained to + /// text run properties of ellispis symbol + public TextLeadingPrefixEllipsis( + ReadOnlySlice ellipsis, + int prefixLength, + double width, + TextRunProperties textRunProperties + ) + { + _prefixLength = prefixLength; + Width = width; + Symbol = new TextCharacters(ellipsis, textRunProperties); + } + + /// + public sealed override double Width { get; } + + /// + public sealed override TextRun Symbol { get; } + + public override IReadOnlyList? Collapse(TextLine textLine) + { + var shapedTextRuns = textLine.TextRuns as List; + + 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(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(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; + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs index e776655284..49bee6e776 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs +++ b/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 shapedRuns) { - return new TextLineImpl(new List(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(_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; diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs index 4bd46e8c75..90424d8424 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs +++ b/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 { /// - /// 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 /// public class TextTrailingCharacterEllipsis : TextCollapsingProperties { - private static readonly ReadOnlySlice s_ellipsis = new ReadOnlySlice(new[] { '\u2026' }); - /// /// Construct a text trailing character ellipsis collapsing properties /// /// width in which collapsing is constrained to /// text run properties of ellispis symbol - public TextTrailingCharacterEllipsis(double width, TextRunProperties textRunProperties) + public TextTrailingCharacterEllipsis(ReadOnlySlice ellipsis, double width, TextRunProperties textRunProperties) { Width = width; - Symbol = new TextCharacters(s_ellipsis, textRunProperties); + Symbol = new TextCharacters(ellipsis, textRunProperties); } /// @@ -27,7 +26,9 @@ namespace Avalonia.Media.TextFormatting /// public sealed override TextRun Symbol { get; } - /// - public sealed override TextCollapsingStyle Style { get; } = TextCollapsingStyle.TrailingCharacter; + public override IReadOnlyList? Collapse(TextLine textLine) + { + return TextEllipsisHelper.Collapse(textLine, this, false); + } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs index 9dffddd207..591df149cb 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs +++ b/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 /// public class TextTrailingWordEllipsis : TextCollapsingProperties { - private static readonly ReadOnlySlice s_ellipsis = new ReadOnlySlice(new[] { '\u2026' }); - /// /// Construct a text trailing word ellipsis collapsing properties /// /// width in which collapsing is constrained to /// text run properties of ellispis symbol public TextTrailingWordEllipsis( + ReadOnlySlice ellipsis, double width, TextRunProperties textRunProperties ) { Width = width; - Symbol = new TextCharacters(s_ellipsis, textRunProperties); + Symbol = new TextCharacters(ellipsis, textRunProperties); } - /// public sealed override double Width { get; } /// public sealed override TextRun Symbol { get; } - /// - public sealed override TextCollapsingStyle Style { get; } = TextCollapsingStyle.TrailingWord; + public override IReadOnlyList? Collapse(TextLine textLine) + { + return TextEllipsisHelper.Collapse(textLine, this, true); + } } } diff --git a/src/Avalonia.Visuals/Media/TextLeadingPrefixTrimming.cs b/src/Avalonia.Visuals/Media/TextLeadingPrefixTrimming.cs new file mode 100644 index 0000000000..66efe96ab9 --- /dev/null +++ b/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 _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(ellipsis); + } + + public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo) + { + return new TextLeadingPrefixEllipsis(_ellipsis, _prefixLength, createInfo.Width, createInfo.TextRunProperties); + } + + public override string ToString() + { + return nameof(PrefixEllipsis); + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextNoneTrimming.cs b/src/Avalonia.Visuals/Media/TextNoneTrimming.cs new file mode 100644 index 0000000000..6fd0bd2154 --- /dev/null +++ b/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); + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextTrailingTrimming.cs b/src/Avalonia.Visuals/Media/TextTrailingTrimming.cs new file mode 100644 index 0000000000..fae001c0c8 --- /dev/null +++ b/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 _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(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); + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextTrimming.cs b/src/Avalonia.Visuals/Media/TextTrimming.cs index f8a63dfede..c79ff4cb1a 100644 --- a/src/Avalonia.Visuals/Media/TextTrimming.cs +++ b/src/Avalonia.Visuals/Media/TextTrimming.cs @@ -1,23 +1,71 @@ -namespace Avalonia.Media +using System; +using Avalonia.Media.TextFormatting; + +namespace Avalonia.Media { /// /// Describes how text is trimmed when it overflows. /// - public enum TextTrimming + public abstract class TextTrimming { + public static char s_defaultEllipsisChar = '\u2026'; + /// /// Text is not trimmed. /// - None, + public static TextTrimming None { get; } = new TextNoneTrimming(); /// /// Text is trimmed at a character boundary. An ellipsis (...) is drawn in place of remaining text. /// - CharacterEllipsis, + public static TextTrimming CharacterEllipsis { get; } = new TextTrailingTrimming(s_defaultEllipsisChar, false); /// /// Text is trimmed at a word boundary. An ellipsis (...) is drawn in place of remaining text. /// - WordEllipsis + public static TextTrimming WordEllipsis { get; } = new TextTrailingTrimming(s_defaultEllipsisChar, true); + + /// + /// Text is trimmed after a given prefix length. An ellipsis (...) is drawn in between prefix and suffix and represents remaining text. + /// + public static TextTrimming PrefixEllipsis { get; } = new TextLeadingPrefixTrimming(s_defaultEllipsisChar, 8); + + /// + /// Creates properties that will be used for collapsing lines of text. + /// + /// Contextual info about text that will be collapsed. + public abstract TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo); + + /// + /// Parses a text trimming string. Names must match static properties defined in this class. + /// + /// The text trimming string. + /// The . + 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}'."); + } } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index b622971d38..88529ae3a0 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/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()); + + 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()); + + return true; + } + } + } + result = null; return false; } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 90c3989238..34a146cf37 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/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 { 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"); } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 326997328b..7c7fb4783e 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/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(new[] {TextTrimming.s_defaultEllipsisChar}), 300, defaultProperties)); } currentHeight += textLine.Height; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index d1a8f175e7..9c0f2d0a9f 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/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 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);