diff --git a/src/Avalonia.Base/Styling/ChildSelector.cs b/src/Avalonia.Base/Styling/ChildSelector.cs
index 5c92182b80..8675ca8820 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)
{
diff --git a/src/Avalonia.Base/Styling/DescendentSelector.cs b/src/Avalonia.Base/Styling/DescendentSelector.cs
index dde88b3436..b164ad1c69 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)
{
diff --git a/src/Avalonia.Base/Styling/NestingSelector.cs b/src/Avalonia.Base/Styling/NestingSelector.cs
new file mode 100644
index 0000000000..1be54dea3c
--- /dev/null
+++ b/src/Avalonia.Base/Styling/NestingSelector.cs
@@ -0,0 +1,29 @@
+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, null, subscribe);
+ }
+
+ throw new InvalidOperationException(
+ "Nesting selector was specified but cannot determine parent selector.");
+ }
+
+ protected override Selector? MovePrevious() => null;
+ }
+}
diff --git a/src/Avalonia.Base/Styling/NotSelector.cs b/src/Avalonia.Base/Styling/NotSelector.cs
index ab4e9d5d7f..c4beca6b9e 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)
{
diff --git a/src/Avalonia.Base/Styling/NthChildSelector.cs b/src/Avalonia.Base/Styling/NthChildSelector.cs
index aff34ea17c..cbb5e64772 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))
{
diff --git a/src/Avalonia.Base/Styling/OrSelector.cs b/src/Avalonia.Base/Styling/OrSelector.cs
index 3d6db9b01e..a34712caaf 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)
{
diff --git a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs
index 1cd1a650ef..34474fb7ab 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)
{
diff --git a/src/Avalonia.Base/Styling/Selector.cs b/src/Avalonia.Base/Styling/Selector.cs
index 0740e0f891..008ce7f3a5 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,12 +71,15 @@ 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.
@@ -83,13 +89,18 @@ namespace Avalonia.Styling
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 foundNested = false;
+ var result = Match(control, start, parent, subscribe, ref activators, ref combinator, ref foundNested);
+
+ if (parent is not null && !foundNested)
+ throw new InvalidOperationException("Nesting selector '&' must appear in child selector.");
return result == SelectorMatchResult.Sometimes ?
new SelectorMatch(activators.Get()) :
@@ -99,9 +110,11 @@ namespace Avalonia.Styling
private static SelectorMatchResult Match(
IStyleable control,
Selector selector,
+ IStyle? parent,
bool subscribe,
ref AndActivatorBuilder activators,
- ref Selector? combinator)
+ ref Selector? combinator,
+ ref bool foundNested)
{
var previous = selector.MovePrevious();
@@ -110,7 +123,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, ref foundNested);
if (previousMatch < SelectorMatchResult.Sometimes)
{
@@ -118,8 +131,10 @@ namespace Avalonia.Styling
}
}
+ foundNested |= selector is NestingSelector;
+
// 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..a036c140c2 100644
--- a/src/Avalonia.Base/Styling/Selectors.cs
+++ b/src/Avalonia.Base/Styling/Selectors.cs
@@ -109,6 +109,15 @@ namespace Avalonia.Styling
}
}
+ public static Selector Nesting(this Selector? previous)
+ {
+ if (previous is not null)
+ throw new InvalidOperationException(
+ "Nesting selector '&' must appear at the start of the style selector.");
+
+ 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..a3a7ea29d3 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,6 +12,7 @@ namespace Avalonia.Styling
public class Style : AvaloniaObject, IStyle, IResourceProvider
{
private IResourceHost? _owner;
+ private StyleChildren? _children;
private IResourceDictionary? _resources;
private List? _setters;
private List? _animations;
@@ -34,6 +33,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 +54,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 +102,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 +110,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))
@@ -156,5 +168,7 @@ namespace Avalonia.Styling
_resources?.RemoveOwner(owner);
}
}
+
+ internal void SetParent(Style? parent) => Parent = parent;
}
}
diff --git a/src/Avalonia.Base/Styling/StyleChildren.cs b/src/Avalonia.Base/Styling/StyleChildren.cs
new file mode 100644
index 0000000000..21ac3c4072
--- /dev/null
+++ b/src/Avalonia.Base/Styling/StyleChildren.cs
@@ -0,0 +1,29 @@
+using System.Collections.ObjectModel;
+
+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)
+ {
+ base.InsertItem(index, item);
+ (item as Style)?.SetParent(_owner);
+ }
+
+ protected override void RemoveItem(int index)
+ {
+ (Items[index] as Style)?.SetParent(null);
+ base.RemoveItem(index);
+ }
+
+ protected override void SetItem(int index, IStyle item)
+ {
+ base.SetItem(index, item);
+ (item as Style)?.SetParent(_owner);
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Styling/TemplateSelector.cs b/src/Avalonia.Base/Styling/TemplateSelector.cs
index e8051efa6d..6f0f9e0900 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,7 +45,7 @@ namespace Avalonia.Styling
return SelectorMatch.NeverThisInstance;
}
- return _parent.Match(templatedParent, subscribe);
+ return _parent.Match(templatedParent, parent, subscribe);
}
protected override Selector? MovePrevious() => null;
diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
index ef48c4a8cd..ae9b14635f 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)
{
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..cee7e748ba
--- /dev/null
+++ b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs
@@ -0,0 +1,132 @@
+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 Parent_Selector_Doesnt_Match_OfType()
+ {
+ 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 Nested_Class_Selector()
+ {
+ 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 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 Nesting_Must_Appear_At_Start_Of_Selector()
+ {
+ var control = new Control1();
+ Assert.Throws(() => new Style(x => x.OfType().Nesting()));
+ }
+
+ [Fact]
+ public void Nesting_Must_Appear()
+ {
+ var control = new Control1();
+ Style nested;
+ var parent = new Style
+ {
+ Children =
+ {
+ (nested = new Style(x => x.OfType().Class("foo"))),
+ }
+ };
+
+ Assert.Throws(() => nested.Selector.Match(control, parent));
+ }
+
+ [Fact]
+ public void Nesting_Must_Appear_In_All_Or_Arguments()
+ {
+ var control = new Control1();
+ Style nested;
+ var parent = new Style(x => x.OfType())
+ {
+ Children =
+ {
+ (nested = new Style(x => Selectors.Or(
+ x.Nesting().Class("foo"),
+ x.Class("bar"))))
+ }
+ };
+
+ Assert.Throws(() => nested.Selector.Match(control, parent));
+ }
+
+ 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;
+ }
+ }
+}