using System.Reflection;
using System.Windows.Media.Animation;
using System.Collections;
namespace NeoUI.Appearance;
///
/// 主题管理核心类:负责
/// 1. 维护当前明暗模式 + 调色板状态(_activeThemeMode/_activeThemePalette)
/// 2. 通过替换 Application.Resources.MergedDictionaries 中的主题/调色板 ResourceDictionary 实现运行时切换
/// 3. 可选颜色平滑动画(针对已有 SolidColorBrush 实例)
/// 4. 确保同一时刻仅存在一个主题字典 + 一个调色板字典,避免覆盖顺序错误
///
/// 重要概念:
/// - “主题字典” (Light.xaml / Dark.xaml) 定义基础背景 / 文本 / 语义色等
/// - “调色板字典” (LightBlue.xaml / DarkBlue.xaml 等) 定义品牌主色系(Primary*)
/// - WPF 资源解析为“后添加优先覆盖前面”,因此顺序必须:主题 -> 调色板 -> 其它控件样式
/// - 控件 XAML 需使用 DynamicResource 才能在字典替换时刷新;StaticResource 仅解析一次
///
public class ThemeManager
{
/// 当前库程序集名称,用于组合 pack:// 路径
public static string LibraryNamespace => Assembly.GetExecutingAssembly().GetName().Name;
/// 主题资源根路径(基础 + 调色板都基于此)
public static string ThemesDictionaryPath => $"pack://application:,,,/{LibraryNamespace};component/Themes/";
/// 内部缓存:当前明暗模式(避免重复加载)
private static ThemeMode _activeThemeMode = ThemeMode.Light;
/// 内部缓存:当前调色板
private static ThemePalette _activeThemePalette = ThemePalette.Blue;
// 持久化开关:通过 EnablePersistence 控制是否写入文件
private static bool _persistenceEnabled;
/// 主题模式变更事件(成功应用后才触发)
public static event Action? ThemeModeChanged;
/// 主题调色板变更事件(成功应用后才触发)
public static event Action? ThemePaletteChanged;
#region Animation
///
/// 对已有 SolidColorBrush 做颜色渐变动画。
/// 注意:它仅改变 Brush 实例的 Color,不负责替换资源字典;动画完成后再真正替换字典可保证:
/// - 已绑定此 Brush 的控件看到平滑过渡
/// - 替换字典后新增键/非 Brush 资源被更新
/// 限制:冻结 (Frozen) 的 Brush 无法动画(会直接跳过)
///
private static Task AnimateBrushColorAsync(SolidColorBrush brush, Color toColor, int durationMs = 500)
{
if (brush.IsFrozen) return Task.CompletedTask; // 设计或某些场景可能出现冻结,如手动 Freeze()
var tcs = new TaskCompletionSource();
var anim = new ColorAnimation
{
To = toColor,
Duration = TimeSpan.FromMilliseconds(durationMs),
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseInOut },
FillBehavior = FillBehavior.HoldEnd // 保留最终值(防止回弹)
};
// Completed 总是回到 UI 线程上下文
anim.Completed += (_, _) => tcs.TrySetResult(true);
brush.BeginAnimation(SolidColorBrush.ColorProperty, anim);
return tcs.Task;
}
#endregion
#region Public Query
///
/// 解析 Application 资源中当前主题字典(文件名包含 light.xaml / dark.xaml)以同步内部缓存。
/// 说明:外部调用可能绕过本类直接替换资源,因此这里以资源实际状态为准。
///
public static ThemeMode GetAppThemeMode()
{
var dict = new ResourceDictionaryManager().LookupDictionary("themes");
if (dict?.Source != null)
{
var uri = dict.Source.ToString();
if (uri.IndexOf("light.xaml", StringComparison.OrdinalIgnoreCase) >= 0)
_activeThemeMode = ThemeMode.Light;
else if (uri.IndexOf("dark.xaml", StringComparison.OrdinalIgnoreCase) >= 0)
_activeThemeMode = ThemeMode.Dark;
}
return _activeThemeMode;
}
///
/// 同上:根据当前调色板字典文件名更新内部缓存。
/// 注意:调色板文件名包含 Light/Dark 前缀 + 颜色名,例如 LightBlue.xaml / DarkBlue.xaml
///
public static ThemePalette GetAppThemePalette()
{
var dict = new ResourceDictionaryManager().LookupDictionary("ColorPalette");
if (dict?.Source != null)
{
var uri = dict.Source.ToString();
if (uri.IndexOf("Blue.xaml", StringComparison.OrdinalIgnoreCase) >= 0)
_activeThemePalette = ThemePalette.Blue;
else if (uri.IndexOf("Green.xaml", StringComparison.OrdinalIgnoreCase) >= 0)
_activeThemePalette = ThemePalette.Green;
else if (uri.IndexOf("Purple.xaml", StringComparison.OrdinalIgnoreCase) >= 0)
_activeThemePalette = ThemePalette.Purple;
}
return _activeThemePalette;
}
#endregion
///
/// 将当前 Application 级主题资源合并到某个单独窗口/控件的 Resources(用于隔离场景提前绑定)。
/// 通常不需要调用;只有在局部字典需要显式继承应用级主题时使用。
///
public static void ApplyResourcesToElement(FrameworkElement frameworkElement)
{
if (frameworkElement == null) return;
var appMerged = Current.Resources.MergedDictionaries;
var target = frameworkElement.Resources.MergedDictionaries;
// 简单判断:数量少于应用就尝试补齐(不会去重深比较)
if (target.Count < appMerged.Count)
{
foreach (var d in appMerged)
if (!target.Contains(d))
target.Add(d);
}
}
#region Unified Theme API
///
/// 异步统一入口:可同时改变明暗模式 + 调色板。只传 mode 或 palette 代表单独改变。
/// animate=true 启用颜色渐变(注意:仅对已经存在的 SolidColorBrush 有效果)。
///
public static Task ApplyThemeAsync(ThemeMode? mode = null,
ThemePalette? palette = null,
bool animate = false,
int animationDurationMs = 350)
=> InternalApplyThemeAsync(mode, palette, animate, animationDurationMs);
///
/// 同步封装(内部实际仍走异步,只是阻塞等待)。
/// UI 线程中调用时不建议长动画阻塞;如果需要平滑体验请直接使用 await ApplyThemeAsync。
///
public static void ApplyTheme(ThemeMode? mode = null,
ThemePalette? palette = null,
bool animate = false,
int animationDurationMs = 350)
=> InternalApplyThemeAsync(mode, palette, animate, animationDurationMs).GetAwaiter().GetResult();
///
/// 主题/调色板应用核心逻辑:
/// 1. 计算是否真的发生变化(避免重复加载)
/// 2. 预加载新字典(不合并)用于取目标颜色参与动画
/// 3. 可选对现有 Brush 做动画(不创建新实例,避免引用断开)
/// 4. 替换 ResourceDictionary(删除旧 -> 添加新,保证唯一顺序)
/// 5. 更新内部状态并触发事件
///
private static async Task InternalApplyThemeAsync(ThemeMode? mode,
ThemePalette? palette,
bool animate,
int durationMs)
{
// Step1: 获取真实当前状态(可能被外部改动过)
var currentMode = GetAppThemeMode();
var currentPalette = GetAppThemePalette();
// Step2: 计算目标(未传则沿用现值)
var newMode = mode ?? currentMode;
var newPalette = palette ?? currentPalette;
// Step3: 判断变化
// 明暗模式改变时:对应调色板文件(LightBlue -> DarkBlue)必然需要重新加载,即便颜色名相同
bool modeChanged = newMode != currentMode;
bool paletteChanged = newPalette != currentPalette || modeChanged;
if (!modeChanged && !paletteChanged) return; // 无变化直接结束
// Step4: 组装文件名(当前命名约定:Dark/Light + 调色板名)
string modeName = newMode == ThemeMode.Dark ? "Dark" : "Light";
string paletteName = newPalette switch
{
ThemePalette.Blue => "Blue",
ThemePalette.Green => "Green",
ThemePalette.Purple => "Purple",
_ => "Blue" // 兜底,理论不应走到
};
// Step5: 构造 Uri(pack://)
var themeUri = new Uri($"{ThemesDictionaryPath}{modeName}.xaml", UriKind.Absolute);
var paletteUri = new Uri($"{ThemesDictionaryPath}ColorPalette/{modeName}{paletteName}.xaml", UriKind.Absolute);
// Step6: 预加载字典(此时资源已经解析,可取得目标 Brush 颜色)
ResourceDictionary? newThemeDict = null;
ResourceDictionary? newPaletteDict = null;
if (modeChanged) newThemeDict = new ResourceDictionary { Source = themeUri };
if (paletteChanged) newPaletteDict = new ResourceDictionary { Source = paletteUri };
// Step7: 动画(仅针对已有 Brush)
if (animate)
{
var tasks = new List();
if (newThemeDict != null) CollectBrushAnimations(newThemeDict, tasks, durationMs);
if (newPaletteDict != null) CollectBrushAnimations(newPaletteDict, tasks, durationMs);
if (tasks.Count > 0) await Task.WhenAll(tasks);
// 动画完成后,将已动画的旧 Brush 实例复用到新字典,避免字典替换产生新实例导致“跳变”
if (newThemeDict != null) ReuseAnimatedBrushInstances(newThemeDict);
if (newPaletteDict != null) ReuseAnimatedBrushInstances(newPaletteDict);
}
// Step8: 真正替换 / 添加字典(保证唯一 + 顺序)
if (modeChanged) ReplaceOrAddDictionary(themeUri, isTheme: true);
if (paletteChanged) ReplaceOrAddDictionary(paletteUri, isTheme: false);
// Step9: 更新内部状态 & 发事件(只在实际变更时触发)
if (modeChanged)
{
_activeThemeMode = newMode;
ThemeModeChanged?.Invoke(newMode);
}
if (paletteChanged)
{
_activeThemePalette = newPalette;
ThemePaletteChanged?.Invoke(newPalette);
}
// Step10: 可选持久化(只要实际有改变才写)
if (_persistenceEnabled && (modeChanged || paletteChanged))
{
try { ThemePreferenceStore.Save(_activeThemeMode, _activeThemePalette); } catch { /* 忽略 */ }
}
#if DEBUG
// 调试辅助:输出当前合并字典列表,便于确认顺序是否正确,以及是否重复遗留
DumpMerged("AfterApplyTheme");
#endif
}
///
/// 为动画阶段收集“可以做颜色过渡”的画刷任务:仅当新字典中存在相同 Key 且旧资源中对应是 SolidColorBrush。
/// 注意:这里不替换 Brush 实例,只修改 Color,保持绑定引用稳定。
///
private static void CollectBrushAnimations(ResourceDictionary? newDict, List tasks, int durationMs)
{
var existing = Current.Resources;
foreach (DictionaryEntry entry in newDict)
{
if (entry.Value is SolidColorBrush newBrush && existing[entry.Key] is SolidColorBrush oldBrush && !ReferenceEquals(oldBrush, newBrush))
tasks.Add(AnimateBrushColorAsync(oldBrush, newBrush.Color, durationMs));
}
}
///
/// 动画后复用已经过渡完成的旧 Brush 实例,避免替换字典时创建新实例导致闪烁或动画丢失。
/// 仅覆盖同 Key SolidColorBrush;其它资源仍使用新字典的对象(确保新增键/不同类型可生效)。
///
private static void ReuseAnimatedBrushInstances(ResourceDictionary? newDict)
{
var existing = Current.Resources;
var keys = new List