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
+117
View File
@@ -0,0 +1,117 @@
using System;
using System.Numerics;
using Dalamud.Interface;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
namespace KamiToolKit.Premade.Nodes;
public unsafe class AlphaBarNode : SimpleComponentNode {
public readonly ImGuiImageNode AlphaBarBackgroundNode;
public readonly ImGuiImageNode AlphaBarGradientNode;
public readonly ImGuiImageNode AlphaBarSelectorNode;
private readonly ViewportEventListener alphaEventListener;
private bool isAlphaDragging;
public AlphaBarNode() {
alphaEventListener = new ViewportEventListener(AlphaSliderEvent);
AlphaBarBackgroundNode = new AlphaImageNode();
AlphaBarBackgroundNode.AttachNode(this);
AlphaBarGradientNode = new ImGuiImageNode {
TexturePath = DalamudInterface.Instance.GetAssetPath("VerticalGradient_WhiteToAlpha.png"),
FitTexture = true,
};
AlphaBarGradientNode.AttachNode(this);
AlphaBarGradientNode.AddEvent(AtkEventType.MouseDown, OnAlphaBarMouseDown);
AlphaBarSelectorNode = new ImGuiImageNode {
TexturePath = DalamudInterface.Instance.GetAssetPath("alpha_selector.png"),
FitTexture = true,
};
AlphaBarSelectorNode.AttachNode(this);
AlphaBarSelectorNode.AddEvent(AtkEventType.MouseDown, OnAlphaBarMouseDown);
}
protected override void Dispose(bool disposing, bool isNativeDestructor) {
if (disposing) {
base.Dispose(disposing, isNativeDestructor);
alphaEventListener.Dispose();
}
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
AlphaBarBackgroundNode.Size = Size;
AlphaBarGradientNode.Size = Size;
AlphaBarSelectorNode.Size = new Vector2(Width + 4.0f, 10.0f);
AlphaBarSelectorNode.Position = new Vector2(-2.0f, 0.0f);
}
private void OnAlphaBarMouseDown() {
if (!isAlphaDragging) {
alphaEventListener.AddEvent(AtkEventType.MouseMove, AlphaBarGradientNode);
alphaEventListener.AddEvent(AtkEventType.MouseUp, AlphaBarGradientNode);
isAlphaDragging = true;
}
}
private void AlphaSliderEvent(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
switch (eventType) {
case AtkEventType.MouseUp:
alphaEventListener.RemoveEvent(AtkEventType.MouseMove);
alphaEventListener.RemoveEvent(AtkEventType.MouseUp);
isAlphaDragging = false;
break;
case AtkEventType.MouseMove: {
var mousePosition = new Vector2(atkEventData->MouseData.PosX, atkEventData->MouseData.PosY);
var scale = ParentAddon is not null ? ParentAddon->Scale : 1.0f;
var scaledHeight = AlphaBarGradientNode.Height * scale;
var minY = AlphaBarGradientNode.ScreenY;
var maxY = AlphaBarGradientNode.ScreenY + scaledHeight;
if (mousePosition.Y >= minY && mousePosition.Y <= maxY) {
var alphaRatio = 1.0f - (mousePosition.Y - AlphaBarGradientNode.ScreenY) / scaledHeight;
AlphaBarSelectorNode.Y = Height - Height * alphaRatio - 5.0f;
OnAlphaChanged?.Invoke(alphaRatio);
}
else if (mousePosition.Y < minY) {
AlphaBarSelectorNode.Y = -4.0f;
OnAlphaChanged?.Invoke(1.0f);
}
else if (mousePosition.Y > maxY) {
AlphaBarSelectorNode.Y = Height - 4.0f;
OnAlphaChanged?.Invoke(0.0f);
}
break;
}
}
}
public Action<float>? OnAlphaChanged { get; init; }
public override Vector4 Color {
get => AlphaBarGradientNode.Color;
set {
AlphaBarGradientNode.MultiplyColor = value.AsVector3();
AlphaBarSelectorNode.Y = Height - Height * value.W - 5.0f;
}
}
public override ColorHelpers.HsvaColor ColorHsva {
get => AlphaBarGradientNode.MultiplyColorHsva;
set {
AlphaBarGradientNode.MultiplyColorHsva = value with { A = 1.0f };
AlphaBarSelectorNode.Y = Height - Height * value.A - 5.0f;
}
}
}
+18
View File
@@ -0,0 +1,18 @@
using System;
using KamiToolKit.Nodes;
namespace KamiToolKit.Premade.Nodes;
public abstract class ConfigNode<T> : SimpleComponentNode {
public T? ConfigurationOption {
get;
set {
field = value;
OptionChanged(value);
}
}
protected abstract void OptionChanged(T? option);
public Action<T>? OnConfigChanged { get; set; }
}
+203
View File
@@ -0,0 +1,203 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Utility;
using KamiToolKit.Nodes;
using KamiToolKit.Premade.Widgets;
namespace KamiToolKit.Premade.Nodes;
/// <summary>
/// A non-owning list node that supports searching, and various callbacks for easily editing a list.
/// </summary>
/// <typeparam name="T">Data type to display the data for.</typeparam>
/// <typeparam name="TU">ListItemNode derived type, for defining the result view.</typeparam>
public class ModifyListNode<T, TU> : SimpleComponentNode where TU : ListItemNode<T>, new() {
private readonly SearchWidget searchWidget;
private readonly ListNode<T, TU> listNode;
private readonly TextButtonNode addButton;
private readonly TextButtonNode editButton;
private readonly TextButtonNode removeButton;
public ModifyListNode() {
searchWidget = new SearchWidget {
OnSortOrderChanged = OnSortOrderChanged,
OnSearchUpdated = OnSearchUpdated,
};
searchWidget.AttachNode(this);
listNode = new ListNode<T, TU> {
OptionsList = [],
OnItemSelected = OnListItemSelected,
};
listNode.AttachNode(this);
addButton = new TextButtonNode {
String = "Add",
OnClick = OnAddClicked,
IsEnabled = false,
};
addButton.AttachNode(this);
editButton = new TextButtonNode {
String = "Edit",
OnClick = OnEditClicked,
IsEnabled = false,
};
editButton.AttachNode(this);
removeButton = new TextButtonNode {
String = "Remove",
OnClick = OnRemoveClicked,
IsEnabled = false,
};
removeButton.AttachNode(this);
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
searchWidget.Size = new Vector2(Width, 65.0f);
searchWidget.Position = Vector2.Zero;
listNode.Size = new Vector2(Width, Height - searchWidget.Height - 40.0f);
listNode.Position = new Vector2(0.0f, searchWidget.Y + searchWidget.Height + 8.0f);
const float buttonPadding = 5.0f;
var buttonWidth = (Width - buttonPadding * 2.0f) / 3.0f;
addButton.Size = new Vector2(buttonWidth, 24.0f);
addButton.Position = new Vector2(0.0f, Height - 24.0f);
editButton.Size = new Vector2(buttonWidth, 24.0f);
editButton.Position = new Vector2(buttonWidth + buttonPadding, Height - 24.0f);
removeButton.Size = new Vector2(buttonWidth, 24.0f);
removeButton.Position = new Vector2(buttonWidth * 2.0f + buttonPadding * 2.0f, Height - 24.0f);
}
public List<T> Options {
get;
set {
field = value;
listNode.OptionsList = value;
}
} = [];
public List<string>? SortOptions {
get => searchWidget.SortingOptions;
set {
searchWidget.SortingOptions = value ?? [];
OnSizeChanged();
if (value is not null && value.Count > 0) {
OnSortOrderChanged(value.First(), false);
}
}
}
public Action<T?>? SelectionChanged { get; init; }
public Action? AddNewEntry {
get;
set {
field = value;
addButton.IsEnabled = value is not null;
}
}
public Action<T>? RemoveEntry {
get;
set {
field = value;
removeButton.IsEnabled = value is not null && SelectedOption is not null;
}
}
public Action<T>? EditEntry {
get;
set {
field = value;
editButton.IsEnabled = value is not null && SelectedOption is not null;
}
}
public delegate int ItemCompareDelegate(T left, T right, string sortingMode);
public ItemCompareDelegate? ItemComparer { get; set; }
public delegate bool IsSearchMatchDelegate(T obj, string searchString);
public IsSearchMatchDelegate? IsSearchMatch { get; set; }
public T? SelectedOption { get; private set; }
public float ItemSpacing {
get => listNode.ItemSpacing;
set {
listNode.ItemSpacing = value;
OnSizeChanged();
}
}
private void OnSortOrderChanged(string sortingString, bool reversed) {
if (ItemComparer is null) return;
var listCopy = Options.ToList();
listCopy.Sort((left, right) => ItemComparer.Invoke(left, right, sortingString) * (reversed ? -1 : 1));
listNode.OptionsList = listCopy;
UpdateButtonStates();
}
private void OnSearchUpdated(string searchString) {
if (IsSearchMatch is null) return;
if (searchString.IsNullOrEmpty()) {
listNode.OptionsList = Options;
}
else {
listNode.OptionsList = Options.Where(item => IsSearchMatch(item, searchString)).ToList();
}
}
private void OnListItemSelected(T? obj) {
SelectedOption = obj;
SelectionChanged?.Invoke(SelectedOption);
UpdateButtonStates();
}
private void OnAddClicked() {
AddNewEntry?.Invoke();
RefreshList();
}
private void OnEditClicked() {
if (SelectedOption is null) return;
EditEntry?.Invoke(SelectedOption);
RefreshList();
}
private void OnRemoveClicked() {
if (SelectedOption is null) return;
RemoveEntry?.Invoke(SelectedOption);
RefreshList();
}
private void UpdateButtonStates() {
editButton.IsEnabled = SelectedOption is not null && EditEntry is not null;
removeButton.IsEnabled = SelectedOption is not null && RemoveEntry is not null;
}
/// <summary>
/// Refreshes the displayed list data.
/// This resets scroll position, so don't spam it.
/// </summary>
public void RefreshList() {
OnSortOrderChanged(searchWidget.SortMode, searchWidget.IsReversed);
OnSearchUpdated(searchWidget.SearchText);
listNode.FullRebuild();
}
}
@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using KamiToolKit.Nodes;
namespace KamiToolKit.Premade.Nodes;
/// <summary>
/// A TextButton that has a configurable set of states
/// </summary>
public class MultiStateButtonNode<T> : TextButtonNode where T : notnull {
public Action<T>? OnStateChanged { get; set; }
public MultiStateButtonNode()
=> OnClick = CycleState;
public required List<T> States {
get;
set {
field = value;
UpdateDisplay();
}
}
private int SelectedIndex {
get;
set {
field = value;
UpdateDisplay();
}
}
public T SelectedState {
get => States[SelectedIndex];
set => SelectedIndex = States.IndexOf(value);
}
private void CycleState() {
if (States.Count is 0) return;
SelectedIndex = (SelectedIndex + 1) % States.Count;
OnStateChanged?.Invoke(SelectedState);
}
private void UpdateDisplay() {
if (SelectedIndex < 0) return;
if (SelectedIndex > States.Count - 1) return;
String = GetStateText(States[SelectedIndex]);
}
protected virtual string GetStateText(T state) {
if (state is Enum enumState) {
return enumState.Description;
}
return state.ToString() ?? "Unable to Parse Type";
}
}
@@ -0,0 +1,44 @@
using System.Numerics;
using KamiToolKit.Nodes;
using Lumina.Text.ReadOnly;
namespace KamiToolKit.Premade.Nodes;
public class UnderlinedTextNode : SimpleComponentNode {
public readonly CategoryTextNode LabelTextNode;
public readonly HorizontalLineNode LineNode;
public UnderlinedTextNode() {
LabelTextNode = new CategoryTextNode();
LabelTextNode.AttachNode(this);
LineNode = new HorizontalLineNode {
Height = 4.0f,
};
LineNode.AttachNode(this);
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
LabelTextNode.Size = new Vector2(Width, Height - 4.0f);
LabelTextNode.Position = new Vector2(0.0f, 0.0f);
LineNode.Position = new Vector2(0.0f, LabelTextNode.Bounds.Bottom - 4.0f);
RecalculateLineSize();
}
public ReadOnlySeString String {
get => LabelTextNode.String;
set {
LabelTextNode.String = value;
RecalculateLineSize();
}
}
private void RecalculateLineSize() {
var textSize = LabelTextNode.GetTextDrawSize();
LineNode.Width = textSize.X + 32.0f;
}
}