PageRenderTime 60ms CodeModel.GetById 22ms app.highlight 31ms RepoModel.GetById 1ms app.codeStats 0ms

/AvalonEdit/ICSharpCode.AvalonEdit/Rendering/BackgroundGeometryBuilder.cs

http://github.com/icsharpcode/ILSpy
C# | 358 lines | 251 code | 30 blank | 77 comment | 64 complexity | 71c3fe81873005d9ebbde62ca5678567 MD5 | raw file
  1// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
  2// 
  3// Permission is hereby granted, free of charge, to any person obtaining a copy of this
  4// software and associated documentation files (the "Software"), to deal in the Software
  5// without restriction, including without limitation the rights to use, copy, modify, merge,
  6// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
  7// to whom the Software is furnished to do so, subject to the following conditions:
  8// 
  9// The above copyright notice and this permission notice shall be included in all copies or
 10// substantial portions of the Software.
 11// 
 12// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 13// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 14// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
 15// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 16// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 17// DEALINGS IN THE SOFTWARE.
 18
 19using System;
 20using System.Collections.Generic;
 21using System.Linq;
 22using System.Windows;
 23using System.Windows.Controls.Primitives;
 24using System.Windows.Media;
 25using System.Windows.Media.TextFormatting;
 26using ICSharpCode.NRefactory.Editor;
 27using ICSharpCode.AvalonEdit.Document;
 28using ICSharpCode.AvalonEdit.Editing;
 29using ICSharpCode.AvalonEdit.Utils;
 30
 31namespace ICSharpCode.AvalonEdit.Rendering
 32{
 33	/// <summary>
 34	/// Helper for creating a PathGeometry.
 35	/// </summary>
 36	public sealed class BackgroundGeometryBuilder
 37	{
 38		double cornerRadius;
 39		
 40		/// <summary>
 41		/// Gets/sets the radius of the rounded corners.
 42		/// </summary>
 43		public double CornerRadius {
 44			get { return cornerRadius; }
 45			set { cornerRadius = value; }
 46		}
 47		
 48		/// <summary>
 49		/// Gets/Sets whether to align the geometry to whole pixels.
 50		/// </summary>
 51		public bool AlignToWholePixels { get; set; }
 52		
 53		/// <summary>
 54		/// Gets/Sets whether to align the geometry to the middle of pixels.
 55		/// </summary>
 56		public bool AlignToMiddleOfPixels { get; set; }
 57		
 58		/// <summary>
 59		/// Gets/Sets whether to extend the rectangles to full width at line end.
 60		/// </summary>
 61		public bool ExtendToFullWidthAtLineEnd { get; set; }
 62		
 63		/// <summary>
 64		/// Creates a new BackgroundGeometryBuilder instance.
 65		/// </summary>
 66		public BackgroundGeometryBuilder()
 67		{
 68		}
 69		
 70		/// <summary>
 71		/// Adds the specified segment to the geometry.
 72		/// </summary>
 73		public void AddSegment(TextView textView, ISegment segment)
 74		{
 75			if (textView == null)
 76				throw new ArgumentNullException("textView");
 77			Size pixelSize = PixelSnapHelpers.GetPixelSize(textView);
 78			foreach (Rect r in GetRectsForSegment(textView, segment, ExtendToFullWidthAtLineEnd)) {
 79				AddRectangle(pixelSize, r);
 80			}
 81		}
 82		
 83		/// <summary>
 84		/// Adds a rectangle to the geometry.
 85		/// </summary>
 86		/// <remarks>
 87		/// This overload will align the coordinates according to
 88		/// <see cref="AlignToWholePixels"/> or <see cref="AlignToMiddleOfPixels"/>.
 89		/// Use the <see cref="AddRectangle(double,double,double,double)"/>-overload instead if the coordinates should not be aligned.
 90		/// </remarks>
 91		public void AddRectangle(TextView textView, Rect rectangle)
 92		{
 93			AddRectangle(PixelSnapHelpers.GetPixelSize(textView), rectangle);
 94		}
 95
 96		void AddRectangle(Size pixelSize, Rect r)
 97		{
 98			if (AlignToWholePixels) {
 99				AddRectangle(PixelSnapHelpers.Round(r.Left, pixelSize.Width),
100				             PixelSnapHelpers.Round(r.Top + 1, pixelSize.Height),
101				             PixelSnapHelpers.Round(r.Right, pixelSize.Width),
102				             PixelSnapHelpers.Round(r.Bottom + 1, pixelSize.Height));
103			} else if (AlignToMiddleOfPixels) {
104				AddRectangle(PixelSnapHelpers.PixelAlign(r.Left, pixelSize.Width),
105				             PixelSnapHelpers.PixelAlign(r.Top + 1, pixelSize.Height),
106				             PixelSnapHelpers.PixelAlign(r.Right, pixelSize.Width),
107				             PixelSnapHelpers.PixelAlign(r.Bottom + 1, pixelSize.Height));
108			} else {
109				AddRectangle(r.Left, r.Top + 1, r.Right, r.Bottom + 1);
110			}
111		}
112		
113		/// <summary>
114		/// Calculates the list of rectangle where the segment in shown.
115		/// This method usually returns one rectangle for each line inside the segment
116		/// (but potentially more, e.g. when bidirectional text is involved).
117		/// </summary>
118		public static IEnumerable<Rect> GetRectsForSegment(TextView textView, ISegment segment, bool extendToFullWidthAtLineEnd = false)
119		{
120			if (textView == null)
121				throw new ArgumentNullException("textView");
122			if (segment == null)
123				throw new ArgumentNullException("segment");
124			return GetRectsForSegmentImpl(textView, segment, extendToFullWidthAtLineEnd);
125		}
126		
127		static IEnumerable<Rect> GetRectsForSegmentImpl(TextView textView, ISegment segment, bool extendToFullWidthAtLineEnd)
128		{
129			int segmentStart = segment.Offset;
130			int segmentEnd = segment.Offset + segment.Length;
131			
132			segmentStart = segmentStart.CoerceValue(0, textView.Document.TextLength);
133			segmentEnd = segmentEnd.CoerceValue(0, textView.Document.TextLength);
134			
135			TextViewPosition start;
136			TextViewPosition end;
137			
138			if (segment is SelectionSegment) {
139				SelectionSegment sel = (SelectionSegment)segment;
140				start = new TextViewPosition(textView.Document.GetLocation(sel.StartOffset), sel.StartVisualColumn);
141				end = new TextViewPosition(textView.Document.GetLocation(sel.EndOffset), sel.EndVisualColumn);
142			} else {
143				start = new TextViewPosition(textView.Document.GetLocation(segmentStart));
144				end = new TextViewPosition(textView.Document.GetLocation(segmentEnd));
145			}
146			
147			foreach (VisualLine vl in textView.VisualLines) {
148				int vlStartOffset = vl.FirstDocumentLine.Offset;
149				if (vlStartOffset > segmentEnd)
150					break;
151				int vlEndOffset = vl.LastDocumentLine.Offset + vl.LastDocumentLine.Length;
152				if (vlEndOffset < segmentStart)
153					continue;
154				
155				int segmentStartVC;
156				if (segmentStart < vlStartOffset)
157					segmentStartVC = 0;
158				else
159					segmentStartVC = vl.ValidateVisualColumn(start, extendToFullWidthAtLineEnd);
160				
161				int segmentEndVC;
162				if (segmentEnd > vlEndOffset)
163					segmentEndVC = extendToFullWidthAtLineEnd ? int.MaxValue : vl.VisualLengthWithEndOfLineMarker;
164				else
165					segmentEndVC = vl.ValidateVisualColumn(end, extendToFullWidthAtLineEnd);
166				
167				foreach (var rect in ProcessTextLines(textView, vl, segmentStartVC, segmentEndVC))
168					yield return rect;
169			}
170		}
171		
172		/// <summary>
173		/// Calculates the rectangles for the visual column segment.
174		/// This returns one rectangle for each line inside the segment.
175		/// </summary>
176		public static IEnumerable<Rect> GetRectsFromVisualSegment(TextView textView, VisualLine line, int startVC, int endVC)
177		{
178			if (textView == null)
179				throw new ArgumentNullException("textView");
180			if (line == null)
181				throw new ArgumentNullException("line");
182			return ProcessTextLines(textView, line, startVC, endVC);
183		}
184
185		static IEnumerable<Rect> ProcessTextLines(TextView textView, VisualLine visualLine, int segmentStartVC, int segmentEndVC)
186		{
187			TextLine lastTextLine = visualLine.TextLines.Last();
188			Vector scrollOffset = textView.ScrollOffset;
189			
190			for (int i = 0; i < visualLine.TextLines.Count; i++) {
191				TextLine line = visualLine.TextLines[i];
192				double y = visualLine.GetTextLineVisualYPosition(line, VisualYPosition.LineTop);
193				int visualStartCol = visualLine.GetTextLineVisualStartColumn(line);
194				int visualEndCol = visualStartCol + line.Length;
195				if (line != lastTextLine)
196					visualEndCol -= line.TrailingWhitespaceLength;
197				
198				if (segmentEndVC < visualStartCol)
199					break;
200				if (lastTextLine != line && segmentStartVC > visualEndCol)
201					continue;
202				int segmentStartVCInLine = Math.Max(segmentStartVC, visualStartCol);
203				int segmentEndVCInLine = Math.Min(segmentEndVC, visualEndCol);
204				y -= scrollOffset.Y;
205				if (segmentStartVCInLine == segmentEndVCInLine) {
206					// GetTextBounds crashes for length=0, so we'll handle this case with GetDistanceFromCharacterHit
207					// We need to return a rectangle to ensure empty lines are still visible
208					double pos = visualLine.GetTextLineVisualXPosition(line, segmentStartVCInLine);
209					pos -= scrollOffset.X;
210					// The following special cases are necessary to get rid of empty rectangles at the end of a TextLine if "Show Spaces" is active.
211					// 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.
212					// 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.
213					if (segmentEndVCInLine == visualEndCol && i < visualLine.TextLines.Count - 1 && segmentEndVC > segmentEndVCInLine && line.TrailingWhitespaceLength == 0)
214						continue;
215					if (segmentStartVCInLine == visualStartCol && i > 0 && segmentStartVC < segmentStartVCInLine && visualLine.TextLines[i - 1].TrailingWhitespaceLength == 0)
216						continue;
217					yield return new Rect(pos, y, 1, line.Height);
218				} else {
219					Rect lastRect = Rect.Empty;
220					if (segmentStartVCInLine <= visualEndCol) {
221						foreach (TextBounds b in line.GetTextBounds(segmentStartVCInLine, segmentEndVCInLine - segmentStartVCInLine)) {
222							double left = b.Rectangle.Left - scrollOffset.X;
223							double right = b.Rectangle.Right - scrollOffset.X;
224							if (!lastRect.IsEmpty)
225								yield return lastRect;
226							// left>right is possible in RTL languages
227							lastRect = new Rect(Math.Min(left, right), y, Math.Abs(right - left), line.Height);
228						}
229					}
230					if (segmentEndVC >= visualLine.VisualLengthWithEndOfLineMarker) {
231						double left = (segmentStartVC > visualLine.VisualLengthWithEndOfLineMarker ? visualLine.GetTextLineVisualXPosition(lastTextLine, segmentStartVC) : line.Width) - scrollOffset.X;
232						double right = ((segmentEndVC == int.MaxValue || line != lastTextLine) ? Math.Max(((IScrollInfo)textView).ExtentWidth, ((IScrollInfo)textView).ViewportWidth) : visualLine.GetTextLineVisualXPosition(lastTextLine, segmentEndVC)) - scrollOffset.X;
233						Rect extendSelection = new Rect(Math.Min(left, right), y, Math.Abs(right - left), line.Height);
234						if (!lastRect.IsEmpty) {
235							if (extendSelection.IntersectsWith(lastRect)) {
236								lastRect.Union(extendSelection);
237								yield return lastRect;
238							} else {
239								yield return lastRect;
240								yield return extendSelection;
241							}
242						} else
243							yield return extendSelection;
244					} else
245						yield return lastRect;
246				}
247			}
248		}
249		
250		PathFigureCollection figures = new PathFigureCollection();
251		PathFigure figure;
252		int insertionIndex;
253		double lastTop, lastBottom;
254		double lastLeft, lastRight;
255		
256		/// <summary>
257		/// Adds a rectangle to the geometry.
258		/// </summary>
259		/// <remarks>
260		/// This overload assumes that the coordinates are aligned properly
261		/// (see <see cref="AlignToWholePixels"/>, <see cref="AlignToMiddleOfPixels"/>).
262		/// Use the <see cref="AddRectangle(TextView,Rect)"/>-overload instead if the coordinates are not yet aligned.
263		/// </remarks>
264		public void AddRectangle(double left, double top, double right, double bottom)
265		{
266			if (!top.IsClose(lastBottom)) {
267				CloseFigure();
268			}
269			if (figure == null) {
270				figure = new PathFigure();
271				figure.StartPoint = new Point(left, top + cornerRadius);
272				if (Math.Abs(left - right) > cornerRadius) {
273					figure.Segments.Add(MakeArc(left + cornerRadius, top, SweepDirection.Clockwise));
274					figure.Segments.Add(MakeLineSegment(right - cornerRadius, top));
275					figure.Segments.Add(MakeArc(right, top + cornerRadius, SweepDirection.Clockwise));
276				}
277				figure.Segments.Add(MakeLineSegment(right, bottom - cornerRadius));
278				insertionIndex = figure.Segments.Count;
279				//figure.Segments.Add(MakeArc(left, bottom - cornerRadius, SweepDirection.Clockwise));
280			} else {
281				if (!lastRight.IsClose(right)) {
282					double cr = right < lastRight ? -cornerRadius : cornerRadius;
283					SweepDirection dir1 = right < lastRight ? SweepDirection.Clockwise : SweepDirection.Counterclockwise;
284					SweepDirection dir2 = right < lastRight ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;
285					figure.Segments.Insert(insertionIndex++, MakeArc(lastRight + cr, lastBottom, dir1));
286					figure.Segments.Insert(insertionIndex++, MakeLineSegment(right - cr, top));
287					figure.Segments.Insert(insertionIndex++, MakeArc(right, top + cornerRadius, dir2));
288				}
289				figure.Segments.Insert(insertionIndex++, MakeLineSegment(right, bottom - cornerRadius));
290				figure.Segments.Insert(insertionIndex, MakeLineSegment(lastLeft, lastTop + cornerRadius));
291				if (!lastLeft.IsClose(left)) {
292					double cr = left < lastLeft ? cornerRadius : -cornerRadius;
293					SweepDirection dir1 = left < lastLeft ? SweepDirection.Counterclockwise : SweepDirection.Clockwise;
294					SweepDirection dir2 = left < lastLeft ? SweepDirection.Clockwise : SweepDirection.Counterclockwise;
295					figure.Segments.Insert(insertionIndex, MakeArc(lastLeft, lastBottom - cornerRadius, dir1));
296					figure.Segments.Insert(insertionIndex, MakeLineSegment(lastLeft - cr, lastBottom));
297					figure.Segments.Insert(insertionIndex, MakeArc(left + cr, lastBottom, dir2));
298				}
299			}
300			this.lastTop = top;
301			this.lastBottom = bottom;
302			this.lastLeft = left;
303			this.lastRight = right;
304		}
305		
306		ArcSegment MakeArc(double x, double y, SweepDirection dir)
307		{
308			ArcSegment arc = new ArcSegment(
309				new Point(x, y),
310				new Size(cornerRadius, cornerRadius),
311				0, false, dir, true);
312			arc.Freeze();
313			return arc;
314		}
315		
316		static LineSegment MakeLineSegment(double x, double y)
317		{
318			LineSegment ls = new LineSegment(new Point(x, y), true);
319			ls.Freeze();
320			return ls;
321		}
322		
323		/// <summary>
324		/// Closes the current figure.
325		/// </summary>
326		public void CloseFigure()
327		{
328			if (figure != null) {
329				figure.Segments.Insert(insertionIndex, MakeLineSegment(lastLeft, lastTop + cornerRadius));
330				if (Math.Abs(lastLeft - lastRight) > cornerRadius) {
331					figure.Segments.Insert(insertionIndex, MakeArc(lastLeft, lastBottom - cornerRadius, SweepDirection.Clockwise));
332					figure.Segments.Insert(insertionIndex, MakeLineSegment(lastLeft + cornerRadius, lastBottom));
333					figure.Segments.Insert(insertionIndex, MakeArc(lastRight - cornerRadius, lastBottom, SweepDirection.Clockwise));
334				}
335				
336				figure.IsClosed = true;
337				figures.Add(figure);
338				figure = null;
339			}
340		}
341		
342		/// <summary>
343		/// Creates the geometry.
344		/// Returns null when the geometry is empty!
345		/// </summary>
346		public Geometry CreateGeometry()
347		{
348			CloseFigure();
349			if (figures.Count != 0) {
350				PathGeometry g = new PathGeometry(figures);
351				g.Freeze();
352				return g;
353			} else {
354				return null;
355			}
356		}
357	}
358}