Browse Source

Fix issues mentioned during review

Benedikt Stebner 3 years ago
parent
commit
27c7a5c724

+ 25 - 23
src/Avalonia.Controls/Presenters/TextPresenter.cs

@@ -385,9 +385,8 @@ namespace Avalonia.Controls.Presenters
             }
             }
 
 
             var (p1, p2) = GetCaretPoints();
             var (p1, p2) = GetCaretPoints();
-            context.DrawLine(
-                new ImmutablePen(caretBrush, 1),
-                p1, p2);
+
+            context.DrawLine(new ImmutablePen(caretBrush), p1, p2);
         }
         }
         
         
         private (Point, Point) GetCaretPoints()
         private (Point, Point) GetCaretPoints()
@@ -396,13 +395,7 @@ namespace Avalonia.Controls.Presenters
             var y = Math.Floor(_caretBounds.Y) + 0.5;
             var y = Math.Floor(_caretBounds.Y) + 0.5;
             var b = Math.Ceiling(_caretBounds.Bottom) - 0.5;
             var b = Math.Ceiling(_caretBounds.Bottom) - 0.5;
 
 
-            var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(_caretIndex);
-
-            var textLine = TextLayout.TextLines[lineIndex];
-
-            var posX = textLine.Start + x;
-            
-            if (posX >= Bounds.Width)
+            if (x >= Bounds.Width)
             {
             {
                 x = Math.Floor(_caretBounds.X - 1) + 0.5;
                 x = Math.Floor(_caretBounds.X - 1) + 0.5;
             }
             }
@@ -444,8 +437,6 @@ namespace Avalonia.Controls.Presenters
 
 
                 if (IsMeasureValid)
                 if (IsMeasureValid)
                 {
                 {
-                    //var rect = TextLayout.HitTestTextPosition(caretIndex);
-                    //_caretPosition = rect;
                     this.BringIntoView(_caretBounds);
                     this.BringIntoView(_caretBounds);
                 }
                 }
                 else
                 else
@@ -456,7 +447,6 @@ namespace Avalonia.Controls.Presenters
                     Dispatcher.UIThread.Post(
                     Dispatcher.UIThread.Post(
                         () =>
                         () =>
                         {
                         {
-                            //var rect = TextLayout.HitTestTextPosition(caretIndex);
                             this.BringIntoView(_caretBounds);
                             this.BringIntoView(_caretBounds);
                         },
                         },
                         DispatcherPriority.Render);
                         DispatcherPriority.Render);
@@ -540,7 +530,7 @@ namespace Avalonia.Controls.Presenters
 
 
         public void MoveCaretToTextPosition(int textPosition, bool trailingEdge = false)
         public void MoveCaretToTextPosition(int textPosition, bool trailingEdge = false)
         {
         {
-            var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(textPosition);
+            var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(textPosition, trailingEdge);
             var textLine = TextLayout.TextLines[lineIndex];
             var textLine = TextLayout.TextLines[lineIndex];
 
 
             var characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(textPosition));
             var characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(textPosition));
@@ -573,16 +563,14 @@ namespace Avalonia.Controls.Presenters
 
 
         public void MoveCaretVertical(LogicalDirection direction = LogicalDirection.Forward)
         public void MoveCaretVertical(LogicalDirection direction = LogicalDirection.Forward)
         {
         {
-            var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(CaretIndex);
+            var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(CaretIndex, _lastCharacterHit.TrailingLength > 0);
 
 
             if (lineIndex < 0)
             if (lineIndex < 0)
             {
             {
                 return;
                 return;
             }
             }
 
 
-            var currentX = _navigationPosition.X;
-
-            var currentY = _navigationPosition.Y;
+            var (currentX, currentY) = _navigationPosition;
 
 
             if (direction == LogicalDirection.Forward)
             if (direction == LogicalDirection.Forward)
             {
             {
@@ -607,9 +595,11 @@ namespace Avalonia.Controls.Presenters
                 currentY -= textLine.Height;
                 currentY -= textLine.Height;
             }
             }
 
 
+            var navigationPosition = _navigationPosition;
+            
             MoveCaretToPoint(new Point(currentX, currentY));
             MoveCaretToPoint(new Point(currentX, currentY));
             
             
-            _navigationPosition = _navigationPosition.WithY(_caretBounds.Y);
+            _navigationPosition = navigationPosition.WithY(_caretBounds.Y);
         }
         }
 
 
         public void MoveCaretHorizontal(LogicalDirection direction = LogicalDirection.Forward)
         public void MoveCaretHorizontal(LogicalDirection direction = LogicalDirection.Forward)
@@ -617,7 +607,7 @@ namespace Avalonia.Controls.Presenters
             var characterHit = _lastCharacterHit;
             var characterHit = _lastCharacterHit;
             var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
             var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
             
             
-            var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(caretIndex);
+            var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(caretIndex, false);
 
 
             if (lineIndex < 0)
             if (lineIndex < 0)
             {
             {
@@ -634,11 +624,23 @@ namespace Avalonia.Controls.Presenters
 
 
                     caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
                     caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
 
 
-                    if (caretIndex - textLine.TrailingWhitespaceLength == textLine.TextRange.End)
+                    if (textLine.NewLineLength > 0 && caretIndex == textLine.TextRange.Start + textLine.TextRange.Length)
                     {
                     {
-                        break;
+                        characterHit = new CharacterHit(caretIndex);
                     }
                     }
                     
                     
+                    if (caretIndex >= Text.Length)
+                    {
+                        characterHit = new CharacterHit(Text.Length);
+                        
+                        break;
+                    }
+
+                    if (caretIndex - textLine.NewLineLength == textLine.TextRange.Start + textLine.TextRange.Length)
+                    {
+                        break;
+                    }
+
                     if (caretIndex <= CaretIndex)
                     if (caretIndex <= CaretIndex)
                     {
                     {
                         lineIndex++;
                         lineIndex++;
@@ -681,7 +683,7 @@ namespace Avalonia.Controls.Presenters
             
             
             var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
             var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
             
             
-            var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(caretIndex);
+            var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(caretIndex, characterHit.TrailingLength > 0);
             var textLine = TextLayout.TextLines[lineIndex];
             var textLine = TextLayout.TextLines[lineIndex];
             var distanceX = textLine.GetDistanceFromCharacterHit(characterHit);
             var distanceX = textLine.GetDistanceFromCharacterHit(characterHit);
 
 

+ 107 - 60
src/Avalonia.Controls/TextBox.cs

@@ -307,7 +307,7 @@ namespace Avalonia.Controls
                 
                 
                 if (SelectionStart == SelectionEnd)
                 if (SelectionStart == SelectionEnd)
                 {
                 {
-                    CaretIndex = SelectionEnd;
+                    CaretIndex = SelectionStart;
                 }
                 }
             }
             }
         }
         }
@@ -328,11 +328,6 @@ namespace Avalonia.Controls
                 {
                 {
                     UpdateCommandStates();
                     UpdateCommandStates();
                 }
                 }
-                
-                if (SelectionStart == SelectionEnd)
-                {
-                    CaretIndex = SelectionEnd;
-                }
             }
             }
         }
         }
 
 
@@ -760,6 +755,11 @@ namespace Avalonia.Controls
 
 
         protected override void OnKeyDown(KeyEventArgs e)
         protected override void OnKeyDown(KeyEventArgs e)
         {
         {
+            if (_presenter == null)
+            {
+                return;
+            }
+            
             var text = Text ?? string.Empty;
             var text = Text ?? string.Empty;
             var caretIndex = CaretIndex;
             var caretIndex = CaretIndex;
             var movement = false;
             var movement = false;
@@ -905,25 +905,45 @@ namespace Avalonia.Controls
                         break;
                         break;
 
 
                     case Key.Up:
                     case Key.Up:
-                        _presenter?.MoveCaretVertical(LogicalDirection.Backward);
-                        if (caretIndex != CaretIndex)
+                    {
+                        selection = DetectSelection();
+                        
+                        _presenter.MoveCaretVertical(LogicalDirection.Backward);
+                        
+                        if (caretIndex != _presenter.CaretIndex)
                         {
                         {
                             movement = true;
                             movement = true;
                         }
                         }
-                        selection = DetectSelection();
-                        break;
 
 
+                        if (selection)
+                        {
+                            SelectionEnd = _presenter.CaretIndex;
+                        }
+                        
+                        break;
+                    }
                     case Key.Down:
                     case Key.Down:
-                        _presenter?.MoveCaretVertical(LogicalDirection.Forward);
-                        if (caretIndex != CaretIndex)
+                    {
+                        selection = DetectSelection();
+                        
+                        _presenter?.MoveCaretVertical();
+                        
+                        if (caretIndex != _presenter.CaretIndex)
                         {
                         {
                             movement = true;
                             movement = true;
                         }
                         }
-                        selection = DetectSelection();
+  
+                        if (selection)
+                        {
+                            SelectionEnd = _presenter.CaretIndex;
+                        }
+                        
                         break;
                         break;
-
+                    }
                     case Key.Back:
                     case Key.Back:
+                    {
                         SnapshotUndoRedo();
                         SnapshotUndoRedo();
+                        
                         if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
                         if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
                         {
                         {
                             SetSelectionForControlBackspace();
                             SetSelectionForControlBackspace();
@@ -952,15 +972,18 @@ namespace Avalonia.Controls
 
 
                             SetTextInternal(text.Substring(0, length) +
                             SetTextInternal(text.Substring(0, length) +
                                             text.Substring(caretIndex));
                                             text.Substring(caretIndex));
+                            
                             CaretIndex = caretIndex - removedCharacters;
                             CaretIndex = caretIndex - removedCharacters;
+                            
                             ClearSelection();
                             ClearSelection();
                         }
                         }
 
 
                         handled = true;
                         handled = true;
                         break;
                         break;
-
+                    }
                     case Key.Delete:
                     case Key.Delete:
                         SnapshotUndoRedo();
                         SnapshotUndoRedo();
+                        
                         if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
                         if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
                         {
                         {
                             SetSelectionForControlDelete();
                             SetSelectionForControlDelete();
@@ -968,16 +991,16 @@ namespace Avalonia.Controls
 
 
                         if (!DeleteSelection() && caretIndex < text.Length)
                         if (!DeleteSelection() && caretIndex < text.Length)
                         {
                         {
-                           _presenter.MoveCaretHorizontal();
+                            _presenter.MoveCaretHorizontal();
 
 
-                           var removedCharacters = Math.Max(0, _presenter.CaretIndex - caretIndex);
+                            var removedCharacters = Math.Max(0, _presenter.CaretIndex - caretIndex);
 
 
-                           SetTextInternal(text.Substring(0, caretIndex) +
-                                           text.Substring(caretIndex + removedCharacters));
-                           
-                           CaretIndex = caretIndex;
+                            SetTextInternal(text.Substring(0, caretIndex) +
+                                            text.Substring(caretIndex + removedCharacters));
+
+                            CaretIndex = caretIndex;
                         }
                         }
-                        
+
                         SnapshotUndoRedo();
                         SnapshotUndoRedo();
 
 
                         handled = true;
                         handled = true;
@@ -1017,11 +1040,7 @@ namespace Avalonia.Controls
                 }
                 }
             }
             }
 
 
-            if (movement && selection)
-            {
-                SelectionEnd = CaretIndex;
-            }
-            else if (movement)
+            if (movement && !selection)
             {
             {
                 ClearSelection();
                 ClearSelection();
             }
             }
@@ -1034,23 +1053,29 @@ namespace Avalonia.Controls
 
 
         protected override void OnPointerPressed(PointerPressedEventArgs e)
         protected override void OnPointerPressed(PointerPressedEventArgs e)
         {
         {
+            if (_presenter == null)
+            {
+                return;
+            }
+            
             var text = Text;
             var text = Text;
-
             var clickInfo = e.GetCurrentPoint(this);
             var clickInfo = e.GetCurrentPoint(this);
-            if (text != null && clickInfo.Properties.IsLeftButtonPressed && !(clickInfo.Pointer?.Captured is Border))
+
+            if (text != null && clickInfo.Properties.IsLeftButtonPressed &&
+                !(clickInfo.Pointer?.Captured is Border))
             {
             {
                 var point = e.GetPosition(_presenter);
                 var point = e.GetPosition(_presenter);
 
 
                 var oldIndex = CaretIndex;
                 var oldIndex = CaretIndex;
-                
+
                 _presenter.MoveCaretToPoint(point);
                 _presenter.MoveCaretToPoint(point);
-                
+
                 var index = _presenter.CaretIndex;
                 var index = _presenter.CaretIndex;
 
 
                 var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
                 var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
 
 
                 SetAndRaise(CaretIndexProperty, ref _caretIndex, index);
                 SetAndRaise(CaretIndexProperty, ref _caretIndex, index);
-                
+
 #pragma warning disable CS0618 // Type or member is obsolete
 #pragma warning disable CS0618 // Type or member is obsolete
                 switch (e.ClickCount)
                 switch (e.ClickCount)
 #pragma warning restore CS0618 // Type or member is obsolete
 #pragma warning restore CS0618 // Type or member is obsolete
@@ -1065,6 +1090,7 @@ namespace Avalonia.Controls
                         {
                         {
                             SelectionStart = SelectionEnd = index;
                             SelectionStart = SelectionEnd = index;
                         }
                         }
+
                         break;
                         break;
                     case 2:
                     case 2:
                         if (!StringUtils.IsStartOfWord(text, index))
                         if (!StringUtils.IsStartOfWord(text, index))
@@ -1086,8 +1112,13 @@ namespace Avalonia.Controls
 
 
         protected override void OnPointerMoved(PointerEventArgs e)
         protected override void OnPointerMoved(PointerEventArgs e)
         {
         {
+            if (_presenter == null)
+            {
+                return;
+            }
+            
             // selection should not change during pointer move if the user right clicks
             // selection should not change during pointer move if the user right clicks
-            if (_presenter != null && e.Pointer.Captured == _presenter && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
+            if (e.Pointer.Captured == _presenter && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
             {
             {
                 var point = e.GetPosition(_presenter);
                 var point = e.GetPosition(_presenter);
 
 
@@ -1095,7 +1126,7 @@ namespace Avalonia.Controls
                     MathUtilities.Clamp(point.X, 0, Math.Max(_presenter.Bounds.Width - 1, 0)),
                     MathUtilities.Clamp(point.X, 0, Math.Max(_presenter.Bounds.Width - 1, 0)),
                     MathUtilities.Clamp(point.Y, 0, Math.Max(_presenter.Bounds.Height - 1, 0)));
                     MathUtilities.Clamp(point.Y, 0, Math.Max(_presenter.Bounds.Height - 1, 0)));
 
 
-                _presenter?.MoveCaretToPoint(point);
+                _presenter.MoveCaretToPoint(point);
 
 
                 SelectionEnd = _presenter.CaretIndex;
                 SelectionEnd = _presenter.CaretIndex;
             }
             }
@@ -1103,27 +1134,37 @@ namespace Avalonia.Controls
 
 
         protected override void OnPointerReleased(PointerReleasedEventArgs e)
         protected override void OnPointerReleased(PointerReleasedEventArgs e)
         {
         {
-            if (_presenter != null && e.Pointer.Captured == _presenter)
+            if (_presenter == null)
+            {
+                return;
+            }
+
+            if (e.Pointer.Captured != _presenter)
+            {
+                return;
+            }
+
+            if (e.InitialPressMouseButton == MouseButton.Right)
             {
             {
-                if (e.InitialPressMouseButton == MouseButton.Right)
+                var point = e.GetPosition(_presenter);
+                    
+                _presenter.MoveCaretToPoint(point);
+                    
+                var caretIndex = _presenter.CaretIndex;
+
+                // see if mouse clicked inside current selection
+                // if it did not, we change the selection to where the user clicked
+                var firstSelection = Math.Min(SelectionStart, SelectionEnd);
+                var lastSelection = Math.Max(SelectionStart, SelectionEnd);
+                var didClickInSelection = SelectionStart != SelectionEnd &&
+                                          caretIndex >= firstSelection && caretIndex <= lastSelection;
+                if (!didClickInSelection)
                 {
                 {
-                    var point = e.GetPosition(_presenter);
-                    _presenter?.MoveCaretToPoint(point);
-                    var caretIndex = _presenter.CaretIndex;
-
-                    // see if mouse clicked inside current selection
-                    // if it did not, we change the selection to where the user clicked
-                    var firstSelection = Math.Min(SelectionStart, SelectionEnd);
-                    var lastSelection = Math.Max(SelectionStart, SelectionEnd);
-                    var didClickInSelection = SelectionStart != SelectionEnd &&
-                        caretIndex >= firstSelection && caretIndex <= lastSelection;
-                    if (!didClickInSelection)
-                    {
-                        CaretIndex = SelectionEnd = SelectionStart = caretIndex;
-                    }
+                    CaretIndex = SelectionEnd = SelectionStart = caretIndex;
                 }
                 }
-                e.Pointer.Capture(null);
             }
             }
+            
+            e.Pointer.Capture(null);
         }
         }
 
 
         protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
         protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
@@ -1170,31 +1211,37 @@ namespace Avalonia.Controls
         private void MoveHorizontal(int direction, bool wholeWord, bool isSelecting)
         private void MoveHorizontal(int direction, bool wholeWord, bool isSelecting)
         {
         {
             var text = Text ?? string.Empty;
             var text = Text ?? string.Empty;
-            var caretIndex = CaretIndex;
+            var selectionStart = SelectionStart;
 
 
             if (!wholeWord)
             if (!wholeWord)
             {
             {
-                if (SelectionStart != SelectionEnd && !isSelecting)
+                if (_presenter == null)
                 {
                 {
-                    var start = Math.Min(SelectionStart, SelectionEnd);
-                    var end = Math.Max(SelectionStart, SelectionEnd);
-                    CaretIndex = direction < 0 ? start : end;
                     return;
                     return;
                 }
                 }
-
+                
                 _presenter.MoveCaretHorizontal(direction > 0 ? LogicalDirection.Forward : LogicalDirection.Backward);
                 _presenter.MoveCaretHorizontal(direction > 0 ? LogicalDirection.Forward : LogicalDirection.Backward);
+
+                if (isSelecting)
+                {
+                    SelectionEnd = _presenter.CaretIndex;
+                }
+                else
+                {
+                    SelectionStart = SelectionEnd = _presenter.CaretIndex;
+                }
             }
             }
             else
             else
             {
             {
                 if (direction > 0)
                 if (direction > 0)
                 {
                 {
-                    var offset = StringUtils.NextWord(text, caretIndex) - caretIndex;
+                    var offset = StringUtils.NextWord(text, selectionStart) - selectionStart;
                     
                     
                     CaretIndex += offset;
                     CaretIndex += offset;
                 }
                 }
                 else
                 else
                 {
                 {
-                    var offset = StringUtils.PreviousWord(text, caretIndex) - caretIndex;
+                    var offset = StringUtils.PreviousWord(text, selectionStart) - selectionStart;
                     
                     
                     CaretIndex += offset;
                     CaretIndex += offset;
                 }
                 }
@@ -1277,7 +1324,7 @@ namespace Avalonia.Controls
                 caretIndex = pos;
                 caretIndex = pos;
             }
             }
 
 
-            CaretIndex = text.Length;
+            CaretIndex = caretIndex;
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 3 - 0
src/Avalonia.Visuals/Media/TextFormatting/FontMetrics.cs

@@ -30,6 +30,9 @@
             StrikethroughPosition = glyphTypeface.StrikethroughPosition * scale;
             StrikethroughPosition = glyphTypeface.StrikethroughPosition * scale;
         }
         }
 
 
+        /// <summary>
+        /// Em size of font used to format and display text
+        /// </summary>
         public double FontRenderingEmSize { get; }
         public double FontRenderingEmSize { get; }
 
 
         /// <summary>
         /// <summary>

+ 12 - 4
src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs

@@ -69,13 +69,11 @@ namespace Avalonia.Media.TextFormatting
             TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties)
             TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties)
         {
         {
             var defaultTypeface = defaultProperties.Typeface;
             var defaultTypeface = defaultProperties.Typeface;
-
             var currentTypeface = defaultTypeface;
             var currentTypeface = defaultTypeface;
+            var previousTypeface = previousProperties?.Typeface;
 
 
             if (TryGetShapeableLength(text, currentTypeface, out var count, out var script))
             if (TryGetShapeableLength(text, currentTypeface, out var count, out var script))
             {
             {
-                var previousTypeface = previousProperties?.Typeface;
-                
                 if (script == Script.Common && previousTypeface is not null)
                 if (script == Script.Common && previousTypeface is not null)
                 {
                 {
                     if(TryGetShapeableLength(text, previousTypeface.Value, out var fallbackCount, out _))
                     if(TryGetShapeableLength(text, previousTypeface.Value, out var fallbackCount, out _))
@@ -90,6 +88,16 @@ namespace Avalonia.Media.TextFormatting
                     new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize,
                     new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize,
                         defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
                         defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
             }
             }
+            
+            if (previousTypeface is not null)
+            {
+                if(TryGetShapeableLength(text, previousTypeface.Value, out count, out _))
+                {
+                    return new ShapeableTextCharacters(text.Take(count),
+                        new GenericTextRunProperties(previousTypeface.Value, defaultProperties.FontRenderingEmSize,
+                            defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
+                }
+            }
 
 
             var codepoint = Codepoint.ReplacementCodepoint;
             var codepoint = Codepoint.ReplacementCodepoint;
 
 
@@ -176,7 +184,7 @@ namespace Avalonia.Media.TextFormatting
                 if (currentScript != script)
                 if (currentScript != script)
                 {
                 {
                     if (script is Script.Unknown || currentScript != Script.Common &&
                     if (script is Script.Unknown || currentScript != Script.Common &&
-                        (script is Script.Common || script is Script.Inherited))
+                        script is Script.Common or Script.Inherited)
                     {
                     {
                         script = currentScript;
                         script = currentScript;
                     }
                     }

+ 12 - 6
src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs

@@ -10,7 +10,7 @@ namespace Avalonia.Media.TextFormatting
     /// </summary>
     /// </summary>
     public class TextLayout
     public class TextLayout
     {
     {
-        private static readonly char[] s_empty = { '\u200B' };
+        private static readonly char[] s_empty = { ' ' };
 
 
         private readonly ReadOnlySlice<char> _text;
         private readonly ReadOnlySlice<char> _text;
         private readonly TextParagraphProperties _paragraphProperties;
         private readonly TextParagraphProperties _paragraphProperties;
@@ -378,14 +378,14 @@ namespace Avalonia.Media.TextFormatting
         }
         }
 
 
         
         
-        public int GetLineIndexFromCharacterIndex(int charIndex)
+        public int GetLineIndexFromCharacterIndex(int charIndex, bool trailingEdge)
         {
         {
             if (charIndex < 0)
             if (charIndex < 0)
             {
             {
-                return -1;
+                return 0;
             }
             }
 
 
-            if (charIndex > _text.Length - 1)
+            if (charIndex > _text.Length)
             {
             {
                 return TextLines.Count - 1;
                 return TextLines.Count - 1;
             }
             }
@@ -399,7 +399,7 @@ namespace Avalonia.Media.TextFormatting
                     continue;
                     continue;
                 }
                 }
 
 
-                if (charIndex >= textLine.Start && charIndex <= textLine.TextRange.Start + textLine.TextRange.Length)
+                if (charIndex >= textLine.Start && charIndex <= textLine.TextRange.End + (trailingEdge ? 1 : 0))
                 {
                 {
                     return index;
                     return index;
                 }
                 }
@@ -430,7 +430,13 @@ namespace Avalonia.Media.TextFormatting
             {
             {
                 textPosition -= textLine.NewLineLength;
                 textPosition -= textLine.NewLineLength;
             }
             }
-            
+
+            if (textLine.NewLineLength > 0 && textPosition + textLine.NewLineLength ==
+                characterHit.FirstCharacterIndex + characterHit.TrailingLength)
+            {
+                characterHit = new CharacterHit(characterHit.FirstCharacterIndex);
+            }
+
             return new TextHitTestResult(characterHit, textPosition, isInside, isTrailing);
             return new TextHitTestResult(characterHit, textPosition, isInside, isTrailing);
         }
         }
 
 

+ 1 - 1
src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs

@@ -557,7 +557,7 @@ namespace Avalonia.Media.TextFormatting
 
 
                 var characterIndex = codepointIndex - run.Text.Start;
                 var characterIndex = codepointIndex - run.Text.Start;
 
 
-                if (characterIndex < 0 && characterHit.TrailingLength == 0)
+                if (characterIndex < 0 && run.ShapedBuffer.IsLeftToRight)
                 {
                 {
                     foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
                     foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
                 }
                 }

+ 1 - 14
src/Avalonia.Visuals/Utilities/BinarySearchExtension.cs

@@ -22,7 +22,7 @@ namespace Avalonia.Utilities
     /// <summary>
     /// <summary>
     /// Extension methods for binary searching an IReadOnlyList collection
     /// Extension methods for binary searching an IReadOnlyList collection
     /// </summary>
     /// </summary>
-    public static class BinarySearchExtension
+    internal static class BinarySearchExtension
     {
     {
         private static int GetMedian(int low, int hi)
         private static int GetMedian(int low, int hi)
         {
         {
@@ -31,18 +31,6 @@ namespace Avalonia.Utilities
             return low + (hi - low >> 1);
             return low + (hi - low >> 1);
         }
         }
 
 
-        /// <summary>
-        /// Performs a binary search on the entire contents of an IReadOnlyList
-        /// </summary>
-        /// <typeparam name="T">The list element type</typeparam>
-        /// <param name="list">The list to be searched</param>
-        /// <param name="value">The value to search for</param>
-        /// <returns>The index of the found item; otherwise the bitwise complement of the index of the next larger item</returns>
-        public static int BinarySearch<T>(this IReadOnlyList<T> list, T value) where T : IComparable
-        {
-            return list.BinarySearch(value, Comparer<T>.Default);
-        }
-
         /// <summary>
         /// <summary>
         /// Performs a binary search on the entire contents of an IReadOnlyList
         /// Performs a binary search on the entire contents of an IReadOnlyList
         /// </summary>
         /// </summary>
@@ -60,7 +48,6 @@ namespace Avalonia.Utilities
         /// Performs a binary search on a a subset of an IReadOnlyList
         /// Performs a binary search on a a subset of an IReadOnlyList
         /// </summary>
         /// </summary>
         /// <typeparam name="T">The list element type</typeparam>
         /// <typeparam name="T">The list element type</typeparam>
-        /// <typeparam name="U">The value type being searched for</typeparam>
         /// <param name="list">The list to be searched</param>
         /// <param name="list">The list to be searched</param>
         /// <param name="index">The start of the range to be searched</param>
         /// <param name="index">The start of the range to be searched</param>
         /// <param name="length">The length of the range to be searched</param>
         /// <param name="length">The length of the range to be searched</param>

+ 2 - 2
tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs

@@ -907,14 +907,14 @@ namespace Avalonia.Controls.UnitTests
                     Name = "PART_TextPresenter",
                     Name = "PART_TextPresenter",
                     [!!TextPresenter.TextProperty] = new Binding
                     [!!TextPresenter.TextProperty] = new Binding
                     {
                     {
-                        Path = "Text",
+                        Path = nameof(TextPresenter.Text),
                         Mode = BindingMode.TwoWay,
                         Mode = BindingMode.TwoWay,
                         Priority = BindingPriority.TemplatedParent,
                         Priority = BindingPriority.TemplatedParent,
                         RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
                         RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
                     },
                     },
                     [!!TextPresenter.CaretIndexProperty] = new Binding
                     [!!TextPresenter.CaretIndexProperty] = new Binding
                     {
                     {
-                        Path = "CaretIndex",
+                        Path = nameof(TextPresenter.CaretIndex),
                         Mode = BindingMode.TwoWay,
                         Mode = BindingMode.TwoWay,
                         Priority = BindingPriority.TemplatedParent,
                         Priority = BindingPriority.TemplatedParent,
                         RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
                         RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),

+ 2 - 2
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@@ -885,14 +885,14 @@ namespace Avalonia.Controls.UnitTests
                     Name = "PART_TextPresenter",
                     Name = "PART_TextPresenter",
                     [!!TextPresenter.TextProperty] = new Binding
                     [!!TextPresenter.TextProperty] = new Binding
                     {
                     {
-                        Path = "Text",
+                        Path = nameof(TextPresenter.Text),
                         Mode = BindingMode.TwoWay,
                         Mode = BindingMode.TwoWay,
                         Priority = BindingPriority.TemplatedParent,
                         Priority = BindingPriority.TemplatedParent,
                         RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
                         RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
                     },
                     },
                     [!!TextPresenter.CaretIndexProperty] = new Binding
                     [!!TextPresenter.CaretIndexProperty] = new Binding
                     {
                     {
-                        Path = "CaretIndex",
+                        Path = nameof(TextPresenter.CaretIndex),
                         Mode = BindingMode.TwoWay,
                         Mode = BindingMode.TwoWay,
                         Priority = BindingPriority.TemplatedParent,
                         Priority = BindingPriority.TemplatedParent,
                         RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
                         RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),

+ 15 - 0
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

@@ -829,6 +829,21 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 }
                 }
             }
             }
         }
         }
+        
+        [Fact]
+        public void Should_Layout_Empty_String()
+        {
+            using (Start())
+            {
+                var layout = new TextLayout(
+                    string.Empty,
+                    Typeface.Default,
+                    12,
+                    Brushes.Black);
+                
+                Assert.True(layout.Size.Height > 0);
+            }
+        }
 
 
         private static IDisposable Start()
         private static IDisposable Start()
         {
         {

+ 0 - 7
tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/BiDiPairedBracketTypeTests.cs

@@ -1,7 +0,0 @@
-namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
-{
-    public class BiDiPairedBracketTypeTests
-    {
-        
-    }
-}