|
|
@ -1,20 +1,27 @@ |
|
|
|
|
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
|
|
|
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
|
|
|
|
|
|
|
using Avalonia.Media; |
|
|
using Avalonia.Media; |
|
|
using Avalonia.Platform; |
|
|
using Avalonia.Platform; |
|
|
using SkiaSharp; |
|
|
using SkiaSharp; |
|
|
using System; |
|
|
using System; |
|
|
using System.Collections.Generic; |
|
|
using System.Collections.Generic; |
|
|
using System.Linq; |
|
|
using System.Linq; |
|
|
using System.Runtime.InteropServices; |
|
|
|
|
|
using System.Text; |
|
|
|
|
|
|
|
|
|
|
|
namespace Avalonia.Skia |
|
|
namespace Avalonia.Skia |
|
|
{ |
|
|
{ |
|
|
unsafe class FormattedTextImpl : IFormattedTextImpl |
|
|
public class FormattedTextImpl : IFormattedTextImpl |
|
|
{ |
|
|
{ |
|
|
public FormattedTextImpl(string text, TextWrapping wrapping = TextWrapping.NoWrap) |
|
|
public FormattedTextImpl(string text, string fontFamilyName, double fontSize, FontStyle fontStyle, |
|
|
|
|
|
TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping) |
|
|
{ |
|
|
{ |
|
|
_text = text ?? string.Empty; |
|
|
_text = text ?? string.Empty; |
|
|
_wrapping = wrapping; |
|
|
|
|
|
|
|
|
// Replace 0 characters with zero-width spaces (200B)
|
|
|
|
|
|
_text = _text.Replace((char)0, (char)0x200B); |
|
|
|
|
|
|
|
|
|
|
|
var typeface = TypefaceCache.GetTypeface(fontFamilyName, fontStyle, fontWeight); |
|
|
|
|
|
|
|
|
_paint = new SKPaint(); |
|
|
_paint = new SKPaint(); |
|
|
|
|
|
|
|
|
//currently Skia does not measure properly with Utf8 !!!
|
|
|
//currently Skia does not measure properly with Utf8 !!!
|
|
|
@ -22,48 +29,32 @@ namespace Avalonia.Skia |
|
|
_paint.TextEncoding = SKTextEncoding.Utf16; |
|
|
_paint.TextEncoding = SKTextEncoding.Utf16; |
|
|
_paint.IsStroke = false; |
|
|
_paint.IsStroke = false; |
|
|
_paint.IsAntialias = true; |
|
|
_paint.IsAntialias = true; |
|
|
LineOffset = 0; |
|
|
_paint.Typeface = typeface; |
|
|
|
|
|
_paint.TextSize = (float)fontSize; |
|
|
// Replace 0 characters with zero-width spaces (200B)
|
|
|
_paint.TextAlign = textAlignment.ToSKTextAlign(); |
|
|
_text = _text.Replace((char)0, (char)0x200B); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public static FormattedTextImpl Create(string text, string fontFamilyName, double fontSize, FontStyle fontStyle, |
|
|
_wrapping = wrapping; |
|
|
TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping) |
|
|
|
|
|
{ |
|
|
|
|
|
var typeface = TypefaceCache.GetTypeface(fontFamilyName, fontStyle, fontWeight); |
|
|
|
|
|
|
|
|
|
|
|
FormattedTextImpl instance = new FormattedTextImpl(text, wrapping); |
|
|
Rebuild(); |
|
|
instance._paint.Typeface = typeface; |
|
|
|
|
|
instance._paint.TextSize = (float)fontSize; |
|
|
|
|
|
instance._paint.TextAlign = textAlignment.ToSKTextAlign(); |
|
|
|
|
|
instance.Rebuild(); |
|
|
|
|
|
return instance; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private readonly SKPaint _paint; |
|
|
public Size Constraint |
|
|
private readonly string _text; |
|
|
{ |
|
|
private readonly TextWrapping _wrapping; |
|
|
get { return _constraint; } |
|
|
|
|
|
set |
|
|
private readonly List<FormattedTextLine> _lines = new List<FormattedTextLine>(); |
|
|
{ |
|
|
private readonly List<Rect> _rects = new List<Rect>(); |
|
|
if (_constraint == value) |
|
|
|
|
|
return; |
|
|
|
|
|
|
|
|
private List<AvaloniaFormattedTextLine> _skiaLines; |
|
|
_constraint = value; |
|
|
private Size _size; |
|
|
|
|
|
|
|
|
|
|
|
const float MAX_LINE_WIDTH = 10000; |
|
|
Rebuild(); |
|
|
private float LineOffset; |
|
|
} |
|
|
private float LineHeight; |
|
|
} |
|
|
|
|
|
|
|
|
struct AvaloniaFormattedTextLine |
|
|
public void Dispose() |
|
|
{ |
|
|
{ |
|
|
public float Top; |
|
|
} |
|
|
public int Start; |
|
|
|
|
|
public int Length; |
|
|
|
|
|
public int TextLength; |
|
|
|
|
|
public float Height; |
|
|
|
|
|
public float Width; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
public IEnumerable<FormattedTextLine> GetLines() |
|
|
public IEnumerable<FormattedTextLine> GetLines() |
|
|
{ |
|
|
{ |
|
|
@ -95,10 +86,10 @@ namespace Avalonia.Skia |
|
|
|
|
|
|
|
|
int offset = 0; |
|
|
int offset = 0; |
|
|
|
|
|
|
|
|
if (point.X >= line.Width / 2 && line.Length > 0) |
|
|
if (point.X >= (rects[line.Start].X + line.Width) / 2 && line.Length > 0) |
|
|
{ |
|
|
{ |
|
|
offset = line.TextLength > line.Length ? |
|
|
offset = line.TextLength > line.Length ? |
|
|
line.Length : line.Length - 1; |
|
|
line.Length : (line.Length - 1); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return new TextHitTestResult |
|
|
return new TextHitTestResult |
|
|
@ -126,13 +117,12 @@ namespace Avalonia.Skia |
|
|
if (index < 0 || index >= rects.Count) |
|
|
if (index < 0 || index >= rects.Count) |
|
|
{ |
|
|
{ |
|
|
var r = rects.LastOrDefault(); |
|
|
var r = rects.LastOrDefault(); |
|
|
return new Rect(r.X + r.Width, r.Y, 0, LineHeight); |
|
|
return new Rect(r.X + r.Width, r.Y, 0, _lineHeight); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (rects.Count == 0) |
|
|
if (rects.Count == 0) |
|
|
{ |
|
|
{ |
|
|
//empty text
|
|
|
return new Rect(0, 0, 1, _lineHeight); |
|
|
return new Rect(0, 0, 1, LineHeight); |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (index == rects.Count) |
|
|
if (index == rects.Count) |
|
|
@ -174,163 +164,27 @@ namespace Avalonia.Skia |
|
|
|
|
|
|
|
|
public void SetForegroundBrush(IBrush brush, int startIndex, int length) |
|
|
public void SetForegroundBrush(IBrush brush, int startIndex, int length) |
|
|
{ |
|
|
{ |
|
|
// TODO: we need an implementation here to properly support FormattedText
|
|
|
var key = new FBrushRange(startIndex, length); |
|
|
} |
|
|
if (brush == null) |
|
|
|
|
|
|
|
|
void Rebuild() |
|
|
|
|
|
{ |
|
|
|
|
|
var length = _text.Length; |
|
|
|
|
|
|
|
|
|
|
|
_lines.Clear(); |
|
|
|
|
|
_rects.Clear(); |
|
|
|
|
|
_skiaLines = new List<AvaloniaFormattedTextLine>(); |
|
|
|
|
|
|
|
|
|
|
|
int curOff = 0; |
|
|
|
|
|
float curY = 0; |
|
|
|
|
|
|
|
|
|
|
|
var metrics = _paint.FontMetrics; |
|
|
|
|
|
var mTop = metrics.Top; // The greatest distance above the baseline for any glyph (will be <= 0).
|
|
|
|
|
|
var mBottom = metrics.Bottom; // The greatest distance below the baseline for any glyph (will be >= 0).
|
|
|
|
|
|
var mLeading = metrics.Leading; // The recommended distance to add between lines of text (will be >= 0).
|
|
|
|
|
|
var mDescent = metrics.Descent; |
|
|
|
|
|
var mAscent = metrics.Ascent; |
|
|
|
|
|
var lastLineDescent = mBottom - mDescent; |
|
|
|
|
|
// This seems like the best measure of full vertical extent
|
|
|
|
|
|
LineHeight = mDescent - mAscent; |
|
|
|
|
|
|
|
|
|
|
|
// Rendering is relative to baseline
|
|
|
|
|
|
LineOffset = -metrics.Top; |
|
|
|
|
|
|
|
|
|
|
|
string subString; |
|
|
|
|
|
|
|
|
|
|
|
float widthConstraint = (_constraint.Width != double.PositiveInfinity) |
|
|
|
|
|
? (float)_constraint.Width |
|
|
|
|
|
: -1; |
|
|
|
|
|
|
|
|
|
|
|
for (int c = 0; curOff < length; c++) |
|
|
|
|
|
{ |
|
|
|
|
|
float lineWidth = -1; |
|
|
|
|
|
int measured; |
|
|
|
|
|
int trailingnumber = 0; |
|
|
|
|
|
|
|
|
|
|
|
subString = _text.Substring(curOff); |
|
|
|
|
|
|
|
|
|
|
|
float constraint = -1; |
|
|
|
|
|
|
|
|
|
|
|
if (_wrapping == TextWrapping.Wrap) |
|
|
|
|
|
{ |
|
|
|
|
|
constraint = widthConstraint <= 0 ? MAX_LINE_WIDTH : widthConstraint; |
|
|
|
|
|
if (constraint > MAX_LINE_WIDTH) |
|
|
|
|
|
constraint = MAX_LINE_WIDTH; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
measured = LineBreak(_text, curOff, length, _paint, constraint, out trailingnumber); |
|
|
|
|
|
|
|
|
|
|
|
AvaloniaFormattedTextLine line = new AvaloniaFormattedTextLine(); |
|
|
|
|
|
line.TextLength = measured; |
|
|
|
|
|
|
|
|
|
|
|
subString = _text.Substring(line.Start, line.TextLength); |
|
|
|
|
|
lineWidth = _paint.MeasureText(subString); |
|
|
|
|
|
|
|
|
|
|
|
// lineHeight = hh;
|
|
|
|
|
|
line.Start = curOff; |
|
|
|
|
|
line.Length = measured - trailingnumber; |
|
|
|
|
|
line.Width = lineWidth; |
|
|
|
|
|
line.Height = LineHeight; |
|
|
|
|
|
line.Top = curY; |
|
|
|
|
|
|
|
|
|
|
|
_skiaLines.Add(line); |
|
|
|
|
|
|
|
|
|
|
|
curY += LineHeight; |
|
|
|
|
|
|
|
|
|
|
|
// TODO: We may want to consider adding Leading to the vertical line spacing but for now
|
|
|
|
|
|
// it appears to make no difference. Revisit as part of FormattedText improvements.
|
|
|
|
|
|
//
|
|
|
|
|
|
//curY += mLeading;
|
|
|
|
|
|
|
|
|
|
|
|
curOff += measured; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Now convert to Avalonia data formats
|
|
|
|
|
|
_lines.Clear(); |
|
|
|
|
|
float maxX = 0; |
|
|
|
|
|
|
|
|
|
|
|
for (var c = 0; c < _skiaLines.Count; c++) |
|
|
|
|
|
{ |
|
|
|
|
|
var w = _skiaLines[c].Width; |
|
|
|
|
|
if (maxX < w) |
|
|
|
|
|
maxX = w; |
|
|
|
|
|
|
|
|
|
|
|
_lines.Add(new FormattedTextLine(_skiaLines[c].TextLength, _skiaLines[c].Height)); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (_skiaLines.Count == 0) |
|
|
|
|
|
{ |
|
|
{ |
|
|
_lines.Add(new FormattedTextLine(0, LineHeight)); |
|
|
if (_foregroundBrushes.ContainsKey(key)) |
|
|
_size = new Size(0, LineHeight + lastLineDescent); |
|
|
_foregroundBrushes.Remove(key); |
|
|
} |
|
|
} |
|
|
else |
|
|
else |
|
|
{ |
|
|
{ |
|
|
var lastLine = _skiaLines[_skiaLines.Count - 1]; |
|
|
_foregroundBrushes[key] = brush; |
|
|
_size = new Size(maxX, lastLine.Top + lastLine.Height + lastLineDescent); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private List<Rect> GetRects() |
|
|
|
|
|
{ |
|
|
|
|
|
if (_text.Length > _rects.Count) |
|
|
|
|
|
{ |
|
|
|
|
|
BuildRects(); |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return _rects; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private void BuildRects() |
|
|
public override string ToString() |
|
|
{ |
|
|
{ |
|
|
// Build character rects
|
|
|
return _text; |
|
|
var fm = _paint.FontMetrics; |
|
|
|
|
|
|
|
|
|
|
|
float width = (float)(Constraint.Width > 0 && !double.IsPositiveInfinity(Constraint.Width) ? |
|
|
|
|
|
Constraint.Width : |
|
|
|
|
|
_size.Width); |
|
|
|
|
|
|
|
|
|
|
|
for (int li = 0; li < _skiaLines.Count; li++) |
|
|
|
|
|
{ |
|
|
|
|
|
var line = _skiaLines[li]; |
|
|
|
|
|
float prevRight = 0; |
|
|
|
|
|
|
|
|
|
|
|
switch (_paint.TextAlign) |
|
|
|
|
|
{ |
|
|
|
|
|
case SKTextAlign.Center: prevRight = (width - line.Width) / 2; break; |
|
|
|
|
|
case SKTextAlign.Right: prevRight = width - line.Width; break; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
double nextTop = line.Top + line.Height; |
|
|
|
|
|
|
|
|
|
|
|
if (li + 1 < _skiaLines.Count) |
|
|
|
|
|
{ |
|
|
|
|
|
nextTop = _skiaLines[li + 1].Top; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
for (int i = line.Start; i < line.Start + line.TextLength; i++) |
|
|
|
|
|
{ |
|
|
|
|
|
float w = _paint.MeasureText(_text[i].ToString()); |
|
|
|
|
|
|
|
|
|
|
|
_rects.Add(new Rect( |
|
|
|
|
|
prevRight, |
|
|
|
|
|
line.Top, |
|
|
|
|
|
w, |
|
|
|
|
|
nextTop - line.Top)); |
|
|
|
|
|
prevRight += w; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
internal void Draw(SKCanvas canvas, SKPoint origin, DrawingContextImpl.PaintWrapper foreground) |
|
|
internal void Draw(SKCanvas canvas, SKPoint origin, |
|
|
|
|
|
DrawingContextImpl.PaintWrapper foreground, |
|
|
|
|
|
Func<IBrush, Size, DrawingContextImpl.PaintWrapper> brushFactory) |
|
|
{ |
|
|
{ |
|
|
SKPaint paint = _paint; |
|
|
|
|
|
|
|
|
|
|
|
/* TODO: This originated from Native code, it might be useful for debugging character positions as |
|
|
/* TODO: This originated from Native code, it might be useful for debugging character positions as |
|
|
* we improve the FormattedText support. Will need to port this to C# obviously. Rmove when |
|
|
* we improve the FormattedText support. Will need to port this to C# obviously. Rmove when |
|
|
* not needed anymore. |
|
|
* not needed anymore. |
|
|
@ -356,67 +210,90 @@ namespace Avalonia.Skia |
|
|
} |
|
|
} |
|
|
ctx->Canvas->restore(); |
|
|
ctx->Canvas->restore(); |
|
|
*/ |
|
|
*/ |
|
|
|
|
|
SKPaint paint = _paint; |
|
|
|
|
|
IDisposable currd = null; |
|
|
|
|
|
var currentWrapper = foreground; |
|
|
|
|
|
|
|
|
using (foreground.ApplyTo(paint)) |
|
|
try |
|
|
{ |
|
|
{ |
|
|
|
|
|
SKPaint currFGPaint = ApplyWrapperTo(ref foreground, ref currd, paint); |
|
|
|
|
|
bool hasCusomFGBrushes = _foregroundBrushes.Any(); |
|
|
|
|
|
|
|
|
for (int c = 0; c < _skiaLines.Count; c++) |
|
|
for (int c = 0; c < _skiaLines.Count; c++) |
|
|
{ |
|
|
{ |
|
|
AvaloniaFormattedTextLine line = _skiaLines[c]; |
|
|
AvaloniaFormattedTextLine line = _skiaLines[c]; |
|
|
var subString = _text.Substring(line.Start, line.Length); |
|
|
|
|
|
|
|
|
|
|
|
float x = 0; |
|
|
|
|
|
|
|
|
|
|
|
//this is a quick fix so we have skia rendering
|
|
|
float x = TransformX(origin.X, 0, paint.TextAlign); |
|
|
//properly right and center align
|
|
|
|
|
|
//TODO: find a better implementation including
|
|
|
|
|
|
//hittesting and text selection working properly
|
|
|
|
|
|
|
|
|
|
|
|
//paint.TextAlign = SKTextAlign.Right;
|
|
|
if (!hasCusomFGBrushes) |
|
|
if (paint.TextAlign == SKTextAlign.Left) |
|
|
|
|
|
{ |
|
|
{ |
|
|
x = origin.X; |
|
|
var subString = _text.Substring(line.Start, line.Length); |
|
|
|
|
|
canvas.DrawText(subString, x, origin.Y + line.Top + _lineOffset, paint); |
|
|
} |
|
|
} |
|
|
else |
|
|
else |
|
|
{ |
|
|
{ |
|
|
double width = Constraint.Width > 0 && !double.IsPositiveInfinity(Constraint.Width) ? |
|
|
float currX = x; |
|
|
Constraint.Width : |
|
|
string subStr; |
|
|
_size.Width; |
|
|
int len; |
|
|
|
|
|
|
|
|
switch (_paint.TextAlign) |
|
|
for (int i = line.Start; i < line.Start + line.Length;) |
|
|
{ |
|
|
{ |
|
|
case SKTextAlign.Center: x = origin.X + (float)width / 2; break; |
|
|
var fb = GetNextForegroundBrush(ref line, i, out len); |
|
|
case SKTextAlign.Right: x = origin.X + (float)width; break; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
canvas.DrawText(subString, x, origin.Y + line.Top + LineOffset, paint); |
|
|
if (fb != null) |
|
|
} |
|
|
{ |
|
|
} |
|
|
//TODO: figure out how to get the brush size
|
|
|
} |
|
|
currentWrapper = brushFactory(fb, new Size()); |
|
|
|
|
|
} |
|
|
|
|
|
else |
|
|
|
|
|
{ |
|
|
|
|
|
if (!currentWrapper.Equals(foreground)) currentWrapper.Dispose(); |
|
|
|
|
|
currentWrapper = foreground; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity); |
|
|
subStr = _text.Substring(i, len); |
|
|
|
|
|
|
|
|
public Size Constraint |
|
|
if (currFGPaint != currentWrapper.Paint) |
|
|
{ |
|
|
{ |
|
|
get { return _constraint; } |
|
|
currFGPaint = ApplyWrapperTo(ref currentWrapper, ref currd, paint); |
|
|
set |
|
|
} |
|
|
{ |
|
|
|
|
|
if (_constraint == value) |
|
|
|
|
|
return; |
|
|
|
|
|
|
|
|
|
|
|
_constraint = value; |
|
|
canvas.DrawText(subStr, currX, origin.Y + line.Top + _lineOffset, paint); |
|
|
|
|
|
|
|
|
Rebuild(); |
|
|
i += len; |
|
|
|
|
|
currX += paint.MeasureText(subStr); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
finally |
|
|
|
|
|
{ |
|
|
|
|
|
if (!currentWrapper.Equals(foreground)) currentWrapper.Dispose(); |
|
|
|
|
|
currd?.Dispose(); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public override string ToString() |
|
|
private const float MAX_LINE_WIDTH = 10000; |
|
|
{ |
|
|
|
|
|
return _text; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public void Dispose() |
|
|
private readonly Dictionary<FBrushRange, IBrush> _foregroundBrushes = |
|
|
|
|
|
new Dictionary<FBrushRange, IBrush>(); |
|
|
|
|
|
private readonly List<FormattedTextLine> _lines = new List<FormattedTextLine>(); |
|
|
|
|
|
private readonly SKPaint _paint; |
|
|
|
|
|
private readonly List<Rect> _rects = new List<Rect>(); |
|
|
|
|
|
private readonly string _text; |
|
|
|
|
|
private readonly TextWrapping _wrapping; |
|
|
|
|
|
private Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity); |
|
|
|
|
|
private float _lineHeight = 0; |
|
|
|
|
|
private float _lineOffset = 0; |
|
|
|
|
|
private Size _size; |
|
|
|
|
|
private List<AvaloniaFormattedTextLine> _skiaLines; |
|
|
|
|
|
|
|
|
|
|
|
private static SKPaint ApplyWrapperTo(ref DrawingContextImpl.PaintWrapper wrapper, |
|
|
|
|
|
ref IDisposable curr, SKPaint paint) |
|
|
{ |
|
|
{ |
|
|
|
|
|
curr?.Dispose(); |
|
|
|
|
|
curr = wrapper.ApplyTo(paint); |
|
|
|
|
|
return wrapper.Paint; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private static bool IsBreakChar(char c) |
|
|
private static bool IsBreakChar(char c) |
|
|
@ -533,5 +410,228 @@ namespace Avalonia.Skia |
|
|
|
|
|
|
|
|
return index - startIndex; |
|
|
return index - startIndex; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private void BuildRects() |
|
|
|
|
|
{ |
|
|
|
|
|
// Build character rects
|
|
|
|
|
|
var fm = _paint.FontMetrics; |
|
|
|
|
|
SKTextAlign align = _paint.TextAlign; |
|
|
|
|
|
|
|
|
|
|
|
for (int li = 0; li < _skiaLines.Count; li++) |
|
|
|
|
|
{ |
|
|
|
|
|
var line = _skiaLines[li]; |
|
|
|
|
|
float prevRight = TransformX(0, line.Width, align); |
|
|
|
|
|
double nextTop = line.Top + line.Height; |
|
|
|
|
|
|
|
|
|
|
|
if (li + 1 < _skiaLines.Count) |
|
|
|
|
|
{ |
|
|
|
|
|
nextTop = _skiaLines[li + 1].Top; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
for (int i = line.Start; i < line.Start + line.TextLength; i++) |
|
|
|
|
|
{ |
|
|
|
|
|
float w = _paint.MeasureText(_text[i].ToString()); |
|
|
|
|
|
|
|
|
|
|
|
_rects.Add(new Rect( |
|
|
|
|
|
prevRight, |
|
|
|
|
|
line.Top, |
|
|
|
|
|
w, |
|
|
|
|
|
nextTop - line.Top)); |
|
|
|
|
|
prevRight += w; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private IBrush GetNextForegroundBrush(ref AvaloniaFormattedTextLine line, int index, out int length) |
|
|
|
|
|
{ |
|
|
|
|
|
IBrush result = null; |
|
|
|
|
|
int len = length = line.Start + line.Length - index; |
|
|
|
|
|
|
|
|
|
|
|
if (_foregroundBrushes.Any()) |
|
|
|
|
|
{ |
|
|
|
|
|
var cbi = _foregroundBrushes.FirstOrDefault(b => b.Key.Intersects(index, len)); |
|
|
|
|
|
|
|
|
|
|
|
if (cbi.Value != null) |
|
|
|
|
|
{ |
|
|
|
|
|
var r = cbi.Key; |
|
|
|
|
|
|
|
|
|
|
|
if (r.StartIndex > index) |
|
|
|
|
|
{ |
|
|
|
|
|
len = r.StartIndex - index; |
|
|
|
|
|
} |
|
|
|
|
|
else |
|
|
|
|
|
{ |
|
|
|
|
|
len = r.EndIndex - index + 1; |
|
|
|
|
|
result = cbi.Value; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (len > 0 && len < length) |
|
|
|
|
|
{ |
|
|
|
|
|
length = len; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return result; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private List<Rect> GetRects() |
|
|
|
|
|
{ |
|
|
|
|
|
if (_text.Length > _rects.Count) |
|
|
|
|
|
{ |
|
|
|
|
|
BuildRects(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return _rects; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private void Rebuild() |
|
|
|
|
|
{ |
|
|
|
|
|
var length = _text.Length; |
|
|
|
|
|
|
|
|
|
|
|
_lines.Clear(); |
|
|
|
|
|
_rects.Clear(); |
|
|
|
|
|
_skiaLines = new List<AvaloniaFormattedTextLine>(); |
|
|
|
|
|
|
|
|
|
|
|
int curOff = 0; |
|
|
|
|
|
float curY = 0; |
|
|
|
|
|
|
|
|
|
|
|
var metrics = _paint.FontMetrics; |
|
|
|
|
|
var mTop = metrics.Top; // The greatest distance above the baseline for any glyph (will be <= 0).
|
|
|
|
|
|
var mBottom = metrics.Bottom; // The greatest distance below the baseline for any glyph (will be >= 0).
|
|
|
|
|
|
var mLeading = metrics.Leading; // The recommended distance to add between lines of text (will be >= 0).
|
|
|
|
|
|
var mDescent = metrics.Descent; //The recommended distance below the baseline. Will be >= 0.
|
|
|
|
|
|
var mAscent = metrics.Ascent; //The recommended distance above the baseline. Will be <= 0.
|
|
|
|
|
|
var lastLineDescent = mBottom - mDescent; |
|
|
|
|
|
|
|
|
|
|
|
// This seems like the best measure of full vertical extent
|
|
|
|
|
|
// matches Direct2D line height
|
|
|
|
|
|
_lineHeight = mDescent - mAscent; |
|
|
|
|
|
|
|
|
|
|
|
// Rendering is relative to baseline
|
|
|
|
|
|
_lineOffset = -metrics.Top; |
|
|
|
|
|
|
|
|
|
|
|
string subString; |
|
|
|
|
|
|
|
|
|
|
|
float widthConstraint = (_constraint.Width != double.PositiveInfinity) |
|
|
|
|
|
? (float)_constraint.Width |
|
|
|
|
|
: -1; |
|
|
|
|
|
|
|
|
|
|
|
for (int c = 0; curOff < length; c++) |
|
|
|
|
|
{ |
|
|
|
|
|
float lineWidth = -1; |
|
|
|
|
|
int measured; |
|
|
|
|
|
int trailingnumber = 0; |
|
|
|
|
|
|
|
|
|
|
|
subString = _text.Substring(curOff); |
|
|
|
|
|
|
|
|
|
|
|
float constraint = -1; |
|
|
|
|
|
|
|
|
|
|
|
if (_wrapping == TextWrapping.Wrap) |
|
|
|
|
|
{ |
|
|
|
|
|
constraint = widthConstraint <= 0 ? MAX_LINE_WIDTH : widthConstraint; |
|
|
|
|
|
if (constraint > MAX_LINE_WIDTH) |
|
|
|
|
|
constraint = MAX_LINE_WIDTH; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
measured = LineBreak(_text, curOff, length, _paint, constraint, out trailingnumber); |
|
|
|
|
|
|
|
|
|
|
|
AvaloniaFormattedTextLine line = new AvaloniaFormattedTextLine(); |
|
|
|
|
|
line.TextLength = measured; |
|
|
|
|
|
|
|
|
|
|
|
subString = _text.Substring(line.Start, line.TextLength); |
|
|
|
|
|
lineWidth = _paint.MeasureText(subString); |
|
|
|
|
|
line.Start = curOff; |
|
|
|
|
|
line.Length = measured - trailingnumber; |
|
|
|
|
|
line.Width = lineWidth; |
|
|
|
|
|
line.Height = _lineHeight; |
|
|
|
|
|
line.Top = curY; |
|
|
|
|
|
|
|
|
|
|
|
_skiaLines.Add(line); |
|
|
|
|
|
|
|
|
|
|
|
curY += _lineHeight; |
|
|
|
|
|
|
|
|
|
|
|
curY += mLeading; |
|
|
|
|
|
|
|
|
|
|
|
curOff += measured; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Now convert to Avalonia data formats
|
|
|
|
|
|
_lines.Clear(); |
|
|
|
|
|
float maxX = 0; |
|
|
|
|
|
|
|
|
|
|
|
for (var c = 0; c < _skiaLines.Count; c++) |
|
|
|
|
|
{ |
|
|
|
|
|
var w = _skiaLines[c].Width; |
|
|
|
|
|
if (maxX < w) |
|
|
|
|
|
maxX = w; |
|
|
|
|
|
|
|
|
|
|
|
_lines.Add(new FormattedTextLine(_skiaLines[c].TextLength, _skiaLines[c].Height)); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (_skiaLines.Count == 0) |
|
|
|
|
|
{ |
|
|
|
|
|
_lines.Add(new FormattedTextLine(0, _lineHeight)); |
|
|
|
|
|
_size = new Size(0, _lineHeight + lastLineDescent); |
|
|
|
|
|
} |
|
|
|
|
|
else |
|
|
|
|
|
{ |
|
|
|
|
|
var lastLine = _skiaLines[_skiaLines.Count - 1]; |
|
|
|
|
|
_size = new Size(maxX, lastLine.Top + lastLine.Height + lastLineDescent); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private float TransformX(float originX, float lineWidth, SKTextAlign align) |
|
|
|
|
|
{ |
|
|
|
|
|
float x = 0; |
|
|
|
|
|
|
|
|
|
|
|
if (align == SKTextAlign.Left) |
|
|
|
|
|
{ |
|
|
|
|
|
x = originX; |
|
|
|
|
|
} |
|
|
|
|
|
else |
|
|
|
|
|
{ |
|
|
|
|
|
double width = Constraint.Width > 0 && !double.IsPositiveInfinity(Constraint.Width) ? |
|
|
|
|
|
Constraint.Width : |
|
|
|
|
|
_size.Width; |
|
|
|
|
|
|
|
|
|
|
|
switch (align) |
|
|
|
|
|
{ |
|
|
|
|
|
case SKTextAlign.Center: x = originX + (float)(width - lineWidth) / 2; break; |
|
|
|
|
|
case SKTextAlign.Right: x = originX + (float)(width - lineWidth); break; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return x; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private struct AvaloniaFormattedTextLine |
|
|
|
|
|
{ |
|
|
|
|
|
public float Height; |
|
|
|
|
|
public int Length; |
|
|
|
|
|
public int Start; |
|
|
|
|
|
public int TextLength; |
|
|
|
|
|
public float Top; |
|
|
|
|
|
public float Width; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
private struct FBrushRange |
|
|
|
|
|
{ |
|
|
|
|
|
public FBrushRange(int startIndex, int length) |
|
|
|
|
|
{ |
|
|
|
|
|
StartIndex = startIndex; |
|
|
|
|
|
Length = length; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public int EndIndex => StartIndex + Length - 1; |
|
|
|
|
|
|
|
|
|
|
|
public int Length { get; private set; } |
|
|
|
|
|
|
|
|
|
|
|
public int StartIndex { get; private set; } |
|
|
|
|
|
|
|
|
|
|
|
public bool Intersects(int index, int len) => |
|
|
|
|
|
(index + len) > StartIndex && |
|
|
|
|
|
(StartIndex + Length) > index; |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |