aboutsummaryrefslogtreecommitdiffstats
path: root/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Rendering/BackgroundGeometryBuilder.cs
blob: d3c839b3528d9cf46c3b463c6e89e5bd5a3b656c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
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;
			}
		}
	}
}