diff --git a/src/Avalonia.Styling/Styling/OrSelector.cs b/src/Avalonia.Styling/Styling/OrSelector.cs
new file mode 100644
index 0000000000..58c5c778fb
--- /dev/null
+++ b/src/Avalonia.Styling/Styling/OrSelector.cs
@@ -0,0 +1,131 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+
+namespace Avalonia.Styling
+{
+ ///
+ /// The OR style selector.
+ ///
+ internal class OrSelector : Selector
+ {
+ private readonly IReadOnlyList _selectors;
+ private string _selectorString;
+ private Type _targetType;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The selectors to OR.
+ public OrSelector(IReadOnlyList selectors)
+ {
+ Contract.Requires(selectors != null);
+ Contract.Requires(selectors.Count > 1);
+
+ _selectors = selectors;
+ }
+
+ ///
+ public override bool InTemplate => false;
+
+ ///
+ public override bool IsCombinator => false;
+
+ ///
+ public override Type TargetType
+ {
+ get
+ {
+ if (_targetType == null)
+ {
+ _targetType = EvaluateTargetType();
+ }
+
+ return _targetType;
+ }
+ }
+
+ ///
+ public override string ToString()
+ {
+ if (_selectorString == null)
+ {
+ _selectorString = string.Join(", ", _selectors);
+ }
+
+ return _selectorString;
+ }
+
+ protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
+ {
+ var activators = new List>();
+ var neverThisInstance = false;
+
+ foreach (var selector in _selectors)
+ {
+ var match = selector.Match(control, subscribe);
+
+ switch (match.Result)
+ {
+ case SelectorMatchResult.AlwaysThisType:
+ case SelectorMatchResult.AlwaysThisInstance:
+ return match;
+ case SelectorMatchResult.NeverThisInstance:
+ neverThisInstance = true;
+ break;
+ case SelectorMatchResult.Sometimes:
+ activators.Add(match.Activator);
+ break;
+ }
+ }
+
+ if (activators.Count > 1)
+ {
+ return new SelectorMatch(StyleActivator.Or(activators));
+ }
+ else if (activators.Count == 1)
+ {
+ return new SelectorMatch(activators[0]);
+ }
+ else if (neverThisInstance)
+ {
+ return SelectorMatch.NeverThisInstance;
+ }
+ else
+ {
+ return SelectorMatch.NeverThisType;
+ }
+ }
+
+ protected override Selector MovePrevious() => null;
+
+ private Type EvaluateTargetType()
+ {
+ var result = default(Type);
+
+ foreach (var selector in _selectors)
+ {
+ if (selector.TargetType == null)
+ {
+ return null;
+ }
+ else if (result == null)
+ {
+ result = selector.TargetType;
+ }
+ else
+ {
+ while (!result.IsAssignableFrom(selector.TargetType))
+ {
+ result = result.BaseType;
+ }
+ }
+ }
+
+ return result;
+ }
+ }
+}
+
diff --git a/src/Avalonia.Styling/Styling/Selectors.cs b/src/Avalonia.Styling/Styling/Selectors.cs
index deb677e04c..3e7a30d389 100644
--- a/src/Avalonia.Styling/Styling/Selectors.cs
+++ b/src/Avalonia.Styling/Styling/Selectors.cs
@@ -2,6 +2,8 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
+using System.Collections.Generic;
+using System.Linq;
namespace Avalonia.Styling
{
@@ -137,6 +139,26 @@ namespace Avalonia.Styling
return previous.OfType(typeof(T));
}
+ ///
+ /// Returns a selector which ORs selectors.
+ ///
+ /// The selectors to be OR'd.
+ /// The selector.
+ public static Selector Or(params Selector[] selectors)
+ {
+ return new OrSelector(selectors);
+ }
+
+ ///
+ /// Returns a selector which ORs selectors.
+ ///
+ /// The selectors to be OR'd.
+ /// The selector.
+ public static Selector Or(IReadOnlyList selectors)
+ {
+ return new OrSelector(selectors);
+ }
+
///
/// Returns a selector which matches a control with the specified property value.
///
diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
index 55c3aab81f..e11e333a49 100644
--- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
+++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
@@ -49,7 +49,7 @@ namespace Avalonia.Markup.Parsers
state = ParseStart(ref r);
break;
case State.Middle:
- state = ParseMiddle(ref r, end);
+ (state, syntax) = ParseMiddle(ref r, end);
break;
case State.CanHaveType:
state = ParseCanHaveType(ref r);
@@ -113,33 +113,37 @@ namespace Avalonia.Markup.Parsers
return State.TypeName;
}
- private static State ParseMiddle(ref CharacterReader r, char? end)
+ private static (State, ISyntax) ParseMiddle(ref CharacterReader r, char? end)
{
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(char.IsWhiteSpace) || r.Peek == '>')
{
- return State.Traversal;
+ return (State.Traversal, null);
}
else if (r.TakeIf('/'))
{
- return State.Template;
+ return (State.Template, null);
}
else if (r.TakeIf('#'))
{
- return State.Name;
+ return (State.Name, null);
+ }
+ else if (r.TakeIf(','))
+ {
+ return (State.Start, new CommaSyntax());
}
else if (end.HasValue && !r.End && r.Peek == end.Value)
{
- return State.End;
+ return (State.End, null);
}
- return State.TypeName;
+ return (State.TypeName, null);
}
private static State ParseCanHaveType(ref CharacterReader r)
@@ -415,5 +419,13 @@ namespace Avalonia.Markup.Parsers
return (obj is NotSyntax not) && Argument.SequenceEqual(not.Argument);
}
}
+
+ public class CommaSyntax : ISyntax
+ {
+ public override bool Equals(object obj)
+ {
+ return obj is CommaSyntax or;
+ }
+ }
}
}
diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
index 8137ac3f48..493579d676 100644
--- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
+++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
@@ -43,6 +43,7 @@ namespace Avalonia.Markup.Parsers
private Selector Create(IEnumerable syntax)
{
var result = default(Selector);
+ var results = default(List);
foreach (var i in syntax)
{
@@ -106,11 +107,30 @@ namespace Avalonia.Markup.Parsers
case SelectorGrammar.NotSyntax not:
result = result.Not(x => Create(not.Argument));
break;
+ case SelectorGrammar.CommaSyntax comma:
+ if (results == null)
+ {
+ results = new List();
+ }
+
+ results.Add(result);
+ result = null;
+ break;
default:
throw new NotSupportedException($"Unsupported selector grammar '{i.GetType()}'.");
}
}
+ if (results != null)
+ {
+ if (result != null)
+ {
+ results.Add(result);
+ }
+
+ result = results.Count > 1 ? Selectors.Or(results) : results[0];
+ }
+
return result;
}
diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs
index e3ce4b0968..216043aa20 100644
--- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs
+++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs
@@ -261,6 +261,22 @@ namespace Avalonia.Markup.UnitTests.Parsers
result);
}
+ [Fact]
+ public void OfType_Comma_Is_Class()
+ {
+ var result = SelectorGrammar.Parse("TextBlock, :is(Button).foo");
+
+ Assert.Equal(
+ new SelectorGrammar.ISyntax[]
+ {
+ new SelectorGrammar.OfTypeSyntax { TypeName = "TextBlock" },
+ new SelectorGrammar.CommaSyntax(),
+ new SelectorGrammar.IsSyntax { TypeName = "Button" },
+ new SelectorGrammar.ClassSyntax { Class = "foo" },
+ },
+ result);
+ }
+
[Fact]
public void Namespace_Alone_Fails()
{
diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs
index 1b1a96a7e2..1c0cba56c9 100644
--- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs
+++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs
@@ -14,6 +14,13 @@ namespace Avalonia.Markup.UnitTests.Parsers
var result = target.Parse("TextBlock[IsPointerOver=True]");
}
+ [Fact]
+ public void Parses_Comma_Separated_Selectors()
+ {
+ var target = new SelectorParser((ns, type) => typeof(TextBlock));
+ var result = target.Parse("TextBlock, TextBlock:foo");
+ }
+
[Fact]
public void Throws_If_OfType_Type_Not_Found()
{
diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Or.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Or.cs
new file mode 100644
index 0000000000..521c73ce27
--- /dev/null
+++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Or.cs
@@ -0,0 +1,106 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Xunit;
+
+namespace Avalonia.Styling.UnitTests
+{
+ public class SelectorTests_Or
+ {
+ [Fact]
+ public void Or_Selector_Should_Have_Correct_String_Representation()
+ {
+ var target = Selectors.Or(
+ default(Selector).OfType().Class("foo"),
+ default(Selector).OfType().Class("bar"));
+
+ Assert.Equal("Control1.foo, Control2.bar", target.ToString());
+ }
+
+ [Fact]
+ public void Or_Selector_Matches_Control_Of_Correct_Type()
+ {
+ var target = Selectors.Or(
+ default(Selector).OfType(),
+ default(Selector).OfType().Class("bar"));
+ var control = new Control1();
+
+ Assert.Equal(SelectorMatchResult.AlwaysThisType, target.Match(control).Result);
+ }
+
+ [Fact]
+ public void Or_Selector_Matches_Control_Of_Correct_Type_With_Class()
+ {
+ var target = Selectors.Or(
+ default(Selector).OfType(),
+ default(Selector).OfType().Class("bar"));
+ var control = new Control2();
+
+ Assert.Equal(SelectorMatchResult.Sometimes, target.Match(control).Result);
+ }
+
+ [Fact]
+ public void Or_Selector_Doesnt_Match_Control_Of_Incorrect_Type()
+ {
+ var target = Selectors.Or(
+ default(Selector).OfType(),
+ default(Selector).OfType().Class("bar"));
+ var control = new Control3();
+
+ Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(control).Result);
+ }
+
+ [Fact]
+ public void Or_Selector_Doesnt_Match_Control_With_Incorrect_Name()
+ {
+ var target = Selectors.Or(
+ default(Selector).OfType().Name("foo"),
+ default(Selector).OfType().Name("foo"));
+ var control = new Control1 { Name = "bar" };
+
+ Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(control).Result);
+ }
+
+ [Fact]
+ public void Returns_Correct_TargetType_When_Types_Same()
+ {
+ var target = Selectors.Or(
+ default(Selector).OfType().Class("foo"),
+ default(Selector).OfType().Class("bar"));
+
+ Assert.Equal(typeof(Control1), target.TargetType);
+ }
+
+ [Fact]
+ public void Returns_Common_TargetType()
+ {
+ var target = Selectors.Or(
+ default(Selector).OfType().Class("foo"),
+ default(Selector).OfType().Class("bar"));
+
+ Assert.Equal(typeof(TestControlBase), target.TargetType);
+ }
+
+ [Fact]
+ public void Returns_Null_TargetType_When_A_Selector_Has_No_TargetType()
+ {
+ var target = Selectors.Or(
+ default(Selector).OfType().Class("foo"),
+ default(Selector).Class("bar"));
+
+ Assert.Equal(null, target.TargetType);
+ }
+
+ public class Control1 : TestControlBase
+ {
+ }
+
+ public class Control2 : TestControlBase
+ {
+ }
+
+ public class Control3 : TestControlBase
+ {
+ }
+ }
+}