committed by
GitHub
21 changed files with 544 additions and 171 deletions
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,23 +1,26 @@ |
|||||
namespace Avalonia.Media.TextFormatting |
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
{ |
{ |
||||
/// <summary>
|
/// <summary>
|
||||
/// Properties of text collapsing
|
/// Properties of text collapsing.
|
||||
/// </summary>
|
/// </summary>
|
||||
public abstract class TextCollapsingProperties |
public abstract class TextCollapsingProperties |
||||
{ |
{ |
||||
/// <summary>
|
/// <summary>
|
||||
/// Gets the width in which the collapsible range is constrained to
|
/// Gets the width in which the collapsible range is constrained to.
|
||||
/// </summary>
|
/// </summary>
|
||||
public abstract double Width { get; } |
public abstract double Width { get; } |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Gets the text run that is used as collapsing symbol
|
/// Gets the text run that is used as collapsing symbol.
|
||||
/// </summary>
|
/// </summary>
|
||||
public abstract TextRun Symbol { get; } |
public abstract TextRun Symbol { get; } |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Gets the style of collapsing
|
/// Collapses given text line.
|
||||
/// </summary>
|
/// </summary>
|
||||
public abstract TextCollapsingStyle Style { get; } |
/// <param name="textLine">Text line to collapse.</param>
|
||||
|
public abstract IReadOnlyList<TextRun>? Collapse(TextLine textLine); |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -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, |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,95 @@ |
|||||
|
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; |
||||
|
var shapedSymbol = TextFormatterImpl.CreateSymbol(properties.Symbol, FlowDirection.LeftToRight); |
||||
|
|
||||
|
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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,146 @@ |
|||||
|
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 at character granularity.
|
||||
|
/// </summary>
|
||||
|
public sealed class TextLeadingPrefixCharacterEllipsis : TextCollapsingProperties |
||||
|
{ |
||||
|
private readonly int _prefixLength; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Construct a text trailing word ellipsis collapsing properties.
|
||||
|
/// </summary>
|
||||
|
/// <param name="ellipsis">Text used as collapsing symbol.</param>
|
||||
|
/// <param name="prefixLength">Length of leading prefix.</param>
|
||||
|
/// <param name="width">width in which collapsing is constrained to</param>
|
||||
|
/// <param name="textRunProperties">text run properties of ellispis symbol</param>
|
||||
|
public TextLeadingPrefixCharacterEllipsis( |
||||
|
ReadOnlySlice<char> ellipsis, |
||||
|
int prefixLength, |
||||
|
double width, |
||||
|
TextRunProperties textRunProperties) |
||||
|
{ |
||||
|
if (_prefixLength < 0) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException(nameof(prefixLength)); |
||||
|
} |
||||
|
|
||||
|
_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; |
||||
|
var shapedSymbol = TextFormatterImpl.CreateSymbol(Symbol, FlowDirection.LeftToRight); |
||||
|
|
||||
|
if (Width < shapedSymbol.GlyphRun.Size.Width) |
||||
|
{ |
||||
|
return new List<ShapedTextCharacters>(0); |
||||
|
} |
||||
|
|
||||
|
// Overview of ellipsis structure
|
||||
|
// Prefix length run | Ellipsis symbol | Post split run growing from the end |
|
||||
|
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) |
||||
|
{ |
||||
|
List<ShapedTextCharacters>? preSplitRuns = null; |
||||
|
List<ShapedTextCharacters>? postSplitRuns = null; |
||||
|
|
||||
|
if (_prefixLength > 0) |
||||
|
{ |
||||
|
var splitResult = TextFormatterImpl.SplitShapedRuns(shapedTextRuns, Math.Min(_prefixLength, measuredLength)); |
||||
|
|
||||
|
shapedTextCharacters.AddRange(splitResult.First); |
||||
|
|
||||
|
TextLineImpl.SortRuns(shapedTextCharacters); |
||||
|
|
||||
|
preSplitRuns = splitResult.First; |
||||
|
postSplitRuns = splitResult.Second; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
postSplitRuns = shapedTextRuns; |
||||
|
} |
||||
|
|
||||
|
shapedTextCharacters.Add(shapedSymbol); |
||||
|
|
||||
|
if (measuredLength > _prefixLength && postSplitRuns is not null) |
||||
|
{ |
||||
|
var availableSuffixWidth = availableWidth; |
||||
|
|
||||
|
if (preSplitRuns is not null) |
||||
|
{ |
||||
|
foreach (var run in preSplitRuns) |
||||
|
{ |
||||
|
availableSuffixWidth -= run.Size.Width; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
for (int i = postSplitRuns.Count - 1; i >= 0; i--) |
||||
|
{ |
||||
|
var run = postSplitRuns[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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,37 +1,39 @@ |
|||||
using Avalonia.Utilities; |
using System.Collections.Generic; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
namespace Avalonia.Media.TextFormatting |
namespace Avalonia.Media.TextFormatting |
||||
{ |
{ |
||||
/// <summary>
|
/// <summary>
|
||||
/// a collapsing properties to collapse whole line toward the end
|
/// a collapsing properties to collapse whole line toward the end
|
||||
/// at word granularity and with ellipsis being the collapsing symbol
|
/// at word granularity.
|
||||
/// </summary>
|
/// </summary>
|
||||
public class TextTrailingWordEllipsis : TextCollapsingProperties |
public sealed class TextTrailingWordEllipsis : TextCollapsingProperties |
||||
{ |
{ |
||||
private static readonly ReadOnlySlice<char> s_ellipsis = new ReadOnlySlice<char>(new[] { '\u2026' }); |
|
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Construct a text trailing word ellipsis collapsing properties
|
/// Construct a text trailing word ellipsis collapsing properties.
|
||||
/// </summary>
|
/// </summary>
|
||||
/// <param name="width">width in which collapsing is constrained to</param>
|
/// <param name="ellipsis">Text used as collapsing symbol.</param>
|
||||
/// <param name="textRunProperties">text run properties of ellispis symbol</param>
|
/// <param name="width">width in which collapsing is constrained to.</param>
|
||||
|
/// <param name="textRunProperties">text run properties of ellispis symbol.</param>
|
||||
public TextTrailingWordEllipsis( |
public TextTrailingWordEllipsis( |
||||
|
ReadOnlySlice<char> ellipsis, |
||||
double width, |
double width, |
||||
TextRunProperties textRunProperties |
TextRunProperties textRunProperties |
||||
) |
) |
||||
{ |
{ |
||||
Width = width; |
Width = width; |
||||
Symbol = new TextCharacters(s_ellipsis, textRunProperties); |
Symbol = new TextCharacters(ellipsis, textRunProperties); |
||||
} |
} |
||||
|
|
||||
|
|
||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||
public sealed override double Width { get; } |
public sealed override double Width { get; } |
||||
|
|
||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||
public sealed override TextRun Symbol { get; } |
public sealed override TextRun Symbol { get; } |
||||
|
|
||||
/// <inheritdoc/>
|
public override IReadOnlyList<TextRun>? Collapse(TextLine textLine) |
||||
public sealed override TextCollapsingStyle Style { get; } = TextCollapsingStyle.TrailingWord; |
{ |
||||
|
return TextEllipsisHelper.Collapse(textLine, this, true); |
||||
|
} |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,31 @@ |
|||||
|
using Avalonia.Media.TextFormatting; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
public sealed 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 TextLeadingPrefixCharacterEllipsis(_ellipsis, _prefixLength, createInfo.Width, createInfo.TextRunProperties); |
||||
|
} |
||||
|
|
||||
|
public override string ToString() |
||||
|
{ |
||||
|
return nameof(PrefixCharacterEllipsis); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Media.TextFormatting; |
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
internal sealed class TextNoneTrimming : TextTrimming |
||||
|
{ |
||||
|
public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo) |
||||
|
{ |
||||
|
throw new NotSupportedException(); |
||||
|
} |
||||
|
|
||||
|
public override string ToString() |
||||
|
{ |
||||
|
return nameof(None); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
using Avalonia.Media.TextFormatting; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
public sealed 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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,23 +1,76 @@ |
|||||
namespace Avalonia.Media |
using System; |
||||
|
using Avalonia.Media.TextFormatting; |
||||
|
|
||||
|
namespace Avalonia.Media |
||||
{ |
{ |
||||
/// <summary>
|
/// <summary>
|
||||
/// Describes how text is trimmed when it overflows.
|
/// Describes how text is trimmed when it overflows.
|
||||
/// </summary>
|
/// </summary>
|
||||
public enum TextTrimming |
public abstract class TextTrimming |
||||
{ |
{ |
||||
|
public static char s_defaultEllipsisChar = '\u2026'; |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Text is not trimmed.
|
/// Text is not trimmed.
|
||||
/// </summary>
|
/// </summary>
|
||||
None, |
public static TextTrimming None { get; } = new TextNoneTrimming(); |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Text is trimmed at a character boundary. An ellipsis (...) is drawn in place of remaining text.
|
/// Text is trimmed at a character boundary. An ellipsis (...) is drawn in place of remaining text.
|
||||
/// </summary>
|
/// </summary>
|
||||
CharacterEllipsis, |
public static TextTrimming CharacterEllipsis { get; } = new TextTrailingTrimming(s_defaultEllipsisChar, false); |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Text is trimmed at a word boundary. An ellipsis (...) is drawn in place of remaining text.
|
/// Text is trimmed at a word boundary. An ellipsis (...) is drawn in place of remaining text.
|
||||
/// </summary>
|
/// </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 PrefixCharacterEllipsis { get; } = new TextLeadingPrefixTrimming(s_defaultEllipsisChar, 8); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Text is trimmed at a character boundary starting from the beginning. An ellipsis (...) is drawn in place of remaining text.
|
||||
|
/// </summary>
|
||||
|
public static TextTrimming LeadingCharacterEllipsis { get; } = new TextLeadingPrefixTrimming(s_defaultEllipsisChar, 0); |
||||
|
|
||||
|
/// <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(PrefixCharacterEllipsis))) |
||||
|
{ |
||||
|
return PrefixCharacterEllipsis; |
||||
|
} |
||||
|
|
||||
|
throw new FormatException($"Invalid text trimming string: '{s}'."); |
||||
|
} |
||||
} |
} |
||||
} |
} |
||||
|
|||||
Loading…
Reference in new issue