diff --git a/Lattice.Core.Docking/Abstractions/IDockCommand.cs b/Lattice.Core.Docking/Abstractions/IDockCommand.cs
new file mode 100644
index 0000000..e7b064f
--- /dev/null
+++ b/Lattice.Core.Docking/Abstractions/IDockCommand.cs
@@ -0,0 +1,8 @@
+namespace Lattice.Core.Docking.Abstractions;
+
+public interface IDockCommand : System.Windows.Input.ICommand
+{
+ string Name { get; }
+ string Icon { get; }
+ string GestureText { get; }
+}
diff --git a/Lattice.Core.Docking/Abstractions/IDockContainer.cs b/Lattice.Core.Docking/Abstractions/IDockContainer.cs
new file mode 100644
index 0000000..404924d
--- /dev/null
+++ b/Lattice.Core.Docking/Abstractions/IDockContainer.cs
@@ -0,0 +1,24 @@
+using Lattice.Core.Docking.Models;
+
+namespace Lattice.Core.Docking.Abstractions;
+
+///
+/// Интерфейс для элементов (листьев дерева), которые физически содержат внутри себя коллекцию вкладок.
+///
+public interface IDockContainer : IDockElement
+{
+ /// Список вкладок, находящихся в данном контейнере.
+ IList Children { get; }
+
+ /// Ссылка на текущую выбранную и отображаемую вкладку.
+ IDockContent? ActiveContent { get; set; }
+
+ /// Добавляет контент в контейнер и делает его активным.
+ void AddContent(IDockContent content);
+
+ /// Удаляет контент. Если Children становится пустым, контейнер может быть удален из дерева макета.
+ void RemoveContent(IDockContent content);
+
+ /// Положение вкладок в интерфейсе.
+ TabPlacement TabPlacement { get; set; }
+}
diff --git a/Lattice.Core.Docking/Abstractions/IDockContent.cs b/Lattice.Core.Docking/Abstractions/IDockContent.cs
new file mode 100644
index 0000000..cd719d5
--- /dev/null
+++ b/Lattice.Core.Docking/Abstractions/IDockContent.cs
@@ -0,0 +1,25 @@
+namespace Lattice.Core.Docking.Abstractions;
+
+///
+/// Описывает объект содержимого (вкладку), который может быть размещен внутри IDockContainer.
+///
+public interface IDockContent
+{
+ /// Уникальный идентификатор контента (например, путь к файлу или ID инструмента).
+ string Id { get; }
+
+ /// Заголовок, отображаемый пользователю в интерфейсе (на вкладке).
+ string Title { get; }
+
+ ///
+ /// Сам визуальный элемент (например, Microsoft.UI.Xaml.UIElement).
+ /// Lattice просто отображает этот объект в теле вкладки.
+ ///
+ object View { get; set; }
+
+ /// Флаг, определяющий доступность кнопки закрытия для пользователя.
+ bool CanClose { get; }
+
+ /// Вызывается системой при попытке закрытия контента. Возвращает true, если закрытие разрешено.
+ bool OnClosing();
+}
diff --git a/Lattice.Core.Docking/Abstractions/IDockElement.cs b/Lattice.Core.Docking/Abstractions/IDockElement.cs
new file mode 100644
index 0000000..9131a85
--- /dev/null
+++ b/Lattice.Core.Docking/Abstractions/IDockElement.cs
@@ -0,0 +1,25 @@
+namespace Lattice.Core.Docking.Abstractions;
+
+///
+/// Базовый интерфейс для любого элемента, который может быть частью дерева компоновки Lattice.
+///
+public interface IDockElement
+{
+ /// Уникальный идентификатор элемента.
+ string Id { get; }
+
+ /// Родительский элемент в иерархии. Если null — элемент является корневым.
+ IDockElement? Parent { get; set; }
+
+ /// Желаемая ширина элемента в относительных или абсолютных единицах.
+ double Width { get; set; }
+
+ /// Желаемая высота элемента в относительных или абсолютных единицах.
+ double Height { get; set; }
+
+ /// Минимально допустимая ширина, при которой элемент сохраняет функциональность.
+ double MinWidth { get; }
+
+ /// Минимально допустимая высота, при которой элемент сохраняет функциональность.
+ double MinHeight { get; }
+}
diff --git a/Lattice.Core.Docking/Abstractions/IDockElementDragSource.cs b/Lattice.Core.Docking/Abstractions/IDockElementDragSource.cs
new file mode 100644
index 0000000..42ac214
--- /dev/null
+++ b/Lattice.Core.Docking/Abstractions/IDockElementDragSource.cs
@@ -0,0 +1,19 @@
+using Lattice.Core.DragDrop.Abstractions;
+
+namespace Lattice.Core.Docking.Abstractions;
+
+///
+/// Расширяет интерфейс элемента док-системы для поддержки операций перетаскивания.
+///
+public interface IDockElementDragSource : IDockElement, IDragSource
+{
+ ///
+ /// Получает или устанавливает признак того, что элемент можно перетаскивать.
+ ///
+ bool CanDrag { get; set; }
+
+ ///
+ /// Получает тип данных для перетаскивания этого элемента.
+ ///
+ string DragDataType { get; }
+}
diff --git a/Lattice.Core.Docking/Abstractions/IDockElementDropTarget.cs b/Lattice.Core.Docking/Abstractions/IDockElementDropTarget.cs
new file mode 100644
index 0000000..642a810
--- /dev/null
+++ b/Lattice.Core.Docking/Abstractions/IDockElementDropTarget.cs
@@ -0,0 +1,19 @@
+using Lattice.Core.DragDrop.Abstractions;
+
+namespace Lattice.Core.Docking.Abstractions;
+
+///
+/// Расширяет интерфейс элемента док-системы для возможности быть целью сброса.
+///
+public interface IDockElementDropTarget : IDockElement, IDropTarget
+{
+ ///
+ /// Получает или устанавливает признак того, что элемент может принимать сброс.
+ ///
+ bool CanDrop { get; set; }
+
+ ///
+ /// Получает типы данных, которые может принимать элемент.
+ ///
+ IEnumerable AcceptableDropTypes { get; }
+}
\ No newline at end of file
diff --git a/Lattice.Core.Docking/Abstractions/IDragService.cs b/Lattice.Core.Docking/Abstractions/IDragService.cs
new file mode 100644
index 0000000..1fbc4bc
--- /dev/null
+++ b/Lattice.Core.Docking/Abstractions/IDragService.cs
@@ -0,0 +1,76 @@
+using Lattice.Core.Docking.Models;
+using Lattice.Core.Geometry;
+
+namespace Lattice.Core.Docking.Abstractions;
+
+///
+/// Предоставляет абстракцию для операции перетаскивания в док-системе.
+/// Эта абстракция позволяет отделить логику перетаскивания от конкретной UI-платформы.
+///
+public interface IDragService
+{
+ ///
+ /// Начинает операцию перетаскивания указанного элемента.
+ ///
+ /// Элемент для перетаскивания.
+ /// Визуальная обратная связь (зависит от платформы).
+ void StartDrag(IDockElement element, object? visualFeedback = null);
+
+ ///
+ /// Обновляет позицию перетаскивания.
+ ///
+ /// Координата X.
+ /// Координата Y.
+ void UpdateDrag(double x, double y);
+
+ ///
+ /// Завершает операцию перетаскивания.
+ ///
+ /// Координата X завершения.
+ /// Координата Y завершения.
+ void EndDrag(double x, double y);
+
+ ///
+ /// Отменяет операцию перетаскивания.
+ ///
+ void CancelDrag();
+}
+
+///
+/// Представляет область для сброса при операции перетаскивания.
+///
+public class DropArea
+{
+ ///
+ /// Целевой элемент для сброса.
+ ///
+ public IDockElement Target { get; set; }
+
+ ///
+ /// Позиция сброса относительно цели.
+ ///
+ public DockPosition Position { get; set; }
+
+ ///
+ /// Границы области в экранных координатах.
+ ///
+ public Rect Bounds { get; set; }
+
+ ///
+ /// Видимость области (для анимации).
+ ///
+ public double Visibility { get; set; } = 0.0;
+
+ ///
+ /// Инициализирует новый экземпляр области сброса.
+ ///
+ /// Целевой элемент.
+ /// Позиция сброса.
+ /// Границы области.
+ public DropArea(IDockElement target, DockPosition position, Rect bounds)
+ {
+ Target = target;
+ Position = position;
+ Bounds = bounds;
+ }
+}
\ No newline at end of file
diff --git a/Lattice.Core.Docking/Engine/DockOperations.cs b/Lattice.Core.Docking/Engine/DockOperations.cs
new file mode 100644
index 0000000..4c6a5c5
--- /dev/null
+++ b/Lattice.Core.Docking/Engine/DockOperations.cs
@@ -0,0 +1,89 @@
+using Lattice.Core.Docking.Abstractions;
+using Lattice.Core.Docking.Models;
+
+namespace Lattice.Core.Docking.Engine;
+
+///
+/// Статический движок для манипуляции иерархией дерева компоновки.
+/// Содержит чистые алгоритмы трансформации графа.
+///
+public static class DockOperations
+{
+ ///
+ /// Извлекает элемент из дерева. Если родительская группа остается с одним ребенком,
+ /// она удаляется, а ребенок занимает её место.
+ ///
+ /// Элемент для удаления.
+ /// Текущий корень дерева.
+ /// Новый корень дерева после оптимизации.
+ public static IDockElement? Remove(IDockElement element, IDockElement root)
+ {
+ if (element == root) return null;
+
+ var parent = element.Parent as DockGroup;
+ if (parent == null) return root;
+
+ // Определяем "выжившего" соседа
+ var sibling = (parent.First == element) ? parent.Second : parent.First;
+ var grandParent = parent.Parent as DockGroup;
+
+ if (grandParent != null)
+ {
+ // Переподключаем соседа напрямую к дедушке
+ if (grandParent.First == parent) grandParent.First = sibling;
+ else grandParent.Second = sibling;
+
+ sibling.Parent = grandParent;
+ return root;
+ }
+
+ // Если дедушки нет, сосед становится новым корнем
+ sibling.Parent = null;
+ return sibling;
+ }
+
+ ///
+ /// Вставляет элемент в дерево, создавая новую группу разделения или объединяя контент.
+ ///
+ public static IDockElement Insert(IDockElement target, IDockElement source, DockPosition pos, IDockElement root)
+ {
+ // Случай 1: Объединение вкладок в центре
+ if (pos == DockPosition.Center)
+ {
+ if (target is IDockContainer targetContainer && source is IDockContainer sourceContainer)
+ {
+ var items = new List(sourceContainer.Children);
+ foreach (var item in items)
+ {
+ sourceContainer.RemoveContent(item);
+ targetContainer.AddContent(item);
+ }
+ }
+ return root;
+ }
+
+ // Случай 2: Разделение (Split)
+ var direction = (pos == DockPosition.Left || pos == DockPosition.Right)
+ ? SplitDirection.Horizontal : SplitDirection.Vertical;
+
+ bool sourceIsFirst = (pos == DockPosition.Left || pos == DockPosition.Top);
+
+ var oldParent = target.Parent;
+
+ // Создаем новую группу. Источник и цель делят пространство 50/50
+ var newGroup = sourceIsFirst
+ ? new DockGroup(source, target, direction) { SplitRatio = 0.5 }
+ : new DockGroup(target, source, direction) { SplitRatio = 0.5 };
+
+ if (oldParent is DockGroup gp)
+ {
+ if (gp.First == target) gp.First = newGroup;
+ else gp.Second = newGroup;
+ newGroup.Parent = gp;
+ return root;
+ }
+
+ newGroup.Parent = null;
+ return newGroup; // Новая группа стала корнем
+ }
+}
\ No newline at end of file
diff --git a/Lattice.Core.Docking/Engine/LayoutManager.cs b/Lattice.Core.Docking/Engine/LayoutManager.cs
new file mode 100644
index 0000000..f652021
--- /dev/null
+++ b/Lattice.Core.Docking/Engine/LayoutManager.cs
@@ -0,0 +1,369 @@
+using Lattice.Core.Docking.Abstractions;
+using Lattice.Core.Docking.Models;
+using System.Collections.ObjectModel;
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Lattice.Serialization.Docking")]
+
+namespace Lattice.Core.Docking.Engine;
+
+///
+/// Расширенный менеджер макета, поддерживающий автоскрываемые панели, группы документов
+/// и расширенные операции управления макетом.
+///
+///
+/// Этот класс является центральным координатором всей док-системы, управляя деревом компоновки,
+/// плавающими окнами, автоскрываемыми панелями и предоставляя API для манипуляции макетом.
+///
+public class LayoutManager
+{
+ private readonly ObservableCollection _autoHidePanels = new();
+
+ ///
+ /// Корневой элемент главного окна IDE.
+ ///
+ public IDockElement? Root { get; internal set; }
+
+ ///
+ /// Список активных плавающих окон.
+ ///
+ public List FloatingWindows { get; } = new();
+
+ ///
+ /// Коллекция автоскрываемых панелей.
+ ///
+ public ReadOnlyObservableCollection AutoHidePanels { get; }
+
+ ///
+ /// Реестр типов контента (опционально).
+ ///
+ public Services.ContentRegistry? ContentRegistry { get; set; }
+
+ ///
+ /// Уведомляет UI, что структура дерева изменилась.
+ ///
+ public event Action? LayoutUpdated;
+
+ ///
+ /// Уведомляет об изменении в коллекции автоскрываемых панелей.
+ ///
+ public event EventHandler? AutoHidePanelsChanged;
+
+ ///
+ /// Событие, возникающее при операции перетаскивания элемента.
+ ///
+ public event EventHandler? DragDropOperation;
+
+ ///
+ /// Инициализирует новый экземпляр менеджера макета.
+ ///
+ public LayoutManager()
+ {
+ AutoHidePanels = new ReadOnlyObservableCollection(_autoHidePanels);
+ }
+
+ ///
+ /// Добавляет автоскрываемую панель.
+ ///
+ /// Содержимое панели.
+ /// Сторона для прикрепления.
+ /// Созданная автоскрываемая панель.
+ public AutoHidePanel AddAutoHidePanel(IDockContent content, DockSide side)
+ {
+ var panel = new AutoHidePanel(content, side);
+ _autoHidePanels.Add(panel);
+ AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
+ return panel;
+ }
+
+ ///
+ /// Удаляет автоскрываемую панель.
+ ///
+ /// Панель для удаления.
+ public void RemoveAutoHidePanel(AutoHidePanel panel)
+ {
+ if (_autoHidePanels.Remove(panel))
+ {
+ AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ ///
+ /// Создает документ из зарегистрированного типа контента.
+ ///
+ /// Идентификатор типа контента.
+ /// Уникальный идентификатор документа.
+ /// Созданный контент или null, если ContentRegistry не установлен.
+ public IDockContent? CreateDocument(string contentTypeId, string id)
+ {
+ if (ContentRegistry == null || !ContentRegistry.IsRegistered(contentTypeId))
+ return null;
+
+ return ContentRegistry.CreateContent(contentTypeId, id);
+ }
+
+ ///
+ /// Основной метод перемещения элементов в макете.
+ ///
+ /// Что перетаскиваем.
+ /// Куда приземляем.
+ /// Позиция относительно цели.
+ ///
+ /// Если true, контент будет добавлен как документ в центральную область.
+ ///
+ public void Move(IDockElement source, IDockElement? target, DockPosition position, bool asDocument = false)
+ {
+ if (source == target) return;
+
+ // 1. Удаляем источник из текущего местоположения
+ bool sourceRemoved = false;
+
+ if (Root != null && IsDescendantOf(source, Root))
+ {
+ Root = DockOperations.Remove(source, Root);
+ sourceRemoved = true;
+ }
+ else
+ {
+ sourceRemoved = RemoveFromFloatingWindows(source);
+ }
+
+ if (!sourceRemoved)
+ {
+ // Проверяем автоскрываемые панели
+ var autoHidePanel = _autoHidePanels.FirstOrDefault(p => p.Content == source);
+ if (autoHidePanel != null)
+ {
+ _autoHidePanels.Remove(autoHidePanel);
+ sourceRemoved = true;
+ AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ if (!sourceRemoved) return;
+
+ // 2. Вставляем в цель
+ if (target == null)
+ {
+ FloatingWindows.Add(new DockWindow { Root = source as IDockElement });
+ }
+ else
+ {
+ if (IsDescendantOf(target, Root))
+ {
+ Root = DockOperations.Insert(target, source, position, Root!);
+ }
+ else
+ {
+ InsertIntoFloatingWindow(target, source, position);
+ }
+ }
+
+ LayoutUpdated?.Invoke();
+ }
+
+ private bool RemoveFromFloatingWindows(IDockElement element)
+ {
+ foreach (var win in FloatingWindows.ToArray())
+ {
+ if (win.Root != null && IsDescendantOf(element, win.Root))
+ {
+ win.Root = DockOperations.Remove(element, win.Root);
+ if (win.Root == null)
+ FloatingWindows.Remove(win);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void InsertIntoFloatingWindow(IDockElement target, IDockElement source, DockPosition position)
+ {
+ foreach (var win in FloatingWindows)
+ {
+ if (win.Root != null && IsDescendantOf(target, win.Root))
+ {
+ win.Root = DockOperations.Insert(target, source, position, win.Root);
+ return;
+ }
+ }
+ }
+
+ private bool IsDescendantOf(IDockElement element, IDockElement ancestor)
+ {
+ if (element == ancestor) return true;
+ if (ancestor is DockGroup group)
+ return IsDescendantOf(element, group.First) || IsDescendantOf(element, group.Second);
+ return false;
+ }
+
+ /// Поиск элемента по ID во всех окнах.
+ public IDockElement? FindById(string id)
+ {
+ var found = FindRecursive(Root, id);
+ if (found != null) return found;
+
+ foreach (var win in FloatingWindows)
+ {
+ found = FindRecursive(win.Root, id);
+ if (found != null) return found;
+ }
+ return null;
+ }
+
+ private IDockElement? FindRecursive(IDockElement? node, string id)
+ {
+ if (node == null || node.Id == id) return node;
+ if (node is DockGroup g) return FindRecursive(g.First, id) ?? FindRecursive(g.Second, id);
+ return null;
+ }
+
+ ///
+ /// Сбрасывает макет к состоянию по умолчанию.
+ ///
+ public void Reset()
+ {
+ Root = null;
+ FloatingWindows.Clear();
+ _autoHidePanels.Clear();
+ LayoutUpdated?.Invoke();
+ AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ ///
+ /// Обрабатывает операцию перетаскивания между элементами.
+ ///
+ /// Источник перетаскивания.
+ /// Цель сброса.
+ /// Позиция сброса относительно цели.
+ /// Данные перетаскивания.
+ /// true, если операция успешно выполнена; иначе false.
+ public bool HandleDragDrop(IDockElement source, IDockElement target,
+ DockPosition position, object data)
+ {
+ try
+ {
+ if (source == target)
+ return false;
+
+ // Определяем тип операции на основе данных
+ if (data is ContentDragData contentData)
+ {
+ return HandleContentDragDrop(contentData, target, position);
+ }
+ else if (data is DockElementDragData elementData)
+ {
+ return HandleElementDragDrop(elementData, target, position);
+ }
+
+ return false;
+ }
+ catch (Exception ex)
+ {
+ DragDropOperation?.Invoke(this, new DragDropEventArgs(
+ source, target, position, false, ex.Message));
+ return false;
+ }
+ }
+
+ private bool HandleContentDragDrop(ContentDragData data, IDockElement target, DockPosition position)
+ {
+ // Находим исходный контейнер с контентом
+ var sourceContainer = FindElementById(data.ElementId) as IDockContainer;
+ if (sourceContainer == null)
+ return false;
+
+ // Находим контент
+ var content = sourceContainer.Children.FirstOrDefault(c => c.Id == data.ContentId);
+ if (content == null)
+ return false;
+
+ if (target is IDockContainer targetContainer && position == DockPosition.Center)
+ {
+ // Объединение вкладок
+ sourceContainer.RemoveContent(content);
+ targetContainer.AddContent(content);
+
+ DragDropOperation?.Invoke(this, new DragDropEventArgs(
+ sourceContainer as IDockElement ?? sourceContainer as IDockElement,
+ target, position, true, "Content merged"));
+ return true;
+ }
+
+ return false;
+ }
+
+ private bool HandleElementDragDrop(DockElementDragData data, IDockElement target, DockPosition position)
+ {
+ // Находим перетаскиваемый элемент
+ var sourceElement = FindElementById(data.ElementId);
+ if (sourceElement == null)
+ return false;
+
+ // Выполняем перемещение
+ Move(sourceElement, target, position);
+
+ DragDropOperation?.Invoke(this, new DragDropEventArgs(
+ sourceElement, target, position, true, "Element moved"));
+ return true;
+ }
+
+ ///
+ /// Находит элемент по идентификатору.
+ ///
+ public IDockElement? FindElementById(string id)
+ {
+ return FindElementByIdRecursive(Root, id) ??
+ FloatingWindows.Select(w => FindElementByIdRecursive(w.Root, id))
+ .FirstOrDefault(result => result != null);
+ }
+
+ private IDockElement? FindElementByIdRecursive(IDockElement? element, string id)
+ {
+ if (element == null) return null;
+ if (element.Id == id) return element;
+
+ if (element is DockGroup group)
+ {
+ return FindElementByIdRecursive(group.First, id) ??
+ FindElementByIdRecursive(group.Second, id);
+ }
+
+ return null;
+ }
+}
+
+///
+/// Аргументы события операции перетаскивания.
+///
+public class DragDropEventArgs : EventArgs
+{
+ /// Источник перетаскивания.
+ public IDockElement Source { get; }
+
+ /// Цель сброса.
+ public IDockElement Target { get; }
+
+ /// Позиция сброса.
+ public DockPosition Position { get; }
+
+ /// Показывает, была ли операция успешной.
+ public bool Success { get; }
+
+ /// Сообщение о результате операции.
+ public string Message { get; }
+
+ /// Время выполнения операции.
+ public DateTime Timestamp { get; }
+
+ public DragDropEventArgs(IDockElement source, IDockElement target,
+ DockPosition position, bool success, string message)
+ {
+ Source = source;
+ Target = target;
+ Position = position;
+ Success = success;
+ Message = message;
+ Timestamp = DateTime.UtcNow;
+ }
+}
\ No newline at end of file
diff --git a/Lattice.Core.Docking/Lattice.Core.Docking.csproj b/Lattice.Core.Docking/Lattice.Core.Docking.csproj
new file mode 100644
index 0000000..a05bde6
--- /dev/null
+++ b/Lattice.Core.Docking/Lattice.Core.Docking.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net8.0;net9.0;net10.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/Lattice.Core.Docking/Models/AutoHidePanel.cs b/Lattice.Core.Docking/Models/AutoHidePanel.cs
new file mode 100644
index 0000000..03df5ed
--- /dev/null
+++ b/Lattice.Core.Docking/Models/AutoHidePanel.cs
@@ -0,0 +1,114 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace Lattice.Core.Docking.Models;
+
+///
+/// Представляет автоскрываемую панель, которая может быть прикреплена к одной из сторон окна.
+/// Автоскрываемые панели скрываются, оставляя только заголовок, и появляются при наведении курсора.
+///
+///
+/// Автоскрываемые панели являются ключевым элементом интерфейса современных IDE,
+/// позволяя экономить пространство экрана при сохранении быстрого доступа к инструментам.
+///
+public class AutoHidePanel : INotifyPropertyChanged
+{
+ public event PropertyChangedEventHandler? PropertyChanged;
+ protected void OnPropertyChanged([CallerMemberName] string? name = null) =>
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
+
+ private bool _isVisible = false;
+ private double _slideOffset = 0;
+
+ ///
+ /// Уникальный идентификатор автоскрываемой панели.
+ ///
+ public string Id { get; } = Guid.NewGuid().ToString();
+
+ ///
+ /// Содержимое панели.
+ ///
+ public Abstractions.IDockContent Content { get; set; }
+
+ ///
+ /// Сторона окна, к которой прикреплена панель.
+ ///
+ public DockSide Side { get; set; }
+
+ ///
+ /// Ширина панели (для левой/правой сторон) или высота (для верхней/нижней сторон).
+ ///
+ public double Size { get; set; } = 300;
+
+ ///
+ /// Признак видимости панели.
+ ///
+ public bool IsVisible
+ {
+ get => _isVisible;
+ set
+ {
+ if (_isVisible != value)
+ {
+ _isVisible = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ ///
+ /// Смещение для анимации выезда/заезда панели (0-1).
+ ///
+ public double SlideOffset
+ {
+ get => _slideOffset;
+ set
+ {
+ if (Math.Abs(_slideOffset - value) > 0.001)
+ {
+ _slideOffset = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ ///
+ /// Заголовок панели (обычно берется из содержимого).
+ ///
+ public string Title => Content?.Title ?? "Auto-hide Panel";
+
+ ///
+ /// Инициализирует новый экземпляр автоскрываемой панели.
+ ///
+ /// Содержимое панели.
+ /// Сторона окна для прикрепления.
+ public AutoHidePanel(Abstractions.IDockContent content, DockSide side)
+ {
+ Content = content ?? throw new ArgumentNullException(nameof(content));
+ Side = side;
+ }
+
+ ///
+ /// Переключает видимость панели.
+ ///
+ public void Toggle()
+ {
+ IsVisible = !IsVisible;
+ }
+
+ ///
+ /// Показывает панель.
+ ///
+ public void Show()
+ {
+ IsVisible = true;
+ }
+
+ ///
+ /// Скрывает панель.
+ ///
+ public void Hide()
+ {
+ IsVisible = false;
+ }
+}
diff --git a/Lattice.Core.Docking/Models/DockGroup.cs b/Lattice.Core.Docking/Models/DockGroup.cs
new file mode 100644
index 0000000..08bc6ff
--- /dev/null
+++ b/Lattice.Core.Docking/Models/DockGroup.cs
@@ -0,0 +1,444 @@
+using Lattice.Core.Docking.Abstractions;
+using Lattice.Core.DragDrop.Abstractions;
+using Lattice.Core.DragDrop.Enums;
+using Lattice.Core.DragDrop.Models;
+using Lattice.Core.Geometry;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace Lattice.Core.Docking.Models;
+
+///
+/// Представляет узел дерева компоновки, который разделяет доступную область
+/// между двумя дочерними элементами. Этот класс является основным структурным
+/// элементом для создания сложных макетов с разделителями.
+///
+///
+///
+/// реализует как (для
+/// возможности перетаскивания всей группы), так и
+/// (для возможности сброса на группу), что делает его полностью интегрированным
+/// в систему перетаскивания док-системы.
+///
+///
+/// Каждая группа содержит два дочерних элемента ( и
+/// ), которые могут быть либо другими группами (для
+/// создания вложенной структуры), либо листами ()
+/// с контентом. Направление разделения определяется свойством
+/// .
+///
+///
+public class DockGroup : IDockElement, IDragSource, IDropTarget, INotifyPropertyChanged
+{
+ ///
+ /// Событие, возникающее при изменении значения свойства.
+ ///
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ private double _splitRatio = 0.5;
+ private string _id;
+
+ ///
+ /// Получает уникальный идентификатор группы.
+ ///
+ ///
+ /// Строковый идентификатор, уникальный в пределах дерева компоновки.
+ ///
+ ///
+ /// Идентификатор используется для сериализации/десериализации макета,
+ /// поиска элементов и отслеживания изменений в дереве.
+ ///
+ public string Id
+ {
+ get => _id;
+ internal set
+ {
+ if (_id != value)
+ {
+ _id = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ ///
+ /// Получает или задает родительский элемент в иерархии дерева компоновки.
+ ///
+ ///
+ /// Родительский элемент или null, если эта группа является корневой.
+ ///
+ ///
+ /// Это свойство управляется системой компоновки при добавлении или
+ /// удалении элементов из дерева.
+ ///
+ public IDockElement? Parent { get; set; }
+
+ ///
+ /// Получает или задает первый дочерний элемент (левую или верхнюю область).
+ ///
+ ///
+ /// Элемент, занимающий первую часть разделенной области.
+ ///
+ ///
+ /// Выбрасывается при попытке установить значение null.
+ ///
+ ///
+ /// При установке нового значения автоматически обновляется свойство
+ /// у дочернего элемента.
+ ///
+ public IDockElement First { get; set; }
+
+ ///
+ /// Получает или задает второй дочерний элемент (правую или нижнюю область).
+ ///
+ ///
+ /// Элемент, занимающий вторую часть разделенной области.
+ ///
+ ///
+ /// Выбрасывается при попытке установить значение null.
+ ///
+ ///
+ /// При установке нового значения автоматически обновляется свойство
+ /// у дочернего элемента.
+ ///
+ public IDockElement Second { get; set; }
+
+ ///
+ /// Получает или задает направление разделения данной группы.
+ ///
+ ///
+ /// Значение перечисления , указывающее,
+ /// как разделена область: горизонтально или вертикально.
+ ///
+ ///
+ ///
+ /// создает левую и правую области.
+ ///
+ ///
+ /// создает верхнюю и нижнюю области.
+ ///
+ ///
+ public SplitDirection Orientation { get; set; }
+
+ ///
+ /// Получает или задает соотношение разделения между первым и вторым элементами.
+ ///
+ ///
+ /// Значение от 0.0 до 1.0, где:
+ ///
+ /// 0.0 - вся область принадлежит второму элементу
+ /// 0.5 - область разделена поровну
+ /// 1.0 - вся область принадлежит первому элементу
+ ///
+ ///
+ ///
+ /// Изменение этого свойства вызывает событие
+ /// и может привести к перерисовке пользовательского интерфейса.
+ ///
+ public double SplitRatio
+ {
+ get => _splitRatio;
+ set
+ {
+ if (Math.Abs(_splitRatio - value) > double.Epsilon)
+ {
+ _splitRatio = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ ///
+ /// Получает или задает желаемую ширину элемента.
+ ///
+ ///
+ /// Ширина в пикселях или относительных единицах.
+ ///
+ public double Width { get; set; }
+
+ ///
+ /// Получает или задает желаемую высоту элемента.
+ ///
+ ///
+ /// Высота в пикселях или относительных единицах.
+ ///
+ public double Height { get; set; }
+
+ ///
+ /// Получает минимально допустимую ширину элемента.
+ ///
+ ///
+ /// Минимальная ширина в пикселях, при которой элемент сохраняет функциональность.
+ ///
+ ///
+ /// Для группы минимальная ширина вычисляется как сумма минимальных ширин
+ /// дочерних элементов при горизонтальной ориентации или максимум минимальных
+ /// ширин при вертикальной ориентации.
+ ///
+ public double MinWidth => Orientation == SplitDirection.Horizontal
+ ? First.MinWidth + Second.MinWidth
+ : Math.Max(First.MinWidth, Second.MinWidth);
+
+ ///
+ /// Получает минимально допустимую высоту элемента.
+ ///
+ ///
+ /// Минимальная высота в пикселях, при которой элемент сохраняет функциональность.
+ ///
+ ///
+ /// Для группы минимальная высота вычисляется как сумма минимальных высот
+ /// дочерних элементов при вертикальной ориентации или максимум минимальных
+ /// высот при горизонтальной ориентации.
+ ///
+ public double MinHeight => Orientation == SplitDirection.Vertical
+ ? First.MinHeight + Second.MinHeight
+ : Math.Max(First.MinHeight, Second.MinHeight);
+
+ ///
+ /// Инициализирует новый экземпляр класса .
+ ///
+ ///
+ /// Первый дочерний элемент (левая или верхняя область).
+ ///
+ ///
+ /// Второй дочерний элемент (правая или нижняя область).
+ ///
+ ///
+ /// Направление разделения между дочерними элементами.
+ ///
+ ///
+ /// Уникальный идентификатор группы. Если не указан, генерируется новый GUID.
+ ///
+ ///
+ /// Выбрасывается, когда или
+ /// равны null.
+ ///
+ ///
+ /// Конструктор автоматически устанавливает свойство
+ /// у дочерних элементов на текущую группу и генерирует уникальный идентификатор,
+ /// если он не был предоставлен.
+ ///
+ public DockGroup(IDockElement first, IDockElement second, SplitDirection orientation, string? id = null)
+ {
+ First = first ?? throw new ArgumentNullException(nameof(first));
+ Second = second ?? throw new ArgumentNullException(nameof(second));
+ Orientation = orientation;
+ Id = id ?? Guid.NewGuid().ToString();
+
+ First.Parent = this;
+ Second.Parent = this;
+ }
+
+ ///
+ /// Вызывает событие .
+ ///
+ ///
+ /// Имя изменившегося свойства. Если не указано, определяется автоматически.
+ ///
+ protected void OnPropertyChanged([CallerMemberName] string? name = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
+ }
+
+ #region Реализация IDragSource
+
+ ///
+ /// Определяет, может ли группа начать операцию перетаскивания.
+ ///
+ ///
+ /// При успешном возврате содержит информацию о перетаскивании;
+ /// в противном случае — null.
+ ///
+ ///
+ /// true, если группа может начать перетаскивание; в противном случае — false.
+ ///
+ ///
+ ///
+ /// Группа может быть перетащена только если она не является корневым
+ /// элементом дерева (имеет родителя).
+ ///
+ ///
+ /// При успешной проверке метод заполняет
+ /// данными типа .
+ ///
+ ///
+ public bool CanStartDrag(out DragInfo? dragInfo)
+ {
+ dragInfo = null;
+
+ // DockGroup можно перетаскивать только если он не является корневым элементом
+ if (Parent == null)
+ return false;
+
+ // Создаем данные для перетаскивания
+ var data = new DockElementDragData
+ {
+ ElementId = Id,
+ ElementType = GetType().Name,
+ IsGroup = true
+ };
+
+ dragInfo = new DragInfo(data, DragDropEffects.Move, Point.Zero, this);
+ return true;
+ }
+
+ ///
+ /// Начинает операцию перетаскивания для группы.
+ ///
+ ///
+ /// Информация о перетаскивании, полученная из .
+ ///
+ ///
+ /// Всегда возвращает true, так как группа не требует специальной подготовки
+ /// для начала перетаскивания.
+ ///
+ ///
+ /// Для этот метод не выполняет дополнительных действий,
+ /// так как все необходимые данные уже содержатся в .
+ ///
+ public bool StartDrag(DragInfo dragInfo)
+ {
+ // DockGroup не требует дополнительной подготовки для перетаскивания
+ return true;
+ }
+
+ ///
+ /// Вызывается при завершении операции перетаскивания.
+ ///
+ ///
+ /// Исходная информация о перетаскивании.
+ ///
+ ///
+ /// Эффекты, которые были применены при сбросе.
+ ///
+ ///
+ ///
+ /// Этот метод вызывается после того, как операция перетаскивания была
+ /// завершена (успешно или неуспешно).
+ ///
+ ///
+ /// Для этот метод не выполняет действий, так как
+ /// все изменения в структуре дерева уже обработаны .
+ ///
+ ///
+ public void DragCompleted(DragInfo dragInfo, DragDropEffects effects)
+ {
+ // Если группа была перемещена, ничего не делаем - LayoutManager уже обработал изменение
+ }
+
+ ///
+ /// Вызывается при отмене операции перетаскивания.
+ ///
+ ///
+ /// Исходная информация о перетаскивании.
+ ///
+ ///
+ /// Для отмена перетаскивания не требует специальных
+ /// действий, так как структура дерева не была изменена.
+ ///
+ public void DragCancelled(DragInfo dragInfo)
+ {
+ // Отмена перетаскивания не требует действий
+ }
+
+ #endregion
+
+ #region Реализация IDropTarget
+
+ ///
+ /// Определяет, может ли группа принять сбрасываемые данные.
+ ///
+ ///
+ /// Информация о потенциальном сбросе.
+ ///
+ ///
+ /// true, если группа может принять данные; в противном случае — false.
+ ///
+ ///
+ ///
+ /// Группа может принимать только данные типа
+ /// для элементов док-системы ( или ).
+ ///
+ ///
+ /// Группа не может принять сброс самой себя (проверяется по идентификатору).
+ ///
+ ///
+ public bool CanAcceptDrop(DropInfo dropInfo)
+ {
+ if (dropInfo.Data is not DockElementDragData dragData)
+ return false;
+
+ // Нельзя сбросить элемент на самого себя
+ if (dragData.ElementId == Id)
+ return false;
+
+ // Можно принимать только элементы док-системы
+ return dragData.ElementType == nameof(DockGroup) || dragData.ElementType == nameof(DockLeaf);
+ }
+
+ ///
+ /// Вызывается, когда перетаскиваемый объект находится над группой.
+ ///
+ ///
+ /// Информация о текущем положении перетаскивания.
+ ///
+ ///
+ ///
+ /// Этот метод вызывается постоянно, пока пользователь перемещает объект
+ /// над целью. Для группы он устанавливает предлагаемые эффекты в
+ /// .
+ ///
+ ///
+ /// Если группа может принять сброс, предлагается эффект перемещения;
+ /// в противном случае эффекты не предлагаются.
+ ///
+ ///
+ public void DragOver(DropInfo dropInfo)
+ {
+ if (CanAcceptDrop(dropInfo))
+ {
+ dropInfo.SuggestedEffects = DragDropEffects.Move;
+ }
+ else
+ {
+ dropInfo.SuggestedEffects = DragDropEffects.None;
+ }
+ }
+
+ ///
+ /// Вызывается, когда пользователь сбрасывает данные на группу.
+ ///
+ ///
+ /// Информация о сбросе.
+ ///
+ ///
+ ///
+ /// Для обработка сброса делегируется
+ /// , поэтому метод просто помечает операцию
+ /// как обработанную.
+ ///
+ ///
+ /// Фактическое изменение структуры дерева выполняется менеджером макета
+ /// на основе данных из .
+ ///
+ ///
+ public void Drop(DropInfo dropInfo)
+ {
+ // Обработка сброса делегируется LayoutManager
+ dropInfo.MarkAsHandled();
+ }
+
+ ///
+ /// Вызывается, когда перетаскиваемый объект покидает область группы.
+ ///
+ ///
+ /// Для группы этот метод не выполняет действий, так как очистка визуальной
+ /// обратной связи выполняется в UI-слое.
+ ///
+ public void DragLeave()
+ {
+ // Очистка визуальной обратной связи (будет выполнена в UI слое)
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/Lattice.Core.Docking/Models/DockLeaf.cs b/Lattice.Core.Docking/Models/DockLeaf.cs
new file mode 100644
index 0000000..f8a7537
--- /dev/null
+++ b/Lattice.Core.Docking/Models/DockLeaf.cs
@@ -0,0 +1,580 @@
+using Lattice.Core.Docking.Abstractions;
+using Lattice.Core.DragDrop.Abstractions;
+using Lattice.Core.DragDrop.Enums;
+using Lattice.Core.DragDrop.Models;
+using Lattice.Core.Geometry;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace Lattice.Core.Docking.Models;
+
+///
+/// Представляет конечный узел (лист) дерева компоновки, который непосредственно
+/// содержит коллекцию вкладок с контентом. Этот класс является контейнером для
+/// отображаемого пользователю содержимого.
+///
+///
+///
+/// реализует интерфейсы ,
+/// и , что позволяет ему:
+///
+///
+/// Управлять коллекцией вкладок
+/// Быть источником перетаскивания (как всего листа, так и отдельных вкладок)
+/// Принимать сброс других элементов или вкладок
+///
+///
+/// Лист является основным элементом, с которым взаимодействует пользователь
+/// при работе с документами или инструментальными панелями в IDE-подобных
+/// приложениях.
+///
+///
+public class DockLeaf : IDockContainer, INotifyPropertyChanged, IDragSource, IDropTarget
+{
+ ///
+ /// Событие, возникающее при изменении значения свойства.
+ ///
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ private readonly ObservableCollection _items = new();
+ private IDockContent? _activeContent;
+ private string _id;
+
+ ///
+ /// Получает уникальный идентификатор листа.
+ ///
+ ///
+ /// Строковый идентификатор, уникальный в пределах дерева компоновки.
+ ///
+ public string Id
+ {
+ get => _id;
+ internal set
+ {
+ if (_id != value)
+ {
+ _id = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ ///
+ /// Получает или задает родительский элемент в иерархии дерева компоновки.
+ ///
+ ///
+ /// Родительский элемент или null, если этот лист является корневым.
+ ///
+ public IDockElement? Parent { get; set; }
+
+ ///
+ /// Получает список вкладок, содержащихся в данном контейнере.
+ ///
+ ///
+ /// Коллекция объектов, реализующих .
+ ///
+ ///
+ /// Эта коллекция является наблюдаемой (ObservableCollection), что позволяет
+ /// автоматически обновлять пользовательский интерфейс при добавлении или
+ /// удалении вкладок.
+ ///
+ public IList Children => _items;
+
+ ///
+ /// Получает или задает активную (выбранную) вкладку в контейнере.
+ ///
+ ///
+ /// Активная вкладка или null, если в контейнере нет вкладок.
+ ///
+ ///
+ ///
+ /// При установке нового значения проверяется, что вкладка действительно
+ /// содержится в коллекции .
+ ///
+ ///
+ /// Изменение этого свойства вызывает событие .
+ ///
+ ///
+ public IDockContent? ActiveContent
+ {
+ get => _activeContent;
+ set
+ {
+ if (value != null && !_items.Contains(value)) return;
+ if (_activeContent != value)
+ {
+ _activeContent = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ ///
+ /// Получает или задает желаемую ширину элемента.
+ ///
+ ///
+ /// Ширина в пикселях или относительных единицах.
+ ///
+ public double Width { get; set; }
+
+ ///
+ /// Получает или задает желаемую высоту элемента.
+ ///
+ ///
+ /// Высота в пикселях или относительных единицах.
+ ///
+ public double Height { get; set; }
+
+ ///
+ /// Получает или задает минимально допустимую ширину элемента.
+ ///
+ ///
+ /// Минимальная ширина в пикселях. Значение по умолчанию: 100.
+ ///
+ public double MinWidth { get; set; } = 100;
+
+ ///
+ /// Получает или задает минимально допустимую высоту элемента.
+ ///
+ ///
+ /// Минимальная высота в пикселях. Значение по умолчанию: 100.
+ ///
+ public double MinHeight { get; set; } = 100;
+
+ ///
+ /// Получает или задает положение полосы вкладок в контейнере.
+ ///
+ ///
+ /// Значение перечисления , определяющее,
+ /// где располагаются вкладки относительно содержимого.
+ ///
+ ///
+ /// Поддерживаются все четыре стороны: верх, низ, лево, право.
+ ///
+ public TabPlacement TabPlacement { get; set; } = TabPlacement.Bottom;
+
+ ///
+ /// Инициализирует новый экземпляр класса .
+ ///
+ ///
+ /// Уникальный идентификатор листа. Если не указан, генерируется новый GUID.
+ ///
+ ///
+ /// Создает пустой лист с коллекцией вкладок и генерирует уникальный
+ /// идентификатор, если он не был предоставлен.
+ ///
+ public DockLeaf(string? id = null)
+ {
+ _id = id ?? Guid.NewGuid().ToString();
+ }
+
+ ///
+ /// Вызывает событие .
+ ///
+ ///
+ /// Имя изменившегося свойства. Если не указано, определяется автоматически.
+ ///
+ protected void OnPropertyChanged([CallerMemberName] string? name = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
+ }
+
+ ///
+ /// Добавляет контент в контейнер и делает его активным.
+ ///
+ ///
+ /// Контент для добавления.
+ ///
+ ///
+ ///
+ /// Если контент уже содержится в коллекции, он не добавляется повторно,
+ /// но становится активным.
+ ///
+ ///
+ /// Этот метод обновляет свойство и вызывает
+ /// соответствующее событие изменения свойства.
+ ///
+ ///
+ public void AddContent(IDockContent content)
+ {
+ if (!_items.Contains(content))
+ {
+ _items.Add(content);
+ }
+ ActiveContent = content;
+ }
+
+ ///
+ /// Удаляет контент из контейнера.
+ ///
+ ///
+ /// Контент для удаления.
+ ///
+ ///
+ ///
+ /// Если удаляемый контент является активным, автоматически выбирается
+ /// новая активная вкладка (следующая в списке или предыдущая, если удалена
+ /// последняя).
+ ///
+ ///
+ /// Если после удаления контейнер становится пустым, он может быть удален
+ /// из дерева макета системой компоновки.
+ ///
+ ///
+ public void RemoveContent(IDockContent content)
+ {
+ int index = _items.IndexOf(content);
+ if (index == -1) return;
+
+ _items.RemoveAt(index);
+
+ if (ActiveContent == content)
+ {
+ if (_items.Count > 0)
+ ActiveContent = _items[Math.Min(index, _items.Count - 1)];
+ else
+ ActiveContent = null;
+ }
+ }
+
+ #region Реализация IDragSource
+
+ ///
+ /// Определяет, может ли лист начать операцию перетаскивания.
+ ///
+ ///
+ /// При успешном возврате содержит информацию о перетаскивании;
+ /// в противном случае — null.
+ ///
+ ///
+ /// true, если лист может начать перетаскивание; в противном случае — false.
+ ///
+ ///
+ ///
+ /// Лист может быть перетащен, если:
+ ///
+ ///
+ /// Он имеет родителя (не является корневым)
+ /// Или имеет хотя бы одну вкладку (не пустой)
+ ///
+ ///
+ /// В зависимости от наличия активного контента создаются разные данные:
+ ///
+ ///
+ ///
+ /// Если есть активный контент - создается
+ /// для перетаскивания конкретной вкладки
+ ///
+ ///
+ /// Если нет активного контента - создается
+ /// для перетаскивания всего листа
+ ///
+ ///
+ ///
+ public bool CanStartDrag(out DragInfo? dragInfo)
+ {
+ dragInfo = null;
+
+ // DockLeaf можно перетаскивать
+ if (Parent == null && Children.Count == 0)
+ return false; // Не перетаскиваем пустые корневые листья
+
+ object data;
+
+ // Если есть активный контент, перетаскиваем контент, иначе перетаскиваем весь лист
+ if (ActiveContent != null)
+ {
+ data = new ContentDragData
+ {
+ ElementId = Id,
+ ContentId = ActiveContent.Id,
+ ContentTitle = ActiveContent.Title,
+ ContentType = ActiveContent.GetType().Name
+ };
+ }
+ else
+ {
+ data = new DockElementDragData
+ {
+ ElementId = Id,
+ ElementType = GetType().Name,
+ IsGroup = false,
+ Width = Width,
+ Height = Height
+ };
+ }
+
+ dragInfo = new DragInfo(data, DragDropEffects.Move | DragDropEffects.Copy, Point.Zero, this);
+ return true;
+ }
+
+ ///
+ /// Начинает операцию перетаскивания для листа.
+ ///
+ ///
+ /// Информация о перетаскивании.
+ ///
+ ///
+ /// Всегда возвращает true.
+ ///
+ ///
+ /// Для этот метод не выполняет дополнительных действий.
+ ///
+ public bool StartDrag(DragInfo dragInfo)
+ {
+ // DockLeaf не требует дополнительной подготовки
+ return true;
+ }
+
+ ///
+ /// Вызывается при завершении операции перетаскивания.
+ ///
+ ///
+ /// Исходная информация о перетаскивании.
+ ///
+ ///
+ /// Эффекты, которые были применены при сбросе.
+ ///
+ ///
+ /// Для этот метод не выполняет действий.
+ ///
+ public void DragCompleted(DragInfo dragInfo, DragDropEffects effects)
+ {
+ // Если лист был перемещен или скопирован, LayoutManager уже обработал это
+ }
+
+ ///
+ /// Вызывается при отмене операции перетаскивания.
+ ///
+ ///
+ /// Исходная информация о перетаскивании.
+ ///
+ ///
+ /// Для отмена перетаскивания не требует действий.
+ ///
+ public void DragCancelled(DragInfo dragInfo)
+ {
+ // Отмена не требует действий
+ }
+
+ #endregion
+
+ #region Реализация IDropTarget
+
+ ///
+ /// Определяет, может ли лист принять сбрасываемые данные.
+ ///
+ ///
+ /// Информация о потенциальном сбросе.
+ ///
+ ///
+ /// true, если лист может принять данные; в противном случае — false.
+ ///
+ ///
+ /// Лист может принимать:
+ ///
+ ///
+ /// для других листов и групп
+ /// (для объединения или разделения)
+ ///
+ ///
+ /// для вкладок (для объединения вкладок)
+ ///
+ ///
+ ///
+ public bool CanAcceptDrop(DropInfo dropInfo)
+ {
+ if (dropInfo.Data is DockElementDragData elementData)
+ {
+ // Можно принимать другие листы и группы
+ return elementData.ElementType == nameof(DockLeaf) ||
+ elementData.ElementType == nameof(DockGroup);
+ }
+ else if (dropInfo.Data is ContentDragData contentData)
+ {
+ // Можно принимать контент для объединения вкладок
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Вызывается, когда перетаскиваемый объект находится над листом.
+ ///
+ ///
+ /// Информация о текущем положении перетаскивания.
+ ///
+ ///
+ ///
+ /// В зависимости от типа данных устанавливаются разные предлагаемые эффекты:
+ ///
+ ///
+ ///
+ /// Для - эффект копирования (объединение вкладок)
+ ///
+ ///
+ /// Для - эффект перемещения
+ ///
+ ///
+ ///
+ public void DragOver(DropInfo dropInfo)
+ {
+ if (CanAcceptDrop(dropInfo))
+ {
+ if (dropInfo.Data is ContentDragData)
+ {
+ // Для контента предлагаем копирование (объединение вкладок)
+ dropInfo.SuggestedEffects = DragDropEffects.Copy;
+ }
+ else
+ {
+ // Для элементов предлагаем перемещение
+ dropInfo.SuggestedEffects = DragDropEffects.Move;
+ }
+ }
+ else
+ {
+ dropInfo.SuggestedEffects = DragDropEffects.None;
+ }
+ }
+
+ ///
+ /// Вызывается, когда пользователь сбрасывает данные на лист.
+ ///
+ ///
+ /// Информация о сбросе.
+ ///
+ ///
+ /// Обработка сброса делегируется .
+ ///
+ public void Drop(DropInfo dropInfo)
+ {
+ // Обработка делегируется LayoutManager
+ dropInfo.MarkAsHandled();
+ }
+
+ ///
+ /// Вызывается, когда перетаскиваемый объект покидает область листа.
+ ///
+ ///
+ /// Очистка визуальной обратной связи выполняется в UI-слое.
+ ///
+ public void DragLeave()
+ {
+ // Очистка визуальной обратной связи
+ }
+
+ #endregion
+}
+
+///
+/// Представляет данные для перетаскивания элементов док-системы (групп или листов).
+/// Используется при перетаскивании целых структурных элементов дерева компоновки.
+///
+///
+/// Этот класс сериализуется и передается между компонентами системы перетаскивания
+/// для идентификации перетаскиваемого элемента и его свойств.
+///
+public class DockElementDragData
+{
+ ///
+ /// Получает или задает уникальный идентификатор элемента.
+ ///
+ ///
+ /// Идентификатор элемента, соответствующий свойству .
+ ///
+ public string ElementId { get; set; } = string.Empty;
+
+ ///
+ /// Получает или задает тип элемента.
+ ///
+ ///
+ /// Имя типа элемента (обычно "DockGroup" или "DockLeaf").
+ ///
+ public string ElementType { get; set; } = string.Empty;
+
+ ///
+ /// Получает или задает значение, указывающее, является ли элемент группой.
+ ///
+ ///
+ /// true, если элемент является ; false, если .
+ ///
+ public bool IsGroup { get; set; }
+
+ ///
+ /// Получает или задает идентификатор родительского элемента.
+ ///
+ ///
+ /// Идентификатор родительского элемента или null, если элемент корневой.
+ ///
+ public string? ParentId { get; set; }
+
+ ///
+ /// Получает или задает ширину элемента.
+ ///
+ ///
+ /// Текущая ширина элемента в пикселях.
+ ///
+ public double Width { get; set; }
+
+ ///
+ /// Получает или задает высоту элемента.
+ ///
+ ///
+ /// Текущая высота элемента в пикселях.
+ ///
+ public double Height { get; set; }
+}
+
+///
+/// Представляет данные для перетаскивания контента (вкладок).
+/// Используется при перетаскивании отдельных вкладок между контейнерами.
+///
+///
+/// Этот класс позволяет идентифицировать конкретную вкладку для операций
+/// объединения или перемещения между контейнерами.
+///
+public class ContentDragData
+{
+ ///
+ /// Получает или задает идентификатор контейнера (листа), содержащего контент.
+ ///
+ ///
+ /// Идентификатор , в котором находится перетаскиваемая вкладка.
+ ///
+ public string ElementId { get; set; } = string.Empty;
+
+ ///
+ /// Получает или задает уникальный идентификатор контента.
+ ///
+ ///
+ /// Идентификатор контента, соответствующий свойству .
+ ///
+ public string ContentId { get; set; } = string.Empty;
+
+ ///
+ /// Получает или задает заголовок контента.
+ ///
+ ///
+ /// Текст, отображаемый на вкладке.
+ ///
+ public string ContentTitle { get; set; } = string.Empty;
+
+ ///
+ /// Получает или задает тип контента.
+ ///
+ ///
+ /// Имя типа контента (например, "TextEditor", "Toolbox", и т.д.).
+ ///
+ public string ContentType { get; set; } = string.Empty;
+
+ ///
+ /// Получает или задает значение, указывающее, можно ли закрыть контент.
+ ///
+ ///
+ /// true, если контент можно закрыть; в противном случае — false.
+ ///
+ public bool CanClose { get; set; } = true;
+}
\ No newline at end of file
diff --git a/Lattice.Core.Docking/Models/DockPosition.cs b/Lattice.Core.Docking/Models/DockPosition.cs
new file mode 100644
index 0000000..f34ea72
--- /dev/null
+++ b/Lattice.Core.Docking/Models/DockPosition.cs
@@ -0,0 +1,13 @@
+namespace Lattice.Core.Docking.Models;
+
+///
+/// Определяет позицию вставки при операции Drag-and-Drop.
+///
+public enum DockPosition
+{
+ Left,
+ Right,
+ Top,
+ Bottom,
+ Center,
+}
diff --git a/Lattice.Core.Docking/Models/DockSide.cs b/Lattice.Core.Docking/Models/DockSide.cs
new file mode 100644
index 0000000..6975ee6
--- /dev/null
+++ b/Lattice.Core.Docking/Models/DockSide.cs
@@ -0,0 +1,19 @@
+namespace Lattice.Core.Docking.Models;
+
+///
+/// Определяет стороны окна, к которым могут быть прикреплены автоскрываемые панели.
+///
+public enum DockSide
+{
+ /// Левая сторона окна.
+ Left,
+
+ /// Правая сторона окна.
+ Right,
+
+ /// Верхняя сторона окна.
+ Top,
+
+ /// Нижняя сторона окна.
+ Bottom
+}
\ No newline at end of file
diff --git a/Lattice.Core.Docking/Models/DockWindow.cs b/Lattice.Core.Docking/Models/DockWindow.cs
new file mode 100644
index 0000000..cb73925
--- /dev/null
+++ b/Lattice.Core.Docking/Models/DockWindow.cs
@@ -0,0 +1,23 @@
+using Lattice.Core.Docking.Abstractions;
+
+namespace Lattice.Core.Docking.Models;
+
+///
+/// Описывает состояние плавающего окна в системе Lattice.
+///
+public class DockWindow
+{
+ /// Уникальный ID окна для сохранения его позиции в конфиге.
+ public string Id { get; } = Guid.NewGuid().ToString();
+
+ /// Корневой элемент макета внутри данного окна.
+ public IDockElement? Root { get; set; }
+
+ public double X { get; set; }
+ public double Y { get; set; }
+ public double Width { get; set; } = 800;
+ public double Height { get; set; } = 600;
+
+ /// Заголовок окна (обычно берется из активного контента).
+ public string Title { get; set; } = "Lattice Tool Window";
+}
diff --git a/Lattice.Core.Docking/Models/SplitDirection.cs b/Lattice.Core.Docking/Models/SplitDirection.cs
new file mode 100644
index 0000000..faee1bf
--- /dev/null
+++ b/Lattice.Core.Docking/Models/SplitDirection.cs
@@ -0,0 +1,12 @@
+namespace Lattice.Core.Docking.Models;
+
+///
+/// Перечисление направлений разделения пространства внутри группы.
+///
+public enum SplitDirection
+{
+ /// Разделение по горизонтали (создает левую и правую области).
+ Horizontal,
+ /// Разделение по вертикали (создает верхнюю и нижнюю области).
+ Vertical
+}
diff --git a/Lattice.Core.Docking/Models/TabPlacement.cs b/Lattice.Core.Docking/Models/TabPlacement.cs
new file mode 100644
index 0000000..5dbd7d4
--- /dev/null
+++ b/Lattice.Core.Docking/Models/TabPlacement.cs
@@ -0,0 +1,12 @@
+namespace Lattice.Core.Docking.Models;
+
+///
+/// Определяет положение полосы вкладок в контейнере.
+///
+public enum TabPlacement
+{
+ Top,
+ Bottom,
+ Left,
+ Right,
+}
\ No newline at end of file
diff --git a/Lattice.Core.Docking/Serialization/ILayoutSerializer.cs b/Lattice.Core.Docking/Serialization/ILayoutSerializer.cs
new file mode 100644
index 0000000..a4b6246
--- /dev/null
+++ b/Lattice.Core.Docking/Serialization/ILayoutSerializer.cs
@@ -0,0 +1,31 @@
+namespace Lattice.Core.Docking.Serialization;
+
+///
+/// Абстракция для сериализации и десериализации состояния макета док-системы.
+/// Позволяет сохранять и восстанавливать расположение панелей, окон и их состояние.
+///
+///
+/// Эта абстракция позволяет реализовать различные форматы сериализации (JSON, XML, бинарный)
+/// и различные хранилища (файлы, базы данных, облако) без изменения основной логики док-системы.
+///
+public interface ILayoutSerializer
+{
+ ///
+ /// Сериализует состояние менеджера макета в строку.
+ ///
+ /// Менеджер макета для сериализации.
+ /// Строковое представление состояния макета.
+ string Serialize(Engine.LayoutManager manager);
+
+ ///
+ /// Десериализует состояние макета из строки и восстанавливает его в менеджере.
+ ///
+ /// Менеджер макета для восстановления состояния.
+ /// Сериализованное состояние макета.
+ ///
+ /// Функция разрешения контента по идентификатору, используемая для восстановления
+ /// ссылок на контент в десериализованном состоянии.
+ ///
+ void Deserialize(Engine.LayoutManager manager, string serializedLayout,
+ Func contentResolver);
+}
diff --git a/Lattice.Core.Docking/Serialization/ISerializableLayout.cs b/Lattice.Core.Docking/Serialization/ISerializableLayout.cs
new file mode 100644
index 0000000..92167ed
--- /dev/null
+++ b/Lattice.Core.Docking/Serialization/ISerializableLayout.cs
@@ -0,0 +1,19 @@
+namespace Lattice.Core.Docking.Serialization;
+
+///
+/// Контракт для объектов, которые могут предоставлять состояние для сериализации.
+///
+public interface ISerializableLayout
+{
+ ///
+ /// Получает состояние для сериализации.
+ ///
+ /// Объект состояния, готовый к сериализации.
+ object GetSerializableState();
+
+ ///
+ /// Восстанавливает состояние из десериализованного объекта.
+ ///
+ /// Десериализованное состояние.
+ void RestoreFromState(object state);
+}
\ No newline at end of file
diff --git a/Lattice.Core.Docking/Services/ContentRegistry.cs b/Lattice.Core.Docking/Services/ContentRegistry.cs
new file mode 100644
index 0000000..0daee2c
--- /dev/null
+++ b/Lattice.Core.Docking/Services/ContentRegistry.cs
@@ -0,0 +1,158 @@
+namespace Lattice.Core.Docking.Services;
+
+///
+/// Реестр типов содержимого, который позволяет создавать экземпляры контента по типу.
+/// Этот сервис является центральным для динамического создания панелей инструментов и документов в IDE.
+///
+///
+/// Реализует шаблон "Фабрика" для создания экземпляров .
+/// Позволяет регистрировать фабричные методы для различных типов контента, что обеспечивает
+/// позднее связывание и возможность плагинной архитектуры.
+///
+public class ContentRegistry
+{
+ private readonly Dictionary _contentTypes = new();
+
+ ///
+ /// Регистрирует фабричный метод для создания контента указанного типа.
+ ///
+ /// Тип контента, реализующий .
+ /// Уникальный идентификатор типа контента.
+ /// Фабричный метод для создания экземпляров контента.
+ /// Метаданные типа контента (опционально).
+ /// Выбрасывается, если contentTypeId или factory равны null.
+ /// Выбрасывается, если contentTypeId уже зарегистрирован.
+ public void Register(string contentTypeId, Func factory, ContentMetadata? metadata = null)
+ where T : Abstractions.IDockContent
+ {
+ if (string.IsNullOrWhiteSpace(contentTypeId))
+ throw new ArgumentNullException(nameof(contentTypeId));
+ if (factory == null)
+ throw new ArgumentNullException(nameof(factory));
+
+ if (_contentTypes.ContainsKey(contentTypeId))
+ throw new ArgumentException($"Content type '{contentTypeId}' is already registered.");
+
+ _contentTypes[contentTypeId] = new ContentDescriptor(
+ typeof(T),
+ () => factory(),
+ metadata ?? new ContentMetadata(contentTypeId, typeof(T).Name)
+ );
+ }
+
+ ///
+ /// Создает новый экземпляр контента указанного типа с заданным идентификатором.
+ ///
+ /// Идентификатор типа контента.
+ /// Уникальный идентификатор для создаваемого экземпляра контента.
+ /// Новый экземпляр контента.
+ /// Выбрасывается, если тип контента не зарегистрирован.
+ public Abstractions.IDockContent CreateContent(string contentTypeId, string id)
+ {
+ if (!_contentTypes.TryGetValue(contentTypeId, out var descriptor))
+ throw new KeyNotFoundException($"Content type '{contentTypeId}' is not registered.");
+
+ var content = descriptor.Factory();
+ // Устанавливаем ID через рефлексию, если есть свойство Id
+ var property = content.GetType().GetProperty("Id");
+ if (property != null && property.CanWrite)
+ {
+ property.SetValue(content, id);
+ }
+
+ return content;
+ }
+
+ ///
+ /// Получает метаданные для указанного типа контента.
+ ///
+ /// Идентификатор типа контента.
+ /// Метаданные типа контента или null, если тип не найден.
+ public ContentMetadata? GetMetadata(string contentTypeId)
+ {
+ return _contentTypes.TryGetValue(contentTypeId, out var descriptor)
+ ? descriptor.Metadata
+ : null;
+ }
+
+ ///
+ /// Получает все зарегистрированные типы контента.
+ ///
+ /// Коллекция идентификаторов зарегистрированных типов контента.
+ public IEnumerable GetRegisteredTypes() => _contentTypes.Keys;
+
+ ///
+ /// Проверяет, зарегистрирован ли указанный тип контента.
+ ///
+ public bool IsRegistered(string contentTypeId) => _contentTypes.ContainsKey(contentTypeId);
+
+ ///
+ /// Дескриптор типа контента, содержащий информацию о фабричном методе и метаданных.
+ ///
+ private class ContentDescriptor
+ {
+ public Type ContentType { get; }
+ public Func Factory { get; }
+ public ContentMetadata Metadata { get; }
+
+ public ContentDescriptor(Type contentType, Func factory, ContentMetadata metadata)
+ {
+ ContentType = contentType;
+ Factory = factory;
+ Metadata = metadata;
+ }
+ }
+}
+
+///
+/// Метаданные типа контента, предоставляющие дополнительную информацию для отображения в UI.
+///
+public class ContentMetadata
+{
+ ///
+ /// Идентификатор типа контента.
+ ///
+ public string ContentTypeId { get; }
+
+ ///
+ /// Отображаемое имя типа контента.
+ ///
+ public string DisplayName { get; set; }
+
+ ///
+ /// Описание типа контента.
+ ///
+ public string Description { get; set; }
+
+ ///
+ /// Имя ресурса для иконки (опционально).
+ ///
+ public string? IconResource { get; set; }
+
+ ///
+ /// Признак того, что контент является документом (а не инструментальной панелью).
+ ///
+ public bool IsDocument { get; set; }
+
+ ///
+ /// Минимальная ширина контента в пикселях.
+ ///
+ public double DefaultWidth { get; set; } = 300;
+
+ ///
+ /// Минимальная высота контента в пикселях.
+ ///
+ public double DefaultHeight { get; set; } = 200;
+
+ ///
+ /// Инициализирует новый экземпляр метаданных контента.
+ ///
+ /// Идентификатор типа контента.
+ /// Отображаемое имя.
+ public ContentMetadata(string contentTypeId, string displayName)
+ {
+ ContentTypeId = contentTypeId;
+ DisplayName = displayName;
+ Description = string.Empty;
+ }
+}
\ No newline at end of file
diff --git a/Lattice.Core.DragDrop.Tests/DragDropServiceTests.cs b/Lattice.Core.DragDrop.Tests/DragDropServiceTests.cs
new file mode 100644
index 0000000..4b02109
--- /dev/null
+++ b/Lattice.Core.DragDrop.Tests/DragDropServiceTests.cs
@@ -0,0 +1,115 @@
+using Lattice.Core.DragDrop.Abstractions;
+using Lattice.Core.DragDrop.Enums;
+using Lattice.Core.DragDrop.Models;
+using Lattice.Core.DragDrop.Services;
+using Lattice.Core.Geometry;
+using Moq;
+using Xunit;
+
+namespace Lattice.Core.DragDrop.Tests;
+
+public class DragDropServiceTests
+{
+ [Fact]
+ public void StartDrag_WithValidSource_StartsDragOperation()
+ {
+ // Arrange
+ var service = new DragDropService();
+ var mockSource = new Mock();
+ var dragInfo = new DragInfo("test", DragDropEffects.Copy, new Point(0, 0));
+
+ mockSource.Setup(s => s.CanStartDrag(out dragInfo)).Returns(true);
+ mockSource.Setup(s => s.StartDrag(It.IsAny())).Returns(true);
+
+ // Act
+ var result = service.StartDrag(mockSource.Object, new Point(0, 0));
+
+ // Assert
+ Assert.True(result);
+ Assert.True(service.IsDragActive);
+ Assert.NotNull(service.CurrentDragInfo);
+ }
+
+ [Fact]
+ public void RegisterDropTarget_ReturnsValidId()
+ {
+ // Arrange
+ var service = new DragDropService();
+ var mockTarget = new Mock();
+ var bounds = new Rect(0, 0, 100, 100);
+
+ // Act
+ var id = service.RegisterDropTarget(mockTarget.Object, bounds);
+
+ // Assert
+ Assert.NotNull(id);
+ Assert.NotEmpty(id);
+ }
+
+ [Fact]
+ public void UpdateDrag_WithValidDropTarget_CallsDragOver()
+ {
+ // Arrange
+ var service = new DragDropService();
+ var mockSource = new Mock();
+ var mockTarget = new Mock();
+
+ var dragInfo = new DragInfo("test", DragDropEffects.Copy, new Point(0, 0));
+ mockSource.Setup(s => s.CanStartDrag(out dragInfo)).Returns(true);
+ mockSource.Setup(s => s.StartDrag(It.IsAny())).Returns(true);
+
+ var targetId = service.RegisterDropTarget(mockTarget.Object, new Rect(0, 0, 100, 100));
+ service.StartDrag(mockSource.Object, new Point(0, 0));
+
+ // Act
+ service.UpdateDrag(new Point(50, 50));
+
+ // Assert
+ mockTarget.Verify(t => t.DragOver(It.IsAny()), Times.AtLeastOnce());
+ }
+
+ [Fact]
+ public void EndDrag_WithValidDrop_CallsDrop()
+ {
+ // Arrange
+ var service = new DragDropService();
+ var mockSource = new Mock();
+ var mockTarget = new Mock();
+
+ var dragInfo = new DragInfo("test", DragDropEffects.Copy, new Point(0, 0));
+ mockSource.Setup(s => s.CanStartDrag(out dragInfo)).Returns(true);
+ mockSource.Setup(s => s.StartDrag(It.IsAny())).Returns(true);
+
+ service.RegisterDropTarget(mockTarget.Object, new Rect(0, 0, 100, 100));
+ service.StartDrag(mockSource.Object, new Point(0, 0));
+ service.UpdateDrag(new Point(50, 50));
+
+ // Act
+ var effects = service.EndDrag(new Point(50, 50));
+
+ // Assert
+ mockTarget.Verify(t => t.Drop(It.IsAny()), Times.Once());
+ Assert.False(service.IsDragActive);
+ }
+
+ [Fact]
+ public void CancelDrag_WithActiveDrag_CallsDragCancelled()
+ {
+ // Arrange
+ var service = new DragDropService();
+ var mockSource = new Mock();
+ var dragInfo = new DragInfo("test", DragDropEffects.Copy, new Point(0, 0));
+
+ mockSource.Setup(s => s.CanStartDrag(out dragInfo)).Returns(true);
+ mockSource.Setup(s => s.StartDrag(It.IsAny())).Returns(true);
+
+ service.StartDrag(mockSource.Object, new Point(0, 0));
+
+ // Act
+ service.CancelDrag();
+
+ // Assert
+ mockSource.Verify(s => s.DragCancelled(It.IsAny()), Times.Once());
+ Assert.False(service.IsDragActive);
+ }
+}
\ No newline at end of file
diff --git a/Lattice.Core.DragDrop.Tests/Lattice.Core.DragDrop.Tests.csproj b/Lattice.Core.DragDrop.Tests/Lattice.Core.DragDrop.Tests.csproj
new file mode 100644
index 0000000..32a369c
--- /dev/null
+++ b/Lattice.Core.DragDrop.Tests/Lattice.Core.DragDrop.Tests.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net8.0
+ enable
+ false
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Lattice.Core.DragDrop/Abstractions/IAsyncDragSource.cs b/Lattice.Core.DragDrop/Abstractions/IAsyncDragSource.cs
new file mode 100644
index 0000000..834b788
--- /dev/null
+++ b/Lattice.Core.DragDrop/Abstractions/IAsyncDragSource.cs
@@ -0,0 +1,28 @@
+namespace Lattice.Core.DragDrop.Abstractions;
+
+///
+/// Определяет контракт для объектов, которые могут быть источником данных
+/// в операции перетаскивания с поддержкой асинхронных операций.
+///
+public interface IAsyncDragSource : IDragSource
+{
+ ///
+ /// Определяет, может ли объект начать операцию перетаскивания (асинхронно).
+ ///
+ Task<(bool CanStart, Models.DragInfo? DragInfo)> CanStartDragAsync();
+
+ ///
+ /// Начинает операцию перетаскивания (асинхронно).
+ ///
+ Task StartDragAsync(Models.DragInfo dragInfo);
+
+ ///
+ /// Вызывается при завершении операции перетаскивания (асинхронно).
+ ///
+ Task DragCompletedAsync(Models.DragInfo dragInfo, Enums.DragDropEffects effects);
+
+ ///
+ /// Вызывается при отмене операции перетаскивания (асинхронно).
+ ///
+ Task DragCancelledAsync(Models.DragInfo dragInfo);
+}
\ No newline at end of file
diff --git a/Lattice.Core.DragDrop/Abstractions/IAsyncDropTarget.cs b/Lattice.Core.DragDrop/Abstractions/IAsyncDropTarget.cs
new file mode 100644
index 0000000..99aa78c
--- /dev/null
+++ b/Lattice.Core.DragDrop/Abstractions/IAsyncDropTarget.cs
@@ -0,0 +1,28 @@
+namespace Lattice.Core.DragDrop.Abstractions;
+
+///
+/// Определяет контракт для объектов, которые могут принимать сбрасываемые данные
+/// в операции перетаскивания с поддержкой асинхронных операций.
+///
+public interface IAsyncDropTarget : IDropTarget
+{
+ ///
+ /// Определяет, может ли объект принять сбрасываемые данные (асинхронно).
+ ///
+ Task CanAcceptDropAsync(Models.DropInfo dropInfo);
+
+ ///
+ /// Вызывается, когда перетаскиваемый объект находится над целью (асинхронно).
+ ///
+ Task DragOverAsync(Models.DropInfo dropInfo);
+
+ ///
+ /// Вызывается, когда пользователь сбрасывает данные на цель (асинхронно).
+ ///
+ Task DropAsync(Models.DropInfo dropInfo);
+
+ ///
+ /// Вызывается, когда перетаскиваемый объект покидает область цели (асинхронно).
+ ///
+ Task DragLeaveAsync();
+}
\ No newline at end of file
diff --git a/Lattice.Core.DragDrop/Abstractions/IDragSource.cs b/Lattice.Core.DragDrop/Abstractions/IDragSource.cs
new file mode 100644
index 0000000..b5fea3b
--- /dev/null
+++ b/Lattice.Core.DragDrop/Abstractions/IDragSource.cs
@@ -0,0 +1,64 @@
+namespace Lattice.Core.DragDrop.Abstractions;
+
+///
+/// Определяет контракт для объектов, которые могут быть источником данных
+/// в операции перетаскивания.
+///
+///
+/// Объекты, реализующие этот интерфейс, могут инициировать операции перетаскивания
+/// и предоставлять данные для передачи другим элементам через механизм drag-and-drop.
+///
+public interface IDragSource
+{
+ ///
+ /// Определяет, может ли объект начать операцию перетаскивания.
+ ///
+ ///
+ /// Информация о перетаскивании, которая будет заполнена данными, если операция разрешена.
+ ///
+ ///
+ /// true, если объект может начать перетаскивание; в противном случае — false.
+ ///
+ ///
+ /// Этот метод вызывается системой перетаскивания для проверки возможности
+ /// начала операции. Если метод возвращает true, он должен заполнить
+ /// необходимыми данными.
+ ///
+ bool CanStartDrag(out Models.DragInfo? dragInfo);
+
+ ///
+ /// Начинает операцию перетаскивания.
+ ///
+ /// Информация о перетаскивании.
+ ///
+ /// true, если операция перетаскивания успешно начата; в противном случае — false.
+ ///
+ ///
+ /// Этот метод вызывается, когда пользователь начинает перетаскивание элемента.
+ /// Реализация должна подготовить данные для перетаскивания и, возможно,
+ /// создать визуальное представление перетаскиваемого объекта.
+ ///
+ bool StartDrag(Models.DragInfo dragInfo);
+
+ ///
+ /// Вызывается при завершении операции перетаскивания.
+ ///
+ /// Исходная информация о перетаскивании.
+ /// Эффекты, которые были применены при сбросе.
+ ///
+ /// Этот метод вызывается после завершения операции перетаскивания
+ /// (успешного или неуспешного). Реализация может выполнить очистку
+ /// или обновить состояние на основе результата операции.
+ ///
+ void DragCompleted(Models.DragInfo dragInfo, Enums.DragDropEffects effects);
+
+ ///
+ /// Вызывается при отмене операции перетаскивания.
+ ///
+ /// Исходная информация о перетаскивании.
+ ///
+ /// Этот метод вызывается, когда операция перетаскивания была отменена
+ /// пользователем (например, нажатием клавиши Escape).
+ ///
+ void DragCancelled(Models.DragInfo dragInfo);
+}
\ No newline at end of file
diff --git a/Lattice.Core.DragDrop/Abstractions/IDropTarget.cs b/Lattice.Core.DragDrop/Abstractions/IDropTarget.cs
new file mode 100644
index 0000000..736666e
--- /dev/null
+++ b/Lattice.Core.DragDrop/Abstractions/IDropTarget.cs
@@ -0,0 +1,55 @@
+namespace Lattice.Core.DragDrop.Abstractions;
+
+///
+/// Определяет контракт для объектов, которые могут принимать сбрасываемые данные
+/// в операции перетаскивания.
+///
+///
+/// Объекты, реализующие этот интерфейс, могут обрабатывать данные, сброшенные
+/// пользователем, и предоставлять визуальную обратную связь во время перетаскивания.
+///
+public interface IDropTarget
+{
+ ///
+ /// Определяет, может ли объект принять сбрасываемые данные.
+ ///
+ /// Информация о потенциальном сбросе.
+ ///
+ /// true, если объект может принять данные; в противном случае — false.
+ ///
+ ///
+ /// Этот метод вызывается, когда перетаскиваемый объект находится над целью.
+ /// Реализация должна проверить, совместимы ли данные с целью, и установить
+ /// предлагаемые эффекты в .
+ ///
+ bool CanAcceptDrop(Models.DropInfo dropInfo);
+
+ ///
+ /// Вызывается, когда перетаскиваемый объект находится над целью.
+ ///
+ /// Информация о текущем положении перетаскивания.
+ ///
+ /// Этот метод вызывается постоянно, пока пользователь перемещает объект над целью.
+ /// Реализация может обновить визуальную обратную связь или изменить предлагаемые эффекты.
+ ///
+ void DragOver(Models.DropInfo dropInfo);
+
+ ///
+ /// Вызывается, когда пользователь сбрасывает данные на цель.
+ ///
+ /// Информация о сбросе.
+ ///
+ /// Этот метод вызывается, когда пользователь отпускает кнопку мыши над целью.
+ /// Реализация должна обработать принятие данных и выполнить соответствующее действие.
+ ///
+ void Drop(Models.DropInfo dropInfo);
+
+ ///
+ /// Вызывается, когда перетаскиваемый объект покидает область цели.
+ ///
+ ///
+ /// Этот метод вызывается, когда пользователь перемещает объект за пределы цели.
+ /// Реализация должна очистить любую визуальную обратную связь, установленную ранее.
+ ///
+ void DragLeave();
+}
\ No newline at end of file
diff --git a/Lattice.Core.DragDrop/Enums/DragDropEffects.cs b/Lattice.Core.DragDrop/Enums/DragDropEffects.cs
new file mode 100644
index 0000000..808b92f
--- /dev/null
+++ b/Lattice.Core.DragDrop/Enums/DragDropEffects.cs
@@ -0,0 +1,102 @@
+namespace Lattice.Core.DragDrop.Enums;
+
+///
+/// Определяет эффекты, которые могут быть применены при операции перетаскивания.
+///
+///
+/// Этот перечисление используется для указания допустимых операций перетаскивания
+/// и передачи информации о результате операции между источником и целью.
+///
+[Flags]
+public enum DragDropEffects
+{
+ ///
+ /// Операция перетаскивания не разрешена.
+ ///
+ None = 0,
+
+ ///
+ /// Данные копируются из источника в цель.
+ ///
+ Copy = 1 << 0,
+
+ ///
+ /// Данные перемещаются из источника в цель.
+ ///
+ Move = 1 << 1,
+
+ ///
+ /// Создается ссылка на исходные данные.
+ ///
+ Link = 1 << 2,
+
+ ///
+ /// Целевой элемент может прокручиваться во время перетаскивания.
+ ///
+ Scroll = 1 << 3,
+
+ ///
+ /// Комбинированный эффект копирования и перемещения.
+ ///
+ CopyOrMove = Copy | Move,
+
+ ///
+ /// Все эффекты разрешены.
+ ///
+ All = Copy | Move | Link | Scroll
+}
+
+///
+/// Расширения для работы с DragDropEffects.
+///
+public static class DragDropEffectsExtensions
+{
+ ///
+ /// Проверяет, содержит ли эффекты указанный эффект.
+ ///
+ public static bool HasEffect(this DragDropEffects effects, DragDropEffects effect)
+ {
+ return (effects & effect) == effect;
+ }
+
+ ///
+ /// Проверяет, содержат ли эффекты копирование.
+ ///
+ public static bool CanCopy(this DragDropEffects effects)
+ {
+ return effects.HasEffect(DragDropEffects.Copy);
+ }
+
+ ///
+ /// Проверяет, содержат ли эффекты перемещение.
+ ///
+ public static bool CanMove(this DragDropEffects effects)
+ {
+ return effects.HasEffect(DragDropEffects.Move);
+ }
+
+ ///
+ /// Проверяет, содержат ли эффекты ссылку.
+ ///
+ public static bool CanLink(this DragDropEffects effects)
+ {
+ return effects.HasEffect(DragDropEffects.Link);
+ }
+
+ ///
+ /// Получает наиболее подходящий эффект на основе модификаторов клавиатуры.
+ ///
+ public static DragDropEffects GetEffectFromKeys(bool controlKey, bool shiftKey, bool altKey)
+ {
+ if (controlKey && altKey)
+ return DragDropEffects.Link;
+ if (controlKey)
+ return DragDropEffects.Copy;
+ if (shiftKey)
+ return DragDropEffects.Move;
+ if (altKey)
+ return DragDropEffects.Link;
+
+ return DragDropEffects.Move; // По умолчанию
+ }
+}
\ No newline at end of file
diff --git a/Lattice.Core.DragDrop/Enums/DropPosition.cs b/Lattice.Core.DragDrop/Enums/DropPosition.cs
new file mode 100644
index 0000000..50e15da
--- /dev/null
+++ b/Lattice.Core.DragDrop/Enums/DropPosition.cs
@@ -0,0 +1,14 @@
+namespace Lattice.Core.DragDrop.Enums;
+
+///
+/// Позиция сброса относительно цели.
+///
+public enum DropPosition
+{
+ Inside,
+ Top,
+ Bottom,
+ Left,
+ Right,
+ Center
+}
\ No newline at end of file
diff --git a/Lattice.Core.DragDrop/Exceptions/DragDropException.cs b/Lattice.Core.DragDrop/Exceptions/DragDropException.cs
new file mode 100644
index 0000000..59874c0
--- /dev/null
+++ b/Lattice.Core.DragDrop/Exceptions/DragDropException.cs
@@ -0,0 +1,85 @@
+namespace Lattice.Core.DragDrop.Exceptions;
+
+///
+/// Исключение, возникающее при ошибках в системе перетаскивания.
+///
+public class DragDropException : Exception
+{
+ ///
+ /// Код ошибки.
+ ///
+ public string ErrorCode { get; }
+
+ ///
+ /// Инициализирует новый экземпляр класса .
+ ///
+ public DragDropException()
+ : base("Drag & Drop operation failed.")
+ {
+ ErrorCode = "DRAGDROP_0001";
+ }
+
+ ///
+ /// Инициализирует новый экземпляр класса с указанным сообщением.
+ ///
+ public DragDropException(string message)
+ : base(message)
+ {
+ ErrorCode = "DRAGDROP_0002";
+ }
+
+ ///
+ /// Инициализирует новый экземпляр класса с кодом ошибки.
+ ///
+ public DragDropException(string errorCode, string message)
+ : base(message)
+ {
+ ErrorCode = errorCode;
+ }
+
+ ///
+ /// Инициализирует новый экземпляр класса
+ /// с указанным сообщением и внутренним исключением.
+ ///
+ public DragDropException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ ErrorCode = "DRAGDROP_0003";
+ }
+
+ ///
+ /// Инициализирует новый экземпляр класса
+ /// с кодом ошибки, сообщением и внутренним исключением.
+ ///
+ public DragDropException(string errorCode, string message, Exception innerException)
+ : base(message, innerException)
+ {
+ ErrorCode = errorCode;
+ }
+}
+
+///
+/// Коды ошибок Drag & Drop системы.
+///
+public static class DragDropErrorCodes
+{
+ // Общие ошибки
+ public const string OperationAlreadyActive = "DRAGDROP_1001";
+ public const string OperationNotActive = "DRAGDROP_1002";
+ public const string InvalidData = "DRAGDROP_1003";
+ public const string Timeout = "DRAGDROP_1004";
+
+ // Ошибки источников
+ public const string SourceCannotDrag = "DRAGDROP_2001";
+ public const string SourceStartFailed = "DRAGDROP_2002";
+
+ // Ошибки целей
+ public const string TargetNotFound = "DRAGDROP_3001";
+ public const string TargetCannotAccept = "DRAGDROP_3002";
+ public const string TargetDropFailed = "DRAGDROP_3003";
+
+ // Ошибки системы
+ public const string SystemNotInitialized = "DRAGDROP_4001";
+ public const string SystemDisposed = "DRAGDROP_4002";
+ public const string MemoryAllocationFailed = "DRAGDROP_4003";
+}
\ No newline at end of file
diff --git a/Lattice.Core.DragDrop/Extensions/ServiceCollectionExtensions.cs b/Lattice.Core.DragDrop/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..d6401ce
--- /dev/null
+++ b/Lattice.Core.DragDrop/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,85 @@
+namespace Lattice.Core.DragDrop.Extensions;
+
+///
+/// Методы расширения для регистрации сервисов перетаскивания.
+///
+public static class ServiceCollectionExtensions
+{
+ ///
+ /// Добавляет сервис перетаскивания.
+ ///
+ /// Коллекция сервисов.
+ /// Коллекция сервисов.
+ ///
+ /// Реализация DI должна быть предоставлена конкретным приложением.
+ ///
+ public static object AddDragDropService(this object serviceCollection)
+ {
+ // Реализация регистрации сервиса должна быть в конкретном приложении
+ // Это абстрактный метод для поддержки DI без зависимостей
+ return serviceCollection;
+ }
+
+ ///
+ /// Добавляет сервис перетаскивания с конфигурацией.
+ ///
+ /// Коллекция сервисов.
+ /// Действие конфигурации.
+ /// Коллекция сервисов.
+ public static object AddDragDropService(
+ this object serviceCollection,
+ Action configure)
+ {
+ var options = new DragDropServiceOptions();
+ configure(options);
+
+ // Реализация регистрации с опциями должна быть в конкретном приложении
+ return serviceCollection;
+ }
+}
+
+///
+/// Опции конфигурации сервиса перетаскивания.
+///
+public class DragDropServiceOptions
+{
+ ///
+ /// Порог начала перетаскивания в пикселях.
+ ///
+ public double DragStartThreshold { get; set; } = 3.0;
+
+ ///
+ /// Включить ведение журнала операций.
+ ///
+ public bool EnableLogging { get; set; } = false;
+
+ ///
+ /// Включить автоматическую очистку неиспользуемых целей.
+ ///
+ public bool EnableAutoCleanup { get; set; } = true;
+
+ ///
+ /// Интервал автоматической очистки в миллисекундах.
+ ///
+ public int AutoCleanupInterval { get; set; } = 60000;
+
+ ///
+ /// Включить асинхронную обработку операций.
+ ///
+ public bool EnableAsyncOperations { get; set; } = true;
+
+ ///
+ /// Время ожидания асинхронных операций в миллисекундах.
+ ///
+ public int AsyncOperationTimeout { get; set; } = 5000;
+
+ ///
+ /// Включить сбор статистики.
+ ///
+ public bool EnableStatistics { get; set; } = true;
+
+ ///
+ /// Включить проверку типов данных.
+ ///
+ public bool EnableTypeChecking { get; set; } = true;
+}
\ No newline at end of file
diff --git a/Lattice.Core.DragDrop/Lattice.Core.DragDrop.csproj b/Lattice.Core.DragDrop/Lattice.Core.DragDrop.csproj
new file mode 100644
index 0000000..c18ab9e
--- /dev/null
+++ b/Lattice.Core.DragDrop/Lattice.Core.DragDrop.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net8.0;net9.0;net10.0
+ enable
+ enable
+ latest
+ true
+ true
+ Lattice.Core.DragDrop
+ 1.0.0
+ FrigaT
+ Professional drag-and-drop system for Lattice UI Framework
+ ui;framework;drag;drop;docking;toolbox
+
+
+
+
+
+
diff --git a/Lattice.Core.DragDrop/Models/DragInfo.cs b/Lattice.Core.DragDrop/Models/DragInfo.cs
new file mode 100644
index 0000000..6f2d85f
--- /dev/null
+++ b/Lattice.Core.DragDrop/Models/DragInfo.cs
@@ -0,0 +1,227 @@
+using Lattice.Core.Geometry;
+using System.Collections.Concurrent;
+
+namespace Lattice.Core.DragDrop.Models;
+
+///
+/// Содержит информацию о начале операции перетаскивания.
+/// Этот класс передается от источника перетаскивания к системе перетаскивания
+/// для инициализации и управления операцией.
+///
+///
+///
+/// является ключевым компонентом системы перетаскивания,
+/// инкапсулирующим все необходимые данные для начала операции. Он содержит:
+///
+///
+/// Данные для передачи
+/// Разрешенные эффекты перетаскивания
+/// Начальную позицию операции
+/// Ссылку на источник перетаскивания
+/// Дополнительные параметры операции
+///
+///
+/// Этот класс используется как внутренний механизм передачи данных между
+/// и системой управления перетаскиванием.
+///
+///
+public class DragInfo : IDisposable, ICloneable
+{
+ private readonly ConcurrentDictionary _parameters = new();
+ private bool _disposed;
+
+ ///
+ /// Получает данные, которые передаются в операции перетаскивания.
+ ///
+ ///
+ /// Объект, содержащий данные для передачи. Может быть любого типа,
+ /// поддерживаемого системой перетаскивания.
+ ///
+ ///
+ /// Эти данные будут доступны цели сброса через .
+ /// Важно, чтобы данные были сериализуемыми, если операция перетаскивания
+ /// может выходить за пределы процесса приложения.
+ ///
+ public object Data { get; }
+
+ ///
+ /// Получает разрешенные эффекты для этой операции перетаскивания.
+ ///
+ ///
+ /// Комбинация флагов , определяющая,
+ /// какие операции разрешены для этого перетаскивания.
+ ///
+ ///
+ /// Этот параметр используется системой для фильтрации допустимых операций
+ /// и предоставления соответствующей визуальной обратной связи пользователю.
+ ///
+ public Enums.DragDropEffects AllowedEffects { get; }
+
+ ///
+ /// Получает начальную позицию операции перетаскивания в координатах экрана.
+ ///
+ ///
+ /// Точка в экранных координатах, где была начата операция перетаскивания.
+ ///
+ ///
+ /// Эта позиция используется для вычисления смещения при создании визуального
+ /// представления перетаскивания и для определения порога начала операции.
+ ///
+ public Point StartPosition { get; }
+
+ ///
+ /// Получает источник перетаскивания, который инициировал операцию.
+ ///
+ ///
+ /// Объект, реализующий , или null,
+ /// если источник не доступен или не требуется.
+ ///
+ ///
+ /// Эта ссылка может использоваться для уведомления источника о результате
+ /// операции перетаскивания (завершении или отмене).
+ ///
+ public object? Source { get; }
+
+ ///
+ /// Получает или задает дополнительные параметры, специфичные для конкретной
+ /// реализации перетаскивания.
+ ///
+ ///
+ /// Словарь, содержащий пары ключ-значение с дополнительными параметрами.
+ ///
+ ///
+ /// Используется для передачи контекстной информации, которая не входит
+ /// в стандартный набор свойств, но может быть полезной для обработки
+ /// операции перетаскивания.
+ ///
+ public IReadOnlyDictionary Parameters => _parameters;
+
+ ///
+ /// Инициализирует новый экземпляр класса .
+ ///
+ ///
+ /// Данные, которые передаются в операции перетаскивания.
+ /// Не может быть null.
+ ///
+ ///
+ /// Разрешенные эффекты для этой операции перетаскивания.
+ ///
+ ///
+ /// Начальная позиция операции перетаскивания в координатах экрана.
+ ///
+ ///
+ /// Источник перетаскивания, который инициировал операцию. Может быть null.
+ ///
+ ///
+ /// Выбрасывается, когда равен null.
+ ///
+ ///
+ /// Конструктор создает экземпляр с указанными
+ /// параметрами и инициализирует коллекцию параметров пустым словарем.
+ ///
+ public DragInfo(object data, Enums.DragDropEffects allowedEffects, Point startPosition, object? source = null)
+ {
+ Data = data ?? throw new ArgumentNullException(nameof(data));
+ AllowedEffects = allowedEffects;
+ StartPosition = startPosition;
+ Source = source;
+ }
+
+ ///
+ /// Создает новый экземпляр с теми же данными,
+ /// но новой позицией.
+ ///
+ ///
+ /// Новая позиция для информации о перетаскивании.
+ ///
+ ///
+ /// Новый экземпляр с обновленной позицией.
+ ///
+ ///
+ /// Этот метод используется для обновления информации о перетаскивании
+ /// при перемещении курсора, сохраняя исходные данные и параметры.
+ ///
+ public DragInfo CloneWithPosition(Point newPosition)
+ {
+ ThrowIfDisposed();
+
+ var clone = new DragInfo(Data, AllowedEffects, newPosition, Source);
+
+ foreach (var kvp in _parameters)
+ {
+ clone._parameters[kvp.Key] = kvp.Value;
+ }
+
+ return clone;
+ }
+
+ ///
+ /// Создает новый экземпляр с теми же данными.
+ ///
+ public DragInfo Clone() => new DragInfo(Data, AllowedEffects, StartPosition, Source);
+
+ ///
+ object ICloneable.Clone() => this.Clone();
+
+ ///
+ /// Получает или дополнительные параметры, специфичные для конкретной
+ /// реализации перетаскивания.
+ ///
+ public T? GetParameter(string key, T? defaultValue = default)
+ {
+ if (Parameters.TryGetValue(key, out var value) && value is T typedValue)
+ {
+ return typedValue;
+ }
+ return defaultValue;
+ }
+
+ ///
+ /// Получает или дополнительные параметры, специфичные для конкретной
+ /// реализации перетаскивания.
+ ///
+ public bool TryGetParameter(string key, out T? value)
+ {
+ value = default;
+
+ if (_parameters.TryGetValue(key, out var objValue) && objValue is T typedValue)
+ {
+ value = typedValue;
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Задает дополнительные параметры, специфичные для конкретной
+ /// реализации перетаскивания.
+ ///
+ public void SetParameter(string key, T value)
+ {
+ _parameters[key] = value!;
+ }
+
+ ///
+ /// Освобождает ресурсы.
+ ///
+ public void Dispose()
+ {
+ if (_disposed) return;
+
+ _parameters.Clear();
+ _disposed = true;
+ GC.SuppressFinalize(this);
+ }
+
+ private void ThrowIfDisposed()
+ {
+ if (_disposed)
+ throw new ObjectDisposedException(nameof(DragInfo));
+ }
+
+ ~DragInfo()
+ {
+ Dispose();
+ }
+}
\ No newline at end of file
diff --git a/Lattice.Core.DragDrop/Models/DropInfo.cs b/Lattice.Core.DragDrop/Models/DropInfo.cs
new file mode 100644
index 0000000..1ee05fb
--- /dev/null
+++ b/Lattice.Core.DragDrop/Models/DropInfo.cs
@@ -0,0 +1,269 @@
+using Lattice.Core.DragDrop.Enums;
+using Lattice.Core.Geometry;
+
+namespace Lattice.Core.DragDrop.Models;
+
+///
+/// Содержит информацию о потенциальном или фактическом сбросе в операции перетаскивания.
+/// Этот класс используется для передачи данных между системой перетаскивания
+/// и целью сброса ().
+///
+///
+///
+/// предоставляет цель сброса всей необходимой информацией
+/// для принятия решения о возможности сброса и выполнения соответствующей операции.
+/// Ключевые аспекты включают:
+///
+///
+/// Предлагаемые для сброса данные
+/// Текущую позицию курсора
+/// Разрешенные эффекты от источника
+/// Предлагаемые эффекты для сброса
+/// Ссылку на цель сброса
+/// Флаг обработки операции
+///
+///
+/// Этот класс является изменяемым, позволяя цели сброса обновлять предлагаемые
+/// эффекты и помечать операцию как обработанную.
+///
+///
+public class DropInfo
+{
+ private DragDropEffects _effects = DragDropEffects.None;
+ public DropPosition DropPosition { get; set; } = DropPosition.Inside;
+ public bool ShowVisualFeedback { get; set; } = true;
+ public object? VisualFeedbackData { get; set; }
+
+ ///
+ /// Получает данные, которые предлагаются для сброса.
+ ///
+ ///
+ /// Данные, переданные от источника перетаскивания, или null, если данные
+ /// не доступны или операция была отменена.
+ ///
+ ///
+ /// Эти данные соответствуют свойству из
+ /// исходной информации о перетаскивании.
+ ///
+ public object? Data { get; }
+
+ ///
+ /// Получает текущую позицию курсора в координатах экрана.
+ ///
+ ///
+ /// Точка в экранных координатах, представляющая текущее положение курсора
+ /// мыши во время операции перетаскивания.
+ ///
+ ///
+ /// Эта позиция используется для определения точного места сброса и может
+ /// влиять на предлагаемые эффекты (например, различные операции для
+ /// разных областей цели сброса).
+ ///
+ public Point Position { get; }
+
+ ///
+ /// Получает разрешенные эффекты от источника перетаскивания.
+ ///
+ ///
+ /// Комбинация флагов , определяющая,
+ /// какие операции разрешил источник.
+ ///
+ ///
+ /// Цель сброса должна уважать эти ограничения и не предлагать эффекты,
+ /// которые не разрешены источником.
+ ///
+ public Enums.DragDropEffects AllowedEffects { get; }
+
+ ///
+ /// Получает или задает предлагаемые эффекты для операции сброса.
+ ///
+ ///
+ /// Комбинация флагов , предлагаемая
+ /// целью сброса. По умолчанию равно .
+ ///
+ ///
+ ///
+ /// Цель сброса должна установить это свойство в методе
+ /// на основе анализа предоставленных данных и текущего контекста.
+ ///
+ ///
+ /// Если цель не устанавливает это свойство, система перетаскивания
+ /// будет использовать эффекты по умолчанию.
+ ///
+ ///
+ public Enums.DragDropEffects SuggestedEffects
+ {
+ get => _effects;
+ set => _effects = value;
+ }
+
+ ///
+ /// Получает цель сброса, которая обрабатывает эту информацию.
+ ///
+ ///
+ /// Объект, реализующий , или null,
+ /// если цель не определена.
+ ///
+ ///
+ /// Эта ссылка позволяет системе идентифицировать, какая цель обрабатывает
+ /// информацию о сбросе, и используется для отслеживания изменений цели
+ /// во время операции перетаскивания.
+ ///
+ public object? Target { get; }
+
+ ///
+ /// Получает или задает дополнительные параметры, специфичные для конкретной
+ /// реализации перетаскивания.
+ ///
+ ///
+ /// Словарь, содержащий пары ключ-значение с дополнительными параметрами.
+ ///
+ ///
+ /// Может использоваться для передачи контекстной информации между
+ /// различными компонентами системы перетаскивания или для хранения
+ /// временных данных во время обработки операции.
+ ///
+ public Dictionary Parameters { get; set; }
+
+ ///
+ /// Получает значение, указывающее, был ли сброс уже обработан.
+ ///
+ ///
+ /// true, если операция сброса была помечена как обработанная;
+ /// в противном случае — false.
+ ///
+ ///
+ ///
+ /// Это свойство используется для предотвращения множественной обработки
+ /// одной и той же операции сброса. После вызова метода ,
+ /// свойство становится true.
+ ///
+ ///
+ /// Система перетаскивания может проверять это свойство, чтобы определить,
+ /// нужно ли выполнять дополнительную обработку по умолчанию.
+ ///
+ ///
+ public bool Handled { get; private set; }
+
+ ///
+ /// Получает дополнительные параметры, специфичные для конкретной
+ /// реализации перетаскивания.
+ ///
+ public T? GetParameter(string key, T? defaultValue = default)
+ {
+ if (Parameters.TryGetValue(key, out var value) && value is T typedValue)
+ {
+ return typedValue;
+ }
+ return defaultValue;
+ }
+
+ ///
+ /// Получает дополнительные параметры, специфичные для конкретной
+ /// реализации перетаскивания.
+ ///
+ public bool TryGetParameter(string key, out T? value)
+ {
+ value = default;
+
+ if (Parameters.TryGetValue(key, out var objValue) && objValue is T typedValue)
+ {
+ value = typedValue;
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Задает дополнительные параметры, специфичные для конкретной
+ /// реализации перетаскивания.
+ ///
+ public void SetParameter(string key, T value)
+ {
+ Parameters[key] = value!;
+ }
+
+ ///
+ /// Инициализирует новый экземпляр класса .
+ ///
+ ///
+ /// Данные, которые предлагаются для сброса. Может быть null.
+ ///
+ ///
+ /// Текущая позиция курсора в координатах экрана.
+ ///
+ ///
+ /// Разрешенные эффекты от источника перетаскивания.
+ ///
+ ///
+ /// Цель сброса, которая обрабатывает эту информацию. Может быть null.
+ ///
+ ///
+ /// Конструктор создает экземпляр с указанными
+ /// параметрами, инициализирует коллекцию параметров пустым словарем
+ /// и устанавливает флаг в false.
+ ///
+ public DropInfo(object? data, Point position, Enums.DragDropEffects allowedEffects, object? target = null)
+ {
+ Data = data;
+ Position = position;
+ AllowedEffects = allowedEffects;
+ Target = target;
+ Parameters = new Dictionary();
+ Handled = false;
+ }
+
+ ///
+ /// Помечает сброс как обработанный.
+ ///
+ ///
+ ///
+ /// Этот метод должен вызываться целью сброса в методе ,
+ /// если она успешно обработала операцию сброса.
+ ///
+ ///
+ /// После вызова этого метода свойство становится true,
+ /// что сигнализирует системе перетаскивания о том, что дополнительная
+ /// обработка не требуется.
+ ///
+ ///
+ public void MarkAsHandled()
+ {
+ Handled = true;
+ }
+
+ ///
+ /// Создает новый экземпляр с теми же данными,
+ /// но новой позицией.
+ ///
+ ///
+ /// Новая позиция для информации о сбросе.
+ ///
+ ///
+ /// Новый экземпляр с обновленной позицией.
+ ///
+ ///
+ /// Этот метод используется для обновления информации о сбросе при
+ /// перемещении курсора, сохраняя исходные данные и параметры.
+ ///
+ public DropInfo WithPosition(Point newPosition)
+ {
+ return new DropInfo(Data, newPosition, AllowedEffects, Target)
+ {
+ Parameters = new Dictionary(Parameters),
+ SuggestedEffects = _effects,
+ DropPosition = DropPosition,
+ ShowVisualFeedback = ShowVisualFeedback,
+ VisualFeedbackData = VisualFeedbackData
+ };
+ }
+
+ ///
+ /// Проверка установки эффекта перетаскивания в разрешенные эффекты.
+ ///
+ public bool CanAcceptEffect(Enums.DragDropEffects effect)
+ {
+ return (AllowedEffects & effect) != Enums.DragDropEffects.None;
+ }
+}
\ No newline at end of file
diff --git a/Lattice.Core.DragDrop/README.md b/Lattice.Core.DragDrop/README.md
new file mode 100644
index 0000000..80849b0
--- /dev/null
+++ b/Lattice.Core.DragDrop/README.md
@@ -0,0 +1,832 @@
+# Lattice.Core.DragDrop
+
+Профессиональная, асинхронная система перетаскивания для .NET приложений. Полностью потокобезопасная, расширяемая архитектура с поддержкой кросс-платформенности.
+
+## 📋 Особенности
+
+- ✅ **Полная асинхронная поддержка** - async/await для всех операций
+- ✅ **Потокобезопасность** - `ReaderWriterLockSlim` для эффективной синхронизации
+- ✅ **Производительность** - Оптимизированные алгоритмы, кэширование, минимальные аллокации
+- ✅ **Расширяемость** - Легкая интеграция с любыми UI фреймворками
+- ✅ **Надежность** - Таймауты, обработка ошибок, корректное освобождение ресурсов
+- ✅ **Статистика** - Встроенный мониторинг производительности
+
+## 🏗️ Архитектура
+
+### Основные компоненты
+
+```
+Lattice.Core.DragDrop/
+├── Abstractions/ # Интерфейсы
+│ ├── IDragSource.cs # Источник перетаскивания (синхронный)
+│ ├── IAsyncDragSource.cs # Асинхронный источник
+│ ├── IDropTarget.cs # Цель сброса (синхронная)
+│ └── IAsyncDropTarget.cs # Асинхронная цель
+├── Enums/ # Перечисления
+├── Exceptions/ # Исключения с кодами ошибок
+├── Extensions/ # Расширения для DI
+├── Models/ # Модели данных
+│ ├── DragInfo.cs # Информация о перетаскивании
+│ └── DropInfo.cs # Информация о сбросе
+├── Services/ # Сервисы
+│ ├── IDragDropService.cs # Основной интерфейс
+│ ├── DragDropService.cs # Реализация сервиса
+│ └── EventArgs/ # Аргументы событий
+└── Utilities/ # Утилиты и фабрики
+ ├── DragDropUtilities.cs # Синхронные утилиты
+ └── AsyncDragDropUtilities.cs # Асинхронные утилиты
+```
+
+## 🚀 Быстрый старт
+
+### 1. Установка
+
+```csharp
+// Добавьте проект Lattice.Core.DragDrop в ваше решение
+// или создайте NuGet пакет
+```
+
+### 2. Базовое использование
+
+```csharp
+using Lattice.Core.DragDrop;
+using Lattice.Core.DragDrop.Abstractions;
+using Lattice.Core.DragDrop.Services;
+using Lattice.Core.Geometry;
+
+// Создаем сервис
+var dragDropService = new DragDropService();
+
+// Создаем простой источник перетаскивания
+var dragSource = DragDropUtilities.CreateSimpleDragSource(
+ dataProvider: () => "Example Data",
+ canDrag: () => true,
+ onCompleted: (dragInfo, effects) =>
+ Console.WriteLine($"Drag completed with effects: {effects}"),
+ onCancelled: dragInfo =>
+ Console.WriteLine("Drag cancelled")
+);
+
+// Создаем простую цель сброса
+var dropTarget = DragDropUtilities.CreateSimpleDropTarget(
+ canAccept: dropInfo => dropInfo.Data is string,
+ onDragOver: dropInfo =>
+ dropInfo.SuggestedEffects = DragDropEffects.Copy,
+ onDrop: dropInfo =>
+ {
+ Console.WriteLine($"Dropped: {dropInfo.Data}");
+ dropInfo.MarkAsHandled();
+ }
+);
+
+// Регистрируем цель
+string targetId = dragDropService.RegisterDropTarget(
+ dropTarget,
+ new Rect(100, 100, 300, 200)
+);
+
+// Начинаем перетаскивание
+bool started = dragDropService.StartDrag(
+ dragSource,
+ new Point(50, 50)
+);
+
+if (started)
+{
+ // Обновляем позицию
+ dragDropService.UpdateDrag(new Point(150, 150));
+
+ // Завершаем
+ var effects = dragDropService.EndDrag(new Point(200, 200));
+}
+```
+
+## 📖 Подробное руководство
+
+### Сервис перетаскивания
+
+Основной класс системы - `DragDropService`, реализующий `IDragDropService`.
+
+```csharp
+// Создание с кастомными настройками
+var service = new DragDropService(options =>
+{
+ options.DragStartThreshold = 5.0;
+ options.EnableAsyncOperations = true;
+ options.AsyncOperationTimeout = 3000;
+ options.EnableAutoCleanup = true;
+});
+
+// Свойства
+bool isActive = service.IsDragActive; // Активна ли операция
+DragInfo? currentDrag = service.CurrentDragInfo; // Текущая информация
+double threshold = service.DragStartThreshold; // Порог начала
+
+// События
+service.DragStarted += OnDragStarted;
+service.DragUpdated += OnDragUpdated;
+service.DragCompleted += OnDragCompleted;
+service.DragCancelled += OnDragCancelled;
+service.ErrorOccurred += OnErrorOccurred;
+
+// Регистрация целей
+string id = service.RegisterDropTarget(
+ target, // IDropTarget
+ bounds, // Rect
+ priority: 1, // Приоритет (выше = выше приоритет)
+ group: "main" // Группа для группового удаления
+);
+
+// Обновление границ
+service.UpdateDropTargetBounds(id, newBounds);
+
+// Удаление
+service.UnregisterDropTarget(id);
+service.UnregisterDropTargetsInGroup("main");
+```
+
+### Асинхронное использование
+
+```csharp
+// Асинхронные методы
+bool started = await service.StartDragAsync(source, startPosition);
+await service.UpdateDragAsync(currentPosition);
+DragDropEffects effects = await service.EndDragAsync(dropPosition);
+await service.CancelDragAsync();
+
+// Статистика
+var stats = service.GetStats();
+Console.WriteLine($"Operations: {stats.TotalDragOperations}");
+Console.WriteLine($"Success rate: {stats.SuccessfulDrops}/{stats.TotalDragOperations}");
+Console.WriteLine($"Avg time: {stats.AverageOperationTime.TotalMilliseconds}ms");
+```
+
+### Создание кастомных источников и целей
+
+#### Синхронная реализация
+
+```csharp
+public class FileDragSource : IDragSource
+{
+ private readonly FileInfo _file;
+
+ public FileDragSource(FileInfo file) => _file = file;
+
+ public bool CanStartDrag(out DragInfo? dragInfo)
+ {
+ // Проверяем условия
+ if (!_file.Exists || _file.Length > 100 * 1024 * 1024) // 100 MB limit
+ {
+ dragInfo = null;
+ return false;
+ }
+
+ // Создаем DragInfo
+ dragInfo = new DragInfo(
+ data: _file,
+ allowedEffects: DragDropEffects.Copy | DragDropEffects.Move,
+ startPosition: Point.Zero,
+ source: this
+ );
+
+ // Добавляем дополнительные параметры
+ dragInfo.SetParameter("FileSize", _file.Length);
+ dragInfo.SetParameter("MimeType", GetMimeType(_file));
+
+ return true;
+ }
+
+ public bool StartDrag(DragInfo dragInfo)
+ {
+ // Подготовка к перетаскиванию
+ // Можно создать визуальное представление и т.д.
+ Console.WriteLine($"Starting drag of {_file.Name}");
+ return true;
+ }
+
+ public void DragCompleted(DragInfo dragInfo, DragDropEffects effects)
+ {
+ Console.WriteLine($"File drag completed with {effects}");
+
+ if (effects == DragDropEffects.Move)
+ {
+ // Файл был перемещен - возможно, удалить оригинал
+ // _file.Delete();
+ }
+ }
+
+ public void DragCancelled(DragInfo dragInfo)
+ {
+ Console.WriteLine("File drag cancelled");
+ }
+}
+```
+
+#### Асинхронная реализация
+
+```csharp
+public class DatabaseItemDragSource : IAsyncDragSource
+{
+ private readonly DatabaseService _db;
+ private readonly int _itemId;
+
+ public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync()
+ {
+ try
+ {
+ // Асинхронные проверки
+ var canDrag = await _db.CanDragItemAsync(_itemId);
+ if (!canDrag) return (false, null);
+
+ // Асинхронная загрузка данных
+ var data = await _db.GetItemForDragAsync(_itemId);
+ if (data == null) return (false, null);
+
+ var dragInfo = new DragInfo(
+ data: data,
+ allowedEffects: DragDropEffects.Copy | DragDropEffects.Move,
+ startPosition: Point.Zero,
+ source: this
+ );
+
+ return (true, dragInfo);
+ }
+ catch (Exception ex)
+ {
+ // Логирование ошибки
+ return (false, null);
+ }
+ }
+
+ public Task StartDragAsync(DragInfo dragInfo)
+ {
+ // Асинхронная подготовка
+ return Task.FromResult(true);
+ }
+
+ public async Task DragCompletedAsync(DragInfo dragInfo, DragDropEffects effects)
+ {
+ // Асинхронная обработка завершения
+ await _db.LogDragOperationAsync(_itemId, effects);
+
+ if (effects == DragDropEffects.Move)
+ {
+ await _db.MarkItemAsMovedAsync(_itemId);
+ }
+ }
+
+ public Task DragCancelledAsync(DragInfo dragInfo)
+ {
+ return Task.CompletedTask;
+ }
+
+ // Синхронные методы для совместимости
+ public bool CanStartDrag(out DragInfo? dragInfo)
+ {
+ var result = Task.Run(() => CanStartDragAsync()).GetAwaiter().GetResult();
+ dragInfo = result.DragInfo;
+ return result.CanStart;
+ }
+
+ // ... остальные синхронные методы
+}
+```
+
+### Работа с моделями данных
+
+#### DragInfo
+
+```csharp
+// Создание
+var dragInfo = new DragInfo(
+ data: myObject,
+ allowedEffects: DragDropEffects.Copy | DragDropEffects.Move,
+ startPosition: new Point(x, y),
+ source: this
+);
+
+// Параметры
+dragInfo.SetParameter("Timestamp", DateTime.UtcNow);
+dragInfo.SetParameter("UserId", currentUser.Id);
+
+// Получение параметров
+if (dragInfo.TryGetParameter("Category", out var category))
+{
+ // Используем категорию
+}
+
+// Клонирование с новой позицией
+var updatedDragInfo = dragInfo.CloneWithPosition(newPosition);
+
+// Очистка ресурсов
+dragInfo.Dispose();
+```
+
+#### DropInfo
+
+```csharp
+// Создается сервисом автоматически
+// Работа с DropInfo в методах цели:
+
+public void DragOver(DropInfo dropInfo)
+{
+ // Проверяем данные
+ if (dropInfo.Data is MyDataType myData)
+ {
+ // Определяем позицию относительно цели
+ dropInfo.DropPosition = CalculateDropPosition(dropInfo.Position);
+
+ // Предлагаем эффекты
+ if (CanAcceptData(myData))
+ {
+ dropInfo.SuggestedEffects = DragDropEffects.Move;
+ dropInfo.ShowVisualFeedback = true;
+ dropInfo.VisualFeedbackData = CreatePreview(myData);
+ }
+ else
+ {
+ dropInfo.SuggestedEffects = DragDropEffects.None;
+ }
+ }
+}
+
+public void Drop(DropInfo dropInfo)
+{
+ if (dropInfo.Data is MyDataType myData)
+ {
+ // Обработка сброса
+ ProcessDrop(myData, dropInfo.DropPosition);
+
+ // Помечаем как обработанное
+ dropInfo.MarkAsHandled();
+ }
+}
+```
+
+### Утилиты и фабрики
+
+#### Синхронные утилиты
+
+```csharp
+// Простые реализации
+var simpleSource = DragDropUtilities.CreateSimpleDragSource(
+ () => data,
+ () => true,
+ (dragInfo, effects) => Console.WriteLine("Completed"),
+ dragInfo => Console.WriteLine("Cancelled")
+);
+
+var simpleTarget = DragDropUtilities.CreateSimpleDropTarget(
+ dropInfo => dropInfo.Data != null,
+ dropInfo => dropInfo.SuggestedEffects = DragDropEffects.Copy,
+ dropInfo => Console.WriteLine($"Dropped: {dropInfo.Data}"),
+ () => Console.WriteLine("Drag left")
+);
+
+// Геометрия
+double distance = DragDropUtilities.CalculateDistance(p1, p2);
+bool exceeded = DragDropUtilities.HasExceededDragThreshold(start, current, threshold);
+DropPosition position = DragDropUtilities.GetDropPosition(point, bounds, edgeThreshold);
+
+// Проверка совместимости
+bool compatible = DragDropUtilities.AreEffectsCompatible(sourceEffects, targetEffects);
+bool typeMatch = DragDropUtilities.IsDataCompatible(data, new[] { typeof(string), typeof(int) });
+```
+
+#### Асинхронные утилиты
+
+```csharp
+// Асинхронные реализации
+var asyncSource = AsyncDragDropUtilities.CreateAsyncDragSource(
+ async () => await LoadDataAsync(),
+ async () => await CanDragAsync(),
+ async (dragInfo, effects) => await OnCompletedAsync(dragInfo, effects),
+ async dragInfo => await OnCancelledAsync(dragInfo)
+);
+
+var asyncTarget = AsyncDragDropUtilities.CreateAsyncDropTarget(
+ async dropInfo => await CanAcceptAsync(dropInfo.Data),
+ async dropInfo => await OnDragOverAsync(dropInfo),
+ async dropInfo => await OnDropAsync(dropInfo),
+ async () => await OnDragLeaveAsync()
+);
+
+// Адаптеры для синхронных интерфейсов
+IAsyncDragSource asyncFromSync = AsyncDragDropUtilities.CreateAsyncAdapter(syncSource);
+IAsyncDropTarget asyncTargetFromSync = AsyncDragDropUtilities.CreateAsyncAdapter(syncTarget);
+
+// Комбинированные реализации (fallback стратегия)
+var combined = AsyncDragDropUtilities.Combine(
+ syncSource,
+ asyncSource,
+ preferAsync: true // При ошибке в async использует sync
+);
+
+// Таймауты
+var result = await AsyncDragDropUtilities.ExecuteWithTimeoutAsync(
+ task: LongOperationAsync(),
+ timeout: TimeSpan.FromSeconds(5),
+ defaultValue: fallbackValue
+);
+```
+
+### Обработка ошибок
+
+```csharp
+// Подписка на ошибки
+service.ErrorOccurred += (sender, e) =>
+{
+ Console.WriteLine($"Error in {e.Operation}: {e.Exception.Message}");
+
+ // Коды ошибок определены в DragDropErrorCodes
+ switch (e.ErrorCode)
+ {
+ case DragDropErrorCodes.Timeout:
+ Console.WriteLine("Operation timed out");
+ break;
+ case DragDropErrorCodes.SourceCannotDrag:
+ Console.WriteLine("Source cannot drag");
+ break;
+ case DragDropErrorCodes.TargetCannotAccept:
+ Console.WriteLine("Target cannot accept");
+ break;
+ }
+};
+
+// Использование в коде
+try
+{
+ await service.StartDragAsync(source, position);
+}
+catch (DragDropException ex)
+{
+ // Обработка специфичных для DragDrop ошибок
+ Console.WriteLine($"DragDrop error {ex.ErrorCode}: {ex.Message}");
+}
+catch (Exception ex)
+{
+ // Обработка других ошибок
+ Console.WriteLine($"General error: {ex.Message}");
+}
+```
+
+## 🔧 Интеграция с UI фреймворками
+
+### Базовый адаптер для WinUI/WPF
+
+```csharp
+public class UIElementDragSource : IAsyncDragSource
+{
+ private readonly FrameworkElement _element;
+ private readonly Func