Skip to content

Commit

Permalink
Merge pull request #413 from Keboo/customColorTheme
Browse files Browse the repository at this point in the history
Adding a new CustomColor theme option
  • Loading branch information
SKProCH authored Nov 26, 2024
2 parents 9e7758b + 9201422 commit 2ca03e7
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 23 deletions.
27 changes: 23 additions & 4 deletions Material.Avalonia.Demo/App.axaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<Application xmlns="https://github.com/avaloniaui"
<Application x:Class="Material.Avalonia.Demo.App"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:themes="clr-namespace:Material.Styles.Themes;assembly=Material.Styles"
xmlns:dialogHostAvalonia="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:dialogHostAvalonia="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
xmlns:grammars="clr-namespace:TextMateSharp.Grammars;assembly=TextMateSharp.Grammars"
x:Class="Material.Avalonia.Demo.App">
xmlns:themes="clr-namespace:Material.Styles.Themes;assembly=Material.Styles">
<Application.Resources>
<FontFamily x:Key="ContentControlThemeFontFamily">fonts:Inter#Inter, $Default</FontFamily>
</Application.Resources>
Expand All @@ -24,6 +24,25 @@
</themes:MaterialTheme.Resources>
</themes:MaterialTheme>

<!--
If you would prefer to use custom primary and second colors you can use the CustomMaterialTheme
-->
<!--<themes:CustomMaterialTheme PrimaryColor="#4BEB59" SecondaryColor="#04C9F0" />-->

<!--
If you need different colors for light and dark theme, the CustomMaterialTheme supports
individual resources for light and dark theme.
-->
<!--<themes:CustomMaterialTheme>
<themes:CustomMaterialTheme.Palettes>
<themes:CustomMaterialThemeResources x:Key="Dark"
PrimaryColor="#4BEB59"
SecondaryColor="#04C9F0" />
<themes:CustomMaterialThemeResources x:Key="Light"
PrimaryColor="#29964A"
SecondaryColor="#0271A4" />
</themes:CustomMaterialTheme.Palettes>
</themes:CustomMaterialTheme>-->
<avalonia:MaterialIconStyles />
<dialogHostAvalonia:DialogHostStyles />

Expand Down
4 changes: 2 additions & 2 deletions Material.Avalonia.Demo/GlobalCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ namespace Material.Avalonia.Demo;

public static class GlobalCommand
{
private static readonly MaterialTheme MaterialThemeStyles =
Application.Current!.LocateMaterialTheme<MaterialTheme>();
private static readonly MaterialThemeBase MaterialThemeStyles =
Application.Current!.LocateMaterialTheme<MaterialThemeBase>();

public static void UseMaterialUIDarkTheme()
{
Expand Down
211 changes: 211 additions & 0 deletions Material.Styles/Themes/CustomMaterialTheme.cs
Original file line number Diff line number Diff line change
@@ -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<Color?> PrimaryColorProperty =
AvaloniaProperty.Register<MaterialTheme, Color?>(nameof(PrimaryColor));

public static readonly StyledProperty<Color?> SecondaryColorProperty =
AvaloniaProperty.Register<MaterialTheme, Color?>(nameof(SecondaryColor));

private readonly ITheme _theme = new Theme();

private bool _isLoaded;
private IThemeVariantHost? _lastThemeVariantHost;
private IDisposable? _themeUpdateDisposable;
private bool _disposedValue;

public IDictionary<ThemeVariant, CustomMaterialThemeResources> Palettes { get; }

/// <summary>
/// Initializes a new instance of the <see cref="MaterialTheme"/> class.
/// </summary>
/// <param name="serviceProvider">The XAML service provider.</param>
public CustomMaterialTheme(IServiceProvider serviceProvider) : base(serviceProvider) {
var palettes = new AvaloniaDictionary<ThemeVariant, CustomMaterialThemeResources>(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)
};
}

/// <inheritdoc />
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) {
base.OnPropertyChanged(change);

if (change.Property == BaseThemeProperty) {
SetupActualTheme();
return;
}

if (change.Property == ActualBaseThemeProperty) {
var baseTheme = change.GetNewValue<BaseThemeMode>().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);
}
}
9 changes: 9 additions & 0 deletions Material.Styles/Themes/CustomMaterialThemeResources.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Avalonia.Media;

namespace Material.Styles.Themes;

public class CustomMaterialThemeResources {
public Color? PrimaryColor { get; set; }

public Color? SecondaryColor { get; set; }
}
17 changes: 0 additions & 17 deletions Material.Styles/Themes/MaterialTheme.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,15 @@ namespace Material.Styles.Themes {
/// You need to setup all these properties: <see cref="BaseTheme"/>, <see cref="PrimaryColor"/>, <see cref="SecondaryColor"/>
/// </remarks>
public class MaterialTheme : MaterialThemeBase, IDisposable {
public static readonly StyledProperty<BaseThemeMode> BaseThemeProperty =
AvaloniaProperty.Register<MaterialTheme, BaseThemeMode>(nameof(BaseTheme));

public static readonly StyledProperty<PrimaryColor> PrimaryColorProperty =
AvaloniaProperty.Register<MaterialTheme, PrimaryColor>(nameof(PrimaryColor));

public static readonly StyledProperty<SecondaryColor> SecondaryColorProperty =
AvaloniaProperty.Register<MaterialTheme, SecondaryColor>(nameof(SecondaryColor));

public static readonly DirectProperty<MaterialTheme, BaseThemeMode> ActualBaseThemeProperty =
AvaloniaProperty.RegisterDirect<MaterialTheme, BaseThemeMode>(
nameof(ActualBaseTheme),
o => o.ActualBaseTheme);
private readonly ITheme _theme = new Theme();

private BaseThemeMode _actualBaseTheme;
private bool _isLoaded;
private IThemeVariantHost? _lastThemeVariantHost;
private IDisposable? _themeUpdateDisposable;
Expand All @@ -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);
Expand Down
21 changes: 21 additions & 0 deletions Material.Styles/Themes/MaterialThemeBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<BaseThemeMode> BaseThemeProperty =
AvaloniaProperty.Register<MaterialThemeBase, BaseThemeMode>(nameof(BaseTheme));

public static readonly DirectProperty<MaterialThemeBase, BaseThemeMode> ActualBaseThemeProperty =
AvaloniaProperty.RegisterDirect<MaterialThemeBase, BaseThemeMode>(
nameof(ActualBaseTheme),
o => o.ActualBaseTheme);

public static readonly DirectProperty<MaterialThemeBase, IReadOnlyTheme> CurrentThemeProperty =
AvaloniaProperty.RegisterDirect<MaterialThemeBase, IReadOnlyTheme>(
nameof(CurrentTheme),
Expand All @@ -25,6 +35,7 @@ public class MaterialThemeBase : Avalonia.Styling.Styles, IResourceNode {
private readonly IServiceProvider? _serviceProvider;
private readonly LightweightSubject<MaterialThemeBase> _themeChangedEndSubject = new();
private IReadOnlyTheme _currentTheme = new ReadOnlyTheme();
private BaseThemeMode _actualBaseTheme;
private Task? _currentThemeUpdateTask;

private IResourceDictionary? _internalResources;
Expand All @@ -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);
}

/// <summary>
/// Get or set current applied theme
/// </summary>
Expand Down

0 comments on commit 2ca03e7

Please sign in to comment.