|
|
|
@ -6,42 +6,43 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
{ |
|
|
|
internal class SimpleTextLine : TextLine |
|
|
|
{ |
|
|
|
public SimpleTextLine(TextPointer textPointer, IReadOnlyList<TextRun> textRuns, TextLineMetrics lineMetrics) : |
|
|
|
base(textPointer, textRuns, lineMetrics) |
|
|
|
{ |
|
|
|
private readonly IReadOnlyList<ShapedTextRun> _textRuns; |
|
|
|
|
|
|
|
public SimpleTextLine(TextPointer textPointer, IReadOnlyList<ShapedTextRun> textRuns, TextLineMetrics lineMetrics) |
|
|
|
{ |
|
|
|
Text = textPointer; |
|
|
|
_textRuns = textRuns; |
|
|
|
LineMetrics = lineMetrics; |
|
|
|
} |
|
|
|
|
|
|
|
/// <inheritdoc/>
|
|
|
|
public override TextPointer Text { get; } |
|
|
|
|
|
|
|
/// <inheritdoc/>
|
|
|
|
public override IReadOnlyList<TextRun> TextRuns => _textRuns; |
|
|
|
|
|
|
|
/// <inheritdoc/>
|
|
|
|
public override TextLineMetrics LineMetrics { get; } |
|
|
|
|
|
|
|
/// <inheritdoc/>
|
|
|
|
public override void Draw(IDrawingContextImpl drawingContext, Point origin) |
|
|
|
{ |
|
|
|
var currentX = origin.X; |
|
|
|
|
|
|
|
foreach (var textRun in TextRuns) |
|
|
|
foreach (var textRun in _textRuns) |
|
|
|
{ |
|
|
|
if (!(textRun is DrawableTextRun drawableRun)) |
|
|
|
{ |
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
var baselineOrigin = new Point(currentX + LineMetrics.BaselineOrigin.X, |
|
|
|
origin.Y + LineMetrics.BaselineOrigin.Y); |
|
|
|
|
|
|
|
drawableRun.Draw(drawingContext, baselineOrigin); |
|
|
|
textRun.Draw(drawingContext, baselineOrigin); |
|
|
|
|
|
|
|
currentX += drawableRun.Bounds.Width; |
|
|
|
currentX += textRun.Bounds.Width; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Client to get the character hit corresponding to the specified
|
|
|
|
/// distance from the beginning of the line.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="distance">distance in text flow direction from the beginning of the line</param>
|
|
|
|
/// <returns>character hit</returns>
|
|
|
|
/// <inheritdoc/>
|
|
|
|
public override CharacterHit GetCharacterHitFromDistance(double distance) |
|
|
|
{ |
|
|
|
var first = Text.Start; |
|
|
|
|
|
|
|
if (distance < 0) |
|
|
|
{ |
|
|
|
// hit happens before the line, return the first position
|
|
|
|
@ -49,45 +50,30 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
} |
|
|
|
|
|
|
|
// process hit that happens within the line
|
|
|
|
var runIndex = new CharacterHit(); |
|
|
|
var characterHit = new CharacterHit(); |
|
|
|
|
|
|
|
foreach (var run in TextRuns) |
|
|
|
foreach (var run in _textRuns) |
|
|
|
{ |
|
|
|
var shapedTextRun = (ShapedTextRun)run; |
|
|
|
|
|
|
|
first += runIndex.TrailingLength; |
|
|
|
characterHit = run.GlyphRun.GetCharacterHitFromDistance(distance, out _); |
|
|
|
|
|
|
|
runIndex = shapedTextRun.GlyphRun.GetCharacterHitFromDistance(distance, out _); |
|
|
|
|
|
|
|
first += runIndex.FirstCharacterIndex; |
|
|
|
|
|
|
|
if (distance <= shapedTextRun.Bounds.Width) |
|
|
|
if (distance <= run.Bounds.Width) |
|
|
|
{ |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
distance -= shapedTextRun.Bounds.Width; |
|
|
|
distance -= run.Bounds.Width; |
|
|
|
} |
|
|
|
|
|
|
|
return new CharacterHit(first, runIndex.TrailingLength); |
|
|
|
return characterHit; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Client to get the distance from the beginning of the line from the specified
|
|
|
|
/// character hit.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="characterHit">character hit of the character to query the distance.</param>
|
|
|
|
/// <returns>distance in text flow direction from the beginning of the line.</returns>
|
|
|
|
/// <inheritdoc/>
|
|
|
|
public override double GetDistanceFromCharacterHit(CharacterHit characterHit) |
|
|
|
{ |
|
|
|
return DistanceFromCp(characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0)); |
|
|
|
return DistanceFromCodepointIndex(characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0)); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Client to get the next character hit for caret navigation
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="characterHit">the current character hit</param>
|
|
|
|
/// <returns>the next character hit</returns>
|
|
|
|
/// <inheritdoc/>
|
|
|
|
public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) |
|
|
|
{ |
|
|
|
int nextVisibleCp; |
|
|
|
@ -95,7 +81,7 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
|
|
|
|
if (characterHit.TrailingLength == 0) |
|
|
|
{ |
|
|
|
navigableCpFound = FindNextVisibleCp(characterHit.FirstCharacterIndex, out nextVisibleCp); |
|
|
|
navigableCpFound = FindNextCodepointIndex(characterHit.FirstCharacterIndex, out nextVisibleCp); |
|
|
|
|
|
|
|
if (navigableCpFound) |
|
|
|
{ |
|
|
|
@ -104,7 +90,7 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
navigableCpFound = FindNextVisibleCp(characterHit.FirstCharacterIndex + 1, out nextVisibleCp); |
|
|
|
navigableCpFound = FindNextCodepointIndex(characterHit.FirstCharacterIndex + 1, out nextVisibleCp); |
|
|
|
|
|
|
|
if (navigableCpFound) |
|
|
|
{ |
|
|
|
@ -116,18 +102,14 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
return characterHit; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Client to get the previous character hit for caret navigation
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="characterHit">the current character hit</param>
|
|
|
|
/// <returns>the previous character hit</returns>
|
|
|
|
/// <inheritdoc/>
|
|
|
|
public override CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit) |
|
|
|
{ |
|
|
|
int previousVisibleCp; |
|
|
|
bool navigableCpFound; |
|
|
|
int previousCodepointIndex; |
|
|
|
bool codepointIndexFound; |
|
|
|
|
|
|
|
int cpHit = characterHit.FirstCharacterIndex; |
|
|
|
bool trailingHit = (characterHit.TrailingLength != 0); |
|
|
|
var cpHit = characterHit.FirstCharacterIndex; |
|
|
|
var trailingHit = characterHit.TrailingLength != 0; |
|
|
|
|
|
|
|
// Input can be right after the end of the current line. Snap it to be at the end of the line.
|
|
|
|
if (cpHit >= Text.Start + Text.Length) |
|
|
|
@ -139,32 +121,28 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
|
|
|
|
if (trailingHit) |
|
|
|
{ |
|
|
|
navigableCpFound = FindPreviousVisibleCp(cpHit, out previousVisibleCp); |
|
|
|
codepointIndexFound = FindPreviousCodepointIndex(cpHit, out previousCodepointIndex); |
|
|
|
|
|
|
|
if (navigableCpFound) |
|
|
|
if (codepointIndexFound) |
|
|
|
{ |
|
|
|
// Move from trailing to leading edge
|
|
|
|
return new CharacterHit(previousVisibleCp, 0); |
|
|
|
return new CharacterHit(previousCodepointIndex, 0); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
navigableCpFound = FindPreviousVisibleCp(cpHit - 1, out previousVisibleCp); |
|
|
|
codepointIndexFound = FindPreviousCodepointIndex(cpHit - 1, out previousCodepointIndex); |
|
|
|
|
|
|
|
if (navigableCpFound) |
|
|
|
if (codepointIndexFound) |
|
|
|
{ |
|
|
|
// Move from leading edge of current character to leading edge of previous
|
|
|
|
return new CharacterHit(previousVisibleCp, 0); |
|
|
|
return new CharacterHit(previousCodepointIndex, 0); |
|
|
|
} |
|
|
|
|
|
|
|
// Can't move, we're before the first character
|
|
|
|
return characterHit; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Client to get the previous character hit after backspacing
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="characterHit">the current character hit</param>
|
|
|
|
/// <returns>the character hit after backspacing</returns>
|
|
|
|
/// <inheritdoc/>
|
|
|
|
public override CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit) |
|
|
|
{ |
|
|
|
// same operation as move-to-previous
|
|
|
|
@ -172,44 +150,41 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Get distance from line start to the specified cp
|
|
|
|
/// Get distance from line start to the specified codepoint index
|
|
|
|
/// </summary>
|
|
|
|
private double DistanceFromCp(int currentIndex) |
|
|
|
private double DistanceFromCodepointIndex(int codepointIndex) |
|
|
|
{ |
|
|
|
var distance = 0.0; |
|
|
|
var dcp = currentIndex - Text.Start; |
|
|
|
var currentDistance = 0.0; |
|
|
|
|
|
|
|
foreach (var textRun in TextRuns) |
|
|
|
foreach (var textRun in _textRuns) |
|
|
|
{ |
|
|
|
var run = (ShapedTextRun)textRun; |
|
|
|
|
|
|
|
distance += run.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(dcp)); |
|
|
|
|
|
|
|
if (dcp <= run.Text.Length) |
|
|
|
if (codepointIndex > textRun.Text.End) |
|
|
|
{ |
|
|
|
break; |
|
|
|
currentDistance += textRun.Bounds.Width; |
|
|
|
|
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
dcp -= run.Text.Length; |
|
|
|
return currentDistance + textRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(codepointIndex)); |
|
|
|
} |
|
|
|
|
|
|
|
return distance; |
|
|
|
return currentDistance; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Search forward from the given cp index (inclusive) to find the next navigable cp index.
|
|
|
|
/// Return true if one such cp is found, false otherwise.
|
|
|
|
/// Search forward from the given codepoint index (inclusive) to find the next navigable codepoint index.
|
|
|
|
/// Return true if one such codepoint index is found, false otherwise.
|
|
|
|
/// </summary>
|
|
|
|
private bool FindNextVisibleCp(int cp, out int cpVisible) |
|
|
|
private bool FindNextCodepointIndex(int codepointIndex, out int nextCodepointIndex) |
|
|
|
{ |
|
|
|
cpVisible = cp; |
|
|
|
nextCodepointIndex = codepointIndex; |
|
|
|
|
|
|
|
if (cp >= Text.Start + Text.Length) |
|
|
|
if (codepointIndex >= Text.Start + Text.Length) |
|
|
|
{ |
|
|
|
return false; // Cannot go forward anymore
|
|
|
|
} |
|
|
|
|
|
|
|
GetRunIndexAtCp(cp, out var runIndex, out var cpRunStart); |
|
|
|
GetRunIndexAtCodepointIndex(codepointIndex, out var runIndex, out var cpRunStart); |
|
|
|
|
|
|
|
while (runIndex < TextRuns.Count) |
|
|
|
{ |
|
|
|
@ -217,7 +192,7 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
// navigable.
|
|
|
|
if (runIndex < TextRuns.Count) |
|
|
|
{ |
|
|
|
cpVisible = Math.Max(cpRunStart, cp); |
|
|
|
nextCodepointIndex = Math.Max(cpRunStart, codepointIndex); |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
@ -228,29 +203,29 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Search backward from the given cp index (inclusive) to find the previous navigable cp index.
|
|
|
|
/// Return true if one such cp is found, false otherwise.
|
|
|
|
/// Search backward from the given codepoint index (inclusive) to find the previous navigable codepoint index.
|
|
|
|
/// Return true if one such codepoint is found, false otherwise.
|
|
|
|
/// </summary>
|
|
|
|
private bool FindPreviousVisibleCp(int cp, out int cpVisible) |
|
|
|
private bool FindPreviousCodepointIndex(int codepointIndex, out int previousCodepointIndex) |
|
|
|
{ |
|
|
|
cpVisible = cp; |
|
|
|
previousCodepointIndex = codepointIndex; |
|
|
|
|
|
|
|
if (cp < Text.Start) |
|
|
|
if (codepointIndex < Text.Start) |
|
|
|
{ |
|
|
|
return false; // Cannot go backward anymore.
|
|
|
|
} |
|
|
|
|
|
|
|
// Position the cpRunEnd at the end of the span that contains the given cp
|
|
|
|
GetRunIndexAtCp(cp, out var runIndex, out var cpRunEnd); |
|
|
|
GetRunIndexAtCodepointIndex(codepointIndex, out var runIndex, out var codepointIndexAtRunEnd); |
|
|
|
|
|
|
|
cpRunEnd += TextRuns[runIndex].Text.End; |
|
|
|
codepointIndexAtRunEnd += TextRuns[runIndex].Text.End; |
|
|
|
|
|
|
|
while (runIndex >= 0) |
|
|
|
{ |
|
|
|
// Visible content has caret stops at its leading edge.
|
|
|
|
if (runIndex + 1 < TextRuns.Count) |
|
|
|
{ |
|
|
|
cpVisible = Math.Min(cpRunEnd, cp); |
|
|
|
previousCodepointIndex = Math.Min(codepointIndexAtRunEnd, codepointIndex); |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
@ -258,25 +233,26 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
if (runIndex == TextRuns.Count) |
|
|
|
{ |
|
|
|
// Get the cp index at the beginning of the newline sequence.
|
|
|
|
cpVisible = cpRunEnd - TextRuns[runIndex].Text.Length + 1; |
|
|
|
previousCodepointIndex = codepointIndexAtRunEnd - TextRuns[runIndex].Text.Length + 1; |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
cpRunEnd -= TextRuns[runIndex--].Text.Length; |
|
|
|
codepointIndexAtRunEnd -= TextRuns[runIndex--].Text.Length; |
|
|
|
} |
|
|
|
|
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
private void GetRunIndexAtCp(int cp, out int runIndex, out int cpRunStart) |
|
|
|
private void GetRunIndexAtCodepointIndex(int codepointIndex, out int runIndex, out int codepointIndexAtRunStart) |
|
|
|
{ |
|
|
|
cpRunStart = Text.Start; |
|
|
|
codepointIndexAtRunStart = Text.Start; |
|
|
|
runIndex = 0; |
|
|
|
|
|
|
|
// Find the span that contains the given cp
|
|
|
|
while (runIndex < TextRuns.Count && cpRunStart + TextRuns[runIndex].Text.Length <= cp) |
|
|
|
while (runIndex < TextRuns.Count && |
|
|
|
codepointIndexAtRunStart + TextRuns[runIndex].Text.Length <= codepointIndex) |
|
|
|
{ |
|
|
|
cpRunStart += TextRuns[runIndex++].Text.Length; |
|
|
|
codepointIndexAtRunStart += TextRuns[runIndex++].Text.Length; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|