Initial commit: AetherBags + KamiToolKit for FC Gitea
Debug Build and Test / Build against Latest Dalamud (push) Has been cancelled
Debug Build and Test / Build against Staging Dalamud (push) Has been cancelled

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-08 14:46:31 -05:00
commit 8db4ce6094
375 changed files with 34124 additions and 0 deletions
@@ -0,0 +1,7 @@
namespace KamiToolKit.Nodes;
public class AlignedHorizontalListNode : HorizontalListNode {
protected override void AdjustNode(NodeBase node) {
node.Y = 0.0f;
}
}
@@ -0,0 +1,7 @@
namespace KamiToolKit.Nodes;
public abstract class AlignedVerticalListNode : VerticalListNode {
protected override void AdjustNode(NodeBase node) {
node.X = 0.0f;
}
}
+70
View File
@@ -0,0 +1,70 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace KamiToolKit.Nodes;
public record GridSize(int Columns, int Rows);
public class GridNode : SimpleComponentNode {
private readonly List<SimpleComponentNode> gridNodes = [];
public SimpleComponentNode this[int x, int y] {
get => gridNodes[x + y * GridSize.Columns];
set => gridNodes[x + y * GridSize.Columns] = value;
}
public SimpleComponentNode this[int index] {
get => gridNodes[index];
set => gridNodes[index] = value;
}
/// <summary>
/// Warning: Changing this value will dispose any existing layout nodes.
/// </summary>
public required GridSize GridSize {
get;
set {
field = value;
ReallocateArray();
}
} = new(0, 0);
private void ReallocateArray() {
foreach (var node in gridNodes) {
node.Dispose();
}
gridNodes.Clear();
foreach (var _ in Enumerable.Range(0, GridSize.Rows * GridSize.Columns)) {
gridNodes.Add(new SimpleComponentNode());
}
foreach (var row in Enumerable.Range(0, GridSize.Rows)) {
foreach (var column in Enumerable.Range(0, GridSize.Columns)) {
this[column, row].AttachNode(this);
this[column, row].IsVisible = true;
}
}
RecalculateLayout();
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
RecalculateLayout();
}
public void RecalculateLayout() {
var gridWidth = Width / GridSize.Columns;
var gridHeight = Height / GridSize.Rows;
foreach (var row in Enumerable.Range(0, GridSize.Rows)) {
foreach (var column in Enumerable.Range(0, GridSize.Columns)) {
this[column, row].Size = new Vector2(gridWidth, gridHeight);
this[column, row].Position = new Vector2(column * gridWidth, row * gridHeight);
}
}
}
}
@@ -0,0 +1,49 @@
using System.Linq;
using KamiToolKit.Enums;
namespace KamiToolKit.Nodes;
public class HorizontalFlexNode : LayoutListNode {
public FlexFlags AlignmentFlags { get; set; } = FlexFlags.FitContentHeight;
public float FitPadding { get; set; } = 4.0f;
public override float Width {
get => base.Width;
set {
base.Width = value;
RecalculateLayout();
}
}
protected override void OnRecalculateLayout() {
var step = Width / NodeList.Count;
if (NodeList.Count != 0 && AlignmentFlags.HasFlag(FlexFlags.FitContentHeight)) {
Height = NodeList.Max(node => node.Height);
}
foreach (var index in Enumerable.Range(0, NodeList.Count)) {
if (AlignmentFlags.HasFlag(FlexFlags.CenterHorizontally)) {
NodeList[index].X = step * index + step / 2.0f - NodeList[index].Width / 2.0f;
}
else {
NodeList[index].X = step * index;
}
if (AlignmentFlags.HasFlag(FlexFlags.FitHeight)) {
NodeList[index].Height = Height;
}
if (AlignmentFlags.HasFlag(FlexFlags.CenterVertically)) {
NodeList[index].Y = Height / 2 - NodeList[index].Height / 2;
}
if (AlignmentFlags.HasFlag(FlexFlags.FitWidth)) {
NodeList[index].Width = step - FitPadding;
}
}
}
}
@@ -0,0 +1,66 @@
using System.Linq;
using KamiToolKit.Enums;
namespace KamiToolKit.Nodes;
public class HorizontalListNode : LayoutListNode {
public HorizontalListAnchor Alignment {
get;
set {
field = value;
RecalculateLayout();
}
}
public override float Width {
get => base.Width;
set {
base.Width = value;
RecalculateLayout();
}
}
/// <summary>
/// Adjusts contained nodes heights to match this nodes height
/// </summary>
public bool FitHeight { get; set; }
/// <summary>
/// Resizes the horizontal list node to fit all contents
/// </summary>
public bool FitToContentHeight { get; set; }
protected override void OnRecalculateLayout() {
var startX = Alignment switch {
HorizontalListAnchor.Left => 0.0f + FirstItemSpacing,
HorizontalListAnchor.Right => Width - FirstItemSpacing,
_ => 0.0f,
};
foreach (var node in NodeList) {
if (!node.IsVisible) continue;
if (Alignment is HorizontalListAnchor.Right) {
startX -= node.Width + ItemSpacing;
}
node.X = startX;
AdjustNode(node);
if (Alignment is HorizontalListAnchor.Left) {
startX += node.Width + ItemSpacing;
}
if (FitHeight) {
node.Height = Height;
}
}
if (FitToContentHeight) {
Height = NodeList.Max(node => node.Height);
}
}
public float AreaRemaining => Width - NodeList.Sum(node => node.Width + ItemSpacing) - ItemSpacing;
}
@@ -0,0 +1,29 @@
using System.Linq;
using System.Numerics;
namespace KamiToolKit.Nodes;
public class LabelLayoutNode : LayoutListNode {
public bool FillWidth { get; set; }
protected override void OnRecalculateLayout() {
if (Nodes.Count is 0) return;
var labelNode = Nodes[0];
var labelNodeWidth = labelNode.Width;
labelNode.Position = new Vector2(0.0f, 0.0f);
var position = labelNodeWidth + FirstItemSpacing;
foreach (var node in Nodes.Skip(1)) {
node.X = position;
if (FillWidth) {
node.Width = (Width - labelNodeWidth - FirstItemSpacing) / (Nodes.Count - 1);
}
position += node.Width + ItemSpacing;
}
}
}
+317
View File
@@ -0,0 +1,317 @@
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();
}
}
+200
View File
@@ -0,0 +1,200 @@
using System;
using System.Linq;
using System.Numerics;
using KamiToolKit.Enums;
namespace KamiToolKit.Nodes;
/// Node that manages the layout of other nodes
public class ListBoxNode : LayoutListNode {
public readonly BackgroundImageNode Background;
public readonly BorderNineGridNode Border;
public ListBoxNode() {
Background = new BackgroundImageNode {
IsVisible = false,
};
Background.AttachNode(this);
Border = new BorderNineGridNode {
IsVisible = false,
};
Border.AttachNode(this);
}
public LayoutAnchor LayoutAnchor {
get;
set {
field = value;
RecalculateLayout();
}
}
public bool FitContents {
get;
set {
field = value;
RecalculateLayout();
Size = GetMinimumSize();
}
}
public LayoutOrientation LayoutOrientation {
get;
set {
field = value;
RecalculateLayout();
}
}
public Vector4 BackgroundColor {
get => Background.Color;
set => Background.Color = value;
}
public bool ShowBackground {
get => Background.IsVisible;
set => Background.IsVisible = value;
}
public bool ShowBorder {
get => Border.IsVisible;
set => Border.IsVisible = value;
}
public override float Height {
get => base.Height;
set => base.Height = FitContents ? GetMinimumSize().Y : value;
}
public override float Width {
get => base.Width;
set => base.Width = FitContents ? GetMinimumSize().X : value;
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
Background.Size = Size;
Border.Size = Size + new Vector2(30.0f, 30.0f);
Border.Position = -new Vector2(15.0f, 15.0f);
RecalculateLayout();
}
protected override void OnRecalculateLayout() {
var runningPosition = LayoutOrientation switch {
LayoutOrientation.Vertical when LayoutAnchor is LayoutAnchor.TopLeft or LayoutAnchor.TopRight
=> GetLayoutStartPosition() + new Vector2(0.0f, FirstItemSpacing),
LayoutOrientation.Vertical when LayoutAnchor is LayoutAnchor.BottomLeft or LayoutAnchor.BottomRight
=> GetLayoutStartPosition() - new Vector2(0.0f, FirstItemSpacing),
LayoutOrientation.Horizontal when LayoutAnchor is LayoutAnchor.BottomLeft or LayoutAnchor.TopLeft
=> GetLayoutStartPosition() + new Vector2(FirstItemSpacing, 0.0f),
LayoutOrientation.Horizontal when LayoutAnchor is LayoutAnchor.BottomRight or LayoutAnchor.TopRight
=> GetLayoutStartPosition() - new Vector2(FirstItemSpacing, 0.0f),
_ => Vector2.Zero,
};
foreach (var node in NodeList.Where(node => node.IsVisible)) {
if (LayoutOrientation is LayoutOrientation.Vertical) {
switch (LayoutAnchor) {
case LayoutAnchor.TopLeft:
node.Position = runningPosition;
runningPosition.Y += node.Height * node.Scale.Y + ItemSpacing;
break;
case LayoutAnchor.TopRight:
node.Position = runningPosition - new Vector2(node.Width * node.Scale.X, 0.0f);
runningPosition.Y += node.Height * node.Scale.Y + ItemSpacing;
break;
case LayoutAnchor.BottomLeft:
node.Position = runningPosition - new Vector2(0.0f, node.Height * node.Scale.Y);
runningPosition.Y -= node.Height * node.Scale.Y + ItemSpacing;
break;
case LayoutAnchor.BottomRight:
node.Position = runningPosition - new Vector2(node.Width * node.Scale.X, node.Height * node.Scale.Y);
runningPosition.Y -= node.Height * node.Scale.Y + ItemSpacing;
break;
}
}
else if (LayoutOrientation is LayoutOrientation.Horizontal) {
switch (LayoutAnchor) {
case LayoutAnchor.TopLeft:
node.Position = runningPosition;
runningPosition.X += node.Width * node.Scale.X + ItemSpacing;
break;
case LayoutAnchor.TopRight:
node.Position = runningPosition - new Vector2(node.Width * node.Scale.X, 0.0f);
runningPosition.X -= node.Width * node.Scale.X + ItemSpacing;
break;
case LayoutAnchor.BottomLeft:
node.Position = runningPosition - new Vector2(0.0f, node.Height * node.Scale.Y);
runningPosition.X += node.Width * node.Scale.X + ItemSpacing;
break;
case LayoutAnchor.BottomRight:
node.Position = runningPosition - new Vector2(node.Width * node.Scale.X, node.Height * node.Scale.Y);
runningPosition.X -= node.Width * node.Scale.X + ItemSpacing;
break;
}
}
}
}
public override void AddNode(NodeBase? node) {
base.AddNode(node);
Size = GetMinimumSize();
}
public override void RemoveNode(NodeBase node) {
base.RemoveNode(node);
Size = GetMinimumSize();
}
/// <summary>
/// Get the current minimum size that would contain all the nodes including their margins.
/// </summary>
public Vector2 GetMinimumSize() {
var size = LayoutOrientation switch {
LayoutOrientation.Vertical => new Vector2(0.0f, FirstItemSpacing),
LayoutOrientation.Horizontal => new Vector2(FirstItemSpacing, 0.0f),
_ => Vector2.Zero,
};
foreach (var node in NodeList.Where(node => node.IsVisible)) {
switch (LayoutOrientation) {
// Horizontal we take max height, and add widths
case LayoutOrientation.Horizontal:
size.Y = MathF.Max(size.Y, node.Height);
size.X += node.Width + ItemSpacing;
break;
// Vertical we take max width, and add heights
case LayoutOrientation.Vertical:
size.X = MathF.Max(size.X, node.Width);
size.Y += node.Height + ItemSpacing;
break;
}
}
return size;
}
private Vector2 GetLayoutStartPosition() => LayoutAnchor switch {
LayoutAnchor.TopLeft => Vector2.Zero,
LayoutAnchor.TopRight => new Vector2(Width, 0.0f),
LayoutAnchor.BottomLeft => new Vector2(0.0f, Height),
LayoutAnchor.BottomRight => new Vector2(Width, Height),
_ => throw new ArgumentOutOfRangeException(),
};
}
+40
View File
@@ -0,0 +1,40 @@
using KamiToolKit.Classes;
namespace KamiToolKit.Nodes;
public abstract class ListItemNode<T> : SelectableNode {
public abstract float ItemHeight { get; }
public T? ItemData {
get;
set {
if (value is not null) {
if (!GenericUtil.AreEqual(field, value)) {
IsSettingNodeData = true;
SetNodeData(value);
IsSettingNodeData = false;
}
}
field = value;
IsVisible = value is not null;
}
}
/// <summary>
/// Bool that indicates if SetNodeDate when different is being called.
/// Used to prevent things like checkboxes from trigger a file save due to the value being changed.
/// </summary>
protected bool IsSettingNodeData { get; private set; }
protected abstract void SetNodeData(T itemData);
public virtual void Update() { }
protected void DisableInteractions() {
EnableSelection = false;
EnableHighlight = false;
DisableCollisionNode = true;
}
}
+181
View File
@@ -0,0 +1,181 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
namespace KamiToolKit.Nodes;
public unsafe class ListNode<T, TU> : SimpleComponentNode where TU : ListItemNode<T>, new() {
public readonly ScrollBarNode ScrollBarNode;
public ListNode() {
using (var displayNode = new TU()) {
itemHeight = displayNode.ItemHeight;
}
ScrollBarNode = new ScrollBarNode {
OnValueChanged = OnScrollUpdate,
ScrollSpeed = (int) itemHeight,
HideWhenDisabled = true,
};
ScrollBarNode.AttachNode(this);
AddEvent(AtkEventType.MouseWheel, OnMouseWheel);
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
ScrollBarNode.Size = new Vector2(8.0f, Height);
ScrollBarNode.Position = new Vector2(Width - 8.0f, 0.0f);
var newNodeCount = (int)(Height / (itemHeight + ItemSpacing));
if (newNodeCount != nodeCount) {
FullRebuild();
}
foreach (var node in nodeList) {
node.Width = ScrollBarNode.Bounds.Left - 8.0f;
}
RecalculateScroll();
}
public Action<T?>? OnItemSelected { get; set; }
public float ItemSpacing {
get;
set {
field = value;
FullRebuild();
}
}
public required List<T> OptionsList {
get;
set {
field = value;
var newNodeCount = (int)(Height / (itemHeight + ItemSpacing));
if (newNodeCount != nodeCount) {
FullRebuild();
}
else {
PopulateNodes();
RecalculateScroll();
}
}
} = [];
private readonly List<TU> nodeList = [];
private readonly float itemHeight;
private T? selectedItem;
private int scrollPosition;
private int nodeCount;
/// <summary>
/// Resets and rebuilds list
/// </summary>
public void FullRebuild() {
foreach (var node in nodeList) {
node.Dispose();
}
nodeList.Clear();
scrollPosition = Math.Clamp(scrollPosition, 0, Math.Max(OptionsList.Count - nodeCount, 0));
selectedItem = default;
RebuildNodeList();
PopulateNodes();
RecalculateScroll();
}
public void Update() {
PopulateNodes();
foreach (var node in nodeList) {
if (node.IsVisible) {
node.Update();
}
}
}
private void RebuildNodeList() {
nodeCount = (int)(Height / (itemHeight + ItemSpacing));
if (nodeCount < 1) return;
foreach (var index in Enumerable.Range(0, nodeCount)) {
var node = new TU {
Size = new Vector2(ScrollBarNode.Bounds.Left - 8.0f, itemHeight),
Position = new Vector2(0.0f, index * (itemHeight + ItemSpacing)),
NodeId = (uint)index + 2,
OnClick = clickedNode => {
SelectItem(((TU)clickedNode).ItemData);
OnItemSelected?.Invoke(selectedItem);
},
IsVisible = false,
};
node.AttachNode(this);
nodeList.Add(node);
}
}
private void PopulateNodes() {
foreach (var (nodeIndex, node) in nodeList.Index()) {
var dataIndex = scrollPosition + nodeIndex;
if (dataIndex < OptionsList.Count) {
var item = OptionsList[dataIndex];
node.ItemData = item;
node.IsVisible = true;
node.IsSelected = GenericUtil.AreEqual(item, selectedItem);
}
else {
node.IsVisible = false;
}
}
}
private void SelectItem(T? item) {
if (item is null) return;
selectedItem = item;
foreach (var node in nodeList) {
if (node.ItemData is null) {
node.IsSelected = false;
}
else {
node.IsSelected = GenericUtil.AreEqual(node.ItemData, selectedItem);
}
}
}
private void RecalculateScroll() {
if (OptionsList.Count < nodeCount) {
ScrollBarNode.ScrollPosition = 0;
ScrollBarNode.IsEnabled = false;
}
var totalHeight = (int)( OptionsList.Count * (itemHeight + ItemSpacing) + ItemSpacing);
ScrollBarNode.UpdateScrollParams((int) (nodeList.Count * (itemHeight + ItemSpacing)), totalHeight);
ScrollBarNode.ScrollPosition = (int)( scrollPosition * (itemHeight + ItemSpacing) );
}
private void OnScrollUpdate(int newPosition) {
scrollPosition = (int)( newPosition / ( itemHeight + ItemSpacing ) );
PopulateNodes();
}
private void OnMouseWheel(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
scrollPosition += atkEventData->IsScrollUp ? -1 : 1;
scrollPosition = Math.Clamp(scrollPosition, 0, Math.Max(0, OptionsList.Count - nodeCount));
ScrollBarNode.ScrollPosition = (int)( scrollPosition * (itemHeight + ItemSpacing) );
PopulateNodes();
atkEvent->SetEventIsHandled();
}
}
@@ -0,0 +1,46 @@
using System;
using System.Linq;
using KamiToolKit.Enums;
namespace KamiToolKit.Nodes;
public class OrderedVerticalListNode<T, TU> : VerticalListNode where T : NodeBase {
public Func<T, TU>? OrderSelector { get; set; }
protected override void OnRecalculateLayout() {
var typedList = NodeList.OfType<T>();
if (OrderSelector is null) {
RecalculateLayout();
return;
}
var orderedList = typedList.OrderBy(OrderSelector).ToList();
var startY = Anchor switch {
VerticalListAnchor.Top => 0.0f + FirstItemSpacing,
VerticalListAnchor.Bottom => Height,
_ => 0.0f,
};
foreach (var node in orderedList) {
if (!node.IsVisible) continue;
if (Anchor is VerticalListAnchor.Bottom) {
startY -= node.Height + ItemSpacing;
}
node.Y = startY;
AdjustNode(node);
if (Anchor is VerticalListAnchor.Top) {
startY += node.Height + ItemSpacing;
}
}
if (FitContents) {
Height = orderedList.Sum(node => node.IsVisible ? node.Height + ItemSpacing : 0.0f) + FirstItemSpacing;
}
}
}
@@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using KamiToolKit.Enums;
namespace KamiToolKit.Nodes;
/// <summary>
/// This is a combination of a ScrollingAreaNode and a VerticalListNode for easy layout
/// </summary>
public class ScrollingListNode : SimpleComponentNode {
private readonly ScrollingAreaNode<VerticalListNode> listNode;
public ScrollingListNode() {
listNode = new ScrollingAreaNode<VerticalListNode> {
ContentHeight = 100.0f,
};
listNode.AttachNode(this);
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
listNode.Size = Size;
listNode.ContentNode.RecalculateLayout();
listNode.FitToContentHeight();
}
public bool FitContents {
get => listNode.ContentNode.FitContents;
set => listNode.ContentNode.FitContents = value;
}
public bool FitWidth {
get => listNode.ContentNode.FitWidth;
set => listNode.ContentNode.FitWidth = value;
}
public VerticalListAnchor Anchor {
get => listNode.ContentNode.Anchor;
set => listNode.ContentNode.Anchor = value;
}
public VerticalListAlignment Alignment {
get => listNode.ContentNode.Alignment;
set => listNode.ContentNode.Alignment = value;
}
public bool ClipListContents {
get => listNode.ContentNode.ClipListContents;
set => listNode.ContentNode.ClipListContents = value;
}
public float ItemSpacing {
get => listNode.ContentNode.ItemSpacing;
set => listNode.ContentNode.ItemSpacing = value;
}
public float FirstItemSpacing {
get => listNode.ContentNode.FirstItemSpacing;
set => listNode.ContentNode.FirstItemSpacing = value;
}
public ICollection<NodeBase> InitialNodes {
init => listNode.ContentNode.AddNode(value);
}
public bool AutoHideScrollBar {
get => listNode.AutoHideScrollBar;
set => listNode.AutoHideScrollBar = value;
}
public int ScrollSpeed {
get => listNode.ScrollSpeed;
set => listNode.ScrollSpeed = value;
}
public int ScrollPosition {
get => listNode.ScrollPosition;
set => listNode.ScrollPosition = value;
}
public float ContentWidth => listNode.ContentNode.Width;
public IReadOnlyList<NodeBase> Nodes => listNode.ContentNode.Nodes;
public IEnumerable<T> GetNodes<T>() where T : NodeBase => listNode.ContentNode.GetNodes<T>();
public void RecalculateLayout() {
listNode.ContentNode.RecalculateLayout();
listNode.FitToContentHeight();
}
public void FitToContentHeight() => listNode.FitToContentHeight();
public void AddNode(IEnumerable<NodeBase> nodes) => listNode.ContentNode.AddNode(nodes);
public void AddNode(NodeBase? node) => listNode.ContentNode.AddNode(node);
public void RemoveNode(params NodeBase[] nodes) => listNode.ContentNode.RemoveNode(nodes);
public void RemoveNode(NodeBase node) => listNode.ContentNode.RemoveNode(node);
public void AddDummy(float size = 0.0f) => listNode.ContentNode.AddDummy(size);
public void Clear() => listNode.ContentNode.Clear();
public void ReorderNodes(Comparison<NodeBase> comparison) => listNode.ContentNode.ReorderNodes(comparison);
public VerticalListNode VerticalListNode => listNode.ContentNode;
}
@@ -0,0 +1,52 @@
using System.Collections.Generic;
using System.Linq;
namespace KamiToolKit.Nodes;
/// <summary>
/// This is a combination of a ScrollingAreaNode and a TreeListNode for easy layout
/// </summary>
public class ScrollingTreeNode : SimpleComponentNode {
private readonly ScrollingAreaNode<TreeListNode> listNode;
public ScrollingTreeNode() {
listNode = new ScrollingAreaNode<TreeListNode> {
ContentHeight = 100.0f,
};
listNode.AttachNode(this);
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
listNode.Size = Size;
RecalculateLayout();
}
public float CategoryVerticalSpacing {
get => listNode.ContentNode.CategoryVerticalSpacing;
set => listNode.ContentNode.CategoryVerticalSpacing = value;
}
public bool AutoHideScrollBar {
get => listNode.AutoHideScrollBar;
set => listNode.AutoHideScrollBar = value;
}
public int ScrollSpeed {
get => listNode.ScrollSpeed;
set => listNode.ScrollSpeed = value;
}
public IReadOnlyList<TreeListCategoryNode> CategoryNodes => listNode.ContentNode.CategoryNodes;
public void RecalculateLayout() {
listNode.ContentNode.RefreshLayout();
listNode.ContentHeight = CategoryNodes.Sum(node => node.IsVisible ? node.Height + CategoryVerticalSpacing : 0.0f);
}
public void AddCategoryNode(TreeListCategoryNode node) => listNode.ContentNode.AddCategoryNode(node);
public TreeListNode TreeListNode => listNode.ContentNode;
}
@@ -0,0 +1,99 @@
using System.Collections.Generic;
using System.Linq;
using KamiToolKit.Classes;
namespace KamiToolKit.Nodes;
public class TabbedVerticalListNode : SimpleComponentNode {
private readonly List<TabbedNodeEntry<NodeBase>> nodeList = [];
public float TabSize { get; set; } = 18.0f;
public float ItemVerticalSpacing { get; set; }
public bool FitWidth { get; set; }
public int TabStep { get; set; }
// Adds tab amount to any following nodes being added
public void AddTab(int tabAmount) {
TabStep += tabAmount;
}
// Removes tab amount from any following nodes being added
public void SubtractTab(int tabAmount) {
TabStep -= tabAmount;
}
public void AddNode(NodeBase node) {
AddNode(0, node);
}
public void AddNode(IEnumerable<NodeBase> nodes) {
AddNode(0, nodes);
}
public void AddNode(int tabIndex, IEnumerable<NodeBase> nodes) {
foreach (var node in nodes) {
AddNode(tabIndex, node);
}
}
public void AddNode(int tabIndex, NodeBase node) {
nodeList.Add(new TabbedNodeEntry<NodeBase>(node, tabIndex + TabStep));
node.AttachNode(this);
node.NodeId = (uint)nodeList.Count + 1;
RecalculateLayout();
}
public void RemoveNode(params NodeBase[] nodes) {
foreach (var node in nodes) {
RemoveNode(node);
}
}
public void RemoveNode(NodeBase node) {
var target = nodeList.FirstOrDefault(item => item.Node == node);
if (target is null) return;
target.Node.DetachNode();
nodeList.Remove(target);
RecalculateLayout();
}
public void Clear() {
foreach (var nodeEntry in nodeList) {
nodeEntry.Node.DetachNode();
}
nodeList.Clear();
RecalculateLayout();
}
public void RecalculateLayout() {
var startY = 0.0f;
foreach (var (node, tab) in nodeList) {
if (!node.IsVisible) continue;
node.Y = startY;
node.X = tab * TabSize;
if (FitWidth) {
node.Width = Width - node.X - ItemVerticalSpacing;
// Also update layout of any contained nodes
if (node is LayoutListNode layoutNode) {
layoutNode.RecalculateLayout();
}
}
startY += node.Height + ItemVerticalSpacing;
}
Height = startY + ItemVerticalSpacing;
}
}
@@ -0,0 +1,82 @@
using System.Linq;
using KamiToolKit.Enums;
namespace KamiToolKit.Nodes;
public class VerticalListNode : LayoutListNode {
/// <summary>
/// Displays items starting from either the bottom or the top of the list
/// </summary>
public VerticalListAnchor Anchor {
get;
set {
field = value;
RecalculateLayout();
}
}
/// <summary>
/// Displays items either left aligned or right aligned
/// </summary>
public VerticalListAlignment Alignment {
get;
set {
field = value;
RecalculateLayout();
}
}
/// <summary>
/// Resizes this layout node to fit the height of the contained nodes.
/// </summary>
public bool FitContents { get; set; }
/// <summary>
/// Resizes nodes that are inserted to be the same width as the content area
/// </summary>
public bool FitWidth { get; set; }
protected override void OnRecalculateLayout() {
var startY = Anchor switch {
VerticalListAnchor.Top => 0.0f + FirstItemSpacing,
VerticalListAnchor.Bottom => Height,
_ => 0.0f,
};
foreach (var node in NodeList) {
if (!node.IsVisible) continue;
if (Anchor is VerticalListAnchor.Bottom) {
startY -= node.Height + ItemSpacing;
}
node.Y = startY;
if (FitWidth) {
node.Width = Width;
}
else {
switch (Alignment) {
case VerticalListAlignment.Right:
node.X = Width - node.Width;
break;
case VerticalListAlignment.Left:
node.X = 0.0f;
break;
}
}
AdjustNode(node);
if (Anchor is VerticalListAnchor.Top) {
startY += node.Height + ItemSpacing;
}
}
if (FitContents) {
Height = NodeList.Sum(node => node.IsVisible ? node.Height + ItemSpacing : 0.0f) + FirstItemSpacing - ItemSpacing;
}
}
}