Merge pull request #2 from Seeker1437/feature/grid-layout-optimization-and-hover
grid layout optimization and hover
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace AetherBags.Nodes;
|
||||
|
||||
public enum FlexGrowDirection
|
||||
{
|
||||
DownRight,
|
||||
DownLeft,
|
||||
UpRight,
|
||||
UpLeft
|
||||
}
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user