Compare commits

...

14 Commits

161 changed files with 11957 additions and 691 deletions

View File

@@ -0,0 +1,23 @@
namespace Lattice.Core.Docking.Abstractions;
/// <summary>
/// Определяет контракт для команды в системе докинга.
/// Команды представляют действия, которые могут быть выполнены над элементами док-системы.
/// </summary>
public interface IDockCommand : System.Windows.Input.ICommand
{
/// <summary>
/// Получает отображаемое имя команды.
/// </summary>
string Name { get; }
/// <summary>
/// Получает идентификатор ресурса для иконки команды.
/// </summary>
string Icon { get; }
/// <summary>
/// Получает текстовое представление жеста (горячей клавиши) для команды.
/// </summary>
string GestureText { get; }
}

View File

@@ -0,0 +1,38 @@
using Lattice.Core.Docking.Models;
namespace Lattice.Core.Docking.Abstractions;
/// <summary>
/// Определяет контракт для контейнеров, содержащих коллекцию вкладок.
/// Контейнеры являются листьями дерева компоновки и непосредственно отображают содержимое.
/// </summary>
public interface IDockContainer : IDockElement
{
/// <summary>
/// Получает список вкладок, находящихся в данном контейнере.
/// </summary>
IList<IDockContent> Children { get; }
/// <summary>
/// Получает или задает текущую активную (выбранную) вкладку.
/// </summary>
IDockContent? ActiveContent { get; set; }
/// <summary>
/// Добавляет контент в контейнер и делает его активным.
/// </summary>
/// <param name="content">Контент для добавления.</param>
void AddContent(IDockContent content);
/// <summary>
/// Удаляет контент из контейнера. Если коллекция становится пустой,
/// контейнер может быть удален из дерева макета.
/// </summary>
/// <param name="content">Контент для удаления.</param>
void RemoveContent(IDockContent content);
/// <summary>
/// Получает или задает положение панели вкладок в интерфейсе.
/// </summary>
TabPlacement TabPlacement { get; set; }
}

View File

@@ -0,0 +1,43 @@
namespace Lattice.Core.Docking.Abstractions;
/// <summary>
/// Определяет контракт для содержимого (вкладки), которое может быть размещено внутри контейнера.
/// </summary>
public interface IDockContent
{
/// <summary>
/// Получает уникальный идентификатор контента.
/// Используется для идентификации вкладки в системе.
/// </summary>
string Id { get; }
/// <summary>
/// Устанавливает идентификатор контента.
/// </summary>
/// <param name="id">Новый идентификатор.</param>
void SetId(string id);
/// <summary>
/// Получает заголовок, отображаемый пользователю на вкладке.
/// </summary>
string Title { get; }
/// <summary>
/// Получает или задает визуальный элемент для отображения в теле вкладки.
/// </summary>
object View { get; set; }
/// <summary>
/// Получает значение, указывающее, можно ли закрыть вкладку.
/// </summary>
bool CanClose { get; }
/// <summary>
/// Вызывается системой при попытке закрытия контента.
/// Позволяет выполнить дополнительные проверки или сохранить состояние.
/// </summary>
/// <returns>
/// true, если закрытие разрешено; в противном случае false.
/// </returns>
bool OnClosing();
}

View File

@@ -0,0 +1,91 @@
namespace Lattice.Core.Docking.Abstractions;
/// <summary>
/// Базовый интерфейс для любого элемента, являющегося частью дерева компоновки.
/// Определяет общие свойства и методы для всех элементов док-системы.
/// </summary>
/// <remarks>
/// Элементы док-системы образуют древовидную структуру, где каждый элемент может иметь
/// родителя и дочерние элементы. Эта иерархия используется для организации пространства
/// главного окна и плавающих окон в IDE-подобных приложениях.
/// </remarks>
public interface IDockElement
{
/// <summary>
/// Получает уникальный идентификатор элемента.
/// Используется для поиска элементов, сериализации состояния и отслеживания изменений.
/// </summary>
/// <value>
/// Строковый идентификатор, гарантированно уникальный в пределах дерева компоновки.
/// Обычно представляет собой GUID в строковом формате.
/// </value>
string Id { get; }
/// <summary>
/// Получает или задает родительский элемент в иерархии дерева компоновки.
/// </summary>
/// <value>
/// Родительский элемент или null, если элемент является корневым.
/// Это свойство управляется системой компоновки при добавлении или удалении элементов.
/// </value>
/// <remarks>
/// Изменение этого свойства вручную может привести к нарушению целостности дерева.
/// Для манипуляции структурой дерева следует использовать методы <see cref="DockOperations"/>.
/// </remarks>
IDockElement? Parent { get; set; }
/// <summary>
/// Получает или задает желаемую ширину элемента.
/// </summary>
/// <value>
/// Ширина элемента в пикселях или относительных единицах.
/// Может быть выражена как абсолютное значение (в пикселях) или как пропорция
/// (например, 0.5 для 50% доступного пространства).
/// </value>
/// <remarks>
/// Фактическая ширина элемента определяется родительским контейнером с учетом
/// минимальных размеров и соотношений разделения.
/// </remarks>
double Width { get; set; }
/// <summary>
/// Получает или задает желаемую высоту элемента.
/// </summary>
/// <value>
/// Высота элемента в пикселях или относительных единицах.
/// Может быть выражена как абсолютное значение (в пикселях) или как пропорция.
/// </value>
/// <remarks>
/// Фактическая высота элемента определяется родительским контейнером с учетом
/// минимальных размеров и соотношений разделения.
/// </remarks>
double Height { get; set; }
/// <summary>
/// Получает минимально допустимую ширину элемента.
/// </summary>
/// <value>
/// Минимальная ширина элемента в пикселях, при которой элемент сохраняет
/// базовую функциональность и читаемость содержимого.
/// </value>
/// <remarks>
/// Система компоновки не позволит уменьшить элемент ниже этого значения.
/// Для групп разделения минимальная ширина вычисляется рекурсивно на основе
/// минимальных размеров дочерних элементов.
/// </remarks>
double MinWidth { get; }
/// <summary>
/// Получает минимально допустимую высоту элемента.
/// </summary>
/// <value>
/// Минимальная высота элемента в пикселях, при которой элемент сохраняет
/// базовую функциональность и читаемость содержимого.
/// </value>
/// <remarks>
/// Система компоновки не позволит уменьшить элемент ниже этого значения.
/// Для групп разделения минимальная высота вычисляется рекурсивно на основе
/// минимальных размеров дочерних элементов.
/// </remarks>
double MinHeight { get; }
}

View File

@@ -0,0 +1,129 @@
using Lattice.Core.Docking.Abstractions;
using Lattice.Core.Docking.Models;
namespace Lattice.Core.Docking.Engine;
/// <summary>
/// Предоставляет статические методы для манипуляции иерархией дерева компоновки.
/// Содержит чистые алгоритмы трансформации графа без зависимости от UI.
/// </summary>
public static class DockOperations
{
/// <summary>
/// Извлекает элемент из дерева компоновки.
/// Если родительская группа остается с одним ребенком, она удаляется,
/// а оставшийся ребенок занимает её место в иерархии.
/// </summary>
/// <param name="element">Элемент для удаления из дерева.</param>
/// <param name="root">Текущий корневой элемент дерева.</param>
/// <returns>
/// Новый корневой элемент дерева после удаления и оптимизации структуры.
/// Возвращает null, если дерево становится пустым.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="root"/> равен null.
/// </exception>
public static IDockElement? Remove(IDockElement element, IDockElement root)
{
if (element == null) throw new ArgumentNullException(nameof(element));
if (root == null) throw new ArgumentNullException(nameof(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;
}
/// <summary>
/// Вставляет элемент в дерево компоновки относительно целевого элемента.
/// Создает новую группу разделения или объединяет контент в зависимости от позиции.
/// </summary>
/// <param name="target">Целевой элемент, относительно которого выполняется вставка.</param>
/// <param name="source">Вставляемый элемент.</param>
/// <param name="pos">Позиция вставки относительно целевого элемента.</param>
/// <param name="root">Текущий корневой элемент дерева.</param>
/// <returns>
/// Новый корневой элемент дерева после вставки и оптимизации структуры.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="target"/>, <paramref name="source"/>
/// или <paramref name="root"/> равны null.
/// </exception>
/// <exception cref="ArgumentException">
/// Выбрасывается, когда <paramref name="pos"/> имеет недопустимое значение.
/// </exception>
public static IDockElement Insert(IDockElement target, IDockElement source,
DockPosition pos, IDockElement root)
{
if (target == null) throw new ArgumentNullException(nameof(target));
if (source == null) throw new ArgumentNullException(nameof(source));
if (root == null) throw new ArgumentNullException(nameof(root));
// Случай 1: Объединение вкладок в центре
if (pos == DockPosition.Center)
{
if (target is IDockContainer targetContainer && source is IDockContainer sourceContainer)
{
// Переносим все вкладки из источника в целевой контейнер
var items = new List<IDockContent>(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;
}
// Если target был корнем, новая группа становится новым корнем
if (target == root)
{
newGroup.Parent = null;
return newGroup;
}
// Эта точка недостижима при правильном использовании,
// но добавляем для безопасности
newGroup.Parent = null;
return newGroup;
}
}

View File

@@ -0,0 +1,345 @@
using Lattice.Core.Docking.Abstractions;
using Lattice.Core.Docking.Models;
using Lattice.Core.Docking.Services;
using System.Collections.ObjectModel;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Lattice.UI.Docking.WinUI")]
namespace Lattice.Core.Docking.Engine;
/// <summary>
/// Центральный менеджер макета, управляющий всей структурой док-системы.
/// Координирует дерево компоновки, плавающие окна, автоскрываемые панели
/// и предоставляет API для манипуляции макетом. Использует кэширование
/// для оптимизации поиска элементов по идентификатору.
/// </summary>
public class LayoutManager
{
private readonly ObservableCollection<AutoHidePanel> _autoHidePanels = new();
private readonly Dictionary<string, IDockElement> _elementCache = new();
private IDockElement? _root;
/// <summary>
/// Получает или задает корневой элемент дерева компоновки главного окна.
/// При изменении значения генерируется событие <see cref="LayoutUpdated"/>.
/// </summary>
public IDockElement? Root
{
get => _root;
internal set
{
if (_root != value)
{
_root = value;
LayoutUpdated?.Invoke();
}
}
}
/// <summary>
/// Получает список активных плавающих окон.
/// </summary>
public List<DockWindow> FloatingWindows { get; } = new();
/// <summary>
/// Получает коллекцию автоскрываемых панелей.
/// </summary>
public ReadOnlyObservableCollection<AutoHidePanel> AutoHidePanels { get; }
/// <summary>
/// Получает или задает реестр типов контента.
/// </summary>
public ContentRegistry? ContentRegistry { get; set; }
/// <summary>
/// Происходит при изменении структуры дерева компоновки.
/// </summary>
public event Action? LayoutUpdated;
/// <summary>
/// Происходит при изменении коллекции автоскрываемых панелей.
/// </summary>
public event EventHandler? AutoHidePanelsChanged;
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="LayoutManager"/>.
/// </summary>
public LayoutManager()
{
AutoHidePanels = new ReadOnlyObservableCollection<AutoHidePanel>(_autoHidePanels);
}
/// <summary>
/// Добавляет автоскрываемую панель с указанным содержимым к заданной стороне окна.
/// </summary>
/// <param name="content">Содержимое панели.</param>
/// <param name="side">Сторона окна для прикрепления панели.</param>
/// <returns>Созданная автоскрываемая панель.</returns>
/// <exception cref="ArgumentNullException">Выбрасывается, когда <paramref name="content"/> равен null.</exception>
public AutoHidePanel AddAutoHidePanel(IDockContent content, DockSide side)
{
if (content == null) throw new ArgumentNullException(nameof(content));
var panel = new AutoHidePanel(content, side);
_autoHidePanels.Add(panel);
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
return panel;
}
/// <summary>
/// Удаляет автоскрываемую панель из коллекции.
/// </summary>
/// <param name="panel">Панель для удаления.</param>
/// <returns>true, если панель была успешно удалена; в противном случае false.</returns>
/// <exception cref="ArgumentNullException">Выбрасывается, когда <paramref name="panel"/> равен null.</exception>
public bool RemoveAutoHidePanel(AutoHidePanel panel)
{
if (panel == null) throw new ArgumentNullException(nameof(panel));
if (_autoHidePanels.Remove(panel))
{
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
return true;
}
return false;
}
/// <summary>
/// Создает документ указанного типа контента с заданным идентификатором.
/// </summary>
/// <param name="contentTypeId">Идентификатор типа контента.</param>
/// <param name="id">Уникальный идентификатор документа.</param>
/// <returns>Созданный контент или null, если ContentRegistry не установлен или тип контента не зарегистрирован.</returns>
public IDockContent? CreateDocument(string contentTypeId, string id)
{
if (ContentRegistry == null || !ContentRegistry.IsRegistered(contentTypeId))
return null;
return ContentRegistry.CreateContent(contentTypeId, id);
}
/// <summary>
/// Выполняет перемещение элемента в макете относительно целевого элемента.
/// </summary>
/// <param name="source">Перемещаемый элемент.</param>
/// <param name="target">Целевой элемент, относительно которого выполняется перемещение.</param>
/// <param name="position">Позиция перемещения относительно цели.</param>
/// <param name="asDocument">Если true, контент будет добавлен как документ в центральную область.</param>
/// <exception cref="ArgumentNullException">Выбрасывается, когда <paramref name="source"/> равен null.</exception>
public void Move(IDockElement source, IDockElement? target,
DockPosition position, bool asDocument = false)
{
if (source == null) throw new ArgumentNullException(nameof(source));
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;
// Обновляем кэш - удаляем перемещенный элемент
_elementCache.Remove(source.Id);
// 2. Вставляем в новое место
if (target == null)
{
// Создаем новое плавающее окно
FloatingWindows.Add(new DockWindow { Root = source });
}
else
{
if (Root != null && IsDescendantOf(target, Root))
{
Root = DockOperations.Insert(target, source, position, Root);
}
else
{
InsertIntoFloatingWindow(target, source, position);
}
}
// Обновляем кэш для вставленного элемента
_elementCache[source.Id] = source;
LayoutUpdated?.Invoke();
}
/// <summary>
/// Удаляет элемент из всех плавающих окон.
/// </summary>
/// <param name="element">Элемент для удаления.</param>
/// <returns>true, если элемент был найден и удален; в противном случае false.</returns>
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;
}
/// <summary>
/// Вставляет элемент в плавающее окно, содержащее целевой элемент.
/// </summary>
/// <param name="target">Целевой элемент в плавающем окне.</param>
/// <param name="source">Вставляемый элемент.</param>
/// <param name="position">Позиция вставки.</param>
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;
}
}
}
/// <summary>
/// Определяет, является ли элемент потомком указанного предка.
/// </summary>
/// <param name="element">Проверяемый элемент.</param>
/// <param name="ancestor">Предполагаемый предок.</param>
/// <returns>true, если элемент является потомком предка; в противном случае false.</returns>
private bool IsDescendantOf(IDockElement element, IDockElement ancestor)
{
var current = element.Parent;
while (current != null)
{
if (current == ancestor)
return true;
current = current.Parent;
}
return false;
}
/// <summary>
/// Находит элемент по его идентификатору во всех окнах (главном и плавающих).
/// Использует кэширование для оптимизации повторных поисков.
/// </summary>
/// <param name="id">Идентификатор элемента для поиска.</param>
/// <returns>Найденный элемент или null, если элемент с таким идентификатором не найден.</returns>
public IDockElement? FindById(string id)
{
if (string.IsNullOrEmpty(id)) return null;
// Проверка кэша
if (_elementCache.TryGetValue(id, out var cached))
return cached;
// Поиск в основном дереве
var found = FindRecursive(Root, id);
if (found != null)
{
_elementCache[id] = found;
return found;
}
// Поиск в плавающих окнах
foreach (var win in FloatingWindows)
{
found = FindRecursive(win.Root, id);
if (found != null)
{
_elementCache[id] = found;
return found;
}
}
return null;
}
/// <summary>
/// Рекурсивно ищет элемент по идентификатору в поддереве.
/// </summary>
/// <param name="node">Корневой узел поддерева для поиска.</param>
/// <param name="id">Идентификатор элемента для поиска.</param>
/// <returns>Найденный элемент или null, если элемент не найден.</returns>
private IDockElement? FindRecursive(IDockElement? node, string id)
{
if (node == null) return null;
if (node.Id == id) return node;
if (node is DockGroup g)
return FindRecursive(g.First, id) ?? FindRecursive(g.Second, id);
return null;
}
/// <summary>
/// Сбрасывает макет к состоянию по умолчанию.
/// Очищает корневой элемент, плавающие окна, автоскрываемые панели и кэш.
/// </summary>
public void Reset()
{
Root = null;
FloatingWindows.Clear();
_autoHidePanels.Clear();
_elementCache.Clear();
LayoutUpdated?.Invoke();
AutoHidePanelsChanged?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Находит элемент по идентификатору в дереве компоновки.
/// </summary>
/// <param name="id">Идентификатор элемента для поиска.</param>
/// <returns>Найденный элемент или null, если элемент с таким идентификатором не найден.</returns>
public IDockElement? FindElementById(string id)
{
return FindElementByIdRecursive(Root, id) ??
FloatingWindows.Select(w => FindElementByIdRecursive(w.Root, id))
.FirstOrDefault(result => result != null);
}
/// <summary>
/// Рекурсивно ищет элемент по идентификатору в поддереве.
/// </summary>
/// <param name="element">Корневой элемент поддерева для поиска.</param>
/// <param name="id">Идентификатор элемента для поиска.</param>
/// <returns>Найденный элемент или null, если элемент не найден.</returns>
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;
}
}

View File

@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Lattice.Core.Geometry\Lattice.Core.Geometry.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,83 @@
using Lattice.Core.Docking.Abstractions;
namespace Lattice.Core.Docking.Models;
/// <summary>
/// Представляет автоскрываемую панель, которая может быть прикреплена к одной из сторон окна.
/// Автоскрываемые панели скрываются, оставляя видимой только заголовок, и разворачиваются при наведении курсора или клике.
/// </summary>
public class AutoHidePanel
{
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="AutoHidePanel"/>.
/// </summary>
/// <param name="content">Содержимое панели.</param>
/// <param name="side">Сторона окна для прикрепления панели.</param>
/// <exception cref="ArgumentNullException">Выбрасывается, когда <paramref name="content"/> равен null.</exception>
public AutoHidePanel(IDockContent content, DockSide side)
{
Content = content ?? throw new ArgumentNullException(nameof(content));
Side = side;
}
/// <summary>
/// Получает уникальный идентификатор панели.
/// </summary>
public string Id { get; } = Guid.NewGuid().ToString();
/// <summary>
/// Получает содержимое панели.
/// </summary>
public IDockContent Content { get; }
/// <summary>
/// Получает или задает сторону окна, к которой прикреплена панель.
/// </summary>
public DockSide Side { get; set; }
/// <summary>
/// Получает или задает размер панели в пикселях.
/// Для левой/правой сторон - ширина, для верхней/нижней - высота.
/// </summary>
public double Size { get; set; } = 300;
/// <summary>
/// Получает или задает значение, указывающее, видима ли панель.
/// </summary>
public bool IsVisible { get; set; }
/// <summary>
/// Получает или задает смещение для анимации выезда/заезда панели.
/// Значение от 0.0 (полностью скрыта) до 1.0 (полностью развернута).
/// </summary>
public double SlideOffset { get; set; }
/// <summary>
/// Получает заголовок панели, взятый из содержимого.
/// </summary>
public string Title => Content?.Title ?? "Auto-hide Panel";
/// <summary>
/// Переключает видимость панели.
/// </summary>
public void Toggle()
{
IsVisible = !IsVisible;
}
/// <summary>
/// Показывает панель.
/// </summary>
public void Show()
{
IsVisible = true;
}
/// <summary>
/// Скрывает панель.
/// </summary>
public void Hide()
{
IsVisible = false;
}
}

View File

@@ -0,0 +1,133 @@
using Lattice.Core.Docking.Abstractions;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Lattice.Core.Docking.Models;
public class DockGroup : IDockElement, INotifyPropertyChanged
{
private IDockElement _first;
private IDockElement _second;
private SplitDirection _orientation;
private double _splitRatio = 0.5;
private IDockElement? _parent;
private double _width;
private double _height;
public event PropertyChangedEventHandler? PropertyChanged;
public DockGroup(IDockElement first, IDockElement second, SplitDirection orientation)
{
First = first ?? throw new ArgumentNullException(nameof(first));
Second = second ?? throw new ArgumentNullException(nameof(second));
Orientation = orientation;
}
public string Id { get; } = Guid.NewGuid().ToString();
public IDockElement? Parent
{
get => _parent;
set
{
if (_parent != value)
{
_parent = value;
OnPropertyChanged();
}
}
}
public IDockElement First
{
get => _first;
set
{
if (_first != value)
{
_first = value ?? throw new ArgumentNullException(nameof(value));
_first.Parent = this;
OnPropertyChanged();
}
}
}
public IDockElement Second
{
get => _second;
set
{
if (_second != value)
{
_second = value ?? throw new ArgumentNullException(nameof(value));
_second.Parent = this;
OnPropertyChanged();
}
}
}
public SplitDirection Orientation
{
get => _orientation;
set
{
if (_orientation != value)
{
_orientation = value;
OnPropertyChanged();
}
}
}
public double SplitRatio
{
get => _splitRatio;
set
{
if (Math.Abs(_splitRatio - value) > 0.001)
{
_splitRatio = value;
OnPropertyChanged();
}
}
}
public double Width
{
get => _width;
set
{
if (Math.Abs(_width - value) > 0.001)
{
_width = value;
OnPropertyChanged();
}
}
}
public double Height
{
get => _height;
set
{
if (Math.Abs(_height - value) > 0.001)
{
_height = value;
OnPropertyChanged();
}
}
}
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);
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

View File

@@ -0,0 +1,131 @@
using Lattice.Core.Docking.Abstractions;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Lattice.Core.Docking.Models;
public class DockLeaf : IDockContainer, INotifyPropertyChanged
{
private readonly ObservableCollection<IDockContent> _items = new();
private IDockContent? _activeContent;
private IDockElement? _parent;
private double _width;
private double _height;
private TabPlacement _tabPlacement = TabPlacement.Top;
public event PropertyChangedEventHandler? PropertyChanged;
public DockLeaf()
{
_items.CollectionChanged += (s, e) => OnPropertyChanged(nameof(Children));
}
public string Id { get; } = Guid.NewGuid().ToString();
public IDockElement? Parent
{
get => _parent;
set
{
if (_parent != value)
{
_parent = value;
OnPropertyChanged();
}
}
}
public IList<IDockContent> Children => _items;
public IDockContent? ActiveContent
{
get => _activeContent;
set
{
if (_activeContent != value)
{
_activeContent = value;
OnPropertyChanged();
}
}
}
public double Width
{
get => _width;
set
{
if (Math.Abs(_width - value) > 0.001)
{
_width = value;
OnPropertyChanged();
}
}
}
public double Height
{
get => _height;
set
{
if (Math.Abs(_height - value) > 0.001)
{
_height = value;
OnPropertyChanged();
}
}
}
public double MinWidth { get; set; } = 100;
public double MinHeight { get; set; } = 100;
public TabPlacement TabPlacement
{
get => _tabPlacement;
set
{
if (_tabPlacement != value)
{
_tabPlacement = value;
OnPropertyChanged();
}
}
}
public void AddContent(IDockContent content)
{
if (content == null)
throw new ArgumentNullException(nameof(content));
if (!_items.Contains(content))
{
_items.Add(content);
}
ActiveContent = content;
}
public void RemoveContent(IDockContent content)
{
if (content == null)
throw new ArgumentNullException(nameof(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;
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

View File

@@ -0,0 +1,33 @@
namespace Lattice.Core.Docking.Models;
/// <summary>
/// Определяет позицию вставки элемента относительно целевого элемента.
/// Используется при операциях перемещения и вставки элементов в дерево компоновки.
/// </summary>
public enum DockPosition
{
/// <summary>
/// Слева от целевого элемента.
/// </summary>
Left,
/// <summary>
/// Справа от целевого элемента.
/// </summary>
Right,
/// <summary>
/// Сверху от целевого элемента.
/// </summary>
Top,
/// <summary>
/// Снизу от целевого элемента.
/// </summary>
Bottom,
/// <summary>
/// В центре целевого элемента (для объединения вкладок).
/// </summary>
Center,
}

View File

@@ -0,0 +1,27 @@
namespace Lattice.Core.Docking.Models;
/// <summary>
/// Определяет стороны окна, к которым могут быть прикреплены автоскрываемые панели.
/// </summary>
public enum DockSide
{
/// <summary>
/// Левая сторона окна.
/// </summary>
Left,
/// <summary>
/// Правая сторона окна.
/// </summary>
Right,
/// <summary>
/// Верхняя сторона окна.
/// </summary>
Top,
/// <summary>
/// Нижняя сторона окна.
/// </summary>
Bottom
}

View File

@@ -0,0 +1,68 @@
using Lattice.Core.Docking.Abstractions;
namespace Lattice.Core.Docking.Models;
/// <summary>
/// Представляет плавающее окно в системе докинга.
/// Плавающие окна могут перемещаться по экрану независимо от главного окна.
/// </summary>
public class DockWindow
{
/// <summary>
/// Получает уникальный идентификатор окна.
/// </summary>
/// <value>
/// Строковый идентификатор, сгенерированный с помощью GUID.
/// Используется для сохранения позиции и размера окна в конфигурации.
/// </value>
public string Id { get; } = Guid.NewGuid().ToString();
/// <summary>
/// Получает или задает корневой элемент макета внутри данного окна.
/// </summary>
/// <value>
/// Корневой элемент дерева компоновки плавающего окна.
/// </value>
public IDockElement? Root { get; set; }
/// <summary>
/// Получает или задает позицию X окна на экране.
/// </summary>
/// <value>
/// Координата X левого верхнего угла окна в пикселях.
/// </value>
public double X { get; set; }
/// <summary>
/// Получает или задает позицию Y окна на экране.
/// </summary>
/// <value>
/// Координата Y левого верхнего угла окна в пикселях.
/// </value>
public double Y { get; set; }
/// <summary>
/// Получает или задает ширину окна.
/// </summary>
/// <value>
/// Ширина окна в пикселях. Значение по умолчанию: 800.
/// </value>
public double Width { get; set; } = 800;
/// <summary>
/// Получает или задает высоту окна.
/// </summary>
/// <value>
/// Высота окна в пикселях. Значение по умолчанию: 600.
/// </value>
public double Height { get; set; } = 600;
/// <summary>
/// Получает или задает заголовок окна.
/// </summary>
/// <value>
/// Текст заголовка окна. Обычно берется из активного контента.
/// Значение по умолчанию: "Lattice Tool Window".
/// </value>
public string Title { get; set; } = "Lattice Tool Window";
}

View File

@@ -0,0 +1,17 @@
namespace Lattice.Core.Docking.Models;
/// <summary>
/// Определяет направление разделения пространства внутри группы.
/// </summary>
public enum SplitDirection
{
/// <summary>
/// Разделение по горизонтали (создает левую и правую области).
/// </summary>
Horizontal,
/// <summary>
/// Разделение по вертикали (создает верхнюю и нижнюю области).
/// </summary>
Vertical
}

View File

@@ -0,0 +1,27 @@
namespace Lattice.Core.Docking.Models;
/// <summary>
/// Определяет положение полосы вкладок в контейнере.
/// </summary>
public enum TabPlacement
{
/// <summary>
/// Вкладки располагаются сверху.
/// </summary>
Top,
/// <summary>
/// Вкладки располагаются снизу.
/// </summary>
Bottom,
/// <summary>
/// Вкладки располагаются слева.
/// </summary>
Left,
/// <summary>
/// Вкладки располагаются справа.
/// </summary>
Right,
}

View File

@@ -0,0 +1,40 @@
namespace Lattice.Core.Docking.Serialization;
/// <summary>
/// Определяет контракт для сериализации и десериализации состояния макета док-системы.
/// Позволяет сохранять и восстанавливать расположение панелей, окон и их состояние.
/// </summary>
/// <remarks>
/// Эта абстракция позволяет реализовать различные форматы сериализации (JSON, XML, бинарный)
/// и различные хранилища (файлы, базы данных, облако) без изменения основной логики док-системы.
/// </remarks>
public interface ILayoutSerializer
{
/// <summary>
/// Сериализует состояние менеджера макета в строку.
/// </summary>
/// <param name="manager">Менеджер макета для сериализации.</param>
/// <returns>
/// Строковое представление состояния макета.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="manager"/> равен null.
/// </exception>
string Serialize(Engine.LayoutManager manager);
/// <summary>
/// Десериализует состояние макета из строки и восстанавливает его в менеджере.
/// </summary>
/// <param name="manager">Менеджер макета для восстановления состояния.</param>
/// <param name="serializedLayout">Сериализованное состояние макета.</param>
/// <param name="contentResolver">
/// Функция разрешения контента по идентификатору, используемая для восстановления
/// ссылок на контент в десериализованном состоянии.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="manager"/> или <paramref name="serializedLayout"/>
/// равны null.
/// </exception>
void Deserialize(Engine.LayoutManager manager, string serializedLayout,
Func<string, Abstractions.IDockContent?> contentResolver);
}

View File

@@ -0,0 +1,24 @@
namespace Lattice.Core.Docking.Serialization;
/// <summary>
/// Определяет контракт для объектов, которые могут предоставлять состояние для сериализации.
/// </summary>
public interface ISerializableLayout
{
/// <summary>
/// Получает состояние объекта для сериализации.
/// </summary>
/// <returns>
/// Объект состояния, готовый к сериализации.
/// </returns>
object GetSerializableState();
/// <summary>
/// Восстанавливает состояние объекта из десериализованного объекта.
/// </summary>
/// <param name="state">Десериализованное состояние.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="state"/> равен null.
/// </exception>
void RestoreFromState(object state);
}

View File

@@ -0,0 +1,235 @@
namespace Lattice.Core.Docking.Services;
/// <summary>
/// Реестр типов содержимого, который позволяет создавать экземпляры контента по типу.
/// Этот сервис является центральным для динамического создания панелей инструментов и документов.
/// </summary>
/// <remarks>
/// Реализует шаблон "Фабрика" для создания экземпляров <see cref="Abstractions.IDockContent"/>.
/// Позволяет регистрировать фабричные методы для различных типов контента, что обеспечивает
/// позднее связывание и возможность плагинной архитектуры.
/// </remarks>
public class ContentRegistry
{
private readonly Dictionary<string, ContentDescriptor> _contentTypes = new();
/// <summary>
/// Регистрирует фабричный метод для создания контента указанного типа.
/// </summary>
/// <typeparam name="T">
/// Тип контента, реализующий <see cref="Abstractions.IDockContent"/>.
/// </typeparam>
/// <param name="contentTypeId">Уникальный идентификатор типа контента.</param>
/// <param name="factory">Фабричный метод для создания экземпляров контента.</param>
/// <param name="metadata">Метаданные типа контента (опционально).</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="contentTypeId"/> или <paramref name="factory"/>
/// равны null.
/// </exception>
/// <exception cref="ArgumentException">
/// Выбрасывается, если <paramref name="contentTypeId"/> уже зарегистрирован.
/// </exception>
public void Register<T>(string contentTypeId, Func<T> 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 (string.IsNullOrEmpty(contentTypeId.Trim()))
throw new ArgumentException("Идентификатор типа контента не может быть пустой строкой.", nameof(contentTypeId));
if (_contentTypes.ContainsKey(contentTypeId))
throw new ArgumentException($"Тип контента '{contentTypeId}' уже зарегистрирован.");
_contentTypes[contentTypeId] = new ContentDescriptor(
typeof(T),
() => factory(),
metadata ?? new ContentMetadata(contentTypeId, typeof(T).Name)
);
}
/// <summary>
/// Создает новый экземпляр контента указанного типа с заданным идентификатором.
/// </summary>
/// <param name="contentTypeId">Идентификатор типа контента.</param>
/// <param name="id">Уникальный идентификатор для создаваемого экземпляра контента.</param>
/// <returns>
/// Новый экземпляр контента.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="contentTypeId"/> равен null или пустой строке.
/// </exception>
/// <exception cref="KeyNotFoundException">
/// Выбрасывается, если тип контента не зарегистрирован.
/// </exception>
public Abstractions.IDockContent CreateContent(string contentTypeId, string id)
{
if (string.IsNullOrWhiteSpace(contentTypeId))
throw new ArgumentNullException(nameof(contentTypeId));
if (!_contentTypes.TryGetValue(contentTypeId, out var descriptor))
throw new KeyNotFoundException($"Тип контента '{contentTypeId}' не зарегистрирован.");
var content = descriptor.Factory();
content.SetId(id);
return content;
}
/// <summary>
/// Получает метаданные для указанного типа контента.
/// </summary>
/// <param name="contentTypeId">Идентификатор типа контента.</param>
/// <returns>
/// Метаданные типа контента или null, если тип не найден.
/// </returns>
public ContentMetadata? GetMetadata(string contentTypeId)
{
if (string.IsNullOrWhiteSpace(contentTypeId))
return null;
return _contentTypes.TryGetValue(contentTypeId, out var descriptor)
? descriptor.Metadata
: null;
}
/// <summary>
/// Получает все зарегистрированные типы контента.
/// </summary>
/// <returns>
/// Коллекция идентификаторов зарегистрированных типов контента.
/// </returns>
public IEnumerable<string> GetRegisteredTypes() => _contentTypes.Keys;
/// <summary>
/// Проверяет, зарегистрирован ли указанный тип контента.
/// </summary>
/// <param name="contentTypeId">Идентификатор типа контента.</param>
/// <returns>
/// true, если тип контента зарегистрирован; в противном случае false.
/// </returns>
public bool IsRegistered(string contentTypeId)
{
if (string.IsNullOrWhiteSpace(contentTypeId))
return false;
return _contentTypes.ContainsKey(contentTypeId);
}
/// <summary>
/// Представляет дескриптор типа контента, содержащий информацию о фабричном методе и метаданных.
/// </summary>
private class ContentDescriptor
{
/// <summary>
/// Получает тип контента.
/// </summary>
public Type ContentType { get; }
/// <summary>
/// Получает фабричный метод для создания экземпляров контента.
/// </summary>
public Func<Abstractions.IDockContent> Factory { get; }
/// <summary>
/// Получает метаданные типа контента.
/// </summary>
public ContentMetadata Metadata { get; }
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="ContentDescriptor"/>.
/// </summary>
/// <param name="contentType">Тип контента.</param>
/// <param name="factory">Фабричный метод.</param>
/// <param name="metadata">Метаданные.</param>
public ContentDescriptor(Type contentType, Func<Abstractions.IDockContent> factory,
ContentMetadata metadata)
{
ContentType = contentType;
Factory = factory;
Metadata = metadata;
}
}
}
/// <summary>
/// Представляет метаданные типа контента, предоставляющие дополнительную информацию для отображения в UI.
/// </summary>
public class ContentMetadata
{
/// <summary>
/// Получает идентификатор типа контента.
/// </summary>
/// <value>
/// Уникальный строковый идентификатор типа контента.
/// </value>
public string ContentTypeId { get; }
/// <summary>
/// Получает или задает отображаемое имя типа контента.
/// </summary>
/// <value>
/// Имя типа контента, отображаемое пользователю.
/// </value>
public string DisplayName { get; set; }
/// <summary>
/// Получает или задает описание типа контента.
/// </summary>
/// <value>
/// Текстовое описание функциональности контента.
/// </value>
public string Description { get; set; }
/// <summary>
/// Получает или задает имя ресурса для иконки типа контента.
/// </summary>
/// <value>
/// Имя ресурса иконки или null, если иконка не определена.
/// </value>
public string? IconResource { get; set; }
/// <summary>
/// Получает или задает значение, указывающее, является ли контент документом
/// (а не инструментальной панелью).
/// </summary>
/// <value>
/// true, если контент является документом; в противном случае false.
/// </value>
public bool IsDocument { get; set; }
/// <summary>
/// Получает или задает ширину контента по умолчанию.
/// </summary>
/// <value>
/// Ширина контента в пикселях. Значение по умолчанию: 300.
/// </value>
public double DefaultWidth { get; set; } = 300;
/// <summary>
/// Получает или задает высоту контента по умолчанию.
/// </summary>
/// <value>
/// Высота контента в пикселях. Значение по умолчанию: 200.
/// </value>
public double DefaultHeight { get; set; } = 200;
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="ContentMetadata"/>.
/// </summary>
/// <param name="contentTypeId">Идентификатор типа контента.</param>
/// <param name="displayName">Отображаемое имя типа контента.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="contentTypeId"/> или <paramref name="displayName"/>
/// равны null.
/// </exception>
public ContentMetadata(string contentTypeId, string displayName)
{
ContentTypeId = contentTypeId ?? throw new ArgumentNullException(nameof(contentTypeId));
DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName));
Description = string.Empty;
}
}

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,79 @@
namespace Lattice.Core.Geometry;
/// <summary>
/// Представляет точку в двумерном пространстве с координатами X и Y.
/// Эта структура является платформонезависимой и может использоваться
/// во всех слоях системы Lattice.
/// </summary>
public struct Point : IEquatable<Point>
{
/// <summary>
/// Получает точку с координатами (0, 0).
/// </summary>
public static readonly Point Zero = new(0, 0);
/// <summary>
/// Координата X (горизонтальная).
/// </summary>
public double X { get; set; }
/// <summary>
/// Координата Y (вертикальная).
/// </summary>
public double Y { get; set; }
/// <summary>
/// Инициализирует новую точку с указанными координатами.
/// </summary>
/// <param name="x">Координата X.</param>
/// <param name="y">Координата Y.</param>
public Point(double x, double y)
{
X = x;
Y = y;
}
/// <summary>
/// Создает точку из System.Drawing.Point.
/// </summary>
public static Point FromDrawingPoint(System.Drawing.Point point) =>
new(point.X, point.Y);
/// <summary>
/// Преобразует точку в System.Drawing.Point.
/// </summary>
public System.Drawing.Point ToDrawingPoint() =>
new((int)X, (int)Y);
/// <summary>
/// Определяет, равна ли эта точка другой точке.
/// </summary>
public bool Equals(Point other) =>
Math.Abs(X - other.X) < double.Epsilon &&
Math.Abs(Y - other.Y) < double.Epsilon;
/// <inheritdoc/>
public override bool Equals(object? obj) =>
obj is Point point && Equals(point);
/// <inheritdoc/>
public override int GetHashCode() =>
HashCode.Combine(X, Y);
/// <summary>
/// Определяет, равны ли две точки.
/// </summary>
public static bool operator ==(Point left, Point right) =>
left.Equals(right);
/// <summary>
/// Определяет, не равны ли две точки.
/// </summary>
public static bool operator !=(Point left, Point right) =>
!left.Equals(right);
/// <summary>
/// Возвращает строковое представление точки.
/// </summary>
public override string ToString() => $"{X}, {Y}";
}

View File

@@ -0,0 +1,153 @@
namespace Lattice.Core.Geometry;
/// <summary>
/// Представляет прямоугольник в двумерном пространстве с позицией и размерами.
/// Эта структура является платформонезависимой и может использоваться
/// во всех слоях системы Lattice.
/// </summary>
public struct Rect : IEquatable<Rect>
{
/// <summary>
/// Получает пустой прямоугольник (позиция (0, 0), размеры (0, 0)).
/// </summary>
public static readonly Rect Empty = new(0, 0, 0, 0);
/// <summary>
/// Координата X левого верхнего угла прямоугольника.
/// </summary>
public double X { get; set; }
/// <summary>
/// Координата Y левого верхнего угла прямоугольника.
/// </summary>
public double Y { get; set; }
/// <summary>
/// Ширина прямоугольника.
/// </summary>
public double Width { get; set; }
/// <summary>
/// Высота прямоугольника.
/// </summary>
public double Height { get; set; }
/// <summary>
/// Получает координату X правого края прямоугольника.
/// </summary>
public double Right => X + Width;
/// <summary>
/// Получает координату Y нижнего края прямоугольника.
/// </summary>
public double Bottom => Y + Height;
/// <summary>
/// Получает левый верхний угол прямоугольника.
/// </summary>
public Point TopLeft => new(X, Y);
/// <summary>
/// Получает правый нижний угол прямоугольника.
/// </summary>
public Point BottomRight => new(Right, Bottom);
/// <summary>
/// Получает центр прямоугольника.
/// </summary>
public Point Center => new(X + Width / 2, Y + Height / 2);
/// <summary>
/// Получает площадь прямоугольника.
/// </summary>
public double Area => Width * Height;
/// <summary>
/// Инициализирует новый прямоугольник с указанными параметрами.
/// </summary>
/// <param name="x">Координата X.</param>
/// <param name="y">Координата Y.</param>
/// <param name="width">Ширина.</param>
/// <param name="height">Высота.</param>
public Rect(double x, double y, double width, double height)
{
X = x;
Y = y;
Width = width;
Height = height;
}
/// <summary>
/// Инициализирует новый прямоугольник с указанной позицией и размером.
/// </summary>
/// <param name="location">Позиция прямоугольника.</param>
/// <param name="size">Размер прямоугольника.</param>
public Rect(Point location, Size size)
{
X = location.X;
Y = location.Y;
Width = size.Width;
Height = size.Height;
}
/// <summary>
/// Создает прямоугольник из System.Drawing.Rectangle.
/// </summary>
public static Rect FromDrawingRectangle(System.Drawing.Rectangle rect) =>
new(rect.X, rect.Y, rect.Width, rect.Height);
/// <summary>
/// Преобразует прямоугольник в System.Drawing.Rectangle.
/// </summary>
public System.Drawing.Rectangle ToDrawingRectangle() =>
new((int)X, (int)Y, (int)Width, (int)Height);
/// <summary>
/// Проверяет, содержит ли прямоугольник указанную точку.
/// </summary>
public bool Contains(Point point) =>
point.X >= X && point.X <= Right &&
point.Y >= Y && point.Y <= Bottom;
/// <summary>
/// Проверяет, пересекается ли этот прямоугольник с другим.
/// </summary>
public bool Intersects(Rect other) =>
X < other.Right && Right > other.X &&
Y < other.Bottom && Bottom > other.Y;
/// <summary>
/// Определяет, равен ли этот прямоугольник другому прямоугольнику.
/// </summary>
public bool Equals(Rect other) =>
Math.Abs(X - other.X) < double.Epsilon &&
Math.Abs(Y - other.Y) < double.Epsilon &&
Math.Abs(Width - other.Width) < double.Epsilon &&
Math.Abs(Height - other.Height) < double.Epsilon;
/// <inheritdoc/>
public override bool Equals(object? obj) =>
obj is Rect rect && Equals(rect);
/// <inheritdoc/>
public override int GetHashCode() =>
HashCode.Combine(X, Y, Width, Height);
/// <summary>
/// Определяет, равны ли два прямоугольника.
/// </summary>
public static bool operator ==(Rect left, Rect right) =>
left.Equals(right);
/// <summary>
/// Определяет, не равны ли два прямоугольника.
/// </summary>
public static bool operator !=(Rect left, Rect right) =>
!left.Equals(right);
/// <summary>
/// Возвращает строковое представление прямоугольника.
/// </summary>
public override string ToString() =>
$"[X={X:F2}, Y={Y:F2}, Width={Width:F2}, Height={Height:F2}]";
}

View File

@@ -0,0 +1,84 @@
namespace Lattice.Core.Geometry;
/// <summary>
/// Представляет размеры в двумерном пространстве с шириной и высотой.
/// Эта структура является платформонезависимой и может использоваться
/// во всех слоях системы Lattice.
/// </summary>
public struct Size : IEquatable<Size>
{
/// <summary>
/// Получает размер с нулевой шириной и высотой.
/// </summary>
public static readonly Size Zero = new(0, 0);
/// <summary>
/// Ширина.
/// </summary>
public double Width { get; set; }
/// <summary>
/// Высота.
/// </summary>
public double Height { get; set; }
/// <summary>
/// Получает признак того, что размер является пустым (нулевая ширина или высота).
/// </summary>
public bool IsEmpty => Width <= 0 || Height <= 0;
/// <summary>
/// Инициализирует новый размер с указанными значениями.
/// </summary>
/// <param name="width">Ширина.</param>
/// <param name="height">Высота.</param>
public Size(double width, double height)
{
Width = width;
Height = height;
}
/// <summary>
/// Создает размер из System.Drawing.Size.
/// </summary>
public static Size FromDrawingSize(System.Drawing.Size size) =>
new(size.Width, size.Height);
/// <summary>
/// Преобразует размер в System.Drawing.Size.
/// </summary>
public System.Drawing.Size ToDrawingSize() =>
new((int)Width, (int)Height);
/// <summary>
/// Определяет, равен ли этот размер другому размеру.
/// </summary>
public bool Equals(Size other) =>
Math.Abs(Width - other.Width) < double.Epsilon &&
Math.Abs(Height - other.Height) < double.Epsilon;
/// <inheritdoc/>
public override bool Equals(object? obj) =>
obj is Size size && Equals(size);
/// <inheritdoc/>
public override int GetHashCode() =>
HashCode.Combine(Width, Height);
/// <summary>
/// Определяет, равны ли два размера.
/// </summary>
public static bool operator ==(Size left, Size right) =>
left.Equals(right);
/// <summary>
/// Определяет, не равны ли два размера.
/// </summary>
public static bool operator !=(Size left, Size right) =>
!left.Equals(right);
/// <summary>
/// Возвращает строковое представление размера.
/// </summary>
public override string ToString() => $"{Width} × {Height}";
}

View File

@@ -1,27 +0,0 @@
namespace Lattice.Core.Abstractions;
/// <summary>
/// Сервис управления контекстом приложения и связанными командами.
/// </summary>
public interface IContextService
{
/// <summary>
/// Имя текущего активного контекста.
/// </summary>
string CurrentContext { get; }
/// <summary>
/// Возникает при смене фокуса между вкладками с разными ContextGroup.
/// </summary>
event EventHandler<string>? ContextChanged;
/// <summary>
/// Устанавливает активный контекст. Вызывается UI-слоем при активации вкладки.
/// </summary>
void SetContext(string contextGroup);
/// <summary>
/// Проверяет, должна ли команда быть видимой в текущем контексте.
/// </summary>
bool IsCommandVisible(string commandId, string commandContext);
}

View File

@@ -1,33 +0,0 @@
namespace Lattice.Core.Abstractions;
/// <summary>
/// Описывает компонент, который может быть размещен внутри узла компоновки Lattice.
/// </summary>
public interface IDockableComponent
{
/// <summary>
/// Уникальный строковый идентификатор компонента (например, "SolutionExplorer").
/// </summary>
string UniqueId { get; }
/// <summary>
/// Заголовок, отображаемый на вкладке или в заголовке панели.
/// </summary>
string DisplayName { get; }
/// <summary>
/// Ключ иконки (для Segoe Fluent Icons или путей к ресурсам).
/// </summary>
string? IconKey { get; }
/// <summary>
/// Группа контекста (например, "CodeEditor", "Debugger").
/// Определяет, какие панели инструментов будут активны.
/// </summary>
string ContextGroup { get; }
/// <summary>
/// Указывает, разрешено ли закрывать данный компонент пользователем.
/// </summary>
bool CanClose { get; }
}

View File

@@ -1,42 +0,0 @@
namespace Lattice.Core.Abstractions;
/// <summary>
/// Представляет базовый элемент иерархии компоновки Lattice.
/// </summary>
public interface ILayoutElement
{
/// <summary>
/// Уникальный идентификатор элемента.
/// </summary>
Guid Id { get; }
/// <summary>
/// Имя элемента для отображения или идентификации в логах.
/// </summary>
string Name { get; set; }
/// <summary>
/// Значение ширины (в пикселях или долях "star").
/// </summary>
double WidthValue { get; set; }
/// <summary>
/// Указывает, является ли ширина пропорциональной (star).
/// </summary>
bool IsWidthStar { get; set; }
/// <summary>
/// Значение высоты (в пикселях или долях "star").
/// </summary>
double HeightValue { get; set; }
/// <summary>
/// Указывает, является ли высота пропорциональной (star).
/// </summary>
bool IsHeightStar { get; set; }
/// <summary>
/// Родительский элемент в дереве компоновки.
/// </summary>
ILayoutElement? Parent { get; set; }
}

View File

@@ -1,40 +0,0 @@
using Lattice.Core.Models;
using Lattice.Core.Models.Enums;
namespace Lattice.Core.Abstractions;
/// <summary>
/// Сервис управления жизненным циклом макета приложения.
/// </summary>
public interface ILayoutService
{
/// <summary>
/// Текущий корневой узел всей структуры окон.
/// </summary>
LayoutNode? Root { get; }
/// <summary>
/// Событие, возникающее при любом изменении структуры (докинг, закрытие, изменение размеров).
/// </summary>
event EventHandler? LayoutUpdated;
/// <summary>
/// Перемещает узел в указанную позицию относительно целевого узла.
/// </summary>
void Dock(LayoutNode source, LayoutNode target, DockDirection direction);
/// <summary>
/// Удаляет узел из макета (например, при закрытии вкладки).
/// </summary>
void Remove(LayoutNode node);
/// <summary>
/// Импортирует структуру макета из снапшота.
/// </summary>
void LoadLayout(string jsonData);
/// <summary>
/// Экспортирует текущую структуру в строку для сохранения.
/// </summary>
string SaveLayout();
}

View File

@@ -1,39 +0,0 @@
using Lattice.Core.Abstractions;
namespace Lattice.Core.Context;
/// <summary>
/// Реализация сервиса управления контекстом приложения.
/// </summary>
public class ContextManager : IContextService
{
private string _currentContext = "Common";
/// <inheritdoc/>
public string CurrentContext => _currentContext;
/// <inheritdoc/>
public event EventHandler<string>? ContextChanged;
/// <inheritdoc/>
public void SetContext(string contextGroup)
{
if (string.IsNullOrWhiteSpace(contextGroup)) contextGroup = "Common";
if (_currentContext != contextGroup)
{
_currentContext = contextGroup;
ContextChanged?.Invoke(this, contextGroup);
}
}
/// <inheritdoc/>
public bool IsCommandVisible(string commandId, string commandContext)
{
// Базовая логика: команда видима, если её контекст совпадает с текущим
// или если команда помечена как общая ("Common" или "Global").
return commandContext == "Common" ||
commandContext == "Global" ||
commandContext == _currentContext;
}
}

View File

@@ -1,228 +0,0 @@
using Lattice.Core.Abstractions;
using Lattice.Core.Models;
using Lattice.Core.Models.Enums;
using Lattice.Core.Persistence;
using Microsoft.Extensions.Logging;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Lattice.Core.Engine;
/// <summary>
/// Реализация сервиса управления макетом.
/// </summary>
public class LayoutManager : ILayoutService
{
private readonly ILogger? _logger;
private LayoutNode? _root;
/// <inheritdoc/>
public LayoutNode? Root => _root;
/// <inheritdoc/>
public event EventHandler? LayoutUpdated;
public LayoutManager(ILogger<LayoutManager>? logger = null)
{
_logger = logger;
}
/// <inheritdoc/>
public void Dock(LayoutNode source, LayoutNode target, DockDirection direction)
{
if (source == target) return;
_logger?.LogDebug("Начало трансформации дерева: {Source} -> {Target} ({Direction})", source.Name, target.Name, direction);
// 1. Извлекаем источник из его текущего места в дереве
Remove(source);
// 2. Если докинг в центр — это логика объединения (например, в TabView)
// В рамках Core это может означать добавление в тот же контейнер
if (direction == DockDirection.Center)
{
HandleCenterDock(source, target);
}
else
{
// 3. Создаем разделение (Split)
HandleSideDock(source, target, direction);
}
LayoutUpdated?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Логика добавления элемента в центральную часть (вкладки).
/// </summary>
private void HandleCenterDock(LayoutNode source, LayoutNode target)
{
if (target.Parent is SplitContainerNode parent)
{
parent.AddChild(source);
}
else if (target == _root)
{
// Если таргет - корень, и мы докаем в центр, создаем контейнер по умолчанию
var container = new SplitContainerNode(SplitOrientation.Horizontal);
_root = container;
container.AddChild(target);
container.AddChild(source);
}
}
/// <summary>
/// Логика разделения существующей области на две (Side Dock).
/// </summary>
private void HandleSideDock(LayoutNode source, LayoutNode target, DockDirection direction)
{
var orientation = (direction == DockDirection.Left || direction == DockDirection.Right)
? SplitOrientation.Horizontal
: SplitOrientation.Vertical;
var parent = target.Parent as SplitContainerNode;
// Создаем новый сплиттер, который заменит target
var newContainer = new SplitContainerNode(orientation);
if (parent != null)
{
// Заменяем target на новый контейнер в списке детей родителя
int index = parent.Children.IndexOf(target);
parent.Children[index] = newContainer;
newContainer.Parent = parent;
}
else
{
// Если родителя нет, значит target был корнем
_root = newContainer;
}
// Настраиваем порядок в новом сплиттере
if (direction == DockDirection.Left || direction == DockDirection.Top)
{
newContainer.AddChild(source);
newContainer.AddChild(target);
}
else
{
newContainer.AddChild(target);
newContainer.AddChild(source);
}
// Корректируем размеры (например, делим пополам)
source.WidthValue = 0.5;
target.WidthValue = 0.5;
source.IsWidthStar = true;
target.IsWidthStar = true;
}
/// <inheritdoc/>
public void Remove(LayoutNode node)
{
if (node.Parent is SplitContainerNode parent)
{
parent.Children.Remove(node);
node.Parent = null;
// Если в контейнере остался один элемент — убираем лишнюю вложенность
if (parent.Children.Count == 1)
{
CollapseContainer(parent);
}
}
else if (node == _root)
{
_root = null;
}
LayoutUpdated?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Убирает ненужные контейнеры, если в них остался только один элемент.
/// </summary>
private void CollapseContainer(SplitContainerNode container)
{
var lastChild = container.Children[0];
var parent = container.Parent as SplitContainerNode;
if (parent != null)
{
int index = parent.Children.IndexOf(container);
parent.Children[index] = lastChild;
lastChild.Parent = parent;
}
else
{
_root = lastChild;
lastChild.Parent = null;
}
}
/// <inheritdoc/>
public string SaveLayout()
{
if (_root == null) return string.Empty;
var options = GetJsonOptions();
try
{
string json = JsonSerializer.Serialize(_root, options);
_logger?.LogInformation("Макет успешно экспортирован в JSON. Длина: {Length}", json.Length);
return json;
}
catch (Exception ex)
{
_logger?.LogError(ex, "Ошибка при сохранении макета Lattice");
return string.Empty;
}
}
/// <inheritdoc/>
public void LoadLayout(string jsonData)
{
if (string.IsNullOrWhiteSpace(jsonData)) return;
var options = GetJsonOptions();
try
{
var importedRoot = JsonSerializer.Deserialize<LayoutNode>(jsonData, options);
if (importedRoot != null)
{
// При загрузке нужно восстановить связи Parent, так как они не сериализуются (циклические ссылки)
RestoreParentLinks(importedRoot, null);
_root = importedRoot;
_logger?.LogInformation("Макет успешно загружен. Корневой узел: {Id}", _root.Id);
LayoutUpdated?.Invoke(this, EventArgs.Empty);
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "Ошибка при десериализации макета Lattice");
}
}
private JsonSerializerOptions GetJsonOptions()
{
return new JsonSerializerOptions
{
WriteIndented = true,
Converters = { new LayoutJsonConverter() },
// Игнорируем циклы, так как мы восстановим Parent вручную
ReferenceHandler = ReferenceHandler.IgnoreCycles
};
}
private void RestoreParentLinks(LayoutNode node, LayoutNode? parent)
{
node.Parent = parent;
if (node is SplitContainerNode container)
{
foreach (var child in container.Children)
{
RestoreParentLinks(child, container);
}
}
}
}

View File

@@ -1,28 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Поддержка LTS версий и актуальной на 2026 год .NET 10 -->
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Lattice.Core</AssemblyName>
<RootNamespace>Lattice.Core</RootNamespace>
<!-- Метаданные разработчика -->
<Authors>FrigaT</Authors>
<Company>FrigaT</Company>
<RepositoryUrl>https://git.frigat.duckdns.org/FrigaT/Lattice</RepositoryUrl>
<PackageProjectUrl>https://git.frigat.duckdns.org/FrigaT/Lattice</PackageProjectUrl>
<Description>Core docking and layout engine for Lattice UI (WinUI 3 / Uno Platform).</Description>
<!-- Совместимость с Uno Platform (Trimming и AOT) -->
<IsTrimmable Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true</IsTrimmable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="System.Text.Json" Version="8.0.0" />
</ItemGroup>
</Project>

View File

@@ -1,32 +0,0 @@
namespace Lattice.Core.Models;
/// <summary>
/// Определение действия (команды), которое может быть отображено в интерфейсе.
/// </summary>
public record ActionDefinition
{
/// <summary>
/// Уникальный идентификатор команды.
/// </summary>
public string Id { get; init; } = Guid.NewGuid().ToString();
/// <summary>
/// Текст кнопки.
/// </summary>
public string Label { get; init; } = "Action";
/// <summary>
/// Группа контекста, к которой привязана кнопка (например, "CodeEditor").
/// </summary>
public string TargetContext { get; init; } = "Common";
/// <summary>
/// Указывает, активна ли кнопка в данный момент.
/// </summary>
public bool IsEnabled { get; set; } = true;
/// <summary>
/// Подсказка (Tooltip).
/// </summary>
public string Tooltip { get; init; } = string.Empty;
}

View File

@@ -1,29 +0,0 @@
using Lattice.Core.Abstractions;
namespace Lattice.Core.Models;
/// <summary>
/// Узел, представляющий конечный контент (вкладку, панель инструментов или документ).
/// </summary>
public class ContentNode : LayoutNode
{
/// <summary>
/// Ссылка на визуальный или логический компонент, закрепленный в этом узле.
/// </summary>
public IDockableComponent? Component { get; set; }
/// <summary>
/// Указывает, является ли данный узел частью основной рабочей области документов.
/// </summary>
public bool IsDocumentArea { get; set; }
/// <summary>
/// Инициализирует новый экземпляр <see cref="ContentNode"/> на основе компонента.
/// </summary>
/// <param name="component">Компонент содержимого.</param>
public ContentNode(IDockableComponent component)
{
Component = component;
Name = component.DisplayName;
}
}

View File

@@ -1,11 +0,0 @@
namespace Lattice.Core.Models.Enums;
public enum DockDirection
{
Center,
Left,
Right,
Top,
Bottom,
Floating,
}

View File

@@ -1,13 +0,0 @@
namespace Lattice.Core.Models.Enums;
public enum SplitOrientation
{
/// <summary>
/// Элементы располагаются друг за другом по горизонтали
/// </summary>
Horizontal,
/// <summary>
/// Элементы располагаются друг за другом по вертикали
/// </summary>
Vertical,
}

View File

@@ -1,35 +0,0 @@
using Lattice.Core.Abstractions;
namespace Lattice.Core.Models;
/// <summary>
/// Абстрактный базовый класс для всех узлов дерева компоновки.
/// </summary>
public abstract class LayoutNode : ILayoutElement
{
/// <inheritdoc/>
public Guid Id { get; } = Guid.NewGuid();
/// <inheritdoc/>
public string Name { get; set; } = string.Empty;
/// <inheritdoc/>
public double WidthValue { get; set; } = 1.0;
/// <inheritdoc/>
public bool IsWidthStar { get; set; } = true;
/// <inheritdoc/>
public double HeightValue { get; set; } = 1.0;
/// <inheritdoc/>
public bool IsHeightStar { get; set; } = true;
/// <inheritdoc/>
public ILayoutElement? Parent { get; set; }
/// <summary>
/// Возвращает строковое представление узла для отладки.
/// </summary>
public override string ToString() => $"{GetType().Name} [{Name}] ({Id.ToString()[..4]})";
}

View File

@@ -1,38 +0,0 @@
using Lattice.Core.Models.Enums;
namespace Lattice.Core.Models;
/// <summary>
/// Узел-контейнер, разделяющий пространство между дочерними элементами в определенной ориентации.
/// </summary>
public class SplitContainerNode : LayoutNode
{
/// <summary>
/// Ориентация разделения (горизонтальная или вертикальная).
/// </summary>
public SplitOrientation Orientation { get; set; }
/// <summary>
/// Список дочерних узлов, находящихся внутри данного контейнера.
/// </summary>
public List<LayoutNode> Children { get; } = new();
/// <summary>
/// Инициализирует новый экземпляр <see cref="SplitContainerNode"/>.
/// </summary>
/// <param name="orientation">Ориентация контейнера.</param>
public SplitContainerNode(SplitOrientation orientation)
{
Orientation = orientation;
}
/// <summary>
/// Добавляет дочерний узел в контейнер и устанавливает связь с родителем.
/// </summary>
/// <param name="child">Узел для добавления.</param>
public void AddChild(LayoutNode child)
{
child.Parent = this;
Children.Add(child);
}
}

View File

@@ -1,12 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Lattice.Core.Models
{
internal class WorkspaceSnapshot
{
}
}

View File

@@ -1,31 +0,0 @@
using Lattice.Core.Models;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Lattice.Core.Persistence;
/// <summary>
/// Конвертер для полиморфной сериализации и десериализации узлов дерева Lattice.
/// </summary>
public class LayoutJsonConverter : JsonConverter<LayoutNode>
{
public override LayoutNode? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var jsonDoc = JsonDocument.ParseValue(ref reader);
var rootElement = jsonDoc.RootElement;
// Определяем тип узла по наличию специфических свойств
if (rootElement.TryGetProperty("Orientation", out _))
{
return JsonSerializer.Deserialize<SplitContainerNode>(rootElement.GetRawText(), options);
}
return JsonSerializer.Deserialize<ContentNode>(rootElement.GetRawText(), options);
}
public override void Write(Utf8JsonWriter writer, LayoutNode value, JsonSerializerOptions options)
{
// Используем стандартную сериализацию для конкретных типов
JsonSerializer.Serialize(writer, (object)value, value.GetType(), options);
}
}

View File

@@ -1,52 +0,0 @@
# Lattice.Core
[![Framework](img.shields.io)](#)
[![Author](img.shields.io)](git.frigat.duckdns.org)
[![Platform](img.shields.io)](#)
**Lattice.Core** — это платформонезависимое ядро (Layout Engine) для построения сложных интерфейсов с системой докинга в стиле Visual Studio 2026.
Библиотека является частью экосистемы **Lattice** и отвечает исключительно за математику макета, управление деревом узлов и контекстное состояние, не имея зависимостей от конкретных UI-фреймворков.
## 🚀 Особенности
- **Агностическая архитектура**: Полная совместимость с .NET 8+, WinUI 3 и Uno Platform.
- **Древовидная компоновка**: Управление интерфейсом через узлы (`Split` и `Content`).
- **Context-Aware System**: Встроенный сервис отслеживания контекста для динамического переключения панелей инструментов.
- **Smart Docking**: Алгоритмы автоматического разделения зон и схлопывания пустых контейнеров.
- **JSON Persistence**: Полиморфная сериализация макетов для сохранения и загрузки состояний пользователя.
## 📁 Структура проекта
* `Abstractions/` — Интерфейсы для расширения системы.
* `Models/` — Базовые сущности дерева (узлы, направления, ориентация).
* `Engine/``LayoutManager`, реализующий логику трансформации дерева.
* `Context/` — Сервисы управления активными состояниями и командами.
* `Persistence/` — Логика сохранения макета в JSON.
## 🛠 Использование
### Создание базового макета
```csharp
var layoutManager = new LayoutManager();
// Создаем контентные узлы
var explorer = new ContentNode(new MyToolComponent("Solution Explorer", "Explorer"));
var editor = new ContentNode(new MyDocumentComponent("Main.cs", "CodeEditor"));
// Устанавливаем редактор как корень
layoutManager.SetRoot(editor);
// Прикрепляем проводник слева от редактора
layoutManager.Dock(explorer, editor, DockDirection.Left);
//Переключение контекста
var contextService = new ContextManager();
// Вызывается при активации вкладки в UI
contextService.SetContext("CodeEditor");
// Проверка видимости команд в текущем контексте
bool isDebugVisible = contextService.IsCommandVisible("btnDebug", "CodeEditor");
```

View File

@@ -0,0 +1,15 @@
<Application
x:Class="Lattice.Example.DragDrop.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Lattice.Example.DragDrop">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- Fluent Theme Resources -->
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -0,0 +1,33 @@
using Lattice.Themes;
using Lattice.Themes.Fluent;
using Microsoft.UI.Xaml;
namespace Lattice.Example.DragDrop;
public partial class App : Application
{
private Window? _window;
public App()
{
InitializeComponent();
}
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
// Регистрируем Fluent тему
var themeManager = ThemeManager.Current;
themeManager.RegisterTheme(new FluentThemePack(false)); // Light тема
themeManager.RegisterTheme(new FluentThemePack(true)); // Dark тема
// Применяем тему по умолчанию
themeManager.ApplyTheme("Fluent Dark");
// Создаем главное окно
_window = new MainWindow();
_window.Activate();
// Регистрируем окно в трекере
WindowTracker.Register(_window);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,64 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>Lattice.Example.DragDrop</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Platforms>x86;x64;ARM64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
<UseWinUI>true</UseWinUI>
<WinUISDKReferences>false</WinUISDKReferences>
<EnableMsixTooling>true</EnableMsixTooling>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Content Include="Assets\SplashScreen.scale-200.png" />
<Content Include="Assets\LockScreenLogo.scale-200.png" />
<Content Include="Assets\Square150x150Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Assets\StoreLogo.png" />
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
</ItemGroup>
<ItemGroup>
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<!--
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
Tools extension to be activated for this project even if the Windows App SDK Nuget
package has not yet been restored.
-->
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<ProjectCapability Include="Msix" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260101001" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Lattice.Themes.Core\Lattice.Themes.Core.csproj" />
<ProjectReference Include="..\Lattice.Themes.Fluent\Lattice.Themes.Fluent.csproj" />
</ItemGroup>
<!--
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
Explorer "Package and Publish" context menu entry to be enabled for this project even if
the Windows App SDK Nuget package has not yet been restored.
-->
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
</PropertyGroup>
<!-- Publish Properties -->
<PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed>
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,141 @@
<Window
x:Class="Lattice.Example.DragDrop.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:lattice="using:Lattice.UI.DragDrop.WinUI"
Title="Drag Drop Demo"
>
<Grid Background="#F0F2F5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Инструкция -->
<TextBlock Grid.Row="0"
Text="Просто перетащите элементы справа влево!"
FontSize="14" Margin="20" HorizontalAlignment="Center"
FontWeight="SemiBold"/>
<!-- Основное содержимое -->
<Grid Grid.Row="1" Margin="20">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- ЦЕЛЕВАЯ ЗОНА (куда бросаем) -->
<Border Grid.Column="0"
Background="White"
CornerRadius="10"
BorderThickness="2"
BorderBrush="#4CAF50"
Padding="20"
Margin="0,0,10,0"
lattice:DragDropProperties.IsDropTarget="True">
<StackPanel>
<TextBlock Text="🟢 ЦЕЛЕВАЯ ЗОНА"
FontSize="16" FontWeight="Bold"
Foreground="#4CAF50"
Margin="0,0,0,15"/>
<TextBlock x:Name="DropInfoText"
Text="Бросьте сюда элементы"
FontSize="14"
Foreground="#666"
TextWrapping="Wrap"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</StackPanel>
</Border>
<!-- ЗОНА С ЭЛЕМЕНТАМИ (откуда тянем) -->
<StackPanel Grid.Column="1"
Background="White"
CornerRadius="10"
Padding="20"
Margin="10,0,0,0">
<TextBlock Text="📦 ЭЛЕМЕНТЫ ДЛЯ ПЕРЕТАСКИВАНИЯ"
FontSize="16" FontWeight="Bold"
Foreground="#2196F3"
Margin="0,0,0,15"/>
<!-- 1. TextBlock элемент -->
<Border Padding="15"
Background="#E3F2FD"
CornerRadius="8"
BorderThickness="1"
BorderBrush="#90CAF9"
Margin="0,0,0,10"
lattice:DragDropProperties.IsDragSource="True"
lattice:DragDropProperties.DragData="TextBlock Element">
<TextBlock Text="📝 Это TextBlock"
FontSize="14"
Foreground="#1565C0"
FontWeight="SemiBold"/>
</Border>
<!-- 2. Border элемент -->
<Border Padding="15"
Background="#E8F5E9"
CornerRadius="8"
BorderThickness="1"
BorderBrush="#A5D6A7"
Margin="0,0,0,10"
lattice:DragDropProperties.IsDragSource="True"
lattice:DragDropProperties.DragData="Border Element">
<StackPanel>
<TextBlock Text="🟩 Это Border"
FontSize="14"
Foreground="#2E7D32"
FontWeight="SemiBold"/>
<TextBlock Text="С рамкой и заливкой"
FontSize="12"
Foreground="#666"
Margin="0,5,0,0"/>
</StackPanel>
</Border>
<!-- 3. Grid элемент -->
<Border Padding="15"
Background="#FFF3E0"
CornerRadius="8"
BorderThickness="1"
BorderBrush="#FFCC80"
lattice:DragDropProperties.IsDragSource="True"
lattice:DragDropProperties.DragData="Grid Element">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Иконка -->
<TextBlock Grid.Column="0"
Text="🔲"
FontSize="18"
Margin="0,0,10,0"
VerticalAlignment="Center"/>
<!-- Контент -->
<StackPanel Grid.Column="1">
<TextBlock Text="Это Grid"
FontSize="14"
Foreground="#EF6C00"
FontWeight="SemiBold"/>
<TextBlock Text="С несколькими колонками"
FontSize="12"
Foreground="#666"/>
</StackPanel>
</Grid>
</Border>
</StackPanel>
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,17 @@
using Lattice.UI.DragDrop.WinUI;
using Microsoft.UI.Xaml;
namespace Lattice.Example.DragDrop;
public sealed partial class MainWindow : Window
{
private bool _isInitialized = false;
public MainWindow()
{
InitializeComponent();
XamlInitializer.Initialize(this);
Title = "✅ Drag & Drop работает! Перетащите элементы →";
}
}

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity
Name="dcfd6640-86d9-4ce7-bc17-24685f01b577"
Publisher="CN=frost"
Version="1.0.0.0" />
<mp:PhoneIdentity PhoneProductId="dcfd6640-86d9-4ce7-bc17-24685f01b577" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>Lattice.Example.DragDrop</DisplayName>
<PublisherDisplayName>frost</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="Lattice.Example.DragDrop"
Description="Lattice.Example.DragDrop"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View File

@@ -0,0 +1,10 @@
{
"profiles": {
"Lattice.Example.DragDrop (Package)": {
"commandName": "MsixPackage"
},
"Lattice.Example.DragDrop (Unpackaged)": {
"commandName": "Project"
}
}
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="Lattice.Example.DragDrop.app"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- The ID below informs the system that this application is compatible with OS features first introduced in Windows 10.
It is necessary to support features in unpackaged applications, for example the custom titlebar implementation.
For more info see https://docs.microsoft.com/windows/apps/windows-app-sdk/use-windows-app-sdk-run-time#declare-os-compatibility-in-your-application-manifest -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>

19
Lattice.IDE/App.xaml Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Application
x:Class="Lattice.IDE.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:lt="using:Lattice.Themes"
xmlns:local="using:Lattice.IDE">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<ResourceDictionary Source="ms-appx:///Lattice.UI.Docking.WinUI/Themes/Generic.xaml" />
<!-- Other merged dictionaries here -->
</ResourceDictionary.MergedDictionaries>
<!-- Other app resources here -->
</ResourceDictionary>
</Application.Resources>
</Application>

37
Lattice.IDE/App.xaml.cs Normal file
View File

@@ -0,0 +1,37 @@
using Lattice.Themes;
using Microsoft.UI.Xaml;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace Lattice.IDE
{
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class App : Application
{
private Window? _window;
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
public App()
{
InitializeComponent();
}
/// <summary>
/// Invoked when the application is launched.
/// </summary>
/// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
ThemeManager.Current.ApplyTheme(new FluentThemePack());
_window = new MainWindow();
_window.Activate();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<UserControl
x:Class="Lattice.IDE.Controls.EditorView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Lattice.IDE.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{ThemeResource Lattice.Brush.Background.Secondary}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<!-- Полоса номеров строк -->
<ColumnDefinition Width="*"/>
<!-- Текст кода -->
</Grid.ColumnDefinitions>
<!-- Левая панель с номерами строк -->
<StackPanel Grid.Column="0" Background="{ThemeResource Lattice.Brush.Background.Primary}" Padding="10,5">
<TextBlock Text="1" Foreground="Gray" FontFamily="Cascadia Code, Consolas"/>
<TextBlock Text="2" Foreground="Gray" FontFamily="Cascadia Code, Consolas"/>
<TextBlock Text="3" Foreground="Gray" FontFamily="Cascadia Code, Consolas"/>
<TextBlock Text="4" Foreground="Gray" FontFamily="Cascadia Code, Consolas"/>
<TextBlock Text="5" Foreground="Gray" FontFamily="Cascadia Code, Consolas"/>
<TextBlock Text="6" Foreground="Gray" FontFamily="Cascadia Code, Consolas"/>
</StackPanel>
<!-- Основная область редактирования -->
<TextBox Grid.Column="1"
AcceptsReturn="True"
IsSpellCheckEnabled="False"
TextWrapping="NoWrap"
FontFamily="Cascadia Code, Consolas"
FontSize="14"
BorderThickness="0"
Padding="10"
Background="Transparent"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
xml:space="preserve">
<TextBox.Text>using System;
using Lattice.Core;
namespace Lattice.IDE.Demo;
public class Program
{
public void Main()
{
Console.WriteLine("Hello, Lattice 2026!");
}
}
</TextBox.Text>
</TextBox>
</Grid>
</UserControl>

View File

@@ -0,0 +1,15 @@
using Microsoft.UI.Xaml.Controls;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace Lattice.IDE.Controls
{
public sealed partial class EditorView : UserControl
{
public EditorView()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<UserControl
x:Class="Lattice.IDE.Controls.SolutionExplorerView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Lattice.IDE.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{ThemeResource LayerFillColorDefaultBrush}" Padding="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
Text="SOLUTION 'LATTICE' (2026)"
FontSize="11"
FontWeight="Bold"
Opacity="0.6"/>
<TreeView Grid.Row="1" Margin="0,10,0,0">
<TreeView.RootNodes>
<TreeViewNode Content="Lattice.Core.Docking" IsExpanded="True">
<TreeViewNode.Children>
<TreeViewNode Content="Models" />
<TreeViewNode Content="Engine" />
</TreeViewNode.Children>
</TreeViewNode>
<TreeViewNode Content="Lattice.UI.Docking.WinUI" IsExpanded="True">
<TreeViewNode.Children>
<TreeViewNode Content="Controls" />
<TreeViewNode Content="Themes" />
</TreeViewNode.Children>
</TreeViewNode>
</TreeView.RootNodes>
</TreeView>
</Grid>
</UserControl>

View File

@@ -0,0 +1,15 @@
using Microsoft.UI.Xaml.Controls;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace Lattice.IDE.Controls
{
public sealed partial class SolutionExplorerView : UserControl
{
public SolutionExplorerView()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,81 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFrameworks>net8.0-windows10.0.19041.0;net9.0-windows10.0.19041.0;net10.0-windows10.0.19041.0</TargetFrameworks>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>Lattice.IDE</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Platforms>x86;x64;ARM64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
<UseWinUI>true</UseWinUI>
<WinUISDKReferences>false</WinUISDKReferences>
<EnableMsixTooling>true</EnableMsixTooling>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Remove="Controls\EditorView.xaml" />
<None Remove="Controls\SolutionExplorerView.xaml" />
</ItemGroup>
<ItemGroup>
<Content Include="Assets\SplashScreen.scale-200.png" />
<Content Include="Assets\LockScreenLogo.scale-200.png" />
<Content Include="Assets\Square150x150Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Assets\StoreLogo.png" />
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
</ItemGroup>
<ItemGroup>
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<!--
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
Tools extension to be activated for this project even if the Windows App SDK Nuget
package has not yet been restored.
-->
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<ProjectCapability Include="Msix" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Lattice.Core.Docking\Lattice.Core.Docking.csproj" />
<ProjectReference Include="..\Lattice.Themes.Core\Lattice.Themes.Core.csproj" />
<ProjectReference Include="..\Lattice.Themes.Fluent\Lattice.Themes.Fluent.csproj" />
<ProjectReference Include="..\Lattice.Themes.VS2026\Lattice.Themes.VS2026.csproj" />
<ProjectReference Include="..\Lattice.UI.Docking.WinUI\Lattice.UI.Docking.WinUI.csproj" />
</ItemGroup>
<ItemGroup>
<Page Update="Controls\EditorView.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\SolutionExplorerView.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!--
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
Explorer "Package and Publish" context menu entry to be enabled for this project even if
the Windows App SDK Nuget package has not yet been restored.
-->
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
</PropertyGroup>
<!-- Publish Properties -->
<PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed>
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,28 @@
using Lattice.Core.Docking.Abstractions;
namespace Lattice.IDE;
/// <summary>
/// Реализация контента для демонстрации, принимающая любой UI-объект.
/// </summary>
public class DemoContent : IDockContent
{
public string Id { get; }
public string Title { get; set; }
/// <summary>
/// Сюда мы передаем наш UserControl (SolutionExplorerView и т.д.)
/// </summary>
public object View { get; set; }
public bool CanClose { get; set; } = true;
public DemoContent(string id, string title, object view)
{
Id = id;
Title = title;
View = view;
}
public bool OnClosing() => true;
}

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<Window
x:Class="Lattice.IDE.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Lattice.IDE"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:lattice="using:Lattice.UI"
mc:Ignorable="d"
Title="Lattice.IDE">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Меню управления Demo -->
<StackPanel Orientation="Horizontal" Spacing="10" Padding="10" Background="{ThemeResource SystemControlBackgroundChromeMediumLowBrush}">
<Button Content="Fluent UI Theme" Click="SetFluentTheme"/>
<Button Content="VS 2026 Theme" Click="SetVSTheme"/>
<TextBlock Text="Lattice IDE Demo 2026" VerticalAlignment="Center" Margin="20,0" FontWeight="Bold"/>
</StackPanel>
<!-- Lattice Docking Host -->
<lattice:LatticeDockHost x:Name="DockHost" Grid.Row="1" />
</Grid>
</Window>

View File

@@ -0,0 +1,64 @@
using Lattice.Core.Docking.Engine;
using Lattice.Core.Docking.Models;
using Lattice.IDE.Controls;
using Lattice.Themes;
using Microsoft.UI.Xaml;
// Ïðåäïîëîæèì, ÷òî VS2026Theme òîæå ðåàëèçîâàí àíàëîãè÷íî Fluent
// using Lattice.Themes.VisualStudio2026;
namespace Lattice.IDE;
public sealed partial class MainWindow : Window
{
private LayoutManager _manager;
public MainWindow()
{
this.InitializeComponent();
WindowTracker.Register(this);
SystemBackdrop = new Microsoft.UI.Xaml.Media.MicaBackdrop();
InitLattice();
}
private void InitLattice()
{
_manager = new LayoutManager();
// Ñîçäàåì êîíòåíò íà îñíîâå XAML UserControls
var solutionExplorer = new DemoContent(
"sln",
"Solution Explorer",
new SolutionExplorerView()
);
var editor = new DemoContent(
"code_01",
"Program.cs",
new EditorView()
);
// Ñîáèðàåì äåðåâî (êàê ðàíüøå)
var leftLeaf = new DockLeaf();
leftLeaf.AddContent(solutionExplorer);
var centerLeaf = new DockLeaf()
{
TabPlacement = TabPlacement.Top,
};
centerLeaf.AddContent(editor);
var rootGroup = new DockGroup(leftLeaf, centerLeaf, SplitDirection.Horizontal)
{
SplitRatio = 0.25
};
_manager.SetRoot(rootGroup);
DockHost.Manager = _manager;
}
private void SetFluentTheme(object sender, RoutedEventArgs e) =>
ThemeManager.Current.ApplyTheme(new FluentThemePack());
private void SetVSTheme(object sender, RoutedEventArgs e) =>
ThemeManager.Current.ApplyTheme(new VS2026ThemePack());
}

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity
Name="c6596033-98b6-4778-8280-bc9256b9be07"
Publisher="CN=frost"
Version="1.0.0.0" />
<mp:PhoneIdentity PhoneProductId="c6596033-98b6-4778-8280-bc9256b9be07" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>Lattice.IDE</DisplayName>
<PublisherDisplayName>frost</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="Lattice.IDE"
Description="Lattice.IDE"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View File

@@ -0,0 +1,10 @@
{
"profiles": {
"Lattice.IDE (Package)": {
"commandName": "MsixPackage"
},
"Lattice.IDE (Unpackaged)": {
"commandName": "Project"
}
}
}

19
Lattice.IDE/app.manifest Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="Lattice.IDE.app"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- The ID below informs the system that this application is compatible with OS features first introduced in Windows 10.
It is necessary to support features in unpackaged applications, for example the custom titlebar implementation.
For more info see https://docs.microsoft.com/windows/apps/windows-app-sdk/use-windows-app-sdk-run-time#declare-os-compatibility-in-your-application-manifest -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>

View File

@@ -0,0 +1,30 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Lattice.Layout.UI.WinUI.Controls;
/// <summary>
/// Контрол для отображения группы вкладок.
/// Содержит заголовки вкладок и область содержимого.
/// </summary>
public sealed class WinUIGroupControl : Grid
{
/// <summary>
/// Контрол TabView, содержащий вкладки и их содержимое.
/// </summary>
public TabView TabView { get; }
/// <summary>
/// Создаёт новый экземпляр <see cref="WinUIGroupControl"/>.
/// </summary>
public WinUIGroupControl()
{
TabView = new TabView
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch
};
Children.Add(TabView);
}
}

View File

@@ -0,0 +1,134 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using System;
namespace Lattice.Layout.UI.WinUI.Controls;
/// <summary>
/// Контрол для отображения содержимого конечного элемента раскладки.
/// Является контейнером для реального UI-контента, подставляемого через ContentResolver.
/// </summary>
public sealed class WinUIItemControl : ContentControl
{
/// <summary>
/// Идентификатор содержимого, связанного с данным элементом.
/// Используется для подстановки реального UI-контрола через ContentResolver.
/// </summary>
public string? ContentId
{
get => (string?)GetValue(ContentIdProperty);
set => SetValue(ContentIdProperty, value);
}
public static readonly DependencyProperty ContentIdProperty =
DependencyProperty.Register(
nameof(ContentId),
typeof(string),
typeof(WinUIItemControl),
new PropertyMetadata(default(string), OnContentIdChanged));
/// <summary>
/// Делегат, который должен вернуть реальный UI-контент по ContentId.
/// Устанавливается WinUILayoutHost.
/// </summary>
public Func<string, UIElement?>? ContentResolver
{
get => (Func<string, UIElement?>?)GetValue(ContentResolverProperty);
set => SetValue(ContentResolverProperty, value);
}
public static readonly DependencyProperty ContentResolverProperty =
DependencyProperty.Register(
nameof(ContentResolver),
typeof(Func<string, UIElement?>),
typeof(WinUIItemControl),
new PropertyMetadata(null, OnContentResolverChanged));
/// <summary>
/// Вызывается, когда контент успешно загружен.
/// </summary>
public event Action<WinUIItemControl>? ContentLoaded;
/// <summary>
/// Вызывается, когда контент был очищен (Detach).
/// </summary>
public event Action<WinUIItemControl>? ContentCleared;
/// <summary>
/// Создаёт новый экземпляр <see cref="WinUIItemControl"/>.
/// </summary>
public WinUIItemControl()
{
HorizontalContentAlignment = HorizontalAlignment.Stretch;
VerticalContentAlignment = VerticalAlignment.Stretch;
// Fallback-контент, если ContentResolver не установлен
Content = CreatePlaceholder();
}
private static void OnContentIdChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is WinUIItemControl control)
control.TryLoadContent();
}
private static void OnContentResolverChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is WinUIItemControl control)
control.TryLoadContent();
}
/// <summary>
/// Пытается загрузить реальный контент по ContentId.
/// </summary>
private void TryLoadContent()
{
if (ContentId is null || ContentResolver is null)
{
Content = CreatePlaceholder();
return;
}
var resolved = ContentResolver(ContentId);
if (resolved is null)
{
Content = CreatePlaceholder($"Контент '{ContentId}' не найден");
return;
}
Content = resolved;
ContentLoaded?.Invoke(this);
}
/// <summary>
/// Очищает контент (используется визуалом при Detach).
/// </summary>
public void ClearContent()
{
Content = CreatePlaceholder();
ContentCleared?.Invoke(this);
}
/// <summary>
/// Создаёт placeholder-контент, отображаемый до загрузки реального UI.
/// </summary>
private static UIElement CreatePlaceholder(string? message = null)
{
return new Border
{
Background = new SolidColorBrush(Microsoft.UI.Colors.Transparent),
BorderBrush = new SolidColorBrush(Microsoft.UI.Colors.Gray),
BorderThickness = new Thickness(1),
Padding = new Thickness(8),
Child = new TextBlock
{
Text = message ?? "Нет содержимого",
Opacity = 0.6,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
}
};
}
}

View File

@@ -0,0 +1,134 @@
using Lattice.Layout.Abstractions;
using Lattice.Layout.UI.Docking;
using Lattice.Layout.UI.WinUI.Docking;
using Lattice.Layout.UI.WinUI.Rendering;
using Lattice.Layout.UI.WinUI.Visuals;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
namespace Lattice.Layout.UI.WinUI.Controls;
/// <summary>
/// WinUI-контрол, отображающий дерево раскладки.
/// Оборачивает LayoutRenderer и размещает визуальное дерево внутри себя.
/// </summary>
public sealed class WinUILayoutHost : UserControl, ILayoutView
{
/// <summary>
/// Слой, в котором размещается визуальное дерево раскладки.
/// </summary>
public Grid LayoutLayer { get; }
/// <summary>
/// Слой для отображения подсветки зон докинга.
/// </summary>
public DockOverlayHost OverlayLayer { get; }
private readonly LayoutRenderer _renderer;
private readonly WinUIVisualFactory _factory;
/// <summary>
/// Функция, возвращающая UI-содержимое по ContentId.
/// Используется визуальными элементами для подстановки реального контрола.
/// </summary>
public Func<string, UIElement>? ContentResolver { get; set; }
/// <summary>
/// Корневой элемент раскладки, который необходимо отобразить.
/// </summary>
public ILayoutRoot? Root
{
get => (ILayoutRoot?)GetValue(RootProperty);
set => SetValue(RootProperty, value);
}
/// <summary>
/// Свойство зависимости для <see cref="Root"/>.
/// </summary>
public static readonly DependencyProperty RootProperty =
DependencyProperty.Register(
nameof(Root),
typeof(ILayoutRoot),
typeof(WinUILayoutHost),
new PropertyMetadata(null, OnRootChanged));
private static void OnRootChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is WinUILayoutHost host)
{
host.Refresh();
}
}
/// <summary>
/// Создаёт новый экземпляр <see cref="WinUILayoutHost"/>.
/// </summary>
public WinUILayoutHost()
{
LayoutLayer = new Grid();
OverlayLayer = new DockOverlayHost();
var rootGrid = new Grid();
rootGrid.Children.Add(LayoutLayer);
rootGrid.Children.Add(OverlayLayer);
Content = rootGrid;
_factory = new WinUIVisualFactory();
_renderer = new LayoutRenderer(_factory);
}
/// <summary>
/// Выполняет полную перерисовку визуального дерева.
/// </summary>
public void Refresh()
{
LayoutLayer.Children.Clear();
if (Root?.Child is null)
return;
var visual = _renderer.Build(Root.Child);
visual.Attach();
switch (visual)
{
case WinUISplitVisual splitVisual:
LayoutLayer.Children.Add(splitVisual.Control);
break;
case WinUIGroupVisual groupVisual:
LayoutLayer.Children.Add(groupVisual.Control);
break;
case WinUIItemVisual itemVisual:
LayoutLayer.Children.Add(itemVisual.Control);
break;
}
}
/// <summary>
/// Показывает подсветку зоны докинга для указанной цели.
/// </summary>
public void ShowDockOverlay(DockTarget target)
{
if (target.Visual is not IWinUIVisual winuiVisual)
return;
var control = winuiVisual.Control;
var bounds = control.TransformToVisual(LayoutLayer)
.TransformBounds(new Windows.Foundation.Rect(0, 0, control.ActualWidth, control.ActualHeight));
OverlayLayer.ShowOverlay(target, bounds);
}
/// <summary>
/// Скрывает подсветку зон докинга.
/// </summary>
public void HideDockOverlay()
{
OverlayLayer.HideOverlay();
}
}

View File

@@ -0,0 +1,85 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Lattice.Layout.UI.WinUI.Controls;
/// <summary>
/// Контейнер для отображения сплит-элемента раскладки.
/// Использует Grid и автоматически создаёт строки/столбцы под детей.
/// </summary>
public sealed class WinUISplitControl : Grid
{
/// <summary>
/// Ориентация сплита (горизонтальная или вертикальная).
/// </summary>
public Orientation LayoutOrientation
{
get => (Orientation)GetValue(LayoutOrientationProperty);
set => SetValue(LayoutOrientationProperty, value);
}
public static readonly DependencyProperty LayoutOrientationProperty =
DependencyProperty.Register(
nameof(LayoutOrientation),
typeof(Orientation),
typeof(WinUISplitControl),
new PropertyMetadata(Orientation.Horizontal, OnOrientationChanged));
private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is WinUISplitControl control)
control.RebuildGrid();
}
public WinUISplitControl()
{
Loaded += (_, _) => RebuildGrid();
}
/// <summary>
/// Перестраивает структуру Grid в зависимости от ориентации и количества детей.
/// </summary>
public void RebuildGrid()
{
RowDefinitions.Clear();
ColumnDefinitions.Clear();
if (Children.Count == 0)
return;
if (LayoutOrientation == Orientation.Horizontal)
{
// Горизонтальный сплит → столбцы
for (int i = 0; i < Children.Count; i++)
{
ColumnDefinitions.Add(new ColumnDefinition
{
Width = new GridLength(1, GridUnitType.Star)
});
if (Children[i] is FrameworkElement fe) Grid.SetColumn(fe, i);
}
}
else
{
// Вертикальный сплит → строки
for (int i = 0; i < Children.Count; i++)
{
RowDefinitions.Add(new RowDefinition
{
Height = new GridLength(1, GridUnitType.Star)
});
if (Children[i] is FrameworkElement fe) Grid.SetColumn(fe, i);
}
}
}
/// <summary>
/// Добавляет дочерний элемент и перестраивает Grid.
/// </summary>
public new void ChildrenChanged()
{
RebuildGrid();
}
}

View File

@@ -0,0 +1,83 @@
using Lattice.Layout.Abstractions;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Windows.Foundation;
namespace Lattice.Layout.UI.WinUI.Docking;
/// <summary>
/// Полупрозрачная подсветка зоны докинга.
/// </summary>
public sealed class DockOverlay : Control
{
public static readonly DependencyProperty ZoneProperty =
DependencyProperty.Register(
nameof(Zone),
typeof(DockZone),
typeof(DockOverlay),
new PropertyMetadata(DockZone.Center, OnVisualPropertyChanged));
public static readonly DependencyProperty BoundsProperty =
DependencyProperty.Register(
nameof(Bounds),
typeof(Rect),
typeof(DockOverlay),
new PropertyMetadata(Rect.Empty, OnVisualPropertyChanged));
/// <summary>
/// Зона докинга, которую нужно подсветить.
/// </summary>
public DockZone Zone
{
get => (DockZone)GetValue(ZoneProperty);
set => SetValue(ZoneProperty, value);
}
/// <summary>
/// Прямоугольник зоны в координатах родительского контейнера.
/// </summary>
public Rect Bounds
{
get => (Rect)GetValue(BoundsProperty);
set => SetValue(BoundsProperty, value);
}
private Border? _border;
public DockOverlay()
{
IsHitTestVisible = false;
DefaultStyleKey = typeof(DockOverlay);
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_border = GetTemplateChild("PART_Border") as Border;
UpdateVisual();
}
private static void OnVisualPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is DockOverlay overlay)
{
overlay.UpdateVisual();
}
}
private void UpdateVisual()
{
if (_border is null)
return;
Canvas.SetLeft(this, Bounds.X);
Canvas.SetTop(this, Bounds.Y);
Width = Bounds.Width;
Height = Bounds.Height;
_border.BorderBrush = new SolidColorBrush(Microsoft.UI.Colors.DeepSkyBlue);
_border.BorderThickness = new Thickness(2);
_border.Background = new SolidColorBrush(Microsoft.UI.Colors.LightSkyBlue) { Opacity = 0.25 };
}
}

View File

@@ -0,0 +1,47 @@
using Lattice.Layout.UI.Docking;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.Foundation;
namespace Lattice.Layout.UI.WinUI.Docking;
/// <summary>
/// Контейнер для отображения подсветки зон докинга.
/// Обычно используется как Overlay-слой внутри WinUILayoutHost.
/// </summary>
public sealed class DockOverlayHost : Canvas
{
private DockOverlay? _currentOverlay;
public DockOverlayHost()
{
IsHitTestVisible = false;
}
/// <summary>
/// Отображает подсветку для указанной цели докинга.
/// </summary>
public void ShowOverlay(DockTarget target, Rect bounds)
{
if (_currentOverlay is null)
{
_currentOverlay = new DockOverlay();
Children.Add(_currentOverlay);
}
_currentOverlay.Zone = target.Zone;
_currentOverlay.Bounds = bounds;
_currentOverlay.Visibility = Visibility.Visible;
}
/// <summary>
/// Скрывает подсветку зоны докинга.
/// </summary>
public void HideOverlay()
{
if (_currentOverlay is not null)
{
_currentOverlay.Visibility = Visibility.Collapsed;
}
}
}

View File

@@ -0,0 +1,78 @@
using Lattice.Layout.UI.Docking;
using Lattice.Layout.UI.WinUI.Controls;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using System;
using System.Linq;
using Windows.Foundation;
namespace Lattice.Layout.UI.WinUI.Docking;
/// <summary>
/// Выполняет hit-test для определения зоны докинга в WinUI.
/// </summary>
public static class DockZoneHitTester
{
/// <summary>
/// Выполняет hit-test по экранной точке и возвращает цель докинга.
/// </summary>
/// <param name="host">WinUI-хост раскладки.</param>
/// <param name="screenPoint">Точка в координатах окна.</param>
public static DockTarget? HitTest(WinUILayoutHost host, Point screenPoint)
{
if (host is null)
throw new ArgumentNullException(nameof(host));
// Предполагаем, что LayoutLayer — основной слой, в котором живёт визуальное дерево.
var layoutLayer = host.LayoutLayer;
if (layoutLayer is null)
return null;
// Переводим координаты в систему координат layoutLayer.
var elements = VisualTreeHelper.FindElementsInHostCoordinates(screenPoint, host.LayoutLayer);
var firstElement = elements.FirstOrDefault();
if (firstElement is null)
return null;
// Ищем ближайший IWinUIVisual.
var visual = FindVisual(firstElement);
if (visual is null)
return null;
if (visual is not ILayoutVisual layoutVisual)
return null;
// Вычисляем зону докинга для найденного контрола.
var control = visual.Control;
var bounds = control.TransformToVisual(layoutLayer)
.TransformBounds(new Rect(0, 0, control.ActualWidth, control.ActualHeight));
var localX = screenPoint.X - bounds.X;
var localY = screenPoint.Y - bounds.Y;
var zone = DockingUtils.GetZone(localX, localY, bounds.Width, bounds.Height);
return new DockTarget(layoutVisual, zone);
}
private static IWinUIVisual? FindVisual(UIElement element)
{
DependencyObject? current = element;
while (current is not null)
{
if (current is FrameworkElement fe && fe.DataContext is IWinUIVisual ctxVisual)
return ctxVisual;
if (current is IWinUIVisual winuiVisual)
return winuiVisual;
current = VisualTreeHelper.GetParent(current);
}
return null;
}
}

View File

@@ -0,0 +1,15 @@
using Microsoft.UI.Xaml;
namespace Lattice.Layout.UI.WinUI.Docking;
/// <summary>
/// Интерфейс для визуальных элементов WinUI, соответствующих элементам раскладки.
/// Нужен для hit-test и расчёта зон докинга.
/// </summary>
public interface IWinUIVisual
{
/// <summary>
/// Реальный WinUI-элемент, отображающий данный визуальный элемент раскладки.
/// </summary>
FrameworkElement Control { get; }
}

View File

@@ -0,0 +1,118 @@
using Lattice.Layout.Abstractions;
using Lattice.Layout.UI.WinUI.Controls;
using Microsoft.UI.Xaml;
using System;
namespace Lattice.Layout.UI.WinUI;
/// <summary>
/// Набор вспомогательных методов для упрощённой работы с визуальным хостом раскладки.
/// Позволяет быстро подключать LayoutManager, обновлять UI и связывать содержимое.
/// </summary>
public static class LayoutHostExtensions
{
// ------------------------------------------------------------------------
// 1. Подключение LayoutManager к любому ILayoutView
// ------------------------------------------------------------------------
/// <summary>
/// Подписывает визуальный хост на события изменения раскладки.
/// При каждом изменении модели вызывается <see cref="ILayoutView.Refresh"/>.
/// </summary>
/// <param name="view">Визуальный хост раскладки.</param>
/// <param name="manager">Менеджер раскладки.</param>
public static void BindToManager(this ILayoutView view, ILayoutManager manager)
{
manager.LayoutChanged += (_, _) => view.Refresh();
}
// ------------------------------------------------------------------------
// 2. Полная инициализация раскладки (Root + Manager)
// ------------------------------------------------------------------------
/// <summary>
/// Устанавливает корневой элемент раскладки, подключает менеджер и выполняет начальную отрисовку.
/// </summary>
/// <param name="view">Визуальный хост раскладки.</param>
/// <param name="root">Корневой элемент раскладки.</param>
/// <param name="manager">Менеджер раскладки.</param>
public static void UseLayout(this ILayoutView view, ILayoutRoot root, ILayoutManager manager)
{
view.Root = root;
view.BindToManager(manager);
view.Refresh();
}
// ------------------------------------------------------------------------
// 3. Удобный fluent-API
// ------------------------------------------------------------------------
/// <summary>
/// Устанавливает корневой элемент раскладки и возвращает хост для fluent-цепочек.
/// </summary>
public static T WithRoot<T>(this T view, ILayoutRoot root)
where T : ILayoutView
{
view.Root = root;
return view;
}
/// <summary>
/// Подключает менеджер раскладки и возвращает хост для fluent-цепочек.
/// </summary>
public static T WithManager<T>(this T view, ILayoutManager manager)
where T : ILayoutView
{
view.BindToManager(manager);
return view;
}
/// <summary>
/// Выполняет начальную отрисовку и возвращает хост для fluent-цепочек.
/// </summary>
public static T Initialize<T>(this T view)
where T : ILayoutView
{
view.Refresh();
return view;
}
// ------------------------------------------------------------------------
// 4. Поддержка WinUILayoutHost: резолвер контента
// ------------------------------------------------------------------------
/// <summary>
/// Устанавливает функцию, которая по ContentId возвращает реальный UI-элемент.
/// Используется для отображения содержимого вкладок.
/// </summary>
/// <param name="host">WinUI-хост раскладки.</param>
/// <param name="resolver">Функция, возвращающая UIElement по ContentId.</param>
public static void UseContentResolver(this WinUILayoutHost host, Func<string, UIElement> resolver)
{
host.ContentResolver = resolver;
}
// ------------------------------------------------------------------------
// 5. Полная WinUI-инициализация (Root + Manager + ContentResolver)
// ------------------------------------------------------------------------
/// <summary>
/// Полностью инициализирует WinUI-хост раскладки:
/// устанавливает корень, подключает менеджер, задаёт резолвер содержимого и выполняет отрисовку.
/// </summary>
/// <param name="host">WinUI-хост раскладки.</param>
/// <param name="root">Корневой элемент раскладки.</param>
/// <param name="manager">Менеджер раскладки.</param>
/// <param name="resolver">Функция получения UI-содержимого по ContentId.</param>
public static void UseLayout(
this WinUILayoutHost host,
ILayoutRoot root,
ILayoutManager manager,
Func<string, UIElement> resolver)
{
host.Root = root;
host.BindToManager(manager);
host.ContentResolver = resolver;
host.Refresh();
}
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0-windows10.0.19041.0;net9.0-windows10.0.19041.0;net10.0-windows10.0.19041.0</TargetFrameworks>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>Lattice.Layout.UI.WinUI</RootNamespace>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<WinUISDKReferences>false</WinUISDKReferences>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Lattice.Layout.UI\Lattice.Layout.UI.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,30 @@
using Lattice.Layout.Abstractions;
using Lattice.Layout.UI.WinUI.Visuals;
using System.Collections.Generic;
namespace Lattice.Layout.UI.WinUI.Rendering;
/// <summary>
/// Фабрика визуальных элементов для WinUI.
/// Создаёт визуальные представления сплитов, групп и элементов.
/// </summary>
public sealed class WinUIVisualFactory : ILayoutVisualFactory
{
/// <inheritdoc />
public ILayoutVisual CreateSplit(ILayoutSplit split, IReadOnlyList<ILayoutVisual> children)
{
return new WinUISplitVisual(split, children);
}
/// <inheritdoc />
public ILayoutVisual CreateGroup(ILayoutGroup group, IReadOnlyList<ILayoutVisual> items)
{
return new WinUIGroupVisual(group, items);
}
/// <inheritdoc />
public ILayoutVisual CreateItem(ILayoutItem item)
{
return new WinUIItemVisual(item);
}
}

View File

@@ -0,0 +1,81 @@
using Lattice.Layout.Abstractions;
using Lattice.Layout.UI.WinUI.Controls;
using Lattice.Layout.UI.WinUI.Docking;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Collections.Generic;
using System.Linq;
namespace Lattice.Layout.UI.WinUI.Visuals;
/// <summary>
/// Визуальное представление группы вкладок для WinUI.
/// Управляет <see cref="WinUIGroupControl"/> и вкладками <see cref="TabViewItem"/>.
/// </summary>
public sealed class WinUIGroupVisual : LayoutVisual, IWinUIVisual
{
/// <summary>
/// Реальный WinUI-контрол группы вкладок.
/// </summary>
public WinUIGroupControl GroupControl { get; }
/// <summary>
/// Контрол, используемый докингом и рендерером.
/// </summary>
public FrameworkElement Control => GroupControl;
/// <summary>
/// Визуальные элементы вкладок.
/// </summary>
public IReadOnlyList<ILayoutVisual> Items { get; }
public WinUIGroupVisual(ILayoutGroup model, IReadOnlyList<ILayoutVisual> items)
: base(model)
{
Items = items;
GroupControl = new WinUIGroupControl();
}
/// <inheritdoc />
public override void Attach()
{
// Полная очистка
GroupControl.TabView.TabItems.Clear();
foreach (var visual in Items)
{
visual.Attach();
if (visual is WinUIItemVisual itemVisual)
{
var tab = new TabViewItem
{
Header = itemVisual.Header,
Content = itemVisual.Control // безопасно, т.к. Control — UserControl
};
GroupControl.TabView.TabItems.Add(tab);
}
}
// Активная вкладка по модели
if (Model is ILayoutGroup group && group.ActiveItem is not null)
{
var index = group.Items is IList<ILayoutItem> list
? list.IndexOf(group.ActiveItem)
: group.Items.ToList().IndexOf(group.ActiveItem);
if (index >= 0 && index < GroupControl.TabView.TabItems.Count)
GroupControl.TabView.SelectedIndex = index;
}
}
/// <inheritdoc />
public override void Detach()
{
foreach (var visual in Items)
visual.Detach();
GroupControl.TabView.TabItems.Clear();
}
}

View File

@@ -0,0 +1,51 @@
using Lattice.Layout.Abstractions;
using Lattice.Layout.UI.WinUI.Controls;
using Lattice.Layout.UI.WinUI.Docking;
using Microsoft.UI.Xaml;
namespace Lattice.Layout.UI.WinUI.Visuals;
/// <summary>
/// Визуальное представление конечного элемента раскладки для WinUI.
/// Оборачивает содержимое в <see cref="WinUIItemControl"/>.
/// </summary>
public sealed class WinUIItemVisual : LayoutVisual, IWinUIVisual
{
/// <summary>
/// Реальный WinUI-контрол, отображающий элемент.
/// </summary>
public WinUIItemControl ItemControl { get; }
/// <summary>
/// Контрол, используемый докингом и рендерером.
/// </summary>
public FrameworkElement Control => ItemControl;
/// <summary>
/// Заголовок вкладки или элемента.
/// </summary>
public string Header => ((ILayoutItem)Model).Title;
public WinUIItemVisual(ILayoutItem model)
: base(model)
{
ItemControl = new WinUIItemControl
{
ContentId = model.ContentId
};
}
/// <inheritdoc />
public override void Attach()
{
// Здесь можно привязать реальное содержимое по ContentId через Shell/Service.
// Например:
// ItemControl.Content = resolver(((ILayoutItem)Model).ContentId);
}
/// <inheritdoc />
public override void Detach()
{
ItemControl.Content = null;
}
}

View File

@@ -0,0 +1,82 @@
using Lattice.Layout.Abstractions;
using Lattice.Layout.UI.WinUI.Controls;
using Lattice.Layout.UI.WinUI.Docking;
using Microsoft.UI.Xaml;
using System.Collections.Generic;
namespace Lattice.Layout.UI.WinUI.Visuals;
/// <summary>
/// Визуальное представление сплит-элемента для WinUI.
/// Управляет жизненным циклом соответствующего <see cref="WinUISplitControl"/>.
/// </summary>
public sealed class WinUISplitVisual : LayoutVisual, IWinUIVisual
{
/// <summary>
/// Реальный WinUI-контрол, отображающий сплит.
/// </summary>
public WinUISplitControl SplitControl { get; }
/// <summary>
/// Контрол, используемый рендерером и докингом.
/// </summary>
public FrameworkElement Control => SplitControl;
/// <summary>
/// Дочерние визуальные элементы.
/// </summary>
public IReadOnlyList<ILayoutVisual> Children { get; }
public WinUISplitVisual(ILayoutSplit model, IReadOnlyList<ILayoutVisual> children)
: base(model)
{
Children = children;
SplitControl = new WinUISplitControl
{
LayoutOrientation = model.Orientation switch
{
Lattice.Layout.Abstractions.Orientation.Horizontal => Microsoft.UI.Xaml.Controls.Orientation.Horizontal,
Lattice.Layout.Abstractions.Orientation.Vertical => Microsoft.UI.Xaml.Controls.Orientation.Vertical,
_ => Microsoft.UI.Xaml.Controls.Orientation.Horizontal
}
};
}
/// <inheritdoc />
public override void Attach()
{
SplitControl.Children.Clear();
foreach (var child in Children)
{
child.Attach();
switch (child)
{
case WinUISplitVisual splitVisual:
SplitControl.Children.Add(splitVisual.Control);
break;
case WinUIGroupVisual groupVisual:
SplitControl.Children.Add(groupVisual.Control);
break;
case WinUIItemVisual itemVisual:
SplitControl.Children.Add(itemVisual.Control);
break;
}
}
SplitControl.RebuildGrid();
}
/// <inheritdoc />
public override void Detach()
{
foreach (var child in Children)
child.Detach();
SplitControl.Children.Clear();
}
}

View File

@@ -0,0 +1,147 @@
using Lattice.Core.Docking.Abstractions;
using Lattice.Core.Docking.Engine;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Lattice.Serialization.Docking;
/// <summary>
/// Реализация сериализатора макета док-системы в формате JSON с использованием System.Text.Json.
/// Предоставляет высокопроизводительную сериализацию и десериализацию с поддержкой
/// опций кастомизации через <see cref="JsonSerializerOptions"/>.
/// </summary>
/// <remarks>
/// Этот сериализатор использует преобразователь <see cref="LayoutConverter"/> для
/// трансформации между объектной моделью и DTO, что обеспечивает независимость
/// формата JSON от внутренней структуры данных.
/// </remarks>
public class JsonLayoutSerializer : ILayoutSerializer
{
private readonly JsonSerializerOptions _options;
/// <summary>
/// Инициализирует новый экземпляр JSON-сериализатора с настройками по умолчанию.
/// </summary>
public JsonLayoutSerializer() : this(null)
{
}
/// <summary>
/// Инициализирует новый экземпляр JSON-сериализатора с указанными опциями.
/// </summary>
/// <param name="options">
/// Опции сериализации JSON. Если не указаны, используются стандартные опции.
/// </param>
public JsonLayoutSerializer(JsonSerializerOptions? options)
{
_options = options ?? CreateDefaultOptions();
}
/// <inheritdoc/>
public string FormatId => "json";
/// <inheritdoc/>
public string MimeType => "application/json";
/// <inheritdoc/>
public string DefaultFileExtension => ".json";
/// <inheritdoc/>
public byte[] Serialize(LayoutManager manager)
{
if (manager == null)
throw new ArgumentNullException(nameof(manager));
var dto = LayoutConverter.ConvertToDto(manager);
return JsonSerializer.SerializeToUtf8Bytes(dto, _options);
}
/// <inheritdoc/>
public string SerializeToString(LayoutManager manager)
{
if (manager == null)
throw new ArgumentNullException(nameof(manager));
var dto = LayoutConverter.ConvertToDto(manager);
return JsonSerializer.Serialize(dto, _options);
}
/// <inheritdoc/>
public void SerializeToStream(LayoutManager manager, Stream stream)
{
if (manager == null)
throw new ArgumentNullException(nameof(manager));
if (stream == null)
throw new ArgumentNullException(nameof(stream));
var dto = LayoutConverter.ConvertToDto(manager);
JsonSerializer.Serialize(stream, dto, _options);
}
/// <inheritdoc/>
public void Deserialize(LayoutManager manager, byte[] data, Func<string, IDockContent?> contentResolver)
{
if (manager == null)
throw new ArgumentNullException(nameof(manager));
if (data == null)
throw new ArgumentNullException(nameof(data));
var dto = JsonSerializer.Deserialize<LayoutDto>(data, _options);
if (dto == null)
throw new InvalidOperationException("Failed to deserialize layout data");
LayoutConverter.RestoreFromDto(manager, dto, contentResolver);
}
/// <inheritdoc/>
public void DeserializeFromString(LayoutManager manager, string serializedData,
Func<string, IDockContent?> contentResolver)
{
if (manager == null)
throw new ArgumentNullException(nameof(manager));
if (serializedData == null)
throw new ArgumentNullException(nameof(serializedData));
var dto = JsonSerializer.Deserialize<LayoutDto>(serializedData, _options);
if (dto == null)
throw new InvalidOperationException("Failed to deserialize layout string");
LayoutConverter.RestoreFromDto(manager, dto, contentResolver);
}
/// <inheritdoc/>
public void DeserializeFromStream(LayoutManager manager, Stream stream,
Func<string, IDockContent?> contentResolver)
{
if (manager == null)
throw new ArgumentNullException(nameof(manager));
if (stream == null)
throw new ArgumentNullException(nameof(stream));
var dto = JsonSerializer.Deserialize<LayoutDto>(stream, _options);
if (dto == null)
throw new InvalidOperationException("Failed to deserialize layout stream");
LayoutConverter.RestoreFromDto(manager, dto, contentResolver);
}
/// <summary>
/// Создает стандартные опции сериализации JSON.
/// </summary>
/// <returns>Экземпляр <see cref="JsonSerializerOptions"/> с настройками по умолчанию.</returns>
private static JsonSerializerOptions CreateDefaultOptions()
{
return new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters = { new JsonStringEnumConverter() },
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
ReferenceHandler = ReferenceHandler.IgnoreCycles,
PropertyNameCaseInsensitive = true,
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
}
}

View File

@@ -0,0 +1,48 @@
using System.Text.Json;
namespace Lattice.Serialization.Docking;
/// <summary>
/// Предоставляет предварительно настроенные опции сериализации JSON для различных сценариев.
/// </summary>
public static class JsonSerializerOptionsPresets
{
/// <summary>
/// Получает опции для красивого форматирования с отступами (для конфигурационных файлов).
/// </summary>
public static JsonSerializerOptions PrettyPrint => new()
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() },
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
/// <summary>
/// Получает опции для компактного форматирования (для сетевой передачи).
/// </summary>
public static JsonSerializerOptions Compact => new()
{
WriteIndented = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() },
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
/// <summary>
/// Получает опции для строгого форматирования (для валидации схемы).
/// </summary>
public static JsonSerializerOptions Strict => new()
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() },
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.Never,
IgnoreReadOnlyProperties = false,
IgnoreReadOnlyFields = true,
IncludeFields = false,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.Default
};
}

View File

@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Lattice.Serialization.Docking\Lattice.Serialization.Docking.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,37 @@
namespace Lattice.Serialization.Docking;
/// <summary>
/// DTO для автоскрываемой панели (AutoHidePanel).
/// </summary>
public class AutoHidePanelDto
{
/// <summary>
/// Уникальный идентификатор панели.
/// </summary>
public string Id { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// Ссылка на контент панели.
/// </summary>
public ContentReferenceDto Content { get; set; } = null!;
/// <summary>
/// Сторона прикрепления в виде строки.
/// </summary>
public string Side { get; set; } = string.Empty;
/// <summary>
/// Размер панели в пикселях.
/// </summary>
public double Size { get; set; } = 300;
/// <summary>
/// Показывает, видима ли панель.
/// </summary>
public bool IsVisible { get; set; } = false;
/// <summary>
/// Смещение для анимации (0.0 - 1.0).
/// </summary>
public double SlideOffset { get; set; } = 0.0;
}

View File

@@ -0,0 +1,32 @@
namespace Lattice.Serialization.Docking;
/// <summary>
/// DTO для ссылки на контент без сериализации самого контента.
/// </summary>
public class ContentReferenceDto
{
/// <summary>
/// Уникальный идентификатор контента.
/// </summary>
public string Id { get; set; } = string.Empty;
/// <summary>
/// Тип контента (для восстановления через ContentRegistry).
/// </summary>
public string TypeId { get; set; } = string.Empty;
/// <summary>
/// Отображаемое название контента.
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// Показывает, можно ли закрыть контент.
/// </summary>
public bool CanClose { get; set; } = true;
/// <summary>
/// Дополнительные свойства контента для восстановления состояния.
/// </summary>
public Dictionary<string, object?> Properties { get; set; } = new();
}

View File

@@ -0,0 +1,37 @@
namespace Lattice.Serialization.Docking;
/// <summary>
/// Базовый DTO для элементов дерева компоновки.
/// </summary>
public abstract class ElementDto
{
/// <summary>
/// Уникальный идентификатор элемента.
/// </summary>
public string Id { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// Тип элемента (для десериализации).
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// Ширина элемента.
/// </summary>
public double Width { get; set; }
/// <summary>
/// Высота элемента.
/// </summary>
public double Height { get; set; }
/// <summary>
/// Минимальная ширина элемента.
/// </summary>
public double MinWidth { get; set; }
/// <summary>
/// Минимальная высота элемента.
/// </summary>
public double MinHeight { get; set; }
}

View File

@@ -0,0 +1,27 @@
namespace Lattice.Serialization.Docking;
/// <summary>
/// DTO для группы разделения (DockGroup).
/// </summary>
public class GroupDto : ElementDto
{
/// <summary>
/// Первый дочерний элемент (левая или верхняя область).
/// </summary>
public ElementDto First { get; set; } = null!;
/// <summary>
/// Второй дочерний элемент (правая или нижняя область).
/// </summary>
public ElementDto Second { get; set; } = null!;
/// <summary>
/// Направление разделения в виде строки.
/// </summary>
public string Orientation { get; set; } = string.Empty;
/// <summary>
/// Соотношение разделения между первым и вторым элементами (0.0 - 1.0).
/// </summary>
public double SplitRatio { get; set; } = 0.5;
}

View File

@@ -0,0 +1,47 @@
namespace Lattice.Serialization.Docking;
/// <summary>
/// Data Transfer Object (DTO) для сериализации состояния макета док-системы.
/// Содержит все необходимые данные для сохранения и восстановления состояния макета.
/// </summary>
/// <remarks>
/// Этот DTO является независимым от формата сериализации (JSON, XML, Binary) и используется
/// как промежуточное представление между объектной моделью и сериализованными данными.
/// </remarks>
public class LayoutDto
{
/// <summary>
/// Версия формата DTO для контроля совместимости.
/// </summary>
public string Version { get; set; } = "1.0";
/// <summary>
/// Дата и время создания DTO в UTC.
/// </summary>
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
/// <summary>
/// Идентификатор приложения, создавшего DTO.
/// </summary>
public string? ApplicationId { get; set; }
/// <summary>
/// Корневой элемент дерева компоновки.
/// </summary>
public ElementDto? Root { get; set; }
/// <summary>
/// Список плавающих окон.
/// </summary>
public List<WindowDto> FloatingWindows { get; set; } = new();
/// <summary>
/// Список автоскрываемых панелей.
/// </summary>
public List<AutoHidePanelDto> AutoHidePanels { get; set; } = new();
/// <summary>
/// Дополнительные метаданные, специфичные для приложения.
/// </summary>
public Dictionary<string, string> Metadata { get; set; } = new();
}

View File

@@ -0,0 +1,22 @@
namespace Lattice.Serialization.Docking;
/// <summary>
/// DTO для контейнера вкладок (DockLeaf).
/// </summary>
public class LeafDto : ElementDto
{
/// <summary>
/// Список ссылок на контент, содержащийся в листе.
/// </summary>
public List<ContentReferenceDto> Contents { get; set; } = new();
/// <summary>
/// Идентификатор активного контента (если есть).
/// </summary>
public string? ActiveContentId { get; set; }
/// <summary>
/// Расположение вкладок в виде строки.
/// </summary>
public string TabPlacement { get; set; } = "Bottom";
}

View File

@@ -0,0 +1,52 @@
namespace Lattice.Serialization.Docking;
/// <summary>
/// DTO для плавающего окна (DockWindow).
/// </summary>
public class WindowDto
{
/// <summary>
/// Уникальный идентификатор окна.
/// </summary>
public string Id { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// Позиция X окна на экране.
/// </summary>
public double X { get; set; }
/// <summary>
/// Позиция Y окна на экране.
/// </summary>
public double Y { get; set; }
/// <summary>
/// Ширина окна.
/// </summary>
public double Width { get; set; } = 800;
/// <summary>
/// Высота окна.
/// </summary>
public double Height { get; set; } = 600;
/// <summary>
/// Заголовок окна.
/// </summary>
public string Title { get; set; } = "Lattice Tool Window";
/// <summary>
/// Корневой элемент макета внутри окна.
/// </summary>
public ElementDto? Root { get; set; }
/// <summary>
/// Показывает, видимо ли окно.
/// </summary>
public bool IsVisible { get; set; } = true;
/// <summary>
/// Показывает, сфокусировано ли окно.
/// </summary>
public bool IsFocused { get; set; } = false;
}

View File

@@ -0,0 +1,103 @@
using Lattice.Core.Docking.Abstractions;
using Lattice.Core.Docking.Engine;
namespace Lattice.Serialization.Docking;
/// <summary>
/// Определяет контракт для сериализации и десериализации состояния макета док-системы.
/// Этот интерфейс позволяет реализовать различные форматы сериализации (JSON, XML, Binary)
/// и различные хранилища (файлы, базы данных, облако) без изменения основной логики.
/// </summary>
/// <remarks>
/// Реализации этого интерфейса должны преобразовывать объектную модель док-системы
/// в промежуточный DTO (<see cref="LayoutDto"/>), который затем сериализуется в целевой формат.
/// Это обеспечивает независимость формата сериализации от структуры DTO.
/// </remarks>
public interface ILayoutSerializer
{
/// <summary>
/// Получает уникальный идентификатор формата сериализации.
/// </summary>
/// <value>Строковый идентификатор формата, например "json", "xml", "binary".</value>
string FormatId { get; }
/// <summary>
/// Получает MIME-тип формата сериализации.
/// </summary>
/// <value>MIME-тип, например "application/json", "application/xml".</value>
string MimeType { get; }
/// <summary>
/// Получает расширение файла по умолчанию для данного формата.
/// </summary>
/// <value>Расширение файла, например ".json", ".xml".</value>
string DefaultFileExtension { get; }
/// <summary>
/// Сериализует состояние менеджера макета в массив байтов.
/// </summary>
/// <param name="manager">Менеджер макета для сериализации.</param>
/// <returns>Массив байтов, содержащий сериализованное состояние макета.</returns>
/// <exception cref="ArgumentNullException">Выбрасывается, если <paramref name="manager"/> равен null.</exception>
byte[] Serialize(LayoutManager manager);
/// <summary>
/// Сериализует состояние менеджера макета в строку.
/// </summary>
/// <param name="manager">Менеджер макета для сериализации.</param>
/// <returns>Строковое представление состояния макета.</returns>
/// <exception cref="ArgumentNullException">Выбрасывается, если <paramref name="manager"/> равен null.</exception>
string SerializeToString(LayoutManager manager);
/// <summary>
/// Сериализует состояние менеджера макета в поток.
/// </summary>
/// <param name="manager">Менеджер макета для сериализации.</param>
/// <param name="stream">Поток для записи сериализованных данных.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="manager"/> или <paramref name="stream"/> равен null.
/// </exception>
void SerializeToStream(LayoutManager manager, Stream stream);
/// <summary>
/// Десериализует состояние макета из массива байтов и восстанавливает его в менеджере.
/// </summary>
/// <param name="manager">Менеджер макета для восстановления состояния.</param>
/// <param name="data">Массив байтов с сериализованным состоянием макета.</param>
/// <param name="contentResolver">
/// Функция разрешения контента по идентификатору, используемая для восстановления
/// ссылок на контент в десериализованном состоянии.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="manager"/> или <paramref name="data"/> равен null.
/// </exception>
void Deserialize(LayoutManager manager, byte[] data, Func<string, IDockContent?> contentResolver);
/// <summary>
/// Десериализует состояние макета из строки и восстанавливает его в менеджере.
/// </summary>
/// <param name="manager">Менеджер макета для восстановления состояния.</param>
/// <param name="serializedData">Строка с сериализованным состоянием макета.</param>
/// <param name="contentResolver">
/// Функция разрешения контента по идентификатору, используемая для восстановления
/// ссылок на контент в десериализованном состоянии.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="manager"/> или <paramref name="serializedData"/> равен null.
/// </exception>
void DeserializeFromString(LayoutManager manager, string serializedData, Func<string, IDockContent?> contentResolver);
/// <summary>
/// Десериализует состояние макета из потока и восстанавливает его в менеджере.
/// </summary>
/// <param name="manager">Менеджер макета для восстановления состояния.</param>
/// <param name="stream">Поток с сериализованными данными.</param>
/// <param name="contentResolver">
/// Функция разрешения контента по идентификатору, используемая для восстановления
/// ссылок на контент в десериализованном состоянии.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="manager"/> или <paramref name="stream"/> равен null.
/// </exception>
void DeserializeFromStream(LayoutManager manager, Stream stream, Func<string, IDockContent?> contentResolver);
}

View File

@@ -0,0 +1,19 @@
namespace Lattice.Serialization.Docking;
/// <summary>
/// Интерфейс для контента, поддерживающего сериализацию дополнительного состояния.
/// </summary>
public interface ISerializableContent
{
/// <summary>
/// Получает состояние для сериализации.
/// </summary>
/// <returns>Словарь свойств и их значений.</returns>
Dictionary<string, object?> GetSerializableState();
/// <summary>
/// Восстанавливает состояние из десериализованных данных.
/// </summary>
/// <param name="state">Словарь свойств и их значений.</param>
void RestoreFromState(Dictionary<string, object?> state);
}

View File

@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Lattice.Core.Docking\Lattice.Core.Docking.csproj" />
</ItemGroup>
</Project>

Some files were not shown because too many files have changed in this diff Show More