aboutsummaryrefslogtreecommitdiffstats
path: root/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering
diff options
context:
space:
mode:
authorVictoria Plitt <Victoria.Plitt@twine-s.com>2019-04-08 13:49:55 +0300
committerVictoria Plitt <Victoria.Plitt@twine-s.com>2019-04-08 13:49:55 +0300
commitfc8a05358a92cc3c77c5f1e30d536807ef0614fd (patch)
treec65f696ebd60f3790145721307c255e5a339923f /Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering
parentb4a71931ea52636c6b36376aa9d71697ccf73524 (diff)
downloadTango-fc8a05358a92cc3c77c5f1e30d536807ef0614fd.tar.gz
Tango-fc8a05358a92cc3c77c5f1e30d536807ef0614fd.zip
were added scripting projects
Diffstat (limited to 'Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering')
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/BackgroundGeometryBuilder.cs343
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/CollapsedLineSection.cs95
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/ColorizingTransformer.cs108
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/ColumnRulerRenderer.cs64
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/DefaultTextRunTypographyProperties.cs171
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/DocumentColorizingTransformer.cs81
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/FormattedTextElement.cs207
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/GlobalTextRunProperties.cs30
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/HeightTree.cs1092
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/HeightTreeLineNode.cs49
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/HeightTreeNode.cs155
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/IBackgroundRenderer.cs26
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/ITextRunConstructionContext.cs47
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/ITextViewConnect.cs24
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/IVisualLineTransformer.cs19
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/InlineObjectRun.cs145
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/Layer.cs43
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/LayerPosition.cs91
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/LinkElementGenerator.cs144
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/MouseHoverLogic.cs134
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/SimpleTextSource.cs39
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/SingleCharacterElementGenerator.cs268
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/TextLayer.cs70
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/TextView.cs2009
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/TextViewCachedElements.cs43
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/TextViewWeakEventManager.cs71
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLine.cs681
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineConstructionStartEventArgs.cs29
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineElement.cs249
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineElementGenerator.cs63
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineElementTextRunProperties.cs241
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineLinkText.cs114
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineText.cs122
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineTextParagraphProperties.cs31
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineTextSource.cs123
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLinesInvalidException.cs44
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualYPosition.cs46
37 files changed, 7311 insertions, 0 deletions
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/BackgroundGeometryBuilder.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/BackgroundGeometryBuilder.cs
new file mode 100644
index 000000000..d3c839b35
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/BackgroundGeometryBuilder.cs
@@ -0,0 +1,343 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls.Primitives;
+using System.Windows.Media;
+using System.Windows.Media.TextFormatting;
+
+using Tango.Scripting.Editors.Document;
+using Tango.Scripting.Editors.Editing;
+using Tango.Scripting.Editors.Utils;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// Helper for creating a PathGeometry.
+ /// </summary>
+ public sealed class BackgroundGeometryBuilder
+ {
+ double cornerRadius;
+
+ /// <summary>
+ /// Gets/sets the radius of the rounded corners.
+ /// </summary>
+ public double CornerRadius {
+ get { return cornerRadius; }
+ set { cornerRadius = value; }
+ }
+
+ /// <summary>
+ /// Gets/Sets whether to align the geometry to whole pixels.
+ /// </summary>
+ public bool AlignToWholePixels { get; set; }
+
+ /// <summary>
+ /// Gets/Sets whether to align the geometry to the middle of pixels.
+ /// </summary>
+ public bool AlignToMiddleOfPixels { get; set; }
+
+ /// <summary>
+ /// Gets/Sets whether to extend the rectangles to full width at line end.
+ /// </summary>
+ public bool ExtendToFullWidthAtLineEnd { get; set; }
+
+ /// <summary>
+ /// Creates a new BackgroundGeometryBuilder instance.
+ /// </summary>
+ public BackgroundGeometryBuilder()
+ {
+ }
+
+ /// <summary>
+ /// Adds the specified segment to the geometry.
+ /// </summary>
+ 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);
+ }
+ }
+
+ /// <summary>
+ /// Adds a rectangle to the geometry.
+ /// </summary>
+ /// <remarks>
+ /// This overload will align the coordinates according to
+ /// <see cref="AlignToWholePixels"/> or <see cref="AlignToMiddleOfPixels"/>.
+ /// Use the <see cref="AddRectangle(double,double,double,double)"/>-overload instead if the coordinates should not be aligned.
+ /// </remarks>
+ public void AddRectangle(TextView textView, Rect rectangle)
+ {
+ AddRectangle(PixelSnapHelpers.GetPixelSize(textView), rectangle);
+ }
+
+ void AddRectangle(Size pixelSize, Rect r)
+ {
+ if (AlignToWholePixels) {
+ AddRectangle(PixelSnapHelpers.Round(r.Left, pixelSize.Width),
+ PixelSnapHelpers.Round(r.Top + 1, pixelSize.Height),
+ PixelSnapHelpers.Round(r.Right, pixelSize.Width),
+ PixelSnapHelpers.Round(r.Bottom + 1, pixelSize.Height));
+ } else if (AlignToMiddleOfPixels) {
+ AddRectangle(PixelSnapHelpers.PixelAlign(r.Left, pixelSize.Width),
+ PixelSnapHelpers.PixelAlign(r.Top + 1, pixelSize.Height),
+ PixelSnapHelpers.PixelAlign(r.Right, pixelSize.Width),
+ PixelSnapHelpers.PixelAlign(r.Bottom + 1, pixelSize.Height));
+ } else {
+ AddRectangle(r.Left, r.Top + 1, r.Right, r.Bottom + 1);
+ }
+ }
+
+ /// <summary>
+ /// 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).
+ /// </summary>
+ public static IEnumerable<Rect> 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);
+ }
+
+ static IEnumerable<Rect> GetRectsForSegmentImpl(TextView textView, ISegment segment, bool extendToFullWidthAtLineEnd)
+ {
+ int segmentStart = segment.Offset;
+ int segmentEnd = segment.Offset + segment.Length;
+
+ segmentStart = segmentStart.CoerceValue(0, textView.Document.TextLength);
+ segmentEnd = segmentEnd.CoerceValue(0, textView.Document.TextLength);
+
+ TextViewPosition start;
+ TextViewPosition end;
+
+ 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), -1);
+ end = new TextViewPosition(textView.Document.GetLocation(segmentEnd), -1);
+ }
+
+ 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;
+
+ int segmentStartVC;
+ if (segmentStart < vlStartOffset)
+ segmentStartVC = 0;
+ else
+ segmentStartVC = vl.ValidateVisualColumn(start, extendToFullWidthAtLineEnd);
+
+ int segmentEndVC;
+ if (segmentEnd > vlEndOffset)
+ segmentEndVC = extendToFullWidthAtLineEnd ? int.MaxValue : vl.VisualLengthWithEndOfLineMarker;
+ else
+ segmentEndVC = vl.ValidateVisualColumn(end, extendToFullWidthAtLineEnd);
+
+ foreach (var rect in ProcessTextLines(textView, vl, segmentStartVC, segmentEndVC))
+ yield return rect;
+ }
+ }
+
+ /// <summary>
+ /// Calculates the rectangles for the visual column segment.
+ /// This returns one rectangle for each line inside the segment.
+ /// </summary>
+ public static IEnumerable<Rect> 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);
+ }
+
+ static IEnumerable<Rect> ProcessTextLines(TextView textView, VisualLine visualLine, int segmentStartVC, int segmentEndVC)
+ {
+ TextLine lastTextLine = visualLine.TextLines.Last();
+ Vector scrollOffset = textView.ScrollOffset;
+
+ 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.Length;
+ if (line != lastTextLine)
+ visualEndCol -= line.TrailingWhitespaceLength;
+
+ 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;
+ 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;
+ yield return new Rect(pos, y, 1, line.Height);
+ } else {
+ Rect lastRect = Rect.Empty;
+ if (segmentStartVCInLine <= visualEndCol) {
+ foreach (TextBounds 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 (segmentEndVC >= visualLine.VisualLengthWithEndOfLineMarker) {
+ double left = (segmentStartVC > visualLine.VisualLengthWithEndOfLineMarker ? visualLine.GetTextLineVisualXPosition(lastTextLine, segmentStartVC) : line.Width) - scrollOffset.X;
+ double right = ((segmentEndVC == int.MaxValue || line != lastTextLine) ? Math.Max(((IScrollInfo)textView).ExtentWidth, ((IScrollInfo)textView).ViewportWidth) : visualLine.GetTextLineVisualXPosition(lastTextLine, segmentEndVC)) - scrollOffset.X;
+ Rect extendSelection = new Rect(Math.Min(left, right), y, Math.Abs(right - left), line.Height);
+ if (!lastRect.IsEmpty) {
+ if (extendSelection.IntersectsWith(lastRect)) {
+ lastRect.Union(extendSelection);
+ yield return lastRect;
+ } else {
+ yield return lastRect;
+ yield return extendSelection;
+ }
+ } else
+ yield return extendSelection;
+ } else
+ yield return lastRect;
+ }
+ }
+ }
+
+ PathFigureCollection figures = new PathFigureCollection();
+ PathFigure figure;
+ int insertionIndex;
+ double lastTop, lastBottom;
+ double lastLeft, lastRight;
+
+ /// <summary>
+ /// Adds a rectangle to the geometry.
+ /// </summary>
+ /// <remarks>
+ /// This overload assumes that the coordinates are aligned properly
+ /// (see <see cref="AlignToWholePixels"/>, <see cref="AlignToMiddleOfPixels"/>).
+ /// Use the <see cref="AddRectangle(TextView,Rect)"/>-overload instead if the coordinates are not yet aligned.
+ /// </remarks>
+ 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;
+ }
+
+ ArcSegment MakeArc(double x, double y, SweepDirection dir)
+ {
+ ArcSegment arc = new ArcSegment(
+ new Point(x, y),
+ new Size(cornerRadius, cornerRadius),
+ 0, false, dir, true);
+ arc.Freeze();
+ return arc;
+ }
+
+ static LineSegment MakeLineSegment(double x, double y)
+ {
+ LineSegment ls = new LineSegment(new Point(x, y), true);
+ ls.Freeze();
+ return ls;
+ }
+
+ /// <summary>
+ /// Closes the current figure.
+ /// </summary>
+ 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));
+ }
+
+ figure.IsClosed = true;
+ figures.Add(figure);
+ figure = null;
+ }
+ }
+
+ /// <summary>
+ /// Creates the geometry.
+ /// Returns null when the geometry is empty!
+ /// </summary>
+ public Geometry CreateGeometry()
+ {
+ CloseFigure();
+ if (figures.Count != 0) {
+ PathGeometry g = new PathGeometry(figures);
+ g.Freeze();
+ return g;
+ } else {
+ return null;
+ }
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/CollapsedLineSection.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/CollapsedLineSection.cs
new file mode 100644
index 000000000..bcbf55798
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/CollapsedLineSection.cs
@@ -0,0 +1,95 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.ComponentModel;
+using Tango.Scripting.Editors.Document;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// Represents a collapsed line section.
+ /// Use the Uncollapse() method to uncollapse the section.
+ /// </summary>
+ public sealed class CollapsedLineSection
+ {
+ DocumentLine start, end;
+ HeightTree heightTree;
+
+ #if DEBUG
+ internal string ID;
+ static int nextId;
+ #else
+ const string ID = "";
+ #endif
+
+ internal CollapsedLineSection(HeightTree heightTree, DocumentLine start, DocumentLine end)
+ {
+ this.heightTree = heightTree;
+ this.start = start;
+ this.end = end;
+ #if DEBUG
+ unchecked {
+ this.ID = " #" + (nextId++);
+ }
+ #endif
+ }
+
+ /// <summary>
+ /// Gets if the document line is collapsed.
+ /// This property initially is true and turns to false when uncollapsing the section.
+ /// </summary>
+ public bool IsCollapsed {
+ get { return start != null; }
+ }
+
+ /// <summary>
+ /// Gets the start line of the section.
+ /// When the section is uncollapsed or the text containing it is deleted,
+ /// this property returns null.
+ /// </summary>
+ public DocumentLine Start {
+ get { return start; }
+ internal set { start = value; }
+ }
+
+ /// <summary>
+ /// Gets the end line of the section.
+ /// When the section is uncollapsed or the text containing it is deleted,
+ /// this property returns null.
+ /// </summary>
+ public DocumentLine End {
+ get { return end; }
+ internal set { end = value; }
+ }
+
+ /// <summary>
+ /// Uncollapses the section.
+ /// This causes the Start and End properties to be set to null!
+ /// Does nothing if the section is already uncollapsed.
+ /// </summary>
+ public void Uncollapse()
+ {
+ if (start == null)
+ return;
+
+ heightTree.Uncollapse(this);
+ #if DEBUG
+ heightTree.CheckProperties();
+ #endif
+
+ start = null;
+ end = null;
+ }
+
+ /// <summary>
+ /// Gets a string representation of the collapsed section.
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.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") + "]";
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/ColorizingTransformer.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/ColorizingTransformer.cs
new file mode 100644
index 000000000..97e2c0cea
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/ColorizingTransformer.cs
@@ -0,0 +1,108 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Collections.Generic;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// Base class for <see cref="IVisualLineTransformer"/> that helps
+ /// splitting visual elements so that colors (and other text properties) can be easily assigned
+ /// to individual words/characters.
+ /// </summary>
+ public abstract class ColorizingTransformer : IVisualLineTransformer, ITextViewConnect
+ {
+ /// <summary>
+ /// Gets the list of elements currently being transformed.
+ /// </summary>
+ protected IList<VisualLineElement> CurrentElements { get; private set; }
+
+ /// <summary>
+ /// <see cref="IVisualLineTransformer.Transform"/> implementation.
+ /// Sets <see cref="CurrentElements"/> and calls <see cref="Colorize"/>.
+ /// </summary>
+ public void Transform(ITextRunConstructionContext context, IList<VisualLineElement> elements)
+ {
+ if (elements == null)
+ throw new ArgumentNullException("elements");
+ if (this.CurrentElements != null)
+ throw new InvalidOperationException("Recursive Transform() call");
+ this.CurrentElements = elements;
+ try {
+ Colorize(context);
+ } finally {
+ this.CurrentElements = null;
+ }
+ }
+
+ /// <summary>
+ /// Performs the colorization.
+ /// </summary>
+ protected abstract void Colorize(ITextRunConstructionContext context);
+
+ /// <summary>
+ /// Changes visual element properties.
+ /// This method accesses <see cref="CurrentElements"/>, so it must be called only during
+ /// a <see cref="Transform"/> call.
+ /// This method splits <see cref="VisualLineElement"/>s as necessary to ensure that the region
+ /// can be colored by setting the <see cref="VisualLineElement.TextRunProperties"/> of whole elements,
+ /// and then calls the <paramref name="action"/> on all elements in the region.
+ /// </summary>
+ /// <param name="visualStartColumn">Start visual column of the region to change</param>
+ /// <param name="visualEndColumn">End visual column of the region to change</param>
+ /// <param name="action">Action that changes an individual <see cref="VisualLineElement"/>.</param>
+ protected void ChangeVisualElements(int visualStartColumn, int visualEndColumn, Action<VisualLineElement> action)
+ {
+ if (action == null)
+ throw new ArgumentNullException("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--);
+ continue;
+ }
+ } else {
+ action(e);
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Called when added to a text view.
+ /// </summary>
+ protected virtual void OnAddToTextView(TextView textView)
+ {
+ }
+
+ /// <summary>
+ /// Called when removed from a text view.
+ /// </summary>
+ protected virtual void OnRemoveFromTextView(TextView textView)
+ {
+ }
+
+ void ITextViewConnect.AddToTextView(TextView textView)
+ {
+ OnAddToTextView(textView);
+ }
+
+ void ITextViewConnect.RemoveFromTextView(TextView textView)
+ {
+ OnRemoveFromTextView(textView);
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/ColumnRulerRenderer.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/ColumnRulerRenderer.cs
new file mode 100644
index 000000000..62a29dcb1
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/ColumnRulerRenderer.cs
@@ -0,0 +1,64 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Windows;
+using System.Windows.Media;
+
+using Tango.Scripting.Editors.Rendering;
+using Tango.Scripting.Editors.Utils;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// Renders a ruler at a certain column.
+ /// </summary>
+ sealed class ColumnRulerRenderer : IBackgroundRenderer
+ {
+ Pen pen;
+ int column;
+ TextView textView;
+
+ public static readonly Color DefaultForeground = Colors.LightGray;
+
+ public ColumnRulerRenderer(TextView textView)
+ {
+ if (textView == null)
+ throw new ArgumentNullException("textView");
+
+ this.pen = new Pen(new SolidColorBrush(DefaultForeground), 1);
+ this.pen.Freeze();
+ this.textView = textView;
+ this.textView.BackgroundRenderers.Add(this);
+ }
+
+ public KnownLayer Layer {
+ get { return KnownLayer.Background; }
+ }
+
+ public void SetRuler(int column, Pen pen)
+ {
+ if (this.column != column) {
+ this.column = column;
+ textView.InvalidateLayer(this.Layer);
+ }
+ if (this.pen != pen) {
+ this.pen = pen;
+ textView.InvalidateLayer(this.Layer);
+ }
+ }
+
+ public void Draw(TextView textView, System.Windows.Media.DrawingContext drawingContext)
+ {
+ if (column < 1) return;
+ double offset = textView.WideSpaceWidth * column;
+ Size pixelSize = PixelSnapHelpers.GetPixelSize(textView);
+ double 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.ActualHeight));
+
+ drawingContext.DrawLine(pen, start, end);
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/DefaultTextRunTypographyProperties.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/DefaultTextRunTypographyProperties.cs
new file mode 100644
index 000000000..2ccf07e3e
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/DefaultTextRunTypographyProperties.cs
@@ -0,0 +1,171 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Windows;
+using System.Windows.Media.TextFormatting;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// Default implementation for TextRunTypographyProperties.
+ /// </summary>
+ public class DefaultTextRunTypographyProperties : TextRunTypographyProperties
+ {
+ /// <inheritdoc/>
+ public override FontVariants Variants {
+ get { return FontVariants.Normal; }
+ }
+
+ /// <inheritdoc/>
+ public override bool StylisticSet1 { get { return false; } }
+ /// <inheritdoc/>
+ public override bool StylisticSet2 { get { return false; } }
+ /// <inheritdoc/>
+ public override bool StylisticSet3 { get { return false; } }
+ /// <inheritdoc/>
+ public override bool StylisticSet4 { get { return false; } }
+ /// <inheritdoc/>
+ public override bool StylisticSet5 { get { return false; } }
+ /// <inheritdoc/>
+ public override bool StylisticSet6 { get { return false; } }
+ /// <inheritdoc/>
+ public override bool StylisticSet7 { get { return false; } }
+ /// <inheritdoc/>
+ public override bool StylisticSet8 { get { return false; } }
+ /// <inheritdoc/>
+ public override bool StylisticSet9 { get { return false; } }
+ /// <inheritdoc/>
+ public override bool StylisticSet10 { get { return false; } }
+ /// <inheritdoc/>
+ public override bool StylisticSet11 { get { return false; } }
+ /// <inheritdoc/>
+ public override bool StylisticSet12 { get { return false; } }
+ /// <inheritdoc/>
+ public override bool StylisticSet13 { get { return false; } }
+ /// <inheritdoc/>
+ public override bool StylisticSet14 { get { return false; } }
+ /// <inheritdoc/>
+ public override bool StylisticSet15 { get { return false; } }
+ /// <inheritdoc/>
+ public override bool StylisticSet16 { get { return false; } }
+ /// <inheritdoc/>
+ public override bool StylisticSet17 { get { return false; } }
+ /// <inheritdoc/>
+ public override bool StylisticSet18 { get { return false; } }
+ /// <inheritdoc/>
+ public override bool StylisticSet19 { get { return false; } }
+ /// <inheritdoc/>
+ public override bool StylisticSet20 { get { return false; } }
+
+ /// <inheritdoc/>
+ public override int StylisticAlternates {
+ get { return 0; }
+ }
+
+ /// <inheritdoc/>
+ public override int StandardSwashes {
+ get { return 0; }
+ }
+
+ /// <inheritdoc/>
+ public override bool StandardLigatures {
+ get { return true; }
+ }
+
+ /// <inheritdoc/>
+ public override bool SlashedZero {
+ get { return false; }
+ }
+
+ /// <inheritdoc/>
+ public override FontNumeralStyle NumeralStyle {
+ get { return FontNumeralStyle.Normal; }
+ }
+
+ /// <inheritdoc/>
+ public override FontNumeralAlignment NumeralAlignment {
+ get { return FontNumeralAlignment.Normal; }
+ }
+
+ /// <inheritdoc/>
+ public override bool MathematicalGreek {
+ get { return false; }
+ }
+
+ /// <inheritdoc/>
+ public override bool Kerning {
+ get { return true; }
+ }
+
+ /// <inheritdoc/>
+ public override bool HistoricalLigatures {
+ get { return false; }
+ }
+
+ /// <inheritdoc/>
+ public override bool HistoricalForms {
+ get { return false; }
+ }
+
+ /// <inheritdoc/>
+ public override FontFraction Fraction {
+ get { return FontFraction.Normal; }
+ }
+
+ /// <inheritdoc/>
+ public override FontEastAsianWidths EastAsianWidths {
+ get { return FontEastAsianWidths.Normal; }
+ }
+
+ /// <inheritdoc/>
+ public override FontEastAsianLanguage EastAsianLanguage {
+ get { return FontEastAsianLanguage.Normal; }
+ }
+
+ /// <inheritdoc/>
+ public override bool EastAsianExpertForms {
+ get { return false; }
+ }
+
+ /// <inheritdoc/>
+ public override bool DiscretionaryLigatures {
+ get { return false; }
+ }
+
+ /// <inheritdoc/>
+ public override int ContextualSwashes {
+ get { return 0; }
+ }
+
+ /// <inheritdoc/>
+ public override bool ContextualLigatures {
+ get { return true; }
+ }
+
+ /// <inheritdoc/>
+ public override bool ContextualAlternates {
+ get { return true; }
+ }
+
+ /// <inheritdoc/>
+ public override bool CaseSensitiveForms {
+ get { return false; }
+ }
+
+ /// <inheritdoc/>
+ public override bool CapitalSpacing {
+ get { return false; }
+ }
+
+ /// <inheritdoc/>
+ public override FontCapitals Capitals {
+ get { return FontCapitals.Normal; }
+ }
+
+ /// <inheritdoc/>
+ public override int AnnotationAlternates {
+ get { return 0; }
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/DocumentColorizingTransformer.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/DocumentColorizingTransformer.cs
new file mode 100644
index 000000000..d4032d64d
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/DocumentColorizingTransformer.cs
@@ -0,0 +1,81 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Linq;
+using Tango.Scripting.Editors.Document;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// Base class for <see cref="IVisualLineTransformer"/> 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.
+ /// </summary>
+ public abstract class DocumentColorizingTransformer : ColorizingTransformer
+ {
+ DocumentLine currentDocumentLine;
+ int firstLineStart;
+ int currentDocumentLineStartOffset, currentDocumentLineEndOffset;
+
+ /// <summary>
+ /// Gets the current ITextRunConstructionContext.
+ /// </summary>
+ protected ITextRunConstructionContext CurrentContext { get; private set; }
+
+ /// <inheritdoc/>
+ protected override void Colorize(ITextRunConstructionContext context)
+ {
+ if (context == null)
+ throw new ArgumentNullException("context");
+ this.CurrentContext = context;
+
+ currentDocumentLine = context.VisualLine.FirstDocumentLine;
+ firstLineStart = currentDocumentLineStartOffset = currentDocumentLine.Offset;
+ currentDocumentLineEndOffset = currentDocumentLineStartOffset + currentDocumentLine.Length;
+
+ 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 (VisualLineElement e in context.VisualLine.Elements.ToArray()) {
+ int elementOffset = firstLineStart + e.RelativeTextOffset;
+ if (elementOffset >= currentDocumentLineEndOffset) {
+ currentDocumentLine = context.Document.GetLineByOffset(elementOffset);
+ currentDocumentLineStartOffset = currentDocumentLine.Offset;
+ currentDocumentLineEndOffset = currentDocumentLineStartOffset + currentDocumentLine.Length;
+ ColorizeLine(currentDocumentLine);
+ }
+ }
+ }
+ currentDocumentLine = null;
+ this.CurrentContext = null;
+ }
+
+ /// <summary>
+ /// Override this method to colorize an individual document line.
+ /// </summary>
+ protected abstract void ColorizeLine(DocumentLine line);
+
+ /// <summary>
+ /// Changes a part of the current document line.
+ /// </summary>
+ /// <param name="startOffset">Start offset of the region to change</param>
+ /// <param name="endOffset">End offset of the region to change</param>
+ /// <param name="action">Action that changes an individual <see cref="VisualLineElement"/>.</param>
+ protected void ChangeLinePart(int startOffset, int endOffset, Action<VisualLineElement> action)
+ {
+ if (startOffset < currentDocumentLineStartOffset || startOffset > currentDocumentLineEndOffset)
+ throw new ArgumentOutOfRangeException("startOffset", startOffset, "Value must be between " + currentDocumentLineStartOffset + " and " + currentDocumentLineEndOffset);
+ if (endOffset < startOffset || endOffset > currentDocumentLineEndOffset)
+ throw new ArgumentOutOfRangeException("endOffset", endOffset, "Value must be between " + startOffset + " and " + currentDocumentLineEndOffset);
+ VisualLine vl = this.CurrentContext.VisualLine;
+ int visualStart = vl.GetVisualColumn(startOffset - firstLineStart);
+ int visualEnd = vl.GetVisualColumn(endOffset - firstLineStart);
+ if (visualStart < visualEnd) {
+ ChangeVisualElements(visualStart, visualEnd, action);
+ }
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/FormattedTextElement.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/FormattedTextElement.cs
new file mode 100644
index 000000000..168dd170d
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/FormattedTextElement.cs
@@ -0,0 +1,207 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Media.TextFormatting;
+using Tango.Scripting.Editors.Utils;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// 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.
+ /// </summary>
+ public class FormattedTextElement : VisualLineElement
+ {
+ internal readonly FormattedText formattedText;
+ internal string text;
+ internal TextLine textLine;
+
+ /// <summary>
+ /// Creates a new FormattedTextElement that displays the specified text
+ /// and occupies the specified length in the document.
+ /// </summary>
+ public FormattedTextElement(string text, int documentLength) : base(1, documentLength)
+ {
+ if (text == null)
+ throw new ArgumentNullException("text");
+ this.text = text;
+ this.BreakBefore = LineBreakCondition.BreakPossible;
+ this.BreakAfter = LineBreakCondition.BreakPossible;
+ }
+
+ /// <summary>
+ /// Creates a new FormattedTextElement that displays the specified text
+ /// and occupies the specified length in the document.
+ /// </summary>
+ public FormattedTextElement(TextLine text, int documentLength) : base(1, documentLength)
+ {
+ if (text == null)
+ throw new ArgumentNullException("text");
+ this.textLine = text;
+ this.BreakBefore = LineBreakCondition.BreakPossible;
+ this.BreakAfter = LineBreakCondition.BreakPossible;
+ }
+
+ /// <summary>
+ /// Creates a new FormattedTextElement that displays the specified text
+ /// and occupies the specified length in the document.
+ /// </summary>
+ public FormattedTextElement(FormattedText text, int documentLength) : base(1, documentLength)
+ {
+ if (text == null)
+ throw new ArgumentNullException("text");
+ this.formattedText = text;
+ this.BreakBefore = LineBreakCondition.BreakPossible;
+ this.BreakAfter = LineBreakCondition.BreakPossible;
+ }
+
+ /// <summary>
+ /// Gets/sets the line break condition before the element.
+ /// The default is 'BreakPossible'.
+ /// </summary>
+ public LineBreakCondition BreakBefore { get; set; }
+
+ /// <summary>
+ /// Gets/sets the line break condition after the element.
+ /// The default is 'BreakPossible'.
+ /// </summary>
+ public LineBreakCondition BreakAfter { get; set; }
+
+ /// <inheritdoc/>
+ public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context)
+ {
+ if (textLine == null) {
+ var formatter = TextFormatterFactory.Create(context.TextView);
+ textLine = PrepareText(formatter, this.text, this.TextRunProperties);
+ this.text = null;
+ }
+ return new FormattedTextRun(this, this.TextRunProperties);
+ }
+
+ /// <summary>
+ /// Constructs a TextLine from a simple text.
+ /// </summary>
+ public static TextLine PrepareText(TextFormatter formatter, string text, TextRunProperties properties)
+ {
+ if (formatter == null)
+ throw new ArgumentNullException("formatter");
+ if (text == null)
+ throw new ArgumentNullException("text");
+ if (properties == null)
+ throw new ArgumentNullException("properties");
+ return formatter.FormatLine(
+ new SimpleTextSource(text, properties),
+ 0,
+ 32000,
+ new VisualLineTextParagraphProperties {
+ defaultTextRunProperties = properties,
+ textWrapping = TextWrapping.NoWrap,
+ tabSize = 40
+ },
+ null);
+ }
+ }
+
+ /// <summary>
+ /// This is the TextRun implementation used by the <see cref="FormattedTextElement"/> class.
+ /// </summary>
+ public class FormattedTextRun : TextEmbeddedObject
+ {
+ readonly FormattedTextElement element;
+ TextRunProperties properties;
+
+ /// <summary>
+ /// Creates a new FormattedTextRun.
+ /// </summary>
+ public FormattedTextRun(FormattedTextElement element, TextRunProperties properties)
+ {
+ if (element == null)
+ throw new ArgumentNullException("element");
+ if (properties == null)
+ throw new ArgumentNullException("properties");
+ this.properties = properties;
+ this.element = element;
+ }
+
+ /// <summary>
+ /// Gets the element for which the FormattedTextRun was created.
+ /// </summary>
+ public FormattedTextElement Element {
+ get { return element; }
+ }
+
+ /// <inheritdoc/>
+ public override LineBreakCondition BreakBefore {
+ get { return element.BreakBefore; }
+ }
+
+ /// <inheritdoc/>
+ public override LineBreakCondition BreakAfter {
+ get { return element.BreakAfter; }
+ }
+
+ /// <inheritdoc/>
+ public override bool HasFixedSize {
+ get { return true; }
+ }
+
+ /// <inheritdoc/>
+ public override CharacterBufferReference CharacterBufferReference {
+ get { return new CharacterBufferReference(); }
+ }
+
+ /// <inheritdoc/>
+ public override int Length {
+ get { return element.VisualLength; }
+ }
+
+ /// <inheritdoc/>
+ public override TextRunProperties Properties {
+ get { return properties; }
+ }
+
+ /// <inheritdoc/>
+ public override TextEmbeddedObjectMetrics Format(double remainingParagraphWidth)
+ {
+ var formattedText = element.formattedText;
+ if (formattedText != null) {
+ return new TextEmbeddedObjectMetrics(formattedText.WidthIncludingTrailingWhitespace,
+ formattedText.Height,
+ formattedText.Baseline);
+ } else {
+ var text = element.textLine;
+ return new TextEmbeddedObjectMetrics(text.WidthIncludingTrailingWhitespace,
+ text.Height,
+ text.Baseline);
+ }
+ }
+
+ /// <inheritdoc/>
+ public override Rect ComputeBoundingBox(bool rightToLeft, bool sideways)
+ {
+ var formattedText = element.formattedText;
+ if (formattedText != null) {
+ return new Rect(0, 0, formattedText.WidthIncludingTrailingWhitespace, formattedText.Height);
+ } else {
+ var text = element.textLine;
+ return new Rect(0, 0, text.WidthIncludingTrailingWhitespace, text.Height);
+ }
+ }
+
+ /// <inheritdoc/>
+ public override void Draw(DrawingContext drawingContext, Point origin, bool rightToLeft, bool sideways)
+ {
+ if (element.formattedText != null) {
+ origin.Y -= element.formattedText.Baseline;
+ drawingContext.DrawText(element.formattedText, origin);
+ } else {
+ origin.Y -= element.textLine.Baseline;
+ element.textLine.Draw(drawingContext, origin, InvertAxes.None);
+ }
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/GlobalTextRunProperties.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/GlobalTextRunProperties.cs
new file mode 100644
index 000000000..f0f40c882
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/GlobalTextRunProperties.cs
@@ -0,0 +1,30 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Media.TextFormatting;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ sealed class GlobalTextRunProperties : TextRunProperties
+ {
+ internal Typeface typeface;
+ internal double fontRenderingEmSize;
+ internal Brush foregroundBrush;
+ private Brush backgroundBrush1;
+ internal System.Globalization.CultureInfo cultureInfo;
+
+ public override Typeface Typeface { get { return typeface; } }
+ public override double FontRenderingEmSize { get { return fontRenderingEmSize; } }
+ public override double FontHintingEmSize { get { return fontRenderingEmSize; } }
+ public override TextDecorationCollection TextDecorations { get { return null; } }
+ public override Brush ForegroundBrush { get { return foregroundBrush; } }
+ public override Brush BackgroundBrush { get { return BackgroundBrush1; } }
+ public override System.Globalization.CultureInfo CultureInfo { get { return cultureInfo; } }
+ public override TextEffectCollection TextEffects { get { return null; } }
+
+ internal Brush BackgroundBrush1 { get => backgroundBrush1; set => backgroundBrush1 = value; }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/HeightTree.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/HeightTree.cs
new file mode 100644
index 000000000..6751af8fa
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/HeightTree.cs
@@ -0,0 +1,1092 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+
+using Tango.Scripting.Editors.Document;
+using Tango.Scripting.Editors.Utils;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// Red-black tree similar to DocumentLineTree, augmented with collapsing and height data.
+ /// </summary>
+ sealed class HeightTree : ILineTracker, IDisposable
+ {
+ // TODO: Optimize this. This tree takes alot of memory.
+ // (56 bytes for HeightTreeNode
+ // We should try to get rid of the dictionary and find height nodes per index. (DONE!)
+ // And we might do much better by compressing lines with the same height into a single node.
+ // That would also improve load times because we would always start with just a single node.
+
+ /* Idea:
+ class NewHeightTreeNode {
+ int totalCount; // =count+left.count+right.count
+ int count; // one node can represent multiple lines
+ double height; // height of each line in this node
+ double totalHeight; // =(collapsedSections!=null?0:height*count) + left.totalHeight + right.totalHeight
+ List<CollapsedSection> collapsedSections; // sections holding this line collapsed
+ // no "nodeCollapsedSections"/"totalCollapsedSections":
+ NewHeightTreeNode left, right, parent;
+ bool color;
+ }
+ totalCollapsedSections: are hard to update and not worth the effort. O(n log n) isn't too bad for
+ collapsing/uncollapsing, especially when compression reduces the n.
+ */
+
+ #region Constructor
+ readonly TextDocument document;
+ HeightTreeNode root;
+ WeakLineTracker weakLineTracker;
+
+ public HeightTree(TextDocument document, double defaultLineHeight)
+ {
+ this.document = document;
+ weakLineTracker = WeakLineTracker.Register(document, this);
+ this.DefaultLineHeight = defaultLineHeight;
+ RebuildDocument();
+ }
+
+ public void Dispose()
+ {
+ if (weakLineTracker != null)
+ weakLineTracker.Deregister();
+ this.root = null;
+ this.weakLineTracker = null;
+ }
+
+ double defaultLineHeight;
+
+ public double DefaultLineHeight {
+ get { return defaultLineHeight; }
+ set {
+ double 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);
+ }
+ }
+ }
+ }
+
+ HeightTreeNode GetNode(DocumentLine ls)
+ {
+ return GetNodeByIndex(ls.LineNumber - 1);
+ }
+ #endregion
+
+ #region RebuildDocument
+ void ILineTracker.SetLineLength(DocumentLine ls, int newTotalLength)
+ {
+ }
+
+ /// <summary>
+ /// Rebuild the tree, in O(n).
+ /// </summary>
+ public void RebuildDocument()
+ {
+ foreach (CollapsedLineSection s in GetAllCollapsedSections()) {
+ s.Start = null;
+ s.End = null;
+ }
+
+ HeightTreeNode[] nodes = new HeightTreeNode[document.LineCount];
+ int lineNumber = 0;
+ foreach (DocumentLine ls in document.Lines) {
+ nodes[lineNumber++] = new HeightTreeNode(ls, defaultLineHeight);
+ }
+ Debug.Assert(nodes.Length > 0);
+ // now build the corresponding balanced tree
+ int 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();
+ #endif
+ }
+
+ /// <summary>
+ /// build a tree from a list of nodes
+ /// </summary>
+ HeightTreeNode BuildTree(HeightTreeNode[] nodes, int start, int end, int subtreeHeight)
+ {
+ Debug.Assert(start <= end);
+ if (start == end) {
+ return null;
+ }
+ int middle = (start + end) / 2;
+ HeightTreeNode 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)
+ {
+ HeightTreeNode node = GetNode(line);
+ if (node.lineNode.collapsedSections != null) {
+ foreach (CollapsedLineSection 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();
+ #endif
+ }
+
+ HeightTreeNode InsertAfter(HeightTreeNode node, DocumentLine newLine)
+ {
+ HeightTreeNode 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 (CollapsedLineSection 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 (CollapsedLineSection cs in node.lineNode.collapsedSections) {
+ if (cs.Start != node.documentLine)
+ newNode.AddDirectlyCollapsed(cs);
+ }
+ }
+ InsertAsLeft(node, newNode);
+ }
+ return newNode;
+ }
+ #endregion
+
+ #region Rotation callbacks
+ enum UpdateAfterChildrenChangeRecursionMode
+ {
+ None,
+ IfRequired,
+ WholeBranch
+ }
+
+ static void UpdateAfterChildrenChange(HeightTreeNode node)
+ {
+ UpdateAugmentedData(node, UpdateAfterChildrenChangeRecursionMode.IfRequired);
+ }
+
+ static void UpdateAugmentedData(HeightTreeNode node, UpdateAfterChildrenChangeRecursionMode mode)
+ {
+ int totalCount = 1;
+ double 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);
+ }
+ }
+
+ 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 (CollapsedLineSection cs in collapsedP) {
+ if (node.parent.right != null)
+ node.parent.right.AddDirectlyCollapsed(cs);
+ node.parent.lineNode.AddDirectlyCollapsed(cs);
+ if (node.right != null)
+ node.right.AddDirectlyCollapsed(cs);
+ }
+ }
+ MergeCollapsedSectionsIfPossible(node);
+
+ UpdateAfterChildrenChange(node);
+
+ // not required: rotations only happen on insertions/deletions
+ // -> totalCount changes -> the parent is always updated
+ //UpdateAfterChildrenChange(node.parent);
+ }
+
+ 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 (CollapsedLineSection cs in collapsedP) {
+ if (node.parent.left != null)
+ node.parent.left.AddDirectlyCollapsed(cs);
+ node.parent.lineNode.AddDirectlyCollapsed(cs);
+ if (node.left != null)
+ node.left.AddDirectlyCollapsed(cs);
+ }
+ }
+ MergeCollapsedSectionsIfPossible(node);
+
+ UpdateAfterChildrenChange(node);
+
+ // not required: rotations only happen on insertions/deletions
+ // -> totalCount changes -> the parent is always updated
+ //UpdateAfterChildrenChange(node.parent);
+ }
+
+ // node removal:
+ // a node in the middle of the tree is removed as following:
+ // its successor is removed
+ // it is replaced with its successor
+
+ void BeforeNodeRemove(HeightTreeNode removedNode)
+ {
+ Debug.Assert(removedNode.left == null || removedNode.right == null);
+
+ var collapsed = removedNode.collapsedSections;
+ if (collapsed != null) {
+ HeightTreeNode childNode = removedNode.left ?? removedNode.right;
+ if (childNode != null) {
+ foreach (CollapsedLineSection cs in collapsed)
+ childNode.AddDirectlyCollapsed(cs);
+ }
+ }
+ if (removedNode.parent != null)
+ MergeCollapsedSectionsIfPossible(removedNode.parent);
+ }
+
+ void BeforeNodeReplace(HeightTreeNode removedNode, HeightTreeNode newNode, HeightTreeNode newNodeOldParent)
+ {
+ Debug.Assert(removedNode != null);
+ Debug.Assert(newNode != null);
+ while (newNodeOldParent != removedNode) {
+ if (newNodeOldParent.collapsedSections != null) {
+ foreach (CollapsedLineSection cs in newNodeOldParent.collapsedSections) {
+ newNode.lineNode.AddDirectlyCollapsed(cs);
+ }
+ }
+ newNodeOldParent = newNodeOldParent.parent;
+ }
+ if (newNode.collapsedSections != null) {
+ foreach (CollapsedLineSection cs in newNode.collapsedSections) {
+ newNode.lineNode.AddDirectlyCollapsed(cs);
+ }
+ }
+ newNode.collapsedSections = removedNode.collapsedSections;
+ MergeCollapsedSectionsIfPossible(newNode);
+ }
+
+ bool inRemoval;
+ List<HeightTreeNode> nodesToCheckForMerging;
+
+ void BeginRemoval()
+ {
+ Debug.Assert(!inRemoval);
+ if (nodesToCheckForMerging == null) {
+ nodesToCheckForMerging = new List<HeightTreeNode>();
+ }
+ inRemoval = true;
+ }
+
+ void EndRemoval()
+ {
+ Debug.Assert(inRemoval);
+ inRemoval = false;
+ foreach (HeightTreeNode node in nodesToCheckForMerging) {
+ MergeCollapsedSectionsIfPossible(node);
+ }
+ nodesToCheckForMerging.Clear();
+ }
+
+ void MergeCollapsedSectionsIfPossible(HeightTreeNode node)
+ {
+ Debug.Assert(node != null);
+ if (inRemoval) {
+ nodesToCheckForMerging.Add(node);
+ return;
+ }
+ // now check if we need to merge collapsedSections together
+ bool merged = false;
+ var collapsedL = node.lineNode.collapsedSections;
+ if (collapsedL != null) {
+ for (int i = collapsedL.Count - 1; i >= 0; i--) {
+ CollapsedLineSection 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
+ HeightTreeNode GetNodeByIndex(int index)
+ {
+ Debug.Assert(index >= 0);
+ Debug.Assert(index < root.totalCount);
+ HeightTreeNode 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;
+ }
+ }
+ }
+
+ HeightTreeNode GetNodeByVisualPosition(double position)
+ {
+ HeightTreeNode node = root;
+ while (true) {
+ double positionAfterLeft = position;
+ if (node.left != null) {
+ positionAfterLeft -= node.left.totalHeight;
+ if (positionAfterLeft < 0) {
+ // Descend into left
+ node = node.left;
+ continue;
+ }
+ }
+ double positionBeforeRight = positionAfterLeft - node.lineNode.TotalHeight;
+ if (positionBeforeRight < 0) {
+ // Found the correct node
+ return node;
+ }
+ if (node.right == null || node.right.totalHeight == 0) {
+ // Can happen when position>node.totalHeight,
+ // i.e. at the end of the document, or due to rounding errors in previous loop iterations.
+
+ // If node.lineNode isn't collapsed, return that.
+ // Also return node.lineNode if there is no previous node that we could return instead.
+ if (node.lineNode.TotalHeight > 0 || node.left == null)
+ return node;
+ // Otherwise, descend into left (find the last non-collapsed node)
+ node = node.left;
+ } else {
+ // Descend into right
+ position = positionBeforeRight;
+ node = node.right;
+ }
+ }
+ }
+
+ static double GetVisualPositionFromNode(HeightTreeNode node)
+ {
+ double position = (node.left != null) ? node.left.totalHeight : 0;
+ while (node.parent != null) {
+ if (node.IsDirectlyCollapsed)
+ position = 0;
+ if (node == node.parent.right) {
+ if (node.parent.left != null)
+ position += node.parent.left.totalHeight;
+ position += node.parent.lineNode.TotalHeight;
+ }
+ node = node.parent;
+ }
+ return position;
+ }
+ #endregion
+
+ #region Public methods
+ public DocumentLine GetLineByNumber(int number)
+ {
+ return GetNodeByIndex(number - 1).documentLine;
+ }
+
+ public DocumentLine GetLineByVisualPosition(double position)
+ {
+ return GetNodeByVisualPosition(position).documentLine;
+ }
+
+ public double GetVisualPosition(DocumentLine line)
+ {
+ return GetVisualPositionFromNode(GetNode(line));
+ }
+
+ public double GetHeight(DocumentLine line)
+ {
+ return GetNode(line).lineNode.height;
+ }
+
+ public void SetHeight(DocumentLine line, double val)
+ {
+ var node = GetNode(line);
+ node.lineNode.height = val;
+ UpdateAfterChildrenChange(node);
+ }
+
+ public bool GetIsCollapsed(int lineNumber)
+ {
+ var node = GetNodeByIndex(lineNumber - 1);
+ return node.lineNode.IsDirectlyCollapsed || GetIsCollapedFromNode(node);
+ }
+
+ /// <summary>
+ /// Collapses the specified text section.
+ /// Runtime: O(log n)
+ /// </summary>
+ public CollapsedLineSection CollapseText(DocumentLine start, DocumentLine end)
+ {
+ if (!document.Lines.Contains(start))
+ throw new ArgumentException("Line is not part of this document", "start");
+ if (!document.Lines.Contains(end))
+ throw new ArgumentException("Line is not part of this document", "end");
+ int length = end.LineNumber - start.LineNumber + 1;
+ if (length < 0)
+ throw new ArgumentException("start must be a line before end");
+ CollapsedLineSection section = new CollapsedLineSection(this, start, end);
+ AddCollapsedSection(section, length);
+ #if DEBUG
+ CheckProperties();
+ #endif
+ return section;
+ }
+ #endregion
+
+ #region LineCount & TotalHeight
+ public int LineCount {
+ get {
+ return root.totalCount;
+ }
+ }
+
+ public double TotalHeight {
+ get {
+ return root.totalHeight;
+ }
+ }
+ #endregion
+
+ #region GetAllCollapsedSections
+ IEnumerable<HeightTreeNode> AllNodes {
+ get {
+ if (root != null) {
+ HeightTreeNode node = root.LeftMost;
+ while (node != null) {
+ yield return node;
+ node = node.Successor;
+ }
+ }
+ }
+ }
+
+ internal IEnumerable<CollapsedLineSection> GetAllCollapsedSections()
+ {
+ List<CollapsedLineSection> emptyCSList = new List<CollapsedLineSection>();
+ return System.Linq.Enumerable.Distinct(
+ System.Linq.Enumerable.SelectMany(
+ AllNodes, node => System.Linq.Enumerable.Concat(node.lineNode.collapsedSections ?? emptyCSList,
+ node.collapsedSections ?? emptyCSList)
+ ));
+ }
+ #endregion
+
+ #region CheckProperties
+ #if DEBUG
+ [Conditional("DATACONSISTENCYTEST")]
+ internal void CheckProperties()
+ {
+ CheckProperties(root);
+
+ foreach (CollapsedLineSection cs in GetAllCollapsedSections()) {
+ Debug.Assert(GetNode(cs.Start).lineNode.collapsedSections.Contains(cs));
+ Debug.Assert(GetNode(cs.End).lineNode.collapsedSections.Contains(cs));
+ int endLine = cs.End.LineNumber;
+ for (int i = cs.Start.LineNumber; i <= endLine; i++) {
+ CheckIsInSection(cs, GetLineByNumber(i));
+ }
+ }
+
+ // check red-black property:
+ int blackCount = -1;
+ CheckNodeProperties(root, null, RED, 0, ref blackCount);
+ }
+
+ void CheckIsInSection(CollapsedLineSection cs, DocumentLine line)
+ {
+ HeightTreeNode 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);
+ }
+
+ void CheckProperties(HeightTreeNode node)
+ {
+ int totalCount = 1;
+ double totalHeight = node.lineNode.TotalHeight;
+ if (node.lineNode.IsDirectlyCollapsed)
+ Debug.Assert(node.lineNode.collapsedSections.Count > 0);
+ if (node.left != null) {
+ CheckProperties(node.left);
+ totalCount += node.left.totalCount;
+ totalHeight += node.left.totalHeight;
+
+ CheckAllContainedIn(node.left.collapsedSections, node.lineNode.collapsedSections);
+ }
+ if (node.right != null) {
+ CheckProperties(node.right);
+ totalCount += node.right.totalCount;
+ totalHeight += node.right.totalHeight;
+
+ CheckAllContainedIn(node.right.collapsedSections, node.lineNode.collapsedSections);
+ }
+ if (node.left != null && node.right != null) {
+ if (node.left.collapsedSections != null && node.right.collapsedSections != null) {
+ var intersection = System.Linq.Enumerable.Intersect(node.left.collapsedSections, node.right.collapsedSections);
+ Debug.Assert(System.Linq.Enumerable.Count(intersection) == 0);
+ }
+ }
+ if (node.IsDirectlyCollapsed) {
+ Debug.Assert(node.collapsedSections.Count > 0);
+ totalHeight = 0;
+ }
+ Debug.Assert(node.totalCount == totalCount);
+ Debug.Assert(node.totalHeight.IsClose(totalHeight));
+ }
+
+ /// <summary>
+ /// Checks that all elements in list1 are contained in list2.
+ /// </summary>
+ static void CheckAllContainedIn(IEnumerable<CollapsedLineSection> list1, ICollection<CollapsedLineSection> list2)
+ {
+ if (list1 == null) list1 = new List<CollapsedLineSection>();
+ if (list2 == null) list2 = new List<CollapsedLineSection>();
+ foreach (CollapsedLineSection 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.)
+ */
+ void CheckNodeProperties(HeightTreeNode node, HeightTreeNode parentNode, bool parentColor, int blackCount, ref int expectedBlackCount)
+ {
+ if (node == null) return;
+
+ Debug.Assert(node.parent == parentNode);
+
+ if (parentColor == RED) {
+ Debug.Assert(node.color == BLACK);
+ }
+ if (node.color == BLACK) {
+ blackCount++;
+ }
+ if (node.left == null && node.right == null) {
+ // node is a leaf node:
+ if (expectedBlackCount == -1)
+ expectedBlackCount = blackCount;
+ else
+ Debug.Assert(expectedBlackCount == blackCount);
+ }
+ CheckNodeProperties(node.left, node, node.color, blackCount, ref expectedBlackCount);
+ CheckNodeProperties(node.right, node, node.color, blackCount, ref expectedBlackCount);
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ public string GetTreeAsString()
+ {
+ StringBuilder b = new StringBuilder();
+ AppendTreeToString(root, b, 0);
+ return b.ToString();
+ }
+
+ static void AppendTreeToString(HeightTreeNode node, StringBuilder b, int indent)
+ {
+ if (node.color == RED)
+ b.Append("RED ");
+ else
+ b.Append("BLACK ");
+ b.AppendLine(node.ToString());
+ indent += 2;
+ if (node.left != null) {
+ b.Append(' ', indent);
+ b.Append("L: ");
+ AppendTreeToString(node.left, b, indent);
+ }
+ if (node.right != null) {
+ b.Append(' ', indent);
+ b.Append("R: ");
+ AppendTreeToString(node.right, b, indent);
+ }
+ }
+ #endif
+ #endregion
+
+ #region Red/Black Tree
+ const bool RED = true;
+ const bool BLACK = false;
+
+ void InsertAsLeft(HeightTreeNode parentNode, HeightTreeNode newNode)
+ {
+ Debug.Assert(parentNode.left == null);
+ parentNode.left = newNode;
+ newNode.parent = parentNode;
+ newNode.color = RED;
+ UpdateAfterChildrenChange(parentNode);
+ FixTreeOnInsert(newNode);
+ }
+
+ void InsertAsRight(HeightTreeNode parentNode, HeightTreeNode newNode)
+ {
+ Debug.Assert(parentNode.right == null);
+ parentNode.right = newNode;
+ newNode.parent = parentNode;
+ newNode.color = RED;
+ UpdateAfterChildrenChange(parentNode);
+ FixTreeOnInsert(newNode);
+ }
+
+ 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);
+
+ HeightTreeNode 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
+ HeightTreeNode grandparentNode = parentNode.parent;
+ HeightTreeNode 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);
+ }
+ }
+
+ void RemoveNode(HeightTreeNode removedNode)
+ {
+ if (removedNode.left != null && removedNode.right != null) {
+ // replace removedNode with it's in-order successor
+
+ HeightTreeNode leftMost = removedNode.right.LeftMost;
+ HeightTreeNode parentOfLeftMost = leftMost.parent;
+ RemoveNode(leftMost); // remove leftMost from its current location
+
+ BeforeNodeReplace(removedNode, leftMost, parentOfLeftMost);
+ // and overwrite the removedNode with it
+ ReplaceNode(removedNode, leftMost);
+ leftMost.left = removedNode.left;
+ if (leftMost.left != null) leftMost.left.parent = leftMost;
+ leftMost.right = removedNode.right;
+ if (leftMost.right != null) leftMost.right.parent = leftMost;
+ leftMost.color = removedNode.color;
+
+ UpdateAfterChildrenChange(leftMost);
+ if (leftMost.parent != null) UpdateAfterChildrenChange(leftMost.parent);
+ return;
+ }
+
+ // now either removedNode.left or removedNode.right is null
+ // get the remaining child
+ HeightTreeNode parentNode = removedNode.parent;
+ HeightTreeNode 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);
+ }
+ }
+ }
+
+ void FixTreeOnDelete(HeightTreeNode node, HeightTreeNode parentNode)
+ {
+ Debug.Assert(node == null || node.parent == parentNode);
+ if (parentNode == null)
+ return;
+
+ // warning: node may be null
+ HeightTreeNode sibling = Sibling(node, parentNode);
+ if (sibling.color == RED) {
+ parentNode.color = RED;
+ sibling.color = BLACK;
+ if (node == parentNode.left) {
+ RotateLeft(parentNode);
+ } else {
+ RotateRight(parentNode);
+ }
+
+ sibling = Sibling(node, parentNode); // update value of sibling after rotation
+ }
+
+ if (parentNode.color == BLACK
+ && sibling.color == BLACK
+ && GetColor(sibling.left) == BLACK
+ && GetColor(sibling.right) == BLACK)
+ {
+ sibling.color = RED;
+ FixTreeOnDelete(parentNode, parentNode.parent);
+ return;
+ }
+
+ if (parentNode.color == RED
+ && sibling.color == BLACK
+ && GetColor(sibling.left) == BLACK
+ && GetColor(sibling.right) == BLACK)
+ {
+ sibling.color = RED;
+ parentNode.color = BLACK;
+ return;
+ }
+
+ if (node == parentNode.left &&
+ sibling.color == BLACK &&
+ GetColor(sibling.left) == RED &&
+ GetColor(sibling.right) == BLACK)
+ {
+ sibling.color = RED;
+ sibling.left.color = BLACK;
+ RotateRight(sibling);
+ }
+ else if (node == parentNode.right &&
+ sibling.color == BLACK &&
+ GetColor(sibling.right) == RED &&
+ GetColor(sibling.left) == BLACK)
+ {
+ sibling.color = RED;
+ sibling.right.color = BLACK;
+ RotateLeft(sibling);
+ }
+ sibling = Sibling(node, parentNode); // update value of sibling after rotation
+
+ sibling.color = parentNode.color;
+ parentNode.color = BLACK;
+ if (node == parentNode.left) {
+ if (sibling.right != null) {
+ Debug.Assert(sibling.right.color == RED);
+ sibling.right.color = BLACK;
+ }
+ RotateLeft(parentNode);
+ } else {
+ if (sibling.left != null) {
+ Debug.Assert(sibling.left.color == RED);
+ sibling.left.color = BLACK;
+ }
+ RotateRight(parentNode);
+ }
+ }
+
+ 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;
+ }
+
+ void RotateLeft(HeightTreeNode p)
+ {
+ // let q be p's right child
+ HeightTreeNode 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);
+ }
+
+ void RotateRight(HeightTreeNode p)
+ {
+ // let q be p's left child
+ HeightTreeNode 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);
+ }
+
+ static HeightTreeNode Sibling(HeightTreeNode node)
+ {
+ if (node == node.parent.left)
+ return node.parent.right;
+ else
+ return node.parent.left;
+ }
+
+ 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;
+ }
+
+ static bool GetColor(HeightTreeNode node)
+ {
+ return node != null ? node.color : BLACK;
+ }
+ #endregion
+
+ #region Collapsing support
+ 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);
+ }
+
+ void AddRemoveCollapsedSection(CollapsedLineSection section, int sectionLength, bool add)
+ {
+ Debug.Assert(sectionLength > 0);
+
+ HeightTreeNode 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
+ HeightTreeNode 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);
+ }
+
+ 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)
+ {
+ int 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/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/HeightTreeLineNode.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/HeightTreeLineNode.cs
new file mode 100644
index 000000000..459f2a56a
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/HeightTreeLineNode.cs
@@ -0,0 +1,49 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ struct HeightTreeLineNode
+ {
+ internal HeightTreeLineNode(double height)
+ {
+ this.collapsedSections = null;
+ this.height = height;
+ }
+
+ internal double height;
+ internal List<CollapsedLineSection> collapsedSections;
+
+ internal bool IsDirectlyCollapsed {
+ get { return collapsedSections != null; }
+ }
+
+ internal void AddDirectlyCollapsed(CollapsedLineSection section)
+ {
+ if (collapsedSections == null)
+ collapsedSections = new List<CollapsedLineSection>();
+ collapsedSections.Add(section);
+ }
+
+ internal void RemoveDirectlyCollapsed(CollapsedLineSection section)
+ {
+ Debug.Assert(collapsedSections.Contains(section));
+ collapsedSections.Remove(section);
+ if (collapsedSections.Count == 0)
+ collapsedSections = null;
+ }
+
+ /// <summary>
+ /// Returns 0 if the line is directly collapsed, otherwise, returns <see cref="height"/>.
+ /// </summary>
+ internal double TotalHeight {
+ get {
+ return IsDirectlyCollapsed ? 0 : height;
+ }
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/HeightTreeNode.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/HeightTreeNode.cs
new file mode 100644
index 000000000..42661edeb
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/HeightTreeNode.cs
@@ -0,0 +1,155 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+using Tango.Scripting.Editors.Document;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// A node in the text view's height tree.
+ /// </summary>
+ sealed class HeightTreeNode
+ {
+ internal readonly DocumentLine documentLine;
+ internal HeightTreeLineNode lineNode;
+
+ internal HeightTreeNode left, right, parent;
+ internal bool color;
+
+ internal HeightTreeNode()
+ {
+ }
+
+ internal HeightTreeNode(DocumentLine documentLine, double height)
+ {
+ this.documentLine = documentLine;
+ this.totalCount = 1;
+ this.lineNode = new HeightTreeLineNode(height);
+ this.totalHeight = height;
+ }
+
+ internal HeightTreeNode LeftMost {
+ get {
+ HeightTreeNode node = this;
+ while (node.left != null)
+ node = node.left;
+ return node;
+ }
+ }
+
+ internal HeightTreeNode RightMost {
+ get {
+ HeightTreeNode node = this;
+ while (node.right != null)
+ node = node.right;
+ return node;
+ }
+ }
+
+ /// <summary>
+ /// Gets the inorder successor of the node.
+ /// </summary>
+ internal HeightTreeNode Successor {
+ get {
+ if (right != null) {
+ return right.LeftMost;
+ } else {
+ HeightTreeNode node = this;
+ HeightTreeNode oldNode;
+ do {
+ oldNode = node;
+ node = node.parent;
+ // go up until we are coming out of a left subtree
+ } while (node != null && node.right == oldNode);
+ return node;
+ }
+ }
+ }
+
+ /// <summary>
+ /// The number of lines in this node and its child nodes.
+ /// Invariant:
+ /// totalCount = 1 + left.totalCount + right.totalCount
+ /// </summary>
+ internal int totalCount;
+
+ /// <summary>
+ /// The total height of this node and its child nodes, excluding directly collapsed nodes.
+ /// Invariant:
+ /// totalHeight = left.IsDirectlyCollapsed ? 0 : left.totalHeight
+ /// + lineNode.IsDirectlyCollapsed ? 0 : lineNode.Height
+ /// + right.IsDirectlyCollapsed ? 0 : right.totalHeight
+ /// </summary>
+ internal double totalHeight;
+
+ /// <summary>
+ /// List of the sections that hold this node collapsed.
+ /// Invariant 1:
+ /// For each document line in the range described by a CollapsedSection, exactly one ancestor
+ /// contains that CollapsedSection.
+ /// Invariant 2:
+ /// A CollapsedSection is contained either in left+middle or middle+right or just middle.
+ /// Invariant 3:
+ /// Start and end of a CollapsedSection always contain the collapsedSection in their
+ /// documentLine (middle node).
+ /// </summary>
+ internal List<CollapsedLineSection> collapsedSections;
+
+ internal bool IsDirectlyCollapsed {
+ get {
+ return collapsedSections != null;
+ }
+ }
+
+ internal void AddDirectlyCollapsed(CollapsedLineSection section)
+ {
+ if (collapsedSections == null) {
+ collapsedSections = new List<CollapsedLineSection>();
+ totalHeight = 0;
+ }
+ Debug.Assert(!collapsedSections.Contains(section));
+ collapsedSections.Add(section);
+ }
+
+
+ internal void RemoveDirectlyCollapsed(CollapsedLineSection section)
+ {
+ Debug.Assert(collapsedSections.Contains(section));
+ collapsedSections.Remove(section);
+ if (collapsedSections.Count == 0) {
+ collapsedSections = null;
+ totalHeight = lineNode.TotalHeight;
+ if (left != null)
+ totalHeight += left.totalHeight;
+ if (right != null)
+ totalHeight += right.totalHeight;
+ }
+ }
+
+ #if DEBUG
+ public override string ToString()
+ {
+ return "[HeightTreeNode "
+ + documentLine.LineNumber + " CS=" + GetCollapsedSections(collapsedSections)
+ + " Line.CS=" + GetCollapsedSections(lineNode.collapsedSections)
+ + " Line.Height=" + lineNode.height
+ + " TotalHeight=" + totalHeight
+ + "]";
+ }
+
+ static string GetCollapsedSections(List<CollapsedLineSection> list)
+ {
+ if (list == null)
+ return "{}";
+ return "{" +
+ string.Join(",",
+ list.ConvertAll(cs=>cs.ID).ToArray())
+ + "}";
+ }
+ #endif
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/IBackgroundRenderer.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/IBackgroundRenderer.cs
new file mode 100644
index 000000000..e16941147
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/IBackgroundRenderer.cs
@@ -0,0 +1,26 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Windows.Media;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// Background renderers draw in the background of a known layer.
+ /// You can use background renderers to draw non-interactive elements on the TextView
+ /// without introducing new UIElements.
+ /// </summary>
+ public interface IBackgroundRenderer
+ {
+ /// <summary>
+ /// Gets the layer on which this background renderer should draw.
+ /// </summary>
+ KnownLayer Layer { get; }
+
+ /// <summary>
+ /// Causes the background renderer to draw.
+ /// </summary>
+ void Draw(TextView textView, DrawingContext drawingContext);
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/ITextRunConstructionContext.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/ITextRunConstructionContext.cs
new file mode 100644
index 000000000..94c319bbf
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/ITextRunConstructionContext.cs
@@ -0,0 +1,47 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Windows.Media.TextFormatting;
+using Tango.Scripting.Editors.Document;
+using Tango.Scripting.Editors.Utils;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// Contains information relevant for text run creation.
+ /// </summary>
+ public interface ITextRunConstructionContext
+ {
+ /// <summary>
+ /// Gets the text document.
+ /// </summary>
+ TextDocument Document { get; }
+
+ /// <summary>
+ /// Gets the text view for which the construction runs.
+ /// </summary>
+ TextView TextView { get; }
+
+ /// <summary>
+ /// Gets the visual line that is currently being constructed.
+ /// </summary>
+ VisualLine VisualLine { get; }
+
+ /// <summary>
+ /// Gets the global text run properties.
+ /// </summary>
+ TextRunProperties GlobalTextRunProperties { get; }
+
+ /// <summary>
+ /// Gets a piece of text from the document.
+ /// </summary>
+ /// <remarks>
+ /// This method is allowed to return a larger string than requested.
+ /// It does this by returning a <see cref="StringSegment"/> that describes the requested segment within the returned string.
+ /// 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.
+ /// </remarks>
+ StringSegment GetText(int offset, int length);
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/ITextViewConnect.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/ITextViewConnect.cs
new file mode 100644
index 000000000..55a5c000c
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/ITextViewConnect.cs
@@ -0,0 +1,24 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// Allows <see cref="VisualLineElementGenerator"/>s, <see cref="IVisualLineTransformer"/>s and
+ /// <see cref="IBackgroundRenderer"/>s to be notified when they are added or removed from a text view.
+ /// </summary>
+ public interface ITextViewConnect
+ {
+ /// <summary>
+ /// Called when added to a text view.
+ /// </summary>
+ void AddToTextView(TextView textView);
+
+ /// <summary>
+ /// Called when removed from a text view.
+ /// </summary>
+ void RemoveFromTextView(TextView textView);
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/IVisualLineTransformer.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/IVisualLineTransformer.cs
new file mode 100644
index 000000000..91c2b6863
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/IVisualLineTransformer.cs
@@ -0,0 +1,19 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Collections.Generic;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// Allows transforming visual line elements.
+ /// </summary>
+ public interface IVisualLineTransformer
+ {
+ /// <summary>
+ /// Applies the transformation to the specified list of visual line elements.
+ /// </summary>
+ void Transform(ITextRunConstructionContext context, IList<VisualLineElement> elements);
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/InlineObjectRun.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/InlineObjectRun.cs
new file mode 100644
index 000000000..7448a8007
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/InlineObjectRun.cs
@@ -0,0 +1,145 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using System.Windows.Media.TextFormatting;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// A inline UIElement in the document.
+ /// </summary>
+ public class InlineObjectElement : VisualLineElement
+ {
+ /// <summary>
+ /// Gets the inline element that is displayed.
+ /// </summary>
+ public UIElement Element { get; private set; }
+
+ /// <summary>
+ /// Creates a new InlineObjectElement.
+ /// </summary>
+ /// <param name="documentLength">The length of the element in the document. Must be non-negative.</param>
+ /// <param name="element">The element to display.</param>
+ public InlineObjectElement(int documentLength, UIElement element)
+ : base(1, documentLength)
+ {
+ if (element == null)
+ throw new ArgumentNullException("element");
+ this.Element = element;
+ }
+
+ /// <inheritdoc/>
+ public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context)
+ {
+ if (context == null)
+ throw new ArgumentNullException("context");
+
+ return new InlineObjectRun(1, this.TextRunProperties, this.Element);
+ }
+ }
+
+ /// <summary>
+ /// A text run with an embedded UIElement.
+ /// </summary>
+ public class InlineObjectRun : TextEmbeddedObject
+ {
+ UIElement element;
+ int length;
+ TextRunProperties properties;
+ internal Size desiredSize;
+
+ /// <summary>
+ /// Creates a new InlineObjectRun instance.
+ /// </summary>
+ /// <param name="length">The length of the TextRun.</param>
+ /// <param name="properties">The <see cref="TextRunProperties"/> to use.</param>
+ /// <param name="element">The <see cref="UIElement"/> to display.</param>
+ public InlineObjectRun(int length, TextRunProperties properties, UIElement element)
+ {
+ if (length <= 0)
+ throw new ArgumentOutOfRangeException("length", length, "Value must be positive");
+ if (properties == null)
+ throw new ArgumentNullException("properties");
+ if (element == null)
+ throw new ArgumentNullException("element");
+
+ this.length = length;
+ this.properties = properties;
+ this.element = element;
+ }
+
+ /// <summary>
+ /// Gets the element displayed by the InlineObjectRun.
+ /// </summary>
+ public UIElement Element {
+ get { return element; }
+ }
+
+ /// <summary>
+ /// Gets the VisualLine that contains this object. This property is only available after the object
+ /// was added to the text view.
+ /// </summary>
+ public VisualLine VisualLine { get; internal set; }
+
+ /// <inheritdoc/>
+ public override LineBreakCondition BreakBefore {
+ get { return LineBreakCondition.BreakDesired; }
+ }
+
+ /// <inheritdoc/>
+ public override LineBreakCondition BreakAfter {
+ get { return LineBreakCondition.BreakDesired; }
+ }
+
+ /// <inheritdoc/>
+ public override bool HasFixedSize {
+ get { return true; }
+ }
+
+ /// <inheritdoc/>
+ public override CharacterBufferReference CharacterBufferReference {
+ get { return new CharacterBufferReference(); }
+ }
+
+ /// <inheritdoc/>
+ public override int Length {
+ get { return length; }
+ }
+
+ /// <inheritdoc/>
+ public override TextRunProperties Properties {
+ get { return properties; }
+ }
+
+ /// <inheritdoc/>
+ public override TextEmbeddedObjectMetrics Format(double remainingParagraphWidth)
+ {
+ double baseline = TextBlock.GetBaselineOffset(element);
+ if (double.IsNaN(baseline))
+ baseline = desiredSize.Height;
+ return new TextEmbeddedObjectMetrics(desiredSize.Width, desiredSize.Height, baseline);
+ }
+
+ /// <inheritdoc/>
+ public override Rect ComputeBoundingBox(bool rightToLeft, bool sideways)
+ {
+ if (this.element.IsArrangeValid) {
+ double baseline = TextBlock.GetBaselineOffset(element);
+ if (double.IsNaN(baseline))
+ baseline = desiredSize.Height;
+ return new Rect(new Point(0, -baseline), desiredSize);
+ } else {
+ return Rect.Empty;
+ }
+ }
+
+ /// <inheritdoc/>
+ public override void Draw(DrawingContext drawingContext, Point origin, bool rightToLeft, bool sideways)
+ {
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/Layer.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/Layer.cs
new file mode 100644
index 000000000..0d4777813
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/Layer.cs
@@ -0,0 +1,43 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Diagnostics;
+using System.Windows;
+using System.Windows.Media;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// Base class for known layers.
+ /// </summary>
+ class Layer : UIElement
+ {
+ protected readonly TextView textView;
+ protected readonly KnownLayer knownLayer;
+
+ public Layer(TextView textView, KnownLayer knownLayer)
+ {
+ Debug.Assert(textView != null);
+ this.textView = textView;
+ this.knownLayer = knownLayer;
+ this.Focusable = false;
+ }
+
+ protected override GeometryHitTestResult HitTestCore(GeometryHitTestParameters hitTestParameters)
+ {
+ return null;
+ }
+
+ protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
+ {
+ return null;
+ }
+
+ protected override void OnRender(DrawingContext drawingContext)
+ {
+ base.OnRender(drawingContext);
+ textView.RenderBackground(drawingContext, knownLayer);
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/LayerPosition.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/LayerPosition.cs
new file mode 100644
index 000000000..f64923eea
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/LayerPosition.cs
@@ -0,0 +1,91 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Windows;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// An enumeration of well-known layers.
+ /// </summary>
+ public enum KnownLayer
+ {
+ /// <summary>
+ /// This layer is in the background.
+ /// There is no UIElement to represent this layer, it is directly drawn in the TextView.
+ /// It is not possible to replace the background layer or insert new layers below it.
+ /// </summary>
+ /// <remarks>This layer is below the Selection layer.</remarks>
+ Background,
+ /// <summary>
+ /// This layer contains the selection rectangle.
+ /// </summary>
+ /// <remarks>This layer is between the Background and the Text layers.</remarks>
+ Selection,
+ /// <summary>
+ /// This layer contains the text and inline UI elements.
+ /// </summary>
+ /// <remarks>This layer is between the Selection and the Caret layers.</remarks>
+ Text,
+ /// <summary>
+ /// This layer contains the blinking caret.
+ /// </summary>
+ /// <remarks>This layer is above the Text layer.</remarks>
+ Caret
+ }
+
+ /// <summary>
+ /// Specifies where a new layer is inserted, in relation to an old layer.
+ /// </summary>
+ public enum LayerInsertionPosition
+ {
+ /// <summary>
+ /// The new layer is inserted below the specified layer.
+ /// </summary>
+ Below,
+ /// <summary>
+ /// The new layer replaces the specified layer. The old layer is removed
+ /// from the <see cref="TextView.Layers"/> collection.
+ /// </summary>
+ Replace,
+ /// <summary>
+ /// The new layer is inserted above the specified layer.
+ /// </summary>
+ Above
+ }
+
+ sealed class LayerPosition : IComparable<LayerPosition>
+ {
+ internal static readonly DependencyProperty LayerPositionProperty =
+ DependencyProperty.RegisterAttached("LayerPosition", typeof(LayerPosition), typeof(LayerPosition));
+
+ public static void SetLayerPosition(UIElement layer, LayerPosition value)
+ {
+ layer.SetValue(LayerPositionProperty, value);
+ }
+
+ public static LayerPosition GetLayerPosition(UIElement layer)
+ {
+ return (LayerPosition)layer.GetValue(LayerPositionProperty);
+ }
+
+ internal readonly KnownLayer KnownLayer;
+ internal readonly LayerInsertionPosition Position;
+
+ public LayerPosition(KnownLayer knownLayer, LayerInsertionPosition position)
+ {
+ this.KnownLayer = knownLayer;
+ this.Position = position;
+ }
+
+ public int CompareTo(LayerPosition other)
+ {
+ int r = this.KnownLayer.CompareTo(other.KnownLayer);
+ if (r != 0)
+ return r;
+ else
+ return this.Position.CompareTo(other.Position);
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/LinkElementGenerator.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/LinkElementGenerator.cs
new file mode 100644
index 000000000..a5cb81c12
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/LinkElementGenerator.cs
@@ -0,0 +1,144 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Text.RegularExpressions;
+using Tango.Scripting.Editors.Utils;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ // This class is public because it can be used as a base class for custom links.
+
+ /// <summary>
+ /// Detects hyperlinks and makes them clickable.
+ /// </summary>
+ /// <remarks>
+ /// This element generator can be easily enabled and configured using the
+ /// <see cref="TextEditorOptions"/>.
+ /// </remarks>
+ public class LinkElementGenerator : VisualLineElementGenerator, IBuiltinElementGenerator
+ {
+ // 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 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");
+
+ readonly Regex linkRegex;
+
+ /// <summary>
+ /// Gets/Sets whether the user needs to press Control to click the link.
+ /// The default value is true.
+ /// </summary>
+ public bool RequireControlModifierForClick { get; set; }
+
+ /// <summary>
+ /// Creates a new LinkElementGenerator.
+ /// </summary>
+ public LinkElementGenerator()
+ {
+ this.linkRegex = defaultLinkRegex;
+ this.RequireControlModifierForClick = true;
+ }
+
+ /// <summary>
+ /// Creates a new LinkElementGenerator using the specified regex.
+ /// </summary>
+ protected LinkElementGenerator(Regex regex) : this()
+ {
+ if (regex == null)
+ throw new ArgumentNullException("regex");
+ this.linkRegex = regex;
+ }
+
+ void IBuiltinElementGenerator.FetchOptions(TextEditorOptions options)
+ {
+ this.RequireControlModifierForClick = options.RequireControlModifierForHyperlinkClick;
+ }
+
+ Match GetMatch(int startOffset, out int matchOffset)
+ {
+ int endOffset = CurrentContext.VisualLine.LastDocumentLine.EndOffset;
+ StringSegment relevantText = CurrentContext.GetText(startOffset, endOffset - startOffset);
+ Match m = linkRegex.Match(relevantText.Text, relevantText.Offset, relevantText.Count);
+ matchOffset = m.Success ? m.Index - relevantText.Offset + startOffset : -1;
+ return m;
+ }
+
+ /// <inheritdoc/>
+ public override int GetFirstInterestedOffset(int startOffset)
+ {
+ int matchOffset;
+ GetMatch(startOffset, out matchOffset);
+ return matchOffset;
+ }
+
+ /// <inheritdoc/>
+ public override VisualLineElement ConstructElement(int offset)
+ {
+ int matchOffset;
+ Match m = GetMatch(offset, out matchOffset);
+ if (m.Success && matchOffset == offset) {
+ return ConstructElementFromMatch(m);
+ } else {
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Constructs a VisualLineElement that replaces the matched text.
+ /// The default implementation will create a <see cref="VisualLineLinkText"/>
+ /// based on the URI provided by <see cref="GetUriFromMatch"/>.
+ /// </summary>
+ protected virtual VisualLineElement ConstructElementFromMatch(Match m)
+ {
+ Uri uri = GetUriFromMatch(m);
+ if (uri == null)
+ return null;
+ VisualLineLinkText linkText = new VisualLineLinkText(CurrentContext.VisualLine, m.Length);
+ linkText.NavigateUri = uri;
+ linkText.RequireControlModifierForClick = this.RequireControlModifierForClick;
+ return linkText;
+ }
+
+ /// <summary>
+ /// Fetches the URI from the regex match. Returns null if the URI format is invalid.
+ /// </summary>
+ protected virtual Uri GetUriFromMatch(Match match)
+ {
+ string targetUrl = match.Value;
+ if (targetUrl.StartsWith("www.", StringComparison.Ordinal))
+ targetUrl = "http://" + targetUrl;
+ if (Uri.IsWellFormedUriString(targetUrl, UriKind.Absolute))
+ return new Uri(targetUrl);
+
+ return null;
+ }
+ }
+
+ // This class is internal because it does not need to be accessed by the user - it can be configured using TextEditorOptions.
+
+ /// <summary>
+ /// Detects e-mail addresses and makes them clickable.
+ /// </summary>
+ /// <remarks>
+ /// This element generator can be easily enabled and configured using the
+ /// <see cref="TextEditorOptions"/>.
+ /// </remarks>
+ sealed class MailLinkElementGenerator : LinkElementGenerator
+ {
+ /// <summary>
+ /// Creates a new MailLinkElementGenerator.
+ /// </summary>
+ public MailLinkElementGenerator()
+ : base(defaultMailRegex)
+ {
+ }
+
+ protected override Uri GetUriFromMatch(Match match)
+ {
+ return new Uri("mailto:" + match.Value);
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/MouseHoverLogic.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/MouseHoverLogic.cs
new file mode 100644
index 000000000..857affcbb
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/MouseHoverLogic.cs
@@ -0,0 +1,134 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Windows;
+using System.Windows.Input;
+using System.Windows.Threading;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// Encapsulates and adds MouseHover support to UIElements.
+ /// </summary>
+ public class MouseHoverLogic : IDisposable
+ {
+ UIElement target;
+
+ DispatcherTimer mouseHoverTimer;
+ Point mouseHoverStartPoint;
+ MouseEventArgs mouseHoverLastEventArgs;
+ bool mouseHovering;
+
+ /// <summary>
+ /// Creates a new instance and attaches itself to the <paramref name="target" /> UIElement.
+ /// </summary>
+ public MouseHoverLogic(UIElement target)
+ {
+ if (target == null)
+ throw new ArgumentNullException("target");
+ this.target = target;
+ this.target.MouseLeave += MouseHoverLogicMouseLeave;
+ this.target.MouseMove += MouseHoverLogicMouseMove;
+ this.target.MouseEnter += MouseHoverLogicMouseEnter;
+ }
+
+ void MouseHoverLogicMouseMove(object sender, MouseEventArgs e)
+ {
+ Vector mouseMovement = mouseHoverStartPoint - e.GetPosition(this.target);
+ if (Math.Abs(mouseMovement.X) > SystemParameters.MouseHoverWidth
+ || Math.Abs(mouseMovement.Y) > SystemParameters.MouseHoverHeight)
+ {
+ StartHovering(e);
+ }
+ // do not set e.Handled - allow others to also handle MouseMove
+ }
+
+ void MouseHoverLogicMouseEnter(object sender, MouseEventArgs e)
+ {
+ StartHovering(e);
+ // do not set e.Handled - allow others to also handle MouseEnter
+ }
+
+ void StartHovering(MouseEventArgs e)
+ {
+ StopHovering();
+ mouseHoverStartPoint = e.GetPosition(this.target);
+ mouseHoverLastEventArgs = e;
+ mouseHoverTimer = new DispatcherTimer(SystemParameters.MouseHoverTime, DispatcherPriority.Background, OnMouseHoverTimerElapsed, this.target.Dispatcher);
+ mouseHoverTimer.Start();
+ }
+
+ void MouseHoverLogicMouseLeave(object sender, MouseEventArgs e)
+ {
+ StopHovering();
+ // do not set e.Handled - allow others to also handle MouseLeave
+ }
+
+ void StopHovering()
+ {
+ if (mouseHoverTimer != null) {
+ mouseHoverTimer.Stop();
+ mouseHoverTimer = null;
+ }
+ if (mouseHovering) {
+ mouseHovering = false;
+ OnMouseHoverStopped(mouseHoverLastEventArgs);
+ }
+ }
+
+ void OnMouseHoverTimerElapsed(object sender, EventArgs e)
+ {
+ mouseHoverTimer.Stop();
+ mouseHoverTimer = null;
+
+ mouseHovering = true;
+ OnMouseHover(mouseHoverLastEventArgs);
+ }
+
+ /// <summary>
+ /// Occurs when the mouse starts hovering over a certain location.
+ /// </summary>
+ public event EventHandler<MouseEventArgs> MouseHover;
+
+ /// <summary>
+ /// Raises the <see cref="MouseHover"/> event.
+ /// </summary>
+ protected virtual void OnMouseHover(MouseEventArgs e)
+ {
+ if (MouseHover != null) {
+ MouseHover(this, e);
+ }
+ }
+
+ /// <summary>
+ /// Occurs when the mouse stops hovering over a certain location.
+ /// </summary>
+ public event EventHandler<MouseEventArgs> MouseHoverStopped;
+
+ /// <summary>
+ /// Raises the <see cref="MouseHoverStopped"/> event.
+ /// </summary>
+ protected virtual void OnMouseHoverStopped(MouseEventArgs e)
+ {
+ if (MouseHoverStopped != null) {
+ MouseHoverStopped(this, e);
+ }
+ }
+
+ bool disposed;
+
+ /// <summary>
+ /// Removes the MouseHover support from the target UIElement.
+ /// </summary>
+ public void Dispose()
+ {
+ if (!disposed) {
+ this.target.MouseLeave -= MouseHoverLogicMouseLeave;
+ this.target.MouseMove -= MouseHoverLogicMouseMove;
+ this.target.MouseEnter -= MouseHoverLogicMouseEnter;
+ }
+ disposed = true;
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/SimpleTextSource.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/SimpleTextSource.cs
new file mode 100644
index 000000000..8e9ac568b
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/SimpleTextSource.cs
@@ -0,0 +1,39 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Diagnostics;
+using System.Windows.Media.TextFormatting;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ sealed class SimpleTextSource : TextSource
+ {
+ readonly string text;
+ readonly TextRunProperties properties;
+
+ public SimpleTextSource(string text, TextRunProperties properties)
+ {
+ this.text = text;
+ this.properties = properties;
+ }
+
+ public override TextRun GetTextRun(int textSourceCharacterIndex)
+ {
+ if (textSourceCharacterIndex < text.Length)
+ return new TextCharacters(text, textSourceCharacterIndex, text.Length - textSourceCharacterIndex, properties);
+ else
+ return new TextEndOfParagraph(1);
+ }
+
+ public override int GetTextEffectCharacterIndexFromTextSourceCharacterIndex(int textSourceCharacterIndex)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override TextSpan<CultureSpecificCharacterBufferRange> GetPrecedingText(int textSourceCharacterIndexLimit)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/SingleCharacterElementGenerator.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/SingleCharacterElementGenerator.cs
new file mode 100644
index 000000000..53c4aac81
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/SingleCharacterElementGenerator.cs
@@ -0,0 +1,268 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Windows;
+using System.Windows.Documents;
+using System.Windows.Media;
+using System.Windows.Media.TextFormatting;
+
+using Tango.Scripting.Editors.Document;
+using Tango.Scripting.Editors.Utils;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ // This class is internal because it does not need to be accessed by the user - it can be configured using TextEditorOptions.
+
+ /// <summary>
+ /// Element generator that displays · for spaces and » for tabs and a box for control characters.
+ /// </summary>
+ /// <remarks>
+ /// This element generator is present in every TextView by default; the enabled features can be configured using the
+ /// <see cref="TextEditorOptions"/>.
+ /// </remarks>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Whitespace")]
+ sealed class SingleCharacterElementGenerator : VisualLineElementGenerator, IBuiltinElementGenerator
+ {
+ /// <summary>
+ /// Gets/Sets whether to show · for spaces.
+ /// </summary>
+ public bool ShowSpaces { get; set; }
+
+ /// <summary>
+ /// Gets/Sets whether to show » for tabs.
+ /// </summary>
+ public bool ShowTabs { get; set; }
+
+ /// <summary>
+ /// Gets/Sets whether to show a box with the hex code for control characters.
+ /// </summary>
+ public bool ShowBoxForControlCharacters { get; set; }
+
+ /// <summary>
+ /// Creates a new SingleCharacterElementGenerator instance.
+ /// </summary>
+ public SingleCharacterElementGenerator()
+ {
+ this.ShowSpaces = true;
+ this.ShowTabs = true;
+ this.ShowBoxForControlCharacters = true;
+ }
+
+ void IBuiltinElementGenerator.FetchOptions(TextEditorOptions options)
+ {
+ this.ShowSpaces = options.ShowSpaces;
+ this.ShowTabs = options.ShowTabs;
+ this.ShowBoxForControlCharacters = options.ShowBoxForControlCharacters;
+ }
+
+ public override int GetFirstInterestedOffset(int startOffset)
+ {
+ DocumentLine endLine = CurrentContext.VisualLine.LastDocumentLine;
+ StringSegment relevantText = CurrentContext.GetText(startOffset, endLine.EndOffset - startOffset);
+
+ for (int i = 0; i < relevantText.Count; i++) {
+ char 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)
+ {
+ char 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;
+ }
+ }
+
+ sealed class SpaceTextElement : FormattedTextElement
+ {
+ public SpaceTextElement(TextLine textLine) : base(textLine, 1)
+ {
+ BreakBefore = LineBreakCondition.BreakPossible;
+ BreakAfter = LineBreakCondition.BreakDesired;
+ }
+
+ public override int GetNextCaretPosition(int visualColumn, LogicalDirection direction, CaretPositioningMode mode)
+ {
+ if (mode == CaretPositioningMode.Normal)
+ return base.GetNextCaretPosition(visualColumn, direction, mode);
+ else
+ return -1;
+ }
+
+ public override bool IsWhitespace(int visualColumn)
+ {
+ return true;
+ }
+ }
+
+ sealed class TabTextElement : VisualLineElement
+ {
+ internal readonly TextLine text;
+
+ public TabTextElement(TextLine text) : base(2, 1)
+ {
+ this.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 == this.VisualColumn)
+ return new TabGlyphRun(this, this.TextRunProperties);
+ else if (startVisualColumn == this.VisualColumn + 1)
+ return new TextCharacters("\t", 0, 1, this.TextRunProperties);
+ else
+ throw new ArgumentOutOfRangeException("startVisualColumn");
+ }
+
+ public override int GetNextCaretPosition(int visualColumn, LogicalDirection direction, CaretPositioningMode mode)
+ {
+ if (mode == CaretPositioningMode.Normal)
+ return base.GetNextCaretPosition(visualColumn, direction, mode);
+ else
+ return -1;
+ }
+
+ public override bool IsWhitespace(int visualColumn)
+ {
+ return true;
+ }
+ }
+
+ sealed class TabGlyphRun : TextEmbeddedObject
+ {
+ readonly TabTextElement element;
+ TextRunProperties properties;
+
+ public TabGlyphRun(TabTextElement element, TextRunProperties properties)
+ {
+ if (properties == null)
+ throw new ArgumentNullException("properties");
+ this.properties = properties;
+ this.element = element;
+ }
+
+ public override LineBreakCondition BreakBefore {
+ get { return LineBreakCondition.BreakPossible; }
+ }
+
+ public override LineBreakCondition BreakAfter {
+ get { return LineBreakCondition.BreakRestrained; }
+ }
+
+ public override bool HasFixedSize {
+ get { return true; }
+ }
+
+ public override CharacterBufferReference CharacterBufferReference {
+ get { return new CharacterBufferReference(); }
+ }
+
+ public override int Length {
+ get { return 1; }
+ }
+
+ public override TextRunProperties Properties {
+ get { return properties; }
+ }
+
+ public override TextEmbeddedObjectMetrics Format(double remainingParagraphWidth)
+ {
+ double width = Math.Min(0, element.text.WidthIncludingTrailingWhitespace - 1);
+ return new TextEmbeddedObjectMetrics(width, element.text.Height, element.text.Baseline);
+ }
+
+ public override Rect ComputeBoundingBox(bool rightToLeft, bool sideways)
+ {
+ double width = Math.Min(0, element.text.WidthIncludingTrailingWhitespace - 1);
+ return new Rect(0, 0, width, element.text.Height);
+ }
+
+ public override void Draw(DrawingContext drawingContext, Point origin, bool rightToLeft, bool sideways)
+ {
+ origin.Y -= element.text.Baseline;
+ element.text.Draw(drawingContext, origin, InvertAxes.None);
+ }
+ }
+
+ sealed class SpecialCharacterBoxElement : FormattedTextElement
+ {
+ public SpecialCharacterBoxElement(TextLine text) : base(text, 1)
+ {
+ }
+
+ public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context)
+ {
+ return new SpecialCharacterTextRun(this, this.TextRunProperties);
+ }
+ }
+
+ sealed class SpecialCharacterTextRun : FormattedTextRun
+ {
+ static readonly SolidColorBrush darkGrayBrush;
+
+ static SpecialCharacterTextRun()
+ {
+ darkGrayBrush = new SolidColorBrush(Color.FromArgb(200, 128, 128, 128));
+ darkGrayBrush.Freeze();
+ }
+
+ public SpecialCharacterTextRun(FormattedTextElement element, TextRunProperties properties)
+ : base(element, properties)
+ {
+ }
+
+ public override void Draw(DrawingContext drawingContext, Point origin, bool rightToLeft, bool sideways)
+ {
+ Point newOrigin = new Point(origin.X + 1.5, origin.Y);
+ var metrics = base.Format(double.PositiveInfinity);
+ Rect r = new Rect(newOrigin.X - 0.5, newOrigin.Y - metrics.Baseline, metrics.Width + 2, metrics.Height);
+ drawingContext.DrawRoundedRectangle(darkGrayBrush, null, r, 2.5, 2.5);
+ base.Draw(drawingContext, newOrigin, rightToLeft, sideways);
+ }
+
+ public override TextEmbeddedObjectMetrics Format(double remainingParagraphWidth)
+ {
+ TextEmbeddedObjectMetrics metrics = base.Format(remainingParagraphWidth);
+ return new TextEmbeddedObjectMetrics(metrics.Width + 3,
+ metrics.Height, metrics.Baseline);
+ }
+
+ public override Rect ComputeBoundingBox(bool rightToLeft, bool sideways)
+ {
+ Rect r = base.ComputeBoundingBox(rightToLeft, sideways);
+ r.Width += 3;
+ return r;
+ }
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/TextLayer.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/TextLayer.cs
new file mode 100644
index 000000000..3f4b8299b
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/TextLayer.cs
@@ -0,0 +1,70 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Collections.Generic;
+using System.Windows;
+using System.Windows.Input;
+using System.Windows.Media;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// The control that contains the text.
+ ///
+ /// This control is used to allow other UIElements to be placed inside the TextView but
+ /// behind the text.
+ /// The text rendering process (VisualLine creation) is controlled by the TextView, this
+ /// class simply displays the created Visual Lines.
+ /// </summary>
+ /// <remarks>
+ /// This class does not contain any input handling and is invisible to hit testing. Input
+ /// is handled by the TextView.
+ /// This allows UIElements that are displayed behind the text, but still can react to mouse input.
+ /// </remarks>
+ sealed class TextLayer : Layer
+ {
+ /// <summary>
+ /// the index of the text layer in the layers collection
+ /// </summary>
+ internal int index;
+
+ public TextLayer(TextView textView) : base(textView, KnownLayer.Text)
+ {
+ }
+
+ List<VisualLineDrawingVisual> visuals = new List<VisualLineDrawingVisual>();
+
+ internal void SetVisualLines(ICollection<VisualLine> visualLines)
+ {
+ foreach (VisualLineDrawingVisual v in visuals) {
+ if (v.VisualLine.IsDisposed)
+ RemoveVisualChild(v);
+ }
+ visuals.Clear();
+ foreach (VisualLine newLine in visualLines) {
+ VisualLineDrawingVisual v = newLine.Render();
+ if (!v.IsAdded) {
+ AddVisualChild(v);
+ v.IsAdded = true;
+ }
+ visuals.Add(v);
+ }
+ InvalidateArrange();
+ }
+
+ protected override int VisualChildrenCount {
+ get { return visuals.Count; }
+ }
+
+ protected override Visual GetVisualChild(int index)
+ {
+ return visuals[index];
+ }
+
+ protected override void ArrangeCore(Rect finalRect)
+ {
+ textView.ArrangeTextLayer(visuals);
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/TextView.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/TextView.cs
new file mode 100644
index 000000000..3dabb6b7a
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/TextView.cs
@@ -0,0 +1,2009 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.ComponentModel.Design;
+using System.Diagnostics;
+using System.Globalization;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.TextFormatting;
+using System.Windows.Threading;
+using Tango.Scripting.Editors.Document;
+using Tango.Scripting.Editors.Editing;
+using Tango.Scripting.Editors.Utils;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// A virtualizing panel producing+showing <see cref="VisualLine"/>s for a <see cref="TextDocument"/>.
+ ///
+ /// This is the heart of the text editor, this class controls the text rendering process.
+ ///
+ /// Taken as a standalone control, it's a text viewer without any editing capability.
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable",
+ Justification = "The user usually doesn't work with TextView but with TextEditor; and nulling the Document property is sufficient to dispose everything.")]
+ public class TextView : FrameworkElement, IScrollInfo, IWeakEventListener, ITextEditorComponent, IServiceProvider
+ {
+ #region Constructor
+ static TextView()
+ {
+ ClipToBoundsProperty.OverrideMetadata(typeof(TextView), new FrameworkPropertyMetadata(Boxes.True));
+ FocusableProperty.OverrideMetadata(typeof(TextView), new FrameworkPropertyMetadata(Boxes.False));
+ }
+
+ ColumnRulerRenderer columnRulerRenderer;
+
+ /// <summary>
+ /// Creates a new TextView instance.
+ /// </summary>
+ public TextView()
+ {
+ services.AddService(typeof(TextView), this);
+ textLayer = new TextLayer(this);
+ elementGenerators = new ObserveAddRemoveCollection<VisualLineElementGenerator>(ElementGenerator_Added, ElementGenerator_Removed);
+ lineTransformers = new ObserveAddRemoveCollection<IVisualLineTransformer>(LineTransformer_Added, LineTransformer_Removed);
+ backgroundRenderers = new ObserveAddRemoveCollection<IBackgroundRenderer>(BackgroundRenderer_Added, BackgroundRenderer_Removed);
+ columnRulerRenderer = new ColumnRulerRenderer(this);
+ this.Options = new TextEditorOptions();
+
+ Debug.Assert(singleCharacterElementGenerator != null); // assert that the option change created the builtin element generators
+
+ layers = new LayerCollection(this);
+ InsertLayer(textLayer, KnownLayer.Text, LayerInsertionPosition.Replace);
+
+ this.hoverLogic = new MouseHoverLogic(this);
+ this.hoverLogic.MouseHover += (sender, e) => RaiseHoverEventPair(e, PreviewMouseHoverEvent, MouseHoverEvent);
+ this.hoverLogic.MouseHoverStopped += (sender, e) => RaiseHoverEventPair(e, PreviewMouseHoverStoppedEvent, MouseHoverStoppedEvent);
+ }
+
+ #endregion
+
+ #region Document Property
+ /// <summary>
+ /// Document property.
+ /// </summary>
+ public static readonly DependencyProperty DocumentProperty =
+ DependencyProperty.Register("Document", typeof(TextDocument), typeof(TextView),
+ new FrameworkPropertyMetadata(OnDocumentChanged));
+
+ TextDocument document;
+ HeightTree heightTree;
+
+ /// <summary>
+ /// Gets/Sets the document displayed by the text editor.
+ /// </summary>
+ public TextDocument Document {
+ get { return (TextDocument)GetValue(DocumentProperty); }
+ set { SetValue(DocumentProperty, value); }
+ }
+
+ static void OnDocumentChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
+ {
+ ((TextView)dp).OnDocumentChanged((TextDocument)e.OldValue, (TextDocument)e.NewValue);
+ }
+
+ internal double FontSize {
+ get {
+ return (double)GetValue(TextBlock.FontSizeProperty);
+ }
+ }
+
+ /// <summary>
+ /// Occurs when the document property has changed.
+ /// </summary>
+ public event EventHandler DocumentChanged;
+
+ void OnDocumentChanged(TextDocument oldValue, TextDocument newValue)
+ {
+ if (oldValue != null) {
+ heightTree.Dispose();
+ heightTree = null;
+ formatter.Dispose();
+ formatter = null;
+ cachedElements.Dispose();
+ cachedElements = null;
+ TextDocumentWeakEventManager.Changing.RemoveListener(oldValue, this);
+ }
+ this.document = newValue;
+ ClearScrollData();
+ ClearVisualLines();
+ if (newValue != null) {
+ TextDocumentWeakEventManager.Changing.AddListener(newValue, this);
+ formatter = TextFormatterFactory.Create(this);
+ InvalidateDefaultTextMetrics(); // measuring DefaultLineHeight depends on formatter
+ heightTree = new HeightTree(newValue, DefaultLineHeight);
+ cachedElements = new TextViewCachedElements();
+ }
+ InvalidateMeasure(DispatcherPriority.Normal);
+ if (DocumentChanged != null)
+ DocumentChanged(this, EventArgs.Empty);
+ }
+
+ /// <summary>
+ /// Recreates the text formatter that is used internally
+ /// by calling <see cref="TextFormatterFactory.Create"/>.
+ /// </summary>
+ void RecreateTextFormatter()
+ {
+ if (formatter != null) {
+ formatter.Dispose();
+ formatter = TextFormatterFactory.Create(this);
+ Redraw();
+ }
+ }
+
+ void RecreateCachedElements()
+ {
+ if (cachedElements != null) {
+ cachedElements.Dispose();
+ cachedElements = new TextViewCachedElements();
+ }
+ }
+
+ /// <inheritdoc cref="IWeakEventListener.ReceiveWeakEvent"/>
+ protected virtual bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
+ {
+ if (managerType == typeof(TextDocumentWeakEventManager.Changing)) {
+ // TODO: put redraw into background so that other input events can be handled before the redraw.
+ // Unfortunately the "easy" approach (just use DispatcherPriority.Background) here makes the editor twice as slow because
+ // the caret position change forces an immediate redraw, and the text input then forces a background redraw.
+ // When fixing this, make sure performance on the SharpDevelop "type text in C# comment" stress test doesn't get significantly worse.
+ DocumentChangeEventArgs change = (DocumentChangeEventArgs)e;
+ Redraw(change.Offset, change.RemovalLength, DispatcherPriority.Normal);
+ return true;
+ } else if (managerType == typeof(PropertyChangedWeakEventManager)) {
+ OnOptionChanged((PropertyChangedEventArgs)e);
+ return true;
+ }
+ return false;
+ }
+
+ bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
+ {
+ return ReceiveWeakEvent(managerType, sender, e);
+ }
+ #endregion
+
+ #region Options property
+ /// <summary>
+ /// Options property.
+ /// </summary>
+ public static readonly DependencyProperty OptionsProperty =
+ DependencyProperty.Register("Options", typeof(TextEditorOptions), typeof(TextView),
+ new FrameworkPropertyMetadata(OnOptionsChanged));
+
+ /// <summary>
+ /// Gets/Sets the options used by the text editor.
+ /// </summary>
+ public TextEditorOptions Options {
+ get { return (TextEditorOptions)GetValue(OptionsProperty); }
+ set { SetValue(OptionsProperty, value); }
+ }
+
+ /// <summary>
+ /// Occurs when a text editor option has changed.
+ /// </summary>
+ public event PropertyChangedEventHandler OptionChanged;
+
+ /// <summary>
+ /// Raises the <see cref="OptionChanged"/> event.
+ /// </summary>
+ protected virtual void OnOptionChanged(PropertyChangedEventArgs e)
+ {
+ if (OptionChanged != null) {
+ OptionChanged(this, e);
+ }
+
+ if (Options.ShowColumnRuler)
+ columnRulerRenderer.SetRuler(Options.ColumnRulerPosition, ColumnRulerPen);
+ else
+ columnRulerRenderer.SetRuler(-1, ColumnRulerPen);
+
+ UpdateBuiltinElementGeneratorsFromOptions();
+ Redraw();
+ }
+
+ static void OnOptionsChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
+ {
+ ((TextView)dp).OnOptionsChanged((TextEditorOptions)e.OldValue, (TextEditorOptions)e.NewValue);
+ }
+
+ void OnOptionsChanged(TextEditorOptions oldValue, TextEditorOptions newValue)
+ {
+ if (oldValue != null) {
+ PropertyChangedWeakEventManager.RemoveListener(oldValue, this);
+ }
+ if (newValue != null) {
+ PropertyChangedWeakEventManager.AddListener(newValue, this);
+ }
+ OnOptionChanged(new PropertyChangedEventArgs(null));
+ }
+ #endregion
+
+ #region ElementGenerators+LineTransformers Properties
+ readonly ObserveAddRemoveCollection<VisualLineElementGenerator> elementGenerators;
+
+ /// <summary>
+ /// Gets a collection where element generators can be registered.
+ /// </summary>
+ public IList<VisualLineElementGenerator> ElementGenerators {
+ get { return elementGenerators; }
+ }
+
+ void ElementGenerator_Added(VisualLineElementGenerator generator)
+ {
+ ConnectToTextView(generator);
+ Redraw();
+ }
+
+ void ElementGenerator_Removed(VisualLineElementGenerator generator)
+ {
+ DisconnectFromTextView(generator);
+ Redraw();
+ }
+
+ readonly ObserveAddRemoveCollection<IVisualLineTransformer> lineTransformers;
+
+ /// <summary>
+ /// Gets a collection where line transformers can be registered.
+ /// </summary>
+ public IList<IVisualLineTransformer> LineTransformers {
+ get { return lineTransformers; }
+ }
+
+ void LineTransformer_Added(IVisualLineTransformer lineTransformer)
+ {
+ ConnectToTextView(lineTransformer);
+ Redraw();
+ }
+
+ void LineTransformer_Removed(IVisualLineTransformer lineTransformer)
+ {
+ DisconnectFromTextView(lineTransformer);
+ Redraw();
+ }
+ #endregion
+
+ #region Builtin ElementGenerators
+// NewLineElementGenerator newLineElementGenerator;
+ SingleCharacterElementGenerator singleCharacterElementGenerator;
+ LinkElementGenerator linkElementGenerator;
+ MailLinkElementGenerator mailLinkElementGenerator;
+
+ void UpdateBuiltinElementGeneratorsFromOptions()
+ {
+ TextEditorOptions options = this.Options;
+
+// AddRemoveDefaultElementGeneratorOnDemand(ref newLineElementGenerator, options.ShowEndOfLine);
+ AddRemoveDefaultElementGeneratorOnDemand(ref singleCharacterElementGenerator, options.ShowBoxForControlCharacters || options.ShowSpaces || options.ShowTabs);
+ AddRemoveDefaultElementGeneratorOnDemand(ref linkElementGenerator, options.EnableHyperlinks);
+ AddRemoveDefaultElementGeneratorOnDemand(ref mailLinkElementGenerator, options.EnableEmailHyperlinks);
+ }
+
+ void AddRemoveDefaultElementGeneratorOnDemand<T>(ref T generator, bool demand)
+ where T : VisualLineElementGenerator, IBuiltinElementGenerator, new()
+ {
+ bool hasGenerator = generator != null;
+ if (hasGenerator != demand) {
+ if (demand) {
+ generator = new T();
+ this.ElementGenerators.Add(generator);
+ } else {
+ this.ElementGenerators.Remove(generator);
+ generator = null;
+ }
+ }
+ if (generator != null)
+ generator.FetchOptions(this.Options);
+ }
+ #endregion
+
+ #region Layers
+ internal readonly TextLayer textLayer;
+ readonly LayerCollection layers;
+
+ /// <summary>
+ /// Gets the list of layers displayed in the text view.
+ /// </summary>
+ public UIElementCollection Layers {
+ get { return layers; }
+ }
+
+ sealed class LayerCollection : UIElementCollection
+ {
+ readonly TextView textView;
+
+ public LayerCollection(TextView textView)
+ : base(textView, textView)
+ {
+ this.textView = textView;
+ }
+
+ public override void Clear()
+ {
+ base.Clear();
+ textView.LayersChanged();
+ }
+
+ public override int Add(UIElement element)
+ {
+ int r = base.Add(element);
+ textView.LayersChanged();
+ return r;
+ }
+
+ public override void RemoveAt(int index)
+ {
+ base.RemoveAt(index);
+ textView.LayersChanged();
+ }
+
+ public override void RemoveRange(int index, int count)
+ {
+ base.RemoveRange(index, count);
+ textView.LayersChanged();
+ }
+ }
+
+ void LayersChanged()
+ {
+ textLayer.index = layers.IndexOf(textLayer);
+ }
+
+ /// <summary>
+ /// Inserts a new layer at a position specified relative to an existing layer.
+ /// </summary>
+ /// <param name="layer">The new layer to insert.</param>
+ /// <param name="referencedLayer">The existing layer</param>
+ /// <param name="position">Specifies whether the layer is inserted above,below, or replaces the referenced layer</param>
+ public void InsertLayer(UIElement layer, KnownLayer referencedLayer, LayerInsertionPosition position)
+ {
+ if (layer == null)
+ throw new ArgumentNullException("layer");
+ if (!Enum.IsDefined(typeof(KnownLayer), referencedLayer))
+ throw new InvalidEnumArgumentException("referencedLayer", (int)referencedLayer, typeof(KnownLayer));
+ if (!Enum.IsDefined(typeof(LayerInsertionPosition), position))
+ throw new InvalidEnumArgumentException("position", (int)position, typeof(LayerInsertionPosition));
+ if (referencedLayer == KnownLayer.Background && position != LayerInsertionPosition.Above)
+ throw new InvalidOperationException("Cannot replace or insert below the background layer.");
+
+ LayerPosition newPosition = new LayerPosition(referencedLayer, position);
+ LayerPosition.SetLayerPosition(layer, newPosition);
+ for (int i = 0; i < layers.Count; i++) {
+ LayerPosition p = LayerPosition.GetLayerPosition(layers[i]);
+ if (p != null) {
+ if (p.KnownLayer == referencedLayer && p.Position == LayerInsertionPosition.Replace) {
+ // found the referenced layer
+ switch (position) {
+ case LayerInsertionPosition.Below:
+ layers.Insert(i, layer);
+ return;
+ case LayerInsertionPosition.Above:
+ layers.Insert(i + 1, layer);
+ return;
+ case LayerInsertionPosition.Replace:
+ layers[i] = layer;
+ return;
+ }
+ } else if (p.KnownLayer == referencedLayer && p.Position == LayerInsertionPosition.Above
+ || p.KnownLayer > referencedLayer) {
+ // we skipped the insertion position (referenced layer does not exist?)
+ layers.Insert(i, layer);
+ return;
+ }
+ }
+ }
+ // inserting after all existing layers:
+ layers.Add(layer);
+ }
+
+ /// <inheritdoc/>
+ protected override int VisualChildrenCount {
+ get { return layers.Count + inlineObjects.Count; }
+ }
+
+ /// <inheritdoc/>
+ protected override Visual GetVisualChild(int index)
+ {
+ int cut = textLayer.index + 1;
+ if (index < cut)
+ return layers[index];
+ else if (index < cut + inlineObjects.Count)
+ return inlineObjects[index - cut].Element;
+ else
+ return layers[index - inlineObjects.Count];
+ }
+
+ /// <inheritdoc/>
+ protected override System.Collections.IEnumerator LogicalChildren {
+ get {
+ return inlineObjects.Select(io => io.Element).Concat(layers.Cast<UIElement>()).GetEnumerator();
+ }
+ }
+ #endregion
+
+ #region Inline object handling
+ List<InlineObjectRun> inlineObjects = new List<InlineObjectRun>();
+
+ /// <summary>
+ /// Adds a new inline object.
+ /// </summary>
+ 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
+ bool alreadyAdded = false;
+ for (int i = 0; i < inlineObjects.Count; i++) {
+ if (inlineObjects[i].Element == inlineObject.Element) {
+ RemoveInlineObjectRun(inlineObjects[i], true);
+ inlineObjects.RemoveAt(i);
+ alreadyAdded = true;
+ break;
+ }
+ }
+
+ inlineObjects.Add(inlineObject);
+ if (!alreadyAdded) {
+ AddVisualChild(inlineObject.Element);
+ }
+ inlineObject.Element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
+ inlineObject.desiredSize = inlineObject.Element.DesiredSize;
+ }
+
+ void MeasureInlineObjects()
+ {
+ // As part of MeasureOverride(), re-measure the inline objects
+ foreach (InlineObjectRun inlineObject in inlineObjects) {
+ if (inlineObject.VisualLine.IsDisposed) {
+ // Don't re-measure inline objects that are going to be removed anyways.
+ // If the inline object will be reused in a different VisualLine, we'll measure it in the AddInlineObject() call.
+ continue;
+ }
+ inlineObject.Element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
+ if (!inlineObject.Element.DesiredSize.IsClose(inlineObject.desiredSize)) {
+ // the element changed size -> recreate its parent visual line
+ inlineObject.desiredSize = inlineObject.Element.DesiredSize;
+ if (allVisualLines.Remove(inlineObject.VisualLine)) {
+ DisposeVisualLine(inlineObject.VisualLine);
+ }
+ }
+ }
+ }
+
+ List<VisualLine> visualLinesWithOutstandingInlineObjects = new List<VisualLine>();
+
+ void RemoveInlineObjects(VisualLine visualLine)
+ {
+ // Delay removing inline objects:
+ // A document change immediately invalidates affected visual lines, but it does not
+ // cause an immediate redraw.
+ // To prevent inline objects from flickering when they are recreated, we delay removing
+ // inline objects until the next redraw.
+ if (visualLine.hasInlineObjects) {
+ visualLinesWithOutstandingInlineObjects.Add(visualLine);
+ }
+ }
+
+ /// <summary>
+ /// Remove the inline objects that were marked for removal.
+ /// </summary>
+ void RemoveInlineObjectsNow()
+ {
+ if (visualLinesWithOutstandingInlineObjects.Count == 0)
+ return;
+ inlineObjects.RemoveAll(
+ ior => {
+ if (visualLinesWithOutstandingInlineObjects.Contains(ior.VisualLine)) {
+ RemoveInlineObjectRun(ior, false);
+ return true;
+ }
+ return false;
+ });
+ visualLinesWithOutstandingInlineObjects.Clear();
+ }
+
+ // Remove InlineObjectRun.Element from TextLayer.
+ // Caller of RemoveInlineObjectRun will remove it from inlineObjects collection.
+ void RemoveInlineObjectRun(InlineObjectRun ior, bool keepElement)
+ {
+ if (!keepElement && ior.Element.IsKeyboardFocusWithin) {
+ // When the inline element that has the focus is removed, WPF will reset the
+ // focus to the main window without raising appropriate LostKeyboardFocus events.
+ // To work around this, we manually set focus to the next focusable parent.
+ UIElement element = this;
+ while (element != null && !element.Focusable) {
+ element = VisualTreeHelper.GetParent(element) as UIElement;
+ }
+ if (element != null)
+ Keyboard.Focus(element);
+ }
+ ior.VisualLine = null;
+ if (!keepElement)
+ RemoveVisualChild(ior.Element);
+ }
+ #endregion
+
+ #region Brushes
+ /// <summary>
+ /// NonPrintableCharacterBrush dependency property.
+ /// </summary>
+ public static readonly DependencyProperty NonPrintableCharacterBrushProperty =
+ DependencyProperty.Register("NonPrintableCharacterBrush", typeof(Brush), typeof(TextView),
+ new FrameworkPropertyMetadata(Brushes.LightGray));
+
+ /// <summary>
+ /// Gets/sets the Brush used for displaying non-printable characters.
+ /// </summary>
+ public Brush NonPrintableCharacterBrush {
+ get { return (Brush)GetValue(NonPrintableCharacterBrushProperty); }
+ set { SetValue(NonPrintableCharacterBrushProperty, value); }
+ }
+
+ /// <summary>
+ /// LinkTextForegroundBrush dependency property.
+ /// </summary>
+ public static readonly DependencyProperty LinkTextForegroundBrushProperty =
+ DependencyProperty.Register("LinkTextForegroundBrush", typeof(Brush), typeof(TextView),
+ new FrameworkPropertyMetadata(Brushes.Blue));
+
+ /// <summary>
+ /// Gets/sets the Brush used for displaying link texts.
+ /// </summary>
+ public Brush LinkTextForegroundBrush {
+ get { return (Brush)GetValue(LinkTextForegroundBrushProperty); }
+ set { SetValue(LinkTextForegroundBrushProperty, value); }
+ }
+
+ /// <summary>
+ /// LinkTextBackgroundBrush dependency property.
+ /// </summary>
+ public static readonly DependencyProperty LinkTextBackgroundBrushProperty =
+ DependencyProperty.Register("LinkTextBackgroundBrush", typeof(Brush), typeof(TextView),
+ new FrameworkPropertyMetadata(Brushes.Transparent));
+
+ /// <summary>
+ /// Gets/sets the Brush used for the background of link texts.
+ /// </summary>
+ public Brush LinkTextBackgroundBrush {
+ get { return (Brush)GetValue(LinkTextBackgroundBrushProperty); }
+ set { SetValue(LinkTextBackgroundBrushProperty, value); }
+ }
+ #endregion
+
+ #region Redraw methods / VisualLine invalidation
+ /// <summary>
+ /// Causes the text editor to regenerate all visual lines.
+ /// </summary>
+ public void Redraw()
+ {
+ Redraw(DispatcherPriority.Normal);
+ }
+
+ /// <summary>
+ /// Causes the text editor to regenerate all visual lines.
+ /// </summary>
+ public void Redraw(DispatcherPriority redrawPriority)
+ {
+ VerifyAccess();
+ ClearVisualLines();
+ InvalidateMeasure(redrawPriority);
+ }
+
+ /// <summary>
+ /// Causes the text editor to regenerate the specified visual line.
+ /// </summary>
+ public void Redraw(VisualLine visualLine, DispatcherPriority redrawPriority = DispatcherPriority.Normal)
+ {
+ VerifyAccess();
+ if (allVisualLines.Remove(visualLine)) {
+ DisposeVisualLine(visualLine);
+ InvalidateMeasure(redrawPriority);
+ }
+ }
+
+ /// <summary>
+ /// Causes the text editor to redraw all lines overlapping with the specified segment.
+ /// </summary>
+ public void Redraw(int offset, int length, DispatcherPriority redrawPriority = DispatcherPriority.Normal)
+ {
+ VerifyAccess();
+ bool changedSomethingBeforeOrInLine = false;
+ for (int i = 0; i < allVisualLines.Count; i++) {
+ VisualLine visualLine = allVisualLines[i];
+ int lineStart = visualLine.FirstDocumentLine.Offset;
+ int lineEnd = visualLine.LastDocumentLine.Offset + visualLine.LastDocumentLine.TotalLength;
+ if (offset <= lineEnd) {
+ changedSomethingBeforeOrInLine = true;
+ if (offset + length >= lineStart) {
+ allVisualLines.RemoveAt(i--);
+ DisposeVisualLine(visualLine);
+ }
+ }
+ }
+ if (changedSomethingBeforeOrInLine) {
+ // Repaint not only when something in visible area was changed, but also when anything in front of it
+ // was changed. We might have to redraw the line number margin. Or the highlighting changed.
+ // However, we'll try to reuse the existing VisualLines.
+ InvalidateMeasure(redrawPriority);
+ }
+ }
+
+ /// <summary>
+ /// Causes a known layer to redraw.
+ /// This method does not invalidate visual lines;
+ /// use the <see cref="Redraw()"/> method to do that.
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "knownLayer",
+ Justification="This method is meant to invalidate only a specific layer - I just haven't figured out how to do that, yet.")]
+ public void InvalidateLayer(KnownLayer knownLayer)
+ {
+ InvalidateMeasure(DispatcherPriority.Normal);
+ }
+
+ /// <summary>
+ /// Causes a known layer to redraw.
+ /// This method does not invalidate visual lines;
+ /// use the <see cref="Redraw()"/> method to do that.
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "knownLayer",
+ Justification="This method is meant to invalidate only a specific layer - I just haven't figured out how to do that, yet.")]
+ public void InvalidateLayer(KnownLayer knownLayer, DispatcherPriority priority)
+ {
+ InvalidateMeasure(priority);
+ }
+
+ /// <summary>
+ /// Causes the text editor to redraw all lines overlapping with the specified segment.
+ /// Does nothing if segment is null.
+ /// </summary>
+ public void Redraw(ISegment segment, DispatcherPriority redrawPriority = DispatcherPriority.Normal)
+ {
+ if (segment != null) {
+ Redraw(segment.Offset, segment.Length, redrawPriority);
+ }
+ }
+
+ /// <summary>
+ /// Invalidates all visual lines.
+ /// The caller of ClearVisualLines() must also call InvalidateMeasure() to ensure
+ /// that the visual lines will be recreated.
+ /// </summary>
+ void ClearVisualLines()
+ {
+ visibleVisualLines = null;
+ if (allVisualLines.Count != 0) {
+ foreach (VisualLine visualLine in allVisualLines) {
+ DisposeVisualLine(visualLine);
+ }
+ allVisualLines.Clear();
+ }
+ }
+
+ void DisposeVisualLine(VisualLine visualLine)
+ {
+ if (newVisualLines != null && newVisualLines.Contains(visualLine)) {
+ throw new ArgumentException("Cannot dispose visual line because it is in construction!");
+ }
+ visibleVisualLines = null;
+ visualLine.Dispose();
+ RemoveInlineObjects(visualLine);
+ }
+ #endregion
+
+ #region InvalidateMeasure(DispatcherPriority)
+ DispatcherOperation invalidateMeasureOperation;
+
+ void InvalidateMeasure(DispatcherPriority priority)
+ {
+ if (priority >= DispatcherPriority.Render) {
+ if (invalidateMeasureOperation != null) {
+ invalidateMeasureOperation.Abort();
+ invalidateMeasureOperation = null;
+ }
+ base.InvalidateMeasure();
+ } else {
+ if (invalidateMeasureOperation != null) {
+ invalidateMeasureOperation.Priority = priority;
+ } else {
+ invalidateMeasureOperation = Dispatcher.BeginInvoke(
+ priority,
+ new Action(
+ delegate {
+ invalidateMeasureOperation = null;
+ base.InvalidateMeasure();
+ }
+ )
+ );
+ }
+ }
+ }
+ #endregion
+
+ #region Get(OrConstruct)VisualLine
+ /// <summary>
+ /// Gets the visual line that contains the document line with the specified number.
+ /// Returns null if the document line is outside the visible range.
+ /// </summary>
+ public VisualLine GetVisualLine(int documentLineNumber)
+ {
+ // TODO: EnsureVisualLines() ?
+ foreach (VisualLine visualLine in allVisualLines) {
+ Debug.Assert(visualLine.IsDisposed == false);
+ int start = visualLine.FirstDocumentLine.LineNumber;
+ int end = visualLine.LastDocumentLine.LineNumber;
+ if (documentLineNumber >= start && documentLineNumber <= end)
+ return visualLine;
+ }
+ return null;
+ }
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ public VisualLine GetOrConstructVisualLine(DocumentLine documentLine)
+ {
+ if (documentLine == null)
+ throw new ArgumentNullException("documentLine");
+ if (!this.Document.Lines.Contains(documentLine))
+ throw new InvalidOperationException("Line belongs to wrong document");
+ VerifyAccess();
+
+ VisualLine l = GetVisualLine(documentLine.LineNumber);
+ if (l == null) {
+ TextRunProperties globalTextRunProperties = CreateGlobalTextRunProperties();
+ VisualLineTextParagraphProperties paragraphProperties = CreateParagraphProperties(globalTextRunProperties);
+
+ while (heightTree.GetIsCollapsed(documentLine.LineNumber)) {
+ documentLine = documentLine.PreviousLine;
+ }
+
+ l = BuildVisualLine(documentLine,
+ globalTextRunProperties, paragraphProperties,
+ elementGenerators.ToArray(), lineTransformers.ToArray(),
+ lastAvailableSize);
+ allVisualLines.Add(l);
+ // update all visual top values (building the line might have changed visual top of other lines due to word wrapping)
+ foreach (var line in allVisualLines) {
+ line.VisualTop = heightTree.GetVisualPosition(line.FirstDocumentLine);
+ }
+ }
+ return l;
+ }
+ #endregion
+
+ #region Visual Lines (fields and properties)
+ List<VisualLine> allVisualLines = new List<VisualLine>();
+ ReadOnlyCollection<VisualLine> visibleVisualLines;
+ double clippedPixelsOnTop;
+ List<VisualLine> newVisualLines;
+
+ /// <summary>
+ /// Gets the currently visible visual lines.
+ /// </summary>
+ /// <exception cref="VisualLinesInvalidException">
+ /// Gets thrown if there are invalid visual lines when this property is accessed.
+ /// You can use the <see cref="VisualLinesValid"/> property to check for this case,
+ /// or use the <see cref="EnsureVisualLines()"/> method to force creating the visual lines
+ /// when they are invalid.
+ /// </exception>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations")]
+ public ReadOnlyCollection<VisualLine> VisualLines {
+ get {
+ if (visibleVisualLines == null)
+ {
+ return new ReadOnlyCollection<VisualLine>(new List<VisualLine>());
+ }
+ else
+ {
+ return visibleVisualLines;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets whether the visual lines are valid.
+ /// Will return false after a call to Redraw().
+ /// Accessing the visual lines property will cause a <see cref="VisualLinesInvalidException"/>
+ /// if this property is <c>false</c>.
+ /// </summary>
+ public bool VisualLinesValid {
+ get { return visibleVisualLines != null; }
+ }
+
+ /// <summary>
+ /// Occurs when the TextView is about to be measured and will regenerate its visual lines.
+ /// This event may be used to mark visual lines as invalid that would otherwise be reused.
+ /// </summary>
+ public event EventHandler<VisualLineConstructionStartEventArgs> VisualLineConstructionStarting;
+
+ /// <summary>
+ /// Occurs when the TextView was measured and changed its visual lines.
+ /// </summary>
+ public event EventHandler VisualLinesChanged;
+
+ /// <summary>
+ /// If the visual lines are invalid, creates new visual lines for the visible part
+ /// of the document.
+ /// If all visual lines are valid, this method does nothing.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">The visual line build process is already running.
+ /// It is not allowed to call this method during the construction of a visual line.</exception>
+ public void EnsureVisualLines()
+ {
+ Dispatcher.VerifyAccess();
+ if (inMeasure)
+ throw new InvalidOperationException("The visual line build process is already running! Cannot EnsureVisualLines() during Measure!");
+ if (!VisualLinesValid) {
+ // increase priority for re-measure
+ InvalidateMeasure(DispatcherPriority.Normal);
+ // force immediate re-measure
+ UpdateLayout();
+ }
+ // Sometimes we still have invalid lines after UpdateLayout - work around the problem
+ // by calling MeasureOverride directly.
+ if (!VisualLinesValid) {
+ //Debug.WriteLine("UpdateLayout() failed in EnsureVisualLines");
+ //MeasureOverride(lastAvailableSize);
+ // UpdateLayout();
+ }
+ //if (!VisualLinesValid)
+ //throw new VisualLinesInvalidException("Internal error: visual lines invalid after EnsureVisualLines call");
+ }
+ #endregion
+
+ #region Measure
+ /// <summary>
+ /// 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.
+ /// </summary>
+ const double AdditionalHorizontalScrollAmount = 3;
+
+ Size lastAvailableSize;
+ bool inMeasure;
+
+ /// <inheritdoc/>
+ 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.Width = 32000;
+
+ if (!canHorizontallyScroll && !availableSize.Width.IsClose(lastAvailableSize.Width))
+ ClearVisualLines();
+ lastAvailableSize = availableSize;
+
+ foreach (UIElement layer in layers) {
+ layer.Measure(availableSize);
+ }
+ MeasureInlineObjects();
+
+ InvalidateVisual(); // = InvalidateArrange+InvalidateRender
+
+ double maxWidth;
+ if (document == null) {
+ // no document -> create empty list of lines
+ allVisualLines = new List<VisualLine>();
+ visibleVisualLines = allVisualLines.AsReadOnly();
+ maxWidth = 0;
+ } else {
+ inMeasure = true;
+ try {
+ maxWidth = CreateAndMeasureVisualLines(availableSize);
+ } finally {
+ inMeasure = false;
+ }
+ }
+
+ // remove inline objects only at the end, so that inline objects that were re-used are not removed from the editor
+ RemoveInlineObjectsNow();
+
+ maxWidth += AdditionalHorizontalScrollAmount;
+ double heightTreeHeight = this.DocumentHeight;
+ TextEditorOptions options = this.Options;
+ if (options.AllowScrollBelowDocument) {
+ if (!double.IsInfinity(scrollViewport.Height)) {
+ heightTreeHeight = Math.Max(heightTreeHeight, Math.Min(heightTreeHeight - 50, scrollOffset.Y) + scrollViewport.Height);
+ }
+ }
+
+ textLayer.SetVisualLines(visibleVisualLines);
+
+ SetScrollData(availableSize,
+ new Size(maxWidth, heightTreeHeight),
+ scrollOffset);
+ if (VisualLinesChanged != null)
+ VisualLinesChanged(this, EventArgs.Empty);
+
+ return new Size(Math.Min(availableSize.Width, maxWidth), Math.Min(availableSize.Height, heightTreeHeight));
+ }
+
+ /// <summary>
+ /// Build all VisualLines in the visible range.
+ /// </summary>
+ /// <returns>Width the longest line</returns>
+ double CreateAndMeasureVisualLines(Size availableSize)
+ {
+ TextRunProperties globalTextRunProperties = CreateGlobalTextRunProperties();
+ VisualLineTextParagraphProperties paragraphProperties = CreateParagraphProperties(globalTextRunProperties);
+
+ //Debug.WriteLine("Measure availableSize=" + availableSize + ", scrollOffset=" + scrollOffset);
+ var firstLineInView = heightTree.GetLineByVisualPosition(scrollOffset.Y);
+
+ // number of pixels clipped from the first visual line(s)
+ clippedPixelsOnTop = scrollOffset.Y - heightTree.GetVisualPosition(firstLineInView);
+ // clippedPixelsOnTop should be >= 0, except for floating point inaccurracy.
+ Debug.Assert(clippedPixelsOnTop >= -ExtensionMethods.Epsilon);
+
+ newVisualLines = new List<VisualLine>();
+
+ if (VisualLineConstructionStarting != null)
+ VisualLineConstructionStarting(this, new VisualLineConstructionStartEventArgs(firstLineInView));
+
+ var elementGeneratorsArray = elementGenerators.ToArray();
+ var lineTransformersArray = lineTransformers.ToArray();
+ var nextLine = firstLineInView;
+ double maxWidth = 0;
+ double yPos = -clippedPixelsOnTop;
+ while (yPos < availableSize.Height && nextLine != null) {
+ VisualLine visualLine = GetVisualLine(nextLine.LineNumber);
+ if (visualLine == null) {
+ visualLine = BuildVisualLine(nextLine,
+ globalTextRunProperties, paragraphProperties,
+ elementGeneratorsArray, lineTransformersArray,
+ availableSize);
+ }
+
+ visualLine.VisualTop = scrollOffset.Y + yPos;
+
+ nextLine = visualLine.LastDocumentLine.NextLine;
+
+ yPos += visualLine.Height;
+
+ foreach (TextLine textLine in visualLine.TextLines) {
+ if (textLine.WidthIncludingTrailingWhitespace > maxWidth)
+ maxWidth = textLine.WidthIncludingTrailingWhitespace;
+ }
+
+ newVisualLines.Add(visualLine);
+ }
+
+ foreach (VisualLine line in allVisualLines) {
+ Debug.Assert(line.IsDisposed == false);
+ if (!newVisualLines.Contains(line))
+ DisposeVisualLine(line);
+ }
+
+ allVisualLines = newVisualLines;
+ // visibleVisualLines = readonly copy of visual lines
+ visibleVisualLines = new ReadOnlyCollection<VisualLine>(newVisualLines.ToArray());
+ newVisualLines = null;
+
+ if (allVisualLines.Any(line => line.IsDisposed)) {
+ throw new InvalidOperationException("A visual line was disposed even though it is still in use.\n" +
+ "This can happen when Redraw() is called during measure for lines " +
+ "that are already constructed.");
+ }
+ return maxWidth;
+ }
+ #endregion
+
+ #region BuildVisualLine
+ TextFormatter formatter;
+ internal TextViewCachedElements cachedElements;
+
+ TextRunProperties CreateGlobalTextRunProperties()
+ {
+ var p = new GlobalTextRunProperties();
+ p.typeface = this.CreateTypeface();
+ p.fontRenderingEmSize = FontSize;
+ p.foregroundBrush = (Brush)GetValue(Control.ForegroundProperty);
+ ExtensionMethods.CheckIsFrozen(p.foregroundBrush);
+ p.cultureInfo = CultureInfo.CurrentCulture;
+ return p;
+ }
+
+ VisualLineTextParagraphProperties CreateParagraphProperties(TextRunProperties defaultTextRunProperties)
+ {
+ return new VisualLineTextParagraphProperties {
+ defaultTextRunProperties = defaultTextRunProperties,
+ textWrapping = canHorizontallyScroll ? TextWrapping.NoWrap : TextWrapping.Wrap,
+ tabSize = Options.IndentationSize * WideSpaceWidth
+ };
+ }
+
+ VisualLine BuildVisualLine(DocumentLine documentLine,
+ TextRunProperties globalTextRunProperties,
+ VisualLineTextParagraphProperties paragraphProperties,
+ VisualLineElementGenerator[] elementGeneratorsArray,
+ IVisualLineTransformer[] lineTransformersArray,
+ Size availableSize)
+ {
+ if (heightTree.GetIsCollapsed(documentLine.LineNumber))
+ throw new InvalidOperationException("Trying to build visual line from collapsed line");
+
+ //Debug.WriteLine("Building line " + documentLine.LineNumber);
+
+ VisualLine visualLine = new VisualLine(this, documentLine);
+ VisualLineTextSource textSource = new VisualLineTextSource(visualLine) {
+ Document = document,
+ GlobalTextRunProperties = globalTextRunProperties,
+ TextView = this
+ };
+
+ visualLine.ConstructVisualElements(textSource, elementGeneratorsArray);
+
+ if (visualLine.FirstDocumentLine != visualLine.LastDocumentLine) {
+ // Check whether the lines are collapsed correctly:
+ double firstLinePos = heightTree.GetVisualPosition(visualLine.FirstDocumentLine.NextLine);
+ double lastLinePos = heightTree.GetVisualPosition(visualLine.LastDocumentLine.NextLine ?? visualLine.LastDocumentLine);
+ if (!firstLinePos.IsClose(lastLinePos)) {
+ for (int i = visualLine.FirstDocumentLine.LineNumber + 1; i <= visualLine.LastDocumentLine.LineNumber; i++) {
+ if (!heightTree.GetIsCollapsed(i))
+ throw new InvalidOperationException("Line " + i + " was skipped by a VisualLineElementGenerator, but it is not collapsed.");
+ }
+ throw new InvalidOperationException("All lines collapsed but visual pos different - height tree inconsistency?");
+ }
+ }
+
+ visualLine.RunTransformers(textSource, lineTransformersArray);
+
+ // now construct textLines:
+ int textOffset = 0;
+ TextLineBreak lastLineBreak = null;
+ var textLines = new List<TextLine>();
+ paragraphProperties.indent = 0;
+ paragraphProperties.firstLineInParagraph = true;
+ while (textOffset <= visualLine.VisualLengthWithEndOfLineMarker) {
+ TextLine textLine = formatter.FormatLine(
+ textSource,
+ textOffset,
+ availableSize.Width,
+ paragraphProperties,
+ lastLineBreak
+ );
+ textLines.Add(textLine);
+ textOffset += textLine.Length;
+
+ // exit loop so that we don't do the indentation calculation if there's only a single line
+ if (textOffset >= visualLine.VisualLengthWithEndOfLineMarker)
+ break;
+
+ if (paragraphProperties.firstLineInParagraph) {
+ paragraphProperties.firstLineInParagraph = false;
+
+ 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.GetTextLineBreak();
+ }
+ visualLine.SetTextLines(textLines);
+ heightTree.SetHeight(visualLine.FirstDocumentLine, visualLine.Height);
+ return visualLine;
+ }
+
+ static int GetIndentationVisualColumn(VisualLine visualLine)
+ {
+ if (visualLine.Elements.Count == 0)
+ return 0;
+ int column = 0;
+ int elementIndex = 0;
+ VisualLineElement element = visualLine.Elements[elementIndex];
+ while (element.IsWhitespace(column)) {
+ column++;
+ if (column == element.VisualColumn + element.VisualLength) {
+ elementIndex++;
+ if (elementIndex == visualLine.Elements.Count)
+ break;
+ element = visualLine.Elements[elementIndex];
+ }
+ }
+ return column;
+ }
+ #endregion
+
+ #region Arrange
+ /// <summary>
+ /// Arrange implementation.
+ /// </summary>
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ EnsureVisualLines();
+
+ foreach (UIElement layer in layers) {
+ layer.Arrange(new Rect(new Point(0, 0), finalSize));
+ }
+
+ if (document == null || allVisualLines.Count == 0)
+ return finalSize;
+
+ // validate scroll position
+ Vector newScrollOffset = scrollOffset;
+ if (scrollOffset.X + finalSize.Width > scrollExtent.Width) {
+ newScrollOffset.X = Math.Max(0, scrollExtent.Width - finalSize.Width);
+ }
+ if (scrollOffset.Y + finalSize.Height > scrollExtent.Height) {
+ newScrollOffset.Y = Math.Max(0, scrollExtent.Height - finalSize.Height);
+ }
+ if (SetScrollData(scrollViewport, scrollExtent, newScrollOffset))
+ InvalidateMeasure(DispatcherPriority.Normal);
+
+ //Debug.WriteLine("Arrange finalSize=" + finalSize + ", scrollOffset=" + scrollOffset);
+
+// double maxWidth = 0;
+
+ if (visibleVisualLines != null) {
+ Point pos = new Point(-scrollOffset.X, -clippedPixelsOnTop);
+ foreach (VisualLine visualLine in visibleVisualLines) {
+ int offset = 0;
+ foreach (TextLine textLine in visualLine.TextLines) {
+ foreach (var span in textLine.GetTextRunSpans()) {
+ InlineObjectRun inline = span.Value as InlineObjectRun;
+ if (inline != null && inline.VisualLine != null) {
+ Debug.Assert(inlineObjects.Contains(inline));
+ double distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(offset, 0));
+ inline.Element.Arrange(new Rect(new Point(pos.X + distance, pos.Y), inline.Element.DesiredSize));
+ }
+ offset += span.Length;
+ }
+ pos.Y += textLine.Height;
+ }
+ }
+ }
+ InvalidateCursor();
+
+ return finalSize;
+ }
+ #endregion
+
+ #region Render
+ readonly ObserveAddRemoveCollection<IBackgroundRenderer> backgroundRenderers;
+
+ /// <summary>
+ /// Gets the list of background renderers.
+ /// </summary>
+ public IList<IBackgroundRenderer> BackgroundRenderers {
+ get { return backgroundRenderers; }
+ }
+
+ void BackgroundRenderer_Added(IBackgroundRenderer renderer)
+ {
+ ConnectToTextView(renderer);
+ InvalidateLayer(renderer.Layer);
+ }
+
+ void BackgroundRenderer_Removed(IBackgroundRenderer renderer)
+ {
+ DisconnectFromTextView(renderer);
+ InvalidateLayer(renderer.Layer);
+ }
+
+ /// <inheritdoc/>
+ protected override void OnRender(DrawingContext drawingContext)
+ {
+ RenderBackground(drawingContext, KnownLayer.Background);
+ foreach (var line in visibleVisualLines) {
+ Brush currentBrush = null;
+ int startVC = 0;
+ int length = 0;
+ foreach (var element in line.Elements) {
+ if (currentBrush == null || !currentBrush.Equals(element.BackgroundBrush)) {
+ if (currentBrush != null) {
+ BackgroundGeometryBuilder builder = new BackgroundGeometryBuilder();
+ builder.AlignToWholePixels = true;
+ builder.CornerRadius = 3;
+ foreach (var rect in BackgroundGeometryBuilder.GetRectsFromVisualSegment(this, line, startVC, startVC + length))
+ builder.AddRectangle(this, rect);
+ Geometry geometry = builder.CreateGeometry();
+ if (geometry != null) {
+ drawingContext.DrawGeometry(currentBrush, null, geometry);
+ }
+ }
+ startVC = element.VisualColumn;
+ length = element.DocumentLength;
+ currentBrush = element.BackgroundBrush;
+ } else {
+ length += element.VisualLength;
+ }
+ }
+ if (currentBrush != null) {
+ BackgroundGeometryBuilder builder = new BackgroundGeometryBuilder();
+ builder.AlignToWholePixels = true;
+ builder.CornerRadius = 3;
+ foreach (var rect in BackgroundGeometryBuilder.GetRectsFromVisualSegment(this, line, startVC, startVC + length))
+ builder.AddRectangle(this, rect);
+ Geometry geometry = builder.CreateGeometry();
+ if (geometry != null) {
+ drawingContext.DrawGeometry(currentBrush, null, geometry);
+ }
+ }
+ }
+ }
+
+ internal void RenderBackground(DrawingContext drawingContext, KnownLayer layer)
+ {
+ foreach (IBackgroundRenderer bg in backgroundRenderers) {
+ if (bg.Layer == layer) {
+ bg.Draw(this, drawingContext);
+ }
+ }
+ }
+
+ internal void ArrangeTextLayer(IList<VisualLineDrawingVisual> visuals)
+ {
+ Point pos = new Point(-scrollOffset.X, -clippedPixelsOnTop);
+ foreach (VisualLineDrawingVisual visual in visuals) {
+ TranslateTransform t = visual.Transform as TranslateTransform;
+ if (t == null || t.X != pos.X || t.Y != pos.Y) {
+ visual.Transform = new TranslateTransform(pos.X, pos.Y);
+ visual.Transform.Freeze();
+ }
+ pos.Y += visual.Height;
+ }
+ }
+ #endregion
+
+ #region IScrollInfo implementation
+ /// <summary>
+ /// Size of the document, in pixels.
+ /// </summary>
+ Size scrollExtent;
+
+ /// <summary>
+ /// Offset of the scroll position.
+ /// </summary>
+ Vector scrollOffset;
+
+ /// <summary>
+ /// Size of the viewport.
+ /// </summary>
+ Size scrollViewport;
+
+ void ClearScrollData()
+ {
+ SetScrollData(new Size(), new Size(), new Vector());
+ }
+
+ bool SetScrollData(Size viewport, Size extent, Vector offset)
+ {
+ if (!(viewport.IsClose(this.scrollViewport)
+ && extent.IsClose(this.scrollExtent)
+ && offset.IsClose(this.scrollOffset)))
+ {
+ this.scrollViewport = viewport;
+ this.scrollExtent = extent;
+ SetScrollOffset(offset);
+ this.OnScrollChange();
+ return true;
+ }
+ return false;
+ }
+
+ void OnScrollChange()
+ {
+ ScrollViewer scrollOwner = ((IScrollInfo)this).ScrollOwner;
+ if (scrollOwner != null) {
+ scrollOwner.InvalidateScrollInfo();
+ }
+ }
+
+ bool canVerticallyScroll;
+ bool IScrollInfo.CanVerticallyScroll {
+ get { return canVerticallyScroll; }
+ set {
+ if (canVerticallyScroll != value) {
+ canVerticallyScroll = value;
+ InvalidateMeasure(DispatcherPriority.Normal);
+ }
+ }
+ }
+ bool canHorizontallyScroll;
+ bool IScrollInfo.CanHorizontallyScroll {
+ get { return canHorizontallyScroll; }
+ set {
+ if (canHorizontallyScroll != value) {
+ canHorizontallyScroll = value;
+ ClearVisualLines();
+ InvalidateMeasure(DispatcherPriority.Normal);
+ }
+ }
+ }
+
+ double IScrollInfo.ExtentWidth {
+ get { return scrollExtent.Width; }
+ }
+
+ double IScrollInfo.ExtentHeight {
+ get { return scrollExtent.Height; }
+ }
+
+ double IScrollInfo.ViewportWidth {
+ get { return scrollViewport.Width; }
+ }
+
+ double IScrollInfo.ViewportHeight {
+ get { return scrollViewport.Height; }
+ }
+
+ /// <summary>
+ /// Gets the horizontal scroll offset.
+ /// </summary>
+ public double HorizontalOffset {
+ get { return scrollOffset.X; }
+ }
+
+ /// <summary>
+ /// Gets the vertical scroll offset.
+ /// </summary>
+ public double VerticalOffset {
+ get { return scrollOffset.Y; }
+ }
+
+ /// <summary>
+ /// Gets the scroll offset;
+ /// </summary>
+ public Vector ScrollOffset {
+ get { return scrollOffset; }
+ }
+
+ /// <summary>
+ /// Occurs when the scroll offset has changed.
+ /// </summary>
+ public event EventHandler ScrollOffsetChanged;
+
+ void SetScrollOffset(Vector vector)
+ {
+ if (!canHorizontallyScroll)
+ vector.X = 0;
+ if (!canVerticallyScroll)
+ vector.Y = 0;
+
+ if (!scrollOffset.IsClose(vector)) {
+ scrollOffset = vector;
+ if (ScrollOffsetChanged != null)
+ ScrollOffsetChanged(this, EventArgs.Empty);
+ }
+ }
+
+ ScrollViewer IScrollInfo.ScrollOwner { get; set; }
+
+ void IScrollInfo.LineUp()
+ {
+ ((IScrollInfo)this).SetVerticalOffset(scrollOffset.Y - DefaultLineHeight);
+ }
+
+ void IScrollInfo.LineDown()
+ {
+ ((IScrollInfo)this).SetVerticalOffset(scrollOffset.Y + DefaultLineHeight);
+ }
+
+ void IScrollInfo.LineLeft()
+ {
+ ((IScrollInfo)this).SetHorizontalOffset(scrollOffset.X - WideSpaceWidth);
+ }
+
+ void IScrollInfo.LineRight()
+ {
+ ((IScrollInfo)this).SetHorizontalOffset(scrollOffset.X + WideSpaceWidth);
+ }
+
+ void IScrollInfo.PageUp()
+ {
+ ((IScrollInfo)this).SetVerticalOffset(scrollOffset.Y - scrollViewport.Height);
+ }
+
+ void IScrollInfo.PageDown()
+ {
+ ((IScrollInfo)this).SetVerticalOffset(scrollOffset.Y + scrollViewport.Height);
+ }
+
+ void IScrollInfo.PageLeft()
+ {
+ ((IScrollInfo)this).SetHorizontalOffset(scrollOffset.X - scrollViewport.Width);
+ }
+
+ void IScrollInfo.PageRight()
+ {
+ ((IScrollInfo)this).SetHorizontalOffset(scrollOffset.X + scrollViewport.Width);
+ }
+
+ void IScrollInfo.MouseWheelUp()
+ {
+ ((IScrollInfo)this).SetVerticalOffset(
+ scrollOffset.Y - (SystemParameters.WheelScrollLines * DefaultLineHeight));
+ OnScrollChange();
+ }
+
+ void IScrollInfo.MouseWheelDown()
+ {
+ ((IScrollInfo)this).SetVerticalOffset(
+ scrollOffset.Y + (SystemParameters.WheelScrollLines * DefaultLineHeight));
+ OnScrollChange();
+ }
+
+ void IScrollInfo.MouseWheelLeft()
+ {
+ ((IScrollInfo)this).SetHorizontalOffset(
+ scrollOffset.X - (SystemParameters.WheelScrollLines * WideSpaceWidth));
+ OnScrollChange();
+ }
+
+ void IScrollInfo.MouseWheelRight()
+ {
+ ((IScrollInfo)this).SetHorizontalOffset(
+ scrollOffset.X + (SystemParameters.WheelScrollLines * WideSpaceWidth));
+ OnScrollChange();
+ }
+
+ bool defaultTextMetricsValid;
+ double wideSpaceWidth; // Width of an 'x'. Used as basis for the tab width, and for scrolling.
+ double defaultLineHeight; // Height of a line containing 'x'. Used for scrolling.
+ double defaultBaseline; // Baseline of a line containing 'x'. Used for TextTop/TextBottom calculation.
+
+ /// <summary>
+ /// Gets the width of a 'wide space' (the space width used for calculating the tab size).
+ /// </summary>
+ /// <remarks>
+ /// This is the width of an 'x' in the current font.
+ /// We do not measure the width of an actual space as that would lead to tiny tabs in
+ /// some proportional fonts.
+ /// For monospaced fonts, this property will return the expected value, as 'x' and ' ' have the same width.
+ /// </remarks>
+ public double WideSpaceWidth {
+ get {
+ CalculateDefaultTextMetrics();
+ return wideSpaceWidth;
+ }
+ }
+
+ /// <summary>
+ /// Gets the default line height. This is the height of an empty line or a line containing regular text.
+ /// Lines that include formatted text or custom UI elements may have a different line height.
+ /// </summary>
+ public double DefaultLineHeight {
+ get {
+ CalculateDefaultTextMetrics();
+ return defaultLineHeight;
+ }
+ }
+
+ /// <summary>
+ /// Gets the default baseline position. This is the difference between <see cref="VisualYPosition.TextTop"/>
+ /// and <see cref="VisualYPosition.Baseline"/> for a line containing regular text.
+ /// Lines that include formatted text or custom UI elements may have a different baseline.
+ /// </summary>
+ public double DefaultBaseline {
+ get {
+ CalculateDefaultTextMetrics();
+ return defaultBaseline;
+ }
+ }
+
+ void InvalidateDefaultTextMetrics()
+ {
+ defaultTextMetricsValid = false;
+ if (heightTree != null) {
+ // calculate immediately so that height tree gets updated
+ CalculateDefaultTextMetrics();
+ }
+ }
+
+ void CalculateDefaultTextMetrics()
+ {
+ if (defaultTextMetricsValid)
+ return;
+ defaultTextMetricsValid = true;
+ if (formatter != null) {
+ var textRunProperties = CreateGlobalTextRunProperties();
+ using (var line = formatter.FormatLine(
+ new SimpleTextSource("x", textRunProperties),
+ 0, 32000,
+ new VisualLineTextParagraphProperties { defaultTextRunProperties = textRunProperties },
+ null))
+ {
+ wideSpaceWidth = Math.Max(1, line.WidthIncludingTrailingWhitespace);
+ defaultBaseline = Math.Max(1, line.Baseline);
+ defaultLineHeight = Math.Max(1, line.Height);
+ }
+ } else {
+ wideSpaceWidth = FontSize / 2;
+ defaultBaseline = FontSize;
+ defaultLineHeight = FontSize + 3;
+ }
+ // Update heightTree.DefaultLineHeight, if a document is loaded.
+ if (heightTree != null)
+ heightTree.DefaultLineHeight = defaultLineHeight;
+ }
+
+ static double ValidateVisualOffset(double offset)
+ {
+ if (double.IsNaN(offset))
+ throw new ArgumentException("offset must not be NaN");
+ if (offset < 0)
+ return 0;
+ else
+ return offset;
+ }
+
+ void IScrollInfo.SetHorizontalOffset(double offset)
+ {
+ offset = ValidateVisualOffset(offset);
+ if (!scrollOffset.X.IsClose(offset)) {
+ SetScrollOffset(new Vector(offset, scrollOffset.Y));
+ InvalidateVisual();
+ textLayer.InvalidateVisual();
+ }
+ }
+
+ void IScrollInfo.SetVerticalOffset(double offset)
+ {
+ offset = ValidateVisualOffset(offset);
+ if (!scrollOffset.Y.IsClose(offset)) {
+ SetScrollOffset(new Vector(scrollOffset.X, offset));
+ InvalidateMeasure(DispatcherPriority.Normal);
+ }
+ }
+
+ Rect IScrollInfo.MakeVisible(Visual visual, Rect rectangle)
+ {
+ if (rectangle.IsEmpty || visual == null || visual == this || !this.IsAncestorOf(visual)) {
+ return Rect.Empty;
+ }
+ // Convert rectangle into our coordinate space.
+ GeneralTransform childTransform = visual.TransformToAncestor(this);
+ rectangle = childTransform.TransformBounds(rectangle);
+
+ MakeVisible(Rect.Offset(rectangle, scrollOffset));
+
+ return rectangle;
+ }
+
+ /// <summary>
+ /// Scrolls the text view so that the specified rectangle gets visible.
+ /// </summary>
+ public void MakeVisible(Rect rectangle)
+ {
+ Rect visibleRectangle = new Rect(scrollOffset.X, scrollOffset.Y,
+ scrollViewport.Width, scrollViewport.Height);
+ Vector newScrollOffset = scrollOffset;
+ if (rectangle.Left < visibleRectangle.Left) {
+ if (rectangle.Right > visibleRectangle.Right) {
+ newScrollOffset.X = rectangle.Left + rectangle.Width / 2;
+ } else {
+ newScrollOffset.X = rectangle.Left;
+ }
+ } else if (rectangle.Right > visibleRectangle.Right) {
+ newScrollOffset.X = rectangle.Right - scrollViewport.Width;
+ }
+ if (rectangle.Top < visibleRectangle.Top) {
+ if (rectangle.Bottom > visibleRectangle.Bottom) {
+ newScrollOffset.Y = rectangle.Top + rectangle.Height / 2;
+ } else {
+ newScrollOffset.Y = rectangle.Top;
+ }
+ } else if (rectangle.Bottom > visibleRectangle.Bottom) {
+ newScrollOffset.Y = rectangle.Bottom - scrollViewport.Height;
+ }
+ newScrollOffset.X = ValidateVisualOffset(newScrollOffset.X);
+ newScrollOffset.Y = ValidateVisualOffset(newScrollOffset.Y);
+ if (!scrollOffset.IsClose(newScrollOffset)) {
+ SetScrollOffset(newScrollOffset);
+ this.OnScrollChange();
+ InvalidateMeasure(DispatcherPriority.Normal);
+ }
+ }
+ #endregion
+
+ #region Visual element mouse handling
+ /// <inheritdoc/>
+ protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
+ {
+ // accept clicks even where the text area draws no background
+ return new PointHitTestResult(this, hitTestParameters.HitPoint);
+ }
+
+ [ThreadStatic] static bool invalidCursor;
+
+ /// <summary>
+ /// Updates the mouse cursor by calling <see cref="Mouse.UpdateCursor"/>, but with input priority.
+ /// </summary>
+ public static void InvalidateCursor()
+ {
+ if (!invalidCursor) {
+ invalidCursor = true;
+ Dispatcher.CurrentDispatcher.BeginInvoke(
+ DispatcherPriority.Input,
+ new Action(
+ delegate {
+ invalidCursor = false;
+ Mouse.UpdateCursor();
+ }));
+ }
+ }
+
+ /// <inheritdoc/>
+ protected override void OnQueryCursor(QueryCursorEventArgs e)
+ {
+ VisualLineElement element = GetVisualLineElementFromPosition(e.GetPosition(this) + scrollOffset);
+ if (element != null) {
+ element.OnQueryCursor(e);
+ }
+ }
+
+ /// <inheritdoc/>
+ protected override void OnMouseDown(MouseButtonEventArgs e)
+ {
+ base.OnMouseDown(e);
+ if (!e.Handled) {
+ EnsureVisualLines();
+ VisualLineElement element = GetVisualLineElementFromPosition(e.GetPosition(this) + scrollOffset);
+ if (element != null) {
+ element.OnMouseDown(e);
+ }
+ }
+ }
+
+ /// <inheritdoc/>
+ protected override void OnMouseUp(MouseButtonEventArgs e)
+ {
+ base.OnMouseUp(e);
+ if (!e.Handled) {
+ EnsureVisualLines();
+ VisualLineElement element = GetVisualLineElementFromPosition(e.GetPosition(this) + scrollOffset);
+ if (element != null) {
+ element.OnMouseUp(e);
+ }
+ }
+ }
+ #endregion
+
+ #region Getting elements from Visual Position
+ /// <summary>
+ /// Gets the visual line at the specified document position (relative to start of document).
+ /// Returns null if there is no visual line for the position (e.g. the position is outside the visible
+ /// text area).
+ /// </summary>
+ public VisualLine GetVisualLineFromVisualTop(double visualTop)
+ {
+ // TODO: change this method to also work outside the visible range -
+ // required to make GetPosition work as expected!
+ EnsureVisualLines();
+ foreach (VisualLine vl in this.VisualLines) {
+ if (visualTop < vl.VisualTop)
+ continue;
+ if (visualTop < vl.VisualTop + vl.Height)
+ return vl;
+ }
+ return null;
+ }
+
+ /// <summary>
+ /// Gets the visual top position (relative to start of document) from a document line number.
+ /// </summary>
+ public double GetVisualTopByDocumentLine(int line)
+ {
+ VerifyAccess();
+ if (heightTree == null)
+ throw ThrowUtil.NoDocumentAssigned();
+ return heightTree.GetVisualPosition(heightTree.GetLineByNumber(line));
+ }
+
+ VisualLineElement GetVisualLineElementFromPosition(Point visualPosition)
+ {
+ VisualLine vl = GetVisualLineFromVisualTop(visualPosition.Y);
+ if (vl != null) {
+ int column = vl.GetVisualColumnFloor(visualPosition);
+// Debug.WriteLine(vl.FirstDocumentLine.LineNumber + " vc " + column);
+ foreach (VisualLineElement element in vl.Elements) {
+ if (element.VisualColumn + element.VisualLength <= column)
+ continue;
+ return element;
+ }
+ }
+ return null;
+ }
+ #endregion
+
+ #region Visual Position <-> TextViewPosition
+ /// <summary>
+ /// Gets the visual position from a text view position.
+ /// </summary>
+ /// <param name="position">The text view position.</param>
+ /// <param name="yPositionMode">The mode how to retrieve the Y position.</param>
+ /// <returns>The position in WPF device-independent pixels relative
+ /// to the top left corner of the document.</returns>
+ public Point GetVisualPosition(TextViewPosition position, VisualYPosition yPositionMode)
+ {
+ VerifyAccess();
+ if (this.Document == null)
+ throw ThrowUtil.NoDocumentAssigned();
+ DocumentLine documentLine = this.Document.GetLineByNumber(position.Line);
+ VisualLine visualLine = GetOrConstructVisualLine(documentLine);
+ int visualColumn = position.VisualColumn;
+ if (visualColumn < 0) {
+ int offset = documentLine.Offset + position.Column - 1;
+ visualColumn = visualLine.GetVisualColumn(offset - visualLine.FirstDocumentLine.Offset);
+ }
+ return visualLine.GetVisualPosition(visualColumn, yPositionMode);
+ }
+
+ /// <summary>
+ /// Gets the text view position from the specified visual position.
+ /// If the position is within a character, it is rounded to the next character boundary.
+ /// </summary>
+ /// <param name="visualPosition">The position in WPF device-independent pixels relative
+ /// to the top left corner of the document.</param>
+ /// <returns>The logical position, or null if the position is outside the document.</returns>
+ public TextViewPosition? GetPosition(Point visualPosition)
+ {
+ VerifyAccess();
+ if (this.Document == null)
+ throw ThrowUtil.NoDocumentAssigned();
+ VisualLine line = GetVisualLineFromVisualTop(visualPosition.Y);
+ if (line == null)
+ return null;
+ int visualColumn = line.GetVisualColumn(visualPosition);
+ int documentOffset = line.GetRelativeOffset(visualColumn) + line.FirstDocumentLine.Offset;
+ return new TextViewPosition(document.GetLocation(documentOffset), visualColumn);
+ }
+
+ /// <summary>
+ /// Gets the text view position from the specified visual position.
+ /// If the position is inside a character, the position in front of the character is returned.
+ /// </summary>
+ /// <param name="visualPosition">The position in WPF device-independent pixels relative
+ /// to the top left corner of the document.</param>
+ /// <returns>The logical position, or null if the position is outside the document.</returns>
+ public TextViewPosition? GetPositionFloor(Point visualPosition)
+ {
+ VerifyAccess();
+ if (this.Document == null)
+ throw ThrowUtil.NoDocumentAssigned();
+ VisualLine line = GetVisualLineFromVisualTop(visualPosition.Y);
+ if (line == null)
+ return null;
+ int visualColumn = line.GetVisualColumnFloor(visualPosition);
+ int documentOffset = line.GetRelativeOffset(visualColumn) + line.FirstDocumentLine.Offset;
+ return new TextViewPosition(document.GetLocation(documentOffset), visualColumn);
+ }
+ #endregion
+
+ #region Service Provider
+ readonly ServiceContainer services = new ServiceContainer();
+
+ /// <summary>
+ /// Gets a service container used to associate services with the text view.
+ /// </summary>
+ public ServiceContainer Services {
+ get { return services; }
+ }
+
+ object IServiceProvider.GetService(Type serviceType)
+ {
+ return services.GetService(serviceType);
+ }
+
+ void ConnectToTextView(object obj)
+ {
+ ITextViewConnect c = obj as ITextViewConnect;
+ if (c != null)
+ c.AddToTextView(this);
+ }
+
+ void DisconnectFromTextView(object obj)
+ {
+ ITextViewConnect c = obj as ITextViewConnect;
+ if (c != null)
+ c.RemoveFromTextView(this);
+ }
+ #endregion
+
+ #region MouseHover
+ /// <summary>
+ /// The PreviewMouseHover event.
+ /// </summary>
+ public static readonly RoutedEvent PreviewMouseHoverEvent =
+ EventManager.RegisterRoutedEvent("PreviewMouseHover", RoutingStrategy.Tunnel,
+ typeof(MouseEventHandler), typeof(TextView));
+ /// <summary>
+ /// The MouseHover event.
+ /// </summary>
+ public static readonly RoutedEvent MouseHoverEvent =
+ EventManager.RegisterRoutedEvent("MouseHover", RoutingStrategy.Bubble,
+ typeof(MouseEventHandler), typeof(TextView));
+
+ /// <summary>
+ /// The PreviewMouseHoverStopped event.
+ /// </summary>
+ public static readonly RoutedEvent PreviewMouseHoverStoppedEvent =
+ EventManager.RegisterRoutedEvent("PreviewMouseHoverStopped", RoutingStrategy.Tunnel,
+ typeof(MouseEventHandler), typeof(TextView));
+ /// <summary>
+ /// The MouseHoverStopped event.
+ /// </summary>
+ public static readonly RoutedEvent MouseHoverStoppedEvent =
+ EventManager.RegisterRoutedEvent("MouseHoverStopped", RoutingStrategy.Bubble,
+ typeof(MouseEventHandler), typeof(TextView));
+
+
+ /// <summary>
+ /// Occurs when the mouse has hovered over a fixed location for some time.
+ /// </summary>
+ public event MouseEventHandler PreviewMouseHover {
+ add { AddHandler(PreviewMouseHoverEvent, value); }
+ remove { RemoveHandler(PreviewMouseHoverEvent, value); }
+ }
+
+ /// <summary>
+ /// Occurs when the mouse has hovered over a fixed location for some time.
+ /// </summary>
+ public event MouseEventHandler MouseHover {
+ add { AddHandler(MouseHoverEvent, value); }
+ remove { RemoveHandler(MouseHoverEvent, value); }
+ }
+
+ /// <summary>
+ /// Occurs when the mouse had previously hovered but now started moving again.
+ /// </summary>
+ public event MouseEventHandler PreviewMouseHoverStopped {
+ add { AddHandler(PreviewMouseHoverStoppedEvent, value); }
+ remove { RemoveHandler(PreviewMouseHoverStoppedEvent, value); }
+ }
+
+ /// <summary>
+ /// Occurs when the mouse had previously hovered but now started moving again.
+ /// </summary>
+ public event MouseEventHandler MouseHoverStopped {
+ add { AddHandler(MouseHoverStoppedEvent, value); }
+ remove { RemoveHandler(MouseHoverStoppedEvent, value); }
+ }
+
+ MouseHoverLogic hoverLogic;
+
+ void RaiseHoverEventPair(MouseEventArgs e, RoutedEvent tunnelingEvent, RoutedEvent bubblingEvent)
+ {
+ var mouseDevice = e.MouseDevice;
+ var stylusDevice = e.StylusDevice;
+ int inputTime = Environment.TickCount;
+ var args1 = new MouseEventArgs(mouseDevice, inputTime, stylusDevice) {
+ RoutedEvent = tunnelingEvent,
+ Source = this
+ };
+ RaiseEvent(args1);
+ var args2 = new MouseEventArgs(mouseDevice, inputTime, stylusDevice) {
+ RoutedEvent = bubblingEvent,
+ Source = this,
+ Handled = args1.Handled
+ };
+ RaiseEvent(args2);
+ }
+ #endregion
+
+ /// <summary>
+ /// Collapses lines for the purpose of scrolling. <see cref="DocumentLine"/>s marked as collapsed will be hidden
+ /// and not used to start the generation of a <see cref="VisualLine"/>.
+ /// </summary>
+ /// <remarks>
+ /// This method is meant for <see cref="VisualLineElementGenerator"/>s that cause <see cref="VisualLine"/>s to span
+ /// multiple <see cref="DocumentLine"/>s. Do not call it without providing a corresponding
+ /// <see cref="VisualLineElementGenerator"/>.
+ /// If you want to create collapsible text sections, see <see cref="Folding.FoldingManager"/>.
+ ///
+ /// Note that if you want a VisualLineElement to span from line N to line M, then you need to collapse only the lines
+ /// N+1 to M. Do not collapse line N itself.
+ ///
+ /// When you no longer need the section to be collapsed, call <see cref="CollapsedLineSection.Uncollapse()"/> on the
+ /// <see cref="CollapsedLineSection"/> returned from this method.
+ /// </remarks>
+ public CollapsedLineSection CollapseLines(DocumentLine start, DocumentLine end)
+ {
+ VerifyAccess();
+ if (heightTree == null)
+ throw ThrowUtil.NoDocumentAssigned();
+ return heightTree.CollapseText(start, end);
+ }
+
+ /// <summary>
+ /// Gets the height of the document.
+ /// </summary>
+ public double DocumentHeight {
+ get {
+ // return 0 if there is no document = no heightTree
+ return heightTree != null ? heightTree.TotalHeight : 0;
+ }
+ }
+
+ /// <summary>
+ /// Gets the document line at the specified visual position.
+ /// </summary>
+ public DocumentLine GetDocumentLineByVisualTop(double visualTop)
+ {
+ VerifyAccess();
+ if (heightTree == null)
+ throw ThrowUtil.NoDocumentAssigned();
+ return heightTree.GetLineByVisualPosition(visualTop);
+ }
+
+ /// <inheritdoc/>
+ protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
+ {
+ base.OnPropertyChanged(e);
+ if (TextFormatterFactory.PropertyChangeAffectsTextFormatter(e.Property)) {
+ // first, create the new text formatter:
+ RecreateTextFormatter();
+ // changing text formatter requires recreating the cached elements
+ RecreateCachedElements();
+ // and we need to re-measure the font metrics:
+ InvalidateDefaultTextMetrics();
+ } else if (e.Property == Control.ForegroundProperty
+ || e.Property == TextView.NonPrintableCharacterBrushProperty
+ || e.Property == TextView.LinkTextBackgroundBrushProperty
+ || e.Property == TextView.LinkTextForegroundBrushProperty)
+ {
+ // changing brushes requires recreating the cached elements
+ RecreateCachedElements();
+ Redraw();
+ }
+ if (e.Property == Control.FontFamilyProperty
+ || e.Property == Control.FontSizeProperty
+ || e.Property == Control.FontStretchProperty
+ || e.Property == Control.FontStyleProperty
+ || e.Property == Control.FontWeightProperty)
+ {
+ // changing font properties requires recreating cached elements
+ RecreateCachedElements();
+ // and we need to re-measure the font metrics:
+ InvalidateDefaultTextMetrics();
+ Redraw();
+ }
+ if (e.Property == ColumnRulerPenProperty) {
+ columnRulerRenderer.SetRuler(this.Options.ColumnRulerPosition, this.ColumnRulerPen);
+ }
+ }
+
+ /// <summary>
+ /// The pen used to draw the column ruler.
+ /// <seealso cref="TextEditorOptions.ShowColumnRuler"/>
+ /// </summary>
+ public static readonly DependencyProperty ColumnRulerPenProperty =
+ DependencyProperty.Register("ColumnRulerBrush", typeof(Pen), typeof(TextView),
+ new FrameworkPropertyMetadata(CreateFrozenPen(Brushes.LightGray)));
+
+ static Pen CreateFrozenPen(SolidColorBrush brush)
+ {
+ Pen pen = new Pen(brush, 1);
+ pen.Freeze();
+ return pen;
+ }
+
+ /// <summary>
+ /// Gets/Sets the pen used to draw the column ruler.
+ /// <seealso cref="TextEditorOptions.ShowColumnRuler"/>
+ /// </summary>
+ public Pen ColumnRulerPen {
+ get { return (Pen)GetValue(ColumnRulerPenProperty); }
+ set { SetValue(ColumnRulerPenProperty, value); }
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/TextViewCachedElements.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/TextViewCachedElements.cs
new file mode 100644
index 000000000..c56e22eaf
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/TextViewCachedElements.cs
@@ -0,0 +1,43 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Collections.Generic;
+using System.Windows.Media;
+using System.Windows.Media.TextFormatting;
+using Tango.Scripting.Editors.Utils;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ sealed class TextViewCachedElements : IDisposable
+ {
+ TextFormatter formatter;
+ Dictionary<string, TextLine> nonPrintableCharacterTexts;
+
+ public TextLine GetTextForNonPrintableCharacter(string text, ITextRunConstructionContext context)
+ {
+ if (nonPrintableCharacterTexts == null)
+ nonPrintableCharacterTexts = new Dictionary<string, TextLine>();
+ TextLine textLine;
+ if (!nonPrintableCharacterTexts.TryGetValue(text, out textLine)) {
+ var p = new VisualLineElementTextRunProperties(context.GlobalTextRunProperties);
+ p.SetForegroundBrush(context.TextView.NonPrintableCharacterBrush);
+ if (formatter == null)
+ formatter = TextFormatterFactory.Create(context.TextView);
+ textLine = FormattedTextElement.PrepareText(formatter, text, p);
+ 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/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/TextViewWeakEventManager.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/TextViewWeakEventManager.cs
new file mode 100644
index 000000000..ca1bb5959
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/TextViewWeakEventManager.cs
@@ -0,0 +1,71 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using Tango.Scripting.Editors.Utils;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// Contains weak event managers for the TextView events.
+ /// </summary>
+ public static class TextViewWeakEventManager
+ {
+ /// <summary>
+ /// Weak event manager for the <see cref="TextView.DocumentChanged"/> event.
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")]
+ public sealed class DocumentChanged : WeakEventManagerBase<DocumentChanged, TextView>
+ {
+ /// <inheritdoc/>
+ protected override void StartListening(TextView source)
+ {
+ source.DocumentChanged += DeliverEvent;
+ }
+
+ /// <inheritdoc/>
+ protected override void StopListening(TextView source)
+ {
+ source.DocumentChanged -= DeliverEvent;
+ }
+ }
+
+ /// <summary>
+ /// Weak event manager for the <see cref="TextView.VisualLinesChanged"/> event.
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")]
+ public sealed class VisualLinesChanged : WeakEventManagerBase<VisualLinesChanged, TextView>
+ {
+ /// <inheritdoc/>
+ protected override void StartListening(TextView source)
+ {
+ source.VisualLinesChanged += DeliverEvent;
+ }
+
+ /// <inheritdoc/>
+ protected override void StopListening(TextView source)
+ {
+ source.VisualLinesChanged -= DeliverEvent;
+ }
+ }
+
+ /// <summary>
+ /// Weak event manager for the <see cref="TextView.ScrollOffsetChanged"/> event.
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")]
+ public sealed class ScrollOffsetChanged : WeakEventManagerBase<ScrollOffsetChanged, TextView>
+ {
+ /// <inheritdoc/>
+ protected override void StartListening(TextView source)
+ {
+ source.ScrollOffsetChanged += DeliverEvent;
+ }
+
+ /// <inheritdoc/>
+ protected override void StopListening(TextView source)
+ {
+ source.ScrollOffsetChanged -= DeliverEvent;
+ }
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLine.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLine.cs
new file mode 100644
index 000000000..70727dd07
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLine.cs
@@ -0,0 +1,681 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System.Linq;
+using System.Windows.Controls;
+using System.Windows.Media;
+using Tango.Scripting.Editors.Utils;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Windows;
+using System.Windows.Documents;
+using System.Windows.Media.TextFormatting;
+using Tango.Scripting.Editors.Document;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// Represents a visual line in the document.
+ /// A visual line usually corresponds to one DocumentLine, but it can span multiple lines if
+ /// all but the first are collapsed.
+ /// </summary>
+ public sealed class VisualLine
+ {
+ enum LifetimePhase : byte
+ {
+ Generating,
+ Transforming,
+ Live,
+ Disposed
+ }
+
+ TextView textView;
+ List<VisualLineElement> elements;
+ internal bool hasInlineObjects;
+ LifetimePhase phase;
+
+ /// <summary>
+ /// Gets the document to which this VisualLine belongs.
+ /// </summary>
+ public TextDocument Document { get; private set; }
+
+ /// <summary>
+ /// Gets the first document line displayed by this visual line.
+ /// </summary>
+ public DocumentLine FirstDocumentLine { get; private set; }
+
+ /// <summary>
+ /// Gets the last document line displayed by this visual line.
+ /// </summary>
+ public DocumentLine LastDocumentLine { get; private set; }
+
+ /// <summary>
+ /// Gets a read-only collection of line elements.
+ /// </summary>
+ public ReadOnlyCollection<VisualLineElement> Elements { get; private set; }
+
+ ReadOnlyCollection<TextLine> textLines;
+
+ /// <summary>
+ /// Gets a read-only collection of text lines.
+ /// </summary>
+ public ReadOnlyCollection<TextLine> TextLines {
+ get {
+ if (phase < LifetimePhase.Live)
+ throw new InvalidOperationException();
+ return textLines;
+ }
+ }
+
+ /// <summary>
+ /// Gets the start offset of the VisualLine inside the document.
+ /// This is equivalent to <c>FirstDocumentLine.Offset</c>.
+ /// </summary>
+ public int StartOffset {
+ get {
+ return FirstDocumentLine.Offset;
+ }
+ }
+
+ /// <summary>
+ /// Length in visual line coordinates.
+ /// </summary>
+ public int VisualLength { get; private set; }
+
+ /// <summary>
+ /// Length in visual line coordinates including the end of line marker, if TextEditorOptions.ShowEndOfLine is enabled.
+ /// </summary>
+ public int VisualLengthWithEndOfLineMarker {
+ get {
+ int length = VisualLength;
+ if (textView.Options.ShowEndOfLine && LastDocumentLine.NextLine != null) length++;
+ return length;
+ }
+ }
+
+ /// <summary>
+ /// Gets the height of the visual line in device-independent pixels.
+ /// </summary>
+ public double Height { get; private set; }
+
+ /// <summary>
+ /// Gets the Y position of the line. This is measured in device-independent pixels relative to the start of the document.
+ /// </summary>
+ public double VisualTop { get; internal set; }
+
+ internal VisualLine(TextView textView, DocumentLine firstDocumentLine)
+ {
+ Debug.Assert(textView != null);
+ Debug.Assert(firstDocumentLine != null);
+ this.textView = textView;
+ this.Document = textView.Document;
+ this.FirstDocumentLine = firstDocumentLine;
+ }
+
+ internal void ConstructVisualElements(ITextRunConstructionContext context, VisualLineElementGenerator[] generators)
+ {
+ Debug.Assert(phase == LifetimePhase.Generating);
+ foreach (VisualLineElementGenerator g in generators) {
+ g.StartGeneration(context);
+ }
+ elements = new List<VisualLineElement>();
+ PerformVisualElementConstruction(generators);
+ foreach (VisualLineElementGenerator g in generators) {
+ g.FinishGeneration();
+ }
+
+ var globalTextRunProperties = context.GlobalTextRunProperties;
+ foreach (var element in elements) {
+ element.SetTextRunProperties(new VisualLineElementTextRunProperties(globalTextRunProperties));
+ }
+ this.Elements = elements.AsReadOnly();
+ CalculateOffsets();
+ phase = LifetimePhase.Transforming;
+ }
+
+ void PerformVisualElementConstruction(VisualLineElementGenerator[] generators)
+ {
+ TextDocument document = this.Document;
+ int offset = FirstDocumentLine.Offset;
+ int currentLineEnd = offset + FirstDocumentLine.Length;
+ LastDocumentLine = FirstDocumentLine;
+ int askInterestOffset = 0; // 0 or 1
+ while (offset + askInterestOffset <= currentLineEnd) {
+ int textPieceEndOffset = currentLineEnd;
+ foreach (VisualLineElementGenerator g in generators) {
+ g.cachedInterest = g.GetFirstInterestedOffset(offset + askInterestOffset);
+ if (g.cachedInterest != -1) {
+ if (g.cachedInterest < offset)
+ throw new ArgumentOutOfRangeException(g.GetType().Name + ".GetFirstInterestedOffset",
+ g.cachedInterest,
+ "GetFirstInterestedOffset must not return an offset less than startOffset. Return -1 to signal no interest.");
+ if (g.cachedInterest < textPieceEndOffset)
+ textPieceEndOffset = g.cachedInterest;
+ }
+ }
+ Debug.Assert(textPieceEndOffset >= offset);
+ if (textPieceEndOffset > offset) {
+ int textPieceLength = textPieceEndOffset - offset;
+ elements.Add(new VisualLineText(this, textPieceLength));
+ offset = textPieceEndOffset;
+ }
+ // If no elements constructed / only zero-length elements constructed:
+ // do not asking the generators again for the same location (would cause endless loop)
+ askInterestOffset = 1;
+ foreach (VisualLineElementGenerator g in generators) {
+ if (g.cachedInterest == offset) {
+ VisualLineElement element = g.ConstructElement(offset);
+ if (element != null) {
+ elements.Add(element);
+ if (element.DocumentLength > 0) {
+ // a non-zero-length element was constructed
+ askInterestOffset = 0;
+ offset += element.DocumentLength;
+ if (offset > currentLineEnd) {
+ DocumentLine newEndLine = document.GetLineByOffset(offset);
+ currentLineEnd = newEndLine.Offset + newEndLine.Length;
+ this.LastDocumentLine = newEndLine;
+ //if (currentLineEnd < offset) {
+ // throw new InvalidOperationException(
+ // "The VisualLineElementGenerator " + g.GetType().Name +
+ // " produced an element which ends within the line delimiter");
+ //}
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ void CalculateOffsets()
+ {
+ int visualOffset = 0;
+ int textOffset = 0;
+ foreach (VisualLineElement element in elements) {
+ element.VisualColumn = visualOffset;
+ element.RelativeTextOffset = textOffset;
+ visualOffset += element.VisualLength;
+ textOffset += element.DocumentLength;
+ }
+ VisualLength = visualOffset;
+ //Debug.Assert(textOffset == LastDocumentLine.EndOffset - FirstDocumentLine.Offset);
+ }
+
+ internal void RunTransformers(ITextRunConstructionContext context, IVisualLineTransformer[] transformers)
+ {
+ Debug.Assert(phase == LifetimePhase.Transforming);
+ foreach (IVisualLineTransformer transformer in transformers) {
+ transformer.Transform(context, elements);
+ }
+ // For some strange reason, WPF requires that either all or none of the typography properties are set.
+ if (elements.Any(e => e.TextRunProperties.TypographyProperties != null)) {
+ // Fix typographic properties
+ foreach (VisualLineElement element in elements) {
+ if (element.TextRunProperties.TypographyProperties == null) {
+ element.TextRunProperties.SetTypographyProperties(new DefaultTextRunTypographyProperties());
+ }
+ }
+ }
+ phase = LifetimePhase.Live;
+ }
+
+ /// <summary>
+ /// Replaces the single element at <paramref name="elementIndex"/> with the specified elements.
+ /// The replacement operation must preserve the document length, but may change the visual length.
+ /// </summary>
+ /// <remarks>
+ /// This method may only be called by line transformers.
+ /// </remarks>
+ public void ReplaceElement(int elementIndex, params VisualLineElement[] newElements)
+ {
+ ReplaceElement(elementIndex, 1, newElements);
+ }
+
+ /// <summary>
+ /// Replaces <paramref name="count"/> elements starting at <paramref name="elementIndex"/> with the specified elements.
+ /// The replacement operation must preserve the document length, but may change the visual length.
+ /// </summary>
+ /// <remarks>
+ /// This method may only be called by line transformers.
+ /// </remarks>
+ public void ReplaceElement(int elementIndex, int count, params VisualLineElement[] newElements)
+ {
+ if (phase != LifetimePhase.Transforming)
+ throw new InvalidOperationException("This method may only be called by line transformers.");
+ int oldDocumentLength = 0;
+ for (int i = elementIndex; i < elementIndex + count; i++) {
+ oldDocumentLength += elements[i].DocumentLength;
+ }
+ int newDocumentLength = 0;
+ foreach (var newElement in newElements) {
+ newDocumentLength += newElement.DocumentLength;
+ }
+ if (oldDocumentLength != newDocumentLength)
+ throw new InvalidOperationException("Old elements have document length " + oldDocumentLength + ", but new elements have length " + newDocumentLength);
+ elements.RemoveRange(elementIndex, count);
+ elements.InsertRange(elementIndex, newElements);
+ CalculateOffsets();
+ }
+
+ internal void SetTextLines(List<TextLine> textLines)
+ {
+ this.textLines = textLines.AsReadOnly();
+ Height = 0;
+ foreach (TextLine line in textLines)
+ Height += line.Height;
+ }
+
+ /// <summary>
+ /// Gets the visual column from a document offset relative to the first line start.
+ /// </summary>
+ public int GetVisualColumn(int relativeTextOffset)
+ {
+ ThrowUtil.CheckNotNegative(relativeTextOffset, "relativeTextOffset");
+ foreach (VisualLineElement element in elements) {
+ if (element.RelativeTextOffset <= relativeTextOffset
+ && element.RelativeTextOffset + element.DocumentLength >= relativeTextOffset)
+ {
+ return element.GetVisualColumn(relativeTextOffset);
+ }
+ }
+ return VisualLength;
+ }
+
+ /// <summary>
+ /// Gets the document offset (relative to the first line start) from a visual column.
+ /// </summary>
+ public int GetRelativeOffset(int visualColumn)
+ {
+ ThrowUtil.CheckNotNegative(visualColumn, "visualColumn");
+ int documentLength = 0;
+ foreach (VisualLineElement element in elements) {
+ if (element.VisualColumn <= visualColumn
+ && element.VisualColumn + element.VisualLength > visualColumn)
+ {
+ return element.GetRelativeOffset(visualColumn);
+ }
+ documentLength += element.DocumentLength;
+ }
+ return documentLength;
+ }
+
+ /// <summary>
+ /// Gets the text line containing the specified visual column.
+ /// </summary>
+ public TextLine GetTextLine(int visualColumn)
+ {
+ if (visualColumn < 0)
+ throw new ArgumentOutOfRangeException("visualColumn");
+ if (visualColumn >= VisualLengthWithEndOfLineMarker)
+ return TextLines[TextLines.Count - 1];
+ foreach (TextLine line in TextLines) {
+ if (visualColumn < line.Length)
+ return line;
+ else
+ visualColumn -= line.Length;
+ }
+ throw new InvalidOperationException("Shouldn't happen (VisualLength incorrect?)");
+ }
+
+ /// <summary>
+ /// Gets the visual top from the specified text line.
+ /// </summary>
+ /// <returns>Distance in device-independent pixels
+ /// from the top of the document to the top of the specified text line.</returns>
+ public double GetTextLineVisualYPosition(TextLine textLine, VisualYPosition yPositionMode)
+ {
+ if (textLine == null)
+ throw new ArgumentNullException("textLine");
+ double pos = VisualTop;
+ foreach (TextLine tl in TextLines) {
+ if (tl == textLine) {
+ switch (yPositionMode) {
+ case VisualYPosition.LineTop:
+ return pos;
+ case VisualYPosition.LineMiddle:
+ return pos + tl.Height / 2;
+ case VisualYPosition.LineBottom:
+ return pos + tl.Height;
+ case VisualYPosition.TextTop:
+ return pos + tl.Baseline - textView.DefaultBaseline;
+ case VisualYPosition.TextBottom:
+ return pos + tl.Baseline - textView.DefaultBaseline + textView.DefaultLineHeight;
+ case VisualYPosition.TextMiddle:
+ return pos + tl.Baseline - textView.DefaultBaseline + textView.DefaultLineHeight / 2;
+ case VisualYPosition.Baseline:
+ return pos + tl.Baseline;
+ default:
+ throw new ArgumentException("Invalid yPositionMode:" + yPositionMode);
+ }
+ } else {
+ pos += tl.Height;
+ }
+ }
+ throw new ArgumentException("textLine is not a line in this VisualLine");
+ }
+
+ /// <summary>
+ /// Gets the start visual column from the specified text line.
+ /// </summary>
+ public int GetTextLineVisualStartColumn(TextLine textLine)
+ {
+ if (!TextLines.Contains(textLine))
+ throw new ArgumentException("textLine is not a line in this VisualLine");
+ int col = 0;
+ foreach (TextLine tl in TextLines) {
+ if (tl == textLine)
+ break;
+ else
+ col += tl.Length;
+ }
+ return col;
+ }
+
+ /// <summary>
+ /// Gets a TextLine by the visual position.
+ /// </summary>
+ public TextLine GetTextLineByVisualYPosition(double visualTop)
+ {
+ const double epsilon = 0.0001;
+ double pos = this.VisualTop;
+ foreach (TextLine tl in TextLines) {
+ pos += tl.Height;
+ if (visualTop + epsilon < pos)
+ return tl;
+ }
+ return TextLines[TextLines.Count - 1];
+ }
+
+ /// <summary>
+ /// Gets the visual position from the specified visualColumn.
+ /// </summary>
+ /// <returns>Position in device-independent pixels
+ /// relative to the top left of the document.</returns>
+ public Point GetVisualPosition(int visualColumn, VisualYPosition yPositionMode)
+ {
+ TextLine textLine = GetTextLine(visualColumn);
+ double xPos = GetTextLineVisualXPosition(textLine, visualColumn);
+ double yPos = GetTextLineVisualYPosition(textLine, yPositionMode);
+ return new Point(xPos, yPos);
+ }
+
+ /// <summary>
+ /// Gets the distance to the left border of the text area of the specified visual column.
+ /// The visual column must belong to the specified text line.
+ /// </summary>
+ public double GetTextLineVisualXPosition(TextLine textLine, int visualColumn)
+ {
+ if (textLine == null)
+ throw new ArgumentNullException("textLine");
+ double xPos = textLine.GetDistanceFromCharacterHit(
+ new CharacterHit(Math.Min(visualColumn, VisualLengthWithEndOfLineMarker), 0));
+ if (visualColumn > VisualLengthWithEndOfLineMarker) {
+ xPos += (visualColumn - VisualLengthWithEndOfLineMarker) * textView.WideSpaceWidth;
+ }
+ return xPos;
+ }
+
+ /// <summary>
+ /// Gets the visual column from a document position (relative to top left of the document).
+ /// If the user clicks between two visual columns, rounds to the nearest column.
+ /// </summary>
+ public int GetVisualColumn(Point point)
+ {
+ return GetVisualColumn(point, textView.Options.EnableVirtualSpace);
+ }
+
+ /// <summary>
+ /// Gets the visual column from a document position (relative to top left of the document).
+ /// If the user clicks between two visual columns, rounds to the nearest column.
+ /// </summary>
+ public int GetVisualColumn(Point point, bool allowVirtualSpace)
+ {
+ return GetVisualColumn(GetTextLineByVisualYPosition(point.Y), point.X, allowVirtualSpace);
+ }
+
+ /// <summary>
+ /// Gets the visual column from a document position (relative to top left of the document).
+ /// If the user clicks between two visual columns, rounds to the nearest column.
+ /// </summary>
+ public int GetVisualColumn(TextLine textLine, double xPos, bool allowVirtualSpace)
+ {
+ if (xPos > textLine.WidthIncludingTrailingWhitespace) {
+ if (allowVirtualSpace && textLine == TextLines[TextLines.Count - 1]) {
+ int virtualX = (int)Math.Round((xPos - textLine.WidthIncludingTrailingWhitespace) / textView.WideSpaceWidth);
+ return VisualLengthWithEndOfLineMarker + virtualX;
+ }
+ }
+ CharacterHit ch = textLine.GetCharacterHitFromDistance(xPos);
+ return ch.FirstCharacterIndex + ch.TrailingLength;
+ }
+
+ /// <summary>
+ /// Validates the visual column and returns the correct one.
+ /// </summary>
+ public int ValidateVisualColumn(TextViewPosition position, bool allowVirtualSpace)
+ {
+ return ValidateVisualColumn(Document.GetOffset(position.Location), position.VisualColumn, allowVirtualSpace);
+ }
+
+ /// <summary>
+ /// Validates the visual column and returns the correct one.
+ /// </summary>
+ public int ValidateVisualColumn(int offset, int visualColumn, bool allowVirtualSpace)
+ {
+ int firstDocumentLineOffset = this.FirstDocumentLine.Offset;
+ if (visualColumn < 0) {
+ return GetVisualColumn(offset - firstDocumentLineOffset);
+ } else {
+ int offsetFromVisualColumn = GetRelativeOffset(visualColumn);
+ offsetFromVisualColumn += firstDocumentLineOffset;
+ if (offsetFromVisualColumn != offset) {
+ return GetVisualColumn(offset - firstDocumentLineOffset);
+ } else {
+ if (visualColumn > VisualLength && !allowVirtualSpace) {
+ return VisualLength;
+ }
+ }
+ }
+ return visualColumn;
+ }
+
+ /// <summary>
+ /// Gets the visual column from a document position (relative to top left of the document).
+ /// If the user clicks between two visual columns, returns the first of those columns.
+ /// </summary>
+ public int GetVisualColumnFloor(Point point)
+ {
+ return GetVisualColumnFloor(point, textView.Options.EnableVirtualSpace);
+ }
+
+ /// <summary>
+ /// Gets the visual column from a document position (relative to top left of the document).
+ /// If the user clicks between two visual columns, returns the first of those columns.
+ /// </summary>
+ public int GetVisualColumnFloor(Point point, bool allowVirtualSpace)
+ {
+ TextLine textLine = GetTextLineByVisualYPosition(point.Y);
+ if (point.X > textLine.WidthIncludingTrailingWhitespace) {
+ if (allowVirtualSpace && textLine == TextLines[TextLines.Count - 1]) {
+ // clicking virtual space in the last line
+ int virtualX = (int)((point.X - textLine.WidthIncludingTrailingWhitespace) / textView.WideSpaceWidth);
+ return VisualLengthWithEndOfLineMarker + virtualX;
+ } else {
+ // GetCharacterHitFromDistance returns a hit with FirstCharacterIndex=last character in line
+ // and TrailingLength=1 when clicking behind the line, so the floor function needs to handle this case
+ // specially and return the line's end column instead.
+ return GetTextLineVisualStartColumn(textLine) + textLine.Length;
+ }
+ }
+ CharacterHit ch = textLine.GetCharacterHitFromDistance(point.X);
+ return ch.FirstCharacterIndex;
+ }
+
+ /// <summary>
+ /// Gets whether the visual line was disposed.
+ /// </summary>
+ public bool IsDisposed {
+ get { return phase == LifetimePhase.Disposed; }
+ }
+
+ internal void Dispose()
+ {
+ if (phase == LifetimePhase.Disposed)
+ return;
+ Debug.Assert(phase == LifetimePhase.Live);
+ phase = LifetimePhase.Disposed;
+ foreach (TextLine textLine in TextLines) {
+ textLine.Dispose();
+ }
+ }
+
+ /// <summary>
+ /// Gets the next possible caret position after visualColumn, or -1 if there is no caret position.
+ /// </summary>
+ public int GetNextCaretPosition(int visualColumn, LogicalDirection direction, CaretPositioningMode mode, bool allowVirtualSpace)
+ {
+ if (!HasStopsInVirtualSpace(mode))
+ allowVirtualSpace = false;
+
+ if (elements.Count == 0) {
+ // special handling for empty visual lines:
+ if (allowVirtualSpace) {
+ if (direction == LogicalDirection.Forward)
+ return Math.Max(0, visualColumn + 1);
+ else if (visualColumn > 0)
+ return visualColumn - 1;
+ else
+ return -1;
+ } else {
+ // even though we don't have any elements,
+ // there's a single caret stop at visualColumn 0
+ if (visualColumn < 0 && direction == LogicalDirection.Forward)
+ return 0;
+ else if (visualColumn > 0 && direction == LogicalDirection.Backward)
+ return 0;
+ else
+ return -1;
+ }
+ }
+
+ int i;
+ if (direction == LogicalDirection.Backward) {
+ // Search Backwards:
+ // If the last element doesn't handle line borders, return the line end as caret stop
+
+ if (visualColumn > this.VisualLength && !elements[elements.Count-1].HandlesLineBorders && HasImplicitStopAtLineEnd(mode)) {
+ if (allowVirtualSpace)
+ return visualColumn - 1;
+ else
+ return this.VisualLength;
+ }
+ // skip elements that start after or at visualColumn
+ for (i = elements.Count - 1; i >= 0; i--) {
+ if (elements[i].VisualColumn < visualColumn)
+ break;
+ }
+ // search last element that has a caret stop
+ for (; i >= 0; i--) {
+ int pos = elements[i].GetNextCaretPosition(
+ Math.Min(visualColumn, elements[i].VisualColumn + elements[i].VisualLength + 1),
+ direction, mode);
+ if (pos >= 0)
+ return pos;
+ }
+ // If we've found nothing, and the first element doesn't handle line borders,
+ // return the line start as normal caret stop.
+ if (visualColumn > 0 && !elements[0].HandlesLineBorders && HasImplicitStopAtLineStart(mode))
+ return 0;
+ } else {
+ // Search Forwards:
+ // If the first element doesn't handle line borders, return the line start as caret stop
+ if (visualColumn < 0 && !elements[0].HandlesLineBorders && HasImplicitStopAtLineStart(mode))
+ return 0;
+ // skip elements that end before or at visualColumn
+ for (i = 0; i < elements.Count; i++) {
+ if (elements[i].VisualColumn + elements[i].VisualLength > visualColumn)
+ break;
+ }
+ // search first element that has a caret stop
+ for (; i < elements.Count; i++) {
+ int pos = elements[i].GetNextCaretPosition(
+ Math.Max(visualColumn, elements[i].VisualColumn - 1),
+ direction, mode);
+ if (pos >= 0)
+ return pos;
+ }
+ // if we've found nothing, and the last element doesn't handle line borders,
+ // return the line end as caret stop
+ if ((allowVirtualSpace || !elements[elements.Count-1].HandlesLineBorders) && HasImplicitStopAtLineEnd(mode)) {
+ if (visualColumn < this.VisualLength)
+ return this.VisualLength;
+ else if (allowVirtualSpace)
+ return visualColumn + 1;
+ }
+ }
+ // we've found nothing, return -1 and let the caret search continue in the next line
+ return -1;
+ }
+
+ static bool HasStopsInVirtualSpace(CaretPositioningMode mode)
+ {
+ return mode == CaretPositioningMode.Normal;
+ }
+
+ static bool HasImplicitStopAtLineStart(CaretPositioningMode mode)
+ {
+ return mode == CaretPositioningMode.Normal;
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "mode",
+ Justification = "make method consistent with HasImplicitStopAtLineStart; might depend on mode in the future")]
+ static bool HasImplicitStopAtLineEnd(CaretPositioningMode mode)
+ {
+ return true;
+ }
+
+ VisualLineDrawingVisual visual;
+
+ internal VisualLineDrawingVisual Render()
+ {
+ Debug.Assert(phase == LifetimePhase.Live);
+ if (visual == null)
+ visual = new VisualLineDrawingVisual(this);
+ return visual;
+ }
+ }
+
+ sealed class VisualLineDrawingVisual : DrawingVisual
+ {
+ public readonly VisualLine VisualLine;
+ public readonly double Height;
+ internal bool IsAdded;
+
+ public VisualLineDrawingVisual(VisualLine visualLine)
+ {
+ this.VisualLine = visualLine;
+ var drawingContext = RenderOpen();
+ double pos = 0;
+ foreach (TextLine textLine in visualLine.TextLines) {
+ textLine.Draw(drawingContext, new Point(0, pos), InvertAxes.None);
+ pos += textLine.Height;
+ }
+ this.Height = pos;
+ drawingContext.Close();
+ }
+
+ protected override GeometryHitTestResult HitTestCore(GeometryHitTestParameters hitTestParameters)
+ {
+ return null;
+ }
+
+ protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
+ {
+ return null;
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineConstructionStartEventArgs.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineConstructionStartEventArgs.cs
new file mode 100644
index 000000000..438e516bd
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineConstructionStartEventArgs.cs
@@ -0,0 +1,29 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using Tango.Scripting.Editors.Document;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// EventArgs for the <see cref="TextView.VisualLineConstructionStarting"/> event.
+ /// </summary>
+ public class VisualLineConstructionStartEventArgs : EventArgs
+ {
+ /// <summary>
+ /// Gets/Sets the first line that is visible in the TextView.
+ /// </summary>
+ public DocumentLine FirstLineInView { get; private set; }
+
+ /// <summary>
+ /// Creates a new VisualLineConstructionStartEventArgs instance.
+ /// </summary>
+ public VisualLineConstructionStartEventArgs(DocumentLine firstLineInView)
+ {
+ if (firstLineInView == null)
+ throw new ArgumentNullException("firstLineInView");
+ this.FirstLineInView = firstLineInView;
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineElement.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineElement.cs
new file mode 100644
index 000000000..7858ac038
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineElement.cs
@@ -0,0 +1,249 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Collections.Generic;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.TextFormatting;
+
+using Tango.Scripting.Editors.Document;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// Represents a visual element in the document.
+ /// </summary>
+ public abstract class VisualLineElement
+ {
+ /// <summary>
+ /// Creates a new VisualLineElement.
+ /// </summary>
+ /// <param name="visualLength">The length of the element in VisualLine coordinates. Must be positive.</param>
+ /// <param name="documentLength">The length of the element in the document. Must be non-negative.</param>
+ protected VisualLineElement(int visualLength, int documentLength)
+ {
+ if (visualLength < 1)
+ throw new ArgumentOutOfRangeException("visualLength", visualLength, "Value must be at least 1");
+ if (documentLength < 0)
+ throw new ArgumentOutOfRangeException("documentLength", documentLength, "Value must be at least 0");
+ this.VisualLength = visualLength;
+ this.DocumentLength = documentLength;
+ }
+
+ /// <summary>
+ /// Gets the length of this element in visual columns.
+ /// </summary>
+ public int VisualLength { get; private set; }
+
+ /// <summary>
+ /// Gets the length of this element in the text document.
+ /// </summary>
+ public int DocumentLength { get; private set; }
+
+ /// <summary>
+ /// Gets the visual column where this element starts.
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods",
+ Justification = "This property holds the start visual column, use GetVisualColumn to get inner visual columns.")]
+ public int VisualColumn { get; internal set; }
+
+ /// <summary>
+ /// Gets the text offset where this element starts, relative to the start text offset of the visual line.
+ /// </summary>
+ public int RelativeTextOffset { get; internal set; }
+
+ /// <summary>
+ /// Gets the text run properties.
+ /// A unique <see cref="VisualLineElementTextRunProperties"/> instance is used for each
+ /// <see cref="VisualLineElement"/>; colorizing code may assume that modifying the
+ /// <see cref="VisualLineElementTextRunProperties"/> will affect only this
+ /// <see cref="VisualLineElement"/>.
+ /// </summary>
+ public VisualLineElementTextRunProperties TextRunProperties { get; private set; }
+
+ /// <summary>
+ /// Gets/sets the brush used for the background of this <see cref="VisualLineElement" />.
+ /// </summary>
+ public Brush BackgroundBrush { get; set; }
+
+ internal void SetTextRunProperties(VisualLineElementTextRunProperties p)
+ {
+ this.TextRunProperties = p;
+ }
+
+ /// <summary>
+ /// Creates the TextRun for this line element.
+ /// </summary>
+ /// <param name="startVisualColumn">
+ /// The visual column from which the run should be constructed.
+ /// Normally the same value as the <see cref="VisualColumn"/> property is used to construct the full run;
+ /// but when word-wrapping is active, partial runs might be created.
+ /// </param>
+ /// <param name="context">
+ /// Context object that contains information relevant for text run creation.
+ /// </param>
+ public abstract TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context);
+
+ /// <summary>
+ /// Retrieves the text span immediately before the visual column.
+ /// </summary>
+ /// <remarks>This method is used for word-wrapping in bidirectional text.</remarks>
+ public virtual TextSpan<CultureSpecificCharacterBufferRange> GetPrecedingText(int visualColumnLimit, ITextRunConstructionContext context)
+ {
+ return null;
+ }
+
+ /// <summary>
+ /// Gets if this VisualLineElement can be split.
+ /// </summary>
+ public virtual bool CanSplit {
+ get { return false; }
+ }
+
+ /// <summary>
+ /// Splits the element.
+ /// </summary>
+ /// <param name="splitVisualColumn">Position inside this element at which it should be broken</param>
+ /// <param name="elements">The collection of line elements</param>
+ /// <param name="elementIndex">The index at which this element is in the elements list.</param>
+ public virtual void Split(int splitVisualColumn, IList<VisualLineElement> elements, int elementIndex)
+ {
+ throw new NotSupportedException();
+ }
+
+ /// <summary>
+ /// Helper method for splitting this line element into two, correctly updating the
+ /// <see cref="VisualLength"/>, <see cref="DocumentLength"/>, <see cref="VisualColumn"/>
+ /// and <see cref="RelativeTextOffset"/> properties.
+ /// </summary>
+ /// <param name="firstPart">The element before the split position.</param>
+ /// <param name="secondPart">The element after the split position.</param>
+ /// <param name="splitVisualColumn">The split position as visual column.</param>
+ /// <param name="splitRelativeTextOffset">The split position as text offset.</param>
+ protected void SplitHelper(VisualLineElement firstPart, VisualLineElement secondPart, int splitVisualColumn, int splitRelativeTextOffset)
+ {
+ if (firstPart == null)
+ throw new ArgumentNullException("firstPart");
+ if (secondPart == null)
+ throw new ArgumentNullException("secondPart");
+ int relativeSplitVisualColumn = splitVisualColumn - VisualColumn;
+ int relativeSplitRelativeTextOffset = splitRelativeTextOffset - RelativeTextOffset;
+
+ if (relativeSplitVisualColumn <= 0 || relativeSplitVisualColumn >= VisualLength)
+ throw new ArgumentOutOfRangeException("splitVisualColumn", splitVisualColumn, "Value must be between " + (VisualColumn + 1) + " and " + (VisualColumn + VisualLength - 1));
+ if (relativeSplitRelativeTextOffset < 0 || relativeSplitRelativeTextOffset > DocumentLength)
+ throw new ArgumentOutOfRangeException("splitRelativeTextOffset", splitRelativeTextOffset, "Value must be between " + (RelativeTextOffset) + " and " + (RelativeTextOffset + DocumentLength));
+ int oldVisualLength = VisualLength;
+ int oldDocumentLength = DocumentLength;
+ int oldVisualColumn = VisualColumn;
+ int oldRelativeTextOffset = RelativeTextOffset;
+ firstPart.VisualColumn = oldVisualColumn;
+ secondPart.VisualColumn = oldVisualColumn + relativeSplitVisualColumn;
+ firstPart.RelativeTextOffset = oldRelativeTextOffset;
+ secondPart.RelativeTextOffset = oldRelativeTextOffset + relativeSplitRelativeTextOffset;
+ firstPart.VisualLength = relativeSplitVisualColumn;
+ secondPart.VisualLength = oldVisualLength - relativeSplitVisualColumn;
+ firstPart.DocumentLength = relativeSplitRelativeTextOffset;
+ secondPart.DocumentLength = oldDocumentLength - relativeSplitRelativeTextOffset;
+ if (firstPart.TextRunProperties == null)
+ firstPart.TextRunProperties = TextRunProperties.Clone();
+ if (secondPart.TextRunProperties == null)
+ secondPart.TextRunProperties = TextRunProperties.Clone();
+ }
+
+ /// <summary>
+ /// Gets the visual column of a text location inside this element.
+ /// The text offset is given relative to the visual line start.
+ /// </summary>
+ public virtual int GetVisualColumn(int relativeTextOffset)
+ {
+ if (relativeTextOffset >= this.RelativeTextOffset + DocumentLength)
+ return VisualColumn + VisualLength;
+ else
+ return VisualColumn;
+ }
+
+ /// <summary>
+ /// Gets the text offset of a visual column inside this element.
+ /// </summary>
+ /// <returns>A text offset relative to the visual line start.</returns>
+ public virtual int GetRelativeOffset(int visualColumn)
+ {
+ if (visualColumn >= this.VisualColumn + VisualLength)
+ return RelativeTextOffset + DocumentLength;
+ else
+ return RelativeTextOffset;
+ }
+
+ /// <summary>
+ /// Gets the next caret position inside this element.
+ /// </summary>
+ /// <param name="visualColumn">The visual column from which the search should be started.</param>
+ /// <param name="direction">The search direction (forwards or backwards).</param>
+ /// <param name="mode">Whether to stop only at word borders.</param>
+ /// <returns>The visual column of the next caret position, or -1 if there is no next caret position.</returns>
+ /// <remarks>
+ /// In the space between two line elements, it is sufficient that one of them contains a caret position;
+ /// though in many cases, both of them contain one.
+ /// </remarks>
+ public virtual int GetNextCaretPosition(int visualColumn, LogicalDirection direction, CaretPositioningMode mode)
+ {
+ int stop1 = this.VisualColumn;
+ int stop2 = this.VisualColumn + this.VisualLength;
+ if (direction == LogicalDirection.Backward) {
+ if (visualColumn > stop2 && mode != CaretPositioningMode.WordStart && mode != CaretPositioningMode.WordStartOrSymbol)
+ return stop2;
+ else if (visualColumn > stop1)
+ return stop1;
+ } else {
+ if (visualColumn < stop1)
+ return stop1;
+ else if (visualColumn < stop2 && mode != CaretPositioningMode.WordStart && mode != CaretPositioningMode.WordStartOrSymbol)
+ return stop2;
+ }
+ return -1;
+ }
+
+ /// <summary>
+ /// Gets whether the specified offset in this element is considered whitespace.
+ /// </summary>
+ public virtual bool IsWhitespace(int visualColumn)
+ {
+ return false;
+ }
+
+ /// <summary>
+ /// Gets whether the <see cref="GetNextCaretPosition"/> implementation handles line borders.
+ /// If this property returns false, the caller of GetNextCaretPosition should handle the line
+ /// borders (i.e. place caret stops at the start and end of the line).
+ /// This property has an effect only for VisualLineElements that are at the start or end of a
+ /// <see cref="VisualLine"/>.
+ /// </summary>
+ public virtual bool HandlesLineBorders {
+ get { return false; }
+ }
+
+ /// <summary>
+ /// Queries the cursor over the visual line element.
+ /// </summary>
+ protected internal virtual void OnQueryCursor(QueryCursorEventArgs e)
+ {
+ }
+
+ /// <summary>
+ /// Allows the visual line element to handle a mouse event.
+ /// </summary>
+ protected internal virtual void OnMouseDown(MouseButtonEventArgs e)
+ {
+ }
+
+ /// <summary>
+ /// Allows the visual line element to handle a mouse event.
+ /// </summary>
+ protected internal virtual void OnMouseUp(MouseButtonEventArgs e)
+ {
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineElementGenerator.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineElementGenerator.cs
new file mode 100644
index 000000000..8b2f7ac5b
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineElementGenerator.cs
@@ -0,0 +1,63 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// Abstract base class for generators that produce new visual line elements.
+ /// </summary>
+ public abstract class VisualLineElementGenerator
+ {
+ /// <summary>
+ /// Gets the text run construction context.
+ /// </summary>
+ protected ITextRunConstructionContext CurrentContext { get; private set; }
+
+ /// <summary>
+ /// Initializes the generator for the <see cref="ITextRunConstructionContext"/>
+ /// </summary>
+ public virtual void StartGeneration(ITextRunConstructionContext context)
+ {
+ if (context == null)
+ throw new ArgumentNullException("context");
+ this.CurrentContext = context;
+ }
+
+ /// <summary>
+ /// De-initializes the generator.
+ /// </summary>
+ public virtual void FinishGeneration()
+ {
+ this.CurrentContext = null;
+ }
+
+ /// <summary>
+ /// Should only be used by VisualLine.ConstructVisualElements.
+ /// </summary>
+ internal int cachedInterest;
+
+ /// <summary>
+ /// Gets the first offset >= startOffset where the generator wants to construct an element.
+ /// Return -1 to signal no interest.
+ /// </summary>
+ public abstract int GetFirstInterestedOffset(int startOffset);
+
+ /// <summary>
+ /// Constructs an element at the specified offset.
+ /// May return null if no element should be constructed.
+ /// </summary>
+ /// <remarks>
+ /// Avoid signalling interest and then building no element by returning null - doing so
+ /// causes the generated <see cref="VisualLineText"/> elements to be unnecessarily split
+ /// at the position where you signalled interest.
+ /// </remarks>
+ public abstract VisualLineElement ConstructElement(int offset);
+ }
+
+ internal interface IBuiltinElementGenerator
+ {
+ void FetchOptions(TextEditorOptions options);
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineElementTextRunProperties.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineElementTextRunProperties.cs
new file mode 100644
index 000000000..a39276c5b
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineElementTextRunProperties.cs
@@ -0,0 +1,241 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Media.TextFormatting;
+using Tango.Scripting.Editors.Utils;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// <see cref="TextRunProperties"/> implementation that allows changing the properties.
+ /// A <see cref="VisualLineElementTextRunProperties"/> instance usually is assigned to a single
+ /// <see cref="VisualLineElement"/>.
+ /// </summary>
+ public class VisualLineElementTextRunProperties : TextRunProperties, ICloneable
+ {
+ Brush backgroundBrush;
+ BaselineAlignment baselineAlignment;
+ CultureInfo cultureInfo;
+ double fontHintingEmSize;
+ double fontRenderingEmSize;
+ Brush foregroundBrush;
+ Typeface typeface;
+ TextDecorationCollection textDecorations;
+ TextEffectCollection textEffects;
+ TextRunTypographyProperties typographyProperties;
+ NumberSubstitution numberSubstitution;
+
+ /// <summary>
+ /// Creates a new VisualLineElementTextRunProperties instance that copies its values
+ /// from the specified <paramref name="textRunProperties"/>.
+ /// For the <see cref="TextDecorations"/> and <see cref="TextEffects"/> collections, deep copies
+ /// are created if those collections are not frozen.
+ /// </summary>
+ public VisualLineElementTextRunProperties(TextRunProperties textRunProperties)
+ {
+ if (textRunProperties == null)
+ throw new ArgumentNullException("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;
+ }
+
+ /// <summary>
+ /// Creates a copy of this instance.
+ /// </summary>
+ public virtual VisualLineElementTextRunProperties Clone()
+ {
+ return new VisualLineElementTextRunProperties(this);
+ }
+
+ object ICloneable.Clone()
+ {
+ return Clone();
+ }
+
+ /// <inheritdoc/>
+ public override Brush BackgroundBrush {
+ get { return backgroundBrush; }
+ }
+
+ /// <summary>
+ /// Sets the <see cref="BackgroundBrush"/>.
+ /// </summary>
+ public void SetBackgroundBrush(Brush value)
+ {
+ ExtensionMethods.CheckIsFrozen(value);
+ backgroundBrush = value;
+ }
+
+ /// <inheritdoc/>
+ public override BaselineAlignment BaselineAlignment {
+ get { return baselineAlignment; }
+ }
+
+ /// <summary>
+ /// Sets the <see cref="BaselineAlignment"/>.
+ /// </summary>
+ public void SetBaselineAlignment(BaselineAlignment value)
+ {
+ baselineAlignment = value;
+ }
+
+ /// <inheritdoc/>
+ public override CultureInfo CultureInfo {
+ get { return cultureInfo; }
+ }
+
+ /// <summary>
+ /// Sets the <see cref="CultureInfo"/>.
+ /// </summary>
+ public void SetCultureInfo(CultureInfo value)
+ {
+ if (value == null)
+ throw new ArgumentNullException("value");
+ cultureInfo = value;
+ }
+
+ /// <inheritdoc/>
+ public override double FontHintingEmSize {
+ get { return fontHintingEmSize; }
+ }
+
+ /// <summary>
+ /// Sets the <see cref="FontHintingEmSize"/>.
+ /// </summary>
+ public void SetFontHintingEmSize(double value)
+ {
+ fontHintingEmSize = value;
+ }
+
+ /// <inheritdoc/>
+ public override double FontRenderingEmSize {
+ get { return fontRenderingEmSize; }
+ }
+
+ /// <summary>
+ /// Sets the <see cref="FontRenderingEmSize"/>.
+ /// </summary>
+ public void SetFontRenderingEmSize(double value)
+ {
+ fontRenderingEmSize = value;
+ }
+
+ /// <inheritdoc/>
+ public override Brush ForegroundBrush {
+ get { return foregroundBrush; }
+ }
+
+ /// <summary>
+ /// Sets the <see cref="ForegroundBrush"/>.
+ /// </summary>
+ public void SetForegroundBrush(Brush value)
+ {
+ ExtensionMethods.CheckIsFrozen(value);
+ foregroundBrush = value;
+ }
+
+ /// <inheritdoc/>
+ public override Typeface Typeface {
+ get { return typeface; }
+ }
+
+ /// <summary>
+ /// Sets the <see cref="Typeface"/>.
+ /// </summary>
+ public void SetTypeface(Typeface value)
+ {
+ if (value == null)
+ throw new ArgumentNullException("value");
+ typeface = value;
+ }
+
+ /// <summary>
+ /// Gets the text decorations. The value may be null, a frozen <see cref="TextDecorationCollection"/>
+ /// or an unfrozen <see cref="TextDecorationCollection"/>.
+ /// If the value is an unfrozen <see cref="TextDecorationCollection"/>, you may assume that the
+ /// collection instance is only used for this <see cref="TextRunProperties"/> instance and it is safe
+ /// to add <see cref="TextDecoration"/>s.
+ /// </summary>
+ public override TextDecorationCollection TextDecorations {
+ get { return textDecorations; }
+ }
+
+ /// <summary>
+ /// Sets the <see cref="TextDecorations"/>.
+ /// </summary>
+ public void SetTextDecorations(TextDecorationCollection value)
+ {
+ ExtensionMethods.CheckIsFrozen(value);
+ textDecorations = value;
+ }
+
+ /// <summary>
+ /// Gets the text effects. The value may be null, a frozen <see cref="TextEffectCollection"/>
+ /// or an unfrozen <see cref="TextEffectCollection"/>.
+ /// If the value is an unfrozen <see cref="TextEffectCollection"/>, you may assume that the
+ /// collection instance is only used for this <see cref="TextRunProperties"/> instance and it is safe
+ /// to add <see cref="TextEffect"/>s.
+ /// </summary>
+ public override TextEffectCollection TextEffects {
+ get { return textEffects; }
+ }
+
+ /// <summary>
+ /// Sets the <see cref="TextEffects"/>.
+ /// </summary>
+ public void SetTextEffects(TextEffectCollection value)
+ {
+ ExtensionMethods.CheckIsFrozen(value);
+ textEffects = value;
+ }
+
+ /// <summary>
+ /// Gets the typography properties for the text run.
+ /// </summary>
+ public override TextRunTypographyProperties TypographyProperties {
+ get { return typographyProperties; }
+ }
+
+ /// <summary>
+ /// Sets the <see cref="TypographyProperties"/>.
+ /// </summary>
+ public void SetTypographyProperties(TextRunTypographyProperties value)
+ {
+ typographyProperties = value;
+ }
+
+ /// <summary>
+ /// Gets the number substitution settings for the text run.
+ /// </summary>
+ public override NumberSubstitution NumberSubstitution {
+ get { return numberSubstitution; }
+ }
+
+ /// <summary>
+ /// Sets the <see cref="NumberSubstitution"/>.
+ /// </summary>
+ public void SetNumberSubstitution(NumberSubstitution value)
+ {
+ numberSubstitution = value;
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineLinkText.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineLinkText.cs
new file mode 100644
index 000000000..57c472641
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineLinkText.cs
@@ -0,0 +1,114 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Windows;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.TextFormatting;
+using System.Windows.Navigation;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// VisualLineElement that represents a piece of text and is a clickable link.
+ /// </summary>
+ public class VisualLineLinkText : VisualLineText
+ {
+ /// <summary>
+ /// Gets/Sets the URL that is navigated to when the link is clicked.
+ /// </summary>
+ public Uri NavigateUri { get; set; }
+
+ /// <summary>
+ /// Gets/Sets the window name where the URL will be opened.
+ /// </summary>
+ public string TargetName { get; set; }
+
+ /// <summary>
+ /// Gets/Sets whether the user needs to press Control to click the link.
+ /// The default value is true.
+ /// </summary>
+ public bool RequireControlModifierForClick { get; set; }
+
+ /// <summary>
+ /// Creates a visual line text element with the specified length.
+ /// It uses the <see cref="ITextRunConstructionContext.VisualLine"/> and its
+ /// <see cref="VisualLineElement.RelativeTextOffset"/> to find the actual text string.
+ /// </summary>
+ public VisualLineLinkText(VisualLine parentVisualLine, int length) : base(parentVisualLine, length)
+ {
+ this.RequireControlModifierForClick = true;
+ }
+
+ /// <inheritdoc/>
+ public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context)
+ {
+ this.TextRunProperties.SetForegroundBrush(context.TextView.LinkTextForegroundBrush);
+ this.TextRunProperties.SetBackgroundBrush(context.TextView.LinkTextBackgroundBrush);
+ this.TextRunProperties.SetTextDecorations(TextDecorations.Underline);
+ return base.CreateTextRun(startVisualColumn, context);
+ }
+
+ /// <summary>
+ /// Gets whether the link is currently clickable.
+ /// </summary>
+ /// <remarks>Returns true when control is pressed; or when
+ /// <see cref="RequireControlModifierForClick"/> is disabled.</remarks>
+ protected bool LinkIsClickable()
+ {
+ if (NavigateUri == null)
+ return false;
+ if (RequireControlModifierForClick)
+ return (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control;
+ else
+ return true;
+ }
+
+ /// <inheritdoc/>
+ protected internal override void OnQueryCursor(QueryCursorEventArgs e)
+ {
+ if (LinkIsClickable()) {
+ e.Handled = true;
+ e.Cursor = Cursors.Hand;
+ }
+ }
+
+ /// <inheritdoc/>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
+ Justification = "I've seen Process.Start throw undocumented exceptions when the mail client / web browser is installed incorrectly")]
+ protected internal override void OnMouseDown(MouseButtonEventArgs e)
+ {
+ if (e.ChangedButton == MouseButton.Left && !e.Handled && LinkIsClickable()) {
+ RequestNavigateEventArgs args = new RequestNavigateEventArgs(this.NavigateUri, this.TargetName);
+ args.RoutedEvent = Hyperlink.RequestNavigateEvent;
+ FrameworkElement element = e.Source as FrameworkElement;
+ if (element != null) {
+ // allow user code to handle the navigation request
+ element.RaiseEvent(args);
+ }
+ if (!args.Handled) {
+ try {
+ Process.Start(this.NavigateUri.ToString());
+ } catch {
+ // ignore all kinds of errors during web browser start
+ }
+ }
+ e.Handled = true;
+ }
+ }
+
+ /// <inheritdoc/>
+ protected override VisualLineText CreateInstance(int length)
+ {
+ return new VisualLineLinkText(ParentVisualLine, length) {
+ NavigateUri = this.NavigateUri,
+ TargetName = this.TargetName,
+ RequireControlModifierForClick = this.RequireControlModifierForClick
+ };
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineText.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineText.cs
new file mode 100644
index 000000000..85b0a9ac4
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineText.cs
@@ -0,0 +1,122 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Collections.Generic;
+using System.Windows.Documents;
+using System.Windows.Media.TextFormatting;
+
+using Tango.Scripting.Editors.Document;
+using Tango.Scripting.Editors.Utils;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// VisualLineElement that represents a piece of text.
+ /// </summary>
+ public class VisualLineText : VisualLineElement
+ {
+ VisualLine parentVisualLine;
+
+ /// <summary>
+ /// Gets the parent visual line.
+ /// </summary>
+ public VisualLine ParentVisualLine {
+ get { return parentVisualLine; }
+ }
+
+ /// <summary>
+ /// Creates a visual line text element with the specified length.
+ /// It uses the <see cref="ITextRunConstructionContext.VisualLine"/> and its
+ /// <see cref="VisualLineElement.RelativeTextOffset"/> to find the actual text string.
+ /// </summary>
+ public VisualLineText(VisualLine parentVisualLine, int length) : base(length, length)
+ {
+ if (parentVisualLine == null)
+ throw new ArgumentNullException("parentVisualLine");
+ this.parentVisualLine = parentVisualLine;
+ }
+
+ /// <summary>
+ /// Override this method to control the type of new VisualLineText instances when
+ /// the visual line is split due to syntax highlighting.
+ /// </summary>
+ protected virtual VisualLineText CreateInstance(int length)
+ {
+ return new VisualLineText(parentVisualLine, length);
+ }
+
+ /// <inheritdoc/>
+ public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context)
+ {
+ if (context == null)
+ throw new ArgumentNullException("context");
+
+ int relativeOffset = startVisualColumn - VisualColumn;
+ StringSegment text = context.GetText(context.VisualLine.FirstDocumentLine.Offset + RelativeTextOffset + relativeOffset, DocumentLength - relativeOffset);
+ return new TextCharacters(text.Text, text.Offset, text.Count, this.TextRunProperties);
+ }
+
+ /// <inheritdoc/>
+ public override bool IsWhitespace(int visualColumn)
+ {
+ int offset = visualColumn - this.VisualColumn + parentVisualLine.FirstDocumentLine.Offset + this.RelativeTextOffset;
+ return char.IsWhiteSpace(parentVisualLine.Document.GetCharAt(offset));
+ }
+
+ /// <inheritdoc/>
+ public override TextSpan<CultureSpecificCharacterBufferRange> GetPrecedingText(int visualColumnLimit, ITextRunConstructionContext context)
+ {
+ if (context == null)
+ throw new ArgumentNullException("context");
+
+ int relativeOffset = visualColumnLimit - VisualColumn;
+ StringSegment text = context.GetText(context.VisualLine.FirstDocumentLine.Offset + RelativeTextOffset, relativeOffset);
+ CharacterBufferRange range = new CharacterBufferRange(text.Text, text.Offset, text.Count);
+ return new TextSpan<CultureSpecificCharacterBufferRange>(range.Length, new CultureSpecificCharacterBufferRange(this.TextRunProperties.CultureInfo, range));
+ }
+
+ /// <inheritdoc/>
+ public override bool CanSplit {
+ get { return true; }
+ }
+
+ /// <inheritdoc/>
+ public override void Split(int splitVisualColumn, IList<VisualLineElement> elements, int elementIndex)
+ {
+ if (splitVisualColumn <= VisualColumn || splitVisualColumn >= VisualColumn + VisualLength)
+ throw new ArgumentOutOfRangeException("splitVisualColumn", splitVisualColumn, "Value must be between " + (VisualColumn + 1) + " and " + (VisualColumn + VisualLength - 1));
+ if (elements == null)
+ throw new ArgumentNullException("elements");
+ if (elements[elementIndex] != this)
+ throw new ArgumentException("Invalid elementIndex - couldn't find this element at the index");
+ int relativeSplitPos = splitVisualColumn - VisualColumn;
+ VisualLineText splitPart = CreateInstance(DocumentLength - relativeSplitPos);
+ SplitHelper(this, splitPart, splitVisualColumn, relativeSplitPos + RelativeTextOffset);
+ elements.Insert(elementIndex + 1, splitPart);
+ }
+
+ /// <inheritdoc/>
+ public override int GetRelativeOffset(int visualColumn)
+ {
+ return this.RelativeTextOffset + visualColumn - this.VisualColumn;
+ }
+
+ /// <inheritdoc/>
+ public override int GetVisualColumn(int relativeTextOffset)
+ {
+ return this.VisualColumn + relativeTextOffset - this.RelativeTextOffset;
+ }
+
+ /// <inheritdoc/>
+ public override int GetNextCaretPosition(int visualColumn, LogicalDirection direction, CaretPositioningMode mode)
+ {
+ int textOffset = parentVisualLine.StartOffset + this.RelativeTextOffset;
+ int pos = TextUtilities.GetNextCaretPosition(parentVisualLine.Document, textOffset + visualColumn - this.VisualColumn, direction, mode);
+ if (pos < textOffset || pos > textOffset + this.DocumentLength)
+ return -1;
+ else
+ return this.VisualColumn + pos - textOffset;
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineTextParagraphProperties.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineTextParagraphProperties.cs
new file mode 100644
index 000000000..9c3aab87f
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineTextParagraphProperties.cs
@@ -0,0 +1,31 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Windows;
+using System.Windows.Media.TextFormatting;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ sealed class VisualLineTextParagraphProperties : TextParagraphProperties
+ {
+ internal TextRunProperties defaultTextRunProperties;
+ internal TextWrapping textWrapping;
+ internal double tabSize;
+ internal double indent;
+ internal bool firstLineInParagraph;
+
+ public override double DefaultIncrementalTab {
+ get { return tabSize; }
+ }
+
+ public override FlowDirection FlowDirection { get { return FlowDirection.LeftToRight; } }
+ public override TextAlignment TextAlignment { get { return TextAlignment.Left; } }
+ public override double LineHeight { get { return double.NaN; } }
+ public override bool FirstLineInParagraph { get { return firstLineInParagraph; } }
+ public override TextRunProperties DefaultTextRunProperties { get { return defaultTextRunProperties; } }
+ public override TextWrapping TextWrapping { get { return textWrapping; } }
+ public override TextMarkerProperties TextMarkerProperties { get { return null; } }
+ public override double Indent { get { return indent; } }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineTextSource.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineTextSource.cs
new file mode 100644
index 000000000..3a5167010
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLineTextSource.cs
@@ -0,0 +1,123 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Diagnostics;
+using System.Windows.Media.TextFormatting;
+
+using Tango.Scripting.Editors.Document;
+using Tango.Scripting.Editors.Utils;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// WPF TextSource implementation that creates TextRuns for a VisualLine.
+ /// </summary>
+ sealed class VisualLineTextSource : TextSource, ITextRunConstructionContext
+ {
+ public VisualLineTextSource(VisualLine visualLine)
+ {
+ this.VisualLine = visualLine;
+ }
+
+ public VisualLine VisualLine { get; private set; }
+ public TextView TextView { get; set; }
+ public TextDocument Document { get; set; }
+ public TextRunProperties GlobalTextRunProperties { get; set; }
+
+ public override 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.Length == 0)
+ throw new ArgumentException("The returned TextRun must not have length 0.", element.GetType().Name + ".Length");
+ if (relativeOffset + run.Length > element.VisualLength)
+ throw new ArgumentException("The returned TextRun is too long.", element.GetType().Name + ".CreateTextRun");
+ InlineObjectRun inlineRun = run as InlineObjectRun;
+ if (inlineRun != null) {
+ 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;
+ }
+ }
+
+ 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);
+ }
+
+ public override TextSpan<CultureSpecificCharacterBufferRange> GetPrecedingText(int textSourceCharacterIndexLimit)
+ {
+ try {
+ foreach (VisualLineElement element in VisualLine.Elements) {
+ if (textSourceCharacterIndexLimit > element.VisualColumn
+ && textSourceCharacterIndexLimit <= element.VisualColumn + element.VisualLength)
+ {
+ TextSpan<CultureSpecificCharacterBufferRange> span = element.GetPrecedingText(textSourceCharacterIndexLimit, this);
+ if (span == null)
+ 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;
+ }
+ }
+ CharacterBufferRange empty = CharacterBufferRange.Empty;
+ return new TextSpan<CultureSpecificCharacterBufferRange>(empty.Length, new CultureSpecificCharacterBufferRange(null, empty));
+ } catch (Exception ex) {
+ Debug.WriteLine(ex.ToString());
+ throw;
+ }
+ }
+
+ public override int GetTextEffectCharacterIndexFromTextSourceCharacterIndex(int textSourceCharacterIndex)
+ {
+ throw new NotSupportedException();
+ }
+
+ string cachedString;
+ int cachedStringOffset;
+
+ 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 = this.Document.GetText(offset, length));
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLinesInvalidException.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLinesInvalidException.cs
new file mode 100644
index 000000000..bfbae27b0
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualLinesInvalidException.cs
@@ -0,0 +1,44 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Runtime.Serialization;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// A VisualLinesInvalidException indicates that you accessed the <see cref="TextView.VisualLines"/> property
+ /// of the <see cref="TextView"/> while the visual lines were invalid.
+ /// </summary>
+ [Serializable]
+ public class VisualLinesInvalidException : Exception
+ {
+ /// <summary>
+ /// Creates a new VisualLinesInvalidException instance.
+ /// </summary>
+ public VisualLinesInvalidException() : base()
+ {
+ }
+
+ /// <summary>
+ /// Creates a new VisualLinesInvalidException instance.
+ /// </summary>
+ public VisualLinesInvalidException(string message) : base(message)
+ {
+ }
+
+ /// <summary>
+ /// Creates a new VisualLinesInvalidException instance.
+ /// </summary>
+ public VisualLinesInvalidException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+
+ /// <summary>
+ /// Creates a new VisualLinesInvalidException instance.
+ /// </summary>
+ protected VisualLinesInvalidException(SerializationInfo info, StreamingContext context) : base(info, context)
+ {
+ }
+ }
+}
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualYPosition.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualYPosition.cs
new file mode 100644
index 000000000..352a3eaa3
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/VisualYPosition.cs
@@ -0,0 +1,46 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+
+namespace Tango.Scripting.Editors.Rendering
+{
+ /// <summary>
+ /// An enum that specifies the possible Y positions that can be returned by VisualLine.GetVisualPosition.
+ /// </summary>
+ public enum VisualYPosition
+ {
+ /// <summary>
+ /// Returns the top of the TextLine.
+ /// </summary>
+ LineTop,
+ /// <summary>
+ /// Returns the top of the text.
+ /// If the line contains inline UI elements larger than the text, TextTop may be below LineTop.
+ /// For a line containing regular text (all in the editor's main font), this will be equal to LineTop.
+ /// </summary>
+ TextTop,
+ /// <summary>
+ /// Returns the bottom of the TextLine.
+ /// </summary>
+ LineBottom,
+ /// <summary>
+ /// The middle between LineTop and LineBottom.
+ /// </summary>
+ LineMiddle,
+ /// <summary>
+ /// Returns the bottom of the text.
+ /// If the line contains inline UI elements larger than the text, TextBottom might be above LineBottom.
+ /// For a line containing regular text (all in the editor's main font), this will be equal to LineBottom.
+ /// </summary>
+ TextBottom,
+ /// <summary>
+ /// The middle between TextTop and TextBottom.
+ /// </summary>
+ TextMiddle,
+ /// <summary>
+ /// Returns the baseline of the text.
+ /// </summary>
+ Baseline
+ }
+}