Skip to content

Commit

Permalink
Merge pull request #10055 from AvaloniaUI/fixes/9997-nth-last-child-i…
Browse files Browse the repository at this point in the history
…temscontrol

Fix nth-last-child styles on virtualizing layouts
  • Loading branch information
maxkatz6 authored Feb 23, 2023
2 parents 80394d0 + 5849167 commit bd5865f
Show file tree
Hide file tree
Showing 13 changed files with 196 additions and 46 deletions.
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

0 comments on commit bd5865f

Please sign in to comment.