Skip to content
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

Control Themes #8263

Merged
merged 37 commits into from
Jul 22, 2022
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
c02439a
Refactored most of Style into StyleBase.
grokys May 30, 2022
088d8cf
Initial implementation of control themes.
grokys Jun 1, 2022
dee353b
Support ControlTheme in XAML compiler.
grokys Jun 1, 2022
a6dc6b1
Prevent ControlTheme as a nested style.
grokys Jun 1, 2022
fc3c036
Move Theme to StyledElement.
grokys Jun 1, 2022
8c61f25
Promote theme to LocalValue if applied from style.
grokys Jun 2, 2022
5cd9532
Move tests to correct place.
grokys Jun 2, 2022
4bdcb8e
Invalidate template control styles when Theme changes.
grokys Jun 2, 2022
49613c7
Add accent button to control catalog.
grokys Jun 3, 2022
d215a1e
Fix nested :not selector.
grokys Jun 3, 2022
05fdc04
Add ControlTheme.BasedOn.
grokys Jun 3, 2022
1d1ef5c
Display control themes in devtools.
grokys Jun 4, 2022
95f7014
Can apply control theme to derived types.
grokys Jun 4, 2022
d21e634
Added support for implicit themes.
grokys Jun 8, 2022
8b4cf63
Additional validation for ControlTheme children.
grokys Jun 10, 2022
d3600ce
Merge branch 'fixes/8372-clear-local-value' into feature/7120-control…
grokys Jun 22, 2022
7b7d658
Added a new failing test.
grokys Jun 22, 2022
ab3b0b3
Merge branch 'fixes/8372-clear-local-value' into feature/7120-control…
grokys Jun 22, 2022
4215466
Add test/fix for promoted themes.
grokys Jun 22, 2022
8abeb76
Added support for Design.PreviewWith in resource dictionaries.
grokys Jun 22, 2022
e50b416
Fix parent selectors with /template/ at end.
grokys Jun 30, 2022
5e1f28f
Make MovePreviousOrParent internal as well.
grokys Jun 30, 2022
5fd854e
Added failing test.
grokys Jun 30, 2022
1970336
Apply own control theme before templated parent's.
grokys Jun 30, 2022
5d9be70
Merge branch 'master' into feature/7120-control-themes
maxkatz6 Jul 3, 2022
f385cc1
Remove debug/unused code.
grokys Jul 4, 2022
4d23058
Don't run an unnecessary batch update.
grokys Jul 4, 2022
030c956
Revert "Make MovePreviousOrParent internal as well."
grokys Jul 5, 2022
a341c33
Revert "Fix parent selectors with /template/ at end."
grokys Jul 5, 2022
3e17bd0
Skip these failing tests for now.
grokys Jul 6, 2022
34cc24e
Merge branch 'master' into feature/7120-control-themes
grokys Jul 7, 2022
de58466
Merge branch 'master' into feature/7120-control-themes
grokys Jul 7, 2022
0023770
Disallow selectors with trailing /template/.
grokys Jul 7, 2022
05c1f58
Added ItemContainerTheme property.
grokys Jul 7, 2022
fb49040
Added some ControlTheme benchmarks.
grokys Jul 13, 2022
edef92f
Merge branch 'master' into feature/7120-control-themes
grokys Jul 19, 2022
9feaa75
Merge branch 'master' into feature/7120-control-themes
maxkatz6 Jul 21, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions samples/ControlCatalog/Pages/ButtonsPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
</Style>
</Button.Styles>
</Button>
<Button Classes="accent">Accent</Button>
</StackPanel>

<StackPanel Orientation="Vertical"
Expand Down
80 changes: 76 additions & 4 deletions src/Avalonia.Base/StyledElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
using Avalonia.LogicalTree;
using Avalonia.Styling;

#nullable enable

namespace Avalonia
{
/// <summary>
Expand Down Expand Up @@ -55,7 +53,14 @@ public class StyledElement : Animatable, IDataContextProvider, IStyledElement, I
nameof(TemplatedParent),
o => o.TemplatedParent,
(o ,v) => o.TemplatedParent = v);

/// <summary>
/// Defines the <see cref="Theme"/> property.
/// </summary>
public static readonly StyledProperty<ControlTheme?> ThemeProperty =
AvaloniaProperty.Register<StyledElement, ControlTheme?>(nameof(Theme));

private static readonly ControlTheme s_invalidTheme = new ControlTheme();
private int _initCount;
private string? _name;
private readonly Classes _classes = new Classes();
Expand All @@ -67,6 +72,8 @@ public class StyledElement : Animatable, IDataContextProvider, IStyledElement, I
private List<IStyleInstance>? _appliedStyles;
private ITemplatedControl? _templatedParent;
private bool _dataContextUpdating;
private bool _hasPromotedTheme;
private ControlTheme? _implicitTheme;

/// <summary>
/// Initializes static members of the <see cref="StyledElement"/> class.
Expand Down Expand Up @@ -230,6 +237,15 @@ public ITemplatedControl? TemplatedParent
internal set => SetAndRaise(TemplatedParentProperty, ref _templatedParent, value);
}

/// <summary>
/// Gets or sets the theme to be applied to the element.
/// </summary>
public ControlTheme? Theme
{
get => GetValue(ThemeProperty);
set => SetValue(ThemeProperty, value);
}

/// <summary>
/// Gets the styled element's logical children.
/// </summary>
Expand Down Expand Up @@ -302,6 +318,7 @@ protected IAvaloniaList<ILogical> LogicalChildren
/// <inheritdoc/>
IStyleHost? IStyleHost.StylingParent => (IStyleHost?)InheritanceParent;


/// <inheritdoc/>
public virtual void BeginInit()
{
Expand Down Expand Up @@ -341,10 +358,15 @@ protected bool ApplyStyling()
}
finally
{
_styled = true;
EndBatchUpdate();
}

_styled = true;
if (_hasPromotedTheme)
{
_hasPromotedTheme = false;
ClearValue(ThemeProperty);
}
}

return _styled;
Expand Down Expand Up @@ -475,6 +497,31 @@ void ISetInheritanceParent.SetParent(IAvaloniaObject? parent)
};
}

ControlTheme? IStyleable.GetEffectiveTheme()
{
var theme = Theme;

// Explitly set Theme property takes precedence.
if (theme is not null)
return theme;

// If the Theme property is not set, try to find a ControlTheme resource with our StyleKey.
if (_implicitTheme is null)
{
var key = ((IStyleable)this).StyleKey;

if (this.TryFindResource(key, out var value) && value is ControlTheme t)
_implicitTheme = t;
else
_implicitTheme = s_invalidTheme;
}

if (_implicitTheme != s_invalidTheme)
return _implicitTheme;

return null;
}

void IStyleable.StyleApplied(IStyleInstance instance)
{
instance = instance ?? throw new ArgumentNullException(nameof(instance));
Expand Down Expand Up @@ -590,6 +637,30 @@ protected virtual void OnInitialized()
{
}

protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);

if (change.Property == ThemeProperty)
{
// Changing the theme detaches all styles, meaning that if the theme property was
// set via a style, it will get cleared! To work around this, if the value was
// applied at less than local value priority then promote the value to local value
// priority until styling is re-applied.
if (change.Priority > BindingPriority.LocalValue)
danwalmsley marked this conversation as resolved.
Show resolved Hide resolved
{
Theme = change.GetNewValue<ControlTheme?>();
_hasPromotedTheme = true;
}
else if (_hasPromotedTheme && change.Priority == BindingPriority.LocalValue)
{
_hasPromotedTheme = false;
}

InvalidateStyles();
}
}

private static void DataContextNotifying(IAvaloniaObject o, bool updateStarted)
{
if (o is StyledElement element)
Expand Down Expand Up @@ -696,6 +767,7 @@ private void OnDetachedFromLogicalTreeCore(LogicalTreeAttachmentEventArgs e)
if (_logicalRoot != null)
{
_logicalRoot = null;
_implicitTheme = null;
DetachStyles();
OnDetachedFromLogicalTree(e);
DetachedFromLogicalTree?.Invoke(this, e);
Expand Down Expand Up @@ -760,7 +832,7 @@ private void ClearLogicalParent(IList children)

private void DetachStyles()
{
if (_appliedStyles is object)
if (_appliedStyles?.Count > 0)
{
BeginBatchUpdate();

Expand Down
2 changes: 1 addition & 1 deletion src/Avalonia.Base/Styling/ChildSelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,6 @@ protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bo
}

protected override Selector? MovePrevious() => null;
internal override bool HasValidNestingSelector() => _parent.HasValidNestingSelector();
protected override Selector? MovePreviousOrParent() => _parent;
}
}
67 changes: 67 additions & 0 deletions src/Avalonia.Base/Styling/ControlTheme.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System;

namespace Avalonia.Styling
{
/// <summary>
/// Defines a switchable theme for a control.
/// </summary>
public class ControlTheme : StyleBase
{
/// <summary>
/// Initializes a new instance of the <see cref="ControlTheme"/> class.
/// </summary>
public ControlTheme() { }

/// <summary>
/// Initializes a new instance of the <see cref="ControlTheme"/> class.
/// </summary>
/// <param name="targetType">The value for <see cref="TargetType"/>.</param>
public ControlTheme(Type targetType) => TargetType = targetType;

/// <summary>
/// Gets or sets the type for which this control theme is intended.
/// </summary>
public Type? TargetType { get; set; }

/// <summary>
/// Gets or sets a control theme that is the basis of the current theme.
/// </summary>
public ControlTheme? BasedOn { get; set; }

public override SelectorMatchResult TryAttach(IStyleable target, object? host)
{
_ = target ?? throw new ArgumentNullException(nameof(target));

if (TargetType is null)
throw new InvalidOperationException("ControlTheme has no TargetType.");

var result = BasedOn?.TryAttach(target, host) ?? SelectorMatchResult.NeverThisType;

if (HasSettersOrAnimations && TargetType.IsAssignableFrom(target.StyleKey))
{
Attach(target, null);
result = SelectorMatchResult.AlwaysThisType;
}

var childResult = TryAttachChildren(target, host);

if (childResult > result)
result = childResult;

return result;
}

public override string ToString()
{
if (TargetType is not null)
return "ControlTheme: " + TargetType.Name;
else
return "ControlTheme";
}

internal override void SetParent(StyleBase? parent)
{
throw new InvalidOperationException("ControlThemes cannot be added as a nested style.");
}
}
}
2 changes: 1 addition & 1 deletion src/Avalonia.Base/Styling/DescendentSelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,6 @@ protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bo
}

protected override Selector? MovePrevious() => null;
internal override bool HasValidNestingSelector() => _parent.HasValidNestingSelector();
protected override Selector? MovePreviousOrParent() => _parent;
}
}
2 changes: 1 addition & 1 deletion src/Avalonia.Base/Styling/IStyle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ public interface IStyle : IResourceNode
/// <returns>
/// A <see cref="SelectorMatchResult"/> describing how the style matches the control.
/// </returns>
SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host);
SelectorMatchResult TryAttach(IStyleable target, object? host);
}
}
7 changes: 5 additions & 2 deletions src/Avalonia.Base/Styling/IStyleable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
using Avalonia.Collections;
using Avalonia.Metadata;

#nullable enable

namespace Avalonia.Styling
{
/// <summary>
Expand All @@ -28,6 +26,11 @@ public interface IStyleable : IAvaloniaObject, INamed
/// </summary>
ITemplatedControl? TemplatedParent { get; }

/// <summary>
/// Gets the effective theme for the control as used by the syling system.
/// </summary>
ControlTheme? GetEffectiveTheme();

/// <summary>
/// Notifies the element that a style has been applied.
/// </summary>
Expand Down
14 changes: 11 additions & 3 deletions src/Avalonia.Base/Styling/NestingSelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,24 @@ internal class NestingSelector : Selector

protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe)
{
if (parent is Style s && s.Selector is Selector selector)
if (parent is Style s && s.Selector is not null)
{
return selector.Match(control, (parent as Style)?.Parent, subscribe);
return s.Selector.Match(control, s.Parent, subscribe);
}
else if (parent is ControlTheme theme)
{
if (theme.TargetType is null)
throw new InvalidOperationException("ControlTheme has no TargetType.");
return theme.TargetType.IsAssignableFrom(control.StyleKey) ?
SelectorMatch.AlwaysThisType :
SelectorMatch.NeverThisType;
}

throw new InvalidOperationException(
"Nesting selector was specified but cannot determine parent selector.");
}

protected override Selector? MovePrevious() => null;
internal override bool HasValidNestingSelector() => true;
protected override Selector? MovePreviousOrParent() => null;
}
}
2 changes: 1 addition & 1 deletion src/Avalonia.Base/Styling/NotSelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,6 @@ protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bo
}

protected override Selector? MovePrevious() => _previous;
internal override bool HasValidNestingSelector() => _argument.HasValidNestingSelector();
protected override Selector? MovePreviousOrParent() => _previous;
}
}
2 changes: 1 addition & 1 deletion src/Avalonia.Base/Styling/NthChildSelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ internal static SelectorMatch Evaluate(
}

protected override Selector? MovePrevious() => _previous;
internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false;
protected override Selector? MovePreviousOrParent() => _previous;

public override string ToString()
{
Expand Down
12 changes: 3 additions & 9 deletions src/Avalonia.Base/Styling/OrSelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,18 +103,12 @@ protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bo
}

protected override Selector? MovePrevious() => null;
protected override Selector? MovePreviousOrParent() => null;

internal override bool HasValidNestingSelector()
internal override void ValidateNestingSelector(bool inControlTheme)
{
foreach (var selector in _selectors)
{
if (!selector.HasValidNestingSelector())
{
return false;
}
}

return true;
selector.ValidateNestingSelector(inControlTheme);
}

private Type? EvaluateTargetType()
Expand Down
2 changes: 1 addition & 1 deletion src/Avalonia.Base/Styling/PropertyEqualsSelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bo
}

protected override Selector? MovePrevious() => _previous;
internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false;
protected override Selector? MovePreviousOrParent() => _previous;

internal static bool Compare(Type propertyType, object? propertyValue, object? value)
{
Expand Down
31 changes: 30 additions & 1 deletion src/Avalonia.Base/Styling/Selector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,36 @@ public SelectorMatch Match(IStyleable control, IStyle? parent = null, bool subsc
/// </summary>
protected abstract Selector? MovePrevious();

internal abstract bool HasValidNestingSelector();
/// <summary>
/// Moves to the previous selector or the parent selector.
/// </summary>
protected abstract Selector? MovePreviousOrParent();

internal virtual void ValidateNestingSelector(bool inControlTheme)
{
var s = this;
var templateCount = 0;

do
{
if (inControlTheme)
{
if (!s.InTemplate && s.IsCombinator)
throw new InvalidOperationException(
"ControlTheme style may not directly contain a child or descendent selector.");
if (s is TemplateSelector && templateCount++ > 0)
throw new InvalidOperationException(
"ControlTemplate styles cannot contain multiple template selectors.");
}

var previous = s.MovePreviousOrParent();

if (previous is null && s is not NestingSelector)
throw new InvalidOperationException("Child styles must have a nesting selector.");

s = previous;
} while (s is not null);
}

private static SelectorMatch MatchUntilCombinator(
IStyleable control,
Expand Down
Loading