Добавлен проект 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

@@ -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);
}
}
}