diff --git a/AetherBags/Addons/AddonInventoryWindow.cs b/AetherBags/Addons/AddonInventoryWindow.cs index af9eff0..5d1b426 100644 --- a/AetherBags/Addons/AddonInventoryWindow.cs +++ b/AetherBags/Addons/AddonInventoryWindow.cs @@ -168,6 +168,8 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase addon->UnsubscribeAtkArrayData(1, (int)NumberArrayType.Inventory); + _lootedCategoryNode?.Dispose(); + IsSetupComplete = false; base.OnFinalize(addon); } diff --git a/AetherBags/Addons/InventoryAddonBase.cs b/AetherBags/Addons/InventoryAddonBase.cs index df7ed7a..7296f7a 100644 --- a/AetherBags/Addons/InventoryAddonBase.cs +++ b/AetherBags/Addons/InventoryAddonBase.cs @@ -52,6 +52,9 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow protected bool RefreshQueued; protected bool RefreshAutosizeQueued; protected bool IsSetupComplete; + private bool _deferredPopulationInProgress; + private bool _initialPopulationComplete; + private const int ItemsPerFrame = 70; protected abstract InventoryStateBase InventoryState { get; } @@ -154,17 +157,18 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow float maxContentWidth = CategoriesNode.Width > 0 ? CategoriesNode.Width : ContentSize.X; int maxItemsPerLine = CalculateOptimalItemsPerLine(maxContentWidth); + bool deferItems = !_deferredPopulationInProgress && !_initialPopulationComplete; + CategoriesNode.SyncWithListDataByKey( dataList: categories, getKeyFromData: categorizedInventory => categorizedInventory.Key, getKeyFromNode: node => node.CategorizedInventory.Key, updateNode: (node, data) => { - node.MaxWidth = maxContentWidth; - node.SetCategoryData(data, Math.Min(data.Items.Count, maxItemsPerLine)); - node.RefreshNodeVisuals(); + node.SetCategoryData(data, Math.Min(data.Items.Count, maxItemsPerLine), deferItemCreation: deferItems); + if (!deferItems) node.RefreshNodeVisuals(); }, - createNodeMethod: _ => CreateCategoryNode(maxContentWidth)); + createNodeMethod: _ => CreateCategoryNode()); if (HasPinning) { @@ -181,6 +185,72 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow LayoutContent(); CategoriesNode.RecalculateLayout(); } + + if (deferItems && !_deferredPopulationInProgress) + { + StartDeferredItemPopulation(); + } + else if (!deferItems && !_initialPopulationComplete) + { + _initialPopulationComplete = true; + } + } + + private void StartDeferredItemPopulation() + { + _deferredPopulationInProgress = true; + Services.Framework.RunOnTick(PopulateCategoryBatch, delayTicks: 1); + } + + private void PopulateCategoryBatch() + { + if (!IsOpen) + { + _deferredPopulationInProgress = false; + return; + } + + int itemsPopulated = 0; + using (CategoriesNode.DeferRecalculateLayout()) + { + foreach (var node in CategoriesNode.Nodes) + { + if (node is InventoryCategoryNode categoryNode && categoryNode.NeedsItemPopulation) + { + int categoryItemCount = categoryNode.CategorizedInventory.Items.Count; + + if (itemsPopulated > 0 && itemsPopulated + categoryItemCount > ItemsPerFrame) + break; + + categoryNode.PopulateItems(); + categoryNode.RefreshNodeVisuals(); + itemsPopulated += categoryItemCount; + + if (itemsPopulated >= ItemsPerFrame) + break; + } + } + } + + bool hasMore = false; + foreach (var node in CategoriesNode.Nodes) + { + if (node is InventoryCategoryNode categoryNode && categoryNode.NeedsItemPopulation) + { + hasMore = true; + break; + } + } + + if (hasMore) + { + Services.Framework.RunOnTick(PopulateCategoryBatch); + } + else + { + _deferredPopulationInProgress = false; + _initialPopulationComplete = true; + } } protected readonly struct HeaderLayout @@ -231,12 +301,11 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow BackgroundDropTarget.AttachNode(this); } - protected virtual InventoryCategoryNode CreateCategoryNode(float? maxWidth = null) + protected virtual InventoryCategoryNode CreateCategoryNode() { return new InventoryCategoryNode { Size = ContentSize with { Y = 120 }, - MaxWidth = maxWidth, OnRefreshRequested = ManualRefresh, OnDragEnd = () => InventoryOrchestrator.RefreshAll(updateMaps: true), }; @@ -461,6 +530,8 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow HoverSubscribed.Clear(); RefreshQueued = false; RefreshAutosizeQueued = false; + _deferredPopulationInProgress = false; + _initialPopulationComplete = false; base.OnFinalize(addon); } diff --git a/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs b/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs index 0a26458..b5f3340 100644 --- a/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs +++ b/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs @@ -25,7 +25,8 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase private readonly TextNode _categoryNameTextNode; private readonly HybridDirectionalFlexNode _itemGridNode; - private const float FallbackItemSize = 46; + private const float ExpectedItemWidth = 42; + private const float ExpectedItemHeight = 46; private const float HeaderHeight = 16; private const float MinWidth = 40; @@ -41,9 +42,11 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase private int _lastItemCount; private ulong _lastItemsHash; private int _lastItemsPerLine; - private float? _lastMaxWidth; + private bool _itemsNeedPopulation; public event Action? HeaderHoverChanged; + + public bool NeedsItemPopulation => _itemsNeedPopulation; public Action? OnRefreshRequested { get; set; } public Action? OnDragEnd { get; set; } @@ -86,11 +89,10 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase set => SetCategoryData(value, _itemGridNode.ItemsPerLine); } - public void SetCategoryData(CategorizedInventory data, int itemsPerLine) + public void SetCategoryData(CategorizedInventory data, int itemsPerLine, bool deferItemCreation = false) { bool categoryChanged = data.Key != _lastCategoryKey; bool itemsPerLineChanged = itemsPerLine != _lastItemsPerLine; - bool maxWidthChanged = _maxWidth != _lastMaxWidth; ulong itemsHash = ComputeItemsHash(CollectionsMarshal.AsSpan(data.Items)); bool itemsChanged = data.Items.Count != _lastItemCount || itemsHash != _lastItemsHash; @@ -99,7 +101,6 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase _lastItemCount = data.Items.Count; _lastItemsHash = itemsHash; _lastItemsPerLine = itemsPerLine; - _lastMaxWidth = _maxWidth; _categorizedInventory = data; @@ -113,10 +114,19 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase if (itemsChanged || categoryChanged) { - using (_itemGridNode.DeferRecalculateLayout()) + _itemGridNode.ItemsPerLine = itemsPerLine; + + if (deferItemCreation) { - _itemGridNode.ItemsPerLine = itemsPerLine; - UpdateItemGrid(); + _itemsNeedPopulation = true; + } + else + { + using (_itemGridNode.DeferRecalculateLayout()) + { + UpdateItemGrid(); + } + _itemsNeedPopulation = false; } } else if (itemsPerLineChanged) @@ -124,12 +134,24 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase _itemGridNode.ItemsPerLine = itemsPerLine; } - if (categoryChanged || itemsChanged || itemsPerLineChanged || maxWidthChanged) + if (categoryChanged || itemsChanged || itemsPerLineChanged) { RecalculateSize(); } } + public void PopulateItems() + { + if (!_itemsNeedPopulation) + return; + + using (_itemGridNode.DeferRecalculateLayout()) + { + UpdateItemGrid(); + } + _itemsNeedPopulation = false; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ulong ComputeItemsHash(ReadOnlySpan items) { @@ -238,8 +260,8 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase { int itemCount = CategorizedInventory.Items.Count; - float cellW = _itemGridNode.Nodes.Count > 0 ? _itemGridNode.Nodes[0].Width : FallbackItemSize; - float cellH = _itemGridNode.Nodes.Count > 0 ? _itemGridNode.Nodes[0].Height : FallbackItemSize; + float cellW = ExpectedItemWidth; + float cellH = ExpectedItemHeight; float hPad = _itemGridNode.HorizontalPadding; float vPad = _itemGridNode.VerticalPadding;