diff --git a/Directory.Build.props b/Directory.Build.props index a55d4d1..30961a3 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 5006bdd..e7bfb7e 100644 --- a/src/AvaloniaEdit.Demo/MainWindow.xaml.cs +++ b/src/AvaloniaEdit.Demo/MainWindow.xaml.cs @@ -156,7 +156,7 @@ namespace AvaloniaEdit.Demo 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 @@ namespace AvaloniaEdit.Demo } } - 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 @@ namespace AvaloniaEdit.Demo /// 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 @@ namespace AvaloniaEdit.Demo 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 1c33580..8f413e6 100644 --- a/src/AvaloniaEdit/Editing/Caret.cs +++ b/src/AvaloniaEdit/Editing/Caret.cs @@ -45,11 +45,11 @@ namespace AvaloniaEdit.Editing _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 @@ namespace AvaloniaEdit.Editing lineBottom - lineTop); } - private Rect CalcCaretOverstrikeRectangle(VisualLine visualLine) - { - if (!_visualColumnValid) - { - RevalidateVisualColumn(visualLine); - } + 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); + 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. - 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 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 86c2511..b37fed9 100644 --- a/src/AvaloniaEdit/Editing/LineNumberMargin.cs +++ b/src/AvaloniaEdit/Editing/LineNumberMargin.cs @@ -50,51 +50,41 @@ namespace AvaloniaEdit.Editing /// protected double EmSize { get; set; } - /// - protected override Size MeasureOverride(Size availableSize) - { - Typeface = new Typeface(GetValue(TextBlock.FontFamilyProperty)); - EmSize = GetValue(TextBlock.FontSizeProperty); + /// + protected override Size MeasureOverride(Size availableSize) + { + Typeface = this.CreateTypeface(); + 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; + 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 5ff424c..e6ee835 100644 --- a/src/AvaloniaEdit/Folding/FoldingElementGenerator.cs +++ b/src/AvaloniaEdit/Folding/FoldingElementGenerator.cs @@ -22,7 +22,9 @@ using Avalonia; 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 @@ namespace AvaloniaEdit.Folding } } 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 @@ namespace AvaloniaEdit.Folding _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 @@ namespace AvaloniaEdit.Folding 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 4fdcefe..af2fa28 100644 --- a/src/AvaloniaEdit/Highlighting/HighlightingColorizer.cs +++ b/src/AvaloniaEdit/Highlighting/HighlightingColorizer.cs @@ -255,62 +255,34 @@ namespace AvaloniaEdit.Highlighting 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 50de0f5..8fa9939 100644 --- a/src/AvaloniaEdit/Rendering/BackgroundGeometryBuilder.cs +++ b/src/AvaloniaEdit/Rendering/BackgroundGeometryBuilder.cs @@ -21,381 +21,362 @@ using System.Collections.Generic; 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 f53d925..e3e6dd9 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 @@ namespace AvaloniaEdit.Rendering /// 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 d61f9df..822ac59 100644 --- a/src/AvaloniaEdit/Rendering/ColorizingTransformer.cs +++ b/src/AvaloniaEdit/Rendering/ColorizingTransformer.cs @@ -21,111 +21,100 @@ using System.Collections.Generic; 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 79689e6..be6d2b7 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 @@ namespace AvaloniaEdit.Rendering _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 5ef6577..243ba8f 100644 --- a/src/AvaloniaEdit/Rendering/CurrentLineHighlightRenderer.cs +++ b/src/AvaloniaEdit/Rendering/CurrentLineHighlightRenderer.cs @@ -37,13 +37,10 @@ namespace AvaloniaEdit.Rendering #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 @@ namespace AvaloniaEdit.Rendering 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 @@ namespace AvaloniaEdit.Rendering 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 @@ namespace AvaloniaEdit.Rendering 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 e823e98..6ba669c 100644 --- a/src/AvaloniaEdit/Rendering/DocumentColorizingTransformer.cs +++ b/src/AvaloniaEdit/Rendering/DocumentColorizingTransformer.cs @@ -23,80 +23,74 @@ using AvaloniaEdit.Document; 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 7b8d5e5..6b8cc21 100644 --- a/src/AvaloniaEdit/Rendering/FormattedTextElement.cs +++ b/src/AvaloniaEdit/Rendering/FormattedTextElement.cs @@ -20,144 +20,136 @@ using System; 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; } + /// + /// 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(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(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)); - } + /// + /// 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); - } + /// + 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. - /// - 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, + /// + /// 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); + } + } - //DefaultIncrementalTab = 40 + /// + /// 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)); + } - new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true, false, - properties, TextWrapping.NoWrap, 0, 0)); - } - } + /// + /// Gets the element for which the FormattedTextRun was created. + /// + public FormattedTextElement Element { get; } - /// - /// 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)); + /// + public override TextRunProperties Properties { get; } - Size = GetSize(); - } + public override double Baseline => Element.FormattedText?.Baseline ?? Element.TextLine.Baseline; - /// - /// Gets the element for which the FormattedTextRun was created. - /// - public FormattedTextElement Element { get; } + /// + public override Size Size + { + get + { + var formattedText = Element.FormattedText; + + if (formattedText != null) { + return new Size(formattedText.WidthIncludingTrailingWhitespace, formattedText.Height); + } - /// - public override int TextSourceLength => Element.VisualLength; + var text = Element.TextLine; + return new Size( text.WidthIncludingTrailingWhitespace, text.Height); + } + } - /// - 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); - } - } - } + /// + 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 2ea559d..0000000 --- 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 0000000..e1ae76a --- /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 e331f6c..5311f3f 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. + /// + /// 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: + /* Idea: class NewHeightTreeNode { int totalCount; // =count+left.count+right.count int count; // one node can represent multiple lines @@ -54,1163 +51,1072 @@ namespace AvaloniaEdit.Rendering collapsing/uncollapsing, especially when compression reduces the n. */ - #region Constructor + #region Constructor - private readonly TextDocument _document; - private HeightTreeNode _root; - private WeakLineTracker _weakLineTracker; + 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 HeightTree(TextDocument document, double defaultLineHeight) + { + this._document = document; + _weakLineTracker = WeakLineTracker.Register(document, this); + this.DefaultLineHeight = defaultLineHeight; + RebuildDocument(); + } - public void Dispose() - { - _weakLineTracker?.Deregister(); - _root = null; - _weakLineTracker = null; - } + public void Dispose() + { + if (_weakLineTracker != null) + _weakLineTracker.Deregister(); + this._root = null; + this._weakLineTracker = null; + } - private double _defaultLineHeight; + public bool IsDisposed { + get { + return _root == null; + } + } - 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 double _defaultLineHeight; - private HeightTreeNode GetNode(DocumentLine ls) - { - return GetNodeByIndex(ls.LineNumber - 1); - } - #endregion + 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); + } + } + } + } - #region RebuildDocument - void ILineTracker.ChangeComplete(DocumentChangeEventArgs e) - { - } + private HeightTreeNode GetNode(DocumentLine ls) + { + return GetNodeByIndex(ls.LineNumber - 1); + } + #endregion - void ILineTracker.SetLineLength(DocumentLine ls, int newTotalLength) - { - } + #region RebuildDocument + void ILineTracker.ChangeComplete(DocumentChangeEventArgs e) + { + } - /// - /// Rebuild the tree, in O(n). - /// - public void RebuildDocument() - { - foreach (var s in GetAllCollapsedSections()) - { - s.Start = null; - s.End = null; - } + void ILineTracker.SetLineLength(DocumentLine ls, int newTotalLength) + { + } - 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; + /// + /// 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 + /// + /// 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); + #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 + 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 + #region Rotation callbacks - private enum UpdateAfterChildrenChangeRecursionMode - { - None, - IfRequired, - WholeBranch - } + private enum UpdateAfterChildrenChangeRecursionMode + { + None, + IfRequired, + WholeBranch + } - private static void UpdateAfterChildrenChange(HeightTreeNode node) - { - UpdateAugmentedData(node, UpdateAfterChildrenChangeRecursionMode.IfRequired); - } + 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 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); + 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); + UpdateAfterChildrenChange(node); - // not required: rotations only happen on insertions/deletions - // -> totalCount changes -> the parent is always updated - //UpdateAfterChildrenChange(node.parent); - } + // 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); + 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); + UpdateAfterChildrenChange(node); - // not required: rotations only happen on insertions/deletions - // -> totalCount changes -> the parent is always updated - //UpdateAfterChildrenChange(node.parent); - } + // 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 + // 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); + 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); - } + 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 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 bool _inRemoval; + private List _nodesToCheckForMerging; - private void BeginRemoval() - { - Debug.Assert(!_inRemoval); - if (_nodesToCheckForMerging == null) - { - _nodesToCheckForMerging = new List(); - } - _inRemoval = true; - } + 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 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 + 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 + #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 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. + 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 (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; - } - } - } + // 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?.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 + 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; - } + #region Public methods + public DocumentLine GetLineByNumber(int number) + { + return GetNodeByIndex(number - 1).DocumentLine; + } - public DocumentLine GetLineByVisualPosition(double position) - { - return GetNodeByVisualPosition(position).DocumentLine; - } + public DocumentLine GetLineByVisualPosition(double position) + { + return GetNodeByVisualPosition(position).DocumentLine; + } - public double GetVisualPosition(DocumentLine line) - { - return GetVisualPositionFromNode(GetNode(line)); - } + public double GetVisualPosition(DocumentLine line) + { + return GetVisualPositionFromNode(GetNode(line)); + } - public double GetHeight(DocumentLine line) - { - return GetNode(line).LineNode.Height; - } + 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 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); - } + 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); + /// + /// 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 + return section; + } + #endregion - #region LineCount & TotalHeight - public int LineCount => _root.TotalCount; + #region LineCount & TotalHeight + public int LineCount { + get { + return _root.TotalCount; + } + } - public double TotalHeight => _root.TotalHeight; + public double TotalHeight { + get { + return _root.TotalHeight; + } + } + #endregion - #endregion + #region GetAllCollapsedSections - #region GetAllCollapsedSections + private IEnumerable AllNodes { + get { + if (_root != null) { + var node = _root.LeftMost; + while (node != null) { + yield return node; + node = node.Successor; + } + } + } + } - 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 - internal IEnumerable GetAllCollapsedSections() - { - var emptyCsList = new List(); - return AllNodes.SelectMany( - node => (node.LineNode.CollapsedSections ?? emptyCsList).Concat( - node.CollapsedSections ?? emptyCsList) - ).Distinct(); - } - #endregion - - #region CheckProperties + #region CheckProperties #if DEBUG - [Conditional("DATACONSISTENCYTEST")] - internal void CheckProperties() - { - CheckProperties(_root); + [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)); - } - } + 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); - } + // 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 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; + 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.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)); - } + 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)); - } - } + /// + /// 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; + private void CheckNodeProperties(HeightTreeNode node, HeightTreeNode parentNode, bool parentColor, int blackCount, ref int expectedBlackCount) + { + if (node == null) return; - Debug.Assert(node.Parent == parentNode); + 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); - } + 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(); - } + [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) - { - 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 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 + #endregion - #region Red/Black Tree + #region Red/Black Tree - private const bool Red = true; - private const bool Black = false; + 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 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 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); + 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! + 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; + // 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); - } - } + // 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 + 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 + 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; + 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; - } + 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); - } - } - } + // 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; + 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); - } + // 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 - } + 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 == 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 (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 + 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); - } - } + 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 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); + 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); - } + // 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); + 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); - } + // 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) + { + 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; - return parentNode.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?.Color ?? Black; - } - #endregion + private static bool GetColor(HeightTreeNode node) + { + return node != null ? node.Color : Black; + } + #endregion - #region Collapsing support + #region Collapsing support - private static bool GetIsCollapedFromNode(HeightTreeNode node) - { - while (node != null) - { - if (node.IsDirectlyCollapsed) - return true; - node = node.Parent; - } - return false; - } + 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); - } + internal void AddCollapsedSection(CollapsedLineSection section, int sectionLength) + { + AddRemoveCollapsedSection(section, sectionLength, true); + } - private void AddRemoveCollapsedSection(CollapsedLineSection section, int sectionLength, bool add) - { - Debug.Assert(sectionLength > 0); + 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); - } + 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); - } - } + 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 - } + 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 6a8131e..f26176a 100644 --- a/src/AvaloniaEdit/Rendering/HeightTreeLineNode.cs +++ b/src/AvaloniaEdit/Rendering/HeightTreeLineNode.cs @@ -32,7 +32,9 @@ namespace AvaloniaEdit.Rendering 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 @@ namespace AvaloniaEdit.Rendering /// /// 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 e605024..c4019dd 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 @@ namespace AvaloniaEdit.Rendering return node; } } - + internal HeightTreeNode RightMost { get { HeightTreeNode node = this; @@ -65,7 +63,7 @@ namespace AvaloniaEdit.Rendering return node; } } - + /// /// Gets the inorder successor of the node. /// @@ -73,25 +71,26 @@ namespace AvaloniaEdit.Rendering 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 @@ namespace AvaloniaEdit.Rendering /// + right.IsDirectlyCollapsed ? 0 : right.totalHeight /// internal double TotalHeight; - + /// /// List of the sections that hold this node collapsed. /// Invariant 1: @@ -113,10 +112,14 @@ namespace AvaloniaEdit.Rendering /// 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 @@ namespace AvaloniaEdit.Rendering Debug.Assert(!CollapsedSections.Contains(section)); CollapsedSections.Add(section); } - - + + internal void RemoveDirectlyCollapsed(CollapsedLineSection section) { Debug.Assert(CollapsedSections.Contains(section)); @@ -140,8 +143,8 @@ namespace AvaloniaEdit.Rendering TotalHeight += Right.TotalHeight; } } - - #if DEBUG + +#if DEBUG public override string ToString() { return "[HeightTreeNode " @@ -151,16 +154,16 @@ namespace AvaloniaEdit.Rendering + " 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 a74f478..d1905cb 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 @@ namespace AvaloniaEdit.Rendering /// 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 @@ namespace AvaloniaEdit.Rendering /// 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 09cc61c..190e469 100644 --- a/src/AvaloniaEdit/Rendering/InlineObjectRun.cs +++ b/src/AvaloniaEdit/Rendering/InlineObjectRun.cs @@ -22,85 +22,99 @@ using Avalonia.Controls; 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; } + /// A inline UIElement in the document. + /// + public class InlineObjectElement : VisualLineElement + { + /// + /// Gets the inline element that is displayed. + /// + public Control 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)); - } + /// + /// 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)); + /// + public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) + { + if (context == null) + throw new ArgumentNullException(nameof(context)); - return new InlineObjectRun(1, TextRunProperties, Element); - } - } + return new InlineObjectRun(1, TextRunProperties, Element); + } + } - /// - /// 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"); + /// + /// A text run with an embedded UIElement. + /// + public class InlineObjectRun : DrawableTextRun + { + internal Size DesiredSize; - TextSourceLength = length; - Properties = properties ?? throw new ArgumentNullException(nameof(properties)); - Element = element ?? throw new ArgumentNullException(nameof(element)); - } + /// + /// 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"); - /// - /// Gets the element displayed by the InlineObjectRun. - /// - public IControl Element { get; } + TextSourceLength = length; + Properties = properties ?? throw new ArgumentNullException(nameof(properties)); + Element = element ?? throw new ArgumentNullException(nameof(element)); - /// - /// 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; } + DesiredSize = element.DesiredSize; + } - /// - public override int TextSourceLength { get; } + /// + /// Gets the element displayed by the InlineObjectRun. + /// + public Control Element { get; } - /// - public override TextRunProperties Properties { 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 double Baseline => Element.DesiredSize.Height; + public override TextRunProperties? Properties { get; } - public override Size Size => Element.IsMeasureValid ? Element.DesiredSize : Size.Empty; - public Size DesiredSize { get; set; } + public override int TextSourceLength { get; } - public override void Draw(DrawingContext drawingContext, Point origin) - { - //noop - } - } + 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 d585a03..075dce7 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 @@ namespace AvaloniaEdit.Rendering { // 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/]"); - - // 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 DefaultLinkRegex = new Regex(@"\b(https?://|ftp://|www\.)[\w\d\._/\-~%@()+:?&=#!]*[\w\d/]"); + + // try to detect email addresses + 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 @@ namespace AvaloniaEdit.Rendering _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 @@ namespace AvaloniaEdit.Rendering 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 @@ namespace AvaloniaEdit.Rendering 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 @@ namespace AvaloniaEdit.Rendering : 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 f74789d..9c171df 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 (textSourceCharacterIndex < _text.Length) + return new TextCharacters( + new ReadOnlySlice(_text.AsMemory(), textSourceCharacterIndex, + _text.Length - textSourceCharacterIndex), _properties); - if (textSourceIndex > _text.Length) - { - return null; - } - - return new TextEndOfParagraph(1); + return new TextEndOfParagraph(1); } } } diff --git a/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs b/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs index dcdbbc6..b2605a2 100644 --- a/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs +++ b/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs @@ -23,195 +23,199 @@ using Avalonia.Media; 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; } + /// + /// 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 » 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; } + /// + /// 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; - } + /// + /// 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; - } + 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); + 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; - } + 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)); - } - 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); + 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; + } + } - 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) + { + } - 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 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; + } + } - public override bool IsWhitespace(int visualColumn) - { - return true; - } - } + private sealed class TabTextElement : VisualLineElement + { + internal readonly TextLine Text; - internal sealed class TabTextElement : VisualLineElement - { - internal readonly TextLine Text; + public TabTextElement(TextLine text) : base(2, 1) + { + Text = 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 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); + else + return -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; + } + } - public override bool IsWhitespace(int visualColumn) - { - return true; - } - } + private sealed class TabGlyphRun : DrawableTextRun + { + private readonly TabTextElement _element; - internal 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 TabGlyphRun(TabTextElement element, TextRunProperties properties) - { - Properties = properties ?? throw new ArgumentNullException(nameof(properties)); - _element = element; - } + public override TextRunProperties Properties { get; } - public override int TextSourceLength => 1; + public override double Baseline => _element.Text.Baseline; - public override TextRunProperties Properties { get; } + public override Size Size + { + get + { + var width = Math.Min(0, _element.Text.WidthIncludingTrailingWhitespace - 1); + + return new Size(width, _element.Text.Height); + } + } - public override double Baseline => _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)); + } + } - public override Size Size => new(0, _element.Text.Height); + private sealed class SpecialCharacterBoxElement : FormattedTextElement + { + public SpecialCharacterBoxElement(TextLine text) : base(text, 1) + { + } - 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); - } - } + public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) + { + return new SpecialCharacterTextRun(this, TextRunProperties); + } + } internal sealed class SpecialCharacterTextRun : FormattedTextRun { @@ -241,12 +245,19 @@ namespace AvaloniaEdit.Rendering 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 79f2b2b..e734aea 100644 --- a/src/AvaloniaEdit/Rendering/TextView.cs +++ b/src/AvaloniaEdit/Rendering/TextView.cs @@ -27,11 +27,13 @@ using System.Linq; 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 @@ namespace AvaloniaEdit.Rendering 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 @@ namespace AvaloniaEdit.Rendering _hoverLogic.PointerHoverStopped += (sender, e) => RaiseHoverEventPair(e, PreviewPointerHoverStoppedEvent, PointerHoverStoppedEvent); } - #endregion + #endregion #region Document Property /// @@ -425,12 +427,12 @@ namespace AvaloniaEdit.Rendering 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 @@ namespace AvaloniaEdit.Rendering 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(); + /// + /// 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(); - var l = GetVisualLine(documentLine.LineNumber); - if (l == null) - { - var globalTextRunProperties = CreateGlobalTextRunProperties(); - var paragraphProperties = CreateParagraphProperties(globalTextRunProperties); + VisualLine l = GetVisualLine(documentLine.LineNumber); + if (l == null) { + TextRunProperties globalTextRunProperties = CreateGlobalTextRunProperties(); + VisualLineTextParagraphProperties paragraphProperties = CreateParagraphProperties(globalTextRunProperties); - while (_heightTree.GetIsCollapsed(documentLine.LineNumber)) - { - documentLine = documentLine.PreviousLine; - } + 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 + 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 @@ namespace AvaloniaEdit.Rendering } #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 @@ namespace AvaloniaEdit.Rendering 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 @@ namespace AvaloniaEdit.Rendering 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 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 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 VisualLineTextParagraphProperties CreateParagraphProperties(TextRunProperties defaultTextRunProperties) + { + return new VisualLineTextParagraphProperties { + defaultTextRunProperties = defaultTextRunProperties, + textWrapping = _canHorizontallyScroll ? TextWrapping.NoWrap : TextWrapping.Wrap, + tabSize = 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"); + 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"); - var visualLine = new VisualLine(this, documentLine); - - var textSource = new VisualLineTextSource(visualLine) - { - Document = _document, - GlobalTextRunProperties = globalTextRunProperties, - TextView = this - }; + //Debug.WriteLine("Building line " + documentLine.LineNumber); - visualLine.ConstructVisualElements(textSource, elementGeneratorsArray); + VisualLine visualLine = new VisualLine(this, documentLine); + VisualLineTextSource textSource = new VisualLineTextSource(visualLine) { + Document = _document, + GlobalTextRunProperties = globalTextRunProperties, + TextView = this + }; - 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?"); - } - } + 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; + // 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; + 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; - } - } + 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; - } + visualLine.SetTextLines(textLines); + _heightTree.SetHeight(visualLine.FirstDocumentLine, visualLine.Height); + return visualLine; + } private static int GetIndentationVisualColumn(VisualLine visualLine) { @@ -1532,11 +1512,11 @@ namespace AvaloniaEdit.Rendering 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 @@ namespace AvaloniaEdit.Rendering _defaultBaseline = FontSize; _defaultLineHeight = FontSize + 3; } + // Update heightTree.DefaultLineHeight, if a document is loaded. if (_heightTree != null) _heightTree.DefaultLineHeight = _defaultLineHeight; @@ -1990,12 +1971,12 @@ namespace AvaloniaEdit.Rendering /// 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 @@ namespace AvaloniaEdit.Rendering /// 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 @@ namespace AvaloniaEdit.Rendering /// /// 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 7edb361..7b481c7 100644 --- a/src/AvaloniaEdit/Rendering/TextViewCachedElements.cs +++ b/src/AvaloniaEdit/Rendering/TextViewCachedElements.cs @@ -21,37 +21,35 @@ using Avalonia.Media.TextFormatting; 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 3da4383..ce7d4c4 100644 --- a/src/AvaloniaEdit/Rendering/VisualLine.cs +++ b/src/AvaloniaEdit/Rendering/VisualLine.cs @@ -145,95 +145,70 @@ namespace AvaloniaEdit.Rendering g.FinishGeneration(); } - var globalTextRunProperties = context.GlobalTextRunProperties; - foreach (var element in _elements) - { - element.SetTextRunProperties(globalTextRunProperties.Clone()); - } - Elements = new ReadOnlyCollection(_elements); - CalculateOffsets(); - _phase = LifetimePhase.Transforming; - } + var globalTextRunProperties = context.GlobalTextRunProperties; + foreach (var element in _elements) { + element.SetTextRunProperties(new VisualLineElementTextRunProperties(globalTextRunProperties)); + } + this.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; - } - } - } - } - } - } + 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 @@ namespace AvaloniaEdit.Rendering 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 f55c22c..1b5babc 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineElement.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineElement.cs @@ -72,19 +72,19 @@ namespace AvaloniaEdit.Rendering /// /// 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 @@ namespace AvaloniaEdit.Rendering /// 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 @@ namespace AvaloniaEdit.Rendering 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 0000000..5f6effb --- /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 d02d83e..88f9f68 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineLinkText.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineLinkText.cs @@ -21,6 +21,7 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Controls; using System.Diagnostics; +using Avalonia.Media; using Avalonia.Media.TextFormatting; namespace AvaloniaEdit.Rendering @@ -66,14 +67,15 @@ namespace AvaloniaEdit.Rendering 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 ec0dab6..b30af39 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineText.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineText.cs @@ -21,6 +21,7 @@ using System.Collections.Generic; 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 @@ namespace AvaloniaEdit.Rendering 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 0000000..8b54a0a --- /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 8d2199b..ed1f9e7 100644 --- a/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs +++ b/src/AvaloniaEdit/Rendering/VisualLineTextSource.cs @@ -21,117 +21,112 @@ using System.Diagnostics; 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 0a6fa12..e274e47 100644 --- a/src/AvaloniaEdit/Utils/ExtensionMethods.cs +++ b/src/AvaloniaEdit/Utils/ExtensionMethods.cs @@ -21,7 +21,9 @@ using System.Collections.Immutable; 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 @@ namespace AvaloniaEdit.Utils 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 002cfa6..4daa89d 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 @@ namespace AvaloniaEdit.Utils /// 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; - } + 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); - public TextRun GetTextRun(int textSourceIndex) - { - if (textSourceIndex < _text.Length) - { - return new TextCharacters(_text, textSourceIndex, _text.Length - textSourceIndex, _defaultProperties); - } - - if (textSourceIndex > _text.Length) - { - return null; - } - - 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 fb68a40..3c95427 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 endX = textLine.GetDistanceFromCharacterHit(new CharacterHit(start + length)); + 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; + } - return new Rect(startX, 0, endX - startX, textLine.Height); + //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