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; 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; var controlParent = ((ILogical)control).LogicalParent;
if (controlParent != null) if (controlParent != null)
{ {
var parentMatch = _parent.Match((IStyleable)controlParent, subscribe); var parentMatch = _parent.Match((IStyleable)controlParent, parent, subscribe);
if (parentMatch.Result == SelectorMatchResult.Sometimes) if (parentMatch.Result == SelectorMatchResult.Sometimes)
{ {

4
src/Avalonia.Base/Styling/DescendentSelector.cs

@ -35,7 +35,7 @@ namespace Avalonia.Styling
return _selectorString; 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 c = (ILogical)control;
var descendantMatches = new OrActivatorBuilder(); var descendantMatches = new OrActivatorBuilder();
@ -46,7 +46,7 @@ namespace Avalonia.Styling
if (c is IStyleable) if (c is IStyleable)
{ {
var match = _parent.Match((IStyleable)c, subscribe); var match = _parent.Match((IStyleable)c, parent, subscribe);
if (match.Result == SelectorMatchResult.Sometimes) 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; 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) switch (innerResult.Result)
{ {

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

@ -48,7 +48,7 @@ namespace Avalonia.Styling
public int Step { get; } public int Step { get; }
public int Offset { 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)) if (!(control is ILogical logical))
{ {

4
src/Avalonia.Base/Styling/OrSelector.cs

@ -65,14 +65,14 @@ namespace Avalonia.Styling
return _selectorString; 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 activators = new OrActivatorBuilder();
var neverThisInstance = false; var neverThisInstance = false;
foreach (var selector in _selectors) foreach (var selector in _selectors)
{ {
var match = selector.Match(control, subscribe); var match = selector.Match(control, parent, subscribe);
switch (match.Result) switch (match.Result)
{ {

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

@ -74,7 +74,7 @@ namespace Avalonia.Styling
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe)
{ {
if (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. /// Tries to match the selector with a control.
/// </summary> /// </summary>
/// <param name="control">The control.</param> /// <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"> /// <param name="subscribe">
/// Whether the match should subscribe to changes in order to track the match over time, /// Whether the match should subscribe to changes in order to track the match over time,
/// or simply return an immediate result. /// or simply return an immediate result.
/// </param> /// </param>
/// <returns>A <see cref="SelectorMatch"/>.</returns> /// <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 // 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 // right-to-left, so MatchUntilCombinator reverses this order because the type selector
// will be on the left. // 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 the pre-combinator selector matches, we can now match the combinator, if any.
if (match.IsMatch && combinator is object) 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 // 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 // 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. /// Evaluates the selector for a match.
/// </summary> /// </summary>
/// <param name="control">The control.</param> /// <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"> /// <param name="subscribe">
/// Whether the match should subscribe to changes in order to track the match over time, /// Whether the match should subscribe to changes in order to track the match over time,
/// or simply return an immediate result. /// or simply return an immediate result.
/// </param> /// </param>
/// <returns>A <see cref="SelectorMatch"/>.</returns> /// <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> /// <summary>
/// Moves to the previous selector. /// Moves to the previous selector.
@ -83,13 +89,18 @@ namespace Avalonia.Styling
private static SelectorMatch MatchUntilCombinator( private static SelectorMatch MatchUntilCombinator(
IStyleable control, IStyleable control,
Selector start, Selector start,
IStyle? parent,
bool subscribe, bool subscribe,
out Selector? combinator) out Selector? combinator)
{ {
combinator = null; combinator = null;
var activators = new AndActivatorBuilder(); 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 ? return result == SelectorMatchResult.Sometimes ?
new SelectorMatch(activators.Get()) : new SelectorMatch(activators.Get()) :
@ -99,9 +110,11 @@ namespace Avalonia.Styling
private static SelectorMatchResult Match( private static SelectorMatchResult Match(
IStyleable control, IStyleable control,
Selector selector, Selector selector,
IStyle? parent,
bool subscribe, bool subscribe,
ref AndActivatorBuilder activators, ref AndActivatorBuilder activators,
ref Selector? combinator) ref Selector? combinator,
ref bool foundNested)
{ {
var previous = selector.MovePrevious(); var previous = selector.MovePrevious();
@ -110,7 +123,7 @@ namespace Avalonia.Styling
// opportunity to exit early. // opportunity to exit early.
if (previous != null && !previous.IsCombinator) 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) if (previousMatch < SelectorMatchResult.Sometimes)
{ {
@ -118,8 +131,10 @@ namespace Avalonia.Styling
} }
} }
foundNested |= selector is NestingSelector;
// Match this selector. // Match this selector.
var match = selector.Evaluate(control, subscribe); var match = selector.Evaluate(control, parent, subscribe);
if (!match.IsMatch) 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> /// <summary>
/// Returns a selector which inverts the results of selector argument. /// Returns a selector which inverts the results of selector argument.
/// </summary> /// </summary>

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

@ -4,8 +4,6 @@ using Avalonia.Animation;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Metadata; using Avalonia.Metadata;
#nullable enable
namespace Avalonia.Styling namespace Avalonia.Styling
{ {
/// <summary> /// <summary>
@ -14,6 +12,7 @@ namespace Avalonia.Styling
public class Style : AvaloniaObject, IStyle, IResourceProvider public class Style : AvaloniaObject, IStyle, IResourceProvider
{ {
private IResourceHost? _owner; private IResourceHost? _owner;
private StyleChildren? _children;
private IResourceDictionary? _resources; private IResourceDictionary? _resources;
private List<ISetter>? _setters; private List<ISetter>? _setters;
private List<IAnimation>? _animations; private List<IAnimation>? _animations;
@ -34,6 +33,14 @@ namespace Avalonia.Styling
Selector = selector(null); 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 public IResourceHost? Owner
{ {
get => _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> /// <summary>
/// Gets or sets a dictionary of style resources. /// Gets or sets a dictionary of style resources.
/// </summary> /// </summary>
@ -90,7 +102,7 @@ namespace Avalonia.Styling
public IList<IAnimation> Animations => _animations ??= new List<IAnimation>(); public IList<IAnimation> Animations => _animations ??= new List<IAnimation>();
bool IResourceNode.HasResources => _resources?.Count > 0; 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; public event EventHandler? OwnerChanged;
@ -98,7 +110,7 @@ namespace Avalonia.Styling
{ {
target = target ?? throw new ArgumentNullException(nameof(target)); 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; target == host ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance;
if (match.IsMatch && (_setters is object || _animations is object)) if (match.IsMatch && (_setters is object || _animations is object))
@ -156,5 +168,7 @@ namespace Avalonia.Styling
_resources?.RemoveOwner(owner); _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; 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; var templatedParent = control.TemplatedParent as IStyleable;
@ -45,7 +45,7 @@ namespace Avalonia.Styling
return SelectorMatch.NeverThisInstance; return SelectorMatch.NeverThisInstance;
} }
return _parent.Match(templatedParent, subscribe); return _parent.Match(templatedParent, parent, subscribe);
} }
protected override Selector? MovePrevious() => null; protected override Selector? MovePrevious() => null;

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

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