Browse Source
* add text selection handles * only show selectors on pen and touch input * show mobile themed context menu on mobile * cleanup and track textbox layout changes with EffectiveViewportChanged * make SelectionHandleType as clr property * fix custom flyouts not showing with selection handles * simplify text context menu for mobile. * cleanup * address review, simplify styles, add docs * remove unused resource * remove unused event handler, reformat selection canvas class * prevent thumb positions from swapping * adjust caret position when touch pointer moves * cleanup, disable scrolling in textbox when intent is selectionpull/13445/head
committed by
GitHub
16 changed files with 908 additions and 68 deletions
@ -0,0 +1,23 @@ |
|||||
|
namespace Avalonia.Controls.Primitives |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents which part of the selection the TextSelectionHandle controls.
|
||||
|
/// </summary>
|
||||
|
public enum SelectionHandleType |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The Handle controls the caret position.
|
||||
|
/// </summary>
|
||||
|
Caret, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The Handle controls the start of the text selection.
|
||||
|
/// </summary>
|
||||
|
Start, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The Handle controls the end of the text selection.
|
||||
|
/// </summary>
|
||||
|
End |
||||
|
} |
||||
|
} |
||||
@ -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<TextBox>(); |
||||
|
|
||||
|
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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,184 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Input; |
||||
|
using Avalonia.Interactivity; |
||||
|
using Avalonia.Media; |
||||
|
|
||||
|
namespace Avalonia.Controls.Primitives |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A controls that enables easy control over text selection using touch based input
|
||||
|
/// </summary>
|
||||
|
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<TextSelectionHandle>((x, e) => x.OnDragStarted(e), RoutingStrategies.Bubble); |
||||
|
DragDeltaEvent.AddClassHandler<TextSelectionHandle>((x, e) => x.OnDragDelta(e), RoutingStrategies.Bubble); |
||||
|
DragCompletedEvent.AddClassHandler<TextSelectionHandle>((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"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<VisualLayerManager>().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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
<ResourceDictionary xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
||||
|
<x:Double x:Key="TextSelectHandleSize">32</x:Double> |
||||
|
<GeometryGroup x:Key="TextSelectionHandlePath" FillRule="NonZero"> |
||||
|
<RectangleGeometry Rect="0,0,16,16"/> |
||||
|
<EllipseGeometry Center="16,16" |
||||
|
RadiusX="16" |
||||
|
RadiusY="16"/> |
||||
|
</GeometryGroup> |
||||
|
<GeometryGroup x:Key="TextCaretHandlePath" FillRule="NonZero"> |
||||
|
<RectangleGeometry Rect="0,0,16,16"> |
||||
|
<RectangleGeometry.Transform> |
||||
|
<TransformGroup > |
||||
|
<RotateTransform CenterX="16" |
||||
|
Angle="-45"/> |
||||
|
</TransformGroup> |
||||
|
</RectangleGeometry.Transform> |
||||
|
</RectangleGeometry> |
||||
|
<EllipseGeometry Center="16,22.7" |
||||
|
RadiusX="16" |
||||
|
RadiusY="16"/> |
||||
|
</GeometryGroup> |
||||
|
<ControlTheme x:Key="{x:Type TextSelectionHandle}" |
||||
|
TargetType="TextSelectionHandle"> |
||||
|
<Setter Property="Background" |
||||
|
Value="{DynamicResource TextControlSelectionHighlightColor}" /> |
||||
|
<Setter Property="HorizontalAlignment" |
||||
|
Value="Center" /> |
||||
|
<Setter Property="Template"> |
||||
|
<Setter.Value> |
||||
|
<ControlTemplate> |
||||
|
<Grid> |
||||
|
<PathIcon |
||||
|
Cursor="Arrow" |
||||
|
Name="PART_HandlePathIcon" |
||||
|
HorizontalAlignment="Stretch" |
||||
|
Height="{DynamicResource TextSelectHandleSize}" |
||||
|
Foreground="{TemplateBinding Background}" |
||||
|
Data="{DynamicResource TextSelectionHandlePath}" |
||||
|
/> |
||||
|
</Grid> |
||||
|
</ControlTemplate> |
||||
|
</Setter.Value> |
||||
|
</Setter> |
||||
|
<Style Selector="^.caret /template/ PathIcon#PART_HandlePathIcon"> |
||||
|
<Setter Property="Data" Value="{DynamicResource TextCaretHandlePath}" /> |
||||
|
</Style> |
||||
|
</ControlTheme> |
||||
|
</ResourceDictionary> |
||||
@ -0,0 +1,52 @@ |
|||||
|
<ResourceDictionary xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
||||
|
<x:Double x:Key="TextSelectHandleSize">32</x:Double> |
||||
|
<ControlTheme x:Key="{x:Type TextSelectionHandle}" |
||||
|
TargetType="TextSelectionHandle"> |
||||
|
<Setter Property="Background" |
||||
|
Value="{DynamicResource HighlightBrush}" /> |
||||
|
<Setter Property="HorizontalAlignment" |
||||
|
Value="Center" /> |
||||
|
<Setter Property="Template"> |
||||
|
<Setter.Value> |
||||
|
<ControlTemplate> |
||||
|
<Grid> |
||||
|
<PathIcon |
||||
|
Cursor="Arrow" |
||||
|
Name="PART_HandlePathIcon" |
||||
|
HorizontalAlignment="Stretch" |
||||
|
Height="{DynamicResource TextSelectHandleSize}" |
||||
|
Foreground="{TemplateBinding Background}" |
||||
|
> |
||||
|
<PathIcon.Data> |
||||
|
<GeometryGroup FillRule="NonZero"> |
||||
|
<RectangleGeometry Rect="0,0,16,16"/> |
||||
|
<EllipseGeometry Center="16,16" |
||||
|
RadiusX="16" |
||||
|
RadiusY="16"/> |
||||
|
</GeometryGroup> |
||||
|
</PathIcon.Data> |
||||
|
</PathIcon> |
||||
|
</Grid> |
||||
|
</ControlTemplate> |
||||
|
</Setter.Value> |
||||
|
</Setter> |
||||
|
<Style Selector="^.caret /template/ PathIcon#PART_HandlePathIcon"> |
||||
|
<Setter Property="Data"> |
||||
|
<GeometryGroup FillRule="NonZero"> |
||||
|
<RectangleGeometry Rect="0,0,16,16"> |
||||
|
<RectangleGeometry.Transform> |
||||
|
<TransformGroup > |
||||
|
<RotateTransform CenterX="16" |
||||
|
Angle="-45"/> |
||||
|
</TransformGroup> |
||||
|
</RectangleGeometry.Transform> |
||||
|
</RectangleGeometry> |
||||
|
<EllipseGeometry Center="16,22.7" |
||||
|
RadiusX="16" |
||||
|
RadiusY="16"/> |
||||
|
</GeometryGroup> |
||||
|
</Setter> |
||||
|
</Style> |
||||
|
</ControlTheme> |
||||
|
</ResourceDictionary> |
||||
Loading…
Reference in new issue