diff --git a/TestBench/Program.cs b/TestBench/Program.cs index 344a408..28ae364 100644 --- a/TestBench/Program.cs +++ b/TestBench/Program.cs @@ -7,7 +7,8 @@ { //BidiTest.Run(); //BidiCharacterTest.Run(); - LineBreakTest.Run(); + //LineBreakTest.Run(); + TextBlockLoadTest.Run(); } } } diff --git a/TestBench/TextBlockLoadTest.cs b/TestBench/TextBlockLoadTest.cs new file mode 100644 index 0000000..689fb8b --- /dev/null +++ b/TestBench/TextBlockLoadTest.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Topten.RichTextKit; + +namespace TestBench +{ + public static class TextBlockLoadTest + { + public static bool Run() + { + Console.WriteLine("Text Block Load Test"); + Console.WriteLine("--------------------"); + Console.WriteLine(); + + string typefaceName = "Arial"; + float Scale = 1.5f; + var styleSmall = new Style() { FontFamily = typefaceName, FontSize = 12 * Scale }; + var styleScript = new Style() { FontFamily = "Segoe Script", FontSize = 18 * Scale }; + var styleHeading = new Style() { FontFamily = typefaceName, FontSize = 24 * Scale, FontWeight = 700 }; + var styleNormal = new Style() { FontFamily = typefaceName, FontSize = 18 * Scale, LineHeight = 1.0f }; + var styleBold = new Style() { FontFamily = typefaceName, FontSize = 18 * Scale, FontWeight = 700 }; + var styleUnderline = new Style() { FontFamily = typefaceName, FontSize = 18 * Scale, Underline = UnderlineStyle.Gapped }; + var styleStrike = new Style() { FontFamily = typefaceName, FontSize = 18 * Scale, StrikeThrough = StrikeThroughStyle.Solid }; + var styleSubScript = new Style() { FontFamily = typefaceName, FontSize = 18 * Scale, FontVariant = FontVariant.SubScript }; + var styleSuperScript = new Style() { FontFamily = typefaceName, FontSize = 18 * Scale, FontVariant = FontVariant.SuperScript }; + var styleItalic = new Style() { FontFamily = typefaceName, FontItalic = true, FontSize = 18 * Scale }; + var styleBoldLarge = new Style() { FontFamily = typefaceName, FontSize = 28 * Scale, FontWeight = 700 }; + var styleRed = new Style() { FontFamily = typefaceName, FontSize = 18 * Scale/*, TextColor = new SKColor(0xFFFF0000) */}; + var styleBlue = new Style() { FontFamily = typefaceName, FontSize = 18 * Scale/*, TextColor = new SKColor(0xFF0000FF) */}; + + + var tr = new TestResults(); + var tb = new TextBlock(); + + for (int i = 0; i < 1000; i++) + { + tr.EnterTest(); + + tb.Clear(); + tb.MaxWidth = 1000; + tb.AddText("Welcome to RichTextKit!\n", styleHeading); + tb.AddText("\nRichTextKit is a rich text layout, rendering and measurement library for SkiaSharp.\n\nIt supports normal, ", styleNormal); + tb.AddText("bold", styleBold); + tb.AddText(", ", styleNormal); + tb.AddText("italic", styleItalic); + tb.AddText(", ", styleNormal); + tb.AddText("underline", styleUnderline); + tb.AddText(" (including ", styleNormal); + tb.AddText("gaps over descenders", styleUnderline); + tb.AddText("), ", styleNormal); + tb.AddText("strikethrough", styleStrike); + tb.AddText(", superscript (E=mc", styleNormal); + tb.AddText("2", styleSuperScript); + tb.AddText("), subscript (H", styleNormal); + tb.AddText("2", styleSubScript); + tb.AddText("O), ", styleNormal); + tb.AddText("colored ", styleRed); + tb.AddText("text", styleBlue); + tb.AddText(" and ", styleNormal); + tb.AddText("mixed ", styleNormal); + tb.AddText("sizes", styleSmall); + tb.AddText(" and ", styleNormal); + tb.AddText("fonts", styleScript); + tb.AddText(".\n\n", styleNormal); + tb.AddText("Font fallback means emojis work: 🌐 🍪 🍕 🚀 and ", styleNormal); + tb.AddText("text shaping and bi-directional text support means complex scripts and languages like Arabic: مرحبا بالعالم, Japanese: ハローワールド, Chinese: 世界您好 and Hindi: हैलो वर्ल्ड are rendered correctly!\n\n", styleNormal); + tb.AddText("RichTextKit also supports left/center/right text alignment, word wrapping, truncation with ellipsis place-holder, text measurement, hit testing, painting a selection range, caret position & shape helpers.", styleNormal); + tb.Layout(); + tr.LeaveTest(); + tr.TestPassed(true); + } + + tr.Dump(); + return tr.AllPassed; + } + } +} diff --git a/Topten.RichTextKit/FontRun.cs b/Topten.RichTextKit/FontRun.cs index 9459cd0..b0c861d 100644 --- a/Topten.RichTextKit/FontRun.cs +++ b/Topten.RichTextKit/FontRun.cs @@ -304,23 +304,21 @@ namespace Topten.RichTextKit } // Create the other run - var newRun = new FontRun() - { - StyleRun = this.StyleRun, - CodePointBuffer = this.CodePointBuffer, - Direction = this.Direction, - Ascent = this.Ascent, - Descent = this.Descent, - Style = this.Style, - Typeface = this.Typeface, - Start = splitAtCodePoint, - Length = this.End - splitAtCodePoint, - Width = sliceRightWidth, - RelativeCodePointXCoords = this.RelativeCodePointXCoords.SubSlice(codePointSplitPos), - GlyphPositions = this.GlyphPositions.SubSlice(glyphSplitPos), - Glyphs = this.Glyphs.SubSlice(glyphSplitPos), - Clusters = this.Clusters.SubSlice(glyphSplitPos), - }; + var newRun = FontRun.Pool.Get(); + newRun.StyleRun = this.StyleRun; + newRun.CodePointBuffer = this.CodePointBuffer; + newRun.Direction = this.Direction; + newRun.Ascent = this.Ascent; + newRun.Descent = this.Descent; + newRun.Style = this.Style; + newRun.Typeface = this.Typeface; + newRun.Start = splitAtCodePoint; + newRun.Length = this.End - splitAtCodePoint; + newRun.Width = sliceRightWidth; + newRun.RelativeCodePointXCoords = this.RelativeCodePointXCoords.SubSlice(codePointSplitPos); + newRun.GlyphPositions = this.GlyphPositions.SubSlice(glyphSplitPos); + newRun.Glyphs = this.Glyphs.SubSlice(glyphSplitPos); + newRun.Clusters = this.Clusters.SubSlice(glyphSplitPos); // Adjust code point positions for (int i = 0; i < newRun.RelativeCodePointXCoords.Length; i++) @@ -375,23 +373,21 @@ namespace Topten.RichTextKit } // Create the other run - var newRun = new FontRun() - { - StyleRun = this.StyleRun, - CodePointBuffer = this.CodePointBuffer, - Direction = this.Direction, - Ascent = this.Ascent, - Descent = this.Descent, - Style = this.Style, - Typeface = this.Typeface, - Start = splitAtCodePoint, - Length = this.End - splitAtCodePoint, - Width = sliceLeftWidth, - RelativeCodePointXCoords = this.RelativeCodePointXCoords.SubSlice(codePointSplitPos), - GlyphPositions = this.GlyphPositions.SubSlice(0, glyphSplitPos), - Glyphs = this.Glyphs.SubSlice(0, glyphSplitPos), - Clusters = this.Clusters.SubSlice(0, glyphSplitPos), - }; + var newRun = FontRun.Pool.Get(); + newRun.StyleRun = this.StyleRun; + newRun.CodePointBuffer = this.CodePointBuffer; + newRun.Direction = this.Direction; + newRun.Ascent = this.Ascent; + newRun.Descent = this.Descent; + newRun.Style = this.Style; + newRun.Typeface = this.Typeface; + newRun.Start = splitAtCodePoint; + newRun.Length = this.End - splitAtCodePoint; + newRun.Width = sliceLeftWidth; + newRun.RelativeCodePointXCoords = this.RelativeCodePointXCoords.SubSlice(codePointSplitPos); + newRun.GlyphPositions = this.GlyphPositions.SubSlice(0, glyphSplitPos); + newRun.Glyphs = this.Glyphs.SubSlice(0, glyphSplitPos); + newRun.Clusters = this.Clusters.SubSlice(0, glyphSplitPos); // Update this run this.RelativeCodePointXCoords = this.RelativeCodePointXCoords.SubSlice(0, codePointSplitPos); @@ -592,5 +588,18 @@ namespace Topten.RichTextKit } } } + + [ThreadStatic] + internal static ObjectPool Pool = new ObjectPool() + { + Cleaner = (r) => + { + r.RunKind = FontRunKind.Normal; + r.CodePointBuffer = null; + r.Style = null; + r.Typeface = null; + r.Line = null; + } + }; } } diff --git a/Topten.RichTextKit/StyleRun.cs b/Topten.RichTextKit/StyleRun.cs index afb8977..ffb781c 100644 --- a/Topten.RichTextKit/StyleRun.cs +++ b/Topten.RichTextKit/StyleRun.cs @@ -64,5 +64,16 @@ namespace Topten.RichTextKit /// The global list of code points /// internal Buffer CodePointBuffer; + + [ThreadStatic] + internal static ObjectPool Pool = new ObjectPool() + { + Cleaner = (r) => + { + r.CodePointBuffer = null; + r.Style = null; + r.TextBlock = null; + } + }; } } diff --git a/Topten.RichTextKit/TextBlock.cs b/Topten.RichTextKit/TextBlock.cs index b5c74d1..d36c896 100644 --- a/Topten.RichTextKit/TextBlock.cs +++ b/Topten.RichTextKit/TextBlock.cs @@ -157,9 +157,9 @@ namespace Topten.RichTextKit { // Reset everything _codePoints.Clear(); - _styleRuns.Clear(); - _fontRuns.Clear(); - _lines.Clear(); + StyleRun.Pool.ReturnAndClear(_styleRuns); + FontRun.Pool.ReturnAndClear(_fontRuns); + TextLine.Pool.ReturnAndClear(_lines); _textShapingBuffers.Clear(); InvalidateLayout(); } @@ -179,14 +179,12 @@ namespace Topten.RichTextKit var utf32 = _codePoints.Add(text); // Create a run - var run = new StyleRun() - { - TextBlock = this, - CodePointBuffer = _codePoints, - Start = utf32.Start, - Length = utf32.Length, - Style = style, - }; + var run = StyleRun.Pool.Get(); + run.TextBlock = this; + run.CodePointBuffer = _codePoints; + run.Start = utf32.Start; + run.Length = utf32.Length; + run.Style = style; // Add run _styleRuns.Add(run); @@ -194,6 +192,8 @@ namespace Topten.RichTextKit return run; } + List _styledRunPool = new List(); + /// /// Add text to this paragraph /// @@ -799,7 +799,8 @@ namespace Topten.RichTextKit return _textAlignment; } - Bidi _bidi = new Bidi(); + // Use the shared Bidi algo instance + Bidi _bidi = Bidi.Instance; /// /// Split into runs based on directionality and style switch points @@ -943,23 +944,22 @@ namespace Topten.RichTextKit } // Create the run - return new FontRun() - { - StyleRun = styleRun, - CodePointBuffer = _codePoints, - Start = codePoints.Start, - Length = codePoints.Length, - Style = style, - Direction = direction, - Typeface = typeface, - Glyphs = shaped.GlyphIndicies, - GlyphPositions = shaped.GlyphPositions, - RelativeCodePointXCoords = shaped.CodePointXCoords, - Clusters = shaped.Clusters, - Ascent = shaped.Ascent, - Descent = shaped.Descent, - Width = shaped.EndXCoord.X, - }; + var fontRun = FontRun.Pool.Get(); + fontRun.StyleRun = styleRun; + fontRun.CodePointBuffer = _codePoints; + fontRun.Start = codePoints.Start; + fontRun.Length = codePoints.Length; + fontRun.Style = style; + fontRun.Direction = direction; + fontRun.Typeface = typeface; + fontRun.Glyphs = shaped.GlyphIndicies; + fontRun.GlyphPositions = shaped.GlyphPositions; + fontRun.RelativeCodePointXCoords = shaped.CodePointXCoords; + fontRun.Clusters = shaped.Clusters; + fontRun.Ascent = shaped.Ascent; + fontRun.Descent = shaped.Descent; + fontRun.Width = shaped.EndXCoord.X; + return fontRun; } /// @@ -1110,7 +1110,7 @@ namespace Topten.RichTextKit void BuildLine(int frIndexStartOfLine, int frSplitIndex, int frTrailingWhiteSpaceIndex) { // Create the line - var line = new TextLine(); + var line = TextLine.Pool.Get(); line.TextBlock = this; line.YCoord = _measuredHeight; diff --git a/Topten.RichTextKit/TextLine.cs b/Topten.RichTextKit/TextLine.cs index 90bf0d6..14ba19a 100644 --- a/Topten.RichTextKit/TextLine.cs +++ b/Topten.RichTextKit/TextLine.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Topten.RichTextKit.Utils; namespace Topten.RichTextKit { @@ -296,5 +297,15 @@ namespace Topten.RichTextKit /// Internal List of runs /// internal List RunsInternal = new List(); + + [ThreadStatic] + internal static ObjectPool Pool = new ObjectPool() + { + Cleaner = (r) => + { + r.TextBlock = null; + r.RunsInternal.Clear(); + } + }; } } diff --git a/Topten.RichTextKit/Utils/Buffer.cs b/Topten.RichTextKit/Utils/Buffer.cs index a1e9640..8907b88 100644 --- a/Topten.RichTextKit/Utils/Buffer.cs +++ b/Topten.RichTextKit/Utils/Buffer.cs @@ -202,6 +202,7 @@ namespace Topten.RichTextKit.Utils return new ArraySliceEnumerator(_data, 0, _length); } + /* public T[] SetMapping(Slice data, Slice mapping) { Length = mapping.Length; @@ -221,5 +222,6 @@ namespace Topten.RichTextKit.Utils } return _data; } + */ } } diff --git a/Topten.RichTextKit/Utils/ObjectPool.cs b/Topten.RichTextKit/Utils/ObjectPool.cs new file mode 100644 index 0000000..9a4e883 --- /dev/null +++ b/Topten.RichTextKit/Utils/ObjectPool.cs @@ -0,0 +1,73 @@ +//#define NO_POOLING +using System; +using System.Collections.Generic; +using System.Text; + +namespace Topten.RichTextKit.Utils +{ + internal class ObjectPool where T : class, new() + { + public ObjectPool() + { + } + + public T Get() + { +#if NO_POOLING + return new T(); +#else + int count = _pool.Count; + if (count == 0) + return new T(); + + var obj = _pool[count - 1]; + _pool.RemoveAt(count - 1); + return obj; +#endif + } + + public void Return(T obj) + { +#if NO_POOLING +#else + Cleaner?.Invoke(obj); + _pool.Add(obj); +#endif + } + + public void Return(IEnumerable objs) + { +#if NO_POOLING +#else + if (Cleaner != null) + { + foreach (var x in objs) + { + Cleaner(x); + } + _pool.AddRange(objs); + } +#endif + } + + public void ReturnAndClear(List objs) + { +#if NO_POOLING +#else + if (Cleaner != null) + { + foreach (var x in objs) + { + Cleaner(x); + } + _pool.AddRange(objs); + } +#endif + objs.Clear(); + } + + public Action Cleaner; + + List _pool = new List(); + } +} diff --git a/build.js b/build.js index fbfd8bb..3a070f2 100644 --- a/build.js +++ b/build.js @@ -31,7 +31,7 @@ if (bt.options.official) bt.git_tag(); // Push nuget package - //bt.nupush(`./build/Release/Topten.RichTextKit/*.${bt.options.version.build}.nupkg`, "http://nuget.toptensoftware.com/v3/index.json"); + bt.nupush(`./build/Release/Topten.RichTextKit/*.${bt.options.version.build}.nupkg`, "http://nuget.toptensoftware.com/v3/index.json"); }