13 changed files with 620 additions and 36 deletions
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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!; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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!; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
|
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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…
Reference in new issue