using Avalonia; using Avalonia.Collections; using Avalonia.Data; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.Styling; using SukiUI.Enums; using SukiUI.Extensions; using SukiUI.Models; using System; using System.Collections.Generic; using System.Linq; namespace SukiUI; public partial class SukiTheme : Styles { public static readonly StyledProperty ThemeColorProperty = AvaloniaProperty.Register(nameof(Color), defaultBindingMode: BindingMode.OneTime, defaultValue: SukiColor.Blue); public static readonly StyledProperty IsRightToLeftProperty = AvaloniaProperty.Register(nameof(IsRightToLeft), defaultBindingMode: BindingMode.OneTime, defaultValue: false); /// /// Used to assign the ColorTheme at launch, /// public SukiColor ThemeColor { get => GetValue(ThemeColorProperty); set { SetValue(ThemeColorProperty, value); SetColorThemeResourcesOnColorThemeChanged(); } } public bool IsRightToLeft { get => GetValue(IsRightToLeftProperty); set => SetValue(IsRightToLeftProperty, value); } /// /// Called whenever the application's is changed. /// Useful where controls cannot use "DynamicResource" /// public Action? OnColorThemeChanged { get; set; } /// /// Called whenever the application's is changed. /// Useful where controls need to change based on light/dark. /// public Action? OnBaseThemeChanged { get; set; } /// /// Currently active /// If you want to change this please use /// public SukiColorTheme? ActiveColorTheme { get; private set; } /// /// All available Color Themes. /// public IAvaloniaReadOnlyList ColorThemes => _allThemes; /// /// Currently active /// If you want to change this please use or /// public ThemeVariant ActiveBaseTheme => _app.ActualThemeVariant; private readonly Application _app; private readonly HashSet _colorThemeHashset = new(); private readonly AvaloniaList _allThemes = new(); public SukiTheme() { AvaloniaXamlLoader.Load(this); _app = Application.Current!; _app.ActualThemeVariantChanged += (_, e) => OnBaseThemeChanged?.Invoke(_app.ActualThemeVariant); foreach (var theme in DefaultColorThemes) AddColorTheme(theme.Value); UpdateFlowDirectionResources(IsRightToLeft); } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { if (change.Property == IsRightToLeftProperty) { UpdateFlowDirectionResources(change.GetNewValue()); } base.OnPropertyChanged(change); } /// /// Change the theme to one of the default themes. /// /// The to change to. public void ChangeColorTheme(SukiColor sukiColor) => ThemeColor = sukiColor; /// /// Tries to change the theme to a specific theme, this can be either a default or a custom defined one. /// /// public void ChangeColorTheme(SukiColorTheme sukiColorTheme) => SetColorTheme(sukiColorTheme); /// /// Blindly switches to the "next" theme available in the collection. /// public void SwitchColorTheme() { var index = -1; for (var i = 0; i < ColorThemes.Count; i++) { if (ColorThemes[i] != ActiveColorTheme) continue; index = i; break; } if (index == -1) return; var newIndex = (index + 1) % ColorThemes.Count; var newColorTheme = ColorThemes[newIndex]; ChangeColorTheme(newColorTheme); } /// /// Add a new to the ones available, without making it active. /// /// New to add. public void AddColorTheme(SukiColorTheme sukiColorTheme) { if (_colorThemeHashset.Contains(sukiColorTheme)) throw new InvalidOperationException("This color theme has already been added."); _colorThemeHashset.Add(sukiColorTheme); _allThemes.Add(sukiColorTheme); } /// /// Adds multiple new to the ones available, without making any active. /// /// A collection of new to add. public void AddColorThemes(IEnumerable sukiColorThemes) { foreach (var colorTheme in sukiColorThemes) AddColorTheme(colorTheme); } /// /// Tries to change the base theme to the one provided, if it is different. /// /// to change to. public void ChangeBaseTheme(ThemeVariant baseTheme) { if (_app.ActualThemeVariant == baseTheme) return; _app.RequestedThemeVariant = baseTheme; } /// /// Simply switches from Light -> Dark and visa versa. /// public void SwitchBaseTheme() { if (Application.Current is null) return; var newBase = Application.Current.ActualThemeVariant == ThemeVariant.Dark ? ThemeVariant.Light : ThemeVariant.Dark; Application.Current.RequestedThemeVariant = newBase; } private void UpdateFlowDirectionResources(bool rightToLeft) { var primary = rightToLeft ? FlowDirection.RightToLeft : FlowDirection.LeftToRight; var opposite = rightToLeft ? FlowDirection.LeftToRight : FlowDirection.RightToLeft; Resources["FlowDirectionPrimary"] = primary; Resources["FlowDirectionOpposite"] = opposite; } /// /// Initializes the color theme resources whenever the property is changed. /// In an ideal world people wouldn't use the property /// private void SetColorThemeResourcesOnColorThemeChanged() { if (!DefaultColorThemes.TryGetValue(ThemeColor, out var colorTheme)) throw new Exception($"{ThemeColor} has no defined color theme."); SetColorTheme(colorTheme); } private void SetColorTheme(SukiColorTheme colorTheme) { SetColorWithOpacities("SukiPrimaryColor", colorTheme.Primary); SetResource("SukiPrimaryDarkColor", colorTheme.PrimaryDark); SetColorWithOpacities("SukiAccentColor", colorTheme.Accent); SetResource("SukiAccentDarkColor", colorTheme.AccentDark); ActiveColorTheme = colorTheme; OnColorThemeChanged?.Invoke(ActiveColorTheme); } private void SetColorWithOpacities(string baseName, Color baseColor) { SetResource(baseName, baseColor); SetResource($"{baseName}75", baseColor.WithAlpha(0.75)); SetResource($"{baseName}50", baseColor.WithAlpha(0.50)); SetResource($"{baseName}25", baseColor.WithAlpha(0.25)); SetResource($"{baseName}20", baseColor.WithAlpha(0.2)); SetResource($"{baseName}15", baseColor.WithAlpha(0.15)); SetResource($"{baseName}10", baseColor.WithAlpha(0.10)); SetResource($"{baseName}7", baseColor.WithAlpha(0.07)); SetResource($"{baseName}5", baseColor.WithAlpha(0.05)); SetResource($"{baseName}3", baseColor.WithAlpha(0.03)); SetResource($"{baseName}1", baseColor.WithAlpha(0.005)); SetResource($"{baseName}0", baseColor.WithAlpha(0.00)); } private void SetResource(string name, Color color) => _app.Resources[name] = color; // Static Members... /// /// The default Color Themes included with SukiUI. /// public static readonly IReadOnlyDictionary DefaultColorThemes; static SukiTheme() { var defaultThemes = new[] { new DefaultSukiColorTheme(SukiColor.Orange, Color.Parse("#d48806"), Color.Parse("#176CE8")), new DefaultSukiColorTheme(SukiColor.Red, Color.Parse("#D03A2F"), Color.Parse("#2FC5D0")), new DefaultSukiColorTheme(SukiColor.Green, Color.Parse("#537834"), Color.Parse("#B24DB0")), new DefaultSukiColorTheme(SukiColor.Blue, Color.Parse("#0A59F7"), Color.Parse("#F7A80A")), }; DefaultColorThemes = defaultThemes.ToDictionary(x => x.ThemeColor, y => (SukiColorTheme)y); } /// /// Retrieves an instance tied to a specific instance of an application. /// /// A instance that can be used to change themes. /// Thrown if no SukiTheme has been defined in App.axaml. public static SukiTheme GetInstance(Application app) { var theme = app.Styles.FirstOrDefault(style => style is SukiTheme); if (theme is not SukiTheme sukiTheme) throw new InvalidOperationException( "No SukiTheme instance available. Ensure SukiTheme has been set in Application.Styles in App.axaml."); return sukiTheme; } /// /// Retrieves an instance tied to the currently active application. /// /// A instance that can be used to change themes. /// Thrown if no SukiTheme has been defined in App.axaml. public static SukiTheme GetInstance() => GetInstance(Application.Current!); }