diff --git a/src/Avalonia.Base/Styling/Selector.cs b/src/Avalonia.Base/Styling/Selector.cs index 008ce7f3a5..6f7f754b7b 100644 --- a/src/Avalonia.Base/Styling/Selector.cs +++ b/src/Avalonia.Base/Styling/Selector.cs @@ -96,11 +96,7 @@ namespace Avalonia.Styling combinator = null; var activators = new AndActivatorBuilder(); - 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."); + var result = Match(control, start, parent, subscribe, ref activators, ref combinator); return result == SelectorMatchResult.Sometimes ? new SelectorMatch(activators.Get()) : @@ -113,8 +109,7 @@ namespace Avalonia.Styling IStyle? parent, bool subscribe, ref AndActivatorBuilder activators, - ref Selector? combinator, - ref bool foundNested) + ref Selector? combinator) { var previous = selector.MovePrevious(); @@ -123,7 +118,7 @@ namespace Avalonia.Styling // opportunity to exit early. if (previous != null && !previous.IsCombinator) { - var previousMatch = Match(control, previous, parent, subscribe, ref activators, ref combinator, ref foundNested); + var previousMatch = Match(control, previous, parent, subscribe, ref activators, ref combinator); if (previousMatch < SelectorMatchResult.Sometimes) { @@ -131,8 +126,6 @@ namespace Avalonia.Styling } } - foundNested |= selector is NestingSelector; - // Match this selector. var match = selector.Evaluate(control, parent, subscribe); diff --git a/src/Avalonia.Base/Styling/Selectors.cs b/src/Avalonia.Base/Styling/Selectors.cs index a036c140c2..476d86cd11 100644 --- a/src/Avalonia.Base/Styling/Selectors.cs +++ b/src/Avalonia.Base/Styling/Selectors.cs @@ -111,10 +111,6 @@ 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(); } diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs index a3a7ea29d3..e79678b20b 100644 --- a/src/Avalonia.Base/Styling/Style.cs +++ b/src/Avalonia.Base/Styling/Style.cs @@ -169,6 +169,16 @@ namespace Avalonia.Styling } } - internal void SetParent(Style? parent) => Parent = parent; + internal void SetParent(Style? parent) + { + if (parent?.Selector is not null) + { + if (Selector is null) + throw new InvalidOperationException("Nested styles must have a selector."); + // TODO: Validate that selector contains & in the right place. + } + + Parent = parent; + } } } diff --git a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs index cee7e748ba..eeb2fad996 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs @@ -9,7 +9,7 @@ namespace Avalonia.Base.UnitTests.Styling public class SelectorTests_Nesting { [Fact] - public void Parent_Selector_Doesnt_Match_OfType() + public void Nesting_Class_Doesnt_Match_Parent_Selector() { var control = new Control2(); Style nested; @@ -26,43 +26,50 @@ namespace Avalonia.Base.UnitTests.Styling } [Fact] - public void Nested_Class_Selector() + public void Or_Nesting_Class_Doesnt_Match_Parent_Selector() { - var control = new Control1 { Classes = { "foo" } }; + var control = new Control2(); Style nested; var parent = new Style(x => x.OfType()) { Children = { - (nested = new Style(x => x.Nesting().Class("foo"))), + (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); + Assert.Equal(SelectorMatchResult.NeverThisType, match.Result); } [Fact] - public void Nesting_With_No_Parent_Style_Fails() + public void Or_Nesting_Child_OfType_Does_Not_Match_Parent_Selector() { var control = new Control1(); - var style = new Style(x => x.Nesting().OfType()); + 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()))), + } + }; - Assert.Throws(() => style.Selector.Match(control, null)); + var match = nested.Selector.Match(control, parent); + Assert.Equal(SelectorMatchResult.NeverThisInstance, match.Result); } [Fact] - public void Nesting_With_No_Parent_Selector_Fails() + public void Nesting_Class_Matches() { - var control = new Control1(); + var control = new Control1 { Classes = { "foo" } }; Style nested; - var parent = new Style + var parent = new Style(x => x.OfType()) { Children = { @@ -70,44 +77,80 @@ namespace Avalonia.Base.UnitTests.Styling } }; - Assert.Throws(() => nested.Selector.Match(control, parent)); + 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_Must_Appear_At_Start_Of_Selector() + public void Or_Nesting_Class_Matches() { - var control = new Control1(); - Assert.Throws(() => new Style(x => x.OfType().Nesting())); + 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 Nesting_Must_Appear() + public void Or_Nesting_Child_OfType_Matches() { - var control = new Control1(); + var control = new Control1 { Classes = { "foo" } }; + var panel = new Panel { Children = { control } }; Style nested; - var parent = new Style + var parent = new Style(x => x.OfType()) { Children = { - (nested = new Style(x => x.OfType().Class("foo"))), + (nested = new Style(x => Selectors.Or( + x.Nesting().Child().OfType(), + x.Nesting().Child().OfType()))), } }; - Assert.Throws(() => nested.Selector.Match(control, parent)); + var match = nested.Selector.Match(control, parent); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, match.Result); } [Fact] - public void Nesting_Must_Appear_In_All_Or_Arguments() + 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(x => x.OfType()) + var parent = new Style { Children = { - (nested = new Style(x => Selectors.Or( - x.Nesting().Class("foo"), - x.Class("bar")))) + (nested = new Style(x => x.Nesting().Class("foo"))), } }; 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 =