Skip to content

Commit

Permalink
Merge pull request #222 from AvaloniaCommunity/supportAvaloniaTheming
Browse files Browse the repository at this point in the history
Support Avalonia RequestedTheme
SKProCH authored Apr 22, 2023
2 parents 0e7376a + ec51990 commit da78161
Showing 15 changed files with 717 additions and 313 deletions.
54 changes: 26 additions & 28 deletions Material.Demo/App.axaml
Original file line number Diff line number Diff line change
@@ -9,33 +9,31 @@
<system:Double x:Key="ControlContentThemeFontSize">14</system:Double>
</Application.Resources>
<Application.Styles>
<themes:MaterialTheme BaseTheme="Light"
PrimaryColor="Purple"
SecondaryColor="Indigo" />
<!-- <StyleInclude Source="avares://Material.Styles/Resources/Compatibility/Index.axaml"/> -->
<StyleInclude Source="avares://Material.Icons.Avalonia/App.xaml" />
<StyleInclude Source="avares://Material.DataGrid/DataGrid.xaml" />
<themes:MaterialTheme PrimaryColor="Purple" SecondaryColor="Indigo" />

<StyleInclude Source="avares://ShowMeTheXaml.Avalonia.AvaloniaEdit/XamlDisplayStyles.axaml" />

<!-- Patching XamlDisplay -->
<Style Selector="showMeTheXaml|XamlDisplay">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="VerticalAlignment" Value="Stretch"/>
<Setter Property="ClipToBounds" Value="False"/>
<Setter Property="Background" Value="Transparent"/>
</Style>

<Style Selector="showMeTheXaml|XamlDisplay /template/ Popup#XamlPopup > Border">
<Setter Property="CornerRadius" Value="4"/>
</Style>

<Style Selector="showMeTheXaml|XamlDisplay /template/ Popup#XamlPopup > Border > Grid">
<Setter Property="Background" Value="{DynamicResource MaterialDesignCardBackground }"/>
</Style>

<Style Selector="showMeTheXaml|XamlDisplay /template/ Popup#XamlPopup > Border > Grid > Button">
<Setter Property="Theme" Value="{StaticResource FlatButton}"/>
</Style>
</Application.Styles>
<StyleInclude Source="avares://Material.Icons.Avalonia/App.xaml" />
<StyleInclude Source="avares://Material.DataGrid/DataGrid.xaml" />

<StyleInclude Source="avares://ShowMeTheXaml.Avalonia.AvaloniaEdit/XamlDisplayStyles.axaml" />

<!-- Patching XamlDisplay -->
<Style Selector="showMeTheXaml|XamlDisplay">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="ClipToBounds" Value="False" />
<Setter Property="Background" Value="Transparent" />
</Style>

<Style Selector="showMeTheXaml|XamlDisplay /template/ Popup#XamlPopup > Border">
<Setter Property="CornerRadius" Value="4" />
</Style>

<Style Selector="showMeTheXaml|XamlDisplay /template/ Popup#XamlPopup > Border > Grid">
<Setter Property="Background" Value="{DynamicResource MaterialDesignCardBackground }" />
</Style>

<Style Selector="showMeTheXaml|XamlDisplay /template/ Popup#XamlPopup > Border > Grid > Button">
<Setter Property="Theme" Value="{StaticResource FlatButton}" />
</Style>
</Application.Styles>
</Application>
2 changes: 1 addition & 1 deletion Material.Styles/Controls/ColorZone.cs
Original file line number Diff line number Diff line change
@@ -49,7 +49,7 @@ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
DisposeSubscription();

var resources = Application.Current!.LocateMaterialTheme<MaterialTheme>();
_subscription = resources.ThemeChangedObservable.Subscribe(OnThemeChanged);
_subscription = resources.ThemeChangedEndObservable.Subscribe(OnThemeChanged);

base.OnAttachedToVisualTree(e);
}
1 change: 1 addition & 0 deletions Material.Styles/MaterialToolKit.xaml
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@
<ResourceInclude Source="avares://Material.Styles/Resources/Themes/TextBlock.axaml" />
<ResourceInclude Source="avares://Material.Styles/Resources/Themes/DataValidationErrors.axaml" />
<ResourceInclude Source="avares://Material.Styles/Resources/Themes/MaterialUnderline.axaml" />
<ResourceInclude Source="avares://Material.Styles/Resources/Themes/ThemeVariantScope.axaml" />

<!-- Seems like we cant reference something from other projects... -->
<!-- I think what we need to move ripple to this project -->
7 changes: 7 additions & 0 deletions Material.Styles/Resources/Themes/ThemeVariantScope.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ControlTheme x:Key="{x:Type ThemeVariantScope}"
TargetType="ThemeVariantScope">
<Setter Property="TextElement.Foreground" Value="{DynamicResource MaterialDesignBody}" />
</ControlTheme>
</ResourceDictionary>
4 changes: 2 additions & 2 deletions Material.Styles/Resources/Themes/Window.axaml
Original file line number Diff line number Diff line change
@@ -3,9 +3,9 @@
<ControlTheme x:Key="{x:Type Window}"
TargetType="Window">
<Setter Property="FontSize" Value="14" />
<Setter Property="Foreground" Value="{DynamicResource MaterialDesignBody}" />
<Setter Property="Background" Value="{DynamicResource MaterialDesignPaper}" />
<Setter Property="TextBox.FontWeight" Value="Regular" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource MaterialDesignBody}" />
<Setter Property="TextElement.FontWeight" Value="Regular" />
<Setter Property="Template">
<ControlTemplate>
<Panel>
68 changes: 34 additions & 34 deletions Material.Styles/Themes/Base/MaterialDesignDarkTheme.cs
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
using Avalonia.Media;

namespace Material.Styles.Themes.Base
{
public class MaterialDesignDarkTheme : IBaseTheme
{
public Color ValidationErrorColor { get; } = Color.Parse("#f44336");
public Color MaterialDesignBackground { get; } = Color.Parse("#FF000000");
public Color MaterialDesignPaper { get; } = Color.Parse("#FF303030");
public Color MaterialDesignCardBackground { get; } = Color.Parse("#FF424242");
public Color MaterialDesignToolBarBackground { get; } = Color.Parse("#FF212121");
public Color MaterialDesignBody { get; } = Color.Parse("#DDFFFFFF");
public Color MaterialDesignBodyLight { get; } = Color.Parse("#89FFFFFF");
public Color MaterialDesignColumnHeader { get; } = Color.Parse("#BCFFFFFF");
public Color MaterialDesignCheckBoxOff { get; } = Color.Parse("#89FFFFFF");
public Color MaterialDesignCheckBoxDisabled { get; } = Color.Parse("#FF647076");
public Color MaterialDesignTextBoxBorder { get; } = Color.Parse("#89FFFFFF");
public Color MaterialDesignDivider { get; } = Color.Parse("#1FFFFFFF");
public Color MaterialDesignSelection { get; } = Color.Parse("#757575");
public Color MaterialDesignToolForeground { get; } = Color.Parse("#FF616161");
public Color MaterialDesignToolBackground { get; } = Color.Parse("#FFe0e0e0");
public Color MaterialDesignFlatButtonClick { get; } = Color.Parse("#19757575");
public Color MaterialDesignFlatButtonRipple { get; } = Color.Parse("#FFB6B6B6");
public Color MaterialDesignToolTipBackground { get; } = Color.Parse("#eeeeee");
public Color MaterialDesignChipBackground { get; } = Color.Parse("#FF2E3C43");
public Color MaterialDesignSnackbarBackground { get; } = Color.Parse("#FFCDCDCD");
public Color MaterialDesignSnackbarMouseOver { get; } = Color.Parse("#FFB9B9BD");
public Color MaterialDesignSnackbarRipple { get; } = Color.Parse("#FF494949");
public Color MaterialDesignTextFieldBoxBackground { get; } = Color.Parse("#1AFFFFFF");
public Color MaterialDesignTextFieldBoxHoverBackground { get; } = Color.Parse("#1FFFFFFF");
public Color MaterialDesignTextFieldBoxDisabledBackground { get; } = Color.Parse("#0DFFFFFF");
public Color MaterialDesignTextAreaBorder { get; } = Color.Parse("#BCFFFFFF");
public Color MaterialDesignTextAreaInactiveBorder { get; } = Color.Parse("#1AFFFFFF");
public Color MaterialDesignDataGridRowHoverBackground { get; } = Color.Parse("#14FFFFFF");
}
}
namespace Material.Styles.Themes.Base;

internal sealed class MaterialDesignDarkTheme : IBaseTheme {
public static IBaseTheme Instance { get; } = new MaterialDesignDarkTheme();

public Color ValidationErrorColor { get; } = Color.Parse("#f44336");
public Color MaterialDesignBackground { get; } = Color.Parse("#FF000000");
public Color MaterialDesignPaper { get; } = Color.Parse("#FF303030");
public Color MaterialDesignCardBackground { get; } = Color.Parse("#FF424242");
public Color MaterialDesignToolBarBackground { get; } = Color.Parse("#FF212121");
public Color MaterialDesignBody { get; } = Color.Parse("#DDFFFFFF");
public Color MaterialDesignBodyLight { get; } = Color.Parse("#89FFFFFF");
public Color MaterialDesignColumnHeader { get; } = Color.Parse("#BCFFFFFF");
public Color MaterialDesignCheckBoxOff { get; } = Color.Parse("#89FFFFFF");
public Color MaterialDesignCheckBoxDisabled { get; } = Color.Parse("#FF647076");
public Color MaterialDesignTextBoxBorder { get; } = Color.Parse("#89FFFFFF");
public Color MaterialDesignDivider { get; } = Color.Parse("#1FFFFFFF");
public Color MaterialDesignSelection { get; } = Color.Parse("#757575");
public Color MaterialDesignToolForeground { get; } = Color.Parse("#FF616161");
public Color MaterialDesignToolBackground { get; } = Color.Parse("#FFe0e0e0");
public Color MaterialDesignFlatButtonClick { get; } = Color.Parse("#19757575");
public Color MaterialDesignFlatButtonRipple { get; } = Color.Parse("#FFB6B6B6");
public Color MaterialDesignToolTipBackground { get; } = Color.Parse("#eeeeee");
public Color MaterialDesignChipBackground { get; } = Color.Parse("#FF2E3C43");
public Color MaterialDesignSnackbarBackground { get; } = Color.Parse("#FFCDCDCD");
public Color MaterialDesignSnackbarMouseOver { get; } = Color.Parse("#FFB9B9BD");
public Color MaterialDesignSnackbarRipple { get; } = Color.Parse("#FF494949");
public Color MaterialDesignTextFieldBoxBackground { get; } = Color.Parse("#1AFFFFFF");
public Color MaterialDesignTextFieldBoxHoverBackground { get; } = Color.Parse("#1FFFFFFF");
public Color MaterialDesignTextFieldBoxDisabledBackground { get; } = Color.Parse("#0DFFFFFF");
public Color MaterialDesignTextAreaBorder { get; } = Color.Parse("#BCFFFFFF");
public Color MaterialDesignTextAreaInactiveBorder { get; } = Color.Parse("#1AFFFFFF");
public Color MaterialDesignDataGridRowHoverBackground { get; } = Color.Parse("#14FFFFFF");
}
68 changes: 34 additions & 34 deletions Material.Styles/Themes/Base/MaterialDesignLightTheme.cs
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
using Avalonia.Media;

namespace Material.Styles.Themes.Base
{
public class MaterialDesignLightTheme : IBaseTheme
{
public Color ValidationErrorColor { get; } = Color.Parse("#F44336");
public Color MaterialDesignBackground { get; } = Color.Parse("#FFFFFFFF");
public Color MaterialDesignPaper { get; } = Color.Parse("#FFFAFAFA");
public Color MaterialDesignCardBackground { get; } = Color.Parse("#FFFFFFFF");
public Color MaterialDesignToolBarBackground { get; } = Color.Parse("#FFF5F5F5");
public Color MaterialDesignBody { get; } = Color.Parse("#DD000000");
public Color MaterialDesignBodyLight { get; } = Color.Parse("#89000000");
public Color MaterialDesignColumnHeader { get; } = Color.Parse("#BC000000");
public Color MaterialDesignCheckBoxOff { get; } = Color.Parse("#89000000");
public Color MaterialDesignCheckBoxDisabled { get; } = Color.Parse("#FFBDBDBD");
public Color MaterialDesignTextBoxBorder { get; } = Color.Parse("#89000000");
public Color MaterialDesignDivider { get; } = Color.Parse("#1F000000");
public Color MaterialDesignSelection { get; } = Color.Parse("#FFDEDEDE");
public Color MaterialDesignToolForeground { get; } = Color.Parse("#FF616161");
public Color MaterialDesignToolBackground { get; } = Color.Parse("#FFE0E0E0");
public Color MaterialDesignFlatButtonClick { get; } = Color.Parse("#FFDEDEDE");
public Color MaterialDesignFlatButtonRipple { get; } = Color.Parse("#FFB6B6B6");
public Color MaterialDesignToolTipBackground { get; } = Color.Parse("#757575");
public Color MaterialDesignChipBackground { get; } = Color.Parse("#12000000");
public Color MaterialDesignSnackbarBackground { get; } = Color.Parse("#FF323232");
public Color MaterialDesignSnackbarMouseOver { get; } = Color.Parse("#FF464642");
public Color MaterialDesignSnackbarRipple { get; } = Color.Parse("#FFB6B6B6");
public Color MaterialDesignTextFieldBoxBackground { get; } = Color.Parse("#0F000000");
public Color MaterialDesignTextFieldBoxHoverBackground { get; } = Color.Parse("#14000000");
public Color MaterialDesignTextFieldBoxDisabledBackground { get; } = Color.Parse("#08000000");
public Color MaterialDesignTextAreaBorder { get; } = Color.Parse("#BC000000");
public Color MaterialDesignTextAreaInactiveBorder { get; } = Color.Parse("#0F000000");
public Color MaterialDesignDataGridRowHoverBackground { get; } = Color.Parse("#0A000000");
}
}
namespace Material.Styles.Themes.Base;

internal sealed class MaterialDesignLightTheme : IBaseTheme {
public static IBaseTheme Instance { get; } = new MaterialDesignLightTheme();

public Color ValidationErrorColor { get; } = Color.Parse("#F44336");
public Color MaterialDesignBackground { get; } = Color.Parse("#FFFFFFFF");
public Color MaterialDesignPaper { get; } = Color.Parse("#FFFAFAFA");
public Color MaterialDesignCardBackground { get; } = Color.Parse("#FFFFFFFF");
public Color MaterialDesignToolBarBackground { get; } = Color.Parse("#FFF5F5F5");
public Color MaterialDesignBody { get; } = Color.Parse("#DD000000");
public Color MaterialDesignBodyLight { get; } = Color.Parse("#89000000");
public Color MaterialDesignColumnHeader { get; } = Color.Parse("#BC000000");
public Color MaterialDesignCheckBoxOff { get; } = Color.Parse("#89000000");
public Color MaterialDesignCheckBoxDisabled { get; } = Color.Parse("#FFBDBDBD");
public Color MaterialDesignTextBoxBorder { get; } = Color.Parse("#89000000");
public Color MaterialDesignDivider { get; } = Color.Parse("#1F000000");
public Color MaterialDesignSelection { get; } = Color.Parse("#FFDEDEDE");
public Color MaterialDesignToolForeground { get; } = Color.Parse("#FF616161");
public Color MaterialDesignToolBackground { get; } = Color.Parse("#FFE0E0E0");
public Color MaterialDesignFlatButtonClick { get; } = Color.Parse("#FFDEDEDE");
public Color MaterialDesignFlatButtonRipple { get; } = Color.Parse("#FFB6B6B6");
public Color MaterialDesignToolTipBackground { get; } = Color.Parse("#757575");
public Color MaterialDesignChipBackground { get; } = Color.Parse("#12000000");
public Color MaterialDesignSnackbarBackground { get; } = Color.Parse("#FF323232");
public Color MaterialDesignSnackbarMouseOver { get; } = Color.Parse("#FF464642");
public Color MaterialDesignSnackbarRipple { get; } = Color.Parse("#FFB6B6B6");
public Color MaterialDesignTextFieldBoxBackground { get; } = Color.Parse("#0F000000");
public Color MaterialDesignTextFieldBoxHoverBackground { get; } = Color.Parse("#14000000");
public Color MaterialDesignTextFieldBoxDisabledBackground { get; } = Color.Parse("#08000000");
public Color MaterialDesignTextAreaBorder { get; } = Color.Parse("#BC000000");
public Color MaterialDesignTextAreaInactiveBorder { get; } = Color.Parse("#0F000000");
public Color MaterialDesignDataGridRowHoverBackground { get; } = Color.Parse("#0A000000");
}
149 changes: 149 additions & 0 deletions Material.Styles/Themes/Base/MaterialPredefinedBaseColors.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:themes="clr-namespace:Material.Styles.Themes">
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="{x:Static themes:Theme.MaterialLight}">
<Color x:Key="ValidationErrorColor">#F44336</Color>
<Color x:Key="MaterialDesignBackground">#FFFFFFFF</Color>
<Color x:Key="MaterialDesignPaper">#FFFAFAFA</Color>
<Color x:Key="MaterialDesignCardBackground">#FFFFFFFF</Color>
<Color x:Key="MaterialDesignToolBarBackground">#FFF5F5F5</Color>
<Color x:Key="MaterialDesignBody">#DD000000</Color>
<Color x:Key="MaterialDesignBodyLight">#89000000</Color>
<Color x:Key="MaterialDesignColumnHeader">#BC000000</Color>
<Color x:Key="MaterialDesignCheckBoxOff">#89000000</Color>
<Color x:Key="MaterialDesignCheckBoxDisabled">#FFBDBDBD</Color>
<Color x:Key="MaterialDesignTextBoxBorder">#89000000</Color>
<Color x:Key="MaterialDesignDivider">#1F000000</Color>
<Color x:Key="MaterialDesignSelection">#FFDEDEDE</Color>
<Color x:Key="MaterialDesignToolForeground">#FF616161</Color>
<Color x:Key="MaterialDesignToolBackground">#FFE0E0E0</Color>
<Color x:Key="MaterialDesignFlatButtonClick">#FFDEDEDE</Color>
<Color x:Key="MaterialDesignFlatButtonRipple">#FFB6B6B6</Color>
<Color x:Key="MaterialDesignToolTipBackground">#757575</Color>
<Color x:Key="MaterialDesignChipBackground">#12000000</Color>
<Color x:Key="MaterialDesignSnackbarBackground">#FF323232</Color>
<Color x:Key="MaterialDesignSnackbarMouseOver">#FF464642</Color>
<Color x:Key="MaterialDesignSnackbarRipple">#FFB6B6B6</Color>
<Color x:Key="MaterialDesignTextFieldBoxBackground">#0F000000</Color>
<Color x:Key="MaterialDesignTextFieldBoxHoverBackground">#14000000</Color>
<Color x:Key="MaterialDesignTextFieldBoxDisabledBackground">#08000000</Color>
<Color x:Key="MaterialDesignTextAreaBorder">#BC000000</Color>
<Color x:Key="MaterialDesignTextAreaInactiveBorder">#0F000000</Color>
<Color x:Key="MaterialDesignDataGridRowHoverBackground">#0A000000</Color>

<SolidColorBrush x:Key="ValidationErrorColorBrush" Color="{StaticResource ValidationErrorColor}" />
<SolidColorBrush x:Key="MaterialDesignBackgroundBrush" Color="{StaticResource MaterialDesignBackground}" />
<SolidColorBrush x:Key="MaterialDesignPaperBrush" Color="{StaticResource MaterialDesignPaper}" />
<SolidColorBrush x:Key="MaterialDesignCardBackgroundBrush" Color="{StaticResource MaterialDesignCardBackground}" />
<SolidColorBrush x:Key="MaterialDesignToolBarBackgroundBrush"
Color="{StaticResource MaterialDesignToolBarBackground}" />
<SolidColorBrush x:Key="MaterialDesignBodyBrush" Color="{StaticResource MaterialDesignBody}" />
<SolidColorBrush x:Key="MaterialDesignBodyLightBrush" Color="{StaticResource MaterialDesignBodyLight}" />
<SolidColorBrush x:Key="MaterialDesignColumnHeaderBrush" Color="{StaticResource MaterialDesignColumnHeader}" />
<SolidColorBrush x:Key="MaterialDesignCheckBoxOffBrush" Color="{StaticResource MaterialDesignCheckBoxOff}" />
<SolidColorBrush x:Key="MaterialDesignCheckBoxDisabledBrush"
Color="{StaticResource MaterialDesignCheckBoxDisabled}" />
<SolidColorBrush x:Key="MaterialDesignTextBoxBorderBrush" Color="{StaticResource MaterialDesignTextBoxBorder}" />
<SolidColorBrush x:Key="MaterialDesignDividerBrush" Color="{StaticResource MaterialDesignDivider}" />
<SolidColorBrush x:Key="MaterialDesignSelectionBrush" Color="{StaticResource MaterialDesignSelection}" />
<SolidColorBrush x:Key="MaterialDesignToolForegroundBrush" Color="{StaticResource MaterialDesignToolForeground}" />
<SolidColorBrush x:Key="MaterialDesignToolBackgroundBrush" Color="{StaticResource MaterialDesignToolBackground}" />
<SolidColorBrush x:Key="MaterialDesignFlatButtonClickBrush"
Color="{StaticResource MaterialDesignFlatButtonClick}" />
<SolidColorBrush x:Key="MaterialDesignFlatButtonRippleBrush"
Color="{StaticResource MaterialDesignFlatButtonRipple}" />
<SolidColorBrush x:Key="MaterialDesignToolTipBackgroundBrush"
Color="{StaticResource MaterialDesignToolTipBackground}" />
<SolidColorBrush x:Key="MaterialDesignChipBackgroundBrush" Color="{StaticResource MaterialDesignChipBackground}" />
<SolidColorBrush x:Key="MaterialDesignSnackbarBackgroundBrush"
Color="{StaticResource MaterialDesignSnackbarBackground}" />
<SolidColorBrush x:Key="MaterialDesignSnackbarMouseOverBrush"
Color="{StaticResource MaterialDesignSnackbarMouseOver}" />
<SolidColorBrush x:Key="MaterialDesignSnackbarRippleBrush" Color="{StaticResource MaterialDesignSnackbarRipple}" />
<SolidColorBrush x:Key="MaterialDesignTextFieldBoxBackgroundBrush"
Color="{StaticResource MaterialDesignTextFieldBoxBackground}" />
<SolidColorBrush x:Key="MaterialDesignTextFieldBoxHoverBackgroundBrush"
Color="{StaticResource MaterialDesignTextFieldBoxHoverBackground}" />
<SolidColorBrush x:Key="MaterialDesignTextFieldBoxDisabledBackgroundBrush"
Color="{StaticResource MaterialDesignTextFieldBoxDisabledBackground}" />
<SolidColorBrush x:Key="MaterialDesignTextAreaBorderBrush" Color="{StaticResource MaterialDesignTextAreaBorder}" />
<SolidColorBrush x:Key="MaterialDesignTextAreaInactiveBorderBrush"
Color="{StaticResource MaterialDesignTextAreaInactiveBorder}" />
<SolidColorBrush x:Key="MaterialDesignDataGridRowHoverBackgroundBrush"
Color="{StaticResource MaterialDesignDataGridRowHoverBackground}" />
</ResourceDictionary>

<ResourceDictionary x:Key="{x:Static themes:Theme.MaterialDark}">
<Color x:Key="ValidationErrorColor">#f44336</Color>
<Color x:Key="MaterialDesignBackground">#FF000000</Color>
<Color x:Key="MaterialDesignPaper">#FF303030</Color>
<Color x:Key="MaterialDesignCardBackground">#FF424242</Color>
<Color x:Key="MaterialDesignToolBarBackground">#FF212121</Color>
<Color x:Key="MaterialDesignBody">#DDFFFFFF</Color>
<Color x:Key="MaterialDesignBodyLight">#89FFFFFF</Color>
<Color x:Key="MaterialDesignColumnHeader">#BCFFFFFF</Color>
<Color x:Key="MaterialDesignCheckBoxOff">#89FFFFFF</Color>
<Color x:Key="MaterialDesignCheckBoxDisabled">#FF647076</Color>
<Color x:Key="MaterialDesignTextBoxBorder">#89FFFFFF</Color>
<Color x:Key="MaterialDesignDivider">#1FFFFFFF</Color>
<Color x:Key="MaterialDesignSelection">#757575</Color>
<Color x:Key="MaterialDesignToolForeground">#FF616161</Color>
<Color x:Key="MaterialDesignToolBackground">#FFe0e0e0</Color>
<Color x:Key="MaterialDesignFlatButtonClick">#19757575</Color>
<Color x:Key="MaterialDesignFlatButtonRipple">#FFB6B6B6</Color>
<Color x:Key="MaterialDesignToolTipBackground">#eeeeee</Color>
<Color x:Key="MaterialDesignChipBackground">#FF2E3C43</Color>
<Color x:Key="MaterialDesignSnackbarBackground">#FFCDCDCD</Color>
<Color x:Key="MaterialDesignSnackbarMouseOver">#FFB9B9BD</Color>
<Color x:Key="MaterialDesignSnackbarRipple">#FF494949</Color>
<Color x:Key="MaterialDesignTextFieldBoxBackground">#1AFFFFFF</Color>
<Color x:Key="MaterialDesignTextFieldBoxHoverBackground">#1FFFFFFF</Color>
<Color x:Key="MaterialDesignTextFieldBoxDisabledBackground">#0DFFFFFF</Color>
<Color x:Key="MaterialDesignTextAreaBorder">#BCFFFFFF</Color>
<Color x:Key="MaterialDesignTextAreaInactiveBorder">#1AFFFFFF</Color>
<Color x:Key="MaterialDesignDataGridRowHoverBackground">#14FFFFFF</Color>

<SolidColorBrush x:Key="ValidationErrorColorBrush" Color="{StaticResource ValidationErrorColor}" />
<SolidColorBrush x:Key="MaterialDesignBackgroundBrush" Color="{StaticResource MaterialDesignBackground}" />
<SolidColorBrush x:Key="MaterialDesignPaperBrush" Color="{StaticResource MaterialDesignPaper}" />
<SolidColorBrush x:Key="MaterialDesignCardBackgroundBrush" Color="{StaticResource MaterialDesignCardBackground}" />
<SolidColorBrush x:Key="MaterialDesignToolBarBackgroundBrush"
Color="{StaticResource MaterialDesignToolBarBackground}" />
<SolidColorBrush x:Key="MaterialDesignBodyBrush" Color="{StaticResource MaterialDesignBody}" />
<SolidColorBrush x:Key="MaterialDesignBodyLightBrush" Color="{StaticResource MaterialDesignBodyLight}" />
<SolidColorBrush x:Key="MaterialDesignColumnHeaderBrush" Color="{StaticResource MaterialDesignColumnHeader}" />
<SolidColorBrush x:Key="MaterialDesignCheckBoxOffBrush" Color="{StaticResource MaterialDesignCheckBoxOff}" />
<SolidColorBrush x:Key="MaterialDesignCheckBoxDisabledBrush"
Color="{StaticResource MaterialDesignCheckBoxDisabled}" />
<SolidColorBrush x:Key="MaterialDesignTextBoxBorderBrush" Color="{StaticResource MaterialDesignTextBoxBorder}" />
<SolidColorBrush x:Key="MaterialDesignDividerBrush" Color="{StaticResource MaterialDesignDivider}" />
<SolidColorBrush x:Key="MaterialDesignSelectionBrush" Color="{StaticResource MaterialDesignSelection}" />
<SolidColorBrush x:Key="MaterialDesignToolForegroundBrush" Color="{StaticResource MaterialDesignToolForeground}" />
<SolidColorBrush x:Key="MaterialDesignToolBackgroundBrush" Color="{StaticResource MaterialDesignToolBackground}" />
<SolidColorBrush x:Key="MaterialDesignFlatButtonClickBrush"
Color="{StaticResource MaterialDesignFlatButtonClick}" />
<SolidColorBrush x:Key="MaterialDesignFlatButtonRippleBrush"
Color="{StaticResource MaterialDesignFlatButtonRipple}" />
<SolidColorBrush x:Key="MaterialDesignToolTipBackgroundBrush"
Color="{StaticResource MaterialDesignToolTipBackground}" />
<SolidColorBrush x:Key="MaterialDesignChipBackgroundBrush" Color="{StaticResource MaterialDesignChipBackground}" />
<SolidColorBrush x:Key="MaterialDesignSnackbarBackgroundBrush"
Color="{StaticResource MaterialDesignSnackbarBackground}" />
<SolidColorBrush x:Key="MaterialDesignSnackbarMouseOverBrush"
Color="{StaticResource MaterialDesignSnackbarMouseOver}" />
<SolidColorBrush x:Key="MaterialDesignSnackbarRippleBrush" Color="{StaticResource MaterialDesignSnackbarRipple}" />
<SolidColorBrush x:Key="MaterialDesignTextFieldBoxBackgroundBrush"
Color="{StaticResource MaterialDesignTextFieldBoxBackground}" />
<SolidColorBrush x:Key="MaterialDesignTextFieldBoxHoverBackgroundBrush"
Color="{StaticResource MaterialDesignTextFieldBoxHoverBackground}" />
<SolidColorBrush x:Key="MaterialDesignTextFieldBoxDisabledBackgroundBrush"
Color="{StaticResource MaterialDesignTextFieldBoxDisabledBackground}" />
<SolidColorBrush x:Key="MaterialDesignTextAreaBorderBrush" Color="{StaticResource MaterialDesignTextAreaBorder}" />
<SolidColorBrush x:Key="MaterialDesignTextAreaInactiveBorderBrush"
Color="{StaticResource MaterialDesignTextAreaInactiveBorder}" />
<SolidColorBrush x:Key="MaterialDesignDataGridRowHoverBackgroundBrush"
Color="{StaticResource MaterialDesignDataGridRowHoverBackground}" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
279 changes: 279 additions & 0 deletions Material.Styles/Themes/InternalStylesCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Styling;

namespace Material.Styles.Themes;

/// <remarks>
/// Copy of <see cref="Styles"/>, with exposing first resources lookup to inheritors
/// </remarks>
public abstract class InternalStylesCollection : AvaloniaObject,
IAvaloniaList<IStyle>,
IStyle,
IResourceProvider {
private readonly AvaloniaList<IStyle> _styles = new();
private bool _isResourcedAccessed;
private IResourceHost? _owner;
private IResourceDictionary? _resources;

public InternalStylesCollection() {
_styles.ResetBehavior = ResetBehavior.Remove;
_styles.CollectionChanged += OnCollectionChanged;
_styles.Validate = i => {
if (i is ControlTheme)
throw new InvalidOperationException("ControlThemes cannot be added to a Styles collection.");
};
}

public InternalStylesCollection(IResourceHost owner)
: this() {
Owner = owner;
}

/// <summary>
/// Gets or sets a dictionary of style resources.
/// </summary>
public IResourceDictionary Resources {
get {
var resources = _resources ?? (Resources = new ResourceDictionary());
EnsureCallToOnResourcedAccessed();
return resources;
}
set {
value = value ?? throw new ArgumentNullException(nameof(Resources));

var currentOwner = Owner;

if (currentOwner is not null) _resources?.RemoveOwner(currentOwner);

_resources = value;

if (currentOwner is not null) _resources.AddOwner(currentOwner);
}
}

public event NotifyCollectionChangedEventHandler? CollectionChanged;

public int Count => _styles.Count;

bool ICollection<IStyle>.IsReadOnly => false;

IStyle IReadOnlyList<IStyle>.this[int index] => _styles[index];

public IStyle this[int index] {
get => _styles[index];
set => _styles[index] = value;
}

/// <inheritdoc/>
public void AddRange(IEnumerable<IStyle> items) {
_styles.AddRange(items);
}

/// <inheritdoc/>
public void InsertRange(int index, IEnumerable<IStyle> items) {
_styles.InsertRange(index, items);
}

/// <inheritdoc/>
public void Move(int oldIndex, int newIndex) {
_styles.Move(oldIndex, newIndex);
}

/// <inheritdoc/>
public void MoveRange(int oldIndex, int count, int newIndex) {
_styles.MoveRange(oldIndex, count, newIndex);
}

/// <inheritdoc/>
public void RemoveAll(IEnumerable<IStyle> items) {
_styles.RemoveAll(items);
}

/// <inheritdoc/>
public void RemoveRange(int index, int count) {
_styles.RemoveRange(index, count);
}

/// <inheritdoc/>
public int IndexOf(IStyle item) {
return _styles.IndexOf(item);
}

/// <inheritdoc/>
public void Insert(int index, IStyle item) {
_styles.Insert(index, item);
}

/// <inheritdoc/>
public void RemoveAt(int index) {
_styles.RemoveAt(index);
}

/// <inheritdoc/>
public void Add(IStyle item) {
_styles.Add(item);
}

/// <inheritdoc/>
public void Clear() {
_styles.Clear();
}

/// <inheritdoc/>
public bool Contains(IStyle item) {
return _styles.Contains(item);
}

/// <inheritdoc/>
public void CopyTo(IStyle[] array, int arrayIndex) {
_styles.CopyTo(array, arrayIndex);
}

/// <inheritdoc/>
public bool Remove(IStyle item) {
return _styles.Remove(item);
}

/// <inheritdoc/>
IEnumerator<IStyle> IEnumerable<IStyle>.GetEnumerator() {
return _styles.GetEnumerator();
}

/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() {
return _styles.GetEnumerator();
}
public event EventHandler? OwnerChanged;

public IResourceHost? Owner {
get => _owner;
private set {
if (_owner != value) {
_owner = value;
OwnerChanged?.Invoke(this, EventArgs.Empty);
}
}
}

/// <inheritdoc/>
void IResourceProvider.AddOwner(IResourceHost owner) {
owner = owner ?? throw new ArgumentNullException(nameof(owner));

if (Owner is not null) throw new InvalidOperationException("The Styles already has a owner.");

Owner = owner;
_resources?.AddOwner(owner);

foreach (var child in this)
if (child is IResourceProvider r)
r.AddOwner(owner);
}

/// <inheritdoc/>
void IResourceProvider.RemoveOwner(IResourceHost owner) {
owner = owner ?? throw new ArgumentNullException(nameof(owner));

if (Owner == owner) {
Owner = null;
_resources?.RemoveOwner(owner);

foreach (var child in this)
if (child is IResourceProvider r)
r.RemoveOwner(owner);
}
}

bool IResourceNode.HasResources {
get {
if (_resources?.Count > 0) return true;

foreach (var i in this)
if (i is IResourceProvider { HasResources: true })
return true;

return false;
}
}

IReadOnlyList<IStyle> IStyle.Children => this;

/// <inheritdoc/>
public bool TryGetResource(object key, ThemeVariant? theme, out object? value) {
EnsureCallToOnResourcedAccessed();
if (_resources != null && _resources.TryGetResource(key, theme, out value)) return true;

for (var i = Count - 1; i >= 0; --i)
if (this[i].TryGetResource(key, theme, out value))
return true;

value = null;
return false;
}

private void EnsureCallToOnResourcedAccessed() {
if (_isResourcedAccessed) return;
_isResourcedAccessed = true;
OnResourcedAccessed();
}

protected abstract void OnResourcedAccessed();

public AvaloniaList<IStyle>.Enumerator GetEnumerator() {
return _styles.GetEnumerator();
}

private static IReadOnlyList<T> ToReadOnlyList<T>(ICollection list) {
if (list is IReadOnlyList<T> readOnlyList) return readOnlyList;

var result = new T[list.Count];
list.CopyTo(result, 0);
return result;
}

private static void InternalAdd(IList items, IResourceHost? owner) {
if (owner is not null) {
for (var i = 0; i < items.Count; ++i)
if (items[i] is IResourceProvider provider)
provider.AddOwner(owner);

(owner as IStyleHost)?.StylesAdded(ToReadOnlyList<IStyle>(items));
}
}

private static void InternalRemove(IList items, IResourceHost? owner) {
if (owner is not null) {
for (var i = 0; i < items.Count; ++i)
if (items[i] is IResourceProvider provider)
provider.RemoveOwner(owner);

(owner as IStyleHost)?.StylesRemoved(ToReadOnlyList<IStyle>(items));
}
}

private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) {
if (e.Action == NotifyCollectionChangedAction.Reset) throw new InvalidOperationException("Reset should not be called on Styles.");

var currentOwner = Owner;

switch (e.Action) {
case NotifyCollectionChangedAction.Add:
InternalAdd(e.NewItems!, currentOwner);
break;
case NotifyCollectionChangedAction.Remove:
InternalRemove(e.OldItems!, currentOwner);
break;
case NotifyCollectionChangedAction.Replace:
InternalRemove(e.OldItems!, currentOwner);
InternalAdd(e.NewItems!, currentOwner);
break;
}

CollectionChanged?.Invoke(this, e);
}
}
126 changes: 84 additions & 42 deletions Material.Styles/Themes/MaterialTheme.cs
Original file line number Diff line number Diff line change
@@ -2,47 +2,52 @@
using System.Reactive;
using System.Reactive.Linq;
using Avalonia;
using Avalonia.Styling;
using Avalonia.Threading;
using Material.Colors;
using Material.Styles.Themes.Base;

namespace Material.Styles.Themes
{
namespace Material.Styles.Themes {
/// <summary>
/// Applies the material theme styles and resources
/// </summary>
/// <remarks>
/// You need to setup all these properties: <see cref="BaseTheme"/>, <see cref="PrimaryColor"/>, <see cref="SecondaryColor"/>
/// </remarks>
public class MaterialTheme : MaterialThemeBase, IDisposable
{
private IDisposable _themeUpdaterDisposable = null!;
private ITheme _theme = new Theme();
public class MaterialTheme : MaterialThemeBase, IDisposable {
public static readonly StyledProperty<BaseThemeMode> BaseThemeProperty =
AvaloniaProperty.Register<MaterialTheme, BaseThemeMode>(nameof(BaseTheme));

/// <summary>
/// Initializes a new instance of the <see cref="MaterialTheme"/> class.
/// </summary>
/// <param name="baseUri">The base URL for the XAML context.</param>
public MaterialTheme(Uri baseUri) : base(baseUri)
=> Initialize();
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 IDisposable _themeUpdaterDisposable;

private BaseThemeMode _actualBaseTheme;
private IDisposable? _baseThemeChangeObservable;
private bool _isLoaded;
private ITheme _theme = new Theme();

/// <summary>
/// Initializes a new instance of the <see cref="MaterialTheme"/> class.
/// </summary>
/// <param name="serviceProvider">The XAML service provider.</param>
public MaterialTheme(IServiceProvider serviceProvider) : base(serviceProvider)
=> Initialize();

private void Initialize()
{
var baseThemeObservable = this.GetObservable(BaseThemeProperty)
public MaterialTheme(IServiceProvider serviceProvider) : base(serviceProvider) {
var baseThemeObservable = this.GetObservable(ActualBaseThemeProperty)
.Do(mode => _theme = _theme.SetBaseTheme(mode.GetBaseTheme()))
.Select(_ => Unit.Default);
var primaryColorObservable = this.GetObservable(PrimaryColorProperty)
.Do(color => _theme = _theme.SetPrimaryColor(SwatchHelper.Lookup[(MaterialColor) color]))
.Do(color => _theme = _theme.SetPrimaryColor(SwatchHelper.Lookup[(MaterialColor)color]))
.Select(_ => Unit.Default);
var secondaryColorObservable = this.GetObservable(SecondaryColorProperty)
.Do(color => _theme = _theme.SetSecondaryColor(SwatchHelper.Lookup[(MaterialColor) color]))
.Do(color => _theme = _theme.SetSecondaryColor(SwatchHelper.Lookup[(MaterialColor)color]))
.Select(_ => Unit.Default);

_themeUpdaterDisposable = baseThemeObservable
@@ -52,44 +57,81 @@ private void Initialize()
.Throttle(TimeSpan.FromMilliseconds(100))
.ObserveOn(new AvaloniaSynchronizationContext())
.Subscribe(_ => CurrentTheme = _theme);
}

private bool _isLoaded;
protected override ITheme? ProvideInitialTheme() {
_isLoaded = true;
return _theme;
OwnerChanged += OnOwnerChanged;
}

public static readonly StyledProperty<BaseThemeMode> BaseThemeProperty
= AvaloniaProperty.Register<MaterialTheme, BaseThemeMode>(nameof(BaseTheme));

public BaseThemeMode BaseTheme
{
public BaseThemeMode BaseTheme {
get => GetValue(BaseThemeProperty);
set => SetValue(BaseThemeProperty, value);
}

public static readonly StyledProperty<PrimaryColor> PrimaryColorProperty
= AvaloniaProperty.Register<MaterialTheme, PrimaryColor>(nameof(PrimaryColor));
public BaseThemeMode ActualBaseTheme {
get => _actualBaseTheme;
private set => SetAndRaise(ActualBaseThemeProperty, ref _actualBaseTheme, value);
}

public PrimaryColor PrimaryColor
{
public PrimaryColor PrimaryColor {
get => GetValue(PrimaryColorProperty);
set => SetValue(PrimaryColorProperty, value);
}

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

public SecondaryColor SecondaryColor
{
public SecondaryColor SecondaryColor {
get => GetValue(SecondaryColorProperty);
set => SetValue(SecondaryColorProperty, value);
}

public void Dispose()
{
public void Dispose() {
_themeUpdaterDisposable.Dispose();
}

private void OnOwnerChanged(object sender, EventArgs e) {
RegisterActualThemeObservable();
}

private void RegisterActualThemeObservable() {
_baseThemeChangeObservable?.Dispose();

var themeVariantHost = Owner as IThemeVariantHost;
var themeVariantObservable = themeVariantHost != null
? Observable.FromEvent<EventHandler, Unit>(action => (_, _) => { action(Unit.Default); },
handler => themeVariantHost.ActualThemeVariantChanged += handler,
handler => themeVariantHost.ActualThemeVariantChanged -= handler)
.Select(_ => Unit.Default)
: Observable.Empty<Unit>();

var targetBaseObservable = this.GetObservable(BaseThemeProperty)
.Select(_ => Unit.Default);

_baseThemeChangeObservable = Observable.Return(Unit.Default)
.Merge(themeVariantObservable)
.Merge(targetBaseObservable)
.Subscribe(_ => {
ActualBaseTheme = GetActualBaseTheme(BaseTheme, themeVariantHost?.ActualThemeVariant);
});
}

private BaseThemeMode GetActualBaseTheme(BaseThemeMode mode, ThemeVariant? variant) {
return mode switch {
BaseThemeMode.Inherit => GetBaseThemeFromVariant(variant) ?? BaseThemeMode.Light,
BaseThemeMode.Light => BaseThemeMode.Light,
BaseThemeMode.Dark => BaseThemeMode.Dark,
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null)
};

static BaseThemeMode? GetBaseThemeFromVariant(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;
}
}
}

protected override ITheme? ProvideInitialTheme() {
_isLoaded = true;
return _theme;
}
}
}
}
19 changes: 19 additions & 0 deletions Material.Styles/Themes/MaterialThemeBase.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<themes:MaterialThemeBase xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:themes="clr-namespace:Material.Styles.Themes"
x:Class="Material.Styles.Themes.MaterialThemeBase">
<themes:MaterialThemeBase.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<MergeResourceInclude Source="avares://Material.Styles/Themes/Base/MaterialPredefinedBaseColors.axaml" />
</ResourceDictionary.MergedDictionaries>
<ResourceDictionary.ThemeDictionaries>
<!-- This dictionary for changeable colors -->
<ResourceDictionary x:Key="Default" />
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</themes:MaterialThemeBase.Resources>

<StyleInclude Source="avares://Material.Styles/MaterialToolKit.xaml" />
<StyleInclude Source="avares://Material.Styles/Resources/Compatibility/Index.axaml" />
</themes:MaterialThemeBase>
123 changes: 41 additions & 82 deletions Material.Styles/Themes/MaterialThemeBase.cs
Original file line number Diff line number Diff line change
@@ -9,47 +9,32 @@
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.Styling;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.Threading;

namespace Material.Styles.Themes;

public class MaterialThemeBase : AvaloniaObject, IStyle, IResourceProvider {
public class MaterialThemeBase : InternalStylesCollection {
public static readonly DirectProperty<MaterialThemeBase, IReadOnlyTheme> CurrentThemeProperty =
AvaloniaProperty.RegisterDirect<MaterialThemeBase, IReadOnlyTheme>(
nameof(CurrentTheme),
o => o.CurrentTheme,
(o, v) => o.CurrentTheme = v);
private readonly IStyle _compabilityStyles;
private readonly IStyle _controlsStyles;

private CancellationTokenSource? _currentCancellationTokenSource;

private IReadOnlyTheme _currentTheme = new ReadOnlyTheme();
private Task? _currentThemeUpdateTask;
private bool _isLoading;
private IStyle? _loaded;

/// <summary>
/// Initializes a new instance of the <see cref="MaterialThemeBase"/> class.
/// </summary>
/// <param name="baseUri">The base URL for the XAML context.</param>
public MaterialThemeBase(Uri baseUri) {
_controlsStyles = new StyleInclude(baseUri) { Source = new Uri("avares://Material.Avalonia/Material.Avalonia.Templates.xaml") };
_compabilityStyles = new StyleInclude(baseUri) { Source = new Uri("avares://Material.Styles/Resources/Compatibility/Index.axaml") };
/// <param name="serviceProvider">The parent's service provider.</param>
public MaterialThemeBase(IServiceProvider? serviceProvider) {
AvaloniaXamlLoader.Load(serviceProvider, this);
}

/// <summary>
/// Initializes a new instance of the <see cref="MaterialThemeBase"/> class.
/// </summary>
/// <param name="serviceProvider">The XAML service provider.</param>
public MaterialThemeBase(IServiceProvider serviceProvider)
: this(((IUriContext)serviceProvider.GetService(typeof(IUriContext))).BaseUri) { }

private IResourceDictionary LoadedResourceDictionary => (_loaded as Avalonia.Styling.Styles)!.Resources;

/// <summary>
/// Get or set current applied theme
/// </summary>
@@ -64,51 +49,22 @@ public IReadOnlyTheme CurrentTheme {

_currentTheme = newTheme;
SetAndRaise(CurrentThemeProperty, ref _currentTheme, newTheme);
if (!_isLoading) StartUpdatingTheme(oldTheme, newTheme);
StartUpdatingTheme(oldTheme, newTheme);
}
}

public IObservable<IReadOnlyTheme> CurrentThemeChanged => this.GetObservable(CurrentThemeProperty);

public IObservable<MaterialThemeBase> ThemeChangedObservable =>
public IObservable<MaterialThemeBase> ThemeChangedEndObservable =>
Observable.FromEvent<EventHandler, MaterialThemeBase>(
conversion => delegate(object sender, EventArgs _) {
if (sender is not MaterialThemeBase theme)
return;

conversion(theme);
},
h => ThemeChanged += h,
h => ThemeChanged -= h);

/// <summary>
/// Gets the loaded style.
/// </summary>
public IStyle Loaded {
get {
if (_loaded != null)
return _loaded!;

_isLoading = true;

_loaded = new Avalonia.Styling.Styles { _controlsStyles, _compabilityStyles };

var initialTheme = ProvideInitialTheme();
if (initialTheme != null) {
UpdateSolidColorBrush(null, initialTheme, LoadedResourceDictionary, InvokeAndReturnTask).Wait();
CurrentTheme = initialTheme;
}

_isLoading = false;

return _loaded!;

Task InvokeAndReturnTask(Action action, DispatcherPriority _) {
action();
return Task.CompletedTask;
}
}
}
h => ThemeChangedEnd += h,
h => ThemeChangedEnd -= h);

private static IReadOnlyDictionary<string, Func<IReadOnlyTheme, Color>> UpdatableColors =>
new Dictionary<string, Func<IReadOnlyTheme, Color>> {
@@ -154,36 +110,10 @@ Task InvokeAndReturnTask(Action action, DispatcherPriority _) {
{ "MaterialDesignDataGridRowHoverBackground", theme => theme.DataGridRowHoverBackground },
};

public IResourceHost? Owner => (Loaded as IResourceProvider)?.Owner;

public event EventHandler? OwnerChanged {
add {
if (Loaded is IResourceProvider rp) {
rp.OwnerChanged += value;
}
}
remove {
if (Loaded is IResourceProvider rp) {
rp.OwnerChanged -= value;
}
}
}

void IResourceProvider.AddOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.AddOwner(owner);
void IResourceProvider.RemoveOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.RemoveOwner(owner);

/// <inheritdoc />
public bool TryGetResource(object key, ThemeVariant? theme, out object? value) {
return _loaded.TryGetResource(key, theme, out value);
}
bool IResourceNode.HasResources => (Loaded as IResourceProvider)?.HasResources ?? false;

IReadOnlyList<IStyle> IStyle.Children => _loaded?.Children ?? Array.Empty<IStyle>();

/// <summary>
/// This event is raised when all brushes is changed.
/// </summary>
public event EventHandler? ThemeChanged;
public event EventHandler? ThemeChangedEnd;

/// <summary>
/// This method will be called to get the theme that will be applied at the start of the application.
@@ -199,6 +129,19 @@ public bool TryGetResource(object key, ThemeVariant? theme, out object? value) {
return null;
}

/// <inheritdoc />
protected override void OnResourcedAccessed() {
var initialTheme = ProvideInitialTheme();
if (initialTheme != null) {
var newTheme = new ReadOnlyTheme(initialTheme);
var defaultThemeDictionary = (ResourceDictionary)Resources.ThemeDictionaries[ThemeVariant.Default];
UpdateSolidColorBrush(null, newTheme, defaultThemeDictionary, InvokeImmediately).Wait();
var oldTheme = _currentTheme;
_currentTheme = newTheme;
RaisePropertyChanged(CurrentThemeProperty, oldTheme, newTheme);
}
}

private void StartUpdatingTheme(IReadOnlyTheme oldTheme, IReadOnlyTheme newTheme) {
Task.Run(async () => {
_currentCancellationTokenSource?.Cancel();
@@ -209,13 +152,24 @@ private void StartUpdatingTheme(IReadOnlyTheme oldTheme, IReadOnlyTheme newTheme

if (_currentThemeUpdateTask != null) await _currentThemeUpdateTask;
if (!currentToken.IsCancellationRequested) {
var task = UpdateSolidColorBrush(oldTheme, newTheme, LoadedResourceDictionary,
Dispatcher.UIThread.InvokeAsync);
// If control is not attached to visual tree (is doesn't have Parent)
// And we inside a dispatcher thread (since it required for SolidColorBrush creation/changing)
// We can just invoke all color changes RIGHT NOW ON CURRENT THREAD
// -------------------------------------------------------------------
// If we already attached to something (e.g. theme was changed while app is running)
// We enqueue everything to dispatcher thread
// Cuz if we execute everything RIGHT NOW on dispatcher thread it will cause lag spike
// So we changing colors one by one
Func<Action, DispatcherPriority, Task> contextSync = Owner is null && Dispatcher.UIThread.CheckAccess()
? InvokeImmediately
: Dispatcher.UIThread.InvokeAsync;
var defaultThemeDictionary = (ResourceDictionary)Resources.ThemeDictionaries[ThemeVariant.Default];
var task = UpdateSolidColorBrush(oldTheme, newTheme, defaultThemeDictionary, contextSync);

_currentThemeUpdateTask = task;

await task.ContinueWith(delegate {
ThemeChanged?.Invoke(this, EventArgs.Empty);
ThemeChangedEnd?.Invoke(this, EventArgs.Empty);
}, CancellationToken.None);
}
});
@@ -244,4 +198,9 @@ Task CreateBrushAsync()
}, DispatcherPriority.Normal);
}
}

private Task InvokeImmediately(Action action, DispatcherPriority priority = default) {
action();
return Task.CompletedTask;
}
}
57 changes: 0 additions & 57 deletions Material.Styles/Themes/SystemThemeProbe.cs

This file was deleted.

33 changes: 26 additions & 7 deletions Material.Styles/Themes/Theme.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
using System;
using Avalonia.Media;
using Avalonia.Styling;
using Material.Colors;
using Material.Styles.Themes.Base;

namespace Material.Styles.Themes
{
public class Theme : ITheme
{
public static IBaseTheme Light { get; } = new MaterialDesignLightTheme();
public static IBaseTheme Dark { get; } = new MaterialDesignDarkTheme();
namespace Material.Styles.Themes {
/// <summary>
/// Specifies a set of colors that should be used for <see cref="MaterialThemeBase"/>
/// </summary>
public class Theme : ITheme {
/// <summary>
/// Provides a <see cref="BaseThemeMode.Light"/> set of base colors
/// </summary>
public static IBaseTheme Light { get; } = MaterialDesignLightTheme.Instance;

/// <summary>
/// Provides a <see cref="BaseThemeMode.Dark"/> set of base colors
/// </summary>
public static IBaseTheme Dark { get; } = MaterialDesignDarkTheme.Instance;

/// <summary>
/// Use the Light variant of colors
/// </summary>
public static ThemeVariant MaterialLight { get; } = new(nameof(MaterialLight), ThemeVariant.Light);

/// <summary>
/// Use the Dark variant of colors
/// </summary>
public static ThemeVariant MaterialDark { get; } = new(nameof(MaterialDark), ThemeVariant.Dark);

public ColorPair SecondaryLight { get; set; }
public ColorPair SecondaryMid { get; set; }
@@ -100,4 +119,4 @@ public static Theme Create(IReadOnlyTheme readOnlyTheme) {
return theme;
}
}
}
}
40 changes: 14 additions & 26 deletions Material.Styles/Themes/ThemeExtensions.cs
Original file line number Diff line number Diff line change
@@ -5,29 +5,23 @@
using Material.Colors.ColorManipulation;
using Material.Styles.Themes.Base;

namespace Material.Styles.Themes
{
public static class ThemeExtensions
{
public static T LocateMaterialTheme<T>(this Application application) where T : MaterialThemeBase
{
namespace Material.Styles.Themes {
public static class ThemeExtensions {
public static T LocateMaterialTheme<T>(this Application application) where T : MaterialThemeBase {
var materialTheme = application.Styles.FirstOrDefault(style => style is T);
if (materialTheme == null)
{
if (materialTheme == null) {
throw new InvalidOperationException(
$"Cannot locate {nameof(T)} in Avalonia application styles. Be sure that you include MaterialTheme in your App.xaml in Application.Styles section");
}

return (T) materialTheme;
return (T)materialTheme;
}

public static IBaseTheme GetBaseTheme(this BaseThemeMode baseThemeMode)
{
return baseThemeMode switch
{
public static IBaseTheme GetBaseTheme(this BaseThemeMode baseThemeMode) {
return baseThemeMode switch {
BaseThemeMode.Dark => Theme.Dark,
BaseThemeMode.Light => Theme.Light,
BaseThemeMode.Inherit => (SystemThemeProbe.GetSystemBaseThemeMode() ?? BaseThemeMode.Light).GetBaseTheme(),
BaseThemeMode.Inherit => Theme.Light,
_ => throw new InvalidOperationException()
};
}
@@ -36,8 +30,7 @@ public static IBaseTheme GetBaseTheme(this BaseThemeMode baseThemeMode)
public static BaseThemeMode GetBaseTheme(this IReadOnlyTheme theme)
=> GetBaseThemeMode(theme);

public static BaseThemeMode GetBaseThemeMode(this IReadOnlyTheme theme)
{
public static BaseThemeMode GetBaseThemeMode(this IReadOnlyTheme theme) {
if (theme is null) throw new ArgumentNullException(nameof(theme));

var foreground = theme.Background.PickContrastColor();
@@ -53,18 +46,15 @@ public static BaseThemeMode GetBaseThemeMode(this IReadOnlyTheme theme)
/// <see cref="BaseThemeMode.Light"/> for <see cref="BaseThemeMode.Dark"/> <br/>
/// Everything else remains unchanged
/// </returns>
public static BaseThemeMode Invert(this BaseThemeMode mode)
{
return mode switch
{
public static BaseThemeMode Invert(this BaseThemeMode mode) {
return mode switch {
BaseThemeMode.Light => BaseThemeMode.Dark,
BaseThemeMode.Dark => BaseThemeMode.Light,
_ => mode
};
}

public static ITheme SetBaseTheme(this ITheme theme, IBaseTheme baseTheme)
{
public static ITheme SetBaseTheme(this ITheme theme, IBaseTheme baseTheme) {
if (theme is null) throw new ArgumentNullException(nameof(theme));

theme.ValidationError = baseTheme.ValidationErrorColor;
@@ -99,8 +89,7 @@ public static ITheme SetBaseTheme(this ITheme theme, IBaseTheme baseTheme)
return theme;
}

public static ITheme SetPrimaryColor(this ITheme theme, Color primaryColor)
{
public static ITheme SetPrimaryColor(this ITheme theme, Color primaryColor) {
if (theme is null) throw new ArgumentNullException(nameof(theme));

theme.PrimaryLight = primaryColor.Lighten();
@@ -110,8 +99,7 @@ public static ITheme SetPrimaryColor(this ITheme theme, Color primaryColor)
return theme;
}

public static ITheme SetSecondaryColor(this ITheme theme, Color accentColor)
{
public static ITheme SetSecondaryColor(this ITheme theme, Color accentColor) {
if (theme == null) throw new ArgumentNullException(nameof(theme));
theme.SecondaryLight = accentColor.Lighten();
theme.SecondaryMid = accentColor;

0 comments on commit da78161

Please sign in to comment.