207 lines
6.7 KiB
C#
207 lines
6.7 KiB
C#
using Avalonia.Media;
|
|
using System.Collections.Generic;
|
|
using Avalonia.Styling;
|
|
using System;
|
|
using Dinah.Core;
|
|
using Newtonsoft.Json;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Themes.Fluent;
|
|
using System.Linq;
|
|
using System.Linq.Expressions;
|
|
using System.Reflection;
|
|
using System.Collections.Frozen;
|
|
|
|
#nullable enable
|
|
namespace LibationAvalonia;
|
|
|
|
public class ChardonnayTheme : IUpdatable, ICloneable
|
|
{
|
|
public event EventHandler? Updated;
|
|
|
|
/// <summary>Theme color overrides</summary>
|
|
[JsonProperty]
|
|
private readonly Dictionary<ThemeVariant, Dictionary<string, Color>> ThemeColors;
|
|
|
|
/// <summary>The two theme variants supported by Fluent themes</summary>
|
|
private static readonly FrozenSet<ThemeVariant> FluentVariants = [ThemeVariant.Light, ThemeVariant.Dark];
|
|
|
|
/// <summary>Reusable color pallets for the two theme variants</summary>
|
|
private static readonly FrozenDictionary<ThemeVariant, ColorPaletteResources> ColorPalettes
|
|
= FluentVariants.ToFrozenDictionary(t => t, _ => new ColorPaletteResources());
|
|
|
|
private ChardonnayTheme()
|
|
{
|
|
ThemeColors = FluentVariants.ToDictionary(t => t, _ => new Dictionary<string, Color>());
|
|
}
|
|
|
|
/// <summary> Invoke <see cref="IUpdatable.Updated"/> </summary>
|
|
public void Save() => Updated?.Invoke(this, EventArgs.Empty);
|
|
|
|
public Color GetColor(string? themeVariant, string itemName)
|
|
=> GetColor(FromVariantName(themeVariant), itemName);
|
|
|
|
public Color GetColor(ThemeVariant themeVariant, string itemName)
|
|
{
|
|
ValidateThemeVariant(themeVariant);
|
|
return ThemeColors[themeVariant].TryGetValue(itemName, out var color) ? color : default;
|
|
}
|
|
|
|
public ChardonnayTheme SetColor(string? themeVariant, Expression<Func<ColorPaletteResources, Color>> colorSelector, Color color)
|
|
=> SetColor(FromVariantName(themeVariant), colorSelector, color);
|
|
|
|
public ChardonnayTheme SetColor(ThemeVariant themeVariant, Expression<Func<ColorPaletteResources, Color>> colorSelector, Color color)
|
|
{
|
|
if (colorSelector.Body.NodeType is ExpressionType.MemberAccess &&
|
|
colorSelector.Body is MemberExpression memberExpression &&
|
|
memberExpression.Member is PropertyInfo colorProperty &&
|
|
colorProperty.DeclaringType == typeof(ColorPaletteResources))
|
|
return SetColor(themeVariant, colorProperty.Name, color);
|
|
return this;
|
|
}
|
|
|
|
public ChardonnayTheme SetColor(string? themeVariant, string itemName, Color itemColor)
|
|
=> SetColor(FromVariantName(themeVariant), itemName, itemColor);
|
|
|
|
public ChardonnayTheme SetColor(ThemeVariant themeVariant, string itemName, Color itemColor)
|
|
{
|
|
ValidateThemeVariant(themeVariant);
|
|
ThemeColors[themeVariant][itemName] = itemColor;
|
|
return this;
|
|
}
|
|
|
|
public FrozenDictionary<string, Color> GetThemeColors(string? themeVariant)
|
|
=> GetThemeColors(FromVariantName(themeVariant));
|
|
|
|
public FrozenDictionary<string, Color> GetThemeColors(ThemeVariant themeVariant)
|
|
{
|
|
ValidateThemeVariant(themeVariant);
|
|
return ThemeColors[themeVariant].ToFrozenDictionary();
|
|
}
|
|
|
|
public void ApplyTheme(string? themeVariant)
|
|
=> ApplyTheme(FromVariantName(themeVariant));
|
|
|
|
public void ApplyTheme(ThemeVariant themeVariant)
|
|
{
|
|
App.Current.RequestedThemeVariant = themeVariant;
|
|
themeVariant = App.Current.ActualThemeVariant;
|
|
ValidateThemeVariant(themeVariant);
|
|
|
|
bool fluentColorChanged = false;
|
|
|
|
//Set the Libation-specific brushes
|
|
var themeBrushes = (ResourceDictionary)App.Current.Resources.ThemeDictionaries[themeVariant];
|
|
foreach (var colorName in themeBrushes.Keys.OfType<string>())
|
|
{
|
|
if (ThemeColors[themeVariant].TryGetValue(colorName, out var color) && color != default)
|
|
{
|
|
if (themeBrushes[colorName] is ISolidColorBrush brush && brush.Color != color)
|
|
themeBrushes[colorName] = new SolidColorBrush(color);
|
|
}
|
|
}
|
|
|
|
//Set the fluent theme colors
|
|
foreach (var p in GetColorResourceProperties())
|
|
{
|
|
if (ThemeColors[themeVariant].TryGetValue(p.Name, out var color) && color != default)
|
|
{
|
|
if (p.GetValue(ColorPalettes[themeVariant]) is not Color c || c != color)
|
|
{
|
|
p.SetValue(ColorPalettes[themeVariant], color);
|
|
fluentColorChanged = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (fluentColorChanged)
|
|
{
|
|
var oldFluent = App.Current.Styles.OfType<FluentTheme>().Single();
|
|
App.Current.Styles.Remove(oldFluent);
|
|
|
|
//We must make a new fluent theme and add it to the app for
|
|
//the changes to the ColorPaletteResources to take effect.
|
|
//Changes to the Libation-specific resources are instant.
|
|
var newFluent = new FluentTheme();
|
|
|
|
foreach (var kvp in ColorPalettes)
|
|
newFluent.Palettes[kvp.Key] = kvp.Value;
|
|
|
|
App.Current.Styles.Add(newFluent);
|
|
}
|
|
}
|
|
|
|
/// <summary> Get the currently-active theme colors. </summary>
|
|
public static ChardonnayTheme GetLiveTheme()
|
|
{
|
|
var theme = new ChardonnayTheme();
|
|
|
|
foreach (var themeVariant in FluentVariants)
|
|
{
|
|
//Get the Libation-specific brushes
|
|
var themeBrushes = (ResourceDictionary)App.Current.Resources.ThemeDictionaries[themeVariant];
|
|
foreach (var colorName in themeBrushes.Keys.OfType<string>())
|
|
{
|
|
if (themeBrushes[colorName] is ISolidColorBrush brush)
|
|
{
|
|
//We're only working with colors, so convert the Brush's opacity to an alpha value
|
|
var color = Color.FromArgb
|
|
(
|
|
(byte)Math.Round(brush.Color.A * brush.Opacity, 0),
|
|
brush.Color.R,
|
|
brush.Color.G,
|
|
brush.Color.B
|
|
);
|
|
|
|
theme.ThemeColors[themeVariant][colorName] = color;
|
|
}
|
|
}
|
|
|
|
//Get the fluent theme colors
|
|
foreach (var p in GetColorResourceProperties())
|
|
{
|
|
var color = (Color)p.GetValue(ColorPalettes[themeVariant])!;
|
|
|
|
//The color isn't being overridden, so get the static resource value.
|
|
if (color == default)
|
|
{
|
|
var staticResourceName = p.Name == nameof(ColorPaletteResources.RegionColor) ? "SystemRegionColor" : $"System{p.Name}Color";
|
|
if (App.Current.TryGetResource(staticResourceName, themeVariant, out var colorObj) && colorObj is Color c)
|
|
color = c;
|
|
}
|
|
|
|
theme.ThemeColors[themeVariant][p.Name] = color;
|
|
}
|
|
}
|
|
return theme;
|
|
}
|
|
|
|
public object Clone()
|
|
{
|
|
var clone = new ChardonnayTheme();
|
|
foreach (var t in ThemeColors)
|
|
{
|
|
clone.ThemeColors[t.Key] = t.Value.ToDictionary();
|
|
}
|
|
return clone;
|
|
}
|
|
|
|
private static IEnumerable<PropertyInfo> GetColorResourceProperties()
|
|
=> typeof(ColorPaletteResources).GetProperties().Where(p => p.PropertyType == typeof(Color));
|
|
|
|
[System.Diagnostics.StackTraceHidden]
|
|
private static void ValidateThemeVariant(ThemeVariant themeVariant)
|
|
{
|
|
if (!FluentVariants.Contains(themeVariant))
|
|
throw new InvalidOperationException("FluentTheme.Palettes only supports Light and Dark variants.");
|
|
}
|
|
|
|
private static ThemeVariant FromVariantName(string? variantName)
|
|
=> variantName switch
|
|
{
|
|
nameof(ThemeVariant.Dark) => ThemeVariant.Dark,
|
|
nameof(ThemeVariant.Light) => ThemeVariant.Light,
|
|
// "System"
|
|
_ => ThemeVariant.Default
|
|
};
|
|
}
|