using Lattice.Core.Geometry;
using Lattice.UI.Docking.Abstractions;
using Lattice.UI.Docking.Models;
using Lattice.UI.Docking.Services;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Media;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
namespace Lattice.UI.Docking.WinUI.Services;
///
/// Предоставляет реализацию сервиса перетаскивания для платформы WinUI с расширенной
/// поддержкой визуальных эффектов и интеграцией с системой докинга Lattice.
/// Координирует взаимодействие между базовым менеджером перетаскивания и UI-контролами,
/// обеспечивая богатую визуальную обратную связь во время операций drag-and-drop.
///
///
///
/// расширяет базовый функционал
/// платформенно-зависимыми визуальными эффектами, включая:
///
///
/// - Прозрачное визуальное представление перетаскиваемого элемента
/// - Интерактивные подсказки областей сброса
/// - Анимации при начале и завершении перетаскивания
/// - Подсветку допустимых целей сброса
///
///
/// Сервис поддерживает регистрацию UI-элементов и автоматически вычисляет их границы
/// для точного определения целей сброса.
///
///
public sealed class WinUIDragDropService : DockDragDropService, IDisposable
{
private readonly ConcurrentDictionary _controlToElement = new();
private readonly DragDropManagerEx _dragDropManager;
private Popup? _dragVisualPopup;
private Border? _dragVisual;
private DropHintOverlay? _dropHintOverlay;
private bool _disposed;
///
/// Инициализирует новый экземпляр сервиса перетаскивания WinUI.
///
///
/// Создает внутренний менеджер перетаскивания, инициализирует визуальные элементы
/// и подписывается на события менеджера для обработки операций перетаскивания.
///
public WinUIDragDropService()
{
_dragDropManager = new DragDropManagerEx();
HookEvents();
InitializeDragVisual();
InitializeDropHintOverlay();
}
///
/// Инициализирует новый экземпляр с указанным менеджером перетаскивания.
///
///
/// Предварительно настроенный менеджер перетаскивания.
///
///
/// Выбрасывается, если равен null.
///
///
/// Позволяет использовать кастомную конфигурацию менеджера перетаскивания
/// при сохранении всех визуальных эффектов WinUI.
///
public WinUIDragDropService(DragDropManagerEx dragDropManager)
{
_dragDropManager = dragDropManager ?? throw new ArgumentNullException(nameof(dragDropManager));
HookEvents();
InitializeDragVisual();
InitializeDropHintOverlay();
}
///
/// Подписывается на события менеджера перетаскивания.
///
///
/// Обрабатывает следующие события:
///
/// - Начало перетаскивания
/// - Обновление позиции перетаскивания
/// - Завершение перетаскивания
/// - Отмена перетаскивания
/// - Изменение цели сброса
///
///
private void HookEvents()
{
_dragDropManager.DragStarted += OnDragStarted;
_dragDropManager.DragUpdated += OnDragUpdated;
_dragDropManager.DragCompleted += OnDragCompleted;
_dragDropManager.DragCancelled += OnDragCancelled;
_dragDropManager.DropTargetChanged += OnDropTargetChanged;
}
///
/// Инициализирует визуальное представление перетаскиваемого элемента.
///
///
/// Создает Popup с Border для отображения полупрозрачной копии
/// перетаскиваемого элемента во время операции drag-and-drop.
///
private void InitializeDragVisual()
{
// Создаем Popup для отображения визуального представления перетаскивания
_dragVisualPopup = new Popup
{
IsHitTestVisible = false,
IsLightDismissEnabled = false,
Child = null
};
// Создаем визуальный элемент для перетаскивания
_dragVisual = new Border
{
Background = new SolidColorBrush(Microsoft.UI.Colors.Transparent),
BorderBrush = new SolidColorBrush(Microsoft.UI.Colors.DodgerBlue),
BorderThickness = new Thickness(2),
CornerRadius = new CornerRadius(4),
Opacity = 0.7
};
}
///
/// Инициализирует оверлей для отображения подсказок при сбросе.
///
///
/// Добавляет оверлей в корневой контейнер приложения для отображения
/// визуальных подсказок о возможных позициях сброса.
///
private void InitializeDropHintOverlay()
{
// Создаем оверлей для подсказок при сбросе
_dropHintOverlay = new DropHintOverlay();
// Добавляем оверлей в корневой контейнер приложения
if (Window.Current?.Content is Panel rootPanel)
{
rootPanel.Children.Add(_dropHintOverlay);
}
}
///
/// Регистрирует связь между абстрактным контролом док-системы и конкретным UI-элементом WinUI.
///
/// Абстрактный контрол док-системы.
/// Конкретный UI-элемент WinUI.
///
/// Выбрасывается, если или равны null.
///
///
/// Эта связь необходима для:
///
/// - Вычисления границ элемента на экране
/// - Создания визуального представления перетаскивания
/// - Определения позиции сброса относительно элемента
///
///
public void RegisterControl(IDockControl control, FrameworkElement element)
{
if (control == null) throw new ArgumentNullException(nameof(control));
if (element == null) throw new ArgumentNullException(nameof(element));
_controlToElement[control] = element;
}
///
/// Отменяет регистрацию связи между абстрактным контролом док-системы и UI-элементом WinUI.
///
/// Абстрактный контрол док-системы.
///
/// Выбрасывается, если равен null.
///
///
/// Удаляет элемент из внутреннего словаря, освобождая связанные с ним ресурсы.
///
public void UnregisterControl(IDockControl control)
{
if (control == null) throw new ArgumentNullException(nameof(control));
_controlToElement.TryRemove(control, out _);
}
///
/// Вычисляет границы элемента на экране.
///
/// Элемент, для которого вычисляются границы.
///
/// Прямоугольник в экранных координатах, представляющий границы элемента.
///
///
///
/// Метод выполняет преобразование координат элемента в экранные координаты
/// с использованием трансформации визуального дерева.
///
///
/// В случае ошибки вычисления возвращает прямоугольник размером 100x100 пикселей
/// в точке (0, 0).
///
///
protected override Rect CalculateBounds(IDockControl element)
{
if (_controlToElement.TryGetValue(element, out var uiElement))
{
try
{
// Получаем преобразование координат в экранные
var transform = uiElement.TransformToVisual(Window.Current.Content);
var point = transform.TransformPoint(new Windows.Foundation.Point(0, 0));
return new Rect(
point.X, point.Y,
uiElement.ActualWidth, uiElement.ActualHeight);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Failed to calculate bounds: {ex.Message}");
}
}
// Возвращаем значения по умолчанию, если не удалось вычислить
return new Rect(0, 0, 100, 100);
}
///
/// Создает визуальное представление перетаскиваемого элемента.
///
/// Информация о перетаскивании.
///
///
/// На основе источника перетаскивания создает полупрозрачную копию элемента,
/// которая следует за курсором мыши во время операции перетаскивания.
///
///
/// Визуальное представление включает:
///
///
/// - Тень для создания эффекта глубины
/// - Прозрачность для видимости фонового содержимого
/// - Синюю границу для визуального выделения
///
///
protected override void CreateDragVisual(UiDragInfo dragInfo)
{
if (_dragVisual == null || _dragVisualPopup == null || dragInfo.SourceControl == null)
return;
// Настраиваем визуальное представление на основе источника
if (_controlToElement.TryGetValue(dragInfo.SourceControl, out var sourceElement))
{
// Устанавливаем размеры визуального представления
_dragVisual.Width = sourceElement.ActualWidth;
_dragVisual.Height = sourceElement.ActualHeight;
// Создаем эффект прозрачности и тени
_dragVisual.Opacity = 0.7;
// Устанавливаем позицию Popup
_dragVisualPopup.HorizontalOffset = dragInfo.BaseDragInfo.StartPosition.X;
_dragVisualPopup.VerticalOffset = dragInfo.BaseDragInfo.StartPosition.Y;
_dragVisualPopup.Child = _dragVisual;
_dragVisualPopup.IsOpen = true;
}
}
///
/// Обновляет позицию визуального представления перетаскивания.
///
/// Новая позиция курсора.
///
/// Перемещает Popup с визуальным представлением в указанную позицию,
/// обеспечивая плавное следование за курсором мыши.
///
protected override void UpdateDragVisualPosition(Point position)
{
if (_dragVisualPopup != null)
{
_dragVisualPopup.HorizontalOffset = position.X;
_dragVisualPopup.VerticalOffset = position.Y;
}
}
///
/// Очищает визуальное представление перетаскивания.
///
///
/// Скрывает и освобождает ресурсы Popup, используемого для отображения
/// визуального представления перетаскиваемого элемента.
///
protected override void CleanupDragVisual()
{
if (_dragVisualPopup != null)
{
_dragVisualPopup.IsOpen = false;
_dragVisualPopup.Child = null;
}
}
///
/// Показывает визуальную подсказку о возможной позиции сброса.
///
/// Элемент, для которого показывается подсказка.
/// Предполагаемая позиция сброса.
///
/// Отображает полупрозрачный прямоугольник в указанной позиции относительно элемента,
/// давая пользователю визуальную обратную связь о том, куда будет помещен элемент.
///
protected override void ShowDropHint(IDockControl element, DropPosition position)
{
_dropHintOverlay?.ShowHint(element, position);
}
///
/// Скрывает текущую визуальную подсказку о сбросе.
///
///
/// Убирает все отображаемые подсказки сброса, очищая оверлей.
///
protected override void HideDropHint()
{
_dropHintOverlay?.HideHint();
}
///
/// Освобождает ресурсы, используемые сервисом перетаскивания.
///
///
///
/// Выполняет следующие действия:
///
///
/// - Отписывается от всех событий менеджера перетаскивания
/// - Удаляет оверлей подсказок из корневого контейнера
/// - Очищает словарь зарегистрированных контролов
/// - Освобождает визуальные элементы
///
///
public void Dispose()
{
if (!_disposed)
{
if (_dragDropManager != null)
{
_dragDropManager.DragStarted -= OnDragStarted;
_dragDropManager.DragUpdated -= OnDragUpdated;
_dragDropManager.DragCompleted -= OnDragCompleted;
_dragDropManager.DragCancelled -= OnDragCancelled;
_dragDropManager.DropTargetChanged -= OnDropTargetChanged;
}
if (_dropHintOverlay != null && Window.Current?.Content is Panel rootPanel)
{
rootPanel.Children.Remove(_dropHintOverlay);
_dropHintOverlay = null;
}
_controlToElement.Clear();
_disposed = true;
}
}
}
///
/// Представляет оверлей для отображения визуальных подсказок при сбросе в операции перетаскивания.
/// Этот элемент отображает полупрозрачные прямоугольники в местах возможного сброса,
/// давая пользователю визуальную обратную связь о допустимых позициях.
///
///
///
/// является внутренним вспомогательным классом,
/// который отображается поверх всего пользовательского интерфейса во время операции
/// перетаскивания для показа визуальных подсказок.
///
///
/// Оверлей поддерживает все позиции сброса, определенные в ,
/// и автоматически вычисляет размеры и положение подсказок на основе целевого элемента.
///
///
internal sealed class DropHintOverlay : Grid
{
private readonly Dictionary _hintRectangles = new();
private readonly SolidColorBrush _hintBrush;
///
/// Инициализирует новый экземпляр класса .
///
///
/// Создает прозрачный оверлей, который не участвует в тестировании попаданий,
/// и инициализирует прямоугольники для всех возможных позиций сброса.
///
public DropHintOverlay()
{
this.IsHitTestVisible = false;
this.Background = new SolidColorBrush(Microsoft.UI.Colors.Transparent);
// Используем акцентный цвет для подсказок
_hintBrush = new SolidColorBrush(Microsoft.UI.Colors.DodgerBlue);
InitializeHintRectangles();
}
///
/// Инициализирует прямоугольники для всех позиций сброса.
///
///
/// Создает отдельный Border для каждой позиции сброса и добавляет их в дочернюю коллекцию.
/// Все прямоугольники изначально скрыты и отображаются только при необходимости.
///
private void InitializeHintRectangles()
{
// Создаем прямоугольники для каждой позиции сброса
var positions = new[]
{
DropPosition.Left, DropPosition.Right,
DropPosition.Top, DropPosition.Bottom,
DropPosition.Center, DropPosition.Tab
};
foreach (var position in positions)
{
var rect = new Border
{
Background = _hintBrush,
Opacity = 0.3,
BorderBrush = new SolidColorBrush(Microsoft.UI.Colors.DodgerBlue),
BorderThickness = new Thickness(2),
Visibility = Visibility.Collapsed,
CornerRadius = new CornerRadius(4)
};
_hintRectangles[position] = rect;
this.Children.Add(rect);
}
}
///
/// Показывает визуальную подсказку для указанного элемента и позиции сброса.
///
/// Элемент, для которого показывается подсказка.
/// Позиция сброса относительно элемента.
///
/// Вычисляет положение и размер подсказки на основе границ элемента и позиции сброса,
/// затем делает соответствующий прямоугольник видимым.
///
public void ShowHint(IDockControl element, DropPosition position)
{
if (element is not FrameworkElement uiElement) return;
if (!_hintRectangles.TryGetValue(position, out var rect)) return;
// Вычисляем позицию и размер подсказки
var bounds = CalculateHintBounds(uiElement, position);
Canvas.SetLeft(rect, bounds.X);
Canvas.SetTop(rect, bounds.Y);
rect.Width = bounds.Width;
rect.Height = bounds.Height;
rect.Visibility = Visibility.Visible;
}
///
/// Вычисляет границы подсказки для указанного элемента и позиции сброса.
///
/// Целевой элемент.
/// Позиция сброса.
/// Прямоугольник с координатами и размерами подсказки.
///
/// Размеры подсказок зависят от позиции:
///
/// - Слева/справа: ширина 50px, высота равна высоте элемента
/// - Сверху/снизу: высота 50px, ширина равна ширине элемента
/// - В центре: размеры равны размерам элемента
/// - Вкладка: высота 30px, ширина равна ширине элемента, позиция сверху
///
///
private Rect CalculateHintBounds(FrameworkElement element, DropPosition position)
{
// Получаем позицию элемента относительно оверлея
var transform = element.TransformToVisual(this);
var point = transform.TransformPoint(new Windows.Foundation.Point(0, 0));
// Вычисляем размеры подсказки в зависимости от позиции
return position switch
{
DropPosition.Left => new Rect(
point.X - 50, point.Y,
50, element.ActualHeight),
DropPosition.Right => new Rect(
point.X + element.ActualWidth, point.Y,
50, element.ActualHeight),
DropPosition.Top => new Rect(
point.X, point.Y - 50,
element.ActualWidth, 50),
DropPosition.Bottom => new Rect(
point.X, point.Y + element.ActualHeight,
element.ActualWidth, 50),
DropPosition.Center => new Rect(
point.X, point.Y,
element.ActualWidth, element.ActualHeight),
DropPosition.Tab => new Rect(
point.X, point.Y - 30,
element.ActualWidth, 30),
_ => new Rect(point.X, point.Y, 100, 100)
};
}
///
/// Скрывает все визуальные подсказки.
///
///
/// Делает все прямоугольники подсказок невидимыми, очищая оверлей.
///
public void HideHint()
{
foreach (var rect in _hintRectangles.Values)
{
rect.Visibility = Visibility.Collapsed;
}
}
}