Initial commit: AetherBags + KamiToolKit for FC Gitea
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user