diff --git a/AetherBags/Configuration/GeneralSettings.cs b/AetherBags/Configuration/GeneralSettings.cs index 40589cb..1acfa4f 100644 --- a/AetherBags/Configuration/GeneralSettings.cs +++ b/AetherBags/Configuration/GeneralSettings.cs @@ -7,6 +7,10 @@ public class GeneralSettings { public InventoryStackMode StackMode { get; set; } = InventoryStackMode.AggregateByItemId; public bool DebugEnabled { get; set; } = false; + public bool CompactPackingEnabled { get; set; } = true; + public int CompactLookahead { get; set; } = 24; + public bool CompactPreferLargestFit { get; set; } = true; + public bool CompactStableInsert { get; set; } = true; } public enum InventoryStackMode : byte diff --git a/AetherBags/Nodes/Configuration/ConfigurationRoot.cs b/AetherBags/Nodes/Configuration/ConfigurationRoot.cs new file mode 100644 index 0000000..5d9b617 --- /dev/null +++ b/AetherBags/Nodes/Configuration/ConfigurationRoot.cs @@ -0,0 +1,7 @@ +using KamiToolKit.Nodes; + +namespace AetherBags.Nodes.Configuration; + +internal class ConfigurationRoot : TabbedVerticalListNode +{ +} \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/GeneralScrollingAreaNode.cs b/AetherBags/Nodes/Configuration/GeneralScrollingAreaNode.cs index b121e95..6de7fa8 100644 --- a/AetherBags/Nodes/Configuration/GeneralScrollingAreaNode.cs +++ b/AetherBags/Nodes/Configuration/GeneralScrollingAreaNode.cs @@ -1,8 +1,9 @@ +using AetherBags.Configuration; +using AetherBags.Nodes.Configuration.Layout; +using KamiToolKit.Nodes; using System; using System.Linq; using System.Numerics; -using AetherBags.Configuration; -using KamiToolKit.Nodes; namespace AetherBags.Nodes.Configuration; @@ -10,7 +11,8 @@ public sealed class GeneralScrollingAreaNode : ScrollingAreaNode { config.DebugEnabled = isChecked; } }; ContentNode.AddNode(_debugCheckboxNode); - } private void RefreshInventory() => System.AddonInventoryWindow.ManualRefresh(); diff --git a/AetherBags/Nodes/Configuration/Layout/CompactLookaheadNode.cs b/AetherBags/Nodes/Configuration/Layout/CompactLookaheadNode.cs new file mode 100644 index 0000000..51689a8 --- /dev/null +++ b/AetherBags/Nodes/Configuration/Layout/CompactLookaheadNode.cs @@ -0,0 +1,60 @@ +using AetherBags.Configuration; +using FFXIVClientStructs.FFXIV.Component.GUI; +using KamiToolKit.Classes; +using KamiToolKit.Classes.Timelines; +using KamiToolKit.Nodes; +using System.Numerics; + +namespace AetherBags.Nodes.Configuration.Layout; + +internal sealed class CompactLookaheadNode : SimpleComponentNode +{ + public readonly LabelTextNode TitleNode; + public readonly NumericInputNode CompactLookahead = null!; + + public unsafe CompactLookaheadNode() + { + GeneralSettings config = System.Config.General; + + TitleNode = new LabelTextNode + { + Size = Size with { Y = 24 }, + String = "Compact Lookahead", + }; + TitleNode.AttachNode(this); + + CompactLookahead = new NumericInputNode + { + Position = Position with { X = 240 }, + Size = Size with { X = 88 }, + IsVisible = true, + Value = config.CompactLookahead, + OnValueUpdate = value => + { + config.CompactLookahead = value; + System.AddonInventoryWindow.ManualRefresh(); + } + }; + CompactLookahead.ComponentBase->SetEnabledState(config.CompactPackingEnabled); + CompactLookahead.AttachNode(this); + + TitleNode.AddTimeline(new TimelineBuilder() + .AddFrameSetWithFrame(1, 10, 1, alpha: 255, multiplyColor: new Vector3(100.0f)) + .AddFrameSetWithFrame(11, 20, 11, alpha: 255, multiplyColor: new Vector3(100.0f)) + .AddFrameSetWithFrame(21, 30, 21, alpha: 255, multiplyColor: new Vector3(100.0f)) + .AddFrameSetWithFrame(31, 40, 31, alpha: 102, multiplyColor: new Vector3(80.0f)) + .AddFrameSetWithFrame(41, 50, 41, alpha: 255, multiplyColor: new Vector3(100.0f)) + .AddFrameSetWithFrame(51, 60, 51, alpha: 255, multiplyColor: new Vector3(100.0f)) + .AddFrameSetWithFrame(61, 70, 61, alpha: 255, multiplyColor: new Vector3(100.0f)) + .AddFrameSetWithFrame(71, 80, 71, alpha: 255, multiplyColor: new Vector3(100.0f)) + .AddFrameSetWithFrame(81, 90, 81, alpha: 255, multiplyColor: new Vector3(100.0f)) + .AddFrameSetWithFrame(91, 100, 91, alpha: 102, multiplyColor: new Vector3(80.0f)) + .AddFrameSetWithFrame(101, 110, 101, alpha: 255, multiplyColor: new Vector3(100.0f)) + .AddFrameSetWithFrame(111, 115, 111, alpha: 255, multiplyColor: new Vector3(100.0f)) + .AddFrameSetWithFrame(116, 135, 116, alpha: 255, multiplyColor: new Vector3(100.0f)) + .AddFrameSetWithFrame(126, 135, 126, alpha: 255, multiplyColor: new Vector3(100.0f)) + .AddFrameSetWithFrame(136, 145, 136, alpha: 255, multiplyColor: new Vector3(100.0f)) + .AddFrameSetWithFrame(146, 155, 146, alpha: 255, multiplyColor: new Vector3(100.0f)) + .Build()); + } +} \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Layout/LayoutConfigurationNode.cs b/AetherBags/Nodes/Configuration/Layout/LayoutConfigurationNode.cs new file mode 100644 index 0000000..121762b --- /dev/null +++ b/AetherBags/Nodes/Configuration/Layout/LayoutConfigurationNode.cs @@ -0,0 +1,83 @@ +using System.Numerics; +using AetherBags.Configuration; +using KamiToolKit.Nodes; +using KamiToolKit.Classes; + +namespace AetherBags.Nodes.Configuration.Layout; + +internal class LayoutConfigurationNode : TabbedVerticalListNode +{ + private readonly CompactLookaheadNode _compactLookaheadNode = null!; + private readonly CheckboxNode _preferLargestFitCheckboxNode = null!; + private readonly CheckboxNode _useStableInsertCheckboxNode = null!; + + public unsafe LayoutConfigurationNode() + { + GeneralSettings config = System.Config.General; + + var titleNode = new LabelTextNode + { + Size = Size with { Y = 18 }, + String = "Layout Configuration", + TextColor = ColorHelper.GetColor(2), + TextOutlineColor = ColorHelper.GetColor(0), + }; + AddNode(titleNode); + + AddTab(1); + + var compactPackingCheckboxNode = new CheckboxNode + { + Size = Size with { Y = 18 }, + IsVisible = true, + String = "Use Compact Packing", + IsChecked = config.CompactPackingEnabled, + OnClick = isChecked => + { + config.CompactPackingEnabled = isChecked; + _preferLargestFitCheckboxNode.IsEnabled = isChecked; + _useStableInsertCheckboxNode.IsEnabled = isChecked; + _compactLookaheadNode.CompactLookahead.ComponentBase->SetEnabledState(isChecked); + System.AddonInventoryWindow.ManualRefresh(); + } + }; + AddNode(compactPackingCheckboxNode); + + AddTab(1); + _preferLargestFitCheckboxNode = new CheckboxNode + { + Size = Size with { Y = 18 }, + IsVisible = true, + String = "Prefer Largest Fit", + IsEnabled = config.CompactPackingEnabled, + IsChecked = config.CompactPreferLargestFit, + OnClick = isChecked => + { + config.CompactPreferLargestFit = isChecked; + System.AddonInventoryWindow.ManualRefresh(); + } + }; + AddNode(_preferLargestFitCheckboxNode); + + _useStableInsertCheckboxNode = new CheckboxNode + { + Size = Size with { Y = 18 }, + IsVisible = true, + String = "Use Stable Insert", + IsEnabled = config.CompactPackingEnabled, + IsChecked = config.CompactStableInsert, + OnClick = isChecked => + { + config.CompactStableInsert = isChecked; + System.AddonInventoryWindow.ManualRefresh(); + } + }; + AddNode(_useStableInsertCheckboxNode); + + _compactLookaheadNode = new CompactLookaheadNode + { + Size = new Vector2(320, 20) + }; + AddNode(_compactLookaheadNode); + } +} \ No newline at end of file diff --git a/AetherBags/Nodes/WrappingGridNode.cs b/AetherBags/Nodes/WrappingGridNode.cs index 5c6147d..4b21e45 100644 --- a/AetherBags/Nodes/WrappingGridNode.cs +++ b/AetherBags/Nodes/WrappingGridNode.cs @@ -31,6 +31,12 @@ public sealed class WrappingGridNode : LayoutListNode where T : NodeBase private float _lastVSpace = float.NaN; private float _lastTopPadding = float.NaN; private float _lastBottomPadding = float.NaN; + private bool _lastuseCompactPacking; + private bool _lastpreferLargestFit; + private bool _lastuseStableInsert; + private int _lastCompactLookahead; + + private int[] _orderScratch = Array.Empty(); public WrappingGridNode() { @@ -55,6 +61,22 @@ public sealed class WrappingGridNode : LayoutListNode where T : NodeBase return; } + if (System.Config.General.CompactPackingEnabled) + { + if (_rows.Count != 0 && LayoutParamsMatchLast() && NodeSetMatchesExistingLayout(count)) + { + RepositionExistingRows(); + _requiredHeightDirty = true; + RememberLayoutParams(); + return; + } + + FullReflowCompact(count); + _requiredHeightDirty = true; + RememberLayoutParams(); + return; + } + if (_rows.Count != 0 && TryUpdateLayoutWithoutReflowOrTailReflow(count)) { _requiredHeightDirty = true; @@ -67,6 +89,20 @@ public sealed class WrappingGridNode : LayoutListNode where T : NodeBase RememberLayoutParams(); } + private bool NodeSetMatchesExistingLayout(int count) + { + if (_rowIndex.Count != count) + return false; + + for (int i = 0; i < count; i++) + { + if (!_rowIndex.ContainsKey(NodeList[i])) + return false; + } + + return true; + } + private bool TryUpdateLayoutWithoutReflowOrTailReflow(int count) { if (!LayoutParamsMatchLast()) @@ -347,6 +383,134 @@ public sealed class WrappingGridNode : LayoutListNode where T : NodeBase } } + private void FullReflowCompact(int count) + { + RecycleAllRows(); + _rowIndex.Clear(); + _rowIndex.EnsureCapacity(count); + + float availableWidth = Width; + float hSpace = HorizontalSpacing; + float vSpace = VerticalSpacing; + float startX = FirstItemSpacing; + + float y = TopPadding; + + EnsureOrderScratch(count); + for (int i = 0; i < count; i++) + _orderScratch[i] = i; + + int lookahead = System.Config.General.CompactLookahead; + if (lookahead < 0) lookahead = 0; + + int p = 0; + int rowIdx = 0; + + while (p < count) + { + List row = RentRowList(capacityHint: 8); + + float x = startX; + float rowHeight = 0f; + + while (p < count) + { + int idx = _orderScratch[p]; + NodeBase node = NodeList[idx]; + float w = node.Width; + + if (row.Count == 0 || (x + w) <= availableWidth) + { + node.X = x; + node.Y = y; + + AdjustNode(node); + + float h = node.Height; + if (h > rowHeight) rowHeight = h; + + row.Add(node); + _rowIndex[node] = rowIdx; + + x += w + hSpace; + p++; + continue; + } + + int bestPos = -1; + float bestWidth = 0f; + + int end = p + lookahead; + if (end >= count) end = count - 1; + + for (int s = p + 1; s <= end; s++) + { + int candIdx = _orderScratch[s]; + NodeBase cand = NodeList[candIdx]; + float cw = cand.Width; + + if ((x + cw) <= availableWidth) + { + if (!System.Config.General.CompactPreferLargestFit) + { + bestPos = s; + break; + } + + if (cw > bestWidth) + { + bestWidth = cw; + bestPos = s; + } + } + } + + if (bestPos < 0) + break; + + if (bestPos != p) + { + int chosen = _orderScratch[bestPos]; + + if (System.Config.General.CompactStableInsert) + { + Array.Copy(_orderScratch, p, _orderScratch, p + 1, bestPos - p); + _orderScratch[p] = chosen; + } + else + { + _orderScratch[bestPos] = _orderScratch[p]; + _orderScratch[p] = chosen; + } + } + } + + if (row.Count == 0) + { + int idx = _orderScratch[p]; + NodeBase node = NodeList[idx]; + float w = node.Width; + + node.X = startX; + node.Y = y; + + AdjustNode(node); + + rowHeight = node.Height; + + row.Add(node); + _rowIndex[node] = rowIdx; + + p++; + } + + _rows.Add(row); + rowIdx++; + + y += rowHeight + vSpace; + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public float GetRequiredHeight() { @@ -400,15 +564,29 @@ public sealed class WrappingGridNode : LayoutListNode where T : NodeBase _rowPool.Push(row); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool NearlyEqual(float a, float b) + { + float diff = MathF.Abs(a - b); + if (diff <= 0.05f) return true; + + float max = MathF.Max(MathF.Abs(a), MathF.Abs(b)); + return diff <= max * 0.0005f; + } + private bool LayoutParamsMatchLast() { return - _lastAvailableWidth == Width && - _lastStartX == FirstItemSpacing && - _lastHSpace == HorizontalSpacing && - _lastVSpace == VerticalSpacing && - _lastTopPadding == TopPadding && - _lastBottomPadding == BottomPadding; + NearlyEqual(_lastAvailableWidth, Width) && + NearlyEqual(_lastStartX, FirstItemSpacing) && + NearlyEqual(_lastHSpace, HorizontalSpacing) && + NearlyEqual(_lastVSpace, VerticalSpacing) && + NearlyEqual(_lastTopPadding, TopPadding) && + NearlyEqual(_lastBottomPadding, BottomPadding) && + _lastuseCompactPacking == System.Config.General.CompactPackingEnabled && + _lastpreferLargestFit == System.Config.General.CompactPreferLargestFit && + _lastuseStableInsert == System.Config.General.CompactStableInsert && + _lastCompactLookahead == System.Config.General.CompactLookahead; } private void RememberLayoutParams() @@ -419,6 +597,22 @@ public sealed class WrappingGridNode : LayoutListNode where T : NodeBase _lastVSpace = VerticalSpacing; _lastTopPadding = TopPadding; _lastBottomPadding = BottomPadding; + + _lastuseCompactPacking = System.Config.General.CompactPackingEnabled; + _lastpreferLargestFit = System.Config.General.CompactPreferLargestFit; + _lastuseStableInsert = System.Config.General.CompactStableInsert; + _lastCompactLookahead = System.Config.General.CompactLookahead; + } + + private void EnsureOrderScratch(int needed) + { + if (_orderScratch.Length >= needed) + return; + + int newSize = _orderScratch.Length == 0 ? 64 : _orderScratch.Length; + while (newSize < needed) newSize *= 2; + + _orderScratch = new int[newSize]; } private sealed class RowsReadOnlyView : IReadOnlyList> diff --git a/KamiToolKit b/KamiToolKit index 9101fc8..4edae5d 160000 --- a/KamiToolKit +++ b/KamiToolKit @@ -1 +1 @@ -Subproject commit 9101fc8a8e861345fe0740e7791cdb06e8764f3e +Subproject commit 4edae5d1dc9944c8cf9f3c31c7b56ae05af6abdd