Skip to content

Commit 9823926

Browse files
committed
improve text selector indicator handling
1 parent e4be7ca commit 9823926

3 files changed

Lines changed: 158 additions & 124 deletions

File tree

src/Avalonia.Controls/Primitives/TextSelectionCanvas.cs

Lines changed: 79 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ internal class TextSelectionHandleCanvas : Canvas
1515
private static bool s_isInTouchMode;
1616

1717
private readonly TextSelectionHandle _caretHandle;
18-
private readonly TextSelectionHandle _startHandle;
19-
private readonly TextSelectionHandle _endHandle;
18+
private readonly TextSelectionHandle _handle1;
19+
private readonly TextSelectionHandle _handle2;
2020
private TextPresenter? _presenter;
2121
private TextBox? _textBox;
2222
private bool _showHandle;
@@ -32,8 +32,8 @@ internal bool ShowHandles
3232

3333
if (!value)
3434
{
35-
_startHandle.IsVisible = false;
36-
_endHandle.IsVisible = false;
35+
_handle1.IsVisible = false;
36+
_handle2.IsVisible = false;
3737
_caretHandle.IsVisible = false;
3838
}
3939

@@ -44,37 +44,37 @@ internal bool ShowHandles
4444
public TextSelectionHandleCanvas()
4545
{
4646
_caretHandle = new TextSelectionHandle() { SelectionHandleType = SelectionHandleType.Caret };
47-
_startHandle = new TextSelectionHandle();
48-
_endHandle = new TextSelectionHandle();
47+
_handle1 = new TextSelectionHandle();
48+
_handle2 = new TextSelectionHandle();
49+
50+
_caretHandle.SelectionHandleType = SelectionHandleType.Caret;
51+
_handle1.SelectionHandleType = SelectionHandleType.Start;
52+
_handle2.SelectionHandleType = SelectionHandleType.End;
4953

5054
Children.Add(_caretHandle);
51-
Children.Add(_startHandle);
52-
Children.Add(_endHandle);
55+
Children.Add(_handle1);
56+
Children.Add(_handle2);
5357

5458
_caretHandle.DragStarted += Handle_DragStarted;
5559
_caretHandle.DragDelta += CaretHandle_DragDelta;
5660
_caretHandle.DragCompleted += Handle_DragCompleted;
57-
_startHandle.DragDelta += StartHandle_DragDelta;
58-
_startHandle.DragCompleted += Handle_DragCompleted;
59-
_startHandle.DragStarted += Handle_DragStarted;
60-
_endHandle.DragDelta += EndHandle_DragDelta;
61-
_endHandle.DragCompleted += Handle_DragCompleted;
62-
_endHandle.DragStarted += Handle_DragStarted;
63-
64-
_caretHandle.Classes.Add("caret");
65-
_startHandle.Classes.Add("start");
66-
_endHandle.Classes.Add("end");
67-
68-
_startHandle.SetTopLeft(default);
61+
_handle1.DragDelta += StartHandle_DragDelta;
62+
_handle1.DragCompleted += Handle_DragCompleted;
63+
_handle1.DragStarted += Handle_DragStarted;
64+
_handle2.DragDelta += EndHandle_DragDelta;
65+
_handle2.DragCompleted += Handle_DragCompleted;
66+
_handle2.DragStarted += Handle_DragStarted;
67+
68+
_handle1.SetTopLeft(default);
6969
_caretHandle.SetTopLeft(default);
70-
_endHandle.SetTopLeft(default);
70+
_handle2.SetTopLeft(default);
7171

72-
_startHandle.ContextCanceled += Caret_ContextCanceled;
72+
_handle1.ContextCanceled += Caret_ContextCanceled;
7373
_caretHandle.ContextCanceled += Caret_ContextCanceled;
74-
_endHandle.ContextCanceled += Caret_ContextCanceled;
75-
_startHandle.ContextRequested += Caret_ContextRequested;
74+
_handle2.ContextCanceled += Caret_ContextCanceled;
75+
_handle1.ContextRequested += Caret_ContextRequested;
7676
_caretHandle.ContextRequested += Caret_ContextRequested;
77-
_endHandle.ContextRequested += Caret_ContextRequested;
77+
_handle2.ContextRequested += Caret_ContextRequested;
7878

7979
IsVisible = ShowHandles;
8080

@@ -176,21 +176,24 @@ private void EnsureVisible()
176176
if (bounds == null)
177177
return;
178178

179-
var hasSelection = _presenter.SelectionStart != _presenter.SelectionEnd;
179+
var clip = bounds.Value.Clip.Inflate(_textBox?.Padding ?? new Thickness(4, 0));
180+
181+
var isSelectionDragging = _handle1.IsDragging || _handle2.IsDragging;
182+
183+
var hasSelection = _presenter.SelectionStart != _presenter.SelectionEnd || isSelectionDragging;
180184

181-
_startHandle.IsVisible = ShowHandles && hasSelection &&
182-
!IsOccluded(new Point(GetLeft(_startHandle), GetTop(_startHandle)));
183-
_endHandle.IsVisible = ShowHandles && hasSelection &&
184-
!IsOccluded(new Point(GetLeft(_endHandle), GetTop(_endHandle)));
185+
_handle1.IsVisible = ShowHandles && hasSelection &&
186+
!IsOccluded(_handle1.IndicatorPosition);
187+
_handle2.IsVisible = ShowHandles && hasSelection &&
188+
!IsOccluded(_handle2.IndicatorPosition);
185189
_caretHandle.IsVisible = ShowHandles && !hasSelection &&
186-
!IsOccluded(new Point(GetLeft(_caretHandle), GetTop(_caretHandle)));
190+
!IsOccluded(_caretHandle.IndicatorPosition);
187191

188192
bool IsOccluded(Point point)
189193
{
190-
return !bounds.Value.Clip.Contains(point);
194+
return !clip.Contains(point);
191195
}
192196

193-
194197
if (ShowHandles && !hasSelection)
195198
{
196199
_showDisposable = DispatcherTimer.RunOnce(() =>
@@ -219,54 +222,49 @@ private void DragSelectionHandle(TextSelectionHandle handle)
219222
{
220223
CloseFlyout();
221224

222-
var indicatorPosiiton = GetSearchPoint(handle);
223-
var point = ToPresenter(indicatorPosiiton);
224-
point = point.WithY(point.Y - _presenter.FontSize / 2);
225-
var hit = _presenter.TextLayout.HitTestPoint(point);
226-
var position = hit.CharacterHit.FirstCharacterIndex + hit.CharacterHit.TrailingLength;
225+
var position = GetTextPosition(handle);
226+
227+
var otherHandle = handle == _handle1 ? _handle2 : _handle1;
228+
229+
var otherPosition = GetTextPosition(otherHandle);
230+
231+
if (position == otherPosition)
232+
{
233+
position = handle.SelectionHandleType == SelectionHandleType.Start ? position - 1 : position + 1;
234+
235+
position = Math.Clamp(position, 0, (_textBox.Text?.Length - 1) ?? 1);
236+
}
227237

228-
var otherHandle = handle == _startHandle ? _endHandle : _startHandle;
229238
using var _ = BeginChange();
230239

231240
if (handle.SelectionHandleType == SelectionHandleType.Start)
232241
{
233-
if (position >= _textBox.SelectionEnd)
234-
position = _textBox.SelectionEnd - 1;
242+
position = position > _textBox.SelectionEnd ? _textBox.SelectionEnd - 1 : position;
235243
_textBox.SetCurrentValue(TextBox.SelectionStartProperty, position);
236244
}
237245
else
238246
{
239-
if (position <= _textBox.SelectionStart)
240-
position = _textBox.SelectionStart + 1;
247+
position = position < _textBox.SelectionStart ? _textBox.SelectionStart + 1 : position;
241248
_textBox.SetCurrentValue(TextBox.SelectionEndProperty, position);
242249
}
243250

244-
var selectionStart = _textBox.SelectionStart;
245-
var selectionEnd = _textBox.SelectionEnd;
246-
var start = Math.Min(selectionStart, selectionEnd);
247-
var length = Math.Max(selectionStart, selectionEnd) - start;
248-
var rects = new List<Rect>(_presenter.TextLayout.HitTestTextRange(start, length));
249-
250-
if (rects.Count > 0)
251-
{
252-
var first = rects[0];
253-
var last = rects[rects.Count - 1];
254-
255-
if (handle.SelectionHandleType == SelectionHandleType.Start)
256-
handle?.SetTopLeft(ToLayer(first.BottomLeft));
257-
else
258-
handle?.SetTopLeft(ToLayer(last.BottomRight));
251+
otherHandle.InvalidateVisual();
259252

260-
if (otherHandle.SelectionHandleType == SelectionHandleType.Start)
261-
otherHandle?.SetTopLeft(ToLayer(first.BottomLeft));
262-
else
263-
otherHandle?.SetTopLeft(ToLayer(last.BottomRight));
264-
}
265-
266-
_presenter?.MoveCaretToTextPosition(position);
253+
_presenter.MoveCaretToTextPosition(position);
254+
var caretBound = _presenter.GetCursorRectangle();
255+
handle.SetTopLeft(ToLayer(handle.SelectionHandleType == SelectionHandleType.Start ? caretBound.BottomLeft : caretBound.BottomLeft));
267256

268257
EnsureVisible();
269258
}
259+
260+
int GetTextPosition(TextSelectionHandle handle)
261+
{
262+
var indicatorPosition = GetSearchPoint(handle);
263+
var point = ToPresenter(indicatorPosition);
264+
var hit = _presenter.TextLayout.HitTestPoint(point);
265+
var position = hit.CharacterHit.FirstCharacterIndex + hit.CharacterHit.TrailingLength;
266+
return position;
267+
}
270268
}
271269

272270
private Point ToLayer(Point point)
@@ -282,9 +280,9 @@ private Point ToPresenter(Point point)
282280
public void MoveHandlesToSelection()
283281
{
284282
if (_presenter == null
285-
|| _startHandle.IsDragging
283+
|| _handle1.IsDragging
286284
|| _caretHandle.IsDragging
287-
|| _endHandle.IsDragging)
285+
|| _handle2.IsDragging)
288286
{
289287
return;
290288
}
@@ -309,21 +307,11 @@ public void MoveHandlesToSelection()
309307
var first = rects[0];
310308
var last = rects[rects.Count - 1];
311309

312-
if (!_startHandle.IsDragging)
313-
{
314-
_startHandle.SetTopLeft(ToLayer(first.BottomLeft));
315-
_startHandle.SelectionHandleType = selectionStart < selectionEnd ?
316-
SelectionHandleType.Start :
317-
SelectionHandleType.End;
318-
}
310+
var position = _handle1.SelectionHandleType == SelectionHandleType.Start ? first.BottomLeft : last.BottomRight;
311+
_handle1.SetTopLeft(ToLayer(position));
319312

320-
if (!_endHandle.IsDragging)
321-
{
322-
_endHandle.SetTopLeft(ToLayer(last.BottomRight));
323-
_endHandle.SelectionHandleType = selectionStart > selectionEnd ?
324-
SelectionHandleType.Start :
325-
SelectionHandleType.End;
326-
}
313+
position = _handle2.SelectionHandleType == SelectionHandleType.Start ? first.BottomLeft : last.BottomRight;
314+
_handle2.SetTopLeft(ToLayer(position));
327315
}
328316
}
329317

@@ -418,10 +406,10 @@ internal bool ShowFlyout(ContextRequestedEventArgs e)
418406

419407
if (_textBox.SelectionStart != _textBox.SelectionEnd)
420408
{
421-
if (_startHandle.IsEffectivelyVisible)
422-
handle = _startHandle;
423-
else if (_endHandle.IsEffectivelyVisible)
424-
handle = _endHandle;
409+
if (_handle1.IsEffectivelyVisible)
410+
handle = _handle1;
411+
else if (_handle2.IsEffectivelyVisible)
412+
handle = _handle2;
425413
}
426414
else
427415
{
@@ -433,11 +421,17 @@ internal bool ShowFlyout(ContextRequestedEventArgs e)
433421

434422
if (handle != null)
435423
{
424+
var oldVerticalOffset = flyout.VerticalOffset;
425+
var oldHorizontalOffset = flyout.HorizontalOffset;
426+
var oldPlacement = flyout.Placement;
436427
var topLeft = ToPresenter(handle.GetTopLeft());
437428
flyout.VerticalOffset = topLeft.Y - verticalOffset;
438429
flyout.HorizontalOffset = topLeft.X;
439430
flyout.Placement = PlacementMode.TopEdgeAlignedLeft;
440431
_textBox.RaiseEvent(new ContextRequestedEventArgs());
432+
flyout.VerticalOffset = oldVerticalOffset;
433+
flyout.HorizontalOffset = oldHorizontalOffset;
434+
flyout.Placement = oldPlacement;
441435

442436
return true;
443437
}

0 commit comments

Comments
 (0)