A generic pool of reusable row slots for a virtualized list UI. It keeps a fixed set of Slot objects sized to the largest visible window, maps visible item indices into those slots with a stable render position, and exposes a Select callback per slot that invokes the provided selection action for the current bound index.
using System;
using System.Collections.Generic;
namespace BlockParty;
/// <summary>
/// A screen-owned pool for a contiguous virtual list window. Render every slot at a stable
/// location in the render tree, hiding inactive slots instead of removing their panels.
/// Only grows to the largest visible window; it never keeps a panel for every list item.
/// </summary>
internal sealed class VirtualRowPool<T>
{
internal sealed class Slot
{
public int Index { get; internal set; }
public T Item { get; internal set; }
public bool Active { get; internal set; }
public Action Select { get; }
internal Slot( Action<int> select )
{
// Read the current binding when clicked, never the index from panel creation.
Select = () => { if ( Active ) select( Index ); };
}
}
private readonly List<Slot> _slots = new();
private readonly Action<int> _select;
private IReadOnlyList<T> _source;
private int _sourceCount;
public IReadOnlyList<Slot> Slots => _slots;
public VirtualRowPool( Action<int> select ) => _select = select;
/// <summary>Updates bindings without allocating once the pool is large enough. Returns true
/// when entries change position/source/visibility, so the host can dismiss stale hover tips.</summary>
public bool Update( IReadOnlyList<T> items, int first, int last )
{
first = Math.Clamp( first, 0, items.Count );
last = Math.Clamp( last, first, items.Count );
bool changed = !ReferenceEquals( _source, items ) || _sourceCount != items.Count;
_source = items;
_sourceCount = items.Count;
while ( _slots.Count < last - first )
{
_slots.Add( new Slot( _select ) );
changed = true;
}
int capacity = _slots.Count;
for ( int s = 0; s < capacity; s++ )
{
var slot = _slots[s];
// Modulo keeps overlapping rows on their existing panels. Only rows that leave
// the window are rebound, including after a large scrollbar jump.
int index = first + (s - first % capacity + capacity) % capacity;
bool active = index < last;
changed |= slot.Active != active || (active && slot.Index != index);
slot.Active = active;
if ( !active ) continue; // Keep the hidden subtree's previous parameters intact.
slot.Index = index;
slot.Item = items[index];
}
return changed;
}
}