From 9a84b61b28296ba88e30e3711536d74addc606a3 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 7 Mar 2022 12:51:12 +0100 Subject: [PATCH 01/24] Start porting to Avalonia's TextFormatter --- .../AvaloniaEdit.Demo.csproj | 2 +- .../GenericLineTransformer.cs | 13 +- src/AvaloniaEdit/AvaloniaEdit.csproj | 2 +- .../Editing/CaretNavigationCommandHandler.cs | 8 +- src/AvaloniaEdit/Editing/LineNumberMargin.cs | 30 ++- .../Editing/SelectionColorizer.cs | 2 +- .../Folding/FoldingElementGenerator.cs | 5 +- src/AvaloniaEdit/Folding/FoldingMargin.cs | 1 + .../Highlighting/HighlightingColorizer.cs | 39 ++- .../Rendering/BackgroundGeometryBuilder.cs | 2 +- .../Rendering/FormattedTextElement.cs | 30 +-- .../Rendering/FormattedTextExtensions.cs | 18 +- .../Rendering/ITextRunConstructionContext.cs | 80 +++++- src/AvaloniaEdit/Rendering/InlineObjectRun.cs | 8 +- .../Rendering/LinkElementGenerator.cs | 4 +- .../Rendering/SimpleTextSource.cs | 16 +- .../SingleCharacterElementGenerator.cs | 18 +- src/AvaloniaEdit/Rendering/TextView.cs | 77 +++--- .../Rendering/TextViewCachedElements.cs | 9 +- src/AvaloniaEdit/Rendering/VisualLine.cs | 30 ++- .../Rendering/VisualLineElement.cs | 15 +- .../Rendering/VisualLineLinkText.cs | 6 +- src/AvaloniaEdit/Rendering/VisualLineText.cs | 14 +- .../Rendering/VisualLineTextSource.cs | 29 ++- src/AvaloniaEdit/Text/StringRange.cs | 78 +----- src/AvaloniaEdit/Text/TextCharacters.cs | 25 -- src/AvaloniaEdit/Text/TextEmbeddedObject.cs | 1 + src/AvaloniaEdit/Text/TextEndOfLine.cs | 19 +- src/AvaloniaEdit/Text/TextEndOfParagraph.cs | 10 - src/AvaloniaEdit/Text/TextFormatter.cs | 7 - src/AvaloniaEdit/Text/TextLine.cs | 16 +- src/AvaloniaEdit/Text/TextLineImpl.cs | 232 +----------------- .../Text/TextParagraphProperties.cs | 11 - src/AvaloniaEdit/Text/TextRun.cs | 7 - src/AvaloniaEdit/Text/TextRunExtensions.cs | 11 +- src/AvaloniaEdit/Text/TextRunProperties.cs | 89 ------- src/AvaloniaEdit/Text/TextSource.cs | 5 +- .../Utils/TextFormatterFactory.cs | 55 +++-- .../AvaloniaEdit.Tests.csproj | 2 +- 39 files changed, 343 insertions(+), 683 deletions(-) diff --git a/src/AvaloniaEdit.Demo/AvaloniaEdit.Demo.csproj b/src/AvaloniaEdit.Demo/AvaloniaEdit.Demo.csproj index 9f886662..85cd21b9 100644 --- a/src/AvaloniaEdit.Demo/AvaloniaEdit.Demo.csproj +++ b/src/AvaloniaEdit.Demo/AvaloniaEdit.Demo.csproj @@ -30,7 +30,7 @@ - + diff --git a/src/AvaloniaEdit.TextMate/GenericLineTransformer.cs b/src/AvaloniaEdit.TextMate/GenericLineTransformer.cs index 1aa95f18..24c10d59 100644 --- a/src/AvaloniaEdit.TextMate/GenericLineTransformer.cs +++ b/src/AvaloniaEdit.TextMate/GenericLineTransformer.cs @@ -77,18 +77,21 @@ void ChangeVisualLine( bool isUnderline) { if (foreground != null) - visualLine.TextRunProperties.ForegroundBrush = foreground; + visualLine.TextRunProperties.SetForegroundBrush(foreground); if (background != null) - visualLine.TextRunProperties.BackgroundBrush = background; + visualLine.TextRunProperties.SetBackgroundBrush(background); - visualLine.TextRunProperties.Underline = isUnderline; + if (isUnderline) + { + visualLine.TextRunProperties.SetTextDecorations(TextDecorations.Underline); + } if (visualLine.TextRunProperties.Typeface.Style != fontStyle || visualLine.TextRunProperties.Typeface.Weight != fontWeigth) { - visualLine.TextRunProperties.Typeface = new Typeface( - visualLine.TextRunProperties.Typeface.FontFamily, fontStyle, fontWeigth); + visualLine.TextRunProperties.SetTypeface(new Typeface( + visualLine.TextRunProperties.Typeface.FontFamily, fontStyle, fontWeigth)); } } diff --git a/src/AvaloniaEdit/AvaloniaEdit.csproj b/src/AvaloniaEdit/AvaloniaEdit.csproj index 9a527395..a470f09c 100644 --- a/src/AvaloniaEdit/AvaloniaEdit.csproj +++ b/src/AvaloniaEdit/AvaloniaEdit.csproj @@ -33,7 +33,7 @@ - + diff --git a/src/AvaloniaEdit/Editing/CaretNavigationCommandHandler.cs b/src/AvaloniaEdit/Editing/CaretNavigationCommandHandler.cs index 8797008c..7955f92a 100644 --- a/src/AvaloniaEdit/Editing/CaretNavigationCommandHandler.cs +++ b/src/AvaloniaEdit/Editing/CaretNavigationCommandHandler.cs @@ -26,6 +26,8 @@ using AvaloniaEdit.Utils; using Avalonia.Input; using Avalonia.Input.Platform; +using Avalonia.Media.TextFormatting; +using LogicalDirection = AvaloniaEdit.Document.LogicalDirection; namespace AvaloniaEdit.Editing { @@ -284,7 +286,7 @@ private static TextViewPosition GetStartOfLineCaretPosition(int oldVisualColumn, private static TextViewPosition GetEndOfLineCaretPosition(VisualLine visualLine, TextLine textLine) { - var newVisualCol = visualLine.GetTextLineVisualStartColumn(textLine) + textLine.Length - textLine.TrailingWhitespaceLength; + var newVisualCol = visualLine.GetTextLineVisualStartColumn(textLine) + textLine.TextRange.Length - textLine.TrailingWhitespaceLength; var pos = visualLine.GetTextViewPosition(newVisualCol); pos.IsAtEndOfLine = true; return pos; @@ -429,10 +431,10 @@ private static TextViewPosition GetUpDownCaretPosition(TextView textView, TextVi // prevent wrapping to the next line; TODO: could 'IsAtEnd' help here? var targetLineStartCol = targetVisualLine.GetTextLineVisualStartColumn(targetLine); - if (newVisualColumn >= targetLineStartCol + targetLine.Length) + if (newVisualColumn >= targetLineStartCol + targetLine.TextRange.Length) { if (newVisualColumn <= targetVisualLine.VisualLength) - newVisualColumn = targetLineStartCol + targetLine.Length - 1; + newVisualColumn = targetLineStartCol + targetLine.TextRange.Length - 1; } return targetVisualLine.GetTextViewPosition(newVisualColumn); } diff --git a/src/AvaloniaEdit/Editing/LineNumberMargin.cs b/src/AvaloniaEdit/Editing/LineNumberMargin.cs index 35e1c1f9..373e3861 100644 --- a/src/AvaloniaEdit/Editing/LineNumberMargin.cs +++ b/src/AvaloniaEdit/Editing/LineNumberMargin.cs @@ -18,6 +18,7 @@ using System; using System.Globalization; +using System.Linq; using Avalonia; using AvaloniaEdit.Document; using AvaloniaEdit.Rendering; @@ -41,7 +42,7 @@ public class LineNumberMargin : AbstractMargin /// The typeface used for rendering the line number margin. /// This field is calculated in MeasureOverride() based on the FontFamily etc. properties. /// - protected FontFamily Typeface { get; set; } + protected Typeface Typeface { get; set; } /// /// The font size used for rendering the line number margin. @@ -52,17 +53,16 @@ public class LineNumberMargin : AbstractMargin /// protected override Size MeasureOverride(Size availableSize) { - Typeface = GetValue(TextBlock.FontFamilyProperty); + Typeface = new Typeface(GetValue(TextBlock.FontFamilyProperty)); EmSize = GetValue(TextBlock.FontSizeProperty); - var text = TextFormatterFactory.CreateFormattedText( - this, - new string('9', MaxLineNumberLength), + var textLine = TextFormatterFactory.FormatLine(Enumerable.Repeat('9', MaxLineNumberLength).ToArray(), Typeface, EmSize, GetValue(TemplatedControl.ForegroundProperty) ); - return new Size(text.Bounds.Width, 0); + + return new Size(textLine.WidthIncludingTrailingWhitespace, textLine.Height); } /// @@ -70,20 +70,24 @@ public override void Render(DrawingContext drawingContext) { var textView = TextView; var renderSize = Bounds.Size; + if (textView != null && textView.VisualLinesValid) { var foreground = GetValue(TemplatedControl.ForegroundProperty); + foreach (var line in textView.VisualLines) { var lineNumber = line.FirstDocumentLine.LineNumber; - var text = TextFormatterFactory.CreateFormattedText( - this, - lineNumber.ToString(CultureInfo.CurrentCulture), - Typeface, EmSize, foreground + var text = lineNumber.ToString(CultureInfo.CurrentCulture); + var textLine = TextFormatterFactory.FormatLine(text.AsMemory(), + Typeface, + EmSize, + GetValue(TemplatedControl.ForegroundProperty) ); + var y = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.TextTop); - drawingContext.DrawText(foreground, new Point(renderSize.Width - text.Bounds.Width, y - textView.VerticalOffset), - text); + + textLine.Draw(drawingContext, new Point(renderSize.Width - textLine.WidthIncludingTrailingWhitespace, y - textView.VerticalOffset)); } } } @@ -193,7 +197,7 @@ private SimpleSegment GetTextLineSegment(PointerEventArgs e) return SimpleSegment.Invalid; var tl = vl.GetTextLineByVisualYPosition(pos.Y); var visualStartColumn = vl.GetTextLineVisualStartColumn(tl); - var visualEndColumn = visualStartColumn + tl.Length; + var visualEndColumn = visualStartColumn + tl.TextRange.Length; var relStart = vl.FirstDocumentLine.Offset; var startOffset = vl.GetRelativeOffset(visualStartColumn) + relStart; var endOffset = vl.GetRelativeOffset(visualEndColumn) + relStart; diff --git a/src/AvaloniaEdit/Editing/SelectionColorizer.cs b/src/AvaloniaEdit/Editing/SelectionColorizer.cs index a82f6a8f..bfc1d622 100644 --- a/src/AvaloniaEdit/Editing/SelectionColorizer.cs +++ b/src/AvaloniaEdit/Editing/SelectionColorizer.cs @@ -63,7 +63,7 @@ protected override void Colorize(ITextRunConstructionContext context) startColumn, endColumn, element => { - element.TextRunProperties.ForegroundBrush = _textArea.SelectionForeground; + element.TextRunProperties.SetForegroundBrush(_textArea.SelectionForeground); }); } } diff --git a/src/AvaloniaEdit/Folding/FoldingElementGenerator.cs b/src/AvaloniaEdit/Folding/FoldingElementGenerator.cs index 0b570aa5..602d9765 100644 --- a/src/AvaloniaEdit/Folding/FoldingElementGenerator.cs +++ b/src/AvaloniaEdit/Folding/FoldingElementGenerator.cs @@ -24,6 +24,7 @@ using AvaloniaEdit.Utils; using Avalonia.Input; using Avalonia.Media; +using Avalonia.Media.TextFormatting; namespace AvaloniaEdit.Folding { @@ -154,8 +155,8 @@ public override VisualLineElement ConstructElement(int offset) if (string.IsNullOrEmpty(title)) title = "..."; var p = CurrentContext.GlobalTextRunProperties.Clone(); - p.ForegroundBrush = TextBrush; - var textFormatter = TextFormatterFactory.Create(); + p.SetForegroundBrush(TextBrush); + var textFormatter = TextFormatter.Current; var text = FormattedTextElement.PrepareText(textFormatter, title, p); return new FoldingLineElement(foldingSection, text, foldedUntil - offset, TextBrush); } diff --git a/src/AvaloniaEdit/Folding/FoldingMargin.cs b/src/AvaloniaEdit/Folding/FoldingMargin.cs index f1978428..3db00642 100644 --- a/src/AvaloniaEdit/Folding/FoldingMargin.cs +++ b/src/AvaloniaEdit/Folding/FoldingMargin.cs @@ -26,6 +26,7 @@ using AvaloniaEdit.Utils; using Avalonia.Controls; using Avalonia.Media; +using Avalonia.Media.TextFormatting; namespace AvaloniaEdit.Folding { diff --git a/src/AvaloniaEdit/Highlighting/HighlightingColorizer.cs b/src/AvaloniaEdit/Highlighting/HighlightingColorizer.cs index 44b05f88..4fdcefe0 100644 --- a/src/AvaloniaEdit/Highlighting/HighlightingColorizer.cs +++ b/src/AvaloniaEdit/Highlighting/HighlightingColorizer.cs @@ -18,6 +18,7 @@ using System; using System.Diagnostics; +using Avalonia.Media; using AvaloniaEdit.Document; using AvaloniaEdit.Rendering; using AvaloniaEdit.Utils; @@ -260,7 +261,7 @@ internal static void ApplyColorToElement(VisualLineElement element, Highlighting { var b = color.Foreground.GetBrush(context); if (b != null) - element.TextRunProperties.ForegroundBrush = b; + element.TextRunProperties.SetForegroundBrush(b); } if (color.Background != null) { @@ -271,18 +272,44 @@ internal static void ApplyColorToElement(VisualLineElement element, Highlighting if (color.FontStyle != null || color.FontWeight != null || color.FontFamily != null) { var tf = element.TextRunProperties.Typeface; - element.TextRunProperties.Typeface = new Avalonia.Media.Typeface( + element.TextRunProperties.SetTypeface(new Avalonia.Media.Typeface( color.FontFamily ?? tf.FontFamily, color.FontStyle ?? tf.Style, - color.FontWeight ?? tf.Weight + color.FontWeight ?? tf.Weight) ); } if (color.FontSize.HasValue) - element.TextRunProperties.FontSize = color.FontSize.Value; + element.TextRunProperties.SetFontSize(color.FontSize.Value); + if (color.Underline ?? false) - element.TextRunProperties.Underline = true; + { + element.TextRunProperties.SetTextDecorations(new TextDecorationCollection{new() + { + Location = TextDecorationLocation.Underline + }}); + } + if (color.Strikethrough ?? false) - element.TextRunProperties.Strikethrough = true; + { + if (element.TextRunProperties.TextDecorations != null) + { + element.TextRunProperties.TextDecorations.Add(new TextDecoration + { + Location = TextDecorationLocation.Strikethrough + }); + } + else + { + element.TextRunProperties.SetTextDecorations(new TextDecorationCollection + { + new() + { + Location = TextDecorationLocation.Strikethrough + } + }); + } + + } } /// diff --git a/src/AvaloniaEdit/Rendering/BackgroundGeometryBuilder.cs b/src/AvaloniaEdit/Rendering/BackgroundGeometryBuilder.cs index c849ff89..1d25abba 100644 --- a/src/AvaloniaEdit/Rendering/BackgroundGeometryBuilder.cs +++ b/src/AvaloniaEdit/Rendering/BackgroundGeometryBuilder.cs @@ -188,7 +188,7 @@ private static IEnumerable ProcessTextLines(TextView textView, VisualLine var line = visualLine.TextLines[i]; var y = visualLine.GetTextLineVisualYPosition(line, VisualYPosition.LineTop); var visualStartCol = visualLine.GetTextLineVisualStartColumn(line); - var visualEndCol = visualStartCol + line.Length; + var visualEndCol = visualStartCol + line.TextRange.Length; if (line == lastTextLine) visualEndCol -= 1; // 1 position for the TextEndOfParagraph // TODO: ? diff --git a/src/AvaloniaEdit/Rendering/FormattedTextElement.cs b/src/AvaloniaEdit/Rendering/FormattedTextElement.cs index 0dff304d..c0db4344 100644 --- a/src/AvaloniaEdit/Rendering/FormattedTextElement.cs +++ b/src/AvaloniaEdit/Rendering/FormattedTextElement.cs @@ -19,8 +19,10 @@ using System; using Avalonia; using Avalonia.Media; +using Avalonia.Media.TextFormatting; using AvaloniaEdit.Text; using AvaloniaEdit.Utils; +using JetBrains.Annotations; namespace AvaloniaEdit.Rendering { @@ -63,11 +65,12 @@ public FormattedTextElement(FormattedText text, int documentLength) : base(1, do } /// + [CanBeNull] public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) { if (TextLine == null) { - var formatter = TextFormatterFactory.Create(); + var formatter = TextFormatter.Current; TextLine = PrepareText(formatter, Text, TextRunProperties); Text = null; } @@ -86,15 +89,14 @@ internal static TextLine PrepareText(TextFormatter formatter, string text, TextR if (properties == null) throw new ArgumentNullException(nameof(properties)); return formatter.FormatLine( - new SimpleTextSource(text, properties), + new SimpleTextSource(text.AsMemory(), properties), 0, 32000, - new TextParagraphProperties - { - DefaultTextRunProperties = properties, - TextWrapping = TextWrapping.NoWrap, - DefaultIncrementalTab = 40 - }); + + //DefaultIncrementalTab = 40 + + new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true, false, + properties, TextWrapping.NoWrap, 0, 0)); } } @@ -118,10 +120,7 @@ public FormattedTextRun(FormattedTextElement element, TextRunProperties properti public FormattedTextElement Element { get; } /// - public override StringRange StringRange => default(StringRange); - - /// - public override int Length => Element.VisualLength; + public override int TextSourceLength => Element.VisualLength; /// public override bool HasFixedSize => true; @@ -132,11 +131,14 @@ public FormattedTextRun(FormattedTextElement element, TextRunProperties properti public override Size GetSize(double remainingParagraphWidth) { var formattedText = Element.FormattedText; + if (formattedText != null) { - return formattedText.Bounds.Size; + return new Size(formattedText.WidthIncludingTrailingWhitespace, formattedText.Height); } + var text = Element.TextLine; + return new Size(text.WidthIncludingTrailingWhitespace, text.Height); } @@ -153,7 +155,7 @@ public override void Draw(DrawingContext drawingContext, Point origin) if (Element.FormattedText != null) { //origin = origin.WithY(origin.Y - Element.formattedText.Baseline); - drawingContext.DrawText(null, origin, Element.FormattedText); + drawingContext.DrawText(Element.FormattedText, origin); } else { diff --git a/src/AvaloniaEdit/Rendering/FormattedTextExtensions.cs b/src/AvaloniaEdit/Rendering/FormattedTextExtensions.cs index c5cd39f0..2ea559d8 100644 --- a/src/AvaloniaEdit/Rendering/FormattedTextExtensions.cs +++ b/src/AvaloniaEdit/Rendering/FormattedTextExtensions.cs @@ -2,21 +2,5 @@ { using Avalonia.Media; using System.Collections.Generic; - - public static class FormattedTextExtensions - { - public static void SetTextStyle(this FormattedText text, int startIndex, int length, IBrush foreground = null) - { - var spans = new List(); - - if (text.Spans != null) - { - spans.AddRange(text.Spans); - } - - spans.Add(new FormattedTextStyleSpan(startIndex, length, foreground)); - - text.Spans = spans; - } - } + } diff --git a/src/AvaloniaEdit/Rendering/ITextRunConstructionContext.cs b/src/AvaloniaEdit/Rendering/ITextRunConstructionContext.cs index fdccb584..8ddeb3b8 100644 --- a/src/AvaloniaEdit/Rendering/ITextRunConstructionContext.cs +++ b/src/AvaloniaEdit/Rendering/ITextRunConstructionContext.cs @@ -16,10 +16,15 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +using System.Globalization; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.Utilities; using AvaloniaEdit.Document; -using AvaloniaEdit.Text; using AvaloniaEdit.Utils; +#nullable enable + namespace AvaloniaEdit.Rendering { /// @@ -45,7 +50,7 @@ public interface ITextRunConstructionContext /// /// Gets the global text run properties. /// - TextRunProperties GlobalTextRunProperties { get; } + CustomTextRunProperties GlobalTextRunProperties { get; } /// /// Gets a piece of text from the document. @@ -56,6 +61,75 @@ public interface ITextRunConstructionContext /// This method should be the preferred text access method in the text transformation pipeline, as it can avoid repeatedly allocating string instances /// for text within the same line. /// - StringSegment GetText(int offset, int length); + ReadOnlySlice GetText(int offset, int length); + } + + public class CustomTextRunProperties : TextRunProperties + { + private Typeface _typeface; + private double _fontRenderingEmSize; + private TextDecorationCollection? _textDecorations; + private IBrush? _foregroundBrush; + private IBrush? _backgroundBrush; + private CultureInfo? _cultureInfo; + private BaselineAlignment _baselineAlignment; + + internal CustomTextRunProperties(Typeface typeface, double fontRenderingEmSize, + TextDecorationCollection? textDecorations, IBrush? foregroundBrush, IBrush? backgroundBrush, + CultureInfo? cultureInfo, BaselineAlignment baselineAlignment) + { + _typeface = typeface; + _fontRenderingEmSize = fontRenderingEmSize; + _textDecorations = textDecorations; + _foregroundBrush = foregroundBrush; + _backgroundBrush = backgroundBrush; + _cultureInfo = cultureInfo; + _baselineAlignment = baselineAlignment; + } + + public override Typeface Typeface => _typeface; + + public override double FontRenderingEmSize => _fontRenderingEmSize; + + public override TextDecorationCollection? TextDecorations => _textDecorations; + + public override IBrush? ForegroundBrush => _foregroundBrush; + + public override IBrush? BackgroundBrush => _backgroundBrush; + + public override CultureInfo? CultureInfo => _cultureInfo; + + public override BaselineAlignment BaselineAlignment => _baselineAlignment; + + public CustomTextRunProperties Clone() + { + return new CustomTextRunProperties(Typeface, FontRenderingEmSize, TextDecorations, ForegroundBrush, + BackgroundBrush, CultureInfo, BaselineAlignment); + } + + public void SetForegroundBrush(IBrush foregroundBrush) + { + _foregroundBrush = foregroundBrush; + } + + public void SetBackgroundBrush(IBrush backgroundBrush) + { + _backgroundBrush = backgroundBrush; + } + + public void SetTypeface(Typeface typeface) + { + _typeface = typeface; + } + + public void SetFontSize(int colorFontSize) + { + _fontRenderingEmSize = colorFontSize; + } + + public void SetTextDecorations(TextDecorationCollection textDecorations) + { + _textDecorations = textDecorations; + } } } diff --git a/src/AvaloniaEdit/Rendering/InlineObjectRun.cs b/src/AvaloniaEdit/Rendering/InlineObjectRun.cs index 3c58f542..04a65bfa 100644 --- a/src/AvaloniaEdit/Rendering/InlineObjectRun.cs +++ b/src/AvaloniaEdit/Rendering/InlineObjectRun.cs @@ -20,6 +20,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Media; +using Avalonia.Media.TextFormatting; using AvaloniaEdit.Text; namespace AvaloniaEdit.Rendering @@ -73,7 +74,7 @@ public InlineObjectRun(int length, TextRunProperties properties, IControl elemen if (length <= 0) throw new ArgumentOutOfRangeException(nameof(length), length, "Value must be positive"); - Length = length; + TextSourceLength = length; Properties = properties ?? throw new ArgumentNullException(nameof(properties)); Element = element ?? throw new ArgumentNullException(nameof(element)); } @@ -93,10 +94,7 @@ public InlineObjectRun(int length, TextRunProperties properties, IControl elemen public override bool HasFixedSize => true; /// - public override StringRange StringRange => default(StringRange); - - /// - public override int Length { get; } + public override int TextSourceLength { get; } /// public override TextRunProperties Properties { get; } diff --git a/src/AvaloniaEdit/Rendering/LinkElementGenerator.cs b/src/AvaloniaEdit/Rendering/LinkElementGenerator.cs index 4a08e583..cefb8504 100644 --- a/src/AvaloniaEdit/Rendering/LinkElementGenerator.cs +++ b/src/AvaloniaEdit/Rendering/LinkElementGenerator.cs @@ -73,8 +73,8 @@ private Match GetMatch(int startOffset, out int matchOffset) { var endOffset = CurrentContext.VisualLine.LastDocumentLine.EndOffset; var relevantText = CurrentContext.GetText(startOffset, endOffset - startOffset); - var m = _linkRegex.Match(relevantText.Text, relevantText.Offset, relevantText.Count); - matchOffset = m.Success ? m.Index - relevantText.Offset + startOffset : -1; + var m = _linkRegex.Match(relevantText.Span.ToString()); + matchOffset = m.Success ? m.Index - relevantText.Start + startOffset : -1; return m; } diff --git a/src/AvaloniaEdit/Rendering/SimpleTextSource.cs b/src/AvaloniaEdit/Rendering/SimpleTextSource.cs index 22500e8a..7f5ab10b 100644 --- a/src/AvaloniaEdit/Rendering/SimpleTextSource.cs +++ b/src/AvaloniaEdit/Rendering/SimpleTextSource.cs @@ -16,25 +16,29 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -using AvaloniaEdit.Text; +using Avalonia.Media.TextFormatting; +using Avalonia.Utilities; namespace AvaloniaEdit.Rendering { - internal sealed class SimpleTextSource : TextSource + internal sealed class SimpleTextSource : ITextSource { - private readonly string _text; + private readonly ReadOnlySlice _text; private readonly TextRunProperties _properties; - public SimpleTextSource(string text, TextRunProperties properties) + public SimpleTextSource(ReadOnlySlice text, TextRunProperties properties) { _text = text; _properties = properties; } - public override TextRun GetTextRun(int characterIndex) + public TextRun GetTextRun(int characterIndex) { - if (characterIndex < _text.Length) + if (characterIndex < _text.Length) + { return new TextCharacters(_text, characterIndex, _text.Length - characterIndex, _properties); + } + return new TextEndOfParagraph(1); } } diff --git a/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs b/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs index 885744b2..3f4b72aa 100644 --- a/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs +++ b/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs @@ -21,9 +21,11 @@ using Avalonia; using Avalonia.Media; using Avalonia.Media.Immutable; +using Avalonia.Media.TextFormatting; using AvaloniaEdit.Document; using AvaloniaEdit.Text; using AvaloniaEdit.Utils; +using LogicalDirection = AvaloniaEdit.Document.LogicalDirection; namespace AvaloniaEdit.Rendering { @@ -76,9 +78,9 @@ public override int GetFirstInterestedOffset(int startOffset) var endLine = CurrentContext.VisualLine.LastDocumentLine; var relevantText = CurrentContext.GetText(startOffset, endLine.EndOffset - startOffset); - for (var i = 0; i < relevantText.Count; i++) + for (var i = 0; i < relevantText.Length; i++) { - var c = relevantText.Text[relevantText.Offset + i]; + var c = relevantText[i]; switch (c) { case ' ': @@ -114,8 +116,10 @@ public override VisualLineElement ConstructElement(int offset) if (ShowBoxForControlCharacters && char.IsControl(c)) { var p = CurrentContext.GlobalTextRunProperties.Clone(); - p.ForegroundBrush = Brushes.White; - var textFormatter = TextFormatterFactory.Create(); + + p.SetForegroundBrush(Brushes.White); + + var textFormatter = TextFormatter.Current; var text = FormattedTextElement.PrepareText(textFormatter, TextUtilities.GetControlCharacterName(c), p); return new SpecialCharacterBoxElement(text); @@ -158,7 +162,7 @@ public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructio if (startVisualColumn == VisualColumn) return new TabGlyphRun(this, TextRunProperties); if (startVisualColumn == VisualColumn + 1) - return new TextCharacters("\t", TextRunProperties); + return new TextCharacters("\t".AsMemory(), TextRunProperties); throw new ArgumentOutOfRangeException(nameof(startVisualColumn)); } @@ -187,9 +191,7 @@ public TabGlyphRun(TabTextElement element, TextRunProperties properties) public override bool HasFixedSize => true; - public override StringRange StringRange => default(StringRange); - - public override int Length => 1; + public override int TextSourceLength => 1; public override TextRunProperties Properties { get; } diff --git a/src/AvaloniaEdit/Rendering/TextView.cs b/src/AvaloniaEdit/Rendering/TextView.cs index 73a6591a..ce693d2a 100644 --- a/src/AvaloniaEdit/Rendering/TextView.cs +++ b/src/AvaloniaEdit/Rendering/TextView.cs @@ -32,6 +32,7 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; +using Avalonia.Media.TextFormatting; using Avalonia.Threading; using Avalonia.VisualTree; using AvaloniaEdit.Document; @@ -142,7 +143,7 @@ private void OnDocumentChanged(TextDocument oldValue, TextDocument newValue) if (newValue != null) { TextDocumentWeakEventManager.Changing.AddHandler(newValue, OnChanging); - _formatter = TextFormatterFactory.Create(); + _formatter = TextFormatter.Current; InvalidateDefaultTextMetrics(); // measuring DefaultLineHeight depends on formatter _heightTree = new HeightTree(newValue, DefaultLineHeight); CachedElements = new TextViewCachedElements(); @@ -1058,30 +1059,41 @@ private double CreateAndMeasureVisualLines(Size availableSize) private TextFormatter _formatter; internal TextViewCachedElements CachedElements; - private TextRunProperties CreateGlobalTextRunProperties() - { - var properties = new TextRunProperties - { - FontSize = FontSize, - Typeface = new Typeface(TextBlock.GetFontFamily(this), TextBlock.GetFontStyle(this), TextBlock.GetFontWeight(this)), - ForegroundBrush = TextBlock.GetForeground(this), - CultureInfo = CultureInfo.CurrentCulture - }; + private CustomTextRunProperties CreateGlobalTextRunProperties() + { + var properties = new CustomTextRunProperties + ( + new Typeface(TextBlock.GetFontFamily(this), TextBlock.GetFontStyle(this), + TextBlock.GetFontWeight(this)), + FontSize, + null, + TextBlock.GetForeground(this), + null, + cultureInfo: CultureInfo.CurrentCulture, + BaselineAlignment.Baseline + ); + return properties; } - private TextParagraphProperties CreateParagraphProperties(TextRunProperties defaultTextRunProperties) + private GenericTextParagraphProperties CreateParagraphProperties(TextRunProperties defaultTextRunProperties) { - return new TextParagraphProperties - { - DefaultTextRunProperties = defaultTextRunProperties, - TextWrapping = _canHorizontallyScroll ? TextWrapping.NoWrap : TextWrapping.Wrap, - DefaultIncrementalTab = Options.IndentationSize * WideSpaceWidth - }; + return new GenericTextParagraphProperties + ( + FlowDirection.LeftToRight, + TextAlignment.Left, + true, + false, + defaultTextRunProperties, + _canHorizontallyScroll ? TextWrapping.NoWrap : TextWrapping.Wrap, + 0, + 0/*, + DefaultIncrementalTab = Options.IndentationSize * WideSpaceWidth*/ + ); } private VisualLine BuildVisualLine(DocumentLine documentLine, - TextRunProperties globalTextRunProperties, + CustomTextRunProperties globalTextRunProperties, TextParagraphProperties paragraphProperties, VisualLineElementGenerator[] elementGeneratorsArray, IVisualLineTransformer[] lineTransformersArray, @@ -1091,6 +1103,7 @@ private VisualLine BuildVisualLine(DocumentLine documentLine, throw new InvalidOperationException("Trying to build visual line from collapsed line"); var visualLine = new VisualLine(this, documentLine); + var textSource = new VisualLineTextSource(visualLine) { Document = _document, @@ -1121,8 +1134,6 @@ private VisualLine BuildVisualLine(DocumentLine documentLine, // now construct textLines: var textOffset = 0; var textLines = new List(); - paragraphProperties.Indent = 0; - paragraphProperties.FirstLineInParagraph = true; while (textOffset <= visualLine.VisualLengthWithEndOfLineMarker) { var textLine = _formatter.FormatLine( @@ -1132,7 +1143,7 @@ private VisualLine BuildVisualLine(DocumentLine documentLine, paragraphProperties ); textLines.Add(textLine); - textOffset += textLine.Length; + textOffset += textLine.TextRange.Length; // exit loop so that we don't do the indentation calculation if there's only a single line if (textOffset >= visualLine.VisualLengthWithEndOfLineMarker) @@ -1140,7 +1151,7 @@ private VisualLine BuildVisualLine(DocumentLine documentLine, if (paragraphProperties.FirstLineInParagraph) { - paragraphProperties.FirstLineInParagraph = false; + //paragraphProperties.FirstLineInParagraph = false; var options = Options; double indentation = 0; @@ -1150,13 +1161,15 @@ private VisualLine BuildVisualLine(DocumentLine documentLine, var indentVisualColumn = GetIndentationVisualColumn(visualLine); if (indentVisualColumn > 0 && indentVisualColumn < textOffset) { - indentation = textLine.GetDistanceFromCharacter(indentVisualColumn, 0); + indentation = textLine.GetDistanceFromCharacterHit(new CharacterHit(indentVisualColumn)); } } indentation += options.WordWrapIndentation; // apply the calculated indentation unless it's more than half of the text editor size: if (indentation > 0 && indentation * 2 < availableSize.Width) - paragraphProperties.Indent = indentation; + { + //paragraphProperties.Indent = indentation; + } } } visualLine.SetTextLines(textLines); @@ -1226,16 +1239,20 @@ protected override Size ArrangeOverride(Size finalSize) var offset = 0; foreach (var textLine in visualLine.TextLines) { - foreach (var span in textLine.GetTextRuns()) + foreach (var span in textLine.TextRuns) { var inline = span as InlineObjectRun; + if (inline?.VisualLine != null) { Debug.Assert(_inlineObjects.Contains(inline)); - var distance = textLine.GetDistanceFromCharacter(offset, 0); + + var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(offset)); + inline.Element.Arrange(new Rect(new Point(pos.X + distance, pos.Y), inline.Element.DesiredSize)); } - offset += span.Length; + + offset += span.TextSourceLength; } pos = new Point(pos.X, pos.Y + textLine.Height); } @@ -1516,10 +1533,12 @@ private void CalculateDefaultTextMetrics() if (_formatter != null) { var textRunProperties = CreateGlobalTextRunProperties(); + var line = _formatter.FormatLine( - new SimpleTextSource("x", textRunProperties), + new SimpleTextSource("x".AsMemory(), textRunProperties), 0, 32000, - new TextParagraphProperties { DefaultTextRunProperties = textRunProperties }); + new GenericTextParagraphProperties(textRunProperties)); + _wideSpaceWidth = Math.Max(1, line.WidthIncludingTrailingWhitespace); _defaultBaseline = Math.Max(1, line.Baseline); _defaultLineHeight = Math.Max(1, line.Height); diff --git a/src/AvaloniaEdit/Rendering/TextViewCachedElements.cs b/src/AvaloniaEdit/Rendering/TextViewCachedElements.cs index a6b799d4..d2ea93de 100644 --- a/src/AvaloniaEdit/Rendering/TextViewCachedElements.cs +++ b/src/AvaloniaEdit/Rendering/TextViewCachedElements.cs @@ -17,6 +17,7 @@ // DEALINGS IN THE SOFTWARE. using System.Collections.Generic; +using Avalonia.Media.TextFormatting; using AvaloniaEdit.Text; using AvaloniaEdit.Utils; @@ -40,14 +41,18 @@ public TextLine GetTextForNonPrintableCharacter(string text, ITextRunConstructio } var properties = context.GlobalTextRunProperties.Clone(); - properties.ForegroundBrush = context.TextView.NonPrintableCharacterBrush; + + properties.SetForegroundBrush(context.TextView.NonPrintableCharacterBrush); + if (_formatter == null) { - _formatter = TextFormatterFactory.Create(); + _formatter = TextFormatter.Current; } textLine = FormattedTextElement.PrepareText(_formatter, text, properties); + _nonPrintableCharacterTexts[text] = textLine; + return textLine; } } diff --git a/src/AvaloniaEdit/Rendering/VisualLine.cs b/src/AvaloniaEdit/Rendering/VisualLine.cs index 38ff6c9d..dc03a397 100644 --- a/src/AvaloniaEdit/Rendering/VisualLine.cs +++ b/src/AvaloniaEdit/Rendering/VisualLine.cs @@ -24,9 +24,11 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Media; +using Avalonia.Media.TextFormatting; using AvaloniaEdit.Document; using AvaloniaEdit.Text; using AvaloniaEdit.Utils; +using LogicalDirection = AvaloniaEdit.Document.LogicalDirection; namespace AvaloniaEdit.Rendering { @@ -362,9 +364,9 @@ public TextLine GetTextLine(int visualColumn, bool isAtEndOfLine) return TextLines[TextLines.Count - 1]; foreach (var line in TextLines) { - if (isAtEndOfLine ? visualColumn <= line.Length : visualColumn < line.Length) + if (isAtEndOfLine ? visualColumn <= line.TextRange.Length : visualColumn < line.TextRange.Length) return line; - visualColumn -= line.Length; + visualColumn -= line.TextRange.Length; } throw new InvalidOperationException("Shouldn't happen (VisualLength incorrect?)"); } @@ -416,7 +418,7 @@ public int GetTextLineVisualStartColumn(TextLine textLine) if (!TextLines.Contains(textLine)) throw new ArgumentException("textLine is not a line in this VisualLine"); - return TextLines.TakeWhile(tl => tl != textLine).Sum(tl => tl.Length); + return TextLines.TakeWhile(tl => tl != textLine).Sum(tl => tl.TextRange.Length); } /// @@ -465,12 +467,15 @@ public double GetTextLineVisualXPosition(TextLine textLine, int visualColumn) { if (textLine == null) throw new ArgumentNullException(nameof(textLine)); - var xPos = textLine.GetDistanceFromCharacter( - Math.Min(visualColumn, VisualLengthWithEndOfLineMarker), 0); + + var xPos = textLine.GetDistanceFromCharacterHit(new CharacterHit(Math.Min(visualColumn, + VisualLengthWithEndOfLineMarker))); + if (visualColumn > VisualLengthWithEndOfLineMarker) { xPos += (visualColumn - VisualLengthWithEndOfLineMarker) * _textView.WideSpaceWidth; } + return xPos; } @@ -496,7 +501,7 @@ internal int GetVisualColumn(Point point, bool allowVirtualSpace, out bool isAtE { var textLine = GetTextLineByVisualYPosition(point.Y); var vc = GetVisualColumn(textLine, point.X, allowVirtualSpace); - isAtEndOfLine = (vc >= GetTextLineVisualStartColumn(textLine) + textLine.Length); + isAtEndOfLine = (vc >= GetTextLineVisualStartColumn(textLine) + textLine.TextRange.Length); return vc; } @@ -515,8 +520,9 @@ public int GetVisualColumn(TextLine textLine, double xPos, bool allowVirtualSpac } } - var ch = textLine.GetCharacterFromDistance(xPos); - return ch.firstIndex + ch.trailingLength; + var ch = textLine.GetCharacterHitFromDistance(xPos); + + return ch.FirstCharacterIndex + ch.TrailingLength; } /// @@ -584,12 +590,14 @@ internal int GetVisualColumnFloor(Point point, bool allowVirtualSpace, out bool // GetCharacterHitFromDistance returns a hit with FirstCharacterIndex=last character in line // and TrailingLength=1 when clicking behind the line, so the floor function needs to handle this case // specially and return the line's end column instead. - return GetTextLineVisualStartColumn(textLine) + textLine.Length; + return GetTextLineVisualStartColumn(textLine) + textLine.TextRange.Length; } isAtEndOfLine = false; - var ch = textLine.GetCharacterFromDistance(point.X); - return ch.firstIndex; + + var ch = textLine.GetCharacterHitFromDistance(point.X); + + return ch.FirstCharacterIndex; } /// diff --git a/src/AvaloniaEdit/Rendering/VisualLineElement.cs b/src/AvaloniaEdit/Rendering/VisualLineElement.cs index 4f64a4a5..709abcf3 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineElement.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineElement.cs @@ -23,6 +23,9 @@ using AvaloniaEdit.Text; using Avalonia.Input; using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.Utilities; +using LogicalDirection = AvaloniaEdit.Document.LogicalDirection; namespace AvaloniaEdit.Rendering { @@ -75,14 +78,14 @@ protected VisualLineElement(int visualLength, int documentLength) /// will affect only this /// . /// - public TextRunProperties TextRunProperties { get; private set; } + public CustomTextRunProperties TextRunProperties { get; private set; } /// /// Gets/sets the brush used for the background of this . /// public IBrush BackgroundBrush { get; set; } - internal void SetTextRunProperties(TextRunProperties p) + internal void SetTextRunProperties(CustomTextRunProperties p) { TextRunProperties = p; } @@ -104,9 +107,9 @@ internal void SetTextRunProperties(TextRunProperties p) /// Retrieves the text span immediately before the visual column. /// /// This method is used for word-wrapping in bidirectional text. - public virtual StringRange GetPrecedingText(int visualColumnLimit, ITextRunConstructionContext context) + public virtual ReadOnlySlice GetPrecedingText(int visualColumnLimit, ITextRunConstructionContext context) { - return StringRange.Empty; + return ReadOnlySlice.Empty; } /// @@ -160,9 +163,9 @@ protected void SplitHelper(VisualLineElement firstPart, VisualLineElement second firstPart.DocumentLength = relativeSplitRelativeTextOffset; secondPart.DocumentLength = oldDocumentLength - relativeSplitRelativeTextOffset; if (firstPart.TextRunProperties == null) - firstPart.TextRunProperties = TextRunProperties.Clone(); + firstPart.TextRunProperties = TextRunProperties; if (secondPart.TextRunProperties == null) - secondPart.TextRunProperties = TextRunProperties.Clone(); + secondPart.TextRunProperties = TextRunProperties; firstPart.BackgroundBrush = BackgroundBrush; secondPart.BackgroundBrush = BackgroundBrush; } diff --git a/src/AvaloniaEdit/Rendering/VisualLineLinkText.cs b/src/AvaloniaEdit/Rendering/VisualLineLinkText.cs index 9edb0de3..58d430ba 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineLinkText.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineLinkText.cs @@ -22,6 +22,7 @@ using Avalonia.Interactivity; using Avalonia.Controls; using System.Diagnostics; +using Avalonia.Media.TextFormatting; namespace AvaloniaEdit.Rendering { @@ -69,8 +70,9 @@ public VisualLineLinkText(VisualLine parentVisualLine, int length) : base(parent /// public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) { - TextRunProperties.ForegroundBrush = context.TextView.LinkTextForegroundBrush; - TextRunProperties.BackgroundBrush = context.TextView.LinkTextBackgroundBrush; + TextRunProperties.SetForegroundBrush(context.TextView.LinkTextForegroundBrush); + TextRunProperties.SetBackgroundBrush(context.TextView.LinkTextBackgroundBrush); + return base.CreateTextRun(startVisualColumn, context); } diff --git a/src/AvaloniaEdit/Rendering/VisualLineText.cs b/src/AvaloniaEdit/Rendering/VisualLineText.cs index d3996ac4..88b9b25d 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineText.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineText.cs @@ -18,8 +18,11 @@ using System; using System.Collections.Generic; +using Avalonia.Media.TextFormatting; +using Avalonia.Utilities; using AvaloniaEdit.Document; using AvaloniaEdit.Text; +using LogicalDirection = AvaloniaEdit.Document.LogicalDirection; namespace AvaloniaEdit.Rendering { @@ -59,8 +62,10 @@ public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructio throw new ArgumentNullException(nameof(context)); var relativeOffset = startVisualColumn - VisualColumn; + var text = context.GetText(context.VisualLine.FirstDocumentLine.Offset + RelativeTextOffset + relativeOffset, DocumentLength - relativeOffset); - return new TextCharacters(text.Text, text.Offset, text.Count, TextRunProperties); + + return new TextCharacters(text, TextRunProperties); } /// @@ -71,15 +76,16 @@ public override bool IsWhitespace(int visualColumn) } /// - public override StringRange GetPrecedingText(int visualColumnLimit, ITextRunConstructionContext context) + public override ReadOnlySlice GetPrecedingText(int visualColumnLimit, ITextRunConstructionContext context) { if (context == null) throw new ArgumentNullException(nameof(context)); var relativeOffset = visualColumnLimit - VisualColumn; + var text = context.GetText(context.VisualLine.FirstDocumentLine.Offset + RelativeTextOffset, relativeOffset); - var range = new StringRange(text.Text, text.Offset, text.Count); - return range; + + return text; } /// diff --git a/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs b/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs index 3ba212af..183e9413 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs @@ -18,16 +18,20 @@ using System; using System.Diagnostics; +using Avalonia.Media.TextFormatting; +using Avalonia.Utilities; using AvaloniaEdit.Document; using AvaloniaEdit.Text; using AvaloniaEdit.Utils; +using JetBrains.Annotations; +using ITextSource = Avalonia.Media.TextFormatting.ITextSource; namespace AvaloniaEdit.Rendering { /// /// TextSource implementation that creates TextRuns for a VisualLine. /// - internal sealed class VisualLineTextSource : TextSource, ITextRunConstructionContext + internal sealed class VisualLineTextSource : ITextSource, ITextRunConstructionContext { public VisualLineTextSource(VisualLine visualLine) { @@ -37,9 +41,10 @@ public VisualLineTextSource(VisualLine visualLine) public VisualLine VisualLine { get; } public TextView TextView { get; set; } public TextDocument Document { get; set; } - public TextRunProperties GlobalTextRunProperties { get; set; } + public CustomTextRunProperties GlobalTextRunProperties { get; set; } - public override TextRun GetTextRun(int characterIndex) + [CanBeNull] + public TextRun GetTextRun(int characterIndex) { try { @@ -52,9 +57,9 @@ public override TextRun GetTextRun(int characterIndex) var run = element.CreateTextRun(characterIndex, this); if (run == null) throw new ArgumentNullException(element.GetType().Name + ".CreateTextRun"); - if (run.Length == 0) + if (run.TextSourceLength == 0) throw new ArgumentException("The returned TextRun must not have length 0.", element.GetType().Name + ".Length"); - if (relativeOffset + run.Length > element.VisualLength) + if (relativeOffset + run.TextSourceLength > element.VisualLength) throw new ArgumentException("The returned TextRun is too long.", element.GetType().Name + ".CreateTextRun"); if (run is InlineObjectRun inlineRun) { @@ -105,20 +110,24 @@ private TextRun CreateTextRunForNewLine() return new FormattedTextRun(new FormattedTextElement(TextView.CachedElements.GetTextForNonPrintableCharacter(newlineText, this), 0), GlobalTextRunProperties); } - private string _cachedString; + private ReadOnlySlice _cachedString; private int _cachedStringOffset; - public StringSegment GetText(int offset, int length) + public ReadOnlySlice GetText(int offset, int length) { - if (_cachedString != null) + if (!_cachedString.IsEmpty) { if (offset >= _cachedStringOffset && offset + length <= _cachedStringOffset + _cachedString.Length) { - return new StringSegment(_cachedString, offset - _cachedStringOffset, length); + return new ReadOnlySlice(_cachedString.Buffer, offset, length, offset - _cachedStringOffset); } } + _cachedStringOffset = offset; - return new StringSegment(_cachedString = Document.GetText(offset, length)); + + _cachedString = new ReadOnlySlice(Document.GetText(offset, length).AsMemory(), offset, length); + + return _cachedString; } } } diff --git a/src/AvaloniaEdit/Text/StringRange.cs b/src/AvaloniaEdit/Text/StringRange.cs index 76b2b45a..0e2dd54f 100644 --- a/src/AvaloniaEdit/Text/StringRange.cs +++ b/src/AvaloniaEdit/Text/StringRange.cs @@ -2,81 +2,5 @@ namespace AvaloniaEdit.Text { - public struct StringRange : IEquatable - { - public string String { get; } - - public int Length { get; set; } - - public static StringRange Empty => default(StringRange); - - internal int OffsetToFirstChar { get; } - - internal char this[int index] => String?[OffsetToFirstChar + index] ?? '\0'; - - public StringRange(string s, int offsetToFirstChar, int length) - { - String = s; - OffsetToFirstChar = offsetToFirstChar; - Length = length; - } - - public override string ToString() - { - return ToString(String, OffsetToFirstChar, Length); - } - - public string ToString(int maxLength) - { - return ToString(String, OffsetToFirstChar, maxLength); - } - - public bool Equals(StringRange other) - { - return string.Equals(String, other.String, StringComparison.Ordinal) && - Length == other.Length && - OffsetToFirstChar == other.OffsetToFirstChar; - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - return obj is StringRange && Equals((StringRange)obj); - } - - public override int GetHashCode() - { - unchecked - { - var hashCode = (String != null ? String.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ Length; - hashCode = (hashCode * 397) ^ OffsetToFirstChar; - return hashCode; - } - } - - public static bool operator ==(StringRange left, StringRange right) - { - return left.Equals(right); - } - - public static bool operator !=(StringRange left, StringRange right) - { - return !left.Equals(right); - } - - public StringRange WithLength(int length) - { - return new StringRange(String, OffsetToFirstChar, length); - } - - static string ToString(string value, int offsetToFirstChar, int length) - { - if (value == null) return string.Empty; - - if (offsetToFirstChar == 0 && length == value.Length) return value; - - return value.Substring(offsetToFirstChar, length); - } - } + } \ No newline at end of file diff --git a/src/AvaloniaEdit/Text/TextCharacters.cs b/src/AvaloniaEdit/Text/TextCharacters.cs index 91143295..9fa375f8 100644 --- a/src/AvaloniaEdit/Text/TextCharacters.cs +++ b/src/AvaloniaEdit/Text/TextCharacters.cs @@ -1,28 +1,3 @@ namespace AvaloniaEdit.Text { - public class TextCharacters : TextRun - { - public sealed override StringRange StringRange { get; } - - public sealed override int Length { get; } - - public sealed override TextRunProperties Properties { get; } - - public TextCharacters(string characterString, TextRunProperties textRunProperties) - : this(characterString, 0, characterString?.Length ?? 0, textRunProperties) - { - } - - public TextCharacters(string characterString, int offsetToFirstChar, int length, TextRunProperties textRunProperties) - : this(new StringRange(characterString, offsetToFirstChar, length), length, textRunProperties) - { - } - - private TextCharacters(StringRange stringRange, int length, TextRunProperties textRunProperties) - { - StringRange = stringRange; - Length = length; - Properties = textRunProperties; - } - } } \ No newline at end of file diff --git a/src/AvaloniaEdit/Text/TextEmbeddedObject.cs b/src/AvaloniaEdit/Text/TextEmbeddedObject.cs index 5220365a..ee3c1c85 100644 --- a/src/AvaloniaEdit/Text/TextEmbeddedObject.cs +++ b/src/AvaloniaEdit/Text/TextEmbeddedObject.cs @@ -1,5 +1,6 @@ using Avalonia; using Avalonia.Media; +using Avalonia.Media.TextFormatting; namespace AvaloniaEdit.Text { diff --git a/src/AvaloniaEdit/Text/TextEndOfLine.cs b/src/AvaloniaEdit/Text/TextEndOfLine.cs index f4e07490..d8b92c47 100644 --- a/src/AvaloniaEdit/Text/TextEndOfLine.cs +++ b/src/AvaloniaEdit/Text/TextEndOfLine.cs @@ -1,21 +1,4 @@ namespace AvaloniaEdit.Text { - public class TextEndOfLine : TextRun - { - public sealed override StringRange StringRange => default(StringRange); - - public sealed override int Length { get; } - - public sealed override TextRunProperties Properties { get; } - - public TextEndOfLine(int length) : this(length, null) - { - } - - public TextEndOfLine(int length, TextRunProperties textRunProperties) - { - Length = length; - Properties = textRunProperties; - } - } + } \ No newline at end of file diff --git a/src/AvaloniaEdit/Text/TextEndOfParagraph.cs b/src/AvaloniaEdit/Text/TextEndOfParagraph.cs index a67218a0..d7567d92 100644 --- a/src/AvaloniaEdit/Text/TextEndOfParagraph.cs +++ b/src/AvaloniaEdit/Text/TextEndOfParagraph.cs @@ -1,14 +1,4 @@ namespace AvaloniaEdit.Text { - public class TextEndOfParagraph : TextEndOfLine - { - public TextEndOfParagraph(int length) : base(length) - { - } - public TextEndOfParagraph(int length, TextRunProperties textRunProperties) - : base(length, textRunProperties) - { - } - } } \ No newline at end of file diff --git a/src/AvaloniaEdit/Text/TextFormatter.cs b/src/AvaloniaEdit/Text/TextFormatter.cs index fd958177..48369715 100644 --- a/src/AvaloniaEdit/Text/TextFormatter.cs +++ b/src/AvaloniaEdit/Text/TextFormatter.cs @@ -1,10 +1,3 @@ namespace AvaloniaEdit.Text { - public class TextFormatter - { - public TextLine FormatLine(TextSource textSource, int firstCharIndex, double paragraphWidth, TextParagraphProperties paragraphProperties) - { - return TextLineImpl.Create(paragraphProperties, firstCharIndex, (int)paragraphWidth, textSource); - } - } } \ No newline at end of file diff --git a/src/AvaloniaEdit/Text/TextLine.cs b/src/AvaloniaEdit/Text/TextLine.cs index 1d8abe8c..fa2ae7d8 100644 --- a/src/AvaloniaEdit/Text/TextLine.cs +++ b/src/AvaloniaEdit/Text/TextLine.cs @@ -4,19 +4,5 @@ namespace AvaloniaEdit.Text { - public abstract class TextLine - { - public abstract int FirstIndex { get; } - public abstract int Length { get; } - public abstract int TrailingWhitespaceLength { get; } - public abstract double Width { get; } - public abstract double WidthIncludingTrailingWhitespace { get; } - public abstract double Height { get; } - public abstract double Baseline { get; } - public abstract void Draw(DrawingContext drawingContext, Point origin); - public abstract double GetDistanceFromCharacter(int firstIndex, int trailingLength); - public abstract (int firstIndex, int trailingLength) GetCharacterFromDistance(double distance); - public abstract Rect GetTextBounds(int firstIndex, int textLength); - public abstract IList GetTextRuns(); - } + } \ No newline at end of file diff --git a/src/AvaloniaEdit/Text/TextLineImpl.cs b/src/AvaloniaEdit/Text/TextLineImpl.cs index faacb136..a9611939 100644 --- a/src/AvaloniaEdit/Text/TextLineImpl.cs +++ b/src/AvaloniaEdit/Text/TextLineImpl.cs @@ -7,235 +7,5 @@ namespace AvaloniaEdit.Text { - internal sealed class TextLineImpl : TextLine - { - private readonly TextLineRun[] _runs; - - public TextLineRun[] LineRuns { get { return _runs; } } - public override int FirstIndex { get; } - - public override int Length { get; } - - public override int TrailingWhitespaceLength { get; } - - public override double Width { get; } - - public override double WidthIncludingTrailingWhitespace { get; } - - public override double Height { get; } - - public override double Baseline { get; } - - internal static TextLineImpl Create(TextParagraphProperties paragraphProperties, int firstIndex, int paragraphLength, TextSource textSource) - { - var index = firstIndex; - var visibleLength = 0; - var widthLeft = paragraphProperties.TextWrapping == TextWrapping.Wrap && paragraphLength > 0 ? paragraphLength : double.MaxValue; - TextLineRun prevRun = null; - var run = TextLineRun.Create(textSource, index, firstIndex, widthLeft, paragraphProperties); - - if (!run.IsEnd && run.Width <= widthLeft) - { - index += run.Length; - widthLeft -= run.Width; - prevRun = run; - run = TextLineRun.Create(textSource, index, firstIndex, widthLeft, paragraphProperties); - } - - var trailing = new TrailingInfo(); - var runs = new List(2); - if (prevRun != null) - { - visibleLength += AddRunReturnVisibleLength(runs, prevRun); - } - - while (true) - { - visibleLength += AddRunReturnVisibleLength(runs, run); - index += run.Length; - widthLeft -= run.Width; - if (run.IsEnd || widthLeft <= 0) - { - trailing.SpaceWidth = 0; - UpdateTrailingInfo(runs, trailing); - return new TextLineImpl(paragraphProperties, firstIndex, runs, trailing); - } - - run = TextLineRun.Create(textSource, index, firstIndex, widthLeft, paragraphProperties); - - if (run.Width > widthLeft) - { - return new TextLineImpl(paragraphProperties, firstIndex, runs, trailing); - } - } - } - - private TextLineImpl(TextParagraphProperties paragraphProperties, int firstIndex, List runs, TrailingInfo trailing) - { - var top = 0.0; - var height = 0.0; - - var index = 0; - _runs = new TextLineRun[runs.Count]; - - foreach (var run in runs) - { - _runs[index++] = run; - - if (run.Length <= 0) continue; - - if (run.IsEnd) - { - trailing.Count += run.Length; - } - else - { - top = Math.Max(top, run.Height - run.Baseline); - height = Math.Max(height, run.Height); - Baseline = Math.Max(Baseline, run.Baseline); - } - - Length += run.Length; - WidthIncludingTrailingWhitespace += run.Width; - } - - Height = Math.Max(height, Baseline + top); - - if (Height <= 0) - { - Height = TextLineRun.GetDefaultLineHeight(paragraphProperties.DefaultTextRunProperties.FontMetrics); - Baseline = TextLineRun.GetDefaultBaseline(paragraphProperties.DefaultTextRunProperties.FontMetrics); - } - - FirstIndex = firstIndex; - TrailingWhitespaceLength = trailing.Count; - Width = WidthIncludingTrailingWhitespace - trailing.SpaceWidth; - } - - private static void UpdateTrailingInfo(List runs, TrailingInfo trailing) - { - for (var index = (runs?.Count ?? 0) - 1; index >= 0; index--) - { - // ReSharper disable once PossibleNullReferenceException - if (!runs[index].UpdateTrailingInfo(trailing)) - { - return; - } - } - } - - private static int AddRunReturnVisibleLength(List runs, TextLineRun run) - { - if (run.Length > 0) - { - runs.Add(run); - if (!run.IsEnd) - { - return run.Length; - } - } - - return 0; - } - - public override void Draw(DrawingContext drawingContext, Point origin) - { - if (drawingContext == null) throw new ArgumentNullException(nameof(drawingContext)); - - if (_runs.Length == 0) - { - return; - } - - double width = 0; - var y = origin.Y; - - foreach (var run in _runs) - { - run.Draw(drawingContext, width + origin.X, y); - width += run.Width; - } - } - - public override double GetDistanceFromCharacter(int firstIndex, int trailingLength) - { - double distance = 0; - var index = firstIndex + (trailingLength != 0 ? 1 : 0) - FirstIndex; - var runs = _runs; - foreach (var run in runs) - { - distance += run.GetDistanceFromCharacter(index); - if (index <= run.Length) - { - break; - } - - index -= run.Length; - } - - return distance; - } - - public override (int firstIndex, int trailingLength) GetCharacterFromDistance(double distance) - { - var firstIndex = FirstIndex; - if (distance < 0) - { - return (FirstIndex, 0); - } - - (int firstIndex, int trailingLength) result = (FirstIndex, 0); - - foreach (var run in _runs) - { - if (!run.IsEnd) - { - firstIndex += result.trailingLength; - result = run.GetCharacterFromDistance(distance); - firstIndex += result.firstIndex; - } - - if (distance <= run.Width) - { - break; - } - - distance -= run.Width; - } - - return (firstIndex, result.trailingLength); - } - - public override Rect GetTextBounds(int firstIndex, int textLength) - { - if (textLength == 0) throw new ArgumentOutOfRangeException(nameof(textLength)); - - if (textLength < 0) - { - firstIndex += textLength; - textLength = -textLength; - } - - if (firstIndex < FirstIndex) - { - textLength += firstIndex - FirstIndex; - firstIndex = FirstIndex; - } - - if (firstIndex + textLength > FirstIndex + Length) - { - textLength = FirstIndex + Length - firstIndex; - } - - var distance = GetDistanceFromCharacter(firstIndex, 0); - var distanceToLast = GetDistanceFromCharacter(firstIndex + textLength, 0); - - return new Rect(distance, 0.0, distanceToLast - distance, Height); - } - - public override IList GetTextRuns() - { - return _runs.Select(x => x.TextRun).ToArray(); - } - } + } \ No newline at end of file diff --git a/src/AvaloniaEdit/Text/TextParagraphProperties.cs b/src/AvaloniaEdit/Text/TextParagraphProperties.cs index fef4ebd1..e2688281 100644 --- a/src/AvaloniaEdit/Text/TextParagraphProperties.cs +++ b/src/AvaloniaEdit/Text/TextParagraphProperties.cs @@ -2,16 +2,5 @@ namespace AvaloniaEdit.Text { - public sealed class TextParagraphProperties - { - public double DefaultIncrementalTab { get; set; } - public bool FirstLineInParagraph { get; set; } - - public TextRunProperties DefaultTextRunProperties { get; set; } - - public TextWrapping TextWrapping { get; set; } - - public double Indent { get; set; } - } } diff --git a/src/AvaloniaEdit/Text/TextRun.cs b/src/AvaloniaEdit/Text/TextRun.cs index 1998ebd7..dd6f40c1 100644 --- a/src/AvaloniaEdit/Text/TextRun.cs +++ b/src/AvaloniaEdit/Text/TextRun.cs @@ -1,11 +1,4 @@ namespace AvaloniaEdit.Text { - public abstract class TextRun - { - public abstract StringRange StringRange { get; } - public abstract int Length { get; } - - public abstract TextRunProperties Properties { get; } - } } \ No newline at end of file diff --git a/src/AvaloniaEdit/Text/TextRunExtensions.cs b/src/AvaloniaEdit/Text/TextRunExtensions.cs index d5c67aff..5e0c4b72 100644 --- a/src/AvaloniaEdit/Text/TextRunExtensions.cs +++ b/src/AvaloniaEdit/Text/TextRunExtensions.cs @@ -2,15 +2,6 @@ namespace AvaloniaEdit.Text { internal static class TextRunExtensions { - internal static StringRange GetStringRange(this TextRun textRun) - { - switch (textRun) - { - case TextCharacters _: - return textRun.StringRange; - default: - return StringRange.Empty; - } - } + } } \ No newline at end of file diff --git a/src/AvaloniaEdit/Text/TextRunProperties.cs b/src/AvaloniaEdit/Text/TextRunProperties.cs index da3ec334..a100d3aa 100644 --- a/src/AvaloniaEdit/Text/TextRunProperties.cs +++ b/src/AvaloniaEdit/Text/TextRunProperties.cs @@ -4,94 +4,5 @@ namespace AvaloniaEdit.Text { - public class TextRunProperties - { - private IBrush _backgroundBrush; - private CultureInfo _cultureInfo; - private IBrush _foregroundBrush; - private Typeface _typeface; - private double _fontSize; - private FontMetrics _fontMetrics; - private bool _underline; - private bool _strikethrough; - public TextRunProperties Clone() - { - TextRunProperties clone = new TextRunProperties(); - - clone._backgroundBrush = BackgroundBrush; - clone._cultureInfo = CultureInfo; - clone._foregroundBrush = ForegroundBrush; - clone._typeface = Typeface; - clone._fontSize = FontSize; - clone._fontMetrics = FontMetrics; - clone._underline = Underline; - clone._strikethrough = Strikethrough; - - return clone; - } - - public IBrush BackgroundBrush - { - get { return _backgroundBrush; } - set { _backgroundBrush = value; } - } - - public CultureInfo CultureInfo - { - get { return _cultureInfo; } - set { _cultureInfo = value; } - } - - public IBrush ForegroundBrush - { - get { return _foregroundBrush; } - set { _foregroundBrush = value; } - } - - public Typeface Typeface - { - get { return _typeface; } - set - { - _typeface = value; - InvalidateFontMetrics(); - } - } - - public double FontSize - { - get { return _fontSize; } - set - { - _fontSize = value; - InvalidateFontMetrics(); - } - } - - public bool Underline - { - get{ return _underline; } - set { _underline = value; } - } - - public bool Strikethrough - { - get { return _strikethrough; } - set { _strikethrough = value; } - } - - public FontMetrics FontMetrics - { - get { return _fontMetrics; } - } - - void InvalidateFontMetrics() - { - if (_typeface.FontFamily == null || _fontSize == 0) - return; - - _fontMetrics = new FontMetrics(_typeface, _fontSize); - } - } } diff --git a/src/AvaloniaEdit/Text/TextSource.cs b/src/AvaloniaEdit/Text/TextSource.cs index 84071bda..dd6f40c1 100644 --- a/src/AvaloniaEdit/Text/TextSource.cs +++ b/src/AvaloniaEdit/Text/TextSource.cs @@ -1,7 +1,4 @@ namespace AvaloniaEdit.Text { - public abstract class TextSource - { - public abstract TextRun GetTextRun(int characterIndex); - } + } \ No newline at end of file diff --git a/src/AvaloniaEdit/Utils/TextFormatterFactory.cs b/src/AvaloniaEdit/Utils/TextFormatterFactory.cs index f36d31f4..1e91101b 100644 --- a/src/AvaloniaEdit/Utils/TextFormatterFactory.cs +++ b/src/AvaloniaEdit/Utils/TextFormatterFactory.cs @@ -20,7 +20,12 @@ using AvaloniaEdit.Text; using Avalonia.Controls; using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.Utilities; using AvaloniaEdit.Rendering; +using TextLine = Avalonia.Media.TextFormatting.TextLine; +using TextRun = Avalonia.Media.TextFormatting.TextRun; +using TextRunProperties = Avalonia.Media.TextFormatting.TextRunProperties; namespace AvaloniaEdit.Utils { @@ -29,11 +34,6 @@ namespace AvaloniaEdit.Utils /// public static class TextFormatterFactory { - public static TextFormatter Create() - { - return new TextFormatter(); - } - /// /// Creates formatted text. /// @@ -43,29 +43,32 @@ public static TextFormatter Create() /// 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, Avalonia.Media.FontFamily typeface, double? emSize, IBrush foreground) + public static TextLine FormatLine(ReadOnlySlice text, Typeface 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); + var defaultProperties = new GenericTextRunProperties(typeface, emSize, null, foreground); + var paragraphProperties = new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, + true, false, defaultProperties, TextWrapping.NoWrap, 0, 0); + + var textSource = new SimpleTextSource(text, defaultProperties); - var formattedText = new FormattedText - { - Text = text, - Typeface = new Typeface(typeface.Name), - FontSize = emSize.Value - }; - - formattedText.SetTextStyle(0, text.Length, foreground); - - return formattedText; + return TextFormatter.Current.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties); } + + private readonly struct SimpleTextSource : ITextSource + { + private readonly ReadOnlySlice _text; + private readonly TextRunProperties _defaultProperties; + + public SimpleTextSource(ReadOnlySlice text, TextRunProperties defaultProperties) + { + _text = text; + _defaultProperties = defaultProperties; + } + + public TextRun GetTextRun(int textSourceIndex) + { + return new TextCharacters(_text, _defaultProperties); + } + } } } diff --git a/test/AvaloniaEdit.Tests/AvaloniaEdit.Tests.csproj b/test/AvaloniaEdit.Tests/AvaloniaEdit.Tests.csproj index f0068349..4fd179d5 100644 --- a/test/AvaloniaEdit.Tests/AvaloniaEdit.Tests.csproj +++ b/test/AvaloniaEdit.Tests/AvaloniaEdit.Tests.csproj @@ -5,7 +5,7 @@ - + From b659e87252d270ffbee099e0353c75e378f07bc0 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 7 Mar 2022 13:14:39 +0100 Subject: [PATCH 02/24] Add missing methods via extensions --- .../Rendering/SimpleTextSource.cs | 11 +- .../Rendering/VisualLineTextSource.cs | 3 +- src/AvaloniaEdit/Text/TextLineRun.cs | 454 +----------------- .../Utils/TextFormatterFactory.cs | 12 +- src/AvaloniaEdit/Utils/TextLineExtensions.cs | 17 + 5 files changed, 39 insertions(+), 458 deletions(-) create mode 100644 src/AvaloniaEdit/Utils/TextLineExtensions.cs diff --git a/src/AvaloniaEdit/Rendering/SimpleTextSource.cs b/src/AvaloniaEdit/Rendering/SimpleTextSource.cs index 7f5ab10b..f74789db 100644 --- a/src/AvaloniaEdit/Rendering/SimpleTextSource.cs +++ b/src/AvaloniaEdit/Rendering/SimpleTextSource.cs @@ -32,11 +32,16 @@ public SimpleTextSource(ReadOnlySlice text, TextRunProperties properties) _properties = properties; } - public TextRun GetTextRun(int characterIndex) + public TextRun GetTextRun(int textSourceIndex) { - if (characterIndex < _text.Length) + if (textSourceIndex < _text.Length) { - return new TextCharacters(_text, characterIndex, _text.Length - characterIndex, _properties); + return new TextCharacters(_text, textSourceIndex, _text.Length - textSourceIndex, _properties); + } + + if (textSourceIndex > _text.Length) + { + return null; } return new TextEndOfParagraph(1); diff --git a/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs b/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs index 183e9413..496404fd 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs @@ -74,7 +74,8 @@ public TextRun GetTextRun(int characterIndex) { return CreateTextRunForNewLine(); } - return new TextEndOfParagraph(1); + + return null; } catch (Exception ex) { diff --git a/src/AvaloniaEdit/Text/TextLineRun.cs b/src/AvaloniaEdit/Text/TextLineRun.cs index 102de813..3db4d5a0 100644 --- a/src/AvaloniaEdit/Text/TextLineRun.cs +++ b/src/AvaloniaEdit/Text/TextLineRun.cs @@ -13,457 +13,5 @@ namespace AvaloniaEdit.Text { - internal sealed class TextLineRun - { - private const string NewlineString = "\r\n"; - private const string TabString = "\t"; - - private FormattedText _formattedText; - private Size _formattedTextSize; - private IReadOnlyList _glyphWidths; - public StringRange StringRange { get; private set; } - - public int Length { get; set; } - - public double Width { get; private set; } - - public TextRun TextRun { get; private set; } - - public bool IsEnd { get; private set; } - - public bool IsTab { get; private set; } - - public bool IsEmbedded { get; private set; } - - public double Baseline - { - get - { - if (IsEnd) - { - return 0.0; - } - - double defaultBaseLine = GetDefaultBaseline(TextRun.Properties.FontMetrics); - - if (IsEmbedded && TextRun is TextEmbeddedObject embeddedObject) - { - var box = embeddedObject.ComputeBoundingBox(); - return defaultBaseLine - box.Y; - } - - return defaultBaseLine; - } - } - - public double Height - { - get - { - if (IsEnd) - { - return 0.0; - } - if (IsEmbedded && TextRun is TextEmbeddedObject embeddedObject) - { - var box = embeddedObject.ComputeBoundingBox(); - return box.Height; - } - - return GetDefaultLineHeight(TextRun.Properties.FontMetrics); - } - } - - public static double GetDefaultLineHeight(FontMetrics fontMetrics) - { - // adding an extra 15% of the line height look good across different font sizes - double extraLineHeight = fontMetrics.LineHeight * 0.15; - return fontMetrics.LineHeight + extraLineHeight; - } - - public static double GetDefaultBaseline(FontMetrics fontMetrics) - { - return Math.Abs(fontMetrics.Ascent); - } - - public Typeface Typeface => TextRun.Properties.Typeface; - - public double FontSize => TextRun.Properties.FontSize; - - private TextLineRun() - { - } - - public static TextLineRun Create(TextSource textSource, int index, int firstIndex, double lengthLeft, TextParagraphProperties paragraphProperties) - { - var textRun = textSource.GetTextRun(index); - var stringRange = textRun.GetStringRange(); - return Create(textSource, stringRange, textRun, index, lengthLeft, paragraphProperties); - } - - private static TextLineRun Create(TextSource textSource, StringRange stringRange, TextRun textRun, int index, double widthLeft, TextParagraphProperties paragraphProperties) - { - if (textRun is TextCharacters) - { - return CreateRunForSpecialChars(textSource, stringRange, textRun, index, paragraphProperties) ?? - CreateRunForText(stringRange, textRun, widthLeft, paragraphProperties); - } - - if (textRun is TextEndOfLine) - { - return new TextLineRun(textRun.Length, textRun) { IsEnd = true }; - } - - if (textRun is TextEmbeddedObject embeddedObject) - { - double width = embeddedObject.GetSize(double.PositiveInfinity).Width; - return new TextLineRun(textRun.Length, textRun) - { - IsEmbedded = true, - _glyphWidths = new double[] { width }, - // Embedded objects must propagate their width to the container. - // Otherwise text runs after the embedded object are drawn at the same x position. - Width = width - }; - } - - throw new NotSupportedException("Unsupported run type"); - } - - private static TextLineRun CreateRunForSpecialChars(TextSource textSource, StringRange stringRange, TextRun textRun, int index, TextParagraphProperties paragraphProperties) - { - switch (stringRange[0]) - { - case '\r': - var runLength = 1; - if (stringRange.Length > 1 && stringRange[1] == '\n') - { - runLength = 2; - } - else if (stringRange.Length == 1) - { - var nextRun = textSource.GetTextRun(index + 1); - var range = nextRun.GetStringRange(); - if (range.Length > 0 && range[0] == '\n') - { - var eolRun = new TextCharacters(NewlineString, textRun.Properties); - return new TextLineRun(eolRun.Length, eolRun) { IsEnd = true }; - } - } - - return new TextLineRun(runLength, textRun) { IsEnd = true }; - case '\n': - return new TextLineRun(1, textRun) { IsEnd = true }; - case '\t': - return CreateRunForTab(textRun, paragraphProperties); - default: - return null; - } - } - - private static TextLineRun CreateRunForTab(TextRun textRun, TextParagraphProperties paragraphProperties) - { - var tabRun = new TextCharacters(TabString, textRun.Properties); - var stringRange = tabRun.StringRange; - var run = new TextLineRun(1, tabRun) - { - IsTab = true, - StringRange = stringRange, - Width = paragraphProperties.DefaultIncrementalTab - }; - - run._glyphWidths = new double[] { run.Width }; - - return run; - } - - internal static TextLineRun CreateRunForText( - StringRange stringRange, - TextRun textRun, - double widthLeft, - TextParagraphProperties paragraphProperties) - { - TextLineRun run = CreateTextLineRun(stringRange, textRun, textRun.Length, paragraphProperties); - - if (run.Width <= widthLeft) - return run; - - TextLineRun wrapped = PerformTextWrapping(run, widthLeft, paragraphProperties); - wrapped.Width = run.Width; - return wrapped; - } - - private static TextLineRun CreateTextLineRun(StringRange stringRange, TextRun textRun, int length, TextParagraphProperties paragraphProperties) - { - var run = new TextLineRun - { - StringRange = stringRange, - TextRun = textRun, - Length = length - }; - - var tf = run.Typeface; - var formattedText = new FormattedText - { - Text = stringRange.ToString(run.Length), - Typeface = new Typeface(tf.FontFamily, tf.Style, tf.Weight), - FontSize = run.FontSize - }; - - run._formattedText = formattedText; - - var size = formattedText.Bounds.Size; - - run._formattedTextSize = size; - - run.Width = size.Width; - - run._glyphWidths = new GlyphWidths( - run.StringRange, - run.Typeface.GlyphTypeface, - run.FontSize, - paragraphProperties.DefaultIncrementalTab); - - return run; - } - - private static TextLineRun PerformTextWrapping(TextLineRun run, double widthLeft, TextParagraphProperties paragraphProperties) - { - (int firstIndex, int trailingLength) characterHit = run.GetCharacterFromDistance(widthLeft); - - int lenForTextWrapping = FindPositionForTextWrapping(run.StringRange, characterHit.firstIndex); - - return CreateTextLineRun( - run.StringRange.WithLength(lenForTextWrapping), - run.TextRun, - lenForTextWrapping, - paragraphProperties); - } - - private static int FindPositionForTextWrapping(StringRange range, int maxIndex) - { - if (maxIndex > range.Length - 1) - maxIndex = range.Length - 1; - - LineBreakEnumerator lineBreakEnumerator = new LineBreakEnumerator( - new ReadOnlySlice(range.String.AsMemory().Slice(range.OffsetToFirstChar, range.Length))); - - LineBreak? lineBreak = null; - - while (lineBreakEnumerator.MoveNext()) - { - if (lineBreakEnumerator.Current.PositionWrap > maxIndex) - break; - - lineBreak = lineBreakEnumerator.Current; - } - - return lineBreak.HasValue ? lineBreak.Value.PositionWrap : maxIndex; - } - - private TextLineRun(int length, TextRun textRun) - { - Length = length; - TextRun = textRun; - } - - public void Draw(DrawingContext drawingContext, double x, double y) - { - if (IsEmbedded) - { - var embeddedObject = (TextEmbeddedObject)TextRun; - embeddedObject.Draw(drawingContext, new Point(x, y)); - return; - } - - if (Length <= 0 || IsEnd) - { - return; - } - - if (_formattedText != null && drawingContext != null) - { - if (TextRun.Properties.BackgroundBrush != null) - { - var bounds = new Rect(x, y, _formattedTextSize.Width, _formattedTextSize.Height); - drawingContext.FillRectangle(TextRun.Properties.BackgroundBrush, bounds); - } - - drawingContext.DrawText(TextRun.Properties.ForegroundBrush, - new Point(x, y), _formattedText); - - var glyphTypeface = TextRun.Properties.Typeface.GlyphTypeface; - - 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); - - var posY = baseline + glyphTypeface.UnderlinePosition * scale; - - drawingContext.DrawLine(pen, - new Point(x, posY), - new Point(x + _formattedTextSize.Width, posY)); - } - - if (TextRun.Properties.Strikethrough) - { - var pen = new Pen(TextRun.Properties.ForegroundBrush, glyphTypeface.StrikethroughThickness * scale); - - var posY = baseline + glyphTypeface.StrikethroughPosition * scale; - - drawingContext.DrawLine(pen, - new Point(x, posY), - new Point(x + _formattedTextSize.Width, posY)); - } - } - } - - public bool UpdateTrailingInfo(TrailingInfo trailing) - { - if (IsEnd) return true; - - if (IsTab) return false; - - var index = Length; - if (index > 0 && IsSpace(StringRange[index - 1])) - { - while (index > 0 && IsSpace(StringRange[index - 1])) - { - trailing.SpaceWidth += _glyphWidths[index - 1]; - index--; - trailing.Count++; - } - - return index == 0; - } - - return false; - } - - public double GetDistanceFromCharacter(int index) - { - if (!IsEnd && !IsTab) - { - if (index > Length) - { - index = Length; - } - - double distance = 0; - for (var i = 0; i < index; i++) - { - distance += _glyphWidths[i]; - } - - return distance; - } - - return index > 0 ? Width : 0; - } - - public (int firstIndex, int trailingLength) GetCharacterFromDistance(double distance) - { - if (IsEnd) return (0, 0); - - if (Length <= 0) return (0, 0); - - var index = 0; - double width = 0; - for (; index < Length; index++) - { - width = IsTab ? Width / Length : _glyphWidths[index]; - if (distance < width) - { - break; - } - - distance -= width; - } - - return index < Length - ? (index, distance > width / 2 ? 1 : 0) - : (Length - 1, 1); - } - - private static bool IsSpace(char ch) - { - return ch == ' ' || ch == '\u00a0'; - } - - class GlyphWidths : 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); - - internal GlyphWidths(StringRange range, GlyphTypeface typeFace, double fontSize, double tabSize) - { - _range = range; - _typeFace = typeFace; - _scale = fontSize / _typeFace.DesignEmHeight; - _tabSize = tabSize; - - InitGlyphWidths(); - } - - double GetAt(int index) - { - if (_glyphWidths.Length == 0) - return 0; - - if (_range[index] == '\t') - return _tabSize; - - if (_glyphWidths[index] == NOT_CALCULATED_YET) - _glyphWidths[index] = MeasureGlyphAt(index); - - return _glyphWidths[index]; - } - - double MeasureGlyphAt(int index) - { - return _typeFace.GetGlyphAdvance( - _typeFace.GetGlyph(_range[index])) * _scale; - } - - void InitGlyphWidths() - { - int capacity = _range.Length; - - bool useCheapGlyphMeasurement = - capacity >= VisualLine.LENGTH_LIMIT && - _typeFace.IsFixedPitch; - - if (useCheapGlyphMeasurement) - { - double size = MeasureGlyphAt(0); - _glyphWidths = Enumerable.Repeat(size, capacity).ToArray(); - return; - } - - _glyphWidths = Enumerable.Repeat(NOT_CALCULATED_YET, capacity).ToArray(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - foreach (double value in _glyphWidths) - yield return value; - } - - IEnumerator IEnumerable.GetEnumerator() - { - return _glyphWidths.GetEnumerator(); - } - } - } + } \ No newline at end of file diff --git a/src/AvaloniaEdit/Utils/TextFormatterFactory.cs b/src/AvaloniaEdit/Utils/TextFormatterFactory.cs index 1e91101b..5ed444af 100644 --- a/src/AvaloniaEdit/Utils/TextFormatterFactory.cs +++ b/src/AvaloniaEdit/Utils/TextFormatterFactory.cs @@ -67,7 +67,17 @@ public SimpleTextSource(ReadOnlySlice text, TextRunProperties defaultPrope public TextRun GetTextRun(int textSourceIndex) { - return new TextCharacters(_text, _defaultProperties); + if (textSourceIndex < _text.Length) + { + return new TextCharacters(_text, textSourceIndex, _text.Length - textSourceIndex, _defaultProperties); + } + + if (textSourceIndex > _text.Length) + { + return null; + } + + return new TextEndOfParagraph(1); } } } diff --git a/src/AvaloniaEdit/Utils/TextLineExtensions.cs b/src/AvaloniaEdit/Utils/TextLineExtensions.cs new file mode 100644 index 00000000..fb68a403 --- /dev/null +++ b/src/AvaloniaEdit/Utils/TextLineExtensions.cs @@ -0,0 +1,17 @@ +using Avalonia; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; + +namespace AvaloniaEdit.Utils; + +public static class TextLineExtensions +{ + public static Rect GetTextBounds(this TextLine textLine, int start, int length) + { + var startX = textLine.GetDistanceFromCharacterHit(new CharacterHit(start)); + + var endX = textLine.GetDistanceFromCharacterHit(new CharacterHit(start + length)); + + return new Rect(startX, 0, endX - startX, textLine.Height); + } +} \ No newline at end of file From 94cbc94cd9d25175d9fc38c96e74af6bf1995d03 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 9 Mar 2022 10:11:04 +0100 Subject: [PATCH 03/24] First successfull render --- .../AvaloniaEdit.Demo.csproj | 2 +- src/AvaloniaEdit/AvaloniaEdit.csproj | 2 +- .../Editing/CaretNavigationCommandHandler.cs | 1 - src/AvaloniaEdit/Editing/LineNumberMargin.cs | 12 ++++--- .../Folding/FoldingElementGenerator.cs | 4 +-- src/AvaloniaEdit/Folding/FoldingMargin.cs | 1 - .../Rendering/FormattedTextElement.cs | 22 ++++++------- src/AvaloniaEdit/Rendering/InlineObjectRun.cs | 31 +++---------------- .../SingleCharacterElementGenerator.cs | 28 +++++++---------- src/AvaloniaEdit/Rendering/TextView.cs | 3 +- .../Rendering/TextViewCachedElements.cs | 2 -- src/AvaloniaEdit/Rendering/VisualLine.cs | 1 - .../Rendering/VisualLineElement.cs | 1 - .../Rendering/VisualLineLinkText.cs | 1 - src/AvaloniaEdit/Rendering/VisualLineText.cs | 1 - .../Rendering/VisualLineTextSource.cs | 10 ++++-- src/AvaloniaEdit/Text/StringRange.cs | 6 ---- src/AvaloniaEdit/Text/TextCharacters.cs | 3 -- src/AvaloniaEdit/Text/TextEmbeddedObject.cs | 17 ---------- src/AvaloniaEdit/Text/TextEndOfLine.cs | 4 --- src/AvaloniaEdit/Text/TextEndOfParagraph.cs | 4 --- src/AvaloniaEdit/Text/TextFormatter.cs | 3 -- src/AvaloniaEdit/Text/TextLine.cs | 8 ----- src/AvaloniaEdit/Text/TextLineImpl.cs | 11 ------- src/AvaloniaEdit/Text/TextLineRun.cs | 17 ---------- .../Text/TextParagraphProperties.cs | 6 ---- src/AvaloniaEdit/Text/TextRun.cs | 4 --- src/AvaloniaEdit/Text/TextRunExtensions.cs | 7 ----- src/AvaloniaEdit/Text/TextRunProperties.cs | 8 ----- src/AvaloniaEdit/Text/TextSource.cs | 4 --- src/AvaloniaEdit/Text/TrailingInfo.cs | 8 ----- .../Utils/TextFormatterFactory.cs | 4 --- 32 files changed, 44 insertions(+), 192 deletions(-) delete mode 100644 src/AvaloniaEdit/Text/StringRange.cs delete mode 100644 src/AvaloniaEdit/Text/TextCharacters.cs delete mode 100644 src/AvaloniaEdit/Text/TextEmbeddedObject.cs delete mode 100644 src/AvaloniaEdit/Text/TextEndOfLine.cs delete mode 100644 src/AvaloniaEdit/Text/TextEndOfParagraph.cs delete mode 100644 src/AvaloniaEdit/Text/TextFormatter.cs delete mode 100644 src/AvaloniaEdit/Text/TextLine.cs delete mode 100644 src/AvaloniaEdit/Text/TextLineImpl.cs delete mode 100644 src/AvaloniaEdit/Text/TextLineRun.cs delete mode 100644 src/AvaloniaEdit/Text/TextParagraphProperties.cs delete mode 100644 src/AvaloniaEdit/Text/TextRun.cs delete mode 100644 src/AvaloniaEdit/Text/TextRunExtensions.cs delete mode 100644 src/AvaloniaEdit/Text/TextRunProperties.cs delete mode 100644 src/AvaloniaEdit/Text/TextSource.cs delete mode 100644 src/AvaloniaEdit/Text/TrailingInfo.cs diff --git a/src/AvaloniaEdit.Demo/AvaloniaEdit.Demo.csproj b/src/AvaloniaEdit.Demo/AvaloniaEdit.Demo.csproj index 85cd21b9..6666011d 100644 --- a/src/AvaloniaEdit.Demo/AvaloniaEdit.Demo.csproj +++ b/src/AvaloniaEdit.Demo/AvaloniaEdit.Demo.csproj @@ -30,7 +30,7 @@ - + diff --git a/src/AvaloniaEdit/AvaloniaEdit.csproj b/src/AvaloniaEdit/AvaloniaEdit.csproj index a470f09c..806270a7 100644 --- a/src/AvaloniaEdit/AvaloniaEdit.csproj +++ b/src/AvaloniaEdit/AvaloniaEdit.csproj @@ -33,7 +33,7 @@ - + diff --git a/src/AvaloniaEdit/Editing/CaretNavigationCommandHandler.cs b/src/AvaloniaEdit/Editing/CaretNavigationCommandHandler.cs index 7955f92a..8fcee202 100644 --- a/src/AvaloniaEdit/Editing/CaretNavigationCommandHandler.cs +++ b/src/AvaloniaEdit/Editing/CaretNavigationCommandHandler.cs @@ -22,7 +22,6 @@ using Avalonia; using AvaloniaEdit.Document; using AvaloniaEdit.Rendering; -using AvaloniaEdit.Text; using AvaloniaEdit.Utils; using Avalonia.Input; using Avalonia.Input.Platform; diff --git a/src/AvaloniaEdit/Editing/LineNumberMargin.cs b/src/AvaloniaEdit/Editing/LineNumberMargin.cs index 373e3861..3cce11c6 100644 --- a/src/AvaloniaEdit/Editing/LineNumberMargin.cs +++ b/src/AvaloniaEdit/Editing/LineNumberMargin.cs @@ -84,10 +84,14 @@ public override void Render(DrawingContext drawingContext) EmSize, GetValue(TemplatedControl.ForegroundProperty) ); - - var y = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.TextTop); - - textLine.Draw(drawingContext, new Point(renderSize.Width - textLine.WidthIncludingTrailingWhitespace, y - textView.VerticalOffset)); + + var y = line.TextLines.Count > 0 + ? line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.TextTop) + : line.VisualTop; + + textLine.Draw(drawingContext, + new Point(renderSize.Width - textLine.WidthIncludingTrailingWhitespace, + y - textView.VerticalOffset)); } } } diff --git a/src/AvaloniaEdit/Folding/FoldingElementGenerator.cs b/src/AvaloniaEdit/Folding/FoldingElementGenerator.cs index 602d9765..5ff424c9 100644 --- a/src/AvaloniaEdit/Folding/FoldingElementGenerator.cs +++ b/src/AvaloniaEdit/Folding/FoldingElementGenerator.cs @@ -20,8 +20,6 @@ using System.Collections.Generic; using Avalonia; using AvaloniaEdit.Rendering; -using AvaloniaEdit.Text; -using AvaloniaEdit.Utils; using Avalonia.Input; using Avalonia.Media; using Avalonia.Media.TextFormatting; @@ -202,7 +200,7 @@ public FoldingLineTextRun(FormattedTextElement element, TextRunProperties proper public override void Draw(DrawingContext drawingContext, Point origin) { - var metrics = GetSize(double.PositiveInfinity); + var metrics = Size; var r = new Rect(origin.X, origin.Y, metrics.Width, metrics.Height); drawingContext.DrawRectangle(new Pen(_textBrush), r); base.Draw(drawingContext, origin); diff --git a/src/AvaloniaEdit/Folding/FoldingMargin.cs b/src/AvaloniaEdit/Folding/FoldingMargin.cs index 3db00642..2f5aabb1 100644 --- a/src/AvaloniaEdit/Folding/FoldingMargin.cs +++ b/src/AvaloniaEdit/Folding/FoldingMargin.cs @@ -22,7 +22,6 @@ using Avalonia; using AvaloniaEdit.Editing; using AvaloniaEdit.Rendering; -using AvaloniaEdit.Text; using AvaloniaEdit.Utils; using Avalonia.Controls; using Avalonia.Media; diff --git a/src/AvaloniaEdit/Rendering/FormattedTextElement.cs b/src/AvaloniaEdit/Rendering/FormattedTextElement.cs index c0db4344..7b8d5e54 100644 --- a/src/AvaloniaEdit/Rendering/FormattedTextElement.cs +++ b/src/AvaloniaEdit/Rendering/FormattedTextElement.cs @@ -20,8 +20,6 @@ using Avalonia; using Avalonia.Media; using Avalonia.Media.TextFormatting; -using AvaloniaEdit.Text; -using AvaloniaEdit.Utils; using JetBrains.Annotations; namespace AvaloniaEdit.Rendering @@ -103,7 +101,7 @@ internal static TextLine PrepareText(TextFormatter formatter, string text, TextR /// /// This is the TextRun implementation used by the class. /// - public class FormattedTextRun : TextEmbeddedObject + public class FormattedTextRun : DrawableTextRun { /// /// Creates a new FormattedTextRun. @@ -112,6 +110,8 @@ public FormattedTextRun(FormattedTextElement element, TextRunProperties properti { Properties = properties ?? throw new ArgumentNullException(nameof(properties)); Element = element ?? throw new ArgumentNullException(nameof(element)); + + Size = GetSize(); } /// @@ -122,13 +122,15 @@ public FormattedTextRun(FormattedTextElement element, TextRunProperties properti /// public override int TextSourceLength => Element.VisualLength; - /// - public override bool HasFixedSize => true; - /// public override TextRunProperties Properties { get; } + + public override Size Size { get; } + + public override double Baseline => + Element.FormattedText?.Baseline ?? Element.TextLine.Baseline; - public override Size GetSize(double remainingParagraphWidth) + private Size GetSize() { var formattedText = Element.FormattedText; @@ -143,12 +145,6 @@ public override Size GetSize(double remainingParagraphWidth) text.Height); } - /// - public override Rect ComputeBoundingBox() - { - return new Rect(GetSize(double.PositiveInfinity)); - } - /// public override void Draw(DrawingContext drawingContext, Point origin) { diff --git a/src/AvaloniaEdit/Rendering/InlineObjectRun.cs b/src/AvaloniaEdit/Rendering/InlineObjectRun.cs index 04a65bfa..09cc61c8 100644 --- a/src/AvaloniaEdit/Rendering/InlineObjectRun.cs +++ b/src/AvaloniaEdit/Rendering/InlineObjectRun.cs @@ -21,7 +21,6 @@ using Avalonia.Controls; using Avalonia.Media; using Avalonia.Media.TextFormatting; -using AvaloniaEdit.Text; namespace AvaloniaEdit.Rendering { @@ -59,10 +58,8 @@ public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructio /// /// A text run with an embedded UIElement. /// - public class InlineObjectRun : TextEmbeddedObject + public class InlineObjectRun : DrawableTextRun { - internal Size DesiredSize; - /// /// Creates a new InlineObjectRun instance. /// @@ -90,38 +87,20 @@ public InlineObjectRun(int length, TextRunProperties properties, IControl elemen /// public VisualLine VisualLine { get; internal set; } - /// - public override bool HasFixedSize => true; - /// public override int TextSourceLength { get; } /// public override TextRunProperties Properties { get; } - public override Size GetSize(double remainingParagraphWidth) - { - if (Element.IsMeasureValid) - { - return DesiredSize; - } + public override double Baseline => Element.DesiredSize.Height; - return Size.Empty; - } - - public override Rect ComputeBoundingBox() - { - if (Element.IsMeasureValid) - { - var baseline = DesiredSize.Height; - return new Rect(DesiredSize); - } - - return Rect.Empty; - } + public override Size Size => Element.IsMeasureValid ? Element.DesiredSize : Size.Empty; + public Size DesiredSize { get; set; } public override void Draw(DrawingContext drawingContext, Point origin) { + //noop } } } diff --git a/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs b/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs index 3f4b72aa..dcdbbc6c 100644 --- a/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs +++ b/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs @@ -23,8 +23,6 @@ using Avalonia.Media.Immutable; using Avalonia.Media.TextFormatting; using AvaloniaEdit.Document; -using AvaloniaEdit.Text; -using AvaloniaEdit.Utils; using LogicalDirection = AvaloniaEdit.Document.LogicalDirection; namespace AvaloniaEdit.Rendering @@ -179,7 +177,7 @@ public override bool IsWhitespace(int visualColumn) } } - internal sealed class TabGlyphRun : TextEmbeddedObject + internal sealed class TabGlyphRun : DrawableTextRun { private readonly TabTextElement _element; @@ -189,21 +187,13 @@ public TabGlyphRun(TabTextElement element, TextRunProperties properties) _element = element; } - public override bool HasFixedSize => true; - public override int TextSourceLength => 1; public override TextRunProperties Properties { get; } - public override Size GetSize(double remainingParagraphWidth) - { - return new Size(0, _element.Text.Height); - } + public override double Baseline => _element.Text.Height; - public override Rect ComputeBoundingBox() - { - return new Rect(GetSize(double.PositiveInfinity)); - } + public override Size Size => new(0, _element.Text.Height); public override void Draw(DrawingContext drawingContext, Point origin) { @@ -239,16 +229,20 @@ public SpecialCharacterTextRun(FormattedTextElement element, TextRunProperties p { } - public override Size GetSize(double remainingParagraphWidth) + public override Size Size { - var s = base.GetSize(remainingParagraphWidth); - return s.WithWidth(s.Width + BoxMargin); + get + { + var s = base.Size; + + return s.WithWidth(s.Width + BoxMargin); + } } public override void Draw(DrawingContext drawingContext, Point origin) { var newOrigin = new Point(origin.X + (BoxMargin / 2), origin.Y); - var metrics = GetSize(double.PositiveInfinity); + var metrics = Size; var r = new Rect(origin.X, origin.Y, metrics.Width, metrics.Height); drawingContext.FillRectangle(DarkGrayBrush, r, 2.5f); base.Draw(drawingContext, newOrigin); diff --git a/src/AvaloniaEdit/Rendering/TextView.cs b/src/AvaloniaEdit/Rendering/TextView.cs index ce693d2a..79f2b2b8 100644 --- a/src/AvaloniaEdit/Rendering/TextView.cs +++ b/src/AvaloniaEdit/Rendering/TextView.cs @@ -37,7 +37,6 @@ using Avalonia.VisualTree; using AvaloniaEdit.Document; using AvaloniaEdit.Editing; -using AvaloniaEdit.Text; using AvaloniaEdit.Utils; namespace AvaloniaEdit.Rendering @@ -1134,7 +1133,7 @@ private VisualLine BuildVisualLine(DocumentLine documentLine, // now construct textLines: var textOffset = 0; var textLines = new List(); - while (textOffset <= visualLine.VisualLengthWithEndOfLineMarker) + while (textOffset < visualLine.VisualLengthWithEndOfLineMarker) { var textLine = _formatter.FormatLine( textSource, diff --git a/src/AvaloniaEdit/Rendering/TextViewCachedElements.cs b/src/AvaloniaEdit/Rendering/TextViewCachedElements.cs index d2ea93de..7edb3615 100644 --- a/src/AvaloniaEdit/Rendering/TextViewCachedElements.cs +++ b/src/AvaloniaEdit/Rendering/TextViewCachedElements.cs @@ -18,8 +18,6 @@ using System.Collections.Generic; using Avalonia.Media.TextFormatting; -using AvaloniaEdit.Text; -using AvaloniaEdit.Utils; namespace AvaloniaEdit.Rendering { diff --git a/src/AvaloniaEdit/Rendering/VisualLine.cs b/src/AvaloniaEdit/Rendering/VisualLine.cs index dc03a397..3da4383a 100644 --- a/src/AvaloniaEdit/Rendering/VisualLine.cs +++ b/src/AvaloniaEdit/Rendering/VisualLine.cs @@ -26,7 +26,6 @@ using Avalonia.Media; using Avalonia.Media.TextFormatting; using AvaloniaEdit.Document; -using AvaloniaEdit.Text; using AvaloniaEdit.Utils; using LogicalDirection = AvaloniaEdit.Document.LogicalDirection; diff --git a/src/AvaloniaEdit/Rendering/VisualLineElement.cs b/src/AvaloniaEdit/Rendering/VisualLineElement.cs index 709abcf3..4873beda 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineElement.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineElement.cs @@ -20,7 +20,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using AvaloniaEdit.Document; -using AvaloniaEdit.Text; using Avalonia.Input; using Avalonia.Media; using Avalonia.Media.TextFormatting; diff --git a/src/AvaloniaEdit/Rendering/VisualLineLinkText.cs b/src/AvaloniaEdit/Rendering/VisualLineLinkText.cs index 58d430ba..d02d83ee 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineLinkText.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineLinkText.cs @@ -17,7 +17,6 @@ // DEALINGS IN THE SOFTWARE. using System; -using AvaloniaEdit.Text; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Controls; diff --git a/src/AvaloniaEdit/Rendering/VisualLineText.cs b/src/AvaloniaEdit/Rendering/VisualLineText.cs index 88b9b25d..faaaa730 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineText.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineText.cs @@ -21,7 +21,6 @@ using Avalonia.Media.TextFormatting; using Avalonia.Utilities; using AvaloniaEdit.Document; -using AvaloniaEdit.Text; using LogicalDirection = AvaloniaEdit.Document.LogicalDirection; namespace AvaloniaEdit.Rendering diff --git a/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs b/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs index 496404fd..ac177a30 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs @@ -21,8 +21,6 @@ using Avalonia.Media.TextFormatting; using Avalonia.Utilities; using AvaloniaEdit.Document; -using AvaloniaEdit.Text; -using AvaloniaEdit.Utils; using JetBrains.Annotations; using ITextSource = Avalonia.Media.TextFormatting.ITextSource; @@ -46,6 +44,11 @@ public VisualLineTextSource(VisualLine visualLine) [CanBeNull] public TextRun GetTextRun(int characterIndex) { + if (characterIndex > VisualLine.VisualLengthWithEndOfLineMarker) + { + return null; + } + try { foreach (var element in VisualLine.Elements) @@ -70,12 +73,13 @@ public TextRun GetTextRun(int characterIndex) return run; } } + if (TextView.Options.ShowEndOfLine && characterIndex == VisualLine.VisualLength) { return CreateTextRunForNewLine(); } - return null; + return new TextEndOfLine(2); } catch (Exception ex) { diff --git a/src/AvaloniaEdit/Text/StringRange.cs b/src/AvaloniaEdit/Text/StringRange.cs deleted file mode 100644 index 0e2dd54f..00000000 --- a/src/AvaloniaEdit/Text/StringRange.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System; - -namespace AvaloniaEdit.Text -{ - -} \ No newline at end of file diff --git a/src/AvaloniaEdit/Text/TextCharacters.cs b/src/AvaloniaEdit/Text/TextCharacters.cs deleted file mode 100644 index 9fa375f8..00000000 --- a/src/AvaloniaEdit/Text/TextCharacters.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace AvaloniaEdit.Text -{ -} \ No newline at end of file diff --git a/src/AvaloniaEdit/Text/TextEmbeddedObject.cs b/src/AvaloniaEdit/Text/TextEmbeddedObject.cs deleted file mode 100644 index ee3c1c85..00000000 --- a/src/AvaloniaEdit/Text/TextEmbeddedObject.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Avalonia; -using Avalonia.Media; -using Avalonia.Media.TextFormatting; - -namespace AvaloniaEdit.Text -{ - public abstract class TextEmbeddedObject : TextRun - { - public abstract bool HasFixedSize { get; } - - public abstract Size GetSize(double remainingParagraphWidth); - - public abstract Rect ComputeBoundingBox(); - - public abstract void Draw(DrawingContext drawingContext, Point origin); - } -} \ No newline at end of file diff --git a/src/AvaloniaEdit/Text/TextEndOfLine.cs b/src/AvaloniaEdit/Text/TextEndOfLine.cs deleted file mode 100644 index d8b92c47..00000000 --- a/src/AvaloniaEdit/Text/TextEndOfLine.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace AvaloniaEdit.Text -{ - -} \ No newline at end of file diff --git a/src/AvaloniaEdit/Text/TextEndOfParagraph.cs b/src/AvaloniaEdit/Text/TextEndOfParagraph.cs deleted file mode 100644 index d7567d92..00000000 --- a/src/AvaloniaEdit/Text/TextEndOfParagraph.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace AvaloniaEdit.Text -{ - -} \ No newline at end of file diff --git a/src/AvaloniaEdit/Text/TextFormatter.cs b/src/AvaloniaEdit/Text/TextFormatter.cs deleted file mode 100644 index 48369715..00000000 --- a/src/AvaloniaEdit/Text/TextFormatter.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace AvaloniaEdit.Text -{ -} \ No newline at end of file diff --git a/src/AvaloniaEdit/Text/TextLine.cs b/src/AvaloniaEdit/Text/TextLine.cs deleted file mode 100644 index fa2ae7d8..00000000 --- a/src/AvaloniaEdit/Text/TextLine.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Collections.Generic; -using Avalonia; -using Avalonia.Media; - -namespace AvaloniaEdit.Text -{ - -} \ No newline at end of file diff --git a/src/AvaloniaEdit/Text/TextLineImpl.cs b/src/AvaloniaEdit/Text/TextLineImpl.cs deleted file mode 100644 index a9611939..00000000 --- a/src/AvaloniaEdit/Text/TextLineImpl.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia; -using AvaloniaEdit.Utils; -using Avalonia.Media; - -namespace AvaloniaEdit.Text -{ - -} \ No newline at end of file diff --git a/src/AvaloniaEdit/Text/TextLineRun.cs b/src/AvaloniaEdit/Text/TextLineRun.cs deleted file mode 100644 index 3db4d5a0..00000000 --- a/src/AvaloniaEdit/Text/TextLineRun.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -using Avalonia; -using Avalonia.Media; -using Avalonia.Media.TextFormatting; -using Avalonia.Media.TextFormatting.Unicode; -using Avalonia.Utilities; - -using AvaloniaEdit.Rendering; - -namespace AvaloniaEdit.Text -{ - -} \ No newline at end of file diff --git a/src/AvaloniaEdit/Text/TextParagraphProperties.cs b/src/AvaloniaEdit/Text/TextParagraphProperties.cs deleted file mode 100644 index e2688281..00000000 --- a/src/AvaloniaEdit/Text/TextParagraphProperties.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Avalonia.Media; - -namespace AvaloniaEdit.Text -{ - -} diff --git a/src/AvaloniaEdit/Text/TextRun.cs b/src/AvaloniaEdit/Text/TextRun.cs deleted file mode 100644 index dd6f40c1..00000000 --- a/src/AvaloniaEdit/Text/TextRun.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace AvaloniaEdit.Text -{ - -} \ No newline at end of file diff --git a/src/AvaloniaEdit/Text/TextRunExtensions.cs b/src/AvaloniaEdit/Text/TextRunExtensions.cs deleted file mode 100644 index 5e0c4b72..00000000 --- a/src/AvaloniaEdit/Text/TextRunExtensions.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace AvaloniaEdit.Text -{ - internal static class TextRunExtensions - { - - } -} \ No newline at end of file diff --git a/src/AvaloniaEdit/Text/TextRunProperties.cs b/src/AvaloniaEdit/Text/TextRunProperties.cs deleted file mode 100644 index a100d3aa..00000000 --- a/src/AvaloniaEdit/Text/TextRunProperties.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Globalization; -using Avalonia.Media; -using Avalonia.Media.TextFormatting; - -namespace AvaloniaEdit.Text -{ - -} diff --git a/src/AvaloniaEdit/Text/TextSource.cs b/src/AvaloniaEdit/Text/TextSource.cs deleted file mode 100644 index dd6f40c1..00000000 --- a/src/AvaloniaEdit/Text/TextSource.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace AvaloniaEdit.Text -{ - -} \ No newline at end of file diff --git a/src/AvaloniaEdit/Text/TrailingInfo.cs b/src/AvaloniaEdit/Text/TrailingInfo.cs deleted file mode 100644 index 7d37c9b7..00000000 --- a/src/AvaloniaEdit/Text/TrailingInfo.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace AvaloniaEdit.Text -{ - internal sealed class TrailingInfo - { - public int Count { get; set; } - public double SpaceWidth { get; set; } - } -} \ No newline at end of file diff --git a/src/AvaloniaEdit/Utils/TextFormatterFactory.cs b/src/AvaloniaEdit/Utils/TextFormatterFactory.cs index 5ed444af..fd00a89e 100644 --- a/src/AvaloniaEdit/Utils/TextFormatterFactory.cs +++ b/src/AvaloniaEdit/Utils/TextFormatterFactory.cs @@ -16,13 +16,9 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -using System; -using AvaloniaEdit.Text; -using Avalonia.Controls; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Utilities; -using AvaloniaEdit.Rendering; using TextLine = Avalonia.Media.TextFormatting.TextLine; using TextRun = Avalonia.Media.TextFormatting.TextRun; using TextRunProperties = Avalonia.Media.TextFormatting.TextRunProperties; From 5e228f0243af27e83c6dc544bc842841aee28ac4 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 9 Mar 2022 15:49:12 +0100 Subject: [PATCH 04/24] Use latest Avalonia preview --- src/AvaloniaEdit.Demo/AvaloniaEdit.Demo.csproj | 2 +- src/AvaloniaEdit/AvaloniaEdit.csproj | 2 +- src/AvaloniaEdit/Editing/LineNumberMargin.cs | 2 +- .../Rendering/BackgroundGeometryBuilder.cs | 5 +++++ .../Rendering/ITextRunConstructionContext.cs | 2 +- src/AvaloniaEdit/Rendering/LinkElementGenerator.cs | 4 ++-- src/AvaloniaEdit/Rendering/VisualLineElement.cs | 4 ++-- src/AvaloniaEdit/Rendering/VisualLineText.cs | 8 +++++--- src/AvaloniaEdit/Rendering/VisualLineTextSource.cs | 11 +++++------ test/AvaloniaEdit.Tests/AvaloniaEdit.Tests.csproj | 2 +- 10 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/AvaloniaEdit.Demo/AvaloniaEdit.Demo.csproj b/src/AvaloniaEdit.Demo/AvaloniaEdit.Demo.csproj index 6666011d..c1882725 100644 --- a/src/AvaloniaEdit.Demo/AvaloniaEdit.Demo.csproj +++ b/src/AvaloniaEdit.Demo/AvaloniaEdit.Demo.csproj @@ -30,7 +30,7 @@ - + diff --git a/src/AvaloniaEdit/AvaloniaEdit.csproj b/src/AvaloniaEdit/AvaloniaEdit.csproj index 806270a7..eabd2a3b 100644 --- a/src/AvaloniaEdit/AvaloniaEdit.csproj +++ b/src/AvaloniaEdit/AvaloniaEdit.csproj @@ -33,7 +33,7 @@ - + diff --git a/src/AvaloniaEdit/Editing/LineNumberMargin.cs b/src/AvaloniaEdit/Editing/LineNumberMargin.cs index 3cce11c6..86c2511d 100644 --- a/src/AvaloniaEdit/Editing/LineNumberMargin.cs +++ b/src/AvaloniaEdit/Editing/LineNumberMargin.cs @@ -82,7 +82,7 @@ public override void Render(DrawingContext drawingContext) var textLine = TextFormatterFactory.FormatLine(text.AsMemory(), Typeface, EmSize, - GetValue(TemplatedControl.ForegroundProperty) + foreground ); var y = line.TextLines.Count > 0 diff --git a/src/AvaloniaEdit/Rendering/BackgroundGeometryBuilder.cs b/src/AvaloniaEdit/Rendering/BackgroundGeometryBuilder.cs index 1d25abba..50de0f5c 100644 --- a/src/AvaloniaEdit/Rendering/BackgroundGeometryBuilder.cs +++ b/src/AvaloniaEdit/Rendering/BackgroundGeometryBuilder.cs @@ -180,6 +180,11 @@ public static IEnumerable GetRectsFromVisualSegment(TextView textView, Vis private static IEnumerable ProcessTextLines(TextView textView, VisualLine visualLine, int segmentStartVc, int segmentEndVc) { + if (visualLine.TextLines.Count == 0) + { + yield break; + } + var lastTextLine = visualLine.TextLines.Last(); var scrollOffset = textView.ScrollOffset; diff --git a/src/AvaloniaEdit/Rendering/ITextRunConstructionContext.cs b/src/AvaloniaEdit/Rendering/ITextRunConstructionContext.cs index 8ddeb3b8..7094f0e8 100644 --- a/src/AvaloniaEdit/Rendering/ITextRunConstructionContext.cs +++ b/src/AvaloniaEdit/Rendering/ITextRunConstructionContext.cs @@ -61,7 +61,7 @@ public interface ITextRunConstructionContext /// This method should be the preferred text access method in the text transformation pipeline, as it can avoid repeatedly allocating string instances /// for text within the same line. /// - ReadOnlySlice GetText(int offset, int length); + string GetText(int offset, int length); } public class CustomTextRunProperties : TextRunProperties diff --git a/src/AvaloniaEdit/Rendering/LinkElementGenerator.cs b/src/AvaloniaEdit/Rendering/LinkElementGenerator.cs index cefb8504..bbfe6d3c 100644 --- a/src/AvaloniaEdit/Rendering/LinkElementGenerator.cs +++ b/src/AvaloniaEdit/Rendering/LinkElementGenerator.cs @@ -73,8 +73,8 @@ private Match GetMatch(int startOffset, out int matchOffset) { var endOffset = CurrentContext.VisualLine.LastDocumentLine.EndOffset; var relevantText = CurrentContext.GetText(startOffset, endOffset - startOffset); - var m = _linkRegex.Match(relevantText.Span.ToString()); - matchOffset = m.Success ? m.Index - relevantText.Start + startOffset : -1; + var m = _linkRegex.Match(relevantText); + matchOffset = m.Success ? m.Index - startOffset : -1; return m; } diff --git a/src/AvaloniaEdit/Rendering/VisualLineElement.cs b/src/AvaloniaEdit/Rendering/VisualLineElement.cs index 4873beda..f55c22cb 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineElement.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineElement.cs @@ -106,9 +106,9 @@ internal void SetTextRunProperties(CustomTextRunProperties p) /// Retrieves the text span immediately before the visual column. /// /// This method is used for word-wrapping in bidirectional text. - public virtual ReadOnlySlice GetPrecedingText(int visualColumnLimit, ITextRunConstructionContext context) + public virtual string GetPrecedingText(int visualColumnLimit, ITextRunConstructionContext context) { - return ReadOnlySlice.Empty; + return string.Empty; } /// diff --git a/src/AvaloniaEdit/Rendering/VisualLineText.cs b/src/AvaloniaEdit/Rendering/VisualLineText.cs index faaaa730..157ce5cd 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineText.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineText.cs @@ -61,10 +61,12 @@ public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructio throw new ArgumentNullException(nameof(context)); var relativeOffset = startVisualColumn - VisualColumn; + + var offset = context.VisualLine.FirstDocumentLine.Offset + RelativeTextOffset + relativeOffset; - var text = context.GetText(context.VisualLine.FirstDocumentLine.Offset + RelativeTextOffset + relativeOffset, DocumentLength - relativeOffset); + var text = context.GetText(offset, DocumentLength - relativeOffset); - return new TextCharacters(text, TextRunProperties); + return new TextCharacters(new ReadOnlySlice(text.AsMemory()), TextRunProperties); } /// @@ -75,7 +77,7 @@ public override bool IsWhitespace(int visualColumn) } /// - public override ReadOnlySlice GetPrecedingText(int visualColumnLimit, ITextRunConstructionContext context) + public override string GetPrecedingText(int visualColumnLimit, ITextRunConstructionContext context) { if (context == null) throw new ArgumentNullException(nameof(context)); diff --git a/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs b/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs index ac177a30..8d2199b5 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs @@ -115,22 +115,21 @@ private TextRun CreateTextRunForNewLine() return new FormattedTextRun(new FormattedTextElement(TextView.CachedElements.GetTextForNonPrintableCharacter(newlineText, this), 0), GlobalTextRunProperties); } - private ReadOnlySlice _cachedString; + private string _cachedString; private int _cachedStringOffset; - public ReadOnlySlice GetText(int offset, int length) + public string GetText(int offset, int length) { - if (!_cachedString.IsEmpty) + if (_cachedString != null) { if (offset >= _cachedStringOffset && offset + length <= _cachedStringOffset + _cachedString.Length) { - return new ReadOnlySlice(_cachedString.Buffer, offset, length, offset - _cachedStringOffset); + return _cachedString.Substring(offset - _cachedStringOffset, length); } } _cachedStringOffset = offset; - - _cachedString = new ReadOnlySlice(Document.GetText(offset, length).AsMemory(), offset, length); + _cachedString = Document.GetText(offset, length); return _cachedString; } diff --git a/test/AvaloniaEdit.Tests/AvaloniaEdit.Tests.csproj b/test/AvaloniaEdit.Tests/AvaloniaEdit.Tests.csproj index 4fd179d5..6fd6158d 100644 --- a/test/AvaloniaEdit.Tests/AvaloniaEdit.Tests.csproj +++ b/test/AvaloniaEdit.Tests/AvaloniaEdit.Tests.csproj @@ -5,7 +5,7 @@ - + From 6684fe667c997d7f23fb820699f173a3e6c2f32b Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 9 Mar 2022 16:03:14 +0100 Subject: [PATCH 05/24] Fix CreateTextRun --- src/AvaloniaEdit/Rendering/VisualLineText.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AvaloniaEdit/Rendering/VisualLineText.cs b/src/AvaloniaEdit/Rendering/VisualLineText.cs index 157ce5cd..274ec5fd 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineText.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineText.cs @@ -66,7 +66,7 @@ public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructio var text = context.GetText(offset, DocumentLength - relativeOffset); - return new TextCharacters(new ReadOnlySlice(text.AsMemory()), TextRunProperties); + return new TextCharacters(new ReadOnlySlice(text.AsMemory(), offset, text.Length), TextRunProperties); } /// From 61526254a0c7976dc62d1e59e101c0cc8fcbfc59 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 9 Mar 2022 17:37:40 +0100 Subject: [PATCH 06/24] Fix LinkElementGenerator --- Directory.Build.props | 2 +- .../AvaloniaEdit.Demo.csproj | 2 +- src/AvaloniaEdit/AvaloniaEdit.csproj | 2 +- .../Rendering/ITextRunConstructionContext.cs | 49 +++++++++++-- .../Rendering/LinkElementGenerator.cs | 2 +- src/AvaloniaEdit/Rendering/VisualLineText.cs | 2 +- .../Utils/TextFormatterFactory.cs | 6 +- .../AvaloniaEdit.Tests.csproj | 2 +- .../AvaloniaMocks/MockFontManagerImpl.cs | 10 +-- .../AvaloniaMocks/MockFormattedTextImpl.cs | 45 ------------ .../MockPlatformRenderInterface.cs | 20 +----- .../AvaloniaMocks/MockTextShaperImpl.cs | 18 +++++ .../AvaloniaMocks/TestServices.cs | 25 +++---- .../AvaloniaMocks/UnitTestApplication.cs | 2 +- .../Rendering/TextViewTests.cs | 8 +-- .../Text/TextLineRunTests.cs | 72 ++----------------- 16 files changed, 95 insertions(+), 172 deletions(-) delete mode 100644 test/AvaloniaEdit.Tests/AvaloniaMocks/MockFormattedTextImpl.cs create mode 100644 test/AvaloniaEdit.Tests/AvaloniaMocks/MockTextShaperImpl.cs diff --git a/Directory.Build.props b/Directory.Build.props index 83878b63..a55d4d1f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ latest true - 0.10.12 + 0.10.999-cibuild0019136-beta 1.0.31 13.0.1 0.10.12.2 diff --git a/src/AvaloniaEdit.Demo/AvaloniaEdit.Demo.csproj b/src/AvaloniaEdit.Demo/AvaloniaEdit.Demo.csproj index c1882725..9f886662 100644 --- a/src/AvaloniaEdit.Demo/AvaloniaEdit.Demo.csproj +++ b/src/AvaloniaEdit.Demo/AvaloniaEdit.Demo.csproj @@ -30,7 +30,7 @@ - + diff --git a/src/AvaloniaEdit/AvaloniaEdit.csproj b/src/AvaloniaEdit/AvaloniaEdit.csproj index eabd2a3b..9a527395 100644 --- a/src/AvaloniaEdit/AvaloniaEdit.csproj +++ b/src/AvaloniaEdit/AvaloniaEdit.csproj @@ -33,7 +33,7 @@ - + diff --git a/src/AvaloniaEdit/Rendering/ITextRunConstructionContext.cs b/src/AvaloniaEdit/Rendering/ITextRunConstructionContext.cs index 7094f0e8..a74f4781 100644 --- a/src/AvaloniaEdit/Rendering/ITextRunConstructionContext.cs +++ b/src/AvaloniaEdit/Rendering/ITextRunConstructionContext.cs @@ -19,7 +19,6 @@ using System.Globalization; using Avalonia.Media; using Avalonia.Media.TextFormatting; -using Avalonia.Utilities; using AvaloniaEdit.Document; using AvaloniaEdit.Utils; @@ -64,8 +63,10 @@ public interface ITextRunConstructionContext string GetText(int offset, int length); } - public class CustomTextRunProperties : TextRunProperties + public sealed class CustomTextRunProperties : TextRunProperties { + public const double DefaultFontRenderingEmSize = 12; + private Typeface _typeface; private double _fontRenderingEmSize; private TextDecorationCollection? _textDecorations; @@ -74,9 +75,13 @@ public class CustomTextRunProperties : TextRunProperties private CultureInfo? _cultureInfo; private BaselineAlignment _baselineAlignment; - internal CustomTextRunProperties(Typeface typeface, double fontRenderingEmSize, - TextDecorationCollection? textDecorations, IBrush? foregroundBrush, IBrush? backgroundBrush, - CultureInfo? cultureInfo, BaselineAlignment baselineAlignment) + internal CustomTextRunProperties(Typeface typeface, + double fontRenderingEmSize = 12, + TextDecorationCollection? textDecorations = null, + IBrush? foregroundBrush = null, + IBrush? backgroundBrush = null, + CultureInfo? cultureInfo = null, + BaselineAlignment baselineAlignment = BaselineAlignment.Baseline) { _typeface = typeface; _fontRenderingEmSize = fontRenderingEmSize; @@ -132,4 +137,38 @@ public void SetTextDecorations(TextDecorationCollection textDecorations) _textDecorations = textDecorations; } } + + public sealed class CustomTextParagraphProperties : TextParagraphProperties + { + public const double DefaultIncrementalTabWidth = 4 * CustomTextRunProperties.DefaultFontRenderingEmSize; + + private TextWrapping _textWrapping; + private double _lineHeight; + private double _indent; + private double _defaultIncrementalTab; + private readonly bool _firstLineInParagraph; + + public CustomTextParagraphProperties(TextRunProperties defaultTextRunProperties, + bool firstLineInParagraph = true, + TextWrapping textWrapping = TextWrapping.NoWrap, + double lineHeight = 0, + double indent = 0, + double defaultIncrementalTab = DefaultIncrementalTabWidth) + { + DefaultTextRunProperties = defaultTextRunProperties; + _firstLineInParagraph = firstLineInParagraph; + _textWrapping = textWrapping; + _lineHeight = lineHeight; + _indent = indent; + _defaultIncrementalTab = defaultIncrementalTab; + } + + public override FlowDirection FlowDirection => FlowDirection.LeftToRight; + public override TextAlignment TextAlignment => TextAlignment.Left; + public override double LineHeight => _lineHeight; + public override bool FirstLineInParagraph => _firstLineInParagraph; + public override TextRunProperties DefaultTextRunProperties { get; } + public override TextWrapping TextWrapping => _textWrapping; + public override double Indent => _indent; + } } diff --git a/src/AvaloniaEdit/Rendering/LinkElementGenerator.cs b/src/AvaloniaEdit/Rendering/LinkElementGenerator.cs index bbfe6d3c..d585a03e 100644 --- a/src/AvaloniaEdit/Rendering/LinkElementGenerator.cs +++ b/src/AvaloniaEdit/Rendering/LinkElementGenerator.cs @@ -74,7 +74,7 @@ private Match GetMatch(int startOffset, out int matchOffset) var endOffset = CurrentContext.VisualLine.LastDocumentLine.EndOffset; var relevantText = CurrentContext.GetText(startOffset, endOffset - startOffset); var m = _linkRegex.Match(relevantText); - matchOffset = m.Success ? m.Index - startOffset : -1; + matchOffset = m.Success ? m.Index + startOffset : -1; return m; } diff --git a/src/AvaloniaEdit/Rendering/VisualLineText.cs b/src/AvaloniaEdit/Rendering/VisualLineText.cs index 274ec5fd..ec0dab60 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineText.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineText.cs @@ -65,7 +65,7 @@ public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructio var offset = context.VisualLine.FirstDocumentLine.Offset + RelativeTextOffset + relativeOffset; var text = context.GetText(offset, DocumentLength - relativeOffset); - + return new TextCharacters(new ReadOnlySlice(text.AsMemory(), offset, text.Length), TextRunProperties); } diff --git a/src/AvaloniaEdit/Utils/TextFormatterFactory.cs b/src/AvaloniaEdit/Utils/TextFormatterFactory.cs index fd00a89e..002cfa63 100644 --- a/src/AvaloniaEdit/Utils/TextFormatterFactory.cs +++ b/src/AvaloniaEdit/Utils/TextFormatterFactory.cs @@ -19,6 +19,7 @@ using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Utilities; +using AvaloniaEdit.Rendering; using TextLine = Avalonia.Media.TextFormatting.TextLine; using TextRun = Avalonia.Media.TextFormatting.TextRun; using TextRunProperties = Avalonia.Media.TextFormatting.TextRunProperties; @@ -41,9 +42,8 @@ public static class TextFormatterFactory /// A FormattedText object using the specified settings. public static TextLine FormatLine(ReadOnlySlice text, Typeface typeface, double emSize, IBrush foreground) { - var defaultProperties = new GenericTextRunProperties(typeface, emSize, null, foreground); - var paragraphProperties = new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, - true, false, defaultProperties, TextWrapping.NoWrap, 0, 0); + var defaultProperties = new CustomTextRunProperties(typeface, emSize, null, foreground); + var paragraphProperties = new CustomTextParagraphProperties(defaultProperties); var textSource = new SimpleTextSource(text, defaultProperties); diff --git a/test/AvaloniaEdit.Tests/AvaloniaEdit.Tests.csproj b/test/AvaloniaEdit.Tests/AvaloniaEdit.Tests.csproj index 6fd6158d..f0068349 100644 --- a/test/AvaloniaEdit.Tests/AvaloniaEdit.Tests.csproj +++ b/test/AvaloniaEdit.Tests/AvaloniaEdit.Tests.csproj @@ -5,7 +5,7 @@ - + diff --git a/test/AvaloniaEdit.Tests/AvaloniaMocks/MockFontManagerImpl.cs b/test/AvaloniaEdit.Tests/AvaloniaMocks/MockFontManagerImpl.cs index d86c0a34..be8fdd5c 100644 --- a/test/AvaloniaEdit.Tests/AvaloniaMocks/MockFontManagerImpl.cs +++ b/test/AvaloniaEdit.Tests/AvaloniaMocks/MockFontManagerImpl.cs @@ -6,6 +6,8 @@ using System.Collections.Generic; using System.Globalization; +#nullable enable + namespace AvaloniaEdit.AvaloniaMocks { public class MockFontManagerImpl : IFontManagerImpl @@ -27,12 +29,12 @@ public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = fa return new[] { _defaultFamilyName }; } - public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, - CultureInfo culture, out Typeface fontKey) + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, + FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface) { - fontKey = new Typeface(_defaultFamilyName); + typeface = new Typeface(_defaultFamilyName); - return false; + return true; } public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) diff --git a/test/AvaloniaEdit.Tests/AvaloniaMocks/MockFormattedTextImpl.cs b/test/AvaloniaEdit.Tests/AvaloniaMocks/MockFormattedTextImpl.cs deleted file mode 100644 index b8aa2384..00000000 --- a/test/AvaloniaEdit.Tests/AvaloniaMocks/MockFormattedTextImpl.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Avalonia; -using Avalonia.Media; -using Avalonia.Platform; - -using System.Collections.Generic; - -namespace AvaloniaEdit.Tests.AvaloniaMocks -{ - internal class MockFormattedTextImpl : IFormattedTextImpl - { - private string _text; - private Typeface _typeface; - - internal MockFormattedTextImpl(string text, Typeface typeface) - { - _text = text; - _typeface = typeface; - } - Size IFormattedTextImpl.Constraint => new Size(0, 0); - - Rect IFormattedTextImpl.Bounds => new Rect(0, 0, _text.Length * _typeface.GlyphTypeface.GetGlyphAdvance(0), 18); - - string IFormattedTextImpl.Text => _text; - - IEnumerable IFormattedTextImpl.GetLines() - { - return null; - } - - TextHitTestResult IFormattedTextImpl.HitTestPoint(Point point) - { - return null; - } - - Rect IFormattedTextImpl.HitTestTextPosition(int index) - { - return Rect.Empty; - } - - IEnumerable IFormattedTextImpl.HitTestTextRange(int index, int length) - { - return new Rect[] { }; - } - } -} diff --git a/test/AvaloniaEdit.Tests/AvaloniaMocks/MockPlatformRenderInterface.cs b/test/AvaloniaEdit.Tests/AvaloniaMocks/MockPlatformRenderInterface.cs index e0a0c610..018c7210 100644 --- a/test/AvaloniaEdit.Tests/AvaloniaMocks/MockPlatformRenderInterface.cs +++ b/test/AvaloniaEdit.Tests/AvaloniaMocks/MockPlatformRenderInterface.cs @@ -6,8 +6,6 @@ using Avalonia.Platform; using Avalonia.Visuals.Media.Imaging; -using AvaloniaEdit.Tests.AvaloniaMocks; - using Moq; namespace AvaloniaEdit.AvaloniaMocks @@ -22,17 +20,6 @@ public class MockPlatformRenderInterface : IPlatformRenderInterface public PixelFormat DefaultPixelFormat => throw new NotImplementedException(); - public IFormattedTextImpl CreateFormattedText( - string text, - Typeface typeface, - TextAlignment textAlignment, - TextWrapping wrapping, - Size constraint, - IReadOnlyList spans) - { - return Mock.Of(); - } - public IGeometryImpl CreateEllipseGeometry(Rect rect) { throw new NotImplementedException(); @@ -92,11 +79,6 @@ public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, V throw new NotImplementedException(); } - public IFormattedTextImpl CreateFormattedText(string text, Typeface typeface, double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, IReadOnlyList spans) - { - return new MockFormattedTextImpl(text, typeface); - } - public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) { throw new NotImplementedException(); @@ -152,7 +134,7 @@ public IBitmapImpl LoadBitmap(PixelFormat format, AlphaFormat alphaFormat, IntPt throw new NotImplementedException(); } - public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) + public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun) { throw new NotImplementedException(); } diff --git a/test/AvaloniaEdit.Tests/AvaloniaMocks/MockTextShaperImpl.cs b/test/AvaloniaEdit.Tests/AvaloniaMocks/MockTextShaperImpl.cs new file mode 100644 index 00000000..c3647b39 --- /dev/null +++ b/test/AvaloniaEdit.Tests/AvaloniaMocks/MockTextShaperImpl.cs @@ -0,0 +1,18 @@ +using System.Globalization; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace AvaloniaEdit.AvaloniaMocks; + +#nullable enable + +public class MockTextShaperImpl : ITextShaperImpl +{ + public ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize, CultureInfo? culture, + sbyte bidiLevel) + { + throw new System.NotImplementedException(); + } +} \ No newline at end of file diff --git a/test/AvaloniaEdit.Tests/AvaloniaMocks/TestServices.cs b/test/AvaloniaEdit.Tests/AvaloniaMocks/TestServices.cs index f7ec8a8d..f204e339 100644 --- a/test/AvaloniaEdit.Tests/AvaloniaMocks/TestServices.cs +++ b/test/AvaloniaEdit.Tests/AvaloniaMocks/TestServices.cs @@ -5,12 +5,10 @@ using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Layout; -using Avalonia.Markup.Xaml; -using Avalonia.Markup.Xaml.Styling; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.PlatformSupport; using Avalonia.Rendering; -using Avalonia.Shared.PlatformSupport; using Avalonia.Styling; using Avalonia.Themes.Default; using Moq; @@ -28,7 +26,8 @@ public class TestServices theme: () => CreateDefaultTheme(), threadingInterface: Mock.Of(x => x.CurrentThreadIsLoopThread == true), windowingPlatform: new MockWindowingPlatform(), - fontManagerImpl: new MockFontManagerImpl()); + fontManagerImpl: new MockFontManagerImpl(), + textShaperImpl: new MockTextShaperImpl()); public static readonly TestServices MockPlatformRenderInterface = new TestServices( renderInterface: new MockPlatformRenderInterface()); @@ -74,7 +73,7 @@ public TestServices( IWindowingPlatform windowingPlatform = null, PlatformHotkeyConfiguration platformHotkeyConfiguration = null, IFontManagerImpl fontManagerImpl = null, - IFormattedTextImpl formattedTextImpl = null) + ITextShaperImpl textShaperImpl = null) { AssetLoader = assetLoader; FocusManager = focusManager; @@ -94,7 +93,6 @@ public TestServices( WindowingPlatform = windowingPlatform; PlatformHotkeyConfiguration = platformHotkeyConfiguration; FontManagerImpl = fontManagerImpl; - FormattedTextImpl = formattedTextImpl; } public IAssetLoader AssetLoader { get; } @@ -115,7 +113,8 @@ public TestServices( public IWindowingPlatform WindowingPlatform { get; } public PlatformHotkeyConfiguration PlatformHotkeyConfiguration { get; } public IFontManagerImpl FontManagerImpl { get; } - public IFormattedTextImpl FormattedTextImpl { get; } + + public ITextShaperImpl TextShaperImpl { get; } public TestServices With( IAssetLoader assetLoader = null, @@ -137,7 +136,7 @@ public TestServices With( IWindowingPlatform windowingPlatform = null, PlatformHotkeyConfiguration platformHotkeyConfiguration = null, IFontManagerImpl fontManagerImpl = null, - IFormattedTextImpl formattedTextImpl = null) + ITextShaperImpl textShaperImpl = null) { return new TestServices( assetLoader: assetLoader ?? AssetLoader, @@ -158,7 +157,7 @@ public TestServices With( windowImpl: windowImpl ?? WindowImpl, platformHotkeyConfiguration: platformHotkeyConfiguration ?? PlatformHotkeyConfiguration, fontManagerImpl: fontManagerImpl ?? FontManagerImpl, - formattedTextImpl : formattedTextImpl ?? FormattedTextImpl); + textShaperImpl: textShaperImpl ?? TextShaperImpl); } private static Styles CreateDefaultTheme() @@ -174,14 +173,6 @@ private static Styles CreateDefaultTheme() private static IPlatformRenderInterface CreateRenderInterfaceMock() { return Mock.Of(x => - x.CreateFormattedText( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>()) == Mock.Of() && x.CreateStreamGeometry() == Mock.Of( y => y.Open() == Mock.Of())); } diff --git a/test/AvaloniaEdit.Tests/AvaloniaMocks/UnitTestApplication.cs b/test/AvaloniaEdit.Tests/AvaloniaMocks/UnitTestApplication.cs index 9c74326b..a8b0332e 100644 --- a/test/AvaloniaEdit.Tests/AvaloniaMocks/UnitTestApplication.cs +++ b/test/AvaloniaEdit.Tests/AvaloniaMocks/UnitTestApplication.cs @@ -58,7 +58,7 @@ public override void RegisterServices() .Bind().ToConstant(Services.WindowingPlatform) .Bind().ToConstant(Services.PlatformHotkeyConfiguration) .Bind().ToConstant(Services.FontManagerImpl) - .Bind().ToConstant(Services.FormattedTextImpl); + .Bind().ToConstant(Services.TextShaperImpl); var styles = Services.Theme?.Invoke(); if (styles != null) diff --git a/test/AvaloniaEdit.Tests/Rendering/TextViewTests.cs b/test/AvaloniaEdit.Tests/Rendering/TextViewTests.cs index bd6edd18..9b57ff72 100644 --- a/test/AvaloniaEdit.Tests/Rendering/TextViewTests.cs +++ b/test/AvaloniaEdit.Tests/Rendering/TextViewTests.cs @@ -1,10 +1,8 @@ using Avalonia.Controls; using Avalonia.Controls.Primitives; - using AvaloniaEdit.AvaloniaMocks; using AvaloniaEdit.Document; using AvaloniaEdit.Rendering; -using AvaloniaEdit.Text; using NUnit.Framework; @@ -34,8 +32,8 @@ public void Visual_Line_Should_Create_Two_Text_Lines_When_Wrapping() VisualLine visualLine = textView.GetOrConstructVisualLine(document.Lines[0]); Assert.AreEqual(2, visualLine.TextLines.Count); - Assert.AreEqual("hello ", ((TextLineImpl)visualLine.TextLines[0]).LineRuns[0].StringRange.ToString()); - Assert.AreEqual("world", ((TextLineImpl)visualLine.TextLines[1]).LineRuns[0].StringRange.ToString()); + Assert.AreEqual("hello ", new string(visualLine.TextLines[0].TextRuns[0].Text.Buffer.Span)); + Assert.AreEqual("world", new string(visualLine.TextLines[1].TextRuns[0].Text.Buffer.Span)); window.Close(); } @@ -61,7 +59,7 @@ public void Visual_Line_Should_Create_One_Text_Lines_When_Not_Wrapping() VisualLine visualLine = textView.GetOrConstructVisualLine(document.Lines[0]); Assert.AreEqual(1, visualLine.TextLines.Count); - Assert.AreEqual("hello world", ((TextLineImpl)visualLine.TextLines[0]).LineRuns[0].StringRange.ToString()); + Assert.AreEqual("hello world", new string(visualLine.TextLines[0].TextRuns[0].Text.Buffer.Span)); window.Close(); } diff --git a/test/AvaloniaEdit.Tests/Text/TextLineRunTests.cs b/test/AvaloniaEdit.Tests/Text/TextLineRunTests.cs index ccb18ad3..ae848620 100644 --- a/test/AvaloniaEdit.Tests/Text/TextLineRunTests.cs +++ b/test/AvaloniaEdit.Tests/Text/TextLineRunTests.cs @@ -1,5 +1,6 @@ using Avalonia; using Avalonia.Media; +using Avalonia.Media.TextFormatting; using Avalonia.Platform; using AvaloniaEdit.AvaloniaMocks; @@ -256,79 +257,16 @@ public void Text_Line_Run_Should_Not_Wrap_Line_When_There_Is_Enough_Available_Sp Assert.AreEqual(10, run.Length); } - - [Test] - public void Text_Line_Run_Should_Perform_Word_Wrap_Line_When_Space_Found() - { - using var app = UnitTestApplication.Start(new TestServices().With( - renderInterface: new MockPlatformRenderInterface(), - fontManagerImpl: new MockFontManagerImpl())); - - SimpleTextSource s = new SimpleTextSource( - "0123456789 0123456789", - CreateDefaultTextProperties()); - - var paragraphProperties = CreateDefaultParagraphProperties(); - - TextLineRun run = TextLineRun.Create(s, 0, 0, MockGlyphTypeface.GlyphAdvance * 13, paragraphProperties); - - Assert.AreEqual(11, run.Length); - } - - [Test] - public void Text_Line_Run_Should_Perform_Character_Line_Wrapping_When_Space_Not_Found() - { - using var app = UnitTestApplication.Start(new TestServices().With( - renderInterface: new MockPlatformRenderInterface(), - fontManagerImpl: new MockFontManagerImpl(), - formattedTextImpl: Mock.Of())); - - SimpleTextSource s = new SimpleTextSource( - "0123456789", - CreateDefaultTextProperties()); - - var paragraphProperties = CreateDefaultParagraphProperties(); - - TextLineRun run = TextLineRun.Create(s, 0, 0, MockGlyphTypeface.GlyphAdvance * 3, paragraphProperties); - - Assert.AreEqual(3, run.Length); - } - - [Test] - public void Text_Line_Run_Should_Update_StringRange_When_Word_Wrap() - { - using var app = UnitTestApplication.Start(new TestServices().With( - renderInterface: new MockPlatformRenderInterface(), - fontManagerImpl: new MockFontManagerImpl())); - - SimpleTextSource s = new SimpleTextSource( - "0123456789 0123456789", - CreateDefaultTextProperties()); - - var paragraphProperties = CreateDefaultParagraphProperties(); - - TextLineRun run = TextLineRun.Create(s, 0, 0, MockGlyphTypeface.GlyphAdvance * 13, paragraphProperties); - - Assert.AreEqual("0123456789 ", run.StringRange.ToString()); - } - + TextRunProperties CreateDefaultTextProperties() { - return new TextRunProperties() - { - Typeface = new Typeface("Default"), - FontSize = MockGlyphTypeface.DefaultFontSize, - }; + return new CustomTextRunProperties(new Typeface("Default"), MockGlyphTypeface.DefaultFontSize); } TextParagraphProperties CreateDefaultParagraphProperties() { - return new TextParagraphProperties() - { - DefaultTextRunProperties = CreateDefaultTextProperties(), - DefaultIncrementalTab = 70, - Indent = 4, - }; + return new CustomTextParagraphProperties(CreateDefaultTextProperties(), defaultIncrementalTab: 70, + indent: 4); } } } From 19dc0f3bcb2c2f2b7d27af93448740cfae52774c Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 10 Mar 2022 14:48:52 +0100 Subject: [PATCH 07/24] Redo Avalonia port of AvalonEdit.Rendering --- Directory.Build.props | 2 +- src/AvaloniaEdit.Demo/MainWindow.xaml.cs | 12 +- src/AvaloniaEdit/Editing/Caret.cs | 78 +- src/AvaloniaEdit/Editing/LineNumberMargin.cs | 78 +- .../Folding/FoldingElementGenerator.cs | 42 +- .../Highlighting/HighlightingColorizer.cs | 84 +- .../Rendering/BackgroundGeometryBuilder.cs | 667 +++-- .../Rendering/CollapsedLineSection.cs | 72 +- .../Rendering/ColorizingTransformer.cs | 189 +- .../Rendering/ColumnRulerRenderer.cs | 30 +- .../Rendering/CurrentLineHighlightRenderer.cs | 28 +- .../DocumentColorizingTransformer.cs | 134 +- .../Rendering/FormattedTextElement.cs | 264 +- .../Rendering/FormattedTextExtensions.cs | 6 - .../Rendering/GlobalTextRunProperties.cs | 46 + src/AvaloniaEdit/Rendering/HeightTree.cs | 2226 ++++++++--------- .../Rendering/HeightTreeLineNode.cs | 10 +- src/AvaloniaEdit/Rendering/HeightTreeNode.cs | 75 +- .../Rendering/ITextRunConstructionContext.cs | 123 +- src/AvaloniaEdit/Rendering/InlineObjectRun.cs | 168 +- .../Rendering/LinkElementGenerator.cs | 68 +- .../Rendering/SimpleTextSource.cs | 30 +- .../SingleCharacterElementGenerator.cs | 383 +-- src/AvaloniaEdit/Rendering/TextView.cs | 363 ++- .../Rendering/TextViewCachedElements.cs | 42 +- src/AvaloniaEdit/Rendering/VisualLine.cs | 155 +- .../Rendering/VisualLineElement.cs | 22 +- .../VisualLineElementTextRunProperties.cs | 244 ++ .../Rendering/VisualLineLinkText.cs | 18 +- src/AvaloniaEdit/Rendering/VisualLineText.cs | 22 +- .../VisualLineTextParagraphProperties.cs | 46 + .../Rendering/VisualLineTextSource.cs | 199 +- src/AvaloniaEdit/Utils/ExtensionMethods.cs | 15 + .../Utils/TextFormatterFactory.cs | 73 +- src/AvaloniaEdit/Utils/TextLineExtensions.cs | 201 +- 35 files changed, 3210 insertions(+), 3005 deletions(-) delete mode 100644 src/AvaloniaEdit/Rendering/FormattedTextExtensions.cs create mode 100644 src/AvaloniaEdit/Rendering/GlobalTextRunProperties.cs create mode 100644 src/AvaloniaEdit/Rendering/VisualLineElementTextRunProperties.cs create mode 100644 src/AvaloniaEdit/Rendering/VisualLineTextParagraphProperties.cs diff --git a/Directory.Build.props b/Directory.Build.props index a55d4d1f..30961a38 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ latest true - 0.10.999-cibuild0019136-beta + 0.10.999-cibuild0019161-beta 1.0.31 13.0.1 0.10.12.2 diff --git a/src/AvaloniaEdit.Demo/MainWindow.xaml.cs b/src/AvaloniaEdit.Demo/MainWindow.xaml.cs index 5006bdd5..e7bfb7e3 100644 --- a/src/AvaloniaEdit.Demo/MainWindow.xaml.cs +++ b/src/AvaloniaEdit.Demo/MainWindow.xaml.cs @@ -156,7 +156,7 @@ private void InitializeComponent() private void AddControlButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) { - _generator.controls.Add(new Pair(_textEditor.CaretOffset, new Button() { Content = "Click me" })); + _generator.controls.Add(new KeyValuePair(_textEditor.CaretOffset, new Button() { Content = "Click me" })); _textEditor.TextArea.TextView.Redraw(); } @@ -290,9 +290,9 @@ public void Complete(TextArea textArea, ISegment completionSegment, } } - class ElementGenerator : VisualLineElementGenerator, IComparer + class ElementGenerator : VisualLineElementGenerator, IComparer> { - public List controls = new List(); + public List> controls = new List>(); /// /// Gets the first interested offset using binary search @@ -301,7 +301,7 @@ class ElementGenerator : VisualLineElementGenerator, IComparer /// Start offset. public override int GetFirstInterestedOffset(int startOffset) { - int pos = controls.BinarySearch(new Pair(startOffset, null), this); + int pos = controls.BinarySearch(new KeyValuePair(startOffset, null), this); if (pos < 0) pos = ~pos; if (pos < controls.Count) @@ -312,14 +312,14 @@ public override int GetFirstInterestedOffset(int startOffset) public override VisualLineElement ConstructElement(int offset) { - int pos = controls.BinarySearch(new Pair(offset, null), this); + int pos = controls.BinarySearch(new KeyValuePair(offset, null), this); if (pos >= 0) return new InlineObjectElement(0, controls[pos].Value); else return null; } - int IComparer.Compare(Pair x, Pair y) + int IComparer>.Compare(KeyValuePair x, KeyValuePair y) { return x.Key.CompareTo(y.Key); } diff --git a/src/AvaloniaEdit/Editing/Caret.cs b/src/AvaloniaEdit/Editing/Caret.cs index 1c33580a..8f413e62 100644 --- a/src/AvaloniaEdit/Editing/Caret.cs +++ b/src/AvaloniaEdit/Editing/Caret.cs @@ -45,11 +45,11 @@ internal Caret(TextArea textArea) _textView = textArea.TextView; _position = new TextViewPosition(1, 1, 0); - _caretAdorner = new CaretLayer(textArea); - _textView.InsertLayer(_caretAdorner, KnownLayer.Caret, LayerInsertionPosition.Replace); - _textView.VisualLinesChanged += TextView_VisualLinesChanged; - _textView.ScrollOffsetChanged += TextView_ScrollOffsetChanged; - } + _caretAdorner = new CaretLayer(textArea); + _textView.InsertLayer(_caretAdorner, KnownLayer.Caret, LayerInsertionPosition.Replace); + _textView.VisualLinesChanged += TextView_VisualLinesChanged; + _textView.ScrollOffsetChanged += TextView_ScrollOffsetChanged; + } internal void UpdateIfVisible() { @@ -402,41 +402,39 @@ private Rect CalcCaretRectangle(VisualLine visualLine) lineBottom - lineTop); } - private Rect CalcCaretOverstrikeRectangle(VisualLine visualLine) - { - if (!_visualColumnValid) - { - RevalidateVisualColumn(visualLine); - } - - var currentPos = _position.VisualColumn; - // The text being overwritten in overstrike mode is everything up to the next normal caret stop - var nextPos = visualLine.GetNextCaretPosition(currentPos, LogicalDirection.Forward, CaretPositioningMode.Normal, true); - var textLine = visualLine.GetTextLine(currentPos); - - Rect r; - if (currentPos < visualLine.VisualLength) - { - // If the caret is within the text, use GetTextBounds() for the text being overwritten. - // This is necessary to ensure the rectangle is calculated correctly in bidirectional text. - r = textLine.GetTextBounds(currentPos, nextPos - currentPos); - r = r.WithY(r.Y + visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.LineTop)); - } - else - { - // If the caret is at the end of the line (or in virtual space), - // use the visual X position of currentPos and nextPos (one or more of which will be in virtual space) - var xPos = visualLine.GetTextLineVisualXPosition(textLine, currentPos); - var xPos2 = visualLine.GetTextLineVisualXPosition(textLine, nextPos); - var lineTop = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextTop); - var lineBottom = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextBottom); - r = new Rect(xPos, lineTop, xPos2 - xPos, lineBottom - lineTop); - } - // If the caret is too small (e.g. in front of zero-width character), ensure it's still visible - if (r.Width < CaretWidth) - r = r.WithWidth(CaretWidth); - return r; - } + Rect CalcCaretOverstrikeRectangle(VisualLine visualLine) + { + if (!_visualColumnValid) { + RevalidateVisualColumn(visualLine); + } + + int currentPos = _position.VisualColumn; + // The text being overwritten in overstrike mode is everything up to the next normal caret stop + int nextPos = visualLine.GetNextCaretPosition(currentPos, LogicalDirection.Forward, CaretPositioningMode.Normal, true); + var textLine = visualLine.GetTextLine(currentPos); + + Rect r; + if (currentPos < visualLine.VisualLength) { + // If the caret is within the text, use GetTextBounds() for the text being overwritten. + // This is necessary to ensure the rectangle is calculated correctly in bidirectional text. + var textBounds = textLine.GetTextBounds(currentPos, nextPos - currentPos)[0]; + r = textBounds.Rectangle; + var y = r.Y + visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.LineTop); + r = r.WithY(y); + } else { + // If the caret is at the end of the line (or in virtual space), + // use the visual X position of currentPos and nextPos (one or more of which will be in virtual space) + double xPos = visualLine.GetTextLineVisualXPosition(textLine, currentPos); + double xPos2 = visualLine.GetTextLineVisualXPosition(textLine, nextPos); + double lineTop = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextTop); + double lineBottom = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextBottom); + r = new Rect(xPos, lineTop, xPos2 - xPos, lineBottom - lineTop); + } + // If the caret is too small (e.g. in front of zero-width character), ensure it's still visible + if (r.Width < CaretWidth) + r = r.WithWidth(CaretWidth); + return r; + } /// /// Returns the caret rectangle. The coordinate system is in device-independent pixels from the top of the document. diff --git a/src/AvaloniaEdit/Editing/LineNumberMargin.cs b/src/AvaloniaEdit/Editing/LineNumberMargin.cs index 86c2511d..b37fed9f 100644 --- a/src/AvaloniaEdit/Editing/LineNumberMargin.cs +++ b/src/AvaloniaEdit/Editing/LineNumberMargin.cs @@ -50,51 +50,41 @@ public class LineNumberMargin : AbstractMargin /// protected double EmSize { get; set; } - /// - protected override Size MeasureOverride(Size availableSize) - { - Typeface = new Typeface(GetValue(TextBlock.FontFamilyProperty)); - EmSize = GetValue(TextBlock.FontSizeProperty); - - var textLine = TextFormatterFactory.FormatLine(Enumerable.Repeat('9', MaxLineNumberLength).ToArray(), - Typeface, - EmSize, - GetValue(TemplatedControl.ForegroundProperty) - ); - - return new Size(textLine.WidthIncludingTrailingWhitespace, textLine.Height); - } - - /// - public override void Render(DrawingContext drawingContext) - { - var textView = TextView; - var renderSize = Bounds.Size; + /// + protected override Size MeasureOverride(Size availableSize) + { + Typeface = this.CreateTypeface(); + EmSize = GetValue(TextBlock.FontSizeProperty); + + var text = TextFormatterFactory.CreateFormattedText( + this, + new string('9', MaxLineNumberLength), + Typeface, + EmSize, + GetValue(TextBlock.ForegroundProperty) + ); + return new Size(text.Width, 0); + } + + public override void Render(DrawingContext drawingContext) + { + var textView = TextView; + var renderSize = Bounds.Size; - if (textView != null && textView.VisualLinesValid) - { - var foreground = GetValue(TemplatedControl.ForegroundProperty); - - foreach (var line in textView.VisualLines) - { - var lineNumber = line.FirstDocumentLine.LineNumber; - var text = lineNumber.ToString(CultureInfo.CurrentCulture); - var textLine = TextFormatterFactory.FormatLine(text.AsMemory(), - Typeface, - EmSize, - foreground - ); - - var y = line.TextLines.Count > 0 - ? line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.TextTop) - : line.VisualTop; - - textLine.Draw(drawingContext, - new Point(renderSize.Width - textLine.WidthIncludingTrailingWhitespace, - y - textView.VerticalOffset)); - } - } - } + if (textView is {VisualLinesValid: true}) { + var foreground = GetValue(TextBlock.ForegroundProperty); + foreach (var line in textView.VisualLines) { + var lineNumber = line.FirstDocumentLine.LineNumber; + var text = TextFormatterFactory.CreateFormattedText( + this, + lineNumber.ToString(CultureInfo.CurrentCulture), + Typeface, EmSize, foreground + ); + var y = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.TextTop); + drawingContext.DrawText(text, new Point(renderSize.Width - text.Width, y - textView.VerticalOffset)); + } + } + } /// protected override void OnTextViewChanged(TextView oldTextView, TextView newTextView) diff --git a/src/AvaloniaEdit/Folding/FoldingElementGenerator.cs b/src/AvaloniaEdit/Folding/FoldingElementGenerator.cs index 5ff424c9..e6ee835c 100644 --- a/src/AvaloniaEdit/Folding/FoldingElementGenerator.cs +++ b/src/AvaloniaEdit/Folding/FoldingElementGenerator.cs @@ -22,7 +22,9 @@ using AvaloniaEdit.Rendering; using Avalonia.Input; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Media.TextFormatting; +using AvaloniaEdit.Utils; namespace AvaloniaEdit.Folding { @@ -149,20 +151,18 @@ public override VisualLineElement ConstructElement(int offset) } } while (foundOverlappingFolding); - var title = foldingSection.Title; - if (string.IsNullOrEmpty(title)) - title = "..."; - var p = CurrentContext.GlobalTextRunProperties.Clone(); - p.SetForegroundBrush(TextBrush); - var textFormatter = TextFormatter.Current; - var text = FormattedTextElement.PrepareText(textFormatter, title, p); - return new FoldingLineElement(foldingSection, text, foldedUntil - offset, TextBrush); - } - else - { - return null; - } - } + string title = foldingSection.Title; + if (string.IsNullOrEmpty(title)) + title = "..."; + var p = new VisualLineElementTextRunProperties(CurrentContext.GlobalTextRunProperties); + p.SetForegroundBrush(TextBrush); + var textFormatter = TextFormatterFactory.Create(CurrentContext.TextView); + var text = FormattedTextElement.PrepareText(textFormatter, title, p); + return new FoldingLineElement(foldingSection, text, foldedUntil - offset, TextBrush); + } else { + return null; + } + } private sealed class FoldingLineElement : FormattedTextElement { @@ -175,10 +175,10 @@ public FoldingLineElement(FoldingSection fs, TextLine text, int documentLength, _textBrush = textBrush; } - public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) - { - return new FoldingLineTextRun(this, TextRunProperties, _textBrush); - } + public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) + { + return new FoldingLineTextRun(this, this.TextRunProperties, _textBrush); + } //DOUBLETAP protected internal override void OnPointerPressed(PointerPressedEventArgs e) @@ -200,9 +200,9 @@ public FoldingLineTextRun(FormattedTextElement element, TextRunProperties proper public override void Draw(DrawingContext drawingContext, Point origin) { - var metrics = Size; - var r = new Rect(origin.X, origin.Y, metrics.Width, metrics.Height); - drawingContext.DrawRectangle(new Pen(_textBrush), r); + var (width, height) = Size; + var r = new Rect(origin.X, origin.Y, width, height); + drawingContext.DrawRectangle(new ImmutablePen(_textBrush.ToImmutable()), r); base.Draw(drawingContext, origin); } } diff --git a/src/AvaloniaEdit/Highlighting/HighlightingColorizer.cs b/src/AvaloniaEdit/Highlighting/HighlightingColorizer.cs index 4fdcefe0..af2fa28f 100644 --- a/src/AvaloniaEdit/Highlighting/HighlightingColorizer.cs +++ b/src/AvaloniaEdit/Highlighting/HighlightingColorizer.cs @@ -255,62 +255,34 @@ protected virtual void ApplyColorToElement(VisualLineElement element, Highlighti ApplyColorToElement(element, color, CurrentContext); } - internal static void ApplyColorToElement(VisualLineElement element, HighlightingColor color, ITextRunConstructionContext context) - { - if (color.Foreground != null) - { - var b = color.Foreground.GetBrush(context); - if (b != null) - element.TextRunProperties.SetForegroundBrush(b); - } - if (color.Background != null) - { - var b = color.Background.GetBrush(context); - if (b != null) - element.BackgroundBrush = b; - } - if (color.FontStyle != null || color.FontWeight != null || color.FontFamily != null) - { - var tf = element.TextRunProperties.Typeface; - element.TextRunProperties.SetTypeface(new Avalonia.Media.Typeface( - color.FontFamily ?? tf.FontFamily, - color.FontStyle ?? tf.Style, - color.FontWeight ?? tf.Weight) - ); - } - if (color.FontSize.HasValue) - element.TextRunProperties.SetFontSize(color.FontSize.Value); - - if (color.Underline ?? false) - { - element.TextRunProperties.SetTextDecorations(new TextDecorationCollection{new() - { - Location = TextDecorationLocation.Underline - }}); - } - - if (color.Strikethrough ?? false) - { - if (element.TextRunProperties.TextDecorations != null) - { - element.TextRunProperties.TextDecorations.Add(new TextDecoration - { - Location = TextDecorationLocation.Strikethrough - }); - } - else - { - element.TextRunProperties.SetTextDecorations(new TextDecorationCollection - { - new() - { - Location = TextDecorationLocation.Strikethrough - } - }); - } - - } - } + internal static void ApplyColorToElement(VisualLineElement element, HighlightingColor color, ITextRunConstructionContext context) + { + if (color.Foreground != null) { + var b = color.Foreground.GetBrush(context); + if (b != null) + element.TextRunProperties.SetForegroundBrush(b); + } + if (color.Background != null) { + var b = color.Background.GetBrush(context); + if (b != null) + element.BackgroundBrush = b; + } + if (color.FontStyle != null || color.FontWeight != null || color.FontFamily != null) { + var tf = element.TextRunProperties.Typeface; + element.TextRunProperties.SetTypeface(new Typeface( + color.FontFamily ?? tf.FontFamily, + color.FontStyle ?? tf.Style, + color.FontWeight ?? tf.Weight, + tf.Stretch + )); + } + if (color.Underline ?? false) + element.TextRunProperties.SetTextDecorations(TextDecorations.Underline); + if (color.Strikethrough ?? false) + element.TextRunProperties.SetTextDecorations(TextDecorations.Strikethrough); + if (color.FontSize.HasValue) + element.TextRunProperties.SetFontRenderingEmSize(color.FontSize.Value); + } /// /// This method is responsible for telling the TextView to redraw lines when the highlighting state has changed. diff --git a/src/AvaloniaEdit/Rendering/BackgroundGeometryBuilder.cs b/src/AvaloniaEdit/Rendering/BackgroundGeometryBuilder.cs index 50de0f5c..8fa99395 100644 --- a/src/AvaloniaEdit/Rendering/BackgroundGeometryBuilder.cs +++ b/src/AvaloniaEdit/Rendering/BackgroundGeometryBuilder.cs @@ -21,381 +21,362 @@ using System.Diagnostics; using System.Linq; using Avalonia; +using Avalonia.Controls.Primitives; using AvaloniaEdit.Document; using AvaloniaEdit.Editing; using AvaloniaEdit.Utils; using Avalonia.Media; +using Avalonia.Media.TextFormatting; namespace AvaloniaEdit.Rendering { - /// - /// Helper for creating a PathGeometry. - /// - public sealed class BackgroundGeometryBuilder - { - /// - /// Gets/sets the radius of the rounded corners. - /// - public double CornerRadius { get; set; } + /// + /// Helper for creating a PathGeometry. + /// + public sealed class BackgroundGeometryBuilder + { + private double _cornerRadius; - /// - /// Gets/Sets whether to align to whole pixels. - /// - /// If BorderThickness is set to 0, the geometry is aligned to whole pixels. - /// If BorderThickness is set to a non-zero value, the outer edge of the border is aligned - /// to whole pixels. - /// - /// The default value is false. - /// - public bool AlignToWholePixels { get; set; } + /// + /// Gets/sets the radius of the rounded corners. + /// + public double CornerRadius { + get { return _cornerRadius; } + set { _cornerRadius = value; } + } - /// - /// Gets/sets the border thickness. - /// - /// This property only has an effect if AlignToWholePixels is enabled. - /// When using the resulting geometry to paint a border, set this property to the border thickness. - /// Otherwise, leave the property set to the default value 0. - /// - public double BorderThickness { get; set; } + /// + /// Gets/Sets whether to align to whole pixels. + /// + /// If BorderThickness is set to 0, the geometry is aligned to whole pixels. + /// If BorderThickness is set to a non-zero value, the outer edge of the border is aligned + /// to whole pixels. + /// + /// The default value is false. + /// + public bool AlignToWholePixels { get; set; } - /// - /// Gets/Sets whether to extend the rectangles to full width at line end. - /// - public bool ExtendToFullWidthAtLineEnd { get; set; } + /// + /// Gets/sets the border thickness. + /// + /// This property only has an effect if AlignToWholePixels is enabled. + /// When using the resulting geometry to paint a border, set this property to the border thickness. + /// Otherwise, leave the property set to the default value 0. + /// + public double BorderThickness { get; set; } - /// - /// Adds the specified segment to the geometry. - /// - public void AddSegment(TextView textView, ISegment segment) - { - if (textView == null) - throw new ArgumentNullException(nameof(textView)); - var pixelSize = PixelSnapHelpers.GetPixelSize(textView); - foreach (var r in GetRectsForSegment(textView, segment, ExtendToFullWidthAtLineEnd)) - { - AddRectangle(pixelSize, r); - } - } + /// + /// Gets/Sets whether to extend the rectangles to full width at line end. + /// + public bool ExtendToFullWidthAtLineEnd { get; set; } - /// - /// Adds a rectangle to the geometry. - /// - /// - /// This overload will align the coordinates according to - /// . - /// Use the -overload instead if the coordinates should not be aligned. - /// - public void AddRectangle(TextView textView, Rect rectangle) - { - AddRectangle(PixelSnapHelpers.GetPixelSize(textView), rectangle); - } + /// + /// Creates a new BackgroundGeometryBuilder instance. + /// + public BackgroundGeometryBuilder() + { + } - private void AddRectangle(Size pixelSize, Rect r) - { - if (AlignToWholePixels) - { - var halfBorder = 0.5 * BorderThickness; - AddRectangle(PixelSnapHelpers.Round(r.X - halfBorder, pixelSize.Width) + halfBorder, - PixelSnapHelpers.Round(r.Y - halfBorder, pixelSize.Height) + halfBorder, - PixelSnapHelpers.Round(r.Right + halfBorder, pixelSize.Width) - halfBorder, - PixelSnapHelpers.Round(r.Bottom + halfBorder, pixelSize.Height) - halfBorder); - } - else - { - AddRectangle(r.X, r.Y, r.Right, r.Bottom); - } - } + /// + /// Adds the specified segment to the geometry. + /// + public void AddSegment(TextView textView, ISegment segment) + { + if (textView == null) + throw new ArgumentNullException("textView"); + Size pixelSize = PixelSnapHelpers.GetPixelSize(textView); + foreach (Rect r in GetRectsForSegment(textView, segment, ExtendToFullWidthAtLineEnd)) { + AddRectangle(pixelSize, r); + } + } - /// - /// Calculates the list of rectangle where the segment in shown. - /// This method usually returns one rectangle for each line inside the segment - /// (but potentially more, e.g. when bidirectional text is involved). - /// - public static IEnumerable GetRectsForSegment(TextView textView, ISegment segment, bool extendToFullWidthAtLineEnd = false) - { - if (textView == null) - throw new ArgumentNullException(nameof(textView)); - if (segment == null) - throw new ArgumentNullException(nameof(segment)); - return GetRectsForSegmentImpl(textView, segment, extendToFullWidthAtLineEnd); - } + /// + /// Adds a rectangle to the geometry. + /// + /// + /// This overload will align the coordinates according to + /// . + /// Use the -overload instead if the coordinates should not be aligned. + /// + public void AddRectangle(TextView textView, Rect rectangle) + { + AddRectangle(PixelSnapHelpers.GetPixelSize(textView), rectangle); + } - private static IEnumerable GetRectsForSegmentImpl(TextView textView, ISegment segment, bool extendToFullWidthAtLineEnd) - { - var segmentStart = segment.Offset; - var segmentEnd = segment.Offset + segment.Length; + private void AddRectangle(Size pixelSize, Rect r) + { + if (AlignToWholePixels) { + double halfBorder = 0.5 * BorderThickness; + AddRectangle(PixelSnapHelpers.Round(r.Left - halfBorder, pixelSize.Width) + halfBorder, + PixelSnapHelpers.Round(r.Top - halfBorder, pixelSize.Height) + halfBorder, + PixelSnapHelpers.Round(r.Right + halfBorder, pixelSize.Width) - halfBorder, + PixelSnapHelpers.Round(r.Bottom + halfBorder, pixelSize.Height) - halfBorder); + //Debug.WriteLine(r.ToString() + " -> " + new Rect(lastLeft, lastTop, lastRight-lastLeft, lastBottom-lastTop).ToString()); + } else { + AddRectangle(r.Left, r.Top, r.Right, r.Bottom); + } + } - segmentStart = segmentStart.CoerceValue(0, textView.Document.TextLength); - segmentEnd = segmentEnd.CoerceValue(0, textView.Document.TextLength); + /// + /// Calculates the list of rectangle where the segment in shown. + /// This method usually returns one rectangle for each line inside the segment + /// (but potentially more, e.g. when bidirectional text is involved). + /// + public static IEnumerable GetRectsForSegment(TextView textView, ISegment segment, bool extendToFullWidthAtLineEnd = false) + { + if (textView == null) + throw new ArgumentNullException("textView"); + if (segment == null) + throw new ArgumentNullException("segment"); + return GetRectsForSegmentImpl(textView, segment, extendToFullWidthAtLineEnd); + } - TextViewPosition start; - TextViewPosition end; + private static IEnumerable GetRectsForSegmentImpl(TextView textView, ISegment segment, bool extendToFullWidthAtLineEnd) + { + int segmentStart = segment.Offset; + int segmentEnd = segment.Offset + segment.Length; - if (segment is SelectionSegment sel) - { - start = new TextViewPosition(textView.Document.GetLocation(sel.StartOffset), sel.StartVisualColumn); - end = new TextViewPosition(textView.Document.GetLocation(sel.EndOffset), sel.EndVisualColumn); - } - else - { - start = new TextViewPosition(textView.Document.GetLocation(segmentStart)); - end = new TextViewPosition(textView.Document.GetLocation(segmentEnd)); - } + segmentStart = segmentStart.CoerceValue(0, textView.Document.TextLength); + segmentEnd = segmentEnd.CoerceValue(0, textView.Document.TextLength); - foreach (var vl in textView.VisualLines) - { - var vlStartOffset = vl.FirstDocumentLine.Offset; - if (vlStartOffset > segmentEnd) - break; - var vlEndOffset = vl.LastDocumentLine.Offset + vl.LastDocumentLine.Length; - if (vlEndOffset < segmentStart) - continue; + TextViewPosition start; + TextViewPosition end; - int segmentStartVc; - segmentStartVc = segmentStart < vlStartOffset ? 0 : vl.ValidateVisualColumn(start, extendToFullWidthAtLineEnd); + if (segment is SelectionSegment) { + SelectionSegment sel = (SelectionSegment)segment; + start = new TextViewPosition(textView.Document.GetLocation(sel.StartOffset), sel.StartVisualColumn); + end = new TextViewPosition(textView.Document.GetLocation(sel.EndOffset), sel.EndVisualColumn); + } else { + start = new TextViewPosition(textView.Document.GetLocation(segmentStart)); + end = new TextViewPosition(textView.Document.GetLocation(segmentEnd)); + } - int segmentEndVc; - if (segmentEnd > vlEndOffset) - segmentEndVc = extendToFullWidthAtLineEnd ? int.MaxValue : vl.VisualLengthWithEndOfLineMarker; - else - segmentEndVc = vl.ValidateVisualColumn(end, extendToFullWidthAtLineEnd); + foreach (VisualLine vl in textView.VisualLines) { + int vlStartOffset = vl.FirstDocumentLine.Offset; + if (vlStartOffset > segmentEnd) + break; + int vlEndOffset = vl.LastDocumentLine.Offset + vl.LastDocumentLine.Length; + if (vlEndOffset < segmentStart) + continue; - foreach (var rect in ProcessTextLines(textView, vl, segmentStartVc, segmentEndVc)) - yield return rect; - } - } + int segmentStartVc; + if (segmentStart < vlStartOffset) + segmentStartVc = 0; + else + segmentStartVc = vl.ValidateVisualColumn(start, extendToFullWidthAtLineEnd); - /// - /// Calculates the rectangles for the visual column segment. - /// This returns one rectangle for each line inside the segment. - /// - public static IEnumerable GetRectsFromVisualSegment(TextView textView, VisualLine line, int startVc, int endVc) - { - if (textView == null) - throw new ArgumentNullException(nameof(textView)); - if (line == null) - throw new ArgumentNullException(nameof(line)); - return ProcessTextLines(textView, line, startVc, endVc); - } + int segmentEndVc; + if (segmentEnd > vlEndOffset) + segmentEndVc = extendToFullWidthAtLineEnd ? int.MaxValue : vl.VisualLengthWithEndOfLineMarker; + else + segmentEndVc = vl.ValidateVisualColumn(end, extendToFullWidthAtLineEnd); - private static IEnumerable ProcessTextLines(TextView textView, VisualLine visualLine, int segmentStartVc, int segmentEndVc) - { - if (visualLine.TextLines.Count == 0) - { - yield break; - } - - var lastTextLine = visualLine.TextLines.Last(); - var scrollOffset = textView.ScrollOffset; + foreach (var rect in ProcessTextLines(textView, vl, segmentStartVc, segmentEndVc)) + yield return rect; + } + } - for (var i = 0; i < visualLine.TextLines.Count; i++) - { - var line = visualLine.TextLines[i]; - var y = visualLine.GetTextLineVisualYPosition(line, VisualYPosition.LineTop); - var visualStartCol = visualLine.GetTextLineVisualStartColumn(line); - var visualEndCol = visualStartCol + line.TextRange.Length; - if (line == lastTextLine) - visualEndCol -= 1; // 1 position for the TextEndOfParagraph - // TODO: ? - //else - // visualEndCol -= line.TrailingWhitespaceLength; + /// + /// Calculates the rectangles for the visual column segment. + /// This returns one rectangle for each line inside the segment. + /// + public static IEnumerable GetRectsFromVisualSegment(TextView textView, VisualLine line, int startVc, int endVc) + { + if (textView == null) + throw new ArgumentNullException("textView"); + if (line == null) + throw new ArgumentNullException("line"); + return ProcessTextLines(textView, line, startVc, endVc); + } - if (segmentEndVc < visualStartCol) - break; - if (lastTextLine != line && segmentStartVc > visualEndCol) - continue; - var segmentStartVcInLine = Math.Max(segmentStartVc, visualStartCol); - var segmentEndVcInLine = Math.Min(segmentEndVc, visualEndCol); - y -= scrollOffset.Y; - var lastRect = Rect.Empty; - if (segmentStartVcInLine == segmentEndVcInLine) - { - // GetTextBounds crashes for length=0, so we'll handle this case with GetDistanceFromCharacterHit - // We need to return a rectangle to ensure empty lines are still visible - var pos = visualLine.GetTextLineVisualXPosition(line, segmentStartVcInLine); - pos -= scrollOffset.X; - // The following special cases are necessary to get rid of empty rectangles at the end of a TextLine if "Show Spaces" is active. - // If not excluded once, the same rectangle is calculated (and added) twice (since the offset could be mapped to two visual positions; end/start of line), if there is no trailing whitespace. - // Skip this TextLine segment, if it is at the end of this line and this line is not the last line of the VisualLine and the selection continues and there is no trailing whitespace. - if (segmentEndVcInLine == visualEndCol && i < visualLine.TextLines.Count - 1 && segmentEndVc > segmentEndVcInLine && line.TrailingWhitespaceLength == 0) - continue; - if (segmentStartVcInLine == visualStartCol && i > 0 && segmentStartVc < segmentStartVcInLine && visualLine.TextLines[i - 1].TrailingWhitespaceLength == 0) - continue; - lastRect = new Rect(pos, y, textView.EmptyLineSelectionWidth, line.Height); - } - else - { - if (segmentStartVcInLine <= visualEndCol) - { - var b = line.GetTextBounds(segmentStartVcInLine, segmentEndVcInLine - segmentStartVcInLine); - var left = b.X - scrollOffset.X; - var right = b.Right - scrollOffset.X; - if (!lastRect.IsEmpty) - yield return lastRect; - // left>right is possible in RTL languages - lastRect = new Rect(Math.Min(left, right), y, Math.Abs(right - left), line.Height); - } - } - // If the segment ends in virtual space, extend the last rectangle with the rectangle the portion of the selection - // after the line end. - // Also, when word-wrap is enabled and the segment continues into the next line, extend lastRect up to the end of the line. - if (segmentEndVc > visualEndCol) - { - double left, right; - if (segmentStartVc > visualLine.VisualLengthWithEndOfLineMarker) - { - // segmentStartVC is in virtual space - left = visualLine.GetTextLineVisualXPosition(lastTextLine, segmentStartVc); - } - else - { - // Otherwise, we already processed the rects from segmentStartVC up to visualEndCol, - // so we only need to do the remainder starting at visualEndCol. - // For word-wrapped lines, visualEndCol doesn't include the whitespace hidden by the wrap, - // so we'll need to include it here. - // For the last line, visualEndCol already includes the whitespace. - left = line == lastTextLine ? line.WidthIncludingTrailingWhitespace : line.Width; - } - // TODO: !!!!!!!!!!!!!!!!!! SCROLL !!!!!!!!!!!!!!!!!! - //if (line != lastTextLine || segmentEndVC == int.MaxValue) { - // // If word-wrap is enabled and the segment continues into the next line, - // // or if the extendToFullWidthAtLineEnd option is used (segmentEndVC == int.MaxValue), - // // we select the full width of the viewport. - // right = Math.Max(((IScrollInfo)textView).ExtentWidth, ((IScrollInfo)textView).ViewportWidth); - //} else { + private static IEnumerable ProcessTextLines(TextView textView, VisualLine visualLine, int segmentStartVc, int segmentEndVc) + { + TextLine lastTextLine = visualLine.TextLines.Last(); + Vector scrollOffset = textView.ScrollOffset; - right = visualLine.GetTextLineVisualXPosition(lastTextLine, segmentEndVc); - //} - var extendSelection = new Rect(Math.Min(left, right), y, Math.Abs(right - left), line.Height); - if (!lastRect.IsEmpty) - { - if (extendSelection.Intersects(lastRect)) - { - lastRect.Union(extendSelection); - yield return lastRect; - } - else - { - // If the end of the line is in an RTL segment, keep lastRect and extendSelection separate. - yield return lastRect; - yield return extendSelection; - } - } - else - yield return extendSelection; - } - else - yield return lastRect; - } - } + for (int i = 0; i < visualLine.TextLines.Count; i++) { + TextLine line = visualLine.TextLines[i]; + double y = visualLine.GetTextLineVisualYPosition(line, VisualYPosition.LineTop); + int visualStartCol = visualLine.GetTextLineVisualStartColumn(line); + int visualEndCol = visualStartCol + line.TextRange.Length; + if (line == lastTextLine) + visualEndCol -= 1; // 1 position for the TextEndOfParagraph + else + visualEndCol -= line.TrailingWhitespaceLength; - private readonly PathFigures _figures = new PathFigures(); - private PathFigure _figure; - private int _insertionIndex; - private double _lastTop, _lastBottom; - private double _lastLeft, _lastRight; + if (segmentEndVc < visualStartCol) + break; + if (lastTextLine != line && segmentStartVc > visualEndCol) + continue; + int segmentStartVcInLine = Math.Max(segmentStartVc, visualStartCol); + int segmentEndVcInLine = Math.Min(segmentEndVc, visualEndCol); + y -= scrollOffset.Y; + Rect lastRect = Rect.Empty; + if (segmentStartVcInLine == segmentEndVcInLine) { + // GetTextBounds crashes for length=0, so we'll handle this case with GetDistanceFromCharacterHit + // We need to return a rectangle to ensure empty lines are still visible + double pos = visualLine.GetTextLineVisualXPosition(line, segmentStartVcInLine); + pos -= scrollOffset.X; + // The following special cases are necessary to get rid of empty rectangles at the end of a TextLine if "Show Spaces" is active. + // If not excluded once, the same rectangle is calculated (and added) twice (since the offset could be mapped to two visual positions; end/start of line), if there is no trailing whitespace. + // Skip this TextLine segment, if it is at the end of this line and this line is not the last line of the VisualLine and the selection continues and there is no trailing whitespace. + if (segmentEndVcInLine == visualEndCol && i < visualLine.TextLines.Count - 1 && segmentEndVc > segmentEndVcInLine && line.TrailingWhitespaceLength == 0) + continue; + if (segmentStartVcInLine == visualStartCol && i > 0 && segmentStartVc < segmentStartVcInLine && visualLine.TextLines[i - 1].TrailingWhitespaceLength == 0) + continue; + lastRect = new Rect(pos, y, textView.EmptyLineSelectionWidth, line.Height); + } else { + if (segmentStartVcInLine <= visualEndCol) { + foreach (var b in line.GetTextBounds(segmentStartVcInLine, segmentEndVcInLine - segmentStartVcInLine)) { + double left = b.Rectangle.Left - scrollOffset.X; + double right = b.Rectangle.Right - scrollOffset.X; + if (!lastRect.IsEmpty) + yield return lastRect; + // left>right is possible in RTL languages + lastRect = new Rect(Math.Min(left, right), y, Math.Abs(right - left), line.Height); + } + } + } + // If the segment ends in virtual space, extend the last rectangle with the rectangle the portion of the selection + // after the line end. + // Also, when word-wrap is enabled and the segment continues into the next line, extend lastRect up to the end of the line. + if (segmentEndVc > visualEndCol) { + double left, right; + if (segmentStartVc > visualLine.VisualLengthWithEndOfLineMarker) { + // segmentStartVC is in virtual space + left = visualLine.GetTextLineVisualXPosition(lastTextLine, segmentStartVc); + } else { + // Otherwise, we already processed the rects from segmentStartVC up to visualEndCol, + // so we only need to do the remainder starting at visualEndCol. + // For word-wrapped lines, visualEndCol doesn't include the whitespace hidden by the wrap, + // so we'll need to include it here. + // For the last line, visualEndCol already includes the whitespace. + left = (line == lastTextLine ? line.WidthIncludingTrailingWhitespace : line.Width); + } + if (line != lastTextLine || segmentEndVc == int.MaxValue) { + // If word-wrap is enabled and the segment continues into the next line, + // or if the extendToFullWidthAtLineEnd option is used (segmentEndVC == int.MaxValue), + // we select the full width of the viewport. + right = Math.Max(((ILogicalScrollable)textView).Extent.Width, ((ILogicalScrollable)textView).Viewport.Width); + } else { + right = visualLine.GetTextLineVisualXPosition(lastTextLine, segmentEndVc); + } + Rect extendSelection = new Rect(Math.Min(left, right), y, Math.Abs(right - left), line.Height); + if (!lastRect.IsEmpty) { + if (extendSelection.Intersects(lastRect)) { + lastRect.Union(extendSelection); + yield return lastRect; + } else { + // If the end of the line is in an RTL segment, keep lastRect and extendSelection separate. + yield return lastRect; + yield return extendSelection; + } + } else + yield return extendSelection; + } else + yield return lastRect; + } + } - /// - /// Adds a rectangle to the geometry. - /// - /// - /// This overload assumes that the coordinates are aligned properly - /// (see ). - /// Use the -overload instead if the coordinates are not yet aligned. - /// - public void AddRectangle(double left, double top, double right, double bottom) - { - if (!top.IsClose(_lastBottom)) - { - CloseFigure(); - } - if (_figure == null) - { - _figure = new PathFigure { StartPoint = new Point(left, top + CornerRadius) }; - if (Math.Abs(left - right) > CornerRadius) - { - _figure.Segments.Add(MakeArc(left + CornerRadius, top, SweepDirection.Clockwise)); - _figure.Segments.Add(MakeLineSegment(right - CornerRadius, top)); - _figure.Segments.Add(MakeArc(right, top + CornerRadius, SweepDirection.Clockwise)); - } - _figure.Segments.Add(MakeLineSegment(right, bottom - CornerRadius)); - _insertionIndex = _figure.Segments.Count; - //figure.Segments.Add(MakeArc(left, bottom - cornerRadius, SweepDirection.Clockwise)); - } - else - { - if (!_lastRight.IsClose(right)) - { - var cr = right < _lastRight ? -CornerRadius : CornerRadius; - var dir1 = right < _lastRight ? SweepDirection.Clockwise : SweepDirection.CounterClockwise; - var dir2 = right < _lastRight ? SweepDirection.CounterClockwise : SweepDirection.Clockwise; - _figure.Segments.Insert(_insertionIndex++, MakeArc(_lastRight + cr, _lastBottom, dir1)); - _figure.Segments.Insert(_insertionIndex++, MakeLineSegment(right - cr, top)); - _figure.Segments.Insert(_insertionIndex++, MakeArc(right, top + CornerRadius, dir2)); - } - _figure.Segments.Insert(_insertionIndex++, MakeLineSegment(right, bottom - CornerRadius)); - _figure.Segments.Insert(_insertionIndex, MakeLineSegment(_lastLeft, _lastTop + CornerRadius)); - if (!_lastLeft.IsClose(left)) - { - var cr = left < _lastLeft ? CornerRadius : -CornerRadius; - var dir1 = left < _lastLeft ? SweepDirection.CounterClockwise : SweepDirection.Clockwise; - var dir2 = left < _lastLeft ? SweepDirection.Clockwise : SweepDirection.CounterClockwise; - _figure.Segments.Insert(_insertionIndex, MakeArc(_lastLeft, _lastBottom - CornerRadius, dir1)); - _figure.Segments.Insert(_insertionIndex, MakeLineSegment(_lastLeft - cr, _lastBottom)); - _figure.Segments.Insert(_insertionIndex, MakeArc(left + cr, _lastBottom, dir2)); - } - } - _lastTop = top; - _lastBottom = bottom; - _lastLeft = left; - _lastRight = right; - } + private readonly PathFigures _figures = new PathFigures(); + private PathFigure _figure; + private int _insertionIndex; + private double _lastTop, _lastBottom; + private double _lastLeft, _lastRight; - private ArcSegment MakeArc(double x, double y, SweepDirection dir) - { - var arc = new ArcSegment - { - Point = new Point(x, y), - Size = new Size(CornerRadius, CornerRadius), - SweepDirection = dir - }; - return arc; - } + /// + /// Adds a rectangle to the geometry. + /// + /// + /// This overload assumes that the coordinates are aligned properly + /// (see ). + /// Use the -overload instead if the coordinates are not yet aligned. + /// + public void AddRectangle(double left, double top, double right, double bottom) + { + if (!top.IsClose(_lastBottom)) { + CloseFigure(); + } + if (_figure == null) { + _figure = new PathFigure(); + _figure.StartPoint = new Point(left, top + _cornerRadius); + if (Math.Abs(left - right) > _cornerRadius) { + _figure.Segments.Add(MakeArc(left + _cornerRadius, top, SweepDirection.Clockwise)); + _figure.Segments.Add(MakeLineSegment(right - _cornerRadius, top)); + _figure.Segments.Add(MakeArc(right, top + _cornerRadius, SweepDirection.Clockwise)); + } + _figure.Segments.Add(MakeLineSegment(right, bottom - _cornerRadius)); + _insertionIndex = _figure.Segments.Count; + //figure.Segments.Add(MakeArc(left, bottom - cornerRadius, SweepDirection.Clockwise)); + } else { + if (!_lastRight.IsClose(right)) { + double cr = right < _lastRight ? -_cornerRadius : _cornerRadius; + SweepDirection dir1 = right < _lastRight ? SweepDirection.Clockwise : SweepDirection.CounterClockwise; + SweepDirection dir2 = right < _lastRight ? SweepDirection.CounterClockwise : SweepDirection.Clockwise; + _figure.Segments.Insert(_insertionIndex++, MakeArc(_lastRight + cr, _lastBottom, dir1)); + _figure.Segments.Insert(_insertionIndex++, MakeLineSegment(right - cr, top)); + _figure.Segments.Insert(_insertionIndex++, MakeArc(right, top + _cornerRadius, dir2)); + } + _figure.Segments.Insert(_insertionIndex++, MakeLineSegment(right, bottom - _cornerRadius)); + _figure.Segments.Insert(_insertionIndex, MakeLineSegment(_lastLeft, _lastTop + _cornerRadius)); + if (!_lastLeft.IsClose(left)) { + double cr = left < _lastLeft ? _cornerRadius : -_cornerRadius; + SweepDirection dir1 = left < _lastLeft ? SweepDirection.CounterClockwise : SweepDirection.Clockwise; + SweepDirection dir2 = left < _lastLeft ? SweepDirection.Clockwise : SweepDirection.CounterClockwise; + _figure.Segments.Insert(_insertionIndex, MakeArc(_lastLeft, _lastBottom - _cornerRadius, dir1)); + _figure.Segments.Insert(_insertionIndex, MakeLineSegment(_lastLeft - cr, _lastBottom)); + _figure.Segments.Insert(_insertionIndex, MakeArc(left + cr, _lastBottom, dir2)); + } + } + this._lastTop = top; + this._lastBottom = bottom; + this._lastLeft = left; + this._lastRight = right; + } - private static LineSegment MakeLineSegment(double x, double y) - { - return new LineSegment { Point = new Point(x, y) }; - } + private ArcSegment MakeArc(double x, double y, SweepDirection dir) + { + var arc = new ArcSegment + { + Point = new Point(x, y), + Size = new Size(CornerRadius, CornerRadius), + SweepDirection = dir + }; + return arc; + } - /// - /// Closes the current figure. - /// - public void CloseFigure() - { - if (_figure != null) - { - _figure.Segments.Insert(_insertionIndex, MakeLineSegment(_lastLeft, _lastTop + CornerRadius)); - if (Math.Abs(_lastLeft - _lastRight) > CornerRadius) - { - _figure.Segments.Insert(_insertionIndex, MakeArc(_lastLeft, _lastBottom - CornerRadius, SweepDirection.Clockwise)); - _figure.Segments.Insert(_insertionIndex, MakeLineSegment(_lastLeft + CornerRadius, _lastBottom)); - _figure.Segments.Insert(_insertionIndex, MakeArc(_lastRight - CornerRadius, _lastBottom, SweepDirection.Clockwise)); - } + private static LineSegment MakeLineSegment(double x, double y) + { + return new LineSegment { Point = new Point(x, y) }; + } - _figure.IsClosed = true; - _figures.Add(_figure); - _figure = null; - } - } + /// + /// Closes the current figure. + /// + public void CloseFigure() + { + if (_figure != null) { + _figure.Segments.Insert(_insertionIndex, MakeLineSegment(_lastLeft, _lastTop + _cornerRadius)); + if (Math.Abs(_lastLeft - _lastRight) > _cornerRadius) { + _figure.Segments.Insert(_insertionIndex, MakeArc(_lastLeft, _lastBottom - _cornerRadius, SweepDirection.Clockwise)); + _figure.Segments.Insert(_insertionIndex, MakeLineSegment(_lastLeft + _cornerRadius, _lastBottom)); + _figure.Segments.Insert(_insertionIndex, MakeArc(_lastRight - _cornerRadius, _lastBottom, SweepDirection.Clockwise)); + } - /// - /// Creates the geometry. - /// Returns null when the geometry is empty! - /// - public Geometry CreateGeometry() - { - CloseFigure(); - return _figures.Count != 0 ? new PathGeometry { Figures = _figures } : null; - } - } + _figure.IsClosed = true; + _figures.Add(_figure); + _figure = null; + } + } + + /// + /// Creates the geometry. + /// Returns null when the geometry is empty! + /// + public Geometry CreateGeometry() + { + CloseFigure(); + return _figures.Count != 0 ? new PathGeometry { Figures = _figures } : null; + } + } } diff --git a/src/AvaloniaEdit/Rendering/CollapsedLineSection.cs b/src/AvaloniaEdit/Rendering/CollapsedLineSection.cs index f53d9252..e3e6dd95 100644 --- a/src/AvaloniaEdit/Rendering/CollapsedLineSection.cs +++ b/src/AvaloniaEdit/Rendering/CollapsedLineSection.cs @@ -27,55 +27,47 @@ namespace AvaloniaEdit.Rendering /// public sealed class CollapsedLineSection { - private DocumentLine _start; - private DocumentLine _end; - private readonly HeightTree _heightTree; - - #if DEBUG + private readonly HeightTree _heightTree; + +#if DEBUG internal string Id; - private static int _nextId; - #else - internal const string Id = ""; - #endif - + private static int _nextId; +#else + const string ID = ""; +#endif + internal CollapsedLineSection(HeightTree heightTree, DocumentLine start, DocumentLine end) { _heightTree = heightTree; - _start = start; - _end = end; - #if DEBUG + Start = start; + End = end; +#if DEBUG unchecked { Id = " #" + (_nextId++); } - #endif +#endif } - + /// /// Gets if the document line is collapsed. /// This property initially is true and turns to false when uncollapsing the section. /// - public bool IsCollapsed => _start != null; + public bool IsCollapsed => Start != null; - /// + /// /// Gets the start line of the section. /// When the section is uncollapsed or the text containing it is deleted, /// this property returns null. /// - public DocumentLine Start { - get => _start; - internal set => _start = value; - } - + public DocumentLine Start { get; internal set; } + /// /// Gets the end line of the section. /// When the section is uncollapsed or the text containing it is deleted, /// this property returns null. /// - public DocumentLine End { - get => _end; - internal set => _end = value; - } - + public DocumentLine End { get; internal set; } + /// /// Uncollapses the section. /// This causes the Start and End properties to be set to null! @@ -83,26 +75,28 @@ public DocumentLine End { /// public void Uncollapse() { - if (_start == null) + if (Start == null) return; - - _heightTree.Uncollapse(this); - #if DEBUG - //heightTree.CheckProperties(); - #endif - - _start = null; - _end = null; + + if (!_heightTree.IsDisposed) { + _heightTree.Uncollapse(this); +#if DEBUG + _heightTree.CheckProperties(); +#endif + } + + Start = null; + End = null; } - + /// /// Gets a string representation of the collapsed section. /// [SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.Int32.ToString")] public override string ToString() { - return "[CollapsedSection" + Id + " Start=" + (_start != null ? _start.LineNumber.ToString() : "null") - + " End=" + (_end != null ? _end.LineNumber.ToString() : "null") + "]"; + return "[CollapsedSection" + Id + " Start=" + (Start != null ? Start.LineNumber.ToString() : "null") + + " End=" + (End != null ? End.LineNumber.ToString() : "null") + "]"; } } } diff --git a/src/AvaloniaEdit/Rendering/ColorizingTransformer.cs b/src/AvaloniaEdit/Rendering/ColorizingTransformer.cs index d61f9dfb..822ac598 100644 --- a/src/AvaloniaEdit/Rendering/ColorizingTransformer.cs +++ b/src/AvaloniaEdit/Rendering/ColorizingTransformer.cs @@ -21,111 +21,100 @@ namespace AvaloniaEdit.Rendering { - /// - /// Base class for that helps - /// splitting visual elements so that colors (and other text properties) can be easily assigned - /// to individual words/characters. - /// - public abstract class ColorizingTransformer : IVisualLineTransformer, ITextViewConnect - { - /// - /// Gets the list of elements currently being transformed. - /// - protected IList CurrentElements { get; private set; } + /// + /// Base class for that helps + /// splitting visual elements so that colors (and other text properties) can be easily assigned + /// to individual words/characters. + /// + public abstract class ColorizingTransformer : IVisualLineTransformer, ITextViewConnect + { + /// + /// Gets the list of elements currently being transformed. + /// + protected IList CurrentElements { get; private set; } - /// - /// implementation. - /// Sets and calls . - /// - public void Transform(ITextRunConstructionContext context, IList elements) - { - if (CurrentElements != null) - throw new InvalidOperationException("Recursive Transform() call"); - CurrentElements = elements ?? throw new ArgumentNullException(nameof(elements)); + /// + /// implementation. + /// Sets and calls . + /// + public void Transform(ITextRunConstructionContext context, IList elements) + { + if (CurrentElements != null) + throw new InvalidOperationException("Recursive Transform() call"); + CurrentElements = elements ?? throw new ArgumentNullException(nameof(elements)); + try { + Colorize(context); + } finally { + CurrentElements = null; + } + } - try - { - Colorize(context); - } - finally - { - CurrentElements = null; - } - } + /// + /// Performs the colorization. + /// + protected abstract void Colorize(ITextRunConstructionContext context); - /// - /// Performs the colorization. - /// - protected abstract void Colorize(ITextRunConstructionContext context); + /// + /// Changes visual element properties. + /// This method accesses , so it must be called only during + /// a call. + /// This method splits s as necessary to ensure that the region + /// can be colored by setting the of whole elements, + /// and then calls the on all elements in the region. + /// + /// Start visual column of the region to change + /// End visual column of the region to change + /// Action that changes an individual . + protected void ChangeVisualElements(int visualStartColumn, int visualEndColumn, Action action) + { + if (action == null) + throw new ArgumentNullException(nameof(action)); + for (var i = 0; i < CurrentElements.Count; i++) { + var e = CurrentElements[i]; + if (e.VisualColumn > visualEndColumn) + break; + if (e.VisualColumn < visualStartColumn && + e.VisualColumn + e.VisualLength > visualStartColumn) + { + if (e.CanSplit) { + e.Split(visualStartColumn, CurrentElements, i--); + continue; + } + } + if (e.VisualColumn >= visualStartColumn && e.VisualColumn < visualEndColumn) { + if (e.VisualColumn + e.VisualLength > visualEndColumn) { + if (e.CanSplit) { + e.Split(visualEndColumn, CurrentElements, i--); + } + } else { + action(e); + } + } + } + } - /// - /// Changes visual element properties. - /// This method accesses , so it must be called only during - /// a call. - /// This method splits s as necessary to ensure that the region - /// can be colored by setting the of whole elements, - /// and then calls the on all elements in the region. - /// - /// Start visual column of the region to change - /// End visual column of the region to change - /// Action that changes an individual . - protected void ChangeVisualElements(int visualStartColumn, int visualEndColumn, Action action) - { - if (action == null) - throw new ArgumentNullException(nameof(action)); - for (int i = 0; i < CurrentElements.Count; i++) - { - VisualLineElement e = CurrentElements[i]; - if (e.VisualColumn > visualEndColumn) - break; - if (e.VisualColumn < visualStartColumn && - e.VisualColumn + e.VisualLength > visualStartColumn) - { - if (e.CanSplit) - { - e.Split(visualStartColumn, CurrentElements, i--); - continue; - } - } - if (e.VisualColumn >= visualStartColumn && e.VisualColumn < visualEndColumn) - { - if (e.VisualColumn + e.VisualLength > visualEndColumn) - { - if (e.CanSplit) - { - e.Split(visualEndColumn, CurrentElements, i--); - } - } - else - { - action(e); - } - } - } - } + /// + /// Called when added to a text view. + /// + protected virtual void OnAddToTextView(TextView textView) + { + } - /// - /// Called when added to a text view. - /// - protected virtual void OnAddToTextView(TextView textView) - { - } + /// + /// Called when removed from a text view. + /// + protected virtual void OnRemoveFromTextView(TextView textView) + { + } - /// - /// Called when removed from a text view. - /// - protected virtual void OnRemoveFromTextView(TextView textView) - { - } + void ITextViewConnect.AddToTextView(TextView textView) + { + OnAddToTextView(textView); + } - void ITextViewConnect.AddToTextView(TextView textView) - { - OnAddToTextView(textView); - } - - void ITextViewConnect.RemoveFromTextView(TextView textView) - { - OnRemoveFromTextView(textView); - } - } + void ITextViewConnect.RemoveFromTextView(TextView textView) + { + OnRemoveFromTextView(textView); + } + } } diff --git a/src/AvaloniaEdit/Rendering/ColumnRulerRenderer.cs b/src/AvaloniaEdit/Rendering/ColumnRulerRenderer.cs index 79689e66..be6d2b7e 100644 --- a/src/AvaloniaEdit/Rendering/ColumnRulerRenderer.cs +++ b/src/AvaloniaEdit/Rendering/ColumnRulerRenderer.cs @@ -29,22 +29,22 @@ namespace AvaloniaEdit.Rendering /// internal sealed class ColumnRulerRenderer : IBackgroundRenderer { - private Pen _pen; - private int _column; - private readonly TextView _textView; - + private IPen _pen; + private int _column; + private readonly TextView _textView; + public static readonly Color DefaultForeground = Colors.LightGray; - + public ColumnRulerRenderer(TextView textView) { - _pen = new Pen(new ImmutableSolidColorBrush(DefaultForeground)); + _pen = new ImmutablePen(new ImmutableSolidColorBrush(DefaultForeground), 1); _textView = textView ?? throw new ArgumentNullException(nameof(textView)); _textView.BackgroundRenderers.Add(this); } - + public KnownLayer Layer => KnownLayer.Background; - public void SetRuler(int column, Pen pen) + public void SetRuler(int column, IPen pen) { if (_column != column) { _column = column; @@ -55,17 +55,17 @@ public void SetRuler(int column, Pen pen) _textView.InvalidateLayer(Layer); } } - + public void Draw(TextView textView, DrawingContext drawingContext) { if (_column < 1) return; - double offset = textView.WideSpaceWidth * _column; - Size pixelSize = PixelSnapHelpers.GetPixelSize(textView); - double markerXPos = PixelSnapHelpers.PixelAlign(offset, pixelSize.Width); + var offset = textView.WideSpaceWidth * _column; + var pixelSize = PixelSnapHelpers.GetPixelSize(textView); + var markerXPos = PixelSnapHelpers.PixelAlign(offset, pixelSize.Width); markerXPos -= textView.ScrollOffset.X; - Point start = new Point(markerXPos, 0); - Point end = new Point(markerXPos, Math.Max(textView.DocumentHeight, textView.Bounds.Height)); - + var start = new Point(markerXPos, 0); + var end = new Point(markerXPos, Math.Max(textView.DocumentHeight, textView.Bounds.Height)); + drawingContext.DrawLine(_pen, start, end); } } diff --git a/src/AvaloniaEdit/Rendering/CurrentLineHighlightRenderer.cs b/src/AvaloniaEdit/Rendering/CurrentLineHighlightRenderer.cs index 5ef6577e..243ba8f3 100644 --- a/src/AvaloniaEdit/Rendering/CurrentLineHighlightRenderer.cs +++ b/src/AvaloniaEdit/Rendering/CurrentLineHighlightRenderer.cs @@ -37,13 +37,10 @@ internal sealed class CurrentLineHighlightRenderer : IBackgroundRenderer #region Properties - public int Line - { + public int Line { get { return _line; } - set - { - if (_line != value) - { + set { + if (_line != value) { _line = value; _textView.InvalidateLayer(Layer); } @@ -52,17 +49,21 @@ public int Line public KnownLayer Layer => KnownLayer.Selection; - public IBrush BackgroundBrush { get; set; } + public IBrush BackgroundBrush { + get; set; + } - public Pen BorderPen { get; set; } + public IPen BorderPen { + get; set; + } #endregion public CurrentLineHighlightRenderer(TextView textView) { - BorderPen = new Pen(new ImmutableSolidColorBrush(DefaultBorder)); + BorderPen = new ImmutablePen(new ImmutableSolidColorBrush(DefaultBorder), 1); - BackgroundBrush = new ImmutableSolidColorBrush(DefaultBackground); + BackgroundBrush = new ImmutableSolidColorBrush(DefaultBackground); _textView = textView ?? throw new ArgumentNullException(nameof(textView)); _textView.BackgroundRenderers.Add(this); @@ -75,7 +76,7 @@ public void Draw(TextView textView, DrawingContext drawingContext) if (!_textView.Options.HighlightCurrentLine) return; - BackgroundGeometryBuilder builder = new BackgroundGeometryBuilder(); + var builder = new BackgroundGeometryBuilder(); var visualLine = _textView.GetVisualLine(_line); if (visualLine == null) return; @@ -84,9 +85,8 @@ public void Draw(TextView textView, DrawingContext drawingContext) builder.AddRectangle(textView, new Rect(0, linePosY, textView.Bounds.Width, visualLine.Height)); - Geometry geometry = builder.CreateGeometry(); - if (geometry != null) - { + var geometry = builder.CreateGeometry(); + if (geometry != null) { drawingContext.DrawGeometry(BackgroundBrush, BorderPen, geometry); } } diff --git a/src/AvaloniaEdit/Rendering/DocumentColorizingTransformer.cs b/src/AvaloniaEdit/Rendering/DocumentColorizingTransformer.cs index e823e98f..6ba669c2 100644 --- a/src/AvaloniaEdit/Rendering/DocumentColorizingTransformer.cs +++ b/src/AvaloniaEdit/Rendering/DocumentColorizingTransformer.cs @@ -23,80 +23,74 @@ namespace AvaloniaEdit.Rendering { /// - /// Base class for that helps - /// colorizing the document. Derived classes can work with document lines - /// and text offsets and this class takes care of the visual lines and visual columns. - /// - public abstract class DocumentColorizingTransformer : ColorizingTransformer - { - private DocumentLine _currentDocumentLine; - private int _firstLineStart; - private int _currentDocumentLineStartOffset, _currentDocumentLineEndOffset; + /// Base class for that helps + /// colorizing the document. Derived classes can work with document lines + /// and text offsets and this class takes care of the visual lines and visual columns. + /// + public abstract class DocumentColorizingTransformer : ColorizingTransformer + { + private DocumentLine _currentDocumentLine; + private int _firstLineStart; + private int _currentDocumentLineStartOffset, _currentDocumentLineEndOffset; - /// - /// Gets the current ITextRunConstructionContext. - /// - protected ITextRunConstructionContext CurrentContext { get; private set; } + /// + /// Gets the current ITextRunConstructionContext. + /// + protected ITextRunConstructionContext CurrentContext { get; private set; } - /// - protected override void Colorize(ITextRunConstructionContext context) - { - CurrentContext = context ?? throw new ArgumentNullException(nameof(context)); + /// + protected override void Colorize(ITextRunConstructionContext context) + { + CurrentContext = context ?? throw new ArgumentNullException(nameof(context)); - _currentDocumentLine = context.VisualLine.FirstDocumentLine; - _firstLineStart = _currentDocumentLineStartOffset = _currentDocumentLine.Offset; - _currentDocumentLineEndOffset = _currentDocumentLineStartOffset + _currentDocumentLine.Length; - var currentDocumentLineTotalEndOffset = _currentDocumentLineStartOffset + _currentDocumentLine.TotalLength; + _currentDocumentLine = context.VisualLine.FirstDocumentLine; + _firstLineStart = _currentDocumentLineStartOffset = _currentDocumentLine.Offset; + _currentDocumentLineEndOffset = _currentDocumentLineStartOffset + _currentDocumentLine.Length; + var currentDocumentLineTotalEndOffset = _currentDocumentLineStartOffset + _currentDocumentLine.TotalLength; - if (context.VisualLine.FirstDocumentLine == context.VisualLine.LastDocumentLine) - { - ColorizeLine(_currentDocumentLine); - } - else - { - ColorizeLine(_currentDocumentLine); - // ColorizeLine modifies the visual line elements, loop through a copy of the line elements - foreach (var e in context.VisualLine.Elements.ToArray()) - { - var elementOffset = _firstLineStart + e.RelativeTextOffset; - if (elementOffset >= currentDocumentLineTotalEndOffset) - { - _currentDocumentLine = context.Document.GetLineByOffset(elementOffset); - _currentDocumentLineStartOffset = _currentDocumentLine.Offset; - _currentDocumentLineEndOffset = _currentDocumentLineStartOffset + _currentDocumentLine.Length; - currentDocumentLineTotalEndOffset = _currentDocumentLineStartOffset + _currentDocumentLine.TotalLength; - ColorizeLine(_currentDocumentLine); - } - } - } - _currentDocumentLine = null; - CurrentContext = null; - } + if (context.VisualLine.FirstDocumentLine == context.VisualLine.LastDocumentLine) { + ColorizeLine(_currentDocumentLine); + } else { + ColorizeLine(_currentDocumentLine); + // ColorizeLine modifies the visual line elements, loop through a copy of the line elements + foreach (var e in context.VisualLine.Elements.ToArray()) { + var elementOffset = _firstLineStart + e.RelativeTextOffset; + if (elementOffset >= currentDocumentLineTotalEndOffset) { + _currentDocumentLine = context.Document.GetLineByOffset(elementOffset); + _currentDocumentLineStartOffset = _currentDocumentLine.Offset; + _currentDocumentLineEndOffset = _currentDocumentLineStartOffset + _currentDocumentLine.Length; + currentDocumentLineTotalEndOffset = _currentDocumentLineStartOffset + _currentDocumentLine.TotalLength; + ColorizeLine(_currentDocumentLine); + } + } + } + _currentDocumentLine = null; + CurrentContext = null; + } - /// - /// Override this method to colorize an individual document line. - /// - protected abstract void ColorizeLine(DocumentLine line); + /// + /// Override this method to colorize an individual document line. + /// + protected abstract void ColorizeLine(DocumentLine line); - /// - /// Changes a part of the current document line. - /// - /// Start offset of the region to change - /// End offset of the region to change - /// Action that changes an individual . - protected void ChangeLinePart(int startOffset, int endOffset, Action action) - { - if (startOffset < _currentDocumentLineStartOffset || startOffset > _currentDocumentLineEndOffset) - throw new ArgumentOutOfRangeException(nameof(startOffset), startOffset, "Value must be between " + _currentDocumentLineStartOffset + " and " + _currentDocumentLineEndOffset); - if (endOffset < _currentDocumentLineStartOffset || endOffset > _currentDocumentLineEndOffset) - throw new ArgumentOutOfRangeException(nameof(endOffset), endOffset, "Value must be between " + _currentDocumentLineStartOffset + " and " + _currentDocumentLineEndOffset); - var vl = CurrentContext.VisualLine; - var visualStart = vl.GetVisualColumn(startOffset - _firstLineStart); - var visualEnd = vl.GetVisualColumn(endOffset - _firstLineStart); - if (visualStart < visualEnd) - { - ChangeVisualElements(visualStart, visualEnd, action); - } - } - } + /// + /// Changes a part of the current document line. + /// + /// Start offset of the region to change + /// End offset of the region to change + /// Action that changes an individual . + protected void ChangeLinePart(int startOffset, int endOffset, Action action) + { + if (startOffset < _currentDocumentLineStartOffset || startOffset > _currentDocumentLineEndOffset) + throw new ArgumentOutOfRangeException(nameof(startOffset), startOffset, "Value must be between " + _currentDocumentLineStartOffset + " and " + _currentDocumentLineEndOffset); + if (endOffset < startOffset || endOffset > _currentDocumentLineEndOffset) + throw new ArgumentOutOfRangeException(nameof(endOffset), endOffset, "Value must be between " + startOffset + " and " + _currentDocumentLineEndOffset); + var vl = CurrentContext.VisualLine; + var visualStart = vl.GetVisualColumn(startOffset - _firstLineStart); + var visualEnd = vl.GetVisualColumn(endOffset - _firstLineStart); + if (visualStart < visualEnd) { + ChangeVisualElements(visualStart, visualEnd, action); + } + } + } } diff --git a/src/AvaloniaEdit/Rendering/FormattedTextElement.cs b/src/AvaloniaEdit/Rendering/FormattedTextElement.cs index 7b8d5e54..6b8cc212 100644 --- a/src/AvaloniaEdit/Rendering/FormattedTextElement.cs +++ b/src/AvaloniaEdit/Rendering/FormattedTextElement.cs @@ -20,144 +20,136 @@ using Avalonia; using Avalonia.Media; using Avalonia.Media.TextFormatting; +using AvaloniaEdit.Utils; using JetBrains.Annotations; namespace AvaloniaEdit.Rendering { - /// - /// Formatted text (not normal document text). - /// This is used as base class for various VisualLineElements that are displayed using a - /// FormattedText, for example newline markers or collapsed folding sections. - /// - public class FormattedTextElement : VisualLineElement - { - internal FormattedText FormattedText { get; } - internal string Text { get; set; } - internal TextLine TextLine { get; set; } - - /// - /// Creates a new FormattedTextElement that displays the specified text - /// and occupies the specified length in the document. - /// - public FormattedTextElement(string text, int documentLength) : base(1, documentLength) - { - Text = text ?? throw new ArgumentNullException(nameof(text)); - } - - /// - /// Creates a new FormattedTextElement that displays the specified text - /// and occupies the specified length in the document. - /// - internal FormattedTextElement(TextLine text, int documentLength) : base(1, documentLength) - { - 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)); - } - - /// - [CanBeNull] - public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) - { - if (TextLine == null) - { - var formatter = TextFormatter.Current; - TextLine = PrepareText(formatter, Text, TextRunProperties); - Text = null; - } - return new FormattedTextRun(this, TextRunProperties); - } - - /// - /// Constructs a TextLine from a simple text. - /// - internal static TextLine PrepareText(TextFormatter formatter, string text, TextRunProperties properties) - { - if (formatter == null) - throw new ArgumentNullException(nameof(formatter)); - if (text == null) - throw new ArgumentNullException(nameof(text)); - if (properties == null) - throw new ArgumentNullException(nameof(properties)); - return formatter.FormatLine( - new SimpleTextSource(text.AsMemory(), properties), - 0, - 32000, - - //DefaultIncrementalTab = 40 - - new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true, false, - properties, TextWrapping.NoWrap, 0, 0)); - } - } - - /// - /// This is the TextRun implementation used by the class. - /// - public class FormattedTextRun : DrawableTextRun - { - /// - /// Creates a new FormattedTextRun. - /// - public FormattedTextRun(FormattedTextElement element, TextRunProperties properties) - { - Properties = properties ?? throw new ArgumentNullException(nameof(properties)); - Element = element ?? throw new ArgumentNullException(nameof(element)); - - Size = GetSize(); - } - - /// - /// Gets the element for which the FormattedTextRun was created. - /// - public FormattedTextElement Element { get; } - - /// - public override int TextSourceLength => Element.VisualLength; - - /// - public override TextRunProperties Properties { get; } - - public override Size Size { get; } - - public override double Baseline => - Element.FormattedText?.Baseline ?? Element.TextLine.Baseline; - - private Size GetSize() - { - 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); - } - - /// - 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); - } - } - } + /// + /// Formatted text (not normal document text). + /// This is used as base class for various VisualLineElements that are displayed using a + /// FormattedText, for example newline markers or collapsed folding sections. + /// + public class FormattedTextElement : VisualLineElement + { + internal readonly FormattedText FormattedText; + internal string Text; + internal TextLine TextLine; + + /// + /// Creates a new FormattedTextElement that displays the specified text + /// and occupies the specified length in the document. + /// + public FormattedTextElement(string text, int documentLength) : base(1, documentLength) + { + Text = 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(TextLine text, int documentLength) : base(1, documentLength) + { + 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) + { + if (TextLine == null) { + var formatter = TextFormatterFactory.Create(context.TextView); + TextLine = PrepareText(formatter, Text, TextRunProperties); + Text = null; + } + return new FormattedTextRun(this, TextRunProperties); + } + + /// + /// Constructs a TextLine from a simple text. + /// + public static TextLine PrepareText(TextFormatter formatter, string text, TextRunProperties properties) + { + if (formatter == null) + throw new ArgumentNullException(nameof(formatter)); + if (text == null) + throw new ArgumentNullException(nameof(text)); + if (properties == null) + throw new ArgumentNullException(nameof(properties)); + return formatter.FormatLine( + new SimpleTextSource(text, properties), + 0, + 32000, + new VisualLineTextParagraphProperties { + defaultTextRunProperties = properties, + textWrapping = TextWrapping.NoWrap, + tabSize = 40 + }, + null); + } + } + + /// + /// This is the TextRun implementation used by the class. + /// + public class FormattedTextRun : DrawableTextRun + { + /// + /// Creates a new FormattedTextRun. + /// + public FormattedTextRun(FormattedTextElement element, TextRunProperties properties) + { + if (properties == null) + throw new ArgumentNullException(nameof(properties)); + Properties = properties; + Element = element ?? throw new ArgumentNullException(nameof(element)); + } + + /// + /// Gets the element for which the FormattedTextRun was created. + /// + public FormattedTextElement Element { get; } + + /// + public override TextRunProperties Properties { get; } + + public override double Baseline => Element.FormattedText?.Baseline ?? Element.TextLine.Baseline; + + /// + public override Size Size + { + get + { + 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); + } + } + + /// + public override void Draw(DrawingContext drawingContext, Point origin) + { + if (Element.FormattedText != null) { + //var y = origin.Y - Element.FormattedText.Baseline; + drawingContext.DrawText(Element.FormattedText, origin); + } else { + //var y = origin.Y - Element.TextLine.Baseline; + Element.TextLine.Draw(drawingContext, origin); + } + } + } } diff --git a/src/AvaloniaEdit/Rendering/FormattedTextExtensions.cs b/src/AvaloniaEdit/Rendering/FormattedTextExtensions.cs deleted file mode 100644 index 2ea559d8..00000000 --- a/src/AvaloniaEdit/Rendering/FormattedTextExtensions.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace AvaloniaEdit.Rendering -{ - using Avalonia.Media; - using System.Collections.Generic; - -} diff --git a/src/AvaloniaEdit/Rendering/GlobalTextRunProperties.cs b/src/AvaloniaEdit/Rendering/GlobalTextRunProperties.cs new file mode 100644 index 00000000..e1ae76a4 --- /dev/null +++ b/src/AvaloniaEdit/Rendering/GlobalTextRunProperties.cs @@ -0,0 +1,46 @@ +// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System.Globalization; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; + +#nullable enable + +namespace AvaloniaEdit.Rendering +{ + internal sealed class GlobalTextRunProperties : TextRunProperties + { + internal Typeface typeface; + internal double fontRenderingEmSize; + internal IBrush? foregroundBrush; + internal CultureInfo? cultureInfo; + + public override Typeface Typeface => typeface; + + public override double FontRenderingEmSize => fontRenderingEmSize; + + //public override double FontHintingEmSize { get { return fontRenderingEmSize; } } + public override TextDecorationCollection? TextDecorations => null; + public override IBrush? ForegroundBrush => foregroundBrush; + public override IBrush? BackgroundBrush => null; + + public override CultureInfo? CultureInfo => cultureInfo; + //public override TextEffectCollection TextEffects { get { return null; } } + } +} diff --git a/src/AvaloniaEdit/Rendering/HeightTree.cs b/src/AvaloniaEdit/Rendering/HeightTree.cs index e331f6cc..5311f3f9 100644 --- a/src/AvaloniaEdit/Rendering/HeightTree.cs +++ b/src/AvaloniaEdit/Rendering/HeightTree.cs @@ -19,27 +19,24 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Text; -using Avalonia.Utilities; using AvaloniaEdit.Document; using AvaloniaEdit.Utils; namespace AvaloniaEdit.Rendering { - /// - /// Red-black tree similar to DocumentLineTree, augmented with collapsing and height data. - /// - internal sealed class HeightTree : ILineTracker, IDisposable - { - // TODO: Optimize this. This tree takes alot of memory. - // (56 bytes for HeightTreeNode - // We should try to get rid of the dictionary and find height nodes per index. (DONE!) - // And we might do much better by compressing lines with the same height into a single node. - // That would also improve load times because we would always start with just a single node. - - /* Idea: + /// + /// Red-black tree similar to DocumentLineTree, augmented with collapsing and height data. + /// + internal sealed class HeightTree : ILineTracker, IDisposable + { + // TODO: Optimize this. This tree takes alot of memory. + // (56 bytes for HeightTreeNode + // We should try to get rid of the dictionary and find height nodes per index. (DONE!) + // And we might do much better by compressing lines with the same height into a single node. + // That would also improve load times because we would always start with just a single node. + + /* Idea: class NewHeightTreeNode { int totalCount; // =count+left.count+right.count int count; // one node can represent multiple lines @@ -54,1163 +51,1072 @@ class NewHeightTreeNode { collapsing/uncollapsing, especially when compression reduces the n. */ - #region Constructor - - private readonly TextDocument _document; - private HeightTreeNode _root; - private WeakLineTracker _weakLineTracker; - - public HeightTree(TextDocument document, double defaultLineHeight) - { - _document = document; - _weakLineTracker = WeakLineTracker.Register(document, this); - DefaultLineHeight = defaultLineHeight; - RebuildDocument(); - } - - public void Dispose() - { - _weakLineTracker?.Deregister(); - _root = null; - _weakLineTracker = null; - } - - private double _defaultLineHeight; - - public double DefaultLineHeight - { - get => _defaultLineHeight; - set - { - var oldValue = _defaultLineHeight; - if (oldValue == value) - return; - _defaultLineHeight = value; - // update the stored value in all nodes: - foreach (var node in AllNodes) - { - if (node.LineNode.Height == oldValue) - { - node.LineNode.Height = value; - UpdateAugmentedData(node, UpdateAfterChildrenChangeRecursionMode.IfRequired); - } - } - } - } - - private HeightTreeNode GetNode(DocumentLine ls) - { - return GetNodeByIndex(ls.LineNumber - 1); - } - #endregion - - #region RebuildDocument - void ILineTracker.ChangeComplete(DocumentChangeEventArgs e) - { - } - - void ILineTracker.SetLineLength(DocumentLine ls, int newTotalLength) - { - } - - /// - /// Rebuild the tree, in O(n). - /// - public void RebuildDocument() - { - foreach (var s in GetAllCollapsedSections()) - { - s.Start = null; - s.End = null; - } - - var nodes = new HeightTreeNode[_document.LineCount]; - var lineNumber = 0; - foreach (var ls in _document.Lines) - { - nodes[lineNumber++] = new HeightTreeNode(ls, _defaultLineHeight); - } - Debug.Assert(nodes.Length > 0); - // now build the corresponding balanced tree - var height = DocumentLineTree.GetTreeHeight(nodes.Length); - Debug.WriteLine("HeightTree will have height: " + height); - _root = BuildTree(nodes, 0, nodes.Length, height); - _root.Color = Black; + #region Constructor + + private readonly TextDocument _document; + private HeightTreeNode _root; + private WeakLineTracker _weakLineTracker; + + public HeightTree(TextDocument document, double defaultLineHeight) + { + this._document = document; + _weakLineTracker = WeakLineTracker.Register(document, this); + this.DefaultLineHeight = defaultLineHeight; + RebuildDocument(); + } + + public void Dispose() + { + if (_weakLineTracker != null) + _weakLineTracker.Deregister(); + this._root = null; + this._weakLineTracker = null; + } + + public bool IsDisposed { + get { + return _root == null; + } + } + + private double _defaultLineHeight; + + public double DefaultLineHeight { + get { return _defaultLineHeight; } + set { + var oldValue = _defaultLineHeight; + if (oldValue == value) + return; + _defaultLineHeight = value; + // update the stored value in all nodes: + foreach (var node in AllNodes) { + if (node.LineNode.Height == oldValue) { + node.LineNode.Height = value; + UpdateAugmentedData(node, UpdateAfterChildrenChangeRecursionMode.IfRequired); + } + } + } + } + + private HeightTreeNode GetNode(DocumentLine ls) + { + return GetNodeByIndex(ls.LineNumber - 1); + } + #endregion + + #region RebuildDocument + void ILineTracker.ChangeComplete(DocumentChangeEventArgs e) + { + } + + void ILineTracker.SetLineLength(DocumentLine ls, int newTotalLength) + { + } + + /// + /// Rebuild the tree, in O(n). + /// + public void RebuildDocument() + { + foreach (var s in GetAllCollapsedSections()) { + s.Start = null; + s.End = null; + } + + var nodes = new HeightTreeNode[_document.LineCount]; + var lineNumber = 0; + foreach (var ls in _document.Lines) { + nodes[lineNumber++] = new HeightTreeNode(ls, _defaultLineHeight); + } + Debug.Assert(nodes.Length > 0); + // now build the corresponding balanced tree + var height = DocumentLineTree.GetTreeHeight(nodes.Length); + Debug.WriteLine("HeightTree will have height: " + height); + _root = BuildTree(nodes, 0, nodes.Length, height); + _root.Color = Black; #if DEBUG - CheckProperties(); + CheckProperties(); #endif - } - - /// - /// build a tree from a list of nodes - /// - private HeightTreeNode BuildTree(HeightTreeNode[] nodes, int start, int end, int subtreeHeight) - { - Debug.Assert(start <= end); - if (start == end) - { - return null; - } - var middle = (start + end) / 2; - var node = nodes[middle]; - node.Left = BuildTree(nodes, start, middle, subtreeHeight - 1); - node.Right = BuildTree(nodes, middle + 1, end, subtreeHeight - 1); - if (node.Left != null) node.Left.Parent = node; - if (node.Right != null) node.Right.Parent = node; - if (subtreeHeight == 1) - node.Color = Red; - UpdateAugmentedData(node, UpdateAfterChildrenChangeRecursionMode.None); - return node; - } - #endregion - - #region Insert/Remove lines - void ILineTracker.BeforeRemoveLine(DocumentLine line) - { - var node = GetNode(line); - if (node.LineNode.CollapsedSections != null) - { - foreach (var cs in node.LineNode.CollapsedSections.ToArray()) - { - if (cs.Start == line && cs.End == line) - { - cs.Start = null; - cs.End = null; - } - else if (cs.Start == line) - { - Uncollapse(cs); - cs.Start = line.NextLine; - AddCollapsedSection(cs, cs.End.LineNumber - cs.Start.LineNumber + 1); - } - else if (cs.End == line) - { - Uncollapse(cs); - cs.End = line.PreviousLine; - AddCollapsedSection(cs, cs.End.LineNumber - cs.Start.LineNumber + 1); - } - } - } - BeginRemoval(); - RemoveNode(node); - // clear collapsedSections from removed line: prevent damage if removed line is in "nodesToCheckForMerging" - node.LineNode.CollapsedSections = null; - EndRemoval(); - } - - // void ILineTracker.AfterRemoveLine(DocumentLine line) - // { - // - // } - - void ILineTracker.LineInserted(DocumentLine insertionPos, DocumentLine newLine) - { - InsertAfter(GetNode(insertionPos), newLine); + } + + /// + /// build a tree from a list of nodes + /// + private HeightTreeNode BuildTree(HeightTreeNode[] nodes, int start, int end, int subtreeHeight) + { + Debug.Assert(start <= end); + if (start == end) { + return null; + } + var middle = (start + end) / 2; + var node = nodes[middle]; + node.Left = BuildTree(nodes, start, middle, subtreeHeight - 1); + node.Right = BuildTree(nodes, middle + 1, end, subtreeHeight - 1); + if (node.Left != null) node.Left.Parent = node; + if (node.Right != null) node.Right.Parent = node; + if (subtreeHeight == 1) + node.Color = Red; + UpdateAugmentedData(node, UpdateAfterChildrenChangeRecursionMode.None); + return node; + } + #endregion + + #region Insert/Remove lines + void ILineTracker.BeforeRemoveLine(DocumentLine line) + { + var node = GetNode(line); + if (node.LineNode.CollapsedSections != null) { + foreach (var cs in node.LineNode.CollapsedSections.ToArray()) { + if (cs.Start == line && cs.End == line) { + cs.Start = null; + cs.End = null; + } else if (cs.Start == line) { + Uncollapse(cs); + cs.Start = line.NextLine; + AddCollapsedSection(cs, cs.End.LineNumber - cs.Start.LineNumber + 1); + } else if (cs.End == line) { + Uncollapse(cs); + cs.End = line.PreviousLine; + AddCollapsedSection(cs, cs.End.LineNumber - cs.Start.LineNumber + 1); + } + } + } + BeginRemoval(); + RemoveNode(node); + // clear collapsedSections from removed line: prevent damage if removed line is in "nodesToCheckForMerging" + node.LineNode.CollapsedSections = null; + EndRemoval(); + } + +// void ILineTracker.AfterRemoveLine(DocumentLine line) +// { +// +// } + + void ILineTracker.LineInserted(DocumentLine insertionPos, DocumentLine newLine) + { + InsertAfter(GetNode(insertionPos), newLine); #if DEBUG - CheckProperties(); + CheckProperties(); #endif - } - - private HeightTreeNode InsertAfter(HeightTreeNode node, DocumentLine newLine) - { - var newNode = new HeightTreeNode(newLine, _defaultLineHeight); - if (node.Right == null) - { - if (node.LineNode.CollapsedSections != null) - { - // we are inserting directly after node - so copy all collapsedSections - // that do not end at node. - foreach (var cs in node.LineNode.CollapsedSections) - { - if (cs.End != node.DocumentLine) - newNode.AddDirectlyCollapsed(cs); - } - } - InsertAsRight(node, newNode); - } - else - { - node = node.Right.LeftMost; - if (node.LineNode.CollapsedSections != null) - { - // we are inserting directly before node - so copy all collapsedSections - // that do not start at node. - foreach (var cs in node.LineNode.CollapsedSections) - { - if (cs.Start != node.DocumentLine) - newNode.AddDirectlyCollapsed(cs); - } - } - InsertAsLeft(node, newNode); - } - return newNode; - } - #endregion - - #region Rotation callbacks - - private enum UpdateAfterChildrenChangeRecursionMode - { - None, - IfRequired, - WholeBranch - } - - private static void UpdateAfterChildrenChange(HeightTreeNode node) - { - UpdateAugmentedData(node, UpdateAfterChildrenChangeRecursionMode.IfRequired); - } - - private static void UpdateAugmentedData(HeightTreeNode node, UpdateAfterChildrenChangeRecursionMode mode) - { - var totalCount = 1; - var totalHeight = node.LineNode.TotalHeight; - if (node.Left != null) - { - totalCount += node.Left.TotalCount; - totalHeight += node.Left.TotalHeight; - } - if (node.Right != null) - { - totalCount += node.Right.TotalCount; - totalHeight += node.Right.TotalHeight; - } - if (node.IsDirectlyCollapsed) - totalHeight = 0; - if (totalCount != node.TotalCount - || !totalHeight.IsClose(node.TotalHeight) - || mode == UpdateAfterChildrenChangeRecursionMode.WholeBranch) - { - node.TotalCount = totalCount; - node.TotalHeight = totalHeight; - if (node.Parent != null && mode != UpdateAfterChildrenChangeRecursionMode.None) - UpdateAugmentedData(node.Parent, mode); - } - } - - private void UpdateAfterRotateLeft(HeightTreeNode node) - { - // node = old parent - // node.parent = pivot, new parent - var collapsedP = node.Parent.CollapsedSections; - var collapsedQ = node.CollapsedSections; - // move collapsedSections from old parent to new parent - node.Parent.CollapsedSections = collapsedQ; - node.CollapsedSections = null; - // split the collapsedSections from the new parent into its old children: - if (collapsedP != null) - { - foreach (var cs in collapsedP) - { - node.Parent.Right?.AddDirectlyCollapsed(cs); - node.Parent.LineNode.AddDirectlyCollapsed(cs); - node.Right?.AddDirectlyCollapsed(cs); - } - } - MergeCollapsedSectionsIfPossible(node); - - UpdateAfterChildrenChange(node); - - // not required: rotations only happen on insertions/deletions - // -> totalCount changes -> the parent is always updated - //UpdateAfterChildrenChange(node.parent); - } - - private void UpdateAfterRotateRight(HeightTreeNode node) - { - // node = old parent - // node.parent = pivot, new parent - var collapsedP = node.Parent.CollapsedSections; - var collapsedQ = node.CollapsedSections; - // move collapsedSections from old parent to new parent - node.Parent.CollapsedSections = collapsedQ; - node.CollapsedSections = null; - // split the collapsedSections from the new parent into its old children: - if (collapsedP != null) - { - foreach (var cs in collapsedP) - { - node.Parent.Left?.AddDirectlyCollapsed(cs); - node.Parent.LineNode.AddDirectlyCollapsed(cs); - node.Left?.AddDirectlyCollapsed(cs); - } - } - MergeCollapsedSectionsIfPossible(node); - - UpdateAfterChildrenChange(node); - - // not required: rotations only happen on insertions/deletions - // -> totalCount changes -> the parent is always updated - //UpdateAfterChildrenChange(node.parent); - } - - // node removal: - // a node in the middle of the tree is removed as following: - // its successor is removed - // it is replaced with its successor - - private void BeforeNodeRemove(HeightTreeNode removedNode) - { - Debug.Assert(removedNode.Left == null || removedNode.Right == null); - - var collapsed = removedNode.CollapsedSections; - if (collapsed != null) - { - var childNode = removedNode.Left ?? removedNode.Right; - if (childNode != null) - { - foreach (var cs in collapsed) - childNode.AddDirectlyCollapsed(cs); - } - } - if (removedNode.Parent != null) - MergeCollapsedSectionsIfPossible(removedNode.Parent); - } - - private void BeforeNodeReplace(HeightTreeNode removedNode, HeightTreeNode newNode, HeightTreeNode newNodeOldParent) - { - Debug.Assert(removedNode != null); - Debug.Assert(newNode != null); - while (newNodeOldParent != removedNode) - { - if (newNodeOldParent.CollapsedSections != null) - { - foreach (var cs in newNodeOldParent.CollapsedSections) - { - newNode.LineNode.AddDirectlyCollapsed(cs); - } - } - newNodeOldParent = newNodeOldParent.Parent; - } - if (newNode.CollapsedSections != null) - { - foreach (var cs in newNode.CollapsedSections) - { - newNode.LineNode.AddDirectlyCollapsed(cs); - } - } - newNode.CollapsedSections = removedNode.CollapsedSections; - MergeCollapsedSectionsIfPossible(newNode); - } - - private bool _inRemoval; - private List _nodesToCheckForMerging; - - private void BeginRemoval() - { - Debug.Assert(!_inRemoval); - if (_nodesToCheckForMerging == null) - { - _nodesToCheckForMerging = new List(); - } - _inRemoval = true; - } - - private void EndRemoval() - { - Debug.Assert(_inRemoval); - _inRemoval = false; - foreach (var node in _nodesToCheckForMerging) - { - MergeCollapsedSectionsIfPossible(node); - } - _nodesToCheckForMerging.Clear(); - } - - private void MergeCollapsedSectionsIfPossible(HeightTreeNode node) - { - Debug.Assert(node != null); - if (_inRemoval) - { - _nodesToCheckForMerging.Add(node); - return; - } - // now check if we need to merge collapsedSections together - var merged = false; - var collapsedL = node.LineNode.CollapsedSections; - if (collapsedL != null) - { - for (var i = collapsedL.Count - 1; i >= 0; i--) - { - var cs = collapsedL[i]; - if (cs.Start == node.DocumentLine || cs.End == node.DocumentLine) - continue; - if (node.Left == null - || (node.Left.CollapsedSections != null && node.Left.CollapsedSections.Contains(cs))) - { - if (node.Right == null - || (node.Right.CollapsedSections != null && node.Right.CollapsedSections.Contains(cs))) - { - // all children of node contain cs: -> merge! - node.Left?.RemoveDirectlyCollapsed(cs); - node.Right?.RemoveDirectlyCollapsed(cs); - collapsedL.RemoveAt(i); - node.AddDirectlyCollapsed(cs); - merged = true; - } - } - } - if (collapsedL.Count == 0) - node.LineNode.CollapsedSections = null; - } - if (merged && node.Parent != null) - { - MergeCollapsedSectionsIfPossible(node.Parent); - } - } - #endregion - - #region GetNodeBy... / Get...FromNode - - private HeightTreeNode GetNodeByIndex(int index) - { - Debug.Assert(index >= 0); - Debug.Assert(index < _root.TotalCount); - var node = _root; - while (true) - { - if (node.Left != null && index < node.Left.TotalCount) - { - node = node.Left; - } - else - { - if (node.Left != null) - { - index -= node.Left.TotalCount; - } - if (index == 0) - return node; - index--; - node = node.Right; - } - } - } - - private HeightTreeNode GetNodeByVisualPosition(double position) - { - var node = _root; - while (true) - { - var positionAfterLeft = position; - if (node.Left != null) - { - positionAfterLeft -= node.Left.TotalHeight; - if (MathUtilities.LessThan(positionAfterLeft, 0)) - { - // Descend into left - node = node.Left; - continue; - } - } - var positionBeforeRight = positionAfterLeft - node.LineNode.TotalHeight; - if (MathUtilities.LessThan(positionBeforeRight, 0)) - { - // Found the correct node - return node; - } - if (node.Right == null || MathUtilities.IsZero(node.Right.TotalHeight)) - { - // Can happen when position>node.totalHeight, - // i.e. at the end of the document, or due to rounding errors in previous loop iterations. - - // If node.lineNode isn't collapsed, return that. - // Also return node.lineNode if there is no previous node that we could return instead. - if (MathUtilities.GreaterThan(node.LineNode.TotalHeight, 0) || node.Left == null) - return node; - // Otherwise, descend into left (find the last non-collapsed node) - node = node.Left; - } - else - { - // Descend into right - position = positionBeforeRight; - node = node.Right; - } - } - } - - private static double GetVisualPositionFromNode(HeightTreeNode node) - { - var position = node.Left?.TotalHeight ?? 0; - while (node.Parent != null) - { - if (node.IsDirectlyCollapsed) - position = 0; - if (node == node.Parent.Right) - { - if (node.Parent.Left != null) - position += node.Parent.Left.TotalHeight; - position += node.Parent.LineNode.TotalHeight; - } - node = node.Parent; - } - return position; - } - #endregion - - #region Public methods - public DocumentLine GetLineByNumber(int number) - { - return GetNodeByIndex(number - 1).DocumentLine; - } - - public DocumentLine GetLineByVisualPosition(double position) - { - return GetNodeByVisualPosition(position).DocumentLine; - } - - public double GetVisualPosition(DocumentLine line) - { - return GetVisualPositionFromNode(GetNode(line)); - } - - public double GetHeight(DocumentLine line) - { - return GetNode(line).LineNode.Height; - } - - public void SetHeight(DocumentLine line, double val) - { - var node = GetNode(line); - node.LineNode.Height = val; - UpdateAfterChildrenChange(node); - } - - public bool GetIsCollapsed(int lineNumber) - { - var node = GetNodeByIndex(lineNumber - 1); - return node.LineNode.IsDirectlyCollapsed || GetIsCollapedFromNode(node); - } - - /// - /// Collapses the specified text section. - /// Runtime: O(log n) - /// - public CollapsedLineSection CollapseText(DocumentLine start, DocumentLine end) - { - if (!_document.Lines.Contains(start)) - throw new ArgumentException("Line is not part of this document", nameof(start)); - if (!_document.Lines.Contains(end)) - throw new ArgumentException("Line is not part of this document", nameof(end)); - var length = end.LineNumber - start.LineNumber + 1; - if (length < 0) - throw new ArgumentException("start must be a line before end"); - var section = new CollapsedLineSection(this, start, end); - AddCollapsedSection(section, length); + } + + private HeightTreeNode InsertAfter(HeightTreeNode node, DocumentLine newLine) + { + var newNode = new HeightTreeNode(newLine, _defaultLineHeight); + if (node.Right == null) { + if (node.LineNode.CollapsedSections != null) { + // we are inserting directly after node - so copy all collapsedSections + // that do not end at node. + foreach (var cs in node.LineNode.CollapsedSections) { + if (cs.End != node.DocumentLine) + newNode.AddDirectlyCollapsed(cs); + } + } + InsertAsRight(node, newNode); + } else { + node = node.Right.LeftMost; + if (node.LineNode.CollapsedSections != null) { + // we are inserting directly before node - so copy all collapsedSections + // that do not start at node. + foreach (var cs in node.LineNode.CollapsedSections) { + if (cs.Start != node.DocumentLine) + newNode.AddDirectlyCollapsed(cs); + } + } + InsertAsLeft(node, newNode); + } + return newNode; + } + #endregion + + #region Rotation callbacks + + private enum UpdateAfterChildrenChangeRecursionMode + { + None, + IfRequired, + WholeBranch + } + + private static void UpdateAfterChildrenChange(HeightTreeNode node) + { + UpdateAugmentedData(node, UpdateAfterChildrenChangeRecursionMode.IfRequired); + } + + private static void UpdateAugmentedData(HeightTreeNode node, UpdateAfterChildrenChangeRecursionMode mode) + { + var totalCount = 1; + var totalHeight = node.LineNode.TotalHeight; + if (node.Left != null) { + totalCount += node.Left.TotalCount; + totalHeight += node.Left.TotalHeight; + } + if (node.Right != null) { + totalCount += node.Right.TotalCount; + totalHeight += node.Right.TotalHeight; + } + if (node.IsDirectlyCollapsed) + totalHeight = 0; + if (totalCount != node.TotalCount + || !totalHeight.IsClose(node.TotalHeight) + || mode == UpdateAfterChildrenChangeRecursionMode.WholeBranch) + { + node.TotalCount = totalCount; + node.TotalHeight = totalHeight; + if (node.Parent != null && mode != UpdateAfterChildrenChangeRecursionMode.None) + UpdateAugmentedData(node.Parent, mode); + } + } + + private void UpdateAfterRotateLeft(HeightTreeNode node) + { + // node = old parent + // node.parent = pivot, new parent + var collapsedP = node.Parent.CollapsedSections; + var collapsedQ = node.CollapsedSections; + // move collapsedSections from old parent to new parent + node.Parent.CollapsedSections = collapsedQ; + node.CollapsedSections = null; + // split the collapsedSections from the new parent into its old children: + if (collapsedP != null) { + foreach (var cs in collapsedP) { + if (node.Parent.Right != null) + node.Parent.Right.AddDirectlyCollapsed(cs); + node.Parent.LineNode.AddDirectlyCollapsed(cs); + if (node.Right != null) + node.Right.AddDirectlyCollapsed(cs); + } + } + MergeCollapsedSectionsIfPossible(node); + + UpdateAfterChildrenChange(node); + + // not required: rotations only happen on insertions/deletions + // -> totalCount changes -> the parent is always updated + //UpdateAfterChildrenChange(node.parent); + } + + private void UpdateAfterRotateRight(HeightTreeNode node) + { + // node = old parent + // node.parent = pivot, new parent + var collapsedP = node.Parent.CollapsedSections; + var collapsedQ = node.CollapsedSections; + // move collapsedSections from old parent to new parent + node.Parent.CollapsedSections = collapsedQ; + node.CollapsedSections = null; + // split the collapsedSections from the new parent into its old children: + if (collapsedP != null) { + foreach (var cs in collapsedP) { + if (node.Parent.Left != null) + node.Parent.Left.AddDirectlyCollapsed(cs); + node.Parent.LineNode.AddDirectlyCollapsed(cs); + if (node.Left != null) + node.Left.AddDirectlyCollapsed(cs); + } + } + MergeCollapsedSectionsIfPossible(node); + + UpdateAfterChildrenChange(node); + + // not required: rotations only happen on insertions/deletions + // -> totalCount changes -> the parent is always updated + //UpdateAfterChildrenChange(node.parent); + } + + // node removal: + // a node in the middle of the tree is removed as following: + // its successor is removed + // it is replaced with its successor + + private void BeforeNodeRemove(HeightTreeNode removedNode) + { + Debug.Assert(removedNode.Left == null || removedNode.Right == null); + + var collapsed = removedNode.CollapsedSections; + if (collapsed != null) { + var childNode = removedNode.Left ?? removedNode.Right; + if (childNode != null) { + foreach (var cs in collapsed) + childNode.AddDirectlyCollapsed(cs); + } + } + if (removedNode.Parent != null) + MergeCollapsedSectionsIfPossible(removedNode.Parent); + } + + private void BeforeNodeReplace(HeightTreeNode removedNode, HeightTreeNode newNode, HeightTreeNode newNodeOldParent) + { + Debug.Assert(removedNode != null); + Debug.Assert(newNode != null); + while (newNodeOldParent != removedNode) { + if (newNodeOldParent.CollapsedSections != null) { + foreach (var cs in newNodeOldParent.CollapsedSections) { + newNode.LineNode.AddDirectlyCollapsed(cs); + } + } + newNodeOldParent = newNodeOldParent.Parent; + } + if (newNode.CollapsedSections != null) { + foreach (var cs in newNode.CollapsedSections) { + newNode.LineNode.AddDirectlyCollapsed(cs); + } + } + newNode.CollapsedSections = removedNode.CollapsedSections; + MergeCollapsedSectionsIfPossible(newNode); + } + + private bool _inRemoval; + private List _nodesToCheckForMerging; + + private void BeginRemoval() + { + Debug.Assert(!_inRemoval); + if (_nodesToCheckForMerging == null) { + _nodesToCheckForMerging = new List(); + } + _inRemoval = true; + } + + private void EndRemoval() + { + Debug.Assert(_inRemoval); + _inRemoval = false; + foreach (var node in _nodesToCheckForMerging) { + MergeCollapsedSectionsIfPossible(node); + } + _nodesToCheckForMerging.Clear(); + } + + private void MergeCollapsedSectionsIfPossible(HeightTreeNode node) + { + Debug.Assert(node != null); + if (_inRemoval) { + _nodesToCheckForMerging.Add(node); + return; + } + // now check if we need to merge collapsedSections together + var merged = false; + var collapsedL = node.LineNode.CollapsedSections; + if (collapsedL != null) { + for (var i = collapsedL.Count - 1; i >= 0; i--) { + var cs = collapsedL[i]; + if (cs.Start == node.DocumentLine || cs.End == node.DocumentLine) + continue; + if (node.Left == null + || (node.Left.CollapsedSections != null && node.Left.CollapsedSections.Contains(cs))) + { + if (node.Right == null + || (node.Right.CollapsedSections != null && node.Right.CollapsedSections.Contains(cs))) + { + // all children of node contain cs: -> merge! + if (node.Left != null) node.Left.RemoveDirectlyCollapsed(cs); + if (node.Right != null) node.Right.RemoveDirectlyCollapsed(cs); + collapsedL.RemoveAt(i); + node.AddDirectlyCollapsed(cs); + merged = true; + } + } + } + if (collapsedL.Count == 0) + node.LineNode.CollapsedSections = null; + } + if (merged && node.Parent != null) { + MergeCollapsedSectionsIfPossible(node.Parent); + } + } + #endregion + + #region GetNodeBy... / Get...FromNode + + private HeightTreeNode GetNodeByIndex(int index) + { + Debug.Assert(index >= 0); + Debug.Assert(index < _root.TotalCount); + var node = _root; + while (true) { + if (node.Left != null && index < node.Left.TotalCount) { + node = node.Left; + } else { + if (node.Left != null) { + index -= node.Left.TotalCount; + } + if (index == 0) + return node; + index--; + node = node.Right; + } + } + } + + private HeightTreeNode GetNodeByVisualPosition(double position) + { + var node = _root; + while (true) { + var positionAfterLeft = position; + if (node.Left != null) { + positionAfterLeft -= node.Left.TotalHeight; + if (positionAfterLeft < 0) { + // Descend into left + node = node.Left; + continue; + } + } + var positionBeforeRight = positionAfterLeft - node.LineNode.TotalHeight; + if (positionBeforeRight < 0) { + // Found the correct node + return node; + } + if (node.Right == null || node.Right.TotalHeight == 0) { + // Can happen when position>node.totalHeight, + // i.e. at the end of the document, or due to rounding errors in previous loop iterations. + + // If node.lineNode isn't collapsed, return that. + // Also return node.lineNode if there is no previous node that we could return instead. + if (node.LineNode.TotalHeight > 0 || node.Left == null) + return node; + // Otherwise, descend into left (find the last non-collapsed node) + node = node.Left; + } else { + // Descend into right + position = positionBeforeRight; + node = node.Right; + } + } + } + + private static double GetVisualPositionFromNode(HeightTreeNode node) + { + var position = (node.Left != null) ? node.Left.TotalHeight : 0; + while (node.Parent != null) { + if (node.IsDirectlyCollapsed) + position = 0; + if (node == node.Parent.Right) { + if (node.Parent.Left != null) + position += node.Parent.Left.TotalHeight; + position += node.Parent.LineNode.TotalHeight; + } + node = node.Parent; + } + return position; + } + #endregion + + #region Public methods + public DocumentLine GetLineByNumber(int number) + { + return GetNodeByIndex(number - 1).DocumentLine; + } + + public DocumentLine GetLineByVisualPosition(double position) + { + return GetNodeByVisualPosition(position).DocumentLine; + } + + public double GetVisualPosition(DocumentLine line) + { + return GetVisualPositionFromNode(GetNode(line)); + } + + public double GetHeight(DocumentLine line) + { + return GetNode(line).LineNode.Height; + } + + public void SetHeight(DocumentLine line, double val) + { + var node = GetNode(line); + node.LineNode.Height = val; + UpdateAfterChildrenChange(node); + } + + public bool GetIsCollapsed(int lineNumber) + { + var node = GetNodeByIndex(lineNumber - 1); + return node.LineNode.IsDirectlyCollapsed || GetIsCollapedFromNode(node); + } + + /// + /// Collapses the specified text section. + /// Runtime: O(log n) + /// + public CollapsedLineSection CollapseText(DocumentLine start, DocumentLine end) + { + if (!_document.Lines.Contains(start)) + throw new ArgumentException("Line is not part of this document", nameof(start)); + if (!_document.Lines.Contains(end)) + throw new ArgumentException("Line is not part of this document", nameof(end)); + var length = end.LineNumber - start.LineNumber + 1; + if (length < 0) + throw new ArgumentException("start must be a line before end"); + var section = new CollapsedLineSection(this, start, end); + AddCollapsedSection(section, length); #if DEBUG - CheckProperties(); + CheckProperties(); #endif - return section; - } - #endregion - - #region LineCount & TotalHeight - public int LineCount => _root.TotalCount; - - public double TotalHeight => _root.TotalHeight; - - #endregion - - #region GetAllCollapsedSections - - private IEnumerable AllNodes - { - get - { - if (_root != null) - { - var node = _root.LeftMost; - while (node != null) - { - yield return node; - node = node.Successor; - } - } - } - } - - internal IEnumerable GetAllCollapsedSections() - { - var emptyCsList = new List(); - return AllNodes.SelectMany( - node => (node.LineNode.CollapsedSections ?? emptyCsList).Concat( - node.CollapsedSections ?? emptyCsList) - ).Distinct(); - } - #endregion - - #region CheckProperties + return section; + } + #endregion + + #region LineCount & TotalHeight + public int LineCount { + get { + return _root.TotalCount; + } + } + + public double TotalHeight { + get { + return _root.TotalHeight; + } + } + #endregion + + #region GetAllCollapsedSections + + private IEnumerable AllNodes { + get { + if (_root != null) { + var node = _root.LeftMost; + while (node != null) { + yield return node; + node = node.Successor; + } + } + } + } + + internal IEnumerable GetAllCollapsedSections() + { + var emptyCsList = new List(); + return System.Linq.Enumerable.Distinct( + System.Linq.Enumerable.SelectMany( + AllNodes, node => System.Linq.Enumerable.Concat(node.LineNode.CollapsedSections ?? emptyCsList, + node.CollapsedSections ?? emptyCsList) + )); + } + #endregion + + #region CheckProperties #if DEBUG - [Conditional("DATACONSISTENCYTEST")] - internal void CheckProperties() - { - CheckProperties(_root); - - foreach (var cs in GetAllCollapsedSections()) - { - Debug.Assert(GetNode(cs.Start).LineNode.CollapsedSections.Contains(cs)); - Debug.Assert(GetNode(cs.End).LineNode.CollapsedSections.Contains(cs)); - var endLine = cs.End.LineNumber; - for (var i = cs.Start.LineNumber; i <= endLine; i++) - { - CheckIsInSection(cs, GetLineByNumber(i)); - } - } - - // check red-black property: - var blackCount = -1; - CheckNodeProperties(_root, null, Red, 0, ref blackCount); - } - - private void CheckIsInSection(CollapsedLineSection cs, DocumentLine line) - { - var node = GetNode(line); - if (node.LineNode.CollapsedSections != null && node.LineNode.CollapsedSections.Contains(cs)) - return; - while (node != null) - { - if (node.CollapsedSections != null && node.CollapsedSections.Contains(cs)) - return; - node = node.Parent; - } - throw new InvalidOperationException(cs + " not found for line " + line); - } - - private void CheckProperties(HeightTreeNode node) - { - var totalCount = 1; - var totalHeight = node.LineNode.TotalHeight; - if (node.LineNode.IsDirectlyCollapsed) - Debug.Assert(node.LineNode.CollapsedSections.Count > 0); - if (node.Left != null) - { - CheckProperties(node.Left); - totalCount += node.Left.TotalCount; - totalHeight += node.Left.TotalHeight; - - CheckAllContainedIn(node.Left.CollapsedSections, node.LineNode.CollapsedSections); - } - if (node.Right != null) - { - CheckProperties(node.Right); - totalCount += node.Right.TotalCount; - totalHeight += node.Right.TotalHeight; - - CheckAllContainedIn(node.Right.CollapsedSections, node.LineNode.CollapsedSections); - } - if (node.Left != null && node.Right != null) - { - if (node.Left.CollapsedSections != null && node.Right.CollapsedSections != null) - { - var intersection = node.Left.CollapsedSections.Intersect(node.Right.CollapsedSections); - Debug.Assert(!intersection.Any()); - } - } - if (node.IsDirectlyCollapsed) - { - Debug.Assert(node.CollapsedSections.Count > 0); - totalHeight = 0; - } - Debug.Assert(node.TotalCount == totalCount); - Debug.Assert(node.TotalHeight.IsClose(totalHeight)); - } - - /// - /// Checks that all elements in list1 are contained in list2. - /// - private static void CheckAllContainedIn(IEnumerable list1, ICollection list2) - { - if (list1 == null) list1 = new List(); - if (list2 == null) list2 = new List(); - foreach (var cs in list1) - { - Debug.Assert(list2.Contains(cs)); - } - } - - /* + [Conditional("DATACONSISTENCYTEST")] + internal void CheckProperties() + { + CheckProperties(_root); + + foreach (var cs in GetAllCollapsedSections()) { + Debug.Assert(GetNode(cs.Start).LineNode.CollapsedSections.Contains(cs)); + Debug.Assert(GetNode(cs.End).LineNode.CollapsedSections.Contains(cs)); + var endLine = cs.End.LineNumber; + for (var i = cs.Start.LineNumber; i <= endLine; i++) { + CheckIsInSection(cs, GetLineByNumber(i)); + } + } + + // check red-black property: + var blackCount = -1; + CheckNodeProperties(_root, null, Red, 0, ref blackCount); + } + + private void CheckIsInSection(CollapsedLineSection cs, DocumentLine line) + { + var node = GetNode(line); + if (node.LineNode.CollapsedSections != null && node.LineNode.CollapsedSections.Contains(cs)) + return; + while (node != null) { + if (node.CollapsedSections != null && node.CollapsedSections.Contains(cs)) + return; + node = node.Parent; + } + throw new InvalidOperationException(cs + " not found for line " + line); + } + + private void CheckProperties(HeightTreeNode node) + { + var totalCount = 1; + var totalHeight = node.LineNode.TotalHeight; + if (node.LineNode.IsDirectlyCollapsed) + Debug.Assert(node.LineNode.CollapsedSections.Count > 0); + if (node.Left != null) { + CheckProperties(node.Left); + totalCount += node.Left.TotalCount; + totalHeight += node.Left.TotalHeight; + + CheckAllContainedIn(node.Left.CollapsedSections, node.LineNode.CollapsedSections); + } + if (node.Right != null) { + CheckProperties(node.Right); + totalCount += node.Right.TotalCount; + totalHeight += node.Right.TotalHeight; + + CheckAllContainedIn(node.Right.CollapsedSections, node.LineNode.CollapsedSections); + } + if (node.Left != null && node.Right != null) { + if (node.Left.CollapsedSections != null && node.Right.CollapsedSections != null) { + var intersection = System.Linq.Enumerable.Intersect(node.Left.CollapsedSections, node.Right.CollapsedSections); + Debug.Assert(System.Linq.Enumerable.Count(intersection) == 0); + } + } + if (node.IsDirectlyCollapsed) { + Debug.Assert(node.CollapsedSections.Count > 0); + totalHeight = 0; + } + Debug.Assert(node.TotalCount == totalCount); + Debug.Assert(node.TotalHeight.IsClose(totalHeight)); + } + + /// + /// Checks that all elements in list1 are contained in list2. + /// + private static void CheckAllContainedIn(IEnumerable list1, ICollection list2) + { + if (list1 == null) list1 = new List(); + if (list2 == null) list2 = new List(); + foreach (var cs in list1) { + Debug.Assert(list2.Contains(cs)); + } + } + + /* 1. A node is either red or black. 2. The root is black. 3. All leaves are black. (The leaves are the NIL children.) 4. Both children of every red node are black. (So every red node must have a black parent.) 5. Every simple path from a node to a descendant leaf contains the same number of black nodes. (Not counting the leaf node.) */ - [SuppressMessage("ReSharper", "UnusedParameter.Local")] - private void CheckNodeProperties(HeightTreeNode node, HeightTreeNode parentNode, bool parentColor, int blackCount, ref int expectedBlackCount) - { - if (node == null) return; - - Debug.Assert(node.Parent == parentNode); - - if (parentColor == Red) - { - Debug.Assert(node.Color == Black); - } - if (node.Color == Black) - { - blackCount++; - } - if (node.Left == null && node.Right == null) - { - // node is a leaf node: - if (expectedBlackCount == -1) - expectedBlackCount = blackCount; - else - Debug.Assert(expectedBlackCount == blackCount); - } - CheckNodeProperties(node.Left, node, node.Color, blackCount, ref expectedBlackCount); - CheckNodeProperties(node.Right, node, node.Color, blackCount, ref expectedBlackCount); - } - - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - public string GetTreeAsString() - { - var b = new StringBuilder(); - AppendTreeToString(_root, b, 0); - return b.ToString(); - } - - private static void AppendTreeToString(HeightTreeNode node, StringBuilder b, int indent) - { - b.Append(node.Color == Red ? "RED " : "BLACK "); - b.AppendLine(node.ToString()); - indent += 2; - if (node.Left != null) - { - b.Append(' ', indent); - b.Append("L: "); - AppendTreeToString(node.Left, b, indent); - } - if (node.Right != null) - { - b.Append(' ', indent); - b.Append("R: "); - AppendTreeToString(node.Right, b, indent); - } - } + private void CheckNodeProperties(HeightTreeNode node, HeightTreeNode parentNode, bool parentColor, int blackCount, ref int expectedBlackCount) + { + if (node == null) return; + + Debug.Assert(node.Parent == parentNode); + + if (parentColor == Red) { + Debug.Assert(node.Color == Black); + } + if (node.Color == Black) { + blackCount++; + } + if (node.Left == null && node.Right == null) { + // node is a leaf node: + if (expectedBlackCount == -1) + expectedBlackCount = blackCount; + else + Debug.Assert(expectedBlackCount == blackCount); + } + CheckNodeProperties(node.Left, node, node.Color, blackCount, ref expectedBlackCount); + CheckNodeProperties(node.Right, node, node.Color, blackCount, ref expectedBlackCount); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string GetTreeAsString() + { + var b = new StringBuilder(); + AppendTreeToString(_root, b, 0); + return b.ToString(); + } + + private static void AppendTreeToString(HeightTreeNode node, StringBuilder b, int indent) + { + if (node.Color == Red) + b.Append("RED "); + else + b.Append("BLACK "); + b.AppendLine(node.ToString()); + indent += 2; + if (node.Left != null) { + b.Append(' ', indent); + b.Append("L: "); + AppendTreeToString(node.Left, b, indent); + } + if (node.Right != null) { + b.Append(' ', indent); + b.Append("R: "); + AppendTreeToString(node.Right, b, indent); + } + } #endif - #endregion - - #region Red/Black Tree - - private const bool Red = true; - private const bool Black = false; - - private void InsertAsLeft(HeightTreeNode parentNode, HeightTreeNode newNode) - { - Debug.Assert(parentNode.Left == null); - parentNode.Left = newNode; - newNode.Parent = parentNode; - newNode.Color = Red; - UpdateAfterChildrenChange(parentNode); - FixTreeOnInsert(newNode); - } - - private void InsertAsRight(HeightTreeNode parentNode, HeightTreeNode newNode) - { - Debug.Assert(parentNode.Right == null); - parentNode.Right = newNode; - newNode.Parent = parentNode; - newNode.Color = Red; - UpdateAfterChildrenChange(parentNode); - FixTreeOnInsert(newNode); - } - - private void FixTreeOnInsert(HeightTreeNode node) - { - Debug.Assert(node != null); - Debug.Assert(node.Color == Red); - Debug.Assert(node.Left == null || node.Left.Color == Black); - Debug.Assert(node.Right == null || node.Right.Color == Black); - - var parentNode = node.Parent; - if (parentNode == null) - { - // we inserted in the root -> the node must be black - // since this is a root node, making the node black increments the number of black nodes - // on all paths by one, so it is still the same for all paths. - node.Color = Black; - return; - } - if (parentNode.Color == Black) - { - // if the parent node where we inserted was black, our red node is placed correctly. - // since we inserted a red node, the number of black nodes on each path is unchanged - // -> the tree is still balanced - return; - } - // parentNode is red, so there is a conflict here! - - // because the root is black, parentNode is not the root -> there is a grandparent node - var grandparentNode = parentNode.Parent; - var uncleNode = Sibling(parentNode); - if (uncleNode != null && uncleNode.Color == Red) - { - parentNode.Color = Black; - uncleNode.Color = Black; - grandparentNode.Color = Red; - FixTreeOnInsert(grandparentNode); - return; - } - // now we know: parent is red but uncle is black - // First rotation: - if (node == parentNode.Right && parentNode == grandparentNode.Left) - { - RotateLeft(parentNode); - node = node.Left; - } - else if (node == parentNode.Left && parentNode == grandparentNode.Right) - { - RotateRight(parentNode); - node = node.Right; - } - // because node might have changed, reassign variables: - // ReSharper disable once PossibleNullReferenceException - parentNode = node.Parent; - grandparentNode = parentNode.Parent; - - // Now recolor a bit: - parentNode.Color = Black; - grandparentNode.Color = Red; - // Second rotation: - if (node == parentNode.Left && parentNode == grandparentNode.Left) - { - RotateRight(grandparentNode); - } - else - { - // because of the first rotation, this is guaranteed: - Debug.Assert(node == parentNode.Right && parentNode == grandparentNode.Right); - RotateLeft(grandparentNode); - } - } - - private void RemoveNode(HeightTreeNode removedNode) - { - if (removedNode.Left != null && removedNode.Right != null) - { - // replace removedNode with it's in-order successor - - var leftMost = removedNode.Right.LeftMost; - var parentOfLeftMost = leftMost.Parent; - RemoveNode(leftMost); // remove leftMost from its current location - - BeforeNodeReplace(removedNode, leftMost, parentOfLeftMost); - // and overwrite the removedNode with it - ReplaceNode(removedNode, leftMost); - leftMost.Left = removedNode.Left; - if (leftMost.Left != null) leftMost.Left.Parent = leftMost; - leftMost.Right = removedNode.Right; - if (leftMost.Right != null) leftMost.Right.Parent = leftMost; - leftMost.Color = removedNode.Color; - - UpdateAfterChildrenChange(leftMost); - if (leftMost.Parent != null) UpdateAfterChildrenChange(leftMost.Parent); - return; - } - - // now either removedNode.left or removedNode.right is null - // get the remaining child - var parentNode = removedNode.Parent; - var childNode = removedNode.Left ?? removedNode.Right; - BeforeNodeRemove(removedNode); - ReplaceNode(removedNode, childNode); - if (parentNode != null) UpdateAfterChildrenChange(parentNode); - if (removedNode.Color == Black) - { - if (childNode != null && childNode.Color == Red) - { - childNode.Color = Black; - } - else - { - FixTreeOnDelete(childNode, parentNode); - } - } - } - - private void FixTreeOnDelete(HeightTreeNode node, HeightTreeNode parentNode) - { - Debug.Assert(node == null || node.Parent == parentNode); - if (parentNode == null) - return; - - // warning: node may be null - var sibling = Sibling(node, parentNode); - if (sibling.Color == Red) - { - parentNode.Color = Red; - sibling.Color = Black; - if (node == parentNode.Left) - { - RotateLeft(parentNode); - } - else - { - RotateRight(parentNode); - } - - sibling = Sibling(node, parentNode); // update value of sibling after rotation - } - - if (parentNode.Color == Black - && sibling.Color == Black - && GetColor(sibling.Left) == Black - && GetColor(sibling.Right) == Black) - { - sibling.Color = Red; - FixTreeOnDelete(parentNode, parentNode.Parent); - return; - } - - if (parentNode.Color == Red - && sibling.Color == Black - && GetColor(sibling.Left) == Black - && GetColor(sibling.Right) == Black) - { - sibling.Color = Red; - parentNode.Color = Black; - return; - } - - if (node == parentNode.Left && - sibling.Color == Black && - GetColor(sibling.Left) == Red && - GetColor(sibling.Right) == Black) - { - sibling.Color = Red; - sibling.Left.Color = Black; - RotateRight(sibling); - } - else if (node == parentNode.Right && - sibling.Color == Black && - GetColor(sibling.Right) == Red && - GetColor(sibling.Left) == Black) - { - sibling.Color = Red; - sibling.Right.Color = Black; - RotateLeft(sibling); - } - sibling = Sibling(node, parentNode); // update value of sibling after rotation - - sibling.Color = parentNode.Color; - parentNode.Color = Black; - if (node == parentNode.Left) - { - if (sibling.Right != null) - { - Debug.Assert(sibling.Right.Color == Red); - sibling.Right.Color = Black; - } - RotateLeft(parentNode); - } - else - { - if (sibling.Left != null) - { - Debug.Assert(sibling.Left.Color == Red); - sibling.Left.Color = Black; - } - RotateRight(parentNode); - } - } - - private void ReplaceNode(HeightTreeNode replacedNode, HeightTreeNode newNode) - { - if (replacedNode.Parent == null) - { - Debug.Assert(replacedNode == _root); - _root = newNode; - } - else - { - if (replacedNode.Parent.Left == replacedNode) - replacedNode.Parent.Left = newNode; - else - replacedNode.Parent.Right = newNode; - } - if (newNode != null) - { - newNode.Parent = replacedNode.Parent; - } - replacedNode.Parent = null; - } - - private void RotateLeft(HeightTreeNode p) - { - // let q be p's right child - var q = p.Right; - Debug.Assert(q != null); - Debug.Assert(q.Parent == p); - // set q to be the new root - ReplaceNode(p, q); - - // set p's right child to be q's left child - p.Right = q.Left; - if (p.Right != null) p.Right.Parent = p; - // set q's left child to be p - q.Left = p; - p.Parent = q; - UpdateAfterRotateLeft(p); - } - - private void RotateRight(HeightTreeNode p) - { - // let q be p's left child - var q = p.Left; - Debug.Assert(q != null); - Debug.Assert(q.Parent == p); - // set q to be the new root - ReplaceNode(p, q); - - // set p's left child to be q's right child - p.Left = q.Right; - if (p.Left != null) p.Left.Parent = p; - // set q's right child to be p - q.Right = p; - p.Parent = q; - UpdateAfterRotateRight(p); - } - - private static HeightTreeNode Sibling(HeightTreeNode node) - { - if (node == node.Parent.Left) - return node.Parent.Right; - return node.Parent.Left; - } - - private static HeightTreeNode Sibling(HeightTreeNode node, HeightTreeNode parentNode) - { - Debug.Assert(node == null || node.Parent == parentNode); - if (node == parentNode.Left) - return parentNode.Right; - return parentNode.Left; - } - - private static bool GetColor(HeightTreeNode node) - { - return node?.Color ?? Black; - } - #endregion - - #region Collapsing support - - private static bool GetIsCollapedFromNode(HeightTreeNode node) - { - while (node != null) - { - if (node.IsDirectlyCollapsed) - return true; - node = node.Parent; - } - return false; - } - - internal void AddCollapsedSection(CollapsedLineSection section, int sectionLength) - { - AddRemoveCollapsedSection(section, sectionLength, true); - } - - private void AddRemoveCollapsedSection(CollapsedLineSection section, int sectionLength, bool add) - { - Debug.Assert(sectionLength > 0); - - var node = GetNode(section.Start); - // Go up in the tree. - while (true) - { - // Mark all middle nodes as collapsed - if (add) - node.LineNode.AddDirectlyCollapsed(section); - else - node.LineNode.RemoveDirectlyCollapsed(section); - sectionLength -= 1; - if (sectionLength == 0) - { - // we are done! - Debug.Assert(node.DocumentLine == section.End); - break; - } - // Mark all right subtrees as collapsed. - if (node.Right != null) - { - if (node.Right.TotalCount < sectionLength) - { - if (add) - node.Right.AddDirectlyCollapsed(section); - else - node.Right.RemoveDirectlyCollapsed(section); - sectionLength -= node.Right.TotalCount; - } - else - { - // mark partially into the right subtree: go down the right subtree. - AddRemoveCollapsedSectionDown(section, node.Right, sectionLength, add); - break; - } - } - // go up to the next node - var parentNode = node.Parent; - Debug.Assert(parentNode != null); - while (parentNode.Right == node) - { - node = parentNode; - parentNode = node.Parent; - Debug.Assert(parentNode != null); - } - node = parentNode; - } - UpdateAugmentedData(GetNode(section.Start), UpdateAfterChildrenChangeRecursionMode.WholeBranch); - UpdateAugmentedData(GetNode(section.End), UpdateAfterChildrenChangeRecursionMode.WholeBranch); - } - - private static void AddRemoveCollapsedSectionDown(CollapsedLineSection section, HeightTreeNode node, int sectionLength, bool add) - { - while (true) - { - if (node.Left != null) - { - if (node.Left.TotalCount < sectionLength) - { - // mark left subtree - if (add) - node.Left.AddDirectlyCollapsed(section); - else - node.Left.RemoveDirectlyCollapsed(section); - sectionLength -= node.Left.TotalCount; - } - else - { - // mark only inside the left subtree - node = node.Left; - Debug.Assert(node != null); - continue; - } - } - if (add) - node.LineNode.AddDirectlyCollapsed(section); - else - node.LineNode.RemoveDirectlyCollapsed(section); - sectionLength -= 1; - if (sectionLength == 0) - { - // done! - Debug.Assert(node.DocumentLine == section.End); - break; - } - // mark inside right subtree: - node = node.Right; - Debug.Assert(node != null); - } - } - - public void Uncollapse(CollapsedLineSection section) - { - var sectionLength = section.End.LineNumber - section.Start.LineNumber + 1; - AddRemoveCollapsedSection(section, sectionLength, false); - // do not call CheckProperties() in here - Uncollapse is also called during line removals - } - #endregion - } + #endregion + + #region Red/Black Tree + + private const bool Red = true; + private const bool Black = false; + + private void InsertAsLeft(HeightTreeNode parentNode, HeightTreeNode newNode) + { + Debug.Assert(parentNode.Left == null); + parentNode.Left = newNode; + newNode.Parent = parentNode; + newNode.Color = Red; + UpdateAfterChildrenChange(parentNode); + FixTreeOnInsert(newNode); + } + + private void InsertAsRight(HeightTreeNode parentNode, HeightTreeNode newNode) + { + Debug.Assert(parentNode.Right == null); + parentNode.Right = newNode; + newNode.Parent = parentNode; + newNode.Color = Red; + UpdateAfterChildrenChange(parentNode); + FixTreeOnInsert(newNode); + } + + private void FixTreeOnInsert(HeightTreeNode node) + { + Debug.Assert(node != null); + Debug.Assert(node.Color == Red); + Debug.Assert(node.Left == null || node.Left.Color == Black); + Debug.Assert(node.Right == null || node.Right.Color == Black); + + var parentNode = node.Parent; + if (parentNode == null) { + // we inserted in the root -> the node must be black + // since this is a root node, making the node black increments the number of black nodes + // on all paths by one, so it is still the same for all paths. + node.Color = Black; + return; + } + if (parentNode.Color == Black) { + // if the parent node where we inserted was black, our red node is placed correctly. + // since we inserted a red node, the number of black nodes on each path is unchanged + // -> the tree is still balanced + return; + } + // parentNode is red, so there is a conflict here! + + // because the root is black, parentNode is not the root -> there is a grandparent node + var grandparentNode = parentNode.Parent; + var uncleNode = Sibling(parentNode); + if (uncleNode != null && uncleNode.Color == Red) { + parentNode.Color = Black; + uncleNode.Color = Black; + grandparentNode.Color = Red; + FixTreeOnInsert(grandparentNode); + return; + } + // now we know: parent is red but uncle is black + // First rotation: + if (node == parentNode.Right && parentNode == grandparentNode.Left) { + RotateLeft(parentNode); + node = node.Left; + } else if (node == parentNode.Left && parentNode == grandparentNode.Right) { + RotateRight(parentNode); + node = node.Right; + } + // because node might have changed, reassign variables: + parentNode = node.Parent; + grandparentNode = parentNode.Parent; + + // Now recolor a bit: + parentNode.Color = Black; + grandparentNode.Color = Red; + // Second rotation: + if (node == parentNode.Left && parentNode == grandparentNode.Left) { + RotateRight(grandparentNode); + } else { + // because of the first rotation, this is guaranteed: + Debug.Assert(node == parentNode.Right && parentNode == grandparentNode.Right); + RotateLeft(grandparentNode); + } + } + + private void RemoveNode(HeightTreeNode removedNode) + { + if (removedNode.Left != null && removedNode.Right != null) { + // replace removedNode with it's in-order successor + + var leftMost = removedNode.Right.LeftMost; + var parentOfLeftMost = leftMost.Parent; + RemoveNode(leftMost); // remove leftMost from its current location + + BeforeNodeReplace(removedNode, leftMost, parentOfLeftMost); + // and overwrite the removedNode with it + ReplaceNode(removedNode, leftMost); + leftMost.Left = removedNode.Left; + if (leftMost.Left != null) leftMost.Left.Parent = leftMost; + leftMost.Right = removedNode.Right; + if (leftMost.Right != null) leftMost.Right.Parent = leftMost; + leftMost.Color = removedNode.Color; + + UpdateAfterChildrenChange(leftMost); + if (leftMost.Parent != null) UpdateAfterChildrenChange(leftMost.Parent); + return; + } + + // now either removedNode.left or removedNode.right is null + // get the remaining child + var parentNode = removedNode.Parent; + var childNode = removedNode.Left ?? removedNode.Right; + BeforeNodeRemove(removedNode); + ReplaceNode(removedNode, childNode); + if (parentNode != null) UpdateAfterChildrenChange(parentNode); + if (removedNode.Color == Black) { + if (childNode != null && childNode.Color == Red) { + childNode.Color = Black; + } else { + FixTreeOnDelete(childNode, parentNode); + } + } + } + + private void FixTreeOnDelete(HeightTreeNode node, HeightTreeNode parentNode) + { + Debug.Assert(node == null || node.Parent == parentNode); + if (parentNode == null) + return; + + // warning: node may be null + var sibling = Sibling(node, parentNode); + if (sibling.Color == Red) { + parentNode.Color = Red; + sibling.Color = Black; + if (node == parentNode.Left) { + RotateLeft(parentNode); + } else { + RotateRight(parentNode); + } + + sibling = Sibling(node, parentNode); // update value of sibling after rotation + } + + if (parentNode.Color == Black + && sibling.Color == Black + && GetColor(sibling.Left) == Black + && GetColor(sibling.Right) == Black) + { + sibling.Color = Red; + FixTreeOnDelete(parentNode, parentNode.Parent); + return; + } + + if (parentNode.Color == Red + && sibling.Color == Black + && GetColor(sibling.Left) == Black + && GetColor(sibling.Right) == Black) + { + sibling.Color = Red; + parentNode.Color = Black; + return; + } + + if (node == parentNode.Left && + sibling.Color == Black && + GetColor(sibling.Left) == Red && + GetColor(sibling.Right) == Black) + { + sibling.Color = Red; + sibling.Left.Color = Black; + RotateRight(sibling); + } + else if (node == parentNode.Right && + sibling.Color == Black && + GetColor(sibling.Right) == Red && + GetColor(sibling.Left) == Black) + { + sibling.Color = Red; + sibling.Right.Color = Black; + RotateLeft(sibling); + } + sibling = Sibling(node, parentNode); // update value of sibling after rotation + + sibling.Color = parentNode.Color; + parentNode.Color = Black; + if (node == parentNode.Left) { + if (sibling.Right != null) { + Debug.Assert(sibling.Right.Color == Red); + sibling.Right.Color = Black; + } + RotateLeft(parentNode); + } else { + if (sibling.Left != null) { + Debug.Assert(sibling.Left.Color == Red); + sibling.Left.Color = Black; + } + RotateRight(parentNode); + } + } + + private void ReplaceNode(HeightTreeNode replacedNode, HeightTreeNode newNode) + { + if (replacedNode.Parent == null) { + Debug.Assert(replacedNode == _root); + _root = newNode; + } else { + if (replacedNode.Parent.Left == replacedNode) + replacedNode.Parent.Left = newNode; + else + replacedNode.Parent.Right = newNode; + } + if (newNode != null) { + newNode.Parent = replacedNode.Parent; + } + replacedNode.Parent = null; + } + + private void RotateLeft(HeightTreeNode p) + { + // let q be p's right child + var q = p.Right; + Debug.Assert(q != null); + Debug.Assert(q.Parent == p); + // set q to be the new root + ReplaceNode(p, q); + + // set p's right child to be q's left child + p.Right = q.Left; + if (p.Right != null) p.Right.Parent = p; + // set q's left child to be p + q.Left = p; + p.Parent = q; + UpdateAfterRotateLeft(p); + } + + private void RotateRight(HeightTreeNode p) + { + // let q be p's left child + var q = p.Left; + Debug.Assert(q != null); + Debug.Assert(q.Parent == p); + // set q to be the new root + ReplaceNode(p, q); + + // set p's left child to be q's right child + p.Left = q.Right; + if (p.Left != null) p.Left.Parent = p; + // set q's right child to be p + q.Right = p; + p.Parent = q; + UpdateAfterRotateRight(p); + } + + private static HeightTreeNode Sibling(HeightTreeNode node) + { + if (node == node.Parent.Left) + return node.Parent.Right; + else + return node.Parent.Left; + } + + private static HeightTreeNode Sibling(HeightTreeNode node, HeightTreeNode parentNode) + { + Debug.Assert(node == null || node.Parent == parentNode); + if (node == parentNode.Left) + return parentNode.Right; + else + return parentNode.Left; + } + + private static bool GetColor(HeightTreeNode node) + { + return node != null ? node.Color : Black; + } + #endregion + + #region Collapsing support + + private static bool GetIsCollapedFromNode(HeightTreeNode node) + { + while (node != null) { + if (node.IsDirectlyCollapsed) + return true; + node = node.Parent; + } + return false; + } + + internal void AddCollapsedSection(CollapsedLineSection section, int sectionLength) + { + AddRemoveCollapsedSection(section, sectionLength, true); + } + + private void AddRemoveCollapsedSection(CollapsedLineSection section, int sectionLength, bool add) + { + Debug.Assert(sectionLength > 0); + + var node = GetNode(section.Start); + // Go up in the tree. + while (true) { + // Mark all middle nodes as collapsed + if (add) + node.LineNode.AddDirectlyCollapsed(section); + else + node.LineNode.RemoveDirectlyCollapsed(section); + sectionLength -= 1; + if (sectionLength == 0) { + // we are done! + Debug.Assert(node.DocumentLine == section.End); + break; + } + // Mark all right subtrees as collapsed. + if (node.Right != null) { + if (node.Right.TotalCount < sectionLength) { + if (add) + node.Right.AddDirectlyCollapsed(section); + else + node.Right.RemoveDirectlyCollapsed(section); + sectionLength -= node.Right.TotalCount; + } else { + // mark partially into the right subtree: go down the right subtree. + AddRemoveCollapsedSectionDown(section, node.Right, sectionLength, add); + break; + } + } + // go up to the next node + var parentNode = node.Parent; + Debug.Assert(parentNode != null); + while (parentNode.Right == node) { + node = parentNode; + parentNode = node.Parent; + Debug.Assert(parentNode != null); + } + node = parentNode; + } + UpdateAugmentedData(GetNode(section.Start), UpdateAfterChildrenChangeRecursionMode.WholeBranch); + UpdateAugmentedData(GetNode(section.End), UpdateAfterChildrenChangeRecursionMode.WholeBranch); + } + + private static void AddRemoveCollapsedSectionDown(CollapsedLineSection section, HeightTreeNode node, int sectionLength, bool add) + { + while (true) { + if (node.Left != null) { + if (node.Left.TotalCount < sectionLength) { + // mark left subtree + if (add) + node.Left.AddDirectlyCollapsed(section); + else + node.Left.RemoveDirectlyCollapsed(section); + sectionLength -= node.Left.TotalCount; + } else { + // mark only inside the left subtree + node = node.Left; + Debug.Assert(node != null); + continue; + } + } + if (add) + node.LineNode.AddDirectlyCollapsed(section); + else + node.LineNode.RemoveDirectlyCollapsed(section); + sectionLength -= 1; + if (sectionLength == 0) { + // done! + Debug.Assert(node.DocumentLine == section.End); + break; + } + // mark inside right subtree: + node = node.Right; + Debug.Assert(node != null); + } + } + + public void Uncollapse(CollapsedLineSection section) + { + var sectionLength = section.End.LineNumber - section.Start.LineNumber + 1; + AddRemoveCollapsedSection(section, sectionLength, false); + // do not call CheckProperties() in here - Uncollapse is also called during line removals + } + #endregion + } } diff --git a/src/AvaloniaEdit/Rendering/HeightTreeLineNode.cs b/src/AvaloniaEdit/Rendering/HeightTreeLineNode.cs index 6a8131e6..f26176a6 100644 --- a/src/AvaloniaEdit/Rendering/HeightTreeLineNode.cs +++ b/src/AvaloniaEdit/Rendering/HeightTreeLineNode.cs @@ -32,7 +32,9 @@ internal HeightTreeLineNode(double height) internal double Height; internal List CollapsedSections; - internal bool IsDirectlyCollapsed => CollapsedSections != null; + internal bool IsDirectlyCollapsed { + get { return CollapsedSections != null; } + } internal void AddDirectlyCollapsed(CollapsedLineSection section) { @@ -52,6 +54,10 @@ internal void RemoveDirectlyCollapsed(CollapsedLineSection section) /// /// Returns 0 if the line is directly collapsed, otherwise, returns . /// - internal double TotalHeight => IsDirectlyCollapsed ? 0 : Height; + internal double TotalHeight { + get { + return IsDirectlyCollapsed ? 0 : Height; + } + } } } diff --git a/src/AvaloniaEdit/Rendering/HeightTreeNode.cs b/src/AvaloniaEdit/Rendering/HeightTreeNode.cs index e6050241..c4019dd6 100644 --- a/src/AvaloniaEdit/Rendering/HeightTreeNode.cs +++ b/src/AvaloniaEdit/Rendering/HeightTreeNode.cs @@ -26,28 +26,26 @@ namespace AvaloniaEdit.Rendering /// /// A node in the text view's height tree. /// - sealed class HeightTreeNode + internal sealed class HeightTreeNode { internal readonly DocumentLine DocumentLine; internal HeightTreeLineNode LineNode; - - internal HeightTreeNode Left; - internal HeightTreeNode Right; - internal HeightTreeNode Parent; - internal bool Color; - + + internal HeightTreeNode Left, Right, Parent; + internal bool Color; + internal HeightTreeNode() { } - + internal HeightTreeNode(DocumentLine documentLine, double height) { - DocumentLine = documentLine; - TotalCount = 1; - LineNode = new HeightTreeLineNode(height); - TotalHeight = height; + this.DocumentLine = documentLine; + this.TotalCount = 1; + this.LineNode = new HeightTreeLineNode(height); + this.TotalHeight = height; } - + internal HeightTreeNode LeftMost { get { HeightTreeNode node = this; @@ -56,7 +54,7 @@ internal HeightTreeNode LeftMost { return node; } } - + internal HeightTreeNode RightMost { get { HeightTreeNode node = this; @@ -65,7 +63,7 @@ internal HeightTreeNode RightMost { return node; } } - + /// /// Gets the inorder successor of the node. /// @@ -73,25 +71,26 @@ internal HeightTreeNode Successor { get { if (Right != null) { return Right.LeftMost; + } else { + HeightTreeNode node = this; + HeightTreeNode oldNode; + do { + oldNode = node; + node = node.Parent; + // go up until we are coming out of a left subtree + } while (node != null && node.Right == oldNode); + return node; } - HeightTreeNode node = this; - HeightTreeNode oldNode; - do { - oldNode = node; - node = node.Parent; - // go up until we are coming out of a left subtree - } while (node != null && node.Right == oldNode); - return node; } } - + /// /// The number of lines in this node and its child nodes. /// Invariant: /// totalCount = 1 + left.totalCount + right.totalCount /// internal int TotalCount; - + /// /// The total height of this node and its child nodes, excluding directly collapsed nodes. /// Invariant: @@ -100,7 +99,7 @@ internal HeightTreeNode Successor { /// + right.IsDirectlyCollapsed ? 0 : right.totalHeight /// internal double TotalHeight; - + /// /// List of the sections that hold this node collapsed. /// Invariant 1: @@ -113,10 +112,14 @@ internal HeightTreeNode Successor { /// documentLine (middle node). /// internal List CollapsedSections; - - internal bool IsDirectlyCollapsed => CollapsedSections != null; - internal void AddDirectlyCollapsed(CollapsedLineSection section) + internal bool IsDirectlyCollapsed { + get { + return CollapsedSections != null; + } + } + + internal void AddDirectlyCollapsed(CollapsedLineSection section) { if (CollapsedSections == null) { CollapsedSections = new List(); @@ -125,8 +128,8 @@ internal void AddDirectlyCollapsed(CollapsedLineSection section) Debug.Assert(!CollapsedSections.Contains(section)); CollapsedSections.Add(section); } - - + + internal void RemoveDirectlyCollapsed(CollapsedLineSection section) { Debug.Assert(CollapsedSections.Contains(section)); @@ -140,8 +143,8 @@ internal void RemoveDirectlyCollapsed(CollapsedLineSection section) TotalHeight += Right.TotalHeight; } } - - #if DEBUG + +#if DEBUG public override string ToString() { return "[HeightTreeNode " @@ -151,16 +154,16 @@ public override string ToString() + " TotalHeight=" + TotalHeight + "]"; } - + static string GetCollapsedSections(List list) { if (list == null) return "{}"; return "{" + string.Join(",", - list.Select(cs=>cs.Id).ToArray()) + list.ConvertAll(cs => cs.Id).ToArray()) + "}"; } - #endif +#endif } } diff --git a/src/AvaloniaEdit/Rendering/ITextRunConstructionContext.cs b/src/AvaloniaEdit/Rendering/ITextRunConstructionContext.cs index a74f4781..d1905cba 100644 --- a/src/AvaloniaEdit/Rendering/ITextRunConstructionContext.cs +++ b/src/AvaloniaEdit/Rendering/ITextRunConstructionContext.cs @@ -16,8 +16,6 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -using System.Globalization; -using Avalonia.Media; using Avalonia.Media.TextFormatting; using AvaloniaEdit.Document; using AvaloniaEdit.Utils; @@ -35,22 +33,22 @@ public interface ITextRunConstructionContext /// Gets the text document. /// TextDocument Document { get; } - + /// /// Gets the text view for which the construction runs. /// TextView TextView { get; } - + /// /// Gets the visual line that is currently being constructed. /// VisualLine VisualLine { get; } - + /// /// Gets the global text run properties. /// - CustomTextRunProperties GlobalTextRunProperties { get; } - + TextRunProperties GlobalTextRunProperties { get; } + /// /// Gets a piece of text from the document. /// @@ -60,115 +58,6 @@ public interface ITextRunConstructionContext /// This method should be the preferred text access method in the text transformation pipeline, as it can avoid repeatedly allocating string instances /// for text within the same line. /// - string GetText(int offset, int length); - } - - public sealed class CustomTextRunProperties : TextRunProperties - { - public const double DefaultFontRenderingEmSize = 12; - - private Typeface _typeface; - private double _fontRenderingEmSize; - private TextDecorationCollection? _textDecorations; - private IBrush? _foregroundBrush; - private IBrush? _backgroundBrush; - private CultureInfo? _cultureInfo; - private BaselineAlignment _baselineAlignment; - - internal CustomTextRunProperties(Typeface typeface, - double fontRenderingEmSize = 12, - TextDecorationCollection? textDecorations = null, - IBrush? foregroundBrush = null, - IBrush? backgroundBrush = null, - CultureInfo? cultureInfo = null, - BaselineAlignment baselineAlignment = BaselineAlignment.Baseline) - { - _typeface = typeface; - _fontRenderingEmSize = fontRenderingEmSize; - _textDecorations = textDecorations; - _foregroundBrush = foregroundBrush; - _backgroundBrush = backgroundBrush; - _cultureInfo = cultureInfo; - _baselineAlignment = baselineAlignment; - } - - public override Typeface Typeface => _typeface; - - public override double FontRenderingEmSize => _fontRenderingEmSize; - - public override TextDecorationCollection? TextDecorations => _textDecorations; - - public override IBrush? ForegroundBrush => _foregroundBrush; - - public override IBrush? BackgroundBrush => _backgroundBrush; - - public override CultureInfo? CultureInfo => _cultureInfo; - - public override BaselineAlignment BaselineAlignment => _baselineAlignment; - - public CustomTextRunProperties Clone() - { - return new CustomTextRunProperties(Typeface, FontRenderingEmSize, TextDecorations, ForegroundBrush, - BackgroundBrush, CultureInfo, BaselineAlignment); - } - - public void SetForegroundBrush(IBrush foregroundBrush) - { - _foregroundBrush = foregroundBrush; - } - - public void SetBackgroundBrush(IBrush backgroundBrush) - { - _backgroundBrush = backgroundBrush; - } - - public void SetTypeface(Typeface typeface) - { - _typeface = typeface; - } - - public void SetFontSize(int colorFontSize) - { - _fontRenderingEmSize = colorFontSize; - } - - public void SetTextDecorations(TextDecorationCollection textDecorations) - { - _textDecorations = textDecorations; - } - } - - public sealed class CustomTextParagraphProperties : TextParagraphProperties - { - public const double DefaultIncrementalTabWidth = 4 * CustomTextRunProperties.DefaultFontRenderingEmSize; - - private TextWrapping _textWrapping; - private double _lineHeight; - private double _indent; - private double _defaultIncrementalTab; - private readonly bool _firstLineInParagraph; - - public CustomTextParagraphProperties(TextRunProperties defaultTextRunProperties, - bool firstLineInParagraph = true, - TextWrapping textWrapping = TextWrapping.NoWrap, - double lineHeight = 0, - double indent = 0, - double defaultIncrementalTab = DefaultIncrementalTabWidth) - { - DefaultTextRunProperties = defaultTextRunProperties; - _firstLineInParagraph = firstLineInParagraph; - _textWrapping = textWrapping; - _lineHeight = lineHeight; - _indent = indent; - _defaultIncrementalTab = defaultIncrementalTab; - } - - public override FlowDirection FlowDirection => FlowDirection.LeftToRight; - public override TextAlignment TextAlignment => TextAlignment.Left; - public override double LineHeight => _lineHeight; - public override bool FirstLineInParagraph => _firstLineInParagraph; - public override TextRunProperties DefaultTextRunProperties { get; } - public override TextWrapping TextWrapping => _textWrapping; - public override double Indent => _indent; + StringSegment GetText(int offset, int length); } } diff --git a/src/AvaloniaEdit/Rendering/InlineObjectRun.cs b/src/AvaloniaEdit/Rendering/InlineObjectRun.cs index 09cc61c8..190e4694 100644 --- a/src/AvaloniaEdit/Rendering/InlineObjectRun.cs +++ b/src/AvaloniaEdit/Rendering/InlineObjectRun.cs @@ -22,85 +22,99 @@ using Avalonia.Media; using Avalonia.Media.TextFormatting; +#nullable enable + namespace AvaloniaEdit.Rendering { /// - /// A inline UIElement in the document. - /// - public class InlineObjectElement : VisualLineElement - { - /// - /// Gets the inline element that is displayed. - /// - public IControl Element { get; } - - /// - /// Creates a new InlineObjectElement. - /// - /// The length of the element in the document. Must be non-negative. - /// The element to display. - public InlineObjectElement(int documentLength, IControl element) - : base(1, documentLength) - { - Element = element ?? throw new ArgumentNullException(nameof(element)); - } - - /// - public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) - { - if (context == null) - throw new ArgumentNullException(nameof(context)); - - return new InlineObjectRun(1, TextRunProperties, Element); - } - } + /// A inline UIElement in the document. + /// + public class InlineObjectElement : VisualLineElement + { + /// + /// Gets the inline element that is displayed. + /// + public Control Element { get; } - /// - /// A text run with an embedded UIElement. - /// - public class InlineObjectRun : DrawableTextRun - { - /// - /// Creates a new InlineObjectRun instance. - /// - /// The length of the TextRun. - /// The to use. - /// The to display. - public InlineObjectRun(int length, TextRunProperties properties, IControl element) - { - if (length <= 0) - throw new ArgumentOutOfRangeException(nameof(length), length, "Value must be positive"); - - TextSourceLength = length; - Properties = properties ?? throw new ArgumentNullException(nameof(properties)); - Element = element ?? throw new ArgumentNullException(nameof(element)); - } - - /// - /// Gets the element displayed by the InlineObjectRun. - /// - public IControl Element { get; } - - /// - /// Gets the VisualLine that contains this object. This property is only available after the object - /// was added to the text view. - /// - public VisualLine VisualLine { get; internal set; } - - /// - public override int TextSourceLength { get; } - - /// - public override TextRunProperties Properties { get; } - - public override double Baseline => Element.DesiredSize.Height; - - public override Size Size => Element.IsMeasureValid ? Element.DesiredSize : Size.Empty; - public Size DesiredSize { get; set; } - - public override void Draw(DrawingContext drawingContext, Point origin) - { - //noop - } - } + /// + /// Creates a new InlineObjectElement. + /// + /// The length of the element in the document. Must be non-negative. + /// The element to display. + public InlineObjectElement(int documentLength, Control element) + : base(1, documentLength) + { + Element = element ?? throw new ArgumentNullException(nameof(element)); + } + + /// + public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) + { + if (context == null) + throw new ArgumentNullException(nameof(context)); + + return new InlineObjectRun(1, TextRunProperties, Element); + } + } + + /// + /// A text run with an embedded UIElement. + /// + public class InlineObjectRun : DrawableTextRun + { + internal Size DesiredSize; + + /// + /// Creates a new InlineObjectRun instance. + /// + /// The length of the TextRun. + /// The to use. + /// The to display. + public InlineObjectRun(int length, TextRunProperties? properties, Control element) + { + if (length <= 0) + throw new ArgumentOutOfRangeException(nameof(length), length, "Value must be positive"); + + TextSourceLength = length; + Properties = properties ?? throw new ArgumentNullException(nameof(properties)); + Element = element ?? throw new ArgumentNullException(nameof(element)); + + DesiredSize = element.DesiredSize; + } + + /// + /// Gets the element displayed by the InlineObjectRun. + /// + public Control Element { get; } + + /// + /// Gets the VisualLine that contains this object. This property is only available after the object + /// was added to the text view. + /// + public VisualLine? VisualLine { get; internal set; } + + public override TextRunProperties? Properties { get; } + + public override int TextSourceLength { get; } + + public override double Baseline + { + get + { + double baseline = TextBlock.GetBaselineOffset(Element); + if (double.IsNaN(baseline)) + baseline = DesiredSize.Height; + return baseline; + } + } + + /// + public override Size Size => Element.IsArrangeValid ? Element.DesiredSize : Size.Empty; + + /// + public override void Draw(DrawingContext drawingContext, Point origin) + { + // noop + } + } } diff --git a/src/AvaloniaEdit/Rendering/LinkElementGenerator.cs b/src/AvaloniaEdit/Rendering/LinkElementGenerator.cs index d585a03e..075dce72 100644 --- a/src/AvaloniaEdit/Rendering/LinkElementGenerator.cs +++ b/src/AvaloniaEdit/Rendering/LinkElementGenerator.cs @@ -18,11 +18,12 @@ using System; using System.Text.RegularExpressions; +using AvaloniaEdit.Utils; namespace AvaloniaEdit.Rendering { // This class is public because it can be used as a base class for custom links. - + /// /// Detects hyperlinks and makes them clickable. /// @@ -34,19 +35,19 @@ public class LinkElementGenerator : VisualLineElementGenerator, IBuiltinElementG { // a link starts with a protocol (or just with www), followed by 0 or more 'link characters', followed by a link end character // (this allows accepting punctuation inside links but not at the end) - internal static readonly Regex DefaultLinkRegex = new Regex(@"\b(https?://|ftp://|www\.)[\w\d\._/\-~%@()+:?&=#!]*[\w\d/]"); - + internal readonly static Regex DefaultLinkRegex = new Regex(@"\b(https?://|ftp://|www\.)[\w\d\._/\-~%@()+:?&=#!]*[\w\d/]"); + // try to detect email addresses - internal static readonly Regex DefaultMailRegex = new Regex(@"\b[\w\d\.\-]+\@[\w\d\.\-]+\.[a-z]{2,6}\b"); + internal readonly static Regex DefaultMailRegex = new Regex(@"\b[\w\d\.\-]+\@[\w\d\.\-]+\.[a-z]{2,6}\b"); + + private readonly Regex _linkRegex; - private readonly Regex _linkRegex; - /// /// Gets/Sets whether the user needs to press Control to click the link. /// The default value is true. /// public bool RequireControlModifierForClick { get; set; } - + /// /// Creates a new LinkElementGenerator. /// @@ -55,46 +56,47 @@ public LinkElementGenerator() _linkRegex = DefaultLinkRegex; RequireControlModifierForClick = true; } - + /// /// Creates a new LinkElementGenerator using the specified regex. /// protected LinkElementGenerator(Regex regex) : this() { - _linkRegex = regex ?? throw new ArgumentNullException(nameof(regex)); + _linkRegex = regex ?? throw new ArgumentNullException(nameof(regex)); } - + void IBuiltinElementGenerator.FetchOptions(TextEditorOptions options) { RequireControlModifierForClick = options.RequireControlModifierForHyperlinkClick; } - private Match GetMatch(int startOffset, out int matchOffset) + private Match GetMatch(int startOffset, out int matchOffset) { var endOffset = CurrentContext.VisualLine.LastDocumentLine.EndOffset; var relevantText = CurrentContext.GetText(startOffset, endOffset - startOffset); - var m = _linkRegex.Match(relevantText); - matchOffset = m.Success ? m.Index + startOffset : -1; + var m = _linkRegex.Match(relevantText.Text, relevantText.Offset, relevantText.Count); + matchOffset = m.Success ? m.Index - relevantText.Offset + startOffset : -1; return m; } - + /// public override int GetFirstInterestedOffset(int startOffset) { GetMatch(startOffset, out var matchOffset); return matchOffset; } - + /// public override VisualLineElement ConstructElement(int offset) { var m = GetMatch(offset, out var matchOffset); if (m.Success && matchOffset == offset) { return ConstructElementFromMatch(m); + } else { + return null; } - return null; } - + /// /// Constructs a VisualLineElement that replaces the matched text. /// The default implementation will create a @@ -105,14 +107,14 @@ protected virtual VisualLineElement ConstructElementFromMatch(Match m) var uri = GetUriFromMatch(m); if (uri == null) return null; - var linkText = new VisualLineLinkText(CurrentContext.VisualLine, m.Length) - { - NavigateUri = uri, - RequireControlModifierForClick = RequireControlModifierForClick - }; - return linkText; + var linkText = new VisualLineLinkText(CurrentContext.VisualLine, m.Length) + { + NavigateUri = uri, + RequireControlModifierForClick = RequireControlModifierForClick + }; + return linkText; } - + /// /// Fetches the URI from the regex match. Returns null if the URI format is invalid. /// @@ -121,15 +123,12 @@ protected virtual Uri GetUriFromMatch(Match match) var targetUrl = match.Value; if (targetUrl.StartsWith("www.", StringComparison.Ordinal)) targetUrl = "http://" + targetUrl; - if (Uri.IsWellFormedUriString(targetUrl, UriKind.Absolute)) - return new Uri(targetUrl); - - return null; + return Uri.IsWellFormedUriString(targetUrl, UriKind.Absolute) ? new Uri(targetUrl) : null; } } - + // This class is internal because it does not need to be accessed by the user - it can be configured using TextEditorOptions. - + /// /// Detects e-mail addresses and makes them clickable. /// @@ -146,14 +145,11 @@ public MailLinkElementGenerator() : base(DefaultMailRegex) { } - + protected override Uri GetUriFromMatch(Match match) { - var targetUrl = "mailto:" + match.Value; - if (Uri.IsWellFormedUriString(targetUrl, UriKind.Absolute)) - return new Uri(targetUrl); - - return null; + var targetUrl = "mailto:" + match.Value; + return Uri.IsWellFormedUriString(targetUrl, UriKind.Absolute) ? new Uri(targetUrl) : null; } } } diff --git a/src/AvaloniaEdit/Rendering/SimpleTextSource.cs b/src/AvaloniaEdit/Rendering/SimpleTextSource.cs index f74789db..9c171df7 100644 --- a/src/AvaloniaEdit/Rendering/SimpleTextSource.cs +++ b/src/AvaloniaEdit/Rendering/SimpleTextSource.cs @@ -16,35 +16,31 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +using System; using Avalonia.Media.TextFormatting; using Avalonia.Utilities; namespace AvaloniaEdit.Rendering { - internal sealed class SimpleTextSource : ITextSource + internal sealed class SimpleTextSource : ITextSource { - private readonly ReadOnlySlice _text; - private readonly TextRunProperties _properties; - - public SimpleTextSource(ReadOnlySlice text, TextRunProperties properties) + private readonly string _text; + private readonly TextRunProperties _properties; + + public SimpleTextSource(string text, TextRunProperties properties) { _text = text; _properties = properties; } - - public TextRun GetTextRun(int textSourceIndex) + + public TextRun GetTextRun(int textSourceCharacterIndex) { - if (textSourceIndex < _text.Length) - { - return new TextCharacters(_text, textSourceIndex, _text.Length - textSourceIndex, _properties); - } - - if (textSourceIndex > _text.Length) - { - return null; - } + if (textSourceCharacterIndex < _text.Length) + return new TextCharacters( + new ReadOnlySlice(_text.AsMemory(), textSourceCharacterIndex, + _text.Length - textSourceCharacterIndex), _properties); - return new TextEndOfParagraph(1); + return new TextEndOfParagraph(1); } } } diff --git a/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs b/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs index dcdbbc6c..b2605a25 100644 --- a/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs +++ b/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs @@ -23,195 +23,199 @@ using Avalonia.Media.Immutable; using Avalonia.Media.TextFormatting; using AvaloniaEdit.Document; +using AvaloniaEdit.Utils; using LogicalDirection = AvaloniaEdit.Document.LogicalDirection; namespace AvaloniaEdit.Rendering { // This class is internal because it does not need to be accessed by the user - it can be configured using TextEditorOptions. - /// - /// Element generator that displays · for spaces and » for tabs and a box for control characters. - /// - /// - /// This element generator is present in every TextView by default; the enabled features can be configured using the - /// . - /// - [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace")] - internal sealed class SingleCharacterElementGenerator : VisualLineElementGenerator, IBuiltinElementGenerator - { - /// - /// Gets/Sets whether to show · for spaces. - /// - public bool ShowSpaces { get; set; } - - /// - /// Gets/Sets whether to show » for tabs. - /// - public bool ShowTabs { get; set; } - - /// - /// Gets/Sets whether to show a box with the hex code for control characters. - /// - public bool ShowBoxForControlCharacters { get; set; } - - /// - /// Creates a new SingleCharacterElementGenerator instance. - /// - public SingleCharacterElementGenerator() - { - ShowSpaces = true; - ShowTabs = true; - ShowBoxForControlCharacters = true; - } - - void IBuiltinElementGenerator.FetchOptions(TextEditorOptions options) - { - ShowSpaces = options.ShowSpaces; - ShowTabs = options.ShowTabs; - ShowBoxForControlCharacters = options.ShowBoxForControlCharacters; - } - - public override int GetFirstInterestedOffset(int startOffset) - { - var endLine = CurrentContext.VisualLine.LastDocumentLine; - var relevantText = CurrentContext.GetText(startOffset, endLine.EndOffset - startOffset); - - for (var i = 0; i < relevantText.Length; i++) - { - var c = relevantText[i]; - switch (c) - { - case ' ': - if (ShowSpaces) - return startOffset + i; - break; - case '\t': - if (ShowTabs) - return startOffset + i; - break; - default: - if (ShowBoxForControlCharacters && char.IsControl(c)) - { - return startOffset + i; - } - break; - } - } - return -1; - } - - public override VisualLineElement ConstructElement(int offset) - { - var c = CurrentContext.Document.GetCharAt(offset); - if (ShowSpaces && c == ' ') - { - return new SpaceTextElement(CurrentContext.TextView.CachedElements.GetTextForNonPrintableCharacter("\u00B7", CurrentContext)); - } - if (ShowTabs && c == '\t') - { - return new TabTextElement(CurrentContext.TextView.CachedElements.GetTextForNonPrintableCharacter("\u00BB", CurrentContext)); - } - if (ShowBoxForControlCharacters && char.IsControl(c)) - { - var p = CurrentContext.GlobalTextRunProperties.Clone(); - - p.SetForegroundBrush(Brushes.White); - - var textFormatter = TextFormatter.Current; - var text = FormattedTextElement.PrepareText(textFormatter, - TextUtilities.GetControlCharacterName(c), p); - return new SpecialCharacterBoxElement(text); - } - return null; - } - - private sealed class SpaceTextElement : FormattedTextElement - { - public SpaceTextElement(TextLine textLine) : base(textLine, 1) - { - } - - public override int GetNextCaretPosition(int visualColumn, LogicalDirection direction, CaretPositioningMode mode) - { - if (mode == CaretPositioningMode.Normal || mode == CaretPositioningMode.EveryCodepoint) - return base.GetNextCaretPosition(visualColumn, direction, mode); - return -1; - } - - public override bool IsWhitespace(int visualColumn) - { - return true; - } - } - - internal sealed class TabTextElement : VisualLineElement - { - internal readonly TextLine Text; - - public TabTextElement(TextLine text) : base(2, 1) - { - Text = text; - } - - public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) - { - // the TabTextElement consists of two TextRuns: - // first a TabGlyphRun, then TextCharacters '\t' to let the fx handle the tab indentation - if (startVisualColumn == VisualColumn) - return new TabGlyphRun(this, TextRunProperties); - if (startVisualColumn == VisualColumn + 1) - return new TextCharacters("\t".AsMemory(), TextRunProperties); - throw new ArgumentOutOfRangeException(nameof(startVisualColumn)); - } - - public override int GetNextCaretPosition(int visualColumn, LogicalDirection direction, CaretPositioningMode mode) - { - if (mode == CaretPositioningMode.Normal || mode == CaretPositioningMode.EveryCodepoint) - return base.GetNextCaretPosition(visualColumn, direction, mode); - return -1; - } - - public override bool IsWhitespace(int visualColumn) - { - return true; - } - } - - internal sealed class TabGlyphRun : DrawableTextRun - { - private readonly TabTextElement _element; - - public TabGlyphRun(TabTextElement element, TextRunProperties properties) - { - Properties = properties ?? throw new ArgumentNullException(nameof(properties)); - _element = element; - } - - public override int TextSourceLength => 1; - - public override TextRunProperties Properties { get; } - - public override double Baseline => _element.Text.Height; - - public override Size Size => new(0, _element.Text.Height); - - public override void Draw(DrawingContext drawingContext, Point origin) - { - _element.Text.Draw(drawingContext, origin); - } - } - - private sealed class SpecialCharacterBoxElement : FormattedTextElement - { - public SpecialCharacterBoxElement(TextLine text) : base(text, 1) - { - } - - public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) - { - return new SpecialCharacterTextRun(this, TextRunProperties); - } - } + /// + /// Element generator that displays · for spaces and » for tabs and a box for control characters. + /// + /// + /// This element generator is present in every TextView by default; the enabled features can be configured using the + /// . + /// + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace")] + internal sealed class SingleCharacterElementGenerator : VisualLineElementGenerator, IBuiltinElementGenerator + { + /// + /// Gets/Sets whether to show · for spaces. + /// + public bool ShowSpaces { get; set; } + + /// + /// Gets/Sets whether to show » for tabs. + /// + public bool ShowTabs { get; set; } + + /// + /// Gets/Sets whether to show a box with the hex code for control characters. + /// + public bool ShowBoxForControlCharacters { get; set; } + + /// + /// Creates a new SingleCharacterElementGenerator instance. + /// + public SingleCharacterElementGenerator() + { + ShowSpaces = true; + ShowTabs = true; + ShowBoxForControlCharacters = true; + } + + void IBuiltinElementGenerator.FetchOptions(TextEditorOptions options) + { + ShowSpaces = options.ShowSpaces; + ShowTabs = options.ShowTabs; + ShowBoxForControlCharacters = options.ShowBoxForControlCharacters; + } + + public override int GetFirstInterestedOffset(int startOffset) + { + var endLine = CurrentContext.VisualLine.LastDocumentLine; + var relevantText = CurrentContext.GetText(startOffset, endLine.EndOffset - startOffset); + + for (var i = 0; i < relevantText.Count; i++) { + var c = relevantText.Text[relevantText.Offset + i]; + switch (c) { + case ' ': + if (ShowSpaces) + return startOffset + i; + break; + case '\t': + if (ShowTabs) + return startOffset + i; + break; + default: + if (ShowBoxForControlCharacters && char.IsControl(c)) { + return startOffset + i; + } + break; + } + } + return -1; + } + + public override VisualLineElement ConstructElement(int offset) + { + var c = CurrentContext.Document.GetCharAt(offset); + if (ShowSpaces && c == ' ') { + return new SpaceTextElement(CurrentContext.TextView.CachedElements.GetTextForNonPrintableCharacter("\u00B7", CurrentContext)); + } else if (ShowTabs && c == '\t') { + return new TabTextElement(CurrentContext.TextView.CachedElements.GetTextForNonPrintableCharacter("\u00BB", CurrentContext)); + } else if (ShowBoxForControlCharacters && char.IsControl(c)) { + var p = new VisualLineElementTextRunProperties(CurrentContext.GlobalTextRunProperties); + p.SetForegroundBrush(Brushes.White); + var textFormatter = TextFormatterFactory.Create(CurrentContext.TextView); + var text = FormattedTextElement.PrepareText(textFormatter, + TextUtilities.GetControlCharacterName(c), p); + return new SpecialCharacterBoxElement(text); + } else { + return null; + } + } + + private sealed class SpaceTextElement : FormattedTextElement + { + public SpaceTextElement(TextLine textLine) : base(textLine, 1) + { + } + + public override int GetNextCaretPosition(int visualColumn, LogicalDirection direction, CaretPositioningMode mode) + { + if (mode == CaretPositioningMode.Normal || mode == CaretPositioningMode.EveryCodepoint) + return base.GetNextCaretPosition(visualColumn, direction, mode); + else + return -1; + } + + public override bool IsWhitespace(int visualColumn) + { + return true; + } + } + + private sealed class TabTextElement : VisualLineElement + { + internal readonly TextLine Text; + + public TabTextElement(TextLine text) : base(2, 1) + { + Text = text; + } + + public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) + { + // the TabTextElement consists of two TextRuns: + // first a TabGlyphRun, then TextCharacters '\t' to let WPF handle the tab indentation + if (startVisualColumn == VisualColumn) + return new TabGlyphRun(this, TextRunProperties); + else if (startVisualColumn == VisualColumn + 1) + return new TextCharacters("\t".AsMemory(), 0, 1, TextRunProperties); + else + throw new ArgumentOutOfRangeException(nameof(startVisualColumn)); + } + + public override int GetNextCaretPosition(int visualColumn, LogicalDirection direction, CaretPositioningMode mode) + { + if (mode == CaretPositioningMode.Normal || mode == CaretPositioningMode.EveryCodepoint) + return base.GetNextCaretPosition(visualColumn, direction, mode); + else + return -1; + } + + public override bool IsWhitespace(int visualColumn) + { + return true; + } + } + + private sealed class TabGlyphRun : DrawableTextRun + { + private readonly TabTextElement _element; + + public TabGlyphRun(TabTextElement element, TextRunProperties properties) + { + if (properties == null) + throw new ArgumentNullException(nameof(properties)); + Properties = properties; + _element = element; + } + + public override TextRunProperties Properties { get; } + + public override double Baseline => _element.Text.Baseline; + + public override Size Size + { + get + { + var width = Math.Min(0, _element.Text.WidthIncludingTrailingWhitespace - 1); + + return new Size(width, _element.Text.Height); + } + } + + public override void Draw(DrawingContext drawingContext, Point origin) + { + var y = origin.Y - _element.Text.Baseline; + _element.Text.Draw(drawingContext, origin.WithY(y)); + } + } + + private sealed class SpecialCharacterBoxElement : FormattedTextElement + { + public SpecialCharacterBoxElement(TextLine text) : base(text, 1) + { + } + + public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) + { + return new SpecialCharacterTextRun(this, TextRunProperties); + } + } internal sealed class SpecialCharacterTextRun : FormattedTextRun { @@ -241,12 +245,19 @@ public override Size Size public override void Draw(DrawingContext drawingContext, Point origin) { - var newOrigin = new Point(origin.X + (BoxMargin / 2), origin.Y); - var metrics = Size; - var r = new Rect(origin.X, origin.Y, metrics.Width, metrics.Height); + var (x, y) = origin; + + var newOrigin = new Point(x + (BoxMargin / 2), y); + + var (width, height) = Size; + + var r = new Rect(x, y, width, height); + drawingContext.FillRectangle(DarkGrayBrush, r, 2.5f); + base.Draw(drawingContext, newOrigin); } } } } + diff --git a/src/AvaloniaEdit/Rendering/TextView.cs b/src/AvaloniaEdit/Rendering/TextView.cs index 79f2b2b8..e734aeae 100644 --- a/src/AvaloniaEdit/Rendering/TextView.cs +++ b/src/AvaloniaEdit/Rendering/TextView.cs @@ -27,11 +27,13 @@ using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Documents; using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Media.TextFormatting; using Avalonia.Threading; using Avalonia.VisualTree; @@ -61,8 +63,8 @@ static TextView() FocusableProperty.OverrideDefaultValue(false); OptionsProperty.Changed.Subscribe(OnOptionsChanged); - DocumentProperty.Changed.Subscribe(OnDocumentChanged); - } + DocumentProperty.Changed.Subscribe(OnDocumentChanged); + } private readonly ColumnRulerRenderer _columnRulerRenderer; private readonly CurrentLineHighlightRenderer _currentLineHighlighRenderer; @@ -92,7 +94,7 @@ public TextView() _hoverLogic.PointerHoverStopped += (sender, e) => RaiseHoverEventPair(e, PreviewPointerHoverStoppedEvent, PointerHoverStoppedEvent); } - #endregion + #endregion #region Document Property /// @@ -425,12 +427,12 @@ public void InsertLayer(Control layer, KnownLayer referencedLayer, LayerInsertio private readonly List _inlineObjects = new List(); - /// - /// Adds a new inline object. - /// - internal void AddInlineObject(InlineObjectRun inlineObject) - { - Debug.Assert(inlineObject.VisualLine != null); + /// + /// Adds a new inline object. + /// + internal void AddInlineObject(InlineObjectRun inlineObject) + { + Debug.Assert(inlineObject.VisualLine != null); // Remove inline object if its already added, can happen e.g. when recreating textrun for word-wrapping var alreadyAdded = false; @@ -782,43 +784,40 @@ public VisualLine GetVisualLine(int documentLineNumber) return null; } - /// - /// Gets the visual line that contains the document line with the specified number. - /// If that line is outside the visible range, a new VisualLine for that document line is constructed. - /// - public VisualLine GetOrConstructVisualLine(DocumentLine documentLine) - { - if (documentLine == null) - throw new ArgumentNullException(nameof(documentLine)); - if (!Document.Lines.Contains(documentLine)) - throw new InvalidOperationException("Line belongs to wrong document"); - VerifyAccess(); - - var l = GetVisualLine(documentLine.LineNumber); - if (l == null) - { - var globalTextRunProperties = CreateGlobalTextRunProperties(); - var paragraphProperties = CreateParagraphProperties(globalTextRunProperties); - - while (_heightTree.GetIsCollapsed(documentLine.LineNumber)) - { - documentLine = documentLine.PreviousLine; - } - - l = BuildVisualLine(documentLine, - globalTextRunProperties, paragraphProperties, - _elementGenerators.ToArray(), _lineTransformers.ToArray(), - _lastAvailableSize); - _allVisualLines.Add(l); - // update all visual top values (building the line might have changed visual top of other lines due to word wrapping) - foreach (var line in _allVisualLines) - { - line.VisualTop = _heightTree.GetVisualPosition(line.FirstDocumentLine); - } - } - return l; - } - #endregion + /// + /// Gets the visual line that contains the document line with the specified number. + /// If that line is outside the visible range, a new VisualLine for that document line is constructed. + /// + public VisualLine GetOrConstructVisualLine(DocumentLine documentLine) + { + if (documentLine == null) + throw new ArgumentNullException("documentLine"); + if (!this.Document.Lines.Contains(documentLine)) + throw new InvalidOperationException("Line belongs to wrong document"); + VerifyAccess(); + + VisualLine l = GetVisualLine(documentLine.LineNumber); + if (l == null) { + TextRunProperties globalTextRunProperties = CreateGlobalTextRunProperties(); + VisualLineTextParagraphProperties paragraphProperties = CreateParagraphProperties(globalTextRunProperties); + + while (_heightTree.GetIsCollapsed(documentLine.LineNumber)) { + documentLine = documentLine.PreviousLine; + } + + l = BuildVisualLine(documentLine, + globalTextRunProperties, paragraphProperties, + _elementGenerators.ToArray(), _lineTransformers.ToArray(), + _lastAvailableSize); + _allVisualLines.Add(l); + // update all visual top values (building the line might have changed visual top of other lines due to word wrapping) + foreach (var line in _allVisualLines) { + line.VisualTop = _heightTree.GetVisualPosition(line.FirstDocumentLine); + } + } + return l; + } + #endregion #region Visual Lines (fields and properties) @@ -897,35 +896,33 @@ public void EnsureVisualLines() } #endregion - #region Measure - /// - /// Additonal amount that allows horizontal scrolling past the end of the longest line. - /// This is necessary to ensure the caret always is visible, even when it is at the end of the longest line. - /// - private const double AdditionalHorizontalScrollAmount = 3; + #region Measure + /// + /// Additonal amount that allows horizontal scrolling past the end of the longest line. + /// This is necessary to ensure the caret always is visible, even when it is at the end of the longest line. + /// + private const double AdditionalHorizontalScrollAmount = 3; - private Size _lastAvailableSize; - private bool _inMeasure; + private Size _lastAvailableSize; + private bool _inMeasure; - /// - protected override Size MeasureOverride(Size availableSize) - { - // We don't support infinite available width, so we'll limit it to 32000 pixels. - if (availableSize.Width > 32000) - availableSize = new Size(32000, availableSize.Height); + /// + protected override Size MeasureOverride(Size availableSize) + { + // We don't support infinite available width, so we'll limit it to 32000 pixels. + if (availableSize.Width > 32000) + availableSize = availableSize.WithWidth(32000); - if (!_canHorizontallyScroll && !availableSize.Width.IsClose(_lastAvailableSize.Width)) - ClearVisualLines(); - _lastAvailableSize = availableSize; + if (!_canHorizontallyScroll && !availableSize.Width.IsClose(_lastAvailableSize.Width)) + ClearVisualLines(); + _lastAvailableSize = availableSize; - foreach (var layer in Layers) - { - layer.Measure(availableSize); - } - MeasureInlineObjects(); + foreach (var layer in Layers) { + layer.Measure(availableSize); + } + MeasureInlineObjects(); - // TODO: is this needed? - //InvalidateVisual(); // = InvalidateArrange+InvalidateRender + InvalidateVisual(); // = InvalidateArrange+InvalidateRender double maxWidth; if (_document == null) @@ -982,14 +979,14 @@ protected override Size MeasureOverride(Size availableSize) return new Size(Math.Min(availableSize.Width, maxWidth), Math.Min(availableSize.Height, heightTreeHeight)); } - /// - /// Build all VisualLines in the visible range. - /// - /// Width the longest line - private double CreateAndMeasureVisualLines(Size availableSize) - { - var globalTextRunProperties = CreateGlobalTextRunProperties(); - var paragraphProperties = CreateParagraphProperties(globalTextRunProperties); + /// + /// Build all VisualLines in the visible range. + /// + /// Width the longest line + private double CreateAndMeasureVisualLines(Size availableSize) + { + TextRunProperties globalTextRunProperties = CreateGlobalTextRunProperties(); + VisualLineTextParagraphProperties paragraphProperties = CreateParagraphProperties(globalTextRunProperties); //Debug.WriteLine("Measure availableSize=" + availableSize + ", scrollOffset=" + _scrollOffset); var firstLineInView = _heightTree.GetLineByVisualPosition(_scrollOffset.Y); @@ -1058,123 +1055,106 @@ private double CreateAndMeasureVisualLines(Size availableSize) private TextFormatter _formatter; internal TextViewCachedElements CachedElements; - private CustomTextRunProperties CreateGlobalTextRunProperties() - { - var properties = new CustomTextRunProperties - ( - new Typeface(TextBlock.GetFontFamily(this), TextBlock.GetFontStyle(this), - TextBlock.GetFontWeight(this)), - FontSize, - null, - TextBlock.GetForeground(this), - null, - cultureInfo: CultureInfo.CurrentCulture, - BaselineAlignment.Baseline - ); - - return properties; - } - - private GenericTextParagraphProperties CreateParagraphProperties(TextRunProperties defaultTextRunProperties) - { - return new GenericTextParagraphProperties - ( - FlowDirection.LeftToRight, - TextAlignment.Left, - true, - false, - defaultTextRunProperties, - _canHorizontallyScroll ? TextWrapping.NoWrap : TextWrapping.Wrap, - 0, - 0/*, - DefaultIncrementalTab = Options.IndentationSize * WideSpaceWidth*/ - ); - } - - private VisualLine BuildVisualLine(DocumentLine documentLine, - CustomTextRunProperties globalTextRunProperties, - TextParagraphProperties paragraphProperties, - VisualLineElementGenerator[] elementGeneratorsArray, - IVisualLineTransformer[] lineTransformersArray, - Size availableSize) - { - if (_heightTree.GetIsCollapsed(documentLine.LineNumber)) - throw new InvalidOperationException("Trying to build visual line from collapsed line"); - - var visualLine = new VisualLine(this, documentLine); - - var textSource = new VisualLineTextSource(visualLine) - { - Document = _document, - GlobalTextRunProperties = globalTextRunProperties, - TextView = this - }; - - visualLine.ConstructVisualElements(textSource, elementGeneratorsArray); - - if (visualLine.FirstDocumentLine != visualLine.LastDocumentLine) - { - // Check whether the lines are collapsed correctly: - var firstLinePos = _heightTree.GetVisualPosition(visualLine.FirstDocumentLine.NextLine); - var lastLinePos = _heightTree.GetVisualPosition(visualLine.LastDocumentLine.NextLine ?? visualLine.LastDocumentLine); - if (!firstLinePos.IsClose(lastLinePos)) - { - for (var i = visualLine.FirstDocumentLine.LineNumber + 1; i <= visualLine.LastDocumentLine.LineNumber; i++) - { - if (!_heightTree.GetIsCollapsed(i)) - throw new InvalidOperationException("Line " + i + " was skipped by a VisualLineElementGenerator, but it is not collapsed."); - } - throw new InvalidOperationException("All lines collapsed but visual pos different - height tree inconsistency?"); - } - } + private TextRunProperties CreateGlobalTextRunProperties() + { + var p = new GlobalTextRunProperties(); + p.typeface = this.CreateTypeface(); + p.fontRenderingEmSize = FontSize; + p.foregroundBrush = GetValue(TextBlock.ForegroundProperty); + ExtensionMethods.CheckIsFrozen(p.foregroundBrush); + p.cultureInfo = CultureInfo.CurrentCulture; + return p; + } + + private VisualLineTextParagraphProperties CreateParagraphProperties(TextRunProperties defaultTextRunProperties) + { + return new VisualLineTextParagraphProperties { + defaultTextRunProperties = defaultTextRunProperties, + textWrapping = _canHorizontallyScroll ? TextWrapping.NoWrap : TextWrapping.Wrap, + tabSize = Options.IndentationSize * WideSpaceWidth + }; + } + + private VisualLine BuildVisualLine(DocumentLine documentLine, + TextRunProperties globalTextRunProperties, + VisualLineTextParagraphProperties paragraphProperties, + VisualLineElementGenerator[] elementGeneratorsArray, + IVisualLineTransformer[] lineTransformersArray, + Size availableSize) + { + if (_heightTree.GetIsCollapsed(documentLine.LineNumber)) + throw new InvalidOperationException("Trying to build visual line from collapsed line"); + + //Debug.WriteLine("Building line " + documentLine.LineNumber); + + VisualLine visualLine = new VisualLine(this, documentLine); + VisualLineTextSource textSource = new VisualLineTextSource(visualLine) { + Document = _document, + GlobalTextRunProperties = globalTextRunProperties, + TextView = this + }; + + visualLine.ConstructVisualElements(textSource, elementGeneratorsArray); + + if (visualLine.FirstDocumentLine != visualLine.LastDocumentLine) { + // Check whether the lines are collapsed correctly: + double firstLinePos = _heightTree.GetVisualPosition(visualLine.FirstDocumentLine.NextLine); + double lastLinePos = _heightTree.GetVisualPosition(visualLine.LastDocumentLine.NextLine ?? visualLine.LastDocumentLine); + if (!firstLinePos.IsClose(lastLinePos)) { + for (int i = visualLine.FirstDocumentLine.LineNumber + 1; i <= visualLine.LastDocumentLine.LineNumber; i++) { + if (!_heightTree.GetIsCollapsed(i)) + throw new InvalidOperationException("Line " + i + " was skipped by a VisualLineElementGenerator, but it is not collapsed."); + } + throw new InvalidOperationException("All lines collapsed but visual pos different - height tree inconsistency?"); + } + } visualLine.RunTransformers(textSource, lineTransformersArray); // now construct textLines: + TextLineBreak lastLineBreak = null; var textOffset = 0; var textLines = new List(); - while (textOffset < visualLine.VisualLengthWithEndOfLineMarker) + while (textOffset <= visualLine.VisualLengthWithEndOfLineMarker) { var textLine = _formatter.FormatLine( textSource, textOffset, availableSize.Width, - paragraphProperties + paragraphProperties, + lastLineBreak ); textLines.Add(textLine); textOffset += textLine.TextRange.Length; - // exit loop so that we don't do the indentation calculation if there's only a single line - if (textOffset >= visualLine.VisualLengthWithEndOfLineMarker) - break; - - if (paragraphProperties.FirstLineInParagraph) - { - //paragraphProperties.FirstLineInParagraph = false; - - var options = Options; - double indentation = 0; - if (options.InheritWordWrapIndentation) - { - // determine indentation for next line: - var indentVisualColumn = GetIndentationVisualColumn(visualLine); - if (indentVisualColumn > 0 && indentVisualColumn < textOffset) - { - indentation = textLine.GetDistanceFromCharacterHit(new CharacterHit(indentVisualColumn)); - } - } - indentation += options.WordWrapIndentation; - // apply the calculated indentation unless it's more than half of the text editor size: - if (indentation > 0 && indentation * 2 < availableSize.Width) - { - //paragraphProperties.Indent = indentation; - } - } - } - visualLine.SetTextLines(textLines); - _heightTree.SetHeight(visualLine.FirstDocumentLine, visualLine.Height); - return visualLine; - } + // exit loop so that we don't do the indentation calculation if there's only a single line + if (textOffset >= visualLine.VisualLengthWithEndOfLineMarker) + break; + + if (paragraphProperties.firstLineInParagraph) { + paragraphProperties.firstLineInParagraph = false; + + TextEditorOptions options = this.Options; + double indentation = 0; + if (options.InheritWordWrapIndentation) { + // determine indentation for next line: + int indentVisualColumn = GetIndentationVisualColumn(visualLine); + if (indentVisualColumn > 0 && indentVisualColumn < textOffset) { + indentation = textLine.GetDistanceFromCharacterHit(new CharacterHit(indentVisualColumn, 0)); + } + } + indentation += options.WordWrapIndentation; + // apply the calculated indentation unless it's more than half of the text editor size: + if (indentation > 0 && indentation * 2 < availableSize.Width) + paragraphProperties.indent = indentation; + } + + lastLineBreak = textLine.TextLineBreak; + } + visualLine.SetTextLines(textLines); + _heightTree.SetHeight(visualLine.FirstDocumentLine, visualLine.Height); + return visualLine; + } private static int GetIndentationVisualColumn(VisualLine visualLine) { @@ -1532,11 +1512,11 @@ private void CalculateDefaultTextMetrics() if (_formatter != null) { var textRunProperties = CreateGlobalTextRunProperties(); - var line = _formatter.FormatLine( - new SimpleTextSource("x".AsMemory(), textRunProperties), + new SimpleTextSource("x", textRunProperties), 0, 32000, - new GenericTextParagraphProperties(textRunProperties)); + new VisualLineTextParagraphProperties {defaultTextRunProperties = textRunProperties}, + null); _wideSpaceWidth = Math.Max(1, line.WidthIncludingTrailingWhitespace); _defaultBaseline = Math.Max(1, line.Baseline); @@ -1548,6 +1528,7 @@ private void CalculateDefaultTextMetrics() _defaultBaseline = FontSize; _defaultLineHeight = FontSize + 3; } + // Update heightTree.DefaultLineHeight, if a document is loaded. if (_heightTree != null) _heightTree.DefaultLineHeight = _defaultLineHeight; @@ -1990,12 +1971,12 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs /// The pen used to draw the column ruler. /// /// - public static readonly StyledProperty ColumnRulerPenProperty = - AvaloniaProperty.Register("ColumnRulerBrush", CreateFrozenPen(Brushes.LightGray)); + public static readonly StyledProperty ColumnRulerPenProperty = + AvaloniaProperty.Register("ColumnRulerBrush", CreateFrozenPen(Brushes.LightGray)); - private static Pen CreateFrozenPen(IBrush brush) + private static ImmutablePen CreateFrozenPen(IBrush brush) { - var pen = new Pen(brush); + var pen = new ImmutablePen(brush?.ToImmutable()); return pen; } @@ -2036,7 +2017,7 @@ void ILogicalScrollable.RaiseScrollInvalidated(EventArgs e) /// Gets/Sets the pen used to draw the column ruler. /// /// - public Pen ColumnRulerPen + public IPen ColumnRulerPen { get => GetValue(ColumnRulerPenProperty); set => SetValue(ColumnRulerPenProperty, value); @@ -2060,13 +2041,13 @@ public IBrush CurrentLineBackground /// /// The property. /// - public static readonly StyledProperty CurrentLineBorderProperty = - AvaloniaProperty.Register("CurrentLineBorder"); + public static readonly StyledProperty CurrentLineBorderProperty = + AvaloniaProperty.Register("CurrentLineBorder"); /// /// Gets/Sets the background brush used for the current line. /// - public Pen CurrentLineBorder + public IPen CurrentLineBorder { get => GetValue(CurrentLineBorderProperty); set => SetValue(CurrentLineBorderProperty, value); diff --git a/src/AvaloniaEdit/Rendering/TextViewCachedElements.cs b/src/AvaloniaEdit/Rendering/TextViewCachedElements.cs index 7edb3615..7b481c7e 100644 --- a/src/AvaloniaEdit/Rendering/TextViewCachedElements.cs +++ b/src/AvaloniaEdit/Rendering/TextViewCachedElements.cs @@ -21,37 +21,35 @@ namespace AvaloniaEdit.Rendering { - internal sealed class TextViewCachedElements + internal sealed class TextViewCachedElements /*: IDisposable*/ { - private Dictionary _nonPrintableCharacterTexts; private TextFormatter _formatter; + private Dictionary _nonPrintableCharacterTexts; public TextLine GetTextForNonPrintableCharacter(string text, ITextRunConstructionContext context) { if (_nonPrintableCharacterTexts == null) - { _nonPrintableCharacterTexts = new Dictionary(); + TextLine textLine; + if (!_nonPrintableCharacterTexts.TryGetValue(text, out textLine)) { + var p = new VisualLineElementTextRunProperties(context.GlobalTextRunProperties); + p.SetForegroundBrush(context.TextView.NonPrintableCharacterBrush); + if (_formatter == null) + _formatter = TextFormatter.Current;//TextFormatterFactory.Create(context.TextView); + textLine = FormattedTextElement.PrepareText(_formatter, text, p); + _nonPrintableCharacterTexts[text] = textLine; } - - if (_nonPrintableCharacterTexts.TryGetValue(text, out var textLine)) - { - return textLine; - } - - var properties = context.GlobalTextRunProperties.Clone(); - - properties.SetForegroundBrush(context.TextView.NonPrintableCharacterBrush); - - if (_formatter == null) - { - _formatter = TextFormatter.Current; - } - - textLine = FormattedTextElement.PrepareText(_formatter, text, properties); - - _nonPrintableCharacterTexts[text] = textLine; - return textLine; } + + /*public void Dispose() + { + if (nonPrintableCharacterTexts != null) { + foreach (TextLine line in nonPrintableCharacterTexts.Values) + line.Dispose(); + } + if (formatter != null) + formatter.Dispose(); + }*/ } } diff --git a/src/AvaloniaEdit/Rendering/VisualLine.cs b/src/AvaloniaEdit/Rendering/VisualLine.cs index 3da4383a..ce7d4c4b 100644 --- a/src/AvaloniaEdit/Rendering/VisualLine.cs +++ b/src/AvaloniaEdit/Rendering/VisualLine.cs @@ -145,95 +145,70 @@ internal void ConstructVisualElements(ITextRunConstructionContext context, Visua g.FinishGeneration(); } - var globalTextRunProperties = context.GlobalTextRunProperties; - foreach (var element in _elements) - { - element.SetTextRunProperties(globalTextRunProperties.Clone()); - } - Elements = new ReadOnlyCollection(_elements); - CalculateOffsets(); - _phase = LifetimePhase.Transforming; - } - - private void PerformVisualElementConstruction(VisualLineElementGenerator[] generators) - { - var document = Document; - var lineLength = FirstDocumentLine.Length; - var offset = FirstDocumentLine.Offset; - var currentLineEnd = offset + FirstDocumentLine.Length; - LastDocumentLine = FirstDocumentLine; - var askInterestOffset = 0; // 0 or 1 - while (offset + askInterestOffset <= currentLineEnd) - { - var textPieceEndOffset = currentLineEnd; - foreach (var g in generators) - { - g.CachedInterest = (lineLength > LENGTH_LIMIT) ? -1: g.GetFirstInterestedOffset(offset + askInterestOffset); - if (g.CachedInterest != -1) - { - if (g.CachedInterest < offset) - throw new ArgumentOutOfRangeException(g.GetType().Name + ".GetFirstInterestedOffset", - g.CachedInterest, - "GetFirstInterestedOffset must not return an offset less than startOffset. Return -1 to signal no interest."); - if (g.CachedInterest < textPieceEndOffset) - textPieceEndOffset = g.CachedInterest; - } - } - Debug.Assert(textPieceEndOffset >= offset); - if (textPieceEndOffset > offset) - { - var textPieceLength = textPieceEndOffset - offset; - int remaining = textPieceLength; - while (true) - { - if (remaining > LENGTH_LIMIT) - { - // split in chunks of LENGTH_LIMIT - _elements.Add(new VisualLineText(this, LENGTH_LIMIT)); - remaining -= LENGTH_LIMIT; - } - else - { - _elements.Add(new VisualLineText(this, remaining)); - break; - } - } - offset = textPieceEndOffset; - } - // If no elements constructed / only zero-length elements constructed: - // do not asking the generators again for the same location (would cause endless loop) - askInterestOffset = 1; - foreach (var g in generators) - { - if (g.CachedInterest == offset) - { - var element = g.ConstructElement(offset); - if (element != null) - { - _elements.Add(element); - if (element.DocumentLength > 0) - { - // a non-zero-length element was constructed - askInterestOffset = 0; - offset += element.DocumentLength; - if (offset > currentLineEnd) - { - var newEndLine = document.GetLineByOffset(offset); - currentLineEnd = newEndLine.Offset + newEndLine.Length; - LastDocumentLine = newEndLine; - if (currentLineEnd < offset) - { - throw new InvalidOperationException( - $"The VisualLineElementGenerator {g.GetType().Name} produced an element which ends within the line delimiter"); - } - } - break; - } - } - } - } - } - } + var globalTextRunProperties = context.GlobalTextRunProperties; + foreach (var element in _elements) { + element.SetTextRunProperties(new VisualLineElementTextRunProperties(globalTextRunProperties)); + } + this.Elements = new ReadOnlyCollection(_elements); + CalculateOffsets(); + _phase = LifetimePhase.Transforming; + } + + void PerformVisualElementConstruction(VisualLineElementGenerator[] generators) + { + TextDocument document = this.Document; + int offset = FirstDocumentLine.Offset; + int currentLineEnd = offset + FirstDocumentLine.Length; + LastDocumentLine = FirstDocumentLine; + int askInterestOffset = 0; // 0 or 1 + while (offset + askInterestOffset <= currentLineEnd) { + int textPieceEndOffset = currentLineEnd; + foreach (VisualLineElementGenerator g in generators) { + g.CachedInterest = g.GetFirstInterestedOffset(offset + askInterestOffset); + if (g.CachedInterest != -1) { + if (g.CachedInterest < offset) + throw new ArgumentOutOfRangeException(g.GetType().Name + ".GetFirstInterestedOffset", + g.CachedInterest, + "GetFirstInterestedOffset must not return an offset less than startOffset. Return -1 to signal no interest."); + if (g.CachedInterest < textPieceEndOffset) + textPieceEndOffset = g.CachedInterest; + } + } + Debug.Assert(textPieceEndOffset >= offset); + if (textPieceEndOffset > offset) { + int textPieceLength = textPieceEndOffset - offset; + _elements.Add(new VisualLineText(this, textPieceLength)); + offset = textPieceEndOffset; + } + // If no elements constructed / only zero-length elements constructed: + // do not asking the generators again for the same location (would cause endless loop) + askInterestOffset = 1; + foreach (VisualLineElementGenerator g in generators) { + if (g.CachedInterest == offset) { + VisualLineElement element = g.ConstructElement(offset); + if (element != null) { + _elements.Add(element); + if (element.DocumentLength > 0) { + // a non-zero-length element was constructed + askInterestOffset = 0; + offset += element.DocumentLength; + if (offset > currentLineEnd) { + DocumentLine newEndLine = document.GetLineByOffset(offset); + currentLineEnd = newEndLine.Offset + newEndLine.Length; + this.LastDocumentLine = newEndLine; + if (currentLineEnd < offset) { + throw new InvalidOperationException( + "The VisualLineElementGenerator " + g.GetType().Name + + " produced an element which ends within the line delimiter"); + } + } + break; + } + } + } + } + } + } private void CalculateOffsets() { @@ -769,7 +744,7 @@ private static bool HasImplicitStopAtLineStart(CaretPositioningMode mode) internal VisualLineDrawingVisual Render() { Debug.Assert(_phase == LifetimePhase.Live); - return _visual ?? (_visual = new VisualLineDrawingVisual(this)); + return _visual ??= new VisualLineDrawingVisual(this); } } diff --git a/src/AvaloniaEdit/Rendering/VisualLineElement.cs b/src/AvaloniaEdit/Rendering/VisualLineElement.cs index f55c22cb..1b5babc4 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineElement.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineElement.cs @@ -72,19 +72,19 @@ protected VisualLineElement(int visualLength, int documentLength) /// /// Gets the text run properties. - /// A unique instance is used for each + /// A unique instance is used for each /// ; colorizing code may assume that modifying the - /// will affect only this + /// will affect only this /// . /// - public CustomTextRunProperties TextRunProperties { get; private set; } - + public VisualLineElementTextRunProperties TextRunProperties { get; private set; } + /// /// Gets/sets the brush used for the background of this . /// public IBrush BackgroundBrush { get; set; } - - internal void SetTextRunProperties(CustomTextRunProperties p) + + internal void SetTextRunProperties(VisualLineElementTextRunProperties p) { TextRunProperties = p; } @@ -106,9 +106,9 @@ internal void SetTextRunProperties(CustomTextRunProperties p) /// Retrieves the text span immediately before the visual column. /// /// This method is used for word-wrapping in bidirectional text. - public virtual string GetPrecedingText(int visualColumnLimit, ITextRunConstructionContext context) + public virtual ReadOnlySlice GetPrecedingText(int visualColumnLimit, ITextRunConstructionContext context) { - return string.Empty; + return ReadOnlySlice.Empty; } /// @@ -162,13 +162,13 @@ protected void SplitHelper(VisualLineElement firstPart, VisualLineElement second firstPart.DocumentLength = relativeSplitRelativeTextOffset; secondPart.DocumentLength = oldDocumentLength - relativeSplitRelativeTextOffset; if (firstPart.TextRunProperties == null) - firstPart.TextRunProperties = TextRunProperties; + firstPart.TextRunProperties = TextRunProperties.Clone(); if (secondPart.TextRunProperties == null) - secondPart.TextRunProperties = TextRunProperties; + secondPart.TextRunProperties = TextRunProperties.Clone(); firstPart.BackgroundBrush = BackgroundBrush; secondPart.BackgroundBrush = BackgroundBrush; } - + /// /// Gets the visual column of a text location inside this element. /// The text offset is given relative to the visual line start. diff --git a/src/AvaloniaEdit/Rendering/VisualLineElementTextRunProperties.cs b/src/AvaloniaEdit/Rendering/VisualLineElementTextRunProperties.cs new file mode 100644 index 00000000..5f6effb2 --- /dev/null +++ b/src/AvaloniaEdit/Rendering/VisualLineElementTextRunProperties.cs @@ -0,0 +1,244 @@ +// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Globalization; +using System.Linq; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using AvaloniaEdit.Utils; + +namespace AvaloniaEdit.Rendering +{ + /// + /// implementation that allows changing the properties. + /// A instance usually is assigned to a single + /// . + /// + public class VisualLineElementTextRunProperties : TextRunProperties, ICloneable + { + private IBrush _backgroundBrush; + private BaselineAlignment _baselineAlignment; + + private CultureInfo _cultureInfo; + //double fontHintingEmSize; + private double _fontRenderingEmSize; + private IBrush _foregroundBrush; + private Typeface _typeface; + + private TextDecorationCollection _textDecorations; + //TextEffectCollection textEffects; + //TextRunTypographyProperties typographyProperties; + //NumberSubstitution numberSubstitution; + + /// + /// Creates a new VisualLineElementTextRunProperties instance that copies its values + /// from the specified . + /// For the and collections, deep copies + /// are created if those collections are not frozen. + /// + public VisualLineElementTextRunProperties(TextRunProperties textRunProperties) + { + if (textRunProperties == null) + throw new ArgumentNullException(nameof(textRunProperties)); + + _backgroundBrush = textRunProperties.BackgroundBrush; + _baselineAlignment = textRunProperties.BaselineAlignment; + _cultureInfo = textRunProperties.CultureInfo; + //fontHintingEmSize = textRunProperties.FontHintingEmSize; + _fontRenderingEmSize = textRunProperties.FontRenderingEmSize; + _foregroundBrush = textRunProperties.ForegroundBrush; + _typeface = textRunProperties.Typeface; + _textDecorations = textRunProperties.TextDecorations; + + /*if (textDecorations != null && !textDecorations.IsFrozen) { + textDecorations = textDecorations.Clone(); + }*/ + /*textEffects = textRunProperties.TextEffects; + if (textEffects != null && !textEffects.IsFrozen) { + textEffects = textEffects.Clone(); + } + typographyProperties = textRunProperties.TypographyProperties; + numberSubstitution = textRunProperties.NumberSubstitution;*/ + } + + /// + /// Creates a copy of this instance. + /// + public virtual VisualLineElementTextRunProperties Clone() + { + return new VisualLineElementTextRunProperties(this); + } + + object ICloneable.Clone() + { + return Clone(); + } + + /// + public override IBrush BackgroundBrush => _backgroundBrush; + + /// + /// Sets the . + /// + public void SetBackgroundBrush(IBrush value) + { + _backgroundBrush = value?.ToImmutable(); + } + + /// + public override BaselineAlignment BaselineAlignment => _baselineAlignment; + + /// + /// Sets the . + /// + public void SetBaselineAlignment(BaselineAlignment value) + { + _baselineAlignment = value; + } + + /// + public override CultureInfo CultureInfo => _cultureInfo; + + /// + /// Sets the . + /// + public void SetCultureInfo(CultureInfo value) + { + _cultureInfo = value ?? throw new ArgumentNullException(nameof(value)); + } + + /*public override double FontHintingEmSize { + get { return fontHintingEmSize; } + } + + /// + /// Sets the . + /// + public void SetFontHintingEmSize(double value) + { + fontHintingEmSize = value; + }*/ + + /// + public override double FontRenderingEmSize => _fontRenderingEmSize; + + /// + /// Sets the . + /// + public void SetFontRenderingEmSize(double value) + { + _fontRenderingEmSize = value; + } + + /// + public override IBrush ForegroundBrush => _foregroundBrush; + + /// + /// Sets the . + /// + public void SetForegroundBrush(IBrush value) + { + _foregroundBrush = value?.ToImmutable(); + } + + /// + public override Typeface Typeface => _typeface; + + /// + /// Sets the . + /// + public void SetTypeface(Typeface value) + { + _typeface = value; + } + + /// + /// Gets the text decorations. The value may be null, a frozen + /// or an unfrozen . + /// If the value is an unfrozen , you may assume that the + /// collection instance is only used for this instance and it is safe + /// to add s. + /// + public override TextDecorationCollection TextDecorations => _textDecorations; + + /// + /// Sets the . + /// + public void SetTextDecorations(TextDecorationCollection value) + { + ExtensionMethods.CheckIsFrozen(value); + if (_textDecorations == null) + _textDecorations = value; + else + _textDecorations = new TextDecorationCollection(_textDecorations.Union(value)); + } + + /* + /// + /// Gets the text effects. The value may be null, a frozen + /// or an unfrozen . + /// If the value is an unfrozen , you may assume that the + /// collection instance is only used for this instance and it is safe + /// to add s. + /// + public override TextEffectCollection TextEffects { + get { return textEffects; } + } + + /// + /// Sets the . + /// + public void SetTextEffects(TextEffectCollection value) + { + ExtensionMethods.CheckIsFrozen(value); + textEffects = value; + } + + /// + /// Gets the typography properties for the text run. + /// + public override TextRunTypographyProperties TypographyProperties { + get { return typographyProperties; } + } + + /// + /// Sets the . + /// + public void SetTypographyProperties(TextRunTypographyProperties value) + { + typographyProperties = value; + } + + /// + /// Gets the number substitution settings for the text run. + /// + public override NumberSubstitution NumberSubstitution { + get { return numberSubstitution; } + } + + /// + /// Sets the . + /// + public void SetNumberSubstitution(NumberSubstitution value) + { + numberSubstitution = value; + } + */ + } +} diff --git a/src/AvaloniaEdit/Rendering/VisualLineLinkText.cs b/src/AvaloniaEdit/Rendering/VisualLineLinkText.cs index d02d83ee..88f9f68c 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineLinkText.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineLinkText.cs @@ -21,6 +21,7 @@ using Avalonia.Interactivity; using Avalonia.Controls; using System.Diagnostics; +using Avalonia.Media; using Avalonia.Media.TextFormatting; namespace AvaloniaEdit.Rendering @@ -66,14 +67,15 @@ public VisualLineLinkText(VisualLine parentVisualLine, int length) : base(parent RequireControlModifierForClick = true; } - /// - public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) - { - TextRunProperties.SetForegroundBrush(context.TextView.LinkTextForegroundBrush); - TextRunProperties.SetBackgroundBrush(context.TextView.LinkTextBackgroundBrush); - - return base.CreateTextRun(startVisualColumn, context); - } + /// + public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) + { + this.TextRunProperties.SetForegroundBrush(context.TextView.LinkTextForegroundBrush); + this.TextRunProperties.SetBackgroundBrush(context.TextView.LinkTextBackgroundBrush); + if (context.TextView.LinkTextUnderline) + this.TextRunProperties.SetTextDecorations(TextDecorations.Underline); + return base.CreateTextRun(startVisualColumn, context); + } /// /// Gets whether the link is currently clickable. diff --git a/src/AvaloniaEdit/Rendering/VisualLineText.cs b/src/AvaloniaEdit/Rendering/VisualLineText.cs index ec0dab60..b30af39c 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineText.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineText.cs @@ -21,6 +21,7 @@ using Avalonia.Media.TextFormatting; using Avalonia.Utilities; using AvaloniaEdit.Document; +using AvaloniaEdit.Utils; using LogicalDirection = AvaloniaEdit.Document.LogicalDirection; namespace AvaloniaEdit.Rendering @@ -61,32 +62,27 @@ public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructio throw new ArgumentNullException(nameof(context)); var relativeOffset = startVisualColumn - VisualColumn; - - var offset = context.VisualLine.FirstDocumentLine.Offset + RelativeTextOffset + relativeOffset; - var text = context.GetText(offset, DocumentLength - relativeOffset); - - return new TextCharacters(new ReadOnlySlice(text.AsMemory(), offset, text.Length), TextRunProperties); + StringSegment text = context.GetText(context.VisualLine.FirstDocumentLine.Offset + RelativeTextOffset + relativeOffset, DocumentLength - relativeOffset); + return new TextCharacters(new ReadOnlySlice(text.Text.AsMemory(), text.Offset, text.Count), this.TextRunProperties); } - + /// public override bool IsWhitespace(int visualColumn) { var offset = visualColumn - VisualColumn + ParentVisualLine.FirstDocumentLine.Offset + RelativeTextOffset; return char.IsWhiteSpace(ParentVisualLine.Document.GetCharAt(offset)); } - + /// - public override string GetPrecedingText(int visualColumnLimit, ITextRunConstructionContext context) + public override ReadOnlySlice GetPrecedingText(int visualColumnLimit, ITextRunConstructionContext context) { if (context == null) throw new ArgumentNullException(nameof(context)); - - var relativeOffset = visualColumnLimit - VisualColumn; - - var text = context.GetText(context.VisualLine.FirstDocumentLine.Offset + RelativeTextOffset, relativeOffset); - return text; + int relativeOffset = visualColumnLimit - VisualColumn; + StringSegment text = context.GetText(context.VisualLine.FirstDocumentLine.Offset + RelativeTextOffset, relativeOffset); + return new ReadOnlySlice(text.Text.AsMemory(), text.Offset, text.Count); } /// diff --git a/src/AvaloniaEdit/Rendering/VisualLineTextParagraphProperties.cs b/src/AvaloniaEdit/Rendering/VisualLineTextParagraphProperties.cs new file mode 100644 index 00000000..8b54a0a3 --- /dev/null +++ b/src/AvaloniaEdit/Rendering/VisualLineTextParagraphProperties.cs @@ -0,0 +1,46 @@ +// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + + +using Avalonia.Media; +using Avalonia.Media.TextFormatting; + +namespace AvaloniaEdit.Rendering +{ + sealed class VisualLineTextParagraphProperties : TextParagraphProperties + { + internal TextRunProperties defaultTextRunProperties; + internal TextWrapping textWrapping; + internal double tabSize; + internal double indent; + internal bool firstLineInParagraph; + + public override double DefaultIncrementalTab => tabSize; + + public override FlowDirection FlowDirection => FlowDirection.LeftToRight; + public override TextAlignment TextAlignment => TextAlignment.Left; + public override double LineHeight => double.NaN; + public override bool FirstLineInParagraph => firstLineInParagraph; + public override TextRunProperties DefaultTextRunProperties => defaultTextRunProperties; + + public override TextWrapping TextWrapping => textWrapping; + + //public override TextMarkerProperties TextMarkerProperties { get { return null; } } + public override double Indent => indent; + } +} diff --git a/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs b/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs index 8d2199b5..ed1f9e70 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs @@ -21,117 +21,112 @@ using Avalonia.Media.TextFormatting; using Avalonia.Utilities; using AvaloniaEdit.Document; +using AvaloniaEdit.Utils; using JetBrains.Annotations; using ITextSource = Avalonia.Media.TextFormatting.ITextSource; namespace AvaloniaEdit.Rendering { - /// - /// TextSource implementation that creates TextRuns for a VisualLine. - /// - internal sealed class VisualLineTextSource : ITextSource, ITextRunConstructionContext - { - public VisualLineTextSource(VisualLine visualLine) - { - VisualLine = visualLine; - } + /// + /// WPF TextSource implementation that creates TextRuns for a VisualLine. + /// + internal sealed class VisualLineTextSource : ITextSource, ITextRunConstructionContext + { + public VisualLineTextSource(VisualLine visualLine) + { + VisualLine = visualLine; + } - public VisualLine VisualLine { get; } - public TextView TextView { get; set; } - public TextDocument Document { get; set; } - public CustomTextRunProperties GlobalTextRunProperties { get; set; } + public VisualLine VisualLine { get; private set; } + public TextView TextView { get; set; } + public TextDocument Document { get; set; } + public TextRunProperties GlobalTextRunProperties { get; set; } - [CanBeNull] - public TextRun GetTextRun(int characterIndex) - { - if (characterIndex > VisualLine.VisualLengthWithEndOfLineMarker) - { - return null; - } - - try - { - foreach (var element in VisualLine.Elements) - { - if (characterIndex >= element.VisualColumn - && characterIndex < element.VisualColumn + element.VisualLength) - { - var relativeOffset = characterIndex - element.VisualColumn; - var run = element.CreateTextRun(characterIndex, this); - if (run == null) - throw new ArgumentNullException(element.GetType().Name + ".CreateTextRun"); - if (run.TextSourceLength == 0) - throw new ArgumentException("The returned TextRun must not have length 0.", element.GetType().Name + ".Length"); - if (relativeOffset + run.TextSourceLength > element.VisualLength) - throw new ArgumentException("The returned TextRun is too long.", element.GetType().Name + ".CreateTextRun"); - if (run is InlineObjectRun inlineRun) - { - inlineRun.VisualLine = VisualLine; - VisualLine.HasInlineObjects = true; - TextView.AddInlineObject(inlineRun); - } - return run; - } - } - - if (TextView.Options.ShowEndOfLine && characterIndex == VisualLine.VisualLength) - { - return CreateTextRunForNewLine(); - } + public TextRun GetTextRun(int textSourceCharacterIndex) + { + try { + foreach (VisualLineElement element in VisualLine.Elements) { + if (textSourceCharacterIndex >= element.VisualColumn + && textSourceCharacterIndex < element.VisualColumn + element.VisualLength) { + int relativeOffset = textSourceCharacterIndex - element.VisualColumn; + TextRun run = element.CreateTextRun(textSourceCharacterIndex, this); + if (run == null) + throw new ArgumentNullException(element.GetType().Name + ".CreateTextRun"); + if (run.TextSourceLength == 0) + throw new ArgumentException("The returned TextRun must not have length 0.", element.GetType().Name + ".Length"); + if (relativeOffset + run.TextSourceLength > element.VisualLength) + throw new ArgumentException("The returned TextRun is too long.", element.GetType().Name + ".CreateTextRun"); + if (run is InlineObjectRun inlineRun) { + inlineRun.VisualLine = VisualLine; + VisualLine.HasInlineObjects = true; + TextView.AddInlineObject(inlineRun); + } + return run; + } + } + if (TextView.Options.ShowEndOfLine && textSourceCharacterIndex == VisualLine.VisualLength) { + return CreateTextRunForNewLine(); + } + return new TextEndOfParagraph(1); + } catch (Exception ex) { + Debug.WriteLine(ex.ToString()); + throw; + } + } - return new TextEndOfLine(2); - } - catch (Exception ex) - { - Debug.WriteLine(ex.ToString()); - throw; - } - } + private TextRun CreateTextRunForNewLine() + { + string newlineText = ""; + DocumentLine lastDocumentLine = VisualLine.LastDocumentLine; + if (lastDocumentLine.DelimiterLength == 2) { + newlineText = "¶"; + } else if (lastDocumentLine.DelimiterLength == 1) { + char newlineChar = Document.GetCharAt(lastDocumentLine.Offset + lastDocumentLine.Length); + if (newlineChar == '\r') + newlineText = "\\r"; + else if (newlineChar == '\n') + newlineText = "\\n"; + else + newlineText = "?"; + } + return new FormattedTextRun(new FormattedTextElement(TextView.CachedElements.GetTextForNonPrintableCharacter(newlineText, this), 0), GlobalTextRunProperties); + } - private TextRun CreateTextRunForNewLine() - { - var newlineText = ""; - var lastDocumentLine = VisualLine.LastDocumentLine; - if (lastDocumentLine.DelimiterLength == 2) - { - newlineText = "¶"; - } - else if (lastDocumentLine.DelimiterLength == 1) - { - var newlineChar = Document.GetCharAt(lastDocumentLine.Offset + lastDocumentLine.Length); - switch (newlineChar) - { - case '\r': - newlineText = "\\r"; - break; - case '\n': - newlineText = "\\n"; - break; - default: - newlineText = "?"; - break; - } - } - return new FormattedTextRun(new FormattedTextElement(TextView.CachedElements.GetTextForNonPrintableCharacter(newlineText, this), 0), GlobalTextRunProperties); - } + public ReadOnlySlice GetPrecedingText(int textSourceCharacterIndexLimit) + { + try { + foreach (VisualLineElement element in VisualLine.Elements) { + if (textSourceCharacterIndexLimit > element.VisualColumn + && textSourceCharacterIndexLimit <= element.VisualColumn + element.VisualLength) { + var span = element.GetPrecedingText(textSourceCharacterIndexLimit, this); + if (span.IsEmpty) + break; + int relativeOffset = textSourceCharacterIndexLimit - element.VisualColumn; + if (span.Length > relativeOffset) + throw new ArgumentException("The returned TextSpan is too long.", element.GetType().Name + ".GetPrecedingText"); + return span; + } + } + + return ReadOnlySlice.Empty; + } catch (Exception ex) { + Debug.WriteLine(ex.ToString()); + throw; + } + } - private string _cachedString; - private int _cachedStringOffset; + private string _cachedString; + private int _cachedStringOffset; - public string GetText(int offset, int length) - { - if (_cachedString != null) - { - if (offset >= _cachedStringOffset && offset + length <= _cachedStringOffset + _cachedString.Length) - { - return _cachedString.Substring(offset - _cachedStringOffset, length); - } - } - - _cachedStringOffset = offset; - _cachedString = Document.GetText(offset, length); - - return _cachedString; - } - } + public StringSegment GetText(int offset, int length) + { + if (_cachedString != null) { + if (offset >= _cachedStringOffset && offset + length <= _cachedStringOffset + _cachedString.Length) { + return new StringSegment(_cachedString, offset - _cachedStringOffset, length); + } + } + _cachedStringOffset = offset; + return new StringSegment(_cachedString = Document.GetText(offset, length)); + } + } } diff --git a/src/AvaloniaEdit/Utils/ExtensionMethods.cs b/src/AvaloniaEdit/Utils/ExtensionMethods.cs index 0a6fa12a..e274e47b 100644 --- a/src/AvaloniaEdit/Utils/ExtensionMethods.cs +++ b/src/AvaloniaEdit/Utils/ExtensionMethods.cs @@ -21,7 +21,9 @@ using System.Diagnostics; using System.Xml; using Avalonia; +using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Media; using Avalonia.VisualTree; namespace AvaloniaEdit.Utils @@ -88,6 +90,19 @@ public static int CoerceValue(this int value, int minimum, int maximum) return Math.Max(Math.Min(value, maximum), minimum); } #endregion + + #region CreateTypeface + /// + /// Creates typeface from the framework element. + /// + public static Typeface CreateTypeface(this Control fe) + { + return new Typeface(fe.GetValue(TextBlock.FontFamilyProperty), + fe.GetValue(TextBlock.FontStyleProperty), + fe.GetValue(TextBlock.FontWeightProperty), + fe.GetValue(TextBlock.FontStretchProperty)); + } + #endregion #region AddRange / Sequence public static void AddRange(this ICollection collection, IEnumerable elements) diff --git a/src/AvaloniaEdit/Utils/TextFormatterFactory.cs b/src/AvaloniaEdit/Utils/TextFormatterFactory.cs index 002cfa63..4daa89d2 100644 --- a/src/AvaloniaEdit/Utils/TextFormatterFactory.cs +++ b/src/AvaloniaEdit/Utils/TextFormatterFactory.cs @@ -16,21 +16,27 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +using System; +using System.Globalization; +using Avalonia.Controls; using Avalonia.Media; using Avalonia.Media.TextFormatting; -using Avalonia.Utilities; -using AvaloniaEdit.Rendering; -using TextLine = Avalonia.Media.TextFormatting.TextLine; -using TextRun = Avalonia.Media.TextFormatting.TextRun; -using TextRunProperties = Avalonia.Media.TextFormatting.TextRunProperties; namespace AvaloniaEdit.Utils { /// - /// Creates TextFormatter instances that with the correct TextFormattingMode, if running on .NET 4.0. - /// - public static class TextFormatterFactory + /// Creates TextFormatter instances that with the correct TextFormattingMode, if running on .NET 4.0. + /// + static class TextFormatterFactory { + /// + /// Creates a using the formatting mode used by the specified owner object. + /// + public static TextFormatter Create(Control owner) + { + return TextFormatter.Current; + } + /// /// Creates formatted text. /// @@ -40,41 +46,26 @@ public static class TextFormatterFactory /// 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 TextLine FormatLine(ReadOnlySlice text, Typeface typeface, double emSize, IBrush foreground) - { - var defaultProperties = new CustomTextRunProperties(typeface, emSize, null, foreground); - var paragraphProperties = new CustomTextParagraphProperties(defaultProperties); - - var textSource = new SimpleTextSource(text, defaultProperties); - - return TextFormatter.Current.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties); - } - - private readonly struct SimpleTextSource : ITextSource + public static FormattedText CreateFormattedText(Control element, string text, Typeface typeface, double? emSize, IBrush foreground) { - private readonly ReadOnlySlice _text; - private readonly TextRunProperties _defaultProperties; - - public SimpleTextSource(ReadOnlySlice text, TextRunProperties defaultProperties) - { - _text = text; - _defaultProperties = defaultProperties; - } - - public TextRun GetTextRun(int textSourceIndex) - { - if (textSourceIndex < _text.Length) - { - return new TextCharacters(_text, textSourceIndex, _text.Length - textSourceIndex, _defaultProperties); - } - - if (textSourceIndex > _text.Length) - { - return null; - } + if (element == null) + throw new ArgumentNullException(nameof(element)); + if (text == null) + throw new ArgumentNullException(nameof(text)); + if (typeface == default) + typeface = element.CreateTypeface(); + if (emSize == null) + emSize = TextBlock.GetFontSize(element); + if (foreground == null) + foreground = TextBlock.GetForeground(element); - return new TextEndOfParagraph(1); - } + return new FormattedText( + text, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + emSize.Value, + foreground); } } } diff --git a/src/AvaloniaEdit/Utils/TextLineExtensions.cs b/src/AvaloniaEdit/Utils/TextLineExtensions.cs index fb68a403..3c954274 100644 --- a/src/AvaloniaEdit/Utils/TextLineExtensions.cs +++ b/src/AvaloniaEdit/Utils/TextLineExtensions.cs @@ -1,17 +1,208 @@ -using Avalonia; +using System; +using System.Collections.Generic; +using Avalonia; using Avalonia.Media; using Avalonia.Media.TextFormatting; +using Avalonia.Utilities; namespace AvaloniaEdit.Utils; +#nullable enable + public static class TextLineExtensions { - public static Rect GetTextBounds(this TextLine textLine, int start, int length) + public static IReadOnlyList GetTextBounds(this TextLine textLine, int start, int length) { - var startX = textLine.GetDistanceFromCharacterHit(new CharacterHit(start)); + if (start + length <= 0) + { + return Array.Empty(); + } + + var result = new List(textLine.TextRuns.Count); + + var currentPosition = 0; + var currentRect = Rect.Empty; + + //Current line isn't covered. + if (textLine.TextRange.Length <= start) + { + return result; + } + + //The whole line is covered. + if (currentPosition >= start && start + length > textLine.TextRange.Length) + { + currentRect = new Rect(textLine.Start, 0, textLine.WidthIncludingTrailingWhitespace, + textLine.Height); + + result.Add(new TextBounds{ Rectangle = currentRect}); + + return result; + } + + var startX = textLine.Start; + + //A portion of the line is covered. + for (var index = 0; index < textLine.TextRuns.Count; index++) + { + var currentRun = textLine.TextRuns[index] as DrawableTextRun; + var currentShaped = currentRun as ShapedTextCharacters; + + if (currentRun is null) + { + continue; + } + + TextRun? nextRun = null; + + if (index + 1 < textLine.TextRuns.Count) + { + nextRun = textLine.TextRuns[index + 1]; + } + + if (nextRun != null) + { + if (nextRun.Text.Start < currentRun.Text.Start && start + length < currentRun.Text.End) + { + goto skip; + } + + if (currentRun.Text.Start >= start + length) + { + goto skip; + } + + if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < start) + { + goto skip; + } + + if (currentRun.Text.End < start) + { + goto skip; + } + + goto noop; + + skip: + { + startX += currentRun.Size.Width; + } + + continue; + + noop: + { + } + } + + var endX = startX; + var endOffset = 0d; + + if (currentShaped != null) + { + endOffset = currentShaped.GlyphRun.GetDistanceFromCharacterHit( + currentShaped.ShapedBuffer.IsLeftToRight ? new CharacterHit(start + length) : new CharacterHit(start)); - var endX = textLine.GetDistanceFromCharacterHit(new CharacterHit(start + length)); + endX += endOffset; - return new Rect(startX, 0, endX - startX, textLine.Height); + var startOffset = currentShaped.GlyphRun.GetDistanceFromCharacterHit( + currentShaped.ShapedBuffer.IsLeftToRight ? new CharacterHit(start) : new CharacterHit(start + length)); + + startX += startOffset; + + var characterHit = currentShaped.GlyphRun.IsLeftToRight + ? currentShaped.GlyphRun.GetCharacterHitFromDistance(endOffset, out _) + : currentShaped.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); + + currentPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + + if (nextRun is ShapedTextCharacters nextShaped) + { + if (currentShaped.ShapedBuffer.IsLeftToRight == nextShaped.ShapedBuffer.IsLeftToRight) + { + endOffset = nextShaped.GlyphRun.GetDistanceFromCharacterHit( + nextShaped.ShapedBuffer.IsLeftToRight + ? new CharacterHit(start + length) + : new CharacterHit(start)); + + index++; + + endX += endOffset; + + currentRun = currentShaped = nextShaped; + + if (nextShaped.ShapedBuffer.IsLeftToRight) + { + characterHit = nextShaped.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); + + currentPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + } + } + } + } + + if (endX < startX) + { + (endX, startX) = (startX, endX); + } + + var width = endX - startX; + + if (result.Count > 0 && MathUtilities.AreClose(currentRect.Top, 0) && + MathUtilities.AreClose(currentRect.Right, startX)) + { + var textBounds = new TextBounds {Rectangle = currentRect.WithWidth(currentRect.Width + width)}; + + result[result.Count - 1] = textBounds; + } + else + { + currentRect = new Rect(startX, 0, width, textLine.Height); + + result.Add(new TextBounds{ Rectangle = currentRect}); + } + + if (currentShaped != null && currentShaped.ShapedBuffer.IsLeftToRight) + { + if (nextRun != null) + { + if (nextRun.Text.Start > currentRun.Text.Start && nextRun.Text.Start >= start + length) + { + break; + } + + currentPosition = nextRun.Text.End; + } + else + { + if (currentPosition >= start + length) + { + break; + } + } + } + else + { + if (currentPosition <= start) + { + break; + } + } + + if (currentShaped != null && !currentShaped.ShapedBuffer.IsLeftToRight && currentPosition != currentRun.Text.Start) + { + endX += currentShaped.GlyphRun.Size.Width - endOffset; + } + + startX = endX; + } + + return result; } +} + +public struct TextBounds +{ + public Rect Rectangle { get; set; } } \ No newline at end of file From 48ca598e0e701405be22cad6618d18235e3700e4 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 11 Mar 2022 06:44:07 +0100 Subject: [PATCH 08/24] More fixes --- Directory.Build.props | 2 +- NuGet.config | 1 + .../Rendering/FormattedTextElement.cs | 8 +++- .../SingleCharacterElementGenerator.cs | 3 +- src/AvaloniaEdit/Rendering/VisualLine.cs | 42 +++++++++++++------ src/AvaloniaEdit/Rendering/VisualLineText.cs | 17 ++++++-- 6 files changed, 53 insertions(+), 20 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 30961a38..00e1a96e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ latest true - 0.10.999-cibuild0019161-beta + 0.10.999-cibuild0019182-beta 1.0.31 13.0.1 0.10.12.2 diff --git a/NuGet.config b/NuGet.config index 5b06eef0..e4d54413 100644 --- a/NuGet.config +++ b/NuGet.config @@ -4,5 +4,6 @@ + diff --git a/src/AvaloniaEdit/Rendering/FormattedTextElement.cs b/src/AvaloniaEdit/Rendering/FormattedTextElement.cs index 6b8cc212..5e58f298 100644 --- a/src/AvaloniaEdit/Rendering/FormattedTextElement.cs +++ b/src/AvaloniaEdit/Rendering/FormattedTextElement.cs @@ -20,6 +20,7 @@ using Avalonia; using Avalonia.Media; using Avalonia.Media.TextFormatting; +using Avalonia.Utilities; using AvaloniaEdit.Utils; using JetBrains.Annotations; @@ -93,8 +94,7 @@ public static TextLine PrepareText(TextFormatter formatter, string text, TextRun defaultTextRunProperties = properties, textWrapping = TextWrapping.NoWrap, tabSize = 40 - }, - null); + }); } } @@ -112,6 +112,8 @@ public FormattedTextRun(FormattedTextElement element, TextRunProperties properti throw new ArgumentNullException(nameof(properties)); Properties = properties; Element = element ?? throw new ArgumentNullException(nameof(element)); + Text = new ReadOnlySlice(new string(' ', element.VisualLength).AsMemory(), element.RelativeTextOffset, + element.VisualLength); } /// @@ -119,6 +121,8 @@ public FormattedTextRun(FormattedTextElement element, TextRunProperties properti /// public FormattedTextElement Element { get; } + public override ReadOnlySlice Text { get; } + /// public override TextRunProperties Properties { get; } diff --git a/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs b/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs index b2605a25..85040b9f 100644 --- a/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs +++ b/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs @@ -22,6 +22,7 @@ using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Media.TextFormatting; +using Avalonia.Utilities; using AvaloniaEdit.Document; using AvaloniaEdit.Utils; using LogicalDirection = AvaloniaEdit.Document.LogicalDirection; @@ -153,7 +154,7 @@ public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructio if (startVisualColumn == VisualColumn) return new TabGlyphRun(this, TextRunProperties); else if (startVisualColumn == VisualColumn + 1) - return new TextCharacters("\t".AsMemory(), 0, 1, TextRunProperties); + return new TextCharacters(new ReadOnlySlice("\t".AsMemory(), RelativeTextOffset, 1), TextRunProperties); else throw new ArgumentOutOfRangeException(nameof(startVisualColumn)); } diff --git a/src/AvaloniaEdit/Rendering/VisualLine.cs b/src/AvaloniaEdit/Rendering/VisualLine.cs index ce7d4c4b..956a0462 100644 --- a/src/AvaloniaEdit/Rendering/VisualLine.cs +++ b/src/AvaloniaEdit/Rendering/VisualLine.cs @@ -156,15 +156,16 @@ internal void ConstructVisualElements(ITextRunConstructionContext context, Visua void PerformVisualElementConstruction(VisualLineElementGenerator[] generators) { - TextDocument document = this.Document; - int offset = FirstDocumentLine.Offset; - int currentLineEnd = offset + FirstDocumentLine.Length; + var lineLength = FirstDocumentLine.Length; + var offset = FirstDocumentLine.Offset; + var currentLineEnd = offset + lineLength; LastDocumentLine = FirstDocumentLine; - int askInterestOffset = 0; // 0 or 1 + var askInterestOffset = 0; // 0 or 1 + while (offset + askInterestOffset <= currentLineEnd) { - int textPieceEndOffset = currentLineEnd; - foreach (VisualLineElementGenerator g in generators) { - g.CachedInterest = g.GetFirstInterestedOffset(offset + askInterestOffset); + var textPieceEndOffset = currentLineEnd; + foreach (var g in generators) { + g.CachedInterest = (lineLength > LENGTH_LIMIT) ? -1: g.GetFirstInterestedOffset(offset + askInterestOffset); if (g.CachedInterest != -1) { if (g.CachedInterest < offset) throw new ArgumentOutOfRangeException(g.GetType().Name + ".GetFirstInterestedOffset", @@ -176,16 +177,33 @@ void PerformVisualElementConstruction(VisualLineElementGenerator[] generators) } Debug.Assert(textPieceEndOffset >= offset); if (textPieceEndOffset > offset) { - int textPieceLength = textPieceEndOffset - offset; - _elements.Add(new VisualLineText(this, textPieceLength)); + var textPieceLength = textPieceEndOffset - offset; + + int remaining = textPieceLength; + + while (true) + { + if (remaining > LENGTH_LIMIT) + { + // split in chunks of LENGTH_LIMIT + _elements.Add(new VisualLineText(this, LENGTH_LIMIT)); + remaining -= LENGTH_LIMIT; + } + else + { + _elements.Add(new VisualLineText(this, remaining)); + break; + } + } + offset = textPieceEndOffset; } // If no elements constructed / only zero-length elements constructed: // do not asking the generators again for the same location (would cause endless loop) askInterestOffset = 1; - foreach (VisualLineElementGenerator g in generators) { + foreach (var g in generators) { if (g.CachedInterest == offset) { - VisualLineElement element = g.ConstructElement(offset); + var element = g.ConstructElement(offset); if (element != null) { _elements.Add(element); if (element.DocumentLength > 0) { @@ -193,7 +211,7 @@ void PerformVisualElementConstruction(VisualLineElementGenerator[] generators) askInterestOffset = 0; offset += element.DocumentLength; if (offset > currentLineEnd) { - DocumentLine newEndLine = document.GetLineByOffset(offset); + var newEndLine = Document.GetLineByOffset(offset); currentLineEnd = newEndLine.Offset + newEndLine.Length; this.LastDocumentLine = newEndLine; if (currentLineEnd < offset) { diff --git a/src/AvaloniaEdit/Rendering/VisualLineText.cs b/src/AvaloniaEdit/Rendering/VisualLineText.cs index b30af39c..5d6248da 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineText.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineText.cs @@ -62,9 +62,16 @@ public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructio throw new ArgumentNullException(nameof(context)); var relativeOffset = startVisualColumn - VisualColumn; + + var offset = context.VisualLine.FirstDocumentLine.Offset + RelativeTextOffset + relativeOffset; + + var text = context.GetText( + offset, + DocumentLength - relativeOffset); - StringSegment text = context.GetText(context.VisualLine.FirstDocumentLine.Offset + RelativeTextOffset + relativeOffset, DocumentLength - relativeOffset); - return new TextCharacters(new ReadOnlySlice(text.Text.AsMemory(), text.Offset, text.Count), this.TextRunProperties); + return new TextCharacters( + new ReadOnlySlice(text.Text.AsMemory(), RelativeTextOffset, text.Count, + text.Offset), TextRunProperties); } /// @@ -80,8 +87,10 @@ public override ReadOnlySlice GetPrecedingText(int visualColumnLimit, ITex if (context == null) throw new ArgumentNullException(nameof(context)); - int relativeOffset = visualColumnLimit - VisualColumn; - StringSegment text = context.GetText(context.VisualLine.FirstDocumentLine.Offset + RelativeTextOffset, relativeOffset); + var relativeOffset = visualColumnLimit - VisualColumn; + + var text = context.GetText(context.VisualLine.FirstDocumentLine.Offset + RelativeTextOffset, relativeOffset); + return new ReadOnlySlice(text.Text.AsMemory(), text.Offset, text.Count); } From 5ca9e4aff3f047e55f046a02ade79c5ceed17df7 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 11 Mar 2022 11:59:36 +0100 Subject: [PATCH 09/24] More fixes --- .../SingleCharacterElementGenerator.cs | 3 +-- src/AvaloniaEdit/Rendering/TextView.cs | 2 ++ src/AvaloniaEdit/Rendering/VisualLine.cs | 23 ++++--------------- 3 files changed, 7 insertions(+), 21 deletions(-) diff --git a/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs b/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs index 85040b9f..b5fbf4b8 100644 --- a/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs +++ b/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs @@ -201,8 +201,7 @@ public override Size Size public override void Draw(DrawingContext drawingContext, Point origin) { - var y = origin.Y - _element.Text.Baseline; - _element.Text.Draw(drawingContext, origin.WithY(y)); + _element.Text.Draw(drawingContext, origin); } } diff --git a/src/AvaloniaEdit/Rendering/TextView.cs b/src/AvaloniaEdit/Rendering/TextView.cs index e734aeae..eed0e44d 100644 --- a/src/AvaloniaEdit/Rendering/TextView.cs +++ b/src/AvaloniaEdit/Rendering/TextView.cs @@ -1115,6 +1115,7 @@ private VisualLine BuildVisualLine(DocumentLine documentLine, TextLineBreak lastLineBreak = null; var textOffset = 0; var textLines = new List(); + while (textOffset <= visualLine.VisualLengthWithEndOfLineMarker) { var textLine = _formatter.FormatLine( @@ -1124,6 +1125,7 @@ private VisualLine BuildVisualLine(DocumentLine documentLine, paragraphProperties, lastLineBreak ); + textLines.Add(textLine); textOffset += textLine.TextRange.Length; diff --git a/src/AvaloniaEdit/Rendering/VisualLine.cs b/src/AvaloniaEdit/Rendering/VisualLine.cs index 956a0462..cdc705ea 100644 --- a/src/AvaloniaEdit/Rendering/VisualLine.cs +++ b/src/AvaloniaEdit/Rendering/VisualLine.cs @@ -165,7 +165,7 @@ void PerformVisualElementConstruction(VisualLineElementGenerator[] generators) while (offset + askInterestOffset <= currentLineEnd) { var textPieceEndOffset = currentLineEnd; foreach (var g in generators) { - g.CachedInterest = (lineLength > LENGTH_LIMIT) ? -1: g.GetFirstInterestedOffset(offset + askInterestOffset); + g.CachedInterest = g.GetFirstInterestedOffset(offset + askInterestOffset); if (g.CachedInterest != -1) { if (g.CachedInterest < offset) throw new ArgumentOutOfRangeException(g.GetType().Name + ".GetFirstInterestedOffset", @@ -179,24 +179,9 @@ void PerformVisualElementConstruction(VisualLineElementGenerator[] generators) if (textPieceEndOffset > offset) { var textPieceLength = textPieceEndOffset - offset; - int remaining = textPieceLength; - - while (true) - { - if (remaining > LENGTH_LIMIT) - { - // split in chunks of LENGTH_LIMIT - _elements.Add(new VisualLineText(this, LENGTH_LIMIT)); - remaining -= LENGTH_LIMIT; - } - else - { - _elements.Add(new VisualLineText(this, remaining)); - break; - } - } - - offset = textPieceEndOffset; + _elements.Add(new VisualLineText(this, textPieceLength)); + + offset = textPieceEndOffset; } // If no elements constructed / only zero-length elements constructed: // do not asking the generators again for the same location (would cause endless loop) From c41ff6a7b87fa9630c9fa0fe20741a1aa45a2637 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 30 Mar 2022 19:06:19 +0200 Subject: [PATCH 10/24] More fixes --- Directory.Build.props | 2 +- .../AvaloniaEdit.Demo.csproj | 1 + src/AvaloniaEdit.Demo/MainWindow.xaml.cs | 21 +- .../AvaloniaEdit.TextMate.csproj | 1 + .../Rendering/GlobalTextRunProperties.cs | 2 +- src/AvaloniaEdit/Rendering/InlineObjectRun.cs | 2 +- .../SingleCharacterElementGenerator.cs | 34 +-- src/AvaloniaEdit/Rendering/TextView.cs | 25 ++- src/AvaloniaEdit/Rendering/VisualLineText.cs | 2 +- .../VisualLineTextParagraphProperties.cs | 2 +- .../Rendering/VisualLineTextSource.cs | 7 +- src/AvaloniaEdit/Search/SearchPanel.xaml | 2 +- src/AvaloniaEdit/Utils/ExtensionMethods.cs | 9 +- .../Utils/TextFormatterFactory.cs | 5 +- src/AvaloniaEdit/Utils/TextLineExtensions.cs | 208 ------------------ 15 files changed, 66 insertions(+), 257 deletions(-) delete mode 100644 src/AvaloniaEdit/Utils/TextLineExtensions.cs diff --git a/Directory.Build.props b/Directory.Build.props index 00e1a96e..ef0eb168 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ latest true - 0.10.999-cibuild0019182-beta + 0.10.999-cibuild0019555-beta 1.0.31 13.0.1 0.10.12.2 diff --git a/src/AvaloniaEdit.Demo/AvaloniaEdit.Demo.csproj b/src/AvaloniaEdit.Demo/AvaloniaEdit.Demo.csproj index 9f886662..4f056449 100644 --- a/src/AvaloniaEdit.Demo/AvaloniaEdit.Demo.csproj +++ b/src/AvaloniaEdit.Demo/AvaloniaEdit.Demo.csproj @@ -30,6 +30,7 @@ + diff --git a/src/AvaloniaEdit.Demo/MainWindow.xaml.cs b/src/AvaloniaEdit.Demo/MainWindow.xaml.cs index e7bfb7e3..3ccf7d1a 100644 --- a/src/AvaloniaEdit.Demo/MainWindow.xaml.cs +++ b/src/AvaloniaEdit.Demo/MainWindow.xaml.cs @@ -19,7 +19,7 @@ namespace AvaloniaEdit.Demo { - using Pair = KeyValuePair; + using Pair = KeyValuePair; public class MainWindow : Window { @@ -57,6 +57,9 @@ public MainWindow() _textEditor.TextArea.TextEntered += textEditor_TextArea_TextEntered; _textEditor.TextArea.TextEntering += textEditor_TextArea_TextEntering; _textEditor.Options.ShowBoxForControlCharacters = true; + _textEditor.Options.ShowTabs = true; + //_textEditor.Options.ShowSpaces = true; + //_textEditor.Options.ShowEndOfLine = true; _textEditor.TextArea.IndentationStrategy = new Indentation.CSharp.CSharpIndentationStrategy(_textEditor.Options); _textEditor.TextArea.Caret.PositionChanged += Caret_PositionChanged; _textEditor.TextArea.RightClickMovesCaret = true; @@ -154,13 +157,13 @@ private void InitializeComponent() AvaloniaXamlLoader.Load(this); } - private void AddControlButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) + private void AddControlButton_Click(object sender, RoutedEventArgs e) { - _generator.controls.Add(new KeyValuePair(_textEditor.CaretOffset, new Button() { Content = "Click me" })); + _generator.controls.Add(new Pair(_textEditor.CaretOffset, new Button() { Content = "Click me" })); _textEditor.TextArea.TextView.Redraw(); } - private void ClearControlButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) + private void ClearControlButton_Click(object sender, RoutedEventArgs e) { //TODO: delete elements using back key _generator.controls.Clear(); @@ -290,9 +293,9 @@ public void Complete(TextArea textArea, ISegment completionSegment, } } - class ElementGenerator : VisualLineElementGenerator, IComparer> + class ElementGenerator : VisualLineElementGenerator, IComparer { - public List> controls = new List>(); + public List controls = new List(); /// /// Gets the first interested offset using binary search @@ -301,7 +304,7 @@ class ElementGenerator : VisualLineElementGenerator, IComparerStart offset. public override int GetFirstInterestedOffset(int startOffset) { - int pos = controls.BinarySearch(new KeyValuePair(startOffset, null), this); + int pos = controls.BinarySearch(new Pair(startOffset, null), this); if (pos < 0) pos = ~pos; if (pos < controls.Count) @@ -312,14 +315,14 @@ public override int GetFirstInterestedOffset(int startOffset) public override VisualLineElement ConstructElement(int offset) { - int pos = controls.BinarySearch(new KeyValuePair(offset, null), this); + int pos = controls.BinarySearch(new Pair(offset, null), this); if (pos >= 0) return new InlineObjectElement(0, controls[pos].Value); else return null; } - int IComparer>.Compare(KeyValuePair x, KeyValuePair y) + int IComparer.Compare(Pair x, Pair y) { return x.Key.CompareTo(y.Key); } diff --git a/src/AvaloniaEdit.TextMate/AvaloniaEdit.TextMate.csproj b/src/AvaloniaEdit.TextMate/AvaloniaEdit.TextMate.csproj index aec05b88..83eadf73 100644 --- a/src/AvaloniaEdit.TextMate/AvaloniaEdit.TextMate.csproj +++ b/src/AvaloniaEdit.TextMate/AvaloniaEdit.TextMate.csproj @@ -10,6 +10,7 @@ + diff --git a/src/AvaloniaEdit/Rendering/GlobalTextRunProperties.cs b/src/AvaloniaEdit/Rendering/GlobalTextRunProperties.cs index e1ae76a4..f3570f04 100644 --- a/src/AvaloniaEdit/Rendering/GlobalTextRunProperties.cs +++ b/src/AvaloniaEdit/Rendering/GlobalTextRunProperties.cs @@ -42,5 +42,5 @@ internal sealed class GlobalTextRunProperties : TextRunProperties public override CultureInfo? CultureInfo => cultureInfo; //public override TextEffectCollection TextEffects { get { return null; } } - } + } } diff --git a/src/AvaloniaEdit/Rendering/InlineObjectRun.cs b/src/AvaloniaEdit/Rendering/InlineObjectRun.cs index 190e4694..79b3ab6d 100644 --- a/src/AvaloniaEdit/Rendering/InlineObjectRun.cs +++ b/src/AvaloniaEdit/Rendering/InlineObjectRun.cs @@ -109,7 +109,7 @@ public override double Baseline } /// - public override Size Size => Element.IsArrangeValid ? Element.DesiredSize : Size.Empty; + public override Size Size => DesiredSize; /// public override void Draw(DrawingContext drawingContext, Point origin) diff --git a/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs b/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs index b5fbf4b8..6dc93a4d 100644 --- a/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs +++ b/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs @@ -102,10 +102,13 @@ public override int GetFirstInterestedOffset(int startOffset) public override VisualLineElement ConstructElement(int offset) { var c = CurrentContext.Document.GetCharAt(offset); + + VisualLineElement element = null; + if (ShowSpaces && c == ' ') { - return new SpaceTextElement(CurrentContext.TextView.CachedElements.GetTextForNonPrintableCharacter("\u00B7", CurrentContext)); + element = new SpaceTextElement(CurrentContext.TextView.CachedElements.GetTextForNonPrintableCharacter("\u00B7", CurrentContext)); } else if (ShowTabs && c == '\t') { - return new TabTextElement(CurrentContext.TextView.CachedElements.GetTextForNonPrintableCharacter("\u00BB", CurrentContext)); + element = new TabTextElement(CurrentContext.TextView.CachedElements.GetTextForNonPrintableCharacter("\u00BB", CurrentContext)); } else if (ShowBoxForControlCharacters && char.IsControl(c)) { var p = new VisualLineElementTextRunProperties(CurrentContext.GlobalTextRunProperties); p.SetForegroundBrush(Brushes.White); @@ -113,9 +116,16 @@ public override VisualLineElement ConstructElement(int offset) var text = FormattedTextElement.PrepareText(textFormatter, TextUtilities.GetControlCharacterName(c), p); return new SpecialCharacterBoxElement(text); - } else { - return null; } + + if(element == null) + { + return null; + } + + element.RelativeTextOffset = offset; + + return element; } private sealed class SpaceTextElement : FormattedTextElement @@ -154,7 +164,7 @@ public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructio if (startVisualColumn == VisualColumn) return new TabGlyphRun(this, TextRunProperties); else if (startVisualColumn == VisualColumn + 1) - return new TextCharacters(new ReadOnlySlice("\t".AsMemory(), RelativeTextOffset, 1), TextRunProperties); + return new TextCharacters(new ReadOnlySlice("\t".AsMemory(), VisualColumn + 1, 1), TextRunProperties); else throw new ArgumentOutOfRangeException(nameof(startVisualColumn)); } @@ -185,19 +195,13 @@ public TabGlyphRun(TabTextElement element, TextRunProperties properties) _element = element; } - public override TextRunProperties Properties { get; } + public override ReadOnlySlice Text { get; } + + public override TextRunProperties Properties { get; } public override double Baseline => _element.Text.Baseline; - public override Size Size - { - get - { - var width = Math.Min(0, _element.Text.WidthIncludingTrailingWhitespace - 1); - - return new Size(width, _element.Text.Height); - } - } + public override Size Size => Size.Empty; public override void Draw(DrawingContext drawingContext, Point origin) { diff --git a/src/AvaloniaEdit/Rendering/TextView.cs b/src/AvaloniaEdit/Rendering/TextView.cs index eed0e44d..287f10ab 100644 --- a/src/AvaloniaEdit/Rendering/TextView.cs +++ b/src/AvaloniaEdit/Rendering/TextView.cs @@ -920,10 +920,11 @@ protected override Size MeasureOverride(Size availableSize) foreach (var layer in Layers) { layer.Measure(availableSize); } - MeasureInlineObjects(); - + InvalidateVisual(); // = InvalidateArrange+InvalidateRender + MeasureInlineObjects(); + double maxWidth; if (_document == null) { @@ -1060,7 +1061,7 @@ private TextRunProperties CreateGlobalTextRunProperties() var p = new GlobalTextRunProperties(); p.typeface = this.CreateTypeface(); p.fontRenderingEmSize = FontSize; - p.foregroundBrush = GetValue(TextBlock.ForegroundProperty); + p.foregroundBrush = GetValue(TextElement.ForegroundProperty); ExtensionMethods.CheckIsFrozen(p.foregroundBrush); p.cultureInfo = CultureInfo.CurrentCulture; return p; @@ -1599,7 +1600,7 @@ public virtual void MakeVisible(Rect rectangle) #region Visual element pointer handling [ThreadStatic] private static bool _invalidCursor; - private VisualLineElement _currentHoveredElement; + //private VisualLineElement _currentHoveredElement; /// /// Updates the pointe cursor, but with background priority. @@ -1635,15 +1636,15 @@ protected override void OnPointerMoved(PointerEventArgs e) { base.OnPointerMoved(e); - var element = GetVisualLineElementFromPosition(e.GetPosition(this) + _scrollOffset); + //var element = GetVisualLineElementFromPosition(e.GetPosition(this) + _scrollOffset); - // Change back to default if hover on a different element - if (_currentHoveredElement != element) - { - Cursor = Parent.Cursor; // uses TextArea's ContentPresenter cursor - _currentHoveredElement = element; - } - element?.OnQueryCursor(e); + //// Change back to default if hover on a different element + //if (_currentHoveredElement != element) + //{ + // Cursor = Parent.Cursor; // uses TextArea's ContentPresenter cursor + // _currentHoveredElement = element; + //} + //element?.OnQueryCursor(e); } protected override void OnPointerPressed(PointerPressedEventArgs e) diff --git a/src/AvaloniaEdit/Rendering/VisualLineText.cs b/src/AvaloniaEdit/Rendering/VisualLineText.cs index 5d6248da..61d2fc12 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineText.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineText.cs @@ -70,7 +70,7 @@ public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructio DocumentLength - relativeOffset); return new TextCharacters( - new ReadOnlySlice(text.Text.AsMemory(), RelativeTextOffset, text.Count, + new ReadOnlySlice(text.Text.AsMemory(), startVisualColumn, text.Count, text.Offset), TextRunProperties); } diff --git a/src/AvaloniaEdit/Rendering/VisualLineTextParagraphProperties.cs b/src/AvaloniaEdit/Rendering/VisualLineTextParagraphProperties.cs index 8b54a0a3..4b0ea45f 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineTextParagraphProperties.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineTextParagraphProperties.cs @@ -34,7 +34,7 @@ sealed class VisualLineTextParagraphProperties : TextParagraphProperties public override FlowDirection FlowDirection => FlowDirection.LeftToRight; public override TextAlignment TextAlignment => TextAlignment.Left; - public override double LineHeight => double.NaN; + public override double LineHeight => DefaultTextRunProperties.FontRenderingEmSize * 1.2; public override bool FirstLineInParagraph => firstLineInParagraph; public override TextRunProperties DefaultTextRunProperties => defaultTextRunProperties; diff --git a/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs b/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs index ed1f9e70..7c4b43ce 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs @@ -89,7 +89,12 @@ private TextRun CreateTextRunForNewLine() else newlineText = "?"; } - return new FormattedTextRun(new FormattedTextElement(TextView.CachedElements.GetTextForNonPrintableCharacter(newlineText, this), 0), GlobalTextRunProperties); + + var textElement = new FormattedTextElement(TextView.CachedElements.GetTextForNonPrintableCharacter(newlineText, this), 0); + + textElement.RelativeTextOffset = lastDocumentLine.Offset + lastDocumentLine.Length; + + return new FormattedTextRun(textElement, GlobalTextRunProperties); } public ReadOnlySlice GetPrecedingText(int textSourceCharacterIndexLimit) diff --git a/src/AvaloniaEdit/Search/SearchPanel.xaml b/src/AvaloniaEdit/Search/SearchPanel.xaml index 1d2fa89b..af84e716 100644 --- a/src/AvaloniaEdit/Search/SearchPanel.xaml +++ b/src/AvaloniaEdit/Search/SearchPanel.xaml @@ -44,7 +44,7 @@ BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" HorizontalAlignment="Right" - VerticalAlignment="Top" TextBlock.FontFamily="Segoi UI" TextBlock.FontSize="10"> + VerticalAlignment="Top" TextElement.FontFamily="Segoi UI" TextElement.FontSize="10"> public static Typeface CreateTypeface(this Control fe) { - return new Typeface(fe.GetValue(TextBlock.FontFamilyProperty), - fe.GetValue(TextBlock.FontStyleProperty), - fe.GetValue(TextBlock.FontWeightProperty), - fe.GetValue(TextBlock.FontStretchProperty)); + return new Typeface(fe.GetValue(TextElement.FontFamilyProperty), + fe.GetValue(TextElement.FontStyleProperty), + fe.GetValue(TextElement.FontWeightProperty), + fe.GetValue(TextElement.FontStretchProperty)); } #endregion diff --git a/src/AvaloniaEdit/Utils/TextFormatterFactory.cs b/src/AvaloniaEdit/Utils/TextFormatterFactory.cs index 4daa89d2..a05f5340 100644 --- a/src/AvaloniaEdit/Utils/TextFormatterFactory.cs +++ b/src/AvaloniaEdit/Utils/TextFormatterFactory.cs @@ -19,6 +19,7 @@ using System; using System.Globalization; using Avalonia.Controls; +using Avalonia.Controls.Documents; using Avalonia.Media; using Avalonia.Media.TextFormatting; @@ -55,9 +56,9 @@ public static FormattedText CreateFormattedText(Control element, string text, Ty if (typeface == default) typeface = element.CreateTypeface(); if (emSize == null) - emSize = TextBlock.GetFontSize(element); + emSize = TextElement.GetFontSize(element); if (foreground == null) - foreground = TextBlock.GetForeground(element); + foreground = TextElement.GetForeground(element); return new FormattedText( text, diff --git a/src/AvaloniaEdit/Utils/TextLineExtensions.cs b/src/AvaloniaEdit/Utils/TextLineExtensions.cs deleted file mode 100644 index 3c954274..00000000 --- a/src/AvaloniaEdit/Utils/TextLineExtensions.cs +++ /dev/null @@ -1,208 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia; -using Avalonia.Media; -using Avalonia.Media.TextFormatting; -using Avalonia.Utilities; - -namespace AvaloniaEdit.Utils; - -#nullable enable - -public static class TextLineExtensions -{ - public static IReadOnlyList GetTextBounds(this TextLine textLine, int start, int length) - { - if (start + length <= 0) - { - return Array.Empty(); - } - - var result = new List(textLine.TextRuns.Count); - - var currentPosition = 0; - var currentRect = Rect.Empty; - - //Current line isn't covered. - if (textLine.TextRange.Length <= start) - { - return result; - } - - //The whole line is covered. - if (currentPosition >= start && start + length > textLine.TextRange.Length) - { - currentRect = new Rect(textLine.Start, 0, textLine.WidthIncludingTrailingWhitespace, - textLine.Height); - - result.Add(new TextBounds{ Rectangle = currentRect}); - - return result; - } - - var startX = textLine.Start; - - //A portion of the line is covered. - for (var index = 0; index < textLine.TextRuns.Count; index++) - { - var currentRun = textLine.TextRuns[index] as DrawableTextRun; - var currentShaped = currentRun as ShapedTextCharacters; - - if (currentRun is null) - { - continue; - } - - TextRun? nextRun = null; - - if (index + 1 < textLine.TextRuns.Count) - { - nextRun = textLine.TextRuns[index + 1]; - } - - if (nextRun != null) - { - if (nextRun.Text.Start < currentRun.Text.Start && start + length < currentRun.Text.End) - { - goto skip; - } - - if (currentRun.Text.Start >= start + length) - { - goto skip; - } - - if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < start) - { - goto skip; - } - - if (currentRun.Text.End < start) - { - goto skip; - } - - goto noop; - - skip: - { - startX += currentRun.Size.Width; - } - - continue; - - noop: - { - } - } - - var endX = startX; - var endOffset = 0d; - - if (currentShaped != null) - { - endOffset = currentShaped.GlyphRun.GetDistanceFromCharacterHit( - currentShaped.ShapedBuffer.IsLeftToRight ? new CharacterHit(start + length) : new CharacterHit(start)); - - endX += endOffset; - - var startOffset = currentShaped.GlyphRun.GetDistanceFromCharacterHit( - currentShaped.ShapedBuffer.IsLeftToRight ? new CharacterHit(start) : new CharacterHit(start + length)); - - startX += startOffset; - - var characterHit = currentShaped.GlyphRun.IsLeftToRight - ? currentShaped.GlyphRun.GetCharacterHitFromDistance(endOffset, out _) - : currentShaped.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); - - currentPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - - if (nextRun is ShapedTextCharacters nextShaped) - { - if (currentShaped.ShapedBuffer.IsLeftToRight == nextShaped.ShapedBuffer.IsLeftToRight) - { - endOffset = nextShaped.GlyphRun.GetDistanceFromCharacterHit( - nextShaped.ShapedBuffer.IsLeftToRight - ? new CharacterHit(start + length) - : new CharacterHit(start)); - - index++; - - endX += endOffset; - - currentRun = currentShaped = nextShaped; - - if (nextShaped.ShapedBuffer.IsLeftToRight) - { - characterHit = nextShaped.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); - - currentPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - } - } - } - } - - if (endX < startX) - { - (endX, startX) = (startX, endX); - } - - var width = endX - startX; - - if (result.Count > 0 && MathUtilities.AreClose(currentRect.Top, 0) && - MathUtilities.AreClose(currentRect.Right, startX)) - { - var textBounds = new TextBounds {Rectangle = currentRect.WithWidth(currentRect.Width + width)}; - - result[result.Count - 1] = textBounds; - } - else - { - currentRect = new Rect(startX, 0, width, textLine.Height); - - result.Add(new TextBounds{ Rectangle = currentRect}); - } - - if (currentShaped != null && currentShaped.ShapedBuffer.IsLeftToRight) - { - if (nextRun != null) - { - if (nextRun.Text.Start > currentRun.Text.Start && nextRun.Text.Start >= start + length) - { - break; - } - - currentPosition = nextRun.Text.End; - } - else - { - if (currentPosition >= start + length) - { - break; - } - } - } - else - { - if (currentPosition <= start) - { - break; - } - } - - if (currentShaped != null && !currentShaped.ShapedBuffer.IsLeftToRight && currentPosition != currentRun.Text.Start) - { - endX += currentShaped.GlyphRun.Size.Width - endOffset; - } - - startX = endX; - } - - return result; - } -} - -public struct TextBounds -{ - public Rect Rectangle { get; set; } -} \ No newline at end of file From ddeffb9de843ee02484d94a165140d87db990dec Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 30 Mar 2022 20:44:13 +0200 Subject: [PATCH 11/24] 1.35 is working as previously for LineHeight --- src/AvaloniaEdit/Rendering/VisualLineTextParagraphProperties.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AvaloniaEdit/Rendering/VisualLineTextParagraphProperties.cs b/src/AvaloniaEdit/Rendering/VisualLineTextParagraphProperties.cs index 4b0ea45f..bebe40a8 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineTextParagraphProperties.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineTextParagraphProperties.cs @@ -34,7 +34,7 @@ sealed class VisualLineTextParagraphProperties : TextParagraphProperties public override FlowDirection FlowDirection => FlowDirection.LeftToRight; public override TextAlignment TextAlignment => TextAlignment.Left; - public override double LineHeight => DefaultTextRunProperties.FontRenderingEmSize * 1.2; + public override double LineHeight => DefaultTextRunProperties.FontRenderingEmSize * 1.35; public override bool FirstLineInParagraph => firstLineInParagraph; public override TextRunProperties DefaultTextRunProperties => defaultTextRunProperties; From 9e0fa63a87f102ae6132eb2745395a94c1bd7bf9 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 30 Mar 2022 21:01:31 +0200 Subject: [PATCH 12/24] Add button to view tabs/spaces/EOL Uniform button height and alignment in the header panel. --- src/AvaloniaEdit.Demo/MainWindow.xaml | 11 +++++++---- src/AvaloniaEdit.Demo/MainWindow.xaml.cs | 6 ++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/AvaloniaEdit.Demo/MainWindow.xaml b/src/AvaloniaEdit.Demo/MainWindow.xaml index 38423aeb..5ebc2fc8 100644 --- a/src/AvaloniaEdit.Demo/MainWindow.xaml +++ b/src/AvaloniaEdit.Demo/MainWindow.xaml @@ -17,10 +17,13 @@ -