diff --git a/Avalonia.sln b/Avalonia.sln
index 9d60fe2e45..a0314b1c33 100644
--- a/Avalonia.sln
+++ b/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
diff --git a/samples/ControlCatalog/Assets/image1.jpg b/samples/ControlCatalog/Assets/image1.jpg
new file mode 100644
index 0000000000..673cb38399
Binary files /dev/null and b/samples/ControlCatalog/Assets/image1.jpg differ
diff --git a/samples/ControlCatalog/Assets/image2.jpg b/samples/ControlCatalog/Assets/image2.jpg
new file mode 100644
index 0000000000..83d721c083
Binary files /dev/null and b/samples/ControlCatalog/Assets/image2.jpg differ
diff --git a/samples/ControlCatalog/Assets/image3.jpg b/samples/ControlCatalog/Assets/image3.jpg
new file mode 100644
index 0000000000..b14700656e
Binary files /dev/null and b/samples/ControlCatalog/Assets/image3.jpg differ
diff --git a/samples/ControlCatalog/Assets/image4.jpg b/samples/ControlCatalog/Assets/image4.jpg
new file mode 100644
index 0000000000..e15787a2e7
Binary files /dev/null and b/samples/ControlCatalog/Assets/image4.jpg differ
diff --git a/samples/ControlCatalog/Assets/image5.jpg b/samples/ControlCatalog/Assets/image5.jpg
new file mode 100644
index 0000000000..b6077334ef
Binary files /dev/null and b/samples/ControlCatalog/Assets/image5.jpg differ
diff --git a/samples/ControlCatalog/Assets/image6.jpg b/samples/ControlCatalog/Assets/image6.jpg
new file mode 100644
index 0000000000..ea5228d178
Binary files /dev/null and b/samples/ControlCatalog/Assets/image6.jpg differ
diff --git a/samples/ControlCatalog/Assets/image7.jpg b/samples/ControlCatalog/Assets/image7.jpg
new file mode 100644
index 0000000000..2f86ce4ae5
Binary files /dev/null and b/samples/ControlCatalog/Assets/image7.jpg differ
diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj
index 8ef96169df..cff0bb4e92 100644
--- a/samples/ControlCatalog/ControlCatalog.csproj
+++ b/samples/ControlCatalog/ControlCatalog.csproj
@@ -15,6 +15,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index e3eed5fb0e..45dcab28fb 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -60,6 +60,9 @@
+
+
+
diff --git a/samples/ControlCatalog/Pages/ContainerQueryPage.xaml b/samples/ControlCatalog/Pages/ContainerQueryPage.xaml
new file mode 100644
index 0000000000..e6b0558a04
--- /dev/null
+++ b/samples/ControlCatalog/Pages/ContainerQueryPage.xaml
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/ContainerQueryPage.xaml.cs b/samples/ControlCatalog/Pages/ContainerQueryPage.xaml.cs
new file mode 100644
index 0000000000..76d43fa81c
--- /dev/null
+++ b/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);
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs
index ad39127cf4..47f2217816 100644
--- a/src/Avalonia.Base/Layout/Layoutable.cs
+++ b/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);
diff --git a/src/Avalonia.Base/Platform/VisualQueryProvider.cs b/src/Avalonia.Base/Platform/VisualQueryProvider.cs
new file mode 100644
index 0000000000..31463e9e61
--- /dev/null
+++ b/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);
+ }
+ }
+}
diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs
index bbdb0e998c..b7f6262d9f 100644
--- a/src/Avalonia.Base/StyledElement.cs
+++ b/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);
diff --git a/src/Avalonia.Base/Styling/Activators/AndQueryActivator.cs b/src/Avalonia.Base/Styling/Activators/AndQueryActivator.cs
new file mode 100644
index 0000000000..20030567ca
--- /dev/null
+++ b/src/Avalonia.Base/Styling/Activators/AndQueryActivator.cs
@@ -0,0 +1,69 @@
+using System.Collections.Generic;
+
+namespace Avalonia.Styling.Activators
+{
+ ///
+ /// An aggregate which is active when all of its inputs are
+ /// active.
+ ///
+ internal class AndQueryActivator : ContainerQueryActivatorBase, IStyleActivatorSink
+ {
+ private List? _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();
+ _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);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Styling/Activators/AndQueryActivatorBuilder.cs b/src/Avalonia.Base/Styling/Activators/AndQueryActivatorBuilder.cs
new file mode 100644
index 0000000000..dfa8684d93
--- /dev/null
+++ b/src/Avalonia.Base/Styling/Activators/AndQueryActivatorBuilder.cs
@@ -0,0 +1,51 @@
+#nullable enable
+
+namespace Avalonia.Styling.Activators
+{
+ ///
+ /// Builds an .
+ ///
+ ///
+ /// When ANDing style activators, if there is more than one input then creates an instance of
+ /// . If there is only one input, returns the input directly.
+ ///
+ 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!;
+ }
+}
diff --git a/src/Avalonia.Base/Styling/Activators/ContainerQueryActivatorBase.cs b/src/Avalonia.Base/Styling/Activators/ContainerQueryActivatorBase.cs
new file mode 100644
index 0000000000..932fab488d
--- /dev/null
+++ b/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();
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Styling/Activators/OrQueryActivator.cs b/src/Avalonia.Base/Styling/Activators/OrQueryActivator.cs
new file mode 100644
index 0000000000..3de6311bd7
--- /dev/null
+++ b/src/Avalonia.Base/Styling/Activators/OrQueryActivator.cs
@@ -0,0 +1,63 @@
+using System.Collections.Generic;
+
+namespace Avalonia.Styling.Activators
+{
+ ///
+ /// An aggregate which is active when any of its inputs are
+ /// active.
+ ///
+ internal class OrQueryActivator : ContainerQueryActivatorBase, IStyleActivatorSink
+ {
+ private List? _sources;
+
+ public OrQueryActivator(Visual visual) : base(visual)
+ {
+ }
+
+ public int Count => _sources?.Count ?? 0;
+
+ public void Add(IStyleActivator activator)
+ {
+ _sources ??= new List();
+ _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);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Styling/Activators/OrQueryActivatorBuilder.cs b/src/Avalonia.Base/Styling/Activators/OrQueryActivatorBuilder.cs
new file mode 100644
index 0000000000..0571f0d3c1
--- /dev/null
+++ b/src/Avalonia.Base/Styling/Activators/OrQueryActivatorBuilder.cs
@@ -0,0 +1,51 @@
+#nullable enable
+
+namespace Avalonia.Styling.Activators
+{
+ ///
+ /// Builds an .
+ ///
+ ///
+ /// When ORing style activators, if there is more than one input then creates an instance of
+ /// . If there is only one input, returns the input directly.
+ ///
+ 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!;
+ }
+}
diff --git a/src/Avalonia.Base/Styling/Activators/ScreenActivator.cs b/src/Avalonia.Base/Styling/Activators/ScreenActivator.cs
new file mode 100644
index 0000000000..5778c2a401
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/src/Avalonia.Base/Styling/AndQuery.cs b/src/Avalonia.Base/Styling/AndQuery.cs
new file mode 100644
index 0000000000..e21b8f228a
--- /dev/null
+++ b/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
+{
+ ///
+ /// The AND style query.
+ ///
+ internal sealed class AndQuery : StyleQuery
+ {
+ private readonly IReadOnlyList _queries;
+ private string? _queryString;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The queries to AND.
+ public AndQuery(IReadOnlyList 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;
+ }
+
+ ///
+ internal override bool IsCombinator => false;
+
+ ///
+ 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;
+ }
+}
+
diff --git a/src/Avalonia.Base/Styling/Container.cs b/src/Avalonia.Base/Styling/Container.cs
new file mode 100644
index 0000000000..1aaa2a34a4
--- /dev/null
+++ b/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
+ {
+ ///
+ /// Defines the Name attached property.
+ ///
+ public static readonly AttachedProperty NameProperty =
+ AvaloniaProperty.RegisterAttached("Name", typeof(Container));
+
+ ///
+ /// Defines the Sizing attached property.
+ ///
+ public static readonly AttachedProperty SizingProperty =
+ AvaloniaProperty.RegisterAttached("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 QueryProviderProperty =
+ AvaloniaProperty.RegisterAttached("QueryProvider", typeof(Container));
+
+ ///
+ /// Gets the value of the Container.Name attached property.
+ ///
+ /// The layoutable to read the value from.
+ /// The container name of the layoutable
+ public static string? GetName(Layoutable layoutable)
+ {
+ return layoutable.GetValue(NameProperty);
+ }
+
+ ///
+ /// Sets the value of the Container.Name attached property.
+ ///
+ /// The layoutable to set the value on.
+ /// The container name.
+ public static void SetName(Layoutable layoutable, string? name)
+ {
+ layoutable.SetValue(NameProperty, name);
+ }
+
+ ///
+ /// Gets the value of the Container.Sizing attached property.
+ ///
+ /// The layoutable to read the value from.
+ /// The container sizing mode of the layoutable
+ public static ContainerSizing GetSizing(Layoutable layoutable)
+ {
+ return layoutable.GetValue(SizingProperty);
+ }
+
+ ///
+ /// Sets the value of the Container.Name attached property.
+ ///
+ /// The layoutable to set the value on.
+ /// The container sizing mode.
+ public static void SetSizing(Layoutable layoutable, ContainerSizing sizing)
+ {
+ layoutable.SetValue(SizingProperty, sizing);
+ }
+
+ internal static VisualQueryProvider? GetQueryProvider(Layoutable layoutable)
+ {
+ return layoutable.GetValue(QueryProviderProperty);
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Styling/ContainerQuery.cs b/src/Avalonia.Base/Styling/ContainerQuery.cs
new file mode 100644
index 0000000000..0c738fd200
--- /dev/null
+++ b/src/Avalonia.Base/Styling/ContainerQuery.cs
@@ -0,0 +1,90 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.PropertyStore;
+
+namespace Avalonia.Styling
+{
+ ///
+ /// Defines a container.
+ ///
+ public class ContainerQuery
+ : StyleBase
+ {
+ private StyleQuery? _query;
+ private string? _name;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ContainerQuery()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The container selector.
+ ///
+ public ContainerQuery(Func query, string? containerName = null)
+ {
+ Query = query(null);
+ _name = containerName;
+ }
+
+ ///
+ /// Gets or sets the container's query.
+ ///
+ public StyleQuery? Query
+ {
+ get => _query;
+ set => _query = value;
+ }
+
+ ///
+ /// Gets or sets the container's name.
+ ///
+ public string? Name
+ {
+ get => _name;
+ set => _name = value;
+ }
+
+ ///
+ /// Returns a string representation of the container.
+ ///
+ /// A string representation of the container.
+ 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;
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Styling/ContainerSizing.cs b/src/Avalonia.Base/Styling/ContainerSizing.cs
new file mode 100644
index 0000000000..482e38bcf3
--- /dev/null
+++ b/src/Avalonia.Base/Styling/ContainerSizing.cs
@@ -0,0 +1,28 @@
+namespace Avalonia.Styling
+{
+ ///
+ /// Defines how a container is queried.
+ ///
+ public enum ContainerSizing
+ {
+ ///
+ /// The container is not included in any size queries.
+ ///
+ Normal,
+
+ ///
+ /// The container size can be queried for width.
+ ///
+ Width,
+
+ ///
+ /// The container size can be queried for height.
+ ///
+ Height,
+
+ ///
+ /// The container size can be queried for width and height.
+ ///
+ WidthAndHeight
+ }
+}
diff --git a/src/Avalonia.Base/Styling/OrQuery.cs b/src/Avalonia.Base/Styling/OrQuery.cs
new file mode 100644
index 0000000000..29ab78c605
--- /dev/null
+++ b/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
+{
+ ///
+ /// The OR style query.
+ ///
+ internal sealed class OrQuery : StyleQuery
+ {
+ private readonly IReadOnlyList _queries;
+ private string? _queryString;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The querys to OR.
+ public OrQuery(IReadOnlyList queries)
+ {
+ if (queries is null)
+ {
+ throw new ArgumentNullException(nameof(queries));
+ }
+
+ if (queries.Count <= 1)
+ {
+ throw new ArgumentException("Need more than one query to OR.");
+ }
+
+ _queries = queries;
+ }
+
+ ///
+ internal override bool IsCombinator => false;
+
+ ///
+ public override string ToString(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;
+ }
+}
+
diff --git a/src/Avalonia.Base/Styling/ScreenQueries.cs b/src/Avalonia.Base/Styling/ScreenQueries.cs
new file mode 100644
index 0000000000..9500b634f3
--- /dev/null
+++ b/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();
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Styling/Selector.cs b/src/Avalonia.Base/Styling/Selector.cs
index 4e1c338553..cb3ddc343c 100644
--- a/src/Avalonia.Base/Styling/Selector.cs
+++ b/src/Avalonia.Base/Styling/Selector.cs
@@ -166,8 +166,24 @@ namespace Avalonia.Styling
}
}
+ SelectorMatch match = SelectorMatch.NeverThisInstance;
+ bool 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;
}
}
}
diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs
index 57ce8d7927..de17363f15 100644
--- a/src/Avalonia.Base/Styling/Style.cs
+++ b/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);
diff --git a/src/Avalonia.Base/Styling/StyleQueries.cs b/src/Avalonia.Base/Styling/StyleQueries.cs
new file mode 100644
index 0000000000..ff78fa18b5
--- /dev/null
+++ b/src/Avalonia.Base/Styling/StyleQueries.cs
@@ -0,0 +1,77 @@
+using Avalonia.Platform;
+using System.Collections.Generic;
+
+namespace Avalonia.Styling
+{
+ ///
+ /// Extension methods for .
+ ///
+ public static class StyleQueries
+ {
+ ///
+ /// Returns a query which matches the device width with a value.
+ ///
+ /// The previous query.
+ /// The operator to match the device width
+ /// The width to match
+ /// The query.
+ public static StyleQuery Width(this StyleQuery? previous, StyleQueryComparisonOperator @operator, double value)
+ {
+ return new WidthQuery(previous, @operator, value);
+ }
+
+
+
+ ///
+ /// Returns a query which matches the device height with a value.
+ ///
+ /// The previous query.
+ /// The operator to match the device height
+ /// The height to match
+ /// The query.
+ public static StyleQuery Height(this StyleQuery? previous, StyleQueryComparisonOperator @operator, double value)
+ {
+ return new HeightQuery(previous, @operator, value);
+ }
+
+ ///
+ /// Returns a query which ORs queries.
+ ///
+ /// The queries to be OR'd.
+ /// The query.
+ public static StyleQuery Or(params StyleQuery[] queries)
+ {
+ return new OrQuery(queries);
+ }
+
+ ///
+ /// Returns a query which ORs queries.
+ ///
+ /// The queries to be OR'd.
+ /// The query.
+ public static StyleQuery Or(IReadOnlyList query)
+ {
+ return new OrQuery(query);
+ }
+
+ ///
+ /// Returns a query which ANDs queries.
+ ///
+ /// The queries to be AND'd.
+ /// The query.
+ public static StyleQuery And(params StyleQuery[] queries)
+ {
+ return new AndQuery(queries);
+ }
+
+ ///
+ /// Returns a query which ANDs queries.
+ ///
+ /// The queries to be AND'd.
+ /// The query.
+ public static StyleQuery And(IReadOnlyList query)
+ {
+ return new AndQuery(query);
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Styling/StyleQuery.cs b/src/Avalonia.Base/Styling/StyleQuery.cs
new file mode 100644
index 0000000000..a0ba962f76
--- /dev/null
+++ b/src/Avalonia.Base/Styling/StyleQuery.cs
@@ -0,0 +1,164 @@
+using System;
+using Avalonia.Styling.Activators;
+
+#nullable enable
+
+namespace Avalonia.Styling
+{
+ ///
+ /// A query in a .
+ ///
+ public abstract class StyleQuery
+ {
+ ///
+ /// Gets a value indicating whether this query is a combinator.
+ ///
+ ///
+ /// A combinator is a query such as Child or Descendent which links simple querys.
+ ///
+ internal abstract bool IsCombinator { get; }
+
+ internal StyleQuery() { }
+
+ ///
+ /// Tries to match the query with a control.
+ ///
+ /// The control.
+ ///
+ /// The parent container, if the container containing the query is a nested container.
+ ///
+ ///
+ /// Whether the match should subscribe to changes in order to track the match over time,
+ /// or simply return an imcontainerte result.
+ ///
+ ///
+ /// The name of container to query on.
+ ///
+ /// A .
+ 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);
+
+ ///
+ /// Gets a string representing the query, with the nesting separator (`^`) replaced with
+ /// the parent query.
+ ///
+ /// The owner container.
+ public abstract string ToString(ContainerQuery? owner);
+
+ ///
+ /// Evaluates the query for a match.
+ ///
+ /// The control.
+ ///
+ /// The parent container, if the container containing the query is a nested container.
+ ///
+ ///
+ /// Whether the match should subscribe to changes in order to track the match over time,
+ /// or simply return an imcontainerte result.
+ ///
+ ///
+ /// The name of the container to evaluate.
+ ///
+ /// A .
+ internal abstract SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe, string? containerName);
+
+ ///
+ /// Moves to the previous query.
+ ///
+ private protected abstract StyleQuery? MovePrevious();
+
+ ///
+ /// Moves to the previous query or the parent query.
+ ///
+ 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;
+ }
+ }
+}
+
diff --git a/src/Avalonia.Base/Styling/StyleQueryComparisonOperator.cs b/src/Avalonia.Base/Styling/StyleQueryComparisonOperator.cs
new file mode 100644
index 0000000000..c20ee62213
--- /dev/null
+++ b/src/Avalonia.Base/Styling/StyleQueryComparisonOperator.cs
@@ -0,0 +1,12 @@
+namespace Avalonia.Styling
+{
+ public enum StyleQueryComparisonOperator
+ {
+ None,
+ Equals,
+ LessThan,
+ GreaterThan,
+ LessThanOrEquals,
+ GreaterThanOrEquals,
+ }
+}
diff --git a/src/Avalonia.Base/Styling/ValueStyleQuery.cs b/src/Avalonia.Base/Styling/ValueStyleQuery.cs
new file mode 100644
index 0000000000..7cea7dc769
--- /dev/null
+++ b/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 : 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;
+ }
+}
diff --git a/src/Avalonia.Base/Utilities/IdentifierParser.cs b/src/Avalonia.Base/Utilities/IdentifierParser.cs
index 76e6459e2e..f32d5f79a2 100644
--- a/src/Avalonia.Base/Utilities/IdentifierParser.cs
+++ b/src/Avalonia.Base/Utilities/IdentifierParser.cs
@@ -45,5 +45,16 @@ namespace Avalonia.Utilities
cat == UnicodeCategory.DecimalDigitNumber;
}
}
+
+ internal static ReadOnlySpan 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;
+ }
}
}
diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
index 1eda0b56ad..fd2c8e59e5 100644
--- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
+++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
@@ -43,6 +43,9 @@
Markup/%(RecursiveDir)%(FileName)%(Extension)
+
+ Markup/%(RecursiveDir)%(FileName)%(Extension)
+
Markup/%(RecursiveDir)%(FileName)%(Extension)
@@ -83,6 +86,9 @@
Markup/%(RecursiveDir)%(FileName)%(Extension)
+
+ Markup/%(RecursiveDir)%(FileName)%(Extension)
+
Markup/%(RecursiveDir)%(FileName)%(Extension)
diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs
index dc82c50a39..b816858632 100644
--- a/src/Avalonia.Controls/Border.cs
+++ b/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;
diff --git a/src/Avalonia.Controls/ContentControl.cs b/src/Avalonia.Controls/ContentControl.cs
index 4f50e3da7c..0f53608bd3 100644
--- a/src/Avalonia.Controls/ContentControl.cs
+++ b/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
{
diff --git a/src/Avalonia.Controls/Decorator.cs b/src/Avalonia.Controls/Decorator.cs
index 85485dc50d..e62ca0000b 100644
--- a/src/Avalonia.Controls/Decorator.cs
+++ b/src/Avalonia.Controls/Decorator.cs
@@ -1,5 +1,7 @@
using Avalonia.Layout;
using Avalonia.Metadata;
+using Avalonia.Platform;
+using Avalonia.Styling;
namespace Avalonia.Controls
{
diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs
index aacdb26667..3279315f9e 100644
--- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs
+++ b/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
diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs
index b897be4fb2..9d95ea5737 100644
--- a/src/Avalonia.Controls/TopLevel.cs
+++ b/src/Avalonia.Controls/TopLevel.cs
@@ -157,6 +157,8 @@ namespace Avalonia.Controls
static TopLevel()
{
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(KeyboardNavigationMode.Cycle);
+ Avalonia.Styling.Container.SizingProperty.OverrideDefaultValue(ContainerSizing.WidthAndHeight);
+
AffectsMeasure(ClientSizeProperty);
SystemBarColorProperty.Changed.AddClassHandler((view, e) =>
@@ -663,6 +665,7 @@ namespace Avalonia.Controls
}
private IDisposable? _insetsPaddings;
+
private void InvalidateChildInsetsPadding()
{
if (Content is Control child
diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs
index af84cdafa8..8a4fbe50f1 100644
--- a/src/Avalonia.Controls/WindowBase.cs
+++ b/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);
}
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
index 863fe5a16b..4fec398c95 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
@@ -56,6 +56,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
InsertBefore(
new AvaloniaXamlIlControlThemeTransformer(),
new AvaloniaXamlIlSelectorTransformer(),
+ new AvaloniaXamlIlQueryTransformer(),
new AvaloniaXamlIlDuplicateSettersChecker(),
new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(),
new AvaloniaXamlIlBindingPathParser(),
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs
index e13e855087..f4dea1b200 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs
+++ b/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,
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlQueryTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlQueryTransformer.cs
new file mode 100644
index 0000000000..f376934e16
--- /dev/null
+++ b/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()
+ .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 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 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
+ {
+ internal XamlIlQueryNode? Previous { get; }
+ public abstract IXamlType? TargetType { get; }
+
+ public XamlIlQueryNode(XamlIlQueryNode? previous,
+ IXamlLineInfo? info = null,
+ IXamlType? queryType = null) : base((info ?? previous)!)
+ {
+ Previous = previous;
+ Type = queryType == null ? previous!.Type : new XamlAstClrTypeReference(this, queryType, false);
+ }
+
+ public IXamlAstTypeReference Type { get; }
+
+ public virtual XamlILNodeEmitResult Emit(XamlEmitContext context, IXamlILEmitter codeGen)
+ {
+ if (Previous != null)
+ context.Emit(Previous, codeGen, Type.GetClrType());
+ DoEmit(context, codeGen);
+ return XamlILNodeEmitResult.Type(0, Type.GetClrType());
+ }
+
+ protected abstract void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen);
+
+ protected void EmitCall(XamlEmitContext context, IXamlILEmitter codeGen, Func method)
+ {
+ var queries = context.Configuration.TypeSystem.GetType("Avalonia.Styling.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 context, IXamlILEmitter codeGen) => codeGen.Ldnull();
+ }
+
+ class XamlIlTypeQuery : XamlIlQueryNode
+ {
+ public bool Concrete { get; }
+
+ public XamlIlTypeQuery(XamlIlQueryNode previous, IXamlType type, bool concrete) : base(previous)
+ {
+ TargetType = type;
+ Concrete = concrete;
+ }
+
+ public override IXamlType TargetType { get; }
+ protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen)
+ {
+ var name = Concrete ? "OfType" : "Is";
+ codeGen.Ldtype(TargetType);
+ EmitCall(context, codeGen,
+ m => m.Name == name && m.Parameters.Count == 2 && m.Parameters[1].FullName == "System.Type");
+ }
+ }
+
+ class XamlIlStringQuery : XamlIlQueryNode
+ {
+ public string String { get; set; }
+ public enum QueryType
+ {
+ Class,
+ Name
+ }
+
+ private QueryType _type;
+
+ public XamlIlStringQuery(XamlIlQueryNode previous, QueryType type, string s) : base(previous)
+ {
+ _type = type;
+ String = s;
+ }
+
+ public override IXamlType? TargetType => Previous?.TargetType;
+ protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen)
+ {
+ codeGen.Ldstr(String);
+ var name = _type.ToString();
+ EmitCall(context, codeGen,
+ m => m.Name == name && m.Parameters.Count == 2 && m.Parameters[1].FullName == "System.String");
+ }
+ }
+
+ class XamlIlCombinatorQuery : XamlIlQueryNode
+ {
+ private readonly CombinatorQueryType _type;
+
+ public enum CombinatorQueryType
+ {
+ Child,
+ Descendant,
+ Template
+ }
+ public XamlIlCombinatorQuery(XamlIlQueryNode previous, CombinatorQueryType type) : base(previous)
+ {
+ _type = type;
+ }
+
+ public CombinatorQueryType QueryType => _type;
+ public override IXamlType? TargetType => null;
+ protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen)
+ {
+ var name = _type.ToString();
+ EmitCall(context, codeGen,
+ m => m.Name == name && m.Parameters.Count == 1);
+ }
+ }
+
+ class 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 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 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 _queries = new List();
+ public XamlIlOrQueryNode(IXamlLineInfo info, IXamlType queryType) : base(null, info, queryType)
+ {
+ }
+
+ public void Add(XamlIlQueryNode node)
+ {
+ _queries.Add(node);
+ }
+
+ public override IXamlType? TargetType
+ {
+ get
+ {
+ IXamlType? result = null;
+
+ foreach (var query in _queries)
+ {
+ if (query.TargetType == null)
+ {
+ return null;
+ }
+ else if (result == null)
+ {
+ result = query.TargetType;
+ }
+ else
+ {
+ while (result?.IsAssignableFrom(query.TargetType) == false)
+ {
+ result = result.BaseType;
+ }
+ }
+ }
+
+ return result;
+ }
+ }
+
+ protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen)
+ {
+ if (_queries.Count == 0)
+ throw new XamlLoadException("Invalid query count", this);
+ if (_queries.Count == 1)
+ {
+ _queries[0].Emit(context, codeGen);
+ return;
+ }
+
+ 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 _queries = new List();
+ 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 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"));
+ }
+ }
+}
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
index b5efb5ca31..32c09b5cc7 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
+++ b/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() { 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");
diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ContainerQueryGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ContainerQueryGrammar.cs
new file mode 100644
index 0000000000..b12a070a33
--- /dev/null
+++ b/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 Parse(string s)
+ {
+ var r = new CharacterReader(s.AsSpan());
+ return Parse(ref r, null);
+ }
+
+ private static IEnumerable Parse(ref CharacterReader r, char? end)
+ {
+ var state = State.Start;
+ var selector = new List();
+ while (!r.End && state != State.End)
+ {
+ ISyntax? syntax = null;
+ switch (state)
+ {
+ case State.Start:
+ (state, syntax) = ParseStart(ref r);
+ break;
+ case State.Middle:
+ (state, syntax) = ParseMiddle(ref r, end);
+ break;
+ }
+ 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(ref CharacterReader r) where T: struct
+ {
+ var identifier = r.ParseIdentifier();
+
+ if (Enum.TryParse(identifier.ToString(), true, out T value))
+ return value;
+
+ throw new ExpressionParseException(r.Position, $"Expected a {typeof(T)} after.");
+ }
+
+ private static string ParseString(ref CharacterReader r)
+ {
+ return r.ParseIdentifier().ToString();
+ }
+
+ private static void Expect(ref CharacterReader r, char c)
+ {
+ if (r.End)
+ {
+ throw new ExpressionParseException(r.Position, $"Expected '{c}', got end of selector.");
+ }
+ else if (!r.TakeIf(')'))
+ {
+ throw new ExpressionParseException(r.Position, $"Expected '{c}', got '{r.Peek}'.");
+ }
+ }
+
+ public class 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 : ISyntax
+ {
+ public T? Value { get; set; }
+ public StyleQueryComparisonOperator Operator { get; set; }
+ }
+
+ public abstract class RangeSyntax : QuerySyntax
+ {
+
+ }
+
+ 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
+ {
+
+ }
+ }
+}
diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ContainerQueryParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ContainerQueryParser.cs
new file mode 100644
index 0000000000..6eb83745ee
--- /dev/null
+++ b/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
+{
+ ///
+ /// Parses a from text.
+ ///
+ internal class ContainerQueryParser
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ContainerQueryParser()
+ {
+ }
+
+ ///
+ /// Parses a from a string.
+ ///
+ /// The string.
+ /// The parsed selector.
+ [RequiresUnreferencedCode(TrimmingMessages.SelectorsParseRequiresUnreferencedCodeMessage)]
+ public StyleQuery? Parse(string s)
+ {
+ var syntax = ContainerQueryGrammar.Parse(s);
+ return Create(syntax);
+ }
+
+ [RequiresUnreferencedCode(TrimmingMessages.SelectorsParseRequiresUnreferencedCodeMessage)]
+ private StyleQuery? Create(IEnumerable syntax)
+ {
+ var result = default(StyleQuery);
+ var results = default(List);
+
+ 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();
+ }
+
+ 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;
+ }
+ }
+}
diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
index f232d5e109..6088d7177c 100644
--- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
+++ b/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(ref CharacterReader r) where T : struct
+ {
+ var identifier = r.ParseIdentifier();
+
+ if (Enum.TryParse(identifier.ToString(), true, out T value))
+ return value;
+
+ throw new ExpressionParseException(r.Position, $"Expected a {typeof(T)} after.");
+ }
+
+ private static string ParseString(ref CharacterReader r)
+ {
+ return r.ParseIdentifier().ToString();
+ }
+
private static (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;
+ }
+ }
}
}
diff --git a/tests/Avalonia.Base.UnitTests/Styling/ContainerTests.cs b/tests/Avalonia.Base.UnitTests/Styling/ContainerTests.cs
new file mode 100644
index 0000000000..e3949a8788
--- /dev/null
+++ b/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(() => 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())
+ {
+ 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())
+ {
+ 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())
+ {
+ 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())
+ {
+ 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())
+ {
+ 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())
+ {
+ 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);
+ }
+ }
+}