-
Notifications
You must be signed in to change notification settings - Fork 81
Expand file tree
/
Copy pathTextLine.cs
More file actions
406 lines (367 loc) · 13.9 KB
/
TextLine.cs
File metadata and controls
406 lines (367 loc) · 13.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
// 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, bool updateTop, bool updateBottom, ref float leftOverhang, ref float rightOverhang, ref float topOverhang, ref float bottomOverhang)
{
foreach (var r in Runs)
{
r.UpdateOverhang(right, updateTop, updateBottom, ref leftOverhang, ref rightOverhang, ref topOverhang, ref bottomOverhang);
}
}
/// <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();
}
});
}
}