using Microsoft.AspNetCore.Components; using System.Collections; namespace Sandbox.UI; /// /// Base class for virtualized, scrollable panels that only create item panels when visible. /// public abstract class BaseVirtualPanel : Panel { protected readonly Dictionary _cellData = new(); // index -> last data used to build the cell protected readonly Dictionary _created = new(); // index -> created panel protected readonly List _removals = new(); // temp list for removal without mutating during iteration protected readonly List _items = new(); // backing store for Items /// /// When setting the items from a List we keep a reference to it here for change detection. /// private IList _sourceList; private int _sourceListCount; protected bool _lastCellCreated; /// /// When true, forces a layout rebuild on the next . /// public bool NeedsRebuild { get; set; } /// /// Template used to render an item into a cell panel. /// [Parameter] public RenderFragment Item { get; set; } /// /// Called when a cell is created. Allows you to fill the cell in /// [Parameter] public Action OnCreateCell { get; set; } /// /// Called when the last cell has been viewed. This allows you to view more. /// [Parameter] public Action OnLastCell { get; set; } /// /// Initializes the base virtual panel with default styles. /// protected BaseVirtualPanel() { Style.Position = PositionMode.Relative; Style.Overflow = OverflowMode.Scroll; } /// /// Replaces the current items. Only triggers a rebuild if the sequence is actually different. /// When set to an IList (like List<T>), changes to the source list will be automatically detected. /// [Parameter] public IEnumerable Items { set { if ( value is null ) { if ( _items.Count == 0 ) return; Clear(); return; } // Try to keep a reference to the original list for change detection _sourceList = value as IList; // Materialize the items - use _sourceList if available to avoid double enumeration IList materializedList; if ( _sourceList != null ) { // Cast from non-generic IList to List materializedList = new List( _sourceList.Count ); foreach ( var item in _sourceList ) materializedList.Add( item ); } else { // Fallback for pure IEnumerable materializedList = value.ToList(); _sourceList = null; _sourceListCount = 0; } _sourceListCount = materializedList.Count; // Fast length check, then content equality with the configured comparer. if ( _items.Count == materializedList.Count && _items.SequenceEqual( materializedList, EqualityComparer.Default ) ) return; _items.Clear(); _items.AddRange( materializedList ); NeedsRebuild = true; _lastCellCreated = false; StateHasChanged(); } } /// /// Adds a single item and marks the panel for rebuild. /// /// The item to append. public void AddItem( object item ) { _items.Add( item ); NeedsRebuild = true; } /// /// Adds multiple items and marks the panel for rebuild. /// /// Items to append. public void AddItems( IEnumerable items ) { _items.AddRange( items as IList ?? items.ToList() ); NeedsRebuild = true; } /// /// Removes the first occurrence of a specific item and marks the panel for rebuild. /// /// The item to remove. /// True if item was found and removed; otherwise false. public bool RemoveItem( object item ) { var removed = _items.Remove( item ); if ( removed ) NeedsRebuild = true; return removed; } /// /// Removes the item at the specified index and marks the panel for rebuild. /// /// The zero-based index of the item to remove. public void RemoveAt( int index ) { _items.RemoveAt( index ); NeedsRebuild = true; } /// /// Inserts an item at the specified index and marks the panel for rebuild. /// /// The zero-based index at which item should be inserted. /// The item to insert. public void InsertItem( int index, object item ) { _items.Insert( index, item ); NeedsRebuild = true; } /// /// Clears all items and destroys created panels. /// public void Clear() { _items.Clear(); _sourceList = null; _sourceListCount = 0; NeedsRebuild = true; foreach ( var p in _created.Values ) p.Delete( true ); _created.Clear(); _cellData.Clear(); } /// /// Checks the source list for changes and updates the internal items if needed. /// private void CheckSourceForChanges() { if ( _sourceList is null ) return; if ( _sourceList.Count == _sourceListCount ) return; // Count has changed - update our cached items _items.Clear(); _sourceListCount = _sourceList.Count; // Cast items from the non-generic IList to object foreach ( var item in _sourceList ) _items.Add( item ); NeedsRebuild = true; _lastCellCreated = false; StateHasChanged(); } /// /// Per-frame update: adjusts spacing from CSS, updates layout, creates/destroys visible panels. /// public override void Tick() { base.Tick(); if ( ComputedStyle is null || !IsVisible ) return; CheckSourceForChanges(); // Pull CSS gaps into layout spacing. UpdateLayoutSpacing( new( ComputedStyle.ColumnGap?.Value ?? 0, ComputedStyle.RowGap?.Value ?? 0 ) ); // Recompute layout if needed or if explicitly requested. if ( UpdateLayout() || NeedsRebuild ) { NeedsRebuild = false; // Get visible index range [first, pastEnd) GetVisibleRange( out var first, out var pastEnd ); // Remove anything outside the visible window or without data. DeleteNotVisible( first, pastEnd - 1 ); // Ensure visible cells exist and are positioned. for ( int i = first; i < pastEnd; i++ ) RefreshCreated( i ); } } /// /// Final layout pass for child panels and scroll bounds. /// /// Layout offset. protected override void FinalLayoutChildren( Vector2 offset ) { foreach ( var kv in _created ) kv.Value.FinalLayout( offset ); // Extend scrollable height to fit all rows. var rect = Box.Rect; rect.Position -= ScrollOffset; rect.Height = MathF.Max( GetTotalHeight( _items.Count ) * ScaleToScreen, rect.Height ); ConstrainScrolling( rect.Size ); } /// /// Returns true if is a valid item index. /// /// Item index. /// True if within bounds; otherwise false. public bool HasData( int i ) => i >= 0 && i < _items.Count; /// /// Gets the number of items in the panel. /// public int ItemCount => _items.Count; /// /// Convenience helper that sets . /// /// New items sequence. public void SetItems( IEnumerable enumerable ) => Items = enumerable; // Remove panels not in [minInclusive, maxInclusive] or with missing data. private void DeleteNotVisible( int minInclusive, int maxInclusive ) { _removals.Clear(); foreach ( var idx in _created.Keys ) { if ( idx < minInclusive || idx > maxInclusive || !HasData( idx ) ) _removals.Add( idx ); } for ( int i = 0; i < _removals.Count; i++ ) { var idx = _removals[i]; if ( _created.Remove( idx, out var panel ) ) panel.Delete( true ); _cellData.Remove( idx ); } _removals.Clear(); } // Ensure a panel exists for index i, rebuilding if data changed, then position it. private void RefreshCreated( int i ) { if ( !HasData( i ) ) return; var data = _items[i]; var needsRebuild = !_cellData.TryGetValue( i, out var last ) || !EqualityComparer.Default.Equals( last, data ); if ( !_created.TryGetValue( i, out var panel ) || needsRebuild ) { panel?.Delete( true ); panel = Add.Panel( "cell" ); panel.Style.Position = PositionMode.Absolute; panel.ChildContent = Item?.Invoke( data ); _created[i] = panel; _cellData[i] = data; OnCreateCell?.Invoke( panel, data ); if ( _items.Count - 1 == i ) { OnCreatedLastCell(); } } PositionPanel( i, panel ); } private void OnCreatedLastCell() { if ( _lastCellCreated ) return; _lastCellCreated = true; OnLastCell?.InvokeWithWarning(); } /// /// Updates the layout spacing based on CSS gaps. /// /// The spacing vector from CSS. protected abstract void UpdateLayoutSpacing( Vector2 spacing ); /// /// Updates the layout and returns true if the layout changed. /// /// True if layout was updated; otherwise false. protected abstract bool UpdateLayout(); /// /// Gets the range of visible item indices. /// /// First visible index (inclusive). /// Past-the-end index (exclusive). protected abstract void GetVisibleRange( out int first, out int pastEnd ); /// /// Positions a panel at the specified index. /// /// Item index. /// Panel to position. protected abstract void PositionPanel( int index, Panel panel ); /// /// Gets the total height needed to display the specified number of items. /// /// Number of items. /// Total height in layout units. protected abstract float GetTotalHeight( int itemCount ); }