diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs
index 2e1bfe100d..13e2a02fe5 100644
--- a/src/Avalonia.Controls/Control.cs
+++ b/src/Avalonia.Controls/Control.cs
@@ -212,14 +212,6 @@ namespace Avalonia.Controls
///
bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null;
- static Control()
- {
- Gestures.HoldingEvent.AddClassHandler((control, e) =>
- {
- control.OnHoldEvent(control, e);
- });
- }
-
///
void ISetterValue.Initialize(SetterBase setter)
{
@@ -382,6 +374,8 @@ namespace Avalonia.Controls
InitializeIfNeeded();
ScheduleOnLoadedCore();
+
+ Holding += OnHoldEvent;
}
private void OnHoldEvent(object? sender, HoldingRoutedEventArgs e)
@@ -399,6 +393,8 @@ namespace Avalonia.Controls
base.OnDetachedFromVisualTreeCore(e);
OnUnloadedCore();
+
+ Holding -= OnHoldEvent;
}
///
diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs
index a837f10eee..d7ecc2e90a 100644
--- a/src/Avalonia.Controls/Presenters/TextPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs
@@ -1,14 +1,15 @@
using System;
using System.Collections.Generic;
+using Avalonia.Controls.Documents;
+using Avalonia.Controls.Primitives;
+using Avalonia.Layout;
using Avalonia.Media;
+using Avalonia.Media.Immutable;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
using Avalonia.Threading;
using Avalonia.Utilities;
using Avalonia.VisualTree;
-using Avalonia.Layout;
-using Avalonia.Media.Immutable;
-using Avalonia.Controls.Documents;
namespace Avalonia.Controls.Presenters
{
@@ -94,6 +95,8 @@ namespace Avalonia.Controls.Presenters
private CharacterHit _lastCharacterHit;
private Rect _caretBounds;
private Point _navigationPosition;
+ private Point? _previousOffset;
+ private TextSelectorLayer? _layer;
static TextPresenter()
{
@@ -298,6 +301,8 @@ namespace Avalonia.Controls.Presenters
protected override bool BypassFlowDirectionPolicies => true;
+ internal TextSelectionHandleCanvas? TextSelectionHandleCanvas { get; set; }
+
///
/// Creates the used to render the text.
///
@@ -374,9 +379,19 @@ namespace Avalonia.Controls.Presenters
}
}
+ if(VisualRoot is Visual root)
+ {
+ var offset = this.TranslatePoint(Bounds.Position, root);
+
+ if(_previousOffset != offset)
+ {
+ _previousOffset = offset;
+ }
+ }
+
RenderInternal(context);
- if (selectionStart != selectionEnd || !_caretBlink)
+ if ((selectionStart != selectionEnd || !_caretBlink))
{
return;
}
@@ -406,7 +421,7 @@ namespace Avalonia.Controls.Presenters
context.DrawLine(new ImmutablePen(caretBrush), p1, p2);
}
- private (Point, Point) GetCaretPoints()
+ internal (Point, Point) GetCaretPoints()
{
var x = Math.Floor(_caretBounds.X) + 0.5;
var y = Math.Floor(_caretBounds.Y) + 0.5;
@@ -434,6 +449,10 @@ namespace Avalonia.Controls.Presenters
public void HideCaret()
{
_caretBlink = false;
+ if (TextSelectionHandleCanvas != null)
+ {
+ TextSelectionHandleCanvas.ShowHandles = false;
+ }
_caretTimer.Stop();
InvalidateVisual();
}
@@ -830,11 +849,25 @@ namespace Avalonia.Controls.Presenters
base.OnAttachedToVisualTree(e);
_caretTimer.Tick += CaretTimerTick;
+
+ if (TextSelectionHandleCanvas == null)
+ {
+ TextSelectionHandleCanvas = new TextSelectionHandleCanvas();
+ }
+
+ _layer = TextSelectorLayer.GetTextSelectorLayer(this);
+ _layer?.Add(TextSelectionHandleCanvas);
+ TextSelectionHandleCanvas.SetPresenter(this);
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
+ if (TextSelectionHandleCanvas is { } c)
+ {
+ _layer?.Remove(c);
+ c.SetPresenter(null);
+ }
_caretTimer.Stop();
diff --git a/src/Avalonia.Controls/Primitives/SelectionHandleType.cs b/src/Avalonia.Controls/Primitives/SelectionHandleType.cs
new file mode 100644
index 0000000000..2e1955de26
--- /dev/null
+++ b/src/Avalonia.Controls/Primitives/SelectionHandleType.cs
@@ -0,0 +1,23 @@
+namespace Avalonia.Controls.Primitives
+{
+ ///
+ /// Represents which part of the selection the TextSelectionHandle controls.
+ ///
+ public enum SelectionHandleType
+ {
+ ///
+ /// The Handle controls the caret position.
+ ///
+ Caret,
+
+ ///
+ /// The Handle controls the start of the text selection.
+ ///
+ Start,
+
+ ///
+ /// The Handle controls the end of the text selection.
+ ///
+ End
+ }
+}
diff --git a/src/Avalonia.Controls/Primitives/TextSelectionCanvas.cs b/src/Avalonia.Controls/Primitives/TextSelectionCanvas.cs
new file mode 100644
index 0000000000..c9a58aed0b
--- /dev/null
+++ b/src/Avalonia.Controls/Primitives/TextSelectionCanvas.cs
@@ -0,0 +1,387 @@
+using System;
+using System.Linq;
+using Avalonia.Controls.Presenters;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Layout;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls.Primitives
+{
+ internal class TextSelectionHandleCanvas : Canvas
+ {
+ private const int ContextMenuPadding = 16;
+
+ private readonly TextSelectionHandle _caretHandle;
+ private readonly TextSelectionHandle _startHandle;
+ private readonly TextSelectionHandle _endHandle;
+ private TextPresenter? _presenter;
+ private TextBox? _textBox;
+ private bool _showHandle;
+
+ internal bool ShowHandles
+ {
+ get => _showHandle;
+ set
+ {
+ _showHandle = value;
+
+ if (!value)
+ {
+ _startHandle.IsVisible = false;
+ _endHandle.IsVisible = false;
+ _caretHandle.IsVisible = false;
+ }
+
+ IsVisible = value;
+ }
+ }
+
+ public TextSelectionHandleCanvas()
+ {
+ _caretHandle = new TextSelectionHandle() { SelectionHandleType = SelectionHandleType.Caret };
+ _startHandle = new TextSelectionHandle();
+ _endHandle = new TextSelectionHandle();
+
+ Children.Add(_caretHandle);
+ Children.Add(_startHandle);
+ Children.Add(_endHandle);
+
+ _caretHandle.DragStarted += Handle_DragStarted;
+ _caretHandle.DragDelta += CaretHandle_DragDelta;
+ _caretHandle.DragCompleted += Handle_DragCompleted;
+ _startHandle.DragDelta += StartHandle_DragDelta;
+ _startHandle.DragCompleted += Handle_DragCompleted;
+ _startHandle.DragStarted += Handle_DragStarted;
+ _endHandle.DragDelta += EndHandle_DragDelta;
+ _endHandle.DragCompleted += Handle_DragCompleted;
+ _endHandle.DragStarted += Handle_DragStarted;
+
+ _caretHandle.Classes.Add("caret");
+ _startHandle.Classes.Add("start");
+ _endHandle.Classes.Add("end");
+
+ _startHandle.SetTopLeft(default);
+ _caretHandle.SetTopLeft(default);
+ _endHandle.SetTopLeft(default);
+
+ IsVisible = ShowHandles;
+
+ ClipToBounds = false;
+ }
+
+ private void Handle_DragStarted(object? sender, VectorEventArgs e)
+ {
+ if (_textBox?.ContextFlyout is { } flyout)
+ {
+ flyout.Hide();
+ }
+ }
+
+ private void EndHandle_DragDelta(object? sender, VectorEventArgs e)
+ {
+ if (sender is TextSelectionHandle handle)
+ DragSelectionHandle(handle);
+ }
+
+ private void StartHandle_DragDelta(object? sender, VectorEventArgs e)
+ {
+ if (sender is TextSelectionHandle handle)
+ DragSelectionHandle(handle);
+ }
+
+ private void CaretHandle_DragDelta(object? sender, VectorEventArgs e)
+ {
+ if (_presenter != null && _textBox != null)
+ {
+ var point = ToPresenter(_caretHandle.IndicatorPosition);
+ _presenter.MoveCaretToPoint(point);
+ _textBox.SelectionStart = _textBox.SelectionEnd = _presenter.CaretIndex;
+ var points = _presenter.GetCaretPoints();
+
+ _caretHandle?.SetTopLeft(ToLayer(points.Item2));
+ }
+ }
+
+ private void Handle_DragCompleted(object? sender, VectorEventArgs e)
+ {
+ MoveHandlesToSelection();
+
+ ShowContextMenu();
+ }
+
+ private void EnsureVisible()
+ {
+ if (_textBox is { } t && t.VisualRoot is Visual r)
+ {
+ var bounds = t.Bounds;
+ var topLeft = t.TranslatePoint(default, r) ?? default;
+ bounds = bounds.WithX(topLeft.X).WithY(topLeft.Y);
+
+ var hasSelection = _textBox.SelectionStart != _textBox.SelectionEnd;
+
+ _startHandle.IsVisible = bounds.Contains(new Point(GetLeft(_startHandle), GetTop(_startHandle))) &&
+ ShowHandles && hasSelection;
+ _endHandle.IsVisible = bounds.Contains(new Point(GetLeft(_endHandle), GetTop(_endHandle))) &&
+ ShowHandles && hasSelection;
+ _caretHandle.IsVisible = bounds.Contains(new Point(GetLeft(_caretHandle), GetTop(_caretHandle))) &&
+ ShowHandles && !hasSelection;
+ }
+ }
+
+ private void DragSelectionHandle(TextSelectionHandle handle)
+ {
+ if (_presenter != null && _textBox != null)
+ {
+ if (_textBox.ContextFlyout is { } flyout)
+ {
+ flyout.Hide();
+ }
+
+ var point = ToPresenter(handle.IndicatorPosition);
+ point = point.WithY(point.Y - _presenter.FontSize / 2);
+ var hit = _presenter.TextLayout.HitTestPoint(point);
+ var position = hit.CharacterHit.FirstCharacterIndex + hit.CharacterHit.TrailingLength;
+
+ var otherHandle = handle == _startHandle ? _endHandle : _startHandle;
+
+ if (handle.SelectionHandleType == SelectionHandleType.Start)
+ {
+ if (position >= _textBox.SelectionEnd)
+ position = _textBox.SelectionEnd - 1;
+ _textBox.SelectionStart = position;
+ }
+ else
+ {
+ if (position <= _textBox.SelectionStart)
+ position = _textBox.SelectionStart + 1;
+ _textBox.SelectionEnd = position;
+ }
+
+ var selectionStart = _textBox.SelectionStart;
+ var selectionEnd = _textBox.SelectionEnd;
+ var start = Math.Min(selectionStart, selectionEnd);
+ var length = Math.Max(selectionStart, selectionEnd) - start;
+
+ var rects = _presenter.TextLayout.HitTestTextRange(start, length);
+
+ if (rects.Any())
+ {
+ var first = rects.First();
+ var last = rects.Last();
+
+ if (handle.SelectionHandleType == SelectionHandleType.Start)
+ handle?.SetTopLeft(ToLayer(first.BottomLeft));
+ else
+ handle?.SetTopLeft(ToLayer(last.BottomRight));
+
+ if (otherHandle.SelectionHandleType == SelectionHandleType.Start)
+ otherHandle?.SetTopLeft(ToLayer(first.BottomLeft));
+ else
+ otherHandle?.SetTopLeft(ToLayer(last.BottomRight));
+ }
+
+ _presenter?.MoveCaretToTextPosition(position);
+ }
+
+ EnsureVisible();
+ }
+
+ private Point ToLayer(Point point)
+ {
+ return (_presenter?.VisualRoot is Visual v) ? _presenter?.TranslatePoint(point, v) ?? point : point;
+ }
+
+ private Point ToPresenter(Point point)
+ {
+ return (_presenter is { } p) ? (p.VisualRoot as Visual)?.TranslatePoint(point, p) ?? point : point;
+ }
+
+ private Point ToTextBox(Point point)
+ {
+ return (_textBox is { } p) ? (p.VisualRoot as Visual)?.TranslatePoint(point, p) ?? point : point;
+ }
+
+ public void MoveHandlesToSelection()
+ {
+ if (_presenter == null || _textBox == null || _startHandle.IsDragging || _endHandle.IsDragging)
+ {
+ return;
+ }
+
+ var hasSelection = _textBox.SelectionStart != _textBox.SelectionEnd;
+
+ var points = _presenter.GetCaretPoints();
+
+ _caretHandle.SetTopLeft(ToLayer(points.Item2));
+
+ if (hasSelection)
+ {
+ var selectionStart = _textBox.SelectionStart;
+ var selectionEnd = _textBox.SelectionEnd;
+ var start = Math.Min(selectionStart, selectionEnd);
+ var length = Math.Max(selectionStart, selectionEnd) - start;
+
+ var rects = _presenter.TextLayout.HitTestTextRange(start, length);
+
+ if (rects.Any())
+ {
+ var first = rects.First();
+ var last = rects.Last();
+
+ if (!_startHandle.IsDragging)
+ {
+ _startHandle.SetTopLeft(ToLayer(first.BottomLeft));
+ _startHandle.SelectionHandleType = selectionStart < selectionEnd ?
+ SelectionHandleType.Start :
+ SelectionHandleType.End;
+ }
+
+ if (!_endHandle.IsDragging)
+ {
+ _endHandle.SetTopLeft(ToLayer(last.BottomRight));
+ _endHandle.SelectionHandleType = selectionStart > selectionEnd ?
+ SelectionHandleType.Start :
+ SelectionHandleType.End;
+ }
+ }
+ }
+ }
+
+ internal void SetPresenter(TextPresenter? textPresenter)
+ {
+ _presenter = textPresenter;
+ if (_presenter != null)
+ {
+ _textBox = _presenter.FindAncestorOfType();
+
+ if (_textBox != null)
+ {
+ _textBox.AddHandler(TextBox.TextChangingEvent, TextChanged, handledEventsToo: true);
+ _textBox.AddHandler(KeyDownEvent, TextBoxKeyDown, handledEventsToo: true);
+ _textBox.AddHandler(LostFocusEvent, TextBoxLostFocus, handledEventsToo: true);
+ _textBox.AddHandler(PointerReleasedEvent, TextBoxPointerReleased, handledEventsToo: true);
+ _textBox.AddHandler(Gestures.HoldingEvent, TextBoxHolding, handledEventsToo: true);
+
+ _textBox.PropertyChanged += TextBoxPropertyChanged;
+ _textBox.EffectiveViewportChanged += TextBoxEffectiveViewportChanged;
+ }
+ }
+ else
+ {
+ if (_textBox != null)
+ {
+ _textBox.RemoveHandler(TextBox.TextChangingEvent, TextChanged);
+ _textBox.RemoveHandler(KeyDownEvent, TextBoxKeyDown);
+ _textBox.RemoveHandler(PointerReleasedEvent, TextBoxPointerReleased);
+ _textBox.RemoveHandler(LostFocusEvent, TextBoxLostFocus);
+ _textBox.RemoveHandler(Gestures.HoldingEvent, TextBoxHolding);
+
+ _textBox.PropertyChanged -= TextBoxPropertyChanged;
+ _textBox.EffectiveViewportChanged -= TextBoxEffectiveViewportChanged;
+ }
+
+ _textBox = null;
+ }
+ }
+
+ private void TextBoxEffectiveViewportChanged(object? sender, EffectiveViewportChangedEventArgs e)
+ {
+ if (ShowHandles)
+ {
+ MoveHandlesToSelection();
+ EnsureVisible();
+ }
+ }
+
+ private void TextBoxHolding(object? sender, HoldingRoutedEventArgs e)
+ {
+ if (ShowContextMenu())
+ e.Handled = true;
+ }
+
+ internal bool ShowContextMenu()
+ {
+ if (_textBox != null)
+ {
+ if (_textBox.ContextFlyout is PopupFlyoutBase flyout)
+ {
+ var verticalOffset = (double.IsNaN(_textBox.LineHeight) ? _textBox.FontSize : _textBox.LineHeight) +
+ ContextMenuPadding;
+
+ TextSelectionHandle? handle = null;
+
+ if (_textBox.SelectionStart != _textBox.SelectionEnd)
+ {
+ if (_startHandle.IsEffectivelyVisible)
+ handle = _startHandle;
+ else if (_endHandle.IsEffectivelyVisible)
+ handle = _endHandle;
+ }
+ else
+ {
+ if (_caretHandle.IsEffectivelyVisible)
+ {
+ handle = _caretHandle;
+ }
+ }
+
+ if (handle != null)
+ {
+ var topLeft = ToTextBox(handle.GetTopLeft());
+ flyout.VerticalOffset = topLeft.Y - verticalOffset;
+ flyout.HorizontalOffset = topLeft.X;
+ flyout.Placement = PlacementMode.TopEdgeAlignedLeft;
+ _textBox.RaiseEvent(new ContextRequestedEventArgs());
+
+ return true;
+ }
+ }
+ else
+ {
+ _textBox.RaiseEvent(new ContextRequestedEventArgs());
+ }
+ }
+
+ return false;
+ }
+
+ private void TextBoxPointerReleased(object? sender, PointerReleasedEventArgs e)
+ {
+ if (e.Pointer.Type != PointerType.Mouse)
+ {
+ ShowHandles = true;
+
+ MoveHandlesToSelection();
+ EnsureVisible();
+ }
+ }
+
+ private void TextBoxPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (ShowHandles && (e.Property == TextBox.SelectionStartProperty ||
+ e.Property == TextBox.SelectionEndProperty))
+ {
+ MoveHandlesToSelection();
+ EnsureVisible();
+ }
+ }
+
+ private void TextBoxLostFocus(object? sender, RoutedEventArgs e)
+ {
+ ShowHandles = false;
+ }
+
+ private void TextBoxKeyDown(object? sender, KeyEventArgs e)
+ {
+ ShowHandles = false;
+ }
+
+ private void TextChanged(object? sender, TextChangingEventArgs e)
+ {
+ ShowHandles = false;
+ if (_textBox?.ContextFlyout is { } flyout && flyout.IsOpen)
+ flyout.Hide();
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Primitives/TextSelectionHandle.cs b/src/Avalonia.Controls/Primitives/TextSelectionHandle.cs
new file mode 100644
index 0000000000..d9bf8b5529
--- /dev/null
+++ b/src/Avalonia.Controls/Primitives/TextSelectionHandle.cs
@@ -0,0 +1,184 @@
+using System;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+
+namespace Avalonia.Controls.Primitives
+{
+ ///
+ /// A controls that enables easy control over text selection using touch based input
+ ///
+ public class TextSelectionHandle : Thumb
+ {
+ internal SelectionHandleType SelectionHandleType { get; set; }
+
+ private Point _startPosition;
+
+ internal Point IndicatorPosition => IsDragging ? _startPosition.WithX(_startPosition.X) + _delta : Bounds.Position.WithX(Bounds.Position.X).WithY(Bounds.Y);
+
+ internal bool IsDragging { get; private set; }
+
+ private Vector _delta;
+ private Point? _lastPoint;
+ private TranslateTransform? _transform;
+
+ static TextSelectionHandle()
+ {
+ DragStartedEvent.AddClassHandler((x, e) => x.OnDragStarted(e), RoutingStrategies.Bubble);
+ DragDeltaEvent.AddClassHandler((x, e) => x.OnDragDelta(e), RoutingStrategies.Bubble);
+ DragCompletedEvent.AddClassHandler((x, e) => x.OnDragCompleted(e), RoutingStrategies.Bubble);
+ }
+
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnAttachedToVisualTree(e);
+
+ if (_transform == null)
+ {
+ _transform = new TranslateTransform();
+ }
+
+ RenderTransform = _transform;
+ }
+
+ protected override void OnLoaded(RoutedEventArgs args)
+ {
+ base.OnLoaded(args);
+
+ InvalidateMeasure();
+ }
+ protected override void OnDragStarted(VectorEventArgs e)
+ {
+ base.OnDragStarted(e);
+
+ _startPosition = Bounds.Position;
+ _delta = default;
+ IsDragging = true;
+ }
+
+ protected override void OnDragDelta(VectorEventArgs e)
+ {
+ base.OnDragDelta(e);
+
+ _delta = e.Vector;
+ UpdateTextSelectionHandlePosition();
+ }
+
+ protected override void OnDragCompleted(VectorEventArgs e)
+ {
+ base.OnDragCompleted(e);
+ IsDragging = false;
+
+ _startPosition = default;
+ }
+
+ private void UpdateTextSelectionHandlePosition()
+ {
+ SetTopLeft(IndicatorPosition);
+ }
+
+ protected override void ArrangeCore(Rect finalRect)
+ {
+ base.ArrangeCore(finalRect);
+
+ if (_transform != null)
+ {
+ if (SelectionHandleType == SelectionHandleType.Caret)
+ {
+ HasMirrorTransform = true;
+ _transform.X = Bounds.Width / 2 * -1;
+ }
+ else if (SelectionHandleType == SelectionHandleType.Start)
+ {
+ HasMirrorTransform = true;
+ _transform.X = Bounds.Width * -1;
+ }
+ else
+ {
+ HasMirrorTransform = false;
+ _transform.X = 0;
+ }
+ }
+ }
+
+ internal void SetTopLeft(Point point)
+ {
+ Canvas.SetTop(this, point.Y);
+ Canvas.SetLeft(this, point.X);
+ }
+
+ internal Point GetTopLeft()
+ {
+ return new Point(Canvas.GetLeft(this), Canvas.GetTop(this));
+ }
+
+ protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
+ {
+ if (_lastPoint.HasValue)
+ {
+ var ev = new VectorEventArgs
+ {
+ RoutedEvent = DragCompletedEvent,
+ Vector = _lastPoint.Value,
+ };
+
+ _lastPoint = null;
+
+ RaiseEvent(ev);
+ }
+
+ PseudoClasses.Remove(":pressed");
+
+ base.OnPointerCaptureLost(e);
+ }
+
+ protected override void OnPointerMoved(PointerEventArgs e)
+ {
+ if (_lastPoint.HasValue)
+ {
+ var ev = new VectorEventArgs
+ {
+ RoutedEvent = DragDeltaEvent,
+ Vector = e.GetPosition(VisualRoot as Visual) - _lastPoint.Value,
+ };
+
+ RaiseEvent(ev);
+ }
+ }
+
+ protected override void OnPointerPressed(PointerPressedEventArgs e)
+ {
+ e.Handled = true;
+ _lastPoint = e.GetPosition(VisualRoot as Visual);
+
+ var ev = new VectorEventArgs
+ {
+ RoutedEvent = DragStartedEvent,
+ Vector = (Vector)_lastPoint,
+ };
+
+ PseudoClasses.Add(":pressed");
+
+ RaiseEvent(ev);
+ }
+
+ protected override void OnPointerReleased(PointerReleasedEventArgs e)
+ {
+ if (_lastPoint.HasValue)
+ {
+ e.Handled = true;
+ _lastPoint = null;
+
+ var ev = new VectorEventArgs
+ {
+ RoutedEvent = DragCompletedEvent,
+ Vector = (Vector)e.GetPosition(VisualRoot as Visual),
+ };
+
+ RaiseEvent(ev);
+ }
+
+ PseudoClasses.Remove(":pressed");
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Primitives/TextSelectorLayer.cs b/src/Avalonia.Controls/Primitives/TextSelectorLayer.cs
new file mode 100644
index 0000000000..07eef005c3
--- /dev/null
+++ b/src/Avalonia.Controls/Primitives/TextSelectorLayer.cs
@@ -0,0 +1,49 @@
+using System.Linq;
+using Avalonia.Rendering;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls.Primitives
+{
+ public class TextSelectorLayer : Canvas
+ {
+ protected override bool BypassFlowDirectionPolicies => true;
+ public Size AvailableSize { get; private set; }
+ public static TextSelectorLayer? GetTextSelectorLayer(Visual visual)
+ {
+ foreach (var v in visual.GetVisualAncestors())
+ if (v is VisualLayerManager vlm)
+ if (vlm.TextSelectorLayer != null)
+ return vlm.TextSelectorLayer;
+ if (visual is TopLevel tl)
+ {
+ var layers = tl.GetVisualDescendants().OfType().FirstOrDefault();
+ return layers?.TextSelectorLayer;
+ }
+
+ return null;
+ }
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ foreach (Control child in Children)
+ child.Measure(availableSize);
+ return availableSize;
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ AvailableSize = finalSize;
+ return base.ArrangeOverride(finalSize);
+ }
+
+ public void Add(Control control)
+ {
+ Children.Add(control);
+ }
+
+ public void Remove(Control control)
+ {
+ Children.Remove(control);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs
index bc47462b56..d188a32940 100644
--- a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs
+++ b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs
@@ -9,6 +9,7 @@ namespace Avalonia.Controls.Primitives
private const int ChromeZIndex = int.MaxValue - 99;
private const int LightDismissOverlayZIndex = int.MaxValue - 98;
private const int OverlayZIndex = int.MaxValue - 97;
+ private const int TextSelectorLayerZIndex = int.MaxValue - 96;
private ILogicalRoot? _logicalRoot;
private readonly List _layers = new();
@@ -65,6 +66,19 @@ namespace Avalonia.Controls.Primitives
}
}
+ public TextSelectorLayer? TextSelectorLayer
+ {
+ get
+ {
+ if (IsPopup)
+ return null;
+ var rv = FindLayer();
+ if (rv == null)
+ AddLayer(rv = new TextSelectorLayer(), TextSelectorLayerZIndex);
+ return rv;
+ }
+ }
+
public LightDismissOverlayLayer LightDismissOverlayLayer
{
get
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index 5d2b7bcb53..b8a141eb5b 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/src/Avalonia.Controls/TextBox.cs
@@ -316,13 +316,11 @@ namespace Avalonia.Controls
private bool _canRedo;
private int _wordSelectionStart = -1;
- private bool _touchDragStarted;
- private bool _inTouchDrag;
- private Point _touchDragStartPoint;
private int _selectedTextChangesMadeSinceLastUndoSnapshot;
private bool _hasDoneSnapshotOnce;
private static bool _isHolding;
private int _currentClickCount;
+ private bool _isDoubleTapped;
private const int _maxCharsBeforeUndoSnapshot = 7;
static TextBox()
@@ -803,8 +801,18 @@ namespace Avalonia.Controls
{
_presenter = e.NameScope.Get("PART_TextPresenter");
+ if (_scrollViewer != null)
+ {
+ _scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged;
+ }
+
_scrollViewer = e.NameScope.Find("PART_ScrollViewer");
+ if(_scrollViewer != null)
+ {
+ _scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged;
+ }
+
_imClient.SetPresenter(_presenter, this);
if (IsFocused)
@@ -813,6 +821,11 @@ namespace Avalonia.Controls
}
}
+ private void ScrollViewer_ScrollChanged(object? sender, ScrollChangedEventArgs e)
+ {
+ _presenter?.TextSelectionHandleCanvas?.MoveHandlesToSelection();
+ }
+
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
@@ -826,16 +839,6 @@ namespace Avalonia.Controls
_presenter.PropertyChanged += PresenterPropertyChanged;
}
-
- AddHandler(Gestures.HoldingEvent, OnHolding);
- }
-
- private void OnHolding(object? sender, HoldingRoutedEventArgs e)
- {
- if (e.Handled || e.HoldingState != HoldingState.Started)
- return;
-
- _isHolding = true;
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
@@ -850,8 +853,6 @@ namespace Avalonia.Controls
}
_imClient.SetPresenter(null, null);
-
- RemoveHandler(Gestures.HoldingEvent, OnHolding);
}
private void PresenterPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
@@ -1470,24 +1471,12 @@ namespace Avalonia.Controls
var text = Text;
var clickInfo = e.GetCurrentPoint(this);
- if (text != null && clickInfo.Properties.IsLeftButtonPressed &&
+ if (text != null && (e.Pointer.Type == PointerType.Mouse || e.ClickCount >= 2) && clickInfo.Properties.IsLeftButtonPressed &&
!(clickInfo.Pointer?.Captured is Border))
{
- var isTouch = e.Pointer.Type == PointerType.Touch;
-
_currentClickCount = e.ClickCount;
var point = e.GetPosition(_presenter);
- if(isTouch && e.ClickCount == 1)
- {
- _wordSelectionStart = -1;
- _touchDragStarted = true;
- _touchDragStartPoint = point;
- e.Pointer.Capture(_presenter);
- e.Handled = true;
- return;
- }
-
_presenter.MoveCaretToPoint(point);
var caretIndex = _presenter.CaretIndex;
@@ -1548,6 +1537,7 @@ namespace Avalonia.Controls
}
}
+ _isDoubleTapped = e.ClickCount == 2;
e.Pointer.Capture(_presenter);
e.Handled = true;
}
@@ -1568,32 +1558,35 @@ namespace Avalonia.Controls
MathUtilities.Clamp(point.X, 0, Math.Max(_presenter.Bounds.Width - 1, 0)),
MathUtilities.Clamp(point.Y, 0, Math.Max(_presenter.Bounds.Height - 1, 0)));
- if(_touchDragStarted)
- {
- _touchDragStarted = false;
- _inTouchDrag = true;
-
- _presenter.MoveCaretToPoint(_touchDragStartPoint);
- _touchDragStartPoint = default;
- SetCurrentValue(SelectionStartProperty, _presenter.CaretIndex);
- }
+ var previousIndex = _presenter.CaretIndex;
_presenter.MoveCaretToPoint(point);
var caretIndex = _presenter.CaretIndex;
- var selectionStart = SelectionStart;
- var selectionEnd = SelectionEnd;
+ if (Math.Abs(caretIndex - previousIndex) == 1)
+ e.PreventGestureRecognition();
- if (_wordSelectionStart >= 0)
+ if (e.Pointer.Type == PointerType.Mouse)
{
- UpdateWordSelectionRange(caretIndex, ref selectionStart, ref selectionEnd);
+ var selectionStart = SelectionStart;
+ var selectionEnd = SelectionEnd;
- SetCurrentValue(SelectionStartProperty, selectionStart);
- SetCurrentValue(SelectionEndProperty, selectionEnd);
+ if (_wordSelectionStart >= 0)
+ {
+ UpdateWordSelectionRange(caretIndex, ref selectionStart, ref selectionEnd);
+
+ SetCurrentValue(SelectionStartProperty, selectionStart);
+ SetCurrentValue(SelectionEndProperty, selectionEnd);
+ }
+ else
+ {
+ SetCurrentValue(SelectionEndProperty, caretIndex);
+ }
}
else
{
+ SetCurrentValue(SelectionStartProperty, caretIndex);
SetCurrentValue(SelectionEndProperty, caretIndex);
}
}
@@ -1632,17 +1625,51 @@ namespace Avalonia.Controls
return;
}
- _touchDragStarted = false;
- _touchDragStartPoint = default;
-
- var isInTouchDrag = _inTouchDrag;
- _inTouchDrag = false;
-
if (e.Pointer.Captured != _presenter)
{
return;
}
+ if (e.Pointer.Type != PointerType.Mouse && !_isDoubleTapped)
+ {
+ var text = Text;
+ var clickInfo = e.GetCurrentPoint(this);
+ if (text != null && !(clickInfo.Pointer?.Captured is Border))
+ {
+ var point = e.GetPosition(_presenter);
+
+ _presenter.MoveCaretToPoint(point);
+
+ var caretIndex = _presenter.CaretIndex;
+ var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
+ var selectionStart = SelectionStart;
+ var selectionEnd = SelectionEnd;
+
+ if (clickToSelect)
+ {
+ if (_wordSelectionStart >= 0)
+ {
+ UpdateWordSelectionRange(caretIndex, ref selectionStart, ref selectionEnd);
+
+ SetCurrentValue(SelectionStartProperty, selectionStart);
+ SetCurrentValue(SelectionEndProperty, selectionEnd);
+ }
+ else
+ {
+ SetCurrentValue(SelectionEndProperty, caretIndex);
+ }
+ }
+ else
+ {
+ SetCurrentValue(SelectionStartProperty, caretIndex);
+ SetCurrentValue(SelectionEndProperty, caretIndex);
+ _wordSelectionStart = -1;
+ }
+
+ _presenter.TextSelectionHandleCanvas?.MoveHandlesToSelection();
+ }
+ }
+
// Don't update selection if the pointer was held
if (_isHolding)
{
@@ -1671,7 +1698,7 @@ namespace Avalonia.Controls
}
else if (e.Pointer.Type == PointerType.Touch)
{
- if (_currentClickCount == 1 && !isInTouchDrag)
+ if (_currentClickCount == 1)
{
var point = e.GetPosition(_presenter);
@@ -1684,7 +1711,7 @@ namespace Avalonia.Controls
if(SelectionStart != SelectionEnd)
{
- RaiseEvent(new ContextRequestedEventArgs(e));
+ _presenter.TextSelectionHandleCanvas?.ShowContextMenu();
}
}
diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml
index 73e8595b11..c906827e50 100644
--- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml
+++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml
@@ -207,6 +207,10 @@
1
+
+ 32
+ 2,0,2,0
+ 8,4,8,7
@@ -996,6 +1000,10 @@
1
+
+ 32
+ 2,0,2,0
+ 8,4,8,7
diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
index bcb5efe80a..34aecd03e0 100644
--- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
@@ -73,6 +73,7 @@
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml b/src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml
index b5dff7b59d..c12d4fcc4e 100644
--- a/src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml
@@ -32,4 +32,12 @@
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
index 8a2d76b940..3cf6261683 100644
--- a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
@@ -146,7 +146,6 @@
-
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
index 2c05ad6f64..e7704ee4e5 100644
--- a/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
@@ -20,12 +20,17 @@
M 11.416016,10 20,1.4160156 18.583984,0 10,8.5839846 1.4160156,0 0,1.4160156 8.5839844,10 0,18.583985 1.4160156,20 10,11.416015 18.583984,20 20,18.583985 Z
m10.051 7.0032c2.215 0 4.0105 1.7901 4.0105 3.9984s-1.7956 3.9984-4.0105 3.9984c-2.215 0-4.0105-1.7901-4.0105-3.9984s1.7956-3.9984 4.0105-3.9984zm0 1.4994c-1.3844 0-2.5066 1.1188-2.5066 2.499s1.1222 2.499 2.5066 2.499 2.5066-1.1188 2.5066-2.499-1.1222-2.499-2.5066-2.499zm0-5.0026c4.6257 0 8.6188 3.1487 9.7267 7.5613 0.10085 0.40165-0.14399 0.80877-0.54686 0.90931-0.40288 0.10054-0.81122-0.14355-0.91208-0.54521-0.94136-3.7492-4.3361-6.4261-8.2678-6.4261-3.9334 0-7.3292 2.6792-8.2689 6.4306-0.10063 0.40171-0.50884 0.64603-0.91177 0.54571s-0.648-0.5073-0.54737-0.90901c1.106-4.4152 5.1003-7.5667 9.728-7.5667z
m0.21967 0.21965c-0.26627 0.26627-0.29047 0.68293-0.07262 0.97654l0.07262 0.08412 4.0346 4.0346c-1.922 1.3495-3.3585 3.365-3.9554 5.7495-0.10058 0.4018 0.14362 0.8091 0.54543 0.9097 0.40182 0.1005 0.80909-0.1436 0.90968-0.5455 0.52947-2.1151 1.8371-3.8891 3.5802-5.0341l1.8096 1.8098c-0.70751 0.7215-1.1438 1.71-1.1438 2.8003 0 2.2092 1.7909 4 4 4 1.0904 0 2.0788-0.4363 2.8004-1.1438l5.9193 5.9195c0.2929 0.2929 0.7677 0.2929 1.0606 0 0.2663-0.2662 0.2905-0.6829 0.0726-0.9765l-0.0726-0.0841-6.1135-6.1142 0.0012-0.0015-1.2001-1.1979-2.8699-2.8693 2e-3 -8e-4 -2.8812-2.8782 0.0012-0.0018-1.1333-1.1305-4.3064-4.3058c-0.29289-0.29289-0.76777-0.29289-1.0607 0zm7.9844 9.0458 3.5351 3.5351c-0.45 0.4358-1.0633 0.704-1.7392 0.704-1.3807 0-2.5-1.1193-2.5-2.5 0-0.6759 0.26824-1.2892 0.7041-1.7391zm1.7959-5.7655c-1.0003 0-1.9709 0.14807-2.8889 0.425l1.237 1.2362c0.5358-0.10587 1.0883-0.16119 1.6519-0.16119 3.9231 0 7.3099 2.6803 8.2471 6.4332 0.1004 0.4018 0.5075 0.6462 0.9094 0.5459 0.4019-0.1004 0.6463-0.5075 0.5459-0.9094-1.103-4.417-5.0869-7.5697-9.7024-7.5697zm0.1947 3.5093 3.8013 3.8007c-0.1018-2.0569-1.7488-3.7024-3.8013-3.8007z
-
-
+
+
+
+
+
+
+
@@ -102,7 +107,7 @@
-
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/TextSelectionHandle.xaml b/src/Avalonia.Themes.Fluent/Controls/TextSelectionHandle.xaml
new file mode 100644
index 0000000000..84bc104036
--- /dev/null
+++ b/src/Avalonia.Themes.Fluent/Controls/TextSelectionHandle.xaml
@@ -0,0 +1,49 @@
+
+ 32
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml
index c5fc3f3cce..51a21720b9 100644
--- a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml
+++ b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml
@@ -71,6 +71,7 @@
+
diff --git a/src/Avalonia.Themes.Simple/Controls/TextSelectionHandle.xaml b/src/Avalonia.Themes.Simple/Controls/TextSelectionHandle.xaml
new file mode 100644
index 0000000000..c9ca5ebc63
--- /dev/null
+++ b/src/Avalonia.Themes.Simple/Controls/TextSelectionHandle.xaml
@@ -0,0 +1,52 @@
+
+ 32
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+