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 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 bi = _foregroundBrushes.FindIndex(b => b.Key.StartIndex <= index && b.Key.EndIndex > index ); if (bi > -1) { var match = _foregroundBrushes[bi]; len = match.Key.EndIndex - index; result = match.Value; if (len > 0 && len < length) { length = len; } } int endIndex = index + length; int max = bi == -1 ? _foregroundBrushes.Count : bi; var next = _foregroundBrushes.Take(max) .Where(b => b.Key.StartIndex <endIndex && b.Key.StartIndex> index) .OrderBy(b => b.Key.StartIndex) .FirstOrDefault(); if (next.Value != null) { length = next.Key.StartIndex - index; } } return(result); }
internal void Draw(SKCanvas canvas, SKPoint origin) { 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. * * SkPaint dpaint; * ctx->Canvas->save(); * ctx->Canvas->translate(origin.fX, origin.fY); * for (int c = 0; c < Lines.size(); c++) * { * dpaint.setARGB(255, 0, 0, 0); * SkRect rc; * rc.fLeft = 0; * rc.fTop = Lines[c].Top; * rc.fRight = Lines[c].Width; * rc.fBottom = rc.fTop + LineOffset; * ctx->Canvas->drawRect(rc, dpaint); * } * for (int c = 0; c < Length; c++) * { * dpaint.setARGB(255, c % 10 * 125 / 10 + 125, (c * 7) % 10 * 250 / 10, (c * 13) % 10 * 250 / 10); * dpaint.setStyle(SkPaint::kFill_Style); * ctx->Canvas->drawRect(Rects[c], dpaint); * } * ctx->Canvas->restore(); */ for (int c = 0; c < _skiaLines.Count; c++) { AvaloniaFormattedTextLine line = _skiaLines[c]; var subString = _text.Substring(line.Start, line.Length); canvas.DrawText(subString, origin.X, origin.Y + line.Top + LineOffset, paint); } }
public TextHitTestResult HitTestPoint(Point point) { float y = (float)point.Y; AvaloniaFormattedTextLine line = default; float nextTop = 0; foreach (var currentLine in _skiaLines) { if (currentLine.Top <= y) { line = currentLine; nextTop = currentLine.Top + currentLine.Height; } else { nextTop = currentLine.Top; break; } } if (!line.Equals(default(AvaloniaFormattedTextLine))) { var rects = GetRects(); for (int c = line.Start; c < line.Start + line.TextLength; c++) { var rc = rects[c]; if (rc.Contains(point)) { return(new TextHitTestResult { IsInside = !(line.TextLength > line.Length), TextPosition = c, IsTrailing = (point.X - rc.X) > rc.Width / 2 }); } } int offset = 0; if (point.X >= (rects[line.Start].X + line.Width) && line.Length > 0) { offset = line.TextLength > line.Length ? line.Length : (line.Length - 1); } if (y < nextTop) { return(new TextHitTestResult { IsInside = false, TextPosition = line.Start + offset, IsTrailing = Text.Length == (line.Start + offset + 1) }); } } bool end = point.X > _bounds.Width || point.Y > _lines.Sum(l => l.Height); return(new TextHitTestResult() { IsInside = false, IsTrailing = end, TextPosition = end ? Text.Length - 1 : 0 }); }
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.Ascent); string subString; float widthConstraint = double.IsPositiveInfinity(_constraint.Width) ? -1 : (float)_constraint.Width; while (curOff < length) { float lineWidth = -1; int measured; int trailingnumber = 0; 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.Start = curOff; line.TextLength = measured; subString = Text.Substring(line.Start, line.TextLength); lineWidth = _paint.MeasureText(subString); line.Length = measured - trailingnumber; line.Width = lineWidth; line.Height = _lineHeight; line.Top = curY; _skiaLines.Add(line); curY += _lineHeight; curY += mLeading; curOff += measured; //if this is the last line and there are trailing newline characters then //insert a additional line if (curOff >= length) { var subStringMinusNewlines = subString.TrimEnd('\n', '\r'); var lengthDiff = subString.Length - subStringMinusNewlines.Length; if (lengthDiff > 0) { AvaloniaFormattedTextLine lastLine = new AvaloniaFormattedTextLine(); lastLine.TextLength = lengthDiff; lastLine.Start = curOff - lengthDiff; var lastLineSubString = Text.Substring(line.Start, line.TextLength); var lastLineWidth = _paint.MeasureText(lastLineSubString); lastLine.Length = 0; lastLine.Width = lastLineWidth; lastLine.Height = _lineHeight; lastLine.Top = curY; lastLine.IsEmptyTrailingLine = true; _skiaLines.Add(lastLine); curY += _lineHeight; curY += mLeading; } } } // 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)); _bounds = new Rect(0, 0, 0, _lineHeight); } else { var lastLine = _skiaLines[_skiaLines.Count - 1]; _bounds = new Rect(0, 0, maxX, lastLine.Top + lastLine.Height); if (double.IsPositiveInfinity(Constraint.Width)) { return; } switch (_paint.TextAlign) { case SKTextAlign.Center: _bounds = new Rect(Constraint).CenterRect(_bounds); break; case SKTextAlign.Right: _bounds = new Rect( Constraint.Width - _bounds.Width, 0, _bounds.Width, _bounds.Height); break; } } }
internal void Draw(DrawingContextImpl context, SKCanvas canvas, SKPoint origin, DrawingContextImpl.PaintWrapper foreground, bool canUseLcdRendering) { /* 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. * * SkPaint dpaint; * ctx->Canvas->save(); * ctx->Canvas->translate(origin.fX, origin.fY); * for (int c = 0; c < Lines.size(); c++) * { * dpaint.setARGB(255, 0, 0, 0); * SkRect rc; * rc.fLeft = 0; * rc.fTop = Lines[c].Top; * rc.fRight = Lines[c].Width; * rc.fBottom = rc.fTop + LineOffset; * ctx->Canvas->drawRect(rc, dpaint); * } * for (int c = 0; c < Length; c++) * { * dpaint.setARGB(255, c % 10 * 125 / 10 + 125, (c * 7) % 10 * 250 / 10, (c * 13) % 10 * 250 / 10); * dpaint.setStyle(SkPaint::kFill_Style); * ctx->Canvas->drawRect(Rects[c], dpaint); * } * ctx->Canvas->restore(); */ using (var paint = _paint.Clone()) { IDisposable currd = null; var currentWrapper = foreground; SKPaint currentPaint = null; try { ApplyWrapperTo(ref currentPaint, foreground, ref currd, paint, canUseLcdRendering); bool hasCusomFGBrushes = _foregroundBrushes.Any(); for (int c = 0; c < _skiaLines.Count; c++) { AvaloniaFormattedTextLine line = _skiaLines[c]; float x = TransformX(origin.X, 0, paint.TextAlign); if (!hasCusomFGBrushes) { var subString = Text.Substring(line.Start, line.Length); canvas.DrawText(subString, x, origin.Y + line.Top + _lineOffset, paint); } else { float currX = x; string subStr; float measure; int len; float factor; switch (paint.TextAlign) { case SKTextAlign.Left: factor = 0; break; case SKTextAlign.Center: factor = 0.5f; break; case SKTextAlign.Right: factor = 1; break; default: throw new ArgumentOutOfRangeException(); } var textLine = Text.Substring(line.Start, line.Length); currX -= textLine.Length == 0 ? 0 : paint.MeasureText(textLine) * factor; for (int i = line.Start; i < line.Start + line.Length;) { var fb = GetNextForegroundBrush(ref line, i, out len); if (fb != null) { //TODO: figure out how to get the brush size currentWrapper = context.CreatePaint(new SKPaint { IsAntialias = true }, fb, new Size()); } else { if (!currentWrapper.Equals(foreground)) { currentWrapper.Dispose(); } currentWrapper = foreground; } subStr = Text.Substring(i, len); measure = paint.MeasureText(subStr); currX += measure * factor; ApplyWrapperTo(ref currentPaint, currentWrapper, ref currd, paint, canUseLcdRendering); canvas.DrawText(subStr, currX, origin.Y + line.Top + _lineOffset, paint); i += len; currX += measure * (1 - factor); } } } } finally { if (!currentWrapper.Equals(foreground)) { currentWrapper.Dispose(); } currd?.Dispose(); } } }
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.Ascent); 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); } }
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 bi = _foregroundBrushes.FindIndex(b => b.Key.StartIndex <= index && b.Key.EndIndex > index ); if (bi > -1) { var match = _foregroundBrushes[bi]; len = match.Key.EndIndex - index + 1; result = match.Value; if (len > 0 && len < length) { length = len; } } int endIndex = index + length; int max = bi == -1 ? _foregroundBrushes.Count : bi; var next = _foregroundBrushes.Take(max) .Where(b => b.Key.StartIndex < endIndex && b.Key.StartIndex > index) .OrderBy(b => b.Key.StartIndex) .FirstOrDefault(); if (next.Value != null) { length = next.Key.StartIndex - index; } } return result; }
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); } }
internal void Draw(DrawingContextImpl context, SKCanvas canvas, SKPoint origin, DrawingContextImpl.PaintWrapper foreground) { /* 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. * * SkPaint dpaint; * ctx->Canvas->save(); * ctx->Canvas->translate(origin.fX, origin.fY); * for (int c = 0; c < Lines.size(); c++) * { * dpaint.setARGB(255, 0, 0, 0); * SkRect rc; * rc.fLeft = 0; * rc.fTop = Lines[c].Top; * rc.fRight = Lines[c].Width; * rc.fBottom = rc.fTop + LineOffset; * ctx->Canvas->drawRect(rc, dpaint); * } * for (int c = 0; c < Length; c++) * { * dpaint.setARGB(255, c % 10 * 125 / 10 + 125, (c * 7) % 10 * 250 / 10, (c * 13) % 10 * 250 / 10); * dpaint.setStyle(SkPaint::kFill_Style); * ctx->Canvas->drawRect(Rects[c], dpaint); * } * ctx->Canvas->restore(); */ SKPaint paint = _paint; IDisposable currd = null; var currentWrapper = foreground; 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]; float x = TransformX(origin.X, 0, paint.TextAlign); if (!hasCusomFGBrushes) { var subString = _text.Substring(line.Start, line.Length); canvas.DrawText(subString, x, origin.Y + line.Top + _lineOffset, paint); } else { float currX = x; string subStr; int len; for (int i = line.Start; i < line.Start + line.Length;) { var fb = GetNextForegroundBrush(ref line, i, out len); if (fb != null) { //TODO: figure out how to get the brush size currentWrapper = context.CreatePaint(fb, new Size()); } else { if (!currentWrapper.Equals(foreground)) { currentWrapper.Dispose(); } currentWrapper = foreground; } subStr = _text.Substring(i, len); if (currFGPaint != currentWrapper.Paint) { currFGPaint = ApplyWrapperTo(ref currentWrapper, ref currd, paint); } canvas.DrawText(subStr, currX, origin.Y + line.Top + _lineOffset, paint); i += len; currX += paint.MeasureText(subStr); } } } } finally { if (!currentWrapper.Equals(foreground)) { currentWrapper.Dispose(); } currd?.Dispose(); } }
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; }
void Rebuild() { var length = _text.Length; _lines.Clear(); _skiaRects = new SKRect[length]; _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). // This seems like the best measure of full vertical extent float lineHeight = mBottom - mTop; // Rendering is relative to baseline LineOffset = -metrics.Top; string subString; byte[] bytes; GCHandle pinnedArray; IntPtr pointer; for (int c = 0; curOff < length; c++) { float lineWidth = -1; int measured; int extraSkip = 0; if (WidthConstraint <= 0) { measured = length; } else { float constraint = WidthConstraint; if (constraint > MAX_LINE_WIDTH) { constraint = MAX_LINE_WIDTH; } subString = _text.Substring(curOff); // TODO: This method is not linking into SkiaSharp so we must use the RAW buffer version for now //measured = (int)Paint.BreakText(subString, constraint, out lineWidth) / 2; bytes = Encoding.UTF8.GetBytes(subString); pinnedArray = GCHandle.Alloc(bytes, GCHandleType.Pinned); pointer = pinnedArray.AddrOfPinnedObject(); // for some reason I have to pass nBytes * 2. I assume under the hood it expects Unicode/WChar?? measured = (int)Paint.BreakText(pointer, (IntPtr)(bytes.Length * 2), constraint, out lineWidth) / 2; pinnedArray.Free(); if (measured == 0) { measured = 1; lineWidth = -1; } char nextChar = ' '; if (curOff + measured < length) { nextChar = _text[curOff + measured]; } if (nextChar != ' ') { // Perform scan for the last space and end the line there for (int si = curOff + measured - 1; si > curOff; si--) { if (_text[si] == ' ') { measured = si - curOff; extraSkip = 1; break; } } } } AvaloniaFormattedTextLine line = new AvaloniaFormattedTextLine(); line.Start = curOff; line.Length = measured; line.Width = lineWidth; line.Height = lineHeight; line.Top = curY; if (line.Width < 0) { line.Width = _skiaRects[line.Start + line.Length - 1].Right; } // Build character rects for (int i = line.Start; i < line.Start + line.Length; i++) { float prevRight = 0; if (i != line.Start) { prevRight = _skiaRects[i - 1].Right; } subString = _text.Substring(line.Start, i - line.Start + 1); float w = Paint.MeasureText(subString); SKRect rc; rc.Left = prevRight; rc.Right = w; rc.Top = line.Top; rc.Bottom = line.Top + line.Height; _skiaRects[i] = rc; } subString = _text.Substring(line.Start, line.Length); line.Width = Paint.MeasureText(subString); _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 + extraSkip; } // Now convert to Avalonia data formats _lines.Clear(); _rects.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].Length, _skiaLines[c].Height)); } for (var c = 0; c < _text.Length; c++) { _rects.Add(_skiaRects[c].ToAvaloniaRect()); } if (_skiaLines.Count == 0) { _size = new Size(); } else { var lastLine = _skiaLines[_skiaLines.Count - 1]; _size = new Size(maxX, lastLine.Top + lastLine.Height); } }