From fc994edf71fb4e3080f6c854ab959479f6eab9d5 Mon Sep 17 00:00:00 2001 From: FrigaT Date: Wed, 7 Jan 2026 21:28:32 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D1=8C=D1=82?= =?UTF-8?q?=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Lattice.Core/Abstractions/IContextService.cs | 27 +++ .../Abstractions/IDockableComponent.cs | 33 ++++ Lattice.Core/Abstractions/ILayoutElement.cs | 42 +++++ Lattice.Core/Abstractions/ILayoutService.cs | 40 +++++ Lattice.Core/Context/ContextManager.cs | 39 +++++ Lattice.Core/Engine/LayoutManager.cs | 165 ++++++++++++++++++ Lattice.Core/Lattice.Core.csproj | 28 +++ Lattice.Core/Models/ActionDefinition.cs | 16 ++ Lattice.Core/Models/ContentNode.cs | 29 +++ Lattice.Core/Models/Enums/DockDirection.cs | 11 ++ Lattice.Core/Models/Enums/SplitOrientation.cs | 13 ++ Lattice.Core/Models/LayoutNode.cs | 35 ++++ Lattice.Core/Models/SplitContainerNode.cs | 38 ++++ Lattice.Core/Models/WorkspaceSnapshot.cs | 12 ++ Lattice.slnx | 3 + 15 files changed, 531 insertions(+) create mode 100644 Lattice.Core/Abstractions/IContextService.cs create mode 100644 Lattice.Core/Abstractions/IDockableComponent.cs create mode 100644 Lattice.Core/Abstractions/ILayoutElement.cs create mode 100644 Lattice.Core/Abstractions/ILayoutService.cs create mode 100644 Lattice.Core/Context/ContextManager.cs create mode 100644 Lattice.Core/Engine/LayoutManager.cs create mode 100644 Lattice.Core/Lattice.Core.csproj create mode 100644 Lattice.Core/Models/ActionDefinition.cs create mode 100644 Lattice.Core/Models/ContentNode.cs create mode 100644 Lattice.Core/Models/Enums/DockDirection.cs create mode 100644 Lattice.Core/Models/Enums/SplitOrientation.cs create mode 100644 Lattice.Core/Models/LayoutNode.cs create mode 100644 Lattice.Core/Models/SplitContainerNode.cs create mode 100644 Lattice.Core/Models/WorkspaceSnapshot.cs create mode 100644 Lattice.slnx diff --git a/Lattice.Core/Abstractions/IContextService.cs b/Lattice.Core/Abstractions/IContextService.cs new file mode 100644 index 0000000..67537ab --- /dev/null +++ b/Lattice.Core/Abstractions/IContextService.cs @@ -0,0 +1,27 @@ +namespace Lattice.Core.Abstractions; + +/// +/// Сервис управления контекстом приложения и связанными командами. +/// +public interface IContextService +{ + /// + /// Имя текущего активного контекста. + /// + string CurrentContext { get; } + + /// + /// Возникает при смене фокуса между вкладками с разными ContextGroup. + /// + event EventHandler? ContextChanged; + + /// + /// Устанавливает активный контекст. Вызывается UI-слоем при активации вкладки. + /// + void SetContext(string contextGroup); + + /// + /// Проверяет, должна ли команда быть видимой в текущем контексте. + /// + bool IsCommandVisible(string commandId, string commandContext); +} diff --git a/Lattice.Core/Abstractions/IDockableComponent.cs b/Lattice.Core/Abstractions/IDockableComponent.cs new file mode 100644 index 0000000..3aac000 --- /dev/null +++ b/Lattice.Core/Abstractions/IDockableComponent.cs @@ -0,0 +1,33 @@ +namespace Lattice.Core.Abstractions; + +/// +/// Описывает компонент, который может быть размещен внутри узла компоновки Lattice. +/// +public interface IDockableComponent +{ + /// + /// Уникальный строковый идентификатор компонента (например, "SolutionExplorer"). + /// + string UniqueId { get; } + + /// + /// Заголовок, отображаемый на вкладке или в заголовке панели. + /// + string DisplayName { get; } + + /// + /// Ключ иконки (для Segoe Fluent Icons или путей к ресурсам). + /// + string? IconKey { get; } + + /// + /// Группа контекста (например, "CodeEditor", "Debugger"). + /// Определяет, какие панели инструментов будут активны. + /// + string ContextGroup { get; } + + /// + /// Указывает, разрешено ли закрывать данный компонент пользователем. + /// + bool CanClose { get; } +} diff --git a/Lattice.Core/Abstractions/ILayoutElement.cs b/Lattice.Core/Abstractions/ILayoutElement.cs new file mode 100644 index 0000000..bf70297 --- /dev/null +++ b/Lattice.Core/Abstractions/ILayoutElement.cs @@ -0,0 +1,42 @@ +namespace Lattice.Core.Abstractions; + +/// +/// Представляет базовый элемент иерархии компоновки Lattice. +/// +public interface ILayoutElement +{ + /// + /// Уникальный идентификатор элемента. + /// + Guid Id { get; } + + /// + /// Имя элемента для отображения или идентификации в логах. + /// + string Name { get; set; } + + /// + /// Значение ширины (в пикселях или долях "star"). + /// + double WidthValue { get; set; } + + /// + /// Указывает, является ли ширина пропорциональной (star). + /// + bool IsWidthStar { get; set; } + + /// + /// Значение высоты (в пикселях или долях "star"). + /// + double HeightValue { get; set; } + + /// + /// Указывает, является ли высота пропорциональной (star). + /// + bool IsHeightStar { get; set; } + + /// + /// Родительский элемент в дереве компоновки. + /// + ILayoutElement? Parent { get; set; } +} diff --git a/Lattice.Core/Abstractions/ILayoutService.cs b/Lattice.Core/Abstractions/ILayoutService.cs new file mode 100644 index 0000000..1653dee --- /dev/null +++ b/Lattice.Core/Abstractions/ILayoutService.cs @@ -0,0 +1,40 @@ +using Lattice.Core.Models; +using Lattice.Core.Models.Enums; + +namespace Lattice.Core.Abstractions; + +/// +/// Сервис управления жизненным циклом макета приложения. +/// +public interface ILayoutService +{ + /// + /// Текущий корневой узел всей структуры окон. + /// + LayoutNode? Root { get; } + + /// + /// Событие, возникающее при любом изменении структуры (докинг, закрытие, изменение размеров). + /// + event EventHandler? LayoutUpdated; + + /// + /// Перемещает узел в указанную позицию относительно целевого узла. + /// + void Dock(LayoutNode source, LayoutNode target, DockDirection direction); + + /// + /// Удаляет узел из макета (например, при закрытии вкладки). + /// + void Remove(LayoutNode node); + + /// + /// Импортирует структуру макета из снапшота. + /// + void LoadLayout(string jsonData); + + /// + /// Экспортирует текущую структуру в строку для сохранения. + /// + string SaveLayout(); +} diff --git a/Lattice.Core/Context/ContextManager.cs b/Lattice.Core/Context/ContextManager.cs new file mode 100644 index 0000000..84bc996 --- /dev/null +++ b/Lattice.Core/Context/ContextManager.cs @@ -0,0 +1,39 @@ +using Lattice.Core.Abstractions; + +namespace Lattice.Core.Context; + +/// +/// Реализация сервиса управления контекстом приложения. +/// +public class ContextManager : IContextService +{ + private string _currentContext = "Common"; + + /// + public string CurrentContext => _currentContext; + + /// + public event EventHandler? ContextChanged; + + /// + public void SetContext(string contextGroup) + { + if (string.IsNullOrWhiteSpace(contextGroup)) contextGroup = "Common"; + + if (_currentContext != contextGroup) + { + _currentContext = contextGroup; + ContextChanged?.Invoke(this, contextGroup); + } + } + + /// + public bool IsCommandVisible(string commandId, string commandContext) + { + // Базовая логика: команда видима, если её контекст совпадает с текущим + // или если команда помечена как общая ("Common" или "Global"). + return commandContext == "Common" || + commandContext == "Global" || + commandContext == _currentContext; + } +} diff --git a/Lattice.Core/Engine/LayoutManager.cs b/Lattice.Core/Engine/LayoutManager.cs new file mode 100644 index 0000000..7637d4c --- /dev/null +++ b/Lattice.Core/Engine/LayoutManager.cs @@ -0,0 +1,165 @@ +using Lattice.Core.Abstractions; +using Lattice.Core.Models; +using Lattice.Core.Models.Enums; +using Microsoft.Extensions.Logging; + +namespace Lattice.Core.Engine; + +/// +/// Реализация сервиса управления макетом. +/// +public class LayoutManager : ILayoutService +{ + private readonly ILogger? _logger; + private LayoutNode? _root; + + /// + public LayoutNode? Root => _root; + + /// + public event EventHandler? LayoutUpdated; + + public LayoutManager(ILogger? logger = null) + { + _logger = logger; + } + + /// + 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); + } + + /// + /// Логика добавления элемента в центральную часть (вкладки). + /// + 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); + } + } + + /// + /// Логика разделения существующей области на две (Side Dock). + /// + 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; + } + + /// + 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); + } + + /// + /// Убирает ненужные контейнеры, если в них остался только один элемент. + /// + 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; + } + } + + /// + public string SaveLayout() { /* Реализация через JsonConverter */ return string.Empty; } + + /// + public void LoadLayout(string jsonData) { /* Реализация через JsonConverter */ } +} diff --git a/Lattice.Core/Lattice.Core.csproj b/Lattice.Core/Lattice.Core.csproj new file mode 100644 index 0000000..e9f25d4 --- /dev/null +++ b/Lattice.Core/Lattice.Core.csproj @@ -0,0 +1,28 @@ + + + + + net8.0;net9.0;net10.0 + enable + enable + Lattice.Core + Lattice.Core + + + FrigaT + FrigaT + https://git.frigat.duckdns.org/FrigaT/Lattice + https://git.frigat.duckdns.org/FrigaT/Lattice + Core docking and layout engine for Lattice UI (WinUI 3 / Uno Platform). + + + true + latest + + + + + + + + diff --git a/Lattice.Core/Models/ActionDefinition.cs b/Lattice.Core/Models/ActionDefinition.cs new file mode 100644 index 0000000..7e11bab --- /dev/null +++ b/Lattice.Core/Models/ActionDefinition.cs @@ -0,0 +1,16 @@ +namespace Lattice.Core.Models; + +/// +/// Определение команды для панели инструментов или меню. +/// +public class ActionDefinition +{ + public string Id { get; init; } = string.Empty; + public string Label { get; init; } = string.Empty; + public string IconKey { get; init; } = string.Empty; + + /// + /// Контекст, в котором эта кнопка должна быть доступна (например, "C#", "XAML", "Common"). + /// + public string TargetContext { get; init; } = "Common"; +} diff --git a/Lattice.Core/Models/ContentNode.cs b/Lattice.Core/Models/ContentNode.cs new file mode 100644 index 0000000..441edba --- /dev/null +++ b/Lattice.Core/Models/ContentNode.cs @@ -0,0 +1,29 @@ +using Lattice.Core.Abstractions; + +namespace Lattice.Core.Models; + +/// +/// Узел, представляющий конечный контент (вкладку, панель инструментов или документ). +/// +public class ContentNode : LayoutNode +{ + /// + /// Ссылка на визуальный или логический компонент, закрепленный в этом узле. + /// + public IDockableComponent? Component { get; set; } + + /// + /// Указывает, является ли данный узел частью основной рабочей области документов. + /// + public bool IsDocumentArea { get; set; } + + /// + /// Инициализирует новый экземпляр на основе компонента. + /// + /// Компонент содержимого. + public ContentNode(IDockableComponent component) + { + Component = component; + Name = component.DisplayName; + } +} diff --git a/Lattice.Core/Models/Enums/DockDirection.cs b/Lattice.Core/Models/Enums/DockDirection.cs new file mode 100644 index 0000000..12d797a --- /dev/null +++ b/Lattice.Core/Models/Enums/DockDirection.cs @@ -0,0 +1,11 @@ +namespace Lattice.Core.Models.Enums; + +public enum DockDirection +{ + Center, + Left, + Right, + Top, + Bottom, + Floating, +} \ No newline at end of file diff --git a/Lattice.Core/Models/Enums/SplitOrientation.cs b/Lattice.Core/Models/Enums/SplitOrientation.cs new file mode 100644 index 0000000..42d3f67 --- /dev/null +++ b/Lattice.Core/Models/Enums/SplitOrientation.cs @@ -0,0 +1,13 @@ +namespace Lattice.Core.Models.Enums; + +public enum SplitOrientation +{ + /// + /// Элементы располагаются друг за другом по горизонтали + /// + Horizontal, + /// + /// Элементы располагаются друг за другом по вертикали + /// + Vertical, +} \ No newline at end of file diff --git a/Lattice.Core/Models/LayoutNode.cs b/Lattice.Core/Models/LayoutNode.cs new file mode 100644 index 0000000..8da7743 --- /dev/null +++ b/Lattice.Core/Models/LayoutNode.cs @@ -0,0 +1,35 @@ +using Lattice.Core.Abstractions; + +namespace Lattice.Core.Models; + +/// +/// Абстрактный базовый класс для всех узлов дерева компоновки. +/// +public abstract class LayoutNode : ILayoutElement +{ + /// + public Guid Id { get; } = Guid.NewGuid(); + + /// + public string Name { get; set; } = string.Empty; + + /// + public double WidthValue { get; set; } = 1.0; + + /// + public bool IsWidthStar { get; set; } = true; + + /// + public double HeightValue { get; set; } = 1.0; + + /// + public bool IsHeightStar { get; set; } = true; + + /// + public ILayoutElement? Parent { get; set; } + + /// + /// Возвращает строковое представление узла для отладки. + /// + public override string ToString() => $"{GetType().Name} [{Name}] ({Id.ToString()[..4]})"; +} diff --git a/Lattice.Core/Models/SplitContainerNode.cs b/Lattice.Core/Models/SplitContainerNode.cs new file mode 100644 index 0000000..64e0d6c --- /dev/null +++ b/Lattice.Core/Models/SplitContainerNode.cs @@ -0,0 +1,38 @@ +using Lattice.Core.Models.Enums; + +namespace Lattice.Core.Models; + +/// +/// Узел-контейнер, разделяющий пространство между дочерними элементами в определенной ориентации. +/// +public class SplitContainerNode : LayoutNode +{ + /// + /// Ориентация разделения (горизонтальная или вертикальная). + /// + public SplitOrientation Orientation { get; set; } + + /// + /// Список дочерних узлов, находящихся внутри данного контейнера. + /// + public List Children { get; } = new(); + + /// + /// Инициализирует новый экземпляр . + /// + /// Ориентация контейнера. + public SplitContainerNode(SplitOrientation orientation) + { + Orientation = orientation; + } + + /// + /// Добавляет дочерний узел в контейнер и устанавливает связь с родителем. + /// + /// Узел для добавления. + public void AddChild(LayoutNode child) + { + child.Parent = this; + Children.Add(child); + } +} diff --git a/Lattice.Core/Models/WorkspaceSnapshot.cs b/Lattice.Core/Models/WorkspaceSnapshot.cs new file mode 100644 index 0000000..ac7bef4 --- /dev/null +++ b/Lattice.Core/Models/WorkspaceSnapshot.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Lattice.Core.Models +{ + internal class WorkspaceSnapshot + { + } +} diff --git a/Lattice.slnx b/Lattice.slnx new file mode 100644 index 0000000..02e1f68 --- /dev/null +++ b/Lattice.slnx @@ -0,0 +1,3 @@ + + +