Skip to content

Commit ac28a74

Browse files
authored
Touch Improvements to TextBox (#20848)
* wip touch improvement textbox * update text selection handle style * change text selector layer z-index * fix build issues * fix caret detection in touch mode * added bottom padding to text handle * add indicator visual to selection handler theme * improve text selector indicator handling * add support for wrap around in selection handles * ensure textbox context menu is shown on hold * dampen scroll inertia * increase default tap and double tap sizes for touch and pen * make textbox context menu horizontal in touch mode. improve context menu show behavior for selection handles * detect overscroll in scroll presenter and handle scroll gesture if overscrolled * add rtl detection for selection handles * improve context flyout behavior in handles * restore textbox page * addressed review * add touch tests textbox * keep dragged handle visible, adjust flyout position to visible handle
1 parent 5ab7d5b commit ac28a74

16 files changed

Lines changed: 1407 additions & 547 deletions

File tree

src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public class ScrollGestureRecognizer : GestureRecognizer
3333
private TimeSpan _lastTime;
3434
private TimeSpan _inertiaStartTime;
3535
private int _currentInertiaGestureId;
36+
private Point _delta;
3637

3738
/// <summary>
3839
/// Defines the <see cref="CanHorizontallyScroll"/> property.
@@ -133,21 +134,30 @@ protected override void PointerMoved(PointerEventArgs e)
133134
_trackedRootPoint = new Point(
134135
_trackedRootPoint.X - (_trackedRootPoint.X >= rootPoint.X ? ScrollStartDistance : -ScrollStartDistance),
135136
_trackedRootPoint.Y - (_trackedRootPoint.Y >= rootPoint.Y ? ScrollStartDistance : -ScrollStartDistance));
136-
137-
Capture(e.Pointer);
138137
}
139138
}
140139

141140
if (_scrolling)
142141
{
143142
var vector = _trackedRootPoint - rootPoint;
144143

145-
_velocityTracker?.AddPosition(TimeSpan.FromMilliseconds(e.Timestamp), _pointerPressedPoint - rootPoint);
144+
var oldDelta = _delta;
145+
_delta = _pointerPressedPoint - rootPoint;
146+
147+
if (oldDelta == _delta)
148+
return;
149+
150+
_velocityTracker?.AddPosition(TimeSpan.FromMilliseconds(e.Timestamp), _delta);
146151

147152
_lastMoveTimestamp = e.Timestamp;
148-
Target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector));
153+
var scrollEventArgs = new ScrollGestureEventArgs(_gestureId, vector);
154+
Target!.RaiseEvent(scrollEventArgs);
149155
_trackedRootPoint = rootPoint;
150-
e.Handled = true;
156+
e.Handled = scrollEventArgs.Handled;
157+
if(e.Handled)
158+
{
159+
Capture(e.Pointer);
160+
}
151161
}
152162
}
153163
}
@@ -166,7 +176,9 @@ void EndGesture()
166176
_stopWatch?.Stop();
167177
_stopWatch = null;
168178
_inertia = default;
179+
_delta = default;
169180
_scrolling = false;
181+
_velocityTracker = null;
170182
Target!.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId));
171183
_gestureId = 0;
172184
_lastMoveTimestamp = null;

src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Diagnostics;
6-
using Avalonia.Utilities;
76

87
namespace Avalonia.Input.GestureRecognizers
98
{
@@ -71,13 +70,16 @@ internal class VelocityTracker
7170
private readonly PointAtTime[] _samples = new PointAtTime[HistorySize];
7271
private int _index = 0;
7372

73+
private Stopwatch _sinceLastSample = new Stopwatch();
74+
7475
/// <summary>
7576
/// Adds a position as the given time to the tracker.
7677
/// </summary>
7778
/// <param name="time"></param>
7879
/// <param name="position"></param>
7980
public void AddPosition(TimeSpan time, Vector position)
8081
{
82+
_sinceLastSample.Restart();
8183
_index++;
8284
if (_index == HistorySize)
8385
{
@@ -94,6 +96,11 @@ public void AddPosition(TimeSpan time, Vector position)
9496
/// Returns null if there is no data on which to base an estimate.
9597
protected virtual VelocityEstimate? GetVelocityEstimate()
9698
{
99+
if (_sinceLastSample.ElapsedMilliseconds > AssumePointerMoveStoppedMilliseconds)
100+
{
101+
return new VelocityEstimate(default, 1.0, default, default);
102+
}
103+
97104
Span<double> x = stackalloc double[HistorySize];
98105
Span<double> y = stackalloc double[HistorySize];
99106
Span<double> w = stackalloc double[HistorySize];
@@ -159,17 +166,6 @@ public void AddPosition(TimeSpan time, Vector position)
159166
}
160167
}
161168
}
162-
else if(sampleCount > 1)
163-
{
164-
// Return linear velocity if we don't have enough samples
165-
var distance = newestSample.Point - oldestSample.Point;
166-
return new VelocityEstimate(
167-
PixelsPerSecond: new Vector(distance.X / duration.Milliseconds * 1000, distance.Y / duration.Milliseconds * 1000),
168-
Confidence: 1,
169-
Duration: duration,
170-
Offset: offset
171-
);
172-
}
173169

174170
// We're unable to make a velocity estimate but we did have at least one
175171
// valid pointer position.
@@ -302,7 +298,7 @@ internal sealed class LeastSquaresSolver
302298
// Solve R B = Qt W Y to find B. This is easy because R is upper triangular.
303299
// We just work from bottom-right to top-left calculating B's coefficients.
304300
// "m" isn't expected to be bigger than HistorySize=20, so allocation on stack is safe.
305-
Span<double> wy = stackalloc double[m];
301+
Span<double> wy = stackalloc double[m];
306302
for (int h = 0; h < m; h += 1)
307303
{
308304
wy[h] = y[h] * w[h];
@@ -360,7 +356,7 @@ private static double Multiply(Span<double> v1, Span<double> v2)
360356
}
361357
return result;
362358
}
363-
359+
364360
private static double Norm(Span<double> v)
365361
{
366362
return Math.Sqrt(Multiply(v, v));

src/Avalonia.Base/Platform/DefaultPlatformSettings.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ namespace Avalonia.Platform
1414
[PrivateApi]
1515
public class DefaultPlatformSettings : IPlatformSettings
1616
{
17+
private const int TouchTapSize = 10;
18+
private const int TouchDoubleTapSize = 50; // Default TouchModeN_DtapDist value on win32
1719
public virtual Size GetTapSize(PointerType type)
1820
{
1921
return type switch
2022
{
21-
PointerType.Touch or PointerType.Pen => new(10, 10),
23+
PointerType.Touch or PointerType.Pen => new(TouchTapSize, TouchTapSize),
2224
_ => new(4, 4),
2325
};
2426
}
@@ -27,7 +29,7 @@ public virtual Size GetDoubleTapSize(PointerType type)
2729
{
2830
return type switch
2931
{
30-
PointerType.Touch or PointerType.Pen => new(16, 16),
32+
PointerType.Touch or PointerType.Pen => new(TouchDoubleTapSize, TouchDoubleTapSize),
3133
_ => new(4, 4),
3234
};
3335
}

src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,8 @@ private void OnScrollGesture(object? sender, ScrollGestureEventArgs e)
535535
var scrollable = Child as ILogicalScrollable;
536536
var isLogical = scrollable?.IsLogicalScrollEnabled == true;
537537
var logicalScrollItemSize = new Vector(1, 1);
538+
var canXScroll = false;
539+
var canYScroll = false;
538540

539541
double x = Offset.X;
540542
double y = Offset.Y;
@@ -565,6 +567,8 @@ private void OnScrollGesture(object? sender, ScrollGestureEventArgs e)
565567
y += dy;
566568
y = Math.Max(y, 0);
567569
y = Math.Min(y, Extent.Height - Viewport.Height);
570+
571+
canYScroll = dy != 0;
568572
}
569573

570574
if (Extent.Width > Viewport.Width)
@@ -581,6 +585,8 @@ private void OnScrollGesture(object? sender, ScrollGestureEventArgs e)
581585
x += dx;
582586
x = Math.Max(x, 0);
583587
x = Math.Min(x, Extent.Width - Viewport.Width);
588+
589+
canXScroll = dx != 0;
584590
}
585591

586592
if (isLogical)
@@ -615,6 +621,12 @@ private void OnScrollGesture(object? sender, ScrollGestureEventArgs e)
615621

616622
e.Handled = !IsScrollChainingEnabled || offsetChanged;
617623

624+
if(!e.Handled && !IsScrollChainingEnabled)
625+
{
626+
// Gesture may cause an overscroll so we mark the event as handled if it did.
627+
e.Handled = canXScroll || canYScroll;
628+
}
629+
618630
e.ShouldEndScrollGesture = !IsScrollChainingEnabled && !offsetChanged;
619631
}
620632
}

src/Avalonia.Controls/Presenters/TextPresenter.cs

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Data;
43
using Avalonia.Controls.Documents;
54
using Avalonia.Controls.Primitives;
6-
using Avalonia.Interactivity;
5+
using Avalonia.Input;
76
using Avalonia.Layout;
87
using Avalonia.Media;
98
using Avalonia.Media.Immutable;
109
using Avalonia.Media.TextFormatting;
1110
using Avalonia.Metadata;
11+
using Avalonia.Platform;
1212
using Avalonia.Threading;
1313
using Avalonia.Utilities;
1414
using Avalonia.VisualTree;
@@ -334,6 +334,7 @@ public int SelectionEnd
334334
protected override bool BypassFlowDirectionPolicies => true;
335335

336336
internal TextSelectionHandleCanvas? TextSelectionHandleCanvas { get; set; }
337+
internal TextBoxTextInputMethodClient? CurrentImClient { get; set; }
337338

338339
/// <summary>
339340
/// Creates the <see cref="TextLayout"/> used to render the text.
@@ -496,7 +497,6 @@ public void ShowCaret()
496497
public void HideCaret()
497498
{
498499
_caretBlink = false;
499-
RemoveTextSelectionCanvas();
500500
_caretTimer?.Stop();
501501
InvalidateTextLayout();
502502
}
@@ -944,7 +944,7 @@ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
944944
ResetCaretTimer();
945945
}
946946

947-
private void EnsureTextSelectionLayer()
947+
internal void EnsureTextSelectionLayer()
948948
{
949949
if (TextSelectionHandleCanvas == null)
950950
{
@@ -963,7 +963,7 @@ private void EnsureTextSelectionLayer()
963963
_layer?.Add(TextSelectionHandleCanvas);
964964
}
965965

966-
private void RemoveTextSelectionCanvas()
966+
internal void RemoveTextSelectionCanvas()
967967
{
968968
if(_layer != null && TextSelectionHandleCanvas is { } canvas)
969969
{
@@ -977,11 +977,8 @@ private void RemoveTextSelectionCanvas()
977977
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
978978
{
979979
base.OnDetachedFromVisualTree(e);
980-
if (TextSelectionHandleCanvas is { } c)
981-
{
982-
_layer?.Remove(c);
983-
c.SetPresenter(null);
984-
}
980+
981+
RemoveTextSelectionCanvas();
985982

986983
if (_caretTimer != null)
987984
{
@@ -1026,15 +1023,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
10261023
OnPreeditChanged(PreeditText, PreeditTextCursorPosition);
10271024
}
10281025

1029-
if(change.Property == TextProperty)
1030-
{
1031-
if (!string.IsNullOrEmpty(PreeditText))
1032-
{
1033-
SetCurrentValue(PreeditTextProperty, null);
1034-
}
1035-
}
1036-
1037-
if(change.Property == CaretIndexProperty)
1026+
if(change.Property == TextProperty || change.Property == CaretIndexProperty)
10381027
{
10391028
if (!string.IsNullOrEmpty(PreeditText))
10401029
{
@@ -1067,7 +1056,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
10671056
case nameof(SelectionStart):
10681057
case nameof(SelectionEnd):
10691058
case nameof(SelectionForegroundBrush):
1070-
case nameof(ShowSelectionHighlightProperty):
1059+
case nameof(ShowSelectionHighlight):
10711060

10721061
case nameof(PasswordChar):
10731062
case nameof(RevealPassword):

0 commit comments

Comments
 (0)