Browse Source

Merge pull request #6381 from AvaloniaUI/feature/nth-child

NthChild and NthLastChild selectors support
pull/6783/head
Jumar Macato 5 years ago
committed by GitHub
parent
commit
43c04c2266
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 24
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
  2. 11
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  3. 43
      src/Avalonia.Controls/ItemsControl.cs
  4. 24
      src/Avalonia.Controls/Panel.cs
  5. 41
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  6. 31
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  7. 27
      src/Avalonia.Controls/Utils/IEnumerableUtils.cs
  8. 26
      src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs
  9. 32
      src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs
  10. 56
      src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs
  11. 145
      src/Avalonia.Styling/Styling/NthChildSelector.cs
  12. 23
      src/Avalonia.Styling/Styling/NthLastChildSelector.cs
  13. 16
      src/Avalonia.Styling/Styling/Selectors.cs
  14. 35
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs
  15. 149
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
  16. 6
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
  17. 159
      tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs
  18. 197
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
  19. 291
      tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs
  20. 220
      tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs
  21. 7
      tests/Avalonia.Styling.UnitTests/StyleActivatorExtensions.cs

24
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml

@ -1,17 +1,33 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.ItemsRepeaterPage">
<UserControl.Styles>
<Style Selector="ItemsRepeater TextBlock.oddTemplate">
<Setter Property="Background" Value="Yellow" />
<Setter Property="Foreground" Value="Black" />
</Style>
<Style Selector="ItemsRepeater TextBlock.evenTemplate">
<Setter Property="Background" Value="Wheat" />
<Setter Property="Foreground" Value="Black" />
</Style>
<Style Selector="ItemsRepeater TextBlock:nth-child(5n+3)">
<Setter Property="Foreground" Value="Red" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
<Style Selector="ItemsRepeater TextBlock:nth-last-child(5n+4)">
<Setter Property="Foreground" Value="Blue" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
</UserControl.Styles>
<UserControl.Resources>
<RecyclePool x:Key="RecyclePool" />
<DataTemplate x:Key="odd">
<TextBlock Background="Yellow"
Foreground="Black"
<TextBlock Classes="oddTemplate"
Height="{Binding Height}"
Text="{Binding Text}"/>
</DataTemplate>
<DataTemplate x:Key="even">
<TextBlock Background="Wheat"
Foreground="Black"
<TextBlock Classes="evenTemplate"
Height="{Binding Height}"
Text="{Binding Text}"/>
</DataTemplate>

11
samples/ControlCatalog/Pages/ListBoxPage.xaml

@ -2,9 +2,20 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.ListBoxPage">
<DockPanel>
<DockPanel.Styles>
<Style Selector="ListBox ListBoxItem:nth-child(5n+3)">
<Setter Property="Foreground" Value="Red" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
<Style Selector="ListBox ListBoxItem:nth-last-child(5n+4)">
<Setter Property="Foreground" Value="Blue" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
</DockPanel.Styles>
<StackPanel DockPanel.Dock="Top" Margin="4">
<TextBlock Classes="h1">ListBox</TextBlock>
<TextBlock Classes="h2">Hosts a collection of ListBoxItem.</TextBlock>
<TextBlock Classes="h2">Each 5th item is highlighted with nth-child(5n+3) and nth-last-child(5n+4) rules.</TextBlock>
</StackPanel>
<StackPanel DockPanel.Dock="Right" Margin="4">
<CheckBox IsChecked="{Binding Multiple}">Multiple</CheckBox>

43
src/Avalonia.Controls/ItemsControl.cs

@ -21,7 +21,7 @@ namespace Avalonia.Controls
/// Displays a collection of items.
/// </summary>
[PseudoClasses(":empty", ":singleitem")]
public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener
public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener, IChildIndexProvider
{
/// <summary>
/// The default value for the <see cref="ItemsPanel"/> property.
@ -56,6 +56,7 @@ namespace Avalonia.Controls
private IEnumerable _items = new AvaloniaList<object>();
private int _itemCount;
private IItemContainerGenerator _itemContainerGenerator;
private EventHandler<ChildIndexChangedEventArgs> _childIndexChanged;
/// <summary>
/// Initializes static members of the <see cref="ItemsControl"/> class.
@ -145,11 +146,28 @@ namespace Avalonia.Controls
protected set;
}
event EventHandler<ChildIndexChangedEventArgs> IChildIndexProvider.ChildIndexChanged
{
add => _childIndexChanged += value;
remove => _childIndexChanged -= value;
}
/// <inheritdoc/>
void IItemsPresenterHost.RegisterItemsPresenter(IItemsPresenter presenter)
{
if (Presenter is IChildIndexProvider oldInnerProvider)
{
oldInnerProvider.ChildIndexChanged -= PresenterChildIndexChanged;
}
Presenter = presenter;
ItemContainerGenerator.Clear();
if (Presenter is IChildIndexProvider innerProvider)
{
innerProvider.ChildIndexChanged += PresenterChildIndexChanged;
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs());
}
}
void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
@ -506,5 +524,28 @@ namespace Avalonia.Controls
return null;
}
private void PresenterChildIndexChanged(object sender, ChildIndexChangedEventArgs e)
{
_childIndexChanged?.Invoke(this, e);
}
int IChildIndexProvider.GetChildIndex(ILogical child)
{
return Presenter is IChildIndexProvider innerProvider
? innerProvider.GetChildIndex(child) : -1;
}
bool IChildIndexProvider.TryGetTotalCount(out int count)
{
if (Presenter is IChildIndexProvider presenter
&& presenter.TryGetTotalCount(out count))
{
return true;
}
count = ItemCount;
return true;
}
}
}

24
src/Avalonia.Controls/Panel.cs

@ -2,8 +2,10 @@ using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Styling;
namespace Avalonia.Controls
{
@ -14,7 +16,7 @@ namespace Avalonia.Controls
/// Controls can be added to a <see cref="Panel"/> by adding them to its <see cref="Children"/>
/// collection. All children are layed out to fill the panel.
/// </remarks>
public class Panel : Control, IPanel
public class Panel : Control, IPanel, IChildIndexProvider
{
/// <summary>
/// Defines the <see cref="Background"/> property.
@ -30,6 +32,8 @@ namespace Avalonia.Controls
AffectsRender<Panel>(BackgroundProperty);
}
private EventHandler<ChildIndexChangedEventArgs> _childIndexChanged;
/// <summary>
/// Initializes a new instance of the <see cref="Panel"/> class.
/// </summary>
@ -53,6 +57,12 @@ namespace Avalonia.Controls
set { SetValue(BackgroundProperty, value); }
}
event EventHandler<ChildIndexChangedEventArgs> IChildIndexProvider.ChildIndexChanged
{
add => _childIndexChanged += value;
remove => _childIndexChanged -= value;
}
/// <summary>
/// Renders the visual to a <see cref="DrawingContext"/>.
/// </summary>
@ -137,6 +147,7 @@ namespace Avalonia.Controls
throw new NotSupportedException();
}
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs());
InvalidateMeasureOnChildrenChanged();
}
@ -160,5 +171,16 @@ namespace Avalonia.Controls
var panel = control?.VisualParent as TPanel;
panel?.InvalidateMeasure();
}
int IChildIndexProvider.GetChildIndex(ILogical child)
{
return child is IControl control ? Children.IndexOf(control) : -1;
}
public bool TryGetTotalCount(out int count)
{
count = Children.Count;
return true;
}
}
}

41
src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs

@ -5,6 +5,7 @@ using Avalonia.Collections;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils;
using Avalonia.LogicalTree;
using Avalonia.Styling;
namespace Avalonia.Controls.Presenters
@ -12,7 +13,7 @@ namespace Avalonia.Controls.Presenters
/// <summary>
/// Base class for controls that present items inside an <see cref="ItemsControl"/>.
/// </summary>
public abstract class ItemsPresenterBase : Control, IItemsPresenter, ITemplatedControl
public abstract class ItemsPresenterBase : Control, IItemsPresenter, ITemplatedControl, IChildIndexProvider
{
/// <summary>
/// Defines the <see cref="Items"/> property.
@ -36,6 +37,7 @@ namespace Avalonia.Controls.Presenters
private IDisposable _itemsSubscription;
private bool _createdPanel;
private IItemContainerGenerator _generator;
private EventHandler<ChildIndexChangedEventArgs> _childIndexChanged;
/// <summary>
/// Initializes static members of the <see cref="ItemsPresenter"/> class.
@ -129,6 +131,12 @@ namespace Avalonia.Controls.Presenters
protected bool IsHosted => TemplatedParent is IItemsPresenterHost;
event EventHandler<ChildIndexChangedEventArgs> IChildIndexProvider.ChildIndexChanged
{
add => _childIndexChanged += value;
remove => _childIndexChanged -= value;
}
/// <inheritdoc/>
public override sealed void ApplyTemplate()
{
@ -149,6 +157,8 @@ namespace Avalonia.Controls.Presenters
if (Panel != null)
{
ItemsChanged(e);
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs());
}
}
@ -169,9 +179,21 @@ namespace Avalonia.Controls.Presenters
result.ItemTemplate = ItemTemplate;
}
result.Materialized += ContainerActionHandler;
result.Dematerialized += ContainerActionHandler;
result.Recycled += ContainerActionHandler;
return result;
}
private void ContainerActionHandler(object sender, ItemContainerEventArgs e)
{
for (var i = 0; i < e.Containers.Count; i++)
{
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(e.Containers[i].ContainerControl));
}
}
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
{
@ -248,5 +270,22 @@ namespace Avalonia.Controls.Presenters
{
(e.NewValue as IItemsPresenterHost)?.RegisterItemsPresenter(this);
}
int IChildIndexProvider.GetChildIndex(ILogical child)
{
if (child is IControl control && ItemContainerGenerator is { } generator)
{
var index = ItemContainerGenerator.IndexFromContainer(control);
return index;
}
return -1;
}
bool IChildIndexProvider.TryGetTotalCount(out int count)
{
return Items.TryGetCountFast(out count);
}
}
}

31
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@ -10,6 +10,7 @@ using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Logging;
using Avalonia.LogicalTree;
using Avalonia.Utilities;
using Avalonia.VisualTree;
@ -19,7 +20,7 @@ namespace Avalonia.Controls
/// Represents a data-driven collection control that incorporates a flexible layout system,
/// custom views, and virtualization.
/// </summary>
public class ItemsRepeater : Panel
public class ItemsRepeater : Panel, IChildIndexProvider
{
/// <summary>
/// Defines the <see cref="HorizontalCacheLength"/> property.
@ -61,8 +62,9 @@ namespace Avalonia.Controls
private readonly ViewportManager _viewportManager;
private IEnumerable _items;
private VirtualizingLayoutContext _layoutContext;
private NotifyCollectionChangedEventArgs _processingItemsSourceChange;
private EventHandler<ChildIndexChangedEventArgs> _childIndexChanged;
private bool _isLayoutInProgress;
private NotifyCollectionChangedEventArgs _processingItemsSourceChange;
private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs;
private ItemsRepeaterElementClearingEventArgs _elementClearingArgs;
private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs;
@ -163,6 +165,25 @@ namespace Avalonia.Controls
}
}
event EventHandler<ChildIndexChangedEventArgs> IChildIndexProvider.ChildIndexChanged
{
add => _childIndexChanged += value;
remove => _childIndexChanged -= value;
}
int IChildIndexProvider.GetChildIndex(ILogical child)
{
return child is IControl control
? GetElementIndex(control)
: -1;
}
bool IChildIndexProvider.TryGetTotalCount(out int count)
{
count = ItemsSourceView.Count;
return true;
}
/// <summary>
/// Occurs each time an element is cleared and made available to be re-used.
/// </summary>
@ -545,6 +566,8 @@ namespace Avalonia.Controls
ElementPrepared(this, _elementPreparedArgs);
}
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element));
}
internal void OnElementClearing(IControl element)
@ -562,6 +585,8 @@ namespace Avalonia.Controls
ElementClearing(this, _elementClearingArgs);
}
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element));
}
internal void OnElementIndexChanged(IControl element, int oldIndex, int newIndex)
@ -579,6 +604,8 @@ namespace Avalonia.Controls
ElementIndexChanged(this, _elementIndexChangedArgs);
}
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element));
}
private void OnDataSourcePropertyChanged(ItemsSourceView oldValue, ItemsSourceView newValue)

27
src/Avalonia.Controls/Utils/IEnumerableUtils.cs

@ -12,23 +12,36 @@ namespace Avalonia.Controls.Utils
return items.IndexOf(item) != -1;
}
public static int Count(this IEnumerable items)
public static bool TryGetCountFast(this IEnumerable items, out int count)
{
if (items != null)
{
if (items is ICollection collection)
{
return collection.Count;
count = collection.Count;
return true;
}
else if (items is IReadOnlyCollection<object> readOnly)
{
return readOnly.Count;
}
else
{
return Enumerable.Count(items.Cast<object>());
count = readOnly.Count;
return true;
}
}
count = 0;
return false;
}
public static int Count(this IEnumerable items)
{
if (TryGetCountFast(items, out var count))
{
return count;
}
else if (items != null)
{
return Enumerable.Count(items.Cast<object>());
}
else
{
return 0;

26
src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs

@ -0,0 +1,26 @@
#nullable enable
using System;
namespace Avalonia.LogicalTree
{
/// <summary>
/// Event args for <see cref="IChildIndexProvider.ChildIndexChanged"/> event.
/// </summary>
public class ChildIndexChangedEventArgs : EventArgs
{
public ChildIndexChangedEventArgs()
{
}
public ChildIndexChangedEventArgs(ILogical child)
{
Child = child;
}
/// <summary>
/// Logical child which index was changed.
/// If null, all children should be reset.
/// </summary>
public ILogical? Child { get; }
}
}

32
src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs

@ -0,0 +1,32 @@
#nullable enable
using System;
namespace Avalonia.LogicalTree
{
/// <summary>
/// Child's index and total count information provider used by list-controls (ListBox, StackPanel, etc.)
/// </summary>
/// <remarks>
/// Used by nth-child and nth-last-child selectors.
/// </remarks>
public interface IChildIndexProvider
{
/// <summary>
/// Gets child's actual index in order of the original source.
/// </summary>
/// <param name="child">Logical child.</param>
/// <returns>Index or -1 if child was not found.</returns>
int GetChildIndex(ILogical child);
/// <summary>
/// Total children count or null if source is infinite.
/// Some Avalonia features might not work if <see cref="TryGetTotalCount"/> returns false, for instance: nth-last-child selector.
/// </summary>
bool TryGetTotalCount(out int count);
/// <summary>
/// Notifies subscriber when child's index or total count was changed.
/// </summary>
event EventHandler<ChildIndexChangedEventArgs>? ChildIndexChanged;
}
}

56
src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs

@ -0,0 +1,56 @@
#nullable enable
using Avalonia.LogicalTree;
namespace Avalonia.Styling.Activators
{
/// <summary>
/// An <see cref="IStyleActivator"/> which is active when control's index was changed.
/// </summary>
internal sealed class NthChildActivator : StyleActivatorBase
{
private readonly ILogical _control;
private readonly IChildIndexProvider _provider;
private readonly int _step;
private readonly int _offset;
private readonly bool _reversed;
public NthChildActivator(
ILogical control,
IChildIndexProvider provider,
int step, int offset, bool reversed)
{
_control = control;
_provider = provider;
_step = step;
_offset = offset;
_reversed = reversed;
}
protected override void Initialize()
{
PublishNext(IsMatching());
_provider.ChildIndexChanged += ChildIndexChanged;
}
protected override void Deinitialize()
{
_provider.ChildIndexChanged -= ChildIndexChanged;
}
private void ChildIndexChanged(object sender, ChildIndexChangedEventArgs e)
{
// Run matching again if:
// 1. Selector is reversed, so other item insertion/deletion might affect total count without changing subscribed item index.
// 2. e.Child is null, when all children indeces were changed.
// 3. Subscribed child index was changed.
if (_reversed
|| e.Child is null
|| e.Child == _control)
{
PublishNext(IsMatching());
}
}
private bool IsMatching() => NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch;
}
}

145
src/Avalonia.Styling/Styling/NthChildSelector.cs

@ -0,0 +1,145 @@
#nullable enable
using System;
using System.Text;
using Avalonia.LogicalTree;
using Avalonia.Styling.Activators;
namespace Avalonia.Styling
{
/// <summary>
/// The :nth-child() pseudo-class matches elements based on their position in a group of siblings.
/// </summary>
/// <remarks>
/// Element indices are 1-based.
/// </remarks>
public class NthChildSelector : Selector
{
private const string NthChildSelectorName = "nth-child";
private const string NthLastChildSelectorName = "nth-last-child";
private readonly Selector? _previous;
private readonly bool _reversed;
internal protected NthChildSelector(Selector? previous, int step, int offset, bool reversed)
{
_previous = previous;
Step = step;
Offset = offset;
_reversed = reversed;
}
/// <summary>
/// Creates an instance of <see cref="NthChildSelector"/>
/// </summary>
/// <param name="previous">Previous selector.</param>
/// <param name="step">Position step.</param>
/// <param name="offset">Initial index offset.</param>
public NthChildSelector(Selector? previous, int step, int offset)
: this(previous, step, offset, false)
{
}
public override bool InTemplate => _previous?.InTemplate ?? false;
public override bool IsCombinator => false;
public override Type? TargetType => _previous?.TargetType;
public int Step { get; }
public int Offset { get; }
protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
{
if (!(control is ILogical logical))
{
return SelectorMatch.NeverThisType;
}
var controlParent = logical.LogicalParent;
if (controlParent is IChildIndexProvider childIndexProvider)
{
return subscribe
? new SelectorMatch(new NthChildActivator(logical, childIndexProvider, Step, Offset, _reversed))
: Evaluate(logical, childIndexProvider, Step, Offset, _reversed);
}
else
{
return SelectorMatch.NeverThisInstance;
}
}
internal static SelectorMatch Evaluate(
ILogical logical, IChildIndexProvider childIndexProvider,
int step, int offset, bool reversed)
{
var index = childIndexProvider.GetChildIndex(logical);
if (index < 0)
{
return SelectorMatch.NeverThisInstance;
}
if (reversed)
{
if (childIndexProvider.TryGetTotalCount(out var totalCountValue))
{
index = totalCountValue - index;
}
else
{
return SelectorMatch.NeverThisInstance;
}
}
else
{
// nth child index is 1-based
index += 1;
}
var n = Math.Sign(step);
var diff = index - offset;
var match = diff == 0 || (Math.Sign(diff) == n && diff % step == 0);
return match ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance;
}
protected override Selector? MovePrevious() => _previous;
public override string ToString()
{
var expectedCapacity = NthLastChildSelectorName.Length + 8;
var stringBuilder = new StringBuilder(_previous?.ToString(), expectedCapacity);
stringBuilder.Append(':');
stringBuilder.Append(_reversed ? NthLastChildSelectorName : NthChildSelectorName);
stringBuilder.Append('(');
var hasStep = false;
if (Step != 0)
{
hasStep = true;
stringBuilder.Append(Step);
stringBuilder.Append('n');
}
if (Offset > 0)
{
if (hasStep)
{
stringBuilder.Append('+');
}
stringBuilder.Append(Offset);
}
else if (Offset < 0)
{
stringBuilder.Append('-');
stringBuilder.Append(-Offset);
}
stringBuilder.Append(')');
return stringBuilder.ToString();
}
}
}

23
src/Avalonia.Styling/Styling/NthLastChildSelector.cs

@ -0,0 +1,23 @@
#nullable enable
namespace Avalonia.Styling
{
/// <summary>
/// The :nth-child() pseudo-class matches elements based on their position among a group of siblings, counting from the end.
/// </summary>
/// <remarks>
/// Element indices are 1-based.
/// </remarks>
public class NthLastChildSelector : NthChildSelector
{
/// <summary>
/// Creates an instance of <see cref="NthLastChildSelector"/>
/// </summary>
/// <param name="previous">Previous selector.</param>
/// <param name="step">Position step.</param>
/// <param name="offset">Initial index offset, counting from the end.</param>
public NthLastChildSelector(Selector? previous, int step, int offset) : base(previous, step, offset, true)
{
}
}
}

16
src/Avalonia.Styling/Styling/Selectors.cs

@ -123,6 +123,22 @@ namespace Avalonia.Styling
return new NotSelector(previous, argument);
}
/// <inheritdoc cref="NthChildSelector"/>
/// <inheritdoc cref="NthChildSelector(Selector?, int, int)"/>
/// <returns>The selector.</returns>
public static Selector NthChild(this Selector previous, int step, int offset)
{
return new NthChildSelector(previous, step, offset);
}
/// <inheritdoc cref="NthLastChildSelector"/>
/// <inheritdoc cref="NthLastChildSelector(Selector?, int, int)"/>
/// <returns>The selector.</returns>
public static Selector NthLastChild(this Selector previous, int step, int offset)
{
return new NthLastChildSelector(previous, step, offset);
}
/// <summary>
/// Returns a selector which matches a type.
/// </summary>

35
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs

@ -97,6 +97,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
case SelectorGrammar.NotSyntax not:
result = new XamlIlNotSelector(result, Create(not.Argument, typeResolver));
break;
case SelectorGrammar.NthChildSyntax nth:
result = new XamlIlNthChildSelector(result, nth.Step, nth.Offset, XamlIlNthChildSelector.SelectorType.NthChild);
break;
case SelectorGrammar.NthLastChildSyntax nth:
result = new XamlIlNthChildSelector(result, nth.Step, nth.Offset, XamlIlNthChildSelector.SelectorType.NthLastChild);
break;
case SelectorGrammar.CommaSyntax comma:
if (results == null)
results = new XamlIlOrSelectorNode(node, selectorType);
@ -273,6 +279,35 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
}
}
class XamlIlNthChildSelector : XamlIlSelectorNode
{
private readonly int _step;
private readonly int _offset;
private readonly SelectorType _type;
public enum SelectorType
{
NthChild,
NthLastChild
}
public XamlIlNthChildSelector(XamlIlSelectorNode previous, int step, int offset, SelectorType type) : base(previous)
{
_step = step;
_offset = offset;
_type = type;
}
public override IXamlType TargetType => Previous?.TargetType;
protected override void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
{
codeGen.Ldc_I4(_step);
codeGen.Ldc_I4(_offset);
EmitCall(context, codeGen,
m => m.Name == _type.ToString() && m.Parameters.Count == 3);
}
}
class XamlIlPropertyEqualsSelector : XamlIlSelectorNode
{
public XamlIlPropertyEqualsSelector(XamlIlSelectorNode previous,

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

@ -160,11 +160,13 @@ namespace Avalonia.Markup.Parsers
if (identifier.IsEmpty)
{
throw new ExpressionParseException(r.Position, "Expected class name or is selector after ':'.");
throw new ExpressionParseException(r.Position, "Expected class name, is, nth-child or nth-last-child selector after ':'.");
}
const string IsKeyword = "is";
const string NotKeyword = "not";
const string NthChildKeyword = "nth-child";
const string NthLastChildKeyword = "nth-last-child";
if (identifier.SequenceEqual(IsKeyword.AsSpan()) && r.TakeIf('('))
{
@ -181,6 +183,20 @@ namespace Avalonia.Markup.Parsers
var syntax = new NotSyntax { Argument = argument };
return (State.Middle, syntax);
}
if (identifier.SequenceEqual(NthChildKeyword.AsSpan()) && r.TakeIf('('))
{
var (step, offset) = ParseNthChildArguments(ref r);
var syntax = new NthChildSyntax { Step = step, Offset = offset };
return (State.Middle, syntax);
}
if (identifier.SequenceEqual(NthLastChildKeyword.AsSpan()) && r.TakeIf('('))
{
var (step, offset) = ParseNthChildArguments(ref r);
var syntax = new NthLastChildSyntax { Step = step, Offset = offset };
return (State.Middle, syntax);
}
else
{
return (
@ -191,7 +207,6 @@ namespace Avalonia.Markup.Parsers
});
}
}
private static (State, ISyntax?) ParseTraversal(ref CharacterReader r)
{
r.SkipWhitespace();
@ -302,6 +317,114 @@ namespace Avalonia.Markup.Parsers
return syntax;
}
private static (int step, int offset) ParseNthChildArguments(ref CharacterReader r)
{
int step = 0;
int offset = 0;
if (r.Peek == 'o')
{
var constArg = r.TakeUntil(')').ToString().Trim();
if (constArg.Equals("odd", StringComparison.Ordinal))
{
step = 2;
offset = 1;
}
else
{
throw new ExpressionParseException(r.Position, $"Expected nth-child(odd). Actual '{constArg}'.");
}
}
else if (r.Peek == 'e')
{
var constArg = r.TakeUntil(')').ToString().Trim();
if (constArg.Equals("even", StringComparison.Ordinal))
{
step = 2;
offset = 0;
}
else
{
throw new ExpressionParseException(r.Position, $"Expected nth-child(even). Actual '{constArg}'.");
}
}
else
{
r.SkipWhitespace();
var stepOrOffset = 0;
var stepOrOffsetStr = r.TakeWhile(c => char.IsDigit(c) || c == '-' || c == '+').ToString();
if (stepOrOffsetStr.Length == 0
|| (stepOrOffsetStr.Length == 1
&& stepOrOffsetStr[0] == '+'))
{
stepOrOffset = 1;
}
else if (stepOrOffsetStr.Length == 1
&& stepOrOffsetStr[0] == '-')
{
stepOrOffset = -1;
}
else if (!int.TryParse(stepOrOffsetStr.ToString(), out stepOrOffset))
{
throw new ExpressionParseException(r.Position, "Couldn't parse nth-child step or offset value. Integer was expected.");
}
r.SkipWhitespace();
if (r.Peek == ')')
{
step = 0;
offset = stepOrOffset;
}
else
{
step = stepOrOffset;
if (r.Peek != 'n')
{
throw new ExpressionParseException(r.Position, "Couldn't parse nth-child step value, \"xn+y\" pattern was expected.");
}
r.Skip(1); // skip 'n'
r.SkipWhitespace();
if (r.Peek != ')')
{
int sign;
var nextChar = r.Take();
if (nextChar == '+')
{
sign = 1;
}
else if (nextChar == '-')
{
sign = -1;
}
else
{
throw new ExpressionParseException(r.Position, "Couldn't parse nth-child sign. '+' or '-' was expected.");
}
r.SkipWhitespace();
if (sign != 0
&& !int.TryParse(r.TakeUntil(')').ToString(), out offset))
{
throw new ExpressionParseException(r.Position, "Couldn't parse nth-child offset value. Integer was expected.");
}
offset *= sign;
}
}
}
Expect(ref r, ')');
return (step, offset);
}
private static void Expect(ref CharacterReader r, char c)
{
if (r.End)
@ -419,6 +542,28 @@ namespace Avalonia.Markup.Parsers
}
}
public class NthChildSyntax : ISyntax
{
public int Offset { get; set; }
public int Step { get; set; }
public override bool Equals(object? obj)
{
return (obj is NthChildSyntax nth) && nth.Offset == Offset && nth.Step == Step;
}
}
public class NthLastChildSyntax : ISyntax
{
public int Offset { get; set; }
public int Step { get; set; }
public override bool Equals(object? obj)
{
return (obj is NthLastChildSyntax nth) && nth.Offset == Offset && nth.Step == Step;
}
}
public class CommaSyntax : ISyntax
{
public override bool Equals(object? obj)

6
src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs

@ -104,6 +104,12 @@ namespace Avalonia.Markup.Parsers
case SelectorGrammar.NotSyntax not:
result = result.Not(x => Create(not.Argument));
break;
case SelectorGrammar.NthChildSyntax nth:
result = result.NthChild(nth.Step, nth.Offset);
break;
case SelectorGrammar.NthLastChildSyntax nth:
result = result.NthLastChild(nth.Step, nth.Offset);
break;
case SelectorGrammar.CommaSyntax comma:
if (results == null)
{

159
tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs

@ -236,6 +236,165 @@ namespace Avalonia.Markup.UnitTests.Parsers
result);
}
[Theory]
[InlineData(":nth-child(xn+2)")]
[InlineData(":nth-child(2n+b)")]
[InlineData(":nth-child(2n+)")]
[InlineData(":nth-child(2na)")]
[InlineData(":nth-child(2x+1)")]
public void NthChild_Invalid_Inputs(string input)
{
Assert.Throws<ExpressionParseException>(() => SelectorGrammar.Parse(input));
}
[Theory]
[InlineData(":nth-child(+1)", 0, 1)]
[InlineData(":nth-child(1)", 0, 1)]
[InlineData(":nth-child(-1)", 0, -1)]
[InlineData(":nth-child(2n+1)", 2, 1)]
[InlineData(":nth-child(n)", 1, 0)]
[InlineData(":nth-child(+n)", 1, 0)]
[InlineData(":nth-child(-n)", -1, 0)]
[InlineData(":nth-child(-2n)", -2, 0)]
[InlineData(":nth-child(n+5)", 1, 5)]
[InlineData(":nth-child(n-5)", 1, -5)]
[InlineData(":nth-child( 2n + 1 )", 2, 1)]
[InlineData(":nth-child( 2n - 1 )", 2, -1)]
public void NthChild_Variations(string input, int step, int offset)
{
var result = SelectorGrammar.Parse(input);
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.NthChildSyntax()
{
Step = step,
Offset = offset
}
},
result);
}
[Theory]
[InlineData(":nth-last-child(+1)", 0, 1)]
[InlineData(":nth-last-child(1)", 0, 1)]
[InlineData(":nth-last-child(-1)", 0, -1)]
[InlineData(":nth-last-child(2n+1)", 2, 1)]
[InlineData(":nth-last-child(n)", 1, 0)]
[InlineData(":nth-last-child(+n)", 1, 0)]
[InlineData(":nth-last-child(-n)", -1, 0)]
[InlineData(":nth-last-child(-2n)", -2, 0)]
[InlineData(":nth-last-child(n+5)", 1, 5)]
[InlineData(":nth-last-child(n-5)", 1, -5)]
[InlineData(":nth-last-child( 2n + 1 )", 2, 1)]
[InlineData(":nth-last-child( 2n - 1 )", 2, -1)]
public void NthLastChild_Variations(string input, int step, int offset)
{
var result = SelectorGrammar.Parse(input);
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.NthLastChildSyntax()
{
Step = step,
Offset = offset
}
},
result);
}
[Fact]
public void OfType_NthChild()
{
var result = SelectorGrammar.Parse("Button:nth-child(2n+1)");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
new SelectorGrammar.NthChildSyntax()
{
Step = 2,
Offset = 1
}
},
result);
}
[Fact]
public void OfType_NthChild_Without_Offset()
{
var result = SelectorGrammar.Parse("Button:nth-child(2147483647n)");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
new SelectorGrammar.NthChildSyntax()
{
Step = int.MaxValue,
Offset = 0
}
},
result);
}
[Fact]
public void OfType_NthLastChild()
{
var result = SelectorGrammar.Parse("Button:nth-last-child(2n+1)");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
new SelectorGrammar.NthLastChildSyntax()
{
Step = 2,
Offset = 1
}
},
result);
}
[Fact]
public void OfType_NthChild_Odd()
{
var result = SelectorGrammar.Parse("Button:nth-child(odd)");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
new SelectorGrammar.NthChildSyntax()
{
Step = 2,
Offset = 1
}
},
result);
}
[Fact]
public void OfType_NthChild_Even()
{
var result = SelectorGrammar.Parse("Button:nth-child(even)");
Assert.Equal(
new SelectorGrammar.ISyntax[]
{
new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
new SelectorGrammar.NthChildSyntax()
{
Step = 2,
Offset = 0
}
},
result);
}
[Fact]
public void Is_Descendent_Not_OfType_Class()
{

197
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

@ -1,6 +1,8 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Xml;
using Avalonia.Controls;
using Avalonia.Markup.Data;
using Avalonia.Markup.Xaml.Styling;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media;
@ -267,6 +269,199 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
[Fact]
public void Style_Can_Use_NthChild_Selector()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Styles>
<Style Selector='Border.foo:nth-child(2n+1)'>
<Setter Property='Background' Value='Red'/>
</Style>
</Window.Styles>
<StackPanel>
<Border x:Name='b1' Classes='foo'/>
<Border x:Name='b2' />
</StackPanel>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var b1 = window.FindControl<Border>("b1");
var b2 = window.FindControl<Border>("b2");
Assert.Equal(Brushes.Red, b1.Background);
Assert.Null(b2.Background);
}
}
[Fact]
public void Style_Can_Use_NthChild_Selector_After_Reoder()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Styles>
<Style Selector='Border:nth-child(2n)'>
<Setter Property='Background' Value='Red'/>
</Style>
</Window.Styles>
<StackPanel x:Name='parent'>
<Border x:Name='b1' />
<Border x:Name='b2' />
</StackPanel>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var parent = window.FindControl<StackPanel>("parent");
var b1 = window.FindControl<Border>("b1");
var b2 = window.FindControl<Border>("b2");
Assert.Null(b1.Background);
Assert.Equal(Brushes.Red, b2.Background);
parent.Children.Remove(b1);
Assert.Null(b1.Background);
Assert.Null(b2.Background);
parent.Children.Add(b1);
Assert.Equal(Brushes.Red, b1.Background);
Assert.Null(b2.Background);
}
}
[Fact]
public void Style_Can_Use_NthLastChild_Selector_After_Reoder()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Styles>
<Style Selector='Border:nth-last-child(2n)'>
<Setter Property='Background' Value='Red'/>
</Style>
</Window.Styles>
<StackPanel x:Name='parent'>
<Border x:Name='b1' />
<Border x:Name='b2' />
</StackPanel>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var parent = window.FindControl<StackPanel>("parent");
var b1 = window.FindControl<Border>("b1");
var b2 = window.FindControl<Border>("b2");
Assert.Equal(Brushes.Red, b1.Background);
Assert.Null(b2.Background);
parent.Children.Remove(b1);
Assert.Null(b1.Background);
Assert.Null(b2.Background);
parent.Children.Add(b1);
Assert.Null(b1.Background);
Assert.Equal(Brushes.Red, b2.Background);
}
}
[Fact]
public void Style_Can_Use_NthChild_Selector_With_ListBox()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Styles>
<Style Selector='ListBoxItem:nth-child(2n)'>
<Setter Property='Background' Value='{Binding}'/>
</Style>
</Window.Styles>
<ListBox x:Name='list' />
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var collection = new ObservableCollection<IBrush>()
{
Brushes.Red, Brushes.Green, Brushes.Blue
};
var list = window.FindControl<ListBox>("list");
list.VirtualizationMode = ItemVirtualizationMode.Simple;
list.Items = collection;
window.Show();
IEnumerable<IBrush> GetColors() => list.Presenter.Panel.Children.Cast<ListBoxItem>().Select(t => t.Background);
Assert.Equal(new[] { Brushes.Transparent, Brushes.Green, Brushes.Transparent }, GetColors());
collection.Remove(Brushes.Green);
Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue }, GetColors());
collection.Add(Brushes.Violet);
collection.Add(Brushes.Black);
Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue, Brushes.Transparent, Brushes.Black }, GetColors());
}
}
[Fact]
public void Style_Can_Use_NthChild_Selector_With_ItemsRepeater()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Styles>
<Style Selector='TextBlock'>
<Setter Property='Foreground' Value='Transparent'/>
</Style>
<Style Selector='TextBlock:nth-child(2n)'>
<Setter Property='Foreground' Value='{Binding}'/>
</Style>
</Window.Styles>
<ItemsRepeater x:Name='list' />
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var collection = new ObservableCollection<IBrush>()
{
Brushes.Red, Brushes.Green, Brushes.Blue
};
var list = window.FindControl<ItemsRepeater>("list");
list.Items = collection;
window.Show();
IEnumerable<IBrush> GetColors() => Enumerable.Range(0, list.ItemsSourceView.Count)
.Select(t => (list.GetOrCreateElement(t) as TextBlock)!.Foreground);
Assert.Equal(new[] { Brushes.Transparent, Brushes.Green, Brushes.Transparent }, GetColors());
collection.Remove(Brushes.Green);
Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue }, GetColors());
collection.Add(Brushes.Violet);
collection.Add(Brushes.Black);
Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue, Brushes.Transparent, Brushes.Black }, GetColors());
}
}
[Fact]
public void Style_Can_Use_Or_Selector_1()
{

291
tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs

@ -0,0 +1,291 @@
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Xunit;
namespace Avalonia.Styling.UnitTests
{
public class SelectorTests_NthChild
{
[Theory]
[InlineData(2, 0, ":nth-child(2n)")]
[InlineData(2, 1, ":nth-child(2n+1)")]
[InlineData(1, 0, ":nth-child(1n)")]
[InlineData(4, -1, ":nth-child(4n-1)")]
[InlineData(0, 1, ":nth-child(1)")]
[InlineData(0, -1, ":nth-child(-1)")]
[InlineData(int.MaxValue, int.MinValue + 1, ":nth-child(2147483647n-2147483647)")]
public void Not_Selector_Should_Have_Correct_String_Representation(int step, int offset, string expected)
{
var target = default(Selector).NthChild(step, offset);
Assert.Equal(expected, target.ToString());
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthChild(2, 0);
Assert.False(await target.Match(b1).Activator!.Take(1));
Assert.True(await target.Match(b2).Activator!.Take(1));
Assert.False(await target.Match(b3).Activator!.Take(1));
Assert.True(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel_With_Offset()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthChild(2, 1);
Assert.True(await target.Match(b1).Activator!.Take(1));
Assert.False(await target.Match(b2).Activator!.Take(1));
Assert.True(await target.Match(b3).Activator!.Take(1));
Assert.False(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel_With_Negative_Offset()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthChild(4, -1);
Assert.False(await target.Match(b1).Activator!.Take(1));
Assert.False(await target.Match(b2).Activator!.Take(1));
Assert.True(await target.Match(b3).Activator!.Take(1));
Assert.False(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel_With_Singular_Step()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthChild(1, 2);
Assert.False(await target.Match(b1).Activator!.Take(1));
Assert.True(await target.Match(b2).Activator!.Take(1));
Assert.True(await target.Match(b3).Activator!.Take(1));
Assert.True(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel_With_Singular_Step_With_Negative_Offset()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthChild(1, -1);
Assert.True(await target.Match(b1).Activator!.Take(1));
Assert.True(await target.Match(b2).Activator!.Take(1));
Assert.True(await target.Match(b3).Activator!.Take(1));
Assert.True(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel_With_Zero_Step_With_Offset()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthChild(0, 2);
Assert.False(await target.Match(b1).Activator!.Take(1));
Assert.True(await target.Match(b2).Activator!.Take(1));
Assert.False(await target.Match(b3).Activator!.Take(1));
Assert.False(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Doesnt_Match_Control_In_Panel_With_Zero_Step_With_Negative_Offset()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthChild(0, -2);
Assert.False(await target.Match(b1).Activator!.Take(1));
Assert.False(await target.Match(b2).Activator!.Take(1));
Assert.False(await target.Match(b3).Activator!.Take(1));
Assert.False(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel_With_Previous_Selector()
{
Border b1, b2;
Button b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new Control[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Button(),
b4 = new Button()
});
var previous = default(Selector).OfType<Border>();
var target = previous.NthChild(2, 0);
Assert.False(await target.Match(b1).Activator!.Take(1));
Assert.True(await target.Match(b2).Activator!.Take(1));
Assert.Null(target.Match(b3).Activator);
Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b3).Result);
Assert.Null(target.Match(b4).Activator);
Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b4).Result);
}
[Fact]
public void Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent()
{
Border b1;
var contentControl = new ContentControl();
contentControl.Content = b1 = new Border();
var target = default(Selector).NthChild(1, 0);
Assert.Equal(SelectorMatch.NeverThisInstance, target.Match(b1));
}
[Theory] // http://nthmaster.com/
[InlineData(+0, 8, false, false, false, false, false, false, false, true , false, false, false)]
[InlineData(+1, 6, false, false, false, false, false, true , true , true , true , true , true )]
[InlineData(-1, 9, true , true , true , true , true , true , true , true , true , false, false)]
public async Task Nth_Child_Master_Com_Test_Sigle_Selector(
int step, int offset, params bool[] items)
{
var panel = new StackPanel();
panel.Children.AddRange(items.Select(_ => new Border()));
var previous = default(Selector).OfType<Border>();
var target = previous.NthChild(step, offset);
var results = new bool[items.Length];
for (int index = 0; index < items.Length; index++)
{
var border = panel.Children[index];
results[index] = await target.Match(border).Activator!.Take(1);
}
Assert.Equal(items, results);
}
[Theory] // http://nthmaster.com/
[InlineData(+1, 4, -1, 8, false, false, false, true , true , true , true , true , false, false, false)]
[InlineData(+3, 1, +2, 0, false, false, false, true , false, false, false, false, false, true , false)]
public async Task Nth_Child_Master_Com_Test_Double_Selector(
int step1, int offset1, int step2, int offset2, params bool[] items)
{
var panel = new StackPanel();
panel.Children.AddRange(items.Select(_ => new Border()));
var previous = default(Selector).OfType<Border>();
var middle = previous.NthChild(step1, offset1);
var target = middle.NthChild(step2, offset2);
var results = new bool[items.Length];
for (int index = 0; index < items.Length; index++)
{
var border = panel.Children[index];
results[index] = await target.Match(border).Activator!.Take(1);
}
Assert.Equal(items, results);
}
[Theory] // http://nthmaster.com/
[InlineData(+1, 2, 2, 1, -1, 9, false, false, true , false, true , false, true , false, true , false, false)]
public async Task Nth_Child_Master_Com_Test_Triple_Selector(
int step1, int offset1, int step2, int offset2, int step3, int offset3, params bool[] items)
{
var panel = new StackPanel();
panel.Children.AddRange(items.Select(_ => new Border()));
var previous = default(Selector).OfType<Border>();
var middle1 = previous.NthChild(step1, offset1);
var middle2 = middle1.NthChild(step2, offset2);
var target = middle2.NthChild(step3, offset3);
var results = new bool[items.Length];
for (int index = 0; index < items.Length; index++)
{
var border = panel.Children[index];
results[index] = await target.Match(border).Activator!.Take(1);
}
Assert.Equal(items, results);
}
[Fact]
public void Returns_Correct_TargetType()
{
var target = new NthChildSelector(default(Selector).OfType<Control1>(), 1, 0);
Assert.Equal(typeof(Control1), target.TargetType);
}
public class Control1 : Control
{
}
}
}

220
tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs

@ -0,0 +1,220 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using Xunit;
namespace Avalonia.Styling.UnitTests
{
public class SelectorTests_NthLastChild
{
[Theory]
[InlineData(2, 0, ":nth-last-child(2n)")]
[InlineData(2, 1, ":nth-last-child(2n+1)")]
[InlineData(1, 0, ":nth-last-child(1n)")]
[InlineData(4, -1, ":nth-last-child(4n-1)")]
[InlineData(0, 1, ":nth-last-child(1)")]
[InlineData(0, -1, ":nth-last-child(-1)")]
[InlineData(int.MaxValue, int.MinValue + 1, ":nth-last-child(2147483647n-2147483647)")]
public void Not_Selector_Should_Have_Correct_String_Representation(int step, int offset, string expected)
{
var target = default(Selector).NthLastChild(step, offset);
Assert.Equal(expected, target.ToString());
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthLastChild(2, 0);
Assert.True(await target.Match(b1).Activator!.Take(1));
Assert.False(await target.Match(b2).Activator!.Take(1));
Assert.True(await target.Match(b3).Activator!.Take(1));
Assert.False(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel_With_Offset()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthLastChild(2, 1);
Assert.False(await target.Match(b1).Activator!.Take(1));
Assert.True(await target.Match(b2).Activator!.Take(1));
Assert.False(await target.Match(b3).Activator!.Take(1));
Assert.True(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel_With_Negative_Offset()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthLastChild(4, -1);
Assert.False(await target.Match(b1).Activator!.Take(1));
Assert.True(await target.Match(b2).Activator!.Take(1));
Assert.False(await target.Match(b3).Activator!.Take(1));
Assert.False(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel_With_Singular_Step()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthLastChild(1, 2);
Assert.True(await target.Match(b1).Activator!.Take(1));
Assert.True(await target.Match(b2).Activator!.Take(1));
Assert.True(await target.Match(b3).Activator!.Take(1));
Assert.False(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel_With_Singular_Step_With_Negative_Offset()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthLastChild(1, -2);
Assert.True(await target.Match(b1).Activator!.Take(1));
Assert.True(await target.Match(b2).Activator!.Take(1));
Assert.True(await target.Match(b3).Activator!.Take(1));
Assert.True(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel_With_Zero_Step_With_Offset()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthLastChild(0, 2);
Assert.False(await target.Match(b1).Activator!.Take(1));
Assert.False(await target.Match(b2).Activator!.Take(1));
Assert.True(await target.Match(b3).Activator!.Take(1));
Assert.False(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Doesnt_Match_Control_In_Panel_With_Zero_Step_With_Negative_Offset()
{
Border b1, b2, b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Border(),
b4 = new Border()
});
var target = default(Selector).NthLastChild(0, -2);
Assert.False(await target.Match(b1).Activator!.Take(1));
Assert.False(await target.Match(b2).Activator!.Take(1));
Assert.False(await target.Match(b3).Activator!.Take(1));
Assert.False(await target.Match(b4).Activator!.Take(1));
}
[Fact]
public async Task Nth_Child_Match_Control_In_Panel_With_Previous_Selector()
{
Border b1, b2;
Button b3, b4;
var panel = new StackPanel();
panel.Children.AddRange(new Control[]
{
b1 = new Border(),
b2 = new Border(),
b3 = new Button(),
b4 = new Button()
});
var previous = default(Selector).OfType<Border>();
var target = previous.NthLastChild(2, 0);
Assert.True(await target.Match(b1).Activator!.Take(1));
Assert.False(await target.Match(b2).Activator!.Take(1));
Assert.Null(target.Match(b3).Activator);
Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b3).Result);
Assert.Null(target.Match(b4).Activator);
Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b4).Result);
}
[Fact]
public void Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent()
{
Border b1;
var contentControl = new ContentControl();
contentControl.Content = b1 = new Border();
var target = default(Selector).NthLastChild(1, 0);
Assert.Equal(SelectorMatch.NeverThisInstance, target.Match(b1));
}
[Fact]
public void Returns_Correct_TargetType()
{
var target = new NthLastChildSelector(default(Selector).OfType<Control1>(), 1, 0);
Assert.Equal(typeof(Control1), target.TargetType);
}
public class Control1 : Control
{
}
}
}

7
tests/Avalonia.Styling.UnitTests/StyleActivatorExtensions.cs

@ -20,13 +20,17 @@ namespace Avalonia.Styling.UnitTests
public static IObservable<bool> ToObservable(this IStyleActivator activator)
{
if (activator == null)
{
throw new ArgumentNullException(nameof(activator));
}
return new ObservableAdapter(activator);
}
private class ObservableAdapter : LightweightObservableBase<bool>, IStyleActivatorSink
{
private readonly IStyleActivator _source;
private bool _value;
public ObservableAdapter(IStyleActivator source) => _source = source;
protected override void Initialize() => _source.Subscribe(this);
@ -34,7 +38,6 @@ namespace Avalonia.Styling.UnitTests
void IStyleActivatorSink.OnNext(bool value, int tag)
{
_value = value;
PublishNext(value);
}
}

Loading…
Cancel
Save