Browse Source

Initial implementation of nested styles.

No XAML/parser support yet.
pull/8024/head
Steven Kirk 4 years ago
parent
commit
98ba0f529b
  1. 4
      src/Avalonia.Base/Styling/ChildSelector.cs
  2. 4
      src/Avalonia.Base/Styling/DescendentSelector.cs
  3. 29
      src/Avalonia.Base/Styling/NestingSelector.cs
  4. 4
      src/Avalonia.Base/Styling/NotSelector.cs
  5. 2
      src/Avalonia.Base/Styling/NthChildSelector.cs
  6. 4
      src/Avalonia.Base/Styling/OrSelector.cs
  7. 2
      src/Avalonia.Base/Styling/PropertyEqualsSelector.cs
  8. 31
      src/Avalonia.Base/Styling/Selector.cs
  9. 9
      src/Avalonia.Base/Styling/Selectors.cs
  10. 22
      src/Avalonia.Base/Styling/Style.cs
  11. 29
      src/Avalonia.Base/Styling/StyleChildren.cs
  12. 4
      src/Avalonia.Base/Styling/TemplateSelector.cs
  13. 2
      src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
  14. 132
      tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Nesting.cs

4
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)
{

4
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)
{

29
src/Avalonia.Base/Styling/NestingSelector.cs

@ -0,0 +1,29 @@
using System;
namespace Avalonia.Styling
{
/// <summary>
/// The `&amp;` nesting style selector.
/// </summary>
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;
}
}

4
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)
{

2
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))
{

4
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)
{

2
src/Avalonia.Base/Styling/PropertyEqualsSelector.cs

@ -74,7 +74,7 @@ namespace Avalonia.Styling
}
/// <inheritdoc/>
protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe)
{
if (subscribe)
{

31
src/Avalonia.Base/Styling/Selector.cs

@ -33,22 +33,25 @@ namespace Avalonia.Styling
/// Tries to match the selector with a control.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="parent">
/// The parent style, if the style containing the selector is a nested style.
/// </param>
/// <param name="subscribe">
/// Whether the match should subscribe to changes in order to track the match over time,
/// or simply return an immediate result.
/// </param>
/// <returns>A <see cref="SelectorMatch"/>.</returns>
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.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="parent">
/// The parent style, if the style containing the selector is a nested style.
/// </param>
/// <param name="subscribe">
/// Whether the match should subscribe to changes in order to track the match over time,
/// or simply return an immediate result.
/// </param>
/// <returns>A <see cref="SelectorMatch"/>.</returns>
protected abstract SelectorMatch Evaluate(IStyleable control, bool subscribe);
protected abstract SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe);
/// <summary>
/// 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)
{

9
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();
}
/// <summary>
/// Returns a selector which inverts the results of selector argument.
/// </summary>

22
src/Avalonia.Base/Styling/Style.cs

@ -4,8 +4,6 @@ using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Metadata;
#nullable enable
namespace Avalonia.Styling
{
/// <summary>
@ -14,6 +12,7 @@ namespace Avalonia.Styling
public class Style : AvaloniaObject, IStyle, IResourceProvider
{
private IResourceHost? _owner;
private StyleChildren? _children;
private IResourceDictionary? _resources;
private List<ISetter>? _setters;
private List<IAnimation>? _animations;
@ -34,6 +33,14 @@ namespace Avalonia.Styling
Selector = selector(null);
}
/// <summary>
/// Gets the children of the style.
/// </summary>
public IList<IStyle> Children => _children ??= new(this);
/// <summary>
/// Gets the <see cref="StyledElement"/> or Application that hosts the style.
/// </summary>
public IResourceHost? Owner
{
get => _owner;
@ -47,6 +54,11 @@ namespace Avalonia.Styling
}
}
/// <summary>
/// Gets the parent style if this style is hosted in a <see cref="Style.Children"/> collection.
/// </summary>
public Style? Parent { get; private set; }
/// <summary>
/// Gets or sets a dictionary of style resources.
/// </summary>
@ -90,7 +102,7 @@ namespace Avalonia.Styling
public IList<IAnimation> Animations => _animations ??= new List<IAnimation>();
bool IResourceNode.HasResources => _resources?.Count > 0;
IReadOnlyList<IStyle> IStyle.Children => Array.Empty<IStyle>();
IReadOnlyList<IStyle> IStyle.Children => (IReadOnlyList<IStyle>?)_children ?? Array.Empty<IStyle>();
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;
}
}

29
src/Avalonia.Base/Styling/StyleChildren.cs

@ -0,0 +1,29 @@
using System.Collections.ObjectModel;
namespace Avalonia.Styling
{
internal class StyleChildren : Collection<IStyle>
{
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);
}
}
}

4
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;

2
src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs

@ -94,7 +94,7 @@ namespace Avalonia.Styling
}
/// <inheritdoc/>
protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe)
{
if (TargetType != null)
{

132
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<Control1>())
{
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<Control1>())
{
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<Control1>());
Assert.Throws<InvalidOperationException>(() => 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<InvalidOperationException>(() => nested.Selector.Match(control, parent));
}
[Fact]
public void Nesting_Must_Appear_At_Start_Of_Selector()
{
var control = new Control1();
Assert.Throws<InvalidOperationException>(() => new Style(x => x.OfType<Control1>().Nesting()));
}
[Fact]
public void Nesting_Must_Appear()
{
var control = new Control1();
Style nested;
var parent = new Style
{
Children =
{
(nested = new Style(x => x.OfType<Control1>().Class("foo"))),
}
};
Assert.Throws<InvalidOperationException>(() => 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<Control1>())
{
Children =
{
(nested = new Style(x => Selectors.Or(
x.Nesting().Class("foo"),
x.Class("bar"))))
}
};
Assert.Throws<InvalidOperationException>(() => 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;
}
}
}
Loading…
Cancel
Save