diff --git a/Directory.Build.props b/Directory.Build.props index 0fc27e3e..c7cff62a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,8 +5,9 @@ 0.10.999-cibuild0018389-beta 1.0.24 13.0.1 - 0.10.12.1 + 0.10.12.1 + https://api.nuget.org/v3/index.json; https://nuget.avaloniaui.net/repository/avalonia-nightly/index.json; diff --git a/src/AvaloniaEdit/Editing/LineNumberMargin.cs b/src/AvaloniaEdit/Editing/LineNumberMargin.cs index 6bf71066..28353f9c 100644 --- a/src/AvaloniaEdit/Editing/LineNumberMargin.cs +++ b/src/AvaloniaEdit/Editing/LineNumberMargin.cs @@ -55,7 +55,7 @@ protected override Size MeasureOverride(Size availableSize) Typeface = GetValue(TextBlock.FontFamilyProperty); EmSize = GetValue(TextBlock.FontSizeProperty); - var text = TextFormatterFactory.CreateFormattedText( + var text = TextFormatterFactory.CreateTextLine( this, new string('9', MaxLineNumberLength), Typeface, @@ -76,13 +76,13 @@ public override void Render(DrawingContext drawingContext) foreach (var line in textView.VisualLines) { var lineNumber = line.FirstDocumentLine.LineNumber; - var text = TextFormatterFactory.CreateFormattedText( + var text = TextFormatterFactory.CreateTextLine( this, lineNumber.ToString(CultureInfo.CurrentCulture), Typeface, EmSize, foreground ); var y = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.TextTop); - drawingContext.DrawText(text, + text.Draw(drawingContext, new Point(renderSize.Width - text.WidthIncludingTrailingWhitespace, y - textView.VerticalOffset)); } } diff --git a/src/AvaloniaEdit/Rendering/FormattedTextElement.cs b/src/AvaloniaEdit/Rendering/FormattedTextElement.cs index 81fbd9b1..845fda21 100644 --- a/src/AvaloniaEdit/Rendering/FormattedTextElement.cs +++ b/src/AvaloniaEdit/Rendering/FormattedTextElement.cs @@ -31,7 +31,6 @@ namespace AvaloniaEdit.Rendering /// public class FormattedTextElement : VisualLineElement { - internal FormattedText FormattedText { get; } internal string Text { get; set; } internal TextLine TextLine { get; set; } @@ -53,15 +52,6 @@ internal FormattedTextElement(TextLine text, int documentLength) : base(1, docum TextLine = text ?? throw new ArgumentNullException(nameof(text)); } - /// - /// Creates a new FormattedTextElement that displays the specified text - /// and occupies the specified length in the document. - /// - public FormattedTextElement(FormattedText text, int documentLength) : base(1, documentLength) - { - FormattedText = text ?? throw new ArgumentNullException(nameof(text)); - } - /// public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) { @@ -131,11 +121,6 @@ public FormattedTextRun(FormattedTextElement element, TextRunProperties properti public override Size GetSize(double remainingParagraphWidth) { - var formattedText = Element.FormattedText; - if (formattedText != null) - { - return new Size(formattedText.WidthIncludingTrailingWhitespace, formattedText.Height); - } var text = Element.TextLine; return new Size(text.WidthIncludingTrailingWhitespace, text.Height); @@ -150,16 +135,8 @@ public override Rect ComputeBoundingBox() /// public override void Draw(DrawingContext drawingContext, Point origin) { - if (Element.FormattedText != null) - { - //origin = origin.WithY(origin.Y - Element.formattedText.Baseline); - drawingContext.DrawText(Element.FormattedText, origin); - } - else - { - //origin.Y -= element.textLine.Baseline; - Element.TextLine.Draw(drawingContext, origin); - } + //origin.Y -= element.textLine.Baseline; + Element.TextLine.Draw(drawingContext, origin); } } } diff --git a/src/AvaloniaEdit/Text/StringRange.cs b/src/AvaloniaEdit/Text/StringRange.cs index a27cf081..2eaa634f 100644 --- a/src/AvaloniaEdit/Text/StringRange.cs +++ b/src/AvaloniaEdit/Text/StringRange.cs @@ -21,6 +21,30 @@ public StringRange(string s, int offsetToFirstChar, int length) Length = length; } + public int IndexOf(char ch, int startIndex = 0) + { + for (int i = startIndex; i < Length; ++i) + { + if (ch == this[i]) + { + return i; + } + } + return -1; + } + + public StringRange SubRange(int start, int length) + { + if (start < 0) + throw new ArgumentOutOfRangeException(nameof(start)); + + if (start + length > Length) + throw new ArgumentOutOfRangeException(nameof(length)); + + + return new StringRange(String, OffsetToFirstChar + start, length); + } + public override string ToString() { if (String == null) return string.Empty; diff --git a/src/AvaloniaEdit/Text/TextLineRun.cs b/src/AvaloniaEdit/Text/TextLineRun.cs index 2d71aa91..6abc2782 100644 --- a/src/AvaloniaEdit/Text/TextLineRun.cs +++ b/src/AvaloniaEdit/Text/TextLineRun.cs @@ -9,6 +9,7 @@ using Avalonia.Media.TextFormatting; using AvaloniaEdit.Rendering; +using AvaloniaEdit.Utils; namespace AvaloniaEdit.Text { @@ -17,7 +18,7 @@ internal sealed class TextLineRun private const string NewlineString = "\r\n"; private const string TabString = "\t"; - private FormattedText _formattedText; + private Avalonia.Media.TextFormatting.TextLine _formattedText; private Size _formattedTextSize; private IReadOnlyList _glyphWidths; public StringRange StringRange { get; private set; } @@ -68,7 +69,7 @@ public double Height var box = embeddedObject.ComputeBoundingBox(); return box.Height; } - + return GetDefaultLineHeight(TextRun.Properties.FontMetrics); } } @@ -178,33 +179,55 @@ private static TextLineRun CreateRunForTab(TextRun textRun, TextParagraphPropert internal static TextLineRun CreateRunForText(StringRange stringRange, TextRun textRun, double widthLeft, bool emergencyWrap, bool breakOnTabs, TextParagraphProperties paragraphProperties) { - var run = new TextLineRun + var ngwdIdx = stringRange.IndexOf('\t'); + + StringRange text; + TextRun run; + + if (ngwdIdx == -1) { - StringRange = stringRange, - TextRun = textRun, - Length = textRun.Length + text = stringRange; + run = textRun; + } + else + { + + text = stringRange.SubRange(0, ngwdIdx); + run = new TextRunImpl(text, textRun.Properties); + + } + + var linerun = new TextLineRun + { + StringRange = text, + TextRun = run, + Length = run.Length }; - var tf = run.Typeface; - var formattedText = new FormattedText(stringRange.ToString(), CultureInfo.CurrentCulture, - FlowDirection.LeftToRight, new Typeface(tf.FontFamily, tf.Style, tf.Weight), run.FontSize, textRun.Properties.ForegroundBrush); + var tf = linerun.Typeface; + var useCheapGlyphMeasurement = + run.Length >= VisualLine.LENGTH_LIMIT && + tf.GlyphTypeface.IsFixedPitch; - run._formattedText = formattedText; - var size = new Size(formattedText.WidthIncludingTrailingWhitespace, formattedText.Height); + var line = TextFormatterFactory.CreateTextLine( + text.ToString(), + new Typeface(tf.FontFamily, tf.Style, tf.Weight), + linerun.FontSize, + run.Properties.ForegroundBrush); - run._formattedTextSize = size; + linerun._formattedText = line; - run.Width = size.Width; + var size = new Size(line.WidthIncludingTrailingWhitespace, line.Height); - run._glyphWidths = new GlyphWidths( - run.StringRange, - run.Typeface.GlyphTypeface, - run.FontSize, - paragraphProperties.DefaultIncrementalTab); + linerun._formattedTextSize = size; - return run; + linerun.Width = size.Width; + + linerun._glyphWidths = new CharacterWidths(line, text.Length, useCheapGlyphMeasurement); + + return linerun; } private TextLineRun(int length, TextRun textRun) @@ -235,14 +258,14 @@ public void Draw(DrawingContext drawingContext, double x, double y) drawingContext.FillRectangle(TextRun.Properties.BackgroundBrush, bounds); } - drawingContext.DrawText(_formattedText, new Point(x, y)); + _formattedText.Draw(drawingContext, new Point(x, y)); var glyphTypeface = TextRun.Properties.Typeface.GlyphTypeface; - - var scale = TextRun.Properties.FontSize / glyphTypeface.DesignEmHeight; + + var scale = TextRun.Properties.FontSize / glyphTypeface.DesignEmHeight; var baseline = y + -glyphTypeface.Ascent * scale; - + if (TextRun.Properties.Underline) { var pen = new Pen(TextRun.Properties.ForegroundBrush, glyphTypeface.UnderlineThickness * scale); @@ -339,76 +362,85 @@ private static bool IsSpace(char ch) return ch == ' ' || ch == '\u00a0'; } - class GlyphWidths : IReadOnlyList + class CharacterWidths : IReadOnlyList { - private const double NOT_CALCULATED_YET = -1; - private double[] _glyphWidths; - private GlyphTypeface _typeFace; - private StringRange _range; - private double _scale; - private double _tabSize; - - public int Count => _glyphWidths.Length; - public double this[int index] => GetAt(index); + private Avalonia.Media.TextFormatting.TextLine _measure; + private double[] _width; + private bool _isMeasured; - internal GlyphWidths(StringRange range, GlyphTypeface typeFace, double fontSize, double tabSize) + public CharacterWidths(Avalonia.Media.TextFormatting.TextLine measure, int length, bool useCheapGlyphMeasurement) { - _range = range; - _typeFace = typeFace; - _scale = fontSize / _typeFace.DesignEmHeight; - _tabSize = tabSize; + _measure = measure; + _width = new double[length]; + Count = length; + _isMeasured = false; + + // TODO: This is not perfect implementation, It don't distinguish between sigle-byte width and double-byte width. + // This implementation is intended to reduce processing when very long characters are entered. + if (useCheapGlyphMeasurement) + { + _isMeasured = true; - InitGlyphWidths(); + var hit = _measure.GetNextCaretCharacterHit(new CharacterHit(0)); + var firstCharWid = _measure.GetDistanceFromCharacterHit(hit); + + for (var i = 0; i < Count; ++i) + _width[i] = firstCharWid; + } } - double GetAt(int index) + + public double this[int index] { - if (_glyphWidths.Length == 0) - return 0; + get + { + if (!_isMeasured) + { + var hit = new CharacterHit(0); + double prevPos = 0; + for (var i = 0; i < Count; ++i) + { + hit = _measure.GetNextCaretCharacterHit(hit); + var dist = _measure.GetDistanceFromCharacterHit(hit); + _width[i] = dist - prevPos; - if (_range[index] == '\t') - return _tabSize; + i = Math.Max(i, hit.FirstCharacterIndex - 1); + prevPos = dist; + } - if (_glyphWidths[index] == NOT_CALCULATED_YET) - _glyphWidths[index] = MeasureGlyphAt(index); + _isMeasured = true; + } - return _glyphWidths[index]; + return _width[index]; + } } - double MeasureGlyphAt(int index) + public int Count { get; } + + public IEnumerator GetEnumerator() { - return _typeFace.GetGlyphAdvance( - _typeFace.GetGlyph(_range[index])) * _scale; + for (int i = 0; i < Count; ++i) + yield return this[i]; } - void InitGlyphWidths() - { - int capacity = _range.Length; + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } - bool useCheapGlyphMeasurement = - capacity >= VisualLine.LENGTH_LIMIT && - _typeFace.IsFixedPitch; + class TextRunImpl : TextRun + { + public sealed override StringRange StringRange { get; } - if (useCheapGlyphMeasurement) - { - double size = MeasureGlyphAt(0); - _glyphWidths = Enumerable.Repeat(size, capacity).ToArray(); - return; - } + public sealed override int Length { get; } - _glyphWidths = Enumerable.Repeat(NOT_CALCULATED_YET, capacity).ToArray(); - } + public sealed override TextRunProperties Properties { get; } - IEnumerator IEnumerable.GetEnumerator() - { - foreach (double value in _glyphWidths) - yield return value; - } - IEnumerator IEnumerable.GetEnumerator() + public TextRunImpl(StringRange stringRange, TextRunProperties textRunProperties) { - return _glyphWidths.GetEnumerator(); + StringRange = stringRange; + Length = stringRange.Length; + Properties = textRunProperties; } } } -} \ No newline at end of file +} diff --git a/src/AvaloniaEdit/Utils/TextFormatterFactory.cs b/src/AvaloniaEdit/Utils/TextFormatterFactory.cs index 6cf798be..fed01deb 100644 --- a/src/AvaloniaEdit/Utils/TextFormatterFactory.cs +++ b/src/AvaloniaEdit/Utils/TextFormatterFactory.cs @@ -28,39 +28,48 @@ namespace AvaloniaEdit.Utils /// Creates TextFormatter instances that with the correct TextFormattingMode, if running on .NET 4.0. /// public static class TextFormatterFactory - { - public static TextFormatter Create() - { - return new TextFormatter(); - } + { + public static TextFormatter Create() + { + return new TextFormatter(); + } - /// - /// Creates formatted text. - /// - /// The owner element. The text formatter setting are read from this element. - /// The text. - /// The typeface to use. If this parameter is null, the typeface of the will be used. - /// The font size. If this parameter is null, the font size of the will be used. - /// The foreground color. If this parameter is null, the foreground of the will be used. - /// A FormattedText object using the specified settings. - public static FormattedText CreateFormattedText(Control element, string text, FontFamily typeface, double? emSize, IBrush foreground) - { - if (element == null) - throw new ArgumentNullException(nameof(element)); - if (text == null) - throw new ArgumentNullException(nameof(text)); - if (typeface == null) - typeface = TextBlock.GetFontFamily(element); - if (emSize == null) - emSize = TextBlock.GetFontSize(element); - if (foreground == null) - foreground = TextBlock.GetForeground(element); + public static Avalonia.Media.TextFormatting.TextLine CreateTextLine( + string text, + Typeface typeface, + double fontSize, + IBrush foreground) + { + return new Avalonia.Media.TextFormatting.TextLayout(text, typeface, fontSize, foreground).TextLines[0]; + } - var formattedText = new FormattedText(text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, - new Typeface(typeface.Name), emSize.Value, foreground!); - + /// + /// Creates text line. + /// + /// The owner element. The text formatter setting are read from this element. + /// The text. + /// The typeface to use. If this parameter is null, the typeface of the will be used. + /// The font size. If this parameter is null, the font size of the will be used. + /// The foreground color. If this parameter is null, the foreground of the will be used. + /// A FormattedText object using the specified settings. + public static Avalonia.Media.TextFormatting.TextLine CreateTextLine( + Control element, + string text, + FontFamily typeface, double? emSize, + IBrush foreground) + { + if (element == null) + throw new ArgumentNullException(nameof(element)); + if (text == null) + throw new ArgumentNullException(nameof(text)); + if (typeface == null) + typeface = TextBlock.GetFontFamily(element); + if (emSize == null) + emSize = TextBlock.GetFontSize(element); + if (foreground == null) + foreground = TextBlock.GetForeground(element); - return formattedText; - } - } + return CreateTextLine(text, new Typeface(typeface.Name), emSize.Value, foreground!); + } + } }