using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Unity.CodeEditor.Document;
using Unity.CodeEditor.Text;
using Unity.CodeEditor.Utils;
using Unity.PlasticSCM.Editor.UI;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace Unity.CodeEditor.Rendering
{
internal class TextView : VisualElement
{
bool _isMeasureValid;
private EventHandler _scrollInvalidated;
private readonly CurrentLineHighlightRenderer _currentLineHighlightRenderer;
private readonly ColumnRulerRenderer _columnRulerRenderer;
private TextDocument _document;
private HeightTree _heightTree;
private TextEditorOptions _options;
private bool _defaultTextMetricsValid;
private float _wideSpaceWidth; // Width of an 'x'. Used as basis for the tab width, and for scrolling.
private float _defaultLineHeight; // Height of a line containing 'x'. Used for scrolling.
private float _defaultBaseline; // Baseline of a line containing 'x'. Used for TextTop/TextBottom calculation.
private List<VisualLine> _allVisualLines = new List<VisualLine>();
private ReadOnlyCollection<VisualLine> _visibleVisualLines;
private float _clippedPixelsOnTop;
private List<VisualLine> _newVisualLines;
private Vector2 _lastAvailableSize;
private bool _inMeasure;
private bool _canVerticallyScroll = true;
private bool _canHorizontallyScroll = true;
/// <summary>
/// Offset of the scroll position.
/// </summary>
private Vector2 _scrollOffset;
/// <summary>
/// Size of the viewport.
/// </summary>
private Vector2 _scrollViewport;
private Vector2 _scrollExtent;
private Font _font;
private int _fontSize = 13;
private Color _foregroundColor = TextEditorColors.DefaultText;
private Color _lineNumbersForegroundColor = TextEditorColors.LineNumbersForeground;
private Color _lineNumbersSeparatorColor = TextEditorColors.LineNumbersSeparator;
private Color _columnRulerColor = TextEditorColors.ColumnRuler;
/// <summary>
/// Gets the currently visible visual lines.
/// </summary>
/// <exception cref="VisualLinesInvalidException">
/// Gets thrown if there are invalid visual lines when this property is accessed.
/// You can use the <see cref="VisualLinesValid"/> property to check for this case,
/// or use the <see cref="EnsureVisualLines()"/> method to force creating the visual lines
/// when they are invalid.
/// </exception>
[SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations")]
internal ReadOnlyCollection<VisualLine> VisualLines
{
get
{
if (_visibleVisualLines == null)
throw new VisualLinesInvalidException();
return _visibleVisualLines;
}
}
internal Rect Bounds => new Rect(0, 0, resolvedStyle.width, resolvedStyle.height);
internal Vector2 ScrollViewport
{
get
{
return _scrollViewport;
}
}
internal Vector2 ScrollExtent
{
get
{
return _scrollExtent;
}
}
/// <summary>
/// Gets the horizontal scroll offset.
/// </summary>
internal float HorizontalOffset => _scrollOffset.x;
/// <summary>
/// Gets the vertical scroll offset.
/// </summary>
internal float VerticalOffset => _scrollOffset.y;
/// <summary>
/// Gets the scroll offset;
/// </summary>
internal Vector2 ScrollOffset
{
get { return _scrollOffset; }
set
{
value = new Vector2(ValidateVisualOffset(value.x), ValidateVisualOffset(value.y));
var isX = !_scrollOffset.x.IsClose(value.x);
var isY = !_scrollOffset.y.IsClose(value.y);
if (isX || isY)
{
SetScrollOffset(value);
if (isX)
{
InvalidateVisual();
}
InvalidateMeasure();
}
}
}
#region colors
internal Color LineNumbersForegroundColor
{
get { return _lineNumbersForegroundColor; }
set
{
if (_lineNumbersForegroundColor != value)
{
_lineNumbersForegroundColor = value;
Redraw();
}
}
}
internal Color LineNumbersSeparatorColor
{
get { return _lineNumbersSeparatorColor; }
set
{
if (_lineNumbersSeparatorColor != value)
{
_lineNumbersSeparatorColor = value;
Redraw();
}
}
}
/// <summary>
/// Gets/sets the Color used for displaying non-printable characters.
/// </summary>
internal Color NonPrintableCharacterColor { get; set; } = TextEditorColors.NonPrintableCharacter;
/// <summary>
/// Gets/Sets the pen used to draw the column ruler.
/// <seealso cref="TextEditorOptions.ShowColumnRulers"/>
/// </summary>
internal Color ColumnRulerColor
{
get { return _columnRulerColor;}
set
{
if (_columnRulerColor != value)
{
_columnRulerColor = value;
_columnRulerRenderer.SetRuler(Options.ColumnRulerPositions, value);
InvalidateVisual();
}
}
}
#endregion
internal void InvalidateVisual()
{
MarkDirtyRepaint();
}
internal void InvalidateMeasure()
{
_isMeasureValid = false;
ScheduleVisualLineUpdate();
}
private bool _updateScheduled;
private void ScheduleVisualLineUpdate()
{
if (_updateScheduled)
return;
_updateScheduled = true;
schedule.Execute(() =>
{
_updateScheduled = false;
UpdateVisualLines();
});
}
/// <summary>
/// Gets/Sets highlighted line number.
/// </summary>
internal int HighlightedLine
{
get => _currentLineHighlightRenderer.Line;
set => _currentLineHighlightRenderer.Line = value;
}
internal bool CanHorizontallyScroll
{
get => _canHorizontallyScroll;
set
{
if (_canHorizontallyScroll != value)
{
_canHorizontallyScroll = value;
ClearVisualLines();
MarkDirtyRepaint();
}
}
}
internal Font Font
{
get { return _font; }
set
{
if (_font != value)
{
_font = value;
// changing font properties requires recreating cached elements
// and we need to re-measure the font metrics:
InvalidateDefaultTextMetrics();
Redraw();
}
}
}
internal int FontSize
{
get { return _fontSize; }
set
{
if (_fontSize != value)
{
_fontSize = value;
// changing font properties requires recreating cached elements
// and we need to re-measure the font metrics:
InvalidateDefaultTextMetrics();
Redraw();
}
}
}
internal Color ForegroundColor
{
get { return _foregroundColor; }
set
{
if (_foregroundColor != value)
{
_foregroundColor = value;
// changing brushes requires recreating the cached elements
Redraw();
}
}
}
/// <summary>
/// Additonal amount that allows horizontal scrolling past the end of the longest line.
/// This is necessary to ensure the caret always is visible, even when it is at the end of the longest line.
/// </summary>
private const float AdditionalHorizontalScrollAmount = 25;
private readonly ObserveAddRemoveCollection<IBackgroundRenderer> _backgroundRenderers;
/// <summary>
/// Gets the list of background renderers.
/// </summary>
internal IList<IBackgroundRenderer> BackgroundRenderers => _backgroundRenderers;
private TextFormatter _formatter;
private TextParagraphProperties CreateParagraphProperties()
{
if (_font == null)
_font = Fonts.GetCascadiaFont();
return new TextParagraphProperties(
_canHorizontallyScroll == false,
Options.IndentationSize * WideSpaceWidth,
Font,
FontSize,
ForegroundColor);
}
internal TextView()
{
style.flexGrow = 1;
style.overflow = Overflow.Hidden;
style.position = Position.Relative;
style.backgroundColor = TextEditorColors.Background;
Services.AddService(this);
TextLayer = new TextLayer(this);
_elementGenerators = new ObserveAddRemoveCollection<VisualLineElementGenerator>(ElementGenerator_Added, ElementGenerator_Removed);
_lineTransformers = new ObserveAddRemoveCollection<IVisualLineTransformer>(LineTransformer_Added, LineTransformer_Removed);
_backgroundRenderers = new ObserveAddRemoveCollection<IBackgroundRenderer>(BackgroundRenderer_Added, BackgroundRenderer_Removed);
_currentLineHighlightRenderer = new CurrentLineHighlightRenderer(this);
_columnRulerRenderer = new ColumnRulerRenderer(this);
Options = new TextEditorOptions();
Debug.Assert(_singleCharacterElementGenerator != null); // assert that the option change created the builtin element generators
Layers = new LayerCollection(this);
InsertLayer(TextLayer, KnownLayer.Text, LayerInsertionPosition.Replace);
this.SetMouseCursor(MouseCursor.Text);
}
#region Layers
internal readonly TextLayer TextLayer;
/// <summary>
/// Gets the list of layers displayed in the text view.
/// </summary>
internal LayerCollection Layers { get; }
internal sealed class LayerCollection : Collection<Layer>
{
private readonly TextView _textView;
internal LayerCollection(TextView textView)
{
_textView = textView;
}
protected override void ClearItems()
{
foreach (var control in Items)
{
control.RemoveFromHierarchy();
}
base.ClearItems();
_textView.LayersChanged();
}
protected override void InsertItem(int index, Layer item)
{
base.InsertItem(index, item);
_textView.Insert(index, item);
_textView.LayersChanged();
}
protected override void RemoveItem(int index)
{
Items[index].RemoveFromHierarchy();
base.RemoveItem(index);
_textView.LayersChanged();
}
protected override void SetItem(int index, Layer item)
{
Items[index].RemoveFromHierarchy();
base.SetItem(index, item);
_textView.Insert(index, item);
_textView.LayersChanged();
}
}
private void LayersChanged()
{
TextLayer.Index = Layers.IndexOf(TextLayer);
}
/// <summary>
/// Inserts a new layer at a position specified relative to an existing layer.
/// </summary>
/// <param name="layer">The new layer to insert.</param>
/// <param name="referencedLayer">The existing layer</param>
/// <param name="position">Specifies whether the layer is inserted above,below, or replaces the referenced layer</param>
internal void InsertLayer(Layer layer, KnownLayer referencedLayer, LayerInsertionPosition position)
{
if (layer == null)
throw new ArgumentNullException(nameof(layer));
if (!Enum.IsDefined(typeof(KnownLayer), referencedLayer))
throw new ArgumentOutOfRangeException(nameof(referencedLayer), (int)referencedLayer, nameof(KnownLayer));
if (!Enum.IsDefined(typeof(LayerInsertionPosition), position))
throw new ArgumentOutOfRangeException(nameof(position), (int)position, nameof(LayerInsertionPosition));
if (referencedLayer == KnownLayer.Background && position != LayerInsertionPosition.Above)
throw new InvalidOperationException("Cannot replace or insert below the background layer.");
var newPosition = new LayerPosition(referencedLayer, position);
if (Layers == null)
return;
layer.LayerPosition = newPosition;
for (var i = 0; i < Layers.Count; i++)
{
var p = Layers[i].LayerPosition;
if (p != null)
{
if (p.KnownLayer == referencedLayer && p.Position == LayerInsertionPosition.Replace)
{
// found the referenced layer
switch (position)
{
case LayerInsertionPosition.Below:
Layers.Insert(i, layer);
return;
case LayerInsertionPosition.Above:
Layers.Insert(i + 1, layer);
return;
case LayerInsertionPosition.Replace:
Layers[i] = layer;
return;
}
}
else if (p.KnownLayer == referencedLayer && p.Position == LayerInsertionPosition.Above
|| p.KnownLayer > referencedLayer)
{
// we skipped the insertion position (referenced layer does not exist?)
Layers.Insert(i, layer);
return;
}
}
}
// inserting after all existing layers:
Layers.Add(layer);
}
#endregion
void Measure(Vector2 availableSize)
{
if (!_canHorizontallyScroll && !availableSize.x.IsClose(_lastAvailableSize.x))
{
ClearVisualLines();
}
_lastAvailableSize = availableSize;
float maxWidth;
if (_document == null)
{
// no document -> create empty list of lines
_allVisualLines = new List<VisualLine>();
_visibleVisualLines = new ReadOnlyCollection<VisualLine>(_allVisualLines.ToArray());
maxWidth = 0;
}
else
{
_inMeasure = true;
try
{
maxWidth = CreateAndMeasureVisualLines(availableSize);
}
finally
{
_inMeasure = false;
}
}
//RemoveInlineObjectsNow();
maxWidth += AdditionalHorizontalScrollAmount;
var heightTreeHeight = DocumentHeight;
var options = Options;
float desiredHeight = Math.Min(availableSize.y, heightTreeHeight);
float extraHeightToAllowScrollBelowDocument = 0;
if (options.AllowScrollBelowDocument)
{
if (!double.IsInfinity(_scrollViewport.y))
{
// HACK: we need to keep at least Caret.MinimumDistanceToViewBorder visible so that we don't scroll back up when the user types after
// scrolling to the very bottom.
var minVisibleDocumentHeight = DefaultLineHeight;
// increase the extend height to allow scrolling below the document
extraHeightToAllowScrollBelowDocument = desiredHeight - minVisibleDocumentHeight;
}
}
TextLayer.SetVisualLines(_visibleVisualLines);
SetScrollData(availableSize,
new Vector2(maxWidth, heightTreeHeight + extraHeightToAllowScrollBelowDocument),
_scrollOffset);
VisualLinesChanged?.Invoke(this, EventArgs.Empty);
}
void Arrange()
{
}
/// <summary>
/// Build all VisualLines in the visible range.
/// </summary>
/// <returns>Width the longest line</returns>
private float CreateAndMeasureVisualLines(Vector2 availableSize)
{
//Debug.Log("Measure availableSize=" + availableSize + ", scrollOffset=" + _scrollOffset);
var firstLineInView = _heightTree.GetLineByVisualPosition(_scrollOffset.y);
// number of pixels clipped from the first visual line(s)
_clippedPixelsOnTop = _scrollOffset.y - _heightTree.GetVisualPosition(firstLineInView);
// clippedPixelsOnTop should be >= 0, except for floating point inaccurracy.
Debug.Assert(_clippedPixelsOnTop >= -ExtensionMethods.Epsilon);
_newVisualLines = new List<VisualLine>();
VisualLineConstructionStarting?.Invoke(this, new VisualLineConstructionStartEventArgs(firstLineInView));
var elementGeneratorsArray = _elementGenerators.ToArray();
var lineTransformersArray = _lineTransformers.ToArray();
var nextLine = firstLineInView;
float maxWidth = 0;
var yPos = -_clippedPixelsOnTop;
while (yPos < availableSize.y && nextLine != null)
{
var visualLine = GetVisualLine(nextLine.LineNumber) ??
BuildVisualLine(nextLine,
CreateParagraphProperties(),
elementGeneratorsArray,
lineTransformersArray,
availableSize);
visualLine.VisualTop = _scrollOffset.y + yPos;
nextLine = visualLine.LastDocumentLine.NextLine;
yPos += visualLine.Height;
foreach (var textLine in visualLine.TextLines)
{
if (textLine.WidthIncludingTrailingWhitespace > maxWidth)
maxWidth = textLine.WidthIncludingTrailingWhitespace;
}
_newVisualLines.Add(visualLine);
}
foreach (var line in _allVisualLines)
{
Debug.Assert(line.IsDisposed == false);
if (!_newVisualLines.Contains(line))
DisposeVisualLine(line);
}
_allVisualLines = _newVisualLines;
// visibleVisualLines = readonly copy of visual lines
_visibleVisualLines = new ReadOnlyCollection<VisualLine>(_newVisualLines.ToArray());
_newVisualLines = null;
if (_allVisualLines.Any(line => line.IsDisposed))
{
throw new InvalidOperationException("A visual line was disposed even though it is still in use.\n" +
"This can happen when Redraw() is called during measure for lines " +
"that are already constructed.");
}
return maxWidth;
}
#region BuildVisualLine
private VisualLine BuildVisualLine(DocumentLine documentLine,
TextParagraphProperties paragraphProperties,
IReadOnlyList<VisualLineElementGenerator> elementGeneratorsArray,
IReadOnlyList<IVisualLineTransformer> lineTransformersArray,
Vector2 availableSize)
{
if (_heightTree.GetIsCollapsed(documentLine.LineNumber))
throw new InvalidOperationException("Trying to build visual line from collapsed line");
//Debug.Log("Building line " + documentLine.LineNumber);
VisualLine visualLine = new VisualLine(this, documentLine);
VisualLineTextSource textSource = new VisualLineTextSource(visualLine)
{
Document = _document,
TextParagraphProperties = paragraphProperties,
TextView = this
};
visualLine.ConstructVisualElements(textSource, elementGeneratorsArray);
if (visualLine.FirstDocumentLine != visualLine.LastDocumentLine)
{
// Check whether the lines are collapsed correctly:
double firstLinePos = _heightTree.GetVisualPosition(visualLine.FirstDocumentLine.NextLine);
double lastLinePos = _heightTree.GetVisualPosition(visualLine.LastDocumentLine.NextLine ?? visualLine.LastDocumentLine);
if (!firstLinePos.IsClose(lastLinePos))
{
for (int i = visualLine.FirstDocumentLine.LineNumber + 1; i <= visualLine.LastDocumentLine.LineNumber; i++)
{
if (!_heightTree.GetIsCollapsed(i))
throw new InvalidOperationException("Line " + i + " was skipped by a VisualLineElementGenerator, but it is not collapsed.");
}
throw new InvalidOperationException("All lines collapsed but visual pos different - height tree inconsistency?");
}
}
visualLine.RunTransformers(textSource, lineTransformersArray);
// now construct textLines:
var textOffset = 0;
var textLines = new List<TextLine>();
while (textOffset <= visualLine.VisualLengthWithEndOfLineMarker)
{
var textLine = _formatter.FormatLine(
textSource,
textOffset,
availableSize.x,
paragraphProperties);
textLines.Add(textLine);
textOffset += textLine.Length;
// exit loop so that we don't do the indentation calculation if there's only a single line
if (textOffset >= visualLine.VisualLengthWithEndOfLineMarker)
break;
if (paragraphProperties.FirstLineInParagraph)
{
paragraphProperties.FirstLineInParagraph = false;
TextEditorOptions options = this.Options;
double indentation = 0;
if (options.InheritWordWrapIndentation)
{
// determine indentation for next line:
int indentVisualColumn = GetIndentationVisualColumn(visualLine);
if (indentVisualColumn > 0 && indentVisualColumn < textOffset)
{
indentation = textLine.GetDistanceFromCharacterHit(new CharacterHit(indentVisualColumn, 0));
}
}
indentation += options.WordWrapIndentation;
// apply the calculated indentation unless it's more than half of the text editor size:
if (indentation > 0 && indentation * 2 < availableSize.x)
paragraphProperties.Indent = indentation;
}
}
visualLine.SetTextLines(textLines);
_heightTree.SetHeight(visualLine.FirstDocumentLine, visualLine.Height);
return visualLine;
}
private static int GetIndentationVisualColumn(VisualLine visualLine)
{
if (visualLine.Elements.Count == 0)
return 0;
var column = 0;
var elementIndex = 0;
var element = visualLine.Elements[elementIndex];
while (element.IsWhitespace(column))
{
column++;
if (column == element.VisualColumn + element.VisualLength)
{
elementIndex++;
if (elementIndex == visualLine.Elements.Count)
break;
element = visualLine.Elements[elementIndex];
}
}
return column;
}
#endregion
private void BackgroundRenderer_Added(IBackgroundRenderer renderer)
{
ConnectToTextView(renderer);
InvalidateLayer(renderer.Layer);
}
private void BackgroundRenderer_Removed(IBackgroundRenderer renderer)
{
DisconnectFromTextView(renderer);
InvalidateLayer(renderer.Layer);
}
/// <summary>
/// Updates visual lines for the current viewport. Called when scroll offset
/// changes or when the element size changes.
/// </summary>
internal void UpdateVisualLines()
{
var availableSize = new Vector2(resolvedStyle.width, resolvedStyle.height);
if (float.IsNaN(availableSize.x) || float.IsNaN(availableSize.y))
return;
if (availableSize.x <= 0 || availableSize.y <= 0)
return;
if (_isMeasureValid && _lastAvailableSize.IsClose(availableSize))
return;
Measure(availableSize);
Arrange();
_isMeasureValid = true;
}
internal void RenderBackground(KnownLayer layer)
{
foreach (var bg in _backgroundRenderers)
{
if (bg.Layer == layer)
{
bg.Draw(this);
}
}
}
internal void ArrangeTextLayer(IList<VisualLineDrawingVisual> visuals)
{
var pos = new Vector2(-_scrollOffset.x, -_clippedPixelsOnTop);
foreach (var visual in visuals)
{
var t = visual.RenderTransform;
if (t == null || t.x != pos.x || t.y != pos.y)
{
visual.RenderTransform = new Vector2(pos.x, pos.y);
}
pos = new Vector2(pos.x, pos.y + visual.LineHeight);
}
}
#region Document Property
/// <summary>
/// Gets/Sets the document displayed by the text editor.
/// </summary>
internal TextDocument Document
{
get
{
return _document;
}
set
{
TextDocument oldValue = _document;
_document = value;
OnDocumentChanged(oldValue, value);
}
}
/// <summary>
/// Occurs when the document property has changed.
/// </summary>
internal event EventHandler<DocumentChangedEventArgs> DocumentChanged;
private void OnDocumentChanged(TextDocument oldValue, TextDocument newValue)
{
if (oldValue != null)
{
_heightTree.Dispose();
_heightTree = null;
_formatter = null;
TextDocumentWeakEventManager.Changing.RemoveHandler(oldValue, OnChanging);
}
_document = newValue;
ClearScrollData();
ClearVisualLines();
if (newValue != null)
{
TextDocumentWeakEventManager.Changing.AddHandler(newValue, OnChanging);
_formatter = new TextFormatter();
InvalidateDefaultTextMetrics(); // measuring DefaultLineHeight depends on formatter
_heightTree = new HeightTree(newValue, DefaultLineHeight);
}
MarkDirtyRepaint();
DocumentChanged?.Invoke(this, new DocumentChangedEventArgs(oldValue, newValue));
}
private void OnChanging(object sender, DocumentChangeEventArgs e)
{
Redraw(e.Offset, e.RemovalLength);
}
#endregion
#region Options property
/// <summary>
/// Gets/Sets the options used by the text editor.
/// </summary>
internal TextEditorOptions Options
{
get
{
return _options;
}
set
{
TextEditorOptions oldValue = _options;
_options = value;
OnOptionsChanged(oldValue, value);
}
}
/// <summary>
/// Occurs when a text editor option has changed.
/// </summary>
internal event PropertyChangedEventHandler OptionChanged;
/// <summary>
/// Raises the <see cref="OptionChanged"/> event.
/// </summary>
protected virtual void OnOptionChanged(PropertyChangedEventArgs e)
{
OptionChanged?.Invoke(this, e);
if (Options.ShowColumnRulers)
_columnRulerRenderer.SetRuler(Options.ColumnRulerPositions, ColumnRulerColor);
else
_columnRulerRenderer.SetRuler(null, ColumnRulerColor);
UpdateBuiltinElementGeneratorsFromOptions();
Redraw();
}
private void OnOptionsChanged(TextEditorOptions oldValue, TextEditorOptions newValue)
{
if (oldValue != null)
{
PropertyChangedWeakEventManager.RemoveHandler(oldValue, OnPropertyChanged);
}
if (newValue != null)
{
PropertyChangedWeakEventManager.AddHandler(newValue, OnPropertyChanged);
}
OnOptionChanged(new PropertyChangedEventArgs(null));
}
private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
OnOptionChanged(e);
}
#endregion
#region Service Provider
/// <summary>
/// Gets a service container used to associate services with the text view.
/// </summary>
internal IServiceContainer Services { get; } = new ServiceContainer();
#endregion
#region ElementGenerators+LineTransformers Properties
private readonly ObserveAddRemoveCollection<VisualLineElementGenerator> _elementGenerators;
/// <summary>
/// Gets a collection where element generators can be registered.
/// </summary>
internal IList<VisualLineElementGenerator> ElementGenerators => _elementGenerators;
private void ElementGenerator_Added(VisualLineElementGenerator generator)
{
ConnectToTextView(generator);
Redraw();
}
private void ElementGenerator_Removed(VisualLineElementGenerator generator)
{
DisconnectFromTextView(generator);
Redraw();
}
private readonly ObserveAddRemoveCollection<IVisualLineTransformer> _lineTransformers;
/// <summary>
/// Gets a collection where line transformers can be registered.
/// </summary>
internal IList<IVisualLineTransformer> LineTransformers => _lineTransformers;
private void LineTransformer_Added(IVisualLineTransformer lineTransformer)
{
ConnectToTextView(lineTransformer);
Redraw();
}
private void LineTransformer_Removed(IVisualLineTransformer lineTransformer)
{
DisconnectFromTextView(lineTransformer);
Redraw();
}
#endregion
#region Builtin ElementGenerators
// NewLineElementGenerator newLineElementGenerator;
private SingleCharacterElementGenerator _singleCharacterElementGenerator;
private LinkElementGenerator _linkElementGenerator;
private MailLinkElementGenerator _mailLinkElementGenerator;
TextFormatter mFormatter;
private void UpdateBuiltinElementGeneratorsFromOptions()
{
var options = Options;
// AddRemoveDefaultElementGeneratorOnDemand(ref newLineElementGenerator, options.ShowEndOfLine);
AddRemoveDefaultElementGeneratorOnDemand(ref _singleCharacterElementGenerator, true);
AddRemoveDefaultElementGeneratorOnDemand(ref _linkElementGenerator, options.EnableHyperlinks);
AddRemoveDefaultElementGeneratorOnDemand(ref _mailLinkElementGenerator, options.EnableEmailHyperlinks);
}
private void AddRemoveDefaultElementGeneratorOnDemand<T>(ref T generator, bool demand)
where T : VisualLineElementGenerator, IBuiltinElementGenerator, new()
{
var hasGenerator = generator != null;
if (hasGenerator != demand)
{
if (demand)
{
generator = new T();
ElementGenerators.Add(generator);
}
else
{
ElementGenerators.Remove(generator);
generator = null;
}
}
generator?.FetchOptions(Options);
}
#endregion
/// <summary>
/// Gets the visual line that contains the document line with the specified number.
/// If that line is outside the visible range, a new VisualLine for that document line is constructed.
/// </summary>
internal VisualLine GetOrConstructVisualLine(DocumentLine documentLine)
{
if (documentLine == null)
throw new ArgumentNullException("documentLine");
if (!this.Document.Lines.Contains(documentLine))
throw new InvalidOperationException("Line belongs to wrong document");
EditorDispatcher.VerifyMainThreadAccess();
VisualLine l = GetVisualLine(documentLine.LineNumber);
if (l == null)
{
TextParagraphProperties paragraphProperties = CreateParagraphProperties();
while (_heightTree.GetIsCollapsed(documentLine.LineNumber))
{
documentLine = documentLine.PreviousLine;
}
l = BuildVisualLine(documentLine,
paragraphProperties,
_elementGenerators.ToArray(),
_lineTransformers.ToArray(),
_lastAvailableSize);
_allVisualLines.Add(l);
// update all visual top values (building the line might have changed visual top of other lines due to word wrapping)
foreach (var line in _allVisualLines)
{
line.VisualTop = _heightTree.GetVisualPosition(line.FirstDocumentLine);
}
}
return l;
}
#region Redraw methods / VisualLine invalidation
/// <summary>
/// Causes the text editor to regenerate all visual lines.
/// </summary>
internal void Redraw()
{
EditorDispatcher.VerifyMainThreadAccess();
ClearVisualLines();
InvalidateMeasure();
}
/// <summary>
/// Causes the text editor to regenerate the specified visual line.
/// </summary>
internal void Redraw(VisualLine visualLine)
{
EditorDispatcher.VerifyMainThreadAccess();
if (_allVisualLines.Remove(visualLine))
{
DisposeVisualLine(visualLine);
MarkDirtyRepaint();
}
}
/// <summary>
/// Causes the text editor to redraw all lines overlapping with the specified segment.
/// </summary>
internal void Redraw(int offset, int length)
{
EditorDispatcher.VerifyMainThreadAccess();
var changedSomethingBeforeOrInLine = false;
for (var i = 0; i < _allVisualLines.Count; i++)
{
try
{
var visualLine = _allVisualLines[i];
var lineStart = visualLine.FirstDocumentLine.Offset;
var lineEnd = visualLine.LastDocumentLine.Offset + visualLine.LastDocumentLine.TotalLength;
if (offset <= lineEnd)
{
changedSomethingBeforeOrInLine = true;
if (offset + length >= lineStart)
{
_allVisualLines.RemoveAt(i--);
DisposeVisualLine(visualLine);
}
}
}
catch (Exception ex)
{
Debug.LogError(ex);
}
}
if (changedSomethingBeforeOrInLine)
{
// Repaint not only when something in visible area was changed, but also when anything in front of it
// was changed. We might have to redraw the line number margin. Or the highlighting changed.
// However, we'll try to reuse the existing VisualLines.
InvalidateMeasure();
}
}
/// <summary>
/// Causes a known layer to redraw.
/// This method does not invalidate visual lines;
/// use the <see cref="Redraw()"/> method to do that.
/// </summary>
internal void InvalidateLayer(KnownLayer knownLayer)
{
MarkDirtyRepaint();
}
/// <summary>
/// Causes the text editor to redraw all lines overlapping with the specified segment.
/// Does nothing if segment is null.
/// </summary>
internal void Redraw(ISegment segment)
{
if (segment != null)
{
Redraw(segment.Offset, segment.Length);
}
}
/// <summary>
/// Invalidates all visual lines.
/// The caller of ClearVisualLines() must also call InvalidateMeasure() to ensure
/// that the visual lines will be recreated.
/// </summary>
private void ClearVisualLines()
{
if (_allVisualLines.Count != 0)
{
foreach (var visualLine in _allVisualLines)
{
DisposeVisualLine(visualLine);
}
_allVisualLines.Clear();
_visibleVisualLines = new ReadOnlyCollection<VisualLine>(_allVisualLines.ToArray());
}
}
private void DisposeVisualLine(VisualLine visualLine)
{
if (_newVisualLines != null && _newVisualLines.Contains(visualLine))
{
throw new ArgumentException("Cannot dispose visual line because it is in construction!");
}
visualLine.Dispose();
//RemoveInlineObjects(visualLine);
}
#endregion
#region Get(OrConstruct)VisualLine
/// <summary>
/// Gets the visual line that contains the document line with the specified number.
/// Returns null if the document line is outside the visible range.
/// </summary>
internal VisualLine GetVisualLine(int documentLineNumber)
{
// TODO: EnsureVisualLines() ?
foreach (var visualLine in _allVisualLines)
{
Debug.Assert(visualLine.IsDisposed == false);
var start = visualLine.FirstDocumentLine.LineNumber;
var end = visualLine.LastDocumentLine.LineNumber;
if (documentLineNumber >= start && documentLineNumber <= end)
return visualLine;
}
return null;
}
#endregion
// Kept for backward compatibility with code that references visual line styling
internal GUIStyle VisualLineStyle { get; set; } = new GUIStyle();
/// <summary>
/// Retrieves a service from the text view.
/// If the service is not found in the <see cref="Services"/> container,
/// this method will also look for it in the current document's service provider.
/// </summary>
internal virtual object GetService(Type serviceType)
{
var instance = Services.GetService(serviceType);
if (instance == null && _document != null)
{
instance = _document.ServiceProvider.GetService(serviceType);
}
return instance;
}
private void ConnectToTextView(object obj)
{
var c = obj as ITextViewConnect;
c?.AddToTextView(this);
}
private void DisconnectFromTextView(object obj)
{
var c = obj as ITextViewConnect;
c?.RemoveFromTextView(this);
}
/// <summary>
/// Gets the width of a 'wide space' (the space width used for calculating the tab size).
/// </summary>
/// <remarks>
/// This is the width of an 'x' in the current font.
/// We do not measure the width of an actual space as that would lead to tiny tabs in
/// some proportional fonts.
/// For monospaced fonts, this property will return the expected value, as 'x' and ' ' have the same width.
/// </remarks>
internal float WideSpaceWidth
{
get
{
CalculateDefaultTextMetrics();
return _wideSpaceWidth;
}
}
/// <summary>
/// Gets the default line height. This is the height of an empty line or a line containing regular text.
/// Lines that include formatted text or custom UI elements may have a different line height.
/// </summary>
internal float DefaultLineHeight
{
get
{
CalculateDefaultTextMetrics();
return _defaultLineHeight;
}
}
private void InvalidateDefaultTextMetrics()
{
_defaultTextMetricsValid = false;
if (_heightTree != null)
{
// calculate immediately so that height tree gets updated
CalculateDefaultTextMetrics();
}
}
private void CalculateDefaultTextMetrics()
{
if (_defaultTextMetricsValid)
return;
_defaultTextMetricsValid = true;
if (_formatter != null)
{
var paragraphProperties = CreateParagraphProperties();
var size = _formatter.CalcSize(
"x",
paragraphProperties);
_wideSpaceWidth = Math.Max(1, size.x);
_defaultLineHeight = Math.Max(1, Mathf.Round(size.y));
}
// Update heightTree.DefaultLineHeight, if a document is loaded.
if (_heightTree != null)
_heightTree.DefaultLineHeight = _defaultLineHeight;
}
private static float ValidateVisualOffset(float offset)
{
if (float.IsNaN(offset))
throw new ArgumentException("offset must not be NaN");
if (offset < 0)
return 0;
return offset;
}
/// <summary>
/// Scrolls the text view so that the specified rectangle gets visible.
/// </summary>
internal virtual void MakeVisible(Rect rectangle)
{
var visibleRectangle = new Rect(_scrollOffset.x, _scrollOffset.y,
_scrollViewport.x, _scrollViewport.y);
var newScrollOffsetX = _scrollOffset.x;
var newScrollOffsetY = _scrollOffset.y;
if (rectangle.x < visibleRectangle.x)
{
if (rectangle.xMax > visibleRectangle.xMax)
{
newScrollOffsetX = rectangle.x + rectangle.width / 2;
}
else
{
newScrollOffsetX = rectangle.x;
}
}
else if (rectangle.xMax > visibleRectangle.xMax)
{
newScrollOffsetX = rectangle.xMax - _scrollViewport.x;
}
if (rectangle.y < visibleRectangle.y)
{
if (rectangle.yMax > visibleRectangle.yMax)
{
newScrollOffsetY = rectangle.y + rectangle.height / 2;
}
else
{
newScrollOffsetY = rectangle.y;
}
}
else if (rectangle.yMax > visibleRectangle.yMax)
{
newScrollOffsetY = rectangle.yMax - _scrollViewport.y;
}
newScrollOffsetX = ValidateVisualOffset(newScrollOffsetX);
newScrollOffsetY = ValidateVisualOffset(newScrollOffsetY);
var newScrollOffset = new Vector2(newScrollOffsetX, newScrollOffsetY);
if (!_scrollOffset.IsClose(newScrollOffset))
{
SetScrollOffset(newScrollOffset);
OnScrollChange();
InvalidateMeasure();
}
}
/// <summary>
/// Gets the height of the document.
/// </summary>
internal float DocumentHeight => _heightTree?.TotalHeight ?? 0;
/// <summary>
/// Occurs when the TextView was measured and changed its visual lines.
/// </summary>
internal event EventHandler VisualLinesChanged;
/// <summary>
/// Gets the default baseline position. This is the difference between <see cref="VisualYPosition.TextTop"/>
/// and <see cref="VisualYPosition.Baseline"/> for a line containing regular text.
/// Lines that include formatted text or custom UI elements may have a different baseline.
/// </summary>
internal float DefaultBaseline
{
get
{
CalculateDefaultTextMetrics();
return _defaultBaseline;
}
}
/// <summary>
/// Gets whether the visual lines are valid.
/// Will return false after a call to Redraw().
/// Accessing the visual lines property will cause a <see cref="VisualLinesInvalidException"/>
/// if this property is <c>false</c>.
/// </summary>
internal bool VisualLinesValid => _visibleVisualLines != null;
/// <summary>
/// Occurs when the TextView is about to be measured and will regenerate its visual lines.
/// This event may be used to mark visual lines as invalid that would otherwise be reused.
/// </summary>
internal event EventHandler<VisualLineConstructionStartEventArgs> VisualLineConstructionStarting;
/// <summary>
/// Occurs when the scroll offset has changed.
/// </summary>
internal event EventHandler ScrollOffsetChanged;
/// <summary>
/// If the visual lines are invalid, creates new visual lines for the visible part
/// of the document.
/// If all visual lines are valid, this method does nothing.
/// </summary>
/// <exception cref="InvalidOperationException">The visual line build process is already running.
/// It is not allowed to call this method during the construction of a visual line.</exception>
internal void EnsureVisualLines()
{
EditorDispatcher.VerifyMainThreadAccess();
if (_inMeasure)
throw new InvalidOperationException("The visual line build process is already running! Cannot EnsureVisualLines() during Measure!");
if (!VisualLinesValid)
{
// force immediate re-measure
UpdateVisualLines();
}
// Sometimes we still have invalid lines after UpdateLayout - work around the problem
// by calling MeasureOverride directly.
if (!VisualLinesValid)
{
Debug.Log("UpdateLayout() failed in EnsureVisualLines");
Measure(_lastAvailableSize);
}
if (!VisualLinesValid)
throw new VisualLinesInvalidException("Internal error: visual lines invalid after EnsureVisualLines call");
}
#region Visual element pointer handling
internal void OnPointerPressed(Vector2 localPosition)
{
EnsureVisualLines();
var element = GetVisualLineElementFromPosition(localPosition + _scrollOffset);
element?.OnPointerPressed(localPosition);
}
internal void OnPointerMoved()
{
if (_lastAvailableSize.x > 0 && _lastAvailableSize.y > 0)
Measure(_lastAvailableSize);
}
internal void OnPointerReleased(Vector2 localPosition)
{
EnsureVisualLines();
var element = GetVisualLineElementFromPosition(localPosition + _scrollOffset);
element?.OnPointerReleased(localPosition);
}
#endregion
#region Getting elements from Visual Position
/// <summary>
/// Gets the visual line at the specified document position (relative to start of document).
/// Returns null if there is no visual line for the position (e.g. the position is outside the visible
/// text area).
/// </summary>
internal VisualLine GetVisualLineFromVisualTop(double visualTop)
{
// TODO: change this method to also work outside the visible range -
// required to make GetPosition work as expected!
EnsureVisualLines();
foreach (var vl in VisualLines)
{
if (visualTop < vl.VisualTop)
continue;
if (visualTop < vl.VisualTop + vl.Height)
return vl;
}
return null;
}
/// <summary>
/// Gets the visual top position (relative to start of document) from a document line number.
/// </summary>
internal double GetVisualTopByDocumentLine(int line)
{
EditorDispatcher.VerifyMainThreadAccess();
if (_heightTree == null)
throw ThrowUtil.NoDocumentAssigned();
return _heightTree.GetVisualPosition(_heightTree.GetLineByNumber(line));
}
private VisualLineElement GetVisualLineElementFromPosition(Vector2 visualPosition)
{
var vl = GetVisualLineFromVisualTop(visualPosition.y);
if (vl != null)
{
var column = vl.GetVisualColumnFloor(visualPosition);
foreach (var element in vl.Elements)
{
if (element.VisualColumn + element.VisualLength <= column)
continue;
return element;
}
}
return null;
}
#endregion
/// <summary>
/// Empty line selection width.
/// </summary>
internal virtual float EmptyLineSelectionWidth => 1;
internal TextFormatter Formatter => _formatter;
#region ScrollInfo implementation
private void ClearScrollData()
{
SetScrollData(new Vector2(), new Vector2(), new Vector2());
}
private bool SetScrollData(Vector2 viewport, Vector2 extent, Vector2 offset)
{
if (!(viewport.IsClose(_scrollViewport)
&& extent.IsClose(_scrollExtent)
&& offset.IsClose(_scrollOffset)))
{
_scrollViewport = viewport;
_scrollExtent = extent;
SetScrollOffset(offset);
OnScrollChange();
return true;
}
return false;
}
private void SetScrollOffset(Vector2 vector)
{
if (!_canHorizontallyScroll)
{
vector = new Vector2(0, vector.y);
}
if (!_canVerticallyScroll)
{
vector = new Vector2(vector.x, 0);
}
if (!_scrollOffset.IsClose(vector))
{
_scrollOffset = vector;
ScrollOffsetChanged?.Invoke(this, EventArgs.Empty);
}
}
private void OnScrollChange()
{
//_repaintAction();
}
internal DocumentLine GetDocumentLineByVisualTop(double visualTop)
{
EditorDispatcher.VerifyMainThreadAccess();
if (_heightTree == null)
throw ThrowUtil.NoDocumentAssigned();
return _heightTree.GetLineByVisualPosition(visualTop);
}
#endregion
}
}