diff --git a/Material.Avalonia.Demo/App.axaml b/Material.Avalonia.Demo/App.axaml index fafb075d..d3e1341f 100644 --- a/Material.Avalonia.Demo/App.axaml +++ b/Material.Avalonia.Demo/App.axaml @@ -1,10 +1,10 @@ - + xmlns:themes="clr-namespace:Material.Styles.Themes;assembly=Material.Styles"> fonts:Inter#Inter, $Default @@ -24,6 +24,25 @@ + + + + + diff --git a/Material.Avalonia.Demo/GlobalCommand.cs b/Material.Avalonia.Demo/GlobalCommand.cs index 54cf9724..6388aa74 100644 --- a/Material.Avalonia.Demo/GlobalCommand.cs +++ b/Material.Avalonia.Demo/GlobalCommand.cs @@ -7,8 +7,8 @@ namespace Material.Avalonia.Demo; public static class GlobalCommand { - private static readonly MaterialTheme MaterialThemeStyles = - Application.Current!.LocateMaterialTheme(); + private static readonly MaterialThemeBase MaterialThemeStyles = + Application.Current!.LocateMaterialTheme(); public static void UseMaterialUIDarkTheme() { diff --git a/Material.Styles/Themes/CustomMaterialTheme.cs b/Material.Styles/Themes/CustomMaterialTheme.cs new file mode 100644 index 00000000..d0f2c740 --- /dev/null +++ b/Material.Styles/Themes/CustomMaterialTheme.cs @@ -0,0 +1,211 @@ +using System; +using System.Collections.Generic; +using Avalonia; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.Threading; +using Material.Styles.Themes.Base; + +namespace Material.Styles.Themes; + +public class CustomMaterialTheme : MaterialThemeBase, IDisposable { + + public static readonly StyledProperty PrimaryColorProperty = + AvaloniaProperty.Register(nameof(PrimaryColor)); + + public static readonly StyledProperty SecondaryColorProperty = + AvaloniaProperty.Register(nameof(SecondaryColor)); + + private readonly ITheme _theme = new Theme(); + + private bool _isLoaded; + private IThemeVariantHost? _lastThemeVariantHost; + private IDisposable? _themeUpdateDisposable; + private bool _disposedValue; + + public IDictionary Palettes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The XAML service provider. + public CustomMaterialTheme(IServiceProvider serviceProvider) : base(serviceProvider) { + var palettes = new AvaloniaDictionary(2); + palettes.ForEachItem( + (key, x) => { + if (Owner is not null) { + ((IResourceProvider)x).AddOwner(Owner); + } + + if (key != ThemeVariant.Dark && key != ThemeVariant.Light) { + throw new InvalidOperationException( + $"{nameof(CustomMaterialTheme)}.{nameof(CustomMaterialTheme.Palettes)} only supports Light and Dark variants."); + } + }, + (_, x) => { + if (Owner is not null) + ((IResourceProvider)x).RemoveOwner(Owner); + }, + () => throw new NotSupportedException("Dictionary reset not supported")); + Palettes = palettes; + + OwnerChanged += OnOwnerChanged; + } + + public Color? PrimaryColor { + get => GetValue(PrimaryColorProperty); + set => SetValue(PrimaryColorProperty, value); + } + + public Color? SecondaryColor { + get => GetValue(SecondaryColorProperty); + set => SetValue(SecondaryColorProperty, value); + } + private void OnOwnerChanged(object? sender, EventArgs e) { + RegisterActualThemeObservable(); + } + + protected override bool TryGetResource(object key, ThemeVariant? theme, out object? value) { + return base.TryGetResource(key, theme, out value) + || base.TryGetResource(key, GetVariantFromMaterialBaseThemeMode(ActualBaseTheme), out value); + } + + private static ThemeVariant GetVariantFromMaterialBaseThemeMode(BaseThemeMode variant) { + return variant switch { + BaseThemeMode.Light => Theme.MaterialLight, + BaseThemeMode.Dark => Theme.MaterialDark, + _ => throw new ArgumentOutOfRangeException(nameof(variant), variant, null) + }; + } + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { + base.OnPropertyChanged(change); + + if (change.Property == BaseThemeProperty) { + SetupActualTheme(); + return; + } + + if (change.Property == ActualBaseThemeProperty) { + var baseTheme = change.GetNewValue().GetBaseTheme(); + _theme.SetBaseTheme(baseTheme); + if (GetPrimaryColor() is { } primaryColor) { + _theme.SetPrimaryColor(primaryColor); + } + if (GetSecondaryColor() is { } secondaryColor) { + _theme.SetSecondaryColor(secondaryColor); + } + EnqueueThemeUpdate(); + return; + } + + if (change.Property == PrimaryColorProperty) { + if (GetPrimaryColor() is { } primaryColor) { + _theme.SetPrimaryColor(primaryColor); + EnqueueThemeUpdate(); + } + return; + } + + if (change.Property == SecondaryColorProperty) { + if (GetSecondaryColor() is { } secondaryColor) { + _theme.SetSecondaryColor(secondaryColor); + EnqueueThemeUpdate(); + } + } + } + + private Color? GetPrimaryColor() { + Color? color = null; + if (GetActualThemeVariant() is { } themeVariant && + Palettes.TryGetValue(themeVariant, out var colorPalate)) { + color = colorPalate.PrimaryColor; + } + return color ?? PrimaryColor; + } + + private Color? GetSecondaryColor() { + Color? color = null; + if (GetActualThemeVariant() is { } themeVariant && + Palettes.TryGetValue(themeVariant, out var colorPalate)) { + color = colorPalate.SecondaryColor; + } + return color ?? SecondaryColor; + } + + private void EnqueueThemeUpdate() { + if (!_isLoaded) + return; + + _themeUpdateDisposable?.Dispose(); + _themeUpdateDisposable = DispatcherTimer.RunOnce(() => CurrentTheme = _theme, TimeSpan.FromMilliseconds(100)); + } + + private void RegisterActualThemeObservable() { + if (_lastThemeVariantHost is not null) + _lastThemeVariantHost.ActualThemeVariantChanged -= HostOnActualThemeVariantChanged; + + _lastThemeVariantHost = Owner as IThemeVariantHost; + if (_lastThemeVariantHost is not null) + _lastThemeVariantHost.ActualThemeVariantChanged += HostOnActualThemeVariantChanged; + SetupActualTheme(); + } + + private void HostOnActualThemeVariantChanged(object? sender, EventArgs e) { + SetupActualTheme(); + } + + private void SetupActualTheme() { + var materialBaseThemeModeFromVariant = BaseTheme switch { + BaseThemeMode.Inherit => GetMaterialBaseThemeModeFromVariant(_lastThemeVariantHost?.ActualThemeVariant) ?? BaseThemeMode.Light, + BaseThemeMode.Light => BaseThemeMode.Light, + BaseThemeMode.Dark => BaseThemeMode.Dark, + _ => throw new ArgumentOutOfRangeException(nameof(BaseTheme), BaseTheme, null) + }; + + ActualBaseTheme = materialBaseThemeModeFromVariant; + } + + private static BaseThemeMode? GetMaterialBaseThemeModeFromVariant(ThemeVariant? variant) { + while (true) { + if (variant is null) + return null; + if (variant == ThemeVariant.Light) + return BaseThemeMode.Light; + if (variant == ThemeVariant.Dark) + return BaseThemeMode.Dark; + variant = variant.InheritVariant; + } + } + + private ThemeVariant? GetActualThemeVariant() { + return ActualBaseTheme switch { + BaseThemeMode.Light => ThemeVariant.Light, + BaseThemeMode.Dark => ThemeVariant.Dark, + _ => null + }; + } + + protected override ITheme ProvideInitialTheme() { + _isLoaded = true; + return _theme; + } + + protected virtual void Dispose(bool disposing) { + if (!_disposedValue) { + if (disposing) { + _themeUpdateDisposable?.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/Material.Styles/Themes/CustomMaterialThemeResources.cs b/Material.Styles/Themes/CustomMaterialThemeResources.cs new file mode 100644 index 00000000..1038240d --- /dev/null +++ b/Material.Styles/Themes/CustomMaterialThemeResources.cs @@ -0,0 +1,9 @@ +using Avalonia.Media; + +namespace Material.Styles.Themes; + +public class CustomMaterialThemeResources { + public Color? PrimaryColor { get; set; } + + public Color? SecondaryColor { get; set; } +} diff --git a/Material.Styles/Themes/MaterialTheme.cs b/Material.Styles/Themes/MaterialTheme.cs index 61f9e8cb..be33019c 100644 --- a/Material.Styles/Themes/MaterialTheme.cs +++ b/Material.Styles/Themes/MaterialTheme.cs @@ -13,8 +13,6 @@ namespace Material.Styles.Themes { /// You need to setup all these properties: , , /// public class MaterialTheme : MaterialThemeBase, IDisposable { - public static readonly StyledProperty BaseThemeProperty = - AvaloniaProperty.Register(nameof(BaseTheme)); public static readonly StyledProperty PrimaryColorProperty = AvaloniaProperty.Register(nameof(PrimaryColor)); @@ -22,13 +20,8 @@ public class MaterialTheme : MaterialThemeBase, IDisposable { public static readonly StyledProperty SecondaryColorProperty = AvaloniaProperty.Register(nameof(SecondaryColor)); - public static readonly DirectProperty ActualBaseThemeProperty = - AvaloniaProperty.RegisterDirect( - nameof(ActualBaseTheme), - o => o.ActualBaseTheme); private readonly ITheme _theme = new Theme(); - private BaseThemeMode _actualBaseTheme; private bool _isLoaded; private IThemeVariantHost? _lastThemeVariantHost; private IDisposable? _themeUpdateDisposable; @@ -41,16 +34,6 @@ public MaterialTheme(IServiceProvider serviceProvider) : base(serviceProvider) { OwnerChanged += OnOwnerChanged; } - public BaseThemeMode BaseTheme { - get => GetValue(BaseThemeProperty); - set => SetValue(BaseThemeProperty, value); - } - - public BaseThemeMode ActualBaseTheme { - get => _actualBaseTheme; - private set => SetAndRaise(ActualBaseThemeProperty, ref _actualBaseTheme, value); - } - public PrimaryColor PrimaryColor { get => GetValue(PrimaryColorProperty); set => SetValue(PrimaryColorProperty, value); diff --git a/Material.Styles/Themes/MaterialThemeBase.cs b/Material.Styles/Themes/MaterialThemeBase.cs index 856eb2d9..c20444c5 100644 --- a/Material.Styles/Themes/MaterialThemeBase.cs +++ b/Material.Styles/Themes/MaterialThemeBase.cs @@ -12,10 +12,20 @@ using Avalonia.Styling; using Avalonia.Threading; using Material.Styles.Internal; +using Material.Styles.Themes.Base; namespace Material.Styles.Themes; public class MaterialThemeBase : Avalonia.Styling.Styles, IResourceNode { + + public static readonly StyledProperty BaseThemeProperty = + AvaloniaProperty.Register(nameof(BaseTheme)); + + public static readonly DirectProperty ActualBaseThemeProperty = + AvaloniaProperty.RegisterDirect( + nameof(ActualBaseTheme), + o => o.ActualBaseTheme); + public static readonly DirectProperty CurrentThemeProperty = AvaloniaProperty.RegisterDirect( nameof(CurrentTheme), @@ -25,6 +35,7 @@ public class MaterialThemeBase : Avalonia.Styling.Styles, IResourceNode { private readonly IServiceProvider? _serviceProvider; private readonly LightweightSubject _themeChangedEndSubject = new(); private IReadOnlyTheme _currentTheme = new ReadOnlyTheme(); + private BaseThemeMode _actualBaseTheme; private Task? _currentThemeUpdateTask; private IResourceDictionary? _internalResources; @@ -41,6 +52,16 @@ public MaterialThemeBase(IServiceProvider? serviceProvider) { _serviceProvider = serviceProvider; } + public BaseThemeMode BaseTheme { + get => GetValue(BaseThemeProperty); + set => SetValue(BaseThemeProperty, value); + } + + public BaseThemeMode ActualBaseTheme { + get => _actualBaseTheme; + protected set => SetAndRaise(ActualBaseThemeProperty, ref _actualBaseTheme, value); + } + /// /// Get or set current applied theme ///