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.
1562 lines
55 KiB
1562 lines
55 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using Avalonia.Media.TextFormatting.Unicode;
|
|
using Avalonia.Utilities;
|
|
|
|
namespace Avalonia.Media.TextFormatting
|
|
{
|
|
internal class TextLineImpl : TextLine
|
|
{
|
|
internal static Comparer<TextBounds> TextBoundsComparer { get; } =
|
|
Comparer<TextBounds>.Create((x, y) => x.Rectangle.Left.CompareTo(y.Rectangle.Left));
|
|
|
|
internal IReadOnlyList<IndexedTextRun>? _indexedTextRuns;
|
|
private readonly TextRun[] _textRuns;
|
|
private readonly double _paragraphWidth;
|
|
private readonly TextParagraphProperties _paragraphProperties;
|
|
private TextLineMetrics _textLineMetrics;
|
|
private TextLineBreak? _textLineBreak;
|
|
private readonly FlowDirection _resolvedFlowDirection;
|
|
|
|
private Rect _inkBounds;
|
|
private Rect _bounds;
|
|
|
|
public TextLineImpl(TextRun[] textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
|
|
TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection = FlowDirection.LeftToRight,
|
|
TextLineBreak? lineBreak = null, bool hasCollapsed = false)
|
|
{
|
|
FirstTextSourceIndex = firstTextSourceIndex;
|
|
Length = length;
|
|
_textLineBreak = lineBreak;
|
|
HasCollapsed = hasCollapsed;
|
|
|
|
_textRuns = textRuns;
|
|
_paragraphWidth = paragraphWidth;
|
|
_paragraphProperties = paragraphProperties;
|
|
|
|
_resolvedFlowDirection = resolvedFlowDirection;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override IReadOnlyList<TextRun> TextRuns => _textRuns;
|
|
|
|
/// <inheritdoc/>
|
|
public override int FirstTextSourceIndex { get; }
|
|
|
|
/// <inheritdoc/>
|
|
public override int Length { get; }
|
|
|
|
/// <inheritdoc/>
|
|
public override TextLineBreak? TextLineBreak => _textLineBreak;
|
|
|
|
/// <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.Extent;
|
|
|
|
/// <inheritdoc/>
|
|
public override double Height => _textLineMetrics.Height;
|
|
|
|
/// <inheritdoc/>
|
|
public override int NewLineLength => _textLineMetrics.NewlineLength;
|
|
|
|
/// <inheritdoc/>
|
|
public override double OverhangAfter => _textLineMetrics.OverhangAfter;
|
|
|
|
/// <inheritdoc/>
|
|
public override double OverhangLeading => _textLineMetrics.OverhangLeading;
|
|
|
|
/// <inheritdoc/>
|
|
public override double OverhangTrailing => _textLineMetrics.OverhangTrailing;
|
|
|
|
/// <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;
|
|
|
|
/// <summary>
|
|
/// Get the logical text bounds.
|
|
/// </summary>
|
|
internal Rect Bounds => _bounds;
|
|
|
|
/// <summary>
|
|
/// Get the bounding box that is covered with black pixels.
|
|
/// </summary>
|
|
internal Rect InkBounds => _inkBounds;
|
|
|
|
/// <inheritdoc/>
|
|
public override void Draw(DrawingContext drawingContext, Point lineOrigin)
|
|
{
|
|
var (currentX, currentY) = lineOrigin + new Point(Start, 0);
|
|
|
|
foreach (var textRun in _textRuns)
|
|
{
|
|
switch (textRun)
|
|
{
|
|
case DrawableTextRun drawableTextRun:
|
|
{
|
|
var offsetY = GetBaselineOffset(this, drawableTextRun);
|
|
|
|
drawableTextRun.Draw(drawingContext, new Point(currentX, currentY + offsetY));
|
|
|
|
currentX += drawableTextRun.Size.Width;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public static double GetBaselineOffset(TextLine textLine, DrawableTextRun textRun)
|
|
{
|
|
var baseline = textRun.Baseline;
|
|
var baselineAlignment = textRun.Properties?.BaselineAlignment;
|
|
|
|
var baselineOffset = -baseline;
|
|
|
|
switch (baselineAlignment)
|
|
{
|
|
case BaselineAlignment.Baseline:
|
|
baselineOffset += textLine.Baseline;
|
|
break;
|
|
case BaselineAlignment.Top:
|
|
case BaselineAlignment.TextTop:
|
|
baselineOffset += textLine.Height - textLine.Extent + textRun.Size.Height / 2;
|
|
break;
|
|
case BaselineAlignment.Center:
|
|
baselineOffset += textLine.Height / 2 + baseline - textRun.Size.Height / 2;
|
|
break;
|
|
case BaselineAlignment.Subscript:
|
|
case BaselineAlignment.Bottom:
|
|
case BaselineAlignment.TextBottom:
|
|
baselineOffset += textLine.Height - textRun.Size.Height + baseline;
|
|
break;
|
|
case BaselineAlignment.Superscript:
|
|
baselineOffset += baseline;
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException(nameof(baselineAlignment), baselineAlignment, null);
|
|
}
|
|
|
|
return baselineOffset;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override TextLine Collapse(params TextCollapsingProperties?[] collapsingPropertiesList)
|
|
{
|
|
if (collapsingPropertiesList.Length == 0)
|
|
{
|
|
return this;
|
|
}
|
|
|
|
var collapsingProperties = collapsingPropertiesList[0];
|
|
|
|
if (collapsingProperties is null)
|
|
{
|
|
return this;
|
|
}
|
|
|
|
var collapsedRuns = collapsingProperties.Collapse(this);
|
|
|
|
if (collapsedRuns is null)
|
|
{
|
|
return this;
|
|
}
|
|
|
|
var collapsedLine = new TextLineImpl(collapsedRuns, FirstTextSourceIndex, Length, _paragraphWidth, _paragraphProperties,
|
|
_resolvedFlowDirection, TextLineBreak, true);
|
|
|
|
if (collapsedRuns.Length > 0)
|
|
{
|
|
collapsedLine.FinalizeLine();
|
|
}
|
|
|
|
return collapsedLine;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override void Justify(JustificationProperties justificationProperties)
|
|
{
|
|
justificationProperties.Justify(this);
|
|
|
|
_textLineMetrics = CreateLineMetrics();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override CharacterHit GetCharacterHitFromDistance(double distance)
|
|
{
|
|
if (_textRuns.Length == 0)
|
|
{
|
|
return new CharacterHit(FirstTextSourceIndex);
|
|
}
|
|
|
|
distance -= Start;
|
|
|
|
var lastIndex = _textRuns.Length - 1;
|
|
var lineLength = Length;
|
|
|
|
if (_textRuns[lastIndex] is TextEndOfLine textEndOfLine)
|
|
{
|
|
lastIndex--;
|
|
lineLength -= textEndOfLine.Length;
|
|
}
|
|
|
|
var currentPosition = FirstTextSourceIndex;
|
|
|
|
if (lastIndex < 0)
|
|
{
|
|
return new CharacterHit(currentPosition);
|
|
}
|
|
|
|
if (distance <= 0)
|
|
{
|
|
var firstRun = _textRuns[0];
|
|
|
|
if (_paragraphProperties.FlowDirection == FlowDirection.RightToLeft)
|
|
{
|
|
currentPosition += lineLength - firstRun.Length;
|
|
}
|
|
|
|
return GetRunCharacterHit(firstRun, currentPosition, 0);
|
|
}
|
|
|
|
if (distance >= WidthIncludingTrailingWhitespace)
|
|
{
|
|
var lastRun = _textRuns[lastIndex];
|
|
|
|
if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
|
|
{
|
|
currentPosition += lineLength - lastRun.Length;
|
|
}
|
|
|
|
return GetRunCharacterHit(lastRun, currentPosition, distance);
|
|
}
|
|
|
|
// process hit that happens within the line
|
|
var characterHit = new CharacterHit();
|
|
var currentDistance = 0.0;
|
|
|
|
for (var i = 0; i <= lastIndex; i++)
|
|
{
|
|
var currentRun = _textRuns[i];
|
|
|
|
if (currentRun is ShapedTextRun shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
|
|
{
|
|
var rightToLeftIndex = i;
|
|
currentPosition += currentRun.Length;
|
|
|
|
while (rightToLeftIndex + 1 <= _textRuns.Length - 1)
|
|
{
|
|
var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextRun;
|
|
|
|
if (nextShaped == null || nextShaped.ShapedBuffer.IsLeftToRight)
|
|
{
|
|
break;
|
|
}
|
|
|
|
currentPosition += nextShaped.Length;
|
|
|
|
rightToLeftIndex++;
|
|
}
|
|
|
|
for (var j = i; i <= rightToLeftIndex; j++)
|
|
{
|
|
if (j > _textRuns.Length - 1)
|
|
{
|
|
break;
|
|
}
|
|
|
|
currentRun = _textRuns[j];
|
|
|
|
if (currentRun is not ShapedTextRun)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
shapedRun = (ShapedTextRun)currentRun;
|
|
|
|
if (currentDistance + shapedRun.Size.Width <= distance)
|
|
{
|
|
currentDistance += shapedRun.Size.Width;
|
|
currentPosition -= currentRun.Length;
|
|
|
|
continue;
|
|
}
|
|
|
|
return GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
|
|
}
|
|
}
|
|
|
|
characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
|
|
|
|
if (currentRun is DrawableTextRun drawableTextRun)
|
|
{
|
|
if (i < _textRuns.Length - 1 && currentDistance + drawableTextRun.Size.Width < distance)
|
|
{
|
|
currentDistance += drawableTextRun.Size.Width;
|
|
|
|
currentPosition += currentRun.Length;
|
|
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
currentPosition += currentRun.Length;
|
|
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
return characterHit;
|
|
}
|
|
|
|
private static CharacterHit GetRunCharacterHit(TextRun run, int currentPosition, double distance)
|
|
{
|
|
CharacterHit characterHit;
|
|
|
|
switch (run)
|
|
{
|
|
case ShapedTextRun shapedRun:
|
|
{
|
|
characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
|
|
|
|
var offset = 0;
|
|
|
|
if (shapedRun.GlyphRun.IsLeftToRight)
|
|
{
|
|
offset = Math.Max(0, currentPosition - shapedRun.GlyphRun.Metrics.FirstCluster);
|
|
}
|
|
|
|
characterHit = new CharacterHit(offset + characterHit.FirstCharacterIndex, characterHit.TrailingLength);
|
|
|
|
break;
|
|
}
|
|
case DrawableTextRun drawableTextRun:
|
|
{
|
|
if (distance < drawableTextRun.Size.Width / 2)
|
|
{
|
|
characterHit = new CharacterHit(currentPosition);
|
|
}
|
|
else
|
|
{
|
|
characterHit = new CharacterHit(currentPosition, run.Length);
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
characterHit = new CharacterHit(currentPosition, run.Length);
|
|
|
|
break;
|
|
}
|
|
|
|
return characterHit;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override double GetDistanceFromCharacterHit(CharacterHit characterHit)
|
|
{
|
|
if (_indexedTextRuns is null || _indexedTextRuns.Count == 0)
|
|
{
|
|
return Start;
|
|
}
|
|
|
|
var characterIndex = Math.Min(
|
|
characterHit.FirstCharacterIndex + characterHit.TrailingLength,
|
|
FirstTextSourceIndex + Length);
|
|
|
|
var currentPosition = FirstTextSourceIndex;
|
|
|
|
static FlowDirection GetDirection(TextRun textRun, FlowDirection currentDirection)
|
|
{
|
|
if (textRun is ShapedTextRun shapedTextRun)
|
|
{
|
|
return shapedTextRun.ShapedBuffer.IsLeftToRight ?
|
|
FlowDirection.LeftToRight :
|
|
FlowDirection.RightToLeft;
|
|
}
|
|
|
|
return currentDirection;
|
|
}
|
|
|
|
IndexedTextRun FindIndexedRun()
|
|
{
|
|
var i = 0;
|
|
|
|
IndexedTextRun currentIndexedRun = _indexedTextRuns[i];
|
|
|
|
while (currentIndexedRun.TextSourceCharacterIndex != currentPosition)
|
|
{
|
|
if (i + 1 == _indexedTextRuns.Count)
|
|
{
|
|
break;
|
|
}
|
|
|
|
i++;
|
|
|
|
currentIndexedRun = _indexedTextRuns[i];
|
|
}
|
|
|
|
return currentIndexedRun;
|
|
}
|
|
|
|
double GetPreceedingDistance(int firstIndex)
|
|
{
|
|
var distance = 0.0;
|
|
|
|
for (var i = 0; i < firstIndex; i++)
|
|
{
|
|
var currentRun = _textRuns[i];
|
|
|
|
if (currentRun is DrawableTextRun drawableTextRun)
|
|
{
|
|
distance += drawableTextRun.Size.Width;
|
|
}
|
|
}
|
|
|
|
return distance;
|
|
}
|
|
|
|
TextRun? currentTextRun = null;
|
|
var currentIndexedRun = FindIndexedRun();
|
|
|
|
while (currentPosition < FirstTextSourceIndex + Length)
|
|
{
|
|
currentTextRun = currentIndexedRun.TextRun;
|
|
|
|
if (currentTextRun == null)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (currentIndexedRun.TextSourceCharacterIndex + currentTextRun.Length <= characterHit.FirstCharacterIndex)
|
|
{
|
|
if (currentPosition + currentTextRun.Length < FirstTextSourceIndex + Length)
|
|
{
|
|
currentPosition += currentTextRun.Length;
|
|
|
|
currentIndexedRun = FindIndexedRun();
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
if (currentTextRun == null)
|
|
{
|
|
return Start;
|
|
}
|
|
|
|
var directionalWidth = 0.0;
|
|
var firstRunIndex = currentIndexedRun.RunIndex;
|
|
var lastRunIndex = firstRunIndex;
|
|
|
|
var currentDirection = GetDirection(currentTextRun, _resolvedFlowDirection);
|
|
|
|
var currentX = Start + GetPreceedingDistance(currentIndexedRun.RunIndex);
|
|
|
|
if (currentTextRun is DrawableTextRun currentDrawable)
|
|
{
|
|
directionalWidth = currentDrawable.Size.Width;
|
|
}
|
|
|
|
if (currentTextRun is not TextEndOfLine)
|
|
{
|
|
if (currentDirection == FlowDirection.LeftToRight)
|
|
{
|
|
// Find consecutive runs of same direction
|
|
for (; lastRunIndex + 1 < _textRuns.Length; lastRunIndex++)
|
|
{
|
|
var nextRun = _textRuns[lastRunIndex + 1];
|
|
|
|
var nextDirection = GetDirection(nextRun, currentDirection);
|
|
|
|
if (currentDirection != nextDirection)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (nextRun is DrawableTextRun nextDrawable)
|
|
{
|
|
directionalWidth += nextDrawable.Size.Width;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Find consecutive runs of same direction
|
|
for (; firstRunIndex - 1 > 0; firstRunIndex--)
|
|
{
|
|
var previousRun = _textRuns[firstRunIndex - 1];
|
|
|
|
var previousDirection = GetDirection(previousRun, currentDirection);
|
|
|
|
if (currentDirection != previousDirection)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (previousRun is DrawableTextRun previousDrawable)
|
|
{
|
|
directionalWidth += previousDrawable.Size.Width;
|
|
|
|
currentX -= previousDrawable.Size.Width;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
switch (currentDirection)
|
|
{
|
|
case FlowDirection.RightToLeft:
|
|
{
|
|
return GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX + directionalWidth, characterIndex,
|
|
currentPosition, 1, out _, out _).Rectangle.Right;
|
|
}
|
|
default:
|
|
{
|
|
return GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, characterIndex,
|
|
currentPosition, 1, out _, out _).Rectangle.Left;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit)
|
|
{
|
|
if (_textRuns.Length == 0 || _indexedTextRuns is null)
|
|
{
|
|
return new CharacterHit();
|
|
}
|
|
|
|
var currentCharacterrHit = characterHit;
|
|
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
|
|
|
|
var currentRun = GetRunAtCharacterIndex(characterIndex, LogicalDirection.Forward, out var currentPosition);
|
|
|
|
var nextCharacterHit = characterHit;
|
|
|
|
switch (currentRun)
|
|
{
|
|
case ShapedTextRun shapedRun:
|
|
{
|
|
var offset = Math.Max(0, currentPosition - shapedRun.GlyphRun.Metrics.FirstCluster - characterHit.TrailingLength);
|
|
|
|
if (offset > 0)
|
|
{
|
|
currentCharacterrHit = new CharacterHit(Math.Max(0, characterHit.FirstCharacterIndex - offset), characterHit.TrailingLength);
|
|
}
|
|
|
|
nextCharacterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(currentCharacterrHit);
|
|
|
|
if (offset > 0)
|
|
{
|
|
nextCharacterHit = new CharacterHit(nextCharacterHit.FirstCharacterIndex + offset, nextCharacterHit.TrailingLength);
|
|
}
|
|
break;
|
|
}
|
|
case TextRun:
|
|
{
|
|
nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (characterIndex == nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength)
|
|
{
|
|
return characterHit;
|
|
}
|
|
|
|
return nextCharacterHit;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit)
|
|
{
|
|
return GetPreviousCharacterHit(characterHit, false);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit)
|
|
{
|
|
return GetPreviousCharacterHit(characterHit, true);
|
|
}
|
|
|
|
public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength)
|
|
{
|
|
if(textLength == 0)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(textLength), textLength, $"{nameof(textLength)} ('0') must be a non-zero value. ");
|
|
}
|
|
|
|
if (_indexedTextRuns is null || _indexedTextRuns.Count == 0)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
var currentPosition = FirstTextSourceIndex;
|
|
var remainingLength = textLength;
|
|
|
|
//We can return early if the requested text range is before the line's text range.
|
|
if (firstTextSourceIndex + textLength < FirstTextSourceIndex)
|
|
{
|
|
var indexedTextRun = _indexedTextRuns[0];
|
|
var currentDirection = GetDirection(indexedTextRun.TextRun, _resolvedFlowDirection);
|
|
|
|
return [new TextBounds(new Rect(0,0,0, Height), currentDirection, [])];
|
|
}
|
|
|
|
//We can return early if the requested text range is after the line's text range.
|
|
if (firstTextSourceIndex >= FirstTextSourceIndex + Length)
|
|
{
|
|
var indexedTextRun = _indexedTextRuns[_indexedTextRuns.Count - 1];
|
|
var currentDirection = GetDirection(indexedTextRun.TextRun, _resolvedFlowDirection);
|
|
|
|
return [new TextBounds(new Rect(WidthIncludingTrailingWhitespace, 0, 0, Height), currentDirection, [])];
|
|
}
|
|
|
|
var result = new List<TextBounds>();
|
|
|
|
TextBounds? lastBounds = null;
|
|
|
|
while (remainingLength > 0 && currentPosition < FirstTextSourceIndex + Length)
|
|
{
|
|
var currentIndexedRun = FindIndexedRun();
|
|
|
|
if (currentIndexedRun == null)
|
|
{
|
|
break;
|
|
}
|
|
|
|
var directionalWidth = 0.0;
|
|
var firstRunIndex = currentIndexedRun.RunIndex;
|
|
var lastRunIndex = firstRunIndex;
|
|
var currentTextRun = currentIndexedRun.TextRun;
|
|
|
|
if (currentTextRun == null)
|
|
{
|
|
break;
|
|
}
|
|
|
|
var currentDirection = GetDirection(currentTextRun, _resolvedFlowDirection);
|
|
|
|
if (currentIndexedRun.TextSourceCharacterIndex + currentTextRun.Length <= firstTextSourceIndex)
|
|
{
|
|
currentPosition += currentTextRun.Length;
|
|
|
|
continue;
|
|
}
|
|
|
|
var currentX = Start + GetPreceedingDistance(currentIndexedRun.RunIndex);
|
|
|
|
if (currentTextRun is DrawableTextRun currentDrawable)
|
|
{
|
|
directionalWidth = currentDrawable.Size.Width;
|
|
}
|
|
|
|
TextBounds currentBounds;
|
|
int coveredLength;
|
|
|
|
switch (currentDirection)
|
|
{
|
|
case FlowDirection.RightToLeft:
|
|
{
|
|
currentBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX + directionalWidth, firstTextSourceIndex,
|
|
currentPosition, remainingLength, out coveredLength, out currentPosition);
|
|
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
currentBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
|
|
currentPosition, remainingLength, out coveredLength, out currentPosition);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (lastBounds != null && TryMergeWithLastBounds(currentBounds, lastBounds))
|
|
{
|
|
currentBounds = lastBounds;
|
|
|
|
result[result.Count - 1] = currentBounds;
|
|
}
|
|
else
|
|
{
|
|
result.Add(currentBounds);
|
|
}
|
|
|
|
lastBounds = currentBounds;
|
|
|
|
if(coveredLength <= 0)
|
|
{
|
|
throw new InvalidOperationException("Covered length must be greater than zero.");
|
|
}
|
|
|
|
remainingLength -= coveredLength;
|
|
}
|
|
|
|
result.Sort(TextBoundsComparer);
|
|
|
|
return result;
|
|
|
|
static FlowDirection GetDirection(TextRun? textRun, FlowDirection currentDirection)
|
|
{
|
|
if (textRun is ShapedTextRun shapedTextRun)
|
|
{
|
|
return shapedTextRun.ShapedBuffer.IsLeftToRight ?
|
|
FlowDirection.LeftToRight :
|
|
FlowDirection.RightToLeft;
|
|
}
|
|
|
|
return currentDirection;
|
|
}
|
|
|
|
IndexedTextRun FindIndexedRun()
|
|
{
|
|
var i = 0;
|
|
|
|
var currentIndexedRun = _indexedTextRuns[i];
|
|
|
|
while (currentIndexedRun.TextSourceCharacterIndex != currentPosition)
|
|
{
|
|
if (i + 1 == _indexedTextRuns.Count)
|
|
{
|
|
break;
|
|
}
|
|
|
|
i++;
|
|
|
|
currentIndexedRun = _indexedTextRuns[i];
|
|
}
|
|
|
|
return currentIndexedRun;
|
|
}
|
|
|
|
double GetPreceedingDistance(int firstIndex)
|
|
{
|
|
var distance = 0.0;
|
|
|
|
for (var i = 0; i < firstIndex; i++)
|
|
{
|
|
var currentRun = _textRuns[i];
|
|
|
|
if (currentRun is DrawableTextRun drawableTextRun)
|
|
{
|
|
distance += drawableTextRun.Size.Width;
|
|
}
|
|
}
|
|
|
|
return distance;
|
|
}
|
|
|
|
bool TryMergeWithLastBounds(TextBounds currentBounds, TextBounds lastBounds)
|
|
{
|
|
if (currentBounds.FlowDirection != lastBounds.FlowDirection)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (currentBounds.Rectangle.Left == lastBounds.Rectangle.Right)
|
|
{
|
|
foreach (var runBounds in currentBounds.TextRunBounds)
|
|
{
|
|
lastBounds.TextRunBounds.Add(runBounds);
|
|
}
|
|
|
|
lastBounds.Rectangle = lastBounds.Rectangle.Union(currentBounds.Rectangle);
|
|
|
|
return true;
|
|
}
|
|
|
|
if (currentBounds.Rectangle.Right == lastBounds.Rectangle.Left)
|
|
{
|
|
for (int i = 0; i < currentBounds.TextRunBounds.Count; i++)
|
|
{
|
|
lastBounds.TextRunBounds.Insert(i, currentBounds.TextRunBounds[i]);
|
|
}
|
|
|
|
lastBounds.Rectangle = lastBounds.Rectangle.Union(currentBounds.Rectangle);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private CharacterHit GetPreviousCharacterHit(CharacterHit characterHit, bool useGraphemeBoundaries)
|
|
{
|
|
if (_textRuns.Length == 0 || _indexedTextRuns is null)
|
|
{
|
|
return new CharacterHit();
|
|
}
|
|
|
|
if (characterHit.TrailingLength > 0 && characterHit.FirstCharacterIndex <= FirstTextSourceIndex)
|
|
{
|
|
return new CharacterHit(FirstTextSourceIndex);
|
|
}
|
|
|
|
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
|
|
|
|
if (characterIndex <= FirstTextSourceIndex)
|
|
{
|
|
return new CharacterHit(FirstTextSourceIndex);
|
|
}
|
|
|
|
var currentCharacterHit = characterHit;
|
|
|
|
var currentRun = GetRunAtCharacterIndex(characterIndex, LogicalDirection.Backward, out var currentPosition);
|
|
|
|
var previousCharacterHit = characterHit;
|
|
|
|
switch (currentRun)
|
|
{
|
|
case ShapedTextRun shapedRun:
|
|
{
|
|
var offset = Math.Max(0, currentPosition - shapedRun.GlyphRun.Metrics.FirstCluster);
|
|
|
|
if (offset > 0)
|
|
{
|
|
currentCharacterHit = new CharacterHit(Math.Max(0, characterHit.FirstCharacterIndex - offset), characterHit.TrailingLength);
|
|
}
|
|
|
|
previousCharacterHit = shapedRun.GlyphRun.GetPreviousCaretCharacterHit(currentCharacterHit);
|
|
|
|
if (useGraphemeBoundaries)
|
|
{
|
|
var textPosition = Math.Max(0, previousCharacterHit.FirstCharacterIndex - shapedRun.GlyphRun.Metrics.FirstCluster);
|
|
|
|
var text = shapedRun.GlyphRun.Characters.Slice(textPosition);
|
|
|
|
var graphemeEnumerator = new GraphemeEnumerator(text.Span);
|
|
|
|
var length = 0;
|
|
|
|
var clusterLength = Math.Max(0, currentCharacterHit.FirstCharacterIndex + currentCharacterHit.TrailingLength -
|
|
previousCharacterHit.FirstCharacterIndex - previousCharacterHit.TrailingLength);
|
|
|
|
while (graphemeEnumerator.MoveNext(out var grapheme))
|
|
{
|
|
if (length + grapheme.Length < clusterLength)
|
|
{
|
|
length += grapheme.Length;
|
|
|
|
continue;
|
|
}
|
|
|
|
previousCharacterHit = new CharacterHit(previousCharacterHit.FirstCharacterIndex + length);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (offset > 0)
|
|
{
|
|
previousCharacterHit = new CharacterHit(previousCharacterHit.FirstCharacterIndex + offset, previousCharacterHit.TrailingLength);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case TextRun:
|
|
{
|
|
previousCharacterHit = new CharacterHit(currentPosition);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (characterIndex == previousCharacterHit.FirstCharacterIndex + previousCharacterHit.TrailingLength)
|
|
{
|
|
return characterHit;
|
|
}
|
|
|
|
return previousCharacterHit;
|
|
}
|
|
|
|
private TextBounds GetTextRunBoundsRightToLeft(int firstRunIndex, int lastRunIndex, double endX,
|
|
int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition)
|
|
{
|
|
coveredLength = 0;
|
|
var textRunBounds = new List<TextRunBounds>();
|
|
var startX = endX;
|
|
|
|
for (int i = lastRunIndex; i >= firstRunIndex; i--)
|
|
{
|
|
var currentRun = _textRuns[i];
|
|
|
|
if (currentRun is ShapedTextRun shapedTextRun)
|
|
{
|
|
var runBounds = GetRunBoundsRightToLeft(shapedTextRun, startX, firstTextSourceIndex, remainingLength, currentPosition, out var offset);
|
|
|
|
if (runBounds.TextSourceCharacterIndex < FirstTextSourceIndex + Length)
|
|
{
|
|
textRunBounds.Insert(0, runBounds);
|
|
}
|
|
|
|
if (offset > 0)
|
|
{
|
|
endX = runBounds.Rectangle.Right;
|
|
|
|
startX = endX;
|
|
}
|
|
|
|
startX -= runBounds.Rectangle.Width;
|
|
|
|
currentPosition += runBounds.Length + offset;
|
|
|
|
coveredLength += runBounds.Length;
|
|
|
|
remainingLength -= runBounds.Length;
|
|
}
|
|
else
|
|
{
|
|
if (currentPosition < FirstTextSourceIndex + Length)
|
|
{
|
|
if (currentRun is DrawableTextRun drawableTextRun)
|
|
{
|
|
startX -= drawableTextRun.Size.Width;
|
|
|
|
var runBounds = new TextRunBounds(
|
|
new Rect(startX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun);
|
|
|
|
textRunBounds.Insert(0, runBounds);
|
|
}
|
|
else
|
|
{
|
|
//Add potential TextEndOfParagraph
|
|
var runBounds = new TextRunBounds(
|
|
new Rect(endX, 0, 0, Height), currentPosition, currentRun.Length, currentRun);
|
|
|
|
textRunBounds.Add(runBounds);
|
|
}
|
|
}
|
|
|
|
currentPosition += currentRun.Length;
|
|
|
|
coveredLength += currentRun.Length;
|
|
|
|
remainingLength -= currentRun.Length;
|
|
}
|
|
|
|
if (remainingLength <= 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
newPosition = currentPosition;
|
|
|
|
var runWidth = endX - startX;
|
|
|
|
var bounds = new Rect(startX, 0, runWidth, Height);
|
|
|
|
return new TextBounds(bounds, FlowDirection.RightToLeft, textRunBounds);
|
|
}
|
|
|
|
private TextBounds GetTextBoundsLeftToRight(int firstRunIndex, int lastRunIndex, double startX,
|
|
int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition)
|
|
{
|
|
coveredLength = 0;
|
|
var textRunBounds = new List<TextRunBounds>(1);
|
|
var endX = startX;
|
|
|
|
for (int i = firstRunIndex; i <= lastRunIndex; i++)
|
|
{
|
|
var currentRun = _textRuns[i];
|
|
|
|
if (currentRun is ShapedTextRun shapedTextRun)
|
|
{
|
|
var runBounds = GetRunBoundsLeftToRight(shapedTextRun, endX, firstTextSourceIndex, remainingLength, currentPosition, out var offset);
|
|
|
|
if(runBounds.TextSourceCharacterIndex < FirstTextSourceIndex + Length)
|
|
{
|
|
textRunBounds.Add(runBounds);
|
|
}
|
|
|
|
if (offset > 0)
|
|
{
|
|
startX = runBounds.Rectangle.Left;
|
|
|
|
endX = startX;
|
|
}
|
|
|
|
currentPosition += runBounds.Length + offset;
|
|
|
|
endX += runBounds.Rectangle.Width;
|
|
|
|
coveredLength += runBounds.Length;
|
|
|
|
remainingLength -= runBounds.Length;
|
|
}
|
|
else
|
|
{
|
|
if (currentPosition < FirstTextSourceIndex + Length)
|
|
{
|
|
if (currentRun is DrawableTextRun drawableTextRun)
|
|
{
|
|
var runBounds = new TextRunBounds(
|
|
new Rect(endX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun);
|
|
|
|
textRunBounds.Add(runBounds);
|
|
|
|
|
|
endX += drawableTextRun.Size.Width;
|
|
}
|
|
else
|
|
{
|
|
//Add potential TextEndOfParagraph
|
|
var runBounds = new TextRunBounds(
|
|
new Rect(endX, 0, 0, Height), currentPosition, currentRun.Length, currentRun);
|
|
|
|
textRunBounds.Add(runBounds);
|
|
}
|
|
}
|
|
|
|
currentPosition += currentRun.Length;
|
|
|
|
coveredLength += currentRun.Length;
|
|
|
|
remainingLength -= currentRun.Length;
|
|
}
|
|
|
|
if (remainingLength <= 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
newPosition = currentPosition;
|
|
|
|
var runWidth = endX - startX;
|
|
|
|
var bounds = new Rect(startX, 0, runWidth, Height);
|
|
|
|
return new TextBounds(bounds, FlowDirection.LeftToRight, textRunBounds);
|
|
}
|
|
|
|
private TextRunBounds GetRunBoundsLeftToRight(ShapedTextRun currentRun, double startX,
|
|
int firstTextSourceIndex, int remainingLength, int currentPosition, out int offset)
|
|
{
|
|
var startIndex = currentPosition;
|
|
|
|
offset = Math.Max(0, firstTextSourceIndex - currentPosition);
|
|
|
|
var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster;
|
|
|
|
if (currentPosition != firstCluster)
|
|
{
|
|
startIndex = firstCluster + offset;
|
|
}
|
|
else
|
|
{
|
|
startIndex += offset;
|
|
}
|
|
|
|
//Make sure we start the hit test at the start of the possible cluster.
|
|
var clusterStartHit = currentRun.GlyphRun.GetPreviousCaretCharacterHit(new CharacterHit(startIndex));
|
|
var clusterEndHit = currentRun.GlyphRun.GetNextCaretCharacterHit(clusterStartHit);
|
|
|
|
var clusterOffset = 0;
|
|
|
|
if (startIndex > clusterStartHit.FirstCharacterIndex && startIndex < clusterEndHit.FirstCharacterIndex + clusterEndHit.TrailingLength)
|
|
{
|
|
clusterOffset = clusterEndHit.FirstCharacterIndex + clusterEndHit.TrailingLength - startIndex;
|
|
|
|
//We need to move the startIndex to the start of the cluster.
|
|
startIndex -= clusterOffset;
|
|
}
|
|
|
|
var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
|
|
var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
|
|
|
|
var endX = startX + endOffset;
|
|
startX += startOffset;
|
|
|
|
var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
|
|
var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
|
|
|
|
//Adjust characterLength by the cluster offset to only cover the remaining length of the cluster.
|
|
var characterLength = Math.Max(0, Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength -
|
|
endHit.FirstCharacterIndex - endHit.TrailingLength) - clusterOffset);
|
|
|
|
if (characterLength == 0 && currentRun.Text.Length > 0 && startIndex < currentRun.Text.Length)
|
|
{
|
|
//Make sure we are properly dealing with zero width space runs
|
|
var codepointEnumerator = new CodepointEnumerator(currentRun.Text.Span.Slice(startIndex));
|
|
|
|
while (remainingLength > 0 && codepointEnumerator.MoveNext(out var codepoint))
|
|
{
|
|
if (codepoint.IsWhiteSpace)
|
|
{
|
|
characterLength++;
|
|
remainingLength--;
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (endX < startX)
|
|
{
|
|
(endX, startX) = (startX, endX);
|
|
}
|
|
|
|
//Lines that only contain a linebreak need to be covered here
|
|
if (characterLength == 0)
|
|
{
|
|
characterLength = NewLineLength;
|
|
}
|
|
|
|
var runWidth = endX - startX;
|
|
|
|
var textSourceIndex = startIndex + Math.Max(0, currentPosition - firstCluster) + clusterOffset;
|
|
|
|
return new TextRunBounds(new Rect(startX, 0, runWidth, Height), textSourceIndex, characterLength, currentRun);
|
|
}
|
|
|
|
private TextRunBounds GetRunBoundsRightToLeft(ShapedTextRun currentRun, double endX,
|
|
int firstTextSourceIndex, int remainingLength, int currentPosition, out int offset)
|
|
{
|
|
var startX = endX;
|
|
|
|
var startIndex = currentPosition;
|
|
|
|
offset = Math.Max(0, firstTextSourceIndex - currentPosition);
|
|
|
|
var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster;
|
|
|
|
if (currentPosition != firstCluster)
|
|
{
|
|
startIndex = firstCluster + offset;
|
|
}
|
|
else
|
|
{
|
|
startIndex += offset;
|
|
}
|
|
|
|
var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
|
|
var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
|
|
|
|
startX -= currentRun.Size.Width - startOffset;
|
|
endX -= currentRun.Size.Width - endOffset;
|
|
|
|
var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
|
|
var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
|
|
|
|
//Make sure we start the hit test at the start of the possible cluster.
|
|
var clusterStartHit = currentRun.GlyphRun.GetNextCaretCharacterHit(new CharacterHit(startIndex));
|
|
var clusterEndHit = currentRun.GlyphRun.GetPreviousCaretCharacterHit(startHit);
|
|
|
|
var clusterOffset = 0;
|
|
|
|
if (startIndex > clusterStartHit.FirstCharacterIndex && startIndex < clusterEndHit.FirstCharacterIndex + clusterEndHit.TrailingLength)
|
|
{
|
|
clusterOffset = clusterEndHit.FirstCharacterIndex + clusterEndHit.TrailingLength - startIndex;
|
|
|
|
//We need to move the startIndex to the start of the cluster.
|
|
startIndex -= clusterOffset;
|
|
}
|
|
|
|
var characterLength = Math.Max(0, Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength -
|
|
endHit.FirstCharacterIndex - endHit.TrailingLength) - clusterOffset);
|
|
|
|
if (characterLength == 0 && currentRun.Text.Length > 0 && startIndex < currentRun.Text.Length)
|
|
{
|
|
//Make sure we are properly dealing with zero width space runs
|
|
var codepointEnumerator = new CodepointEnumerator(currentRun.Text.Span.Slice(startIndex));
|
|
|
|
while (remainingLength > 0 && codepointEnumerator.MoveNext(out var codepoint))
|
|
{
|
|
if (codepoint.IsWhiteSpace)
|
|
{
|
|
characterLength++;
|
|
remainingLength--;
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (startHit.FirstCharacterIndex > endHit.FirstCharacterIndex)
|
|
{
|
|
startHit = endHit;
|
|
}
|
|
|
|
if (endX < startX)
|
|
{
|
|
(endX, startX) = (startX, endX);
|
|
}
|
|
|
|
//Lines that only contain a linebreak need to be covered here
|
|
if (characterLength == 0)
|
|
{
|
|
characterLength = NewLineLength;
|
|
}
|
|
|
|
var runWidth = endX - startX;
|
|
|
|
var textSourceIndex = startIndex + Math.Max(0, currentPosition - firstCluster) + clusterOffset;
|
|
|
|
return new TextRunBounds(new Rect(startX, 0, runWidth, Height), textSourceIndex, characterLength, currentRun);
|
|
}
|
|
|
|
public override void Dispose()
|
|
{
|
|
for (int i = 0; i < _textRuns.Length; i++)
|
|
{
|
|
if (_textRuns[i] is ShapedTextRun shapedTextRun)
|
|
{
|
|
shapedTextRun.Dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
public void FinalizeLine()
|
|
{
|
|
_indexedTextRuns = BidiReorderer.Instance.BidiReorder(_textRuns, _paragraphProperties.FlowDirection, FirstTextSourceIndex);
|
|
|
|
_textLineMetrics = CreateLineMetrics();
|
|
|
|
if (_textLineBreak is null && _textRuns.Length > 1 && _textRuns[_textRuns.Length - 1] is TextEndOfLine textEndOfLine)
|
|
{
|
|
_textLineBreak = new TextLineBreak(textEndOfLine);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the run index of the specified codepoint index.
|
|
/// </summary>
|
|
/// <param name="codepointIndex">The codepoint index.</param>
|
|
/// <param name="direction">The logical direction.</param>
|
|
/// <param name="textPosition">The text position of the found run index.</param>
|
|
/// <returns>The text run index.</returns>
|
|
private TextRun? GetRunAtCharacterIndex(int codepointIndex, LogicalDirection direction, out int textPosition)
|
|
{
|
|
var runIndex = 0;
|
|
textPosition = FirstTextSourceIndex;
|
|
|
|
if (_indexedTextRuns is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
TextRun? currentRun = null;
|
|
|
|
while (runIndex < _indexedTextRuns.Count)
|
|
{
|
|
var indexedRun = _indexedTextRuns[runIndex];
|
|
currentRun = indexedRun.TextRun;
|
|
|
|
switch (currentRun)
|
|
{
|
|
case ShapedTextRun shapedRun:
|
|
{
|
|
var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster;
|
|
|
|
firstCluster += Math.Max(0, indexedRun.TextSourceCharacterIndex - firstCluster);
|
|
|
|
if (direction == LogicalDirection.Forward)
|
|
{
|
|
if (codepointIndex >= firstCluster && codepointIndex < firstCluster + currentRun.Length)
|
|
{
|
|
return currentRun;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (codepointIndex > firstCluster && codepointIndex <= firstCluster + currentRun.Length)
|
|
{
|
|
return currentRun;
|
|
}
|
|
}
|
|
|
|
if (runIndex + 1 >= _textRuns.Length)
|
|
{
|
|
return currentRun;
|
|
}
|
|
|
|
textPosition += currentRun.Length;
|
|
|
|
break;
|
|
}
|
|
case not null:
|
|
{
|
|
if(direction == LogicalDirection.Forward)
|
|
{
|
|
if (textPosition == codepointIndex)
|
|
{
|
|
return currentRun;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (textPosition + currentRun.Length == codepointIndex)
|
|
{
|
|
return currentRun;
|
|
}
|
|
}
|
|
|
|
if (runIndex + 1 >= _textRuns.Length)
|
|
{
|
|
return currentRun;
|
|
}
|
|
|
|
textPosition += currentRun.Length;
|
|
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
runIndex++;
|
|
}
|
|
|
|
return currentRun;
|
|
}
|
|
|
|
private TextLineMetrics CreateLineMetrics()
|
|
{
|
|
var fontMetrics = _paragraphProperties.DefaultTextRunProperties.CachedGlyphTypeface.Metrics;
|
|
var fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize;
|
|
var scale = fontRenderingEmSize / fontMetrics.DesignEmHeight;
|
|
var widthIncludingWhitespace = 0d;
|
|
var trailingWhitespaceLength = 0;
|
|
var newLineLength = 0;
|
|
var ascent = fontMetrics.Ascent * scale;
|
|
var descent = fontMetrics.Descent * scale;
|
|
var lineGap = fontMetrics.LineGap * scale;
|
|
|
|
var lineHeight = _paragraphProperties.LineHeight;
|
|
var lineSpacing = _paragraphProperties.LineSpacing;
|
|
|
|
for (var index = 0; index < _textRuns.Length; index++)
|
|
{
|
|
switch (_textRuns[index])
|
|
{
|
|
case ShapedTextRun textRun:
|
|
{
|
|
var textMetrics = textRun.TextMetrics;
|
|
|
|
if (ascent > textMetrics.Ascent)
|
|
{
|
|
ascent = textMetrics.Ascent;
|
|
}
|
|
|
|
if (descent < textMetrics.Descent)
|
|
{
|
|
descent = textMetrics.Descent;
|
|
}
|
|
|
|
if (lineGap < textMetrics.LineGap)
|
|
{
|
|
lineGap = textMetrics.LineGap;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case DrawableTextRun drawableTextRun:
|
|
{
|
|
if (drawableTextRun.Size.Height > -ascent)
|
|
{
|
|
ascent = -drawableTextRun.Size.Height;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
var height = descent - ascent + lineGap;
|
|
|
|
var inkBounds = new Rect();
|
|
|
|
for (var index = 0; index < _textRuns.Length; index++)
|
|
{
|
|
switch (_textRuns[index])
|
|
{
|
|
case ShapedTextRun textRun:
|
|
{
|
|
var glyphRun = textRun.GlyphRun;
|
|
//Align the ink bounds at the common baseline
|
|
var offsetY = -ascent - textRun.Baseline;
|
|
|
|
var runBounds = glyphRun.InkBounds.Translate(new Vector(widthIncludingWhitespace, offsetY));
|
|
|
|
inkBounds = inkBounds.Union(runBounds);
|
|
|
|
widthIncludingWhitespace += textRun.Size.Width;
|
|
|
|
break;
|
|
}
|
|
|
|
case DrawableTextRun drawableTextRun:
|
|
{
|
|
//Align the bounds at the common baseline
|
|
var offsetY = -ascent - drawableTextRun.Baseline;
|
|
|
|
inkBounds = inkBounds.Union(new Rect(new Point(widthIncludingWhitespace, offsetY), drawableTextRun.Size));
|
|
|
|
widthIncludingWhitespace += drawableTextRun.Size.Width;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
height += lineSpacing;
|
|
|
|
var width = widthIncludingWhitespace;
|
|
|
|
var isRtl = _paragraphProperties.FlowDirection == FlowDirection.RightToLeft;
|
|
|
|
for (int i = 0; i < _textRuns.Length; i++)
|
|
{
|
|
var index = isRtl ? i : _textRuns.Length - 1 - i;
|
|
var currentRun = _textRuns[index];
|
|
|
|
if (currentRun is ShapedTextRun shapedText)
|
|
{
|
|
var glyphRun = shapedText.GlyphRun;
|
|
var glyphRunMetrics = glyphRun.Metrics;
|
|
|
|
newLineLength += glyphRunMetrics.NewLineLength;
|
|
|
|
if (glyphRunMetrics.TrailingWhitespaceLength == 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
trailingWhitespaceLength += glyphRunMetrics.TrailingWhitespaceLength;
|
|
|
|
var whitespaceWidth = glyphRun.Bounds.Width - glyphRunMetrics.Width;
|
|
|
|
width -= whitespaceWidth;
|
|
}
|
|
}
|
|
|
|
var extent = inkBounds.Height;
|
|
//The width of overhanging pixels at the bottom
|
|
var overhangAfter = inkBounds.Bottom - height;
|
|
//The width of overhanging pixels at the natural alignment point. Positive value means we are inside.
|
|
var overhangLeading = inkBounds.Left;
|
|
//The width of overhanging pixels at the end of the natural bounds. Positive value means we are inside.
|
|
var overhangTrailing = widthIncludingWhitespace - inkBounds.Right;
|
|
var hasOverflowed = width > _paragraphWidth;
|
|
|
|
if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
|
|
{
|
|
//Center the line
|
|
var offset = (height - lineHeight) / 2;
|
|
|
|
ascent += offset;
|
|
|
|
height = lineHeight;
|
|
}
|
|
|
|
var start = GetParagraphOffsetX(width, widthIncludingWhitespace);
|
|
|
|
_inkBounds = inkBounds.Translate(new Vector(start, 0));
|
|
|
|
_bounds = new Rect(start, 0, widthIncludingWhitespace, height);
|
|
|
|
return new TextLineMetrics
|
|
{
|
|
HasOverflowed = hasOverflowed,
|
|
Height = height,
|
|
Extent = extent,
|
|
NewlineLength = newLineLength,
|
|
Start = start,
|
|
TextBaseline = -ascent,
|
|
TrailingWhitespaceLength = trailingWhitespaceLength,
|
|
Width = width,
|
|
WidthIncludingTrailingWhitespace = widthIncludingWhitespace,
|
|
OverhangLeading = overhangLeading,
|
|
OverhangTrailing = overhangTrailing,
|
|
OverhangAfter = overhangAfter
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the text line offset x.
|
|
/// </summary>
|
|
/// <param name="width">The line width.</param>
|
|
/// <param name="widthIncludingTrailingWhitespace">The paragraph width including whitespace.</param>
|
|
|
|
/// <returns>The paragraph offset.</returns>
|
|
private double GetParagraphOffsetX(double width, double widthIncludingTrailingWhitespace)
|
|
{
|
|
if (double.IsPositiveInfinity(_paragraphWidth))
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
var textAlignment = _paragraphProperties.TextAlignment;
|
|
var paragraphFlowDirection = _paragraphProperties.FlowDirection;
|
|
|
|
if (textAlignment == TextAlignment.Justify)
|
|
{
|
|
textAlignment = TextAlignment.Start;
|
|
}
|
|
|
|
switch (textAlignment)
|
|
{
|
|
case TextAlignment.Start:
|
|
{
|
|
textAlignment = paragraphFlowDirection == FlowDirection.LeftToRight ? TextAlignment.Left : TextAlignment.Right;
|
|
break;
|
|
}
|
|
case TextAlignment.End:
|
|
{
|
|
textAlignment = paragraphFlowDirection == FlowDirection.RightToLeft ? TextAlignment.Left : TextAlignment.Right;
|
|
break;
|
|
}
|
|
case TextAlignment.DetectFromContent:
|
|
{
|
|
textAlignment = _resolvedFlowDirection == FlowDirection.LeftToRight ? TextAlignment.Left : TextAlignment.Right;
|
|
break;
|
|
}
|
|
}
|
|
|
|
switch (textAlignment)
|
|
{
|
|
case TextAlignment.Center:
|
|
var start = (_paragraphWidth - width) / 2;
|
|
|
|
if (paragraphFlowDirection == FlowDirection.RightToLeft)
|
|
{
|
|
start -= (widthIncludingTrailingWhitespace - width);
|
|
}
|
|
|
|
return Math.Max(0, start);
|
|
case TextAlignment.Right:
|
|
return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace);
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|