From 69fb1c056f6e4498752264f5fbdb9aa9e7c02114 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 8 Aug 2021 03:07:17 -0400 Subject: [PATCH 1/6] FIrst version of nth-child --- samples/ControlCatalog/Pages/ListBoxPage.xaml | 6 + src/Avalonia.Controls/ItemsControl.cs | 19 +- src/Avalonia.Controls/Panel.cs | 17 +- .../Presenters/ItemsPresenterBase.cs | 25 +- .../Utils/IEnumerableUtils.cs | 27 ++- .../Styling/NthChildSelector.cs | 134 +++++++++++ src/Avalonia.Styling/Styling/Selectors.cs | 10 + .../AvaloniaXamlIlSelectorTransformer.cs | 35 +++ .../Markup/Parsers/SelectorGrammar.cs | 105 ++++++++- .../Markup/Parsers/SelectorParser.cs | 6 + .../Parsers/SelectorGrammarTests.cs | 90 ++++++++ .../Xaml/StyleTests.cs | 59 +++++ .../SelectorTests_NthChild.cs | 217 ++++++++++++++++++ .../SelectorTests_NthLastChild.cs | 217 ++++++++++++++++++ 14 files changed, 955 insertions(+), 12 deletions(-) create mode 100644 src/Avalonia.Styling/Styling/NthChildSelector.cs create mode 100644 tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs create mode 100644 tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index f515db84d40..897134badbd 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -2,9 +2,15 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.Pages.ListBoxPage"> + + + ListBox Hosts a collection of ListBoxItem. + Each 2nd item is highlighted Multiple diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 55645d4dbb5..9c86aeb0c88 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -13,6 +13,7 @@ using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Metadata; +using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -21,7 +22,7 @@ namespace Avalonia.Controls /// Displays a collection of items. /// [PseudoClasses(":empty", ":singleitem")] - public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener + public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener, IChildIndexProvider { /// /// The default value for the property. @@ -506,5 +507,21 @@ protected static IInputElement GetNextControl( return null; } + + (int Index, int? TotalCount) IChildIndexProvider.GetChildIndex(ILogical child) + { + if (Presenter is IChildIndexProvider innerProvider) + { + return innerProvider.GetChildIndex(child); + } + + if (child is IControl control) + { + var index = ItemContainerGenerator.IndexFromContainer(control); + return (index, ItemCount); + } + + return (-1, ItemCount); + } } } diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index b7eeb065daf..7a3e93ffc20 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -2,8 +2,12 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; + +using Avalonia.Controls.Presenters; +using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; +using Avalonia.Styling; namespace Avalonia.Controls { @@ -14,7 +18,7 @@ namespace Avalonia.Controls /// Controls can be added to a by adding them to its /// collection. All children are layed out to fill the panel. /// - public class Panel : Control, IPanel + public class Panel : Control, IPanel, IChildIndexProvider { /// /// Defines the property. @@ -160,5 +164,16 @@ private static void AffectsParentMeasureInvalidate(AvaloniaPropertyChang var panel = control?.VisualParent as TPanel; panel?.InvalidateMeasure(); } + + (int Index, int? TotalCount) IChildIndexProvider.GetChildIndex(ILogical child) + { + if (child is IControl control) + { + var index = Children.IndexOf(control); + return (index, Children.Count); + } + + return (-1, Children.Count); + } } } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index 52f173fc711..cf5fb8ac42d 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -5,6 +5,7 @@ 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 /// /// Base class for controls that present items inside an . /// - public abstract class ItemsPresenterBase : Control, IItemsPresenter, ITemplatedControl + public abstract class ItemsPresenterBase : Control, IItemsPresenter, ITemplatedControl, IChildIndexProvider { /// /// Defines the property. @@ -248,5 +249,27 @@ private void TemplatedParentChanged(AvaloniaPropertyChangedEventArgs e) { (e.NewValue as IItemsPresenterHost)?.RegisterItemsPresenter(this); } + + (int Index, int? TotalCount) IChildIndexProvider.GetChildIndex(ILogical child) + { + int? totalCount = null; + if (Items.TryGetCountFast(out var count)) + { + totalCount = count; + } + + if (child is IControl control) + { + + if (ItemContainerGenerator is { } generator) + { + var index = ItemContainerGenerator.IndexFromContainer(control); + + return (index, totalCount); + } + } + + return (-1, totalCount); + } } } diff --git a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs index 9614d079d98..fa5a09e2453 100644 --- a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs +++ b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs @@ -12,23 +12,36 @@ public static bool Contains(this IEnumerable items, object item) 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 readOnly) { - return readOnly.Count; - } - else - { - return Enumerable.Count(items.Cast()); + 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()); + } else { return 0; diff --git a/src/Avalonia.Styling/Styling/NthChildSelector.cs b/src/Avalonia.Styling/Styling/NthChildSelector.cs new file mode 100644 index 00000000000..a6b91dea5f4 --- /dev/null +++ b/src/Avalonia.Styling/Styling/NthChildSelector.cs @@ -0,0 +1,134 @@ +#nullable enable +using System; +using System.Text; + +using Avalonia.LogicalTree; + +namespace Avalonia.Styling +{ + public interface IChildIndexProvider + { + (int Index, int? TotalCount) GetChildIndex(ILogical child); + } + + public class NthLastChildSelector : NthChildSelector + { + public NthLastChildSelector(Selector? previous, int step, int offset) : base(previous, step, offset, true) + { + } + } + + 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; + } + + 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) + { + var logical = (ILogical)control; + var controlParent = logical.LogicalParent; + + if (controlParent is IChildIndexProvider childIndexProvider) + { + var (index, totalCount) = childIndexProvider.GetChildIndex(logical); + if (index < 0) + { + return SelectorMatch.NeverThisInstance; + } + + if (_reversed) + { + if (totalCount is int 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; + } + else + { + return 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(); + } + } +} diff --git a/src/Avalonia.Styling/Styling/Selectors.cs b/src/Avalonia.Styling/Styling/Selectors.cs index 762ed7b58cf..0bccccbd7c6 100644 --- a/src/Avalonia.Styling/Styling/Selectors.cs +++ b/src/Avalonia.Styling/Styling/Selectors.cs @@ -123,6 +123,16 @@ public static Selector Not(this Selector previous, Selector argument) return new NotSelector(previous, argument); } + public static Selector NthChild(this Selector previous, int step, int offset) + { + return new NthChildSelector(previous, step, offset); + } + + public static Selector NthLastChild(this Selector previous, int step, int offset) + { + return new NthLastChildSelector(previous, step, offset); + } + /// /// Returns a selector which matches a type. /// diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs index b81d25d6132..dfabd66d179 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs @@ -97,6 +97,12 @@ XamlIlSelectorNode Create(IEnumerable syntax, 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 @@ protected override void DoEmit(XamlEmitContext Previous?.TargetType; + protected override void DoEmit(XamlEmitContext 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, diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index 9d03341f929..56e64329b7a 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -160,11 +160,13 @@ private static (State, ISyntax) ParseColon(ref CharacterReader r) 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 @@ private static (State, ISyntax) ParseColon(ref CharacterReader r) 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 @@ private static (State, ISyntax) ParseColon(ref CharacterReader r) }); } } - private static (State, ISyntax?) ParseTraversal(ref CharacterReader r) { r.SkipWhitespace(); @@ -302,6 +317,70 @@ private static TSyntax ParseType(ref CharacterReader r, TSyntax syntax) 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 + { + var stepOrOffsetSpan = r.TakeWhile(c => c != ')' && c != 'n'); + if (!int.TryParse(stepOrOffsetSpan.ToString().Trim(), out var stepOrOffset)) + { + throw new ExpressionParseException(r.Position, "Couldn't parse nth-child step or offset value. Integer was expected."); + } + + if (r.Peek == ')') + { + step = 0; + offset = stepOrOffset; + } + else + { + step = stepOrOffset; + + r.Skip(1); // skip 'n' + var offsetSpan = r.TakeUntil(')').TrimStart(); + + if (offsetSpan.Length != 0 + && !int.TryParse(offsetSpan.ToString().Trim(), out offset)) + { + throw new ExpressionParseException(r.Position, "Couldn't parse nth-child offset value. Integer was expected."); + } + } + } + + Expect(ref r, ')'); + + return (step, offset); + } + private static void Expect(ref CharacterReader r, char c) { if (r.End) @@ -419,6 +498,28 @@ public override bool Equals(object? obj) } } + 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) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs index 92ba744ee17..11fb287d462 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs @@ -104,6 +104,12 @@ public SelectorParser(Func typeResolver) 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) { diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs index 03f1120796d..543d44c4926 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs @@ -236,6 +236,96 @@ public void OfType_Not_Class() 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() { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 06b494c3d8a..3824b797081 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -267,6 +267,65 @@ public void Style_Can_Use_Not_Selector() } } + [Fact] + public void Style_Can_Use_NthChild_Selector() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var b1 = window.FindControl("b1"); + var b2 = window.FindControl("b2"); + + Assert.Equal(Colors.Red, ((ISolidColorBrush)b1.Background).Color); + Assert.Null(b2.Background); + } + } + + [Fact] + public void Style_Can_Use_NthChild_Selector_After_Reoder() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + + var parent = window.FindControl("parent"); + var b1 = window.FindControl("b1"); + var b2 = window.FindControl("b2"); + + parent.Children.Remove(b1); + parent.Children.Add(b1); + + Assert.Null(b1.Background); + Assert.Equal(Colors.Red, ((ISolidColorBrush)b2.Background).Color); + } + } + [Fact] public void Style_Can_Use_Or_Selector_1() { diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs new file mode 100644 index 00000000000..b72e9808216 --- /dev/null +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs @@ -0,0 +1,217 @@ +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 void 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.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + } + + [Fact] + public void 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.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void 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.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void 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.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + } + + [Fact] + public void 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.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + } + + [Fact] + public void 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.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void 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.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void 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(); + var target = previous.NthChild(2, 0); + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b3).Result); + 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(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + } + + [Fact] + public void Returns_Correct_TargetType() + { + var target = new NthChildSelector(default(Selector).OfType(), 1, 0); + + Assert.Equal(typeof(Control1), target.TargetType); + } + + public class Control1 : Control + { + } + } +} diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs new file mode 100644 index 00000000000..3698e07d3e8 --- /dev/null +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs @@ -0,0 +1,217 @@ +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 void 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.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void 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.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + } + + [Fact] + public void 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.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void 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.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void 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.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + } + + [Fact] + public void 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.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void 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.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void 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(); + var target = previous.NthLastChild(2, 0); + + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b3).Result); + 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(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + } + + [Fact] + public void Returns_Correct_TargetType() + { + var target = new NthLastChildSelector(default(Selector).OfType(), 1, 0); + + Assert.Equal(typeof(Control1), target.TargetType); + } + + public class Control1 : Control + { + } + } +} From e5ca5c38e8c6cff3f0fd6494578b83a8f4df6d22 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 10 Sep 2021 02:06:02 -0400 Subject: [PATCH 2/6] Update IChildIndexProvider interface with ChildIndexChanged and implement in on items controls --- .../Pages/ItemsRepeaterPage.xaml | 19 ++- src/Avalonia.Controls/ItemsControl.cs | 39 ++++-- src/Avalonia.Controls/Panel.cs | 21 ++-- .../Presenters/ItemsPresenterBase.cs | 43 ++++--- .../Repeater/ItemsRepeater.cs | 29 ++++- .../LogicalTree/ChildIndexChangedEventArgs.cs | 23 ++++ .../LogicalTree/IChildIndexProvider.cs | 14 +++ .../Styling/Activators/NthChildActivator.cs | 56 +++++++++ .../Styling/NthChildSelector.cs | 70 +++++------ .../Styling/NthLastChildSelector.cs | 11 ++ .../Xaml/StyleTests.cs | 119 +++++++++++++++++- .../SelectorTests_NthChild.cs | 85 +++++++------ .../SelectorTests_NthLastChild.cs | 84 +++++++------ .../StyleActivatorExtensions.cs | 7 +- 14 files changed, 456 insertions(+), 164 deletions(-) create mode 100644 src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs create mode 100644 src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs create mode 100644 src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs create mode 100644 src/Avalonia.Styling/Styling/NthLastChildSelector.cs diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml index 392ccb57c34..93f3c334346 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml @@ -1,17 +1,28 @@ + + + + + - - diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 9c86aeb0c88..7b28335a6d0 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -57,6 +57,7 @@ public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionCh private IEnumerable _items = new AvaloniaList(); private int _itemCount; private IItemContainerGenerator _itemContainerGenerator; + private EventHandler _childIndexChanged; /// /// Initializes static members of the class. @@ -146,11 +147,30 @@ public IItemsPresenter Presenter protected set; } + int? IChildIndexProvider.TotalCount => (Presenter as IChildIndexProvider)?.TotalCount ?? ItemCount; + + event EventHandler IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + /// 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) @@ -508,20 +528,15 @@ protected static IInputElement GetNextControl( return null; } - (int Index, int? TotalCount) IChildIndexProvider.GetChildIndex(ILogical child) + private void PresenterChildIndexChanged(object sender, ChildIndexChangedEventArgs e) { - if (Presenter is IChildIndexProvider innerProvider) - { - return innerProvider.GetChildIndex(child); - } - - if (child is IControl control) - { - var index = ItemContainerGenerator.IndexFromContainer(control); - return (index, ItemCount); - } + _childIndexChanged?.Invoke(this, e); + } - return (-1, ItemCount); + int IChildIndexProvider.GetChildIndex(ILogical child) + { + return Presenter is IChildIndexProvider innerProvider + ? innerProvider.GetChildIndex(child) : -1; } } } diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 7a3e93ffc20..9c93126506e 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -34,6 +34,8 @@ static Panel() AffectsRender(BackgroundProperty); } + private EventHandler _childIndexChanged; + /// /// Initializes a new instance of the class. /// @@ -57,6 +59,14 @@ public IBrush Background set { SetValue(BackgroundProperty, value); } } + int? IChildIndexProvider.TotalCount => Children.Count; + + event EventHandler IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + /// /// Renders the visual to a . /// @@ -141,6 +151,7 @@ protected virtual void ChildrenChanged(object sender, NotifyCollectionChangedEve throw new NotSupportedException(); } + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs()); InvalidateMeasureOnChildrenChanged(); } @@ -165,15 +176,9 @@ private static void AffectsParentMeasureInvalidate(AvaloniaPropertyChang panel?.InvalidateMeasure(); } - (int Index, int? TotalCount) IChildIndexProvider.GetChildIndex(ILogical child) + int IChildIndexProvider.GetChildIndex(ILogical child) { - if (child is IControl control) - { - var index = Children.IndexOf(control); - return (index, Children.Count); - } - - return (-1, Children.Count); + return child is IControl control ? Children.IndexOf(control) : -1; } } } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index cf5fb8ac42d..d58ef2e510f 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -1,6 +1,8 @@ using System; using System.Collections; using System.Collections.Specialized; +using System.Linq; + using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Templates; @@ -37,6 +39,7 @@ public abstract class ItemsPresenterBase : Control, IItemsPresenter, ITemplatedC private IDisposable _itemsSubscription; private bool _createdPanel; private IItemContainerGenerator _generator; + private EventHandler _childIndexChanged; /// /// Initializes static members of the class. @@ -130,6 +133,14 @@ public IPanel Panel protected bool IsHosted => TemplatedParent is IItemsPresenterHost; + int? IChildIndexProvider.TotalCount => Items.TryGetCountFast(out var count) ? count : null; + + event EventHandler IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + /// public override sealed void ApplyTemplate() { @@ -170,9 +181,21 @@ protected virtual IItemContainerGenerator CreateItemContainerGenerator() 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(sender, new ChildIndexChangedEventArgs(e.Containers[i].ContainerControl)); + } + } + /// protected override Size MeasureOverride(Size availableSize) { @@ -250,26 +273,16 @@ private void TemplatedParentChanged(AvaloniaPropertyChangedEventArgs e) (e.NewValue as IItemsPresenterHost)?.RegisterItemsPresenter(this); } - (int Index, int? TotalCount) IChildIndexProvider.GetChildIndex(ILogical child) + int IChildIndexProvider.GetChildIndex(ILogical child) { - int? totalCount = null; - if (Items.TryGetCountFast(out var count)) - { - totalCount = count; - } - - if (child is IControl control) + if (child is IControl control && ItemContainerGenerator is { } generator) { + var index = ItemContainerGenerator.IndexFromContainer(control); - if (ItemContainerGenerator is { } generator) - { - var index = ItemContainerGenerator.IndexFromContainer(control); - - return (index, totalCount); - } + return index; } - return (-1, totalCount); + return -1; } } } diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 0ff8fcbd280..6d89a706700 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -6,10 +6,13 @@ using System; using System.Collections; using System.Collections.Specialized; + using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Logging; +using Avalonia.LogicalTree; +using Avalonia.Styling; using Avalonia.Utilities; using Avalonia.VisualTree; @@ -19,7 +22,7 @@ namespace Avalonia.Controls /// Represents a data-driven collection control that incorporates a flexible layout system, /// custom views, and virtualization. /// - public class ItemsRepeater : Panel + public class ItemsRepeater : Panel, IChildIndexProvider { /// /// Defines the property. @@ -61,8 +64,9 @@ public class ItemsRepeater : Panel private readonly ViewportManager _viewportManager; private IEnumerable _items; private VirtualizingLayoutContext _layoutContext; - private NotifyCollectionChangedEventArgs _processingItemsSourceChange; + private EventHandler _childIndexChanged; private bool _isLayoutInProgress; + private NotifyCollectionChangedEventArgs _processingItemsSourceChange; private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs; private ItemsRepeaterElementClearingEventArgs _elementClearingArgs; private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs; @@ -163,6 +167,21 @@ private LayoutContext LayoutContext } } + int? IChildIndexProvider.TotalCount => ItemsSourceView.Count; + + event EventHandler IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + + int IChildIndexProvider.GetChildIndex(ILogical child) + { + return child is IControl control + ? GetElementIndex(control) + : -1; + } + /// /// Occurs each time an element is cleared and made available to be re-used. /// @@ -545,6 +564,8 @@ internal void OnElementPrepared(IControl element, VirtualizationInfo virtInfo) ElementPrepared(this, _elementPreparedArgs); } + + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element)); } internal void OnElementClearing(IControl element) @@ -562,6 +583,8 @@ internal void OnElementClearing(IControl element) ElementClearing(this, _elementClearingArgs); } + + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element)); } internal void OnElementIndexChanged(IControl element, int oldIndex, int newIndex) @@ -579,6 +602,8 @@ internal void OnElementIndexChanged(IControl element, int oldIndex, int newIndex ElementIndexChanged(this, _elementIndexChangedArgs); } + + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element)); } private void OnDataSourcePropertyChanged(ItemsSourceView oldValue, ItemsSourceView newValue) diff --git a/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs b/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs new file mode 100644 index 00000000000..1c90851e133 --- /dev/null +++ b/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs @@ -0,0 +1,23 @@ +#nullable enable +using System; + +namespace Avalonia.LogicalTree +{ + public class ChildIndexChangedEventArgs : EventArgs + { + public ChildIndexChangedEventArgs() + { + } + + public ChildIndexChangedEventArgs(ILogical child) + { + Child = child; + } + + /// + /// Logical child which index was changed. + /// If null, all children should be reset. + /// + public ILogical? Child { get; } + } +} diff --git a/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs b/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs new file mode 100644 index 00000000000..fdba99baa23 --- /dev/null +++ b/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs @@ -0,0 +1,14 @@ +#nullable enable +using System; + +namespace Avalonia.LogicalTree +{ + public interface IChildIndexProvider + { + int GetChildIndex(ILogical child); + + int? TotalCount { get; } + + event EventHandler? ChildIndexChanged; + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs b/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs new file mode 100644 index 00000000000..34cca1a396c --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs @@ -0,0 +1,56 @@ +#nullable enable +using System; + +using Avalonia.LogicalTree; + +namespace Avalonia.Styling.Activators +{ + /// + /// An which is active when control's index was changed. + /// + 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; + private EventHandler? _childIndexChangedHandler; + + public NthChildActivator( + ILogical control, + IChildIndexProvider provider, + int step, int offset, bool reversed) + { + _control = control; + _provider = provider; + _step = step; + _offset = offset; + _reversed = reversed; + } + + private EventHandler ChildIndexChangedHandler => _childIndexChangedHandler ??= ChildIndexChanged; + + protected override void Initialize() + { + PublishNext(IsMatching()); + _provider.ChildIndexChanged += ChildIndexChangedHandler; + } + + protected override void Deinitialize() + { + _provider.ChildIndexChanged -= ChildIndexChangedHandler; + } + + private void ChildIndexChanged(object sender, ChildIndexChangedEventArgs e) + { + if (e.Child is null + || e.Child == _control) + { + PublishNext(IsMatching()); + } + } + + private bool IsMatching() => NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch; + } +} diff --git a/src/Avalonia.Styling/Styling/NthChildSelector.cs b/src/Avalonia.Styling/Styling/NthChildSelector.cs index a6b91dea5f4..16b97e22f62 100644 --- a/src/Avalonia.Styling/Styling/NthChildSelector.cs +++ b/src/Avalonia.Styling/Styling/NthChildSelector.cs @@ -3,21 +3,10 @@ using System.Text; using Avalonia.LogicalTree; +using Avalonia.Styling.Activators; namespace Avalonia.Styling { - public interface IChildIndexProvider - { - (int Index, int? TotalCount) GetChildIndex(ILogical child); - } - - public class NthLastChildSelector : NthChildSelector - { - public NthLastChildSelector(Selector? previous, int step, int offset) : base(previous, step, offset, true) - { - } - } - public class NthChildSelector : Selector { private const string NthChildSelectorName = "nth-child"; @@ -55,42 +44,49 @@ protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) if (controlParent is IChildIndexProvider childIndexProvider) { - var (index, totalCount) = childIndexProvider.GetChildIndex(logical); - if (index < 0) - { - return SelectorMatch.NeverThisInstance; - } + 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 (reversed) + { + if (childIndexProvider.TotalCount is int totalCountValue) { - if (totalCount is int totalCountValue) - { - index = totalCountValue - index; - } - else - { - return SelectorMatch.NeverThisInstance; - } + index = totalCountValue - index; } else { - // nth child index is 1-based - index += 1; + return SelectorMatch.NeverThisInstance; } - - - 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; } else { - return SelectorMatch.NeverThisInstance; + // 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; diff --git a/src/Avalonia.Styling/Styling/NthLastChildSelector.cs b/src/Avalonia.Styling/Styling/NthLastChildSelector.cs new file mode 100644 index 00000000000..ff7cf0faa1e --- /dev/null +++ b/src/Avalonia.Styling/Styling/NthLastChildSelector.cs @@ -0,0 +1,11 @@ +#nullable enable + +namespace Avalonia.Styling +{ + public class NthLastChildSelector : NthChildSelector + { + public NthLastChildSelector(Selector? previous, int step, int offset) : base(previous, step, offset, true) + { + } + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 3824b797081..ee633ee66fb 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -1,3 +1,6 @@ +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; using System.Xml; using Avalonia.Controls; using Avalonia.Markup.Data; @@ -289,7 +292,7 @@ public void Style_Can_Use_NthChild_Selector() var b1 = window.FindControl("b1"); var b2 = window.FindControl("b2"); - Assert.Equal(Colors.Red, ((ISolidColorBrush)b1.Background).Color); + Assert.Equal(Brushes.Red, b1.Background); Assert.Null(b2.Background); } } @@ -303,7 +306,7 @@ public void Style_Can_Use_NthChild_Selector_After_Reoder() - @@ -318,11 +321,119 @@ public void Style_Can_Use_NthChild_Selector_After_Reoder() var b1 = window.FindControl("b1"); var b2 = window.FindControl("b2"); + Assert.Null(b1.Background); + Assert.Equal(Brushes.Red, b2.Background); + parent.Children.Remove(b1); - parent.Children.Add(b1); Assert.Null(b1.Background); - Assert.Equal(Colors.Red, ((ISolidColorBrush)b2.Background).Color); + Assert.Null(b2.Background); + + parent.Children.Add(b1); + + Assert.Equal(Brushes.Red, b1.Background); + Assert.Null(b2.Background); + } + } + + + [Fact] + public void Style_Can_Use_NthChild_Selector_With_ListBox() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var collection = new ObservableCollection() + { + Brushes.Red, Brushes.Green, Brushes.Blue + }; + + var list = window.FindControl("list"); + list.VirtualizationMode = ItemVirtualizationMode.Simple; + list.Items = collection; + + window.Show(); + + var items = list.Presenter.Panel.Children.Cast(); + ListBoxItem At(int index) => items.ElementAt(index); + + Assert.Equal(Brushes.Transparent, At(0).Background); + Assert.Equal(Brushes.Green, At(1).Background); + Assert.Equal(Brushes.Transparent, At(2).Background); + + collection.Remove(Brushes.Green); + + Assert.Equal(Brushes.Transparent, At(0).Background); + Assert.Equal(Brushes.Blue, At(1).Background); + + collection.Add(Brushes.Violet); + collection.Add(Brushes.Black); + + Assert.Equal(Brushes.Transparent, At(0).Background); + Assert.Equal(Brushes.Blue, At(1).Background); + Assert.Equal(Brushes.Transparent, At(2).Background); + Assert.Equal(Brushes.Black, At(3).Background); + } + } + + [Fact] + public void Style_Can_Use_NthChild_Selector_With_ItemsRepeater() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var collection = new ObservableCollection() + { + Brushes.Red, Brushes.Green, Brushes.Blue + }; + + var list = window.FindControl("list"); + list.Items = collection; + + window.Show(); + + var items = list.Children; + TextBlock At(int index) => (TextBlock)list.GetOrCreateElement(index); + + Assert.Equal(Brushes.Transparent, At(0).Foreground); + Assert.Equal(Brushes.Green, At(1).Foreground); + Assert.Equal(Brushes.Transparent, At(2).Foreground); + + collection.Remove(Brushes.Green); + + Assert.Equal(Brushes.Transparent, At(0).Foreground); + Assert.Equal(Brushes.Blue, At(1).Foreground); + + collection.Add(Brushes.Violet); + collection.Add(Brushes.Black); + + Assert.Equal(Brushes.Transparent, At(0).Foreground); + Assert.Equal(Brushes.Blue, At(1).Foreground); + Assert.Equal(Brushes.Transparent, At(2).Foreground); + Assert.Equal(Brushes.Black, At(3).Foreground); } } diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs index b72e9808216..a70b3c9f293 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs @@ -1,4 +1,7 @@ +using System.Threading.Tasks; + using Avalonia.Controls; + using Xunit; namespace Avalonia.Styling.UnitTests @@ -21,7 +24,7 @@ public void Not_Selector_Should_Have_Correct_String_Representation(int step, int } [Fact] - public void Nth_Child_Match_Control_In_Panel() + public async Task Nth_Child_Match_Control_In_Panel() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -35,14 +38,14 @@ public void Nth_Child_Match_Control_In_Panel() var target = default(Selector).NthChild(2, 0); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + 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 void Nth_Child_Match_Control_In_Panel_With_Offset() + public async Task Nth_Child_Match_Control_In_Panel_With_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -56,14 +59,14 @@ public void Nth_Child_Match_Control_In_Panel_With_Offset() var target = default(Selector).NthChild(2, 1); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + 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 void Nth_Child_Match_Control_In_Panel_With_Negative_Offset() + public async Task Nth_Child_Match_Control_In_Panel_With_Negative_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -77,14 +80,14 @@ public void Nth_Child_Match_Control_In_Panel_With_Negative_Offset() var target = default(Selector).NthChild(4, -1); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + 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 void Nth_Child_Match_Control_In_Panel_With_Singular_Step() + public async Task Nth_Child_Match_Control_In_Panel_With_Singular_Step() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -98,14 +101,14 @@ public void Nth_Child_Match_Control_In_Panel_With_Singular_Step() var target = default(Selector).NthChild(1, 2); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + 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 void Nth_Child_Match_Control_In_Panel_With_Singular_Step_With_Negative_Offset() + public async Task Nth_Child_Match_Control_In_Panel_With_Singular_Step_With_Negative_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -119,14 +122,14 @@ public void Nth_Child_Match_Control_In_Panel_With_Singular_Step_With_Negative_Of var target = default(Selector).NthChild(1, -1); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + 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 void Nth_Child_Match_Control_In_Panel_With_Zero_Step_With_Offset() + public async Task Nth_Child_Match_Control_In_Panel_With_Zero_Step_With_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -140,14 +143,14 @@ public void Nth_Child_Match_Control_In_Panel_With_Zero_Step_With_Offset() var target = default(Selector).NthChild(0, 2); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + 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 void Nth_Child_Doesnt_Match_Control_In_Panel_With_Zero_Step_With_Negative_Offset() + 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(); @@ -161,14 +164,14 @@ public void Nth_Child_Doesnt_Match_Control_In_Panel_With_Zero_Step_With_Negative var target = default(Selector).NthChild(0, -2); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + 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 void Nth_Child_Match_Control_In_Panel_With_Previous_Selector() + public async Task Nth_Child_Match_Control_In_Panel_With_Previous_Selector() { Border b1, b2; Button b3, b4; @@ -184,14 +187,16 @@ public void Nth_Child_Match_Control_In_Panel_With_Previous_Selector() var previous = default(Selector).OfType(); var target = previous.NthChild(2, 0); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + 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() + public async Task Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() { Border b1; var contentControl = new ContentControl(); @@ -199,7 +204,7 @@ public void Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() var target = default(Selector).NthChild(1, 0); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); } [Fact] diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs index 3698e07d3e8..ed88106295f 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs @@ -1,3 +1,5 @@ +using System.Threading.Tasks; + using Avalonia.Controls; using Xunit; @@ -21,7 +23,7 @@ public void Not_Selector_Should_Have_Correct_String_Representation(int step, int } [Fact] - public void Nth_Child_Match_Control_In_Panel() + public async Task Nth_Child_Match_Control_In_Panel() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -35,14 +37,14 @@ public void Nth_Child_Match_Control_In_Panel() var target = default(Selector).NthLastChild(2, 0); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + 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 void Nth_Child_Match_Control_In_Panel_With_Offset() + public async Task Nth_Child_Match_Control_In_Panel_With_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -56,14 +58,14 @@ public void Nth_Child_Match_Control_In_Panel_With_Offset() var target = default(Selector).NthLastChild(2, 1); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + 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 void Nth_Child_Match_Control_In_Panel_With_Negative_Offset() + public async Task Nth_Child_Match_Control_In_Panel_With_Negative_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -77,14 +79,14 @@ public void Nth_Child_Match_Control_In_Panel_With_Negative_Offset() var target = default(Selector).NthLastChild(4, -1); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + 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 void Nth_Child_Match_Control_In_Panel_With_Singular_Step() + public async Task Nth_Child_Match_Control_In_Panel_With_Singular_Step() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -98,14 +100,14 @@ public void Nth_Child_Match_Control_In_Panel_With_Singular_Step() var target = default(Selector).NthLastChild(1, 2); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + 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 void Nth_Child_Match_Control_In_Panel_With_Singular_Step_With_Negative_Offset() + public async Task Nth_Child_Match_Control_In_Panel_With_Singular_Step_With_Negative_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -119,14 +121,14 @@ public void Nth_Child_Match_Control_In_Panel_With_Singular_Step_With_Negative_Of var target = default(Selector).NthLastChild(1, -2); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + 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 void Nth_Child_Match_Control_In_Panel_With_Zero_Step_With_Offset() + public async Task Nth_Child_Match_Control_In_Panel_With_Zero_Step_With_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -140,14 +142,14 @@ public void Nth_Child_Match_Control_In_Panel_With_Zero_Step_With_Offset() var target = default(Selector).NthLastChild(0, 2); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + 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 void Nth_Child_Doesnt_Match_Control_In_Panel_With_Zero_Step_With_Negative_Offset() + 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(); @@ -161,14 +163,14 @@ public void Nth_Child_Doesnt_Match_Control_In_Panel_With_Zero_Step_With_Negative var target = default(Selector).NthLastChild(0, -2); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + 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 void Nth_Child_Match_Control_In_Panel_With_Previous_Selector() + public async Task Nth_Child_Match_Control_In_Panel_With_Previous_Selector() { Border b1, b2; Button b3, b4; @@ -184,14 +186,16 @@ public void Nth_Child_Match_Control_In_Panel_With_Previous_Selector() var previous = default(Selector).OfType(); var target = previous.NthLastChild(2, 0); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); + 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() + public async Task Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() { Border b1; var contentControl = new ContentControl(); @@ -199,7 +203,7 @@ public void Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() var target = default(Selector).NthLastChild(1, 0); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); } [Fact] diff --git a/tests/Avalonia.Styling.UnitTests/StyleActivatorExtensions.cs b/tests/Avalonia.Styling.UnitTests/StyleActivatorExtensions.cs index eb3dabce0bb..22f4db79d13 100644 --- a/tests/Avalonia.Styling.UnitTests/StyleActivatorExtensions.cs +++ b/tests/Avalonia.Styling.UnitTests/StyleActivatorExtensions.cs @@ -20,13 +20,17 @@ public static async Task Take(this IStyleActivator activator, int value) public static IObservable ToObservable(this IStyleActivator activator) { + if (activator == null) + { + throw new ArgumentNullException(nameof(activator)); + } + return new ObservableAdapter(activator); } private class ObservableAdapter : LightweightObservableBase, 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 @@ private class ObservableAdapter : LightweightObservableBase, IStyleActivat void IStyleActivatorSink.OnNext(bool value, int tag) { - _value = value; PublishNext(value); } } From 031e8ac2f0e45c91150f48f1d1d2393c7ae9606c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 16 Oct 2021 22:15:16 -0400 Subject: [PATCH 3/6] Complete --- .../Pages/ItemsRepeaterPage.xaml | 5 ++ samples/ControlCatalog/Pages/ListBoxPage.xaml | 7 +- .../Presenters/ItemsPresenterBase.cs | 7 +- .../LogicalTree/ChildIndexChangedEventArgs.cs | 3 + .../LogicalTree/IChildIndexProvider.cs | 18 +++++ .../Styling/Activators/NthChildActivator.cs | 7 +- .../Styling/NthChildSelector.cs | 12 ++++ .../Styling/NthLastChildSelector.cs | 12 ++++ src/Avalonia.Styling/Styling/Selectors.cs | 6 ++ .../Xaml/StyleTests.cs | 71 +++++++++++++------ .../SelectorTests_NthChild.cs | 4 +- .../SelectorTests_NthLastChild.cs | 4 +- 12 files changed, 125 insertions(+), 31 deletions(-) diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml index 93f3c334346..4d0bd663dfc 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml @@ -12,6 +12,11 @@ + diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index 897134badbd..cb29f54c940 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -3,8 +3,13 @@ x:Class="ControlCatalog.Pages.ListBoxPage"> - + diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index d58ef2e510f..b92af1eb9ca 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Specialized; -using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Generators; @@ -133,7 +132,7 @@ public IPanel Panel protected bool IsHosted => TemplatedParent is IItemsPresenterHost; - int? IChildIndexProvider.TotalCount => Items.TryGetCountFast(out var count) ? count : null; + int? IChildIndexProvider.TotalCount => Items.TryGetCountFast(out var count) ? count : (int?)null; event EventHandler IChildIndexProvider.ChildIndexChanged { @@ -161,6 +160,8 @@ void IItemsPresenter.ItemsChanged(NotifyCollectionChangedEventArgs e) if (Panel != null) { ItemsChanged(e); + + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs()); } } @@ -192,7 +193,7 @@ private void ContainerActionHandler(object sender, ItemContainerEventArgs e) { for (var i = 0; i < e.Containers.Count; i++) { - _childIndexChanged?.Invoke(sender, new ChildIndexChangedEventArgs(e.Containers[i].ContainerControl)); + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(e.Containers[i].ContainerControl)); } } diff --git a/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs b/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs index 1c90851e133..de41f5292c2 100644 --- a/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs +++ b/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs @@ -3,6 +3,9 @@ namespace Avalonia.LogicalTree { + /// + /// Event args for event. + /// public class ChildIndexChangedEventArgs : EventArgs { public ChildIndexChangedEventArgs() diff --git a/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs b/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs index fdba99baa23..53e2199d28e 100644 --- a/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs +++ b/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs @@ -3,12 +3,30 @@ namespace Avalonia.LogicalTree { + /// + /// Child's index and total count information provider used by list-controls (ListBox, StackPanel, etc.) + /// + /// + /// Used by nth-child and nth-last-child selectors. + /// public interface IChildIndexProvider { + /// + /// Gets child's actual index in order of the original source. + /// + /// Logical child. + /// Index or -1 if child was not found. int GetChildIndex(ILogical child); + /// + /// Total children count or null if source is infinite. + /// Some Avalonia features might not work if is null, for instance: nth-last-child selector. + /// int? TotalCount { get; } + /// + /// Notifies subscriber when child's index or total count was changed. + /// event EventHandler? ChildIndexChanged; } } diff --git a/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs b/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs index 34cca1a396c..5d23d1ffd1a 100644 --- a/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs +++ b/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs @@ -44,7 +44,12 @@ protected override void Deinitialize() private void ChildIndexChanged(object sender, ChildIndexChangedEventArgs e) { - if (e.Child is null + // 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()); diff --git a/src/Avalonia.Styling/Styling/NthChildSelector.cs b/src/Avalonia.Styling/Styling/NthChildSelector.cs index 16b97e22f62..e844fb51f84 100644 --- a/src/Avalonia.Styling/Styling/NthChildSelector.cs +++ b/src/Avalonia.Styling/Styling/NthChildSelector.cs @@ -7,6 +7,12 @@ namespace Avalonia.Styling { + /// + /// The :nth-child() pseudo-class matches elements based on their position in a group of siblings. + /// + /// + /// Element indices are 1-based. + /// public class NthChildSelector : Selector { private const string NthChildSelectorName = "nth-child"; @@ -22,6 +28,12 @@ internal protected NthChildSelector(Selector? previous, int step, int offset, bo _reversed = reversed; } + /// + /// Creates an instance of + /// + /// Previous selector. + /// Position step. + /// Initial index offset. public NthChildSelector(Selector? previous, int step, int offset) : this(previous, step, offset, false) { diff --git a/src/Avalonia.Styling/Styling/NthLastChildSelector.cs b/src/Avalonia.Styling/Styling/NthLastChildSelector.cs index ff7cf0faa1e..6f6abbae6a8 100644 --- a/src/Avalonia.Styling/Styling/NthLastChildSelector.cs +++ b/src/Avalonia.Styling/Styling/NthLastChildSelector.cs @@ -2,8 +2,20 @@ namespace Avalonia.Styling { + /// + /// The :nth-child() pseudo-class matches elements based on their position among a group of siblings, counting from the end. + /// + /// + /// Element indices are 1-based. + /// public class NthLastChildSelector : NthChildSelector { + /// + /// Creates an instance of + /// + /// Previous selector. + /// Position step. + /// Initial index offset, counting from the end. public NthLastChildSelector(Selector? previous, int step, int offset) : base(previous, step, offset, true) { } diff --git a/src/Avalonia.Styling/Styling/Selectors.cs b/src/Avalonia.Styling/Styling/Selectors.cs index 0bccccbd7c6..64d0a0e96b3 100644 --- a/src/Avalonia.Styling/Styling/Selectors.cs +++ b/src/Avalonia.Styling/Styling/Selectors.cs @@ -123,11 +123,17 @@ public static Selector Not(this Selector previous, Selector argument) return new NotSelector(previous, argument); } + /// + /// + /// The selector. public static Selector NthChild(this Selector previous, int step, int offset) { return new NthChildSelector(previous, step, offset); } + /// + /// + /// The selector. public static Selector NthLastChild(this Selector previous, int step, int offset) { return new NthLastChildSelector(previous, step, offset); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index ee633ee66fb..28960c8bf6c 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; @@ -336,6 +337,45 @@ public void Style_Can_Use_NthChild_Selector_After_Reoder() } } + [Fact] + public void Style_Can_Use_NthLastChild_Selector_After_Reoder() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + + var parent = window.FindControl("parent"); + var b1 = window.FindControl("b1"); + var b2 = window.FindControl("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() @@ -364,25 +404,18 @@ public void Style_Can_Use_NthChild_Selector_With_ListBox() window.Show(); - var items = list.Presenter.Panel.Children.Cast(); - ListBoxItem At(int index) => items.ElementAt(index); + IEnumerable GetColors() => list.Presenter.Panel.Children.Cast().Select(t => t.Background); - Assert.Equal(Brushes.Transparent, At(0).Background); - Assert.Equal(Brushes.Green, At(1).Background); - Assert.Equal(Brushes.Transparent, At(2).Background); + Assert.Equal(new[] { Brushes.Transparent, Brushes.Green, Brushes.Transparent }, GetColors()); collection.Remove(Brushes.Green); - Assert.Equal(Brushes.Transparent, At(0).Background); - Assert.Equal(Brushes.Blue, At(1).Background); + Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue }, GetColors()); collection.Add(Brushes.Violet); collection.Add(Brushes.Black); - Assert.Equal(Brushes.Transparent, At(0).Background); - Assert.Equal(Brushes.Blue, At(1).Background); - Assert.Equal(Brushes.Transparent, At(2).Background); - Assert.Equal(Brushes.Black, At(3).Background); + Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue, Brushes.Transparent, Brushes.Black }, GetColors()); } } @@ -415,25 +448,19 @@ public void Style_Can_Use_NthChild_Selector_With_ItemsRepeater() window.Show(); - var items = list.Children; - TextBlock At(int index) => (TextBlock)list.GetOrCreateElement(index); + IEnumerable GetColors() => Enumerable.Range(0, list.ItemsSourceView.Count) + .Select(t => (list.GetOrCreateElement(t) as TextBlock)!.Foreground); - Assert.Equal(Brushes.Transparent, At(0).Foreground); - Assert.Equal(Brushes.Green, At(1).Foreground); - Assert.Equal(Brushes.Transparent, At(2).Foreground); + Assert.Equal(new[] { Brushes.Transparent, Brushes.Green, Brushes.Transparent }, GetColors()); collection.Remove(Brushes.Green); - Assert.Equal(Brushes.Transparent, At(0).Foreground); - Assert.Equal(Brushes.Blue, At(1).Foreground); + Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue }, GetColors()); collection.Add(Brushes.Violet); collection.Add(Brushes.Black); - Assert.Equal(Brushes.Transparent, At(0).Foreground); - Assert.Equal(Brushes.Blue, At(1).Foreground); - Assert.Equal(Brushes.Transparent, At(2).Foreground); - Assert.Equal(Brushes.Black, At(3).Foreground); + Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue, Brushes.Transparent, Brushes.Black }, GetColors()); } } diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs index a70b3c9f293..8a8e46fc4bd 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs @@ -196,7 +196,7 @@ public async Task Nth_Child_Match_Control_In_Panel_With_Previous_Selector() } [Fact] - public async Task Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() + public void Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() { Border b1; var contentControl = new ContentControl(); @@ -204,7 +204,7 @@ public async Task Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() var target = default(Selector).NthChild(1, 0); - Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.Equal(SelectorMatch.NeverThisInstance, target.Match(b1)); } [Fact] diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs index ed88106295f..8d9d490724d 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs @@ -195,7 +195,7 @@ public async Task Nth_Child_Match_Control_In_Panel_With_Previous_Selector() } [Fact] - public async Task Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() + public void Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() { Border b1; var contentControl = new ContentControl(); @@ -203,7 +203,7 @@ public async Task Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() var target = default(Selector).NthLastChild(1, 0); - Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.Equal(SelectorMatch.NeverThisInstance, target.Match(b1)); } [Fact] From f276c4ed8b992017003d94a863128effdd068fe2 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 21 Oct 2021 18:50:02 -0400 Subject: [PATCH 4/6] Changes after review --- samples/ControlCatalog/Pages/ListBoxPage.xaml | 2 +- src/Avalonia.Controls/ItemsControl.cs | 15 ++++++++++++--- src/Avalonia.Controls/Panel.cs | 10 ++++++---- .../Presenters/ItemsPresenterBase.cs | 8 +++++--- src/Avalonia.Controls/Repeater/ItemsRepeater.cs | 10 ++++++---- .../LogicalTree/IChildIndexProvider.cs | 4 ++-- .../Styling/Activators/NthChildActivator.cs | 9 ++------- src/Avalonia.Styling/Styling/NthChildSelector.cs | 9 ++++++--- .../Xaml/StyleTests.cs | 2 -- .../SelectorTests_NthChild.cs | 2 -- .../SelectorTests_NthLastChild.cs | 1 - 11 files changed, 40 insertions(+), 32 deletions(-) diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index cb29f54c940..b36629fb2ae 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -15,7 +15,7 @@ ListBox Hosts a collection of ListBoxItem. - Each 2nd item is highlighted + Each 5th item is highlighted with nth-child(5n+3) and nth-last-child(5n+4) rules. Multiple diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 7b28335a6d0..1ff49326b67 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -13,7 +13,6 @@ using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Metadata; -using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -147,8 +146,6 @@ public IItemsPresenter Presenter protected set; } - int? IChildIndexProvider.TotalCount => (Presenter as IChildIndexProvider)?.TotalCount ?? ItemCount; - event EventHandler IChildIndexProvider.ChildIndexChanged { add => _childIndexChanged += value; @@ -538,5 +535,17 @@ 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; + } } } diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 9c93126506e..b182f9d2610 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; - -using Avalonia.Controls.Presenters; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; @@ -59,8 +57,6 @@ public IBrush Background set { SetValue(BackgroundProperty, value); } } - int? IChildIndexProvider.TotalCount => Children.Count; - event EventHandler IChildIndexProvider.ChildIndexChanged { add => _childIndexChanged += value; @@ -180,5 +176,11 @@ 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; + } } } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index b92af1eb9ca..aeead7bfd0e 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Specialized; - using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Templates; @@ -132,8 +131,6 @@ public IPanel Panel protected bool IsHosted => TemplatedParent is IItemsPresenterHost; - int? IChildIndexProvider.TotalCount => Items.TryGetCountFast(out var count) ? count : (int?)null; - event EventHandler IChildIndexProvider.ChildIndexChanged { add => _childIndexChanged += value; @@ -285,5 +282,10 @@ int IChildIndexProvider.GetChildIndex(ILogical child) return -1; } + + bool IChildIndexProvider.TryGetTotalCount(out int count) + { + return Items.TryGetCountFast(out count); + } } } diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 6d89a706700..ecc0fa3a484 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -6,13 +6,11 @@ using System; using System.Collections; using System.Collections.Specialized; - using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Logging; using Avalonia.LogicalTree; -using Avalonia.Styling; using Avalonia.Utilities; using Avalonia.VisualTree; @@ -167,8 +165,6 @@ private LayoutContext LayoutContext } } - int? IChildIndexProvider.TotalCount => ItemsSourceView.Count; - event EventHandler IChildIndexProvider.ChildIndexChanged { add => _childIndexChanged += value; @@ -182,6 +178,12 @@ int IChildIndexProvider.GetChildIndex(ILogical child) : -1; } + bool IChildIndexProvider.TryGetTotalCount(out int count) + { + count = ItemsSourceView.Count; + return true; + } + /// /// Occurs each time an element is cleared and made available to be re-used. /// diff --git a/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs b/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs index 53e2199d28e..7fcd73273c7 100644 --- a/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs +++ b/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs @@ -20,9 +20,9 @@ public interface IChildIndexProvider /// /// Total children count or null if source is infinite. - /// Some Avalonia features might not work if is null, for instance: nth-last-child selector. + /// Some Avalonia features might not work if returns false, for instance: nth-last-child selector. /// - int? TotalCount { get; } + bool TryGetTotalCount(out int count); /// /// Notifies subscriber when child's index or total count was changed. diff --git a/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs b/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs index 5d23d1ffd1a..803809a8ce5 100644 --- a/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs +++ b/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs @@ -1,6 +1,4 @@ #nullable enable -using System; - using Avalonia.LogicalTree; namespace Avalonia.Styling.Activators @@ -15,7 +13,6 @@ internal sealed class NthChildActivator : StyleActivatorBase private readonly int _step; private readonly int _offset; private readonly bool _reversed; - private EventHandler? _childIndexChangedHandler; public NthChildActivator( ILogical control, @@ -29,17 +26,15 @@ public NthChildActivator( _reversed = reversed; } - private EventHandler ChildIndexChangedHandler => _childIndexChangedHandler ??= ChildIndexChanged; - protected override void Initialize() { PublishNext(IsMatching()); - _provider.ChildIndexChanged += ChildIndexChangedHandler; + _provider.ChildIndexChanged += ChildIndexChanged; } protected override void Deinitialize() { - _provider.ChildIndexChanged -= ChildIndexChangedHandler; + _provider.ChildIndexChanged -= ChildIndexChanged; } private void ChildIndexChanged(object sender, ChildIndexChangedEventArgs e) diff --git a/src/Avalonia.Styling/Styling/NthChildSelector.cs b/src/Avalonia.Styling/Styling/NthChildSelector.cs index e844fb51f84..aff34ea17ce 100644 --- a/src/Avalonia.Styling/Styling/NthChildSelector.cs +++ b/src/Avalonia.Styling/Styling/NthChildSelector.cs @@ -1,7 +1,6 @@ #nullable enable using System; using System.Text; - using Avalonia.LogicalTree; using Avalonia.Styling.Activators; @@ -51,7 +50,11 @@ public NthChildSelector(Selector? previous, int step, int offset) protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) { - var logical = (ILogical)control; + if (!(control is ILogical logical)) + { + return SelectorMatch.NeverThisType; + } + var controlParent = logical.LogicalParent; if (controlParent is IChildIndexProvider childIndexProvider) @@ -78,7 +81,7 @@ internal static SelectorMatch Evaluate( if (reversed) { - if (childIndexProvider.TotalCount is int totalCountValue) + if (childIndexProvider.TryGetTotalCount(out var totalCountValue)) { index = totalCountValue - index; } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 28960c8bf6c..022ff0c3a4e 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -1,10 +1,8 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using System.Threading.Tasks; using System.Xml; using Avalonia.Controls; -using Avalonia.Markup.Data; using Avalonia.Markup.Xaml.Styling; using Avalonia.Markup.Xaml.Templates; using Avalonia.Media; diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs index 8a8e46fc4bd..1d101b8ea05 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs @@ -1,7 +1,5 @@ using System.Threading.Tasks; - using Avalonia.Controls; - using Xunit; namespace Avalonia.Styling.UnitTests diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs index 8d9d490724d..00a99523c74 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; - using Avalonia.Controls; using Xunit; From d64a700b4fd2fae11ee3de45f83e18c4c3984562 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 21 Oct 2021 20:08:12 -0400 Subject: [PATCH 5/6] Imrpove nth-child parsing --- .../Markup/Parsers/SelectorGrammar.cs | 56 +++++++++++++-- .../Parsers/SelectorGrammarTests.cs | 69 +++++++++++++++++++ 2 files changed, 119 insertions(+), 6 deletions(-) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index 56e64329b7a..953a7e9a15a 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -350,12 +350,28 @@ private static (int step, int offset) ParseNthChildArguments(ref CharacterReader } else { - var stepOrOffsetSpan = r.TakeWhile(c => c != ')' && c != 'n'); - if (!int.TryParse(stepOrOffsetSpan.ToString().Trim(), out var stepOrOffset)) + 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; @@ -365,13 +381,41 @@ private static (int step, int offset) ParseNthChildArguments(ref CharacterReader { 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' - var offsetSpan = r.TakeUntil(')').TrimStart(); - if (offsetSpan.Length != 0 - && !int.TryParse(offsetSpan.ToString().Trim(), out offset)) + r.SkipWhitespace(); + + if (r.Peek != ')') { - throw new ExpressionParseException(r.Position, "Couldn't parse nth-child offset value. Integer was expected."); + 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; } } } diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs index 543d44c4926..568f6deaf28 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs @@ -236,6 +236,75 @@ public void OfType_Not_Class() 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(() => 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() { From bab044980569a7f80ead99792d48da4969a2fd0f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 21 Oct 2021 20:46:24 -0400 Subject: [PATCH 6/6] Added tests from nthmaster.com --- .../SelectorTests_NthChild.cs | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs index 1d101b8ea05..e1507be1102 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; using Xunit; @@ -205,6 +206,76 @@ public void Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() 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(); + 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(); + 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(); + 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() {