DragAndDrop core

This commit is contained in:
FrigaT
2026-01-18 16:33:35 +03:00
parent 9ea82af329
commit 79bdd8bc62
229 changed files with 21214 additions and 2494 deletions

View File

@@ -0,0 +1,41 @@
using Lattice.Core.Geometry;
namespace Lattice.UI.DragDrop.Abstractions;
/// <summary>
/// Хост для отображения визуальных элементов перетаскивания.
/// </summary>
public interface IDragDropHost
{
/// <summary>
/// Показывает визуальное представление перетаскивания.
/// </summary>
/// <param name="dragVisual">Визуальное представление.</param>
/// <param name="position">Начальная позиция.</param>
void ShowDragVisual(object dragVisual, Point position);
/// <summary>
/// Обновляет позицию визуального представления перетаскивания.
/// </summary>
/// <param name="dragVisual">Визуальное представление.</param>
/// <param name="position">Новая позиция.</param>
void UpdateDragVisualPosition(object dragVisual, Point position);
/// <summary>
/// Скрывает визуальное представление перетаскивания.
/// </summary>
/// <param name="dragVisual">Визуальное представление.</param>
void HideDragVisual(object dragVisual);
/// <summary>
/// Показывает визуальную обратную связь для цели сброса.
/// </summary>
/// <param name="adorner">Элемент обратной связи.</param>
void ShowDropAdorner(IDropVisualAdorner adorner);
/// <summary>
/// Скрывает визуальную обратную связь для цели сброса.
/// </summary>
/// <param name="adorner">Элемент обратной связи.</param>
void HideDropAdorner(IDropVisualAdorner adorner);
}

View File

@@ -0,0 +1,31 @@
using Lattice.Core.DragDrop.Models;
using Lattice.Core.Geometry;
namespace Lattice.UI.DragDrop.Abstractions;
/// <summary>
/// Поставщик визуального представления для перетаскиваемого элемента.
/// </summary>
public interface IDragVisualProvider
{
/// <summary>
/// Создает визуальное представление для перетаскивания.
/// </summary>
/// <param name="dragInfo">Информация о перетаскивании.</param>
/// <param name="initialPosition">Начальная позиция в экранных координатах.</param>
/// <returns>Объект, представляющий визуальное отображение.</returns>
object CreateDragVisual(DragInfo dragInfo, Point initialPosition);
/// <summary>
/// Обновляет позицию визуального представления.
/// </summary>
/// <param name="dragVisual">Визуальное представление.</param>
/// <param name="position">Новая позиция.</param>
void UpdateDragVisualPosition(object dragVisual, Point position);
/// <summary>
/// Освобождает ресурсы визуального представления.
/// </summary>
/// <param name="dragVisual">Визуальное представление.</param>
void ReleaseDragVisual(object dragVisual);
}

View File

@@ -0,0 +1,28 @@
using Lattice.Core.DragDrop.Models;
using Lattice.Core.Geometry;
namespace Lattice.UI.DragDrop.Abstractions;
/// <summary>
/// Визуальный элемент, показывающий обратную связь при наведении на цель сброса.
/// </summary>
public interface IDropVisualAdorner
{
/// <summary>
/// Показывает визуальную обратную связь для цели сброса.
/// </summary>
/// <param name="dropInfo">Информация о сбросе.</param>
/// <param name="targetBounds">Границы цели.</param>
void Show(DropInfo dropInfo, Rect targetBounds);
/// <summary>
/// Обновляет позицию и состояние визуальной обратной связи.
/// </summary>
/// <param name="dropInfo">Информация о сбросе.</param>
void Update(DropInfo dropInfo);
/// <summary>
/// Скрывает визуальную обратную связь.
/// </summary>
void Hide();
}

View File

@@ -0,0 +1,256 @@
using Lattice.Core.DragDrop.Abstractions;
using Lattice.Core.DragDrop.Models;
using Lattice.Core.DragDrop.Services;
using Lattice.Core.Geometry;
using Microsoft.Extensions.DependencyInjection;
using System;
namespace Lattice.UI.DragDrop.Behaviors;
/// <summary>
/// Базовый класс поведения источника перетаскивания.
/// </summary>
/// <typeparam name="TElement">Тип UI элемента.</typeparam>
public abstract class DragSourceBehaviorBase<TElement> : IDragSource
where TElement : class
{
private IDragDropService? _dragDropService;
private Point _dragStartPosition;
private bool _isDragging;
private TElement? _associatedElement;
/// <summary>
/// Получает или задает связанный элемент.
/// </summary>
protected TElement? AssociatedElement
{
get => _associatedElement;
set
{
if (_associatedElement != value)
{
DetachFromElement();
_associatedElement = value;
AttachToElement();
}
}
}
/// <summary>
/// Получает сервис перетаскивания.
/// </summary>
protected IDragDropService DragDropService
{
get
{
if (_dragDropService == null)
{
_dragDropService = ServiceProvider.GetRequiredService<IDragDropService>();
}
return _dragDropService;
}
}
/// <summary>
/// Получает провайдер сервисов.
/// </summary>
protected IServiceProvider ServiceProvider { get; }
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragSourceBehaviorBase{TElement}"/>.
/// </summary>
/// <param name="serviceProvider">Провайдер сервисов.</param>
protected DragSourceBehaviorBase(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}
/// <summary>
/// Вызывается при прикреплении к элементу.
/// </summary>
protected virtual void AttachToElement()
{
if (_associatedElement != null)
{
SubscribeToEvents(_associatedElement);
}
}
/// <summary>
/// Вызывается при откреплении от элемента.
/// </summary>
protected virtual void DetachFromElement()
{
if (_associatedElement != null)
{
UnsubscribeFromEvents(_associatedElement);
}
}
/// <summary>
/// Подписывается на события элемента.
/// </summary>
/// <param name="element">Элемент.</param>
protected abstract void SubscribeToEvents(TElement element);
/// <summary>
/// Отписывается от событий элемента.
/// </summary>
/// <param name="element">Элемент.</param>
protected abstract void UnsubscribeFromEvents(TElement element);
/// <summary>
/// Обрабатывает начало взаимодействия (например, нажатие мыши).
/// </summary>
/// <param name="position">Позиция в координатах элемента.</param>
protected virtual void OnInteractionStarted(Point position)
{
if (_isDragging)
return;
_dragStartPosition = position;
}
/// <summary>
/// Обрабатывает перемещение во время взаимодействия.
/// </summary>
/// <param name="position">Позиция в координатах элемента.</param>
protected virtual void OnInteractionMoved(Point position)
{
if (_isDragging)
return;
var distance = CalculateDistance(_dragStartPosition, position);
if (distance > DragDropService.DragStartThreshold)
{
StartDragOperation();
}
}
/// <summary>
/// Обрабатывает завершение взаимодействия.
/// </summary>
protected virtual void OnInteractionEnded()
{
// Сброс состояния, если перетаскивание не началось
if (!_isDragging)
{
Reset();
}
}
/// <summary>
/// Обрабатывает отмену взаимодействия.
/// </summary>
protected virtual void OnInteractionCancelled()
{
if (_isDragging)
{
DragDropService.CancelDrag();
}
Reset();
}
/// <summary>
/// Начинает операцию перетаскивания.
/// </summary>
protected virtual void StartDragOperation()
{
if (_isDragging || AssociatedElement == null)
return;
// Получаем начальную позицию в экранных координатах
var screenPosition = ConvertToScreenCoordinates(_dragStartPosition);
// Начинаем перетаскивание
_isDragging = DragDropService.StartDrag(this, screenPosition);
}
/// <summary>
/// Преобразует координаты элемента в экранные координаты.
/// </summary>
/// <param name="point">Точка в координатах элемента.</param>
/// <returns>Точка в экранных координатах.</returns>
protected abstract Point ConvertToScreenCoordinates(Point point);
/// <summary>
/// Вычисляет расстояние между двумя точками.
/// </summary>
protected virtual 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);
}
/// <summary>
/// Сбрасывает состояние поведения.
/// </summary>
protected virtual void Reset()
{
_isDragging = false;
_dragStartPosition = default;
}
#region IDragSource Implementation
/// <inheritdoc/>
public abstract bool CanStartDrag(out DragInfo? dragInfo);
/// <inheritdoc/>
public virtual bool StartDrag(DragInfo dragInfo)
{
// Базовая реализация всегда разрешает начало перетаскивания
return true;
}
/// <inheritdoc/>
public virtual void DragCompleted(DragInfo dragInfo, Core.DragDrop.Enums.DragDropEffects effects)
{
_isDragging = false;
// Оповещаем о завершении перетаскивания
OnDragCompleted(dragInfo, effects);
}
/// <inheritdoc/>
public virtual void DragCancelled(DragInfo dragInfo)
{
_isDragging = false;
// Оповещаем об отмене перетаскивания
OnDragCancelled(dragInfo);
}
#endregion
#region Virtual Methods for Derived Classes
/// <summary>
/// Вызывается при успешном завершении перетаскивания.
/// </summary>
/// <param name="dragInfo">Информация о перетаскивании.</param>
/// <param name="effects">Примененные эффекты.</param>
protected virtual void OnDragCompleted(DragInfo dragInfo, Core.DragDrop.Enums.DragDropEffects effects)
{
}
/// <summary>
/// Вызывается при отмене перетаскивания.
/// </summary>
/// <param name="dragInfo">Информация о перетаскивании.</param>
protected virtual void OnDragCancelled(DragInfo dragInfo)
{
}
#endregion
/// <summary>
/// Освобождает ресурсы.
/// </summary>
public virtual void Detach()
{
DetachFromElement();
_associatedElement = null;
}
}

View File

@@ -0,0 +1,214 @@
using Lattice.Core.DragDrop.Abstractions;
using Lattice.Core.DragDrop.Models;
using Lattice.Core.DragDrop.Services;
using Lattice.Core.Geometry;
using Microsoft.Extensions.DependencyInjection;
using System;
namespace Lattice.UI.DragDrop.Behaviors;
/// <summary>
/// Базовый класс поведения цели сброса.
/// </summary>
/// <typeparam name="TElement">Тип UI элемента.</typeparam>
public abstract class DropTargetBehaviorBase<TElement> : IDropTarget
where TElement : class
{
private IDragDropService? _dragDropService;
private string? _registrationId;
private TElement? _associatedElement;
private Rect _currentBounds;
/// <summary>
/// Получает или задает связанный элемент.
/// </summary>
protected TElement? AssociatedElement
{
get => _associatedElement;
set
{
if (_associatedElement != value)
{
UnregisterFromService();
_associatedElement = value;
RegisterToService();
}
}
}
/// <summary>
/// Получает или задает приоритет цели сброса.
/// </summary>
public int Priority { get; set; }
/// <summary>
/// Получает или задает группу цели сброса.
/// </summary>
public string? Group { get; set; }
/// <summary>
/// Получает сервис перетаскивания.
/// </summary>
protected IDragDropService DragDropService
{
get
{
if (_dragDropService == null)
{
_dragDropService = ServiceProvider.GetRequiredService<IDragDropService>();
}
return _dragDropService;
}
}
/// <summary>
/// Получает провайдер сервисов.
/// </summary>
protected IServiceProvider ServiceProvider { get; }
/// <summary>
/// Получает текущие границы элемента в экранных координатах.
/// </summary>
protected Rect CurrentBounds => _currentBounds;
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DropTargetBehaviorBase{TElement}"/>.
/// </summary>
/// <param name="serviceProvider">Провайдер сервисов.</param>
protected DropTargetBehaviorBase(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}
/// <summary>
/// Вызывается при прикреплении к элементу.
/// </summary>
protected virtual void AttachToElement()
{
if (_associatedElement != null)
{
SubscribeToEvents(_associatedElement);
UpdateBounds();
RegisterToService();
}
}
/// <summary>
/// Вызывается при откреплении от элемента.
/// </summary>
protected virtual void DetachFromElement()
{
if (_associatedElement != null)
{
UnsubscribeFromEvents(_associatedElement);
UnregisterFromService();
}
}
/// <summary>
/// Подписывается на события элемента.
/// </summary>
/// <param name="element">Элемент.</param>
protected abstract void SubscribeToEvents(TElement element);
/// <summary>
/// Отписывается от событий элемента.
/// </summary>
/// <param name="element">Элемент.</param>
protected abstract void UnsubscribeFromEvents(TElement element);
/// <summary>
/// Обновляет границы элемента в экранных координатах.
/// </summary>
protected virtual void UpdateBounds()
{
if (_associatedElement != null)
{
_currentBounds = GetScreenBounds(_associatedElement);
// Обновляем регистрацию в сервисе
if (_registrationId != null)
{
DragDropService.UpdateDropTargetBounds(_registrationId, _currentBounds);
}
}
}
/// <summary>
/// Получает границы элемента в экранных координатах.
/// </summary>
/// <param name="element">Элемент.</param>
/// <returns>Границы в экранных координатах.</returns>
protected abstract Rect GetScreenBounds(TElement element);
/// <summary>
/// Регистрирует цель в сервисе перетаскивания.
/// </summary>
protected virtual void RegisterToService()
{
if (_associatedElement != null && _registrationId == null)
{
UpdateBounds();
_registrationId = DragDropService.RegisterDropTarget(this, _currentBounds, Priority, Group);
}
}
/// <summary>
/// Отменяет регистрацию цели в сервисе перетаскивания.
/// </summary>
protected virtual void UnregisterFromService()
{
if (_registrationId != null)
{
DragDropService.UnregisterDropTarget(_registrationId);
_registrationId = null;
}
}
/// <summary>
/// Вызывается при изменении размера или позиции элемента.
/// </summary>
protected virtual void OnElementLayoutChanged()
{
UpdateBounds();
}
#region IDropTarget Implementation
/// <inheritdoc/>
public abstract bool CanAcceptDrop(DropInfo dropInfo);
/// <inheritdoc/>
public virtual void DragOver(DropInfo dropInfo)
{
// Базовая реализация устанавливает эффект по умолчанию
if (CanAcceptDrop(dropInfo))
{
dropInfo.SuggestedEffects = Core.DragDrop.Enums.DragDropEffects.Move;
}
else
{
dropInfo.SuggestedEffects = Core.DragDrop.Enums.DragDropEffects.None;
}
}
/// <inheritdoc/>
public abstract void Drop(DropInfo dropInfo);
/// <inheritdoc/>
public virtual void DragLeave()
{
// Базовая реализация не делает ничего
}
#endregion
/// <summary>
/// Освобождает ресурсы.
/// </summary>
public virtual void Detach()
{
DetachFromElement();
_associatedElement = null;
}
}

View File

@@ -0,0 +1,52 @@
using Lattice.UI.DragDrop.Abstractions;
using Microsoft.Extensions.DependencyInjection;
namespace Lattice.UI.DragDrop.Extensions;
/// <summary>
/// Методы расширения для регистрации сервисов перетаскивания.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Добавляет сервисы перетаскивания.
/// </summary>
public static IServiceCollection AddLatticeDragDrop(this IServiceCollection services)
{
// Регистрируем абстракции, которые будут реализованы в платформенных проектах
services.AddSingleton(typeof(IDragVisualProvider), typeof(DefaultDragVisualProvider));
services.AddSingleton(typeof(IDropVisualAdorner), typeof(DefaultDropVisualAdorner));
services.AddSingleton(typeof(IDragDropHost), typeof(DefaultDragDropHost));
return services;
}
/// <summary>
/// Реализация по умолчанию для платформ, которые еще не имеют своей реализации.
/// </summary>
private class DefaultDragVisualProvider : IDragVisualProvider
{
public object CreateDragVisual(Core.DragDrop.Models.DragInfo dragInfo, Core.Geometry.Point initialPosition)
=> new object();
public void UpdateDragVisualPosition(object dragVisual, Core.Geometry.Point position) { }
public void ReleaseDragVisual(object dragVisual) { }
}
private class DefaultDropVisualAdorner : IDropVisualAdorner
{
public void Show(Core.DragDrop.Models.DropInfo dropInfo, Core.Geometry.Rect targetBounds) { }
public void Update(Core.DragDrop.Models.DropInfo dropInfo) { }
public void Hide() { }
}
private class DefaultDragDropHost : IDragDropHost
{
public void ShowDragVisual(object dragVisual, Core.Geometry.Point position) { }
public void UpdateDragVisualPosition(object dragVisual, Core.Geometry.Point position) { }
public void HideDragVisual(object dragVisual) { }
public void ShowDropAdorner(IDropVisualAdorner adorner) { }
public void HideDropAdorner(IDropVisualAdorner adorner) { }
}
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Lattice.Core.DragDrop\Lattice.Core.DragDrop.csproj" />
<ProjectReference Include="..\Lattice.Core.Geometry\Lattice.Core.Geometry.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,136 @@
# Lattice.UI.DragDrop
![Lattice Framework](https://img.shields.io/badge/Lattice-UI%20Framework-blueviolet)
![Version](https://img.shields.io/badge/version-1.0.0-green)
![License](https://img.shields.io/badge/license-MIT-blue)
Кроссплатформенные абстракции для системы перетаскивания в Lattice UI Framework.
## 📦 О проекте
`Lattice.UI.DragDrop` предоставляет платформонезависимые интерфейсы и базовые классы для реализации drag-and-drop функциональности.
Этот проект служит основой для конкретных реализаций на различных платформах (WinUI, Uno Platform, MAUI и т.д.).
## 🎯 Особенности
- **Абстрактные интерфейсы** для источников перетаскивания и целей сброса
- **Базовые классы поведения** для упрощения реализации
- **Платформонезависимая архитектура**
- **Поддержка сложных сценариев** (переупорядочивание, вложенное перетаскивание)
- **Расширяемая система событий**
## 🔧 Интерфейсы
### IDragVisualProvider
```csharp
public interface IDragVisualProvider
{
object CreateDragVisual(DragInfo dragInfo, Point initialPosition);
void UpdateDragVisualPosition(object dragVisual, Point position);
void ReleaseDragVisual(object dragVisual);
}
```
### IDropVisualAdorner
```csharp
public interface IDropVisualAdorner
{
void Show(DropInfo dropInfo, Rect targetBounds);
void Update(DropInfo dropInfo);
void Hide();
}
```
## 📦 Установка
Добавьте проект как ссылку в ваше решение или установите как NuGet пакет:
```xml
<PackageReference Include="Lattice.UI.DragDrop" Version="1.0.0" />
```
## 🔗 Зависимости
- `Lattice.Core.DragDrop` >= 1.0.0
- `Lattice.Core.Geometry` >= 1.0.0
- `Microsoft.Extensions.DependencyInjection.Abstractions` >= 8.0.0
## 🚀 Быстрый старт
### 1. Регистрация сервисов
```csharp
using Lattice.UI.DragDrop.Extensions;
public void ConfigureServices(IServiceCollection services)
{
services.AddLatticeDragDrop();
}
```
### 2. Создание кастомного поведения
```csharp
using Lattice.UI.DragDrop.Behaviors;
public class MyDragSource : DragSourceBehaviorBase<MyElement>
{
protected override void SubscribeToEvents(MyElement element)
{
// Подписка на события элемента
}
public override bool CanStartDrag(out DragInfo? dragInfo)
{
// Реализация проверки возможности перетаскивания
}
}
```
## 📚 API Reference
### Основные типы
| Тип | Описание |
|-----|----------|
| `DragSourceBehaviorBase<T>` | Базовый класс для поведения источника |
| `DropTargetBehaviorBase<T>` | Базовый класс для поведения цели |
| `IDragVisualProvider` | Поставщик визуального представления |
| `IDropVisualAdorner` | Визуальный элемент обратной связи |
### Расширения DI
- `AddLatticeDragDrop()` - регистрация сервисов перетаскивания
## 🔄 Интеграция с платформенными проектами
Этот проект предназначен для наследования платформенными реализациями:
1. **WinUI**: `Lattice.UI.DragDrop.WinUI`
2. **Uno Platform**: `Lattice.UI.DragDrop.Uno` (планируется)
3. **MAUI**: `Lattice.UI.DragDrop.Maui` (планируется)
## 🧪 Тестирование
Проект включает модульные тесты для всех публичных API:
```bash
dotnet test Lattice.UI.DragDrop.Tests
```
## 📄 Лицензия
MIT License. Подробности в файле [LICENSE](LICENSE).
## 🤝 Участие в разработке
1. Форкните репозиторий
2. Создайте ветку для вашей функции
3. Сделайте коммит изменений
4. Отправьте пул-реквест
## 📞 Поддержка
- Документация: [lattice-framework.github.io](https://lattice-framework.github.io)
- Issues: [GitHub Issues](https://github.com/lattice-framework/ui-dragdrop/issues)
- Обсуждения: [GitHub Discussions](https://github.com/lattice-framework/ui-dragdrop/discussions)