Sfoglia il codice sorgente

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
Emmanuel Hansen 6 mesi fa
parent
commit
cecc928dcc
49 ha cambiato i file con 2439 aggiunte e 11 eliminazioni
  1. 1 1
      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 0
      samples/ControlCatalog/ControlCatalog.csproj
  10. 3 0
      samples/ControlCatalog/MainView.xaml
  11. 110 0
      samples/ControlCatalog/Pages/ContainerQueryPage.xaml
  12. 18 0
      samples/ControlCatalog/Pages/ContainerQueryPage.xaml.cs
  13. 32 2
      src/Avalonia.Base/Layout/Layoutable.cs
  14. 36 0
      src/Avalonia.Base/Platform/VisualQueryProvider.cs
  15. 3 0
      src/Avalonia.Base/StyledElement.cs
  16. 69 0
      src/Avalonia.Base/Styling/Activators/AndQueryActivator.cs
  17. 51 0
      src/Avalonia.Base/Styling/Activators/AndQueryActivatorBuilder.cs
  18. 95 0
      src/Avalonia.Base/Styling/Activators/ContainerQueryActivatorBase.cs
  19. 63 0
      src/Avalonia.Base/Styling/Activators/OrQueryActivator.cs
  20. 51 0
      src/Avalonia.Base/Styling/Activators/OrQueryActivatorBuilder.cs
  21. 37 0
      src/Avalonia.Base/Styling/Activators/ScreenActivator.cs
  22. 99 0
      src/Avalonia.Base/Styling/AndQuery.cs
  23. 91 0
      src/Avalonia.Base/Styling/Container.cs
  24. 90 0
      src/Avalonia.Base/Styling/ContainerQuery.cs
  25. 28 0
      src/Avalonia.Base/Styling/ContainerSizing.cs
  26. 99 0
      src/Avalonia.Base/Styling/OrQuery.cs
  27. 149 0
      src/Avalonia.Base/Styling/ScreenQueries.cs
  28. 18 2
      src/Avalonia.Base/Styling/Selector.cs
  29. 2 1
      src/Avalonia.Base/Styling/Style.cs
  30. 77 0
      src/Avalonia.Base/Styling/StyleQueries.cs
  31. 164 0
      src/Avalonia.Base/Styling/StyleQuery.cs
  32. 12 0
      src/Avalonia.Base/Styling/StyleQueryComparisonOperator.cs
  33. 31 0
      src/Avalonia.Base/Styling/ValueStyleQuery.cs
  34. 11 0
      src/Avalonia.Base/Utilities/IdentifierParser.cs
  35. 6 0
      src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
  36. 0 3
      src/Avalonia.Controls/Border.cs
  37. 2 0
      src/Avalonia.Controls/ContentControl.cs
  38. 2 0
      src/Avalonia.Controls/Decorator.cs
  39. 2 0
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  40. 3 0
      src/Avalonia.Controls/TopLevel.cs
  41. 2 1
      src/Avalonia.Controls/WindowBase.cs
  42. 1 0
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  43. 2 1
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs
  44. 424 0
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlQueryTransformer.cs
  45. 2 0
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  46. 267 0
      src/Markup/Avalonia.Markup/Markup/Parsers/ContainerQueryGrammar.cs
  47. 77 0
      src/Markup/Avalonia.Markup/Markup/Parsers/ContainerQueryParser.cs
  48. 36 0
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
  49. 164 0
      tests/Avalonia.Base.UnitTests/Styling/ContainerTests.cs

+ 1 - 1
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


BIN
samples/ControlCatalog/Assets/image2.jpg


BIN
samples/ControlCatalog/Assets/image3.jpg


BIN
samples/ControlCatalog/Assets/image4.jpg


BIN
samples/ControlCatalog/Assets/image5.jpg


BIN
samples/ControlCatalog/Assets/image6.jpg


BIN
samples/ControlCatalog/Assets/image7.jpg


+ 9 - 0
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 - 0
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 - 0
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 - 0
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);
+        }
+    }
+}

+ 32 - 2
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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();
+        }
+    }
+}

+ 18 - 2
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;
         }
     }
 }

+ 2 - 1
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 - 0
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 - 0
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 - 0
src/Avalonia.Base/Styling/StyleQueryComparisonOperator.cs

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

+ 31 - 0
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 - 0
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 - 0
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>

+ 0 - 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 - 0
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 - 0
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 - 0
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 - 0
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

+ 2 - 1
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 - 0
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(),

+ 2 - 1
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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);
+        }
+    }
+}