diff --git a/Avalonia.sln b/Avalonia.sln index 9d60fe2e45..a0314b1c33 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -274,7 +274,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tizen", "Tizen", "{D1300000 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Tizen", "src\Tizen\Avalonia.Tizen\Avalonia.Tizen.csproj", "{DFFBDBF5-5DBE-47ED-9EAE-D40B75AC99E8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Tizen", "samples\ControlCatalog.Tizen\ControlCatalog.Tizen.csproj", "{A0B29221-2B6F-4B29-A4D5-2227811B5915}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Tizen", "samples\ControlCatalog.Tizen\ControlCatalog.Tizen.csproj", "{A0B29221-2B6F-4B29-A4D5-2227811B5915}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Metal", "src\Avalonia.Metal\Avalonia.Metal.csproj", "{60B4ED1F-ECFA-453B-8A70-1788261C8355}" EndProject diff --git a/samples/ControlCatalog/Assets/image1.jpg b/samples/ControlCatalog/Assets/image1.jpg new file mode 100644 index 0000000000..673cb38399 Binary files /dev/null and b/samples/ControlCatalog/Assets/image1.jpg differ diff --git a/samples/ControlCatalog/Assets/image2.jpg b/samples/ControlCatalog/Assets/image2.jpg new file mode 100644 index 0000000000..83d721c083 Binary files /dev/null and b/samples/ControlCatalog/Assets/image2.jpg differ diff --git a/samples/ControlCatalog/Assets/image3.jpg b/samples/ControlCatalog/Assets/image3.jpg new file mode 100644 index 0000000000..b14700656e Binary files /dev/null and b/samples/ControlCatalog/Assets/image3.jpg differ diff --git a/samples/ControlCatalog/Assets/image4.jpg b/samples/ControlCatalog/Assets/image4.jpg new file mode 100644 index 0000000000..e15787a2e7 Binary files /dev/null and b/samples/ControlCatalog/Assets/image4.jpg differ diff --git a/samples/ControlCatalog/Assets/image5.jpg b/samples/ControlCatalog/Assets/image5.jpg new file mode 100644 index 0000000000..b6077334ef Binary files /dev/null and b/samples/ControlCatalog/Assets/image5.jpg differ diff --git a/samples/ControlCatalog/Assets/image6.jpg b/samples/ControlCatalog/Assets/image6.jpg new file mode 100644 index 0000000000..ea5228d178 Binary files /dev/null and b/samples/ControlCatalog/Assets/image6.jpg differ diff --git a/samples/ControlCatalog/Assets/image7.jpg b/samples/ControlCatalog/Assets/image7.jpg new file mode 100644 index 0000000000..2f86ce4ae5 Binary files /dev/null and b/samples/ControlCatalog/Assets/image7.jpg differ diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 8ef96169df..cff0bb4e92 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -15,6 +15,15 @@ + + + + + + + + + diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index e3eed5fb0e..45dcab28fb 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -60,6 +60,9 @@ + + + diff --git a/samples/ControlCatalog/Pages/ContainerQueryPage.xaml b/samples/ControlCatalog/Pages/ContainerQueryPage.xaml new file mode 100644 index 0000000000..e6b0558a04 --- /dev/null +++ b/samples/ControlCatalog/Pages/ContainerQueryPage.xaml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ContainerQueryPage.xaml.cs b/samples/ControlCatalog/Pages/ContainerQueryPage.xaml.cs new file mode 100644 index 0000000000..76d43fa81c --- /dev/null +++ b/samples/ControlCatalog/Pages/ContainerQueryPage.xaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class ContainerQueryPage : UserControl + { + public ContainerQueryPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs index ad39127cf4..47f2217816 100644 --- a/src/Avalonia.Base/Layout/Layoutable.cs +++ b/src/Avalonia.Base/Layout/Layoutable.cs @@ -2,6 +2,7 @@ using System; using Avalonia.Diagnostics; using Avalonia.Logging; using Avalonia.Reactive; +using Avalonia.Styling; using Avalonia.Utilities; using Avalonia.VisualTree; @@ -553,14 +554,43 @@ namespace Avalonia.Layout var minMax = new MinMax(this); - var constrained = LayoutHelper.ApplyLayoutConstraints( + var constrainedSize = LayoutHelper.ApplyLayoutConstraints( minMax, availableSize.Deflate(margin)); - var measured = MeasureOverride(constrained); + + var isContainer = false; + ContainerSizing containerSizing = ContainerSizing.Normal; + + if (Container.GetQueryProvider(this) is { } queryProvider && Container.GetSizing(this) is { } sizing && sizing != ContainerSizing.Normal) + { + isContainer = true; + containerSizing = sizing; + queryProvider.SetSize(constrainedSize.Width, constrainedSize.Height, containerSizing); + } + + var measured = MeasureOverride(constrainedSize); var width = MathUtilities.Clamp(measured.Width, minMax.MinWidth, minMax.MaxWidth); var height = MathUtilities.Clamp(measured.Height, minMax.MinHeight, minMax.MaxHeight); + if (isContainer) + { + switch (containerSizing) + { + case ContainerSizing.Width: + width = double.IsInfinity(constrainedSize.Width) ? width : constrainedSize.Width; + break; + case ContainerSizing.Height: + width = measured.Width; + height = double.IsInfinity(constrainedSize.Height) ? height : constrainedSize.Height; + break; + case ContainerSizing.WidthAndHeight: + width = double.IsInfinity(constrainedSize.Width) ? width : constrainedSize.Width; + height = double.IsInfinity(constrainedSize.Height) ? height : constrainedSize.Height; + break; + } + } + if (useLayoutRounding) { (width, height) = LayoutHelper.RoundLayoutSizeUp(new Size(width, height), scale); diff --git a/src/Avalonia.Base/Platform/VisualQueryProvider.cs b/src/Avalonia.Base/Platform/VisualQueryProvider.cs new file mode 100644 index 0000000000..31463e9e61 --- /dev/null +++ b/src/Avalonia.Base/Platform/VisualQueryProvider.cs @@ -0,0 +1,36 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Platform +{ + internal class VisualQueryProvider + { + private readonly Visual _visual; + + public double Width { get; private set; } = double.PositiveInfinity; + + public double Height { get; private set; } = double.PositiveInfinity; + + public VisualQueryProvider(Visual visual) + { + _visual = visual; + } + + public event EventHandler? WidthChanged; + public event EventHandler? HeightChanged; + + public virtual void SetSize(double width, double height, Styling.ContainerSizing containerType) + { + var currentWidth = Width; + var currentHeight = Height; + + Width = width; + Height = height; + + if (currentWidth != Width && (containerType == Styling.ContainerSizing.Width || containerType == Styling.ContainerSizing.WidthAndHeight)) + WidthChanged?.Invoke(this, EventArgs.Empty); + if (currentHeight != Height && (containerType == Styling.ContainerSizing.Height || containerType == Styling.ContainerSizing.WidthAndHeight)) + HeightChanged?.Invoke(this, EventArgs.Empty); + } + } +} diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index bbdb0e998c..b7f6262d9f 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -840,6 +840,9 @@ namespace Avalonia private void ApplyStyle(IStyle style, IStyleHost? host, FrameType type) { + if (style is Styling.ContainerQuery m) + m.TryAttach(this, host, type); + if (style is Style s) s.TryAttach(this, host, type); diff --git a/src/Avalonia.Base/Styling/Activators/AndQueryActivator.cs b/src/Avalonia.Base/Styling/Activators/AndQueryActivator.cs new file mode 100644 index 0000000000..20030567ca --- /dev/null +++ b/src/Avalonia.Base/Styling/Activators/AndQueryActivator.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; + +namespace Avalonia.Styling.Activators +{ + /// + /// An aggregate which is active when all of its inputs are + /// active. + /// + internal class AndQueryActivator : ContainerQueryActivatorBase, IStyleActivatorSink + { + private List? _sources; + + public AndQueryActivator(Visual visual) : base(visual) + { + } + + public int Count => _sources?.Count ?? 0; + + public void Add(IStyleActivator activator) + { + if (IsSubscribed) + throw new AvaloniaInternalException("AndActivator is already subscribed."); + _sources ??= new List(); + _sources.Add(activator); + } + + void IStyleActivatorSink.OnNext(bool value) => ReevaluateIsActive(); + + protected override bool EvaluateIsActive() + { + if (_sources is null || _sources.Count == 0) + return true; + + var count = _sources.Count; + var mask = (1ul << count) - 1; + var flags = 0UL; + + for (var i = 0; i < count; ++i) + { + if (_sources[i].GetIsActive()) + flags |= 1ul << i; + } + + return flags == mask; + } + + protected override void Initialize() + { + if (_sources is object) + { + foreach (var source in _sources) + { + source.Subscribe(this); + } + } + } + + protected override void Deinitialize() + { + if (_sources is object) + { + foreach (var source in _sources) + { + source.Unsubscribe(this); + } + } + } + } +} diff --git a/src/Avalonia.Base/Styling/Activators/AndQueryActivatorBuilder.cs b/src/Avalonia.Base/Styling/Activators/AndQueryActivatorBuilder.cs new file mode 100644 index 0000000000..dfa8684d93 --- /dev/null +++ b/src/Avalonia.Base/Styling/Activators/AndQueryActivatorBuilder.cs @@ -0,0 +1,51 @@ +#nullable enable + +namespace Avalonia.Styling.Activators +{ + /// + /// Builds an . + /// + /// + /// When ANDing style activators, if there is more than one input then creates an instance of + /// . If there is only one input, returns the input directly. + /// + internal struct AndQueryActivatorBuilder + { + private readonly Visual _visual; + private IStyleActivator? _single; + private AndQueryActivator? _multiple; + + public AndQueryActivatorBuilder(Visual visual) : this() + { + _visual = visual; + } + + public int Count => _multiple?.Count ?? (_single is object ? 1 : 0); + + public void Add(IStyleActivator? activator) + { + if (activator == null) + { + return; + } + + if (_single is null && _multiple is null) + { + _single = activator; + } + else + { + if (_multiple is null) + { + _multiple = new AndQueryActivator(_visual); + _multiple.Add(_single!); + _single = null; + } + + _multiple.Add(activator); + } + } + + public IStyleActivator Get() => _single ?? _multiple!; + } +} diff --git a/src/Avalonia.Base/Styling/Activators/ContainerQueryActivatorBase.cs b/src/Avalonia.Base/Styling/Activators/ContainerQueryActivatorBase.cs new file mode 100644 index 0000000000..932fab488d --- /dev/null +++ b/src/Avalonia.Base/Styling/Activators/ContainerQueryActivatorBase.cs @@ -0,0 +1,95 @@ +using System; +using System.Linq; +using Avalonia.Layout; +using Avalonia.VisualTree; + +namespace Avalonia.Styling.Activators +{ + internal abstract class ContainerQueryActivatorBase : StyleActivatorBase, IStyleActivatorSink + { + private readonly Visual _visual; + private readonly string? _containerName; + private Layoutable? _currentScreenSizeProvider; + + public ContainerQueryActivatorBase( + Visual visual, string? containerName = null) + { + _visual = visual; + _containerName = containerName; + } + + private void Visual_DetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e) + { + DeInitializeScreenSizeProvider(); + } + + private void Visual_AttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) + { + InitializeScreenSizeProvider(); + } + + protected Layoutable? CurrentContainer => _currentScreenSizeProvider; + + void IStyleActivatorSink.OnNext(bool value) => ReevaluateIsActive(); + + protected override void Initialize() + { + InitializeScreenSizeProvider(); + + _visual.AttachedToVisualTree += Visual_AttachedToVisualTree; + _visual.DetachedFromVisualTree += Visual_DetachedFromVisualTree; + } + + protected override void Deinitialize() + { + _visual.AttachedToVisualTree -= Visual_AttachedToVisualTree; + _visual.DetachedFromVisualTree -= Visual_DetachedFromVisualTree; + + DeInitializeScreenSizeProvider(); + } + + private void DeInitializeScreenSizeProvider() + { + if (_currentScreenSizeProvider is { } && Container.GetQueryProvider(_currentScreenSizeProvider) is { } provider) + { + provider.WidthChanged -= WidthChanged; + provider.HeightChanged -= HeightChanged; + _currentScreenSizeProvider = null; + } + } + + private void InitializeScreenSizeProvider() + { + if (_currentScreenSizeProvider == null && GetContainer(_visual, _containerName) is { } container && Container.GetQueryProvider(container) is { } provider) + { + _currentScreenSizeProvider = container; + + provider.WidthChanged += WidthChanged; + provider.HeightChanged += HeightChanged; + } + + ReevaluateIsActive(); + } + + internal static Layoutable? GetContainer(Visual visual, string? containerName) + { + return visual.GetVisualAncestors().Where(x => x is Layoutable layoutable && + (Container.GetName(layoutable) == containerName)).FirstOrDefault() as Layoutable; + } + + private void HeightChanged(object? sender, EventArgs e) + { + ReevaluateIsActive(); + } + + private void WidthChanged(object? sender, EventArgs e) + { + ReevaluateIsActive(); + } + + private void OrientationChanged(object? sender, EventArgs e) + { + ReevaluateIsActive(); + } + } +} diff --git a/src/Avalonia.Base/Styling/Activators/OrQueryActivator.cs b/src/Avalonia.Base/Styling/Activators/OrQueryActivator.cs new file mode 100644 index 0000000000..3de6311bd7 --- /dev/null +++ b/src/Avalonia.Base/Styling/Activators/OrQueryActivator.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; + +namespace Avalonia.Styling.Activators +{ + /// + /// An aggregate which is active when any of its inputs are + /// active. + /// + internal class OrQueryActivator : ContainerQueryActivatorBase, IStyleActivatorSink + { + private List? _sources; + + public OrQueryActivator(Visual visual) : base(visual) + { + } + + public int Count => _sources?.Count ?? 0; + + public void Add(IStyleActivator activator) + { + _sources ??= new List(); + _sources.Add(activator); + } + + void IStyleActivatorSink.OnNext(bool value) => ReevaluateIsActive(); + + protected override bool EvaluateIsActive() + { + if (_sources is null || _sources.Count == 0) + return true; + + foreach (var source in _sources) + { + if (source.GetIsActive()) + return true; + } + + return false; + } + + protected override void Initialize() + { + if (_sources is object) + { + foreach (var source in _sources) + { + source.Subscribe(this); + } + } + } + + protected override void Deinitialize() + { + if (_sources is object) + { + foreach (var source in _sources) + { + source.Unsubscribe(this); + } + } + } + } +} diff --git a/src/Avalonia.Base/Styling/Activators/OrQueryActivatorBuilder.cs b/src/Avalonia.Base/Styling/Activators/OrQueryActivatorBuilder.cs new file mode 100644 index 0000000000..0571f0d3c1 --- /dev/null +++ b/src/Avalonia.Base/Styling/Activators/OrQueryActivatorBuilder.cs @@ -0,0 +1,51 @@ +#nullable enable + +namespace Avalonia.Styling.Activators +{ + /// + /// Builds an . + /// + /// + /// When ORing style activators, if there is more than one input then creates an instance of + /// . If there is only one input, returns the input directly. + /// + internal struct OrQueryActivatorBuilder + { + private IStyleActivator? _single; + private OrQueryActivator? _multiple; + private Visual _visual; + + public OrQueryActivatorBuilder(Visual visual) : this() + { + _visual = visual; + } + + public int Count => _multiple?.Count ?? (_single is object ? 1 : 0); + + public void Add(IStyleActivator? activator) + { + if (activator == null) + { + return; + } + + if (_single is null && _multiple is null) + { + _single = activator; + } + else + { + if (_multiple is null) + { + _multiple = new OrQueryActivator(_visual); + _multiple.Add(_single!); + _single = null; + } + + _multiple.Add(activator); + } + } + + public IStyleActivator Get() => _single ?? _multiple!; + } +} diff --git a/src/Avalonia.Base/Styling/Activators/ScreenActivator.cs b/src/Avalonia.Base/Styling/Activators/ScreenActivator.cs new file mode 100644 index 0000000000..5778c2a401 --- /dev/null +++ b/src/Avalonia.Base/Styling/Activators/ScreenActivator.cs @@ -0,0 +1,37 @@ +using Avalonia.Layout; + +namespace Avalonia.Styling.Activators +{ + internal sealed class WidthActivator : ContainerQueryActivatorBase + { + private readonly (StyleQueryComparisonOperator @operator, double value) _argument; + + public WidthActivator(Visual visual, (StyleQueryComparisonOperator @operator, double value) argument, string? containerName = null) : base(visual, containerName) + { + _argument = argument; + } + + protected override bool EvaluateIsActive() => (CurrentContainer is Layoutable layoutable + && Container.GetSizing(layoutable) is { } sizing + && Container.GetQueryProvider(layoutable) is { } queryProvider + + && (sizing is Styling.ContainerSizing.Width or Styling.ContainerSizing.WidthAndHeight)) + && WidthQuery.Evaluate(queryProvider, _argument).IsMatch; + } + + internal sealed class HeightActivator : ContainerQueryActivatorBase + { + private readonly (StyleQueryComparisonOperator @operator, double value) _argument; + + public HeightActivator(Visual visual, (StyleQueryComparisonOperator @operator, double value) argument, string? containerName = null) : base(visual, containerName) + { + _argument = argument; + } + + protected override bool EvaluateIsActive() => (CurrentContainer is Layoutable layoutable + && Container.GetSizing(layoutable) is { } sizing + && Container.GetQueryProvider(layoutable) is { } queryProvider + && (sizing is Styling.ContainerSizing.Height or Styling.ContainerSizing.WidthAndHeight)) + && HeightQuery.Evaluate(queryProvider, _argument).IsMatch; + } +} diff --git a/src/Avalonia.Base/Styling/AndQuery.cs b/src/Avalonia.Base/Styling/AndQuery.cs new file mode 100644 index 0000000000..e21b8f228a --- /dev/null +++ b/src/Avalonia.Base/Styling/AndQuery.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Styling.Activators; + +#nullable enable + +namespace Avalonia.Styling +{ + /// + /// The AND style query. + /// + internal sealed class AndQuery : StyleQuery + { + private readonly IReadOnlyList _queries; + private string? _queryString; + + /// + /// Initializes a new instance of the class. + /// + /// The queries to AND. + public AndQuery(IReadOnlyList queries) + { + if (queries is null) + { + throw new ArgumentNullException(nameof(queries)); + } + + if (queries.Count <= 1) + { + throw new ArgumentException("Need more than one query to AND."); + } + + _queries = queries; + } + + /// + internal override bool IsCombinator => false; + + /// + public override string ToString(ContainerQuery? owner) + { + if (_queryString == null) + { + _queryString = string.Join(" and ", _queries.Select(x => x.ToString(owner))); + } + + return _queryString; + } + + internal override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe, string? containerName = null) + { + if (control is not Visual visual) + { + return SelectorMatch.NeverThisType; + } + + var activators = new AndQueryActivatorBuilder(visual); + var alwaysThisInstance = false; + + var count = _queries.Count; + + for (var i = 0; i < count; i++) + { + var match = _queries[i].Match(control, parent, subscribe, containerName); + + switch (match.Result) + { + case SelectorMatchResult.AlwaysThisInstance: + alwaysThisInstance = true; + break; + case SelectorMatchResult.NeverThisInstance: + case SelectorMatchResult.NeverThisType: + return match; + case SelectorMatchResult.Sometimes: + activators.Add(match.Activator!); + break; + } + } + + if (activators.Count > 0) + { + return new SelectorMatch(activators.Get()); + } + else if (alwaysThisInstance) + { + return SelectorMatch.AlwaysThisInstance; + } + else + { + return SelectorMatch.AlwaysThisType; + } + } + + private protected override StyleQuery? MovePrevious() => null; + private protected override StyleQuery? MovePreviousOrParent() => null; + } +} + diff --git a/src/Avalonia.Base/Styling/Container.cs b/src/Avalonia.Base/Styling/Container.cs new file mode 100644 index 0000000000..1aaa2a34a4 --- /dev/null +++ b/src/Avalonia.Base/Styling/Container.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Layout; +using Avalonia.Platform; + +namespace Avalonia.Styling +{ + public static class Container + { + /// + /// Defines the Name attached property. + /// + public static readonly AttachedProperty NameProperty = + AvaloniaProperty.RegisterAttached("Name", typeof(Container)); + + /// + /// Defines the Sizing attached property. + /// + public static readonly AttachedProperty SizingProperty = + AvaloniaProperty.RegisterAttached("Sizing", typeof(Container), coerce:UpdateQueryProvider); + + private static ContainerSizing UpdateQueryProvider(AvaloniaObject obj, ContainerSizing sizing) + { + if (obj is Layoutable layoutable) + { + if (sizing != ContainerSizing.Normal) + { + if (GetQueryProvider(layoutable) == null) + layoutable.SetValue(QueryProviderProperty, new VisualQueryProvider(layoutable)); + } + else + { + layoutable.SetValue(QueryProviderProperty, null); + } + } + + return sizing; + } + + internal static readonly AttachedProperty QueryProviderProperty = + AvaloniaProperty.RegisterAttached("QueryProvider", typeof(Container)); + + /// + /// Gets the value of the Container.Name attached property. + /// + /// The layoutable to read the value from. + /// The container name of the layoutable + public static string? GetName(Layoutable layoutable) + { + return layoutable.GetValue(NameProperty); + } + + /// + /// Sets the value of the Container.Name attached property. + /// + /// The layoutable to set the value on. + /// The container name. + public static void SetName(Layoutable layoutable, string? name) + { + layoutable.SetValue(NameProperty, name); + } + + /// + /// Gets the value of the Container.Sizing attached property. + /// + /// The layoutable to read the value from. + /// The container sizing mode of the layoutable + public static ContainerSizing GetSizing(Layoutable layoutable) + { + return layoutable.GetValue(SizingProperty); + } + + /// + /// Sets the value of the Container.Name attached property. + /// + /// The layoutable to set the value on. + /// The container sizing mode. + public static void SetSizing(Layoutable layoutable, ContainerSizing sizing) + { + layoutable.SetValue(SizingProperty, sizing); + } + + internal static VisualQueryProvider? GetQueryProvider(Layoutable layoutable) + { + return layoutable.GetValue(QueryProviderProperty); + } + } +} diff --git a/src/Avalonia.Base/Styling/ContainerQuery.cs b/src/Avalonia.Base/Styling/ContainerQuery.cs new file mode 100644 index 0000000000..0c738fd200 --- /dev/null +++ b/src/Avalonia.Base/Styling/ContainerQuery.cs @@ -0,0 +1,90 @@ +using System; +using Avalonia.Controls; +using Avalonia.PropertyStore; + +namespace Avalonia.Styling +{ + /// + /// Defines a container. + /// + public class ContainerQuery + : StyleBase + { + private StyleQuery? _query; + private string? _name; + + /// + /// Initializes a new instance of the class. + /// + public ContainerQuery() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The container selector. + /// + public ContainerQuery(Func query, string? containerName = null) + { + Query = query(null); + _name = containerName; + } + + /// + /// Gets or sets the container's query. + /// + public StyleQuery? Query + { + get => _query; + set => _query = value; + } + + /// + /// Gets or sets the container's name. + /// + public string? Name + { + get => _name; + set => _name = value; + } + + /// + /// Returns a string representation of the container. + /// + /// A string representation of the container. + public override string ToString() => Query?.ToString(this) ?? "ContainerQuery"; + + internal override void SetParent(StyleBase? parent) + { + if (parent is ControlTheme) + base.SetParent(parent); + else + throw new InvalidOperationException("Container cannot be added as a nested style."); + } + + internal SelectorMatchResult TryAttach(StyledElement target, object? host, FrameType type) + { + _ = target ?? throw new ArgumentNullException(nameof(target)); + + var result = SelectorMatchResult.NeverThisType; + + if (HasChildren) + { + var match = Query?.Match(target, Parent, true, Name) ?? + (target == host ? + SelectorMatch.AlwaysThisInstance : + SelectorMatch.NeverThisInstance); + + if (match.IsMatch) + { + Attach(target, match.Activator, type, true); + } + + result = match.Result; + } + + return result; + } + } +} diff --git a/src/Avalonia.Base/Styling/ContainerSizing.cs b/src/Avalonia.Base/Styling/ContainerSizing.cs new file mode 100644 index 0000000000..482e38bcf3 --- /dev/null +++ b/src/Avalonia.Base/Styling/ContainerSizing.cs @@ -0,0 +1,28 @@ +namespace Avalonia.Styling +{ + /// + /// Defines how a container is queried. + /// + public enum ContainerSizing + { + /// + /// The container is not included in any size queries. + /// + Normal, + + /// + /// The container size can be queried for width. + /// + Width, + + /// + /// The container size can be queried for height. + /// + Height, + + /// + /// The container size can be queried for width and height. + /// + WidthAndHeight + } +} diff --git a/src/Avalonia.Base/Styling/OrQuery.cs b/src/Avalonia.Base/Styling/OrQuery.cs new file mode 100644 index 0000000000..29ab78c605 --- /dev/null +++ b/src/Avalonia.Base/Styling/OrQuery.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Styling.Activators; + +#nullable enable + +namespace Avalonia.Styling +{ + /// + /// The OR style query. + /// + internal sealed class OrQuery : StyleQuery + { + private readonly IReadOnlyList _queries; + private string? _queryString; + + /// + /// Initializes a new instance of the class. + /// + /// The querys to OR. + public OrQuery(IReadOnlyList queries) + { + if (queries is null) + { + throw new ArgumentNullException(nameof(queries)); + } + + if (queries.Count <= 1) + { + throw new ArgumentException("Need more than one query to OR."); + } + + _queries = queries; + } + + /// + internal override bool IsCombinator => false; + + /// + public override string ToString(ContainerQuery? owner) + { + if (_queryString == null) + { + _queryString = string.Join(", ", _queries.Select(x => x.ToString(owner))); + } + + return _queryString; + } + + internal override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe, string? containerName = null) + { + if (control is not Visual visual) + { + return SelectorMatch.NeverThisType; + } + + var activators = new OrQueryActivatorBuilder(visual); + var neverThisInstance = false; + + var count = _queries.Count; + + for (var i = 0; i < count; i++) + { + var match = _queries[i].Match(control, parent, subscribe, containerName); + + switch (match.Result) + { + case SelectorMatchResult.AlwaysThisType: + case SelectorMatchResult.AlwaysThisInstance: + return match; + case SelectorMatchResult.NeverThisInstance: + neverThisInstance = true; + break; + case SelectorMatchResult.Sometimes: + activators.Add(match.Activator!); + break; + } + } + + if (activators.Count > 0) + { + return new SelectorMatch(activators.Get()); + } + else if (neverThisInstance) + { + return SelectorMatch.NeverThisInstance; + } + else + { + return SelectorMatch.NeverThisType; + } + } + + private protected override StyleQuery? MovePrevious() => null; + private protected override StyleQuery? MovePreviousOrParent() => null; + } +} + diff --git a/src/Avalonia.Base/Styling/ScreenQueries.cs b/src/Avalonia.Base/Styling/ScreenQueries.cs new file mode 100644 index 0000000000..9500b634f3 --- /dev/null +++ b/src/Avalonia.Base/Styling/ScreenQueries.cs @@ -0,0 +1,149 @@ +using System; +using Avalonia.Layout; +using Avalonia.Platform; +using Avalonia.Styling.Activators; + +namespace Avalonia.Styling +{ + internal sealed class WidthQuery : ValueStyleQuery<(StyleQueryComparisonOperator @operator, double value)> + { + public WidthQuery(StyleQuery? previous, StyleQueryComparisonOperator @operator, double value) : base(previous, (@operator, value)) + { + } + + internal override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe, string? containerName = null) + { + if (control is not Visual visual) + { + return SelectorMatch.NeverThisType; + } + + if (subscribe) + { + return new SelectorMatch(new WidthActivator(visual, Argument, containerName)); + } + + if (ContainerQueryActivatorBase.GetContainer(visual, containerName) is { } container + && container is Layoutable layoutable + && Container.GetQueryProvider(layoutable) is { } queryProvider + && Container.GetSizing(layoutable) == Styling.ContainerSizing.WidthAndHeight) + { + return Evaluate(queryProvider, Argument); + } + + return SelectorMatch.NeverThisInstance; + } + + internal static SelectorMatch Evaluate(VisualQueryProvider queryProvider, (StyleQueryComparisonOperator @operator, double value) argument) + { + var width = queryProvider.Width; + if (double.IsNaN(width)) + { + return SelectorMatch.NeverThisInstance; + } + + bool IsTrue(StyleQueryComparisonOperator comparisonOperator, double value) + { + switch (comparisonOperator) + { + case StyleQueryComparisonOperator.None: + return true; + case StyleQueryComparisonOperator.Equals: + return width == value; + case StyleQueryComparisonOperator.LessThan: + return width < value; + case StyleQueryComparisonOperator.GreaterThan: + return width > value; + case StyleQueryComparisonOperator.LessThanOrEquals: + return width <= value; + case StyleQueryComparisonOperator.GreaterThanOrEquals: + return width >= value; + } + + return false; + } + + return IsTrue(argument.@operator, argument.value) ? + new SelectorMatch(SelectorMatchResult.AlwaysThisInstance) : SelectorMatch.NeverThisInstance; + } + + public override string ToString() => "width"; + + public override string ToString(ContainerQuery? owner) + { + throw new NotImplementedException(); + } + } + + internal sealed class HeightQuery : ValueStyleQuery<(StyleQueryComparisonOperator @operator, double value)> + { + public HeightQuery(StyleQuery? previous, StyleQueryComparisonOperator @operator, double value) : base(previous, (@operator, value)) + { + } + + internal override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe, string? containerName = null) + { + if (control is not Visual visual) + { + return SelectorMatch.NeverThisType; + } + + if (subscribe) + { + return new SelectorMatch(new HeightActivator(visual, Argument)); + } + + if (ContainerQueryActivatorBase.GetContainer(visual, containerName) is { } container + && container is Layoutable layoutable + && Container.GetQueryProvider(layoutable) is { } queryProvider + && Container.GetSizing(layoutable) == Styling.ContainerSizing.WidthAndHeight) + { + return Evaluate(queryProvider, Argument); + } + + return SelectorMatch.NeverThisInstance; + } + + internal static SelectorMatch Evaluate(VisualQueryProvider screenSizeProvider, (StyleQueryComparisonOperator @operator, double value) argument) + { + var height = screenSizeProvider.Height; + if (double.IsNaN(height)) + { + return SelectorMatch.NeverThisInstance; + } + + var isvalueTrue = IsTrue(argument.@operator, argument.value); + + bool IsTrue(StyleQueryComparisonOperator comparisonOperator, double value) + { + switch (comparisonOperator) + { + case StyleQueryComparisonOperator.None: + return true; + case StyleQueryComparisonOperator.Equals: + return height == value; + case StyleQueryComparisonOperator.LessThan: + return height < value; + case StyleQueryComparisonOperator.GreaterThan: + return height > value; + case StyleQueryComparisonOperator.LessThanOrEquals: + return height <= value; + case StyleQueryComparisonOperator.GreaterThanOrEquals: + return height >= value; + } + + return false; + } + + return IsTrue(argument.@operator, argument.value) ? + SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; + } + + public override string ToString() => "height"; + + public override string ToString(ContainerQuery? owner) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Avalonia.Base/Styling/Selector.cs b/src/Avalonia.Base/Styling/Selector.cs index 4e1c338553..cb3ddc343c 100644 --- a/src/Avalonia.Base/Styling/Selector.cs +++ b/src/Avalonia.Base/Styling/Selector.cs @@ -166,8 +166,24 @@ namespace Avalonia.Styling } } + SelectorMatch match = SelectorMatch.NeverThisInstance; + bool containerMatchesSometimes = false; + // Match any parent Container query + if (parent is ContainerQuery container) + { + match = container.Query?.Evaluate(control, container.Parent, subscribe, container.Name) ?? SelectorMatch.NeverThisInstance; + + if (!match.IsMatch) + return match.Result; + + containerMatchesSometimes = match.Result == SelectorMatchResult.Sometimes; + + if (containerMatchesSometimes) + activators.Add(match.Activator!); + } + // Match this selector. - var match = selector.Evaluate(control, parent, subscribe); + match = selector.Evaluate(control, parent, subscribe); if (!match.IsMatch) { @@ -184,7 +200,7 @@ namespace Avalonia.Styling combinator = previous; } - return match.Result; + return containerMatchesSometimes ? SelectorMatchResult.Sometimes : match.Result; } } } diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs index 57ce8d7927..de17363f15 100644 --- a/src/Avalonia.Base/Styling/Style.cs +++ b/src/Avalonia.Base/Styling/Style.cs @@ -73,7 +73,8 @@ namespace Avalonia.Styling var match = Selector?.Match(target, Parent, true) ?? (target == host ? - SelectorMatch.AlwaysThisInstance : + (Parent is ContainerQuery containerQuery ? containerQuery.Query?.Match(target, containerQuery.Parent, true, containerQuery.Name) ?? + SelectorMatch.NeverThisInstance : SelectorMatch.AlwaysThisInstance) : SelectorMatch.NeverThisInstance); activity?.AddTag(Diagnostic.Tags.SelectorResult, match.Result); diff --git a/src/Avalonia.Base/Styling/StyleQueries.cs b/src/Avalonia.Base/Styling/StyleQueries.cs new file mode 100644 index 0000000000..ff78fa18b5 --- /dev/null +++ b/src/Avalonia.Base/Styling/StyleQueries.cs @@ -0,0 +1,77 @@ +using Avalonia.Platform; +using System.Collections.Generic; + +namespace Avalonia.Styling +{ + /// + /// Extension methods for . + /// + public static class StyleQueries + { + /// + /// Returns a query which matches the device width with a value. + /// + /// The previous query. + /// The operator to match the device width + /// The width to match + /// The query. + public static StyleQuery Width(this StyleQuery? previous, StyleQueryComparisonOperator @operator, double value) + { + return new WidthQuery(previous, @operator, value); + } + + + + /// + /// Returns a query which matches the device height with a value. + /// + /// The previous query. + /// The operator to match the device height + /// The height to match + /// The query. + public static StyleQuery Height(this StyleQuery? previous, StyleQueryComparisonOperator @operator, double value) + { + return new HeightQuery(previous, @operator, value); + } + + /// + /// Returns a query which ORs queries. + /// + /// The queries to be OR'd. + /// The query. + public static StyleQuery Or(params StyleQuery[] queries) + { + return new OrQuery(queries); + } + + /// + /// Returns a query which ORs queries. + /// + /// The queries to be OR'd. + /// The query. + public static StyleQuery Or(IReadOnlyList query) + { + return new OrQuery(query); + } + + /// + /// Returns a query which ANDs queries. + /// + /// The queries to be AND'd. + /// The query. + public static StyleQuery And(params StyleQuery[] queries) + { + return new AndQuery(queries); + } + + /// + /// Returns a query which ANDs queries. + /// + /// The queries to be AND'd. + /// The query. + public static StyleQuery And(IReadOnlyList query) + { + return new AndQuery(query); + } + } +} diff --git a/src/Avalonia.Base/Styling/StyleQuery.cs b/src/Avalonia.Base/Styling/StyleQuery.cs new file mode 100644 index 0000000000..a0ba962f76 --- /dev/null +++ b/src/Avalonia.Base/Styling/StyleQuery.cs @@ -0,0 +1,164 @@ +using System; +using Avalonia.Styling.Activators; + +#nullable enable + +namespace Avalonia.Styling +{ + /// + /// A query in a . + /// + public abstract class StyleQuery + { + /// + /// Gets a value indicating whether this query is a combinator. + /// + /// + /// A combinator is a query such as Child or Descendent which links simple querys. + /// + internal abstract bool IsCombinator { get; } + + internal StyleQuery() { } + + /// + /// Tries to match the query with a control. + /// + /// The control. + /// + /// The parent container, if the container containing the query is a nested container. + /// + /// + /// Whether the match should subscribe to changes in order to track the match over time, + /// or simply return an imcontainerte result. + /// + /// + /// The name of container to query on. + /// + /// A . + internal virtual SelectorMatch Match(StyledElement control, IStyle? parent = null, bool subscribe = true, string? containerName = null) + { + // First match the query until a combinator is found. Selectors are stored from + // right-to-left, so MatchUntilCombinator reverses this order because the type query + // will be on the left. + var match = MatchUntilCombinator(control, this, parent, subscribe, out var combinator, containerName); + + // If the pre-combinator query matches, we can now match the combinator, if any. + if (match.IsMatch && combinator is object) + { + match = match.And(combinator.Match(control, parent, subscribe, containerName)); + + // If we have a combinator then we can never say that we always match a control of + // this type, because by definition the combinator matches on things outside of the + // control. + match = match.Result switch + { + SelectorMatchResult.AlwaysThisType => SelectorMatch.AlwaysThisInstance, + SelectorMatchResult.NeverThisType => SelectorMatch.NeverThisInstance, + _ => match + }; + } + + return match; + } + + public override string ToString() => ToString(null); + + /// + /// Gets a string representing the query, with the nesting separator (`^`) replaced with + /// the parent query. + /// + /// The owner container. + public abstract string ToString(ContainerQuery? owner); + + /// + /// Evaluates the query for a match. + /// + /// The control. + /// + /// The parent container, if the container containing the query is a nested container. + /// + /// + /// Whether the match should subscribe to changes in order to track the match over time, + /// or simply return an imcontainerte result. + /// + /// + /// The name of the container to evaluate. + /// + /// A . + internal abstract SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe, string? containerName); + + /// + /// Moves to the previous query. + /// + private protected abstract StyleQuery? MovePrevious(); + + /// + /// Moves to the previous query or the parent query. + /// + private protected abstract StyleQuery? MovePreviousOrParent(); + + private static SelectorMatch MatchUntilCombinator( + StyledElement control, + StyleQuery start, + IStyle? parent, + bool subscribe, + out StyleQuery? combinator, + string? containerName = null) + { + combinator = null; + + var activators = new AndActivatorBuilder(); + var result = Match(control, start, parent, subscribe, ref activators, ref combinator, containerName); + + return result == SelectorMatchResult.Sometimes ? + new SelectorMatch(activators.Get()) : + new SelectorMatch(result); + } + + private static SelectorMatchResult Match( + StyledElement control, + StyleQuery query, + IStyle? parent, + bool subscribe, + ref AndActivatorBuilder activators, + ref StyleQuery? combinator, + string? containerName) + { + var previous = query.MovePrevious(); + + // Selectors are stored from right-to-left, so we recurse into the query in order to + // reverse this order, because the type query will be on the left and is our best + // opportunity to exit early. + if (previous != null && !previous.IsCombinator) + { + var previousMatch = Match(control, previous, parent, subscribe, ref activators, ref combinator, containerName); + + if (previousMatch < SelectorMatchResult.Sometimes) + { + return previousMatch; + } + } + + // Match this query. + var match = query.Evaluate(control, parent, subscribe, containerName); + + if (!match.IsMatch) + { + combinator = null; + return match.Result; + } + else if (match.Activator is object) + { + activators.Add(match.Activator!); + } + + if (previous?.IsCombinator == true) + { + combinator = previous; + } + + return match.Result; + } + } +} + diff --git a/src/Avalonia.Base/Styling/StyleQueryComparisonOperator.cs b/src/Avalonia.Base/Styling/StyleQueryComparisonOperator.cs new file mode 100644 index 0000000000..c20ee62213 --- /dev/null +++ b/src/Avalonia.Base/Styling/StyleQueryComparisonOperator.cs @@ -0,0 +1,12 @@ +namespace Avalonia.Styling +{ + public enum StyleQueryComparisonOperator + { + None, + Equals, + LessThan, + GreaterThan, + LessThanOrEquals, + GreaterThanOrEquals, + } +} diff --git a/src/Avalonia.Base/Styling/ValueStyleQuery.cs b/src/Avalonia.Base/Styling/ValueStyleQuery.cs new file mode 100644 index 0000000000..7cea7dc769 --- /dev/null +++ b/src/Avalonia.Base/Styling/ValueStyleQuery.cs @@ -0,0 +1,31 @@ +using System; +using System.Linq; +using Avalonia.VisualTree; + +namespace Avalonia.Styling +{ + internal abstract class ValueStyleQuery : StyleQuery + { + private readonly StyleQuery? _previous; + private T _argument; + + internal ValueStyleQuery(StyleQuery? previous, T argument) + { + _previous = previous; + _argument = argument; + } + + protected T Argument => _argument; + + internal override bool IsCombinator => false; + + public override string ToString(ContainerQuery? owner) + { + throw new NotImplementedException(); + } + + private protected override StyleQuery? MovePrevious() => _previous; + + private protected override StyleQuery? MovePreviousOrParent() => _previous; + } +} diff --git a/src/Avalonia.Base/Utilities/IdentifierParser.cs b/src/Avalonia.Base/Utilities/IdentifierParser.cs index 76e6459e2e..f32d5f79a2 100644 --- a/src/Avalonia.Base/Utilities/IdentifierParser.cs +++ b/src/Avalonia.Base/Utilities/IdentifierParser.cs @@ -45,5 +45,16 @@ namespace Avalonia.Utilities cat == UnicodeCategory.DecimalDigitNumber; } } + + internal static ReadOnlySpan ParseNumber(this ref CharacterReader r) + { + return r.TakeWhile(c => IsValidNumberChar(c)); + } + + private static bool IsValidNumberChar(char c) + { + var cat = CharUnicodeInfo.GetUnicodeCategory(c); + return cat == UnicodeCategory.DecimalDigitNumber; + } } } diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index 1eda0b56ad..fd2c8e59e5 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -43,6 +43,9 @@ Markup/%(RecursiveDir)%(FileName)%(Extension) + + Markup/%(RecursiveDir)%(FileName)%(Extension) + Markup/%(RecursiveDir)%(FileName)%(Extension) @@ -83,6 +86,9 @@ Markup/%(RecursiveDir)%(FileName)%(Extension) + + Markup/%(RecursiveDir)%(FileName)%(Extension) + Markup/%(RecursiveDir)%(FileName)%(Extension) diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index dc82c50a39..b816858632 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -1,6 +1,3 @@ -using System; -using Avalonia.Collections; -using Avalonia.Controls.Shapes; using Avalonia.Controls.Utils; using Avalonia.Layout; using Avalonia.Media; diff --git a/src/Avalonia.Controls/ContentControl.cs b/src/Avalonia.Controls/ContentControl.cs index 4f50e3da7c..0f53608bd3 100644 --- a/src/Avalonia.Controls/ContentControl.cs +++ b/src/Avalonia.Controls/ContentControl.cs @@ -8,6 +8,8 @@ using Avalonia.Data; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Metadata; +using Avalonia.Platform; +using Avalonia.Styling; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/Decorator.cs b/src/Avalonia.Controls/Decorator.cs index 85485dc50d..e62ca0000b 100644 --- a/src/Avalonia.Controls/Decorator.cs +++ b/src/Avalonia.Controls/Decorator.cs @@ -1,5 +1,7 @@ using Avalonia.Layout; using Avalonia.Metadata; +using Avalonia.Platform; +using Avalonia.Styling; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index aacdb26667..3279315f9e 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -10,6 +10,8 @@ using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; +using Avalonia.Platform; +using Avalonia.Styling; using Avalonia.Utilities; namespace Avalonia.Controls.Presenters diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index b897be4fb2..9d95ea5737 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -157,6 +157,8 @@ namespace Avalonia.Controls static TopLevel() { KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(KeyboardNavigationMode.Cycle); + Avalonia.Styling.Container.SizingProperty.OverrideDefaultValue(ContainerSizing.WidthAndHeight); + AffectsMeasure(ClientSizeProperty); SystemBarColorProperty.Changed.AddClassHandler((view, e) => @@ -663,6 +665,7 @@ namespace Avalonia.Controls } private IDisposable? _insetsPaddings; + private void InvalidateChildInsetsPadding() { if (Content is Control child diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index af84cdafa8..8a4fbe50f1 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -4,7 +4,6 @@ using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Platform; -using Avalonia.Styling; namespace Avalonia.Controls { @@ -286,6 +285,8 @@ namespace Avalonia.Controls var constraint = LayoutHelper.ApplyLayoutConstraints(this, availableSize); + Avalonia.Styling.Container.GetQueryProvider(this)?.SetSize(constraint.Width, constraint.Height, Avalonia.Styling.Container.GetSizing(this)); + return MeasureOverride(constraint); } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 863fe5a16b..4fec398c95 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -56,6 +56,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions InsertBefore( new AvaloniaXamlIlControlThemeTransformer(), new AvaloniaXamlIlSelectorTransformer(), + new AvaloniaXamlIlQueryTransformer(), new AvaloniaXamlIlDuplicateSettersChecker(), new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), new AvaloniaXamlIlBindingPathParser(), diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs index e13e855087..f4dea1b200 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs @@ -104,7 +104,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { Style = 1, ControlTemplate, - Transitions + Transitions, + Container } public AvaloniaXamlIlTargetTypeMetadataNode(IXamlAstValueNode value, IXamlAstTypeReference targetType, diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlQueryTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlQueryTransformer.cs new file mode 100644 index 0000000000..f376934e16 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlQueryTransformer.cs @@ -0,0 +1,424 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Markup.Parsers; +using Avalonia.Styling; +using XamlX; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + using static Avalonia.Markup.Parsers.ContainerQueryGrammar; + using XamlLoadException = XamlX.XamlLoadException; + using XamlParseException = XamlX.XamlParseException; + class AvaloniaXamlIlQueryTransformer : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (node is not XamlAstObjectNode on || + !context.GetAvaloniaTypes().Container.IsAssignableFrom(on.Type.GetClrType())) + return node; + + var pn = on.Children.OfType() + .FirstOrDefault(p => p.Property.GetClrProperty().Name == "Query"); + + if (pn == null || pn.Property.GetClrProperty().Getter is not { } getter) + return node; + + if (pn.Values.Count != 1) + throw new XamlParseException("Query property should should have exactly one value", node); + + if (pn.Values[0] is XamlIlQueryNode) + //Deja vu. I've just been in this place before + return node; + + if (!(pn.Values[0] is XamlAstTextNode tn)) + throw new XamlParseException("Query property should be a text node", node); + + var queryType = getter.ReturnType; + var initialNode = new XamlIlQueryInitialNode(node, queryType); + var avaloniaAttachedPropertyT = context.GetAvaloniaTypes().AvaloniaAttachedPropertyT; + XamlIlQueryNode Create(IEnumerable syntax) + { + XamlIlQueryNode result = initialNode; + XamlIlOrQueryNode? results = null; + XamlIlAndQueryNode? andNode = null; + foreach (var i in syntax) + { + switch (i) + { + case ContainerQueryGrammar.WidthSyntax width: + result = new XamlIlWidthQuery(result, width); + break; + case ContainerQueryGrammar.HeightSyntax height: + result = new XamlIlHeightQuery(result, height); + break; + case ContainerQueryGrammar.OrSyntax or: + if (results == null) + results = new XamlIlOrQueryNode(node, queryType); + if (andNode != null && result == initialNode) + throw new XamlParseException($"Previously opened And node is not closed.", node); + results.Add(andNode ?? result); + result = initialNode; + andNode = null; + break; + case ContainerQueryGrammar.AndSyntax and: + if (andNode == null) + andNode = new XamlIlAndQueryNode(node, queryType); + andNode.Add(result); + result = initialNode; + break; + default: + throw new XamlParseException($"Unsupported query grammar '{i.GetType()}'.", node); + } + + if (andNode != null && result != initialNode) + { + andNode.Add(result); + } + } + + if (results != null && result != null) + { + results.Add(result); + } + + return results ?? result ?? initialNode; + } + + IEnumerable parsed; + try + { + parsed = ContainerQueryGrammar.Parse(tn.Text); + } + catch (Exception e) + { + throw new XamlParseException("Unable to parse query: " + e.Message, node); + } + + var query = Create(parsed); + pn.Values[0] = query; + + return new AvaloniaXamlIlTargetTypeMetadataNode(on, + new XamlAstClrTypeReference(query, query.TargetType!, false), + AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Container); + } + } + + abstract class XamlIlQueryNode : XamlAstNode, IXamlAstValueNode, IXamlAstEmitableNode + { + internal XamlIlQueryNode? Previous { get; } + public abstract IXamlType? TargetType { get; } + + public XamlIlQueryNode(XamlIlQueryNode? previous, + IXamlLineInfo? info = null, + IXamlType? queryType = null) : base((info ?? previous)!) + { + Previous = previous; + Type = queryType == null ? previous!.Type : new XamlAstClrTypeReference(this, queryType, false); + } + + public IXamlAstTypeReference Type { get; } + + public virtual XamlILNodeEmitResult Emit(XamlEmitContext context, IXamlILEmitter codeGen) + { + if (Previous != null) + context.Emit(Previous, codeGen, Type.GetClrType()); + DoEmit(context, codeGen); + return XamlILNodeEmitResult.Type(0, Type.GetClrType()); + } + + protected abstract void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen); + + protected void EmitCall(XamlEmitContext context, IXamlILEmitter codeGen, Func method) + { + var queries = context.Configuration.TypeSystem.GetType("Avalonia.Styling.StyleQueries"); + var found = queries.FindMethod(m => m.IsStatic && m.Parameters.Count > 0 && method(m)); + if(found == null) + throw new XamlTypeSystemException( + $"Unable to find {TargetType} in Avalonia.Styling.StyleQueries"); + codeGen.EmitCall(found); + } + } + + class XamlIlQueryInitialNode : XamlIlQueryNode + { + public XamlIlQueryInitialNode(IXamlLineInfo info, + IXamlType queryType) : base(null, info, queryType) + { + } + + public override IXamlType? TargetType => null; + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) => codeGen.Ldnull(); + } + + class XamlIlTypeQuery : XamlIlQueryNode + { + public bool Concrete { get; } + + public XamlIlTypeQuery(XamlIlQueryNode previous, IXamlType type, bool concrete) : base(previous) + { + TargetType = type; + Concrete = concrete; + } + + public override IXamlType TargetType { get; } + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) + { + var name = Concrete ? "OfType" : "Is"; + codeGen.Ldtype(TargetType); + EmitCall(context, codeGen, + m => m.Name == name && m.Parameters.Count == 2 && m.Parameters[1].FullName == "System.Type"); + } + } + + class XamlIlStringQuery : XamlIlQueryNode + { + public string String { get; set; } + public enum QueryType + { + Class, + Name + } + + private QueryType _type; + + public XamlIlStringQuery(XamlIlQueryNode previous, QueryType type, string s) : base(previous) + { + _type = type; + String = s; + } + + public override IXamlType? TargetType => Previous?.TargetType; + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.Ldstr(String); + var name = _type.ToString(); + EmitCall(context, codeGen, + m => m.Name == name && m.Parameters.Count == 2 && m.Parameters[1].FullName == "System.String"); + } + } + + class XamlIlCombinatorQuery : XamlIlQueryNode + { + private readonly CombinatorQueryType _type; + + public enum CombinatorQueryType + { + Child, + Descendant, + Template + } + public XamlIlCombinatorQuery(XamlIlQueryNode previous, CombinatorQueryType type) : base(previous) + { + _type = type; + } + + public CombinatorQueryType QueryType => _type; + public override IXamlType? TargetType => null; + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) + { + var name = _type.ToString(); + EmitCall(context, codeGen, + m => m.Name == name && m.Parameters.Count == 1); + } + } + + class XamlIlWidthQuery : XamlIlQueryNode + { + private ContainerQueryGrammar.WidthSyntax _argument; + + public XamlIlWidthQuery(XamlIlQueryNode previous, ContainerQueryGrammar.WidthSyntax argument) : base(previous) + { + _argument = argument; + } + + public override IXamlType? TargetType => Previous?.TargetType; + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.Ldc_I4((int)_argument.Operator); + codeGen.Ldc_R8(_argument.Value); + EmitCall(context, codeGen, + m => m.Name == "Width" && m.Parameters.Count == 3); + } + } + + class XamlIlHeightQuery : XamlIlQueryNode + { + private ContainerQueryGrammar.HeightSyntax _argument; + + public XamlIlHeightQuery(XamlIlQueryNode previous, ContainerQueryGrammar.HeightSyntax argument) : base(previous) + { + _argument = argument; + } + + public override IXamlType? TargetType => Previous?.TargetType; + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.Ldc_I4((int)_argument.Operator); + codeGen.Ldc_R8(_argument.Value); + EmitCall(context, codeGen, + m => m.Name == "Height" && m.Parameters.Count == 3); + } + } + + class XamlIlOrQueryNode : XamlIlQueryNode + { + List _queries = new List(); + public XamlIlOrQueryNode(IXamlLineInfo info, IXamlType queryType) : base(null, info, queryType) + { + } + + public void Add(XamlIlQueryNode node) + { + _queries.Add(node); + } + + public override IXamlType? TargetType + { + get + { + IXamlType? result = null; + + foreach (var query in _queries) + { + if (query.TargetType == null) + { + return null; + } + else if (result == null) + { + result = query.TargetType; + } + else + { + while (result?.IsAssignableFrom(query.TargetType) == false) + { + result = result.BaseType; + } + } + } + + return result; + } + } + + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) + { + if (_queries.Count == 0) + throw new XamlLoadException("Invalid query count", this); + if (_queries.Count == 1) + { + _queries[0].Emit(context, codeGen); + return; + } + + if (context.Configuration.TypeSystem.FindType("System.Collections.Generic.List`1") is not { } type) + return; + + IXamlType listType = type.MakeGenericType(base.Type.GetClrType()); + var add = listType.FindMethod("Add", context.Configuration.WellKnownTypes.Void, false, Type.GetClrType()); + if (add == null) + return; + + var ctor = listType.FindConstructor(); + if (ctor == null) + { + return; + } + codeGen + .Newobj(ctor); + foreach (var s in _queries) + { + codeGen.Dup(); + context.Emit(s, codeGen, Type.GetClrType()); + codeGen.EmitCall(add, true); + } + + EmitCall(context, codeGen, + m => m.Name == "Or" && m.Parameters.Count == 1 && m.Parameters[0].Name.StartsWith("IReadOnlyList")); + } + } + + class XamlIlAndQueryNode : XamlIlQueryNode + { + List _queries = new List(); + public XamlIlAndQueryNode(IXamlLineInfo info, IXamlType queryType) : base(null, info, queryType) + { + } + + public void Add(XamlIlQueryNode node) + { + _queries.Add(node); + } + + public override IXamlType? TargetType + { + get + { + IXamlType? result = null; + + foreach (var query in _queries) + { + if (query.TargetType == null) + { + return null; + } + else if (result == null) + { + result = query.TargetType; + } + else + { + while (result?.IsAssignableFrom(query.TargetType) == false) + { + result = result.BaseType; + } + } + } + + return result; + } + } + + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) + { + if (_queries.Count == 0) + throw new XamlLoadException("Invalid query count", this); + if (_queries.Count == 1) + { + _queries[0].Emit(context, codeGen); + return; + } + + if (context.Configuration.TypeSystem.FindType("System.Collections.Generic.List`1") is not { } type) + return; + + IXamlType listType = type.MakeGenericType(base.Type.GetClrType()); + var add = listType.FindMethod("Add", context.Configuration.WellKnownTypes.Void, false, Type.GetClrType()); + if (add == null) + return; + + var ctor = listType.FindConstructor(); + if (ctor == null) + { + return; + } + codeGen + .Newobj(ctor); + foreach (var s in _queries) + { + codeGen.Dup(); + context.Emit(s, codeGen, Type.GetClrType()); + codeGen.EmitCall(add, true); + } + + EmitCall(context, codeGen, + m => m.Name == "And" && m.Parameters.Count == 1 && m.Parameters[0].Name.StartsWith("IReadOnlyList")); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index b5efb5ca31..32c09b5cc7 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -127,6 +127,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType UriKind { get; } public IXamlConstructor UriConstructor { get; } public IXamlType Style { get; } + public IXamlType Container { get; } public IXamlType Styles { get; } public IXamlType ControlTheme { get; } public IXamlType WindowTransparencyLevel { get; } @@ -328,6 +329,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers UriKind = cfg.TypeSystem.GetType("System.UriKind"); UriConstructor = Uri.GetConstructor(new List() { cfg.WellKnownTypes.String, UriKind }); Style = cfg.TypeSystem.GetType("Avalonia.Styling.Style"); + Container = cfg.TypeSystem.GetType("Avalonia.Styling.ContainerQuery"); Styles = cfg.TypeSystem.GetType("Avalonia.Styling.Styles"); ControlTheme = cfg.TypeSystem.GetType("Avalonia.Styling.ControlTheme"); ControlTemplate = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Templates.ControlTemplate"); diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ContainerQueryGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ContainerQueryGrammar.cs new file mode 100644 index 0000000000..b12a070a33 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ContainerQueryGrammar.cs @@ -0,0 +1,267 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Data.Core; +using Avalonia.Styling; +using Avalonia.Utilities; + +// Don't need to override GetHashCode as the ISyntax objects will not be stored in a hash; the +// only reason they have overridden Equals methods is for unit testing. +#pragma warning disable 659 + +namespace Avalonia.Markup.Parsers +{ + internal static class ContainerQueryGrammar + { + const string MinWidthKeyword = "min-width"; + const string MinHeightKeyword = "min-height"; + const string MaxWidthKeyword = "max-width"; + const string MaxHeightKeyword = "max-height"; + const string WidthKeyword = "width"; + const string HeightKeyword = "height"; + static string[] Keywords = new string[] { MinWidthKeyword, MinHeightKeyword, MaxWidthKeyword, MaxHeightKeyword, WidthKeyword, HeightKeyword }; + + private enum State + { + Start, + Middle, + End, + } + + public static IEnumerable Parse(string s) + { + var r = new CharacterReader(s.AsSpan()); + return Parse(ref r, null); + } + + private static IEnumerable Parse(ref CharacterReader r, char? end) + { + var state = State.Start; + var selector = new List(); + while (!r.End && state != State.End) + { + ISyntax? syntax = null; + switch (state) + { + case State.Start: + (state, syntax) = ParseStart(ref r); + break; + case State.Middle: + (state, syntax) = ParseMiddle(ref r, end); + break; + } + if (syntax != null) + { + selector.Add(syntax); + } + } + + if (state != State.Start && state != State.Middle && state != State.End) + { + throw new ExpressionParseException(r.Position, "Unexpected end of selector"); + } + + return selector; + } + + private static ISyntax? ParseFeature(ref CharacterReader r) + { + r.SkipWhitespace(); + + var identifier = r.ParseStyleClass(); + + if (identifier.IsEmpty) + { + throw new ExpressionParseException(r.Position, "Expected query feature name."); + } + + var s = identifier.ToString(); + if (!Keywords.Any(x => s == x)) + { + throw new InvalidOperationException($"Unknown feature name found: {identifier.ToString()}"); + } + + if (identifier.SequenceEqual(MinWidthKeyword.AsSpan()) || identifier.SequenceEqual(MaxWidthKeyword.AsSpan()) || identifier.SequenceEqual(WidthKeyword.AsSpan())) + { + if (!r.TakeIf(':')) + throw new ExpressionParseException(r.Position, "Expected ':' after 'orientation'."); + double val = ParseDecimal(ref r); + + var syntax = new WidthSyntax() + { + Value = val, + Operator = identifier.SequenceEqual(WidthKeyword.AsSpan()) ? StyleQueryComparisonOperator.Equals + : identifier.SequenceEqual(MinWidthKeyword.AsSpan()) ? StyleQueryComparisonOperator.GreaterThanOrEquals + : StyleQueryComparisonOperator.LessThanOrEquals + }; + + return syntax; + } + + if (identifier.SequenceEqual(MinHeightKeyword.AsSpan()) || identifier.SequenceEqual(MaxHeightKeyword.AsSpan()) || identifier.SequenceEqual(HeightKeyword.AsSpan())) + { + if (!r.TakeIf(':')) + throw new ExpressionParseException(r.Position, "Expected ':' after 'orientation'."); + double val = ParseDecimal(ref r); + + var syntax = new HeightSyntax() + { + Value = val, + Operator = identifier.SequenceEqual(WidthKeyword.AsSpan()) ? StyleQueryComparisonOperator.Equals + : identifier.SequenceEqual(MinHeightKeyword.AsSpan()) ? StyleQueryComparisonOperator.GreaterThanOrEquals + : StyleQueryComparisonOperator.LessThanOrEquals + }; + + return syntax; + } + + return null; + } + + private static (State, ISyntax?) ParseStart(ref CharacterReader r) + { + r.SkipWhitespace(); + if (r.End) + { + return (State.End, null); + } + + if(char.IsLetter(r.Peek)) + { + return (State.Middle, ParseFeature(ref r)); + } + + throw new InvalidOperationException("Invalid syntax found"); + } + + private static (State, ISyntax?) ParseMiddle(ref CharacterReader r, char? end) + { + r.SkipWhitespace(); + + if (r.TakeIf(',')) + { + return (State.Start, new OrSyntax()); + } + else if (end.HasValue && !r.End && r.Peek == end.Value) + { + return (State.End, null); + } + else + { + var identifier = r.TakeWhile(c => !char.IsWhiteSpace(c)); + + if (identifier.SequenceEqual("and".AsSpan())) + { + return (State.Start, new AndSyntax()); + } + } + throw new InvalidOperationException("Invalid syntax found"); + } + + private static double ParseDecimal(ref CharacterReader r) + { + r.SkipWhitespace(); + var number = r.ParseNumber(); + if (number.IsEmpty) + { + throw new ExpressionParseException(r.Position, $"Expected a number after."); + } + + return double.Parse(number.ToString()); + } + + private static StyleQueryComparisonOperator ParseOperator(ref CharacterReader r) + { + r.SkipWhitespace(); + var queryOperator = r.TakeWhile(x => !char.IsWhiteSpace(x)); + + return queryOperator.ToString() switch + { + "=" => StyleQueryComparisonOperator.Equals, + "<" => StyleQueryComparisonOperator.LessThan, + ">" => StyleQueryComparisonOperator.GreaterThan, + "<=" => StyleQueryComparisonOperator.LessThanOrEquals, + ">=" => StyleQueryComparisonOperator.GreaterThanOrEquals, + "" => StyleQueryComparisonOperator.None, + _ => throw new ExpressionParseException(r.Position, $"Expected a comparison operator after.") + }; + } + + private static T ParseEnum(ref CharacterReader r) where T: struct + { + var identifier = r.ParseIdentifier(); + + if (Enum.TryParse(identifier.ToString(), true, out T value)) + return value; + + throw new ExpressionParseException(r.Position, $"Expected a {typeof(T)} after."); + } + + private static string ParseString(ref CharacterReader r) + { + return r.ParseIdentifier().ToString(); + } + + private static void Expect(ref CharacterReader r, char c) + { + if (r.End) + { + throw new ExpressionParseException(r.Position, $"Expected '{c}', got end of selector."); + } + else if (!r.TakeIf(')')) + { + throw new ExpressionParseException(r.Position, $"Expected '{c}', got '{r.Peek}'."); + } + } + + public class OrSyntax : ISyntax + { + public override bool Equals(object? obj) + { + return obj is OrSyntax; + } + } + + public class AndSyntax : ISyntax + { + public override bool Equals(object? obj) + { + return obj is AndSyntax; + } + } + + public abstract class QuerySyntax : ISyntax + { + public T? Value { get; set; } + public StyleQueryComparisonOperator Operator { get; set; } + } + + public abstract class RangeSyntax : QuerySyntax + { + + } + + public class WidthSyntax : RangeSyntax + { + public override bool Equals(object? obj) + { + return (obj is WidthSyntax width) && width.Value == Value + && width.Operator == Operator; + } + } + + public class HeightSyntax : RangeSyntax + { + public override bool Equals(object? obj) + { + return (obj is HeightSyntax height) && height.Value == Value + && height.Operator == Operator; + } + } + + public interface ISyntax + { + + } + } +} diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ContainerQueryParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ContainerQueryParser.cs new file mode 100644 index 0000000000..6eb83745ee --- /dev/null +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ContainerQueryParser.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Styling; +using static Avalonia.Markup.Parsers.ContainerQueryGrammar; + +namespace Avalonia.Markup.Parsers +{ + /// + /// Parses a from text. + /// + internal class ContainerQueryParser + { + /// + /// Initializes a new instance of the class. + /// + public ContainerQueryParser() + { + } + + /// + /// Parses a from a string. + /// + /// The string. + /// The parsed selector. + [RequiresUnreferencedCode(TrimmingMessages.SelectorsParseRequiresUnreferencedCodeMessage)] + public StyleQuery? Parse(string s) + { + var syntax = ContainerQueryGrammar.Parse(s); + return Create(syntax); + } + + [RequiresUnreferencedCode(TrimmingMessages.SelectorsParseRequiresUnreferencedCodeMessage)] + private StyleQuery? Create(IEnumerable syntax) + { + var result = default(StyleQuery); + var results = default(List); + + foreach (var i in syntax) + { + switch (i) + { + case ContainerQueryGrammar.WidthSyntax width: + result = result.Width(width.Operator, width.Value); + break; + case ContainerQueryGrammar.HeightSyntax height: + result = result.Height(height.Operator, height.Value); + break; + case ContainerQueryGrammar.OrSyntax or: + case ContainerQueryGrammar.AndSyntax and: + if (results == null) + { + results = new List(); + } + + results.Add(result ?? throw new NotSupportedException("Invalid query!")); + result = null; + break; + default: + throw new NotSupportedException($"Unsupported selector grammar '{i.GetType()}'."); + } + } + + if (results != null) + { + if (result != null) + { + results.Add(result); + } + + result = results.Count > 1 ? StyleQueries.Or(results) : results[0]; + } + + return result; + } + } +} diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index f232d5e109..6088d7177c 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -275,6 +275,32 @@ namespace Avalonia.Markup.Parsers return (State.CanHaveType, new NameSyntax { Name = name.ToString() }); } + private static double ParseDecimal(ref CharacterReader r) + { + var number = r.ParseNumber(); + if (number.IsEmpty) + { + throw new ExpressionParseException(r.Position, $"Expected a number after."); + } + + return double.Parse(number.ToString()); + } + + private static T ParseEnum(ref CharacterReader r) where T : struct + { + var identifier = r.ParseIdentifier(); + + if (Enum.TryParse(identifier.ToString(), true, out T value)) + return value; + + throw new ExpressionParseException(r.Position, $"Expected a {typeof(T)} after."); + } + + private static string ParseString(ref CharacterReader r) + { + return r.ParseIdentifier().ToString(); + } + private static (State, ISyntax) ParseTypeName(ref CharacterReader r) { return (State.CanHaveType, ParseType(ref r, new OfTypeSyntax())); @@ -651,5 +677,15 @@ namespace Avalonia.Markup.Parsers return obj is NestingSyntax; } } + + public class DecimalSyntax : ISyntax + { + public decimal Number { get; set; } + + public override bool Equals(object? obj) + { + return obj is DecimalSyntax dec && dec.Number == Number; + } + } } } diff --git a/tests/Avalonia.Base.UnitTests/Styling/ContainerTests.cs b/tests/Avalonia.Base.UnitTests/Styling/ContainerTests.cs new file mode 100644 index 0000000000..e3949a8788 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Styling/ContainerTests.cs @@ -0,0 +1,164 @@ +using System; +using Avalonia.Base.UnitTests.Layout; +using Avalonia.Controls; +using Avalonia.Styling; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Base.UnitTests.Styling +{ + public class ContainerTests + { + [Fact] + public void Container_Cannot_Be_Added_To_Style_Children() + { + var target = new ContainerQuery(); + var style = new Style(); + + Assert.Throws(() => style.Children.Add(target)); + } + + [Fact] + public void Container_Width_Queries_Matches() + { + using var app = UnitTestApplication.Start(); + var root = new LayoutTestRoot() + { + ClientSize = new Size(400, 400) + }; + var containerQuery1 = new ContainerQuery(x => new WidthQuery(x, StyleQueryComparisonOperator.LessThanOrEquals, 500)); + containerQuery1.Children.Add(new Style(x => x.Is()) + { + Setters = { new Setter(Control.WidthProperty, 200.0) } + }); + var containerQuery2 = new ContainerQuery(x => new WidthQuery(x, StyleQueryComparisonOperator.GreaterThan, 500)); + containerQuery2.Children.Add(new Style(x => x.Is()) + { + Setters = { new Setter(Control.WidthProperty, 500.0) } + }); + root.Styles.Add(containerQuery1); + root.Styles.Add(containerQuery2); + var child = new Border() + { + Name = "Child", + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Stretch + }; + var border = new Border() + { + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Stretch, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Stretch, + Child = child, + Name = "Parent" + }; + Container.SetSizing(border, Avalonia.Styling.ContainerSizing.Width); + + root.Child = border; + + root.LayoutManager.ExecuteInitialLayoutPass(); + Assert.Equal(child.Width, 200.0); + + root.ClientSize = new Size(600, 600); + root.InvalidateMeasure(); + + root.LayoutManager.ExecuteLayoutPass(); + Assert.Equal(child.Width, 500.0); + } + + [Fact] + public void Container_Height_Queries_Matches() + { + using var app = UnitTestApplication.Start(); + var root = new LayoutTestRoot() + { + ClientSize = new Size(400, 400) + }; + var containerQuery1 = new ContainerQuery(x => new HeightQuery(x, StyleQueryComparisonOperator.LessThanOrEquals, 500)); + containerQuery1.Children.Add(new Style(x => x.Is()) + { + Setters = { new Setter(Control.HeightProperty, 200.0) } + }); + var containerQuery2 = new ContainerQuery(x => new HeightQuery(x, StyleQueryComparisonOperator.GreaterThan, 500)); + containerQuery2.Children.Add(new Style(x => x.Is()) + { + Setters = { new Setter(Control.HeightProperty, 500.0) } + }); + root.Styles.Add(containerQuery1); + root.Styles.Add(containerQuery2); + var child = new Border() + { + Name = "Child", + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Stretch + }; + var border = new Border() + { + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Stretch, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Stretch, + Child = child, + Name = "Parent" + }; + Container.SetSizing(border, Avalonia.Styling.ContainerSizing.Height); + + root.Child = border; + + root.LayoutManager.ExecuteInitialLayoutPass(); + Assert.Equal(child.Height, 200.0); + + root.ClientSize = new Size(600, 600); + root.InvalidateMeasure(); + + root.LayoutManager.ExecuteLayoutPass(); + Assert.Equal(child.Height, 500.0); + } + + [Fact] + public void Container_Queries_Matches_Name() + { + using var app = UnitTestApplication.Start(); + var root = new LayoutTestRoot() + { + ClientSize = new Size(600, 600) + }; + var containerQuery1 = new ContainerQuery(x => new WidthQuery(x, StyleQueryComparisonOperator.LessThanOrEquals, 500)); + containerQuery1.Children.Add(new Style(x => x.Is()) + { + Setters = { new Setter(Control.WidthProperty, 200.0) } + }); + var containerQuery2 = new ContainerQuery(x => new WidthQuery(x, StyleQueryComparisonOperator.LessThanOrEquals, 500), "TEST"); + containerQuery2.Children.Add(new Style(x => x.Is()) + { + Setters = { new Setter(Control.WidthProperty, 300.0) } + }); + root.Styles.Add(containerQuery2); + root.Styles.Add(containerQuery1); + var child = new Border() + { + Name = "Child", + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Stretch + }; + var controlInner = new ContentControl() + { + Width = 400, + Height = 400, + Content = child, + Name = "Inner" + }; + Container.SetSizing(controlInner, Avalonia.Styling.ContainerSizing.Width); + Container.SetName(controlInner, "TEST"); + var border = new Border() + { + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Stretch, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Stretch, + Child = controlInner, + Name = "Parent" + }; + Container.SetSizing(border, Avalonia.Styling.ContainerSizing.Width); + + root.Child = border; + + root.LayoutManager.ExecuteInitialLayoutPass(); + + root.LayoutManager.ExecuteLayoutPass(); + Assert.Equal(child.Width, 300.0); + } + } +}