Browse Source

implemented SetForegroundBrush in skia FormattedTextImpl and refactored

pull/559/head
donandren 10 years ago
parent
commit
39db3f0d0f
  1. 2
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  2. 556
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  3. 2
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

2
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -306,7 +306,7 @@ namespace Avalonia.Skia
using (var paint = CreatePaint(foreground, text.Measure()))
{
var textImpl = text.PlatformImpl as FormattedTextImpl;
textImpl.Draw(this.Canvas, origin.ToSKPoint(), paint);
textImpl.Draw(Canvas, origin.ToSKPoint(), paint, CreatePaint);
}
}

556
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@ -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.Platform;
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
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;
_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();
//currently Skia does not measure properly with Utf8 !!!
@ -22,48 +29,32 @@ namespace Avalonia.Skia
_paint.TextEncoding = SKTextEncoding.Utf16;
_paint.IsStroke = false;
_paint.IsAntialias = true;
LineOffset = 0;
// Replace 0 characters with zero-width spaces (200B)
_text = _text.Replace((char)0, (char)0x200B);
}
_paint.Typeface = typeface;
_paint.TextSize = (float)fontSize;
_paint.TextAlign = textAlignment.ToSKTextAlign();
public static FormattedTextImpl Create(string text, string fontFamilyName, double fontSize, FontStyle fontStyle,
TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping)
{
var typeface = TypefaceCache.GetTypeface(fontFamilyName, fontStyle, fontWeight);
_wrapping = wrapping;
FormattedTextImpl instance = new FormattedTextImpl(text, wrapping);
instance._paint.Typeface = typeface;
instance._paint.TextSize = (float)fontSize;
instance._paint.TextAlign = textAlignment.ToSKTextAlign();
instance.Rebuild();
return instance;
Rebuild();
}
private readonly SKPaint _paint;
private readonly string _text;
private readonly TextWrapping _wrapping;
private readonly List<FormattedTextLine> _lines = new List<FormattedTextLine>();
private readonly List<Rect> _rects = new List<Rect>();
public Size Constraint
{
get { return _constraint; }
set
{
if (_constraint == value)
return;
private List<AvaloniaFormattedTextLine> _skiaLines;
private Size _size;
_constraint = value;
const float MAX_LINE_WIDTH = 10000;
private float LineOffset;
private float LineHeight;
Rebuild();
}
}
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()
{
@ -95,10 +86,10 @@ namespace Avalonia.Skia
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 ?
line.Length : line.Length - 1;
line.Length : (line.Length - 1);
}
return new TextHitTestResult
@ -126,13 +117,12 @@ namespace Avalonia.Skia
if (index < 0 || index >= rects.Count)
{
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)
{
//empty text
return new Rect(0, 0, 1, LineHeight);
return new Rect(0, 0, 1, _lineHeight);
}
if (index == rects.Count)
@ -174,163 +164,27 @@ namespace Avalonia.Skia
public void SetForegroundBrush(IBrush brush, int startIndex, int length)
{
// TODO: we need an implementation here to properly support FormattedText
}
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)
var key = new FBrushRange(startIndex, length);
if (brush == null)
{
_lines.Add(new FormattedTextLine(0, LineHeight));
_size = new Size(0, LineHeight + lastLineDescent);
if (_foregroundBrushes.ContainsKey(key))
_foregroundBrushes.Remove(key);
}
else
{
var lastLine = _skiaLines[_skiaLines.Count - 1];
_size = new Size(maxX, lastLine.Top + lastLine.Height + lastLineDescent);
}
}
private List<Rect> GetRects()
{
if (_text.Length > _rects.Count)
{
BuildRects();
_foregroundBrushes[key] = brush;
}
return _rects;
}
private void BuildRects()
public override string ToString()
{
// Build character rects
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;
}
}
return _text;
}
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
* we improve the FormattedText support. Will need to port this to C# obviously. Rmove when
* not needed anymore.
@ -356,67 +210,90 @@ namespace Avalonia.Skia
}
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++)
{
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
//properly right and center align
//TODO: find a better implementation including
//hittesting and text selection working properly
float x = TransformX(origin.X, 0, paint.TextAlign);
//paint.TextAlign = SKTextAlign.Right;
if (paint.TextAlign == SKTextAlign.Left)
if (!hasCusomFGBrushes)
{
x = origin.X;
var subString = _text.Substring(line.Start, line.Length);
canvas.DrawText(subString, x, origin.Y + line.Top + _lineOffset, paint);
}
else
{
double width = Constraint.Width > 0 && !double.IsPositiveInfinity(Constraint.Width) ?
Constraint.Width :
_size.Width;
float currX = x;
string subStr;
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;
case SKTextAlign.Right: x = origin.X + (float)width; break;
}
}
var fb = GetNextForegroundBrush(ref line, i, out len);
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
{
get { return _constraint; }
set
{
if (_constraint == value)
return;
if (currFGPaint != currentWrapper.Paint)
{
currFGPaint = ApplyWrapperTo(ref currentWrapper, ref currd, paint);
}
_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()
{
return _text;
}
private const float MAX_LINE_WIDTH = 10000;
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)
@ -533,5 +410,228 @@ namespace Avalonia.Skia
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;
}
}
}

2
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -20,7 +20,7 @@ namespace Avalonia.Skia
public IFormattedTextImpl CreateFormattedText(string text, string fontFamilyName, double fontSize, FontStyle fontStyle,
TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping)
{
return FormattedTextImpl.Create(text, fontFamilyName, fontSize, fontStyle, textAlignment, fontWeight, wrapping);
return new FormattedTextImpl(text, fontFamilyName, fontSize, fontStyle, textAlignment, fontWeight, wrapping);
}
public IStreamGeometryImpl CreateStreamGeometry()

Loading…
Cancel
Save