-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ThemeDictionary + ThemeVariantScope #8166
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
a96fb54
to
cc446d2
Compare
This comment was marked as outdated.
This comment was marked as outdated.
I've not fully reviewed this PR, but so far I only have two comments:
I realise that the second point was done so that |
Internally we agreed on ThemeVariant property name as for this moment. Might be changed depending on ControlTheme PR status. We want to avoid naming conflicts. |
We now have Control.Theme property on each control.
Other frameworks:
Bottom line, it feels like no matter what we choose, there is no "correct" term for dark/light mode or theme or style or brightness or variant or |
Yeah I think I still prefer |
AppThemeColor? |
This comment was marked as outdated.
This comment was marked as outdated.
For me that would conflict with AccentColor for newbies. Just my two cents. |
c25b6dc
to
dd2f821
Compare
aac1e0c
to
a75fe22
Compare
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
You can test this PR using the following package version. |
@amwx thanks, done. |
You can test this PR using the following package version. |
@maxkatz6 Found one issue so far trying to integrate this with FluentAvalonia. Light and Dark switching works perfectly, but it will not work with other themes (HighContrast specifically). The resource lookups are coming back with transparent. What seems to be happening is (example): When the DeferredItem factory runs for the Brush, the lookup for Interestingly, when Dark (for example) mode is used, when that Brush initializes in Note: that I just kind of threw this PR into FA so it's possible I'm doing something that's messing with it. The only special thing I do is in FATheme I force search the Application ResourceDictionary first for overriding purposes. But I took that out to test and the same behavior occurs. I'll test some more tomorrow Otherwise, I think the API itself is fine. IMO I'm not a huge fan of the "Default" variant (I don't like it in WinUI either) as it is ambiguous as to its meaning, especially if the meaning can be changed between Avalonia & third parties. I haven't looked closely enough to know if the ThemeVariant.Default member has a purpose, but at least in the ThemeDictionaries IMO I think we should use actual theme names and just implicitly map "Default" to "Light" |
maybe |
The problem is, "ThemeVariant.Default" is used in two different ways depending on the context. |
@maxkatz6 in Material.Avalonia we have |
@SKProCH yes, only dark/light mode, I don't think there is any meaningful way to abstract app accent colors across the app. As different themes might have one or many accent colors. Like material 3 on android which has three accent color. |
@maxkatz6 Expanding on the issue I noted, take this repro into the Sandbox project: MainWindow: <Window xmlns="https://github.com/avaloniaui"
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
x:Class="Sandbox.MainWindow"
Background="#202020">
<Border Width="300" Height="300"
Background="{DynamicResource UniqueColorBrush}" />
</Window> MainWindow.cs public ThemeVariant CustomTheme = new ThemeVariant("Custom")
{
InheritVariant = ThemeVariant.Light
};
public MainWindow()
{
this.InitializeComponent();
this.AttachDevTools();
// A method to switch themes
KeyDown += (s, e) =>
{
switch (e.Key)
{
case Avalonia.Input.Key.L:
Application.Current.RequestedThemeVariant = ThemeVariant.Light;
break;
case Avalonia.Input.Key.D:
Application.Current.RequestedThemeVariant = ThemeVariant.Dark;
break;
case Avalonia.Input.Key.Space:
Application.Current.RequestedThemeVariant = CustomTheme;
break;
}
};
} Now, take this ResourceDictionary and put it in the Window's Resources: <ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
<Color x:Key="UniqueColor">Red</Color>
<SolidColorBrush x:Key="UniqueColorBrush" Color="{StaticResource UniqueColor}" />
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<Color x:Key="UniqueColor">Blue</Color>
<SolidColorBrush x:Key="UniqueColorBrush" Color="{StaticResource UniqueColor}" />
</ResourceDictionary>
<ResourceDictionary x:Key="Custom">
<Color x:Key="UniqueColor">Purple</Color>
<SolidColorBrush x:Key="UniqueColorBrush" Color="{StaticResource UniqueColor}" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary> So far this should all work as expected. Now make the following changes:
Switch to the Custom theme, then to either light or dark, then back to custom and the Border will be transparent. If you return the light/dark resources (keeping the custom theme as a DynamicResource) it will work. Interestingly, only the colors need to be kept in all dictionaries for the lookup to succeed, removing just the brushes from light/dark have no effect. Now, remove the ResourceDictionary from the window. Copy the original (from above) to the Application resources. Change the binding on custom theme's brush to DynamicResource (but leave the others active). The lookup will fail and fall back to Light theme (Red) when you switch to the Custom theme (this worked in the Window's resources, but fails at the App level). Thus, not having the resources present in light/dark would lead to the lookup failing, and this is basically the same issue/result with the HighContrast theme I already noted. It seems to be an issue with DynamicResource |
You can test this PR using the following package version. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM except for the compiler thingy
...rkup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
Outdated
Show resolved
Hide resolved
You can test this PR using the following package version. |
Hi, I just upgraded to Preview 5 and figured out the Fluent Mode property has been removed. It seems like I must set the mode in code using:
That seems to work. However I am also using |
What does the pull request do?
Up to date API changes:
Avalonia.Base:
Avalonia.Controls:
Avalonia.Theme.xxx
P.S. IGlobalThemeVariantProvider can be avoided if we directly subscribe on Application.PropertyChanged from the TopLevel. As property inheriting doesn't work from the Application to the Window normally. cc @grokys
How was the solution implemented (if it's not obvious)?
How to define a set of theme specific resources
In Avalonia, theme variant specific resources can be defined in the
ResourceDictionary
using theThemeDictionaries
dictionary. TheThemeDictionaries
dictionary usesThemeVariant
as the key and a childResourceDictionary
as the value.Typically, developers use
Light
orDark
as the key for the theme variants, which are recognized at compile time. UsingDefault
as the key marks a specific theme dictionary as a fallback in case the theme variant or resource key is not found in other theme dictionaries.In addition to the built-in values of
Light
,Dark
, andDefault
, any object value can be used as a key (since it's wrapped in theThemeVariant(object key)
structure).{x:Static}
markup extension can also be used here, if a developer wants to define multiple custom themes as static properties and reference them from the XAML code.Example:
Note, in this PR for the Simple and Fluent theme I defined light-variant resources as
Default
. It's an exact opposite of UWP definitions, which define dark-variant resources as aDefault
.Why? It's theme specific and we historically used light-variant as a default. Not to mention, most of modern OSes are light by default now. Custom third-party themes can still define it how they decide.
Defining custom ThemeVariant:
Custom Theme Variant can be defined as:
Then, when a resource lookup is performed, the theme dictionary defined with the
Pink
key is selected. If the resource is not found in that dictionary, theLight
variant is used as a fallback.It is important for developers to always set
InheritVariant
, as otherwise Avalonia will not be able to convert the application-level variant to the system-level variant (which is always eitherLight
orDark
) in order to change the window frame colors.However, when the system theme variant is used (i.e., no requested variant is set), it will always use the "Dark" or "Light" default variants. In the future, there may be a need for an API to convert system-
Light
to a custom application-Pink
variant, for example.The theme variant lookup in this PR works as follows:
Each control has two properties:
RequestedThemeVariant
andActualThemeVariant
.RequestedThemeVariant
is only accessible forApplication
,TopLevel
(includingWindow
), andThemeVariantScrope
controls. Potentially it can be added to thePage
control to improve mobile support. We need this restriction because Avalonia has inheritable properties, while UWP does not. This would cause problems with font and background colors inherited from the parent which has different theme variant. WhileThemeVariantScrope
override most common inheritable properties.When
Application.RequestedThemeVariant
is not set, the application will inherit the system theme variant. The same goes for Window and its child controls tree, which will inherit theActualThemeVariant
from the application. IfRequestedThemeVariant
is set on any supported control, the subtree will have an overriddenActualThemeVariant
.Setting
RequestedThemeVariant
toDefault
ornull
will reset actual theme and value will be inherited from the parent (or from the system in case of Application.RequestedThemeVariant).It's worth noting that there is a design flaw in that a user can still go one level lower and call
control.SetValue(RequestedThemeVariant)
orcontrol.SetValue(ActualThemeVariant)
on any control, which will still work but will not override inherited controls automatically. Ideally,ActualThemeVariant
should be a fully read-only property, but it also needs to be inheritable, which is not currently possible to combine in Avalonia.The resource lookup in this PR works as follows:
The
ThemeVariant
is picked up from theStyledControl.ActualThemeVariant
, regardless of whether the extension was used on the control or attached with a style. EachResourceDictionary
looks up the resource in the following priority:ResourceDictionary
.ThemeVariant
is provided:2.1. Uses the theme dictionary with the variant key, if defined.
2.2. Otherwise, uses
ThemeVariant.InheritVariant
recursively, if defined.Default
key, if defined.Steps 2 and 3 are new and may add overhead to existing applications, but only if the
ResourceDictionary
has at least one theme dictionary set.In the case of
StaticResource
, when an extension is used from theResourceDictionary
, it won't use any theme variant during the lookup process. Additionally, it is not able to understand if the extension was used from a specific theme dictionary, as our markup extension does not have this context information. However, it will still work if both resources are defined in the same dictionary or have been merged into a single dictionary. This limitation is similar to that of UWP, which also merges dictionaries.Checklist
Breaking changes
Yes, SimpleThemeMode and FluentThemeMode removed. IResourceNode interface was changed.
c.mp4