123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407 |
- // 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();
- }
- });
- }
- }
|