From 98ba0f529bcbf708276b6fda48e4130e10a76751 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 15 Apr 2022 10:44:54 +0200 Subject: [PATCH 01/11] Initial implementation of nested styles. No XAML/parser support yet. --- src/Avalonia.Base/Styling/ChildSelector.cs | 4 +- .../Styling/DescendentSelector.cs | 4 +- src/Avalonia.Base/Styling/NestingSelector.cs | 29 ++++ src/Avalonia.Base/Styling/NotSelector.cs | 4 +- src/Avalonia.Base/Styling/NthChildSelector.cs | 2 +- src/Avalonia.Base/Styling/OrSelector.cs | 4 +- .../Styling/PropertyEqualsSelector.cs | 2 +- src/Avalonia.Base/Styling/Selector.cs | 31 ++-- src/Avalonia.Base/Styling/Selectors.cs | 9 ++ src/Avalonia.Base/Styling/Style.cs | 22 ++- src/Avalonia.Base/Styling/StyleChildren.cs | 29 ++++ src/Avalonia.Base/Styling/TemplateSelector.cs | 4 +- .../Styling/TypeNameAndClassSelector.cs | 2 +- .../Styling/SelectorTests_Nesting.cs | 132 ++++++++++++++++++ 14 files changed, 253 insertions(+), 25 deletions(-) create mode 100644 src/Avalonia.Base/Styling/NestingSelector.cs create mode 100644 src/Avalonia.Base/Styling/StyleChildren.cs create mode 100644 tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs 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; + } + } +} From a506737a0f72ee1e56face79bfdbdd6e1bb0ffdd Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 15 Apr 2022 11:16:12 +0200 Subject: [PATCH 02/11] Parse nesting selector. --- .../Markup/Parsers/SelectorGrammar.cs | 30 +++- .../Parsers/SelectorGrammarTests.cs | 139 ++++++++++++++++++ 2 files changed, 162 insertions(+), 7 deletions(-) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index a9fc18474c..6562afc7ff 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.Markup.UnitTests/Parsers/SelectorGrammarTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs index 6fbf024ff1..5c59101307 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs @@ -469,6 +469,145 @@ 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() { From a91bad4d3b7a9047c544876c8a4c8899980989a1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 15 Apr 2022 13:55:57 +0200 Subject: [PATCH 03/11] Remove nesting selector validation. Needs to be added later. --- src/Avalonia.Base/Styling/Selector.cs | 13 +-- src/Avalonia.Base/Styling/Selectors.cs | 4 - src/Avalonia.Base/Styling/Style.cs | 12 +- .../Styling/SelectorTests_Nesting.cs | 105 ++++++++++++------ .../Styling/StyleTests.cs | 42 +++++++ 5 files changed, 130 insertions(+), 46 deletions(-) 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 = From 646ce23bb4bdfdce57f9d9052e5904440e094dfa Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 15 Apr 2022 14:29:00 +0200 Subject: [PATCH 04/11] Initial XAML implementation of nested styles. --- src/Avalonia.Base/Styling/Style.cs | 8 +++++ .../AvaloniaXamlIlSelectorTransformer.cs | 24 ++++++++++++++ .../Parsers/SelectorGrammarTests.cs | 1 - .../Xaml/StyleTests.cs | 32 +++++++++++++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs index e79678b20b..6020dfe25f 100644 --- a/src/Avalonia.Base/Styling/Style.cs +++ b/src/Avalonia.Base/Styling/Style.cs @@ -120,6 +120,14 @@ namespace Avalonia.Styling instance.Start(); } + if (_children is not null) + { + foreach (var child in _children) + { + child.TryAttach(target, host); + } + } + return match.Result; } 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/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs index 5c59101307..685f9eab6f 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs @@ -573,7 +573,6 @@ namespace Avalonia.Markup.UnitTests.Parsers result); } - [Fact] public void Nesting_NthChild() { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 022ff0c3a4..9301647411 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); + } + } } } From 43a3841dcbdc224c528f95c9f25eb585b141c8fc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 15 Apr 2022 18:02:43 +0200 Subject: [PATCH 05/11] Add/remove resource owner. --- src/Avalonia.Base/Styling/StyleChildren.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Styling/StyleChildren.cs b/src/Avalonia.Base/Styling/StyleChildren.cs index 21ac3c4072..64d838e4ce 100644 --- a/src/Avalonia.Base/Styling/StyleChildren.cs +++ b/src/Avalonia.Base/Styling/StyleChildren.cs @@ -1,4 +1,5 @@ using System.Collections.ObjectModel; +using Avalonia.Controls; namespace Avalonia.Styling { @@ -16,7 +17,10 @@ namespace Avalonia.Styling protected override void RemoveItem(int index) { - (Items[index] as Style)?.SetParent(null); + var item = Items[index]; + if (_owner.Owner is IResourceHost host) + (item as IResourceProvider)?.RemoveOwner(host); + (item as Style)?.SetParent(null); base.RemoveItem(index); } @@ -24,6 +28,8 @@ namespace Avalonia.Styling { base.SetItem(index, item); (item as Style)?.SetParent(_owner); + if (_owner.Owner is IResourceHost host) + (item as IResourceProvider)?.AddOwner(host); } } } From 82835ef95708edaf80a4ac2406179192d2db3d59 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 21 Apr 2022 09:53:28 +0200 Subject: [PATCH 06/11] Use nested styles in Button template. And related fixes to make this work. --- src/Avalonia.Base/Styling/NestingSelector.cs | 2 +- src/Avalonia.Base/Styling/Style.cs | 8 +- .../Controls/Button.xaml | 80 ++++++++++--------- .../Styling/SelectorTests_Nesting.cs | 68 +++++++++++++++- 4 files changed, 116 insertions(+), 42 deletions(-) diff --git a/src/Avalonia.Base/Styling/NestingSelector.cs b/src/Avalonia.Base/Styling/NestingSelector.cs index 1be54dea3c..741eb7e9ca 100644 --- a/src/Avalonia.Base/Styling/NestingSelector.cs +++ b/src/Avalonia.Base/Styling/NestingSelector.cs @@ -17,7 +17,7 @@ namespace Avalonia.Styling { if (parent is Style s && s.Selector is Selector selector) { - return selector.Match(control, null, subscribe); + return selector.Match(control, (parent as Style)?.Parent, subscribe); } throw new InvalidOperationException( diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs index 6020dfe25f..7a83322355 100644 --- a/src/Avalonia.Base/Styling/Style.cs +++ b/src/Avalonia.Base/Styling/Style.cs @@ -120,15 +120,19 @@ namespace Avalonia.Styling instance.Start(); } + var result = match.Result; + if (_children is not null) { foreach (var child in _children) { - child.TryAttach(target, host); + var childResult = child.TryAttach(target, host); + if (childResult > result) + result = childResult; } } - return match.Result; + return result; } public bool TryGetResource(object key, out object? result) diff --git a/src/Avalonia.Themes.Fluent/Controls/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml index f545206a2f..282f575605 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/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs index eeb2fad996..f7e8793ede 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 Nesting_Class_Doesnt_Match_Parent_Selector() + public void Nesting_Class_Doesnt_Match_Parent_OfType_Selector() { var control = new Control2(); Style nested; @@ -26,7 +26,7 @@ namespace Avalonia.Base.UnitTests.Styling } [Fact] - public void Or_Nesting_Class_Doesnt_Match_Parent_Selector() + public void Or_Nesting_Class_Doesnt_Match_Parent_OfType_Selector() { var control = new Control2(); Style nested; @@ -45,7 +45,7 @@ namespace Avalonia.Base.UnitTests.Styling } [Fact] - public void Or_Nesting_Child_OfType_Does_Not_Match_Parent_Selector() + public void Or_Nesting_Child_OfType_Doesnt_Match_Parent_OfType_Selector() { var control = new Control1(); var panel = new DockPanel { Children = { control } }; @@ -64,6 +64,34 @@ namespace Avalonia.Base.UnitTests.Styling 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() { @@ -87,6 +115,40 @@ namespace Avalonia.Base.UnitTests.Styling 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() { From 33679377f8a806d30357510cf8451a126dd6a4e1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 19 May 2022 16:02:14 +0200 Subject: [PATCH 07/11] Validate presence of nesting selector. --- src/Avalonia.Base/Styling/ChildSelector.cs | 1 + .../Styling/DescendentSelector.cs | 1 + src/Avalonia.Base/Styling/NestingSelector.cs | 1 + src/Avalonia.Base/Styling/NotSelector.cs | 1 + src/Avalonia.Base/Styling/NthChildSelector.cs | 1 + src/Avalonia.Base/Styling/OrSelector.cs | 13 ++++++ .../Styling/PropertyEqualsSelector.cs | 1 + src/Avalonia.Base/Styling/Selector.cs | 2 + src/Avalonia.Base/Styling/Style.cs | 5 ++- src/Avalonia.Base/Styling/StyleChildren.cs | 6 +-- src/Avalonia.Base/Styling/TemplateSelector.cs | 1 + .../Styling/TypeNameAndClassSelector.cs | 1 + .../Styling/SelectorTests_Nesting.cs | 42 +++++++++++++++++++ 13 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Styling/ChildSelector.cs b/src/Avalonia.Base/Styling/ChildSelector.cs index 8675ca8820..34f3a76b61 100644 --- a/src/Avalonia.Base/Styling/ChildSelector.cs +++ b/src/Avalonia.Base/Styling/ChildSelector.cs @@ -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 b164ad1c69..4ffaff6861 100644 --- a/src/Avalonia.Base/Styling/DescendentSelector.cs +++ b/src/Avalonia.Base/Styling/DescendentSelector.cs @@ -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 index 741eb7e9ca..f1c797c768 100644 --- a/src/Avalonia.Base/Styling/NestingSelector.cs +++ b/src/Avalonia.Base/Styling/NestingSelector.cs @@ -25,5 +25,6 @@ namespace Avalonia.Styling } 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 c4beca6b9e..cdc3254d38 100644 --- a/src/Avalonia.Base/Styling/NotSelector.cs +++ b/src/Avalonia.Base/Styling/NotSelector.cs @@ -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 cbb5e64772..047bf434da 100644 --- a/src/Avalonia.Base/Styling/NthChildSelector.cs +++ b/src/Avalonia.Base/Styling/NthChildSelector.cs @@ -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 a34712caaf..913c27bf0c 100644 --- a/src/Avalonia.Base/Styling/OrSelector.cs +++ b/src/Avalonia.Base/Styling/OrSelector.cs @@ -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 34474fb7ab..7a37daf087 100644 --- a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs +++ b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs @@ -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 6f7f754b7b..1e06f3d375 100644 --- a/src/Avalonia.Base/Styling/Selector.cs +++ b/src/Avalonia.Base/Styling/Selector.cs @@ -86,6 +86,8 @@ namespace Avalonia.Styling /// protected abstract Selector? MovePrevious(); + internal abstract bool HasValidNestingSelector(); + private static SelectorMatch MatchUntilCombinator( IStyleable control, Selector start, diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs index 7a83322355..a8707e00c0 100644 --- a/src/Avalonia.Base/Styling/Style.cs +++ b/src/Avalonia.Base/Styling/Style.cs @@ -186,8 +186,9 @@ namespace Avalonia.Styling 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. + 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/StyleChildren.cs b/src/Avalonia.Base/Styling/StyleChildren.cs index 64d838e4ce..5f8635f155 100644 --- a/src/Avalonia.Base/Styling/StyleChildren.cs +++ b/src/Avalonia.Base/Styling/StyleChildren.cs @@ -11,23 +11,23 @@ namespace Avalonia.Styling protected override void InsertItem(int index, IStyle item) { - base.InsertItem(index, 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); - (item as Style)?.SetParent(null); base.RemoveItem(index); } protected override void SetItem(int index, IStyle item) { - base.SetItem(index, 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/TemplateSelector.cs b/src/Avalonia.Base/Styling/TemplateSelector.cs index 6f0f9e0900..b0a2dae8d6 100644 --- a/src/Avalonia.Base/Styling/TemplateSelector.cs +++ b/src/Avalonia.Base/Styling/TemplateSelector.cs @@ -49,5 +49,6 @@ namespace Avalonia.Styling } 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 ae9b14635f..24d5d6bbbf 100644 --- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs @@ -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/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs index f7e8793ede..c5f779cbbb 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs @@ -219,6 +219,48 @@ namespace Avalonia.Base.UnitTests.Styling Assert.Throws(() => nested.Selector.Match(control, parent)); } + [Fact] + public void Adding_Child_With_No_Nesting_Selector_Fails() + { + var control = new Control1(); + 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 control = new Control1(); + 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 control = new Control1(); + 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 control = new Control1(); + var parent = new Style(); + var child = new Style(x => x.Class("foo")); + + parent.Children.Add(child); + } + public class Control1 : Control { } From 556adb4bc7d421201ff33227cb554526ceb9171f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 19 May 2022 16:35:39 +0200 Subject: [PATCH 08/11] Use ^ as nesting selector. `&` causes problems in xml files. --- src/Avalonia.Base/Styling/NestingSelector.cs | 4 ++-- .../Controls/Button.xaml | 16 ++++++++-------- .../Markup/Parsers/SelectorGrammar.cs | 4 ++-- .../Parsers/SelectorGrammarTests.cs | 18 +++++++++--------- .../Xaml/StyleTests.cs | 2 +- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/Avalonia.Base/Styling/NestingSelector.cs b/src/Avalonia.Base/Styling/NestingSelector.cs index f1c797c768..481a937867 100644 --- a/src/Avalonia.Base/Styling/NestingSelector.cs +++ b/src/Avalonia.Base/Styling/NestingSelector.cs @@ -3,7 +3,7 @@ namespace Avalonia.Styling { /// - /// The `&` nesting style selector. + /// The `^` nesting style selector. /// internal class NestingSelector : Selector { @@ -11,7 +11,7 @@ namespace Avalonia.Styling public override bool IsCombinator => false; public override Type? TargetType => null; - public override string ToString() => "&"; + public override string ToString() => "^"; protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe) { diff --git a/src/Avalonia.Themes.Fluent/Controls/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml index 282f575605..a93fb6831d 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Button.xaml @@ -41,45 +41,45 @@ - - - - - - - From 3fccb1417459a5cb96fedb156895af11ec460a0c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 19 May 2022 16:43:58 +0200 Subject: [PATCH 09/11] Added style caching to Style children. Extracted out the caching code from `Styles` and reuse for `Style.Children`. --- src/Avalonia.Base/Styling/Style.cs | 11 +++-- src/Avalonia.Base/Styling/StyleCache.cs | 58 +++++++++++++++++++++++++ src/Avalonia.Base/Styling/Styles.cs | 41 ++--------------- 3 files changed, 66 insertions(+), 44 deletions(-) create mode 100644 src/Avalonia.Base/Styling/StyleCache.cs diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs index a8707e00c0..8fcf5eec8a 100644 --- a/src/Avalonia.Base/Styling/Style.cs +++ b/src/Avalonia.Base/Styling/Style.cs @@ -16,6 +16,7 @@ namespace Avalonia.Styling private IResourceDictionary? _resources; private List? _setters; private List? _animations; + private StyleCache? _childCache; /// /// Initializes a new instance of the class. @@ -124,12 +125,10 @@ namespace Avalonia.Styling if (_children is not null) { - foreach (var child in _children) - { - var childResult = child.TryAttach(target, host); - if (childResult > result) - result = childResult; - } + _childCache ??= new StyleCache(); + var childResult = _childCache.TryAttach(_children, target, host); + if (childResult > result) + result = childResult; } return result; 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/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); } /// From 0a7f34f4c681d0e445d91637cfc54108006c56d9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 19 May 2022 20:25:14 +0200 Subject: [PATCH 10/11] Use nested styles in CheckBox template. --- .../Controls/CheckBox.xaml | 603 +++++++++--------- 1 file changed, 315 insertions(+), 288 deletions(-) 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 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 55b19b445f210d579fad8105baaa9acbc3a12f9e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 20 May 2022 16:59:53 +0200 Subject: [PATCH 11/11] Remove unused vars. --- .../Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs index c5f779cbbb..d49fcf03a2 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs @@ -222,7 +222,6 @@ namespace Avalonia.Base.UnitTests.Styling [Fact] public void Adding_Child_With_No_Nesting_Selector_Fails() { - var control = new Control1(); var parent = new Style(x => x.OfType()); var child = new Style(x => x.Class("foo")); @@ -232,7 +231,6 @@ namespace Avalonia.Base.UnitTests.Styling [Fact] public void Adding_Combinator_Selector_Child_With_No_Nesting_Selector_Fails() { - var control = new Control1(); var parent = new Style(x => x.OfType()); var child = new Style(x => x.Class("foo").Descendant().Class("bar")); @@ -242,7 +240,6 @@ namespace Avalonia.Base.UnitTests.Styling [Fact] public void Adding_Or_Selector_Child_With_No_Nesting_Selector_Fails() { - var control = new Control1(); var parent = new Style(x => x.OfType()); var child = new Style(x => Selectors.Or( x.Nesting().Class("foo"), @@ -254,7 +251,6 @@ namespace Avalonia.Base.UnitTests.Styling [Fact] public void Can_Add_Child_Without_Nesting_Selector_To_Style_Without_Selector() { - var control = new Control1(); var parent = new Style(); var child = new Style(x => x.Class("foo"));