Pinning, Hoisting, Recently Lotted
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AetherBags.Inventory.Items;
|
||||
using AetherBags.Nodes.Layout;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Nodes;
|
||||
|
||||
namespace AetherBags.Nodes.Inventory;
|
||||
|
||||
/// <summary>
|
||||
/// A special category node for displaying recently looted items.
|
||||
/// Items are not draggable but can be dismissed individually or cleared entirely.
|
||||
/// </summary>
|
||||
public class LootedItemsCategoryNode : InventoryCategoryNodeBase
|
||||
{
|
||||
private const uint LootedCategoryKey = 0x20000001;
|
||||
|
||||
public override uint Key => LootedCategoryKey;
|
||||
private readonly TextNode _headerTextNode;
|
||||
private readonly CircleButtonNode _clearButton;
|
||||
private readonly HybridDirectionalFlexNode<LootedItemDisplayNode> _itemGridNode;
|
||||
|
||||
private const float HeaderHeight = 20;
|
||||
private const float ClearButtonSize = 20;
|
||||
private const float MinWidth = 100;
|
||||
|
||||
private IReadOnlyList<LootedItemInfo> _lootedItems = Array.Empty<LootedItemInfo>();
|
||||
|
||||
private int _hoverRefs;
|
||||
private bool _headerExpanded;
|
||||
private float _baseHeaderWidth = 96f;
|
||||
private string _fullHeaderText = "Recently Looted";
|
||||
|
||||
public event Action<LootedItemsCategoryNode, bool>? HeaderHoverChanged;
|
||||
public Action<int>? OnDismissItem { get; set; }
|
||||
public Action? OnClearAll { get; set; }
|
||||
|
||||
public int ItemsPerLine
|
||||
{
|
||||
get => _itemGridNode.ItemsPerLine;
|
||||
set
|
||||
{
|
||||
if (_itemGridNode.ItemsPerLine == value) return;
|
||||
_itemGridNode.ItemsPerLine = value;
|
||||
RecalculateSize();
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasItems => _lootedItems.Count > 0;
|
||||
|
||||
public LootedItemsCategoryNode()
|
||||
{
|
||||
_headerTextNode = new TextNode
|
||||
{
|
||||
Position = Vector2.Zero,
|
||||
Size = new Vector2(96, HeaderHeight),
|
||||
AlignmentType = AlignmentType.Left,
|
||||
String = "Recently Looted",
|
||||
TextFlags = TextFlags.OverflowHidden | TextFlags.Ellipsis,
|
||||
TextColor = new Vector4(0.9f, 0.8f, 0.5f, 1.0f), // Gold-ish color
|
||||
};
|
||||
|
||||
_headerTextNode.AddEvent(AtkEventType.MouseOver, BeginHeaderHover);
|
||||
_headerTextNode.AddEvent(AtkEventType.MouseOut, EndHeaderHover);
|
||||
|
||||
_headerTextNode.TextFlags |= TextFlags.OverflowHidden | TextFlags.Ellipsis;
|
||||
_headerTextNode.TextFlags &= ~(TextFlags.WordWrap | TextFlags.MultiLine);
|
||||
|
||||
_headerTextNode.AddFlags(NodeFlags.EmitsEvents | NodeFlags.HasCollision);
|
||||
_headerTextNode.AttachNode(this);
|
||||
|
||||
_clearButton = new CircleButtonNode
|
||||
{
|
||||
Size = new Vector2(ClearButtonSize),
|
||||
Icon = ButtonIcon.CrossSmall,
|
||||
OnClick = () => OnClearAll?.Invoke(),
|
||||
};
|
||||
_clearButton.AttachNode(this);
|
||||
|
||||
_itemGridNode = new HybridDirectionalFlexNode<LootedItemDisplayNode>
|
||||
{
|
||||
Position = new Vector2(0, HeaderHeight),
|
||||
Size = new Vector2(240, 92),
|
||||
FillRowsFirst = true,
|
||||
ItemsPerLine = 10,
|
||||
HorizontalPadding = 5,
|
||||
VerticalPadding = 2,
|
||||
};
|
||||
_itemGridNode.NodeFlags |= NodeFlags.EmitsEvents;
|
||||
_itemGridNode.AttachNode(this);
|
||||
|
||||
RecalculateSize();
|
||||
}
|
||||
|
||||
public void UpdateLootedItems(IReadOnlyList<LootedItemInfo> lootedItems)
|
||||
{
|
||||
_lootedItems = lootedItems;
|
||||
UpdateHeaderText();
|
||||
SyncItemGrid();
|
||||
RecalculateSize();
|
||||
}
|
||||
|
||||
private void UpdateHeaderText()
|
||||
{
|
||||
_fullHeaderText = _lootedItems.Count > 0
|
||||
? $"Recently Looted ({_lootedItems.Count})"
|
||||
: "Recently Looted";
|
||||
|
||||
_headerTextNode.String = _fullHeaderText;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private void ApplyHeaderVisualStateAndSize()
|
||||
{
|
||||
var flags = _headerTextNode.TextFlags;
|
||||
flags &= ~(TextFlags.WordWrap | TextFlags.MultiLine);
|
||||
|
||||
if (_headerExpanded)
|
||||
{
|
||||
flags &= ~(TextFlags.OverflowHidden | TextFlags.Ellipsis);
|
||||
_headerTextNode.TextFlags = flags;
|
||||
|
||||
if (!string.IsNullOrEmpty(_fullHeaderText))
|
||||
_headerTextNode.String = _fullHeaderText;
|
||||
|
||||
Vector2 drawSize = _headerTextNode.GetTextDrawSize();
|
||||
float expandedWidth = MathF.Max(_baseHeaderWidth, drawSize.X + 4f);
|
||||
_headerTextNode.Size = _headerTextNode.Size with { X = expandedWidth };
|
||||
}
|
||||
else
|
||||
{
|
||||
_headerTextNode.Size = _headerTextNode.Size with { X = _baseHeaderWidth };
|
||||
|
||||
if (!string.IsNullOrEmpty(_fullHeaderText))
|
||||
_headerTextNode.String = _fullHeaderText;
|
||||
|
||||
flags |= TextFlags.OverflowHidden | TextFlags.Ellipsis;
|
||||
_headerTextNode.TextFlags = flags;
|
||||
}
|
||||
}
|
||||
|
||||
private void SyncItemGrid()
|
||||
{
|
||||
_itemGridNode.SyncWithListData(
|
||||
_lootedItems,
|
||||
node => node.LootedItem,
|
||||
CreateLootedItemNode);
|
||||
}
|
||||
|
||||
private LootedItemDisplayNode CreateLootedItemNode(LootedItemInfo lootedItem)
|
||||
{
|
||||
var node = new LootedItemDisplayNode
|
||||
{
|
||||
OnDismiss = OnItemDismissed,
|
||||
};
|
||||
node.SetLootedItem(lootedItem);
|
||||
return node;
|
||||
}
|
||||
|
||||
private void OnItemDismissed(LootedItemDisplayNode node)
|
||||
{
|
||||
int index = node.LootedItem.Index;
|
||||
OnDismissItem?.Invoke(index);
|
||||
}
|
||||
|
||||
private void RecalculateSize()
|
||||
{
|
||||
int itemCount = _lootedItems.Count;
|
||||
|
||||
if (itemCount == 0)
|
||||
{
|
||||
float width = MinWidth;
|
||||
Size = new Vector2(width, HeaderHeight);
|
||||
_baseHeaderWidth = width - ClearButtonSize - 4;
|
||||
_headerTextNode.Size = new Vector2(_baseHeaderWidth, HeaderHeight);
|
||||
_clearButton.Position = new Vector2(width - ClearButtonSize, (HeaderHeight - ClearButtonSize) / 2);
|
||||
_clearButton.IsVisible = false;
|
||||
_itemGridNode.Position = new Vector2(0, HeaderHeight);
|
||||
_itemGridNode.Size = new Vector2(width, 0);
|
||||
ApplyHeaderVisualStateAndSize();
|
||||
return;
|
||||
}
|
||||
|
||||
int itemsPerLine = Math.Max(1, _itemGridNode.ItemsPerLine);
|
||||
int rows = (itemCount + itemsPerLine - 1) / itemsPerLine;
|
||||
int actualColumns = Math.Min(itemCount, itemsPerLine);
|
||||
|
||||
const float cellW = 42f;
|
||||
const float cellH = 46f;
|
||||
|
||||
float hPad = _itemGridNode.HorizontalPadding;
|
||||
float vPad = _itemGridNode.VerticalPadding;
|
||||
|
||||
float calculatedWidth = Math.Max(MinWidth, actualColumns * cellW + (actualColumns - 1) * hPad);
|
||||
float gridHeight = rows * cellH + (rows - 1) * vPad;
|
||||
float totalHeight = HeaderHeight + gridHeight;
|
||||
|
||||
Size = new Vector2(calculatedWidth, totalHeight);
|
||||
_baseHeaderWidth = calculatedWidth - ClearButtonSize - 4;
|
||||
_headerTextNode.Size = new Vector2(_baseHeaderWidth, HeaderHeight);
|
||||
_clearButton.Position = new Vector2(calculatedWidth - ClearButtonSize, (HeaderHeight - ClearButtonSize) / 2);
|
||||
_clearButton.IsVisible = true;
|
||||
_itemGridNode.Position = new Vector2(0, HeaderHeight);
|
||||
_itemGridNode.Size = new Vector2(calculatedWidth, gridHeight);
|
||||
ApplyHeaderVisualStateAndSize();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user