Merge pull request #2 from Seeker1437/feature/grid-layout-optimization-and-hover

grid layout optimization and hover
This commit is contained in:
Jeffrey Veenhuis
2025-12-20 15:35:01 +01:00
committed by GitHub
7 changed files with 700 additions and 305 deletions
+76 -12
View File
@@ -17,6 +17,9 @@ namespace AetherBags.Addons;
public class AddonInventoryWindow : NativeAddon
{
private readonly InventoryCategoryHoverCoordinator _hoverCoordinator = new();
private readonly HashSet<InventoryCategoryNode> _hoverSubscribed = new();
private WrappingGridNode<InventoryCategoryNode> _categoriesNode = null!;
private TextInputWithHintNode _searchInputNode = null!;
private InventoryFooterNode _footerNode = null!;
@@ -30,7 +33,10 @@ public class AddonInventoryWindow : NativeAddon
// Layout settings
private const float CategorySpacing = 12;
private const float ItemSize = 40;
private const float ItemPadding = 6;
private const float ItemPadding = 4;
private const float FooterHeight = 28f;
private const float FooterTopSpacing = 4f;
protected override unsafe void OnSetup(AtkUnitBase* addon)
{
@@ -39,14 +45,17 @@ public class AddonInventoryWindow : NativeAddon
Position = ContentStartPosition,
Size = ContentSize,
HorizontalSpacing = CategorySpacing,
VerticalSpacing = CategorySpacing
VerticalSpacing = CategorySpacing,
TopPadding = 4.0f,
BottomPadding = 4.0f,
};
_categoriesNode.AttachNode(this);
var size = new Vector2(addon->Size.X / 2.0f, 28.0f);
Vector2 headerSize = new Vector2(addon->WindowHeaderCollisionNode->Width, addon->WindowHeaderCollisionNode->Height);
_searchInputNode = new TextInputWithHintNode {
_searchInputNode = new TextInputWithHintNode
{
Position = headerSize / 2.0f - size / 2.0f + new Vector2(25.0f, 10.0f),
Size = size,
OnInputReceived = _ => RefreshCategories(false),
@@ -55,11 +64,13 @@ public class AddonInventoryWindow : NativeAddon
_footerNode = new InventoryFooterNode
{
Size = ContentSize with { Y = 28 },
Size = ContentSize with { Y = FooterHeight },
SlotAmountText = InventoryState.GetEmptyItemSlotsString()
};
_footerNode.AttachNode(this);
LayoutContent();
Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "Inventory", OnInventoryUpdate);
addon->SubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
@@ -69,7 +80,6 @@ public class AddonInventoryWindow : NativeAddon
protected override unsafe void OnUpdate(AtkUnitBase* addon)
{
// Haven't needed it yet but just in case.
base.OnUpdate(addon);
}
@@ -78,13 +88,16 @@ public class AddonInventoryWindow : NativeAddon
RefreshCategories();
}
protected override unsafe void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) {
protected override unsafe void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
{
base.OnRequestedUpdate(addon, numberArrayData, stringArrayData);
RefreshCategories();
}
private void RefreshCategories(bool autosize = true)
{
_footerNode.SlotAmountText = InventoryState.GetEmptyItemSlotsString();
var categories = InventoryState.GetInventoryItemCategories(_searchInputNode.SearchString.ExtractText());
float maxContentWidth = MaxWindowWidth - (ContentStartPosition.X * 2);
@@ -105,7 +118,32 @@ public class AddonInventoryWindow : NativeAddon
node.ItemsPerLine = Math.Min(node.CategorizedInventory.Items.Count, maxItemsPerLine);
}
WireHoverHandlers();
if (autosize) AutoSizeWindow();
else
{
LayoutContent();
_categoriesNode.RecalculateLayout();
}
}
private void WireHoverHandlers()
{
List<InventoryCategoryNode> categoryNodes = _categoriesNode.GetNodes<InventoryCategoryNode>().ToList();
for (int i = 0; i < categoryNodes.Count; i++)
{
InventoryCategoryNode node = categoryNodes[i];
if (!_hoverSubscribed.Add(node))
continue;
node.HeaderHoverChanged += (src, hovering) =>
{
_hoverCoordinator.OnCategoryHoverChanged(_categoriesNode, src, hovering);
};
}
}
private int CalculateOptimalItemsPerLine(float availableWidth)
@@ -113,6 +151,23 @@ public class AddonInventoryWindow : NativeAddon
return Math.Clamp((int)Math.Floor((availableWidth + ItemPadding) / (ItemSize + ItemPadding)), 1, 15);
}
private void LayoutContent()
{
Vector2 contentPos = ContentStartPosition;
Vector2 contentSize = ContentSize;
float footerH = FooterHeight;
_footerNode.Position = new Vector2(contentPos.X, contentPos.Y + contentSize.Y - footerH);
_footerNode.Size = new Vector2(contentSize.X, footerH);
float gridH = contentSize.Y - footerH - FooterTopSpacing;
if (gridH < 0) gridH = 0;
_categoriesNode.Position = contentPos;
_categoriesNode.Size = new Vector2(contentSize.X, gridH);
}
private void AutoSizeWindow()
{
List<InventoryCategoryNode> childNodes = _categoriesNode.GetNodes<InventoryCategoryNode>().ToList();
@@ -127,14 +182,20 @@ public class AddonInventoryWindow : NativeAddon
float finalWidth = Math.Clamp(requiredWidth, MinWindowWidth, MaxWindowWidth);
float contentWidth = finalWidth - (ContentStartPosition.X * 2);
_categoriesNode.Size = new Vector2(contentWidth, MaxWindowHeight);
float gridBudget = Math.Max(0f, MaxWindowHeight - FooterHeight - FooterTopSpacing);
_categoriesNode.Position = ContentStartPosition;
_categoriesNode.Size = new Vector2(contentWidth, gridBudget);
_categoriesNode.RecalculateLayout();
float requiredHeight = _categoriesNode.GetRequiredHeight();
requiredHeight += ContentStartPosition.Y + ContentStartPosition.X;
float requiredGridHeight = _categoriesNode.GetRequiredHeight();
float requiredContentHeight = requiredGridHeight + FooterTopSpacing + FooterHeight;
float finalHeight = Math.Clamp(requiredHeight, MinWindowHeight, MaxWindowHeight);
float requiredWindowHeight = requiredContentHeight + ContentStartPosition.Y + ContentStartPosition.X;
float finalHeight = Math.Clamp(requiredWindowHeight, MinWindowHeight, MaxWindowHeight);
ResizeWindow(finalWidth, finalHeight);
}
@@ -142,8 +203,9 @@ public class AddonInventoryWindow : NativeAddon
private void ResizeWindow(float width, float height)
{
SetWindowSize(width, height);
_categoriesNode.Size = ContentSize;
_footerNode.Size = ContentSize with { Y = 28 };
LayoutContent();
_categoriesNode.RecalculateLayout();
}
@@ -152,5 +214,7 @@ public class AddonInventoryWindow : NativeAddon
base.OnFinalize(addon);
Services.AddonLifecycle.UnregisterListener(OnInventoryUpdate);
addon->UnsubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
_hoverSubscribed.Clear();
}
}
+13
View File
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace AetherBags.Nodes;
public enum FlexGrowDirection
{
DownRight,
DownLeft,
UpRight,
UpLeft
}
+72 -46
View File
@@ -1,57 +1,64 @@
using System;
using System.Linq;
using KamiToolKit;
using KamiToolKit.Nodes;
using System;
using System.Runtime.CompilerServices;
namespace AetherBags.Nodes
{
public enum FlexGrowDirection
{
DownRight,
DownLeft,
UpRight,
UpLeft
}
namespace AetherBags.Nodes;
public class HybridDirectionalFlexNode : HybridDirectionalFlexNode<NodeBase> { }
public class HybridDirectionalFlexNode<T> : LayoutListNode where T : NodeBase
{
public FlexGrowDirection GrowDirection { get;
set {
public FlexGrowDirection GrowDirection
{
get => field;
set
{
if (field == value) return;
field = value;
RecalculateLayout();
}
} = FlexGrowDirection.DownRight;
public int ItemsPerLine {
get;
set {
public int ItemsPerLine
{
get => field;
set
{
if (field == value) return;
field = value;
RecalculateLayout();
}
} = 1;
public bool FillRowsFirst {
get;
set {
public bool FillRowsFirst
{
get => field;
set
{
if (field == value) return;
field = value;
RecalculateLayout();
}
} = true;
public float HorizontalPadding {
get;
set {
public float HorizontalPadding
{
get => field;
set
{
if (field.Equals(value)) return;
field = value;
RecalculateLayout();
}
} = 1;
public float VerticalPadding {
get;
set {
public float VerticalPadding
{
get => field;
set
{
if (field.Equals(value)) return;
field = value;
RecalculateLayout();
}
@@ -59,48 +66,67 @@ namespace AetherBags.Nodes
protected override void InternalRecalculateLayout()
{
if (NodeList.Count == 0) {
return;
}
int count = NodeList.Count;
if (count == 0) return;
int itemsPerLine = Math.Max(1, ItemsPerLine);
int itemsPerLine = ItemsPerLine;
if (itemsPerLine < 1) itemsPerLine = 1;
float nodeWidth = NodeList.First().Width;
float nodeHeight = NodeList.First().Height;
NodeBase first = NodeList[0];
float nodeWidth = first.Width;
float nodeHeight = first.Height;
bool alignRight = GrowDirection is FlexGrowDirection.DownLeft or FlexGrowDirection.UpLeft;
bool alignBottom = GrowDirection is FlexGrowDirection.UpRight or FlexGrowDirection.UpLeft;
float hPad = HorizontalPadding;
float vPad = VerticalPadding;
FlexGrowDirection dir = GrowDirection;
bool alignRight = dir == FlexGrowDirection.DownLeft || dir == FlexGrowDirection.UpLeft;
bool alignBottom = dir == FlexGrowDirection.UpRight || dir == FlexGrowDirection.UpLeft;
float startX = alignRight ? Width : 0f;
float startY = alignBottom ? Height : 0f;
int idx = 0;
foreach (var node in NodeList)
float stepX = nodeWidth + hPad;
float stepY = nodeHeight + vPad;
bool fillRowsFirst = FillRowsFirst;
int major = 0;
int minor = 0;
for (int i = 0; i < count; i++)
{
int row, col;
if (FillRowsFirst)
if (fillRowsFirst)
{
row = idx / itemsPerLine;
col = idx % itemsPerLine;
row = major;
col = minor;
}
else
{
col = idx / itemsPerLine;
row = idx % itemsPerLine;
col = major;
row = minor;
}
float x = alignRight
? startX - (col + 1) * nodeWidth - col * HorizontalPadding
: startX + col * (nodeWidth + HorizontalPadding);
? startX - nodeWidth - col * stepX
: startX + col * stepX;
float y = alignBottom
? startY - (row + 1) * nodeHeight - row * VerticalPadding
: startY + row * (nodeHeight + VerticalPadding);
? startY - nodeHeight - row * stepY
: startY + row * stepY;
NodeBase node = NodeList[i];
node.X = x;
node.Y = y;
AdjustNode(node);
idx++;
minor++;
if (minor == itemsPerLine)
{
minor = 0;
major++;
}
}
}
+58 -36
View File
@@ -2,15 +2,16 @@ using System;
using KamiToolKit;
using KamiToolKit.Nodes;
namespace AetherBags.Nodes
{
namespace AetherBags.Nodes;
public class HybridDirectionalStackNode<T> : LayoutListNode where T : NodeBase
{
public FlexGrowDirection GrowDirection
{
get;
get => field;
set
{
if (field == value) return;
field = value;
RecalculateLayout();
}
@@ -18,9 +19,10 @@ namespace AetherBags.Nodes
public bool Vertical
{
get;
get => field;
set
{
if (field == value) return;
field = value;
RecalculateLayout();
}
@@ -28,9 +30,10 @@ namespace AetherBags.Nodes
public float Spacing
{
get;
get => field;
set
{
if (field.Equals(value)) return;
field = value;
RecalculateLayout();
}
@@ -38,9 +41,10 @@ namespace AetherBags.Nodes
public bool StretchCrossAxis
{
get;
get => field;
set
{
if (field == value) return;
field = value;
RecalculateLayout();
}
@@ -48,46 +52,64 @@ namespace AetherBags.Nodes
protected override void InternalRecalculateLayout()
{
if (NodeList.Count == 0)
return;
int count = NodeList.Count;
if (count == 0) return;
bool alignRight = GrowDirection is FlexGrowDirection.DownLeft or FlexGrowDirection.UpLeft;
bool alignBottom = GrowDirection is FlexGrowDirection.UpRight or FlexGrowDirection.UpLeft;
FlexGrowDirection dir = GrowDirection;
bool alignRight = dir == FlexGrowDirection.DownLeft || dir == FlexGrowDirection.UpLeft;
bool alignBottom = dir == FlexGrowDirection.UpRight || dir == FlexGrowDirection.UpLeft;
float startX = alignRight ? Width : 0f;
float startY = alignBottom ? Height : 0f;
bool vertical = Vertical;
bool stretchCross = StretchCrossAxis;
float containerW = Width;
float containerH = Height;
float startX = alignRight ? containerW : 0f;
float startY = alignBottom ? containerH : 0f;
float spacing = Spacing;
float cursor = 0f;
for (int i = 0; i < NodeList.Count; i++)
if (vertical)
{
var node = NodeList[i];
for (int i = 0; i < count; i++)
{
NodeBase node = NodeList[i];
if (StretchCrossAxis)
{
if (Vertical)
node.Width = Width;
else
node.Height = Height;
}
if (stretchCross)
node.Width = containerW;
float x, y;
if (Vertical)
{
x = alignRight ? startX - node.Width : startX;
y = alignBottom ? startY - node.Height - cursor : startY + cursor;
cursor += node.Height + Spacing;
}
else
{
x = alignRight ? startX - node.Width - cursor : startX + cursor;
y = alignBottom ? startY - node.Height : startY;
cursor += node.Width + Spacing;
}
float w = node.Width;
float h = node.Height;
node.X = alignRight ? startX - w : startX;
node.Y = alignBottom ? startY - h - cursor : startY + cursor;
node.X = x;
node.Y = y;
AdjustNode(node);
cursor += node.Height + spacing;
}
}
else
{
for (int i = 0; i < count; i++)
{
NodeBase node = NodeList[i];
if (stretchCross)
node.Height = containerH;
float w = node.Width;
float h = node.Height;
node.X = alignRight ? startX - w - cursor : startX + cursor;
node.Y = alignBottom ? startY - h : startY;
AdjustNode(node);
cursor += node.Width + spacing;
}
}
}
@@ -0,0 +1,81 @@
using System;
namespace AetherBags.Nodes;
public sealed class InventoryCategoryHoverCoordinator
{
private InventoryCategoryNode? _active;
private int _activeRowIndex = -1;
public void OnCategoryHoverChanged(
WrappingGridNode<InventoryCategoryNode> grid,
InventoryCategoryNode source,
bool hovering)
{
grid.RecalculateLayout();
if (hovering)
{
_active = source;
if (!grid.TryGetRowIndex(source, out _activeRowIndex))
{
SuppressAllExcept(grid, source);
source.SetHeaderSuppressed(false);
return;
}
ClearAll(grid);
var row = grid.Rows[_activeRowIndex];
for (int i = 0; i < row.Count; i++)
{
if (row[i] is InventoryCategoryNode cat && !ReferenceEquals(cat, source))
cat.SetHeaderSuppressed(true);
}
source.SetHeaderSuppressed(false);
return;
}
if (!ReferenceEquals(_active, source))
return;
_active = null;
if (_activeRowIndex >= 0 && _activeRowIndex < grid.Rows.Count)
{
var row = grid.Rows[_activeRowIndex];
for (int i = 0; i < row.Count; i++)
{
if (row[i] is InventoryCategoryNode cat)
cat.SetHeaderSuppressed(false);
}
}
else
{
ClearAll(grid);
}
_activeRowIndex = -1;
}
public void ResetAll(WrappingGridNode<InventoryCategoryNode> grid)
{
_active = null;
_activeRowIndex = -1;
ClearAll(grid);
}
private static void ClearAll(WrappingGridNode<InventoryCategoryNode> grid)
{
foreach (var cat in grid.GetNodes<InventoryCategoryNode>())
cat.SetHeaderSuppressed(false);
}
private static void SuppressAllExcept(WrappingGridNode<InventoryCategoryNode> grid, InventoryCategoryNode source)
{
foreach (var cat in grid.GetNodes<InventoryCategoryNode>())
cat.SetHeaderSuppressed(!ReferenceEquals(cat, source));
}
}
+159 -26
View File
@@ -1,13 +1,12 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using AetherBags.Extensions;
using AetherBags.Inventory;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
using System;
using System.Numerics;
namespace AetherBags.Nodes;
@@ -16,32 +15,50 @@ public class InventoryCategoryNode : SimpleComponentNode
private readonly TextNode _categoryNameTextNode;
private readonly HybridDirectionalFlexNode<DragDropNode> _itemGridNode;
private const float ItemSize = 40;
private const float ItemHorizontalPadding = 6;
private const float ItemVerticalPadding = 6;
private const float FallbackItemSize = 46;
private const float HeaderHeight = 16;
private const float MinWidth = 40;
private float? _fixedWidth;
private int _hoverRefs;
private bool _headerSuppressed;
private bool _headerExpanded;
private float _baseHeaderWidth = 96f;
private string _fullHeaderText = string.Empty;
public event Action<InventoryCategoryNode, bool>? HeaderHoverChanged;
public InventoryCategoryNode()
{
_categoryNameTextNode = new TextNode
{
Size = new Vector2(100, 14),
AlignmentType = AlignmentType.Left
Size = new Vector2(96, 16),
AlignmentType = AlignmentType.Left,
};
_categoryNameTextNode.AddEvent(AtkEventType.MouseOver, BeginHeaderHover);
_categoryNameTextNode.AddEvent(AtkEventType.MouseOut, EndHeaderHover);
_categoryNameTextNode.TextFlags |= TextFlags.OverflowHidden | TextFlags.Ellipsis;
_categoryNameTextNode.TextFlags &= ~(TextFlags.WordWrap | TextFlags.MultiLine);
_categoryNameTextNode.AddFlags(NodeFlags.EmitsEvents | NodeFlags.HasCollision);
_categoryNameTextNode.AttachNode(this);
_itemGridNode = new HybridDirectionalFlexNode<DragDropNode>
{
Position = new Vector2(0, HeaderHeight),
Size = new Vector2(240, 100),
Size = new Vector2(240, 92),
FillRowsFirst = true,
ItemsPerLine = 10,
HorizontalPadding = ItemHorizontalPadding,
VerticalPadding = ItemVerticalPadding,
HorizontalPadding = 5,
VerticalPadding = 2,
};
_itemGridNode.NodeFlags |= NodeFlags.EmitsEvents;
_itemGridNode.AttachNode(this);
}
@@ -52,8 +69,11 @@ public class InventoryCategoryNode : SimpleComponentNode
{
field = value;
_categoryNameTextNode.String = value.Category.Name;
_fullHeaderText = value.Category.Name;
_categoryNameTextNode.String = _fullHeaderText;
_categoryNameTextNode.TextColor = value.Category.Color;
_categoryNameTextNode.TooltipString = value.Category.Description;
UpdateItemGrid();
@@ -66,6 +86,7 @@ public class InventoryCategoryNode : SimpleComponentNode
get => _itemGridNode.ItemsPerLine;
set
{
if (_itemGridNode.ItemsPerLine == value) return;
_itemGridNode.ItemsPerLine = value;
RecalculateSize();
}
@@ -76,24 +97,117 @@ public class InventoryCategoryNode : SimpleComponentNode
get => _fixedWidth;
set
{
if (_fixedWidth.Equals(value)) return;
_fixedWidth = value;
RecalculateSize();
}
}
public void BeginHeaderHover()
{
_hoverRefs++;
if (_hoverRefs != 1) return;
_headerExpanded = true;
ApplyHeaderVisualStateAndSize();
HeaderHoverChanged?.Invoke(this, true);
}
public void EndHeaderHover()
{
if (_hoverRefs <= 0) return;
_hoverRefs--;
if (_hoverRefs != 0) return;
_headerExpanded = false;
ApplyHeaderVisualStateAndSize();
HeaderHoverChanged?.Invoke(this, false);
}
public void SetHeaderSuppressed(bool suppressed)
{
if (_headerSuppressed == suppressed) return;
_headerSuppressed = suppressed;
ApplyHeaderVisualStateAndSize();
}
private void ApplyHeaderVisualStateAndSize()
{
_categoryNameTextNode.IsVisible = !_headerSuppressed;
if (_headerSuppressed)
return;
var flags = _categoryNameTextNode.TextFlags;
flags &= ~(TextFlags.WordWrap | TextFlags.MultiLine);
if (_headerExpanded)
{
flags &= ~(TextFlags.OverflowHidden | TextFlags.Ellipsis);
_categoryNameTextNode.TextFlags = flags;
if (!string.IsNullOrEmpty(_fullHeaderText))
_categoryNameTextNode.String = _fullHeaderText;
Vector2 drawSize = _categoryNameTextNode.GetTextDrawSize();
float expandedWidth = MathF.Max(_baseHeaderWidth, drawSize.X + 4f);
_categoryNameTextNode.Size = _categoryNameTextNode.Size with { X = expandedWidth };
}
else
{
_categoryNameTextNode.Size = _categoryNameTextNode.Size with { X = _baseHeaderWidth };
if (!string.IsNullOrEmpty(_fullHeaderText))
_categoryNameTextNode.String = _fullHeaderText;
flags |= (TextFlags.OverflowHidden | TextFlags.Ellipsis);
_categoryNameTextNode.TextFlags = flags;
}
}
private void RecalculateSize()
{
int itemCount = CategorizedInventory.Items.Count;
if (itemCount == 0)
{
float width = _fixedWidth ?? MinWidth;
Size = new Vector2(width, HeaderHeight);
_categoryNameTextNode.Size = _categoryNameTextNode.Size with { X = width };
_baseHeaderWidth = width;
_itemGridNode.Position = new Vector2(0, HeaderHeight);
_itemGridNode.Size = new Vector2(width, 0);
ApplyHeaderVisualStateAndSize();
return;
}
int itemsPerLine = Math.Max(1, _itemGridNode.ItemsPerLine);
int rows = (int)Math.Ceiling((float)itemCount / itemsPerLine);
int itemsPerLine = _itemGridNode.ItemsPerLine;
if (itemsPerLine < 1) itemsPerLine = 1;
int rows = (itemCount + itemsPerLine - 1) / itemsPerLine;
int actualColumns = Math.Min(itemCount, itemsPerLine);
float cellW, cellH;
if (_itemGridNode.Nodes.Count > 0)
{
var firstChild = _itemGridNode.Nodes[0];
cellW = firstChild.Width;
cellH = firstChild.Height;
}
else
{
cellW = FallbackItemSize;
cellH = FallbackItemSize;
}
float hPad = _itemGridNode.HorizontalPadding;
float vPad = _itemGridNode.VerticalPadding;
float calculatedWidth;
if (_fixedWidth.HasValue)
@@ -102,29 +216,37 @@ public class InventoryCategoryNode : SimpleComponentNode
}
else
{
int actualColumns = Math.Min(itemCount, itemsPerLine);
calculatedWidth = actualColumns * ItemSize + (actualColumns - 1) * ItemHorizontalPadding;
calculatedWidth = Math.Max(calculatedWidth, MinWidth);
calculatedWidth = actualColumns * cellW + (actualColumns - 1) * hPad;
if (calculatedWidth < MinWidth) calculatedWidth = MinWidth;
}
float height = HeaderHeight + rows * ItemSize + (rows - 1) * ItemVerticalPadding;
float height = HeaderHeight + rows * cellH + (rows - 1) * vPad;
Size = new Vector2(calculatedWidth, height);
_itemGridNode.Position = new Vector2(0, HeaderHeight);
_itemGridNode.Size = new Vector2(calculatedWidth, height - HeaderHeight);
_categoryNameTextNode.Size = _categoryNameTextNode.Size with { X = calculatedWidth };
_baseHeaderWidth = calculatedWidth;
ApplyHeaderVisualStateAndSize();
}
private void UpdateItemGrid()
{
_itemGridNode.SyncWithListData(CategorizedInventory.Items, node => node.ItemInfo, CreateInventoryDragDropNode);
_itemGridNode.SyncWithListData(
CategorizedInventory.Items,
node => node.ItemInfo,
CreateInventoryDragDropNode);
}
private InventoryDragDropNode CreateInventoryDragDropNode(ItemInfo data)
{
InventoryItem item = data.Item;
InventoryDragDropNode node = new InventoryDragDropNode
var node = new InventoryDragDropNode
{
Size = new Vector2(40),
Size = new Vector2(42, 46),
IsVisible = true,
IconId = item.IconId,
AcceptedType = DragDropType.Nothing,
@@ -136,10 +258,21 @@ public class InventoryCategoryNode : SimpleComponentNode
Int2 = (int)item.ItemId,
},
IsClickable = true,
OnRollOver = node => node.ShowInventoryItemTooltip(item.Container, item.Slot),
OnRollOut = node => node.HideTooltip(),
OnRollOver = n =>
{
BeginHeaderHover();
n.ShowInventoryItemTooltip(item.Container, item.Slot);
},
OnRollOut = n =>
{
EndHeaderHover();
n.HideTooltip();
},
ItemInfo = data
};
return node;
}
}
+63 -7
View File
@@ -1,26 +1,44 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using KamiToolKit;
using KamiToolKit.Nodes;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace AetherBags.Nodes;
namespace AetherBags.Nodes
{
public sealed class WrappingGridNode<T> : LayoutListNode where T : NodeBase
{
public float HorizontalSpacing { get; set; } = 10f;
public float VerticalSpacing { get; set; } = 10f;
public float TopPadding { get; set; } = 0f;
public float BottomPadding { get; set; } = 0f;
private readonly List<List<NodeBase>> _rows = new();
private readonly Stack<List<NodeBase>> _rowPool = new();
private readonly Dictionary<NodeBase, int> _rowIndex = new(ReferenceEqualityComparer<NodeBase>.Instance);
private float _requiredHeight;
private bool _requiredHeightDirty = true;
private readonly IReadOnlyList<IReadOnlyList<NodeBase>> _rowsView;
public WrappingGridNode()
{
_rowsView = new RowsReadOnlyView(_rows);
}
public IReadOnlyList<IReadOnlyList<NodeBase>> Rows => _rowsView;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetRowIndex(NodeBase node, out int rowIndex) => _rowIndex.TryGetValue(node, out rowIndex);
protected override void InternalRecalculateLayout()
{
RecycleAllRows();
_rowIndex.Clear();
int count = NodeList.Count;
if (count == 0)
@@ -30,15 +48,18 @@ namespace AetherBags.Nodes
return;
}
_rowIndex.EnsureCapacity(count);
float availableWidth = Width;
float hSpace = HorizontalSpacing;
float vSpace = VerticalSpacing;
float startX = FirstItemSpacing;
float currentX = startX;
float currentY = 0f;
float currentY = TopPadding;
float rowHeight = 0f;
int currentRowIndex = 0;
List<NodeBase> currentRow = RentRowList(capacityHint: 8);
for (int i = 0; i < count; i++)
@@ -50,6 +71,7 @@ namespace AetherBags.Nodes
if (currentRow.Count != 0 && (currentX + nodeWidth) > availableWidth)
{
_rows.Add(currentRow);
currentRowIndex++;
currentY += rowHeight + vSpace;
currentX = startX;
@@ -67,6 +89,7 @@ namespace AetherBags.Nodes
if (nodeHeight > rowHeight) rowHeight = nodeHeight;
currentRow.Add(node);
_rowIndex[node] = currentRowIndex;
currentX += nodeWidth + hSpace;
}
@@ -98,6 +121,9 @@ namespace AetherBags.Nodes
if (bottom > maxBottom) maxBottom = bottom;
}
maxBottom += BottomPadding;
_requiredHeight = maxBottom;
_requiredHeightDirty = false;
return maxBottom;
@@ -133,5 +159,35 @@ namespace AetherBags.Nodes
row.Clear();
_rowPool.Push(row);
}
private sealed class RowsReadOnlyView : IReadOnlyList<IReadOnlyList<NodeBase>>
{
private readonly List<List<NodeBase>> _rows;
public RowsReadOnlyView(List<List<NodeBase>> rows) => _rows = rows;
public int Count => _rows.Count;
public IReadOnlyList<NodeBase> this[int index] => _rows[index];
public IEnumerator<IReadOnlyList<NodeBase>> GetEnumerator()
{
for (int i = 0; i < _rows.Count; i++)
yield return _rows[i];
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
private sealed class ReferenceEqualityComparer<TRef> : IEqualityComparer<TRef> where TRef : class
{
public static readonly ReferenceEqualityComparer<TRef> Instance = new();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(TRef? x, TRef? y) => ReferenceEquals(x, y);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetHashCode(TRef obj) => RuntimeHelpers.GetHashCode(obj);
}
}