* add container queries * make visual query provider SetSize virtual * fix container name matching * add tests * move border container implementation up to decorator * move container query demo to ControlCatalog * make QueryProvider internal * update container behavior in toplevelRename Query to StyleQuery and make IContainer internal * fix comment typos * remove unused usings * isolate container tests * update api * fix tests * fix no-selector styles in containers being applied all the time * simplify container search * add docs to container properties * addressed api review * remove weird unmerge cooments * remove width and height event container subscriptions when visual is detachedrelease/11.3.0-beta1
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 60 KiB |
@ -0,0 +1,110 @@ |
|||
<UserControl x:Class="ControlCatalog.Pages.ContainerQueryPage" |
|||
xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
|||
xmlns:viewModels="using:ControlCatalog.ViewModels" |
|||
d:DesignHeight="800" |
|||
d:DesignWidth="400" |
|||
mc:Ignorable="d"> |
|||
<StackPanel Spacing="10"> |
|||
<StackPanel.Styles> |
|||
<ContainerQuery Name="UniformGrid" |
|||
Query="max-width:400"> |
|||
<Style Selector="UniformGrid#ContentGrid"> |
|||
<Setter Property="Columns" |
|||
Value="1"/> |
|||
</Style> |
|||
</ContainerQuery> |
|||
<ContainerQuery Name="UniformGrid" |
|||
Query="min-width:400"> |
|||
<Style Selector="UniformGrid#ContentGrid"> |
|||
<Setter Property="Columns" |
|||
Value="2"/> |
|||
</Style> |
|||
</ContainerQuery> |
|||
<ContainerQuery Name="UniformGrid" |
|||
Query="min-width:800"> |
|||
<Style Selector="UniformGrid#ContentGrid"> |
|||
<Setter Property="Columns" |
|||
Value="3"/> |
|||
</Style> |
|||
</ContainerQuery> |
|||
<ContainerQuery Name="UniformGrid" |
|||
Query="min-width:1200"> |
|||
<Style Selector="UniformGrid#ContentGrid"> |
|||
<Setter Property="Columns" |
|||
Value="4"/> |
|||
</Style> |
|||
</ContainerQuery> |
|||
</StackPanel.Styles> |
|||
<TextBlock Text="Dynamically change properties of controls based on the size of a parent container."/> |
|||
<Border Container.Name="UniformGrid" |
|||
VerticalAlignment="Stretch" |
|||
HorizontalAlignment="Stretch" |
|||
Container.Sizing="Width"> |
|||
<ScrollViewer VerticalScrollBarVisibility="Auto" |
|||
HorizontalScrollBarVisibility="Disabled"> |
|||
<Grid RowDefinitions="Auto,*"> |
|||
<UniformGrid Name="ContentGrid"> |
|||
<Border Margin="10" |
|||
HorizontalAlignment="Stretch" |
|||
CornerRadius="20" |
|||
ClipToBounds="True"> |
|||
<Image Stretch="Uniform" |
|||
HorizontalAlignment="Stretch" |
|||
Source="/Assets/image1.jpg"/> |
|||
</Border> |
|||
<Border Margin="10" |
|||
HorizontalAlignment="Stretch" |
|||
CornerRadius="20" |
|||
ClipToBounds="True"> |
|||
<Image Stretch="Uniform" |
|||
HorizontalAlignment="Stretch" |
|||
Source="/Assets/image2.jpg"/> |
|||
</Border> |
|||
<Border Margin="10" |
|||
HorizontalAlignment="Stretch" |
|||
CornerRadius="20" |
|||
ClipToBounds="True"> |
|||
<Image Stretch="Uniform" |
|||
HorizontalAlignment="Stretch" |
|||
Source="/Assets/image3.jpg"/> |
|||
</Border> |
|||
<Border Margin="10" |
|||
HorizontalAlignment="Stretch" |
|||
CornerRadius="20" |
|||
ClipToBounds="True"> |
|||
<Image Stretch="Uniform" |
|||
HorizontalAlignment="Stretch" |
|||
Source="/Assets/image4.jpg"/> |
|||
</Border> |
|||
<Border Margin="10" |
|||
HorizontalAlignment="Stretch" |
|||
CornerRadius="20" |
|||
ClipToBounds="True"> |
|||
<Image Stretch="Uniform" |
|||
HorizontalAlignment="Stretch" |
|||
Source="/Assets/image5.jpg"/> |
|||
</Border> |
|||
<Border Margin="10" |
|||
HorizontalAlignment="Stretch" |
|||
CornerRadius="20" |
|||
ClipToBounds="True"> |
|||
<Image Stretch="Uniform" |
|||
HorizontalAlignment="Stretch" |
|||
Source="/Assets/image6.jpg"/> |
|||
</Border> |
|||
<Border HorizontalAlignment="Stretch" |
|||
CornerRadius="20" |
|||
ClipToBounds="True"> |
|||
<Image Stretch="Uniform" |
|||
HorizontalAlignment="Stretch" |
|||
Source="/Assets/image7.jpg"/> |
|||
</Border> |
|||
</UniformGrid> |
|||
</Grid> |
|||
</ScrollViewer> |
|||
</Border> |
|||
</StackPanel> |
|||
</UserControl> |
|||
@ -0,0 +1,18 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace ControlCatalog.Pages |
|||
{ |
|||
public class ContainerQueryPage : UserControl |
|||
{ |
|||
public ContainerQueryPage() |
|||
{ |
|||
this.InitializeComponent(); |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
using System; |
|||
using System.Runtime.InteropServices; |
|||
|
|||
namespace Avalonia.Platform |
|||
{ |
|||
internal class VisualQueryProvider |
|||
{ |
|||
private readonly Visual _visual; |
|||
|
|||
public double Width { get; private set; } = double.PositiveInfinity; |
|||
|
|||
public double Height { get; private set; } = double.PositiveInfinity; |
|||
|
|||
public VisualQueryProvider(Visual visual) |
|||
{ |
|||
_visual = visual; |
|||
} |
|||
|
|||
public event EventHandler? WidthChanged; |
|||
public event EventHandler? HeightChanged; |
|||
|
|||
public virtual void SetSize(double width, double height, Styling.ContainerSizing containerType) |
|||
{ |
|||
var currentWidth = Width; |
|||
var currentHeight = Height; |
|||
|
|||
Width = width; |
|||
Height = height; |
|||
|
|||
if (currentWidth != Width && (containerType == Styling.ContainerSizing.Width || containerType == Styling.ContainerSizing.WidthAndHeight)) |
|||
WidthChanged?.Invoke(this, EventArgs.Empty); |
|||
if (currentHeight != Height && (containerType == Styling.ContainerSizing.Height || containerType == Styling.ContainerSizing.WidthAndHeight)) |
|||
HeightChanged?.Invoke(this, EventArgs.Empty); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,69 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Styling.Activators |
|||
{ |
|||
/// <summary>
|
|||
/// An aggregate <see cref="ContainerQueryActivatorBase"/> which is active when all of its inputs are
|
|||
/// active.
|
|||
/// </summary>
|
|||
internal class AndQueryActivator : ContainerQueryActivatorBase, IStyleActivatorSink |
|||
{ |
|||
private List<IStyleActivator>? _sources; |
|||
|
|||
public AndQueryActivator(Visual visual) : base(visual) |
|||
{ |
|||
} |
|||
|
|||
public int Count => _sources?.Count ?? 0; |
|||
|
|||
public void Add(IStyleActivator activator) |
|||
{ |
|||
if (IsSubscribed) |
|||
throw new AvaloniaInternalException("AndActivator is already subscribed."); |
|||
_sources ??= new List<IStyleActivator>(); |
|||
_sources.Add(activator); |
|||
} |
|||
|
|||
void IStyleActivatorSink.OnNext(bool value) => ReevaluateIsActive(); |
|||
|
|||
protected override bool EvaluateIsActive() |
|||
{ |
|||
if (_sources is null || _sources.Count == 0) |
|||
return true; |
|||
|
|||
var count = _sources.Count; |
|||
var mask = (1ul << count) - 1; |
|||
var flags = 0UL; |
|||
|
|||
for (var i = 0; i < count; ++i) |
|||
{ |
|||
if (_sources[i].GetIsActive()) |
|||
flags |= 1ul << i; |
|||
} |
|||
|
|||
return flags == mask; |
|||
} |
|||
|
|||
protected override void Initialize() |
|||
{ |
|||
if (_sources is object) |
|||
{ |
|||
foreach (var source in _sources) |
|||
{ |
|||
source.Subscribe(this); |
|||
} |
|||
} |
|||
} |
|||
|
|||
protected override void Deinitialize() |
|||
{ |
|||
if (_sources is object) |
|||
{ |
|||
foreach (var source in _sources) |
|||
{ |
|||
source.Unsubscribe(this); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Styling.Activators |
|||
{ |
|||
/// <summary>
|
|||
/// Builds an <see cref="AndActivator"/>.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// When ANDing style activators, if there is more than one input then creates an instance of
|
|||
/// <see cref="AndActivator"/>. If there is only one input, returns the input directly.
|
|||
/// </remarks>
|
|||
internal struct AndQueryActivatorBuilder |
|||
{ |
|||
private readonly Visual _visual; |
|||
private IStyleActivator? _single; |
|||
private AndQueryActivator? _multiple; |
|||
|
|||
public AndQueryActivatorBuilder(Visual visual) : this() |
|||
{ |
|||
_visual = visual; |
|||
} |
|||
|
|||
public int Count => _multiple?.Count ?? (_single is object ? 1 : 0); |
|||
|
|||
public void Add(IStyleActivator? activator) |
|||
{ |
|||
if (activator == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (_single is null && _multiple is null) |
|||
{ |
|||
_single = activator; |
|||
} |
|||
else |
|||
{ |
|||
if (_multiple is null) |
|||
{ |
|||
_multiple = new AndQueryActivator(_visual); |
|||
_multiple.Add(_single!); |
|||
_single = null; |
|||
} |
|||
|
|||
_multiple.Add(activator); |
|||
} |
|||
} |
|||
|
|||
public IStyleActivator Get() => _single ?? _multiple!; |
|||
} |
|||
} |
|||
@ -0,0 +1,95 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using Avalonia.Layout; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Styling.Activators |
|||
{ |
|||
internal abstract class ContainerQueryActivatorBase : StyleActivatorBase, IStyleActivatorSink |
|||
{ |
|||
private readonly Visual _visual; |
|||
private readonly string? _containerName; |
|||
private Layoutable? _currentScreenSizeProvider; |
|||
|
|||
public ContainerQueryActivatorBase( |
|||
Visual visual, string? containerName = null) |
|||
{ |
|||
_visual = visual; |
|||
_containerName = containerName; |
|||
} |
|||
|
|||
private void Visual_DetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e) |
|||
{ |
|||
DeInitializeScreenSizeProvider(); |
|||
} |
|||
|
|||
private void Visual_AttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) |
|||
{ |
|||
InitializeScreenSizeProvider(); |
|||
} |
|||
|
|||
protected Layoutable? CurrentContainer => _currentScreenSizeProvider; |
|||
|
|||
void IStyleActivatorSink.OnNext(bool value) => ReevaluateIsActive(); |
|||
|
|||
protected override void Initialize() |
|||
{ |
|||
InitializeScreenSizeProvider(); |
|||
|
|||
_visual.AttachedToVisualTree += Visual_AttachedToVisualTree; |
|||
_visual.DetachedFromVisualTree += Visual_DetachedFromVisualTree; |
|||
} |
|||
|
|||
protected override void Deinitialize() |
|||
{ |
|||
_visual.AttachedToVisualTree -= Visual_AttachedToVisualTree; |
|||
_visual.DetachedFromVisualTree -= Visual_DetachedFromVisualTree; |
|||
|
|||
DeInitializeScreenSizeProvider(); |
|||
} |
|||
|
|||
private void DeInitializeScreenSizeProvider() |
|||
{ |
|||
if (_currentScreenSizeProvider is { } && Container.GetQueryProvider(_currentScreenSizeProvider) is { } provider) |
|||
{ |
|||
provider.WidthChanged -= WidthChanged; |
|||
provider.HeightChanged -= HeightChanged; |
|||
_currentScreenSizeProvider = null; |
|||
} |
|||
} |
|||
|
|||
private void InitializeScreenSizeProvider() |
|||
{ |
|||
if (_currentScreenSizeProvider == null && GetContainer(_visual, _containerName) is { } container && Container.GetQueryProvider(container) is { } provider) |
|||
{ |
|||
_currentScreenSizeProvider = container; |
|||
|
|||
provider.WidthChanged += WidthChanged; |
|||
provider.HeightChanged += HeightChanged; |
|||
} |
|||
|
|||
ReevaluateIsActive(); |
|||
} |
|||
|
|||
internal static Layoutable? GetContainer(Visual visual, string? containerName) |
|||
{ |
|||
return visual.GetVisualAncestors().Where(x => x is Layoutable layoutable && |
|||
(Container.GetName(layoutable) == containerName)).FirstOrDefault() as Layoutable; |
|||
} |
|||
|
|||
private void HeightChanged(object? sender, EventArgs e) |
|||
{ |
|||
ReevaluateIsActive(); |
|||
} |
|||
|
|||
private void WidthChanged(object? sender, EventArgs e) |
|||
{ |
|||
ReevaluateIsActive(); |
|||
} |
|||
|
|||
private void OrientationChanged(object? sender, EventArgs e) |
|||
{ |
|||
ReevaluateIsActive(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Styling.Activators |
|||
{ |
|||
/// <summary>
|
|||
/// An aggregate <see cref="ContainerQueryActivatorBase"/> which is active when any of its inputs are
|
|||
/// active.
|
|||
/// </summary>
|
|||
internal class OrQueryActivator : ContainerQueryActivatorBase, IStyleActivatorSink |
|||
{ |
|||
private List<IStyleActivator>? _sources; |
|||
|
|||
public OrQueryActivator(Visual visual) : base(visual) |
|||
{ |
|||
} |
|||
|
|||
public int Count => _sources?.Count ?? 0; |
|||
|
|||
public void Add(IStyleActivator activator) |
|||
{ |
|||
_sources ??= new List<IStyleActivator>(); |
|||
_sources.Add(activator); |
|||
} |
|||
|
|||
void IStyleActivatorSink.OnNext(bool value) => ReevaluateIsActive(); |
|||
|
|||
protected override bool EvaluateIsActive() |
|||
{ |
|||
if (_sources is null || _sources.Count == 0) |
|||
return true; |
|||
|
|||
foreach (var source in _sources) |
|||
{ |
|||
if (source.GetIsActive()) |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
protected override void Initialize() |
|||
{ |
|||
if (_sources is object) |
|||
{ |
|||
foreach (var source in _sources) |
|||
{ |
|||
source.Subscribe(this); |
|||
} |
|||
} |
|||
} |
|||
|
|||
protected override void Deinitialize() |
|||
{ |
|||
if (_sources is object) |
|||
{ |
|||
foreach (var source in _sources) |
|||
{ |
|||
source.Unsubscribe(this); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Styling.Activators |
|||
{ |
|||
/// <summary>
|
|||
/// Builds an <see cref="OrActivator"/>.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// When ORing style activators, if there is more than one input then creates an instance of
|
|||
/// <see cref="OrActivator"/>. If there is only one input, returns the input directly.
|
|||
/// </remarks>
|
|||
internal struct OrQueryActivatorBuilder |
|||
{ |
|||
private IStyleActivator? _single; |
|||
private OrQueryActivator? _multiple; |
|||
private Visual _visual; |
|||
|
|||
public OrQueryActivatorBuilder(Visual visual) : this() |
|||
{ |
|||
_visual = visual; |
|||
} |
|||
|
|||
public int Count => _multiple?.Count ?? (_single is object ? 1 : 0); |
|||
|
|||
public void Add(IStyleActivator? activator) |
|||
{ |
|||
if (activator == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (_single is null && _multiple is null) |
|||
{ |
|||
_single = activator; |
|||
} |
|||
else |
|||
{ |
|||
if (_multiple is null) |
|||
{ |
|||
_multiple = new OrQueryActivator(_visual); |
|||
_multiple.Add(_single!); |
|||
_single = null; |
|||
} |
|||
|
|||
_multiple.Add(activator); |
|||
} |
|||
} |
|||
|
|||
public IStyleActivator Get() => _single ?? _multiple!; |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
using Avalonia.Layout; |
|||
|
|||
namespace Avalonia.Styling.Activators |
|||
{ |
|||
internal sealed class WidthActivator : ContainerQueryActivatorBase |
|||
{ |
|||
private readonly (StyleQueryComparisonOperator @operator, double value) _argument; |
|||
|
|||
public WidthActivator(Visual visual, (StyleQueryComparisonOperator @operator, double value) argument, string? containerName = null) : base(visual, containerName) |
|||
{ |
|||
_argument = argument; |
|||
} |
|||
|
|||
protected override bool EvaluateIsActive() => (CurrentContainer is Layoutable layoutable |
|||
&& Container.GetSizing(layoutable) is { } sizing |
|||
&& Container.GetQueryProvider(layoutable) is { } queryProvider |
|||
|
|||
&& (sizing is Styling.ContainerSizing.Width or Styling.ContainerSizing.WidthAndHeight)) |
|||
&& WidthQuery.Evaluate(queryProvider, _argument).IsMatch; |
|||
} |
|||
|
|||
internal sealed class HeightActivator : ContainerQueryActivatorBase |
|||
{ |
|||
private readonly (StyleQueryComparisonOperator @operator, double value) _argument; |
|||
|
|||
public HeightActivator(Visual visual, (StyleQueryComparisonOperator @operator, double value) argument, string? containerName = null) : base(visual, containerName) |
|||
{ |
|||
_argument = argument; |
|||
} |
|||
|
|||
protected override bool EvaluateIsActive() => (CurrentContainer is Layoutable layoutable |
|||
&& Container.GetSizing(layoutable) is { } sizing |
|||
&& Container.GetQueryProvider(layoutable) is { } queryProvider |
|||
&& (sizing is Styling.ContainerSizing.Height or Styling.ContainerSizing.WidthAndHeight)) |
|||
&& HeightQuery.Evaluate(queryProvider, _argument).IsMatch; |
|||
} |
|||
} |
|||
@ -0,0 +1,99 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Styling.Activators; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Styling |
|||
{ |
|||
/// <summary>
|
|||
/// The AND style query.
|
|||
/// </summary>
|
|||
internal sealed class AndQuery : StyleQuery |
|||
{ |
|||
private readonly IReadOnlyList<StyleQuery> _queries; |
|||
private string? _queryString; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AndQuery"/> class.
|
|||
/// </summary>
|
|||
/// <param name="queries">The queries to AND.</param>
|
|||
public AndQuery(IReadOnlyList<StyleQuery> queries) |
|||
{ |
|||
if (queries is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(queries)); |
|||
} |
|||
|
|||
if (queries.Count <= 1) |
|||
{ |
|||
throw new ArgumentException("Need more than one query to AND."); |
|||
} |
|||
|
|||
_queries = queries; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
internal override bool IsCombinator => false; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override string ToString(ContainerQuery? owner) |
|||
{ |
|||
if (_queryString == null) |
|||
{ |
|||
_queryString = string.Join(" and ", _queries.Select(x => x.ToString(owner))); |
|||
} |
|||
|
|||
return _queryString; |
|||
} |
|||
|
|||
internal override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe, string? containerName = null) |
|||
{ |
|||
if (control is not Visual visual) |
|||
{ |
|||
return SelectorMatch.NeverThisType; |
|||
} |
|||
|
|||
var activators = new AndQueryActivatorBuilder(visual); |
|||
var alwaysThisInstance = false; |
|||
|
|||
var count = _queries.Count; |
|||
|
|||
for (var i = 0; i < count; i++) |
|||
{ |
|||
var match = _queries[i].Match(control, parent, subscribe, containerName); |
|||
|
|||
switch (match.Result) |
|||
{ |
|||
case SelectorMatchResult.AlwaysThisInstance: |
|||
alwaysThisInstance = true; |
|||
break; |
|||
case SelectorMatchResult.NeverThisInstance: |
|||
case SelectorMatchResult.NeverThisType: |
|||
return match; |
|||
case SelectorMatchResult.Sometimes: |
|||
activators.Add(match.Activator!); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (activators.Count > 0) |
|||
{ |
|||
return new SelectorMatch(activators.Get()); |
|||
} |
|||
else if (alwaysThisInstance) |
|||
{ |
|||
return SelectorMatch.AlwaysThisInstance; |
|||
} |
|||
else |
|||
{ |
|||
return SelectorMatch.AlwaysThisType; |
|||
} |
|||
} |
|||
|
|||
private protected override StyleQuery? MovePrevious() => null; |
|||
private protected override StyleQuery? MovePreviousOrParent() => null; |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,91 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Layout; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Styling |
|||
{ |
|||
public static class Container |
|||
{ |
|||
/// <summary>
|
|||
/// Defines the Name attached property.
|
|||
/// </summary>
|
|||
public static readonly AttachedProperty<string?> NameProperty = |
|||
AvaloniaProperty.RegisterAttached<Layoutable, string?>("Name", typeof(Container)); |
|||
|
|||
/// <summary>
|
|||
/// Defines the Sizing attached property.
|
|||
/// </summary>
|
|||
public static readonly AttachedProperty<ContainerSizing> SizingProperty = |
|||
AvaloniaProperty.RegisterAttached<Layoutable, ContainerSizing>("Sizing", typeof(Container), coerce:UpdateQueryProvider); |
|||
|
|||
private static ContainerSizing UpdateQueryProvider(AvaloniaObject obj, ContainerSizing sizing) |
|||
{ |
|||
if (obj is Layoutable layoutable) |
|||
{ |
|||
if (sizing != ContainerSizing.Normal) |
|||
{ |
|||
if (GetQueryProvider(layoutable) == null) |
|||
layoutable.SetValue(QueryProviderProperty, new VisualQueryProvider(layoutable)); |
|||
} |
|||
else |
|||
{ |
|||
layoutable.SetValue(QueryProviderProperty, null); |
|||
} |
|||
} |
|||
|
|||
return sizing; |
|||
} |
|||
|
|||
internal static readonly AttachedProperty<VisualQueryProvider?> QueryProviderProperty = |
|||
AvaloniaProperty.RegisterAttached<Layoutable, VisualQueryProvider?>("QueryProvider", typeof(Container)); |
|||
|
|||
/// <summary>
|
|||
/// Gets the value of the Container.Name attached property.
|
|||
/// </summary>
|
|||
/// <param name="layoutable">The layoutable to read the value from.</param>
|
|||
/// <returns>The container name of the layoutable</returns>
|
|||
public static string? GetName(Layoutable layoutable) |
|||
{ |
|||
return layoutable.GetValue(NameProperty); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets the value of the Container.Name attached property.
|
|||
/// </summary>
|
|||
/// <param name="layoutable">The layoutable to set the value on.</param>
|
|||
/// <param name="name">The container name.</param>
|
|||
public static void SetName(Layoutable layoutable, string? name) |
|||
{ |
|||
layoutable.SetValue(NameProperty, name); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the value of the Container.Sizing attached property.
|
|||
/// </summary>
|
|||
/// <param name="layoutable">The layoutable to read the value from.</param>
|
|||
/// <returns>The container sizing mode of the layoutable</returns>
|
|||
public static ContainerSizing GetSizing(Layoutable layoutable) |
|||
{ |
|||
return layoutable.GetValue(SizingProperty); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets the value of the Container.Name attached property.
|
|||
/// </summary>
|
|||
/// <param name="layoutable">The layoutable to set the value on.</param>
|
|||
/// <param name="sizing">The container sizing mode.</param>
|
|||
public static void SetSizing(Layoutable layoutable, ContainerSizing sizing) |
|||
{ |
|||
layoutable.SetValue(SizingProperty, sizing); |
|||
} |
|||
|
|||
internal static VisualQueryProvider? GetQueryProvider(Layoutable layoutable) |
|||
{ |
|||
return layoutable.GetValue(QueryProviderProperty); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,90 @@ |
|||
using System; |
|||
using Avalonia.Controls; |
|||
using Avalonia.PropertyStore; |
|||
|
|||
namespace Avalonia.Styling |
|||
{ |
|||
/// <summary>
|
|||
/// Defines a container.
|
|||
/// </summary>
|
|||
public class ContainerQuery |
|||
: StyleBase |
|||
{ |
|||
private StyleQuery? _query; |
|||
private string? _name; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ContainerQuery"/> class.
|
|||
/// </summary>
|
|||
public ContainerQuery() |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ContainerQuery"/> class.
|
|||
/// </summary>
|
|||
/// <param name="query">The container selector.</param>
|
|||
/// <param name="containerName"></param>
|
|||
public ContainerQuery(Func<StyleQuery?, StyleQuery> query, string? containerName = null) |
|||
{ |
|||
Query = query(null); |
|||
_name = containerName; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the container's query.
|
|||
/// </summary>
|
|||
public StyleQuery? Query |
|||
{ |
|||
get => _query; |
|||
set => _query = value; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the container's name.
|
|||
/// </summary>
|
|||
public string? Name |
|||
{ |
|||
get => _name; |
|||
set => _name = value; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a string representation of the container.
|
|||
/// </summary>
|
|||
/// <returns>A string representation of the container.</returns>
|
|||
public override string ToString() => Query?.ToString(this) ?? "ContainerQuery"; |
|||
|
|||
internal override void SetParent(StyleBase? parent) |
|||
{ |
|||
if (parent is ControlTheme) |
|||
base.SetParent(parent); |
|||
else |
|||
throw new InvalidOperationException("Container cannot be added as a nested style."); |
|||
} |
|||
|
|||
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, Name) ?? |
|||
(target == host ? |
|||
SelectorMatch.AlwaysThisInstance : |
|||
SelectorMatch.NeverThisInstance); |
|||
|
|||
if (match.IsMatch) |
|||
{ |
|||
Attach(target, match.Activator, type, true); |
|||
} |
|||
|
|||
result = match.Result; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
namespace Avalonia.Styling |
|||
{ |
|||
/// <summary>
|
|||
/// Defines how a container is queried.
|
|||
/// </summary>
|
|||
public enum ContainerSizing |
|||
{ |
|||
/// <summary>
|
|||
/// The container is not included in any size queries.
|
|||
/// </summary>
|
|||
Normal, |
|||
|
|||
/// <summary>
|
|||
/// The container size can be queried for width.
|
|||
/// </summary>
|
|||
Width, |
|||
|
|||
/// <summary>
|
|||
/// The container size can be queried for height.
|
|||
/// </summary>
|
|||
Height, |
|||
|
|||
/// <summary>
|
|||
/// The container size can be queried for width and height.
|
|||
/// </summary>
|
|||
WidthAndHeight |
|||
} |
|||
} |
|||
@ -0,0 +1,99 @@ |
|||
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 : StyleQuery |
|||
{ |
|||
private readonly IReadOnlyList<StyleQuery> _queries; |
|||
private string? _queryString; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="OrQuery"/> class.
|
|||
/// </summary>
|
|||
/// <param name="queries">The querys to OR.</param>
|
|||
public OrQuery(IReadOnlyList<StyleQuery> 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(ContainerQuery? 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, string? containerName = null) |
|||
{ |
|||
if (control is not Visual visual) |
|||
{ |
|||
return SelectorMatch.NeverThisType; |
|||
} |
|||
|
|||
var activators = new OrQueryActivatorBuilder(visual); |
|||
var neverThisInstance = false; |
|||
|
|||
var count = _queries.Count; |
|||
|
|||
for (var i = 0; i < count; i++) |
|||
{ |
|||
var match = _queries[i].Match(control, parent, subscribe, containerName); |
|||
|
|||
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 StyleQuery? MovePrevious() => null; |
|||
private protected override StyleQuery? MovePreviousOrParent() => null; |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,149 @@ |
|||
using System; |
|||
using Avalonia.Layout; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Styling.Activators; |
|||
|
|||
namespace Avalonia.Styling |
|||
{ |
|||
internal sealed class WidthQuery : ValueStyleQuery<(StyleQueryComparisonOperator @operator, double value)> |
|||
{ |
|||
public WidthQuery(StyleQuery? previous, StyleQueryComparisonOperator @operator, double value) : base(previous, (@operator, value)) |
|||
{ |
|||
} |
|||
|
|||
internal override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe, string? containerName = null) |
|||
{ |
|||
if (control is not Visual visual) |
|||
{ |
|||
return SelectorMatch.NeverThisType; |
|||
} |
|||
|
|||
if (subscribe) |
|||
{ |
|||
return new SelectorMatch(new WidthActivator(visual, Argument, containerName)); |
|||
} |
|||
|
|||
if (ContainerQueryActivatorBase.GetContainer(visual, containerName) is { } container |
|||
&& container is Layoutable layoutable |
|||
&& Container.GetQueryProvider(layoutable) is { } queryProvider |
|||
&& Container.GetSizing(layoutable) == Styling.ContainerSizing.WidthAndHeight) |
|||
{ |
|||
return Evaluate(queryProvider, Argument); |
|||
} |
|||
|
|||
return SelectorMatch.NeverThisInstance; |
|||
} |
|||
|
|||
internal static SelectorMatch Evaluate(VisualQueryProvider queryProvider, (StyleQueryComparisonOperator @operator, double value) argument) |
|||
{ |
|||
var width = queryProvider.Width; |
|||
if (double.IsNaN(width)) |
|||
{ |
|||
return SelectorMatch.NeverThisInstance; |
|||
} |
|||
|
|||
bool IsTrue(StyleQueryComparisonOperator comparisonOperator, double value) |
|||
{ |
|||
switch (comparisonOperator) |
|||
{ |
|||
case StyleQueryComparisonOperator.None: |
|||
return true; |
|||
case StyleQueryComparisonOperator.Equals: |
|||
return width == value; |
|||
case StyleQueryComparisonOperator.LessThan: |
|||
return width < value; |
|||
case StyleQueryComparisonOperator.GreaterThan: |
|||
return width > value; |
|||
case StyleQueryComparisonOperator.LessThanOrEquals: |
|||
return width <= value; |
|||
case StyleQueryComparisonOperator.GreaterThanOrEquals: |
|||
return width >= value; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
return IsTrue(argument.@operator, argument.value) ? |
|||
new SelectorMatch(SelectorMatchResult.AlwaysThisInstance) : SelectorMatch.NeverThisInstance; |
|||
} |
|||
|
|||
public override string ToString() => "width"; |
|||
|
|||
public override string ToString(ContainerQuery? owner) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
} |
|||
|
|||
internal sealed class HeightQuery : ValueStyleQuery<(StyleQueryComparisonOperator @operator, double value)> |
|||
{ |
|||
public HeightQuery(StyleQuery? previous, StyleQueryComparisonOperator @operator, double value) : base(previous, (@operator, value)) |
|||
{ |
|||
} |
|||
|
|||
internal override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe, string? containerName = null) |
|||
{ |
|||
if (control is not Visual visual) |
|||
{ |
|||
return SelectorMatch.NeverThisType; |
|||
} |
|||
|
|||
if (subscribe) |
|||
{ |
|||
return new SelectorMatch(new HeightActivator(visual, Argument)); |
|||
} |
|||
|
|||
if (ContainerQueryActivatorBase.GetContainer(visual, containerName) is { } container |
|||
&& container is Layoutable layoutable |
|||
&& Container.GetQueryProvider(layoutable) is { } queryProvider |
|||
&& Container.GetSizing(layoutable) == Styling.ContainerSizing.WidthAndHeight) |
|||
{ |
|||
return Evaluate(queryProvider, Argument); |
|||
} |
|||
|
|||
return SelectorMatch.NeverThisInstance; |
|||
} |
|||
|
|||
internal static SelectorMatch Evaluate(VisualQueryProvider screenSizeProvider, (StyleQueryComparisonOperator @operator, double value) argument) |
|||
{ |
|||
var height = screenSizeProvider.Height; |
|||
if (double.IsNaN(height)) |
|||
{ |
|||
return SelectorMatch.NeverThisInstance; |
|||
} |
|||
|
|||
var isvalueTrue = IsTrue(argument.@operator, argument.value); |
|||
|
|||
bool IsTrue(StyleQueryComparisonOperator comparisonOperator, double value) |
|||
{ |
|||
switch (comparisonOperator) |
|||
{ |
|||
case StyleQueryComparisonOperator.None: |
|||
return true; |
|||
case StyleQueryComparisonOperator.Equals: |
|||
return height == value; |
|||
case StyleQueryComparisonOperator.LessThan: |
|||
return height < value; |
|||
case StyleQueryComparisonOperator.GreaterThan: |
|||
return height > value; |
|||
case StyleQueryComparisonOperator.LessThanOrEquals: |
|||
return height <= value; |
|||
case StyleQueryComparisonOperator.GreaterThanOrEquals: |
|||
return height >= value; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
return IsTrue(argument.@operator, argument.value) ? |
|||
SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; |
|||
} |
|||
|
|||
public override string ToString() => "height"; |
|||
|
|||
public override string ToString(ContainerQuery? owner) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,77 @@ |
|||
using Avalonia.Platform; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Styling |
|||
{ |
|||
/// <summary>
|
|||
/// Extension methods for <see cref="StyleQuery"/>.
|
|||
/// </summary>
|
|||
public static class StyleQueries |
|||
{ |
|||
/// <summary>
|
|||
/// Returns a query which matches the device width with a value.
|
|||
/// </summary>
|
|||
/// <param name="previous">The previous query.</param>
|
|||
/// <param name="operator">The operator to match the device width</param>
|
|||
/// <param name="value">The width to match</param>
|
|||
/// <returns>The query.</returns>
|
|||
public static StyleQuery Width(this StyleQuery? previous, StyleQueryComparisonOperator @operator, double value) |
|||
{ |
|||
return new WidthQuery(previous, @operator, value); |
|||
} |
|||
|
|||
|
|||
|
|||
/// <summary>
|
|||
/// Returns a query which matches the device height with a value.
|
|||
/// </summary>
|
|||
/// <param name="previous">The previous query.</param>
|
|||
/// <param name="operator">The operator to match the device height</param>
|
|||
/// <param name="value">The height to match</param>
|
|||
/// <returns>The query.</returns>
|
|||
public static StyleQuery Height(this StyleQuery? previous, StyleQueryComparisonOperator @operator, double value) |
|||
{ |
|||
return new HeightQuery(previous, @operator, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a query which ORs queries.
|
|||
/// </summary>
|
|||
/// <param name="queries">The queries to be OR'd.</param>
|
|||
/// <returns>The query.</returns>
|
|||
public static StyleQuery Or(params StyleQuery[] 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 StyleQuery Or(IReadOnlyList<StyleQuery> query) |
|||
{ |
|||
return new OrQuery(query); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a query which ANDs queries.
|
|||
/// </summary>
|
|||
/// <param name="queries">The queries to be AND'd.</param>
|
|||
/// <returns>The query.</returns>
|
|||
public static StyleQuery And(params StyleQuery[] queries) |
|||
{ |
|||
return new AndQuery(queries); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a query which ANDs queries.
|
|||
/// </summary>
|
|||
/// <param name="query">The queries to be AND'd.</param>
|
|||
/// <returns>The query.</returns>
|
|||
public static StyleQuery And(IReadOnlyList<StyleQuery> query) |
|||
{ |
|||
return new AndQuery(query); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,164 @@ |
|||
using System; |
|||
using Avalonia.Styling.Activators; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Styling |
|||
{ |
|||
/// <summary>
|
|||
/// A query in a <see cref="ContainerQuery"/>.
|
|||
/// </summary>
|
|||
public abstract class StyleQuery |
|||
{ |
|||
/// <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; } |
|||
|
|||
internal StyleQuery() { } |
|||
|
|||
/// <summary>
|
|||
/// Tries to match the query with a control.
|
|||
/// </summary>
|
|||
/// <param name="control">The control.</param>
|
|||
/// <param name="parent">
|
|||
/// The parent container, if the container containing the query is a nested container.
|
|||
/// </param>
|
|||
/// <param name="subscribe">
|
|||
/// Whether the match should subscribe to changes in order to track the match over time,
|
|||
/// or simply return an imcontainerte result.
|
|||
/// </param>
|
|||
/// <param name="containerName">
|
|||
/// The name of container to query on.
|
|||
/// </param>
|
|||
/// <returns>A <see cref="SelectorMatch"/>.</returns>
|
|||
internal virtual SelectorMatch Match(StyledElement control, IStyle? parent = null, bool subscribe = true, string? containerName = null) |
|||
{ |
|||
// 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, containerName); |
|||
|
|||
// 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, containerName)); |
|||
|
|||
// 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 container.</param>
|
|||
public abstract string ToString(ContainerQuery? owner); |
|||
|
|||
/// <summary>
|
|||
/// Evaluates the query for a match.
|
|||
/// </summary>
|
|||
/// <param name="control">The control.</param>
|
|||
/// <param name="parent">
|
|||
/// The parent container, if the container containing the query is a nested container.
|
|||
/// </param>
|
|||
/// <param name="subscribe">
|
|||
/// Whether the match should subscribe to changes in order to track the match over time,
|
|||
/// or simply return an imcontainerte result.
|
|||
/// </param>
|
|||
/// <param name="containerName">
|
|||
/// The name of the container to evaluate.
|
|||
/// </param>
|
|||
/// <returns>A <see cref="SelectorMatch"/>.</returns>
|
|||
internal abstract SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe, string? containerName); |
|||
|
|||
/// <summary>
|
|||
/// Moves to the previous query.
|
|||
/// </summary>
|
|||
private protected abstract StyleQuery? MovePrevious(); |
|||
|
|||
/// <summary>
|
|||
/// Moves to the previous query or the parent query.
|
|||
/// </summary>
|
|||
private protected abstract StyleQuery? MovePreviousOrParent(); |
|||
|
|||
private static SelectorMatch MatchUntilCombinator( |
|||
StyledElement control, |
|||
StyleQuery start, |
|||
IStyle? parent, |
|||
bool subscribe, |
|||
out StyleQuery? combinator, |
|||
string? containerName = null) |
|||
{ |
|||
combinator = null; |
|||
|
|||
var activators = new AndActivatorBuilder(); |
|||
var result = Match(control, start, parent, subscribe, ref activators, ref combinator, containerName); |
|||
|
|||
return result == SelectorMatchResult.Sometimes ? |
|||
new SelectorMatch(activators.Get()) : |
|||
new SelectorMatch(result); |
|||
} |
|||
|
|||
private static SelectorMatchResult Match( |
|||
StyledElement control, |
|||
StyleQuery query, |
|||
IStyle? parent, |
|||
bool subscribe, |
|||
ref AndActivatorBuilder activators, |
|||
ref StyleQuery? combinator, |
|||
string? containerName) |
|||
{ |
|||
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, containerName); |
|||
|
|||
if (previousMatch < SelectorMatchResult.Sometimes) |
|||
{ |
|||
return previousMatch; |
|||
} |
|||
} |
|||
|
|||
// Match this query.
|
|||
var match = query.Evaluate(control, parent, subscribe, containerName); |
|||
|
|||
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,12 @@ |
|||
namespace Avalonia.Styling |
|||
{ |
|||
public enum StyleQueryComparisonOperator |
|||
{ |
|||
None, |
|||
Equals, |
|||
LessThan, |
|||
GreaterThan, |
|||
LessThanOrEquals, |
|||
GreaterThanOrEquals, |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Styling |
|||
{ |
|||
internal abstract class ValueStyleQuery<T> : StyleQuery |
|||
{ |
|||
private readonly StyleQuery? _previous; |
|||
private T _argument; |
|||
|
|||
internal ValueStyleQuery(StyleQuery? previous, T argument) |
|||
{ |
|||
_previous = previous; |
|||
_argument = argument; |
|||
} |
|||
|
|||
protected T Argument => _argument; |
|||
|
|||
internal override bool IsCombinator => false; |
|||
|
|||
public override string ToString(ContainerQuery? owner) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
private protected override StyleQuery? MovePrevious() => _previous; |
|||
|
|||
private protected override StyleQuery? MovePreviousOrParent() => _previous; |
|||
} |
|||
} |
|||
@ -0,0 +1,424 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Markup.Parsers; |
|||
using Avalonia.Styling; |
|||
using XamlX; |
|||
using XamlX.Ast; |
|||
using XamlX.Emit; |
|||
using XamlX.IL; |
|||
using XamlX.Transform; |
|||
using XamlX.TypeSystem; |
|||
|
|||
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers |
|||
{ |
|||
using static Avalonia.Markup.Parsers.ContainerQueryGrammar; |
|||
using XamlLoadException = XamlX.XamlLoadException; |
|||
using XamlParseException = XamlX.XamlParseException; |
|||
class AvaloniaXamlIlQueryTransformer : IXamlAstTransformer |
|||
{ |
|||
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) |
|||
{ |
|||
if (node is not XamlAstObjectNode on || |
|||
!context.GetAvaloniaTypes().Container.IsAssignableFrom(on.Type.GetClrType())) |
|||
return node; |
|||
|
|||
var pn = on.Children.OfType<XamlAstXamlPropertyValueNode>() |
|||
.FirstOrDefault(p => p.Property.GetClrProperty().Name == "Query"); |
|||
|
|||
if (pn == null || pn.Property.GetClrProperty().Getter is not { } getter) |
|||
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 = getter.ReturnType; |
|||
var initialNode = new XamlIlQueryInitialNode(node, queryType); |
|||
var avaloniaAttachedPropertyT = context.GetAvaloniaTypes().AvaloniaAttachedPropertyT; |
|||
XamlIlQueryNode Create(IEnumerable<ISyntax> syntax) |
|||
{ |
|||
XamlIlQueryNode result = initialNode; |
|||
XamlIlOrQueryNode? results = null; |
|||
XamlIlAndQueryNode? andNode = null; |
|||
foreach (var i in syntax) |
|||
{ |
|||
switch (i) |
|||
{ |
|||
case ContainerQueryGrammar.WidthSyntax width: |
|||
result = new XamlIlWidthQuery(result, width); |
|||
break; |
|||
case ContainerQueryGrammar.HeightSyntax height: |
|||
result = new XamlIlHeightQuery(result, height); |
|||
break; |
|||
case ContainerQueryGrammar.OrSyntax or: |
|||
if (results == null) |
|||
results = new XamlIlOrQueryNode(node, queryType); |
|||
if (andNode != null && result == initialNode) |
|||
throw new XamlParseException($"Previously opened And node is not closed.", node); |
|||
results.Add(andNode ?? result); |
|||
result = initialNode; |
|||
andNode = null; |
|||
break; |
|||
case ContainerQueryGrammar.AndSyntax and: |
|||
if (andNode == null) |
|||
andNode = new XamlIlAndQueryNode(node, queryType); |
|||
andNode.Add(result); |
|||
result = initialNode; |
|||
break; |
|||
default: |
|||
throw new XamlParseException($"Unsupported query grammar '{i.GetType()}'.", node); |
|||
} |
|||
|
|||
if (andNode != null && result != initialNode) |
|||
{ |
|||
andNode.Add(result); |
|||
} |
|||
} |
|||
|
|||
if (results != null && result != null) |
|||
{ |
|||
results.Add(result); |
|||
} |
|||
|
|||
return results ?? result ?? initialNode; |
|||
} |
|||
|
|||
IEnumerable<ISyntax> parsed; |
|||
try |
|||
{ |
|||
parsed = ContainerQueryGrammar.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.Container); |
|||
} |
|||
} |
|||
|
|||
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.StyleQueries"); |
|||
var found = queries.FindMethod(m => m.IsStatic && m.Parameters.Count > 0 && method(m)); |
|||
if(found == null) |
|||
throw new XamlTypeSystemException( |
|||
$"Unable to find {TargetType} in Avalonia.Styling.StyleQueries"); |
|||
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 XamlIlWidthQuery : XamlIlQueryNode |
|||
{ |
|||
private ContainerQueryGrammar.WidthSyntax _argument; |
|||
|
|||
public XamlIlWidthQuery(XamlIlQueryNode previous, ContainerQueryGrammar.WidthSyntax 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.Operator); |
|||
codeGen.Ldc_R8(_argument.Value); |
|||
EmitCall(context, codeGen, |
|||
m => m.Name == "Width" && m.Parameters.Count == 3); |
|||
} |
|||
} |
|||
|
|||
class XamlIlHeightQuery : XamlIlQueryNode |
|||
{ |
|||
private ContainerQueryGrammar.HeightSyntax _argument; |
|||
|
|||
public XamlIlHeightQuery(XamlIlQueryNode previous, ContainerQueryGrammar.HeightSyntax 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.Operator); |
|||
codeGen.Ldc_R8(_argument.Value); |
|||
EmitCall(context, codeGen, |
|||
m => m.Name == "Height" && m.Parameters.Count == 3); |
|||
} |
|||
} |
|||
|
|||
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) == false) |
|||
{ |
|||
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; |
|||
} |
|||
|
|||
if (context.Configuration.TypeSystem.FindType("System.Collections.Generic.List`1") is not { } type) |
|||
return; |
|||
|
|||
IXamlType listType = type.MakeGenericType(base.Type.GetClrType()); |
|||
var add = listType.FindMethod("Add", context.Configuration.WellKnownTypes.Void, false, Type.GetClrType()); |
|||
if (add == null) |
|||
return; |
|||
|
|||
var ctor = listType.FindConstructor(); |
|||
if (ctor == null) |
|||
{ |
|||
return; |
|||
} |
|||
codeGen |
|||
.Newobj(ctor); |
|||
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")); |
|||
} |
|||
} |
|||
|
|||
class XamlIlAndQueryNode : XamlIlQueryNode |
|||
{ |
|||
List<XamlIlQueryNode> _queries = new List<XamlIlQueryNode>(); |
|||
public XamlIlAndQueryNode(IXamlLineInfo info, IXamlType queryType) : base(null, info, queryType) |
|||
{ |
|||
} |
|||
|
|||
public void Add(XamlIlQueryNode node) |
|||
{ |
|||
_queries.Add(node); |
|||
} |
|||
|
|||
public override IXamlType? TargetType |
|||
{ |
|||
get |
|||
{ |
|||
IXamlType? result = null; |
|||
|
|||
foreach (var query in _queries) |
|||
{ |
|||
if (query.TargetType == null) |
|||
{ |
|||
return null; |
|||
} |
|||
else if (result == null) |
|||
{ |
|||
result = query.TargetType; |
|||
} |
|||
else |
|||
{ |
|||
while (result?.IsAssignableFrom(query.TargetType) == false) |
|||
{ |
|||
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; |
|||
} |
|||
|
|||
if (context.Configuration.TypeSystem.FindType("System.Collections.Generic.List`1") is not { } type) |
|||
return; |
|||
|
|||
IXamlType listType = type.MakeGenericType(base.Type.GetClrType()); |
|||
var add = listType.FindMethod("Add", context.Configuration.WellKnownTypes.Void, false, Type.GetClrType()); |
|||
if (add == null) |
|||
return; |
|||
|
|||
var ctor = listType.FindConstructor(); |
|||
if (ctor == null) |
|||
{ |
|||
return; |
|||
} |
|||
codeGen |
|||
.Newobj(ctor); |
|||
foreach (var s in _queries) |
|||
{ |
|||
codeGen.Dup(); |
|||
context.Emit(s, codeGen, Type.GetClrType()); |
|||
codeGen.EmitCall(add, true); |
|||
} |
|||
|
|||
EmitCall(context, codeGen, |
|||
m => m.Name == "And" && m.Parameters.Count == 1 && m.Parameters[0].Name.StartsWith("IReadOnlyList")); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,267 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Data.Core; |
|||
using Avalonia.Styling; |
|||
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 ContainerQueryGrammar |
|||
{ |
|||
const string MinWidthKeyword = "min-width"; |
|||
const string MinHeightKeyword = "min-height"; |
|||
const string MaxWidthKeyword = "max-width"; |
|||
const string MaxHeightKeyword = "max-height"; |
|||
const string WidthKeyword = "width"; |
|||
const string HeightKeyword = "height"; |
|||
static string[] Keywords = new string[] { MinWidthKeyword, MinHeightKeyword, MaxWidthKeyword, MaxHeightKeyword, WidthKeyword, HeightKeyword }; |
|||
|
|||
private enum State |
|||
{ |
|||
Start, |
|||
Middle, |
|||
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; |
|||
} |
|||
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 ISyntax? ParseFeature(ref CharacterReader r) |
|||
{ |
|||
r.SkipWhitespace(); |
|||
|
|||
var identifier = r.ParseStyleClass(); |
|||
|
|||
if (identifier.IsEmpty) |
|||
{ |
|||
throw new ExpressionParseException(r.Position, "Expected query feature name."); |
|||
} |
|||
|
|||
var s = identifier.ToString(); |
|||
if (!Keywords.Any(x => s == x)) |
|||
{ |
|||
throw new InvalidOperationException($"Unknown feature name found: {identifier.ToString()}"); |
|||
} |
|||
|
|||
if (identifier.SequenceEqual(MinWidthKeyword.AsSpan()) || identifier.SequenceEqual(MaxWidthKeyword.AsSpan()) || identifier.SequenceEqual(WidthKeyword.AsSpan())) |
|||
{ |
|||
if (!r.TakeIf(':')) |
|||
throw new ExpressionParseException(r.Position, "Expected ':' after 'orientation'."); |
|||
double val = ParseDecimal(ref r); |
|||
|
|||
var syntax = new WidthSyntax() |
|||
{ |
|||
Value = val, |
|||
Operator = identifier.SequenceEqual(WidthKeyword.AsSpan()) ? StyleQueryComparisonOperator.Equals |
|||
: identifier.SequenceEqual(MinWidthKeyword.AsSpan()) ? StyleQueryComparisonOperator.GreaterThanOrEquals |
|||
: StyleQueryComparisonOperator.LessThanOrEquals |
|||
}; |
|||
|
|||
return syntax; |
|||
} |
|||
|
|||
if (identifier.SequenceEqual(MinHeightKeyword.AsSpan()) || identifier.SequenceEqual(MaxHeightKeyword.AsSpan()) || identifier.SequenceEqual(HeightKeyword.AsSpan())) |
|||
{ |
|||
if (!r.TakeIf(':')) |
|||
throw new ExpressionParseException(r.Position, "Expected ':' after 'orientation'."); |
|||
double val = ParseDecimal(ref r); |
|||
|
|||
var syntax = new HeightSyntax() |
|||
{ |
|||
Value = val, |
|||
Operator = identifier.SequenceEqual(WidthKeyword.AsSpan()) ? StyleQueryComparisonOperator.Equals |
|||
: identifier.SequenceEqual(MinHeightKeyword.AsSpan()) ? StyleQueryComparisonOperator.GreaterThanOrEquals |
|||
: StyleQueryComparisonOperator.LessThanOrEquals |
|||
}; |
|||
|
|||
return syntax; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
private static (State, ISyntax?) ParseStart(ref CharacterReader r) |
|||
{ |
|||
r.SkipWhitespace(); |
|||
if (r.End) |
|||
{ |
|||
return (State.End, null); |
|||
} |
|||
|
|||
if(char.IsLetter(r.Peek)) |
|||
{ |
|||
return (State.Middle, ParseFeature(ref r)); |
|||
} |
|||
|
|||
throw new InvalidOperationException("Invalid syntax found"); |
|||
} |
|||
|
|||
private static (State, ISyntax?) ParseMiddle(ref CharacterReader r, char? end) |
|||
{ |
|||
r.SkipWhitespace(); |
|||
|
|||
if (r.TakeIf(',')) |
|||
{ |
|||
return (State.Start, new OrSyntax()); |
|||
} |
|||
else if (end.HasValue && !r.End && r.Peek == end.Value) |
|||
{ |
|||
return (State.End, null); |
|||
} |
|||
else |
|||
{ |
|||
var identifier = r.TakeWhile(c => !char.IsWhiteSpace(c)); |
|||
|
|||
if (identifier.SequenceEqual("and".AsSpan())) |
|||
{ |
|||
return (State.Start, new AndSyntax()); |
|||
} |
|||
} |
|||
throw new InvalidOperationException("Invalid syntax found"); |
|||
} |
|||
|
|||
private static double ParseDecimal(ref CharacterReader r) |
|||
{ |
|||
r.SkipWhitespace(); |
|||
var number = r.ParseNumber(); |
|||
if (number.IsEmpty) |
|||
{ |
|||
throw new ExpressionParseException(r.Position, $"Expected a number after."); |
|||
} |
|||
|
|||
return double.Parse(number.ToString()); |
|||
} |
|||
|
|||
private static StyleQueryComparisonOperator ParseOperator(ref CharacterReader r) |
|||
{ |
|||
r.SkipWhitespace(); |
|||
var queryOperator = r.TakeWhile(x => !char.IsWhiteSpace(x)); |
|||
|
|||
return queryOperator.ToString() switch |
|||
{ |
|||
"=" => StyleQueryComparisonOperator.Equals, |
|||
"<" => StyleQueryComparisonOperator.LessThan, |
|||
">" => StyleQueryComparisonOperator.GreaterThan, |
|||
"<=" => StyleQueryComparisonOperator.LessThanOrEquals, |
|||
">=" => StyleQueryComparisonOperator.GreaterThanOrEquals, |
|||
"" => StyleQueryComparisonOperator.None, |
|||
_ => throw new ExpressionParseException(r.Position, $"Expected a comparison operator after.") |
|||
}; |
|||
} |
|||
|
|||
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 OrSyntax : ISyntax |
|||
{ |
|||
public override bool Equals(object? obj) |
|||
{ |
|||
return obj is OrSyntax; |
|||
} |
|||
} |
|||
|
|||
public class AndSyntax : ISyntax |
|||
{ |
|||
public override bool Equals(object? obj) |
|||
{ |
|||
return obj is AndSyntax; |
|||
} |
|||
} |
|||
|
|||
public abstract class QuerySyntax<T> : ISyntax |
|||
{ |
|||
public T? Value { get; set; } |
|||
public StyleQueryComparisonOperator Operator { get; set; } |
|||
} |
|||
|
|||
public abstract class RangeSyntax : QuerySyntax<double> |
|||
{ |
|||
|
|||
} |
|||
|
|||
public class WidthSyntax : RangeSyntax |
|||
{ |
|||
public override bool Equals(object? obj) |
|||
{ |
|||
return (obj is WidthSyntax width) && width.Value == Value |
|||
&& width.Operator == Operator; |
|||
} |
|||
} |
|||
|
|||
public class HeightSyntax : RangeSyntax |
|||
{ |
|||
public override bool Equals(object? obj) |
|||
{ |
|||
return (obj is HeightSyntax height) && height.Value == Value |
|||
&& height.Operator == Operator; |
|||
} |
|||
} |
|||
|
|||
public interface ISyntax |
|||
{ |
|||
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,77 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using Avalonia.Styling; |
|||
using static Avalonia.Markup.Parsers.ContainerQueryGrammar; |
|||
|
|||
namespace Avalonia.Markup.Parsers |
|||
{ |
|||
/// <summary>
|
|||
/// Parses a <see cref="Selector"/> from text.
|
|||
/// </summary>
|
|||
internal class ContainerQueryParser |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ContainerQueryParser"/> class.
|
|||
/// </summary>
|
|||
public ContainerQueryParser() |
|||
{ |
|||
} |
|||
|
|||
/// <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 StyleQuery? Parse(string s) |
|||
{ |
|||
var syntax = ContainerQueryGrammar.Parse(s); |
|||
return Create(syntax); |
|||
} |
|||
|
|||
[RequiresUnreferencedCode(TrimmingMessages.SelectorsParseRequiresUnreferencedCodeMessage)] |
|||
private StyleQuery? Create(IEnumerable<ISyntax> syntax) |
|||
{ |
|||
var result = default(StyleQuery); |
|||
var results = default(List<StyleQuery>); |
|||
|
|||
foreach (var i in syntax) |
|||
{ |
|||
switch (i) |
|||
{ |
|||
case ContainerQueryGrammar.WidthSyntax width: |
|||
result = result.Width(width.Operator, width.Value); |
|||
break; |
|||
case ContainerQueryGrammar.HeightSyntax height: |
|||
result = result.Height(height.Operator, height.Value); |
|||
break; |
|||
case ContainerQueryGrammar.OrSyntax or: |
|||
case ContainerQueryGrammar.AndSyntax and: |
|||
if (results == null) |
|||
{ |
|||
results = new List<StyleQuery>(); |
|||
} |
|||
|
|||
results.Add(result ?? throw new NotSupportedException("Invalid query!")); |
|||
result = null; |
|||
break; |
|||
default: |
|||
throw new NotSupportedException($"Unsupported selector grammar '{i.GetType()}'."); |
|||
} |
|||
} |
|||
|
|||
if (results != null) |
|||
{ |
|||
if (result != null) |
|||
{ |
|||
results.Add(result); |
|||
} |
|||
|
|||
result = results.Count > 1 ? StyleQueries.Or(results) : results[0]; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,164 @@ |
|||
using System; |
|||
using Avalonia.Base.UnitTests.Layout; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Styling; |
|||
using Avalonia.UnitTests; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Base.UnitTests.Styling |
|||
{ |
|||
public class ContainerTests |
|||
{ |
|||
[Fact] |
|||
public void Container_Cannot_Be_Added_To_Style_Children() |
|||
{ |
|||
var target = new ContainerQuery(); |
|||
var style = new Style(); |
|||
|
|||
Assert.Throws<InvalidOperationException>(() => style.Children.Add(target)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Container_Width_Queries_Matches() |
|||
{ |
|||
using var app = UnitTestApplication.Start(); |
|||
var root = new LayoutTestRoot() |
|||
{ |
|||
ClientSize = new Size(400, 400) |
|||
}; |
|||
var containerQuery1 = new ContainerQuery(x => new WidthQuery(x, StyleQueryComparisonOperator.LessThanOrEquals, 500)); |
|||
containerQuery1.Children.Add(new Style(x => x.Is<Border>()) |
|||
{ |
|||
Setters = { new Setter(Control.WidthProperty, 200.0) } |
|||
}); |
|||
var containerQuery2 = new ContainerQuery(x => new WidthQuery(x, StyleQueryComparisonOperator.GreaterThan, 500)); |
|||
containerQuery2.Children.Add(new Style(x => x.Is<Border>()) |
|||
{ |
|||
Setters = { new Setter(Control.WidthProperty, 500.0) } |
|||
}); |
|||
root.Styles.Add(containerQuery1); |
|||
root.Styles.Add(containerQuery2); |
|||
var child = new Border() |
|||
{ |
|||
Name = "Child", |
|||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Stretch |
|||
}; |
|||
var border = new Border() |
|||
{ |
|||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Stretch, |
|||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Stretch, |
|||
Child = child, |
|||
Name = "Parent" |
|||
}; |
|||
Container.SetSizing(border, Avalonia.Styling.ContainerSizing.Width); |
|||
|
|||
root.Child = border; |
|||
|
|||
root.LayoutManager.ExecuteInitialLayoutPass(); |
|||
Assert.Equal(child.Width, 200.0); |
|||
|
|||
root.ClientSize = new Size(600, 600); |
|||
root.InvalidateMeasure(); |
|||
|
|||
root.LayoutManager.ExecuteLayoutPass(); |
|||
Assert.Equal(child.Width, 500.0); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Container_Height_Queries_Matches() |
|||
{ |
|||
using var app = UnitTestApplication.Start(); |
|||
var root = new LayoutTestRoot() |
|||
{ |
|||
ClientSize = new Size(400, 400) |
|||
}; |
|||
var containerQuery1 = new ContainerQuery(x => new HeightQuery(x, StyleQueryComparisonOperator.LessThanOrEquals, 500)); |
|||
containerQuery1.Children.Add(new Style(x => x.Is<Border>()) |
|||
{ |
|||
Setters = { new Setter(Control.HeightProperty, 200.0) } |
|||
}); |
|||
var containerQuery2 = new ContainerQuery(x => new HeightQuery(x, StyleQueryComparisonOperator.GreaterThan, 500)); |
|||
containerQuery2.Children.Add(new Style(x => x.Is<Border>()) |
|||
{ |
|||
Setters = { new Setter(Control.HeightProperty, 500.0) } |
|||
}); |
|||
root.Styles.Add(containerQuery1); |
|||
root.Styles.Add(containerQuery2); |
|||
var child = new Border() |
|||
{ |
|||
Name = "Child", |
|||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Stretch |
|||
}; |
|||
var border = new Border() |
|||
{ |
|||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Stretch, |
|||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Stretch, |
|||
Child = child, |
|||
Name = "Parent" |
|||
}; |
|||
Container.SetSizing(border, Avalonia.Styling.ContainerSizing.Height); |
|||
|
|||
root.Child = border; |
|||
|
|||
root.LayoutManager.ExecuteInitialLayoutPass(); |
|||
Assert.Equal(child.Height, 200.0); |
|||
|
|||
root.ClientSize = new Size(600, 600); |
|||
root.InvalidateMeasure(); |
|||
|
|||
root.LayoutManager.ExecuteLayoutPass(); |
|||
Assert.Equal(child.Height, 500.0); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Container_Queries_Matches_Name() |
|||
{ |
|||
using var app = UnitTestApplication.Start(); |
|||
var root = new LayoutTestRoot() |
|||
{ |
|||
ClientSize = new Size(600, 600) |
|||
}; |
|||
var containerQuery1 = new ContainerQuery(x => new WidthQuery(x, StyleQueryComparisonOperator.LessThanOrEquals, 500)); |
|||
containerQuery1.Children.Add(new Style(x => x.Is<Border>()) |
|||
{ |
|||
Setters = { new Setter(Control.WidthProperty, 200.0) } |
|||
}); |
|||
var containerQuery2 = new ContainerQuery(x => new WidthQuery(x, StyleQueryComparisonOperator.LessThanOrEquals, 500), "TEST"); |
|||
containerQuery2.Children.Add(new Style(x => x.Is<Border>()) |
|||
{ |
|||
Setters = { new Setter(Control.WidthProperty, 300.0) } |
|||
}); |
|||
root.Styles.Add(containerQuery2); |
|||
root.Styles.Add(containerQuery1); |
|||
var child = new Border() |
|||
{ |
|||
Name = "Child", |
|||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Stretch |
|||
}; |
|||
var controlInner = new ContentControl() |
|||
{ |
|||
Width = 400, |
|||
Height = 400, |
|||
Content = child, |
|||
Name = "Inner" |
|||
}; |
|||
Container.SetSizing(controlInner, Avalonia.Styling.ContainerSizing.Width); |
|||
Container.SetName(controlInner, "TEST"); |
|||
var border = new Border() |
|||
{ |
|||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Stretch, |
|||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Stretch, |
|||
Child = controlInner, |
|||
Name = "Parent" |
|||
}; |
|||
Container.SetSizing(border, Avalonia.Styling.ContainerSizing.Width); |
|||
|
|||
root.Child = border; |
|||
|
|||
root.LayoutManager.ExecuteInitialLayoutPass(); |
|||
|
|||
root.LayoutManager.ExecuteLayoutPass(); |
|||
Assert.Equal(child.Width, 300.0); |
|||
} |
|||
} |
|||
} |
|||