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 }