Browse Source

andd and and or queries

pull/7938/head
Emmanuel Hansen 3 years ago
parent
commit
6f16d57efd
  1. 7
      samples/ControlCatalog/MainView.xaml
  2. 69
      src/Avalonia.Base/Styling/Activators/AndQueryActivator.cs
  3. 51
      src/Avalonia.Base/Styling/Activators/AndQueryActivatorBuilder.cs
  4. 63
      src/Avalonia.Base/Styling/Activators/OrQueryActivator.cs
  5. 51
      src/Avalonia.Base/Styling/Activators/OrQueryActivatorBuilder.cs
  6. 100
      src/Avalonia.Base/Styling/AndQuery.cs
  7. 7
      src/Avalonia.Base/Styling/OrQuery.cs
  8. 20
      src/Avalonia.Base/Styling/Queries.cs
  9. 94
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlQueryTransformer.cs
  10. 25
      src/Markup/Avalonia.Markup/Markup/Parsers/MediaQueryGrammar.cs
  11. 41
      src/Markup/Avalonia.Markup/Markup/Parsers/MediaQueryParser.cs
  12. 104
      tests/Avalonia.Markup.UnitTests/Parsers/QueryGrammarTests.cs
  13. 24
      tests/Avalonia.Markup.UnitTests/Parsers/QueryParserTests.cs

7
samples/ControlCatalog/MainView.xaml

@ -14,6 +14,12 @@
Value="Red" />
</Style>
</Media>
<Media Query="width >= 600 and height &lt;= 550 and orientation:portrait">
<Style Selector="TextBlock#Multi">
<Setter Property="Foreground"
Value="Red" />
</Style>
</Media>
<Media Query="width &lt;= 800">
<Style Selector="TextBlock#MaxWidth">
<Setter Property="Foreground"
@ -78,6 +84,7 @@
<StackPanel Spacing="10">
<TextBlock Name="MinWidth" Classes="h2" Text="width >= 600" />
<TextBlock Name="MaxWidth" Classes="h2" Text="width &lt;= 800" />
<TextBlock Name="Multi" Classes="h2" Text="width >= 600 and height &lt;= 550 and orientation:portrait" />
<TextBlock Name="MinHeight" Classes="h2" Text="height >= 600" />
<TextBlock Name="MaxHeight" Classes="h2" Text="height &lt;= 800" />
<TextBlock Name="Landscape" Classes="h2" Text="orientation:landscape" />

69
src/Avalonia.Base/Styling/Activators/AndQueryActivator.cs

@ -0,0 +1,69 @@
using System.Collections.Generic;
namespace Avalonia.Styling.Activators
{
/// <summary>
/// An aggregate <see cref="IStyleActivator"/> which is active when all of its inputs are
/// active.
/// </summary>
internal class AndQueryActivator : MediaQueryActivatorBase, IStyleActivatorSink
{
private List<IStyleActivator>? _sources;
public AndQueryActivator(Visual visual) : base(visual)
{
}
public int Count => _sources?.Count ?? 0;
public void Add(IStyleActivator activator)
{
if (IsSubscribed)
throw new AvaloniaInternalException("AndActivator is already subscribed.");
_sources ??= new List<IStyleActivator>();
_sources.Add(activator);
}
void IStyleActivatorSink.OnNext(bool value) => ReevaluateIsActive();
protected override bool EvaluateIsActive()
{
if (_sources is null || _sources.Count == 0)
return true;
var count = _sources.Count;
var mask = (1ul << count) - 1;
var flags = 0UL;
for (var i = 0; i < count; ++i)
{
if (_sources[i].GetIsActive())
flags |= 1ul << i;
}
return flags == mask;
}
protected override void Initialize()
{
if (_sources is object)
{
foreach (var source in _sources)
{
source.Subscribe(this);
}
}
}
protected override void Deinitialize()
{
if (_sources is object)
{
foreach (var source in _sources)
{
source.Unsubscribe(this);
}
}
}
}
}

51
src/Avalonia.Base/Styling/Activators/AndQueryActivatorBuilder.cs

@ -0,0 +1,51 @@
#nullable enable
namespace Avalonia.Styling.Activators
{
/// <summary>
/// Builds an <see cref="AndActivator"/>.
/// </summary>
/// <remarks>
/// When ANDing style activators, if there is more than one input then creates an instance of
/// <see cref="AndActivator"/>. If there is only one input, returns the input directly.
/// </remarks>
internal struct AndQueryActivatorBuilder
{
private readonly Visual _visual;
private IStyleActivator? _single;
private AndQueryActivator? _multiple;
public AndQueryActivatorBuilder(Visual visual) : this()
{
_visual = visual;
}
public int Count => _multiple?.Count ?? (_single is object ? 1 : 0);
public void Add(IStyleActivator? activator)
{
if (activator == null)
{
return;
}
if (_single is null && _multiple is null)
{
_single = activator;
}
else
{
if (_multiple is null)
{
_multiple = new AndQueryActivator(_visual);
_multiple.Add(_single!);
_single = null;
}
_multiple.Add(activator);
}
}
public IStyleActivator Get() => _single ?? _multiple!;
}
}

63
src/Avalonia.Base/Styling/Activators/OrQueryActivator.cs

@ -0,0 +1,63 @@
using System.Collections.Generic;
namespace Avalonia.Styling.Activators
{
/// <summary>
/// An aggregate <see cref="IStyleActivator"/> which is active when any of its inputs are
/// active.
/// </summary>
internal class OrQueryActivator : MediaQueryActivatorBase, IStyleActivatorSink
{
private List<IStyleActivator>? _sources;
public OrQueryActivator(Visual visual) : base(visual)
{
}
public int Count => _sources?.Count ?? 0;
public void Add(IStyleActivator activator)
{
_sources ??= new List<IStyleActivator>();
_sources.Add(activator);
}
void IStyleActivatorSink.OnNext(bool value) => ReevaluateIsActive();
protected override bool EvaluateIsActive()
{
if (_sources is null || _sources.Count == 0)
return true;
foreach (var source in _sources)
{
if (source.GetIsActive())
return true;
}
return false;
}
protected override void Initialize()
{
if (_sources is object)
{
foreach (var source in _sources)
{
source.Subscribe(this);
}
}
}
protected override void Deinitialize()
{
if (_sources is object)
{
foreach (var source in _sources)
{
source.Unsubscribe(this);
}
}
}
}
}

51
src/Avalonia.Base/Styling/Activators/OrQueryActivatorBuilder.cs

@ -0,0 +1,51 @@
#nullable enable
namespace Avalonia.Styling.Activators
{
/// <summary>
/// Builds an <see cref="OrActivator"/>.
/// </summary>
/// <remarks>
/// When ORing style activators, if there is more than one input then creates an instance of
/// <see cref="OrActivator"/>. If there is only one input, returns the input directly.
/// </remarks>
internal struct OrQueryActivatorBuilder
{
private IStyleActivator? _single;
private OrQueryActivator? _multiple;
private Visual _visual;
public OrQueryActivatorBuilder(Visual visual) : this()
{
_visual = visual;
}
public int Count => _multiple?.Count ?? (_single is object ? 1 : 0);
public void Add(IStyleActivator? activator)
{
if (activator == null)
{
return;
}
if (_single is null && _multiple is null)
{
_single = activator;
}
else
{
if (_multiple is null)
{
_multiple = new OrQueryActivator(_visual);
_multiple.Add(_single!);
_single = null;
}
_multiple.Add(activator);
}
}
public IStyleActivator Get() => _single ?? _multiple!;
}
}

100
src/Avalonia.Base/Styling/AndQuery.cs

@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Styling.Activators;
#nullable enable
namespace Avalonia.Styling
{
/// <summary>
/// The OR style query.
/// </summary>
internal sealed class AndQuery : Query
{
private readonly IReadOnlyList<Query> _queries;
private string? _queryString;
private Type? _targetType;
/// <summary>
/// Initializes a new instance of the <see cref="AndQuery"/> class.
/// </summary>
/// <param name="queries">The queries to AND.</param>
public AndQuery(IReadOnlyList<Query> queries)
{
if (queries is null)
{
throw new ArgumentNullException(nameof(queries));
}
if (queries.Count <= 1)
{
throw new ArgumentException("Need more than one query to AND.");
}
_queries = queries;
}
/// <inheritdoc/>
internal override bool IsCombinator => false;
/// <inheritdoc/>
public override string ToString(Media? owner)
{
if (_queryString == null)
{
_queryString = string.Join(" and ", _queries.Select(x => x.ToString(owner)));
}
return _queryString;
}
internal override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe)
{
if (!(control is Visual visual))
{
return SelectorMatch.NeverThisType;
}
var activators = new AndQueryActivatorBuilder(visual);
var alwaysThisInstance = false;
var count = _queries.Count;
for (var i = 0; i < count; i++)
{
var match = _queries[i].Match(control, parent, subscribe);
switch (match.Result)
{
case SelectorMatchResult.AlwaysThisInstance:
alwaysThisInstance = true;
break;
case SelectorMatchResult.NeverThisInstance:
case SelectorMatchResult.NeverThisType:
return match;
case SelectorMatchResult.Sometimes:
activators.Add(match.Activator!);
break;
}
}
if (activators.Count > 0)
{
return new SelectorMatch(activators.Get());
}
else if (alwaysThisInstance)
{
return SelectorMatch.AlwaysThisInstance;
}
else
{
return SelectorMatch.AlwaysThisType;
}
}
private protected override Query? MovePrevious() => null;
private protected override Query? MovePreviousOrParent() => null;
}
}

7
src/Avalonia.Base/Styling/OrQuery.cs

@ -51,7 +51,12 @@ namespace Avalonia.Styling
internal override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe)
{
var activators = new OrActivatorBuilder();
if (!(control is Visual visual))
{
return SelectorMatch.NeverThisType;
}
var activators = new OrQueryActivatorBuilder(visual);
var neverThisInstance = false;
var count = _queries.Count;

20
src/Avalonia.Base/Styling/Queries.cs

@ -44,5 +44,25 @@ namespace Avalonia.Styling
{
return new OrQuery(query);
}
/// <summary>
/// Returns a query which ANDs queries.
/// </summary>
/// <param name="queries">The queries to be AND'd.</param>
/// <returns>The query.</returns>
public static Query And(params Query[] queries)
{
return new AndQuery(queries);
}
/// <summary>
/// Returns a query which ANDs queries.
/// </summary>
/// <param name="query">The queries to be AND'd.</param>
/// <returns>The query.</returns>
public static Query And(IReadOnlyList<Query> query)
{
return new AndQuery(query);
}
}
}

94
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlQueryTransformer.cs

@ -44,6 +44,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
XamlIlQueryNode result = initialNode;
XamlIlOrQueryNode results = null;
XamlIlAndQueryNode andNode = null;
foreach (var i in syntax)
{
switch (i)
@ -58,17 +59,31 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
result = new XamlIlWidthQuery(result, width);
break;
case MediaQueryGrammar.HeightSyntax height:
result = new XamlIHeightQuery(result, height);
result = new XamlIlHeightQuery(result, height);
break;
case MediaQueryGrammar.CommaSyntax comma:
if (results == null)
case MediaQueryGrammar.OrSyntax or:
if (results == null)
results = new XamlIlOrQueryNode(node, queryType);
results.Add(result);
if (andNode != null && result == initialNode)
throw new XamlParseException($"Previously opened And node is not closed.", node);
results.Add(andNode ?? result);
result = initialNode;
andNode = null;
break;
case MediaQueryGrammar.AndSyntax and:
if (andNode == null)
andNode = new XamlIlAndQueryNode(node, queryType);
andNode.Add(result);
result = initialNode;
break;
default:
throw new XamlParseException($"Unsupported query grammar '{i.GetType()}'.", node);
}
if (andNode != null && result != initialNode)
{
andNode.Add(result);
}
}
if (results != null && result != null)
@ -254,11 +269,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
}
}
class XamlIHeightQuery : XamlIlQueryNode
class XamlIlHeightQuery : XamlIlQueryNode
{
private MediaQueryGrammar.HeightSyntax _argument;
public XamlIHeightQuery(XamlIlQueryNode previous, MediaQueryGrammar.HeightSyntax argument) : base(previous)
public XamlIlHeightQuery(XamlIlQueryNode previous, MediaQueryGrammar.HeightSyntax argument) : base(previous)
{
_argument = argument;
}
@ -359,4 +374,71 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
m => m.Name == "Or" && m.Parameters.Count == 1 && m.Parameters[0].Name.StartsWith("IReadOnlyList"));
}
}
class XamlIlAndQueryNode : XamlIlQueryNode
{
List<XamlIlQueryNode> _queries = new List<XamlIlQueryNode>();
public XamlIlAndQueryNode(IXamlLineInfo info, IXamlType queryType) : base(null, info, queryType)
{
}
public void Add(XamlIlQueryNode node)
{
_queries.Add(node);
}
public override IXamlType TargetType
{
get
{
IXamlType result = null;
foreach (var query in _queries)
{
if (query.TargetType == null)
{
return null;
}
else if (result == null)
{
result = query.TargetType;
}
else
{
while (!result.IsAssignableFrom(query.TargetType))
{
result = result.BaseType;
}
}
}
return result;
}
}
protected override void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
{
if (_queries.Count == 0)
throw new XamlLoadException("Invalid query count", this);
if (_queries.Count == 1)
{
_queries[0].Emit(context, codeGen);
return;
}
var listType = context.Configuration.TypeSystem.FindType("System.Collections.Generic.List`1")
.MakeGenericType(base.Type.GetClrType());
var add = listType.FindMethod("Add", context.Configuration.WellKnownTypes.Void, false, Type.GetClrType());
codeGen
.Newobj(listType.FindConstructor());
foreach (var s in _queries)
{
codeGen.Dup();
context.Emit(s, codeGen, Type.GetClrType());
codeGen.EmitCall(add, true);
}
EmitCall(context, codeGen,
m => m.Name == "And" && m.Parameters.Count == 1 && m.Parameters[0].Name.StartsWith("IReadOnlyList"));
}
}
}

25
src/Markup/Avalonia.Markup/Markup/Parsers/MediaQueryGrammar.cs

@ -217,14 +217,25 @@ namespace Avalonia.Markup.Parsers
private static (State, ISyntax?) ParseMiddle(ref CharacterReader r, char? end)
{
r.SkipWhitespace();
if (r.TakeIf(','))
{
return (State.Start, new CommaSyntax());
return (State.Start, new OrSyntax());
}
else if (end.HasValue && !r.End && r.Peek == end.Value)
{
return (State.End, null);
}
else
{
var identifier = r.TakeWhile(c => !char.IsWhiteSpace(c));
if (identifier.SequenceEqual("and".AsSpan()))
{
return (State.Start, new AndSyntax());
}
}
throw new InvalidOperationException("Invalid syntax found");
}
@ -284,11 +295,19 @@ namespace Avalonia.Markup.Parsers
}
}
public class CommaSyntax : ISyntax
public class OrSyntax : ISyntax
{
public override bool Equals(object? obj)
{
return obj is OrSyntax;
}
}
public class AndSyntax : ISyntax
{
public override bool Equals(object? obj)
{
return obj is CommaSyntax;
return obj is AndSyntax;
}
}

41
src/Markup/Avalonia.Markup/Markup/Parsers/MediaQueryParser.cs

@ -13,19 +13,11 @@ namespace Avalonia.Markup.Parsers
/// </summary>
internal class MediaQueryParser
{
private readonly Func<string, string, Type> _typeResolver;
/// <summary>
/// Initializes a new instance of the <see cref="MediaQueryParser"/> class.
/// </summary>
/// <param name="typeResolver">
/// The type resolver to use. The type resolver is a function which accepts two strings:
/// a type name and a XML namespace prefix and a type name, and should return the resolved
/// type or throw an exception.
/// </param>
public MediaQueryParser(Func<string, string, Type> typeResolver)
public MediaQueryParser()
{
_typeResolver = typeResolver;
}
/// <summary>
@ -51,16 +43,26 @@ namespace Avalonia.Markup.Parsers
switch (i)
{
case MediaQueryGrammar.OrientationSyntax orientation:
result = Queries.Orientation(result, orientation.Argument);
result = result.Orientation(orientation.Argument);
break;
case MediaQueryGrammar.WidthSyntax width:
result = Queries.Width(result, width.LeftOperator, width.Left, width.RightOperator, width.Right);
result = result.Width(width.LeftOperator, width.Left, width.RightOperator, width.Right);
break;
case MediaQueryGrammar.HeightSyntax height:
result = Queries.Height(result, height.LeftOperator, height.Left, height.RightOperator, height.Right);
result = result.Height(height.LeftOperator, height.Left, height.RightOperator, height.Right);
break;
case MediaQueryGrammar.PlatformSyntax platform:
result = Queries.Platform(result, platform.Argument);
result = result.Platform(platform.Argument);
break;
case MediaQueryGrammar.OrSyntax or:
case MediaQueryGrammar.AndSyntax and:
if (results == null)
{
results = new List<Query>();
}
results.Add(result ?? throw new NotSupportedException("Invalid query!"));
result = null;
break;
default:
throw new NotSupportedException($"Unsupported selector grammar '{i.GetType()}'.");
@ -79,18 +81,5 @@ namespace Avalonia.Markup.Parsers
return result;
}
private Type Resolve(string xmlns, string typeName)
{
var result = _typeResolver(xmlns, typeName);
if (result == null)
{
var type = string.IsNullOrWhiteSpace(xmlns) ? typeName : xmlns + ':' + typeName;
throw new InvalidOperationException($"Could not resolve type '{type}'");
}
return result;
}
}
}

104
tests/Avalonia.Markup.UnitTests/Parsers/QueryGrammarTests.cs

@ -0,0 +1,104 @@
using System.Linq;
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using Xunit;
namespace Avalonia.Markup.UnitTests.Parsers
{
public class QueryGrammarTests
{
[Fact]
public void Width()
{
var result = MediaQueryGrammar.Parse("width >= 100");
Assert.Equal(
new[] { new MediaQueryGrammar.WidthSyntax()
{
Right = 100,
RightOperator = Styling.QueryComparisonOperator.GreaterThanOrEquals
}, },
result);
result = MediaQueryGrammar.Parse("100 <= width");
Assert.Equal(
new[] { new MediaQueryGrammar.WidthSyntax()
{
Left = 100,
LeftOperator = Styling.QueryComparisonOperator.GreaterThanOrEquals
}, },
result);
result = MediaQueryGrammar.Parse("100 <= width < 200");
Assert.Equal(
new[] { new MediaQueryGrammar.WidthSyntax()
{
Left = 100,
LeftOperator = Styling.QueryComparisonOperator.GreaterThanOrEquals,
Right = 200,
RightOperator = Styling.QueryComparisonOperator.LessThan
}, },
result);
}
[Fact]
public void Height()
{
var result = MediaQueryGrammar.Parse("height >= 100");
Assert.Equal(
new[] { new MediaQueryGrammar.HeightSyntax()
{
Right = 100,
RightOperator = Styling.QueryComparisonOperator.GreaterThanOrEquals
}, },
result);
result = MediaQueryGrammar.Parse("100 <= height");
Assert.Equal(
new[] { new MediaQueryGrammar.HeightSyntax()
{
Left = 100,
LeftOperator = Styling.QueryComparisonOperator.GreaterThanOrEquals
}, },
result);
result = MediaQueryGrammar.Parse("100 <= height < 200");
Assert.Equal(
new[] { new MediaQueryGrammar.HeightSyntax()
{
Left = 100,
LeftOperator = Styling.QueryComparisonOperator.GreaterThanOrEquals,
Right = 200,
RightOperator = Styling.QueryComparisonOperator.LessThan
}, },
result);
}
[Fact]
public void Orientation()
{
var result = MediaQueryGrammar.Parse("orientation:portrait");
Assert.Equal(
new[] { new MediaQueryGrammar.OrientationSyntax()
{
Argument = Styling.MediaOrientation.Portrait
}, },
result);
result = MediaQueryGrammar.Parse("orientation:landscape");
Assert.Equal(
new[] { new MediaQueryGrammar.OrientationSyntax()
{
Argument = Styling.MediaOrientation.Landscape
}, },
result);
}
}
}

24
tests/Avalonia.Markup.UnitTests/Parsers/QueryParserTests.cs

@ -0,0 +1,24 @@
using System;
using Avalonia.Controls;
using Avalonia.Markup.Parsers;
using Xunit;
namespace Avalonia.Markup.UnitTests.Parsers
{
public class QueryParserTests
{
[Fact]
public void Parses_Or_Queries()
{
var target = new MediaQueryParser();
var result = target.Parse("orientation:portrait , width > 0");
}
[Fact]
public void Parses_And_Queries()
{
var target = new MediaQueryParser();
var result = target.Parse("orientation:portrait and width > 0");
}
}
}
Loading…
Cancel
Save