Skip to content

Commit

Permalink
Improve text rendering performance (refs #91)
Browse files Browse the repository at this point in the history
The built-in `FastTextBlock` control was updated to support multi line text which now enables the implementation of `ITextElementFactory` to use this component more frequently instead of the slower default WPF TextBlock.
  • Loading branch information
ElektroKill committed May 9, 2024
1 parent 5c26872 commit 076a3c0
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 21 deletions.
77 changes: 60 additions & 17 deletions dnSpy/dnSpy.Contracts.DnSpy/Controls/FastTextBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,21 @@ THE SOFTWARE.
*/

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Media.TextFormatting;
using dnSpy.Contracts.Text;

namespace dnSpy.Contracts.Controls {
sealed class FastTextBlock : FrameworkElement {
public interface IFastTextSource {
void UpdateParent(FastTextBlock ftb);
TextSource Source { get; }
bool GetNextLineIndex(ref int index);
}

public string Text {
Expand All @@ -48,7 +51,6 @@ public FastTextBlock()
this.src = src;
}


public static readonly DependencyProperty TextProperty;
public static readonly DependencyProperty FontFamilyProperty;
public static readonly DependencyProperty FontStyleProperty;
Expand Down Expand Up @@ -103,7 +105,7 @@ public FastTextBlock()
TextFormattingMode? textFormattingMode;

class TextProps : TextRunProperties {
FastTextBlock tb;
readonly FastTextBlock tb;

public TextProps(FastTextBlock tb) => this.tb = tb;

Expand Down Expand Up @@ -143,11 +145,23 @@ class TextSrc : TextSource, IFastTextSource {
}

public TextSource Source => this;

public bool GetNextLineIndex(ref int index) {
if (index >= text.Length || index < 0)
return false;
index = text.IndexOfAny(LineConstants.newLineChars, index);
if (index < 0)
return false;
if (text[index] == '\r' && index + 1 < text.Length && text[index + 1] == '\n')
index++;
index++;
return true;
}
}

internal sealed class ParaProps : TextParagraphProperties {
FastTextBlock tb;
TextProps props;
sealed class ParaProps : TextParagraphProperties {
readonly FastTextBlock tb;
readonly TextProps props;

public ParaProps(FastTextBlock tb) {
this.tb = tb;
Expand All @@ -164,9 +178,9 @@ internal sealed class ParaProps : TextParagraphProperties {
public override TextWrapping TextWrapping => TextWrapping.NoWrap;
}


TextFormatter? fmt = null;
TextLine? line = null;
List<TextLine>? lines = null;

Typeface GetTypeface() {
var fontFamily = (FontFamily)GetValue(FontFamilyProperty);
Expand All @@ -176,17 +190,29 @@ internal sealed class ParaProps : TextParagraphProperties {
return new Typeface(fontFamily, fontStyle, fontWeight, fontStrech, null);
}

IFastTextSource src;
readonly IFastTextSource src;

void MakeNewText() {
if (fmt is null)
fmt = TextFormatterFactory.GetTextFormatter(this);
fmt ??= TextFormatterFactory.GetTextFormatter(this);

if (line is not null)
line.Dispose();
line?.Dispose();
lines?.Clear();

src.UpdateParent(this);
line = fmt.FormatLine(src.Source, 0, 0, new ParaProps(this), null);

var paragraphProperties = new ParaProps(this);
line = fmt.FormatLine(src.Source, 0, 0, paragraphProperties, null);

int index = 0;
while (src.GetNextLineIndex(ref index)) {
if (line is not null) {
lines ??= new List<TextLine>();
lines.Add(line);
line = null;
}
// lines should not be empty at this point!
lines!.Add(fmt.FormatLine(src.Source, index, 0, paragraphProperties, null));
}
}

void EnsureText() {
Expand All @@ -197,17 +223,34 @@ internal sealed class ParaProps : TextParagraphProperties {
}
}


protected override Size MeasureOverride(Size availableSize) {
EnsureText();
Debug2.Assert(line is not null);
return new Size(line.Width, line.Height);
if (line is not null)
return new Size(line.Width, line.Height);

Debug2.Assert(lines is not null);
var size = new Size();
for (int i = 0; i < lines.Count; i++) {
var textLine = lines[i];
size.Width = Math.Max(size.Width, textLine.Width);
size.Height += textLine.Height;
}
return size;
}

protected override void OnRender(DrawingContext drawingContext) {
EnsureText();
Debug2.Assert(line is not null);
line.Draw(drawingContext, new Point(0, 0), InvertAxes.None);
if (line is not null) {
line.Draw(drawingContext, new Point(0, 0), InvertAxes.None);
return;
}
Debug2.Assert(lines is not null);
double y = 0;
for (int i = 0; i < lines.Count; i++) {
var textLine = lines[i];
textLine.Draw(drawingContext, new Point(0, y), InvertAxes.None);
y += textLine.Height;
}
}
}

Expand Down
19 changes: 15 additions & 4 deletions dnSpy/dnSpy/Controls/TextElementFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ static class TextElementFactory {
}

public static FrameworkElement Create(IClassificationFormatMap classificationFormatMap, string text, IList<TextClassificationTag> tags, TextElementFlags flags) {
bool useFastTextBlock = (flags & (TextElementFlags.TrimmingMask | TextElementFlags.WrapMask | TextElementFlags.FilterOutNewLines)) == (TextElementFlags.NoTrimming | TextElementFlags.NoWrap | TextElementFlags.FilterOutNewLines);
bool useFastTextBlock = (flags & (TextElementFlags.TrimmingMask | TextElementFlags.WrapMask)) == (TextElementFlags.NoTrimming | TextElementFlags.NoWrap);
bool filterOutNewLines = (flags & TextElementFlags.FilterOutNewLines) != 0;
if (tags.Count != 0) {
if (useFastTextBlock) {
Expand Down Expand Up @@ -232,16 +232,15 @@ sealed class TextProps : TextRunProperties {
info = tagsList[collIndex + 1];
}

int startIndex = info.Span.Start;
int endIndex = info.Span.End;

int nlIndex = text.IndexOfAny(LineConstants.newLineChars, index, endIndex - startIndex);
int nlIndex = text.IndexOfAny(LineConstants.newLineChars, index, endIndex - index);
if (nlIndex > 0)
endIndex = nlIndex;

var props = classificationFormatMap.GetTextProperties(info.ClassificationType);

var tokenText = text.Substring(index, endIndex - startIndex);
var tokenText = text.Substring(index, endIndex - index);

var textProps = new TextProps();
textProps.fontSize = TextElement.GetFontSize(parent);
Expand All @@ -261,6 +260,18 @@ sealed class TextProps : TextRunProperties {

return new TextCharacters(tokenText.Length == 0 ? " " : tokenText, textProps);
}

public bool GetNextLineIndex(ref int index) {
if (index >= text.Length || index < 0)
return false;
index = text.IndexOfAny(LineConstants.newLineChars, index);
if (index < 0)
return false;
if (text[index] == '\r' && index + 1 < text.Length && text[index + 1] == '\n')
index++;
index++;
return true;
}
}
}
}

0 comments on commit 076a3c0

Please sign in to comment.