Skip to content

Commit

Permalink
FIrst version of nth-child
Browse files Browse the repository at this point in the history
  • Loading branch information
maxkatz6 committed Aug 8, 2021
1 parent f952406 commit 39a68e5
Show file tree
Hide file tree
Showing 14 changed files with 955 additions and 12 deletions.
6 changes: 6 additions & 0 deletions samples/ControlCatalog/Pages/ListBoxPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.ListBoxPage">
<DockPanel>
<DockPanel.Styles>
<Style Selector="ListBox ListBoxItem:nth-child(2n)">
<Setter Property="Foreground" Value="Blue" />
</Style>
</DockPanel.Styles>
<StackPanel DockPanel.Dock="Top" Margin="4">
<TextBlock Classes="h1">ListBox</TextBlock>
<TextBlock Classes="h2">Hosts a collection of ListBoxItem.</TextBlock>
<TextBlock Classes="h2">Each 2nd item is highlighted</TextBlock>
</StackPanel>
<StackPanel DockPanel.Dock="Right" Margin="4">
<CheckBox IsChecked="{Binding Multiple}">Multiple</CheckBox>
Expand Down
19 changes: 18 additions & 1 deletion src/Avalonia.Controls/ItemsControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.Styling;
using Avalonia.VisualTree;

namespace Avalonia.Controls
Expand All @@ -21,7 +22,7 @@ namespace Avalonia.Controls
/// Displays a collection of items.
/// </summary>
[PseudoClasses(":empty", ":singleitem")]
public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener
public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener, IChildIndexProvider
{
/// <summary>
/// The default value for the <see cref="ItemsPanel"/> property.
Expand Down Expand Up @@ -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);
}
}
}
17 changes: 16 additions & 1 deletion src/Avalonia.Controls/Panel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -14,7 +18,7 @@ namespace Avalonia.Controls
/// Controls can be added to a <see cref="Panel"/> by adding them to its <see cref="Children"/>
/// collection. All children are layed out to fill the panel.
/// </remarks>
public class Panel : Control, IPanel
public class Panel : Control, IPanel, IChildIndexProvider
{
/// <summary>
/// Defines the <see cref="Background"/> property.
Expand Down Expand Up @@ -160,5 +164,16 @@ private static void AffectsParentMeasureInvalidate<TPanel>(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);
}
}
}
25 changes: 24 additions & 1 deletion src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
using Avalonia.Controls.Generators;
using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils;
using Avalonia.LogicalTree;
using Avalonia.Styling;

namespace Avalonia.Controls.Presenters
{
/// <summary>
/// Base class for controls that present items inside an <see cref="ItemsControl"/>.
/// </summary>
public abstract class ItemsPresenterBase : Control, IItemsPresenter, ITemplatedControl
public abstract class ItemsPresenterBase : Control, IItemsPresenter, ITemplatedControl, IChildIndexProvider
{
/// <summary>
/// Defines the <see cref="Items"/> property.
Expand Down Expand Up @@ -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);
}
}
}
27 changes: 20 additions & 7 deletions src/Avalonia.Controls/Utils/IEnumerableUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<object> readOnly)
{
return readOnly.Count;
}
else
{
return Enumerable.Count(items.Cast<object>());
count = readOnly.Count;
return true;
}
}

count = 0;
return false;
}

public static int Count(this IEnumerable items)
{
if (TryGetCountFast(items, out var count))
{
return count;
}
else if (items != null)
{
return Enumerable.Count(items.Cast<object>());
}
else
{
return 0;
Expand Down
134 changes: 134 additions & 0 deletions src/Avalonia.Styling/Styling/NthChildSelector.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
10 changes: 10 additions & 0 deletions src/Avalonia.Styling/Styling/Selectors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/// <summary>
/// Returns a selector which matches a type.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ XamlIlSelectorNode Create(IEnumerable<SelectorGrammar.ISyntax> 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);
Expand Down Expand Up @@ -273,6 +279,35 @@ protected override void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitRes
}
}

class XamlIlNthChildSelector : XamlIlSelectorNode
{
private readonly int _step;
private readonly int _offset;
private readonly SelectorType _type;

public enum SelectorType
{
NthChild,
NthLastChild
}

public XamlIlNthChildSelector(XamlIlSelectorNode previous, int step, int offset, SelectorType type) : base(previous)
{
_step = step;
_offset = offset;
_type = type;
}

public override IXamlType TargetType => Previous?.TargetType;
protected override void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
{
codeGen.Ldc_I4(_step);
codeGen.Ldc_I4(_offset);
EmitCall(context, codeGen,
m => m.Name == _type.ToString() && m.Parameters.Count == 3);
}
}

class XamlIlPropertyEqualsSelector : XamlIlSelectorNode
{
public XamlIlPropertyEqualsSelector(XamlIlSelectorNode previous,
Expand Down
Loading

0 comments on commit 39a68e5

Please sign in to comment.