diff --git a/Lattice.Core/Models/ActionDefinition.cs b/Lattice.Core/Models/ActionDefinition.cs
index 91ec363..35d7652 100644
--- a/Lattice.Core/Models/ActionDefinition.cs
+++ b/Lattice.Core/Models/ActionDefinition.cs
@@ -11,12 +11,17 @@ public record ActionDefinition
public string Id { get; init; } = Guid.NewGuid().ToString();
///
- /// Текст кнопки.
+ /// Текст кнопки, отображаемый пользователю.
///
public string Label { get; init; } = "Action";
///
- /// Группа контекста, к которой привязана кнопка (например, "CodeEditor").
+ /// Код иконки из шрифта Segoe Fluent Icons (например, "\uE102").
+ ///
+ public string IconKey { get; init; } = "\uE102";
+
+ ///
+ /// Группа контекста, к которой привязана кнопка (например, "CodeEditor", "Common").
///
public string TargetContext { get; init; } = "Common";
@@ -26,7 +31,7 @@ public record ActionDefinition
public bool IsEnabled { get; set; } = true;
///
- /// Подсказка (Tooltip).
+ /// Подсказка, отображаемая при наведении (Tooltip).
///
public string Tooltip { get; init; } = string.Empty;
}
diff --git a/Lattice.UI/Controls/LatticeContextualToolbar.cs b/Lattice.UI/Controls/LatticeContextualToolbar.cs
new file mode 100644
index 0000000..6705087
--- /dev/null
+++ b/Lattice.UI/Controls/LatticeContextualToolbar.cs
@@ -0,0 +1,49 @@
+using Lattice.Core.Models;
+using Lattice.UI.Primitives; // Для доступа к LatticeIcon
+using Microsoft.UI.Xaml.Controls;
+
+namespace Lattice.UI.Controls;
+
+///
+/// Панель инструментов, автоматически фильтрующая команды на основе текущего контекста Core.
+///
+public class LatticeContextualToolbar : CommandBar
+{
+ ///
+ /// Обновляет список команд на основе предоставленных определений и текущего контекста.
+ ///
+ /// Полный список доступных действий.
+ /// Строковый идентификатор активного контекста (например, "CodeEditor").
+ public void UpdateItems(IEnumerable 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);
+ }
+ }
+ }
+}
diff --git a/Lattice.UI/Controls/LatticeDockHost.cs b/Lattice.UI/Controls/LatticeDockHost.cs
new file mode 100644
index 0000000..5d5c4f8
--- /dev/null
+++ b/Lattice.UI/Controls/LatticeDockHost.cs
@@ -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;
+
+///
+/// Корневой контрол Lattice, отвечающий за отображение и управление макетом докинга.
+///
+public class LatticeDockHost : Control
+{
+ public DockAnchorOverlay? AnchorOverlay => GetTemplateChild("AnchorOverlay") as DockAnchorOverlay;
+
+ ///
+ /// Определяет свойство зависимости для LayoutManager.
+ ///
+ public static readonly DependencyProperty ManagerProperty =
+ DependencyProperty.Register(nameof(Manager), typeof(ILayoutService), typeof(LatticeDockHost), new PropertyMetadata(null, OnManagerChanged));
+
+ ///
+ /// Сервис управления макетом, привязанный к данному хосту.
+ ///
+ public ILayoutService? Manager
+ {
+ get => (ILayoutService?)GetValue(ManagerProperty);
+ set => SetValue(ManagerProperty, value);
+ }
+
+ ///
+ /// Указывает конкретный узел, который должен стать корнем для этого хоста.
+ /// Если null — используется Manager.Root.
+ ///
+ 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();
+ }
+ }
+ }
+
+ ///
+ /// Именованный метод для обработки обновления макета.
+ /// Позволяет корректно отписываться от событий и избегать утечек памяти.
+ ///
+ private void OnLayoutUpdated(object? sender, EventArgs e)
+ {
+ // WinUI 3 требует обновления UI только из основного потока
+ this.DispatcherQueue.TryEnqueue(() =>
+ {
+ this.RebuildUI();
+ });
+ }
+
+
+ ///
+ /// Полностью перестраивает визуальное дерево на основе текущего состояния Core-движка.
+ ///
+ 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;
+ }
+ }
+ }
+
+
+}
diff --git a/Lattice.UI/Controls/LatticeFloatingWindow.cs b/Lattice.UI/Controls/LatticeFloatingWindow.cs
new file mode 100644
index 0000000..932768c
--- /dev/null
+++ b/Lattice.UI/Controls/LatticeFloatingWindow.cs
@@ -0,0 +1,49 @@
+using Lattice.Core.Abstractions;
+using Lattice.Core.Models;
+using Microsoft.UI.Windowing;
+using Microsoft.UI.Xaml;
+
+namespace Lattice.UI.Controls;
+
+///
+/// Обеспечивает поддержку выноса панелей в отдельные нативные окна Windows (Floating Windows).
+///
+public class LatticeFloatingWindowHost
+{
+ private readonly ILayoutService _manager;
+
+ ///
+ /// Инициализирует хост плавающих окон.
+ ///
+ /// Общий менеджер макета приложения.
+ public LatticeFloatingWindowHost(ILayoutService manager)
+ {
+ _manager = manager;
+ }
+
+ ///
+ /// Создает новое окно Windows для конкретного узла макета.
+ ///
+ /// Узел (панель), который нужно вынести в отдельное окно.
+ 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();
+ }
+}
diff --git a/Lattice.UI/Controls/LatticePane.cs b/Lattice.UI/Controls/LatticePane.cs
new file mode 100644
index 0000000..7a1a8ae
--- /dev/null
+++ b/Lattice.UI/Controls/LatticePane.cs
@@ -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;
+
+///
+/// Представляет визуальный контейнер для содержимого (панели или документа) в системе Lattice.
+///
+[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));
+
+ ///
+ /// Событие, возникающее при нажатии на кнопку закрытия в шаблоне.
+ ///
+ 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);
+ }
+}
diff --git a/Lattice.UI/Controls/LatticeSplitter.cs b/Lattice.UI/Controls/LatticeSplitter.cs
new file mode 100644
index 0000000..4057ad6
--- /dev/null
+++ b/Lattice.UI/Controls/LatticeSplitter.cs
@@ -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;
+
+///
+/// Разделитель между панелями Lattice, позволяющий динамически изменять их размеры.
+///
+[TemplatePart(Name = "PART_Thumb", Type = typeof(Thumb))]
+public class LatticeSplitter : Control
+{
+ private Thumb? _thumb;
+
+ ///
+ /// Узел макета, находящийся слева или сверху от разделителя.
+ ///
+ public LayoutNode? LeftNode { get; set; }
+
+ ///
+ /// Узел макета, находящийся справа или снизу от разделителя.
+ ///
+ public LayoutNode? RightNode { get; set; }
+
+ ///
+ /// Ориентация разделителя, определяющая направление изменения размера.
+ ///
+ 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();
+ }
+
+ ///
+ /// Находит хост и вызывает обновление визуального дерева.
+ ///
+ 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);
+ }
+ }
+}
diff --git a/Lattice.UI/Controls/LatticeTabStrip.cs b/Lattice.UI/Controls/LatticeTabStrip.cs
new file mode 100644
index 0000000..580ce46
--- /dev/null
+++ b/Lattice.UI/Controls/LatticeTabStrip.cs
@@ -0,0 +1,43 @@
+using Lattice.Core.Abstractions;
+using Microsoft.UI.Xaml.Controls;
+
+namespace Lattice.UI.Controls;
+
+///
+/// Расширенный TabView для центральной области Lattice.
+///
+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);
+ }
+ }
+}
diff --git a/Lattice.UI/DragDrop/DockTabHandler.cs b/Lattice.UI/DragDrop/DockTabHandler.cs
new file mode 100644
index 0000000..3a098b6
--- /dev/null
+++ b/Lattice.UI/DragDrop/DockTabHandler.cs
@@ -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;
+
+///
+/// Обработчик перетаскивания панелей и вкладок для системы Lattice.
+///
+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;
+ }
+
+ ///
+ /// Привязывает логику перетаскивания к визуальному элементу (заголовку панели).
+ ///
+ /// Элемент, за который пользователь "хватает" панель.
+ /// Узел макета, связанный с этой панелью.
+ 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();
+ };
+ }
+
+ ///
+ /// Обновляет положение визуальных подсказок и рассчитывает зоны сброса.
+ ///
+ 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()
+ .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();
+ }
+ }
+
+ ///
+ /// Рассчитывает прямоугольник предпросмотра на основе выбранной стороны.
+ ///
+ 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
+ };
+ }
+
+ ///
+ /// Завершает операцию докинга, передавая данные в Core Engine.
+ ///
+ 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;
+ }
+}
diff --git a/Lattice.UI/Lattice.UI.csproj b/Lattice.UI/Lattice.UI.csproj
new file mode 100644
index 0000000..74d34ae
--- /dev/null
+++ b/Lattice.UI/Lattice.UI.csproj
@@ -0,0 +1,73 @@
+
+
+ net8.0-windows10.0.19041.0;net9.0-windows10.0.19041.0;net10.0-windows10.0.19041.0
+ enable
+ enable
+ Lattice.UI
+ Lattice.UI
+
+ FrigaT
+ FrigaT
+ https://git.frigat.duckdns.org/FrigaT/Lattice
+ https://git.frigat.duckdns.org/FrigaT/Lattice
+
+ true
+ false
+ latest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ MSBuild:Compile
+
+
+
+
+
+ MSBuild:Compile
+
+
+
+
+
+ MSBuild:Compile
+
+
+
+
+
+ MSBuild:Compile
+
+
+
+
+
+ MSBuild:Compile
+
+
+
+
+
+ MSBuild:Compile
+
+
+
+
+
\ No newline at end of file
diff --git a/Lattice.UI/Primitives/DockAnchorOverlay.cs b/Lattice.UI/Primitives/DockAnchorOverlay.cs
new file mode 100644
index 0000000..8342f6f
--- /dev/null
+++ b/Lattice.UI/Primitives/DockAnchorOverlay.cs
@@ -0,0 +1,71 @@
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Shapes;
+
+namespace Lattice.UI.Primitives;
+
+///
+/// Визуальный оверлей, отображающий зоны приземления (Drop Zones) и якоря докинга.
+///
+[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;
+ }
+
+ ///
+ /// Отображает превью будущей зоны закрепления.
+ ///
+ /// Координаты и размер зоны.
+ 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;
+ }
+
+ ///
+ /// Скрывает превью зоны.
+ ///
+ public void HidePreview()
+ {
+ if (_dropPreview != null)
+ _dropPreview.Visibility = Visibility.Collapsed;
+ }
+
+ ///
+ /// Центрирует группу якорей (ромб) относительно указанной точки.
+ ///
+ 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));
+ }
+}
diff --git a/Lattice.UI/Primitives/LatticeIcon.cs b/Lattice.UI/Primitives/LatticeIcon.cs
new file mode 100644
index 0000000..55911e9
--- /dev/null
+++ b/Lattice.UI/Primitives/LatticeIcon.cs
@@ -0,0 +1,16 @@
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Media;
+
+namespace Lattice.UI.Primitives;
+
+///
+/// Утилита для быстрого получения иконок в стиле Fluent UI 2.
+///
+public static class LatticeIcon
+{
+ public static FontIcon GetIcon(string glyph) => new FontIcon
+ {
+ Glyph = glyph,
+ FontFamily = new FontFamily("Segoe Fluent Icons")
+ };
+}
diff --git a/Lattice.UI/Primitives/LayoutPanel.cs b/Lattice.UI/Primitives/LayoutPanel.cs
new file mode 100644
index 0000000..91a8901
--- /dev/null
+++ b/Lattice.UI/Primitives/LayoutPanel.cs
@@ -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;
+
+///
+/// Кастомный контейнер, преобразующий иерархию узлов Lattice в визуальные элементы WinUI 3.
+///
+public class LayoutPanel : Grid
+{
+ private readonly LatticeDockHost _host;
+
+ ///
+ /// Создает новый экземпляр панели компоновки.
+ ///
+ /// Корневой хост, управляющий макетом.
+ public LayoutPanel(LatticeDockHost host)
+ {
+ _host = host;
+ }
+
+ ///
+ /// Выполняет рекурсивную отрисовку дерева узлов.
+ ///
+ 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);
+ }
+}
diff --git a/Lattice.UI/README.md b/Lattice.UI/README.md
new file mode 100644
index 0000000..d121cc2
--- /dev/null
+++ b/Lattice.UI/README.md
@@ -0,0 +1,56 @@
+# Lattice.UI
+
+[](#)
+[](git.frigat.duckdns.org)
+[](#)
+
+**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
+
+
+
+
+
+
+
+```
+
+### 🚀 Быстрый старт (XAML)
+```
+
+
+
+
+```
+
+### 📐 Математика докинга
+- Библиотека использует алгоритм «Конверта» для расчета зон приземления:
+- Центр: Объединение в группу вкладок.
+- Края (L/R/T/B): Разделение текущей области на две части в соответствующей ориентации.
+
+### 🔗 Ссылки
+- Core Engine: Lattice.Core
+- Репозиторий: git.frigat.duckdns.org
+- Разработчик: FrigaT
\ No newline at end of file
diff --git a/Lattice.UI/Services/VisualTreeService.cs b/Lattice.UI/Services/VisualTreeService.cs
new file mode 100644
index 0000000..45a9d9f
--- /dev/null
+++ b/Lattice.UI/Services/VisualTreeService.cs
@@ -0,0 +1,52 @@
+using Lattice.Core.Models.Enums;
+using Microsoft.UI.Xaml;
+using Windows.Foundation;
+
+namespace Lattice.UI.Services;
+
+///
+/// Сервис для анализа визуального дерева и расчета зон взаимодействия.
+///
+public static class VisualTreeService
+{
+ ///
+ /// Определяет зону докинга на основе позиции курсора относительно элемента.
+ ///
+ /// Визуальный элемент (панель), над которым находится курсор.
+ /// Координаты курсора относительно левого верхнего угла элемента.
+ /// Направление докинга (DockDirection).
+ 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;
+ }
+}
diff --git a/Lattice.UI/Themes/Generic.xaml b/Lattice.UI/Themes/Generic.xaml
new file mode 100644
index 0000000..024198b
--- /dev/null
+++ b/Lattice.UI/Themes/Generic.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Lattice.UI/Themes/Styles/DockAnchorOverlay.xaml b/Lattice.UI/Themes/Styles/DockAnchorOverlay.xaml
new file mode 100644
index 0000000..cdb075c
--- /dev/null
+++ b/Lattice.UI/Themes/Styles/DockAnchorOverlay.xaml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
diff --git a/Lattice.UI/Themes/Styles/LatticeDockHost.xaml b/Lattice.UI/Themes/Styles/LatticeDockHost.xaml
new file mode 100644
index 0000000..559b65f
--- /dev/null
+++ b/Lattice.UI/Themes/Styles/LatticeDockHost.xaml
@@ -0,0 +1,33 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Lattice.UI/Themes/Styles/LatticePane.xaml b/Lattice.UI/Themes/Styles/LatticePane.xaml
new file mode 100644
index 0000000..40aa1d5
--- /dev/null
+++ b/Lattice.UI/Themes/Styles/LatticePane.xaml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Lattice.UI/Themes/Styles/LatticeSplitter.xaml b/Lattice.UI/Themes/Styles/LatticeSplitter.xaml
new file mode 100644
index 0000000..3f93ee4
--- /dev/null
+++ b/Lattice.UI/Themes/Styles/LatticeSplitter.xaml
@@ -0,0 +1,30 @@
+
+
+
+
+
diff --git a/Lattice.UI/Themes/Styles/SharedResources.xaml b/Lattice.UI/Themes/Styles/SharedResources.xaml
new file mode 100644
index 0000000..6ce89d5
--- /dev/null
+++ b/Lattice.UI/Themes/Styles/SharedResources.xaml
@@ -0,0 +1,26 @@
+
+
+
+
+ 4.0
+ 32.0
+ 4
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Lattice.slnx b/Lattice.slnx
index 02e1f68..311d94a 100644
--- a/Lattice.slnx
+++ b/Lattice.slnx
@@ -1,3 +1,4 @@
-
+
+