// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Unity.CodeEditor.Document;
using Unity.CodeEditor.Text;
using Unity.CodeEditor.Utils;
using UnityEngine;
using Debug = System.Diagnostics.Debug;
using LogicalDirection = Unity.CodeEditor.Document.LogicalDirection;
namespace Unity.CodeEditor.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>
internal sealed class VisualLine
{
internal const int LENGTH_LIMIT = 3000;
private enum LifetimePhase : byte
{
Generating,
Transforming,
Live,
Disposed
}
private readonly TextView _textView;
private List<VisualLineElement> _elements;
internal bool HasInlineObjects;
private LifetimePhase _phase;
/// <summary>
/// Gets the document to which this VisualLine belongs.
/// </summary>
internal TextDocument Document { get; }
/// <summary>
/// Gets the first document line displayed by this visual line.
/// </summary>
internal DocumentLine FirstDocumentLine { get; }
/// <summary>
/// Gets the last document line displayed by this visual line.
/// </summary>
internal DocumentLine LastDocumentLine { get; private set; }
/// <summary>
/// Gets a read-only collection of line elements.
/// </summary>
internal ReadOnlyCollection<VisualLineElement> Elements { get; private set; }
private ReadOnlyCollection<TextLine> _textLines;
/// <summary>
/// Gets a read-only collection of text lines.
/// </summary>
internal ReadOnlyCollection<TextLine> TextLines
{
get
{
if (_phase < LifetimePhase.Live)
throw new InvalidOperationException();
return _textLines;
}
}
/// <summary>
/// Gets the start offset of the VisualLine inside the document.
/// This is equivalent to <c>FirstDocumentLine.Offset</c>.
/// </summary>
internal int StartOffset => FirstDocumentLine.Offset;
/// <summary>
/// Length in visual line coordinates.
/// </summary>
internal int VisualLength { get; private set; }
/// <summary>
/// Length in visual line coordinates including the end of line marker, if TextEditorOptions.ShowEndOfLine is enabled.
/// </summary>
internal int VisualLengthWithEndOfLineMarker
{
get
{
var length = VisualLength;
if (_textView.Options.ShowEndOfLine && LastDocumentLine.NextLine != null) length++;
return length;
}
}
/// <summary>
/// Gets the height of the visual line in device-independent pixels.
/// </summary>
internal float 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>
internal float VisualTop { get; set; }
internal VisualLine(TextView textView, DocumentLine firstDocumentLine)
{
Debug.Assert(textView != null);
Debug.Assert(firstDocumentLine != null);
_textView = textView;
Document = textView.Document;
FirstDocumentLine = firstDocumentLine;
}
internal void ConstructVisualElements(ITextRunConstructionContext context, IReadOnlyList<VisualLineElementGenerator> generators)
{
Debug.Assert(_phase == LifetimePhase.Generating);
foreach (var g in generators)
{
g.StartGeneration(context);
}
_elements = new List<VisualLineElement>();
PerformVisualElementConstruction(generators);
foreach (var g in generators)
{
g.FinishGeneration();
}
var textParagraphProperties = context.TextParagraphProperties;
foreach (var element in _elements)
{
element.SetTextParagraphProperties(textParagraphProperties);
}
this.Elements = new ReadOnlyCollection<VisualLineElement>(_elements);
CalculateOffsets();
_phase = LifetimePhase.Transforming;
}
void PerformVisualElementConstruction(IReadOnlyList<VisualLineElementGenerator> generators)
{
var lineLength = FirstDocumentLine.Length;
var offset = FirstDocumentLine.Offset;
var currentLineEnd = offset + lineLength;
LastDocumentLine = FirstDocumentLine;
var askInterestOffset = 0; // 0 or 1
while (offset + askInterestOffset <= currentLineEnd)
{
var textPieceEndOffset = currentLineEnd;
foreach (var 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)
{
var 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 (var g in generators)
{
if (g.CachedInterest == offset)
{
var 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)
{
var newEndLine = Document.GetLineByOffset(offset);
currentLineEnd = newEndLine.Offset + newEndLine.Length;
this.LastDocumentLine = newEndLine;
if (currentLineEnd < offset)
{
throw new InvalidOperationException(
"The VisualLineElementGenerator " + g.GetType().Name +
" produced an element which ends within the line delimiter");
}
}
break;
}
}
}
}
}
}
private void CalculateOffsets()
{
var visualOffset = 0;
var textOffset = 0;
foreach (var element in _elements)
{
element.VisualColumn = visualOffset;
element.RelativeTextOffset = textOffset;
visualOffset += element.VisualLength;
textOffset += element.DocumentLength;
}
VisualLength = visualOffset;
Debug.Assert(textOffset == LastDocumentLine.EndOffset - FirstDocumentLine.Offset);
}
internal void RunTransformers(ITextRunConstructionContext context, IReadOnlyList<IVisualLineTransformer> transformers)
{
Debug.Assert(_phase == LifetimePhase.Transforming);
foreach (var transformer in transformers)
{
transformer.Transform(context, _elements);
}
_phase = LifetimePhase.Live;
}
/// <summary>
/// Replaces the single element at <paramref name="elementIndex"/> with the specified elements.
/// The replacement operation must preserve the document length, but may change the visual length.
/// </summary>
/// <remarks>
/// This method may only be called by line transformers.
/// </remarks>
internal void ReplaceElement(int elementIndex, params VisualLineElement[] newElements)
{
ReplaceElement(elementIndex, 1, newElements);
}
/// <summary>
/// Replaces <paramref name="count"/> elements starting at <paramref name="elementIndex"/> with the specified elements.
/// The replacement operation must preserve the document length, but may change the visual length.
/// </summary>
/// <remarks>
/// This method may only be called by line transformers.
/// </remarks>
internal void ReplaceElement(int elementIndex, int count, params VisualLineElement[] newElements)
{
if (_phase != LifetimePhase.Transforming)
throw new InvalidOperationException("This method may only be called by line transformers.");
var oldDocumentLength = 0;
for (var i = elementIndex; i < elementIndex + count; i++)
{
oldDocumentLength += _elements[i].DocumentLength;
}
var newDocumentLength = 0;
foreach (var newElement in newElements)
{
newDocumentLength += newElement.DocumentLength;
}
if (oldDocumentLength != newDocumentLength)
throw new InvalidOperationException("Old elements have document length " + oldDocumentLength + ", but new elements have length " + newDocumentLength);
_elements.RemoveRange(elementIndex, count);
_elements.InsertRange(elementIndex, newElements);
CalculateOffsets();
}
internal void SetTextLines(List<TextLine> textLines)
{
_textLines = new ReadOnlyCollection<TextLine>(textLines);
Height = 0;
foreach (var line in textLines)
Height += line.Height;
}
/// <summary>
/// Gets the visual column from a document offset relative to the first line start.
/// </summary>
internal int GetVisualColumn(int relativeTextOffset)
{
ThrowUtil.CheckNotNegative(relativeTextOffset, "relativeTextOffset");
foreach (var 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>
internal int GetRelativeOffset(int visualColumn)
{
ThrowUtil.CheckNotNegative(visualColumn, "visualColumn");
var documentLength = 0;
foreach (var 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>
internal TextLine GetTextLine(int visualColumn)
{
return GetTextLine(visualColumn, false);
}
/// <summary>
/// Gets the text line containing the specified visual column.
/// </summary>
internal TextLine GetTextLine(int visualColumn, bool isAtEndOfLine)
{
if (visualColumn < 0)
throw new ArgumentOutOfRangeException(nameof(visualColumn));
if (visualColumn >= VisualLengthWithEndOfLineMarker)
return TextLines[TextLines.Count - 1];
foreach (var line in TextLines)
{
if (isAtEndOfLine ? visualColumn <= line.Length : visualColumn < line.Length)
return line;
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>
internal float GetTextLineVisualYPosition(TextLine textLine, VisualYPosition yPositionMode)
{
if (textLine == null)
throw new ArgumentNullException(nameof(textLine));
var pos = VisualTop;
foreach (var 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.LineRect.yMin - _textView.DefaultBaseline;
case VisualYPosition.TextBottom:
return pos + tl.LineRect.yMin - _textView.DefaultBaseline + _textView.DefaultLineHeight;
case VisualYPosition.TextMiddle:
return pos + tl.LineRect.yMin - _textView.DefaultBaseline + _textView.DefaultLineHeight / 2;
case VisualYPosition.Baseline:
return pos + tl.LineRect.yMin;
default:
throw new ArgumentException("Invalid yPositionMode:" + yPositionMode);
}
}
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>
internal int GetTextLineVisualStartColumn(TextLine textLine)
{
if (!TextLines.Contains(textLine))
throw new ArgumentException("textLine is not a line in this VisualLine");
return TextLines.TakeWhile(tl => tl != textLine).Sum(tl => tl.Length);
}
/// <summary>
/// Gets a TextLine by the visual position.
/// </summary>
internal TextLine GetTextLineByVisualYPosition(double visualTop)
{
const double epsilon = 0.0001;
var pos = VisualTop;
foreach (var 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>
internal Vector2 GetVisualPosition(int visualColumn, VisualYPosition yPositionMode)
{
var textLine = GetTextLine(visualColumn);
var xPos = GetTextLineVisualXPosition(textLine, visualColumn);
var yPos = GetTextLineVisualYPosition(textLine, yPositionMode);
return new Vector2(xPos, yPos);
}
internal Vector2 GetVisualPosition(int visualColumn, bool isAtEndOfLine, VisualYPosition yPositionMode)
{
var textLine = GetTextLine(visualColumn, isAtEndOfLine);
var xPos = GetTextLineVisualXPosition(textLine, visualColumn);
var yPos = GetTextLineVisualYPosition(textLine, yPositionMode);
return new Vector2(xPos, yPos);
}
/// <summary>
/// Gets the distance to the left border of the text area of the specified visual column.
/// The visual column must belong to the specified text line.
/// </summary>
internal float GetTextLineVisualXPosition(TextLine textLine, int visualColumn)
{
if (textLine == null)
throw new ArgumentNullException(nameof(textLine));
var xPos = textLine.GetDistanceFromCharacterHit(new CharacterHit(Math.Min(visualColumn,
VisualLengthWithEndOfLineMarker)));
if (visualColumn > VisualLengthWithEndOfLineMarker)
{
xPos += (visualColumn - VisualLengthWithEndOfLineMarker) * _textView.WideSpaceWidth;
}
return xPos;
}
/// <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>
internal int GetVisualColumn(Vector2 point)
{
return GetVisualColumn(point, _textView.Options.EnableVirtualSpace);
}
/// <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>
internal int GetVisualColumn(Vector2 point, bool allowVirtualSpace)
{
return GetVisualColumn(GetTextLineByVisualYPosition(point.y), point.x, allowVirtualSpace);
}
internal int GetVisualColumn(Vector2 point, bool allowVirtualSpace, out bool isAtEndOfLine)
{
var textLine = GetTextLineByVisualYPosition(point.y);
var vc = GetVisualColumn(textLine, point.x, allowVirtualSpace);
isAtEndOfLine = (vc >= GetTextLineVisualStartColumn(textLine) + textLine.Length);
return vc;
}
/// <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>
internal int GetVisualColumn(TextLine textLine, float xPos, bool allowVirtualSpace)
{
if (xPos > textLine.WidthIncludingTrailingWhitespace)
{
if (allowVirtualSpace && textLine == TextLines[TextLines.Count - 1])
{
var virtualX = (int)Math.Round((xPos - textLine.WidthIncludingTrailingWhitespace) / _textView.WideSpaceWidth, MidpointRounding.AwayFromZero);
return VisualLengthWithEndOfLineMarker + virtualX;
}
}
var ch = textLine.GetCharacterHitFromDistance(xPos);
return ch.FirstCharacterIndex + ch.TrailingLength;
}
/// <summary>
/// Validates the visual column and returns the correct one.
/// </summary>
internal int ValidateVisualColumn(TextViewPosition position, bool allowVirtualSpace)
{
return ValidateVisualColumn(Document.GetOffset(position.Location), position.VisualColumn, allowVirtualSpace);
}
/// <summary>
/// Validates the visual column and returns the correct one.
/// </summary>
internal int ValidateVisualColumn(int offset, int visualColumn, bool allowVirtualSpace)
{
var firstDocumentLineOffset = FirstDocumentLine.Offset;
if (visualColumn < 0)
{
return GetVisualColumn(offset - firstDocumentLineOffset);
}
var offsetFromVisualColumn = GetRelativeOffset(visualColumn);
offsetFromVisualColumn += firstDocumentLineOffset;
if (offsetFromVisualColumn != offset)
{
return GetVisualColumn(offset - firstDocumentLineOffset);
}
if (visualColumn > VisualLength && !allowVirtualSpace)
{
return VisualLength;
}
return visualColumn;
}
/// <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>
internal int GetVisualColumnFloor(Vector2 point)
{
return GetVisualColumnFloor(point, _textView.Options.EnableVirtualSpace);
}
/// <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>
internal int GetVisualColumnFloor(Vector2 point, bool allowVirtualSpace)
{
return GetVisualColumnFloor(point, allowVirtualSpace, out _);
}
internal int GetVisualColumnFloor(Vector2 point, bool allowVirtualSpace, out bool isAtEndOfLine)
{
var textLine = GetTextLineByVisualYPosition(point.y);
if (point.x > textLine.WidthIncludingTrailingWhitespace)
{
isAtEndOfLine = true;
if (allowVirtualSpace && textLine == TextLines[TextLines.Count - 1])
{
// clicking virtual space in the last line
var virtualX = (int)((point.x - textLine.WidthIncludingTrailingWhitespace) / _textView.WideSpaceWidth);
return VisualLengthWithEndOfLineMarker + virtualX;
}
// GetCharacterHitFromDistance returns a hit with FirstCharacterIndex=last character in line
// and TrailingLength=1 when clicking behind the line, so the floor function needs to handle this case
// specially and return the line's end column instead.
return GetTextLineVisualStartColumn(textLine) + textLine.Length;
}
isAtEndOfLine = false;
var ch = textLine.GetCharacterHitFromDistance(point.x);
return ch.FirstCharacterIndex;
}
/// <summary>
/// Gets the text view position from the specified visual column.
/// </summary>
internal TextViewPosition GetTextViewPosition(int visualColumn)
{
var documentOffset = GetRelativeOffset(visualColumn) + FirstDocumentLine.Offset;
return new TextViewPosition(Document.GetLocation(documentOffset), visualColumn);
}
/// <summary>
/// Gets the text view position from the specified visual position.
/// If the position is within a character, it is rounded to the next character boundary.
/// </summary>
/// <param name="visualPosition">The position in device-independent pixels relative
/// to the top left corner of the document.</param>
/// <param name="allowVirtualSpace">Controls whether positions in virtual space may be returned.</param>
internal TextViewPosition GetTextViewPosition(Vector2 visualPosition, bool allowVirtualSpace)
{
var visualColumn = GetVisualColumn(visualPosition, allowVirtualSpace, out var isAtEndOfLine);
var documentOffset = GetRelativeOffset(visualColumn) + FirstDocumentLine.Offset;
var pos = new TextViewPosition(Document.GetLocation(documentOffset), visualColumn)
{
IsAtEndOfLine = isAtEndOfLine
};
return pos;
}
/// <summary>
/// Gets the text view position from the specified visual position.
/// If the position is inside a character, the position in front of the character is returned.
/// </summary>
/// <param name="visualPosition">The position in device-independent pixels relative
/// to the top left corner of the document.</param>
/// <param name="allowVirtualSpace">Controls whether positions in virtual space may be returned.</param>
internal TextViewPosition GetTextViewPositionFloor(Vector2 visualPosition, bool allowVirtualSpace)
{
var visualColumn = GetVisualColumnFloor(visualPosition, allowVirtualSpace, out var isAtEndOfLine);
var documentOffset = GetRelativeOffset(visualColumn) + FirstDocumentLine.Offset;
var pos = new TextViewPosition(Document.GetLocation(documentOffset), visualColumn)
{
IsAtEndOfLine = isAtEndOfLine
};
return pos;
}
/// <summary>
/// Gets whether the visual line was disposed.
/// </summary>
internal bool IsDisposed => _phase == LifetimePhase.Disposed;
internal void Dispose()
{
if (_phase == LifetimePhase.Disposed)
{
return;
}
Debug.Assert(_phase == LifetimePhase.Live);
_phase = LifetimePhase.Disposed;
}
/// <summary>
/// Gets the next possible caret position after visualColumn, or -1 if there is no caret position.
/// </summary>
internal int GetNextCaretPosition(int visualColumn, LogicalDirection direction, CaretPositioningMode mode, bool allowVirtualSpace)
{
if (!HasStopsInVirtualSpace(mode))
allowVirtualSpace = false;
if (_elements.Count == 0)
{
// special handling for empty visual lines:
if (allowVirtualSpace)
{
if (direction == LogicalDirection.Forward)
return Math.Max(0, visualColumn + 1);
if (visualColumn > 0)
return visualColumn - 1;
return -1;
}
// 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;
if (visualColumn > 0 && direction == LogicalDirection.Backward)
return 0;
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 > VisualLength && !_elements[_elements.Count - 1].HandlesLineBorders && HasImplicitStopAtLineEnd())
{
if (allowVirtualSpace)
return visualColumn - 1;
return 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--)
{
var 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++)
{
var 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 ((allowVirtualSpace || !_elements[_elements.Count - 1].HandlesLineBorders) && HasImplicitStopAtLineEnd())
{
if (visualColumn < VisualLength)
return VisualLength;
if (allowVirtualSpace)
return visualColumn + 1;
}
}
// we've found nothing, return -1 and let the caret search continue in the next line
return -1;
}
private static bool HasStopsInVirtualSpace(CaretPositioningMode mode)
{
return mode == CaretPositioningMode.Normal || mode == CaretPositioningMode.EveryCodepoint;
}
private static bool HasImplicitStopAtLineStart(CaretPositioningMode mode)
{
return mode == CaretPositioningMode.Normal || mode == CaretPositioningMode.EveryCodepoint;
}
private static bool HasImplicitStopAtLineEnd() => true;
private VisualLineDrawingVisual _visual;
internal VisualLineDrawingVisual Render()
{
Debug.Assert(_phase == LifetimePhase.Live);
if (_visual == null)
{
_visual = new VisualLineDrawingVisual(this);
}
return _visual;
}
}
// TODO: can inherit from Layoutable, but dev tools crash
internal sealed class VisualLineDrawingVisual
{
internal VisualLine VisualLine { get; }
internal float LineHeight { get; }
internal bool IsAdded { get; set; }
internal Vector2 RenderTransform { get; set; }
internal VisualLineDrawingVisual(VisualLine visualLine)
{
VisualLine = visualLine;
LineHeight = VisualLine.TextLines.Sum(textLine => textLine.Height);
}
// Rendering is now handled by TextLayer which creates Label elements for each line.
// This class just stores the layout/transform information.
}
}