Добавьте файлы проекта.

This commit is contained in:
2026-01-07 21:28:32 +03:00
parent 02603e60ad
commit fc994edf71
15 changed files with 531 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
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

@@ -0,0 +1,33 @@
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

@@ -0,0 +1,42 @@
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

@@ -0,0 +1,40 @@
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

@@ -0,0 +1,39 @@
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

@@ -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;
/// <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() { /* Реализация через JsonConverter */ return string.Empty; }
/// <inheritdoc/>
public void LoadLayout(string jsonData) { /* Реализация через JsonConverter */ }
}

View File

@@ -0,0 +1,28 @@
<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

@@ -0,0 +1,16 @@
namespace Lattice.Core.Models;
/// <summary>
/// Определение команды для панели инструментов или меню.
/// </summary>
public class ActionDefinition
{
public string Id { get; init; } = string.Empty;
public string Label { get; init; } = string.Empty;
public string IconKey { get; init; } = string.Empty;
/// <summary>
/// Контекст, в котором эта кнопка должна быть доступна (например, "C#", "XAML", "Common").
/// </summary>
public string TargetContext { get; init; } = "Common";
}

View File

@@ -0,0 +1,29 @@
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

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

View File

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

View File

@@ -0,0 +1,35 @@
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

@@ -0,0 +1,38 @@
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

@@ -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
{
}
}

3
Lattice.slnx Normal file
View File

@@ -0,0 +1,3 @@
<Solution>
<Project Path="../../../../../../../../Job/Projects/FrigaT/Lattice/Lattice.Core/Lattice.Core.csproj" />
</Solution>