mirror of https://github.com/icsharpcode/ILSpy.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
416 lines
14 KiB
416 lines
14 KiB
// 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 ICSharpCode.AvalonEdit.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 ICSharpCode.AvalonEdit.Document; |
|
|
|
namespace ICSharpCode.AvalonEdit.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 |
|
{ |
|
TextView textView; |
|
List<VisualLineElement> elements; |
|
internal bool hasInlineObjects; |
|
|
|
/// <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; } |
|
|
|
/// <summary> |
|
/// Gets a read-only collection of text lines. |
|
/// </summary> |
|
public ReadOnlyCollection<TextLine> TextLines { get; private set; } |
|
|
|
/// <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> |
|
/// 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) |
|
{ |
|
foreach (VisualLineElementGenerator g in generators) { |
|
g.StartGeneration(context); |
|
} |
|
elements = new List<VisualLineElement>(); |
|
PerformVisualElementConstruction(generators); |
|
foreach (VisualLineElementGenerator g in generators) { |
|
g.FinishGeneration(); |
|
} |
|
|
|
this.Elements = elements.AsReadOnly(); |
|
CalculateOffsets(context.GlobalTextRunProperties); |
|
} |
|
|
|
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) { |
|
LastDocumentLine = document.GetLineByOffset(offset); |
|
currentLineEnd = LastDocumentLine.Offset + LastDocumentLine.Length; |
|
} |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
void CalculateOffsets(TextRunProperties globalTextRunProperties) |
|
{ |
|
int visualOffset = 0; |
|
int textOffset = 0; |
|
foreach (VisualLineElement element in Elements) { |
|
element.VisualColumn = visualOffset; |
|
element.RelativeTextOffset = textOffset; |
|
element.SetTextRunProperties(new VisualLineElementTextRunProperties(globalTextRunProperties)); |
|
visualOffset += element.VisualLength; |
|
textOffset += element.DocumentLength; |
|
} |
|
VisualLength = visualOffset; |
|
Debug.Assert(textOffset == LastDocumentLine.Offset + LastDocumentLine.Length - FirstDocumentLine.Offset); |
|
} |
|
|
|
internal void RunTransformers(ITextRunConstructionContext context, IVisualLineTransformer[] transformers) |
|
{ |
|
foreach (IVisualLineTransformer transformer in transformers) { |
|
transformer.Transform(context, elements); |
|
} |
|
} |
|
|
|
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) |
|
{ |
|
ThrowUtil.CheckInRangeInclusive(visualColumn, "visualColumn", 0, VisualLength); |
|
if (visualColumn == VisualLength) |
|
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.Height - textView.FontSize; |
|
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 = textLine.GetDistanceFromCharacterHit(new CharacterHit(visualColumn, 0)); |
|
double yPos = GetTextLineVisualYPosition(textLine, yPositionMode); |
|
return new Point(xPos, yPos); |
|
} |
|
|
|
/// <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) |
|
{ |
|
TextLine textLine = GetTextLineByVisualYPosition(point.Y); |
|
CharacterHit ch = textLine.GetCharacterHitFromDistance(point.X); |
|
return ch.FirstCharacterIndex + ch.TrailingLength; |
|
} |
|
|
|
/// <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) |
|
{ |
|
TextLine textLine = GetTextLineByVisualYPosition(point.Y); |
|
CharacterHit ch = textLine.GetCharacterHitFromDistance(point.X); |
|
return ch.FirstCharacterIndex; |
|
} |
|
|
|
/// <summary> |
|
/// Gets whether the visual line was disposed. |
|
/// </summary> |
|
public bool IsDisposed { get; internal set; } |
|
|
|
/// <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) |
|
{ |
|
if (elements.Count == 0) { |
|
// special handling for empty visual lines: |
|
// 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)) { |
|
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 (visualColumn < this.VisualLength && !elements[elements.Count-1].HandlesLineBorders && HasImplicitStopAtLineEnd(mode)) |
|
return this.VisualLength; |
|
} |
|
// we've found nothing, return -1 and let the caret search continue in the next line |
|
return -1; |
|
} |
|
|
|
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; |
|
} |
|
} |
|
}
|
|
|