Files
AetherBags/KamiToolKit/Nodes/Layout/LayoutListNode.cs
T
KnackAtNite 8db4ce6094
Debug Build and Test / Build against Latest Dalamud (push) Has been cancelled
Debug Build and Test / Build against Staging Dalamud (push) Has been cancelled
Initial commit: AetherBags + KamiToolKit for FC Gitea
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 14:46:31 -05:00

318 lines
8.3 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace KamiToolKit.Nodes;
public abstract class LayoutListNode : SimpleComponentNode {
protected readonly List<NodeBase> NodeList = [];
private bool suppressRecalculateLayout;
public IEnumerable<T> GetNodes<T>() where T : NodeBase => NodeList.OfType<T>();
public IReadOnlyList<NodeBase> Nodes => NodeList;
public bool ClipListContents {
get => NodeFlags.HasFlag(NodeFlags.Clip);
set {
if (value) {
AddNodeFlags(NodeFlags.Clip);
}
else {
RemoveNodeFlags(NodeFlags.Clip);
}
}
}
public float ItemSpacing { get; set; }
public float FirstItemSpacing { get; set; }
public void RecalculateLayout() {
if (suppressRecalculateLayout) return;
OnRecalculateLayout();
foreach (var node in NodeList) {
if (node is LayoutListNode subNode) {
subNode.RecalculateLayout();
}
}
}
protected abstract void OnRecalculateLayout();
protected virtual void AdjustNode(NodeBase node) { }
public ICollection<NodeBase> InitialNodes {
init => AddNode(value);
}
public void AddNode(IEnumerable<NodeBase> nodes)
{
suppressRecalculateLayout = true;
try
{
foreach (var node in nodes)
{
AddNode(node);
}
}
finally
{
suppressRecalculateLayout = false;
}
RecalculateLayout();
}
public virtual void AddNode(NodeBase? node) {
if (node is null) return;
NodeList.Add(node);
node.AttachNode(this);
RecalculateLayout();
}
public void RemoveNode(params NodeBase[] items)
{
suppressRecalculateLayout = true;
try
{
foreach (var node in items)
{
RemoveNode(node);
}
}
finally
{
suppressRecalculateLayout = false;
}
RecalculateLayout();
}
public virtual void RemoveNode(NodeBase node) {
if (!NodeList.Contains(node)) return;
NodeList.Remove(node);
node.Dispose();
RecalculateLayout();
}
public void AddDummy(float size = 0.0f) {
var dummyNode = new ResNode {
Size = new Vector2(size, size),
};
AddNode(dummyNode);
}
public virtual void Clear()
{
suppressRecalculateLayout = true;
try
{
foreach (var node in NodeList.ToList())
{
RemoveNode(node);
}
}
finally
{
suppressRecalculateLayout = false;
}
RecalculateLayout();
}
public delegate TU CreateNewNode<in T, out TU>(T data) where TU : NodeBase;
public delegate T GetDataFromNode<out T, in TU>(TU node) where TU : NodeBase;
public bool SyncWithListData<T, TU>(IEnumerable<T> dataList, GetDataFromNode<T?, TU> getDataFromNode, CreateNewNode<T, TU> createNodeMethod) where TU : NodeBase
{
suppressRecalculateLayout = true;
var anythingChanged = false;
try
{
var nodesOfType = GetNodes<TU>().ToList();
var dataSet = dataList.ToHashSet(EqualityComparer<T>.Default);
var represented = new HashSet<T>(EqualityComparer<T>.Default);
foreach (var node in nodesOfType)
{
var nodeData = getDataFromNode(node);
if (nodeData is null || !dataSet.Contains(nodeData))
{
RemoveNode(node);
anythingChanged = true;
continue;
}
represented.Add(nodeData);
}
foreach (var data in dataSet)
{
if (represented.Contains(data))
continue;
var newNode = createNodeMethod(data);
AddNode(newNode);
anythingChanged = true;
}
}
finally
{
suppressRecalculateLayout = false;
}
RecalculateLayout();
return anythingChanged;
}
public bool SyncWithListDataByKey<T, TU, TKey>(
IReadOnlyList<T> dataList,
Func<T, TKey> getKeyFromData,
Func<TU, TKey> getKeyFromNode,
Action<TU, T> updateNode,
CreateNewNode<T, TU> createNodeMethod,
IEqualityComparer<TKey>? keyComparer = null) where TU : NodeBase where TKey : notnull
{
suppressRecalculateLayout = true;
var anythingChanged = false;
try
{
keyComparer ??= EqualityComparer<TKey>.Default;
var existing = new List<TU>(capacity: NodeList.Count);
foreach (var t in NodeList)
{
if (t is TU tu)
existing.Add(tu);
}
var byKey = new Dictionary<TKey, TU>(existing.Count, keyComparer);
List<TU>? duplicates = null;
foreach (var node in existing)
{
var key = getKeyFromNode(node);
if (!byKey.TryAdd(key, node))
(duplicates ??= new List<TU>(4)).Add(node);
}
var desired = new List<TU>(dataList.Count);
foreach (var data in dataList)
{
var key = getKeyFromData(data);
if (byKey.TryGetValue(key, out var existingNode))
{
updateNode(existingNode, data);
desired.Add(existingNode);
byKey.Remove(key);
}
else
{
var newNode = createNodeMethod(data);
AddNode(newNode);
updateNode(newNode, data);
desired.Add(newNode);
anythingChanged = true;
}
}
if (byKey.Count != 0)
{
foreach (var kv in byKey)
{
RemoveNode(kv.Value);
anythingChanged = true;
}
}
if (duplicates is not null)
{
for (var i = 0; i < duplicates.Count; i++)
{
RemoveNode(duplicates[i]);
anythingChanged = true;
}
}
var desiredCount = desired.Count;
var j = 0;
var mismatch = false;
for (var i = 0; i < NodeList.Count; i++)
{
if (NodeList[i] is TU)
{
if (j >= desiredCount)
{
mismatch = true;
break;
}
NodeBase desiredNode = desired[j++];
if (!ReferenceEquals(NodeList[i], desiredNode))
{
NodeList[i] = desiredNode;
anythingChanged = true;
}
}
}
if (!mismatch && j != desiredCount)
mismatch = true;
if (mismatch)
{
var firstTuIndex = -1;
for (var i = 0; i < NodeList.Count; i++)
{
if (NodeList[i] is TU)
{
firstTuIndex = i;
break;
}
}
if (firstTuIndex < 0)
firstTuIndex = NodeList.Count;
for (var i = NodeList.Count - 1; i >= 0; i--)
{
if (NodeList[i] is TU)
NodeList.RemoveAt(i);
}
NodeList.InsertRange(firstTuIndex, desired);
anythingChanged = true;
}
}
finally
{
suppressRecalculateLayout = false;
}
RecalculateLayout();
return anythingChanged;
}
public void ReorderNodes(Comparison<NodeBase> comparison) {
NodeList.Sort(comparison);
RecalculateLayout();
}
}