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,318 @@
using System;
using System.Drawing;
using System.Numerics;
using System.Runtime.InteropServices;
using Dalamud.Interface;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Graphics;
using FFXIVClientStructs.FFXIV.Client.System.Input;
using FFXIVClientStructs.FFXIV.Component.GUI;
using InteropGenerator.Runtime;
using KamiToolKit.Classes;
using KamiToolKit.Enums;
using KamiToolKit.Timelines;
using Lumina.Text.ReadOnly;
namespace KamiToolKit.Nodes;
public unsafe class TextInputNode : ComponentNode<AtkComponentTextInput, AtkUldComponentDataTextInput> {
public readonly NineGridNode BackgroundNode;
public readonly TextNode CurrentTextNode;
public readonly CursorNode CursorNode;
public readonly NineGridNode FocusNode;
public readonly TextInputSelectionListNode SelectionListNode;
public readonly TextNode TextLimitsNode;
public readonly TextNode PlaceholderTextNode;
private AtkComponentInputBase.CallbackDelegate? pinnedCallbackFunction;
public TextInputNode() {
SetInternalComponentType(ComponentType.TextInput);
BackgroundNode = new SimpleNineGridNode {
NodeId = 19,
TexturePath = "ui/uld/TextInputA.tex",
TextureCoordinates = new Vector2(24.0f, 0.0f),
TextureSize = new Vector2(24.0f, 24.0f),
Offsets = new Vector4(10.0f),
Size = new Vector2(152.0f, 28.0f),
};
BackgroundNode.AttachNode(this);
FocusNode = new SimpleNineGridNode {
NodeId = 18,
TexturePath = "ui/uld/TextInputA.tex",
TextureCoordinates = new Vector2(0.0f, 0.0f),
TextureSize = new Vector2(24.0f, 24.0f),
Offsets = new Vector4(10.0f),
Size = new Vector2(152.0f, 28.0f),
};
FocusNode.AttachNode(this);
TextLimitsNode = new TextNode {
NodeId = 17,
Position = new Vector2(-24.0f, 6.0f),
Size = new Vector2(170.0f, 19.0f),
FontType = FontType.MiedingerMed,
FontSize = 14,
AlignmentType = (AlignmentType)21,
};
TextLimitsNode.AttachNode(this);
CurrentTextNode = new TextNode {
NodeId = 16,
Position = new Vector2(10.0f, 6.0f),
Size = new Vector2(132.0f, 18.0f),
AlignmentType = AlignmentType.TopLeft,
TextFlags = TextFlags.AutoAdjustNodeSize,
TextColor = ColorHelper.GetColor(1),
};
CurrentTextNode.AttachNode(this);
SelectionListNode = new TextInputSelectionListNode {
NodeId = 4,
Position = new Vector2(0.0f, 22.0f),
Size = new Vector2(186.0f, 208.0f),
};
SelectionListNode.AttachNode(this);
CursorNode = new CursorNode {
NodeId = 2,
Position = new Vector2(10.0f, 2.0f),
Size = new Vector2(4.0f, 24.0f),
OriginY = 4.0f,
};
CursorNode.AttachNode(this);
PlaceholderTextNode = new TextNode {
Position = new Vector2(8.0f, 0.0f),
TextColor = ColorHelper.GetColor(3),
};
PlaceholderTextNode.AttachNode(this);
Data->Nodes[0] = CurrentTextNode.NodeId;
Data->Nodes[1] = BackgroundNode.NodeId;
Data->Nodes[2] = CursorNode.NodeId;
Data->Nodes[3] = SelectionListNode.NodeId;
Data->Nodes[4] = SelectionListNode.Buttons[8].NodeId;
Data->Nodes[5] = SelectionListNode.Buttons[7].NodeId;
Data->Nodes[6] = SelectionListNode.Buttons[6].NodeId;
Data->Nodes[7] = SelectionListNode.Buttons[5].NodeId;
Data->Nodes[8] = SelectionListNode.Buttons[4].NodeId;
Data->Nodes[9] = SelectionListNode.Buttons[3].NodeId;
Data->Nodes[10] = SelectionListNode.Buttons[2].NodeId;
Data->Nodes[11] = SelectionListNode.Buttons[1].NodeId;
Data->Nodes[12] = SelectionListNode.Buttons[0].NodeId;
Data->Nodes[13] = SelectionListNode.LabelNode.NodeId;
Data->Nodes[14] = SelectionListNode.BackgroundNode.NodeId;
Data->Nodes[15] = TextLimitsNode.NodeId;
Data->CandidateColor = new ByteColor { R = 66 };
Data->IMEColor = new ByteColor { R = 67 };
Data->FocusColor = KnownColor.Black.Vector().ToByteColor();
Flags = TextInputFlags.EnableIme | TextInputFlags.AllowUpperCase | TextInputFlags.AllowLowerCase |
TextInputFlags.EnableDictionary | TextInputFlags.AllowNumberInput | TextInputFlags.AllowSymbolInput;
EnableCompletion = false;
Component->EnableTabCallback = true;
LoadTimelines();
pinnedCallbackFunction = OnCallback;
Component->Callback = (delegate* unmanaged<AtkUnitBase*, InputCallbackType, CStringPointer, CStringPointer, int, InputCallbackResult>) Marshal.GetFunctionPointerForDelegate(pinnedCallbackFunction);
InitializeComponentEvents();
CollisionNode.AddEvent(AtkEventType.FocusStart, () => {
PlaceholderTextNode.IsVisible = false;
OnFocused?.Invoke();
if (AutoSelectAll && Component->EvaluatedString.Length > 0) {
DalamudInterface.Instance.Framework.RunOnTick(() => {
var keyModifiers = new AtkTextInput.KeyModifiers {
IsControlDown = true,
};
AtkStage.Instance()->AtkInputManager->TextInput->ProcessKeyShortcut(SeVirtualKey.A, &keyModifiers);
}, delayTicks: 1);
}
});
CollisionNode.AddEvent(AtkEventType.FocusStop, () => {
OnUnfocused?.Invoke();
if (!PlaceholderString.IsNullOrEmpty() && String.IsEmpty) {
PlaceholderTextNode.IsVisible = true;
PlaceholderTextNode.String = PlaceholderString;
}
});
}
protected override void Dispose(bool disposing, bool isNativeDestructor) {
if (disposing) {
base.Dispose(disposing, isNativeDestructor);
pinnedCallbackFunction = null;
}
}
public bool IsFocused
=> AtkStage.Instance()->AtkInputManager->FocusedNode == CollisionNode.Node;
public int MaxCharacters {
get => (int)Component->ComponentTextData.MaxChar;
set => Component->ComponentTextData.MaxChar = (uint)value;
}
public bool ShowLimitText {
get => TextLimitsNode.IsVisible;
set => TextLimitsNode.IsVisible = value;
}
public TextInputFlags Flags {
get => (TextInputFlags) ((byte)Data->Flags1 | (byte)Data->Flags2 << 8);
set {
Data->Flags1 = (TextInputFlags1)((ushort)value & 0xFF);
Data->Flags2 = (TextInputFlags2)((ushort)value >> 8);
}
}
public bool EnableCompletion {
get => Component->EnableCompletion;
set => Component->EnableCompletion = value;
}
public bool EnableFocusSounds {
get => Component->EnableFocusSounds;
set => Component->EnableFocusSounds = value;
}
public virtual ReadOnlySeString String {
get => Component->EvaluatedString.AsSpan();
set {
Component->SetText(value);
UpdatePlaceholderVisibility();
}
}
public string? PlaceholderString {
get;
set {
field = value;
UpdatePlaceholderVisibility();
}
}
public bool IsError {
get => FocusNode.MultiplyColor == new Vector3(1.0f, 0.6f, 0.6f);
set => FocusNode.MultiplyColor = value ? new Vector3(1.0f, 0.6f, 0.6f) : Vector3.One;
}
public bool AutoSelectAll { get; set; } = true;
public void ClearFocus() {
if (IsFocused) {
AtkStage.Instance()->AtkInputManager->SetFocus(null, ParentAddon, 0);
}
}
public virtual Action<ReadOnlySeString>? OnInputReceived { get; set; }
public virtual Action<ReadOnlySeString>? OnInputComplete { get; set; }
public Action? OnFocusLost { get; set; }
public Action? OnEscapeEntered { get; set; }
public Action? OnTabEntered { get; set; }
public Action? OnFocused { get; set; }
public Action? OnUnfocused { get; set; }
private InputCallbackResult OnCallback(AtkUnitBase* addon, InputCallbackType type, CStringPointer rawString, CStringPointer evaluatedString, int eventKind) {
try {
switch (type) {
case InputCallbackType.Enter:
if (this is TextMultiLineInputNode) break;
OnInputComplete?.Invoke(Component->EvaluatedString.AsSpan());
ClearFocus();
break;
case InputCallbackType.TextChanged:
OnInputReceived?.Invoke(Component->EvaluatedString.AsSpan());
break;
case InputCallbackType.Escape:
OnEscapeEntered?.Invoke();
break;
case InputCallbackType.FocusLost:
OnFocusLost?.Invoke();
break;
case InputCallbackType.Tab:
OnTabEntered?.Invoke();
break;
}
return InputCallbackResult.None;
}
catch (Exception e) {
Log.Exception(e);
return InputCallbackResult.None;
}
}
private void UpdatePlaceholderVisibility() {
PlaceholderTextNode.String = PlaceholderString ?? string.Empty;
PlaceholderTextNode.IsVisible = String.IsEmpty && !PlaceholderString.IsNullOrEmpty();
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
BackgroundNode.Size = Size;
FocusNode.Size = Size;
PlaceholderTextNode.Size = Size;
TextLimitsNode.Size = new Vector2(Width + 18.0f, Height - 9.0f);
CurrentTextNode.Size = new Vector2(Width - 20.0f, Height - 10.0f);
}
private void LoadTimelines() {
AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 29)
.AddLabelPair(1, 9, 17)
.AddLabelPair(10, 19, 18)
.AddLabelPair(20, 29, 7)
.EndFrameSet()
.Build());
BackgroundNode.AddTimeline(new TimelineBuilder()
.AddFrameSetWithFrame(1, 9, 1, alpha: 255)
.BeginFrameSet(10, 19)
.AddFrame(10, alpha: 255)
.AddFrame(12, alpha: 255)
.EndFrameSet()
.AddFrameSetWithFrame(20, 29, 20, alpha: 127)
.Build());
FocusNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(10, 19)
.AddFrame(10, alpha: 0)
.AddFrame(12, alpha: 255)
.EndFrameSet()
.Build());
TextLimitsNode.AddTimeline(new TimelineBuilder()
.AddFrameSetWithFrame(1, 9, 1, alpha: 102)
.BeginFrameSet(10, 19)
.AddFrame(10, alpha: 102)
.AddFrame(12, alpha: 127)
.EndFrameSet()
.AddFrameSetWithFrame(20, 29, 20, alpha: 76)
.Build());
CursorNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 15)
.AddLabel(1, 101, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(15, 0, AtkTimelineJumpBehavior.LoopForever, 101)
.EndFrameSet()
.Build());
}
}