UI/VirtualRowPool.cs

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.

Reflection
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;
	}
}