From 1ca55b4288d0d9209359dba0dc74353b672ff166 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Mon, 12 Jun 2023 08:11:44 +0000 Subject: [PATCH] add media tags --- samples/ControlCatalog/MainView.xaml | 109 +++-- src/Avalonia.Base/PropertyStore/ValueFrame.cs | 2 +- src/Avalonia.Base/StyledElement.cs | 3 + .../Styling/Activators/DeviceActivator.cs | 2 +- .../Styling/Activators/ScreenActivator.cs | 10 +- .../Styling/DeviceMediaSelector.cs | 8 +- src/Avalonia.Base/Styling/Media.cs | 79 ++++ src/Avalonia.Base/Styling/MediaQuery.cs | 29 ++ src/Avalonia.Base/Styling/MediaSelector.cs | 33 -- src/Avalonia.Base/Styling/OrQuery.cs | 95 +++++ src/Avalonia.Base/Styling/Queries.cs | 58 +++ src/Avalonia.Base/Styling/Query.cs | 154 +++++++ .../{ScreenSelector.cs => ScreenQueries.cs} | 40 +- src/Avalonia.Base/Styling/Selector.cs | 20 +- src/Avalonia.Base/Styling/Selectors.cs | 30 -- src/Avalonia.Base/Styling/Styles.cs | 1 + .../Avalonia.Build.Tasks.csproj | 6 + .../AvaloniaXamlIlCompiler.cs | 1 + ...olTemplateTargetTypeMetadataTransformer.cs | 3 +- .../AvaloniaXamlIlQueryTransformer.cs | 403 ++++++++++++++++++ .../AvaloniaXamlIlSelectorTransformer.cs | 131 +----- .../AvaloniaXamlIlWellKnownTypes.cs | 2 + .../IncludeXamlIlSre.props | 1 + .../Avalonia.Markup/Markup/Parsers/ISyntax.cs | 21 + .../Markup/Parsers/MediaQueryGrammar.cs | 271 ++++++++++++ .../Markup/Parsers/MediaQueryParser.cs | 102 +++++ .../Markup/Parsers/SelectorGrammar.cs | 129 +----- .../Markup/Parsers/SelectorParser.cs | 20 +- .../Parsers/SelectorGrammarTests.cs | 68 +-- 29 files changed, 1388 insertions(+), 443 deletions(-) create mode 100644 src/Avalonia.Base/Styling/Media.cs create mode 100644 src/Avalonia.Base/Styling/MediaQuery.cs delete mode 100644 src/Avalonia.Base/Styling/MediaSelector.cs create mode 100644 src/Avalonia.Base/Styling/OrQuery.cs create mode 100644 src/Avalonia.Base/Styling/Queries.cs create mode 100644 src/Avalonia.Base/Styling/Query.cs rename src/Avalonia.Base/Styling/{ScreenSelector.cs => ScreenQueries.cs} (74%) create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlQueryTransformer.cs create mode 100644 src/Markup/Avalonia.Markup/Markup/Parsers/ISyntax.cs create mode 100644 src/Markup/Avalonia.Markup/Markup/Parsers/MediaQueryGrammar.cs create mode 100644 src/Markup/Avalonia.Markup/Markup/Parsers/MediaQueryParser.cs diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index dbc495d933..f57cc82483 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -8,43 +8,78 @@ x:DataType="viewModels:MainWindowViewModel"> - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Base/PropertyStore/ValueFrame.cs b/src/Avalonia.Base/PropertyStore/ValueFrame.cs index 7a9d1bb13a..adbc651622 100644 --- a/src/Avalonia.Base/PropertyStore/ValueFrame.cs +++ b/src/Avalonia.Base/PropertyStore/ValueFrame.cs @@ -10,7 +10,7 @@ namespace Avalonia.PropertyStore { Style, TemplatedParentTheme, - Theme, + Theme } internal abstract class ValueFrame diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index b98e378338..f91a25feb4 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -845,6 +845,9 @@ namespace Avalonia private void ApplyStyle(IStyle style, IStyleHost? host, FrameType type) { + if (style is Styling.Media m) + m.TryAttach(this, host, type); + if (style is Style s) s.TryAttach(this, host, type); diff --git a/src/Avalonia.Base/Styling/Activators/DeviceActivator.cs b/src/Avalonia.Base/Styling/Activators/DeviceActivator.cs index 236aa77f2b..4c3e47d491 100644 --- a/src/Avalonia.Base/Styling/Activators/DeviceActivator.cs +++ b/src/Avalonia.Base/Styling/Activators/DeviceActivator.cs @@ -9,6 +9,6 @@ _argument = argument; } - protected override bool EvaluateIsActive() => CurrentMediaInfoProvider != null && IsOsMediaSelector.Evaluate(CurrentMediaInfoProvider, _argument).IsMatch; + protected override bool EvaluateIsActive() => CurrentMediaInfoProvider != null && IsOsMediaQuery.Evaluate(CurrentMediaInfoProvider, _argument).IsMatch; } } diff --git a/src/Avalonia.Base/Styling/Activators/ScreenActivator.cs b/src/Avalonia.Base/Styling/Activators/ScreenActivator.cs index 60b39b3fbf..5816db235d 100644 --- a/src/Avalonia.Base/Styling/Activators/ScreenActivator.cs +++ b/src/Avalonia.Base/Styling/Activators/ScreenActivator.cs @@ -13,7 +13,7 @@ namespace Avalonia.Styling.Activators _argument = argument; } - protected override bool EvaluateIsActive() => CurrentMediaInfoProvider != null && MinWidthMediaSelector.Evaluate(CurrentMediaInfoProvider, _argument).IsMatch; + protected override bool EvaluateIsActive() => CurrentMediaInfoProvider != null && MinWidthMediaQuery.Evaluate(CurrentMediaInfoProvider, _argument).IsMatch; } internal sealed class MaxWidthActivator : MediaQueryActivatorBase @@ -25,7 +25,7 @@ namespace Avalonia.Styling.Activators _argument = argument; } - protected override bool EvaluateIsActive() => CurrentMediaInfoProvider != null && MaxWidthMediaSelector.Evaluate(CurrentMediaInfoProvider, _argument).IsMatch; + protected override bool EvaluateIsActive() => CurrentMediaInfoProvider != null && MaxWidthMediaQuery.Evaluate(CurrentMediaInfoProvider, _argument).IsMatch; } internal sealed class MinHeightActivator : MediaQueryActivatorBase @@ -37,7 +37,7 @@ namespace Avalonia.Styling.Activators _argument = argument; } - protected override bool EvaluateIsActive() => CurrentMediaInfoProvider != null && MinHeightMediaSelector.Evaluate(CurrentMediaInfoProvider, _argument).IsMatch; + protected override bool EvaluateIsActive() => CurrentMediaInfoProvider != null && MinHeightMediaQuery.Evaluate(CurrentMediaInfoProvider, _argument).IsMatch; } internal sealed class MaxHeightActivator : MediaQueryActivatorBase @@ -49,7 +49,7 @@ namespace Avalonia.Styling.Activators _argument = argument; } - protected override bool EvaluateIsActive() => CurrentMediaInfoProvider != null && MaxHeightMediaSelector.Evaluate(CurrentMediaInfoProvider, _argument).IsMatch; + protected override bool EvaluateIsActive() => CurrentMediaInfoProvider != null && MaxHeightMediaQuery.Evaluate(CurrentMediaInfoProvider, _argument).IsMatch; } internal sealed class OrientationActivator : MediaQueryActivatorBase @@ -61,6 +61,6 @@ namespace Avalonia.Styling.Activators _argument = argument; } - protected override bool EvaluateIsActive() => CurrentMediaInfoProvider != null && OrientationMediaSelector.Evaluate(CurrentMediaInfoProvider, _argument).IsMatch; + protected override bool EvaluateIsActive() => CurrentMediaInfoProvider != null && OrientationMediaQuery.Evaluate(CurrentMediaInfoProvider, _argument).IsMatch; } } diff --git a/src/Avalonia.Base/Styling/DeviceMediaSelector.cs b/src/Avalonia.Base/Styling/DeviceMediaSelector.cs index cfeee24ea1..21ee0671f2 100644 --- a/src/Avalonia.Base/Styling/DeviceMediaSelector.cs +++ b/src/Avalonia.Base/Styling/DeviceMediaSelector.cs @@ -4,13 +4,13 @@ using Avalonia.Styling.Activators; namespace Avalonia.Styling { - internal sealed class IsOsMediaSelector : MediaSelector + internal sealed class IsOsMediaQuery : MediaQuery { - public IsOsMediaSelector(Selector? previous, string argument) : base(previous, argument) + public IsOsMediaQuery(Query? previous, string argument) : base(previous, argument) { } - private protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe) + internal override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe) { if (!(control is Visual visual)) { @@ -37,7 +37,7 @@ namespace Avalonia.Styling public override string ToString() => "is-os"; - public override string ToString(Style? owner) + public override string ToString(Media? owner) { throw new NotImplementedException(); } diff --git a/src/Avalonia.Base/Styling/Media.cs b/src/Avalonia.Base/Styling/Media.cs new file mode 100644 index 0000000000..3bacc8b1e3 --- /dev/null +++ b/src/Avalonia.Base/Styling/Media.cs @@ -0,0 +1,79 @@ +using System; +using Avalonia.Controls; +using Avalonia.PropertyStore; + +namespace Avalonia.Styling +{ + /// + /// Defines a media. + /// + public class Media : StyleBase + { + private Query? _query; + + /// + /// Initializes a new instance of the class. + /// + public Media() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The media selector. + public Media(Func selector) + { + Query = selector(null); + } + + /// + /// Gets or sets the media's selector. + /// + public Query? Query + { + get => _query; + set => _query = value; + } + + /// + /// Returns a string representation of the media. + /// + /// A string representation of the media. + 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; + } + } +} diff --git a/src/Avalonia.Base/Styling/MediaQuery.cs b/src/Avalonia.Base/Styling/MediaQuery.cs new file mode 100644 index 0000000000..7ce252b1a1 --- /dev/null +++ b/src/Avalonia.Base/Styling/MediaQuery.cs @@ -0,0 +1,29 @@ +using System; + +namespace Avalonia.Styling +{ + public abstract class MediaQuery : 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; + } +} diff --git a/src/Avalonia.Base/Styling/MediaSelector.cs b/src/Avalonia.Base/Styling/MediaSelector.cs deleted file mode 100644 index 9721c51e8d..0000000000 --- a/src/Avalonia.Base/Styling/MediaSelector.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; - -namespace Avalonia.Styling -{ - public abstract class MediaSelector : 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; - } -} diff --git a/src/Avalonia.Base/Styling/OrQuery.cs b/src/Avalonia.Base/Styling/OrQuery.cs new file mode 100644 index 0000000000..2c1bd65d5c --- /dev/null +++ b/src/Avalonia.Base/Styling/OrQuery.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Styling.Activators; + +#nullable enable + +namespace Avalonia.Styling +{ + /// + /// The OR style query. + /// + internal sealed class OrQuery : Query + { + private readonly IReadOnlyList _queries; + private string? _queryString; + private Type? _targetType; + + /// + /// Initializes a new instance of the class. + /// + /// The querys to OR. + public OrQuery(IReadOnlyList 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; + } + + /// + internal override bool IsCombinator => false; + + /// + 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; + } +} + diff --git a/src/Avalonia.Base/Styling/Queries.cs b/src/Avalonia.Base/Styling/Queries.cs new file mode 100644 index 0000000000..94b723834c --- /dev/null +++ b/src/Avalonia.Base/Styling/Queries.cs @@ -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); + } + + /// + /// Returns a query which ORs queries. + /// + /// The queries to be OR'd. + /// The query. + public static Query Or(params Query[] queries) + { + return new OrQuery(queries); + } + + /// + /// Returns a query which ORs queries. + /// + /// The queries to be OR'd. + /// The query. + public static Query Or(IReadOnlyList query) + { + return new OrQuery(query); + } + } +} diff --git a/src/Avalonia.Base/Styling/Query.cs b/src/Avalonia.Base/Styling/Query.cs new file mode 100644 index 0000000000..bc9a6dabb4 --- /dev/null +++ b/src/Avalonia.Base/Styling/Query.cs @@ -0,0 +1,154 @@ +using System; +using Avalonia.Styling.Activators; + +#nullable enable + +namespace Avalonia.Styling +{ + /// + /// A query in a . + /// + public abstract class Query + { + /// + /// Gets a value indicating whether this query is a combinator. + /// + /// + /// A combinator is a query such as Child or Descendent which links simple querys. + /// + internal abstract bool IsCombinator { get; } + + /// + /// Tries to match the query with a control. + /// + /// The control. + /// + /// The parent media, if the media containing the query is a nested media. + /// + /// + /// Whether the match should subscribe to changes in order to track the match over time, + /// or simply return an immediate result. + /// + /// A . + 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); + + /// + /// Gets a string representing the query, with the nesting separator (`^`) replaced with + /// the parent query. + /// + /// The owner media. + public abstract string ToString(Media? owner); + + /// + /// Evaluates the query for a match. + /// + /// The control. + /// + /// The parent media, if the media containing the query is a nested media. + /// + /// + /// Whether the match should subscribe to changes in order to track the match over time, + /// or simply return an immediate result. + /// + /// A . + internal abstract SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe); + + /// + /// Moves to the previous query. + /// + private protected abstract Query? MovePrevious(); + + /// + /// Moves to the previous query or the parent query. + /// + 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; + } + } +} + diff --git a/src/Avalonia.Base/Styling/ScreenSelector.cs b/src/Avalonia.Base/Styling/ScreenQueries.cs similarity index 74% rename from src/Avalonia.Base/Styling/ScreenSelector.cs rename to src/Avalonia.Base/Styling/ScreenQueries.cs index 740ccb56ff..96ec18655c 100644 --- a/src/Avalonia.Base/Styling/ScreenSelector.cs +++ b/src/Avalonia.Base/Styling/ScreenQueries.cs @@ -5,13 +5,13 @@ using Avalonia.Styling.Activators; namespace Avalonia.Styling { - internal sealed class MinWidthMediaSelector : MediaSelector + internal sealed class MinWidthMediaQuery : MediaQuery { - public MinWidthMediaSelector(Selector? previous, double argument) : base(previous, argument) + public MinWidthMediaQuery(Query? previous, double argument) : base(previous, argument) { } - private protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe) + internal override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe) { if (!(control is Visual visual)) { @@ -38,19 +38,19 @@ namespace Avalonia.Styling public override string ToString() => "min-width"; - public override string ToString(Style? owner) + public override string ToString(Media? owner) { throw new NotImplementedException(); } } - public sealed class MaxWidthMediaSelector : MediaSelector + public sealed class MaxWidthMediaQuery : MediaQuery { - public MaxWidthMediaSelector(Selector? previous, double argument) : base(previous, argument) + public MaxWidthMediaQuery(Query? previous, double argument) : base(previous, argument) { } - private protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe) + internal override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe) { if (!(control is Visual visual)) { @@ -77,19 +77,19 @@ namespace Avalonia.Styling public override string ToString() => "max-width"; - public override string ToString(Style? owner) + public override string ToString(Media? owner) { throw new NotImplementedException(); } } - public sealed class MinHeightMediaSelector : MediaSelector + public sealed class MinHeightMediaQuery : MediaQuery { - public MinHeightMediaSelector(Selector? previous, double argument) : base(previous, argument) + public MinHeightMediaQuery(Query? previous, double argument) : base(previous, argument) { } - private protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe) + internal override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe) { if (!(control is Visual visual)) { @@ -116,19 +116,19 @@ namespace Avalonia.Styling public override string ToString() => "min-height"; - public override string ToString(Style? owner) + public override string ToString(Media? owner) { throw new NotImplementedException(); } } - public sealed class MaxHeightMediaSelector : MediaSelector + public sealed class MaxHeightMediaQuery : MediaQuery { - public MaxHeightMediaSelector(Selector? previous, double argument) : base(previous, argument) + public MaxHeightMediaQuery(Query? previous, double argument) : base(previous, argument) { } - private protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe) + internal override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe) { if (!(control is Visual visual)) { @@ -155,19 +155,19 @@ namespace Avalonia.Styling public override string ToString() => "max-height"; - public override string ToString(Style? owner) + public override string ToString(Media? owner) { throw new NotImplementedException(); } } - internal sealed class OrientationMediaSelector : MediaSelector + internal sealed class OrientationMediaQuery : MediaQuery { - public OrientationMediaSelector(Selector? previous, DeviceOrientation argument) : base(previous, argument) + public OrientationMediaQuery(Query? previous, DeviceOrientation argument) : base(previous, argument) { } - private protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe) + internal override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe) { if (!(control is Visual visual)) { @@ -194,7 +194,7 @@ namespace Avalonia.Styling public override string ToString() => "orientation"; - public override string ToString(Style? owner) + public override string ToString(Media? owner) { throw new NotImplementedException(); } diff --git a/src/Avalonia.Base/Styling/Selector.cs b/src/Avalonia.Base/Styling/Selector.cs index 5ddc83cd6f..489c626142 100644 --- a/src/Avalonia.Base/Styling/Selector.cs +++ b/src/Avalonia.Base/Styling/Selector.cs @@ -166,8 +166,24 @@ namespace Avalonia.Styling } } + SelectorMatch match = SelectorMatch.NeverThisInstance; + bool mediaMatchesSometimes = false; + // Match any parent Media query + if (parent is Media media) + { + match = media.Query?.Evaluate(control, media.Parent, subscribe) ?? SelectorMatch.NeverThisInstance; + + if (!match.IsMatch) + return match.Result; + + mediaMatchesSometimes = match.Result == SelectorMatchResult.Sometimes; + + if (mediaMatchesSometimes) + activators.Add(match.Activator!); + } + // Match this selector. - var match = selector.Evaluate(control, parent, subscribe); + match = selector.Evaluate(control, parent, subscribe); if (!match.IsMatch) { @@ -184,7 +200,7 @@ namespace Avalonia.Styling combinator = previous; } - return match.Result; + return mediaMatchesSometimes ? SelectorMatchResult.Sometimes : match.Result; } } } diff --git a/src/Avalonia.Base/Styling/Selectors.cs b/src/Avalonia.Base/Styling/Selectors.cs index 4b196e3515..a42e32476e 100644 --- a/src/Avalonia.Base/Styling/Selectors.cs +++ b/src/Avalonia.Base/Styling/Selectors.cs @@ -137,26 +137,6 @@ namespace Avalonia.Styling return new NotSelector(previous, argument); } - public static Selector MinWidth(this Selector? previous, double argument) - { - return new MinWidthMediaSelector(previous, argument); - } - - public static Selector MaxWidth(this Selector? previous, double argument) - { - return new MaxWidthMediaSelector(previous, argument); - } - - public static Selector MinHeight(this Selector? previous, double argument) - { - return new MinHeightMediaSelector(previous, argument); - } - - public static Selector MaxHeight(this Selector? previous, double argument) - { - return new MaxHeightMediaSelector(previous, argument); - } - /// /// /// The selector. @@ -216,16 +196,6 @@ namespace Avalonia.Styling { return new OrSelector(selectors); } - - public static Selector Orientation(this Selector? previous, DeviceOrientation argument) - { - return new OrientationMediaSelector(previous, (DeviceOrientation)argument); - } - - public static Selector IsOs(this Selector? previous, string argument) - { - return new IsOsMediaSelector(previous, argument); - } /// /// Returns a selector which matches a control with the specified property value. diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs index 5d5b1617aa..01a1965225 100644 --- a/src/Avalonia.Base/Styling/Styles.cs +++ b/src/Avalonia.Base/Styling/Styles.cs @@ -17,6 +17,7 @@ namespace Avalonia.Styling IResourceProvider { private readonly AvaloniaList _styles = new(); + private readonly AvaloniaList _medias = new(); private IResourceHost? _owner; private IResourceDictionary? _resources; diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index 1d064e6662..5b226ecb2e 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -37,6 +37,12 @@ Markup/%(RecursiveDir)%(FileName)%(Extension) + + Markup/%(RecursiveDir)%(FileName)%(Extension) + + + Markup/%(RecursiveDir)%(FileName)%(Extension) + Markup/%(RecursiveDir)%(FileName)%(Extension) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 23c67df810..5b231bd1ef 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -50,6 +50,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions InsertBefore( new AvaloniaXamlIlControlThemeTransformer(), new AvaloniaXamlIlSelectorTransformer(), + new AvaloniaXamlIlQueryTransformer(), new AvaloniaXamlIlDuplicateSettersChecker(), new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), new AvaloniaXamlIlBindingPathParser(), diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs index f95d086bf6..417c02e601 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs @@ -57,7 +57,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { Style, ControlTemplate, - Transitions + Transitions, + Media } public AvaloniaXamlIlTargetTypeMetadataNode(IXamlAstValueNode value, IXamlAstTypeReference targetType, diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlQueryTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlQueryTransformer.cs new file mode 100644 index 0000000000..569ca159f2 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlQueryTransformer.cs @@ -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() + .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 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 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 + { + 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 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 context, IXamlILEmitter codeGen); + + protected void EmitCall(XamlEmitContext context, IXamlILEmitter codeGen, Func 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 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 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 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 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 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 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 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 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 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 context, IXamlILEmitter codeGen) + { + codeGen.Ldstr(_argument); + EmitCall(context, codeGen, + m => m.Name == "IsOs" && m.Parameters.Count == 2); + } + } + + class XamlIlOrQueryNode : XamlIlQueryNode + { + List _queries = new List(); + 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 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")); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs index 3454b25c8c..df9f12ea1a 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs @@ -43,7 +43,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers var selectorType = pn.Property.GetClrProperty().Getter.ReturnType; var initialNode = new XamlIlSelectorInitialNode(node, selectorType); var avaloniaAttachedPropertyT = context.GetAvaloniaTypes().AvaloniaAttachedPropertyT; - XamlIlSelectorNode Create(IEnumerable syntax, + XamlIlSelectorNode Create(IEnumerable syntax, Func typeResolver) { XamlIlSelectorNode result = initialNode; @@ -141,30 +141,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers case SelectorGrammar.NotSyntax not: result = new XamlIlNotSelector(result, Create(not.Argument, typeResolver)); break; - case SelectorGrammar.MinWidthSyntax minWidth: - result = new XamlIlMinWidthSelector(result, minWidth.Argument); - break; - case SelectorGrammar.MaxWidthSyntax maxWidth: - result = new XamlIlMaxWidthSelector(result, maxWidth.Argument); - break; - case SelectorGrammar.MinHeightSyntax minHeight: - result = new XamlIlMinHeightSelector(result, minHeight.Argument); - break; - case SelectorGrammar.MaxHeightSyntax maxHeight: - result = new XamlIlMaxHeightSelector(result, maxHeight.Argument); - break; case SelectorGrammar.NthChildSyntax nth: result = new XamlIlNthChildSelector(result, nth.Step, nth.Offset, XamlIlNthChildSelector.SelectorType.NthChild); break; case SelectorGrammar.NthLastChildSyntax nth: result = new XamlIlNthChildSelector(result, nth.Step, nth.Offset, XamlIlNthChildSelector.SelectorType.NthLastChild); break; - case SelectorGrammar.OrientationSyntax orientation: - result = new XamlIlOrientationSelector(result, orientation.Argument); - break; - case SelectorGrammar.IsOsSyntax isOs: - result = new XamlIlIsOsSelector(result, isOs.Argument); - break; case SelectorGrammar.CommaSyntax comma: if (results == null) results = new XamlIlOrSelectorNode(node, selectorType); @@ -192,7 +174,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers return results ?? result; } - IEnumerable parsed; + IEnumerable parsed; try { parsed = SelectorGrammar.Parse(tn.Text); @@ -403,115 +385,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } } - class XamlIlMinWidthSelector : XamlIlSelectorNode - { - private double _argument; - - public XamlIlMinWidthSelector(XamlIlSelectorNode previous, double argument) : base(previous) - { - _argument = argument; - } - - public override IXamlType TargetType => Previous?.TargetType; - protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) - { - codeGen.Ldc_R8(_argument); - EmitCall(context, codeGen, - m => m.Name == "MinWidth" && m.Parameters.Count == 2); - } - } - - class XamlIlMaxWidthSelector : XamlIlSelectorNode - { - private double _argument; - - public XamlIlMaxWidthSelector(XamlIlSelectorNode previous, double argument) : base(previous) - { - _argument = argument; - } - - public override IXamlType TargetType => Previous?.TargetType; - protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) - { - codeGen.Ldc_R8(_argument); - EmitCall(context, codeGen, - m => m.Name == "MaxWidth" && m.Parameters.Count == 2); - } - } - - class XamlIlMinHeightSelector : XamlIlSelectorNode - { - private double _argument; - - public XamlIlMinHeightSelector(XamlIlSelectorNode previous, double argument) : base(previous) - { - _argument = argument; - } - - public override IXamlType TargetType => Previous?.TargetType; - - protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) - { - codeGen.Ldc_R8(_argument); - EmitCall(context, codeGen, - m => m.Name == "MinHeight" && m.Parameters.Count == 2); - } - } - - class XamlIlMaxHeightSelector : XamlIlSelectorNode - { - private double _argument; - - public XamlIlMaxHeightSelector(XamlIlSelectorNode previous, double argument) : base(previous) - { - _argument = argument; - } - - public override IXamlType TargetType => Previous?.TargetType; - protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) - { - codeGen.Ldc_R8(_argument); - EmitCall(context, codeGen, - m => m.Name == "MaxHeight" && m.Parameters.Count == 2); - } - } - - class XamlIlOrientationSelector : XamlIlSelectorNode - { - private DeviceOrientation _argument; - - public XamlIlOrientationSelector(XamlIlSelectorNode previous, DeviceOrientation argument) : base(previous) - { - _argument = argument; - } - - public override IXamlType TargetType => Previous?.TargetType; - protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) - { - codeGen.Ldc_I4((int)_argument); - EmitCall(context, codeGen, - m => m.Name == "Orientation" && m.Parameters.Count == 2); - } - } - - class XamlIlIsOsSelector : XamlIlSelectorNode - { - private string _argument; - - public XamlIlIsOsSelector(XamlIlSelectorNode previous, string argument) : base(previous) - { - _argument = argument; - } - - public override IXamlType TargetType => Previous?.TargetType; - protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) - { - codeGen.Ldstr(_argument); - EmitCall(context, codeGen, - m => m.Name == "IsOs" && m.Parameters.Count == 2); - } - } - class XamlIlPropertyEqualsSelector : XamlIlSelectorNode { public XamlIlPropertyEqualsSelector(XamlIlSelectorNode previous, diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 05fe4bd2b8..5a6d11e6a4 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -116,6 +116,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType UriKind { get; } public IXamlConstructor UriConstructor { get; } public IXamlType Style { get; } + public IXamlType Media { get; } public IXamlType ControlTheme { get; } public IXamlType WindowTransparencyLevel { get; } public IXamlType IReadOnlyListOfT { get; } @@ -262,6 +263,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers UriKind = cfg.TypeSystem.GetType("System.UriKind"); UriConstructor = Uri.GetConstructor(new List() { cfg.WellKnownTypes.String, UriKind }); Style = cfg.TypeSystem.GetType("Avalonia.Styling.Style"); + Media = cfg.TypeSystem.GetType("Avalonia.Styling.Media"); ControlTheme = cfg.TypeSystem.GetType("Avalonia.Styling.ControlTheme"); IReadOnlyListOfT = cfg.TypeSystem.GetType("System.Collections.Generic.IReadOnlyList`1"); } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/IncludeXamlIlSre.props b/src/Markup/Avalonia.Markup.Xaml.Loader/IncludeXamlIlSre.props index c902fa956a..b8addd7233 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/IncludeXamlIlSre.props +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/IncludeXamlIlSre.props @@ -6,6 +6,7 @@ + diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ISyntax.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ISyntax.cs new file mode 100644 index 0000000000..ace13873df --- /dev/null +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ISyntax.cs @@ -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; } + } +} diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/MediaQueryGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/MediaQueryGrammar.cs new file mode 100644 index 0000000000..1832ce122d --- /dev/null +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/MediaQueryGrammar.cs @@ -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 Parse(string s) + { + var r = new CharacterReader(s.AsSpan()); + return Parse(ref r, null); + } + + private static IEnumerable Parse(ref CharacterReader r, char? end) + { + var state = State.Start; + var selector = new List(); + 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(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(ref CharacterReader r) where T: struct + { + var identifier = r.ParseIdentifier(); + + if (Enum.TryParse(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; + } + } + } +} diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/MediaQueryParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/MediaQueryParser.cs new file mode 100644 index 0000000000..b94e383298 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/MediaQueryParser.cs @@ -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 +{ + /// + /// Parses a from text. + /// + internal class MediaQueryParser + { + private readonly Func _typeResolver; + + /// + /// Initializes a new instance of the class. + /// + /// + /// 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. + /// + public MediaQueryParser(Func typeResolver) + { + _typeResolver = typeResolver; + } + + /// + /// Parses a from a string. + /// + /// The string. + /// The parsed selector. + [RequiresUnreferencedCode(TrimmingMessages.SelectorsParseRequiresUnreferencedCodeMessage)] + public Query? Parse(string s) + { + var syntax = MediaQueryGrammar.Parse(s); + return Create(syntax); + } + + [RequiresUnreferencedCode(TrimmingMessages.SelectorsParseRequiresUnreferencedCodeMessage)] + private Query? Create(IEnumerable syntax) + { + var result = default(Query); + var results = default(List); + + 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; + } + } +} diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index 97d71525fa..2c8ca7d991 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -176,14 +176,8 @@ namespace Avalonia.Markup.Parsers const string IsKeyword = "is"; const string NotKeyword = "not"; - const string MinWidthKeyword = "min-width"; - const string MaxWidthKeyword = "max-width"; - const string MinHeightKeyword = "min-height"; - const string MaxHeightKeyword = "max-height"; const string NthChildKeyword = "nth-child"; const string NthLastChildKeyword = "nth-last-child"; - const string OrientationKeyword = "orientation"; - const string IsOsKeyword = "is-os"; if (identifier.SequenceEqual(IsKeyword.AsSpan()) && r.TakeIf('(')) { @@ -200,38 +194,6 @@ namespace Avalonia.Markup.Parsers var syntax = new NotSyntax { Argument = argument }; return (State.Middle, syntax); } - 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(NthChildKeyword.AsSpan()) && r.TakeIf('(')) { var (step, offset) = ParseNthChildArguments(ref r); @@ -246,22 +208,6 @@ namespace Avalonia.Markup.Parsers var syntax = new NthLastChildSyntax { Step = step, Offset = offset }; return (State.Middle, syntax); } - if (identifier.SequenceEqual(OrientationKeyword.AsSpan()) && r.TakeIf('(')) - { - var argument = ParseEnum(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); - } else { return ( @@ -568,17 +514,6 @@ namespace Avalonia.Markup.Parsers } } - public interface ISyntax - { - } - - public interface ITypeSyntax - { - string TypeName { get; set; } - - string Xmlns { get; set; } - } - public class OfTypeSyntax : ISyntax, ITypeSyntax { public string TypeName { get; set; } = string.Empty; @@ -646,11 +581,11 @@ namespace Avalonia.Markup.Parsers syntax.Name == Name; } } - + public class DecimalSyntax : ISyntax { public decimal Number { get; set; } - + public override bool Equals(object? obj) { return obj is DecimalSyntax dec && dec.Number == Number; @@ -705,46 +640,6 @@ namespace Avalonia.Markup.Parsers } } - 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; - } - } - public class NthChildSyntax : ISyntax { public int Offset { get; set; } @@ -767,26 +662,6 @@ namespace Avalonia.Markup.Parsers } } - 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 CommaSyntax : ISyntax { public override bool Equals(object? obj) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs index b0f227005b..a141201e73 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs @@ -41,7 +41,7 @@ namespace Avalonia.Markup.Parsers } [RequiresUnreferencedCode(TrimmingMessages.SelectorsParseRequiresUnreferencedCodeMessage)] - private Selector? Create(IEnumerable syntax) + private Selector? Create(IEnumerable syntax) { var result = default(Selector); var results = default(List); @@ -152,30 +152,12 @@ namespace Avalonia.Markup.Parsers case SelectorGrammar.NotSyntax not: result = result.Not(x => Create(not.Argument)!); break; - case SelectorGrammar.MinWidthSyntax minWidth: - result = result.MinWidth(minWidth.Argument); - break; - case SelectorGrammar.MaxWidthSyntax maxWidth: - result = result.MaxWidth(maxWidth.Argument); - break; - case SelectorGrammar.MinHeightSyntax minHeight: - result = result.MinHeight(minHeight.Argument); - break; - case SelectorGrammar.MaxHeightSyntax maxHeight: - result = result.MaxHeight(maxHeight.Argument); - break; case SelectorGrammar.NthChildSyntax nth: result = result.NthChild(nth.Step, nth.Offset); break; case SelectorGrammar.NthLastChildSyntax nth: result = result.NthLastChild(nth.Step, nth.Offset); break; - case SelectorGrammar.OrientationSyntax orientation: - result = result.Orientation(orientation.Argument); - break; - case SelectorGrammar.IsOsSyntax isOs: - result = result.IsOs(isOs.Argument); - break; case SelectorGrammar.CommaSyntax comma: if (results == null) { diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs index b41f37eb3d..050acc9064 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs @@ -43,7 +43,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse("Button#foo"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, new SelectorGrammar.NameSyntax { Name = "foo" }, @@ -67,7 +67,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse(":is(Button)#foo"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.IsSyntax { TypeName = "Button" }, new SelectorGrammar.NameSyntax { Name = "foo" }, @@ -81,7 +81,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse(":is(x|Button)#foo"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.IsSyntax { TypeName = "Button", Xmlns = "x" }, new SelectorGrammar.NameSyntax { Name = "foo" }, @@ -115,7 +115,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse("Button.foo"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, new SelectorGrammar.ClassSyntax { Class = "foo" }, @@ -129,7 +129,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse("Button > .foo"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, new SelectorGrammar.ChildSyntax { }, @@ -144,7 +144,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse("Button>.foo"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, new SelectorGrammar.ChildSyntax { }, @@ -159,7 +159,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse("Button .foo"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, new SelectorGrammar.DescendantSyntax { }, @@ -174,7 +174,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse("Button /template/ .foo"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, new SelectorGrammar.TemplateSyntax { }, @@ -189,7 +189,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse("Button[Foo=bar]"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, new SelectorGrammar.PropertySyntax { Property = "Foo", Value = "bar" }, @@ -203,7 +203,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse("Button[(Grid.Column)=1]"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, new SelectorGrammar.AttachedPropertySyntax { @@ -221,7 +221,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse("Button[(x|Grid.Column)=1]"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, new SelectorGrammar.AttachedPropertySyntax { @@ -239,11 +239,11 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse(":not(Button)"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.NotSyntax { - Argument = new SelectorGrammar.ISyntax[] + Argument = new ISyntax[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, }, @@ -258,12 +258,12 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse("Button:not(.foo)"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, new SelectorGrammar.NotSyntax { - Argument = new SelectorGrammar.ISyntax[] + Argument = new ISyntax[] { new SelectorGrammar.ClassSyntax { Class = "foo" }, }, @@ -301,7 +301,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse(input); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.NthChildSyntax() { @@ -330,7 +330,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse(input); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.NthLastChildSyntax() { @@ -347,7 +347,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse("Button:nth-child(2n+1)"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, new SelectorGrammar.NthChildSyntax() @@ -365,7 +365,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse("Button:nth-child(2147483647n)"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, new SelectorGrammar.NthChildSyntax() @@ -383,7 +383,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse("Button:nth-last-child(2n+1)"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, new SelectorGrammar.NthLastChildSyntax() @@ -401,7 +401,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse("Button:nth-child(odd)"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, new SelectorGrammar.NthChildSyntax() @@ -419,7 +419,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse("Button:nth-child(even)"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, new SelectorGrammar.NthChildSyntax() @@ -437,13 +437,13 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse(":is(Control) :not(Button.foo)"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.IsSyntax { TypeName = "Control" }, new SelectorGrammar.DescendantSyntax { }, new SelectorGrammar.NotSyntax { - Argument = new SelectorGrammar.ISyntax[] + Argument = new ISyntax[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, new SelectorGrammar.ClassSyntax { Class = "foo" }, @@ -459,7 +459,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse("TextBlock, :is(Button).foo"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.OfTypeSyntax { TypeName = "TextBlock" }, new SelectorGrammar.CommaSyntax(), @@ -475,7 +475,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse("^.foo"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.NestingSyntax(), new SelectorGrammar.ClassSyntax { Class = "foo" }, @@ -489,7 +489,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse("^ > .foo"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.NestingSyntax(), new SelectorGrammar.ChildSyntax { }, @@ -504,7 +504,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse("^ .foo"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.NestingSyntax(), new SelectorGrammar.DescendantSyntax { }, @@ -519,7 +519,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse("^ /template/ .foo"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.NestingSyntax(), new SelectorGrammar.TemplateSyntax { }, @@ -534,7 +534,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse("Button /template/ ^"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, new SelectorGrammar.TemplateSyntax { }, @@ -549,7 +549,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse("^[Foo=bar]"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.NestingSyntax(), new SelectorGrammar.PropertySyntax { Property = "Foo", Value = "bar" }, @@ -563,7 +563,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse(":not(^)"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.NotSyntax { @@ -579,7 +579,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse("^:nth-child(2n+1)"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.NestingSyntax(), new SelectorGrammar.NthChildSyntax() @@ -597,7 +597,7 @@ namespace Avalonia.Markup.UnitTests.Parsers var result = SelectorGrammar.Parse("^, ^.foo"); Assert.Equal( - new SelectorGrammar.ISyntax[] + new ISyntax[] { new SelectorGrammar.NestingSyntax(), new SelectorGrammar.CommaSyntax(),