csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
492 lines
17 KiB
492 lines
17 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using Avalonia.Media.TextFormatting.Unicode;
|
|
using Avalonia.Platform;
|
|
using Avalonia.Utilities;
|
|
|
|
namespace Avalonia.Media.TextFormatting
|
|
{
|
|
internal class TextLineImpl : TextLine
|
|
{
|
|
private readonly List<ShapedTextCharacters> _shapedTextRuns;
|
|
private readonly double _paragraphWidth;
|
|
private readonly TextParagraphProperties _paragraphProperties;
|
|
private readonly TextLineMetrics _textLineMetrics;
|
|
|
|
public TextLineImpl(List<ShapedTextCharacters> textRuns, TextRange textRange, double paragraphWidth,
|
|
TextParagraphProperties paragraphProperties, TextLineBreak? lineBreak = null, bool hasCollapsed = false)
|
|
{
|
|
TextRange = textRange;
|
|
TextLineBreak = lineBreak;
|
|
HasCollapsed = hasCollapsed;
|
|
|
|
_shapedTextRuns = textRuns;
|
|
_paragraphWidth = paragraphWidth;
|
|
_paragraphProperties = paragraphProperties;
|
|
|
|
_textLineMetrics = CreateLineMetrics();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override IReadOnlyList<TextRun> TextRuns => _shapedTextRuns;
|
|
|
|
/// <inheritdoc/>
|
|
public override TextRange TextRange { get; }
|
|
|
|
/// <inheritdoc/>
|
|
public override TextLineBreak? TextLineBreak { get; }
|
|
|
|
/// <inheritdoc/>
|
|
public override bool HasCollapsed { get; }
|
|
|
|
/// <inheritdoc/>
|
|
public override bool HasOverflowed => _textLineMetrics.HasOverflowed;
|
|
|
|
/// <inheritdoc/>
|
|
public override double Baseline => _textLineMetrics.TextBaseline;
|
|
|
|
/// <inheritdoc/>
|
|
public override double Extent => _textLineMetrics.Height;
|
|
|
|
/// <inheritdoc/>
|
|
public override double Height => _textLineMetrics.Height;
|
|
|
|
/// <inheritdoc/>
|
|
public override int NewLineLength => _textLineMetrics.NewLineLength;
|
|
|
|
/// <inheritdoc/>
|
|
public override double OverhangAfter => 0;
|
|
|
|
/// <inheritdoc/>
|
|
public override double OverhangLeading => 0;
|
|
|
|
/// <inheritdoc/>
|
|
public override double OverhangTrailing => 0;
|
|
|
|
/// <inheritdoc/>
|
|
public override int TrailingWhitespaceLength => _textLineMetrics.TrailingWhitespaceLength;
|
|
|
|
/// <inheritdoc/>
|
|
public override double Start => _textLineMetrics.Start;
|
|
|
|
/// <inheritdoc/>
|
|
public override double Width => _textLineMetrics.Width;
|
|
|
|
/// <inheritdoc/>
|
|
public override double WidthIncludingTrailingWhitespace => _textLineMetrics.WidthIncludingTrailingWhitespace;
|
|
|
|
/// <inheritdoc/>
|
|
public override void Draw(DrawingContext drawingContext, Point lineOrigin)
|
|
{
|
|
var (currentX, currentY) = lineOrigin;
|
|
|
|
foreach (var textRun in _shapedTextRuns)
|
|
{
|
|
var offsetY = Baseline - textRun.GlyphRun.BaselineOrigin.Y;
|
|
|
|
textRun.Draw(drawingContext, new Point(currentX, currentY + offsetY));
|
|
|
|
currentX += textRun.Size.Width;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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;
|
|
|
|
var shapedSymbol = CreateShapedSymbol(collapsingProperties.Symbol);
|
|
|
|
var availableWidth = collapsingProperties.Width - shapedSymbol.Size.Width;
|
|
|
|
while (runIndex < _shapedTextRuns.Count)
|
|
{
|
|
var currentRun = _shapedTextRuns[runIndex];
|
|
|
|
currentWidth += currentRun.Size.Width;
|
|
|
|
if (currentWidth > availableWidth)
|
|
{
|
|
if (TextFormatterImpl.TryMeasureCharacters(currentRun, 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 splitResult = TextFormatterImpl.SplitTextRuns(_shapedTextRuns, collapsedLength);
|
|
|
|
var shapedTextCharacters = new List<ShapedTextCharacters>(splitResult.First.Count + 1);
|
|
|
|
shapedTextCharacters.AddRange(splitResult.First);
|
|
|
|
shapedTextCharacters.Add(shapedSymbol);
|
|
|
|
textRange = new TextRange(textRange.Start, collapsedLength);
|
|
|
|
return new TextLineImpl(shapedTextCharacters, textRange, _paragraphWidth, _paragraphProperties,
|
|
TextLineBreak, true);
|
|
}
|
|
|
|
availableWidth -= currentRun.Size.Width;
|
|
|
|
collapsedLength += currentRun.GlyphRun.Characters.Length;
|
|
|
|
runIndex++;
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
private TextLineMetrics CreateLineMetrics()
|
|
{
|
|
var width = 0d;
|
|
var widthIncludingWhitespace = 0d;
|
|
var trailingWhitespaceLength = 0;
|
|
var newLineLength = 0;
|
|
var ascent = 0d;
|
|
var descent = 0d;
|
|
var lineGap = 0d;
|
|
|
|
for (var index = 0; index < _shapedTextRuns.Count; index++)
|
|
{
|
|
var textRun = _shapedTextRuns[index];
|
|
|
|
var fontMetrics =
|
|
new FontMetrics(textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize);
|
|
|
|
if (ascent > fontMetrics.Ascent)
|
|
{
|
|
ascent = fontMetrics.Ascent;
|
|
}
|
|
|
|
if (descent < fontMetrics.Descent)
|
|
{
|
|
descent = fontMetrics.Descent;
|
|
}
|
|
|
|
if (lineGap < fontMetrics.LineGap)
|
|
{
|
|
lineGap = fontMetrics.LineGap;
|
|
}
|
|
|
|
if (index == _shapedTextRuns.Count - 1)
|
|
{
|
|
width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
|
|
widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;
|
|
trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
|
|
newLineLength = textRun.GlyphRun.Metrics.NewlineLength;
|
|
}
|
|
else
|
|
{
|
|
widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;
|
|
}
|
|
}
|
|
|
|
var start = GetParagraphOffsetX(width, _paragraphWidth, _paragraphProperties.TextAlignment);
|
|
|
|
var lineHeight = _paragraphProperties.LineHeight;
|
|
|
|
var height = double.IsNaN(lineHeight) || MathUtilities.IsZero(lineHeight) ?
|
|
descent - ascent + lineGap :
|
|
lineHeight;
|
|
|
|
return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start,
|
|
-ascent, trailingWhitespaceLength, width, widthIncludingWhitespace);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override CharacterHit GetCharacterHitFromDistance(double distance)
|
|
{
|
|
distance -= Start;
|
|
|
|
if (distance < 0)
|
|
{
|
|
// hit happens before the line, return the first position
|
|
return new CharacterHit(TextRange.Start);
|
|
}
|
|
|
|
// process hit that happens within the line
|
|
var characterHit = new CharacterHit();
|
|
|
|
foreach (var run in _shapedTextRuns)
|
|
{
|
|
characterHit = run.GlyphRun.GetCharacterHitFromDistance(distance, out _);
|
|
|
|
if (distance <= run.Size.Width)
|
|
{
|
|
break;
|
|
}
|
|
|
|
distance -= run.Size.Width;
|
|
}
|
|
|
|
return characterHit;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override double GetDistanceFromCharacterHit(CharacterHit characterHit)
|
|
{
|
|
var characterIndex = characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0);
|
|
|
|
if (characterIndex > TextRange.End)
|
|
{
|
|
if (NewLineLength > 0)
|
|
{
|
|
return Start + Width;
|
|
}
|
|
return Start + WidthIncludingTrailingWhitespace;
|
|
}
|
|
|
|
var currentDistance = Start;
|
|
|
|
foreach (var textRun in _shapedTextRuns)
|
|
{
|
|
if (characterIndex > textRun.Text.End)
|
|
{
|
|
currentDistance += textRun.Size.Width;
|
|
|
|
continue;
|
|
}
|
|
|
|
return currentDistance + textRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(characterIndex));
|
|
}
|
|
|
|
return currentDistance;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit)
|
|
{
|
|
if (TryFindNextCharacterHit(characterHit, out var nextCharacterHit))
|
|
{
|
|
return nextCharacterHit;
|
|
}
|
|
|
|
if (characterHit.FirstCharacterIndex + characterHit.TrailingLength <= TextRange.Start + TextRange.Length)
|
|
{
|
|
return characterHit; // Can't move, we're after the last character
|
|
}
|
|
|
|
var runIndex = GetRunIndexAtCodepointIndex(TextRange.End);
|
|
|
|
var textRun = _shapedTextRuns[runIndex];
|
|
|
|
characterHit = textRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
|
|
|
|
return characterHit; // Can't move, we're after the last character
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit)
|
|
{
|
|
if (TryFindPreviousCharacterHit(characterHit, out var previousCharacterHit))
|
|
{
|
|
return previousCharacterHit;
|
|
}
|
|
|
|
if (characterHit.FirstCharacterIndex < TextRange.Start)
|
|
{
|
|
characterHit = new CharacterHit(TextRange.Start);
|
|
}
|
|
|
|
return characterHit; // Can't move, we're before the first character
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit)
|
|
{
|
|
// same operation as move-to-previous
|
|
return GetPreviousCaretCharacterHit(characterHit);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to find the next character hit.
|
|
/// </summary>
|
|
/// <param name="characterHit">The current character hit.</param>
|
|
/// <param name="nextCharacterHit">The next character hit.</param>
|
|
/// <returns></returns>
|
|
private bool TryFindNextCharacterHit(CharacterHit characterHit, out CharacterHit nextCharacterHit)
|
|
{
|
|
nextCharacterHit = characterHit;
|
|
|
|
var codepointIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
|
|
|
|
if (codepointIndex > TextRange.End)
|
|
{
|
|
return false; // Cannot go forward anymore
|
|
}
|
|
|
|
if (codepointIndex < TextRange.Start)
|
|
{
|
|
codepointIndex = TextRange.Start;
|
|
}
|
|
|
|
var runIndex = GetRunIndexAtCodepointIndex(codepointIndex);
|
|
|
|
while (runIndex < _shapedTextRuns.Count)
|
|
{
|
|
var run = _shapedTextRuns[runIndex];
|
|
|
|
var foundCharacterHit =
|
|
run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
|
|
|
|
var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength ==
|
|
TextRange.Length;
|
|
|
|
var characterIndex = codepointIndex - run.Text.Start;
|
|
|
|
if (characterIndex < 0 && characterHit.TrailingLength == 0)
|
|
{
|
|
foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
|
|
}
|
|
|
|
nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ?
|
|
foundCharacterHit :
|
|
new CharacterHit(foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength);
|
|
|
|
if (isAtEnd || nextCharacterHit.FirstCharacterIndex > characterHit.FirstCharacterIndex)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
runIndex++;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to find the previous character hit.
|
|
/// </summary>
|
|
/// <param name="characterHit">The current character hit.</param>
|
|
/// <param name="previousCharacterHit">The previous character hit.</param>
|
|
/// <returns></returns>
|
|
private bool TryFindPreviousCharacterHit(CharacterHit characterHit, out CharacterHit previousCharacterHit)
|
|
{
|
|
if (characterHit.FirstCharacterIndex == TextRange.Start)
|
|
{
|
|
previousCharacterHit = new CharacterHit(TextRange.Start);
|
|
|
|
return true;
|
|
}
|
|
|
|
previousCharacterHit = characterHit;
|
|
|
|
var codepointIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
|
|
|
|
if (codepointIndex < TextRange.Start)
|
|
{
|
|
return false; // Cannot go backward anymore.
|
|
}
|
|
|
|
var runIndex = GetRunIndexAtCodepointIndex(codepointIndex);
|
|
|
|
while (runIndex >= 0)
|
|
{
|
|
var run = _shapedTextRuns[runIndex];
|
|
|
|
var foundCharacterHit = run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
|
|
|
|
previousCharacterHit = characterHit.TrailingLength != 0 ?
|
|
foundCharacterHit :
|
|
new CharacterHit(foundCharacterHit.FirstCharacterIndex);
|
|
|
|
if (previousCharacterHit.FirstCharacterIndex < characterHit.FirstCharacterIndex)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
runIndex--;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the run index of the specified codepoint index.
|
|
/// </summary>
|
|
/// <param name="codepointIndex">The codepoint index.</param>
|
|
/// <returns>The text run index.</returns>
|
|
private int GetRunIndexAtCodepointIndex(int codepointIndex)
|
|
{
|
|
if (codepointIndex > TextRange.End)
|
|
{
|
|
return _shapedTextRuns.Count - 1;
|
|
}
|
|
|
|
if (codepointIndex <= 0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
var runIndex = 0;
|
|
|
|
while (runIndex < _shapedTextRuns.Count)
|
|
{
|
|
var run = _shapedTextRuns[runIndex];
|
|
|
|
if (run.Text.End >= codepointIndex)
|
|
{
|
|
return runIndex;
|
|
}
|
|
|
|
runIndex++;
|
|
}
|
|
|
|
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 properties = textRun.Properties;
|
|
|
|
_ = properties ?? throw new InvalidOperationException($"{nameof(TextRun.Properties)} should not be null.");
|
|
|
|
var glyphRun = TextShaper.Current.ShapeText(textRun.Text, properties.Typeface, properties.FontRenderingEmSize, properties.CultureInfo);
|
|
|
|
return new ShapedTextCharacters(glyphRun, properties);
|
|
}
|
|
}
|
|
}
|
|
|