using Lattice.Core.DragDrop.Services; 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; } } }