DragAndDrop core

This commit is contained in:
FrigaT
2026-01-18 16:33:35 +03:00
parent 9ea82af329
commit 79bdd8bc62
229 changed files with 21214 additions and 2494 deletions

View File

@@ -0,0 +1,19 @@
<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>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Lattice.Themes.Core</RootNamespace>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<WinUISDKReferences>false</WinUISDKReferences>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,231 @@
namespace Lattice.Themes.Core.Tokens;
/// <summary>
/// Статические ключи для ресурсов Lattice Framework.
/// Используются в XAML через {DynamicResource} или {ThemeResource}.
/// </summary>
public static class LatticeTokens
{
// ============ Цвета (Colors) ============
// Основные цвета
public const string ColorPrimary = "Lattice.Color.Primary";
public const string ColorSecondary = "Lattice.Color.Secondary";
public const string ColorTertiary = "Lattice.Color.Tertiary";
// Акцентные цвета
public const string ColorAccent = "Lattice.Color.Accent";
public const string ColorAccentLight = "Lattice.Color.Accent.Light";
public const string ColorAccentDark = "Lattice.Color.Accent.Dark";
public const string ColorAccentAction = "Lattice.Color.Accent.Action";
// Фоновые цвета
public const string ColorBackgroundPrimary = "Lattice.Color.Background.Primary";
public const string ColorBackgroundSecondary = "Lattice.Color.Background.Secondary";
public const string ColorBackgroundTertiary = "Lattice.Color.Background.Tertiary";
public const string ColorBackgroundQuaternary = "Lattice.Color.Background.Quaternary";
// Текстовые цвета
public const string ColorTextPrimary = "Lattice.Color.Text.Primary";
public const string ColorTextSecondary = "Lattice.Color.Text.Secondary";
public const string ColorTextDisabled = "Lattice.Color.Text.Disabled";
public const string ColorTextOnAccent = "Lattice.Color.Text.OnAccent";
// Граничные цвета
public const string ColorBorderPrimary = "Lattice.Color.Border.Primary";
public const string ColorBorderSecondary = "Lattice.Color.Border.Secondary";
public const string ColorBorderAccent = "Lattice.Color.Border.Accent";
// Состояния
public const string ColorSuccess = "Lattice.Color.Success";
public const string ColorWarning = "Lattice.Color.Warning";
public const string ColorError = "Lattice.Color.Error";
public const string ColorInfo = "Lattice.Color.Info";
// ============ Кисти (Brushes) ============
// Основные кисти
public const string BrushPrimary = "Lattice.Brush.Primary";
public const string BrushSecondary = "Lattice.Brush.Secondary";
public const string BrushTertiary = "Lattice.Brush.Tertiary";
// Акцентные кисти
public const string BrushAccent = "Lattice.Brush.Accent";
public const string BrushAccentLight = "Lattice.Brush.Accent.Light";
public const string BrushAccentDark = "Lattice.Brush.Accent.Dark";
public const string BrushAccentAction = "Lattice.Brush.Accent.Action";
// Фоновые кисти
public const string BrushBackgroundPrimary = "Lattice.Brush.Background.Primary";
public const string BrushBackgroundSecondary = "Lattice.Brush.Background.Secondary";
public const string BrushBackgroundTertiary = "Lattice.Brush.Background.Tertiary";
public const string BrushBackgroundQuaternary = "Lattice.Brush.Background.Quaternary";
// Текстовые кисти
public const string BrushTextPrimary = "Lattice.Brush.Text.Primary";
public const string BrushTextSecondary = "Lattice.Brush.Text.Secondary";
public const string BrushTextDisabled = "Lattice.Brush.Text.Disabled";
public const string BrushTextOnAccent = "Lattice.Brush.Text.OnAccent";
// Граничные кисти
public const string BrushBorderPrimary = "Lattice.Brush.Border.Primary";
public const string BrushBorderSecondary = "Lattice.Brush.Border.Secondary";
public const string BrushBorderAccent = "Lattice.Brush.Border.Accent";
// Кисти состояний
public const string BrushSuccess = "Lattice.Brush.Success";
public const string BrushWarning = "Lattice.Brush.Warning";
public const string BrushError = "Lattice.Brush.Error";
public const string BrushInfo = "Lattice.Brush.Info";
// Кисти для перетаскивания
public const string BrushDragOverlay = "Lattice.Brush.Drag.Overlay";
public const string BrushDropPreview = "Lattice.Brush.Drop.Preview";
public const string BrushDropValid = "Lattice.Brush.Drop.Valid";
public const string BrushDropInvalid = "Lattice.Brush.Drop.Invalid";
// Кисти для панелей и разделителей
public const string BrushPanelBorder = "Lattice.Brush.Panel.Border";
public const string BrushSplitterNormal = "Lattice.Brush.Splitter.Normal";
public const string BrushSplitterHover = "Lattice.Brush.Splitter.Hover";
// ============ Геометрия (Geometry) ============
public const string SizeSplitterWidth = "Lattice.Size.Splitter.Width";
// Радиусы скругления
public const string CornerRadiusNone = "Lattice.CornerRadius.None";
public const string CornerRadiusSmall = "Lattice.CornerRadius.Small";
public const string CornerRadiusMedium = "Lattice.CornerRadius.Medium";
public const string CornerRadiusLarge = "Lattice.CornerRadius.Large";
public const string CornerRadiusXLarge = "Lattice.CornerRadius.XLarge";
public const string CornerRadiusCircle = "Lattice.CornerRadius.Circle";
public const string CornerRadiusPanel = "Lattice.CornerRadius.Panel";
// Отступы
public const string SpacingNone = "Lattice.Spacing.None";
public const string SpacingXSmall = "Lattice.Spacing.XSmall";
public const string SpacingSmall = "Lattice.Spacing.Small";
public const string SpacingMedium = "Lattice.Spacing.Medium";
public const string SpacingLarge = "Lattice.Spacing.Large";
public const string SpacingXLarge = "Lattice.Spacing.XLarge";
public const string SpacingXXLarge = "Lattice.Spacing.XXLarge";
public const string SpacingPanel = "Lattice.Spacing.Panel";
// Толщины границ
public const string BorderThicknessNone = "Lattice.BorderThickness.None";
public const string BorderThicknessThin = "Lattice.BorderThickness.Thin";
public const string BorderThicknessMedium = "Lattice.BorderThickness.Medium";
public const string BorderThicknessThick = "Lattice.BorderThickness.Thick";
public const string BorderThicknessPanel = "Lattice.BorderThickness.Panel";
// Размеры теней
public const string ShadowDepthNone = "Lattice.Shadow.Depth.None";
public const string ShadowDepthSmall = "Lattice.Shadow.Depth.Small";
public const string ShadowDepthMedium = "Lattice.Shadow.Depth.Medium";
public const string ShadowDepthLarge = "Lattice.Shadow.Depth.Large";
// ============ Текстовые стили (Typography) ============
// Размеры шрифтов
public const string FontSizeCaption = "Lattice.FontSize.Caption";
public const string FontSizeBody = "Lattice.FontSize.Body";
public const string FontSizeBodyStrong = "Lattice.FontSize.BodyStrong";
public const string FontSizeSubtitle = "Lattice.FontSize.Subtitle";
public const string FontSizeTitle = "Lattice.FontSize.Title";
public const string FontSizeTitleLarge = "Lattice.FontSize.TitleLarge";
public const string FontSizeDisplay = "Lattice.FontSize.Display";
// Высота строк
public const string LineHeightTight = "Lattice.LineHeight.Tight";
public const string LineHeightNormal = "Lattice.LineHeight.Normal";
public const string LineHeightRelaxed = "Lattice.LineHeight.Relaxed";
// Веса шрифтов
public const string FontWeightLight = "Lattice.FontWeight.Light";
public const string FontWeightNormal = "Lattice.FontWeight.Normal";
public const string FontWeightMedium = "Lattice.FontWeight.Medium";
public const string FontWeightSemibold = "Lattice.FontWeight.Semibold";
public const string FontWeightBold = "Lattice.FontWeight.Bold";
// ============ Анимации (Animations) ============
// Длительности
public const string DurationInstant = "Lattice.Duration.Instant";
public const string DurationFast = "Lattice.Duration.Fast";
public const string DurationNormal = "Lattice.Duration.Normal";
public const string DurationSlow = "Lattice.Duration.Slow";
// Кривые анимаций
public const string EasingLinear = "Lattice.Easing.Linear";
public const string EasingStandard = "Lattice.Easing.Standard";
public const string EasingStandardAccelerate = "Lattice.Easing.Standard.Accelerate";
public const string EasingStandardDecelerate = "Lattice.Easing.Standard.Decelerate";
public const string EasingEmphasized = "Lattice.Easing.Emphasized";
// ============ Разное (Miscellaneous) ============
// Прозрачности
public const string OpacityDisabled = "Lattice.Opacity.Disabled";
public const string OpacityOverlay = "Lattice.Opacity.Overlay";
public const string OpacityDrag = "Lattice.Opacity.Drag";
public const string OpacityDropPreview = "Lattice.Opacity.DropPreview";
// Z-индексы
public const string ZIndexDefault = "Lattice.ZIndex.Default";
public const string ZIndexOverlay = "Lattice.ZIndex.Overlay";
public const string ZIndexDialog = "Lattice.ZIndex.Dialog";
public const string ZIndexTooltip = "Lattice.ZIndex.Tooltip";
public const string ZIndexDrag = "Lattice.ZIndex.Drag";
public const string ZIndexDropPreview = "Lattice.ZIndex.DropPreview";
// Размеры иконок
public const string IconSizeSmall = "Lattice.IconSize.Small";
public const string IconSizeMedium = "Lattice.IconSize.Medium";
public const string IconSizeLarge = "Lattice.IconSize.Large";
// ============ Вспомогательные методы ============
/// <summary>
/// Получает все токены как коллекцию ключ-значение.
/// </summary>
public static IReadOnlyDictionary<string, string> GetAllTokens()
{
var fields = typeof(LatticeTokens).GetFields(
System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.Static);
var tokens = new Dictionary<string, string>();
foreach (var field in fields)
{
if (field.FieldType == typeof(string) && field.IsLiteral)
{
tokens[field.Name] = (string)field.GetValue(null)!;
}
}
return tokens;
}
/// <summary>
/// Проверяет, существует ли токен с указанным ключом.
/// </summary>
public static bool ContainsToken(string key)
{
return GetAllTokens().ContainsKey(key);
}
/// <summary>
/// Получает токен по его имени (не по значению).
/// </summary>
public static string? GetTokenByName(string name)
{
var field = typeof(LatticeTokens).GetField(
name,
System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.Static);
return field?.GetValue(null) as string;
}
}

View File

@@ -0,0 +1,13 @@
namespace Lattice.Themes;
public sealed class ThemeChangedEventArgs : EventArgs
{
public ThemePack OldTheme { get; }
public ThemePack NewTheme { get; }
public ThemeChangedEventArgs(ThemePack oldTheme, ThemePack newTheme)
{
OldTheme = oldTheme;
NewTheme = newTheme;
}
}

View File

@@ -0,0 +1,7 @@
using Microsoft.UI.Xaml;
namespace Lattice.Themes;
public sealed class ThemeDictionary : ResourceDictionary
{
}

View File

@@ -0,0 +1,293 @@
using Lattice.Themes.Core.Tokens;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
namespace Lattice.Themes;
/// <summary>
/// Менеджер тем для Lattice Framework.
/// </summary>
public sealed class ThemeManager
{
public static ThemeManager Current { get; } = new();
private ThemePack? _currentTheme;
private readonly Dictionary<string, ThemePack> _registeredThemes = new();
public ThemePack? CurrentTheme => _currentTheme;
public event EventHandler<ThemeChangedEventArgs>? ThemeChanged;
private ThemeManager() { }
/// <summary>
/// Регистрирует тему в менеджере.
/// </summary>
public void RegisterTheme(ThemePack theme)
{
if (theme == null)
throw new ArgumentNullException(nameof(theme));
_registeredThemes[theme.Name] = theme;
}
/// <summary>
/// Получает зарегистрированную тему по имени.
/// </summary>
public ThemePack? GetTheme(string name)
{
_registeredThemes.TryGetValue(name, out var theme);
return theme;
}
/// <summary>
/// Получает список всех зарегистрированных тем.
/// </summary>
public IReadOnlyCollection<ThemePack> GetRegisteredThemes()
{
return _registeredThemes.Values.ToList();
}
/// <summary>
/// Получение информации о теме.
/// </summary>
/// <param name="themeName"></param>
/// <returns></returns>
public ThemeInfo GetThemeInfo(string themeName)
{
if (!_registeredThemes.TryGetValue(themeName, out var theme))
return null;
return new ThemeInfo
{
Name = theme.Name,
Description = theme.Description,
Version = theme.Version,
IsDark = theme.IsDark,
TokenCount = CountTokensInTheme(theme)
};
}
/// <summary>
/// Применяет тему по имени.
/// </summary>
public void ApplyTheme(string themeName)
{
if (!_registeredThemes.TryGetValue(themeName, out var theme))
{
throw new ArgumentException($"Theme '{themeName}' is not registered.", nameof(themeName));
}
ApplyTheme(theme);
}
/// <summary>
/// Применяет указанную тему.
/// </summary>
public void ApplyTheme(ThemePack theme)
{
if (theme == null)
throw new ArgumentNullException(nameof(theme));
if (_currentTheme == theme)
return;
var old = _currentTheme;
_currentTheme = theme;
try
{
ReplaceApplicationResources(theme);
ThemeChanged?.Invoke(this, new ThemeChangedEventArgs(old!, theme));
}
catch (Exception ex)
{
// В случае ошибки возвращаемся к старой теме
_currentTheme = old;
if (old != null)
{
ReplaceApplicationResources(old);
}
throw new InvalidOperationException($"Failed to apply theme '{theme.Name}'.", ex);
}
}
/// <summary>
/// Загружает ресурсы темы в указанный словарь ресурсов.
/// </summary>
public void LoadThemeIntoDictionary(ResourceDictionary targetDictionary, ThemePack theme)
{
if (targetDictionary == null)
throw new ArgumentNullException(nameof(targetDictionary));
if (theme == null)
throw new ArgumentNullException(nameof(theme));
// Очищаем старые словари Lattice
for (int i = targetDictionary.MergedDictionaries.Count - 1; i >= 0; i--)
{
if (targetDictionary.MergedDictionaries[i] is ThemeDictionary)
targetDictionary.MergedDictionaries.RemoveAt(i);
}
// Добавляем новые словари темы
foreach (var uri in theme.GetResourceUris())
{
try
{
var themeDict = new ThemeDictionary { Source = uri };
targetDictionary.MergedDictionaries.Add(themeDict);
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to load theme resource from '{uri}'.", ex);
}
}
}
private int CountTokensInTheme(ThemePack theme)
{
try
{
var dict = new ResourceDictionary();
LoadThemeIntoDictionary(dict, theme);
return dict.Count;
}
catch
{
return 0;
}
}
private void ReplaceApplicationResources(ThemePack theme)
{
var app = Application.Current;
if (app == null)
return;
var root = app.Resources;
LoadThemeIntoDictionary(root, theme);
ForceUpdateUI();
}
private void ForceUpdateUI()
{
foreach (var window in WindowTracker.Windows)
{
if (window.Content is FrameworkElement root)
RefreshElement(root);
}
}
private void RefreshElement(FrameworkElement element)
{
var stack = new Stack<FrameworkElement>();
stack.Push(element);
while (stack.Count > 0)
{
var current = stack.Pop();
// Пересоздаём Template только у Control
if (current is Control control)
{
var template = control.Template;
control.Template = null;
control.Template = template;
}
else if (current is ContentPresenter contentPresenter)
{
// Обновляем ContentPresenter
var content = contentPresenter.Content;
contentPresenter.Content = null;
contentPresenter.Content = content;
}
// Добавляем детей в стек
int count = VisualTreeHelper.GetChildrenCount(current);
for (int i = 0; i < count; i++)
{
if (VisualTreeHelper.GetChild(current, i) is FrameworkElement child)
stack.Push(child);
}
}
}
/// <summary>
/// Проверяет, что все необходимые токены определены в текущей теме.
/// </summary>
public bool ValidateThemeTokens()
{
if (_currentTheme == null)
return false;
var app = Application.Current;
if (app == null)
return false;
var requiredTokens = LatticeTokens.GetAllTokens().Values;
var missingTokens = new List<string>();
foreach (var token in requiredTokens)
{
if (!app.Resources.ContainsKey(token))
{
missingTokens.Add(token);
}
}
if (missingTokens.Any())
{
System.Diagnostics.Debug.WriteLine($"Missing theme tokens: {string.Join(", ", missingTokens)}");
return false;
}
return true;
}
/// <summary>
/// Получает значение токена из текущей темы.
/// </summary>
public object? GetTokenValue(string tokenKey)
{
var app = Application.Current;
if (app == null)
return null;
if (app.Resources.TryGetValue(tokenKey, out var value))
{
return value;
}
return null;
}
/// <summary>
/// Получает значение токена с приведением к указанному типу.
/// </summary>
public T? GetTokenValue<T>(string tokenKey)
{
object? value = GetTokenValue(tokenKey);
if (value is T typedValue)
return typedValue;
return default;
}
}
/// <summary>
/// Информация о теме.
/// </summary>
public class ThemeInfo
{
/// <summary>
/// Название темы.
/// </summary>
public string Name { get; set; }
public string Description { get; set; }
public string Version { get; set; }
public bool IsDark { get; set; }
public int TokenCount { get; set; }
}

View File

@@ -0,0 +1,111 @@
using System.ComponentModel;
namespace Lattice.Themes;
/// <summary>
/// Базовый класс для пакетов тем.
/// </summary>
public abstract class ThemePack : INotifyPropertyChanged
{
private string _name;
private string _description = string.Empty;
private string _version = "1.0.0";
private bool _isDark;
public event PropertyChangedEventHandler? PropertyChanged;
protected ThemePack(string name)
{
_name = name ?? throw new ArgumentNullException(nameof(name));
}
/// <summary>
/// Название темы.
/// </summary>
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged(nameof(Name));
}
}
}
/// <summary>
/// Описание темы.
/// </summary>
public string Description
{
get => _description;
set
{
if (_description != value)
{
_description = value;
OnPropertyChanged(nameof(Description));
}
}
}
/// <summary>
/// Версия темы.
/// </summary>
public string Version
{
get => _version;
set
{
if (_version != value)
{
_version = value;
OnPropertyChanged(nameof(Version));
}
}
}
/// <summary>
/// Определяет, является ли тема тёмной.
/// </summary>
public bool IsDark
{
get => _isDark;
set
{
if (_isDark != value)
{
_isDark = value;
OnPropertyChanged(nameof(IsDark));
}
}
}
/// <summary>
/// Возвращает список словарей ресурсов, которые должны быть подключены.
/// </summary>
public abstract IReadOnlyList<Uri> GetResourceUris();
/// <summary>
/// Вызывается при применении темы.
/// </summary>
public virtual void OnApply()
{
// Может быть переопределено в производных классах
}
/// <summary>
/// Вызывается при удалении темы.
/// </summary>
public virtual void OnRemove()
{
// Может быть переопределено в производных классах
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

View File

@@ -0,0 +1,18 @@
using Microsoft.UI.Xaml;
namespace Lattice.Themes;
public static class WindowTracker
{
private static readonly List<Window> _windows = new();
public static IReadOnlyList<Window> Windows => _windows;
public static void Register(Window window)
{
if (!_windows.Contains(window))
_windows.Add(window);
window.Closed += (_, _) => _windows.Remove(window);
}
}