1483 lines
53 KiB
C#
1483 lines
53 KiB
C#
|
|
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
|
||
|
|
}
|
||
|
|
}
|