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>
|
|||
/// Properties of text collapsing
|
|||
/// Properties of text collapsing.
|
|||
/// </summary>
|
|||
public abstract class TextCollapsingProperties |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the width in which the collapsible range is constrained to
|
|||
/// 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
|
|||
/// Gets the text run that is used as collapsing symbol.
|
|||
/// </summary>
|
|||
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); |
|||
} |
|||
} |
|||
|
|||
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// a collapsing properties to collapse whole line toward the end
|
|||
/// at word granularity and with ellipsis being the collapsing symbol
|
|||
/// at word granularity.
|
|||
/// </summary>
|
|||
public class TextTrailingWordEllipsis : TextCollapsingProperties |
|||
public sealed class TextTrailingWordEllipsis : TextCollapsingProperties |
|||
{ |
|||
private static readonly ReadOnlySlice<char> s_ellipsis = new ReadOnlySlice<char>(new[] { '\u2026' }); |
|||
|
|||
/// <summary>
|
|||
/// Construct a text trailing word ellipsis collapsing properties
|
|||
/// 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>
|
|||
/// <param name="ellipsis">Text used as collapsing 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( |
|||
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); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -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>
|
|||
/// 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 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