Browse Source

Feature - Container Queries (#16846)

* 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 detached
release/11.3.0-beta1
Emmanuel Hansen 10 months ago
committed by GitHub
parent
commit
cecc928dcc
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      Avalonia.sln
  2. BIN
      samples/ControlCatalog/Assets/image1.jpg
  3. BIN
      samples/ControlCatalog/Assets/image2.jpg
  4. BIN
      samples/ControlCatalog/Assets/image3.jpg
  5. BIN
      samples/ControlCatalog/Assets/image4.jpg
  6. BIN
      samples/ControlCatalog/Assets/image5.jpg
  7. BIN
      samples/ControlCatalog/Assets/image6.jpg
  8. BIN
      samples/ControlCatalog/Assets/image7.jpg
  9. 9
      samples/ControlCatalog/ControlCatalog.csproj
  10. 3
      samples/ControlCatalog/MainView.xaml
  11. 110
      samples/ControlCatalog/Pages/ContainerQueryPage.xaml
  12. 18
      samples/ControlCatalog/Pages/ContainerQueryPage.xaml.cs
  13. 34
      src/Avalonia.Base/Layout/Layoutable.cs
  14. 36
      src/Avalonia.Base/Platform/VisualQueryProvider.cs
  15. 3
      src/Avalonia.Base/StyledElement.cs
  16. 69
      src/Avalonia.Base/Styling/Activators/AndQueryActivator.cs
  17. 51
      src/Avalonia.Base/Styling/Activators/AndQueryActivatorBuilder.cs
  18. 95
      src/Avalonia.Base/Styling/Activators/ContainerQueryActivatorBase.cs
  19. 63
      src/Avalonia.Base/Styling/Activators/OrQueryActivator.cs
  20. 51
      src/Avalonia.Base/Styling/Activators/OrQueryActivatorBuilder.cs
  21. 37
      src/Avalonia.Base/Styling/Activators/ScreenActivator.cs
  22. 99
      src/Avalonia.Base/Styling/AndQuery.cs
  23. 91
      src/Avalonia.Base/Styling/Container.cs
  24. 90
      src/Avalonia.Base/Styling/ContainerQuery.cs
  25. 28
      src/Avalonia.Base/Styling/ContainerSizing.cs
  26. 99
      src/Avalonia.Base/Styling/OrQuery.cs
  27. 149
      src/Avalonia.Base/Styling/ScreenQueries.cs
  28. 20
      src/Avalonia.Base/Styling/Selector.cs
  29. 3
      src/Avalonia.Base/Styling/Style.cs
  30. 77
      src/Avalonia.Base/Styling/StyleQueries.cs
  31. 164
      src/Avalonia.Base/Styling/StyleQuery.cs
  32. 12
      src/Avalonia.Base/Styling/StyleQueryComparisonOperator.cs
  33. 31
      src/Avalonia.Base/Styling/ValueStyleQuery.cs
  34. 11
      src/Avalonia.Base/Utilities/IdentifierParser.cs
  35. 6
      src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
  36. 3
      src/Avalonia.Controls/Border.cs
  37. 2
      src/Avalonia.Controls/ContentControl.cs
  38. 2
      src/Avalonia.Controls/Decorator.cs
  39. 2
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  40. 3
      src/Avalonia.Controls/TopLevel.cs
  41. 3
      src/Avalonia.Controls/WindowBase.cs
  42. 1
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  43. 3
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs
  44. 424
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlQueryTransformer.cs
  45. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  46. 267
      src/Markup/Avalonia.Markup/Markup/Parsers/ContainerQueryGrammar.cs
  47. 77
      src/Markup/Avalonia.Markup/Markup/Parsers/ContainerQueryParser.cs
  48. 36
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
  49. 164
      tests/Avalonia.Base.UnitTests/Styling/ContainerTests.cs

2
Avalonia.sln

@ -274,7 +274,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tizen", "Tizen", "{D1300000
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Tizen", "src\Tizen\Avalonia.Tizen\Avalonia.Tizen.csproj", "{DFFBDBF5-5DBE-47ED-9EAE-D40B75AC99E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Tizen", "samples\ControlCatalog.Tizen\ControlCatalog.Tizen.csproj", "{A0B29221-2B6F-4B29-A4D5-2227811B5915}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Tizen", "samples\ControlCatalog.Tizen\ControlCatalog.Tizen.csproj", "{A0B29221-2B6F-4B29-A4D5-2227811B5915}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Metal", "src\Avalonia.Metal\Avalonia.Metal.csproj", "{60B4ED1F-ECFA-453B-8A70-1788261C8355}"
EndProject

BIN
samples/ControlCatalog/Assets/image1.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
samples/ControlCatalog/Assets/image2.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
samples/ControlCatalog/Assets/image3.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
samples/ControlCatalog/Assets/image4.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

BIN
samples/ControlCatalog/Assets/image5.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
samples/ControlCatalog/Assets/image6.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

BIN
samples/ControlCatalog/Assets/image7.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

9
samples/ControlCatalog/ControlCatalog.csproj

@ -15,6 +15,15 @@
<AvaloniaResource Include="Assets\*" />
<AvaloniaResource Include="Assets\Fonts\*" />
</ItemGroup>
<ItemGroup>
<None Remove="Assets\image1.jpg" />
<None Remove="Assets\image2.jpg" />
<None Remove="Assets\image3.jpg" />
<None Remove="Assets\image4.jpg" />
<None Remove="Assets\image5.jpg" />
<None Remove="Assets\image6.jpg" />
<None Remove="Assets\image7.jpg" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Assets\Fonts\SourceSansPro-Bold.ttf" />
<EmbeddedResource Include="Assets\Fonts\SourceSansPro-BoldItalic.ttf" />

3
samples/ControlCatalog/MainView.xaml

@ -60,6 +60,9 @@
<TabItem Header="ComboBox">
<pages:ComboBoxPage />
</TabItem>
<TabItem Header="Container Queries">
<pages:ContainerQueryPage />
</TabItem>
<TabItem Header="ContextFlyout">
<pages:ContextFlyoutPage />
</TabItem>

110
samples/ControlCatalog/Pages/ContainerQueryPage.xaml

@ -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>

18
samples/ControlCatalog/Pages/ContainerQueryPage.xaml.cs

@ -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);
}
}
}

34
src/Avalonia.Base/Layout/Layoutable.cs

@ -2,6 +2,7 @@ using System;
using Avalonia.Diagnostics;
using Avalonia.Logging;
using Avalonia.Reactive;
using Avalonia.Styling;
using Avalonia.Utilities;
using Avalonia.VisualTree;
@ -553,14 +554,43 @@ namespace Avalonia.Layout
var minMax = new MinMax(this);
var constrained = LayoutHelper.ApplyLayoutConstraints(
var constrainedSize = LayoutHelper.ApplyLayoutConstraints(
minMax,
availableSize.Deflate(margin));
var measured = MeasureOverride(constrained);
var isContainer = false;
ContainerSizing containerSizing = ContainerSizing.Normal;
if (Container.GetQueryProvider(this) is { } queryProvider && Container.GetSizing(this) is { } sizing && sizing != ContainerSizing.Normal)
{
isContainer = true;
containerSizing = sizing;
queryProvider.SetSize(constrainedSize.Width, constrainedSize.Height, containerSizing);
}
var measured = MeasureOverride(constrainedSize);
var width = MathUtilities.Clamp(measured.Width, minMax.MinWidth, minMax.MaxWidth);
var height = MathUtilities.Clamp(measured.Height, minMax.MinHeight, minMax.MaxHeight);
if (isContainer)
{
switch (containerSizing)
{
case ContainerSizing.Width:
width = double.IsInfinity(constrainedSize.Width) ? width : constrainedSize.Width;
break;
case ContainerSizing.Height:
width = measured.Width;
height = double.IsInfinity(constrainedSize.Height) ? height : constrainedSize.Height;
break;
case ContainerSizing.WidthAndHeight:
width = double.IsInfinity(constrainedSize.Width) ? width : constrainedSize.Width;
height = double.IsInfinity(constrainedSize.Height) ? height : constrainedSize.Height;
break;
}
}
if (useLayoutRounding)
{
(width, height) = LayoutHelper.RoundLayoutSizeUp(new Size(width, height), scale);

36
src/Avalonia.Base/Platform/VisualQueryProvider.cs

@ -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);
}
}
}

3
src/Avalonia.Base/StyledElement.cs

@ -840,6 +840,9 @@ namespace Avalonia
private void ApplyStyle(IStyle style, IStyleHost? host, FrameType type)
{
if (style is Styling.ContainerQuery m)
m.TryAttach(this, host, type);
if (style is Style s)
s.TryAttach(this, host, type);

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

@ -0,0 +1,69 @@
using System.Collections.Generic;
namespace Avalonia.Styling.Activators
{
/// <summary>
/// An aggregate <see cref="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);
}
}
}
}
}

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

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

95
src/Avalonia.Base/Styling/Activators/ContainerQueryActivatorBase.cs

@ -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();
}
}
}

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

@ -0,0 +1,63 @@
using System.Collections.Generic;
namespace Avalonia.Styling.Activators
{
/// <summary>
/// An aggregate <see cref="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);
}
}
}
}
}

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

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

37
src/Avalonia.Base/Styling/Activators/ScreenActivator.cs

@ -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;
}
}

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

@ -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;
}
}

91
src/Avalonia.Base/Styling/Container.cs

@ -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);
}
}
}

90
src/Avalonia.Base/Styling/ContainerQuery.cs

@ -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;
}
}
}

28
src/Avalonia.Base/Styling/ContainerSizing.cs

@ -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
}
}

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

@ -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;
}
}

149
src/Avalonia.Base/Styling/ScreenQueries.cs

@ -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();
}
}
}

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

@ -166,8 +166,24 @@ namespace Avalonia.Styling
}
}
SelectorMatch match = SelectorMatch.NeverThisInstance;
bool containerMatchesSometimes = false;
// Match any parent Container query
if (parent is ContainerQuery container)
{
match = container.Query?.Evaluate(control, container.Parent, subscribe, container.Name) ?? SelectorMatch.NeverThisInstance;
if (!match.IsMatch)
return match.Result;
containerMatchesSometimes = match.Result == SelectorMatchResult.Sometimes;
if (containerMatchesSometimes)
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 containerMatchesSometimes ? SelectorMatchResult.Sometimes : match.Result;
}
}
}

3
src/Avalonia.Base/Styling/Style.cs

@ -73,7 +73,8 @@ namespace Avalonia.Styling
var match = Selector?.Match(target, Parent, true) ??
(target == host ?
SelectorMatch.AlwaysThisInstance :
(Parent is ContainerQuery containerQuery ? containerQuery.Query?.Match(target, containerQuery.Parent, true, containerQuery.Name) ??
SelectorMatch.NeverThisInstance : SelectorMatch.AlwaysThisInstance) :
SelectorMatch.NeverThisInstance);
activity?.AddTag(Diagnostic.Tags.SelectorResult, match.Result);

77
src/Avalonia.Base/Styling/StyleQueries.cs

@ -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);
}
}
}

164
src/Avalonia.Base/Styling/StyleQuery.cs

@ -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;
}
}
}

12
src/Avalonia.Base/Styling/StyleQueryComparisonOperator.cs

@ -0,0 +1,12 @@
namespace Avalonia.Styling
{
public enum StyleQueryComparisonOperator
{
None,
Equals,
LessThan,
GreaterThan,
LessThanOrEquals,
GreaterThanOrEquals,
}
}

31
src/Avalonia.Base/Styling/ValueStyleQuery.cs

@ -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;
}
}

11
src/Avalonia.Base/Utilities/IdentifierParser.cs

@ -45,5 +45,16 @@ namespace Avalonia.Utilities
cat == UnicodeCategory.DecimalDigitNumber;
}
}
internal static ReadOnlySpan<char> ParseNumber(this ref CharacterReader r)
{
return r.TakeWhile(c => IsValidNumberChar(c));
}
private static bool IsValidNumberChar(char c)
{
var cat = CharUnicodeInfo.GetUnicodeCategory(c);
return cat == UnicodeCategory.DecimalDigitNumber;
}
}
}

6
src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj

@ -43,6 +43,9 @@
<Compile Include="../Markup/Avalonia.Markup\Markup\Parsers\SelectorGrammar.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
<Compile Include="../Markup/Avalonia.Markup\Markup\Parsers\ContainerQueryGrammar.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
<Compile Include="../Markup/Avalonia.Markup\Markup\Parsers\PropertyPathGrammar.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
@ -83,6 +86,9 @@
<Compile Include="../Avalonia.Base/Thickness.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
<Compile Include="../Avalonia.Base/Styling/StyleQueryComparisonOperator.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
<Compile Include="../Avalonia.Base/Size.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>

3
src/Avalonia.Controls/Border.cs

@ -1,6 +1,3 @@
using System;
using Avalonia.Collections;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Utils;
using Avalonia.Layout;
using Avalonia.Media;

2
src/Avalonia.Controls/ContentControl.cs

@ -8,6 +8,8 @@ using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.Styling;
namespace Avalonia.Controls
{

2
src/Avalonia.Controls/Decorator.cs

@ -1,5 +1,7 @@
using Avalonia.Layout;
using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.Styling;
namespace Avalonia.Controls
{

2
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@ -10,6 +10,8 @@ using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.Utilities;
namespace Avalonia.Controls.Presenters

3
src/Avalonia.Controls/TopLevel.cs

@ -157,6 +157,8 @@ namespace Avalonia.Controls
static TopLevel()
{
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue<TopLevel>(KeyboardNavigationMode.Cycle);
Avalonia.Styling.Container.SizingProperty.OverrideDefaultValue<TopLevel>(ContainerSizing.WidthAndHeight);
AffectsMeasure<TopLevel>(ClientSizeProperty);
SystemBarColorProperty.Changed.AddClassHandler<Control>((view, e) =>
@ -663,6 +665,7 @@ namespace Avalonia.Controls
}
private IDisposable? _insetsPaddings;
private void InvalidateChildInsetsPadding()
{
if (Content is Control child

3
src/Avalonia.Controls/WindowBase.cs

@ -4,7 +4,6 @@ using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Platform;
using Avalonia.Styling;
namespace Avalonia.Controls
{
@ -286,6 +285,8 @@ namespace Avalonia.Controls
var constraint = LayoutHelper.ApplyLayoutConstraints(this, availableSize);
Avalonia.Styling.Container.GetQueryProvider(this)?.SetSize(constraint.Width, constraint.Height, Avalonia.Styling.Container.GetSizing(this));
return MeasureOverride(constraint);
}

1
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs

@ -56,6 +56,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
InsertBefore<ContentConvertTransformer>(
new AvaloniaXamlIlControlThemeTransformer(),
new AvaloniaXamlIlSelectorTransformer(),
new AvaloniaXamlIlQueryTransformer(),
new AvaloniaXamlIlDuplicateSettersChecker(),
new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(),
new AvaloniaXamlIlBindingPathParser(),

3
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs

@ -104,7 +104,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
Style = 1,
ControlTemplate,
Transitions
Transitions,
Container
}
public AvaloniaXamlIlTargetTypeMetadataNode(IXamlAstValueNode value, IXamlAstTypeReference targetType,

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

@ -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"));
}
}
}

2
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@ -127,6 +127,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType UriKind { get; }
public IXamlConstructor UriConstructor { get; }
public IXamlType Style { get; }
public IXamlType Container { get; }
public IXamlType Styles { get; }
public IXamlType ControlTheme { get; }
public IXamlType WindowTransparencyLevel { get; }
@ -328,6 +329,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
UriKind = cfg.TypeSystem.GetType("System.UriKind");
UriConstructor = Uri.GetConstructor(new List<IXamlType>() { cfg.WellKnownTypes.String, UriKind });
Style = cfg.TypeSystem.GetType("Avalonia.Styling.Style");
Container = cfg.TypeSystem.GetType("Avalonia.Styling.ContainerQuery");
Styles = cfg.TypeSystem.GetType("Avalonia.Styling.Styles");
ControlTheme = cfg.TypeSystem.GetType("Avalonia.Styling.ControlTheme");
ControlTemplate = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Templates.ControlTemplate");

267
src/Markup/Avalonia.Markup/Markup/Parsers/ContainerQueryGrammar.cs

@ -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
{
}
}
}

77
src/Markup/Avalonia.Markup/Markup/Parsers/ContainerQueryParser.cs

@ -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;
}
}
}

36
src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs

@ -275,6 +275,32 @@ namespace Avalonia.Markup.Parsers
return (State.CanHaveType, new NameSyntax { Name = name.ToString() });
}
private static double ParseDecimal(ref CharacterReader r)
{
var number = r.ParseNumber();
if (number.IsEmpty)
{
throw new ExpressionParseException(r.Position, $"Expected a number after.");
}
return double.Parse(number.ToString());
}
private static T ParseEnum<T>(ref CharacterReader r) where T : struct
{
var identifier = r.ParseIdentifier();
if (Enum.TryParse<T>(identifier.ToString(), true, out T value))
return value;
throw new ExpressionParseException(r.Position, $"Expected a {typeof(T)} after.");
}
private static string ParseString(ref CharacterReader r)
{
return r.ParseIdentifier().ToString();
}
private static (State, ISyntax) ParseTypeName(ref CharacterReader r)
{
return (State.CanHaveType, ParseType(ref r, new OfTypeSyntax()));
@ -651,5 +677,15 @@ namespace Avalonia.Markup.Parsers
return obj is NestingSyntax;
}
}
public class DecimalSyntax : ISyntax
{
public decimal Number { get; set; }
public override bool Equals(object? obj)
{
return obj is DecimalSyntax dec && dec.Number == Number;
}
}
}
}

164
tests/Avalonia.Base.UnitTests/Styling/ContainerTests.cs

@ -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);
}
}
}
Loading…
Cancel
Save