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 + +[![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 + + + + + + + +``` + +### 🚀 Быстрый старт (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 @@ - + +