29 changed files with 1388 additions and 443 deletions
@ -0,0 +1,79 @@ |
|||
using System; |
|||
using Avalonia.Controls; |
|||
using Avalonia.PropertyStore; |
|||
|
|||
namespace Avalonia.Styling |
|||
{ |
|||
/// <summary>
|
|||
/// Defines a media.
|
|||
/// </summary>
|
|||
public class Media : StyleBase |
|||
{ |
|||
private Query? _query; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="Media"/> class.
|
|||
/// </summary>
|
|||
public Media() |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="Media"/> class.
|
|||
/// </summary>
|
|||
/// <param name="selector">The media selector.</param>
|
|||
public Media(Func<Query?, Query> selector) |
|||
{ |
|||
Query = selector(null); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the media's selector.
|
|||
/// </summary>
|
|||
public Query? Query |
|||
{ |
|||
get => _query; |
|||
set => _query = value; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a string representation of the media.
|
|||
/// </summary>
|
|||
/// <returns>A string representation of the media.</returns>
|
|||
public override string ToString() => Query?.ToString(this) ?? "Media"; |
|||
|
|||
internal override void SetParent(StyleBase? parent) |
|||
{ |
|||
if (parent is Style) |
|||
{ |
|||
throw new InvalidOperationException("Media can not be children of Style."); |
|||
} |
|||
|
|||
base.SetParent(parent); |
|||
} |
|||
|
|||
internal SelectorMatchResult TryAttach(StyledElement target, object? host, FrameType type) |
|||
{ |
|||
_ = target ?? throw new ArgumentNullException(nameof(target)); |
|||
|
|||
var result = SelectorMatchResult.NeverThisType; |
|||
|
|||
if (HasChildren) |
|||
{ |
|||
var match = Query?.Match(target, Parent, true) ?? |
|||
(target == host ? |
|||
SelectorMatch.AlwaysThisInstance : |
|||
SelectorMatch.NeverThisInstance); |
|||
|
|||
if (match.IsMatch) |
|||
{ |
|||
Attach(target, match.Activator, type); |
|||
} |
|||
|
|||
result = match.Result; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Styling |
|||
{ |
|||
public abstract class MediaQuery<T> : Query |
|||
{ |
|||
private readonly Query? _previous; |
|||
private T _argument; |
|||
|
|||
public MediaQuery(Query? previous, T argument) |
|||
{ |
|||
_previous = previous; |
|||
_argument = argument; |
|||
} |
|||
|
|||
protected T Argument => _argument; |
|||
|
|||
internal override bool IsCombinator => false; |
|||
|
|||
public override string ToString(Media? owner) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
private protected override Query? MovePrevious() => _previous; |
|||
|
|||
private protected override Query? MovePreviousOrParent() => _previous; |
|||
} |
|||
} |
|||
@ -1,33 +0,0 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Styling |
|||
{ |
|||
public abstract class MediaSelector<T> : Selector |
|||
{ |
|||
private readonly Selector? _previous; |
|||
private T _argument; |
|||
|
|||
public MediaSelector(Selector? previous, T argument) |
|||
{ |
|||
_previous = previous; |
|||
_argument = argument; |
|||
} |
|||
|
|||
protected T Argument => _argument; |
|||
|
|||
internal override bool InTemplate => _previous?.InTemplate ?? false; |
|||
|
|||
internal override bool IsCombinator => false; |
|||
|
|||
internal override Type? TargetType => _previous?.TargetType; |
|||
|
|||
public override string ToString(Style? owner) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
private protected override Selector? MovePrevious() => _previous; |
|||
|
|||
private protected override Selector? MovePreviousOrParent() => _previous; |
|||
} |
|||
} |
|||
@ -0,0 +1,95 @@ |
|||
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 OrQuery : Query |
|||
{ |
|||
private readonly IReadOnlyList<Query> _queries; |
|||
private string? _queryString; |
|||
private Type? _targetType; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="OrQuery"/> class.
|
|||
/// </summary>
|
|||
/// <param name="queries">The querys to OR.</param>
|
|||
public OrQuery(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 OR."); |
|||
} |
|||
|
|||
_queries = queries; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
internal override bool IsCombinator => false; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override string ToString(Media? owner) |
|||
{ |
|||
if (_queryString == null) |
|||
{ |
|||
_queryString = string.Join(", ", _queries.Select(x => x.ToString(owner))); |
|||
} |
|||
|
|||
return _queryString; |
|||
} |
|||
|
|||
internal override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe) |
|||
{ |
|||
var activators = new OrActivatorBuilder(); |
|||
var neverThisInstance = 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.AlwaysThisType: |
|||
case SelectorMatchResult.AlwaysThisInstance: |
|||
return match; |
|||
case SelectorMatchResult.NeverThisInstance: |
|||
neverThisInstance = true; |
|||
break; |
|||
case SelectorMatchResult.Sometimes: |
|||
activators.Add(match.Activator!); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (activators.Count > 0) |
|||
{ |
|||
return new SelectorMatch(activators.Get()); |
|||
} |
|||
else if (neverThisInstance) |
|||
{ |
|||
return SelectorMatch.NeverThisInstance; |
|||
} |
|||
else |
|||
{ |
|||
return SelectorMatch.NeverThisType; |
|||
} |
|||
} |
|||
|
|||
private protected override Query? MovePrevious() => null; |
|||
private protected override Query? MovePreviousOrParent() => null; |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,58 @@ |
|||
using Avalonia.Platform; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Styling |
|||
{ |
|||
internal static class Queries |
|||
{ |
|||
public static Query IsOs(this Query? previous, string argument) |
|||
{ |
|||
return new IsOsMediaQuery(previous, argument); |
|||
} |
|||
|
|||
public static Query MaxHeight(this Query? previous, double argument) |
|||
{ |
|||
return new MaxHeightMediaQuery(previous, argument); |
|||
} |
|||
|
|||
public static Query MaxWidth(this Query? previous, double argument) |
|||
{ |
|||
return new MaxWidthMediaQuery(previous, argument); |
|||
} |
|||
|
|||
public static Query MinHeight(this Query? previous, double argument) |
|||
{ |
|||
return new MinHeightMediaQuery(previous, argument); |
|||
} |
|||
|
|||
public static Query MinWidth(this Query? previous, double argument) |
|||
{ |
|||
return new MinWidthMediaQuery(previous, argument); |
|||
} |
|||
|
|||
public static Query Orientation(this Query? previous, DeviceOrientation argument) |
|||
{ |
|||
return new OrientationMediaQuery(previous, argument); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a query which ORs queries.
|
|||
/// </summary>
|
|||
/// <param name="queries">The queries to be OR'd.</param>
|
|||
/// <returns>The query.</returns>
|
|||
public static Query Or(params Query[] queries) |
|||
{ |
|||
return new OrQuery(queries); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a query which ORs queries.
|
|||
/// </summary>
|
|||
/// <param name="query">The queries to be OR'd.</param>
|
|||
/// <returns>The query.</returns>
|
|||
public static Query Or(IReadOnlyList<Query> query) |
|||
{ |
|||
return new OrQuery(query); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,154 @@ |
|||
using System; |
|||
using Avalonia.Styling.Activators; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Styling |
|||
{ |
|||
/// <summary>
|
|||
/// A query in a <see cref="Style"/>.
|
|||
/// </summary>
|
|||
public abstract class Query |
|||
{ |
|||
/// <summary>
|
|||
/// Gets a value indicating whether this query is a combinator.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// A combinator is a query such as Child or Descendent which links simple querys.
|
|||
/// </remarks>
|
|||
internal abstract bool IsCombinator { get; } |
|||
|
|||
/// <summary>
|
|||
/// Tries to match the query with a control.
|
|||
/// </summary>
|
|||
/// <param name="control">The control.</param>
|
|||
/// <param name="parent">
|
|||
/// The parent media, if the media containing the query is a nested media.
|
|||
/// </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>
|
|||
internal virtual SelectorMatch Match(StyledElement control, IStyle? parent = null, bool subscribe = true) |
|||
{ |
|||
// First match the query until a combinator is found. Selectors are stored from
|
|||
// right-to-left, so MatchUntilCombinator reverses this order because the type query
|
|||
// will be on the left.
|
|||
var match = MatchUntilCombinator(control, this, parent, subscribe, out var combinator); |
|||
|
|||
// If the pre-combinator query matches, we can now match the combinator, if any.
|
|||
if (match.IsMatch && combinator is object) |
|||
{ |
|||
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
|
|||
// control.
|
|||
match = match.Result switch |
|||
{ |
|||
SelectorMatchResult.AlwaysThisType => SelectorMatch.AlwaysThisInstance, |
|||
SelectorMatchResult.NeverThisType => SelectorMatch.NeverThisInstance, |
|||
_ => match |
|||
}; |
|||
} |
|||
|
|||
return match; |
|||
} |
|||
|
|||
public override string ToString() => ToString(null); |
|||
|
|||
/// <summary>
|
|||
/// Gets a string representing the query, with the nesting separator (`^`) replaced with
|
|||
/// the parent query.
|
|||
/// </summary>
|
|||
/// <param name="owner">The owner media.</param>
|
|||
public abstract string ToString(Media? owner); |
|||
|
|||
/// <summary>
|
|||
/// Evaluates the query for a match.
|
|||
/// </summary>
|
|||
/// <param name="control">The control.</param>
|
|||
/// <param name="parent">
|
|||
/// The parent media, if the media containing the query is a nested media.
|
|||
/// </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>
|
|||
internal abstract SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe); |
|||
|
|||
/// <summary>
|
|||
/// Moves to the previous query.
|
|||
/// </summary>
|
|||
private protected abstract Query? MovePrevious(); |
|||
|
|||
/// <summary>
|
|||
/// Moves to the previous query or the parent query.
|
|||
/// </summary>
|
|||
private protected abstract Query? MovePreviousOrParent(); |
|||
|
|||
private static SelectorMatch MatchUntilCombinator( |
|||
StyledElement control, |
|||
Query start, |
|||
IStyle? parent, |
|||
bool subscribe, |
|||
out Query? combinator) |
|||
{ |
|||
combinator = null; |
|||
|
|||
var activators = new AndActivatorBuilder(); |
|||
var result = Match(control, start, parent, subscribe, ref activators, ref combinator); |
|||
|
|||
return result == SelectorMatchResult.Sometimes ? |
|||
new SelectorMatch(activators.Get()) : |
|||
new SelectorMatch(result); |
|||
} |
|||
|
|||
private static SelectorMatchResult Match( |
|||
StyledElement control, |
|||
Query query, |
|||
IStyle? parent, |
|||
bool subscribe, |
|||
ref AndActivatorBuilder activators, |
|||
ref Query? combinator) |
|||
{ |
|||
var previous = query.MovePrevious(); |
|||
|
|||
// Selectors are stored from right-to-left, so we recurse into the query in order to
|
|||
// reverse this order, because the type query will be on the left and is our best
|
|||
// opportunity to exit early.
|
|||
if (previous != null && !previous.IsCombinator) |
|||
{ |
|||
var previousMatch = Match(control, previous, parent, subscribe, ref activators, ref combinator); |
|||
|
|||
if (previousMatch < SelectorMatchResult.Sometimes) |
|||
{ |
|||
return previousMatch; |
|||
} |
|||
} |
|||
|
|||
// Match this query.
|
|||
var match = query.Evaluate(control, parent, subscribe); |
|||
|
|||
if (!match.IsMatch) |
|||
{ |
|||
combinator = null; |
|||
return match.Result; |
|||
} |
|||
else if (match.Activator is object) |
|||
{ |
|||
activators.Add(match.Activator!); |
|||
} |
|||
|
|||
if (previous?.IsCombinator == true) |
|||
{ |
|||
combinator = previous; |
|||
} |
|||
|
|||
return match.Result; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,403 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
using Avalonia.Markup.Parsers; |
|||
using Avalonia.Platform; |
|||
using XamlX; |
|||
using XamlX.Ast; |
|||
using XamlX.Emit; |
|||
using XamlX.IL; |
|||
using XamlX.Transform; |
|||
using XamlX.Transform.Transformers; |
|||
using XamlX.TypeSystem; |
|||
|
|||
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers |
|||
{ |
|||
using XamlParseException = XamlX.XamlParseException; |
|||
using XamlLoadException = XamlX.XamlLoadException; |
|||
class AvaloniaXamlIlQueryTransformer : IXamlAstTransformer |
|||
{ |
|||
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) |
|||
{ |
|||
if (node is not XamlAstObjectNode on || |
|||
!context.GetAvaloniaTypes().Media.IsAssignableFrom(on.Type.GetClrType())) |
|||
return node; |
|||
|
|||
var pn = on.Children.OfType<XamlAstXamlPropertyValueNode>() |
|||
.FirstOrDefault(p => p.Property.GetClrProperty().Name == "Query"); |
|||
|
|||
if (pn == null) |
|||
return node; |
|||
|
|||
if (pn.Values.Count != 1) |
|||
throw new XamlParseException("Query property should should have exactly one value", node); |
|||
|
|||
if (pn.Values[0] is XamlIlQueryNode) |
|||
//Deja vu. I've just been in this place before
|
|||
return node; |
|||
|
|||
if (!(pn.Values[0] is XamlAstTextNode tn)) |
|||
throw new XamlParseException("Query property should be a text node", node); |
|||
|
|||
var queryType = pn.Property.GetClrProperty().Getter.ReturnType; |
|||
var initialNode = new XamlIlQueryInitialNode(node, queryType); |
|||
var avaloniaAttachedPropertyT = context.GetAvaloniaTypes().AvaloniaAttachedPropertyT; |
|||
XamlIlQueryNode Create(IEnumerable<ISyntax> syntax) |
|||
{ |
|||
XamlIlQueryNode result = initialNode; |
|||
XamlIlOrQueryNode results = null; |
|||
foreach (var i in syntax) |
|||
{ |
|||
switch (i) |
|||
{ |
|||
|
|||
case MediaQueryGrammar.MinWidthSyntax minWidth: |
|||
result = new XamlIlMinWidthQuery(result, minWidth.Argument); |
|||
break; |
|||
case MediaQueryGrammar.MaxWidthSyntax maxWidth: |
|||
result = new XamlIlMaxWidthQuery(result, maxWidth.Argument); |
|||
break; |
|||
case MediaQueryGrammar.MinHeightSyntax minHeight: |
|||
result = new XamlIlMinHeightQuery(result, minHeight.Argument); |
|||
break; |
|||
case MediaQueryGrammar.MaxHeightSyntax maxHeight: |
|||
result = new XamlIlMaxHeightQuery(result, maxHeight.Argument); |
|||
break; |
|||
case MediaQueryGrammar.OrientationSyntax orientation: |
|||
result = new XamlIlOrientationQuery(result, orientation.Argument); |
|||
break; |
|||
case MediaQueryGrammar.IsOsSyntax isOs: |
|||
result = new XamlIlIsOsQuery(result, isOs.Argument); |
|||
break; |
|||
case MediaQueryGrammar.CommaSyntax comma: |
|||
if (results == null) |
|||
results = new XamlIlOrQueryNode(node, queryType); |
|||
results.Add(result); |
|||
result = initialNode; |
|||
break; |
|||
default: |
|||
throw new XamlParseException($"Unsupported query grammar '{i.GetType()}'.", node); |
|||
} |
|||
} |
|||
|
|||
if (results != null && result != null) |
|||
{ |
|||
results.Add(result); |
|||
} |
|||
|
|||
return results ?? result; |
|||
} |
|||
|
|||
IEnumerable<ISyntax> parsed; |
|||
try |
|||
{ |
|||
parsed = MediaQueryGrammar.Parse(tn.Text); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
throw new XamlParseException("Unable to parse query: " + e.Message, node); |
|||
} |
|||
|
|||
var query = Create(parsed); |
|||
pn.Values[0] = query; |
|||
|
|||
return new AvaloniaXamlIlTargetTypeMetadataNode(on, |
|||
new XamlAstClrTypeReference(query, query.TargetType, false), |
|||
AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style); |
|||
} |
|||
} |
|||
|
|||
abstract class XamlIlQueryNode : XamlAstNode, IXamlAstValueNode, IXamlAstEmitableNode<IXamlILEmitter, XamlILNodeEmitResult> |
|||
{ |
|||
internal XamlIlQueryNode Previous { get; } |
|||
public abstract IXamlType TargetType { get; } |
|||
|
|||
public XamlIlQueryNode(XamlIlQueryNode previous, |
|||
IXamlLineInfo info = null, |
|||
IXamlType queryType = null) : base(info ?? previous) |
|||
{ |
|||
Previous = previous; |
|||
Type = queryType == null ? previous.Type : new XamlAstClrTypeReference(this, queryType, false); |
|||
} |
|||
|
|||
public IXamlAstTypeReference Type { get; } |
|||
|
|||
public virtual XamlILNodeEmitResult Emit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen) |
|||
{ |
|||
if (Previous != null) |
|||
context.Emit(Previous, codeGen, Type.GetClrType()); |
|||
DoEmit(context, codeGen); |
|||
return XamlILNodeEmitResult.Type(0, Type.GetClrType()); |
|||
} |
|||
|
|||
protected abstract void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen); |
|||
|
|||
protected void EmitCall(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen, Func<IXamlMethod, bool> method) |
|||
{ |
|||
var queries = context.Configuration.TypeSystem.GetType("Avalonia.Styling.Queries"); |
|||
var found = queries.FindMethod(m => m.IsStatic && m.Parameters.Count > 0 && method(m)); |
|||
codeGen.EmitCall(found); |
|||
} |
|||
} |
|||
|
|||
class XamlIlQueryInitialNode : XamlIlQueryNode |
|||
{ |
|||
public XamlIlQueryInitialNode(IXamlLineInfo info, |
|||
IXamlType queryType) : base(null, info, queryType) |
|||
{ |
|||
} |
|||
|
|||
public override IXamlType TargetType => null; |
|||
protected override void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen) => codeGen.Ldnull(); |
|||
} |
|||
|
|||
class XamlIlTypeQuery : XamlIlQueryNode |
|||
{ |
|||
public bool Concrete { get; } |
|||
|
|||
public XamlIlTypeQuery(XamlIlQueryNode previous, IXamlType type, bool concrete) : base(previous) |
|||
{ |
|||
TargetType = type; |
|||
Concrete = concrete; |
|||
} |
|||
|
|||
public override IXamlType TargetType { get; } |
|||
protected override void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen) |
|||
{ |
|||
var name = Concrete ? "OfType" : "Is"; |
|||
codeGen.Ldtype(TargetType); |
|||
EmitCall(context, codeGen, |
|||
m => m.Name == name && m.Parameters.Count == 2 && m.Parameters[1].FullName == "System.Type"); |
|||
} |
|||
} |
|||
|
|||
class XamlIlStringQuery : XamlIlQueryNode |
|||
{ |
|||
public string String { get; set; } |
|||
public enum QueryType |
|||
{ |
|||
Class, |
|||
Name |
|||
} |
|||
|
|||
private QueryType _type; |
|||
|
|||
public XamlIlStringQuery(XamlIlQueryNode previous, QueryType type, string s) : base(previous) |
|||
{ |
|||
_type = type; |
|||
String = s; |
|||
} |
|||
|
|||
|
|||
public override IXamlType TargetType => Previous?.TargetType; |
|||
protected override void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen) |
|||
{ |
|||
codeGen.Ldstr(String); |
|||
var name = _type.ToString(); |
|||
EmitCall(context, codeGen, |
|||
m => m.Name == name && m.Parameters.Count == 2 && m.Parameters[1].FullName == "System.String"); |
|||
} |
|||
} |
|||
|
|||
class XamlIlCombinatorQuery : XamlIlQueryNode |
|||
{ |
|||
private readonly CombinatorQueryType _type; |
|||
|
|||
public enum CombinatorQueryType |
|||
{ |
|||
Child, |
|||
Descendant, |
|||
Template |
|||
} |
|||
public XamlIlCombinatorQuery(XamlIlQueryNode previous, CombinatorQueryType type) : base(previous) |
|||
{ |
|||
_type = type; |
|||
} |
|||
|
|||
public CombinatorQueryType QueryType => _type; |
|||
public override IXamlType TargetType => null; |
|||
protected override void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen) |
|||
{ |
|||
var name = _type.ToString(); |
|||
EmitCall(context, codeGen, |
|||
m => m.Name == name && m.Parameters.Count == 1); |
|||
} |
|||
} |
|||
|
|||
class XamlIlMinWidthQuery : XamlIlQueryNode |
|||
{ |
|||
private double _argument; |
|||
|
|||
public XamlIlMinWidthQuery(XamlIlQueryNode previous, double argument) : base(previous) |
|||
{ |
|||
_argument = argument; |
|||
} |
|||
|
|||
public override IXamlType TargetType => Previous?.TargetType; |
|||
protected override void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen) |
|||
{ |
|||
codeGen.Ldc_R8(_argument); |
|||
EmitCall(context, codeGen, |
|||
m => m.Name == "MinWidth" && m.Parameters.Count == 2); |
|||
} |
|||
} |
|||
|
|||
class XamlIlMaxWidthQuery : XamlIlQueryNode |
|||
{ |
|||
private double _argument; |
|||
|
|||
public XamlIlMaxWidthQuery(XamlIlQueryNode previous, double argument) : base(previous) |
|||
{ |
|||
_argument = argument; |
|||
} |
|||
|
|||
public override IXamlType TargetType => Previous?.TargetType; |
|||
protected override void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen) |
|||
{ |
|||
codeGen.Ldc_R8(_argument); |
|||
EmitCall(context, codeGen, |
|||
m => m.Name == "MaxWidth" && m.Parameters.Count == 2); |
|||
} |
|||
} |
|||
|
|||
class XamlIlMinHeightQuery : XamlIlQueryNode |
|||
{ |
|||
private double _argument; |
|||
|
|||
public XamlIlMinHeightQuery(XamlIlQueryNode previous, double argument) : base(previous) |
|||
{ |
|||
_argument = argument; |
|||
} |
|||
|
|||
public override IXamlType TargetType => Previous?.TargetType; |
|||
|
|||
protected override void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen) |
|||
{ |
|||
codeGen.Ldc_R8(_argument); |
|||
EmitCall(context, codeGen, |
|||
m => m.Name == "MinHeight" && m.Parameters.Count == 2); |
|||
} |
|||
} |
|||
|
|||
class XamlIlMaxHeightQuery : XamlIlQueryNode |
|||
{ |
|||
private double _argument; |
|||
|
|||
public XamlIlMaxHeightQuery(XamlIlQueryNode previous, double argument) : base(previous) |
|||
{ |
|||
_argument = argument; |
|||
} |
|||
|
|||
public override IXamlType TargetType => Previous?.TargetType; |
|||
protected override void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen) |
|||
{ |
|||
codeGen.Ldc_R8(_argument); |
|||
EmitCall(context, codeGen, |
|||
m => m.Name == "MaxHeight" && m.Parameters.Count == 2); |
|||
} |
|||
} |
|||
|
|||
class XamlIlOrientationQuery : XamlIlQueryNode |
|||
{ |
|||
private DeviceOrientation _argument; |
|||
|
|||
public XamlIlOrientationQuery(XamlIlQueryNode previous, DeviceOrientation argument) : base(previous) |
|||
{ |
|||
_argument = argument; |
|||
} |
|||
|
|||
public override IXamlType TargetType => Previous?.TargetType; |
|||
protected override void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen) |
|||
{ |
|||
codeGen.Ldc_I4((int)_argument); |
|||
EmitCall(context, codeGen, |
|||
m => m.Name == "Orientation" && m.Parameters.Count == 2); |
|||
} |
|||
} |
|||
|
|||
class XamlIlIsOsQuery : XamlIlQueryNode |
|||
{ |
|||
private string _argument; |
|||
|
|||
public XamlIlIsOsQuery(XamlIlQueryNode previous, string argument) : base(previous) |
|||
{ |
|||
_argument = argument; |
|||
} |
|||
|
|||
public override IXamlType TargetType => Previous?.TargetType; |
|||
protected override void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen) |
|||
{ |
|||
codeGen.Ldstr(_argument); |
|||
EmitCall(context, codeGen, |
|||
m => m.Name == "IsOs" && m.Parameters.Count == 2); |
|||
} |
|||
} |
|||
|
|||
class XamlIlOrQueryNode : XamlIlQueryNode |
|||
{ |
|||
List<XamlIlQueryNode> _queries = new List<XamlIlQueryNode>(); |
|||
public XamlIlOrQueryNode(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 == "Or" && m.Parameters.Count == 1 && m.Parameters[0].Name.StartsWith("IReadOnlyList")); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
// Don't need to override GetHashCode as the ISyntax objects will not be stored in a hash; the
|
|||
// only reason they have overridden Equals methods is for unit testing.
|
|||
#pragma warning disable 659
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Markup.Parsers |
|||
{ |
|||
public interface ISyntax |
|||
{ |
|||
} |
|||
|
|||
public interface ITypeSyntax |
|||
{ |
|||
string TypeName { get; set; } |
|||
|
|||
string Xmlns { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,271 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Data.Core; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Utilities; |
|||
|
|||
// Don't need to override GetHashCode as the ISyntax objects will not be stored in a hash; the
|
|||
// only reason they have overridden Equals methods is for unit testing.
|
|||
#pragma warning disable 659
|
|||
|
|||
namespace Avalonia.Markup.Parsers |
|||
{ |
|||
internal static class MediaQueryGrammar |
|||
{ |
|||
private enum State |
|||
{ |
|||
Start, |
|||
Middle, |
|||
Colon, |
|||
End, |
|||
} |
|||
|
|||
public static IEnumerable<ISyntax> Parse(string s) |
|||
{ |
|||
var r = new CharacterReader(s.AsSpan()); |
|||
return Parse(ref r, null); |
|||
} |
|||
|
|||
private static IEnumerable<ISyntax> Parse(ref CharacterReader r, char? end) |
|||
{ |
|||
var state = State.Start; |
|||
var selector = new List<ISyntax>(); |
|||
while (!r.End && state != State.End) |
|||
{ |
|||
ISyntax? syntax = null; |
|||
switch (state) |
|||
{ |
|||
case State.Start: |
|||
(state, syntax) = ParseStart(ref r); |
|||
break; |
|||
case State.Middle: |
|||
(state, syntax) = ParseMiddle(ref r, end); |
|||
break; |
|||
case State.Colon: |
|||
(state, syntax) = ParseColon(ref r); |
|||
break; |
|||
} |
|||
if (syntax != null) |
|||
{ |
|||
selector.Add(syntax); |
|||
} |
|||
} |
|||
|
|||
if (state != State.Start && state != State.Middle && state != State.End) |
|||
{ |
|||
throw new ExpressionParseException(r.Position, "Unexpected end of selector"); |
|||
} |
|||
|
|||
return selector; |
|||
} |
|||
|
|||
private static (State, ISyntax?) ParseStart(ref CharacterReader r) |
|||
{ |
|||
r.SkipWhitespace(); |
|||
if (r.End) |
|||
{ |
|||
return (State.End, null); |
|||
} |
|||
|
|||
if (r.TakeIf(':')) |
|||
{ |
|||
return (State.Colon, null); |
|||
} |
|||
|
|||
throw new InvalidOperationException("Invalid syntax found"); |
|||
} |
|||
|
|||
private static (State, ISyntax?) ParseMiddle(ref CharacterReader r, char? end) |
|||
{ |
|||
if (r.TakeIf(':')) |
|||
{ |
|||
return (State.Colon, null); |
|||
} |
|||
else if (r.TakeIf(',')) |
|||
{ |
|||
return (State.Start, new CommaSyntax()); |
|||
} |
|||
else if (end.HasValue && !r.End && r.Peek == end.Value) |
|||
{ |
|||
return (State.End, null); |
|||
} |
|||
throw new InvalidOperationException("Invalid syntax found"); |
|||
} |
|||
|
|||
private static (State, ISyntax) ParseColon(ref CharacterReader r) |
|||
{ |
|||
var identifier = r.ParseStyleClass(); |
|||
|
|||
if (identifier.IsEmpty) |
|||
{ |
|||
throw new ExpressionParseException(r.Position, "Expected class name, is, nth-child or nth-last-child selector after ':'."); |
|||
} |
|||
|
|||
const string MinWidthKeyword = "min-width"; |
|||
const string MaxWidthKeyword = "max-width"; |
|||
const string MinHeightKeyword = "min-height"; |
|||
const string MaxHeightKeyword = "max-height"; |
|||
const string OrientationKeyword = "orientation"; |
|||
const string IsOsKeyword = "is-os"; |
|||
|
|||
if(identifier.SequenceEqual(MinWidthKeyword.AsSpan()) && r.TakeIf('(')) |
|||
{ |
|||
var argument = ParseDecimal(ref r); |
|||
Expect(ref r, ')'); |
|||
|
|||
var syntax = new MinWidthSyntax { Argument = argument }; |
|||
return (State.Middle, syntax); |
|||
} |
|||
if(identifier.SequenceEqual(MaxWidthKeyword.AsSpan()) && r.TakeIf('(')) |
|||
{ |
|||
var argument = ParseDecimal(ref r); |
|||
Expect(ref r, ')'); |
|||
|
|||
var syntax = new MaxWidthSyntax { Argument = argument }; |
|||
return (State.Middle, syntax); |
|||
} |
|||
if(identifier.SequenceEqual(MinHeightKeyword.AsSpan()) && r.TakeIf('(')) |
|||
{ |
|||
var argument = ParseDecimal(ref r); |
|||
Expect(ref r, ')'); |
|||
|
|||
var syntax = new MinHeightSyntax { Argument = argument }; |
|||
return (State.Middle, syntax); |
|||
} |
|||
if(identifier.SequenceEqual(MaxHeightKeyword.AsSpan()) && r.TakeIf('(')) |
|||
{ |
|||
var argument = ParseDecimal(ref r); |
|||
Expect(ref r, ')'); |
|||
|
|||
var syntax = new MaxHeightSyntax { Argument = argument }; |
|||
return (State.Middle, syntax); |
|||
} |
|||
if (identifier.SequenceEqual(OrientationKeyword.AsSpan()) && r.TakeIf('(')) |
|||
{ |
|||
var argument = ParseEnum<DeviceOrientation>(ref r); |
|||
Expect(ref r, ')'); |
|||
|
|||
var syntax = new OrientationSyntax { Argument = argument }; |
|||
return (State.Middle, syntax); |
|||
} |
|||
if (identifier.SequenceEqual(IsOsKeyword.AsSpan()) && r.TakeIf('(')) |
|||
{ |
|||
var argument = ParseString(ref r); |
|||
Expect(ref r, ')'); |
|||
|
|||
var syntax = new IsOsSyntax { Argument = argument }; |
|||
return (State.Middle, syntax); |
|||
|
|||
} |
|||
|
|||
throw new InvalidOperationException("Invalid syntax found"); |
|||
} |
|||
|
|||
private static double ParseDecimal(ref CharacterReader r) |
|||
{ |
|||
var number = r.ParseNumber(); |
|||
if (number.IsEmpty) |
|||
{ |
|||
throw new ExpressionParseException(r.Position, $"Expected a number after."); |
|||
} |
|||
|
|||
return double.Parse(number.ToString()); |
|||
} |
|||
|
|||
private static T ParseEnum<T>(ref CharacterReader r) where T: struct |
|||
{ |
|||
var identifier = r.ParseIdentifier(); |
|||
|
|||
if (Enum.TryParse<T>(identifier.ToString(), true, out T value)) |
|||
return value; |
|||
|
|||
throw new ExpressionParseException(r.Position, $"Expected a {typeof(T)} after."); |
|||
} |
|||
|
|||
private static string ParseString(ref CharacterReader r) |
|||
{ |
|||
return r.ParseIdentifier().ToString(); |
|||
} |
|||
|
|||
private static void Expect(ref CharacterReader r, char c) |
|||
{ |
|||
if (r.End) |
|||
{ |
|||
throw new ExpressionParseException(r.Position, $"Expected '{c}', got end of selector."); |
|||
} |
|||
else if (!r.TakeIf(')')) |
|||
{ |
|||
throw new ExpressionParseException(r.Position, $"Expected '{c}', got '{r.Peek}'."); |
|||
} |
|||
} |
|||
|
|||
public class CommaSyntax : ISyntax |
|||
{ |
|||
public override bool Equals(object? obj) |
|||
{ |
|||
return obj is CommaSyntax; |
|||
} |
|||
} |
|||
|
|||
public class OrientationSyntax : ISyntax |
|||
{ |
|||
public DeviceOrientation Argument { get; set; } |
|||
|
|||
public override bool Equals(object? obj) |
|||
{ |
|||
return (obj is OrientationSyntax orientation) && orientation.Argument == Argument; |
|||
} |
|||
} |
|||
|
|||
public class IsOsSyntax : ISyntax |
|||
{ |
|||
public string Argument { get; set; } = string.Empty; |
|||
|
|||
public override bool Equals(object? obj) |
|||
{ |
|||
return (obj is IsOsSyntax orientation) && orientation.Argument == Argument; |
|||
} |
|||
} |
|||
|
|||
public class MinWidthSyntax : ISyntax |
|||
{ |
|||
public double Argument { get; set; } |
|||
|
|||
public override bool Equals(object? obj) |
|||
{ |
|||
return (obj is MinWidthSyntax minwidth) && minwidth.Argument == Argument; |
|||
} |
|||
} |
|||
|
|||
public class MinHeightSyntax : ISyntax |
|||
{ |
|||
public double Argument { get; set; } |
|||
|
|||
public override bool Equals(object? obj) |
|||
{ |
|||
return (obj is MinHeightSyntax minwidth) && minwidth.Argument == Argument; |
|||
} |
|||
} |
|||
|
|||
public class MaxWidthSyntax : ISyntax |
|||
{ |
|||
public double Argument { get; set; } |
|||
|
|||
public override bool Equals(object? obj) |
|||
{ |
|||
return (obj is MaxWidthSyntax maxwidth) && maxwidth.Argument == Argument; |
|||
} |
|||
} |
|||
|
|||
public class MaxHeightSyntax : ISyntax |
|||
{ |
|||
public double Argument { get; set; } |
|||
|
|||
public override bool Equals(object? obj) |
|||
{ |
|||
return (obj is MaxHeightSyntax maxHeight) && maxHeight.Argument == Argument; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,102 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using Avalonia.Styling; |
|||
using Avalonia.Utilities; |
|||
using System.Linq; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
|
|||
namespace Avalonia.Markup.Parsers |
|||
{ |
|||
/// <summary>
|
|||
/// Parses a <see cref="Selector"/> from text.
|
|||
/// </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) |
|||
{ |
|||
_typeResolver = typeResolver; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Parses a <see cref="Selector"/> from a string.
|
|||
/// </summary>
|
|||
/// <param name="s">The string.</param>
|
|||
/// <returns>The parsed selector.</returns>
|
|||
[RequiresUnreferencedCode(TrimmingMessages.SelectorsParseRequiresUnreferencedCodeMessage)] |
|||
public Query? Parse(string s) |
|||
{ |
|||
var syntax = MediaQueryGrammar.Parse(s); |
|||
return Create(syntax); |
|||
} |
|||
|
|||
[RequiresUnreferencedCode(TrimmingMessages.SelectorsParseRequiresUnreferencedCodeMessage)] |
|||
private Query? Create(IEnumerable<ISyntax> syntax) |
|||
{ |
|||
var result = default(Query); |
|||
var results = default(List<Query>); |
|||
|
|||
foreach (var i in syntax) |
|||
{ |
|||
switch (i) |
|||
{ |
|||
case MediaQueryGrammar.MinWidthSyntax minWidth: |
|||
result = Styling.Queries.MinWidth(result, minWidth.Argument); |
|||
break; |
|||
case MediaQueryGrammar.MaxWidthSyntax maxWidth: |
|||
result = Styling.Queries.MaxWidth(result, maxWidth.Argument); |
|||
break; |
|||
case MediaQueryGrammar.MinHeightSyntax minHeight: |
|||
result = Styling.Queries.MinHeight(result, minHeight.Argument); |
|||
break; |
|||
case MediaQueryGrammar.MaxHeightSyntax maxHeight: |
|||
result = Styling.Queries.MaxHeight(result, maxHeight.Argument); |
|||
break; |
|||
case MediaQueryGrammar.OrientationSyntax orientation: |
|||
result = Styling.Queries.Orientation(result, orientation.Argument); |
|||
break; |
|||
/*case MediaQueryGrammar.IsOsSyntax isOs: |
|||
result = Queries.IsOs(result, isOs.Argument); |
|||
break;*/ |
|||
default: |
|||
throw new NotSupportedException($"Unsupported selector grammar '{i.GetType()}'."); |
|||
} |
|||
} |
|||
|
|||
if (results != null) |
|||
{ |
|||
if (result != null) |
|||
{ |
|||
results.Add(result); |
|||
} |
|||
|
|||
result = results.Count > 1 ? Queries.Or(results) : results[0]; |
|||
} |
|||
|
|||
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; |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue