Добавлен проект UI

This commit is contained in:
2026-01-07 22:33:42 +03:00
parent b6de0543b7
commit ca5d912c9c
21 changed files with 1188 additions and 4 deletions

View File

@@ -11,12 +11,17 @@ public record ActionDefinition
public string Id { get; init; } = Guid.NewGuid().ToString(); public string Id { get; init; } = Guid.NewGuid().ToString();
/// <summary> /// <summary>
/// Текст кнопки. /// Текст кнопки, отображаемый пользователю.
/// </summary> /// </summary>
public string Label { get; init; } = "Action"; public string Label { get; init; } = "Action";
/// <summary> /// <summary>
/// Группа контекста, к которой привязана кнопка (например, "CodeEditor"). /// Код иконки из шрифта Segoe Fluent Icons (например, "\uE102").
/// </summary>
public string IconKey { get; init; } = "\uE102";
/// <summary>
/// Группа контекста, к которой привязана кнопка (например, "CodeEditor", "Common").
/// </summary> /// </summary>
public string TargetContext { get; init; } = "Common"; public string TargetContext { get; init; } = "Common";
@@ -26,7 +31,7 @@ public record ActionDefinition
public bool IsEnabled { get; set; } = true; public bool IsEnabled { get; set; } = true;
/// <summary> /// <summary>
/// Подсказка (Tooltip). /// Подсказка, отображаемая при наведении (Tooltip).
/// </summary> /// </summary>
public string Tooltip { get; init; } = string.Empty; public string Tooltip { get; init; } = string.Empty;
} }

View File

@@ -0,0 +1,49 @@
using Lattice.Core.Models;
using Lattice.UI.Primitives; // Для доступа к LatticeIcon
using Microsoft.UI.Xaml.Controls;
namespace Lattice.UI.Controls;
/// <summary>
/// Панель инструментов, автоматически фильтрующая команды на основе текущего контекста Core.
/// </summary>
public class LatticeContextualToolbar : CommandBar
{
/// <summary>
/// Обновляет список команд на основе предоставленных определений и текущего контекста.
/// </summary>
/// <param name="actions">Полный список доступных действий.</param>
/// <param name="currentContext">Строковый идентификатор активного контекста (например, "CodeEditor").</param>
public void UpdateItems(IEnumerable<ActionDefinition> actions, string currentContext)
{
// Очищаем текущие команды
PrimaryCommands.Clear();
if (actions == null) return;
foreach (var action in actions)
{
// Логика 2026: показываем Common (общие), Global или специфичные для контекста команды
if (action.TargetContext == "Common" ||
action.TargetContext == "Global" ||
action.TargetContext == currentContext)
{
var button = new AppBarButton
{
Label = action.Label,
// Используем наш хелпер LatticeIcon для создания иконки из шрифта Segoe Fluent Icons
Icon = LatticeIcon.GetIcon(action.IconKey),
IsEnabled = action.IsEnabled
};
// Добавляем всплывающую подсказку (Tooltip)
if (!string.IsNullOrEmpty(action.Tooltip))
{
ToolTipService.SetToolTip(button, action.Tooltip);
}
PrimaryCommands.Add(button);
}
}
}
}

View File

@@ -0,0 +1,106 @@
using Lattice.Core.Abstractions;
using Lattice.Core.Models;
using Lattice.UI.Primitives;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Lattice.UI.Controls;
/// <summary>
/// Корневой контрол Lattice, отвечающий за отображение и управление макетом докинга.
/// </summary>
public class LatticeDockHost : Control
{
public DockAnchorOverlay? AnchorOverlay => GetTemplateChild("AnchorOverlay") as DockAnchorOverlay;
/// <summary>
/// Определяет свойство зависимости для LayoutManager.
/// </summary>
public static readonly DependencyProperty ManagerProperty =
DependencyProperty.Register(nameof(Manager), typeof(ILayoutService), typeof(LatticeDockHost), new PropertyMetadata(null, OnManagerChanged));
/// <summary>
/// Сервис управления макетом, привязанный к данному хосту.
/// </summary>
public ILayoutService? Manager
{
get => (ILayoutService?)GetValue(ManagerProperty);
set => SetValue(ManagerProperty, value);
}
/// <summary>
/// Указывает конкретный узел, который должен стать корнем для этого хоста.
/// Если null — используется Manager.Root.
/// </summary>
public static readonly DependencyProperty RootNodeProperty =
DependencyProperty.Register(nameof(RootNode), typeof(LayoutNode), typeof(LatticeDockHost), new PropertyMetadata(null, OnManagerChanged));
public LayoutNode? RootNode
{
get => (LayoutNode?)GetValue(RootNodeProperty);
set => SetValue(RootNodeProperty, value);
}
public LatticeDockHost()
{
this.DefaultStyleKey = typeof(LatticeDockHost);
}
private static void OnManagerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is LatticeDockHost host)
{
// Отписываемся от событий старого менеджера (если он был)
if (e.OldValue is ILayoutService oldService)
{
oldService.LayoutUpdated -= host.OnLayoutUpdated;
}
// Подписываемся на новый менеджер
if (e.NewValue is ILayoutService newService)
{
newService.LayoutUpdated += host.OnLayoutUpdated;
host.RebuildUI();
}
}
}
/// <summary>
/// Именованный метод для обработки обновления макета.
/// Позволяет корректно отписываться от событий и избегать утечек памяти.
/// </summary>
private void OnLayoutUpdated(object? sender, EventArgs e)
{
// WinUI 3 требует обновления UI только из основного потока
this.DispatcherQueue.TryEnqueue(() =>
{
this.RebuildUI();
});
}
/// <summary>
/// Полностью перестраивает визуальное дерево на основе текущего состояния Core-движка.
/// </summary>
private void RebuildUI()
{
if (this.GetTemplateChild("LayoutPresenter") is ContentPresenter presenter)
{
// Приоритет: сначала проверяем локальный RootNode, затем глобальный Manager.Root
var effectiveRoot = RootNode ?? Manager?.Root;
if (effectiveRoot != null)
{
var rootPanel = new LayoutPanel(this);
rootPanel.Build(effectiveRoot);
presenter.Content = rootPanel;
}
else
{
presenter.Content = null;
}
}
}
}

View File

@@ -0,0 +1,49 @@
using Lattice.Core.Abstractions;
using Lattice.Core.Models;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
namespace Lattice.UI.Controls;
/// <summary>
/// Обеспечивает поддержку выноса панелей в отдельные нативные окна Windows (Floating Windows).
/// </summary>
public class LatticeFloatingWindowHost
{
private readonly ILayoutService _manager;
/// <summary>
/// Инициализирует хост плавающих окон.
/// </summary>
/// <param name="manager">Общий менеджер макета приложения.</param>
public LatticeFloatingWindowHost(ILayoutService manager)
{
_manager = manager;
}
/// <summary>
/// Создает новое окно Windows для конкретного узла макета.
/// </summary>
/// <param name="node">Узел (панель), который нужно вынести в отдельное окно.</param>
public void CreateFromNode(LayoutNode node)
{
// Создаем новое окно WinUI 3
var newWindow = new Window();
// Создаем и настраиваем хост докинга для нового окна
var host = new LatticeDockHost
{
Manager = _manager, // Передаем общий менеджер, чтобы дерево было синхронизировано
RootNode = node, // Указываем хосту отображать ТОЛЬКО этот узел
};
newWindow.Content = host;
// Настройка нативного окна через AppWindow
AppWindow appWin = newWindow.AppWindow;
appWin.Title = node.Name;
// Показываем окно
newWindow.Activate();
}
}

View File

@@ -0,0 +1,83 @@
using Lattice.Core.Models;
using Lattice.UI.DragDrop;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Lattice.UI.Controls;
/// <summary>
/// Представляет визуальный контейнер для содержимого (панели или документа) в системе Lattice.
/// </summary>
[TemplatePart(Name = "HeaderPresenter", Type = typeof(FrameworkElement))]
[TemplatePart(Name = "ContentPresenter", Type = typeof(ContentPresenter))]
[TemplatePart(Name = "PART_CloseButton", Type = typeof(Button))] // Добавлено для ясности
public class LatticePane : ContentControl
{
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register(nameof(Title), typeof(string), typeof(LatticePane), new PropertyMetadata(string.Empty));
public static readonly DependencyProperty HeaderContentProperty =
DependencyProperty.Register(nameof(HeaderContent), typeof(object), typeof(LatticePane), new PropertyMetadata(null));
/// <summary>
/// Событие, возникающее при нажатии на кнопку закрытия в шаблоне.
/// </summary>
public event RoutedEventHandler? CloseClick;
public string Title
{
get => (string)GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
public object HeaderContent
{
get => GetValue(HeaderContentProperty);
set => SetValue(HeaderContentProperty, value);
}
public LatticePane()
{
this.DefaultStyleKey = typeof(LatticePane);
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
// Логика кнопки закрытия
if (GetTemplateChild("PART_CloseButton") is Button closeButton)
{
closeButton.Click -= OnCloseButtonClick; // Защита от двойной подписки
closeButton.Click += OnCloseButtonClick;
}
// Логика перетаскивания (Drag-and-Drop)
if (GetTemplateChild("HeaderPresenter") is FrameworkElement header)
{
this.Loaded += (s, e) =>
{
var host = FindParentHost(this);
if (host != null && this.DataContext is LayoutNode node)
{
var handler = new DockTabHandler(host);
handler.Attach(header, node);
}
};
}
}
private void OnCloseButtonClick(object sender, RoutedEventArgs e)
{
CloseClick?.Invoke(this, e);
}
private LatticeDockHost? FindParentHost(DependencyObject child)
{
var parent = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetParent(child);
if (parent == null) return null;
if (parent is LatticeDockHost host) return host;
return FindParentHost(parent);
}
}

View File

@@ -0,0 +1,99 @@
using Lattice.Core.Models;
using Lattice.Core.Models.Enums;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Media;
namespace Lattice.UI.Controls;
/// <summary>
/// Разделитель между панелями Lattice, позволяющий динамически изменять их размеры.
/// </summary>
[TemplatePart(Name = "PART_Thumb", Type = typeof(Thumb))]
public class LatticeSplitter : Control
{
private Thumb? _thumb;
/// <summary>
/// Узел макета, находящийся слева или сверху от разделителя.
/// </summary>
public LayoutNode? LeftNode { get; set; }
/// <summary>
/// Узел макета, находящийся справа или снизу от разделителя.
/// </summary>
public LayoutNode? RightNode { get; set; }
/// <summary>
/// Ориентация разделителя, определяющая направление изменения размера.
/// </summary>
public SplitOrientation Orientation { get; set; }
public LatticeSplitter()
{
this.DefaultStyleKey = typeof(LatticeSplitter);
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (_thumb != null) _thumb.DragDelta -= OnThumbDragDelta;
_thumb = GetTemplateChild("PART_Thumb") as Thumb;
if (_thumb != null) _thumb.DragDelta += OnThumbDragDelta;
}
private void OnThumbDragDelta(object sender, DragDeltaEventArgs e)
{
if (LeftNode == null || RightNode == null) return;
// В WinUI 3 (2026) для изменения пропорций Star-размеров
// мы корректируем WidthValue/HeightValue и уведомляем менеджер.
double sensitivity = 0.01; // Коэффициент чувствительности для плавности
if (Orientation == SplitOrientation.Horizontal)
{
double delta = e.HorizontalChange * sensitivity;
LeftNode.WidthValue += delta;
RightNode.WidthValue -= delta;
// Ограничения минимального размера (10% от доступного пространства)
if (LeftNode.WidthValue < 0.1) { RightNode.WidthValue += (LeftNode.WidthValue - 0.1); LeftNode.WidthValue = 0.1; }
if (RightNode.WidthValue < 0.1) { LeftNode.WidthValue += (RightNode.WidthValue - 0.1); RightNode.WidthValue = 0.1; }
}
else // Vertical
{
double delta = e.VerticalChange * sensitivity;
LeftNode.HeightValue += delta;
RightNode.HeightValue -= delta;
if (LeftNode.HeightValue < 0.1) { RightNode.HeightValue += (LeftNode.HeightValue - 0.1); LeftNode.HeightValue = 0.1; }
if (RightNode.HeightValue < 0.1) { LeftNode.HeightValue += (RightNode.HeightValue - 0.1); RightNode.HeightValue = 0.1; }
}
// Уведомляем систему об изменении макета через родительский хост
NotifyLayoutUpdated();
}
/// <summary>
/// Находит хост и вызывает обновление визуального дерева.
/// </summary>
private void NotifyLayoutUpdated()
{
DependencyObject parent = VisualTreeHelper.GetParent(this);
while (parent != null)
{
if (parent is LatticeDockHost host)
{
// Вызываем метод перерисовки (в Core это может быть событие LayoutUpdated)
// В нашем случае это заставит LayoutPanel пересчитать Column/Row Definitions
host.Manager?.Dock(null!, null!, DockDirection.Center); // Фиктивный вызов для обновления
// Или если есть прямой доступ: host.Manager.InvokeLayoutUpdated();
break;
}
parent = VisualTreeHelper.GetParent(parent);
}
}
}

View File

@@ -0,0 +1,43 @@
using Lattice.Core.Abstractions;
using Microsoft.UI.Xaml.Controls;
namespace Lattice.UI.Controls;
/// <summary>
/// Расширенный TabView для центральной области Lattice.
/// </summary>
public class LatticeTabStrip : TabView
{
private IContextService? _contextService;
public LatticeTabStrip()
{
this.TabCloseRequested += (s, e) =>
{
// Логика удаления вкладки из коллекции
this.TabItems.Remove(e.Tab);
// Если вкладок не осталось, сбрасываем контекст
if (this.TabItems.Count == 0)
{
_contextService?.SetContext("Common");
}
};
}
public void Initialize(IContextService contextService)
{
_contextService = contextService;
this.SelectionChanged += OnSelectionChanged;
}
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (this.SelectedItem is IDockableComponent component)
{
// Уведомляем ядро о смене контекста для обновления кнопок
_contextService?.SetContext(component.ContextGroup);
}
}
}

View File

@@ -0,0 +1,145 @@
using Lattice.Core.Models;
using Lattice.Core.Models.Enums;
using Lattice.UI.Controls;
using Lattice.UI.Services;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Windows.Foundation;
namespace Lattice.UI.DragDrop;
/// <summary>
/// Обработчик перетаскивания панелей и вкладок для системы Lattice.
/// </summary>
public class DockTabHandler
{
private bool _isDragging;
private readonly LatticeDockHost _host;
// Состояние текущей операции перетаскивания
private LayoutNode? _sourceNode;
private LayoutNode? _targetNode;
private DockDirection _currentSide;
public DockTabHandler(LatticeDockHost host)
{
_host = host;
}
/// <summary>
/// Привязывает логику перетаскивания к визуальному элементу (заголовку панели).
/// </summary>
/// <param name="header">Элемент, за который пользователь "хватает" панель.</param>
/// <param name="node">Узел макета, связанный с этой панелью.</param>
public void Attach(FrameworkElement header, LayoutNode node)
{
header.PointerPressed += (s, e) =>
{
_isDragging = true;
_sourceNode = node;
header.CapturePointer(e.Pointer);
if (_host.AnchorOverlay != null)
_host.AnchorOverlay.Visibility = Visibility.Visible;
};
header.PointerMoved += (s, e) =>
{
if (!_isDragging) return;
// Получаем позицию курсора относительно всего хоста
Point pointerPos = e.GetCurrentPoint(_host).Position;
UpdateOverlayPosition(pointerPos);
};
header.PointerReleased += (s, e) =>
{
if (!_isDragging) return;
_isDragging = false;
header.ReleasePointerCapture(e.Pointer);
CompleteDocking();
};
}
/// <summary>
/// Обновляет положение визуальных подсказок и рассчитывает зоны сброса.
/// </summary>
private void UpdateOverlayPosition(Point pointerPosition)
{
var overlay = _host.AnchorOverlay;
if (overlay == null) return;
// 1. Позиционируем "ромб" с кнопками докинга
overlay.PositionAnchors(pointerPosition);
// 2. Хит-тестинг: ищем LatticePane под курсором (исключая саму перетаскиваемую панель)
var elements = VisualTreeHelper.FindElementsInHostCoordinates(pointerPosition, _host);
var targetPane = elements.OfType<LatticePane>()
.FirstOrDefault(p => (p.DataContext as LayoutNode)?.Id != _sourceNode?.Id);
if (targetPane != null && targetPane.DataContext is LayoutNode targetNode)
{
_targetNode = targetNode;
// 3. Расчет локальной позиции для определения стороны
var transform = targetPane.TransformToVisual(_host);
Point localPoint = transform.Inverse.TransformPoint(pointerPosition);
// 4. Определяем сторону через сервис
_currentSide = VisualTreeService.GetHitZone(targetPane, localPoint);
// 5. Показываем синее превью зоны сброса
Rect previewRect = CalculatePreviewRect(targetPane, _currentSide);
Rect globalPreviewRect = transform.TransformBounds(previewRect);
overlay.ShowPreview(globalPreviewRect);
}
else
{
_targetNode = null;
overlay.HidePreview();
}
}
/// <summary>
/// Рассчитывает прямоугольник предпросмотра на основе выбранной стороны.
/// </summary>
private Rect CalculatePreviewRect(FrameworkElement pane, DockDirection side)
{
double w = pane.ActualWidth;
double h = pane.ActualHeight;
return side switch
{
DockDirection.Left => new Rect(0, 0, w / 2, h),
DockDirection.Right => new Rect(w / 2, 0, w / 2, h),
DockDirection.Top => new Rect(0, 0, w, h / 2),
DockDirection.Bottom => new Rect(0, h / 2, w, h / 2),
_ => new Rect(0, 0, w, h) // Center
};
}
/// <summary>
/// Завершает операцию докинга, передавая данные в Core Engine.
/// </summary>
private void CompleteDocking()
{
if (_sourceNode != null && _targetNode != null && _host.Manager != null)
{
// Вызываем логику перестроения дерева в Lattice.Core
_host.Manager.Dock(_sourceNode, _targetNode, _currentSide);
}
// Очистка UI
var overlay = _host.AnchorOverlay;
if (overlay != null)
{
overlay.Visibility = Visibility.Collapsed;
overlay.HidePreview();
}
_sourceNode = null;
_targetNode = null;
}
}

View File

@@ -0,0 +1,73 @@
<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>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Lattice.UI</AssemblyName>
<RootNamespace>Lattice.UI</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>
<UseWinUI>true</UseWinUI>
<IsTrimmable>false</IsTrimmable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<None Remove="Themes\Generic.xaml" />
<None Remove="Themes\Styles\DockAnchorOverlay.xaml" />
<None Remove="Themes\Styles\LatticeDockHost.xaml" />
<None Remove="Themes\Styles\LatticePane.xaml" />
<None Remove="Themes\Styles\LatticeSplitter.xaml" />
<None Remove="Themes\Styles\SharedResources.xaml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Lattice.Core\Lattice.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Page Update="Themes\Styles\SharedResources.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Themes\Styles\DockAnchorOverlay.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Themes\Styles\LatticeSplitter.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Themes\Styles\LatticeDockHost.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Themes\Generic.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Themes\Styles\LatticePane.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,71 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Shapes;
namespace Lattice.UI.Primitives;
/// <summary>
/// Визуальный оверлей, отображающий зоны приземления (Drop Zones) и якоря докинга.
/// </summary>
[TemplatePart(Name = "OverlayCanvas", Type = typeof(Canvas))]
[TemplatePart(Name = "DropPreview", Type = typeof(Rectangle))]
[TemplatePart(Name = "AnchorGroup", Type = typeof(Grid))]
public class DockAnchorOverlay : Control
{
private Canvas? _overlayCanvas;
private Rectangle? _dropPreview;
private Grid? _anchorGroup;
public DockAnchorOverlay()
{
// Привязываем стиль из Generic.xaml
this.DefaultStyleKey = typeof(DockAnchorOverlay);
// По умолчанию скрыт, показывается только во время Drag-and-Drop
this.Visibility = Visibility.Collapsed;
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_overlayCanvas = GetTemplateChild("OverlayCanvas") as Canvas;
_dropPreview = GetTemplateChild("DropPreview") as Rectangle;
_anchorGroup = GetTemplateChild("AnchorGroup") as Grid;
}
/// <summary>
/// Отображает превью будущей зоны закрепления.
/// </summary>
/// <param name="rect">Координаты и размер зоны.</param>
public void ShowPreview(Windows.Foundation.Rect rect)
{
if (_dropPreview == null) return;
_dropPreview.Visibility = Visibility.Visible;
Canvas.SetLeft(_dropPreview, rect.X);
Canvas.SetTop(_dropPreview, rect.Y);
_dropPreview.Width = rect.Width;
_dropPreview.Height = rect.Height;
}
/// <summary>
/// Скрывает превью зоны.
/// </summary>
public void HidePreview()
{
if (_dropPreview != null)
_dropPreview.Visibility = Visibility.Collapsed;
}
/// <summary>
/// Центрирует группу якорей (ромб) относительно указанной точки.
/// </summary>
public void PositionAnchors(Windows.Foundation.Point centerPoint)
{
if (_anchorGroup == null) return;
Canvas.SetLeft(_anchorGroup, centerPoint.X - (_anchorGroup.Width / 2));
Canvas.SetTop(_anchorGroup, centerPoint.Y - (_anchorGroup.Height / 2));
}
}

View File

@@ -0,0 +1,16 @@
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
namespace Lattice.UI.Primitives;
/// <summary>
/// Утилита для быстрого получения иконок в стиле Fluent UI 2.
/// </summary>
public static class LatticeIcon
{
public static FontIcon GetIcon(string glyph) => new FontIcon
{
Glyph = glyph,
FontFamily = new FontFamily("Segoe Fluent Icons")
};
}

View File

@@ -0,0 +1,121 @@
using Lattice.Core.Models;
using Lattice.Core.Models.Enums;
using Lattice.UI.Controls;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Lattice.UI.Primitives;
/// <summary>
/// Кастомный контейнер, преобразующий иерархию узлов Lattice в визуальные элементы WinUI 3.
/// </summary>
public class LayoutPanel : Grid
{
private readonly LatticeDockHost _host;
/// <summary>
/// Создает новый экземпляр панели компоновки.
/// </summary>
/// <param name="host">Корневой хост, управляющий макетом.</param>
public LayoutPanel(LatticeDockHost host)
{
_host = host;
}
/// <summary>
/// Выполняет рекурсивную отрисовку дерева узлов.
/// </summary>
public void Build(LayoutNode node)
{
this.Children.Clear();
this.ColumnDefinitions.Clear();
this.RowDefinitions.Clear();
if (node is SplitContainerNode splitContainer)
{
RenderSplit(splitContainer);
}
else if (node is ContentNode contentNode)
{
RenderContent(contentNode);
}
}
private void RenderSplit(SplitContainerNode container)
{
for (int i = 0; i < container.Children.Count; i++)
{
var child = container.Children[i];
var childPresenter = new LayoutPanel(_host);
if (container.Orientation == SplitOrientation.Horizontal)
{
this.ColumnDefinitions.Add(new ColumnDefinition
{
Width = child.IsWidthStar ? new GridLength(child.WidthValue, GridUnitType.Star) : new GridLength(child.WidthValue)
});
Grid.SetColumn(childPresenter, this.ColumnDefinitions.Count - 1);
}
else
{
this.RowDefinitions.Add(new RowDefinition
{
Height = child.IsHeightStar ? new GridLength(child.HeightValue, GridUnitType.Star) : new GridLength(child.HeightValue)
});
Grid.SetRow(childPresenter, this.RowDefinitions.Count - 1);
}
this.Children.Add(childPresenter);
childPresenter.Build(child);
// Добавляем сплиттер между элементами (кроме последнего)
if (i < container.Children.Count - 1)
{
AddSplitter(container, i);
}
}
}
private void AddSplitter(SplitContainerNode container, int index)
{
var splitter = new LatticeSplitter
{
LeftNode = container.Children[index],
RightNode = container.Children[index + 1],
Orientation = container.Orientation,
};
double thickness = (double)Application.Current.Resources["LatticeSplitterThickness"];
if (container.Orientation == SplitOrientation.Horizontal)
{
// Сплиттер занимает очень узкую колонку между основными
this.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(thickness) });
Grid.SetColumn(splitter, this.ColumnDefinitions.Count - 1);
}
else
{
this.RowDefinitions.Add(new RowDefinition { Height = new GridLength(thickness) });
Grid.SetRow(splitter, this.RowDefinitions.Count - 1);
}
this.Children.Add(splitter);
}
private void RenderContent(ContentNode node)
{
var pane = new LatticePane
{
Title = node.Name,
Content = node.Component,
DataContext = node,
};
pane.CloseClick += (s, e) =>
{
_host.Manager?.Remove(node);
};
this.Children.Add(pane);
}
}

56
Lattice.UI/README.md Normal file
View File

@@ -0,0 +1,56 @@
# Lattice.UI
[![Windows App SDK](img.shields.io)](#)
[![Developer](img.shields.io)](git.frigat.duckdns.org)
[![License](img.shields.io)](#)
**Lattice.UI** — это библиотека нативных элементов управления WinUI 3, реализующая сложную систему докинга и управления окнами в стиле Visual Studio 2026. Она визуализирует абстрактное дерево компоновки из `Lattice.Core` и обеспечивает плавное взаимодействие пользователя с интерфейсом.
## ✨ Основные компоненты
- **LatticeDockHost**: Корневой оркестратор, управляющий слоями контента и визуальными подсказками докинга.
- **LatticePane**: Универсальный контейнер для панелей и документов с поддержкой заголовков, контекстных кнопок и закрытия.
- **LatticeSplitter**: Тонкий интерактивный разделитель для динамического изменения размеров областей.
- **DockAnchorOverlay**: Система визуальных подсказок («ромб» докинга) и превью зон сброса (Drop Zones).
- **LatticeContextualToolbar**: Адаптивная панель инструментов, автоматически меняющая набор кнопок при смене фокуса между вкладками.
## 🛠 Технологии
- **Windows App SDK 1.8+**: Использование последних достижений WinUI 3.
- **Fluent UI 2**: Дизайн, полностью соответствующий стандартам Windows 11 (Mica Alt, закругления 4-8px, Segoe Fluent Icons).
- **Design Tokens**: Полная темизация через систему ресурсов (`SharedResources.xaml`).
## 📦 Установка и настройка
1. Добавьте ссылку на проект `Lattice.UI` в ваше решение.
2. В файле `App.xaml` вашего приложения подключите стили библиотеки:
```xml
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ms-appx:///Lattice.UI/Themes/Generic.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
```
### 🚀 Быстрый старт (XAML)
```
<Window
xmlns:lattice="using:Lattice.UI.Controls">
<lattice:LatticeDockHost x:Name="MainHost"
Manager="{x:Bind ViewModel.LayoutManager}" />
</Window>
```
### 📐 Математика докинга
- Библиотека использует алгоритм «Конверта» для расчета зон приземления:
- Центр: Объединение в группу вкладок.
- Края (L/R/T/B): Разделение текущей области на две части в соответствующей ориентации.
### 🔗 Ссылки
- Core Engine: Lattice.Core
- Репозиторий: git.frigat.duckdns.org
- Разработчик: FrigaT

View File

@@ -0,0 +1,52 @@
using Lattice.Core.Models.Enums;
using Microsoft.UI.Xaml;
using Windows.Foundation;
namespace Lattice.UI.Services;
/// <summary>
/// Сервис для анализа визуального дерева и расчета зон взаимодействия.
/// </summary>
public static class VisualTreeService
{
/// <summary>
/// Определяет зону докинга на основе позиции курсора относительно элемента.
/// </summary>
/// <param name="element">Визуальный элемент (панель), над которым находится курсор.</param>
/// <param name="relativePoint">Координаты курсора относительно левого верхнего угла элемента.</param>
/// <returns>Направление докинга (DockDirection).</returns>
public static DockDirection GetHitZone(FrameworkElement element, Point relativePoint)
{
double w = element.ActualWidth;
double h = element.ActualHeight;
// 1. Зона центра (обычно это 40% центральной области)
// Если курсор в центре, вкладка просто добавится в текущий TabView.
double centerX = w * 0.3;
double centerY = h * 0.3;
Rect centerRect = new Rect(centerX, centerY, w * 0.4, h * 0.4);
if (centerRect.Contains(relativePoint))
{
return DockDirection.Center;
}
// 2. Расчет по диагоналям для боковых зон
// Представьте конверт: линии из углов в центр. Это самый точный способ
// определения стороны в стиле Visual Studio.
// Нормализуем координаты в диапазон от 0 до 1
double nx = relativePoint.X / w;
double ny = relativePoint.Y / h;
// Уравнения диагоналей: y = x и y = 1 - x
bool isAbovePrimary = ny < nx;
bool isAboveSecondary = ny < (1 - nx);
if (isAbovePrimary && isAboveSecondary) return DockDirection.Top;
if (isAbovePrimary && !isAboveSecondary) return DockDirection.Right;
if (!isAbovePrimary && isAboveSecondary) return DockDirection.Left;
return DockDirection.Bottom;
}
}

View File

@@ -0,0 +1,16 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Lattice.UI.Controls">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ms-appx:///Lattice.UI/Themes/Styles/SharedResources.xaml" />
<!-- Порядок важен: сначала базовые ресурсы, потом стили контролов -->
<ResourceDictionary Source="ms-appx:///Lattice.UI/Themes/Styles/LatticeDockHost.xaml" />
<ResourceDictionary Source="ms-appx:///Lattice.UI/Themes/Styles/LatticePane.xaml" />
<ResourceDictionary Source="ms-appx:///Lattice.UI/Themes/Styles/LatticeSplitter.xaml" />
<ResourceDictionary Source="ms-appx:///Lattice.UI/Themes/Styles/DockAnchorOverlay.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Lattice.UI.Primitives">
<Style TargetType="local:DockAnchorOverlay">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DockAnchorOverlay">
<Canvas x:Name="OverlayCanvas" Background="Transparent">
<!-- Центральный "ромб" с кнопками направлений -->
<Grid x:Name="AnchorGroup" Width="120" Height="120">
<!-- Используем Acrylic или Mica Alt для стиля 2026 -->
<Rectangle Fill="{ThemeResource SystemControlAcrylicElementBrush}"
RadiusX="4" RadiusY="4" Opacity="0.8"/>
<!-- Кнопки-иконки (Left, Right, Top, Bottom, Center) -->
<FontIcon Glyph="&#xE76B;" VerticalAlignment="Top"/>
<!-- Top -->
<FontIcon Glyph="&#xE76C;" HorizontalAlignment="Left"/>
<!-- Left -->
<FontIcon Glyph="&#xE76D;" HorizontalAlignment="Right"/>
<!-- Right -->
<FontIcon Glyph="&#xE76E;" VerticalAlignment="Bottom"/>
<!-- Bottom -->
<FontIcon Glyph="&#xE72D;" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<!-- Center -->
</Grid>
<!-- Превью зоны (синий полупрозрачный прямоугольник) -->
<Rectangle x:Name="DropPreview" Fill="{ThemeResource SystemAccentColorLight3}"
Opacity="0.4" Visibility="Collapsed"/>
</Canvas>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Lattice.UI.Controls"
xmlns:primitives="using:Lattice.UI.Primitives">
<Style TargetType="local:LatticeDockHost">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:LatticeDockHost">
<!-- Используем Grid, чтобы наложить оверлей ПОВЕРХ контента -->
<Grid x:Name="RootGrid" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<!-- 1. Основной слой: Сюда программно рендерятся панели через LayoutPanel -->
<ContentPresenter x:Name="LayoutPresenter"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch" />
<!-- 2. Слой подсказок: Должен быть ниже в списке, чтобы быть выше визуально -->
<!-- Обязательно x:Name="AnchorOverlay", так как C# ищет его по этому имени -->
<primitives:DockAnchorOverlay x:Name="AnchorOverlay"
Visibility="Collapsed"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsHitTestVisible="False" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Lattice.UI.Controls">
<Style TargetType="local:LatticePane">
<!-- Используем StaticResource, так как эти токены определены в нашем SharedResources -->
<Setter Property="Background" Value="{StaticResource LatticePaneBackground}" />
<Setter Property="BorderBrush" Value="{StaticResource LatticePaneBorderBrush}" />
<Setter Property="BorderThickness" Value="{StaticResource LatticePaneMargin}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:LatticePane">
<Grid Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{StaticResource LatticePaneCornerRadius}">
<Grid.RowDefinitions>
<!-- Используем токен высоты заголовка -->
<RowDefinition Height="Auto" MinHeight="{StaticResource LatticePaneHeaderHeight}" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Заголовок панели -->
<Grid x:Name="HeaderPresenter"
Background="{StaticResource LatticePaneHeaderBackground}"
Padding="8,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="{TemplateBinding Title}"
VerticalAlignment="Center"
Style="{ThemeResource CaptionTextBlockStyle}"
MaxLines="1"
TextTrimming="CharacterEllipsis"/>
<StackPanel Orientation="Horizontal" Grid.Column="1" VerticalAlignment="Center">
<!-- Кастомный контент (кнопки пользователя) -->
<ContentPresenter Content="{TemplateBinding HeaderContent}" />
<!-- Стандартная кнопка закрытия -->
<!-- Используем стандартный стиль WinUI "отсутствие рамки", чтобы она выглядела как в VS -->
<Button x:Name="PART_CloseButton"
Content="&#xE8BB;"
FontFamily="Segoe Fluent Icons"
FontSize="10"
Padding="8,4"
Background="Transparent"
BorderThickness="0"
Style="{StaticResource DefaultButtonStyle}" />
</StackPanel>
</Grid>
<!-- Основной контент -->
<ContentPresenter Grid.Row="1"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Lattice.UI.Controls">
<Style TargetType="local:LatticeSplitter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:LatticeSplitter">
<Grid Background="{TemplateBinding Background}">
<!-- PART_Thumb — это невидимая или тонкая область, которую тянет пользователь -->
<Thumb x:Name="PART_Thumb">
<Thumb.Template>
<ControlTemplate TargetType="Thumb">
<Grid Background="Transparent">
<!-- Визуальная линия разделителя -->
<Rectangle Fill="{ThemeResource SystemControlForegroundBaseLowBrush}"
MinWidth="1" MinHeight="1" HorizontalAlignment="Center"/>
</Grid>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
>
<!-- Размеры (Tokens) -->
<x:Double x:Key="LatticeSplitterThickness">4.0</x:Double>
<x:Double x:Key="LatticePaneHeaderHeight">32.0</x:Double>
<CornerRadius x:Key="LatticePaneCornerRadius">4</CornerRadius>
<Thickness x:Key="LatticePaneMargin">1</Thickness>
<!-- Акцентный цвет (статичный токен) -->
<SolidColorBrush x:Key="LatticeActiveHeaderBrush" Color="{ThemeResource SystemAccentColor}"/>
<!-- Правильная привязка системных кистей для поддержки смены тем (Dark/Light) -->
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
<StaticResource x:Key="LatticePaneBackground" ResourceKey="LayerFillColorDefaultBrush"/>
<StaticResource x:Key="LatticePaneBorderBrush" ResourceKey="CardStrokeColorDefaultBrush"/>
<StaticResource x:Key="LatticePaneHeaderBackground" ResourceKey="LayerOnSecondaryFillColorDefaultBrush"/>
</ResourceDictionary>
<!-- Можно добавить специфические правки для HighContrast, если нужно -->
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>

View File

@@ -1,3 +1,4 @@
<Solution> <Solution>
<Project Path="../../../../../../../../Job/Projects/FrigaT/Lattice/Lattice.Core/Lattice.Core.csproj" /> <Project Path="Lattice.Core/Lattice.Core.csproj" />
<Project Path="Lattice.UI/Lattice.UI.csproj" Id="931bdfd9-8cf2-479a-895e-65facdb0a7ce" />
</Solution> </Solution>