Click here to Skip to main content
Click here to Skip to main content
Add your own
alternative version

Using AvalonEdit (WPF Text Editor)

, 1 Apr 2013 LGPL3
AvalonEdit is an extensible Open-Source text editor with support for syntax highlighting and folding.
AvalonEdit_4.3.1_Binaries-noexe.zip
AvalonEdit_4.3.1_Binaries.zip
AvalonEdit.Sample.exe
ICSharpCode.AvalonEdit.dll
AvalonEdit_4.3.1_Source-noexe.zip
AvalonEdit.Sample
AvalonEdit
dependencies.png
folding.png
renderingPipeline.png
screenshot.png
snoop.png
CustomHighlighting.xshd
Images
Copy.png
Cut.png
Delete.png
Open.png
Paste.png
Redo.png
Save.png
Undo.png
WordWrap.png
Properties
RenderingPipeline.pptx
Documentation
Architecture.aml
Code Completion.aml
Coordinate Systems.aml
Folding.aml
ICSharpCode.AvalonEdit.content
ICSharpCode.AvalonEdit.shfbproj
Media
NamespaceDependencies.png
RenderingPipeline.png
VisualTree.png
WelcomeScreenshot.png
Sample Application.aml
Syntax Highlighting.aml
Text Rendering.aml
Welcome.aml
ICSharpCode.AvalonEdit.Tests
Document
Editing
Highlighting
ICSharpCode.AvalonEdit.Tests.PartCover.Settings
Properties
Search
Utils
ICSharpCode.AvalonEdit
CodeCompletion
Document
Editing
Folding
Highlighting
Resources
ASPX.xshd
Boo.xshd
Coco-Mode.xshd
CPP-Mode.xshd
CSharp-Mode.xshd
CSS-Mode.xshd
HTML-Mode.xshd
Java-Mode.xshd
JavaScript-Mode.xshd
MarkDown-Mode.xshd
Patch-Mode.xshd
PHP-Mode.xshd
PowerShell.xshd
Tex-Mode.xshd
VBNET-Mode.xshd
XML-Mode.xshd
XmlDoc.xshd
Xshd
ICSharpCode.AvalonEdit.snk
Indentation
CSharp
Properties
Rendering
Search
next.png
prev.png
Snippets
themes
RightArrow.cur
Utils
Xml
AvalonEdit_4.3.1_Source.zip
dependencies.png
folding.png
renderingPipeline.png
screenshot.png
snoop.png
CustomHighlighting.xshd
Copy.png
Cut.png
Delete.png
Open.png
Paste.png
Redo.png
Save.png
Undo.png
WordWrap.png
RenderingPipeline.pptx
Architecture.aml
Code Completion.aml
Coordinate Systems.aml
Folding.aml
ICSharpCode.AvalonEdit.content
ICSharpCode.AvalonEdit.shfbproj
NamespaceDependencies.png
RenderingPipeline.png
VisualTree.png
WelcomeScreenshot.png
Sample Application.aml
Syntax Highlighting.aml
Text Rendering.aml
Welcome.aml
ICSharpCode.AvalonEdit.Tests.PartCover.Settings
nunit.framework.dll
ASPX.xshd
Boo.xshd
Coco-Mode.xshd
CPP-Mode.xshd
CSharp-Mode.xshd
CSS-Mode.xshd
HTML-Mode.xshd
Java-Mode.xshd
JavaScript-Mode.xshd
MarkDown-Mode.xshd
Patch-Mode.xshd
PHP-Mode.xshd
PowerShell.xshd
Tex-Mode.xshd
VBNET-Mode.xshd
XML-Mode.xshd
XmlDoc.xshd
ICSharpCode.AvalonEdit.snk
next.png
prev.png
RightArrow.cur
AvalonEdit_Binaries-noexe.zip
AvalonEdit_Binaries.zip
AvalonEdit.Sample.exe
ICSharpCode.AvalonEdit.dll
AvalonEdit_CHM_Documentation.zip
AvalonEdit Documentation.chm
AvalonEdit_Source.zip
dependencies.png
folding.png
renderingPipeline.png
screenshot.png
snoop.png
CustomHighlighting.xshd
Copy.png
Cut.png
Delete.png
Open.png
Paste.png
Redo.png
Save.png
Undo.png
WordWrap.png
RenderingPipeline.pptx
Architecture.aml
Code Completion.aml
Coordinate Systems.aml
Folding.aml
Help
ICSharpCode.AvalonEdit.content
ICSharpCode.AvalonEdit.shfbproj
NamespaceDependencies.png
RenderingPipeline.png
VisualTree.png
WelcomeScreenshot.png
Sample Application.aml
Syntax Highlighting.aml
Text Rendering.aml
Welcome.aml
ICSharpCode.AvalonEdit.Tests.PartCover.Settings
nunit.framework.dll
ASPX.xshd
Boo.xshd
Coco-Mode.xshd
CPP-Mode.xshd
CSharp-Mode.xshd
CSS-Mode.xshd
HTML-Mode.xshd
Java-Mode.xshd
JavaScript-Mode.xshd
Patch-Mode.xshd
PHP-Mode.xshd
PowerShell.xshd
Tex-Mode.xshd
VBNET-Mode.xshd
XML-Mode.xshd
XmlDoc.xshd
ICSharpCode.AvalonEdit.snk
next.png
prev.png
RightArrow.cur
// 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 ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Editing;
using ICSharpCode.AvalonEdit.Utils;

namespace ICSharpCode.AvalonEdit.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;
			}
		}
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)

Share

About the Author

Daniel Grunwald

Germany Germany
I am the lead developer on the SharpDevelop open source project.
Follow on   Twitter   Google+   LinkedIn

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.150331.1 | Last Updated 1 Apr 2013
Article Copyright 2009 by Daniel Grunwald
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid