using Lattice.Core.DragDrop.Abstractions;
using Lattice.Core.DragDrop.Models;
using Lattice.Core.DragDrop.Services;
using Lattice.Core.Geometry;
using Lattice.UI.DragDrop.WinUI.Services;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Input;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Lattice.UI.DragDrop.WinUI.Behaviors;
///
/// Реализация поведения источника перетаскивания для элементов WinUI.
///
///
///
/// Этот класс обрабатывает события мыши/тач и преобразует их в операции перетаскивания.
/// Он реализует интерфейс для интеграции с системой перетаскивания.
///
///
/// Поведение автоматически отслеживает начало перемещения мыши, проверяет порог
/// перетаскивания и инициирует операцию через .
///
///
public sealed class WinUIDragSourceBehavior : IDragSource
{
#region Поля
private readonly IDragDropService _dragDropService;
private readonly WinUIDragDropHost _host;
private FrameworkElement? _element;
private object? _dragData;
private Point _dragStartPosition;
private bool _isDragging;
private CancellationTokenSource? _cancellationTokenSource;
#endregion
#region Конструктор
///
/// Инициализирует новый экземпляр класса .
///
/// Сервис для управления операциями перетаскивания.
/// Хост для отображения визуальных элементов.
///
/// Выбрасывается, если любой из параметров равен null.
///
public WinUIDragSourceBehavior(IDragDropService dragDropService, WinUIDragDropHost host)
{
_dragDropService = dragDropService ?? throw new ArgumentNullException(nameof(dragDropService));
_host = host ?? throw new ArgumentNullException(nameof(host));
}
#endregion
#region Публичные методы
///
/// Прикрепляет поведение к указанному элементу.
///
/// Элемент, к которому прикрепляется поведение.
/// Данные для перетаскивания. Если не указано, используются
/// DataContext или Tag элемента.
///
/// Выбрасывается, если равен null.
///
///
/// После вызова этого метода элемент начинает обрабатывать события перетаскивания.
///
public void Attach(FrameworkElement element, object? dragData = null)
{
Detach();
_element = element ?? throw new ArgumentNullException(nameof(element));
_dragData = dragData ?? element.DataContext ?? element.Tag;
SubscribeToEvents();
}
///
/// Открепляет поведение от элемента.
///
public void Detach()
{
if (_element == null) return;
UnsubscribeFromEvents();
if (_isDragging)
{
_dragDropService.CancelDragAsync().ConfigureAwait(false);
}
_element = null;
_dragData = null;
_isDragging = false;
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
}
#endregion
#region Обработчики событий
private void SubscribeToEvents()
{
if (_element == null) return;
_element.PointerPressed += OnPointerPressed;
_element.PointerMoved += OnPointerMoved;
_element.PointerReleased += OnPointerReleased;
_element.PointerCanceled += OnPointerCanceled;
_element.PointerCaptureLost += OnPointerCaptureLost;
}
private void UnsubscribeFromEvents()
{
if (_element == null) return;
_element.PointerPressed -= OnPointerPressed;
_element.PointerMoved -= OnPointerMoved;
_element.PointerReleased -= OnPointerReleased;
_element.PointerCanceled -= OnPointerCanceled;
_element.PointerCaptureLost -= OnPointerCaptureLost;
}
private void OnPointerPressed(object sender, PointerRoutedEventArgs e)
{
if (_element == null || _isDragging) return;
var point = e.GetCurrentPoint(_element);
_dragStartPosition = new Point(point.Position.X, point.Position.Y);
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = new CancellationTokenSource();
}
private async void OnPointerMoved(object sender, PointerRoutedEventArgs e)
{
if (_element == null || _isDragging || _cancellationTokenSource?.IsCancellationRequested == true)
return;
var point = e.GetCurrentPoint(_element);
var currentPosition = new Point(point.Position.X, point.Position.Y);
var distance = CalculateDistance(_dragStartPosition, currentPosition);
if (distance > _dragDropService.DragStartThreshold)
{
await StartDragAsync(currentPosition);
}
}
private void OnPointerReleased(object sender, PointerRoutedEventArgs e)
{
ResetState();
}
private void OnPointerCanceled(object sender, PointerRoutedEventArgs e)
{
ResetState();
}
private void OnPointerCaptureLost(object sender, PointerRoutedEventArgs e)
{
ResetState();
}
private double CalculateDistance(Point p1, Point p2)
{
var dx = p2.X - p1.X;
var dy = p2.Y - p1.Y;
return Math.Sqrt(dx * dx + dy * dy);
}
///
/// Преобразует координаты элемента в экранные координаты.
///
/// Точка в координатах элемента.
/// Точка в экранных координатах.
private Point ConvertToScreenCoordinates(Point point)
{
if (_element == null) return point;
try
{
var transform = _element.TransformToVisual(Window.Current.Content);
var screenPoint = transform.TransformPoint(new Windows.Foundation.Point(point.X, point.Y));
return new Point(screenPoint.X, screenPoint.Y);
}
catch
{
return point;
}
}
private async Task StartDragAsync(Point position)
{
if (_element == null || _dragData == null || _isDragging) return;
try
{
var screenPosition = ConvertToScreenCoordinates(position);
var started = await _dragDropService.StartDragAsync(this, screenPosition);
_isDragging = started;
}
catch
{
ResetState();
}
}
private void ResetState()
{
_isDragging = false;
_dragStartPosition = default;
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
}
#endregion
#region IDragSource Implementation
public async Task TryStartDragAsync(Point startPosition, CancellationToken cancellationToken = default)
{
if (_element == null || _dragData == null)
return null;
try
{
var dragInfo = new DragInfo(
data: _dragData,
allowedEffects: Core.DragDrop.Enums.DragDropEffects.Copy |
Core.DragDrop.Enums.DragDropEffects.Move,
startPosition: startPosition,
source: this
);
return dragInfo;
}
catch
{
return null;
}
}
public Task OnDragCompletedAsync(DragInfo dragInfo, Core.DragDrop.Enums.DragDropEffects effects, CancellationToken cancellationToken = default)
{
_isDragging = false;
return Task.CompletedTask;
}
public Task OnDragCancelledAsync(DragInfo dragInfo, CancellationToken cancellationToken = default)
{
_isDragging = false;
return Task.CompletedTask;
}
#endregion
}