// 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 { /// /// 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. /// public sealed class VisualLine { TextView textView; List elements; internal bool hasInlineObjects; /// /// Gets the document to which this VisualLine belongs. /// public TextDocument Document { get; private set; } /// /// Gets the first document line displayed by this visual line. /// public DocumentLine FirstDocumentLine { get; private set; } /// /// Gets the last document line displayed by this visual line. /// public DocumentLine LastDocumentLine { get; private set; } /// /// Gets a read-only collection of line elements. /// public ReadOnlyCollection Elements { get; private set; } /// /// Gets a read-only collection of text lines. /// public ReadOnlyCollection TextLines { get; private set; } /// /// Gets the start offset of the VisualLine inside the document. /// This is equivalent to FirstDocumentLine.Offset. /// public int StartOffset { get { return FirstDocumentLine.Offset; } } /// /// Length in visual line coordinates. /// public int VisualLength { get; private set; } /// /// Gets the height of the visual line in device-independent pixels. /// public double Height { get; private set; } /// /// Gets the Y position of the line. This is measured in device-independent pixels relative to the start of the document. /// 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(); 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) { DocumentLine newEndLine = document.GetLineByOffset(offset); if (newEndLine == this.LastDocumentLine) { throw new InvalidOperationException( "The VisualLineElementGenerator " + g.GetType().Name + " produced an element which ends within the line delimiter"); } currentLineEnd = newEndLine.Offset + newEndLine.Length; this.LastDocumentLine = newEndLine; } 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 textLines) { this.TextLines = textLines.AsReadOnly(); Height = 0; foreach (TextLine line in textLines) Height += line.Height; } /// /// Gets the visual column from a document offset relative to the first line start. /// 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; } /// /// Gets the document offset (relative to the first line start) from a visual column. /// 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; } /// /// Gets the text line containing the specified visual column. /// 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?)"); } /// /// Gets the visual top from the specified text line. /// /// Distance in device-independent pixels /// from the top of the document to the top of the specified text line. 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"); } /// /// Gets the start visual column from the specified text line. /// 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; } /// /// Gets a TextLine by the visual position. /// 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]; } /// /// Gets the visual position from the specified visualColumn. /// /// Position in device-independent pixels /// relative to the top left of the document. 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); } /// /// 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. /// public int GetVisualColumn(Point point) { TextLine textLine = GetTextLineByVisualYPosition(point.Y); CharacterHit ch = textLine.GetCharacterHitFromDistance(point.X); return ch.FirstCharacterIndex + ch.TrailingLength; } /// /// 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. /// public int GetVisualColumnFloor(Point point) { TextLine textLine = GetTextLineByVisualYPosition(point.Y); if (point.X > textLine.WidthIncludingTrailingWhitespace) { // GetCharacterHitFromDistance returns a hit with FirstCharacterIndex=last character inline // and TrailingLength=1 when clicking behind the line, so the floor function needs to handle this case // specially end return the line's end column instead. return GetTextLineVisualStartColumn(textLine) + textLine.Length; } CharacterHit ch = textLine.GetCharacterHitFromDistance(point.X); return ch.FirstCharacterIndex; } /// /// Gets whether the visual line was disposed. /// public bool IsDisposed { get; internal set; } /// /// Gets the next possible caret position after visualColumn, or -1 if there is no caret position. /// 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; } } }