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
|
public class AddonInventoryWindow : NativeAddon
|
||||||
{
|
{
|
||||||
|
private readonly InventoryCategoryHoverCoordinator _hoverCoordinator = new();
|
||||||
|
private readonly HashSet<InventoryCategoryNode> _hoverSubscribed = new();
|
||||||
|
|
||||||
private WrappingGridNode<InventoryCategoryNode> _categoriesNode = null!;
|
private WrappingGridNode<InventoryCategoryNode> _categoriesNode = null!;
|
||||||
private TextInputWithHintNode _searchInputNode = null!;
|
private TextInputWithHintNode _searchInputNode = null!;
|
||||||
private InventoryFooterNode _footerNode = null!;
|
private InventoryFooterNode _footerNode = null!;
|
||||||
@@ -30,7 +33,10 @@ public class AddonInventoryWindow : NativeAddon
|
|||||||
// Layout settings
|
// Layout settings
|
||||||
private const float CategorySpacing = 12;
|
private const float CategorySpacing = 12;
|
||||||
private const float ItemSize = 40;
|
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)
|
protected override unsafe void OnSetup(AtkUnitBase* addon)
|
||||||
{
|
{
|
||||||
@@ -39,14 +45,17 @@ public class AddonInventoryWindow : NativeAddon
|
|||||||
Position = ContentStartPosition,
|
Position = ContentStartPosition,
|
||||||
Size = ContentSize,
|
Size = ContentSize,
|
||||||
HorizontalSpacing = CategorySpacing,
|
HorizontalSpacing = CategorySpacing,
|
||||||
VerticalSpacing = CategorySpacing
|
VerticalSpacing = CategorySpacing,
|
||||||
|
TopPadding = 4.0f,
|
||||||
|
BottomPadding = 4.0f,
|
||||||
};
|
};
|
||||||
_categoriesNode.AttachNode(this);
|
_categoriesNode.AttachNode(this);
|
||||||
|
|
||||||
var size = new Vector2(addon->Size.X / 2.0f, 28.0f);
|
var size = new Vector2(addon->Size.X / 2.0f, 28.0f);
|
||||||
|
|
||||||
Vector2 headerSize = new Vector2(addon->WindowHeaderCollisionNode->Width, addon->WindowHeaderCollisionNode->Height);
|
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),
|
Position = headerSize / 2.0f - size / 2.0f + new Vector2(25.0f, 10.0f),
|
||||||
Size = size,
|
Size = size,
|
||||||
OnInputReceived = _ => RefreshCategories(false),
|
OnInputReceived = _ => RefreshCategories(false),
|
||||||
@@ -55,11 +64,13 @@ public class AddonInventoryWindow : NativeAddon
|
|||||||
|
|
||||||
_footerNode = new InventoryFooterNode
|
_footerNode = new InventoryFooterNode
|
||||||
{
|
{
|
||||||
Size = ContentSize with { Y = 28 },
|
Size = ContentSize with { Y = FooterHeight },
|
||||||
SlotAmountText = InventoryState.GetEmptyItemSlotsString()
|
SlotAmountText = InventoryState.GetEmptyItemSlotsString()
|
||||||
};
|
};
|
||||||
_footerNode.AttachNode(this);
|
_footerNode.AttachNode(this);
|
||||||
|
|
||||||
|
LayoutContent();
|
||||||
|
|
||||||
Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "Inventory", OnInventoryUpdate);
|
Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "Inventory", OnInventoryUpdate);
|
||||||
addon->SubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
|
addon->SubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
|
||||||
|
|
||||||
@@ -69,7 +80,6 @@ public class AddonInventoryWindow : NativeAddon
|
|||||||
|
|
||||||
protected override unsafe void OnUpdate(AtkUnitBase* addon)
|
protected override unsafe void OnUpdate(AtkUnitBase* addon)
|
||||||
{
|
{
|
||||||
// Haven't needed it yet but just in case.
|
|
||||||
base.OnUpdate(addon);
|
base.OnUpdate(addon);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,13 +88,16 @@ public class AddonInventoryWindow : NativeAddon
|
|||||||
RefreshCategories();
|
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);
|
base.OnRequestedUpdate(addon, numberArrayData, stringArrayData);
|
||||||
RefreshCategories();
|
RefreshCategories();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RefreshCategories(bool autosize = true)
|
private void RefreshCategories(bool autosize = true)
|
||||||
{
|
{
|
||||||
|
_footerNode.SlotAmountText = InventoryState.GetEmptyItemSlotsString();
|
||||||
|
|
||||||
var categories = InventoryState.GetInventoryItemCategories(_searchInputNode.SearchString.ExtractText());
|
var categories = InventoryState.GetInventoryItemCategories(_searchInputNode.SearchString.ExtractText());
|
||||||
|
|
||||||
float maxContentWidth = MaxWindowWidth - (ContentStartPosition.X * 2);
|
float maxContentWidth = MaxWindowWidth - (ContentStartPosition.X * 2);
|
||||||
@@ -105,7 +118,32 @@ public class AddonInventoryWindow : NativeAddon
|
|||||||
node.ItemsPerLine = Math.Min(node.CategorizedInventory.Items.Count, maxItemsPerLine);
|
node.ItemsPerLine = Math.Min(node.CategorizedInventory.Items.Count, maxItemsPerLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(autosize) AutoSizeWindow();
|
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)
|
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);
|
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()
|
private void AutoSizeWindow()
|
||||||
{
|
{
|
||||||
List<InventoryCategoryNode> childNodes = _categoriesNode.GetNodes<InventoryCategoryNode>().ToList();
|
List<InventoryCategoryNode> childNodes = _categoriesNode.GetNodes<InventoryCategoryNode>().ToList();
|
||||||
@@ -122,19 +177,25 @@ public class AddonInventoryWindow : NativeAddon
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
float requiredWidth = childNodes.Max(node => node. Width);
|
float requiredWidth = childNodes.Max(node => node.Width);
|
||||||
requiredWidth += ContentStartPosition.X * 2;
|
requiredWidth += ContentStartPosition.X * 2;
|
||||||
float finalWidth = Math.Clamp(requiredWidth, MinWindowWidth, MaxWindowWidth);
|
float finalWidth = Math.Clamp(requiredWidth, MinWindowWidth, MaxWindowWidth);
|
||||||
|
|
||||||
float contentWidth = finalWidth - (ContentStartPosition.X * 2);
|
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();
|
_categoriesNode.RecalculateLayout();
|
||||||
|
|
||||||
float requiredHeight = _categoriesNode.GetRequiredHeight();
|
float requiredGridHeight = _categoriesNode.GetRequiredHeight();
|
||||||
requiredHeight += ContentStartPosition.Y + ContentStartPosition.X;
|
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);
|
ResizeWindow(finalWidth, finalHeight);
|
||||||
}
|
}
|
||||||
@@ -142,8 +203,9 @@ public class AddonInventoryWindow : NativeAddon
|
|||||||
private void ResizeWindow(float width, float height)
|
private void ResizeWindow(float width, float height)
|
||||||
{
|
{
|
||||||
SetWindowSize(width, height);
|
SetWindowSize(width, height);
|
||||||
_categoriesNode.Size = ContentSize;
|
|
||||||
_footerNode.Size = ContentSize with { Y = 28 };
|
LayoutContent();
|
||||||
|
|
||||||
_categoriesNode.RecalculateLayout();
|
_categoriesNode.RecalculateLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,5 +214,7 @@ public class AddonInventoryWindow : NativeAddon
|
|||||||
base.OnFinalize(addon);
|
base.OnFinalize(addon);
|
||||||
Services.AddonLifecycle.UnregisterListener(OnInventoryUpdate);
|
Services.AddonLifecycle.UnregisterListener(OnInventoryUpdate);
|
||||||
addon->UnsubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
|
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;
|
||||||
using KamiToolKit.Nodes;
|
using KamiToolKit.Nodes;
|
||||||
|
using System;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace AetherBags.Nodes
|
namespace AetherBags.Nodes;
|
||||||
|
|
||||||
|
public class HybridDirectionalFlexNode : HybridDirectionalFlexNode<NodeBase> { }
|
||||||
|
|
||||||
|
public class HybridDirectionalFlexNode<T> : LayoutListNode where T : NodeBase
|
||||||
{
|
{
|
||||||
public enum FlexGrowDirection
|
public FlexGrowDirection GrowDirection
|
||||||
{
|
{
|
||||||
DownRight,
|
get => field;
|
||||||
DownLeft,
|
set
|
||||||
UpRight,
|
|
||||||
UpLeft
|
|
||||||
}
|
|
||||||
|
|
||||||
public class HybridDirectionalFlexNode : HybridDirectionalFlexNode<NodeBase> { }
|
|
||||||
|
|
||||||
public class HybridDirectionalFlexNode<T> : LayoutListNode where T : NodeBase
|
|
||||||
{
|
{
|
||||||
|
if (field == value) return;
|
||||||
public FlexGrowDirection GrowDirection { get;
|
|
||||||
set {
|
|
||||||
field = value;
|
field = value;
|
||||||
RecalculateLayout();
|
RecalculateLayout();
|
||||||
}
|
}
|
||||||
} = FlexGrowDirection.DownRight;
|
} = FlexGrowDirection.DownRight;
|
||||||
|
|
||||||
public int ItemsPerLine {
|
public int ItemsPerLine
|
||||||
get;
|
{
|
||||||
set {
|
get => field;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (field == value) return;
|
||||||
field = value;
|
field = value;
|
||||||
RecalculateLayout();
|
RecalculateLayout();
|
||||||
}
|
}
|
||||||
} = 1;
|
} = 1;
|
||||||
|
|
||||||
public bool FillRowsFirst {
|
public bool FillRowsFirst
|
||||||
get;
|
{
|
||||||
set {
|
get => field;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (field == value) return;
|
||||||
field = value;
|
field = value;
|
||||||
RecalculateLayout();
|
RecalculateLayout();
|
||||||
}
|
}
|
||||||
} = true;
|
} = true;
|
||||||
|
|
||||||
public float HorizontalPadding {
|
public float HorizontalPadding
|
||||||
get;
|
{
|
||||||
set {
|
get => field;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (field.Equals(value)) return;
|
||||||
field = value;
|
field = value;
|
||||||
RecalculateLayout();
|
RecalculateLayout();
|
||||||
}
|
}
|
||||||
} = 1;
|
} = 1;
|
||||||
|
|
||||||
public float VerticalPadding {
|
public float VerticalPadding
|
||||||
get;
|
{
|
||||||
set {
|
get => field;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (field.Equals(value)) return;
|
||||||
field = value;
|
field = value;
|
||||||
RecalculateLayout();
|
RecalculateLayout();
|
||||||
}
|
}
|
||||||
@@ -59,48 +66,67 @@ namespace AetherBags.Nodes
|
|||||||
|
|
||||||
protected override void InternalRecalculateLayout()
|
protected override void InternalRecalculateLayout()
|
||||||
{
|
{
|
||||||
if (NodeList.Count == 0) {
|
int count = NodeList.Count;
|
||||||
return;
|
if (count == 0) return;
|
||||||
}
|
|
||||||
|
|
||||||
int itemsPerLine = Math.Max(1, ItemsPerLine);
|
int itemsPerLine = ItemsPerLine;
|
||||||
|
if (itemsPerLine < 1) itemsPerLine = 1;
|
||||||
|
|
||||||
float nodeWidth = NodeList.First().Width;
|
NodeBase first = NodeList[0];
|
||||||
float nodeHeight = NodeList.First().Height;
|
float nodeWidth = first.Width;
|
||||||
|
float nodeHeight = first.Height;
|
||||||
|
|
||||||
bool alignRight = GrowDirection is FlexGrowDirection.DownLeft or FlexGrowDirection.UpLeft;
|
float hPad = HorizontalPadding;
|
||||||
bool alignBottom = GrowDirection is FlexGrowDirection.UpRight or FlexGrowDirection.UpLeft;
|
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 startX = alignRight ? Width : 0f;
|
||||||
float startY = alignBottom ? Height : 0f;
|
float startY = alignBottom ? Height : 0f;
|
||||||
|
|
||||||
int idx = 0;
|
float stepX = nodeWidth + hPad;
|
||||||
foreach (var node in NodeList)
|
float stepY = nodeHeight + vPad;
|
||||||
|
|
||||||
|
bool fillRowsFirst = FillRowsFirst;
|
||||||
|
|
||||||
|
int major = 0;
|
||||||
|
int minor = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
int row, col;
|
int row, col;
|
||||||
if (FillRowsFirst)
|
if (fillRowsFirst)
|
||||||
{
|
{
|
||||||
row = idx / itemsPerLine;
|
row = major;
|
||||||
col = idx % itemsPerLine;
|
col = minor;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
col = idx / itemsPerLine;
|
col = major;
|
||||||
row = idx % itemsPerLine;
|
row = minor;
|
||||||
}
|
}
|
||||||
|
|
||||||
float x = alignRight
|
float x = alignRight
|
||||||
? startX - (col + 1) * nodeWidth - col * HorizontalPadding
|
? startX - nodeWidth - col * stepX
|
||||||
: startX + col * (nodeWidth + HorizontalPadding);
|
: startX + col * stepX;
|
||||||
|
|
||||||
float y = alignBottom
|
float y = alignBottom
|
||||||
? startY - (row + 1) * nodeHeight - row * VerticalPadding
|
? startY - nodeHeight - row * stepY
|
||||||
: startY + row * (nodeHeight + VerticalPadding);
|
: startY + row * stepY;
|
||||||
|
|
||||||
|
NodeBase node = NodeList[i];
|
||||||
node.X = x;
|
node.X = x;
|
||||||
node.Y = y;
|
node.Y = y;
|
||||||
|
|
||||||
AdjustNode(node);
|
AdjustNode(node);
|
||||||
idx++;
|
|
||||||
|
minor++;
|
||||||
|
if (minor == itemsPerLine)
|
||||||
|
{
|
||||||
|
minor = 0;
|
||||||
|
major++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,16 @@ using System;
|
|||||||
using KamiToolKit;
|
using KamiToolKit;
|
||||||
using KamiToolKit.Nodes;
|
using KamiToolKit.Nodes;
|
||||||
|
|
||||||
namespace AetherBags.Nodes
|
namespace AetherBags.Nodes;
|
||||||
|
|
||||||
|
public class HybridDirectionalStackNode<T> : LayoutListNode where T : NodeBase
|
||||||
{
|
{
|
||||||
public class HybridDirectionalStackNode<T> : LayoutListNode where T : NodeBase
|
|
||||||
{
|
|
||||||
public FlexGrowDirection GrowDirection
|
public FlexGrowDirection GrowDirection
|
||||||
{
|
{
|
||||||
get;
|
get => field;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
|
if (field == value) return;
|
||||||
field = value;
|
field = value;
|
||||||
RecalculateLayout();
|
RecalculateLayout();
|
||||||
}
|
}
|
||||||
@@ -18,9 +19,10 @@ namespace AetherBags.Nodes
|
|||||||
|
|
||||||
public bool Vertical
|
public bool Vertical
|
||||||
{
|
{
|
||||||
get;
|
get => field;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
|
if (field == value) return;
|
||||||
field = value;
|
field = value;
|
||||||
RecalculateLayout();
|
RecalculateLayout();
|
||||||
}
|
}
|
||||||
@@ -28,9 +30,10 @@ namespace AetherBags.Nodes
|
|||||||
|
|
||||||
public float Spacing
|
public float Spacing
|
||||||
{
|
{
|
||||||
get;
|
get => field;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
|
if (field.Equals(value)) return;
|
||||||
field = value;
|
field = value;
|
||||||
RecalculateLayout();
|
RecalculateLayout();
|
||||||
}
|
}
|
||||||
@@ -38,9 +41,10 @@ namespace AetherBags.Nodes
|
|||||||
|
|
||||||
public bool StretchCrossAxis
|
public bool StretchCrossAxis
|
||||||
{
|
{
|
||||||
get;
|
get => field;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
|
if (field == value) return;
|
||||||
field = value;
|
field = value;
|
||||||
RecalculateLayout();
|
RecalculateLayout();
|
||||||
}
|
}
|
||||||
@@ -48,46 +52,64 @@ namespace AetherBags.Nodes
|
|||||||
|
|
||||||
protected override void InternalRecalculateLayout()
|
protected override void InternalRecalculateLayout()
|
||||||
{
|
{
|
||||||
if (NodeList.Count == 0)
|
int count = NodeList.Count;
|
||||||
return;
|
if (count == 0) return;
|
||||||
|
|
||||||
bool alignRight = GrowDirection is FlexGrowDirection.DownLeft or FlexGrowDirection.UpLeft;
|
FlexGrowDirection dir = GrowDirection;
|
||||||
bool alignBottom = GrowDirection is FlexGrowDirection.UpRight or FlexGrowDirection.UpLeft;
|
bool alignRight = dir == FlexGrowDirection.DownLeft || dir == FlexGrowDirection.UpLeft;
|
||||||
|
bool alignBottom = dir == FlexGrowDirection.UpRight || dir == FlexGrowDirection.UpLeft;
|
||||||
|
|
||||||
float startX = alignRight ? Width : 0f;
|
bool vertical = Vertical;
|
||||||
float startY = alignBottom ? Height : 0f;
|
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;
|
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 (stretchCross)
|
||||||
{
|
node.Width = containerW;
|
||||||
if (Vertical)
|
|
||||||
node.Width = Width;
|
|
||||||
else
|
|
||||||
node.Height = Height;
|
|
||||||
}
|
|
||||||
|
|
||||||
float x, y;
|
float w = node.Width;
|
||||||
if (Vertical)
|
float h = node.Height;
|
||||||
{
|
|
||||||
x = alignRight ? startX - node.Width : startX;
|
node.X = alignRight ? startX - w : startX;
|
||||||
y = alignBottom ? startY - node.Height - cursor : startY + cursor;
|
node.Y = alignBottom ? startY - h - 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
node.X = x;
|
|
||||||
node.Y = y;
|
|
||||||
AdjustNode(node);
|
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.Extensions;
|
||||||
using AetherBags.Inventory;
|
using AetherBags.Inventory;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
|
using KamiToolKit;
|
||||||
using KamiToolKit.Classes;
|
using KamiToolKit.Classes;
|
||||||
using KamiToolKit.Nodes;
|
using KamiToolKit.Nodes;
|
||||||
|
using System;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
namespace AetherBags.Nodes;
|
namespace AetherBags.Nodes;
|
||||||
|
|
||||||
@@ -16,32 +15,50 @@ public class InventoryCategoryNode : SimpleComponentNode
|
|||||||
private readonly TextNode _categoryNameTextNode;
|
private readonly TextNode _categoryNameTextNode;
|
||||||
private readonly HybridDirectionalFlexNode<DragDropNode> _itemGridNode;
|
private readonly HybridDirectionalFlexNode<DragDropNode> _itemGridNode;
|
||||||
|
|
||||||
private const float ItemSize = 40;
|
private const float FallbackItemSize = 46;
|
||||||
private const float ItemHorizontalPadding = 6;
|
|
||||||
private const float ItemVerticalPadding = 6;
|
|
||||||
private const float HeaderHeight = 16;
|
private const float HeaderHeight = 16;
|
||||||
private const float MinWidth = 40;
|
private const float MinWidth = 40;
|
||||||
|
|
||||||
private float? _fixedWidth;
|
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()
|
public InventoryCategoryNode()
|
||||||
{
|
{
|
||||||
_categoryNameTextNode = new TextNode
|
_categoryNameTextNode = new TextNode
|
||||||
{
|
{
|
||||||
Size = new Vector2(100, 14),
|
Size = new Vector2(96, 16),
|
||||||
AlignmentType = AlignmentType.Left
|
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);
|
_categoryNameTextNode.AttachNode(this);
|
||||||
|
|
||||||
_itemGridNode = new HybridDirectionalFlexNode<DragDropNode>
|
_itemGridNode = new HybridDirectionalFlexNode<DragDropNode>
|
||||||
{
|
{
|
||||||
Position = new Vector2(0, HeaderHeight),
|
Position = new Vector2(0, HeaderHeight),
|
||||||
Size = new Vector2(240, 100),
|
Size = new Vector2(240, 92),
|
||||||
FillRowsFirst = true,
|
FillRowsFirst = true,
|
||||||
ItemsPerLine = 10,
|
ItemsPerLine = 10,
|
||||||
HorizontalPadding = ItemHorizontalPadding,
|
HorizontalPadding = 5,
|
||||||
VerticalPadding = ItemVerticalPadding,
|
VerticalPadding = 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_itemGridNode.NodeFlags |= NodeFlags.EmitsEvents;
|
||||||
_itemGridNode.AttachNode(this);
|
_itemGridNode.AttachNode(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,8 +69,11 @@ public class InventoryCategoryNode : SimpleComponentNode
|
|||||||
{
|
{
|
||||||
field = value;
|
field = value;
|
||||||
|
|
||||||
_categoryNameTextNode.String = value.Category.Name;
|
_fullHeaderText = value.Category.Name;
|
||||||
|
|
||||||
|
_categoryNameTextNode.String = _fullHeaderText;
|
||||||
_categoryNameTextNode.TextColor = value.Category.Color;
|
_categoryNameTextNode.TextColor = value.Category.Color;
|
||||||
|
|
||||||
_categoryNameTextNode.TooltipString = value.Category.Description;
|
_categoryNameTextNode.TooltipString = value.Category.Description;
|
||||||
|
|
||||||
UpdateItemGrid();
|
UpdateItemGrid();
|
||||||
@@ -66,6 +86,7 @@ public class InventoryCategoryNode : SimpleComponentNode
|
|||||||
get => _itemGridNode.ItemsPerLine;
|
get => _itemGridNode.ItemsPerLine;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
|
if (_itemGridNode.ItemsPerLine == value) return;
|
||||||
_itemGridNode.ItemsPerLine = value;
|
_itemGridNode.ItemsPerLine = value;
|
||||||
RecalculateSize();
|
RecalculateSize();
|
||||||
}
|
}
|
||||||
@@ -76,55 +97,156 @@ public class InventoryCategoryNode : SimpleComponentNode
|
|||||||
get => _fixedWidth;
|
get => _fixedWidth;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
|
if (_fixedWidth.Equals(value)) return;
|
||||||
_fixedWidth = value;
|
_fixedWidth = value;
|
||||||
RecalculateSize();
|
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()
|
private void RecalculateSize()
|
||||||
{
|
{
|
||||||
int itemCount = CategorizedInventory.Items.Count;
|
int itemCount = CategorizedInventory.Items.Count;
|
||||||
|
|
||||||
if (itemCount == 0)
|
if (itemCount == 0)
|
||||||
{
|
{
|
||||||
float width = _fixedWidth ?? MinWidth;
|
float width = _fixedWidth ?? MinWidth;
|
||||||
|
|
||||||
Size = new Vector2(width, HeaderHeight);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int itemsPerLine = Math.Max(1, _itemGridNode.ItemsPerLine);
|
int itemsPerLine = _itemGridNode.ItemsPerLine;
|
||||||
int rows = (int)Math.Ceiling((float)itemCount / 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;
|
float calculatedWidth;
|
||||||
if (_fixedWidth. HasValue)
|
if (_fixedWidth.HasValue)
|
||||||
{
|
{
|
||||||
calculatedWidth = _fixedWidth.Value;
|
calculatedWidth = _fixedWidth.Value;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
int actualColumns = Math.Min(itemCount, itemsPerLine);
|
calculatedWidth = actualColumns * cellW + (actualColumns - 1) * hPad;
|
||||||
calculatedWidth = actualColumns * ItemSize + (actualColumns - 1) * ItemHorizontalPadding;
|
if (calculatedWidth < MinWidth) calculatedWidth = MinWidth;
|
||||||
calculatedWidth = Math.Max(calculatedWidth, MinWidth);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
float height = HeaderHeight + rows * ItemSize + (rows - 1) * ItemVerticalPadding;
|
float height = HeaderHeight + rows * cellH + (rows - 1) * vPad;
|
||||||
|
|
||||||
Size = new Vector2(calculatedWidth, height);
|
Size = new Vector2(calculatedWidth, height);
|
||||||
|
|
||||||
|
_itemGridNode.Position = new Vector2(0, HeaderHeight);
|
||||||
_itemGridNode.Size = new Vector2(calculatedWidth, height - HeaderHeight);
|
_itemGridNode.Size = new Vector2(calculatedWidth, height - HeaderHeight);
|
||||||
_categoryNameTextNode.Size = _categoryNameTextNode.Size with { X = calculatedWidth };
|
|
||||||
|
_baseHeaderWidth = calculatedWidth;
|
||||||
|
|
||||||
|
ApplyHeaderVisualStateAndSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateItemGrid()
|
private void UpdateItemGrid()
|
||||||
{
|
{
|
||||||
_itemGridNode.SyncWithListData(CategorizedInventory.Items, node => node.ItemInfo, CreateInventoryDragDropNode);
|
_itemGridNode.SyncWithListData(
|
||||||
|
CategorizedInventory.Items,
|
||||||
|
node => node.ItemInfo,
|
||||||
|
CreateInventoryDragDropNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private InventoryDragDropNode CreateInventoryDragDropNode(ItemInfo data)
|
private InventoryDragDropNode CreateInventoryDragDropNode(ItemInfo data)
|
||||||
{
|
{
|
||||||
InventoryItem item = data.Item;
|
InventoryItem item = data.Item;
|
||||||
InventoryDragDropNode node = new InventoryDragDropNode
|
|
||||||
|
var node = new InventoryDragDropNode
|
||||||
{
|
{
|
||||||
Size = new Vector2(40),
|
Size = new Vector2(42, 46),
|
||||||
IsVisible = true,
|
IsVisible = true,
|
||||||
IconId = item.IconId,
|
IconId = item.IconId,
|
||||||
AcceptedType = DragDropType.Nothing,
|
AcceptedType = DragDropType.Nothing,
|
||||||
@@ -136,10 +258,21 @@ public class InventoryCategoryNode : SimpleComponentNode
|
|||||||
Int2 = (int)item.ItemId,
|
Int2 = (int)item.ItemId,
|
||||||
},
|
},
|
||||||
IsClickable = true,
|
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
|
ItemInfo = data
|
||||||
};
|
};
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,26 +1,44 @@
|
|||||||
|
using KamiToolKit;
|
||||||
|
using KamiToolKit.Nodes;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using KamiToolKit;
|
|
||||||
using KamiToolKit. Nodes;
|
|
||||||
|
|
||||||
namespace AetherBags.Nodes
|
namespace AetherBags.Nodes;
|
||||||
|
|
||||||
|
public sealed class WrappingGridNode<T> : LayoutListNode where T : NodeBase
|
||||||
{
|
{
|
||||||
public sealed class WrappingGridNode<T> : LayoutListNode where T : NodeBase
|
|
||||||
{
|
|
||||||
|
|
||||||
public float HorizontalSpacing { get; set; } = 10f;
|
public float HorizontalSpacing { get; set; } = 10f;
|
||||||
public float VerticalSpacing { 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 List<List<NodeBase>> _rows = new();
|
||||||
private readonly Stack<List<NodeBase>> _rowPool = new();
|
private readonly Stack<List<NodeBase>> _rowPool = new();
|
||||||
|
|
||||||
|
private readonly Dictionary<NodeBase, int> _rowIndex = new(ReferenceEqualityComparer<NodeBase>.Instance);
|
||||||
|
|
||||||
private float _requiredHeight;
|
private float _requiredHeight;
|
||||||
private bool _requiredHeightDirty = true;
|
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()
|
protected override void InternalRecalculateLayout()
|
||||||
{
|
{
|
||||||
RecycleAllRows();
|
RecycleAllRows();
|
||||||
|
_rowIndex.Clear();
|
||||||
|
|
||||||
int count = NodeList.Count;
|
int count = NodeList.Count;
|
||||||
if (count == 0)
|
if (count == 0)
|
||||||
@@ -30,15 +48,18 @@ namespace AetherBags.Nodes
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_rowIndex.EnsureCapacity(count);
|
||||||
|
|
||||||
float availableWidth = Width;
|
float availableWidth = Width;
|
||||||
float hSpace = HorizontalSpacing;
|
float hSpace = HorizontalSpacing;
|
||||||
float vSpace = VerticalSpacing;
|
float vSpace = VerticalSpacing;
|
||||||
float startX = FirstItemSpacing;
|
float startX = FirstItemSpacing;
|
||||||
|
|
||||||
float currentX = startX;
|
float currentX = startX;
|
||||||
float currentY = 0f;
|
float currentY = TopPadding;
|
||||||
float rowHeight = 0f;
|
float rowHeight = 0f;
|
||||||
|
|
||||||
|
int currentRowIndex = 0;
|
||||||
List<NodeBase> currentRow = RentRowList(capacityHint: 8);
|
List<NodeBase> currentRow = RentRowList(capacityHint: 8);
|
||||||
|
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
@@ -50,6 +71,7 @@ namespace AetherBags.Nodes
|
|||||||
if (currentRow.Count != 0 && (currentX + nodeWidth) > availableWidth)
|
if (currentRow.Count != 0 && (currentX + nodeWidth) > availableWidth)
|
||||||
{
|
{
|
||||||
_rows.Add(currentRow);
|
_rows.Add(currentRow);
|
||||||
|
currentRowIndex++;
|
||||||
|
|
||||||
currentY += rowHeight + vSpace;
|
currentY += rowHeight + vSpace;
|
||||||
currentX = startX;
|
currentX = startX;
|
||||||
@@ -67,6 +89,7 @@ namespace AetherBags.Nodes
|
|||||||
if (nodeHeight > rowHeight) rowHeight = nodeHeight;
|
if (nodeHeight > rowHeight) rowHeight = nodeHeight;
|
||||||
|
|
||||||
currentRow.Add(node);
|
currentRow.Add(node);
|
||||||
|
_rowIndex[node] = currentRowIndex;
|
||||||
|
|
||||||
currentX += nodeWidth + hSpace;
|
currentX += nodeWidth + hSpace;
|
||||||
}
|
}
|
||||||
@@ -98,6 +121,9 @@ namespace AetherBags.Nodes
|
|||||||
if (bottom > maxBottom) maxBottom = bottom;
|
if (bottom > maxBottom) maxBottom = bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
maxBottom += BottomPadding;
|
||||||
|
|
||||||
_requiredHeight = maxBottom;
|
_requiredHeight = maxBottom;
|
||||||
_requiredHeightDirty = false;
|
_requiredHeightDirty = false;
|
||||||
return maxBottom;
|
return maxBottom;
|
||||||
@@ -133,5 +159,35 @@ namespace AetherBags.Nodes
|
|||||||
row.Clear();
|
row.Clear();
|
||||||
_rowPool.Push(row);
|
_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