Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix nth-last-child styles on virtualizing layouts #10055

Merged
merged 11 commits into from
Feb 23, 2023
2 changes: 1 addition & 1 deletion samples/ControlCatalog/Pages/ListBoxPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<Setter Property="FontWeight" Value="Bold" />
</Style>
<Style Selector="ListBox ListBoxItem:nth-last-child(5n+4)">
<Setter Property="Foreground" Value="Blue" />
<Setter Property="Background" Value="Blue" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
</DockPanel.Styles>
Expand Down
69 changes: 60 additions & 9 deletions src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,79 @@
#nullable enable
using System;
using System;

#nullable enable

namespace Avalonia.LogicalTree
{
/// <summary>
/// Describes the action that caused a <see cref="IChildIndexProvider.ChildIndexChanged"/> event.
/// </summary>
public enum ChildIndexChangedAction
{
/// <summary>
/// The index of a single child changed.
/// </summary>
ChildIndexChanged,

/// <summary>
/// The index of multiple children changed and all children should be re-evaluated.
/// </summary>
ChildIndexesReset,

/// <summary>
/// The total number of children changed.
/// </summary>
TotalCountChanged,
}

/// <summary>
/// Event args for <see cref="IChildIndexProvider.ChildIndexChanged"/> event.
/// </summary>
public class ChildIndexChangedEventArgs : EventArgs
{
public static new ChildIndexChangedEventArgs Empty { get; } = new ChildIndexChangedEventArgs();

private ChildIndexChangedEventArgs()
/// <summary>
/// Initializes a new instance of the <see cref="ChildIndexChangedEventArgs"/> class with
/// an action of <see cref="ChildIndexChangedAction.ChildIndexChanged"/>.
/// </summary>
/// <param name="child">The child whose index was changed.</param>
/// <param name="index">The new index of the child.</param>
public ChildIndexChangedEventArgs(ILogical child, int index)
{
Action = ChildIndexChangedAction.ChildIndexChanged;
Child = child;
Index = index;
}

public ChildIndexChangedEventArgs(ILogical child)
private ChildIndexChangedEventArgs(ChildIndexChangedAction action)
{
Child = child;
Action = action;
Index = -1;
}

/// <summary>
/// Logical child which index was changed.
/// If null, all children should be reset.
/// Gets the type of change action that ocurred on the list control.
/// </summary>
public ChildIndexChangedAction Action { get; }

/// <summary>
/// Gets the logical child whose index was changed or null if all children should be re-evaluated.
/// </summary>
public ILogical? Child { get; }

/// <summary>
/// Gets the new index of <see cref="Child"/> or -1 if all children should be re-evaluated.
/// </summary>
public int Index { get; }

/// <summary>
/// Gets an instance of the <see cref="ChildIndexChangedEventArgs"/> with an action of
/// <see cref="ChildIndexChangedAction.ChildIndexesReset"/>.
/// </summary>
public static ChildIndexChangedEventArgs ChildIndexesReset { get; } = new(ChildIndexChangedAction.ChildIndexesReset);

/// <summary>
/// Gets an instance of the <see cref="ChildIndexChangedEventArgs"/> with an action of
/// <see cref="ChildIndexChangedAction.TotalCountChanged"/>.
/// </summary>
public static ChildIndexChangedEventArgs TotalCountChanged { get; } = new(ChildIndexChangedAction.TotalCountChanged);
}
}
2 changes: 1 addition & 1 deletion src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public interface IChildIndexProvider
bool TryGetTotalCount(out int count);

/// <summary>
/// Notifies subscriber when child's index or total count was changed.
/// Notifies subscriber when a child's index was changed.
/// </summary>
event EventHandler<ChildIndexChangedEventArgs>? ChildIndexChanged;
}
Expand Down
47 changes: 38 additions & 9 deletions src/Avalonia.Base/Styling/Activators/NthChildActivator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#nullable enable
using System;
using Avalonia.LogicalTree;

namespace Avalonia.Styling.Activators
Expand All @@ -13,6 +14,7 @@ internal sealed class NthChildActivator : StyleActivatorBase
private readonly int _step;
private readonly int _offset;
private readonly bool _reversed;
private int _index = -1;

public NthChildActivator(
ILogical control,
Expand All @@ -28,24 +30,51 @@ public NthChildActivator(

protected override bool EvaluateIsActive()
{
return NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch;
var index = _index >= 0 ? _index : _provider.GetChildIndex(_control);
return NthChildSelector.Evaluate(index, _provider, _step, _offset, _reversed).IsMatch;
}

protected override void Initialize() => _provider.ChildIndexChanged += ChildIndexChanged;
protected override void Deinitialize() => _provider.ChildIndexChanged -= ChildIndexChanged;
protected override void Initialize()
{
_provider.ChildIndexChanged += ChildIndexChanged;
}

protected override void Deinitialize()
{
_provider.ChildIndexChanged -= ChildIndexChanged;
}

private void ChildIndexChanged(object? sender, ChildIndexChangedEventArgs e)
{
// Run matching again if:
// 1. Selector is reversed, so other item insertion/deletion might affect total count without changing subscribed item index.
// 2. e.Child is null, when all children indices were changed.
// 3. Subscribed child index was changed.
if (_reversed
|| e.Child is null
|| e.Child == _control)
// 1. Subscribed child index was changed
// 2. Child indexes were reset
// 3. We're a reversed (nth-last-child) selector and total count has changed
if ((e.Child == _control || e.Action == ChildIndexChangedAction.ChildIndexesReset) ||
(_reversed && e.Action == ChildIndexChangedAction.TotalCountChanged))
{
// We're using the _index field to pass the index of the child to EvaluateIsActive
// *only* when the active state is re-evaluated via this event handler. The docs
// for EvaluateIsActive say:
//
// > This method should read directly from its inputs and not rely on any
// > subscriptions to fire in order to be up-to-date.
//
// Which is good advice in general, however in this case we need to break the rule
// and use the value from the event subscription instead of calling
// IChildIndexProvider.GetChildIndex. This is because this event can be fired during
// the process of realizing an element of a virtualized list; in this case calling
// GetChildIndex may not return the correct index as the element isn't yet realized.
_index = e.Index;
ReevaluateIsActive();
_index = -1;
}
}

private void TotalCountChanged(object? sender, EventArgs e)
{
if (_reversed)
ReevaluateIsActive();
}
}
}
5 changes: 2 additions & 3 deletions src/Avalonia.Base/Styling/NthChildSelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent,
{
return subscribe
? new SelectorMatch(new NthChildActivator(logical, childIndexProvider, Step, Offset, _reversed))
: Evaluate(logical, childIndexProvider, Step, Offset, _reversed);
: Evaluate(childIndexProvider.GetChildIndex(logical), childIndexProvider, Step, Offset, _reversed);
}
else
{
Expand All @@ -70,10 +70,9 @@ protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent,
}

internal static SelectorMatch Evaluate(
ILogical logical, IChildIndexProvider childIndexProvider,
int index, IChildIndexProvider childIndexProvider,
int step, int offset, bool reversed)
{
var index = childIndexProvider.GetChildIndex(logical);
if (index < 0)
{
return SelectorMatch.NeverThisInstance;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ internal void InvalidateDesiredHeight()

internal void InvalidateChildIndex()
{
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty);
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset);
}

private bool ShouldDisplayCell(DataGridColumn column, double frozenLeftEdge, double scrollingLeftEdge)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ protected override void ChildrenChanged(object sender, NotifyCollectionChangedEv

internal void InvalidateChildIndex()
{
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty);
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ bool IChildIndexProvider.TryGetTotalCount(out int count)

internal void InvalidateChildIndex(DataGridRow row)
{
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(row));
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(row, row.Index));
}

/// <summary>
Expand Down
9 changes: 5 additions & 4 deletions src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -536,11 +536,12 @@ private Control GetOrCreateElementImpl(int index)

internal void OnElementPrepared(Control element, VirtualizationInfo virtInfo)
{
var index = virtInfo.Index;

_viewportManager.OnElementPrepared(element, virtInfo);

if (ElementPrepared != null)
{
var index = virtInfo.Index;

if (_elementPreparedArgs == null)
{
Expand All @@ -554,7 +555,7 @@ internal void OnElementPrepared(Control element, VirtualizationInfo virtInfo)
ElementPrepared(this, _elementPreparedArgs);
}

_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element));
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, index));
}

internal void OnElementClearing(Control element)
Expand All @@ -573,7 +574,7 @@ internal void OnElementClearing(Control element)
ElementClearing(this, _elementClearingArgs);
}

_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element));
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, -1));
}

internal void OnElementIndexChanged(Control element, int oldIndex, int newIndex)
Expand All @@ -592,7 +593,7 @@ internal void OnElementIndexChanged(Control element, int oldIndex, int newIndex)
ElementIndexChanged(this, _elementIndexChangedArgs);
}

_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element));
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, newIndex));
}

private void OnDataSourcePropertyChanged(ItemsSourceView? oldValue, ItemsSourceView? newValue)
Expand Down
15 changes: 4 additions & 11 deletions src/Avalonia.Controls/ItemsControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ public IBinding? DisplayMemberBinding
private ItemContainerGenerator? _itemContainerGenerator;
private EventHandler<ChildIndexChangedEventArgs>? _childIndexChanged;
private IDataTemplate? _displayMemberItemTemplate;
private Tuple<int, Control>? _containerBeingPrepared;
private ScrollViewer? _scrollViewer;
private ItemsPresenter? _itemsPresenter;

Expand Down Expand Up @@ -218,7 +217,6 @@ event EventHandler<ChildIndexChangedEventArgs>? IChildIndexProvider.ChildIndexCh
remove => _childIndexChanged -= value;
}


/// <inheritdoc />
public event EventHandler<RoutedEventArgs> HorizontalSnapPointsChanged
{
Expand Down Expand Up @@ -495,6 +493,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
else if (change.Property == ItemCountProperty)
{
UpdatePseudoClasses(change.GetNewValue<int>());
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged);
}
else if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null)
{
Expand Down Expand Up @@ -579,7 +578,7 @@ internal void AddLogicalChild(Control c)
internal void RegisterItemsPresenter(ItemsPresenter presenter)
{
Presenter = presenter;
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty);
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset);
}

internal void PrepareItemContainer(Control container, object? item, int index)
Expand All @@ -601,17 +600,14 @@ internal void PrepareItemContainer(Control container, object? item, int index)

internal void ItemContainerPrepared(Control container, object? item, int index)
{
_containerBeingPrepared = new(index, container);
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container));
_containerBeingPrepared = null;

_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, index));
_scrollViewer?.RegisterAnchorCandidate(container);
}

internal void ItemContainerIndexChanged(Control container, int oldIndex, int newIndex)
{
ContainerIndexChangedOverride(container, oldIndex, newIndex);
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container));
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, newIndex));
}

internal void ClearItemContainer(Control container)
Expand Down Expand Up @@ -742,9 +738,6 @@ private void UpdatePseudoClasses(int itemCount)

int IChildIndexProvider.GetChildIndex(ILogical child)
{
if (_containerBeingPrepared?.Item2 == child)
return _containerBeingPrepared.Item1;

return child is Control container ? IndexFromContainer(container) : -1;
}

Expand Down
24 changes: 21 additions & 3 deletions src/Avalonia.Controls/Panel.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using Avalonia.LogicalTree;
using Avalonia.Media;
Expand Down Expand Up @@ -60,8 +61,19 @@ public IBrush? Background

event EventHandler<ChildIndexChangedEventArgs>? IChildIndexProvider.ChildIndexChanged
{
add => _childIndexChanged += value;
remove => _childIndexChanged -= value;
add
{
if (_childIndexChanged is null)
Children.PropertyChanged += ChildrenPropertyChanged;
_childIndexChanged += value;
}

remove
{
_childIndexChanged -= value;
if (_childIndexChanged is null)
Children.PropertyChanged -= ChildrenPropertyChanged;
}
}

/// <summary>
Expand Down Expand Up @@ -152,7 +164,7 @@ protected virtual void ChildrenChanged(object? sender, NotifyCollectionChangedEv
throw new NotSupportedException();
}

_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty);
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset);
InvalidateMeasureOnChildrenChanged();
}

Expand All @@ -161,6 +173,12 @@ private protected virtual void InvalidateMeasureOnChildrenChanged()
InvalidateMeasure();
}

private void ChildrenPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Children.Count) || e.PropertyName is null)
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged);
}

private static void AffectsParentArrangeInvalidate<TPanel>(AvaloniaPropertyChangedEventArgs e)
where TPanel : Panel
{
Expand Down
2 changes: 1 addition & 1 deletion src/Avalonia.Controls/VirtualizingStackPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1167,7 +1167,7 @@ public void ItemsRemoved(

// Update the indexes of the elements after the removed range.
end = _elements.Count;
var newIndex = first;
var newIndex = first + start;
for (var i = start; i < end; ++i)
{
if (_elements[i] is Control element)
Expand Down
Loading