// 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.Diagnostics; using Unity.CodeEditor.Document; using Unity.CodeEditor.Rendering; using Unity.CodeEditor.Utils; using Unity.CodeEditor.Text; using UnityEngine; using Debug = UnityEngine.Debug; namespace Unity.CodeEditor.Editing { /// /// Helper class with caret-related methods. /// internal sealed class Caret { private const float CaretWidth = 0.5f; private readonly TextArea _textArea; private readonly TextView _textView; private readonly CaretLayer _caretAdorner; private bool _visible; internal Caret(TextArea textArea) { _textArea = textArea; _textView = textArea.TextView; _position = new TextViewPosition(1, 1, 0); _caretAdorner = new CaretLayer(textArea); _textView.InsertLayer(_caretAdorner, KnownLayer.Caret, LayerInsertionPosition.Replace); _textView.VisualLinesChanged += TextView_VisualLinesChanged; _textView.ScrollOffsetChanged += TextView_ScrollOffsetChanged; } internal void UpdateIfVisible() { if (_visible) { Show(); } } private void TextView_VisualLinesChanged(object sender, EventArgs e) { if (_visible) { Show(); } // required because the visual columns might have changed if the // element generators did something differently than on the last run // (e.g. a FoldingSection was collapsed) InvalidateVisualColumn(); } private void TextView_ScrollOffsetChanged(object sender, EventArgs e) { //_caretAdorner?.InvalidateVisual(); } private TextViewPosition _position; /// /// Gets/Sets the position of the caret. /// Retrieving this property will validate the visual column (which can be expensive). /// Use the property instead if you don't need the visual column. /// internal TextViewPosition Position { get { ValidateVisualColumn(); return _position; } set { if (_position != value) { _position = value; _storedCaretOffset = -1; ValidatePosition(); InvalidateVisualColumn(); RaisePositionChanged(); CalculateCaretRectangle(); //Debug.Log("Caret position changed to " + value); if (_visible) Show(); } } } /// /// Gets the caret position without validating it. /// internal TextViewPosition NonValidatedPosition => _position; /// /// Gets/Sets the location of the caret. /// The getter of this property is faster than because it doesn't have /// to validate the visual column. /// internal TextLocation Location { get => _position.Location; set => Position = new TextViewPosition(value); } /// /// Gets/Sets the caret line. /// internal int Line { get => _position.Line; set => Position = new TextViewPosition(value, _position.Column); } /// /// Gets/Sets the caret column. /// internal int Column { get => _position.Column; set => Position = new TextViewPosition(_position.Line, value); } /// /// Gets/Sets the caret visual column. /// internal int VisualColumn { get { ValidateVisualColumn(); return _position.VisualColumn; } set => Position = new TextViewPosition(_position.Line, _position.Column, value); } private bool _isInVirtualSpace; /// /// Gets whether the caret is in virtual space. /// internal bool IsInVirtualSpace { get { ValidateVisualColumn(); return _isInVirtualSpace; } } private int _storedCaretOffset; internal void OnDocumentChanging() { _storedCaretOffset = Offset; InvalidateVisualColumn(); } internal void OnDocumentChanged(DocumentChangeEventArgs e) { InvalidateVisualColumn(); if (_storedCaretOffset >= 0) { // If the caret is at the end of a selection, we don't expand the selection if something // is inserted at the end. Thus we also need to keep the caret in front of the insertion. AnchorMovementType caretMovementType; if (!_textArea.Selection.IsEmpty && _storedCaretOffset == _textArea.Selection.SurroundingSegment.EndOffset) caretMovementType = AnchorMovementType.BeforeInsertion; else caretMovementType = AnchorMovementType.Default; var newCaretOffset = e.GetNewOffset(_storedCaretOffset, caretMovementType); var document = _textArea.Document; if (document != null) { // keep visual column Position = new TextViewPosition(document.GetLocation(newCaretOffset), _position.VisualColumn); } } _storedCaretOffset = -1; } /// /// Gets/Sets the caret offset. /// Setting the caret offset has the side effect of setting the to NaN. /// internal int Offset { get { var document = _textArea.Document; if (document == null) { return 0; } return document.GetOffset(_position.Location); } set { var document = _textArea.Document; if (document != null) { Position = new TextViewPosition(document.GetLocation(value)); DesiredXPos = float.NaN; } } } /// /// Gets/Sets the desired x-position of the caret, in device-independent pixels. /// This property is NaN if the caret has no desired position. /// internal float DesiredXPos { get; set; } = float.NaN; private void ValidatePosition() { if (_position.Line < 1) _position.Line = 1; if (_position.Column < 1) _position.Column = 1; if (_position.VisualColumn < -1) _position.VisualColumn = -1; var document = _textArea.Document; if (document != null) { if (_position.Line > document.LineCount) { _position.Line = document.LineCount; _position.Column = document.GetLineByNumber(_position.Line).Length + 1; _position.VisualColumn = -1; } else { var line = document.GetLineByNumber(_position.Line); if (_position.Column > line.Length + 1) { _position.Column = line.Length + 1; _position.VisualColumn = -1; } } } } /// /// Event raised when the caret position has changed. /// If the caret position is changed inside a document update (between BeginUpdate/EndUpdate calls), /// the PositionChanged event is raised only once at the end of the document update. /// internal event EventHandler PositionChanged; private bool _raisePositionChangedOnUpdateFinished; private void RaisePositionChanged() { if (_textArea.Document != null && _textArea.Document.IsInUpdate) { _raisePositionChangedOnUpdateFinished = true; } else { PositionChanged?.Invoke(this, EventArgs.Empty); } } internal void OnDocumentUpdateFinished() { if (_raisePositionChangedOnUpdateFinished) { _raisePositionChangedOnUpdateFinished = false; PositionChanged?.Invoke(this, EventArgs.Empty); } } private bool _visualColumnValid; private void ValidateVisualColumn() { if (!_visualColumnValid) { var document = _textArea.Document; if (document != null) { // Debug.WriteLine("Explicit validation of caret column"); var documentLine = document.GetLineByNumber(_position.Line); RevalidateVisualColumn(_textView.GetOrConstructVisualLine(documentLine)); } } } private void InvalidateVisualColumn() { _visualColumnValid = false; } /// /// Validates the visual column of the caret using the specified visual line. /// The visual line must contain the caret offset. /// private void RevalidateVisualColumn(VisualLine visualLine) { if (visualLine == null) throw new ArgumentNullException(nameof(visualLine)); // mark column as validated _visualColumnValid = true; var caretOffset = _textView.Document.GetOffset(_position.Location); var firstDocumentLineOffset = visualLine.FirstDocumentLine.Offset; _position.VisualColumn = visualLine.ValidateVisualColumn(_position, _textArea.Selection.EnableVirtualSpace); // search possible caret positions var newVisualColumnForwards = visualLine.GetNextCaretPosition(_position.VisualColumn - 1, LogicalDirection.Forward, CaretPositioningMode.Normal, _textArea.Selection.EnableVirtualSpace); // If position.VisualColumn was valid, we're done with validation. if (newVisualColumnForwards != _position.VisualColumn) { // also search backwards so that we can pick the better match var newVisualColumnBackwards = visualLine.GetNextCaretPosition(_position.VisualColumn + 1, LogicalDirection.Backward, CaretPositioningMode.Normal, _textArea.Selection.EnableVirtualSpace); if (newVisualColumnForwards < 0 && newVisualColumnBackwards < 0) throw ThrowUtil.NoValidCaretPosition(); // determine offsets for new visual column positions int newOffsetForwards; if (newVisualColumnForwards >= 0) newOffsetForwards = visualLine.GetRelativeOffset(newVisualColumnForwards) + firstDocumentLineOffset; else newOffsetForwards = -1; int newOffsetBackwards; if (newVisualColumnBackwards >= 0) newOffsetBackwards = visualLine.GetRelativeOffset(newVisualColumnBackwards) + firstDocumentLineOffset; else newOffsetBackwards = -1; int newVisualColumn, newOffset; // if there's only one valid position, use it if (newVisualColumnForwards < 0) { newVisualColumn = newVisualColumnBackwards; newOffset = newOffsetBackwards; } else if (newVisualColumnBackwards < 0) { newVisualColumn = newVisualColumnForwards; newOffset = newOffsetForwards; } else { // two valid positions: find the better match if (Math.Abs(newOffsetBackwards - caretOffset) < Math.Abs(newOffsetForwards - caretOffset)) { // backwards is better newVisualColumn = newVisualColumnBackwards; newOffset = newOffsetBackwards; } else { // forwards is better newVisualColumn = newVisualColumnForwards; newOffset = newOffsetForwards; } } Position = new TextViewPosition(_textView.Document.GetLocation(newOffset), newVisualColumn); } _isInVirtualSpace = (_position.VisualColumn > visualLine.VisualLength); } private Rect CalcCaretRectangle(VisualLine visualLine) { if (!_visualColumnValid) { RevalidateVisualColumn(visualLine); } var textLine = visualLine.GetTextLine(_position.VisualColumn, _position.IsAtEndOfLine); var xPos = visualLine.GetTextLineVisualXPosition(textLine, _position.VisualColumn); var lineTop = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.LineTop); var lineBottom = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.LineBottom); return new Rect(xPos, lineTop, CaretWidth, lineBottom - lineTop); } Rect CalcCaretOverstrikeRectangle(VisualLine visualLine) { if (!_visualColumnValid) { RevalidateVisualColumn(visualLine); } int currentPos = _position.VisualColumn; // The text being overwritten in overstrike mode is everything up to the next normal caret stop int nextPos = visualLine.GetNextCaretPosition(currentPos, LogicalDirection.Forward, CaretPositioningMode.Normal, true); var textLine = visualLine.GetTextLine(currentPos); Rect r; if (currentPos < visualLine.VisualLength) { // If the caret is within the text, use GetTextBounds() for the text being overwritten. // This is necessary to ensure the rectangle is calculated correctly in bidirectional text. var textBounds = textLine.GetTextBounds(currentPos, nextPos - currentPos)[0]; r = textBounds; var y = r.y + visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.LineTop); r = r.WithY(y); } else { // If the caret is at the end of the line (or in virtual space), // use the visual X position of currentPos and nextPos (one or more of which will be in virtual space) float xPos = visualLine.GetTextLineVisualXPosition(textLine, currentPos); float xPos2 = visualLine.GetTextLineVisualXPosition(textLine, nextPos); float lineTop = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextTop); float lineBottom = visualLine.GetTextLineVisualYPosition(textLine, VisualYPosition.TextBottom); r = new Rect(xPos, lineTop, xPos2 - xPos, lineBottom - lineTop); } // If the caret is too small (e.g. in front of zero-width character), ensure it's still visible if (r.width < CaretWidth) r = r.WithWidth(CaretWidth); return r; } /// /// Returns the caret rectangle. The coordinate system is in device-independent pixels from the top of the document. /// internal Rect CalculateCaretRectangle() { if (_textView?.Document != null) { var visualLine = _textView.GetOrConstructVisualLine(_textView.Document.GetLineByNumber(_position.Line)); return /*_textArea.OverstrikeMode ? CalcCaretOverstrikeRectangle(visualLine); : */CalcCaretRectangle(visualLine); } return default; } /// /// Minimum distance of the caret to the view border. /// internal const double MinimumDistanceToViewBorder = 0; /// /// Scrolls the text view so that the caret is visible. /// internal void BringCaretToView() { BringCaretToView(0); } internal void BringCaretToView(float border) { var caretRectangle = CalculateCaretRectangle(); if (caretRectangle != default) { caretRectangle = caretRectangle.Inflate(border); _textView.MakeVisible(caretRectangle); } } /// /// Makes the caret visible and updates its on-screen position. /// internal void Show() { _visible = true; ShowInternal(); } private bool _hasWin32Caret; private void ShowInternal() { if (_caretAdorner != null && _textView != null) { var visualLine = _textView.GetVisualLine(_position.Line); if (visualLine != null) { var caretRect = /*_textArea.OverstrikeMode ? CalcCaretOverstrikeRectangle(visualLine); :*/ CalcCaretRectangle(visualLine); // TODO: win32 caret // Create Win32 caret so that Windows knows where our managed caret is. This is necessary for // features like 'Follow text editing' in the Windows Magnifier. //if (!hasWin32Caret) { // hasWin32Caret = Win32.CreateCaret(textView, caretRect.Size); //} //if (hasWin32Caret) { // Win32.SetCaretPosition(textView, caretRect.Location - textView.ScrollOffset); //} _caretAdorner.Show(caretRect); //textArea.ime.UpdateCompositionWindow(); } else { _caretAdorner.Hide(); } } } /// /// Makes the caret invisible. /// internal void Hide() { _visible = false; if (_hasWin32Caret) { // TODO: win32 caret //Win32.DestroyCaret(); _hasWin32Caret = false; } _caretAdorner?.Hide(); } /// /// Gets/Sets the color of the caret. /// internal Color? CaretBrush { get => _caretAdorner.CaretBrush; set => _caretAdorner.CaretBrush = value; } } }