408 строки
14 KiB
C#
408 строки
14 KiB
C#
// RichTextKit
|
|
// Copyright © 2019-2020 Topten Software. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
// not use this product except in compliance with the License. You may obtain
|
|
// a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
// License for the specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
using SkiaSharp;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Topten.RichTextKit.Utils;
|
|
|
|
namespace Topten.RichTextKit
|
|
{
|
|
/// <summary>
|
|
/// Represents a laid out line of text.
|
|
/// </summary>
|
|
public class TextLine
|
|
{
|
|
/// <summary>
|
|
/// Constructs a new TextLine.
|
|
/// </summary>
|
|
public TextLine()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the set of text runs comprising this line.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Font runs are order logically (ie: in code point index order)
|
|
/// but may have unordered <see cref="FontRun.XCoord"/>'s when right to
|
|
/// left text is in use.
|
|
/// </remarks>
|
|
public IReadOnlyList<FontRun> Runs => RunsInternal;
|
|
|
|
/// <summary>
|
|
/// Gets the text block that owns this line.
|
|
/// </summary>
|
|
public TextBlock TextBlock
|
|
{
|
|
get;
|
|
internal set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the next line in this text block, or null if this is the last line.
|
|
/// </summary>
|
|
public TextLine NextLine
|
|
{
|
|
get
|
|
{
|
|
int index = (TextBlock.Lines as List<TextLine>).IndexOf(this);
|
|
if (index < 0 || index + 1 >= TextBlock.Lines.Count)
|
|
return null;
|
|
return TextBlock.Lines[index + 1];
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the previous line in this text block, or null if this is the first line.
|
|
/// </summary>
|
|
public TextLine PreviousLine
|
|
{
|
|
get
|
|
{
|
|
int index = (TextBlock.Lines as List<TextLine>).IndexOf(this);
|
|
if (index <= 0)
|
|
return null;
|
|
return TextBlock.Lines[index - 1];
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the y-coordinate of the top of this line, relative to the top of the text block.
|
|
/// </summary>
|
|
public float YCoord
|
|
{
|
|
get;
|
|
internal set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the base line of this line (relative to <see cref="YCoord"/>)
|
|
/// </summary>
|
|
public float BaseLine
|
|
{
|
|
get;
|
|
internal set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the maximum magnitude ascent of all font runs in this line.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The ascent is reported as a negative value from the base line.
|
|
/// </remarks>
|
|
public float MaxAscent
|
|
{
|
|
get;
|
|
internal set;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Gets the maximum descent of all font runs in this line.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The descent is reported as a positive value from the base line.
|
|
/// </remarks>
|
|
public float MaxDescent
|
|
{
|
|
get;
|
|
internal set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the text height of this line.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The text height of a line is the sum of the ascent and desent.
|
|
/// </remarks>
|
|
public float TextHeight => -MaxAscent + MaxDescent;
|
|
|
|
/// <summary>
|
|
/// Gets the height of this line
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The height of a line is based on the font and <see cref="IStyle.LineHeight"/>
|
|
/// value of all runs in this line.
|
|
/// </remarks>
|
|
public float Height
|
|
{
|
|
get;
|
|
internal set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The width of the content on this line, excluding trailing whitespace and overhang.
|
|
/// </summary>
|
|
public float Width
|
|
{
|
|
get;
|
|
internal set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Paint this line
|
|
/// </summary>
|
|
/// <param name="ctx">The paint context</param>
|
|
internal void Paint(PaintTextContext ctx)
|
|
{
|
|
foreach (var r in Runs)
|
|
{
|
|
r.PaintBackground(ctx);
|
|
}
|
|
|
|
foreach (var r in Runs)
|
|
{
|
|
r.Paint(ctx);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Code point index of start of this line
|
|
/// </summary>
|
|
public int Start
|
|
{
|
|
get
|
|
{
|
|
var pl = PreviousLine;
|
|
return PreviousLine == null ? 0 : PreviousLine.End;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The length of this line in codepoints
|
|
/// </summary>
|
|
public int Length => End - Start;
|
|
|
|
/// <summary>
|
|
/// The code point index of the first character after this line
|
|
/// </summary>
|
|
public int End
|
|
{
|
|
get
|
|
{
|
|
// Get the last run that's not an ellipsis
|
|
var lastRun = this.Runs.LastOrDefault(x => x.RunKind != FontRunKind.Ellipsis);
|
|
|
|
// If last run found, then it's the end of the run, other wise it's the start index
|
|
return lastRun == null ? Start : lastRun.End;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hit test this line, working out the cluster the x position is over
|
|
/// and closest to.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This method only populates the code point indicies in the returned result
|
|
/// and the line indicies will be -1
|
|
/// </remarks>
|
|
/// <param name="x">The xcoord relative to the text block</param>
|
|
public HitTestResult HitTest(float x)
|
|
{
|
|
var htr = new HitTestResult();
|
|
htr.OverLine = -1;
|
|
htr.ClosestLine = -1;
|
|
HitTest(x, ref htr);
|
|
return htr;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Hit test this line, working out the cluster the x position is over
|
|
/// and closest to.
|
|
/// </summary>
|
|
/// <param name="x">The xcoord relative to the text block</param>
|
|
/// <param name="htr">HitTestResult to be filled out</param>
|
|
internal void HitTest(float x, ref HitTestResult htr)
|
|
{
|
|
// Working variables
|
|
float closestXPosition = 0;
|
|
int closestCodePointIndex = -1;
|
|
|
|
if (Runs.Count > 0)
|
|
{
|
|
// If caret is beyond the end of the line...
|
|
var lastRun = Runs[Runs.Count - 1];
|
|
if ((lastRun.Direction == TextDirection.LTR && x >= lastRun.XCoord + lastRun.Width) ||
|
|
(lastRun.Direction == TextDirection.RTL && x < lastRun.XCoord))
|
|
{
|
|
// Special handling for clicking after a soft line break ('\n') in which case
|
|
// the caret should be positioned before the new line character, not after it
|
|
// as this would cause the cursor to appear on the next line).
|
|
if (lastRun.RunKind == FontRunKind.TrailingWhitespace || lastRun.RunKind == FontRunKind.Ellipsis)
|
|
{
|
|
if (lastRun.CodePoints.Length > 0 &&
|
|
(lastRun.CodePoints[lastRun.CodePoints.Length - 1] == '\n') ||
|
|
(lastRun.CodePoints[lastRun.CodePoints.Length - 1] == 0x2029)
|
|
)
|
|
{
|
|
htr.ClosestCodePointIndex = lastRun.End - 1;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check all runs
|
|
foreach (var r in Runs)
|
|
{
|
|
// Ignore ellipsis runs
|
|
if (r.RunKind == FontRunKind.Ellipsis)
|
|
continue;
|
|
|
|
if (x < r.XCoord)
|
|
{
|
|
// Before the run...
|
|
updateClosest(r.XCoord, r.Direction == TextDirection.LTR ? r.Start : r.End, r.Direction);
|
|
}
|
|
else if (x >= r.XCoord + r.Width)
|
|
{
|
|
// After the run...
|
|
updateClosest(r.XCoord + r.Width, r.Direction == TextDirection.RTL ? r.Start : r.End, r.Direction);
|
|
}
|
|
else
|
|
{
|
|
// Inside the run
|
|
for (int i = 0; i < r.Clusters.Length;)
|
|
{
|
|
// Get the xcoord of this cluster
|
|
var codePointIndex = r.Clusters[i];
|
|
var xcoord1 = r.GetXCoordOfCodePointIndex(codePointIndex);
|
|
|
|
// Find the code point of the next cluster
|
|
var j = i;
|
|
while (j < r.Clusters.Length && r.Clusters[j] == r.Clusters[i])
|
|
j++;
|
|
|
|
// Get the xcoord of other side of this cluster
|
|
int codePointIndexOther;
|
|
if (r.Direction == TextDirection.LTR)
|
|
{
|
|
if (j == r.Clusters.Length)
|
|
{
|
|
codePointIndexOther = r.End;
|
|
}
|
|
else
|
|
{
|
|
codePointIndexOther = r.Clusters[j];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (i > 0)
|
|
{
|
|
codePointIndexOther = r.Clusters[i - 1];
|
|
}
|
|
else
|
|
{
|
|
codePointIndexOther = r.End;
|
|
}
|
|
}
|
|
|
|
// Gethte xcoord of the other side of the cluster
|
|
var xcoord2 = r.GetXCoordOfCodePointIndex(codePointIndexOther);
|
|
|
|
// Ensure order correct for easier in-range check
|
|
if (xcoord1 > xcoord2)
|
|
{
|
|
var temp = xcoord1;
|
|
xcoord1 = xcoord2;
|
|
xcoord2 = temp;
|
|
}
|
|
|
|
// On the character?
|
|
if (x >= xcoord1 && x < xcoord2)
|
|
{
|
|
// Store this as the cluster the point is over
|
|
htr.OverCodePointIndex = codePointIndex;
|
|
|
|
// Don't move to the rhs (or lhs) of a line break
|
|
if (r.CodePoints[codePointIndex - r.Start] == '\n')
|
|
{
|
|
htr.ClosestCodePointIndex = codePointIndex;
|
|
}
|
|
else
|
|
{
|
|
// Work out if position is closer to the left or right side of the cluster
|
|
if (x < (xcoord1 + xcoord2) / 2)
|
|
{
|
|
htr.ClosestCodePointIndex = r.Direction == TextDirection.LTR ? codePointIndex : codePointIndexOther;
|
|
}
|
|
else
|
|
{
|
|
htr.ClosestCodePointIndex = r.Direction == TextDirection.LTR ? codePointIndexOther : codePointIndex;
|
|
}
|
|
}
|
|
if (htr.ClosestCodePointIndex == End)
|
|
{
|
|
htr.AltCaretPosition = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Move to the next cluster
|
|
i = j;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Store closest character
|
|
htr.ClosestCodePointIndex = closestCodePointIndex;
|
|
|
|
if (htr.ClosestCodePointIndex == End)
|
|
{
|
|
htr.AltCaretPosition = true;
|
|
}
|
|
|
|
// Helper for updating closest caret position
|
|
void updateClosest(float xPosition, int codePointIndex, TextDirection dir)
|
|
{
|
|
if (closestCodePointIndex == -1 || Math.Abs(xPosition - x) < Math.Abs(closestXPosition - x))
|
|
{
|
|
closestXPosition = xPosition;
|
|
closestCodePointIndex = codePointIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal void UpdateOverhang(float right, ref float leftOverhang, ref float rightOverhang)
|
|
{
|
|
foreach (var r in Runs)
|
|
{
|
|
r.UpdateOverhang(right, ref leftOverhang, ref rightOverhang);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Internal List of runs
|
|
/// </summary>
|
|
internal List<FontRun> RunsInternal = new List<FontRun>();
|
|
|
|
internal static ThreadLocal<ObjectPool<TextLine>> Pool = new ThreadLocal<ObjectPool<TextLine>>(() => new ObjectPool<TextLine>()
|
|
{
|
|
Cleaner = (r) =>
|
|
{
|
|
r.TextBlock = null;
|
|
r.RunsInternal.Clear();
|
|
}
|
|
});
|
|
}
|
|
}
|