diff --git a/src/Avalonia.Base/Styling/ChildSelector.cs b/src/Avalonia.Base/Styling/ChildSelector.cs
index 5c92182b80..34f3a76b61 100644
--- a/src/Avalonia.Base/Styling/ChildSelector.cs
+++ b/src/Avalonia.Base/Styling/ChildSelector.cs
@@ -37,13 +37,13 @@ namespace Avalonia.Styling
return _selectorString;
}
- protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
+ protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe)
{
var controlParent = ((ILogical)control).LogicalParent;
if (controlParent != null)
{
- var parentMatch = _parent.Match((IStyleable)controlParent, subscribe);
+ var parentMatch = _parent.Match((IStyleable)controlParent, parent, subscribe);
if (parentMatch.Result == SelectorMatchResult.Sometimes)
{
@@ -65,5 +65,6 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => null;
+ internal override bool HasValidNestingSelector() => _parent.HasValidNestingSelector();
}
}
diff --git a/src/Avalonia.Base/Styling/DescendentSelector.cs b/src/Avalonia.Base/Styling/DescendentSelector.cs
index dde88b3436..4ffaff6861 100644
--- a/src/Avalonia.Base/Styling/DescendentSelector.cs
+++ b/src/Avalonia.Base/Styling/DescendentSelector.cs
@@ -35,7 +35,7 @@ namespace Avalonia.Styling
return _selectorString;
}
- protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
+ protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe)
{
var c = (ILogical)control;
var descendantMatches = new OrActivatorBuilder();
@@ -46,7 +46,7 @@ namespace Avalonia.Styling
if (c is IStyleable)
{
- var match = _parent.Match((IStyleable)c, subscribe);
+ var match = _parent.Match((IStyleable)c, parent, subscribe);
if (match.Result == SelectorMatchResult.Sometimes)
{
@@ -70,5 +70,6 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => null;
+ internal override bool HasValidNestingSelector() => _parent.HasValidNestingSelector();
}
}
diff --git a/src/Avalonia.Base/Styling/NestingSelector.cs b/src/Avalonia.Base/Styling/NestingSelector.cs
new file mode 100644
index 0000000000..481a937867
--- /dev/null
+++ b/src/Avalonia.Base/Styling/NestingSelector.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace Avalonia.Styling
+{
+ ///
+ /// The `^` nesting style selector.
+ ///
+ internal class NestingSelector : Selector
+ {
+ public override bool InTemplate => false;
+ public override bool IsCombinator => false;
+ public override Type? TargetType => null;
+
+ public override string ToString() => "^";
+
+ protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe)
+ {
+ if (parent is Style s && s.Selector is Selector selector)
+ {
+ return selector.Match(control, (parent as Style)?.Parent, subscribe);
+ }
+
+ throw new InvalidOperationException(
+ "Nesting selector was specified but cannot determine parent selector.");
+ }
+
+ protected override Selector? MovePrevious() => null;
+ internal override bool HasValidNestingSelector() => true;
+ }
+}
diff --git a/src/Avalonia.Base/Styling/NotSelector.cs b/src/Avalonia.Base/Styling/NotSelector.cs
index ab4e9d5d7f..cdc3254d38 100644
--- a/src/Avalonia.Base/Styling/NotSelector.cs
+++ b/src/Avalonia.Base/Styling/NotSelector.cs
@@ -45,9 +45,9 @@ namespace Avalonia.Styling
return _selectorString;
}
- protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
+ protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe)
{
- var innerResult = _argument.Match(control, subscribe);
+ var innerResult = _argument.Match(control, parent, subscribe);
switch (innerResult.Result)
{
@@ -67,5 +67,6 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => _previous;
+ internal override bool HasValidNestingSelector() => _argument.HasValidNestingSelector();
}
}
diff --git a/src/Avalonia.Base/Styling/NthChildSelector.cs b/src/Avalonia.Base/Styling/NthChildSelector.cs
index aff34ea17c..047bf434da 100644
--- a/src/Avalonia.Base/Styling/NthChildSelector.cs
+++ b/src/Avalonia.Base/Styling/NthChildSelector.cs
@@ -48,7 +48,7 @@ namespace Avalonia.Styling
public int Step { get; }
public int Offset { get; }
- protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
+ protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe)
{
if (!(control is ILogical logical))
{
@@ -105,6 +105,7 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => _previous;
+ internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false;
public override string ToString()
{
diff --git a/src/Avalonia.Base/Styling/OrSelector.cs b/src/Avalonia.Base/Styling/OrSelector.cs
index 3d6db9b01e..913c27bf0c 100644
--- a/src/Avalonia.Base/Styling/OrSelector.cs
+++ b/src/Avalonia.Base/Styling/OrSelector.cs
@@ -65,14 +65,14 @@ namespace Avalonia.Styling
return _selectorString;
}
- protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
+ protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe)
{
var activators = new OrActivatorBuilder();
var neverThisInstance = false;
foreach (var selector in _selectors)
{
- var match = selector.Match(control, subscribe);
+ var match = selector.Match(control, parent, subscribe);
switch (match.Result)
{
@@ -104,6 +104,19 @@ namespace Avalonia.Styling
protected override Selector? MovePrevious() => null;
+ internal override bool HasValidNestingSelector()
+ {
+ foreach (var selector in _selectors)
+ {
+ if (!selector.HasValidNestingSelector())
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
private Type? EvaluateTargetType()
{
Type? result = null;
diff --git a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs
index 1cd1a650ef..7a37daf087 100644
--- a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs
+++ b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs
@@ -74,7 +74,7 @@ namespace Avalonia.Styling
}
///
- protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
+ protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe)
{
if (subscribe)
{
@@ -90,6 +90,7 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => _previous;
+ internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false;
internal static bool Compare(Type propertyType, object? propertyValue, object? value)
{
diff --git a/src/Avalonia.Base/Styling/Selector.cs b/src/Avalonia.Base/Styling/Selector.cs
index 0740e0f891..1e06f3d375 100644
--- a/src/Avalonia.Base/Styling/Selector.cs
+++ b/src/Avalonia.Base/Styling/Selector.cs
@@ -33,22 +33,25 @@ namespace Avalonia.Styling
/// Tries to match the selector with a control.
///
/// The control.
+ ///
+ /// The parent style, if the style containing the selector is a nested style.
+ ///
///
/// Whether the match should subscribe to changes in order to track the match over time,
/// or simply return an immediate result.
///
/// A .
- public SelectorMatch Match(IStyleable control, bool subscribe = true)
+ public SelectorMatch Match(IStyleable control, IStyle? parent = null, bool subscribe = true)
{
// First match the selector until a combinator is found. Selectors are stored from
// right-to-left, so MatchUntilCombinator reverses this order because the type selector
// will be on the left.
- var match = MatchUntilCombinator(control, this, subscribe, out var combinator);
+ var match = MatchUntilCombinator(control, this, parent, subscribe, out var combinator);
// If the pre-combinator selector matches, we can now match the combinator, if any.
if (match.IsMatch && combinator is object)
{
- match = match.And(combinator.Match(control, subscribe));
+ match = match.And(combinator.Match(control, parent, subscribe));
// If we have a combinator then we can never say that we always match a control of
// this type, because by definition the combinator matches on things outside of the
@@ -68,28 +71,34 @@ namespace Avalonia.Styling
/// Evaluates the selector for a match.
///
/// The control.
+ ///
+ /// The parent style, if the style containing the selector is a nested style.
+ ///
///
/// Whether the match should subscribe to changes in order to track the match over time,
/// or simply return an immediate result.
///
/// A .
- protected abstract SelectorMatch Evaluate(IStyleable control, bool subscribe);
+ protected abstract SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe);
///
/// Moves to the previous selector.
///
protected abstract Selector? MovePrevious();
+ internal abstract bool HasValidNestingSelector();
+
private static SelectorMatch MatchUntilCombinator(
IStyleable control,
Selector start,
+ IStyle? parent,
bool subscribe,
out Selector? combinator)
{
combinator = null;
var activators = new AndActivatorBuilder();
- var result = Match(control, start, subscribe, ref activators, ref combinator);
+ var result = Match(control, start, parent, subscribe, ref activators, ref combinator);
return result == SelectorMatchResult.Sometimes ?
new SelectorMatch(activators.Get()) :
@@ -99,6 +108,7 @@ namespace Avalonia.Styling
private static SelectorMatchResult Match(
IStyleable control,
Selector selector,
+ IStyle? parent,
bool subscribe,
ref AndActivatorBuilder activators,
ref Selector? combinator)
@@ -110,7 +120,7 @@ namespace Avalonia.Styling
// opportunity to exit early.
if (previous != null && !previous.IsCombinator)
{
- var previousMatch = Match(control, previous, subscribe, ref activators, ref combinator);
+ var previousMatch = Match(control, previous, parent, subscribe, ref activators, ref combinator);
if (previousMatch < SelectorMatchResult.Sometimes)
{
@@ -119,7 +129,7 @@ namespace Avalonia.Styling
}
// Match this selector.
- var match = selector.Evaluate(control, subscribe);
+ var match = selector.Evaluate(control, parent, subscribe);
if (!match.IsMatch)
{
diff --git a/src/Avalonia.Base/Styling/Selectors.cs b/src/Avalonia.Base/Styling/Selectors.cs
index 7c66469cf1..476d86cd11 100644
--- a/src/Avalonia.Base/Styling/Selectors.cs
+++ b/src/Avalonia.Base/Styling/Selectors.cs
@@ -109,6 +109,11 @@ namespace Avalonia.Styling
}
}
+ public static Selector Nesting(this Selector? previous)
+ {
+ return new NestingSelector();
+ }
+
///
/// Returns a selector which inverts the results of selector argument.
///
diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs
index 00819ef7be..8fcf5eec8a 100644
--- a/src/Avalonia.Base/Styling/Style.cs
+++ b/src/Avalonia.Base/Styling/Style.cs
@@ -4,8 +4,6 @@ using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Metadata;
-#nullable enable
-
namespace Avalonia.Styling
{
///
@@ -14,9 +12,11 @@ namespace Avalonia.Styling
public class Style : AvaloniaObject, IStyle, IResourceProvider
{
private IResourceHost? _owner;
+ private StyleChildren? _children;
private IResourceDictionary? _resources;
private List? _setters;
private List? _animations;
+ private StyleCache? _childCache;
///
/// Initializes a new instance of the class.
@@ -34,6 +34,14 @@ namespace Avalonia.Styling
Selector = selector(null);
}
+ ///
+ /// Gets the children of the style.
+ ///
+ public IList Children => _children ??= new(this);
+
+ ///
+ /// Gets the or Application that hosts the style.
+ ///
public IResourceHost? Owner
{
get => _owner;
@@ -47,6 +55,11 @@ namespace Avalonia.Styling
}
}
+ ///
+ /// Gets the parent style if this style is hosted in a collection.
+ ///
+ public Style? Parent { get; private set; }
+
///
/// Gets or sets a dictionary of style resources.
///
@@ -90,7 +103,7 @@ namespace Avalonia.Styling
public IList Animations => _animations ??= new List();
bool IResourceNode.HasResources => _resources?.Count > 0;
- IReadOnlyList IStyle.Children => Array.Empty();
+ IReadOnlyList IStyle.Children => (IReadOnlyList?)_children ?? Array.Empty();
public event EventHandler? OwnerChanged;
@@ -98,7 +111,7 @@ namespace Avalonia.Styling
{
target = target ?? throw new ArgumentNullException(nameof(target));
- var match = Selector is object ? Selector.Match(target) :
+ var match = Selector is object ? Selector.Match(target, Parent) :
target == host ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance;
if (match.IsMatch && (_setters is object || _animations is object))
@@ -108,7 +121,17 @@ namespace Avalonia.Styling
instance.Start();
}
- return match.Result;
+ var result = match.Result;
+
+ if (_children is not null)
+ {
+ _childCache ??= new StyleCache();
+ var childResult = _childCache.TryAttach(_children, target, host);
+ if (childResult > result)
+ result = childResult;
+ }
+
+ return result;
}
public bool TryGetResource(object key, out object? result)
@@ -156,5 +179,18 @@ namespace Avalonia.Styling
_resources?.RemoveOwner(owner);
}
}
+
+ internal void SetParent(Style? parent)
+ {
+ if (parent?.Selector is not null)
+ {
+ if (Selector is null)
+ throw new InvalidOperationException("Child styles must have a selector.");
+ if (!Selector.HasValidNestingSelector())
+ throw new InvalidOperationException("Child styles must have a nesting selector.");
+ }
+
+ Parent = parent;
+ }
}
}
diff --git a/src/Avalonia.Base/Styling/StyleCache.cs b/src/Avalonia.Base/Styling/StyleCache.cs
new file mode 100644
index 0000000000..3285476880
--- /dev/null
+++ b/src/Avalonia.Base/Styling/StyleCache.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+
+namespace Avalonia.Styling
+{
+ ///
+ /// Simple cache for improving performance of applying styles.
+ ///
+ ///
+ /// Maps to a list of styles that are known be be possible
+ /// matches.
+ ///
+ internal class StyleCache : Dictionary?>
+ {
+ public SelectorMatchResult TryAttach(IList styles, IStyleable target, IStyleHost? host)
+ {
+ if (TryGetValue(target.StyleKey, out var cached))
+ {
+ if (cached is object)
+ {
+ var result = SelectorMatchResult.NeverThisType;
+
+ foreach (var style in cached)
+ {
+ var childResult = style.TryAttach(target, host);
+ if (childResult > result)
+ result = childResult;
+ }
+
+ return result;
+ }
+ else
+ {
+ return SelectorMatchResult.NeverThisType;
+ }
+ }
+ else
+ {
+ List? matches = null;
+
+ foreach (var child in styles)
+ {
+ if (child.TryAttach(target, host) != SelectorMatchResult.NeverThisType)
+ {
+ matches ??= new List();
+ matches.Add(child);
+ }
+ }
+
+ Add(target.StyleKey, matches);
+
+ return matches is null ?
+ SelectorMatchResult.NeverThisType :
+ SelectorMatchResult.AlwaysThisType;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Styling/StyleChildren.cs b/src/Avalonia.Base/Styling/StyleChildren.cs
new file mode 100644
index 0000000000..5f8635f155
--- /dev/null
+++ b/src/Avalonia.Base/Styling/StyleChildren.cs
@@ -0,0 +1,35 @@
+using System.Collections.ObjectModel;
+using Avalonia.Controls;
+
+namespace Avalonia.Styling
+{
+ internal class StyleChildren : Collection
+ {
+ private readonly Style _owner;
+
+ public StyleChildren(Style owner) => _owner = owner;
+
+ protected override void InsertItem(int index, IStyle item)
+ {
+ (item as Style)?.SetParent(_owner);
+ base.InsertItem(index, item);
+ }
+
+ protected override void RemoveItem(int index)
+ {
+ var item = Items[index];
+ (item as Style)?.SetParent(null);
+ if (_owner.Owner is IResourceHost host)
+ (item as IResourceProvider)?.RemoveOwner(host);
+ base.RemoveItem(index);
+ }
+
+ protected override void SetItem(int index, IStyle item)
+ {
+ (item as Style)?.SetParent(_owner);
+ base.SetItem(index, item);
+ if (_owner.Owner is IResourceHost host)
+ (item as IResourceProvider)?.AddOwner(host);
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs
index d79081152e..7c0bc4ad7f 100644
--- a/src/Avalonia.Base/Styling/Styles.cs
+++ b/src/Avalonia.Base/Styling/Styles.cs
@@ -20,7 +20,7 @@ namespace Avalonia.Styling
private readonly AvaloniaList _styles = new AvaloniaList();
private IResourceHost? _owner;
private IResourceDictionary? _resources;
- private Dictionary?>? _cache;
+ private StyleCache? _cache;
public Styles()
{
@@ -111,43 +111,8 @@ namespace Avalonia.Styling
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host)
{
- _cache ??= new Dictionary?>();
-
- if (_cache.TryGetValue(target.StyleKey, out var cached))
- {
- if (cached is object)
- {
- foreach (var style in cached)
- {
- style.TryAttach(target, host);
- }
-
- return SelectorMatchResult.AlwaysThisType;
- }
- else
- {
- return SelectorMatchResult.NeverThisType;
- }
- }
- else
- {
- List? matches = null;
-
- foreach (var child in this)
- {
- if (child.TryAttach(target, host) != SelectorMatchResult.NeverThisType)
- {
- matches ??= new List();
- matches.Add(child);
- }
- }
-
- _cache.Add(target.StyleKey, matches);
-
- return matches is null ?
- SelectorMatchResult.NeverThisType :
- SelectorMatchResult.AlwaysThisType;
- }
+ _cache ??= new StyleCache();
+ return _cache.TryAttach(this, target, host);
}
///
diff --git a/src/Avalonia.Base/Styling/TemplateSelector.cs b/src/Avalonia.Base/Styling/TemplateSelector.cs
index e8051efa6d..b0a2dae8d6 100644
--- a/src/Avalonia.Base/Styling/TemplateSelector.cs
+++ b/src/Avalonia.Base/Styling/TemplateSelector.cs
@@ -36,7 +36,7 @@ namespace Avalonia.Styling
return _selectorString;
}
- protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
+ protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe)
{
var templatedParent = control.TemplatedParent as IStyleable;
@@ -45,9 +45,10 @@ namespace Avalonia.Styling
return SelectorMatch.NeverThisInstance;
}
- return _parent.Match(templatedParent, subscribe);
+ return _parent.Match(templatedParent, parent, subscribe);
}
protected override Selector? MovePrevious() => null;
+ internal override bool HasValidNestingSelector() => _parent?.HasValidNestingSelector() ?? false;
}
}
diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
index ef48c4a8cd..24d5d6bbbf 100644
--- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
+++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
@@ -94,7 +94,7 @@ namespace Avalonia.Styling
}
///
- protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
+ protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe)
{
if (TargetType != null)
{
@@ -140,6 +140,7 @@ namespace Avalonia.Styling
}
protected override Selector? MovePrevious() => _previous;
+ internal override bool HasValidNestingSelector() => _previous?.HasValidNestingSelector() ?? false;
private string BuildSelectorString()
{
diff --git a/src/Avalonia.Themes.Fluent/Controls/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml
index f545206a2f..a93fb6831d 100644
--- a/src/Avalonia.Themes.Fluent/Controls/Button.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/Button.xaml
@@ -7,9 +7,11 @@
+
8,5,8,6
+
-
-
+
+
-
+
-
+
-
+
-
+
-
+
+
+
+
+
-
diff --git a/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml b/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml
index f16e1ed99f..2d70a35b13 100644
--- a/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml
@@ -1,294 +1,321 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs
index 4c4df1f53a..70209fb3ad 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs
@@ -151,6 +151,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
results.Add(result);
result = initialNode;
break;
+ case SelectorGrammar.NestingSyntax:
+ var parentTargetType = context.ParentNodes().OfType().FirstOrDefault();
+
+ if (parentTargetType is null)
+ throw new XamlParseException($"Cannot find parent style for nested selector.", node);
+
+ result = new XamlIlNestingSelector(result, parentTargetType.TargetType.GetClrType());
+ break;
default:
throw new XamlParseException($"Unsupported selector grammar '{i.GetType()}'.", node);
}
@@ -474,4 +482,20 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
m => m.Name == "Or" && m.Parameters.Count == 1 && m.Parameters[0].Name.StartsWith("IReadOnlyList"));
}
}
+
+ class XamlIlNestingSelector : XamlIlSelectorNode
+ {
+ public XamlIlNestingSelector(XamlIlSelectorNode previous, IXamlType targetType)
+ : base(previous)
+ {
+ TargetType = targetType;
+ }
+
+ public override IXamlType TargetType { get; }
+ protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen)
+ {
+ EmitCall(context, codeGen,
+ m => m.Name == "Nesting" && m.Parameters.Count == 1);
+ }
+ }
}
diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
index a9fc18474c..16856e674d 100644
--- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
+++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
@@ -46,7 +46,7 @@ namespace Avalonia.Markup.Parsers
switch (state)
{
case State.Start:
- state = ParseStart(ref r);
+ (state, syntax) = ParseStart(ref r);
break;
case State.Middle:
(state, syntax) = ParseMiddle(ref r, end);
@@ -93,27 +93,31 @@ namespace Avalonia.Markup.Parsers
return selector;
}
- private static State ParseStart(ref CharacterReader r)
+ private static (State, ISyntax?) ParseStart(ref CharacterReader r)
{
r.SkipWhitespace();
if (r.End)
{
- return State.End;
+ return (State.End, null);
}
if (r.TakeIf(':'))
{
- return State.Colon;
+ return (State.Colon, null);
}
else if (r.TakeIf('.'))
{
- return State.Class;
+ return (State.Class, null);
}
else if (r.TakeIf('#'))
{
- return State.Name;
+ return (State.Name, null);
+ }
+ else if (r.TakeIf('^'))
+ {
+ return (State.CanHaveType, new NestingSyntax());
}
- return State.TypeName;
+ return (State.TypeName, null);
}
private static (State, ISyntax?) ParseMiddle(ref CharacterReader r, char? end)
@@ -142,6 +146,10 @@ namespace Avalonia.Markup.Parsers
{
return (State.Start, new CommaSyntax());
}
+ else if (r.TakeIf('^'))
+ {
+ return (State.CanHaveType, new NestingSyntax());
+ }
else if (end.HasValue && !r.End && r.Peek == end.Value)
{
return (State.End, null);
@@ -635,5 +643,13 @@ namespace Avalonia.Markup.Parsers
return obj is CommaSyntax or;
}
}
+
+ public class NestingSyntax : ISyntax
+ {
+ public override bool Equals(object? obj)
+ {
+ return obj is NestingSyntax;
+ }
+ }
}
}
diff --git a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs
new file mode 100644
index 0000000000..d49fcf03a2
--- /dev/null
+++ b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs
@@ -0,0 +1,275 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Styling;
+using Avalonia.Styling.Activators;
+using Xunit;
+
+namespace Avalonia.Base.UnitTests.Styling
+{
+ public class SelectorTests_Nesting
+ {
+ [Fact]
+ public void Nesting_Class_Doesnt_Match_Parent_OfType_Selector()
+ {
+ var control = new Control2();
+ Style nested;
+ var parent = new Style(x => x.OfType())
+ {
+ Children =
+ {
+ (nested = new Style(x => x.Nesting().Class("foo"))),
+ }
+ };
+
+ var match = nested.Selector.Match(control, parent);
+ Assert.Equal(SelectorMatchResult.NeverThisType, match.Result);
+ }
+
+ [Fact]
+ public void Or_Nesting_Class_Doesnt_Match_Parent_OfType_Selector()
+ {
+ var control = new Control2();
+ Style nested;
+ var parent = new Style(x => x.OfType())
+ {
+ Children =
+ {
+ (nested = new Style(x => Selectors.Or(
+ x.Nesting().Class("foo"),
+ x.Nesting().Class("bar")))),
+ }
+ };
+
+ var match = nested.Selector.Match(control, parent);
+ Assert.Equal(SelectorMatchResult.NeverThisType, match.Result);
+ }
+
+ [Fact]
+ public void Or_Nesting_Child_OfType_Doesnt_Match_Parent_OfType_Selector()
+ {
+ var control = new Control1();
+ var panel = new DockPanel { Children = { control } };
+ Style nested;
+ var parent = new Style(x => x.OfType())
+ {
+ Children =
+ {
+ (nested = new Style(x => Selectors.Or(
+ x.Nesting().Child().OfType(),
+ x.Nesting().Child().OfType()))),
+ }
+ };
+
+ var match = nested.Selector.Match(control, parent);
+ Assert.Equal(SelectorMatchResult.NeverThisInstance, match.Result);
+ }
+
+ [Fact]
+ public void Double_Nesting_Class_Doesnt_Match_Grandparent_OfType_Selector()
+ {
+ var control = new Control2
+ {
+ Classes = { "foo", "bar" },
+ };
+
+ Style parent;
+ Style nested;
+ var grandparent = new Style(x => x.OfType())
+ {
+ Children =
+ {
+ (parent = new Style(x => x.Nesting().Class("foo"))
+ {
+ Children =
+ {
+ (nested = new Style(x => x.Nesting().Class("bar")))
+ }
+ })
+ }
+ };
+
+ var match = nested.Selector.Match(control, parent);
+ Assert.Equal(SelectorMatchResult.NeverThisType, match.Result);
+ }
+
+ [Fact]
+ public void Nesting_Class_Matches()
+ {
+ var control = new Control1 { Classes = { "foo" } };
+ Style nested;
+ var parent = new Style(x => x.OfType())
+ {
+ Children =
+ {
+ (nested = new Style(x => x.Nesting().Class("foo"))),
+ }
+ };
+
+ var match = nested.Selector.Match(control, parent);
+ Assert.Equal(SelectorMatchResult.Sometimes, match.Result);
+
+ var sink = new ActivatorSink(match.Activator);
+
+ Assert.True(sink.Active);
+ control.Classes.Clear();
+ Assert.False(sink.Active);
+ }
+
+ [Fact]
+ public void Double_Nesting_Class_Matches()
+ {
+ var control = new Control1
+ {
+ Classes = { "foo", "bar" },
+ };
+
+ Style parent;
+ Style nested;
+ var grandparent = new Style(x => x.OfType())
+ {
+ Children =
+ {
+ (parent = new Style(x => x.Nesting().Class("foo"))
+ {
+ Children =
+ {
+ (nested = new Style(x => x.Nesting().Class("bar")))
+ }
+ })
+ }
+ };
+
+ var match = nested.Selector.Match(control, parent);
+ Assert.Equal(SelectorMatchResult.Sometimes, match.Result);
+
+ var sink = new ActivatorSink(match.Activator);
+
+ Assert.True(sink.Active);
+ control.Classes.Remove("foo");
+ Assert.False(sink.Active);
+ }
+
+ [Fact]
+ public void Or_Nesting_Class_Matches()
+ {
+ var control = new Control1 { Classes = { "foo" } };
+ Style nested;
+ var parent = new Style(x => x.OfType())
+ {
+ Children =
+ {
+ (nested = new Style(x => Selectors.Or(
+ x.Nesting().Class("foo"),
+ x.Nesting().Class("bar")))),
+ }
+ };
+
+ var match = nested.Selector.Match(control, parent);
+ Assert.Equal(SelectorMatchResult.Sometimes, match.Result);
+
+ var sink = new ActivatorSink(match.Activator);
+
+ Assert.True(sink.Active);
+ control.Classes.Clear();
+ Assert.False(sink.Active);
+ }
+
+ [Fact]
+ public void Or_Nesting_Child_OfType_Matches()
+ {
+ var control = new Control1 { Classes = { "foo" } };
+ var panel = new Panel { Children = { control } };
+ Style nested;
+ var parent = new Style(x => x.OfType())
+ {
+ Children =
+ {
+ (nested = new Style(x => Selectors.Or(
+ x.Nesting().Child().OfType(),
+ x.Nesting().Child().OfType()))),
+ }
+ };
+
+ var match = nested.Selector.Match(control, parent);
+ Assert.Equal(SelectorMatchResult.AlwaysThisInstance, match.Result);
+ }
+
+ [Fact]
+ public void Nesting_With_No_Parent_Style_Fails()
+ {
+ var control = new Control1();
+ var style = new Style(x => x.Nesting().OfType());
+
+ Assert.Throws(() => style.Selector.Match(control, null));
+ }
+
+ [Fact]
+ public void Nesting_With_No_Parent_Selector_Fails()
+ {
+ var control = new Control1();
+ Style nested;
+ var parent = new Style
+ {
+ Children =
+ {
+ (nested = new Style(x => x.Nesting().Class("foo"))),
+ }
+ };
+
+ Assert.Throws(() => nested.Selector.Match(control, parent));
+ }
+
+ [Fact]
+ public void Adding_Child_With_No_Nesting_Selector_Fails()
+ {
+ var parent = new Style(x => x.OfType());
+ var child = new Style(x => x.Class("foo"));
+
+ Assert.Throws(() => parent.Children.Add(child));
+ }
+
+ [Fact]
+ public void Adding_Combinator_Selector_Child_With_No_Nesting_Selector_Fails()
+ {
+ var parent = new Style(x => x.OfType());
+ var child = new Style(x => x.Class("foo").Descendant().Class("bar"));
+
+ Assert.Throws(() => parent.Children.Add(child));
+ }
+
+ [Fact]
+ public void Adding_Or_Selector_Child_With_No_Nesting_Selector_Fails()
+ {
+ var parent = new Style(x => x.OfType());
+ var child = new Style(x => Selectors.Or(
+ x.Nesting().Class("foo"),
+ x.Class("bar")));
+
+ Assert.Throws(() => parent.Children.Add(child));
+ }
+
+ [Fact]
+ public void Can_Add_Child_Without_Nesting_Selector_To_Style_Without_Selector()
+ {
+ var parent = new Style();
+ var child = new Style(x => x.Class("foo"));
+
+ parent.Children.Add(child);
+ }
+
+ public class Control1 : Control
+ {
+ }
+
+ public class Control2 : Control
+ {
+ }
+
+ private class ActivatorSink : IStyleActivatorSink
+ {
+ public ActivatorSink(IStyleActivator source) => source.Subscribe(this);
+ public bool Active { get; private set; }
+ public void OnNext(bool value, int tag) => Active = value;
+ }
+ }
+}
diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs b/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs
index 8dedf3471f..7aa86a1328 100644
--- a/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs
@@ -722,6 +722,48 @@ namespace Avalonia.Base.UnitTests.Styling
resources.Verify(x => x.AddOwner(host.Object), Times.Once);
}
+ [Fact]
+ public void Nested_Style_Can_Be_Added()
+ {
+ var parent = new Style(x => x.OfType());
+ var nested = new Style(x => x.Nesting().Class("foo"));
+
+ parent.Children.Add(nested);
+
+ Assert.Same(parent, nested.Parent);
+ }
+
+ [Fact]
+ public void Nested_Or_Style_Can_Be_Added()
+ {
+ var parent = new Style(x => x.OfType());
+ var nested = new Style(x => Selectors.Or(
+ x.Nesting().Class("foo"),
+ x.Nesting().Class("bar")));
+
+ parent.Children.Add(nested);
+
+ Assert.Same(parent, nested.Parent);
+ }
+
+ [Fact]
+ public void Nested_Style_Without_Selector_Throws()
+ {
+ var parent = new Style(x => x.OfType());
+ var nested = new Style();
+
+ Assert.Throws(() => parent.Children.Add(nested));
+ }
+
+ [Fact(Skip = "TODO")]
+ public void Nested_Style_Without_Nesting_Operator_Throws()
+ {
+ var parent = new Style(x => x.OfType());
+ var nested = new Style(x => x.Class("foo"));
+
+ Assert.Throws(() => parent.Children.Add(nested));
+ }
+
private class Class1 : Control
{
public static readonly StyledProperty FooProperty =
diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs
index 6fbf024ff1..b41f37eb3d 100644
--- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs
+++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs
@@ -469,6 +469,144 @@ namespace Avalonia.Markup.UnitTests.Parsers
result);
}
+ [Fact]
+ public void Nesting_Class()
+ {
+ var result = SelectorGrammar.Parse("^.foo");
+
+ Assert.Equal(
+ new SelectorGrammar.ISyntax[]
+ {
+ new SelectorGrammar.NestingSyntax(),
+ new SelectorGrammar.ClassSyntax { Class = "foo" },
+ },
+ result);
+ }
+
+ [Fact]
+ public void Nesting_Child_Class()
+ {
+ var result = SelectorGrammar.Parse("^ > .foo");
+
+ Assert.Equal(
+ new SelectorGrammar.ISyntax[]
+ {
+ new SelectorGrammar.NestingSyntax(),
+ new SelectorGrammar.ChildSyntax { },
+ new SelectorGrammar.ClassSyntax { Class = "foo" },
+ },
+ result);
+ }
+
+ [Fact]
+ public void Nesting_Descendant_Class()
+ {
+ var result = SelectorGrammar.Parse("^ .foo");
+
+ Assert.Equal(
+ new SelectorGrammar.ISyntax[]
+ {
+ new SelectorGrammar.NestingSyntax(),
+ new SelectorGrammar.DescendantSyntax { },
+ new SelectorGrammar.ClassSyntax { Class = "foo" },
+ },
+ result);
+ }
+
+ [Fact]
+ public void Nesting_Template_Class()
+ {
+ var result = SelectorGrammar.Parse("^ /template/ .foo");
+
+ Assert.Equal(
+ new SelectorGrammar.ISyntax[]
+ {
+ new SelectorGrammar.NestingSyntax(),
+ new SelectorGrammar.TemplateSyntax { },
+ new SelectorGrammar.ClassSyntax { Class = "foo" },
+ },
+ result);
+ }
+
+ [Fact]
+ public void OfType_Template_Nesting()
+ {
+ var result = SelectorGrammar.Parse("Button /template/ ^");
+
+ Assert.Equal(
+ new SelectorGrammar.ISyntax[]
+ {
+ new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
+ new SelectorGrammar.TemplateSyntax { },
+ new SelectorGrammar.NestingSyntax(),
+ },
+ result);
+ }
+
+ [Fact]
+ public void Nesting_Property()
+ {
+ var result = SelectorGrammar.Parse("^[Foo=bar]");
+
+ Assert.Equal(
+ new SelectorGrammar.ISyntax[]
+ {
+ new SelectorGrammar.NestingSyntax(),
+ new SelectorGrammar.PropertySyntax { Property = "Foo", Value = "bar" },
+ },
+ result);
+ }
+
+ [Fact]
+ public void Not_Nesting()
+ {
+ var result = SelectorGrammar.Parse(":not(^)");
+
+ Assert.Equal(
+ new SelectorGrammar.ISyntax[]
+ {
+ new SelectorGrammar.NotSyntax
+ {
+ Argument = new[] { new SelectorGrammar.NestingSyntax() },
+ }
+ },
+ result);
+ }
+
+ [Fact]
+ public void Nesting_NthChild()
+ {
+ var result = SelectorGrammar.Parse("^:nth-child(2n+1)");
+
+ Assert.Equal(
+ new SelectorGrammar.ISyntax[]
+ {
+ new SelectorGrammar.NestingSyntax(),
+ new SelectorGrammar.NthChildSyntax()
+ {
+ Step = 2,
+ Offset = 1
+ }
+ },
+ result);
+ }
+
+ [Fact]
+ public void Nesting_Comma_Nesting_Class()
+ {
+ var result = SelectorGrammar.Parse("^, ^.foo");
+
+ Assert.Equal(
+ new SelectorGrammar.ISyntax[]
+ {
+ new SelectorGrammar.NestingSyntax(),
+ new SelectorGrammar.CommaSyntax(),
+ new SelectorGrammar.NestingSyntax(),
+ new SelectorGrammar.ClassSyntax { Class = "foo" },
+ },
+ result);
+ }
+
[Fact]
public void Namespace_Alone_Fails()
{
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
index 022ff0c3a4..bdd5cbbe2b 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
@@ -617,5 +617,37 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal(Colors.Red, ((ISolidColorBrush)foo.Background).Color);
}
}
+
+ [Fact]
+ public void Can_Use_Nested_Styles()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var xaml = @"
+
+
+
+
+
+
+
+
+
+";
+ var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+ var foo = window.FindControl("foo");
+
+ Assert.Null(foo.Background);
+
+ foo.Classes.Add("foo");
+
+ Assert.Equal(Colors.Red, ((ISolidColorBrush)foo.Background).Color);
+ }
+ }
}
}