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
+93
View File
@@ -0,0 +1,93 @@
using System;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Timelines;
namespace KamiToolKit.Nodes;
public abstract unsafe class ButtonBase : ComponentNode<AtkComponentButton, AtkUldComponentDataButton> {
protected ButtonBase() {
SetInternalComponentType(ComponentType.Button);
AddEvent(AtkEventType.ButtonClick, ClickHandler);
}
public Action? OnClick { get; set; }
public bool IsChecked {
get => Component->IsChecked;
set => Component->SetChecked(value);
}
private void ClickHandler() {
OnClick?.Invoke();
}
protected static void LoadTwoPartTimelines(NodeBase parent, NodeBase foreground) {
parent.AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 59)
.AddLabelPair(1, 9, 1)
.AddLabelPair(10, 19, 2)
.AddLabelPair(20, 29, 3)
.AddLabelPair(30, 39, 7)
.AddLabelPair(40, 49, 6)
.AddLabelPair(50, 59, 4)
.EndFrameSet()
.Build());
foreground.AddTimeline(new TimelineBuilder()
.AddFrameSetWithFrame(1, 9, 1, Vector2.Zero, 255, multiplyColor: new Vector3(100.0f))
.BeginFrameSet(10, 19)
.AddFrame(10, Vector2.Zero, 255, multiplyColor: new Vector3(100.0f))
.AddFrame(12, Vector2.Zero, 255, multiplyColor: new Vector3(100.0f), addColor: new Vector3(16.0f))
.EndFrameSet()
.AddFrameSetWithFrame(20, 29, 20, new Vector2(0.0f, 1.0f), 255, multiplyColor: new Vector3(100.0f), addColor: new Vector3(16.0f))
.AddFrameSetWithFrame(30, 39, 30, Vector2.Zero, 178, multiplyColor: new Vector3(50.0f))
.AddFrameSetWithFrame(40, 49, 40, Vector2.Zero, 255, multiplyColor: new Vector3(100.0f), addColor: new Vector3(16.0f))
.BeginFrameSet(50, 59)
.AddFrame(50, Vector2.Zero, 255, multiplyColor: new Vector3(100.0f), addColor: new Vector3(16.0f))
.AddFrame(52, Vector2.Zero, 255, multiplyColor: new Vector3(100.0f))
.EndFrameSet()
.AddFrameSetWithFrame(130, 139, 130, Vector2.Zero, 255, new Vector3(16.0f), new Vector3(100.0f))
.AddFrameSetWithFrame(140, 149, 140, Vector2.Zero, 255, multiplyColor: new Vector3(100.0f))
.AddFrameSetWithFrame(150, 159, 150, Vector2.Zero, 255, multiplyColor: new Vector3(100.0f))
.Build());
}
protected static void LoadThreePartTimelines(NodeBase parent, NodeBase background, NodeBase foreground, Vector2 foregroundPositionOffset) {
parent.AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 53)
.AddLabelPair(1, 10, 1)
.AddLabelPair(11, 17, 2)
.AddLabelPair(18, 26, 3)
.AddLabelPair(27, 36, 7)
.AddLabelPair(37, 46, 6)
.AddLabelPair(47, 53, 4)
.EndFrameSet()
.Build());
background.AddTimeline(new TimelineBuilder()
.AddFrameSetWithFrame(1, 10, 1, Vector2.Zero, 255, multiplyColor: new Vector3(100.0f))
.BeginFrameSet(11, 17)
.AddFrame(11, Vector2.Zero, 255, multiplyColor: new Vector3(100.0f))
.AddFrame(13, Vector2.Zero, 255, multiplyColor: new Vector3(100.0f), addColor: new Vector3(16.0f))
.EndFrameSet()
.AddFrameSetWithFrame(18, 26, 18, new Vector2(0.0f, 1.0f), 255, new Vector3(16.0f))
.AddFrameSetWithFrame(27, 36, 27, Vector2.Zero, 178, multiplyColor: new Vector3(50.0f))
.AddFrameSetWithFrame(37, 46, 37, Vector2.Zero, 255, multiplyColor: new Vector3(100.0f), addColor: new Vector3(16.0f))
.BeginFrameSet(47, 53)
.AddFrame(47, Vector2.Zero, 255, multiplyColor: new Vector3(100.0f), addColor: new Vector3(16.0f))
.AddFrame(53, Vector2.Zero, 255, multiplyColor: new Vector3(100.0f))
.EndFrameSet()
.Build());
foreground.AddTimeline(new TimelineBuilder()
.AddFrameSetWithFrame(1, 10, 1, foregroundPositionOffset, 255, multiplyColor: new Vector3(100.0f))
.AddFrameSetWithFrame(11, 17, 11, foregroundPositionOffset, 255, multiplyColor: new Vector3(100.0f))
.AddFrameSetWithFrame(18, 26, 18, foregroundPositionOffset + new Vector2(0.0f, 1.0f), 255, multiplyColor: new Vector3(100.0f))
.AddFrameSetWithFrame(27, 36, 27, foregroundPositionOffset, 153, multiplyColor: new Vector3(80.0f))
.AddFrameSetWithFrame(37, 46, 37, foregroundPositionOffset, 255, multiplyColor: new Vector3(100.0f))
.AddFrameSetWithFrame(47, 53, 47, foregroundPositionOffset, 255, multiplyColor: new Vector3(100.0f))
.Build());
}
}
@@ -0,0 +1,261 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Enums;
using KamiToolKit.Timelines;
namespace KamiToolKit.Nodes;
public abstract class ListNode : SimpleComponentNode;
/// Note, automatically inserts buttons to fill the set height, please ensure option count is greater than button count.
public abstract unsafe class ButtonListNode<T> : ListNode {
public readonly NineGridNode BackgroundNode;
public readonly ResNode ContainerNode;
public readonly ScrollBarNode ScrollBarNode;
public List<ListButtonNode> Nodes = [];
protected ButtonListNode() {
SetInternalComponentType(ComponentType.Base);
BackgroundNode = new SimpleNineGridNode {
TexturePath = "ui/uld/ListB.tex",
TextureCoordinates = new Vector2(0.0f, 0.0f),
TextureSize = new Vector2(32.0f, 32.0f),
TopOffset = 10,
BottomOffset = 12,
LeftOffset = 10,
RightOffset = 10,
};
BackgroundNode.AttachNode(this);
ContainerNode = new ResNode {
NodeFlags = NodeFlags.Visible | NodeFlags.Clip,
};
ContainerNode.AttachNode(this);
ScrollBarNode = new ScrollBarNode {
Position = new Vector2(0.0f, 9.0f),
Size = new Vector2(8.0f, 0.0f),
OnValueChanged = OnScrollUpdate,
HideWhenDisabled = true,
};
ScrollBarNode.AttachNode(this);
BuildTimelines();
ContainerNode.AddEvent(AtkEventType.MouseWheel, OnMouseWheel);
}
protected override void Dispose(bool disposing, bool isNativeDestructor) {
if (disposing) {
if (isFocusSet && !isNativeDestructor) {
if (ParentAddon is not null) {
ClearFocusable(ParentAddon);
}
}
base.Dispose(disposing, isNativeDestructor);
}
}
public T? SelectedOption {
get;
set {
field = value;
UpdateSelected();
}
}
public List<T>? Options {
get;
set {
field = value;
RebuildNodeList();
}
}
protected float NodeHeight { get; set; } = 22.0f;
private int ButtonCount { get; set; }
public int MaxButtons {
get;
set {
field = value;
RebuildNodeList();
}
} = 5;
public int CurrentStartIndex { get; set; }
public Action<T>? OnOptionSelected { get; set; }
protected override void OnSizeChanged() {
base.OnSizeChanged();
BackgroundNode.Size = Size;
ContainerNode.Size = new Vector2(Width - 25.0f, Height);
foreach (var buttonNode in Nodes) {
buttonNode.Width = Width - 25.0f;
}
ScrollBarNode.X = Width - 17.0f;
}
private void OnScrollUpdate(int scrollPosition) {
var index = scrollPosition / 22.0f;
CurrentStartIndex = (int)index;
UpdateNodes();
}
private void OnMouseWheel(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
CurrentStartIndex -= atkEventData->MouseData.WheelDirection;
UpdateNodes();
ScrollBarNode.ScrollPosition = (int)(CurrentStartIndex * NodeHeight + 9.0f);
atkEvent->SetEventIsHandled();
}
private void RebuildNodeList() {
foreach (var button in Nodes) {
button.DetachNode();
button.Dispose();
}
Nodes.Clear();
ButtonCount = Math.Min(MaxButtons, Options?.Count ?? 0);
var height = ButtonCount * NodeHeight + 24.0f;
Height = height;
BackgroundNode.Height = height;
ContainerNode.Height = height;
ScrollBarNode.Height = height - 23.0f;
foreach (var index in Enumerable.Range(0, ButtonCount)) {
var newButton = new ListButtonNode {
NodeId = (uint)index,
Size = new Vector2(Width - 25.0f, NodeHeight),
Position = new Vector2(8.0f, NodeHeight * index + 9.0f),
String = $"Button {index}",
OnClick = () => OnOptionClick(index),
};
Nodes.Add(newButton);
newButton.AttachNode(ContainerNode);
}
RecalculateScrollParams();
UpdateNodes();
}
public void RecalculateScrollParams() {
if (Options is not null) {
ScrollBarNode.UpdateScrollParams((int)ScrollBarNode.Height, (int)(Options.Count * NodeHeight));
}
}
protected virtual void OnOptionClick(int nodeId) {
if (Options is null) return;
SelectedOption = Options[nodeId + CurrentStartIndex];
OnOptionSelected?.Invoke(Options[nodeId + CurrentStartIndex]);
UpdateSelected();
}
private void UpdateSelected() {
if (Options is null) return;
foreach (var index in Enumerable.Range(0, ButtonCount)) {
var option = Options[index + CurrentStartIndex];
Nodes[index].Selected = SelectedOption?.Equals(option) ?? false;
Nodes[index].String = GetLabelForOption(option);
}
}
protected abstract string GetLabelForOption(T option);
protected void UpdateNodes() {
if (Options is null) return;
var maxStartIndex = Options.Count - Nodes.Count;
var max = Math.Max(0, maxStartIndex);
CurrentStartIndex = Math.Clamp(CurrentStartIndex, 0, max);
UpdateSelected();
}
public void SelectDefaultOption() {
if (Options is not null && Options.Count > 0) {
SelectedOption = Options.First();
}
}
public void Show() {
IsVisible = true;
AddDrawFlags(DrawFlags.RenderOnTop);
if (ParentAddon is not null) {
SetFocusable(ParentAddon);
}
}
public void Hide() {
IsVisible = false;
RemoveDrawFlags(DrawFlags.RenderOnTop);
if (ParentAddon is not null) {
ClearFocusable(ParentAddon);
}
}
public void Toggle(bool newState) {
if (newState) {
Show();
}
else {
Hide();
}
}
private bool isFocusSet;
public void SetFocusable(AtkUnitBase* addon) {
foreach (ref var focusableNode in addon->AdditionalFocusableNodes) {
if (focusableNode.Value is null) {
focusableNode = ResNode;
isFocusSet = true;
}
}
}
public void ClearFocusable(AtkUnitBase* addon) {
foreach (ref var focusableNode in addon->AdditionalFocusableNodes) {
if (focusableNode.Value == ResNode) {
focusableNode = null;
isFocusSet = false;
}
}
}
private void BuildTimelines() {
AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 29)
.AddLabel(1, 17, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(9, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(10, 18, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(19, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(20, 7, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(29, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.EndFrameSet()
.Build()
);
}
}
@@ -0,0 +1,150 @@
using System.Numerics;
using KamiToolKit.Enums;
namespace KamiToolKit.Nodes;
public class CircleButtonNode : ButtonBase {
public readonly SimpleImageNode ImageNode;
public CircleButtonNode() {
ImageNode = new SimpleImageNode {
TexturePath = "ui/uld/CircleButtons.tex",
TextureSize = new Vector2(24.0f, 24.0f),
TextureCoordinates = new Vector2(0.0f, 112.0f),
WrapMode = WrapMode.Stretch,
};
ImageNode.AttachNode(this);
LoadTimelines();
InitializeComponentEvents();
}
public ButtonIcon Icon {
get;
set {
field = value;
var uldInfo = GetTextureCoordinateForIcon(value);
ImageNode.TextureCoordinates = uldInfo.TextureCoordinates;
ImageNode.TextureSize = uldInfo.TextureSize;
}
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
ImageNode.Size = Size;
}
private static UldTextureInfo GetTextureCoordinateForIcon(ButtonIcon icon) => icon switch {
ButtonIcon.GearCog => new UldTextureInfo(0.0f, 0.0f, 28.0f, 28.0f),
ButtonIcon.Filter => new UldTextureInfo(28.0f, 0.0f, 28.0f, 28.0f),
ButtonIcon.Sort => new UldTextureInfo(56.0f, 0.0f, 28.0f, 28.0f),
ButtonIcon.QuestionMark => new UldTextureInfo(84.0f, 0.0f, 28.0f, 28.0f),
ButtonIcon.Refresh => new UldTextureInfo(112.0f, 0.0f, 28.0f, 28.0f),
ButtonIcon.ChatBubble => new UldTextureInfo(140.0f, 0.0f, 28.0f, 28.0f),
ButtonIcon.LeftArrow => new UldTextureInfo(168.0f, 0.0f, 28.0f, 28.0f),
ButtonIcon.UpArrow => new UldTextureInfo(196.0f, 0.0f, 28.0f, 28.0f),
ButtonIcon.Chest => new UldTextureInfo(224.0f, 0.0f, 28.0f, 28.0f),
ButtonIcon.Document => new UldTextureInfo(0.0f, 28.0f, 28.0f, 28.0f),
ButtonIcon.Edit => new UldTextureInfo(28.0f, 28.0f, 28.0f, 28.0f),
ButtonIcon.Add => new UldTextureInfo(56.0f, 28.0f, 28.0f, 28.0f),
ButtonIcon.RightArrow => new UldTextureInfo(84.0f, 28.0f, 28.0f, 28.0f),
ButtonIcon.MusicNote => new UldTextureInfo(112.0f, 28.0f, 28.0f, 28.0f),
ButtonIcon.Sprout => new UldTextureInfo(140.0f, 28.0f, 28.0f, 28.0f),
ButtonIcon.Dice => new UldTextureInfo(168.0f, 28.0f, 28.0f, 28.0f),
ButtonIcon.ArrowDown => new UldTextureInfo(196.0f, 28.0f, 28.0f, 28.0f),
ButtonIcon.Eye => new UldTextureInfo(0.0f, 56.0f, 28.0f, 28.0f),
ButtonIcon.Envelope => new UldTextureInfo(28.0f, 56.0f, 28.0f, 28.0f),
ButtonIcon.Volume => new UldTextureInfo(56.0f, 56.0f, 28.0f, 28.0f),
ButtonIcon.Mute => new UldTextureInfo(84.0f, 56.0f, 28.0f, 28.0f),
ButtonIcon.WavePulse => new UldTextureInfo(112.0f, 56.0f, 28.0f, 28.0f),
ButtonIcon.CheckedBox => new UldTextureInfo(140.0f, 56.0f, 28.0f, 28.0f),
ButtonIcon.Cross => new UldTextureInfo(168.0f, 56.0f, 28.0f, 28.0f),
ButtonIcon.Globe => new UldTextureInfo(196.0f, 56.0f, 28.0f, 28.0f),
ButtonIcon.ActiveGearCog => new UldTextureInfo(0.0f, 84.0f, 28.0f, 28.0f),
ButtonIcon.ActiveFilter => new UldTextureInfo(28.0f, 84.0f, 28.0f, 28.0f),
ButtonIcon.Update => new UldTextureInfo(56.0f, 84.0f, 28.0f, 28.0f),
ButtonIcon.ActiveRing => new UldTextureInfo(84.0f, 84.0f, 28.0f, 28.0f),
ButtonIcon.Exclamation => new UldTextureInfo(112.0f, 84.0f, 28.0f, 28.0f),
ButtonIcon.InsetDocument => new UldTextureInfo(140.0f, 84.0f, 28.0f, 28.0f),
ButtonIcon.GearCogWithChatBubble => new UldTextureInfo(168.0f, 84.0f, 28.0f, 28.0f),
ButtonIcon.FlatbedCartBoxes => new UldTextureInfo(196.0f, 84.0f, 28.0f, 28.0f),
ButtonIcon.MagnifyingGlass => new UldTextureInfo(0.0f, 112.0f, 24.0f, 24.0f),
ButtonIcon.EditSmall => new UldTextureInfo(24.0f, 112.0f, 24.0f, 24.0f),
ButtonIcon.WeaponDraw => new UldTextureInfo(48.0f, 112.0f, 24.0f, 24.0f),
ButtonIcon.Headgear => new UldTextureInfo(72.0f, 112.0f, 24.0f, 24.0f),
ButtonIcon.Sword => new UldTextureInfo(96.0f, 112.0f, 24.0f, 24.0f),
ButtonIcon.Emotes => new UldTextureInfo(120.0f, 112.0f, 24.0f, 24.0f),
ButtonIcon.PersonStanding => new UldTextureInfo(144.0f, 112.0f, 24.0f, 24.0f),
ButtonIcon.PaintBucket => new UldTextureInfo(0.0f, 136.0f, 24.0f, 24.0f),
ButtonIcon.EyeSmall => new UldTextureInfo(24.0f, 136.0f, 24.0f, 24.0f),
ButtonIcon.Undo => new UldTextureInfo(48.0f, 136.0f, 24.0f, 24.0f),
ButtonIcon.PinPaper => new UldTextureInfo(72.0f, 136.0f, 24.0f, 24.0f),
ButtonIcon.CrossSmall => new UldTextureInfo(96.0f, 136.0f, 24.0f, 24.0f),
_ => new UldTextureInfo(0.0f, 0.0f, 28.0f, 28.0f),
};
private void LoadTimelines()
=> LoadTwoPartTimelines(this, ImageNode);
}
public enum ButtonIcon {
GearCog,
Filter,
Sort,
QuestionMark,
Refresh,
ChatBubble,
LeftArrow,
UpArrow,
Chest,
Document,
Edit,
Add,
RightArrow,
MusicNote,
Sprout,
Dice,
ArrowDown,
Eye,
Envelope,
Volume,
Mute,
WavePulse,
CheckedBox,
Cross,
Globe,
ActiveGearCog,
ActiveFilter,
Update,
ActiveRing,
Exclamation,
InsetDocument,
GearCogWithChatBubble,
FlatbedCartBoxes,
MagnifyingGlass,
EditSmall,
WeaponDraw,
Headgear,
Sword,
Emotes,
PersonStanding,
PaintBucket,
EyeSmall,
Undo,
PinPaper,
CrossSmall,
}
internal record UldTextureInfo(float PositionX = 0.0f, float PositionY = 0.0f, float Width = 0.0f, float Height = 0.0f) {
public Vector2 TextureCoordinates => new(PositionX, PositionY);
public Vector2 TextureSize => new(Width, Height);
}
@@ -0,0 +1,116 @@
using System.Numerics;
using Dalamud.Interface;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Premade.Color;
using KamiToolKit.Timelines;
using Lumina.Text.ReadOnly;
namespace KamiToolKit.Nodes;
public unsafe class ColorOptionTextButtonNode : ButtonBase {
public readonly NineGridNode BackgroundNode;
public readonly TextNode LabelNode;
public readonly ColorPreviewNode ColorNode;
public ColorOptionTextButtonNode() {
BackgroundNode = new SimpleNineGridNode {
TexturePath = "ui/uld/ButtonA.tex",
TextureSize = new Vector2(100.0f, 28.0f),
LeftOffset = 16.0f,
RightOffset = 16.0f,
};
BackgroundNode.AttachNode(this);
ColorNode = new ColorPreviewNode {
DisableCollisionNode = true,
};
ColorNode.AttachNode(this);
LabelNode = new TextNode {
AlignmentType = AlignmentType.Center,
Position = new Vector2(16.0f, 3.0f),
};
LabelNode.AttachNode(this);
LoadTimelines();
Data->Nodes[0] = LabelNode.NodeId;
Data->Nodes[1] = BackgroundNode.NodeId;
InitializeComponentEvents();
}
public ReadOnlySeString String {
get => LabelNode.String;
set => LabelNode.String = value;
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
LabelNode.Size = new Vector2(Width - 32.0f, Height - 8.0f);
BackgroundNode.Size = Size;
ColorNode.Size = new Vector2(17.0f, 17.0f);
}
public ColorHelpers.HsvaColor? DefaultHsvaColor {
get => ColorNode.ColorHsva;
set => ColorNode.ColorHsva = value ?? default;
}
public Vector4? DefaultColor {
get => ColorNode.Color;
set => ColorNode.Color = value ?? default;
}
private void LoadTimelines() {
var foregroundPositionOffset = new Vector2(24.0f, 3.0f);
var colorElementPositionOffset = new Vector2(16.0f, 2.0f);
AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 53)
.AddLabelPair(1, 10, 1)
.AddLabelPair(11, 17, 2)
.AddLabelPair(18, 26, 3)
.AddLabelPair(27, 36, 7)
.AddLabelPair(37, 46, 6)
.AddLabelPair(47, 53, 4)
.EndFrameSet()
.Build());
BackgroundNode.AddTimeline(new TimelineBuilder()
.AddFrameSetWithFrame(1, 10, 1, Vector2.Zero, 255, multiplyColor: new Vector3(100.0f))
.BeginFrameSet(11, 17)
.AddFrame(11, Vector2.Zero, 255, multiplyColor: new Vector3(100.0f))
.AddFrame(13, Vector2.Zero, 255, multiplyColor: new Vector3(100.0f), addColor: new Vector3(16.0f))
.EndFrameSet()
.AddFrameSetWithFrame(18, 26, 18, new Vector2(0.0f, 1.0f), 255, new Vector3(16.0f))
.AddFrameSetWithFrame(27, 36, 27, Vector2.Zero, 178, multiplyColor: new Vector3(50.0f))
.AddFrameSetWithFrame(37, 46, 37, Vector2.Zero, 255, multiplyColor: new Vector3(100.0f), addColor: new Vector3(16.0f))
.BeginFrameSet(47, 53)
.AddFrame(47, Vector2.Zero, 255, multiplyColor: new Vector3(100.0f), addColor: new Vector3(16.0f))
.AddFrame(53, Vector2.Zero, 255, multiplyColor: new Vector3(100.0f))
.EndFrameSet()
.Build());
ColorNode.AddTimeline(new TimelineBuilder()
.AddFrameSetWithFrame(1, 10, 1, colorElementPositionOffset, 255, multiplyColor: new Vector3(100.0f))
.AddFrameSetWithFrame(11, 17, 11, colorElementPositionOffset, 255, multiplyColor: new Vector3(100.0f))
.AddFrameSetWithFrame(18, 26, 18, colorElementPositionOffset + new Vector2(0.0f, 1.0f), 255, multiplyColor: new Vector3(100.0f))
.AddFrameSetWithFrame(27, 36, 27, colorElementPositionOffset, 153, multiplyColor: new Vector3(80.0f))
.AddFrameSetWithFrame(37, 46, 37, colorElementPositionOffset, 255, multiplyColor: new Vector3(100.0f))
.AddFrameSetWithFrame(47, 53, 47, colorElementPositionOffset, 255, multiplyColor: new Vector3(100.0f))
.Build());
LabelNode.AddTimeline(new TimelineBuilder()
.AddFrameSetWithFrame(1, 10, 1, foregroundPositionOffset, 255, multiplyColor: new Vector3(100.0f))
.AddFrameSetWithFrame(11, 17, 11, foregroundPositionOffset, 255, multiplyColor: new Vector3(100.0f))
.AddFrameSetWithFrame(18, 26, 18, foregroundPositionOffset + new Vector2(0.0f, 1.0f), 255, multiplyColor: new Vector3(100.0f))
.AddFrameSetWithFrame(27, 36, 27, foregroundPositionOffset, 153, multiplyColor: new Vector3(80.0f))
.AddFrameSetWithFrame(37, 46, 37, foregroundPositionOffset, 255, multiplyColor: new Vector3(100.0f))
.AddFrameSetWithFrame(47, 53, 47, foregroundPositionOffset, 255, multiplyColor: new Vector3(100.0f))
.Build());
}
}
@@ -0,0 +1,112 @@
using System;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
namespace KamiToolKit.Nodes;
public abstract unsafe class ComponentNode(NodeType nodeType) : NodeBase<AtkComponentNode>(nodeType) {
public abstract CollisionNode CollisionNode { get; }
public abstract AtkComponentBase* ComponentBase { get; }
public abstract AtkUldComponentDataBase* DataBase { get; }
}
public abstract unsafe class ComponentNode<T, TU> : ComponentNode where T : unmanaged, ICreatable where TU : unmanaged {
public sealed override CollisionNode CollisionNode { get; }
public sealed override AtkComponentBase* ComponentBase => Node->Component;
public sealed override AtkUldComponentDataBase* DataBase => Node->Component->UldManager.ComponentData;
protected ComponentNode() : base(NodeType.Component) {
Node->Component = (AtkComponentBase*) NativeMemoryHelper.Create<T>();
Node->Component->UldManager.ComponentData = (AtkUldComponentDataBase*)NativeMemoryHelper.UiAlloc<TU>();
ComponentBase->Initialize();
CollisionNode = new CollisionNode {
NodeId = 1,
LinkedComponent = ComponentBase,
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.HasCollision |
NodeFlags.RespondToMouse | NodeFlags.Focusable | NodeFlags.EmitsEvents | NodeFlags.Fill,
};
CollisionNode.ResNode->ParentNode = ResNode;
CollisionNode.ParentUldManager = &((AtkComponentBase*)Component)->UldManager;
ChildNodes.Add(CollisionNode);
ComponentBase->OwnerNode = Node;
ComponentBase->ComponentFlags = 1;
ref var uldManager = ref ComponentBase->UldManager;
uldManager.Objects = (AtkUldObjectInfo*)NativeMemoryHelper.UiAlloc<AtkUldComponentInfo>();
ref var objects = ref uldManager.Objects;
uldManager.ObjectCount = 1;
SetInternalComponentType(ComponentType.Base);
objects->NodeList = (AtkResNode**)NativeMemoryHelper.Malloc(8);
objects->NodeList[0] = CollisionNode;
objects->NodeCount = 1;
objects->Id = 1000;
uldManager.InitializeResourceRendererManager();
uldManager.RootNode = CollisionNode;
uldManager.UpdateDrawNodeList();
uldManager.ResourceFlags = AtkUldManagerResourceFlag.Initialized | AtkUldManagerResourceFlag.ArraysAllocated;
uldManager.LoadedState = AtkLoadState.Loaded;
}
protected override void Dispose(bool disposing, bool isNativeDestructor) {
if (disposing) {
try {
if (!isNativeDestructor) {
Node->Component->Deinitialize();
Node->Component->Dtor(1);
Node->Component = null;
}
}
catch (Exception e) {
Log.Exception(e);
} finally {
base.Dispose(disposing, isNativeDestructor);
}
}
}
public static implicit operator AtkEventListener*(ComponentNode<T, TU> node) => &node.ComponentBase->AtkEventListener;
public static implicit operator T*(ComponentNode<T, TU> node) => node.Component;
public static implicit operator TU*(ComponentNode<T, TU> node) => node.Data;
protected void SetInternalComponentType(ComponentType type) {
var componentInfo = (AtkUldComponentInfo*)ComponentBase->UldManager.Objects;
componentInfo->ComponentType = type;
}
protected void InitializeComponentEvents() {
ComponentBase->InitializeFromComponentData(DataBase);
ComponentBase->Setup();
ComponentBase->SetEnabledState(true);
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
CollisionNode.Size = Size;
ComponentBase->UldManager.RootNodeHeight = (ushort)Height;
ComponentBase->UldManager.RootNodeWidth = (ushort)Width;
}
public virtual bool IsEnabled {
get => NodeFlags.HasFlag(NodeFlags.Enabled);
set => ComponentBase->SetEnabledState(value);
}
public override int ChildCount => ComponentBase->UldManager.NodeListCount;
public T* Component => (T*)ComponentBase;
public TU* Data => (TU*)DataBase;
}
+461
View File
@@ -0,0 +1,461 @@
using System;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Enums;
using KamiToolKit.Timelines;
namespace KamiToolKit.Nodes;
public abstract unsafe class DropDownNode<T, TU> : SimpleComponentNode where T : ButtonListNode<TU>, new() {
public readonly NineGridNode BackgroundNode;
public readonly ImageNode CollapseArrowNode;
public readonly CollisionNode DropDownFocusCollisionNode;
public readonly TextNode LabelNode;
public readonly T OptionListNode;
protected DropDownNode() {
BackgroundNode = new SimpleNineGridNode {
TexturePath = "ui/uld/DropDownA.tex",
TextureSize = new Vector2(44.0f, 23.0f),
TextureCoordinates = new Vector2(0.0f, 0.0f),
Size = new Vector2(250.0f, 24.0f),
Height = 23.0f,
LeftOffset = 16.0f,
RightOffset = 16.0f,
};
BackgroundNode.AttachNode(this);
CollapseArrowNode = new SimpleImageNode {
TexturePath = "ui/uld/DropDownA.tex",
TextureCoordinates = new Vector2(44.0f, 0.0f),
TextureSize = new Vector2(12.0f, 12.0f),
Position = new Vector2(6.0f, 17.0f),
Size = new Vector2(12.0f, 12.0f),
WrapMode = WrapMode.Stretch,
};
CollapseArrowNode.AttachNode(this);
LabelNode = new TextNode {
Position = new Vector2(20.0f, 0.0f),
Size = new Vector2(218.0f, 21.0f),
FontType = FontType.Axis,
FontSize = 12,
AlignmentType = AlignmentType.Left,
TextColor = ColorHelper.GetColor(50),
TextOutlineColor = ColorHelper.GetColor(7),
String = "Demo",
};
LabelNode.AttachNode(this);
OptionListNode = new T {
NodeId = NodeIdBase,
Position = new Vector2(4.0f, 21.0f),
Size = new Vector2(242.0f, 243.0f),
IsVisible = false,
};
OptionListNode.AttachNode(this);
DropDownFocusCollisionNode = new CollisionNode();
DropDownFocusCollisionNode.AttachNode(OptionListNode.CollisionNode, NodePosition.AfterTarget);
DropDownFocusCollisionNode.AddEvent(AtkEventType.MouseDown, Toggle);
DropDownFocusCollisionNode.AddEvent(AtkEventType.MouseWheel, Toggle);
BuildTimelines();
Timeline?.PlayAnimation(4);
CollisionNode.ShowClickableCursor = true;
CollisionNode.AddEvent(AtkEventType.MouseOver, () => Timeline?.PlayAnimation(IsCollapsed ? 2 : 9));
CollisionNode.AddEvent(AtkEventType.MouseOut, () => Timeline?.PlayAnimation(IsCollapsed ? 4 : 11));
CollisionNode.AddEvent(AtkEventType.MouseClick, Toggle);
Component->SoundEffectId = 1;
Component->SetEnabledState(true);
}
public bool IsCollapsed { get; set; } = true;
public int MaxListOptions {
get => OptionListNode.MaxButtons;
set => OptionListNode.MaxButtons = value;
}
public TU? SelectedOption {
get => OptionListNode.SelectedOption;
set {
OptionListNode.SelectedOption = value;
UpdateLabel(value);
}
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
CollisionNode.Size = Size;
BackgroundNode.Size = new Vector2(Width, Height - 1.0f);
LabelNode.Size = new Vector2(Width - 32.0f, Height - 3.0f);
OptionListNode.Width = Width - 8.0f;
OptionListNode.Position = new Vector2(4.0f, Height - 3.0f);
}
public Action<bool>? OnCollapseToggled { get; set; }
public Action? OnUncollapsed { get; set; }
public Action? OnCollapsed { get; set; }
public void Collapse(bool playSoundEffect = true) {
if (!IsEnabled) return;
if (IsCollapsed) return;
IsCollapsed = true;
Timeline?.PlayAnimation(4);
OptionListNode.Toggle(false);
// TODO: replace this (and in Uncollapse) with just a check for playSoundEffect and a call to Component->PlaySoundEffect();
// when https://github.com/aers/FFXIVClientStructs/commit/e5b6fc51 landed in Dalamud
if (playSoundEffect && Component->SoundEffectId is not -1)
UIGlobals.PlaySoundEffect((uint)Component->SoundEffectId);
OptionListNode.ReattachNode(this);
// Need to reset position after reattaching, so screen position is recalculated correctly
OptionListNode.Position = Size with { X = 0.0f } + new Vector2(4.0f, -4.0f);
OnCollapsed?.Invoke();
}
public void Uncollapse(bool playSoundEffect = true) {
if (!IsEnabled) return;
if (!IsCollapsed) return;
IsCollapsed = false;
Timeline?.PlayAnimation(11);
OptionListNode.Toggle(true);
if (playSoundEffect && Component->SoundEffectId is not -1)
UIGlobals.PlaySoundEffect((uint)Component->SoundEffectId);
if (ParentAddon is not null) {
OptionListNode.Position = (ScreenPosition - ParentAddon->Position) / ParentAddon->Scale + Size with { X = 0.0f } + new Vector2(4.0f, -4.0f);
MoveListOnScreen();
DropDownFocusCollisionNode.Position = -OptionListNode.Position;
DropDownFocusCollisionNode.Size = ParentAddon->RootSize;
OptionListNode.ReattachNode(ParentAddon->RootNode);
}
OnUncollapsed?.Invoke();
}
public void Toggle() {
Toggle(true);
}
public void Toggle(bool playSoundEffect) {
if (!IsEnabled) return;
if (IsCollapsed) {
Uncollapse(playSoundEffect);
}
else {
Collapse(playSoundEffect);
}
OnCollapseToggled?.Invoke(IsCollapsed);
}
public void RecalculateScrollParams()
=> OptionListNode.RecalculateScrollParams();
private void MoveListOnScreen() {
var screenSize = AtkStage.Instance()->ScreenSize;
var parentAddon = RaptureAtkUnitManager.Instance()->GetAddonByNode(ResNode);
if (parentAddon == null) {
return;
}
var scale = parentAddon->Scale;
var scaledListSize = OptionListNode.Size * scale;
if (ScreenPosition.X + scaledListSize.X > screenSize.Width) {
OptionListNode.X += (screenSize.Width - OptionListNode.ScreenPosition.X - scaledListSize.X - 4f) / scale;
}
else if (ScreenPosition.X < 0) {
OptionListNode.X -= OptionListNode.ScreenPosition.X / scale;
}
if (OptionListNode.ScreenPosition.Y + scaledListSize.Y > screenSize.Height) {
OptionListNode.Y += (screenSize.Height - OptionListNode.ScreenPosition.Y - scaledListSize.Y) / scale;
}
}
protected abstract void UpdateLabel(TU? option);
private void BuildTimelines() {
AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 120)
.AddLabel(1, 1, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(9, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(10, 2, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(19, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(20, 3, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(29, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(30, 7, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(39, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(40, 6, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(49, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(50, 4, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(59, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(60, 8, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(69, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(70, 9, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(79, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(80, 10, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(89, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(90, 14, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(99, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(100, 13, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(109, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(110, 11, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(120, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.EndFrameSet()
.Build()
);
CollapseArrowNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 9)
.AddFrame(1, new Vector2(6, 17))
.AddFrame(1, rotation: 4.712389f)
.AddFrame(1, alpha: 255)
.AddFrame(1, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(10, 19)
.AddFrame(10, new Vector2(6, 17))
.AddFrame(12, new Vector2(6, 17))
.AddFrame(10, rotation: 4.712389f)
.AddFrame(12, rotation: 4.712389f)
.AddFrame(10, alpha: 255)
.AddFrame(12, alpha: 255)
.AddFrame(10, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(12, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(20, 29)
.AddFrame(20, new Vector2(6, 18))
.AddFrame(20, rotation: 4.712389f)
.AddFrame(20, alpha: 255)
.AddFrame(20, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(30, 39)
.AddFrame(30, new Vector2(6, 17))
.AddFrame(30, rotation: 4.712389f)
.AddFrame(30, alpha: 178)
.AddFrame(30, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(50, 50, 50))
.EndFrameSet()
.BeginFrameSet(40, 49)
.AddFrame(40, new Vector2(6, 17))
.AddFrame(40, rotation: 4.712389f)
.AddFrame(40, alpha: 255)
.AddFrame(40, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(50, 59)
.AddFrame(50, new Vector2(6, 17))
.AddFrame(52, new Vector2(6, 17))
.AddFrame(50, rotation: 4.712389f)
.AddFrame(52, rotation: 4.712389f)
.AddFrame(50, alpha: 255)
.AddFrame(52, alpha: 255)
.AddFrame(50, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(52, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(60, 69)
.AddFrame(60, new Vector2(6, 6))
.AddFrame(60, rotation: 0)
.AddFrame(60, alpha: 255)
.AddFrame(60, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(70, 79)
.AddFrame(70, new Vector2(6, 6))
.AddFrame(72, new Vector2(6, 6))
.AddFrame(70, rotation: 0)
.AddFrame(72, rotation: 0)
.AddFrame(70, alpha: 255)
.AddFrame(72, alpha: 255)
.AddFrame(70, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(72, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(80, 89)
.AddFrame(80, new Vector2(6, 7))
.AddFrame(80, rotation: 0)
.AddFrame(80, alpha: 255)
.AddFrame(80, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(90, 99)
.AddFrame(90, new Vector2(6, 6))
.AddFrame(90, rotation: 0)
.AddFrame(90, alpha: 178)
.AddFrame(90, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(50, 50, 50))
.EndFrameSet()
.BeginFrameSet(100, 109)
.AddFrame(100, new Vector2(6, 6))
.AddFrame(100, rotation: 0)
.AddFrame(100, alpha: 255)
.AddFrame(100, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(110, 120)
.AddFrame(110, new Vector2(6, 6))
.AddFrame(112, new Vector2(6, 6))
.AddFrame(110, rotation: 0)
.AddFrame(112, rotation: 0)
.AddFrame(110, alpha: 255)
.AddFrame(112, alpha: 255)
.AddFrame(110, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(112, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.Build()
);
LabelNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 9)
.AddFrame(1, new Vector2(20, 0))
.AddFrame(1, alpha: 255)
.AddFrame(1, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(10, 19)
.AddFrame(10, new Vector2(20, 0))
.AddFrame(10, alpha: 255)
.AddFrame(10, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(20, 29)
.AddFrame(20, new Vector2(20, 1))
.AddFrame(20, alpha: 255)
.AddFrame(20, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(30, 39)
.AddFrame(30, new Vector2(20, 0))
.AddFrame(30, alpha: 153)
.AddFrame(30, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(80, 80, 80))
.EndFrameSet()
.BeginFrameSet(40, 49)
.AddFrame(40, new Vector2(20, 0))
.AddFrame(40, alpha: 255)
.AddFrame(40, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(50, 59)
.AddFrame(50, new Vector2(20, 0))
.AddFrame(50, alpha: 255)
.AddFrame(50, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(60, 69)
.AddFrame(60, new Vector2(20, 0))
.AddFrame(60, alpha: 255)
.AddFrame(60, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(70, 79)
.AddFrame(70, new Vector2(20, 0))
.AddFrame(70, alpha: 255)
.AddFrame(70, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(80, 89)
.AddFrame(80, new Vector2(20, 1))
.AddFrame(80, alpha: 255)
.AddFrame(80, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(90, 99)
.AddFrame(90, new Vector2(20, 0))
.AddFrame(90, alpha: 153)
.AddFrame(90, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(80, 80, 80))
.EndFrameSet()
.BeginFrameSet(100, 109)
.AddFrame(100, new Vector2(20, 0))
.AddFrame(100, alpha: 255)
.AddFrame(100, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(110, 120)
.AddFrame(110, new Vector2(20, 0))
.AddFrame(110, alpha: 255)
.AddFrame(110, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.Build()
);
BackgroundNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 9)
.AddFrame(1, new Vector2(0, 0))
.AddFrame(1, alpha: 255)
.AddFrame(1, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(10, 19)
.AddFrame(10, new Vector2(0, 0))
.AddFrame(12, new Vector2(0, 0))
.AddFrame(10, alpha: 255)
.AddFrame(12, alpha: 255)
.AddFrame(10, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(12, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(20, 29)
.AddFrame(20, new Vector2(0, 1))
.AddFrame(20, alpha: 255)
.AddFrame(20, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(30, 39)
.AddFrame(30, new Vector2(0, 0))
.AddFrame(30, alpha: 178)
.AddFrame(30, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(50, 50, 50))
.EndFrameSet()
.BeginFrameSet(40, 49)
.AddFrame(40, new Vector2(0, 0))
.AddFrame(40, alpha: 255)
.AddFrame(40, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(50, 59)
.AddFrame(50, new Vector2(0, 0))
.AddFrame(52, new Vector2(0, 0))
.AddFrame(50, alpha: 255)
.AddFrame(52, alpha: 255)
.AddFrame(50, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(52, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(60, 69)
.AddFrame(60, new Vector2(0, 0))
.AddFrame(60, alpha: 255)
.AddFrame(60, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(70, 79)
.AddFrame(70, new Vector2(0, 0))
.AddFrame(72, new Vector2(0, 0))
.AddFrame(70, alpha: 255)
.AddFrame(72, alpha: 255)
.AddFrame(70, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(72, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(80, 89)
.AddFrame(80, new Vector2(0, 1))
.AddFrame(80, alpha: 255)
.AddFrame(80, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(90, 99)
.AddFrame(90, new Vector2(0, 0))
.AddFrame(90, alpha: 178)
.AddFrame(90, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(50, 50, 50))
.EndFrameSet()
.BeginFrameSet(100, 109)
.AddFrame(100, new Vector2(0, 0))
.AddFrame(100, alpha: 255)
.AddFrame(100, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(110, 120)
.AddFrame(110, new Vector2(0, 0))
.AddFrame(112, new Vector2(0, 0))
.AddFrame(110, alpha: 255)
.AddFrame(112, alpha: 255)
.AddFrame(110, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(112, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.Build()
);
}
}
@@ -0,0 +1,9 @@
using System;
namespace KamiToolKit.Nodes;
public class EnumButtonListNode<T> : ButtonListNode<T> where T : Enum {
protected override string GetLabelForOption(T option)
=> option.Description;
}
@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
namespace KamiToolKit.Nodes;
public class EnumDropDownNode<T> : DropDownNode<EnumButtonListNode<T>, T> where T : Enum{
public EnumDropDownNode() {
OptionListNode.OnOptionSelected += OptionSelectedHandler;
}
public Action<T>? OnOptionSelected { get; set; }
public required List<T>? Options {
get => OptionListNode.Options;
set {
OptionListNode.Options = value;
OptionListNode.SelectDefaultOption();
UpdateLabel(OptionListNode.SelectedOption);
}
}
private void OptionSelectedHandler(T option) {
OnOptionSelected?.Invoke(option);
UpdateLabel(option);
Toggle(false);
}
protected override void UpdateLabel(T? option) {
LabelNode.String = option?.Description;
}
}
@@ -0,0 +1,270 @@
using System;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Timelines;
using Lumina.Text.ReadOnly;
namespace KamiToolKit.Nodes;
public unsafe class HoldButtonNode : ComponentNode<AtkComponentHoldButton, AtkUldComponentDataHoldButton> {
public readonly NineGridNode BackgroundNode;
public readonly NineGridNode FrameNode;
public readonly HoldButtonProgressNode ProgressNode;
public readonly TextNode TextNode;
public HoldButtonNode() {
SetInternalComponentType(ComponentType.HoldButton);
BackgroundNode = new SimpleNineGridNode {
TexturePath = "ui/uld/LongPressButtonA.tex",
TextureCoordinates = new Vector2(0.0f, 0.0f),
TextureSize = new Vector2(100.0f, 36.0f),
Size = new Vector2(100.0f, 36.0f),
LeftOffset = 16,
RightOffset = 16,
};
BackgroundNode.AttachNode(this);
ProgressNode = new HoldButtonProgressNode {
Size = new Vector2(100.0f, 36.0f),
};
ProgressNode.AttachNode(this);
FrameNode = new SimpleNineGridNode {
TexturePath = "ui/uld/LongPressButtonA.tex",
TextureCoordinates = new Vector2(0.0f, 72.0f),
TextureSize = new Vector2(100.0f, 36.0f),
Size = new Vector2(100.0f, 36.0f),
};
FrameNode.AttachNode(this);
TextNode = new TextNode {
Position = new Vector2(16.0f, 8.0f),
Size = new Vector2(68.0f, 20.0f),
AlignmentType = AlignmentType.Center,
String = "OK",
};
TextNode.AttachNode(this);
Data->Nodes[0] = TextNode.NodeId;
Data->Nodes[1] = BackgroundNode.NodeId;
Data->Nodes[2] = ProgressNode.NodeId;
Data->Nodes[3] = ProgressNode.ImageNode.NodeId;
InitializeComponentEvents();
AddEvent(AtkEventType.ButtonClick, ClickHandler);
BuildTimelines();
}
public bool UnlockAfterClick { get; set; }
public Action? OnClick { get; set; }
public ReadOnlySeString String {
get => TextNode.String;
set => TextNode.String = value;
}
private void ClickHandler() {
OnClick?.Invoke();
if (UnlockAfterClick) {
Reset();
}
}
public void Reset() {
Component->IsTargetReached = false;
Component->IsEventFired = false;
Component->Progress.StartValue = 0;
Component->Progress.TargetValue = 0;
Component->Progress.CurrentValue = 0;
Component->Progress.EndValue = 0;
}
private void BuildTimelines() {
AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 20)
.AddLabel(1, 17, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(10, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(11, 101, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(20, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.EndFrameSet()
.Build()
);
BackgroundNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 10)
.AddFrame(1, new Vector2(0, 0))
.AddFrame(1, alpha: 255)
.AddFrame(1, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(11, 17)
.AddFrame(11, new Vector2(0, 0))
.AddFrame(13, new Vector2(0, 0))
.AddFrame(11, alpha: 255)
.AddFrame(13, alpha: 255)
.AddFrame(11, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(13, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(18, 26)
.AddFrame(18, new Vector2(0, 1))
.AddFrame(18, alpha: 255)
.AddFrame(18, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(27, 36)
.AddFrame(27, new Vector2(0, 0))
.AddFrame(27, alpha: 178)
.AddFrame(27, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(50, 50, 50))
.EndFrameSet()
.BeginFrameSet(37, 46)
.AddFrame(37, new Vector2(0, 0))
.AddFrame(37, alpha: 255)
.AddFrame(37, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(47, 53)
.AddFrame(47, new Vector2(0, 0))
.AddFrame(53, new Vector2(0, 0))
.AddFrame(47, alpha: 255)
.AddFrame(53, alpha: 255)
.AddFrame(47, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(53, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(54, 64)
.AddFrame(54, new Vector2(0, 0))
.AddFrame(54, alpha: 255)
.AddFrame(54, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(65, 71)
.AddFrame(65, new Vector2(0, 0))
.AddFrame(71, new Vector2(0, 0))
.AddFrame(65, alpha: 255)
.AddFrame(71, alpha: 255)
.AddFrame(65, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(71, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.Build()
);
ProgressNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 83)
.AddLabel(1, 29, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(60, 30, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(61, 31, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(73, 32, AtkTimelineJumpBehavior.PlayOnce, 31)
.AddLabel(74, 33, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(83, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.EndFrameSet()
.BeginFrameSet(18, 26)
.AddEmptyFrame(18)
.EndFrameSet()
.BeginFrameSet(37, 53)
.AddEmptyFrame(37)
.EndFrameSet()
.BeginFrameSet(54, 71)
.AddEmptyFrame(54)
.EndFrameSet()
.Build()
);
FrameNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 10)
.AddFrame(1, new Vector2(0, 0))
.AddFrame(1, alpha: 255)
.AddFrame(1, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(11, 17)
.AddFrame(11, new Vector2(0, 0))
.AddFrame(13, new Vector2(0, 0))
.AddFrame(11, alpha: 255)
.AddFrame(13, alpha: 255)
.AddFrame(11, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(13, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(95, 95, 95))
.EndFrameSet()
.BeginFrameSet(18, 26)
.AddFrame(18, new Vector2(0, 0))
.AddFrame(18, alpha: 255)
.AddFrame(18, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(95, 95, 95))
.EndFrameSet()
.BeginFrameSet(27, 36)
.AddFrame(27, new Vector2(0, 0))
.AddFrame(27, alpha: 178)
.AddFrame(27, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(50, 50, 50))
.EndFrameSet()
.BeginFrameSet(37, 46)
.AddFrame(37, new Vector2(0, 0))
.AddFrame(37, alpha: 255)
.AddFrame(37, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(95, 95, 95))
.EndFrameSet()
.BeginFrameSet(47, 53)
.AddFrame(47, new Vector2(0, 0))
.AddFrame(53, new Vector2(0, 0))
.AddFrame(47, alpha: 255)
.AddFrame(53, alpha: 255)
.AddFrame(47, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(95, 95, 95))
.AddFrame(53, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(54, 64)
.AddFrame(54, new Vector2(0, 0))
.AddFrame(54, alpha: 255)
.AddFrame(54, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(95, 95, 95))
.EndFrameSet()
.BeginFrameSet(65, 71)
.AddFrame(65, new Vector2(0, 0))
.AddFrame(71, new Vector2(0, 0))
.AddFrame(65, alpha: 255)
.AddFrame(71, alpha: 255)
.AddFrame(65, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(95, 95, 95))
.AddFrame(71, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.Build()
);
TextNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 10)
.AddFrame(1, new Vector2(16, 8))
.AddFrame(1, alpha: 255)
.AddFrame(1, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(11, 17)
.AddFrame(11, new Vector2(16, 8))
.AddFrame(11, alpha: 255)
.AddFrame(11, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(18, 26)
.AddFrame(18, new Vector2(16, 9))
.AddFrame(18, alpha: 255)
.AddFrame(18, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(27, 36)
.AddFrame(27, new Vector2(16, 8))
.AddFrame(27, alpha: 153)
.AddFrame(27, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(80, 80, 80))
.EndFrameSet()
.BeginFrameSet(37, 46)
.AddFrame(37, new Vector2(16, 8))
.AddFrame(37, alpha: 255)
.AddFrame(37, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(47, 53)
.AddFrame(47, new Vector2(16, 8))
.AddFrame(47, alpha: 255)
.AddFrame(47, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(54, 64)
.AddFrame(54, new Vector2(16, 8))
.AddFrame(54, alpha: 255)
.AddFrame(54, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(65, 71)
.AddFrame(65, new Vector2(16, 8))
.AddFrame(65, alpha: 255)
.AddFrame(65, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.Build()
);
}
}
@@ -0,0 +1,51 @@
using System.Numerics;
namespace KamiToolKit.Nodes;
/// <summary>
/// Uses a GameIconId to display that icon as the decorator for the button.
/// </summary>
public class IconButtonNode : ButtonBase {
public readonly NineGridNode BackgroundNode;
public readonly IconImageNode ImageNode;
public IconButtonNode() {
BackgroundNode = new SimpleNineGridNode {
TexturePath = "ui/uld/BgParts.tex",
TextureSize = new Vector2(32.0f, 32.0f),
TextureCoordinates = new Vector2(33.0f, 65.0f),
TopOffset = 8.0f,
LeftOffset = 8.0f,
RightOffset = 8.0f,
BottomOffset = 8.0f,
};
BackgroundNode.AttachNode(this);
ImageNode = new IconImageNode {
TextureSize = new Vector2(32.0f, 32.0f),
FitTexture = true,
};
ImageNode.AttachNode(this);
LoadTimelines();
InitializeComponentEvents();
}
public uint IconId {
get => ImageNode.IconId;
set => ImageNode.IconId = value;
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
ImageNode.Size = Size - new Vector2(16.0f, 16.0f);
ImageNode.Position = BackgroundNode.Position + new Vector2(BackgroundNode.LeftOffset, BackgroundNode.TopOffset);
BackgroundNode.Size = Size;
}
private void LoadTimelines()
=> LoadThreePartTimelines(this, BackgroundNode, ImageNode, new Vector2(8.0f, 8.0f));
}
+133
View File
@@ -0,0 +1,133 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Enums;
using KamiToolKit.Timelines;
namespace KamiToolKit.Nodes;
public unsafe class IconNode : ComponentNode<AtkComponentIcon, AtkUldComponentDataIcon> {
public readonly IconExtras IconExtras;
public readonly IconImageNode IconImage;
public readonly IconIndicator IconIndicator1;
public readonly IconIndicator IconIndicator2;
public IconNode() {
SetInternalComponentType(ComponentType.Icon);
IconImage = new IconImageNode {
NodeId = 20,
Size = new Vector2(40.0f, 40.0f),
Position = new Vector2(2.0f, 3.0f),
WrapMode = WrapMode.Tile,
ImageNodeFlags = ImageNodeFlags.AutoFit,
};
IconImage.AttachNode(this);
IconExtras = new IconExtras {
NodeId = 6,
Size = new Vector2(60, 60),
Position = new Vector2(-2.0f, 0.0f),
};
IconExtras.AttachNode(this);
IconIndicator1 = new IconIndicator(5) {
NodeId = 4,
Size = new Vector2(18.0f, 18.0f),
Position = new Vector2(27.0f, 11.0f),
};
IconIndicator1.AttachNode(this);
IconIndicator2 = new IconIndicator(3) {
NodeId = 2,
Size = new Vector2(18.0f, 18.0f),
Position = new Vector2(27.0f, -2.0f),
};
IconIndicator2.AttachNode(this);
BuildTimeline();
Data->Nodes[0] = IconImage.NodeId;
Data->Nodes[1] = IconExtras.CooldownNode.NodeId;
Data->Nodes[2] = IconExtras.NodeId;
Data->Nodes[3] = IconExtras.ResourceCostTextNode.NodeId;
Data->Nodes[4] = IconExtras.QuantityTextNode.NodeId;
Data->Nodes[5] = IconExtras.AntsNode.NodeId;
Data->Nodes[6] = IconIndicator1.IconNode.NodeId;
Data->Nodes[7] = IconIndicator2.IconNode.NodeId;
InitializeComponentEvents();
}
public uint IconId {
get => Component->IconId;
set => Component->LoadIcon(value);
}
public bool IsIconLoading
=> Component->Flags.HasFlag(IconComponentFlags.IsIconLoading);
public bool IsIconDisabled {
get => Component->Flags.HasFlag(IconComponentFlags.IsDisabled);
set => Component->SetIconImageDisableState(value);
}
public byte ComboLevel {
get {
if (Component->Flags.HasFlag(IconComponentFlags.ComboLevel3))
return 3;
if (Component->Flags.HasFlag(IconComponentFlags.ComboLevel2))
return 2;
if (Component->Flags.HasFlag(IconComponentFlags.ComboLevel1))
return 1;
return 0;
}
set => Component->SetComboLevel(value is >= 1 and <= 3, (byte)(value - 1));
}
public bool IsMacro {
get => Component->Flags.HasFlag(IconComponentFlags.IsMacro);
set => Component->SetIsMacro(value);
}
public bool IsRecipe {
get => Component->Flags.HasFlag(IconComponentFlags.IsRecipe);
set => Component->SetIsRecipe(value);
}
public bool IsBeingDragged
=> Component->Flags.HasFlag(IconComponentFlags.IsBeingDragged);
private void BuildTimeline() {
IconExtras.AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 59)
.AddLabelPair(1, 9, 1)
.AddLabelPair(10, 19, 2)
.AddLabelPair(20, 29, 3)
.AddLabelPair(30, 39, 7)
.AddLabelPair(40, 49, 6)
.AddLabelPair(50, 59, 4)
.EndFrameSet()
.Build());
var iconIndicatorTimeline = new TimelineBuilder()
.BeginFrameSet(1, 129)
.AddLabel(1, 17, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(11, 101, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(21, 102, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(31, 103, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(41, 104, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(51, 105, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(61, 106, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(71, 107, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(80, 108, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(90, 109, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(100, 110, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(110, 111, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(120, 112, AtkTimelineJumpBehavior.PlayOnce, 0)
.EndFrameSet();
IconIndicator1.AddTimeline(iconIndicatorTimeline.Build());
IconIndicator2.AddTimeline(iconIndicatorTimeline.Build());
}
}
@@ -0,0 +1,81 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace KamiToolKit.Nodes;
public class IconToggleNode : SimpleComponentNode {
private readonly IconImageNode iconNode;
private readonly ClippingMaskNode clipNode;
private readonly SimpleImageNode highlightNode; // For selected
private readonly SimpleImageNode lowlightNode; // For unselected
public IconToggleNode() {
iconNode = new IconImageNode {
TextureSize = new Vector2(36.0f, 36.0f),
FitTexture = true,
};
iconNode.AttachNode(this);
clipNode = new SimpleClippingMaskNode {
TextureCoordinates = Vector2.Zero,
TextureSize = new Vector2(32.0f, 32.0f),
TexturePath = "ui/uld/BgPartsMask.tex",
Size = new Vector2(32.0f, 32.0f),
};
clipNode.AttachNode(this);
highlightNode = new SimpleImageNode {
Size = new Vector2(36.0f, 36.0f),
IsVisible = false,
TextureCoordinates = new Vector2(69.0f, 1.0f),
TextureSize = new Vector2(36.0f, 36.0f),
TexturePath = "ui/uld/BgParts.tex",
};
highlightNode.AttachNode(this);
lowlightNode = new SimpleImageNode {
Size = new Vector2(36.0f, 36.0f),
IsVisible = false,
TextureCoordinates = new Vector2(141.0f, 1.0f),
TextureSize = new Vector2(36.0f, 36.0f),
TexturePath = "ui/uld/BgParts.tex",
};
lowlightNode.AttachNode(this);
CollisionNode.AddEvent(AtkEventType.MouseClick, () => UIGlobals.PlaySoundEffect(1));
}
public uint IconId {
get => iconNode.IconId;
set => iconNode.IconId = value;
}
public bool IsToggled {
get;
set {
field = value;
highlightNode.IsVisible = value;
lowlightNode.IsVisible = !value;
}
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
// Icon is 32x32 centered within the 36x36 node
var iconSize = Size - new Vector2(4.0f, 4.0f);
var iconOffset = new Vector2(2.0f, 2.0f);
iconNode.Size = iconSize;
iconNode.Position = iconOffset;
clipNode.Size = iconSize;
clipNode.Position = iconOffset;
highlightNode.Size = Size;
highlightNode.Position = Vector2.Zero;
lowlightNode.Size = Size;
lowlightNode.Position = Vector2.Zero;
}
}
@@ -0,0 +1,59 @@
using System.Numerics;
using Dalamud.Interface.Textures.TextureWraps;
namespace KamiToolKit.Nodes;
public class ImGuiIconButtonNode : ButtonBase {
public readonly NineGridNode BackgroundNode;
public readonly ImGuiImageNode ImageNode;
public ImGuiIconButtonNode() {
BackgroundNode = new SimpleNineGridNode {
TexturePath = "ui/uld/BgParts.tex",
TextureSize = new Vector2(32.0f, 32.0f),
TextureCoordinates = new Vector2(33.0f, 65.0f),
TopOffset = 8.0f,
LeftOffset = 8.0f,
RightOffset = 8.0f,
BottomOffset = 8.0f,
};
BackgroundNode.AttachNode(this);
ImageNode = new ImGuiImageNode {
FitTexture = true,
};
ImageNode.AttachNode(this);
LoadTimelines();
InitializeComponentEvents();
}
public bool ShowBackground {
get => BackgroundNode.IsVisible;
set => BackgroundNode.IsVisible = value;
}
public string TexturePath {
get => ImageNode.TexturePath;
set => ImageNode.TexturePath = value;
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
ImageNode.Size = Size - new Vector2(16.0f, 16.0f);
ImageNode.Position = BackgroundNode.Position + new Vector2(BackgroundNode.LeftOffset, BackgroundNode.TopOffset);
BackgroundNode.Size = Size;
}
public void LoadTexture(IDalamudTextureWrap texture)
=> ImageNode.LoadTexture(texture);
public void LoadTextureFromFile(string path)
=> ImageNode.LoadTextureFromFile(path);
private void LoadTimelines()
=> LoadThreePartTimelines(this, BackgroundNode, ImageNode, new Vector2(8.0f, 8.0f));
}
@@ -0,0 +1,192 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Timelines;
using Lumina.Text.ReadOnly;
namespace KamiToolKit.Nodes;
public unsafe class ListButtonNode : ButtonBase {
public readonly NineGridNode HoverBackgroundNode;
public readonly TextNode LabelNode;
public readonly NineGridNode SelectedBackgroundNode;
public ListButtonNode() {
HoverBackgroundNode = new SimpleNineGridNode {
TexturePath = "ui/uld/ListItemA.tex",
TextureCoordinates = new Vector2(0.0f, 22.0f),
TextureSize = new Vector2(64.0f, 22.0f),
LeftOffset = 16,
RightOffset = 1,
};
HoverBackgroundNode.AttachNode(this);
SelectedBackgroundNode = new SimpleNineGridNode {
TexturePath = "ui/uld/ListItemA.tex",
TextureCoordinates = new Vector2(0.0f, 0.0f),
TextureSize = new Vector2(64.0f, 22.0f),
LeftOffset = 16,
RightOffset = 1,
};
SelectedBackgroundNode.AttachNode(this);
LabelNode = new TextNode {
Position = new Vector2(10.0f, 1.0f),
TextColor = ColorHelper.GetColor(8),
TextOutlineColor = ColorHelper.GetColor(7),
FontType = FontType.Axis,
FontSize = 14,
AlignmentType = AlignmentType.Left,
String = "Label Not Set",
};
LabelNode.AttachNode(this);
LoadTimelines();
InitializeComponentEvents();
}
public bool Selected {
get => Component->IsChecked;
set => Component->SetChecked(value);
}
public ReadOnlySeString String {
get => LabelNode.String;
set => LabelNode.String = value;
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
HoverBackgroundNode.Size = Size;
SelectedBackgroundNode.Size = Size;
LabelNode.Size = new Vector2(Width - 10.0f, Height - 1.0f);
}
private void LoadTimelines() {
AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 120)
.AddLabel(1, 1, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(9, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(10, 2, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(19, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(20, 3, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(29, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(30, 7, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(39, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(40, 6, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(49, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(50, 4, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(59, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(60, 8, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(69, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(70, 9, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(79, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(80, 10, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(89, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(90, 14, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(99, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(100, 13, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(109, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(110, 11, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(120, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.EndFrameSet()
.Build()
);
HoverBackgroundNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 9)
.AddFrame(1, alpha: 0)
.EndFrameSet()
.BeginFrameSet(10, 19)
.AddFrame(10, alpha: 0)
.AddFrame(13, alpha: 255)
.EndFrameSet()
.BeginFrameSet(20, 29)
.AddFrame(20, alpha: 255)
.EndFrameSet()
.BeginFrameSet(40, 49)
.AddFrame(40, alpha: 255)
.EndFrameSet()
.BeginFrameSet(50, 59)
.AddFrame(50, alpha: 255)
.AddFrame(52, alpha: 0)
.EndFrameSet()
.Build()
);
SelectedBackgroundNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(60, 69)
.AddFrame(60, alpha: 214)
.AddFrame(60, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(70, 79)
.AddFrame(70, alpha: 214)
.AddFrame(72, alpha: 255)
.AddFrame(70, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(72, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(80, 89)
.AddFrame(80, alpha: 255)
.AddFrame(80, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(90, 99)
.AddFrame(90, alpha: 178)
.AddFrame(90, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(50, 50, 50))
.EndFrameSet()
.BeginFrameSet(100, 109)
.AddFrame(100, alpha: 255)
.AddFrame(100, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(110, 120)
.AddFrame(110, alpha: 255)
.AddFrame(112, alpha: 214)
.AddFrame(110, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(112, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.Build()
);
LabelNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 9)
.AddFrame(1, alpha: 255)
.EndFrameSet()
.BeginFrameSet(10, 19)
.AddFrame(10, alpha: 255)
.EndFrameSet()
.BeginFrameSet(20, 29)
.AddFrame(20, alpha: 255)
.EndFrameSet()
.BeginFrameSet(30, 39)
.AddFrame(30, alpha: 127)
.EndFrameSet()
.BeginFrameSet(40, 49)
.AddFrame(40, alpha: 255)
.EndFrameSet()
.BeginFrameSet(50, 59)
.AddFrame(50, alpha: 255)
.EndFrameSet()
.BeginFrameSet(60, 69)
.AddFrame(60, alpha: 255)
.EndFrameSet()
.BeginFrameSet(70, 79)
.AddFrame(70, alpha: 255)
.EndFrameSet()
.BeginFrameSet(80, 89)
.AddFrame(80, alpha: 255)
.EndFrameSet()
.BeginFrameSet(90, 99)
.AddFrame(90, alpha: 127)
.EndFrameSet()
.BeginFrameSet(100, 109)
.AddFrame(100, alpha: 255)
.EndFrameSet()
.BeginFrameSet(110, 120)
.AddFrame(110, alpha: 255)
.EndFrameSet()
.Build()
);
}
}
@@ -0,0 +1,40 @@
using System.Linq;
using KamiToolKit.Classes;
using Lumina.Excel;
namespace KamiToolKit.Nodes;
public class LuminaButtonListNode<T> : ButtonListNode<T> where T : struct, IExcelRow<T> {
public delegate string GetLabel(T excelRow);
public delegate bool ShouldShow(T excelRow);
public GetLabel? LabelFunction {
get;
set {
field = value;
ResolveOptions();
}
}
public ShouldShow? FilterFunction {
get;
set {
field = value;
ResolveOptions();
}
}
private void ResolveOptions() {
if (LabelFunction is null) return;
if (FilterFunction is null) return;
Options = DalamudInterface.Instance.DataManager.GetExcelSheet<T>()
.Where(row => FilterFunction(row))
.ToList();
}
protected override string GetLabelForOption(T option)
=> LabelFunction?.Invoke(option) ?? "ERROR: Label Function Not Found";
}
@@ -0,0 +1,47 @@
using System;
using Lumina.Excel;
namespace KamiToolKit.Nodes;
public class LuminaDropDownNode<T> : DropDownNode<LuminaButtonListNode<T>, T> where T : struct, IExcelRow<T> {
public LuminaDropDownNode() {
OptionListNode.OnOptionSelected += OptionSelectedHandler;
}
public Action<T>? OnOptionSelected { get; set; }
public LuminaButtonListNode<T>.GetLabel? LabelFunction {
get => OptionListNode.LabelFunction;
set {
OptionListNode.LabelFunction = value;
ResolveOptions();
}
}
public LuminaButtonListNode<T>.ShouldShow? FilterFunction {
get => OptionListNode.FilterFunction;
set {
OptionListNode.FilterFunction = value;
ResolveOptions();
}
}
private void OptionSelectedHandler(T option) {
OnOptionSelected?.Invoke(option);
UpdateLabel(option);
Toggle(false);
}
private void ResolveOptions() {
if (LabelFunction is null) return;
if (FilterFunction is null) return;
OptionListNode.SelectDefaultOption();
LabelNode.String = LabelFunction.Invoke(OptionListNode.SelectedOption);
}
protected override void UpdateLabel(T option) {
LabelNode.String = LabelFunction?.Invoke(option) ?? "ERROR: Label Function Not Set";
}
}
@@ -0,0 +1,95 @@
using System.Drawing;
using System.Numerics;
using Dalamud.Interface;
namespace KamiToolKit.Nodes;
public unsafe class ProgressBarCastNode : ProgressNode {
public readonly NineGridNode BackgroundImageNode;
public readonly NineGridNode ProgressNode;
public readonly NineGridNode BorderImageNode;
public ProgressBarCastNode() {
BackgroundImageNode = new SimpleNineGridNode {
TexturePath = "ui/uld/Parameter_Gauge.tex",
TextureSize = new Vector2(160.0f, 20.0f),
TextureCoordinates = new Vector2(0.0f, 100.0f),
LeftOffset = 20,
RightOffset = 20,
};
BackgroundImageNode.AttachNode(this);
ProgressNode = new SimpleNineGridNode {
TexturePath = "ui/uld/Parameter_Gauge.tex",
TextureSize = new Vector2(160.0f, 20.0f),
TextureCoordinates = new Vector2(0.0f, 40.0f),
MultiplyColor = new Vector3(90.0f, 75.0f, 75.0f) / 255.0f,
AddColor = KnownColor.Yellow.Vector().AsVector3Color() / 255.0f,
LeftOffset = 10,
RightOffset = 10,
};
ProgressNode.AttachNode(this);
BorderImageNode = new SimpleNineGridNode {
TexturePath = "ui/uld/Parameter_Gauge.tex",
TextureSize = new Vector2(160.0f, 20.0f),
TextureCoordinates = new Vector2(0.0f, 0.0f),
LeftOffset = 20,
RightOffset = 20,
};
BorderImageNode.AttachNode(this);
}
public override float Progress {
get => ProgressNode.Width / Width;
set => ProgressNode.Width = Width * value;
}
public override Vector4 BackgroundColor {
get => new(BackgroundImageNode.AddColor.X, BackgroundImageNode.AddColor.Y, BackgroundImageNode.AddColor.Z, BackgroundImageNode.ResNode->Color.A / 255.0f);
set {
BackgroundImageNode.ResNode->Color = new Vector4(1.0f, 1.0f, 1.0f, value.W).ToByteColor();
BackgroundImageNode.AddColor = value.AsVector3Color();
}
}
public Vector4 BorderColor {
get => new(BorderImageNode.AddColor.X, BorderImageNode.AddColor.Y, BorderImageNode.AddColor.Z, BorderImageNode.ResNode->Color.A / 255.0f);
set {
BorderImageNode.ResNode->Color = new Vector4(1.0f, 1.0f, 1.0f, value.W).ToByteColor();
BorderImageNode.AddColor = value.AsVector3Color();
}
}
public override Vector4 BarColor {
get => new(ProgressNode.AddColor.X, ProgressNode.AddColor.Y, ProgressNode.AddColor.Z, ProgressNode.ResNode->Color.A / 255.0f);
set {
ProgressNode.ResNode->Color = new Vector4(1.0f, 1.0f, 1.0f, value.W).ToByteColor();
ProgressNode.AddColor = value.AsVector3Color();
}
}
public override Vector3 MultiplyColor {
get => base.MultiplyColor;
set {
base.MultiplyColor = value;
BackgroundImageNode.MultiplyColor = value;
ProgressNode.MultiplyColor = value;
BorderImageNode.MultiplyColor = value;
}
}
public bool BorderVisible {
get => BorderImageNode.IsVisible;
set => BorderImageNode.IsVisible = value;
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
BackgroundImageNode.Size = Size;
ProgressNode.Size = Size;
BorderImageNode.Size = Size;
}
}
@@ -0,0 +1,57 @@
using System.Numerics;
namespace KamiToolKit.Nodes;
public unsafe class ProgressBarEnemyCastNode : ProgressNode {
public readonly NineGridNode BackgroundImageNode;
public readonly NineGridNode ProgressNode;
public ProgressBarEnemyCastNode() {
BackgroundImageNode = new SimpleNineGridNode {
TexturePath = "ui/uld/PartyList_GaugeCast.tex",
TextureSize = new Vector2(204.0f, 20.0f),
TextureCoordinates = new Vector2(0.0f, 12.0f),
LeftOffset = 20,
RightOffset = 20,
};
BackgroundImageNode.AttachNode(this);
ProgressNode = new SimpleNineGridNode {
TexturePath = "ui/uld/PartyList_GaugeCast.tex",
TextureSize = new Vector2(188.0f, 7.0f),
TextureCoordinates = new Vector2(8.0f, 3.0f),
LeftOffset = 10,
RightOffset = 10,
};
ProgressNode.AttachNode(this);
}
public override float Progress {
get => ProgressNode.Width / Width;
set => ProgressNode.Width = Width * value;
}
public override Vector4 BackgroundColor {
get => new(BackgroundImageNode.AddColor.X, BackgroundImageNode.AddColor.Y, BackgroundImageNode.AddColor.Z, BackgroundImageNode.ResNode->Color.A / 255.0f);
set {
BackgroundImageNode.ResNode->Color = new Vector4(1.0f, 1.0f, 1.0f, value.W).ToByteColor();
BackgroundImageNode.AddColor = value.AsVector3Color();
}
}
public override Vector4 BarColor {
get => new(ProgressNode.AddColor.X, ProgressNode.AddColor.Y, ProgressNode.AddColor.Z, ProgressNode.ResNode->Color.A / 255.0f);
set {
ProgressNode.ResNode->Color = new Vector4(1.0f, 1.0f, 1.0f, value.W).ToByteColor();
ProgressNode.AddColor = value.AsVector3Color();
}
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
BackgroundImageNode.Size = Size;
ProgressNode.Size = Size;
}
}
@@ -0,0 +1,51 @@
using System.Numerics;
namespace KamiToolKit.Nodes;
public class ProgressBarNode : ProgressNode {
public readonly NineGridNode BackgroundNode;
public readonly NineGridNode ForegroundNode;
public ProgressBarNode() {
BackgroundNode = new SimpleNineGridNode {
TexturePath = "ui/uld/ToDoList.tex",
TextureCoordinates = new Vector2(108.0f, 8.0f),
TextureSize = new Vector2(44.0f, 12.0f),
LeftOffset = 6,
RightOffset = 6,
};
BackgroundNode.AttachNode(this);
ForegroundNode = new SimpleNineGridNode {
TexturePath = "ui/uld/ToDoList.tex",
TextureCoordinates = new Vector2(112.0f, 0.0f),
TextureSize = new Vector2(40.0f, 8.0f),
LeftOffset = 4,
RightOffset = 4,
};
ForegroundNode.AttachNode(this);
}
public override Vector4 BackgroundColor {
get => BackgroundNode.Color;
set => BackgroundNode.Color = value;
}
public override Vector4 BarColor {
get => ForegroundNode.Color;
set => ForegroundNode.Color = value;
}
public override float Progress {
get => ForegroundNode.Width / Width;
set => ForegroundNode.Width = Width * value;
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
BackgroundNode.Size = Size;
ForegroundNode.Size = Size;
}
}
@@ -0,0 +1,9 @@
using System.Numerics;
namespace KamiToolKit.Nodes;
public abstract class ProgressNode : SimpleComponentNode {
public abstract float Progress { get; set; }
public abstract Vector4 BarColor { get; set; }
public abstract Vector4 BackgroundColor { get; set; }
}
@@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Timelines;
using Lumina.Text.ReadOnly;
namespace KamiToolKit.Nodes;
public class RadioButtonGroupNode : SimpleComponentNode {
private readonly List<RadioButtonNode> radioButtons = [];
public RadioButtonGroupNode() {
BuildTimelines();
}
public ReadOnlySeString? SelectedOption {
get => radioButtons.FirstOrDefault(button => button.IsSelected)?.String;
set {
if (value == null)
return;
foreach (var radioButton in radioButtons) {
radioButton.IsChecked = radioButton.String == value;
radioButton.IsSelected = radioButton.String == value;
}
RecalculateLayout();
}
}
public float VerticalPadding { get; set; } = 2.0f;
public void AddButton(ReadOnlySeString label, Action callback) {
var newRadioButton = new RadioButtonNode {
Height = 16.0f,
String = label,
Callback = callback,
};
newRadioButton.AddEvent(AtkEventType.ButtonClick, () => ClickHandler(newRadioButton));
radioButtons.Add(newRadioButton);
newRadioButton.AttachNode(this);
if (radioButtons.Count is 1) {
newRadioButton.IsChecked = true;
newRadioButton.IsSelected = true;
}
RecalculateLayout();
}
public void RemoveButton(ReadOnlySeString label) {
var button = radioButtons.FirstOrDefault(button => button.String == label);
if (button is null) return;
button.Dispose();
radioButtons.Remove(button);
RecalculateLayout();
}
public void Clear() {
foreach (var node in radioButtons) {
node.Dispose();
}
radioButtons.Clear();
}
private void RecalculateLayout() {
var yPosition = 0.0f;
foreach (var index in Enumerable.Range(0, radioButtons.Count)) {
var button = radioButtons[index];
button.Y = yPosition;
yPosition += button.Height + VerticalPadding;
}
Height = yPosition;
}
private void ClickHandler(RadioButtonNode selectedButton) {
foreach (var radioButton in radioButtons) {
radioButton.IsChecked = false;
radioButton.IsSelected = false;
}
selectedButton.IsChecked = true;
selectedButton.IsSelected = true;
}
private void BuildTimelines() {
AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 19)
.AddLabel(1, 101, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(10, 102, AtkTimelineJumpBehavior.PlayOnce, 0)
.EndFrameSet()
.Build()
);
}
}
@@ -0,0 +1,309 @@
using System;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Enums;
using KamiToolKit.Timelines;
using Lumina.Text.ReadOnly;
namespace KamiToolKit.Nodes;
internal unsafe class RadioButtonNode : ComponentNode<AtkComponentRadioButton, AtkUldComponentDataRadioButton> {
public readonly TextNode LabelNode;
public readonly ImageNode SelectedImageNode;
public readonly ImageNode UnselectedImageNode;
public RadioButtonNode() {
SetInternalComponentType(ComponentType.RadioButton);
UnselectedImageNode = new SimpleImageNode {
NodeId = 4,
TexturePath = "ui/uld/RadioButtonA.tex",
TextureCoordinates = new Vector2(0.0f, 0.0f),
TextureSize = new Vector2(16.0f, 16.0f),
Size = new Vector2(16.0f, 16.0f),
WrapMode = WrapMode.Tile,
};
UnselectedImageNode.AttachNode(this);
SelectedImageNode = new SimpleImageNode {
NodeId = 3,
TexturePath = "ui/uld/RadioButtonA.tex",
TextureCoordinates = new Vector2(16.0f, 0.0f),
TextureSize = new Vector2(16.0f, 16.0f),
Size = new Vector2(16.0f, 16.0f),
IsVisible = false,
WrapMode = WrapMode.Tile,
};
SelectedImageNode.AttachNode(this);
LabelNode = new TextNode {
NodeId = 2,
Position = new Vector2(20.0f, 0.0f),
Size = new Vector2(98.0f, 16.0f),
FontSize = 14,
TextColor = ColorHelper.GetColor(8),
TextOutlineColor = ColorHelper.GetColor(7),
AlignmentType = AlignmentType.Left,
};
LabelNode.AttachNode(this);
BuildTimelines();
Data->Nodes[0] = LabelNode.NodeId;
Data->Nodes[1] = UnselectedImageNode.NodeId;
Data->Nodes[2] = 0;
Data->Nodes[3] = 0;
AddEvent(AtkEventType.ButtonClick, ClickHandler);
InitializeComponentEvents();
}
public Action? Callback { get; set; }
public ReadOnlySeString String {
get => LabelNode.String;
set {
LabelNode.String = value;
Width = LabelNode.Width + LabelNode.Position.X;
}
}
public bool IsChecked {
get => Component->IsChecked;
set => Component->SetChecked(value);
}
public bool IsSelected {
get => Component->IsSelected;
set {
Component->IsSelected = value;
SelectedImageNode.IsVisible = value;
}
}
private void ClickHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
Callback?.Invoke();
}
private void BuildTimelines() {
AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 9)
.AddFrame(1, new Vector2(24, 62))
.EndFrameSet()
.BeginFrameSet(10, 19)
.AddFrame(10, new Vector2(24, 44))
.EndFrameSet()
.Build()
);
CollisionNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 159)
.AddEmptyFrame(1)
.EndFrameSet()
.Build()
);
UnselectedImageNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 9)
.AddFrame(1, alpha: 255)
.AddFrame(1, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(10, 19)
.AddFrame(10, alpha: 255)
.AddFrame(12, alpha: 255)
.AddFrame(10, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(12, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(20, 29)
.AddFrame(20, alpha: 255)
.AddFrame(20, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(30, 39)
.AddFrame(30, alpha: 102)
.AddFrame(30, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(80, 80, 80))
.EndFrameSet()
.BeginFrameSet(40, 49)
.AddFrame(40, alpha: 255)
.AddFrame(40, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(50, 59)
.AddFrame(50, alpha: 255)
.AddFrame(52, alpha: 255)
.AddFrame(50, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(52, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(60, 69)
.AddFrame(60, alpha: 255)
.AddFrame(60, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(70, 79)
.AddFrame(70, alpha: 255)
.AddFrame(72, alpha: 255)
.AddFrame(70, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(72, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(80, 89)
.AddFrame(80, alpha: 255)
.AddFrame(80, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(90, 99)
.AddFrame(90, alpha: 102)
.AddFrame(90, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(80, 80, 80))
.EndFrameSet()
.BeginFrameSet(100, 109)
.AddFrame(100, alpha: 255)
.AddFrame(100, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(110, 119)
.AddFrame(110, alpha: 255)
.AddFrame(112, alpha: 255)
.AddFrame(110, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(112, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(120, 129)
.AddFrame(120, alpha: 255)
.AddFrame(120, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(130, 139)
.AddFrame(130, alpha: 255)
.AddFrame(130, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(140, 149)
.AddFrame(140, alpha: 255)
.AddFrame(140, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(150, 159)
.AddFrame(150, alpha: 255)
.AddFrame(150, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.Build()
);
SelectedImageNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(60, 69)
.AddFrame(60, alpha: 255)
.AddFrame(60, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(70, 79)
.AddFrame(70, alpha: 255)
.AddFrame(72, alpha: 255)
.AddFrame(70, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(72, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(80, 89)
.AddFrame(80, alpha: 255)
.AddFrame(80, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(90, 99)
.AddFrame(90, alpha: 102)
.AddFrame(90, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(80, 80, 80))
.EndFrameSet()
.BeginFrameSet(100, 109)
.AddFrame(100, alpha: 255)
.AddFrame(100, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(110, 119)
.AddFrame(110, alpha: 255)
.AddFrame(112, alpha: 255)
.AddFrame(110, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(112, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(120, 129)
.AddFrame(120, alpha: 0)
.AddFrame(122, alpha: 255)
.AddFrame(120, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(122, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(130, 139)
.AddFrame(130, alpha: 255)
.AddFrame(132, alpha: 0)
.AddFrame(130, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(132, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(140, 149)
.AddFrame(140, alpha: 0)
.AddFrame(142, alpha: 255)
.AddFrame(140, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(142, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(150, 159)
.AddFrame(150, alpha: 255)
.AddFrame(152, alpha: 0)
.AddFrame(150, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(152, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.Build()
);
LabelNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 9)
.AddFrame(1, alpha: 255)
.AddFrame(1, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(10, 19)
.AddFrame(10, alpha: 255)
.AddFrame(10, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(20, 29)
.AddFrame(20, alpha: 255)
.AddFrame(20, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(30, 39)
.AddFrame(30, alpha: 102)
.AddFrame(30, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(80, 80, 80))
.EndFrameSet()
.BeginFrameSet(40, 49)
.AddFrame(40, alpha: 255)
.AddFrame(40, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(50, 59)
.AddFrame(50, alpha: 255)
.AddFrame(50, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(60, 69)
.AddFrame(60, alpha: 255)
.AddFrame(60, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(70, 79)
.AddFrame(70, alpha: 255)
.AddFrame(70, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(80, 89)
.AddFrame(80, alpha: 255)
.AddFrame(80, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(90, 99)
.AddFrame(90, alpha: 102)
.AddFrame(90, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(80, 80, 80))
.EndFrameSet()
.BeginFrameSet(100, 109)
.AddFrame(100, alpha: 255)
.AddFrame(100, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(110, 119)
.AddFrame(110, alpha: 255)
.AddFrame(110, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(120, 129)
.AddFrame(120, alpha: 255)
.AddFrame(120, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(130, 139)
.AddFrame(130, alpha: 255)
.AddFrame(130, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(140, 149)
.AddFrame(140, alpha: 255)
.AddFrame(140, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(150, 159)
.AddFrame(150, alpha: 255)
.AddFrame(150, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.Build()
);
}
}
@@ -0,0 +1,58 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Enums;
namespace KamiToolKit.Nodes;
// Not intended for public use, this is specialized for KamiToolKit.NodeBase.Resize
internal class ResizeButtonNode : SimpleComponentNode {
public readonly ImageNode SelectedImageNode;
public readonly ImageNode UnselectedImageNode;
public ResizeButtonNode(ResizeDirection direction) {
UnselectedImageNode = new SimpleImageNode {
TexturePath = "ui/uld/ChatLog.tex",
TextureCoordinates = new Vector2(32.0f, 34.0f),
TextureSize = new Vector2(18.0f, 18.0f),
Size = new Vector2(16.0f, 16.0f),
Origin = new Vector2(8.0f, 8.0f),
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents,
WrapMode = WrapMode.Tile,
ImageNodeFlags = direction is ResizeDirection.BottomRight ? ImageNodeFlags.FlipV : ImageNodeFlags.FlipH | ImageNodeFlags.FlipV,
};
UnselectedImageNode.AttachNode(this);
SelectedImageNode = new SimpleImageNode {
TexturePath = "ui/uld/ChatLog.tex",
TextureCoordinates = new Vector2(4.0f, 34.0f),
TextureSize = new Vector2(18.0f, 18.0f),
Size = new Vector2(16.0f, 16.0f),
Origin = new Vector2(8.0f, 8.0f),
NodeFlags = NodeFlags.Enabled | NodeFlags.EmitsEvents,
WrapMode = WrapMode.Tile,
ImageNodeFlags = direction is ResizeDirection.BottomRight ? ImageNodeFlags.FlipV : ImageNodeFlags.FlipH | ImageNodeFlags.FlipV,
};
SelectedImageNode.AttachNode(this);
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
UnselectedImageNode.Size = Size - new Vector2(4.0f, 4.0f);
UnselectedImageNode.Position = new Vector2(2.0f, 2.0f);
SelectedImageNode.Size = Size - new Vector2(4.0f, 4.0f);
SelectedImageNode.Position = new Vector2(2.0f, 2.0f);
}
public bool IsHovered {
get;
set {
field = value;
UnselectedImageNode.IsVisible = !value;
SelectedImageNode.IsVisible = value;
}
}
}
@@ -0,0 +1,16 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace KamiToolKit.Nodes;
public unsafe class ScrollBarBackgroundButtonNode : ComponentNode<AtkComponentButton, AtkUldComponentDataButton> {
public ScrollBarBackgroundButtonNode() {
SetInternalComponentType(ComponentType.Button);
Component->ButtonBGNode = CollisionNode;
Data->Nodes[0] = 0;
Data->Nodes[1] = CollisionNode.NodeId;
InitializeComponentEvents();
}
}
@@ -0,0 +1,88 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Timelines;
namespace KamiToolKit.Nodes;
public unsafe class ScrollBarForegroundButtonNode : ComponentNode<AtkComponentButton, AtkUldComponentDataButton> {
public readonly NineGridNode ButtonTexture;
public ScrollBarForegroundButtonNode() {
SetInternalComponentType(ComponentType.Button);
ButtonTexture = new SimpleNineGridNode {
TexturePath = "ui/uld/ScrollBarA.tex",
TextureCoordinates = new Vector2(0.0f, 0.0f),
TextureSize = new Vector2(8.0f, 16.0f),
TopOffset = 4,
BottomOffset = 4,
};
ButtonTexture.AttachNode(this);
Data->Nodes[0] = 0;
Data->Nodes[1] = ButtonTexture.NodeId;
BuildTimelines();
InitializeComponentEvents();
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
ButtonTexture.Size = Size;
}
private void BuildTimelines() {
AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 59)
.AddLabel(1, 1, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(9, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(10, 2, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(19, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(20, 3, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(29, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(30, 7, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(39, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(40, 6, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(49, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(50, 4, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(59, 0, AtkTimelineJumpBehavior.PlayOnce, 0)
.EndFrameSet()
.Build()
);
ButtonTexture.AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 9)
.AddFrame(1, alpha: 255)
.AddFrame(1, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(10, 19)
.AddFrame(10, alpha: 255)
.AddFrame(12, alpha: 255)
.AddFrame(10, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(12, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(20, 29)
.AddFrame(20, alpha: 255)
.AddFrame(20, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(30, 39)
.AddFrame(30, alpha: 178)
.AddFrame(30, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(50, 50, 50))
.EndFrameSet()
.BeginFrameSet(40, 49)
.AddFrame(40, alpha: 255)
.AddFrame(40, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(50, 59)
.AddFrame(50, alpha: 255)
.AddFrame(52, alpha: 255)
.AddFrame(50, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(52, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.Build()
);
}
}
@@ -0,0 +1,128 @@
using System;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace KamiToolKit.Nodes;
public unsafe class ScrollBarNode : ComponentNode<AtkComponentScrollBar, AtkUldComponentDataScrollBar> {
public readonly ScrollBarBackgroundButtonNode BackgroundButtonNode;
public readonly ScrollBarForegroundButtonNode ForegroundButtonNode;
public ScrollBarNode() {
SetInternalComponentType(ComponentType.ScrollBar);
BackgroundButtonNode = new ScrollBarBackgroundButtonNode {
Size = new Vector2(8.0f, 306.0f),
};
BackgroundButtonNode.AttachNode(this);
ForegroundButtonNode = new ScrollBarForegroundButtonNode {
Size = new Vector2(8.0f, 306.0f),
};
ForegroundButtonNode.AttachNode(this);
Data->Nodes[0] = ForegroundButtonNode.NodeId;
Data->Nodes[1] = 0; // Arrow Up Button
Data->Nodes[2] = 0; // Arrow Down Button
Data->Nodes[3] = BackgroundButtonNode.NodeId;
Data->Vertical = 1;
Data->Margin = 0;
InitializeComponentEvents();
Component->MouseDownScreenPos = 0;
Component->MouseWheelSpeed = 24;
AddEvent(AtkEventType.ValueUpdate, UpdateHandler);
}
public Action<int>? OnValueChanged { get; set; }
public NodeBase? ContentNode {
get;
set {
field = value;
if (value is not null) {
Component->ContentNode = value;
UpdateScrollParams();
}
}
}
public CollisionNode? ContentCollisionNode {
get;
set {
field = value;
Component->ContentCollisionNode = value is null ? null : value.Node;
UpdateScrollParams();
}
}
public int ScrollPosition {
get => Component->ScrollPosition;
set => Component->SetScrollPosition(value);
}
public int ScrollSpeed {
get => Component->MouseWheelSpeed;
set => Component->MouseWheelSpeed = (short)value;
}
public bool HideWhenDisabled { get; set; }
private void UpdateHandler() {
OnValueChanged?.Invoke(Component->PendingScrollPosition);
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
BackgroundButtonNode.Size = Size;
ForegroundButtonNode.Size = Size;
}
/// <summary>
/// Updates from attached Content and Collision nodes
/// </summary>
public void UpdateScrollParams() {
if (Component->ContentNode is null) return;
if (Component->ContentCollisionNode is null) return;
var content = Component->ContentNode;
var collision = Component->ContentCollisionNode;
UpdateScrollParams(collision->Height, content->Height);
}
public void UpdateScrollParams(int barHeight, int offScreenHeight) {
var distance = offScreenHeight - barHeight;
Component->ScrollbarLength = (short)barHeight;
Component->ScrollMaxPosition = Math.Max(distance, 0);
Component->ContentNodeOffScreenLength = Math.Max((short)distance, (short)0);
Component->EmptyLength = Math.Max(barHeight - (int)((float)barHeight / offScreenHeight * barHeight), 0);
ForegroundButtonNode.Height = barHeight - Component->EmptyLength;
if (Component->ScrollPosition > Component->ScrollMaxPosition) {
Component->SetScrollPosition(Component->ScrollMaxPosition);
}
if (Component->EmptyLength is 0) {
ForegroundButtonNode.Y = 0.0f;
ContentNode?.Y = 0;
}
var enabledState = Component->EmptyLength is not 0;
Component->SetEnabledState(enabledState);
if (HideWhenDisabled) {
BackgroundButtonNode.IsVisible = enabledState;
ForegroundButtonNode.IsVisible = enabledState;
}
}
}
@@ -0,0 +1,100 @@
using System.Linq;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace KamiToolKit.Nodes;
public unsafe class ScrollingAreaNode<T> : SimpleComponentNode where T : NodeBase, new() {
public readonly SimpleComponentNode ContentAreaClipNode;
public readonly T ContentAreaNode;
public readonly ScrollBarNode ScrollBarNode;
public readonly CollisionNode ScrollingCollisionNode;
public ScrollingAreaNode() {
ScrollingCollisionNode = new CollisionNode();
ScrollingCollisionNode.AttachNode(this);
ContentAreaClipNode = new SimpleComponentNode {
NodeFlags = NodeFlags.Clip | NodeFlags.EmitsEvents | NodeFlags.Visible,
};
ContentAreaClipNode.AttachNode(this);
ContentAreaNode = new T();
ContentAreaNode.AttachNode(ContentAreaClipNode);
ScrollBarNode = new ScrollBarNode {
ContentNode = ContentAreaNode,
ContentCollisionNode = ScrollingCollisionNode,
HideWhenDisabled = true,
};
ScrollBarNode.AttachNode(this);
ContentAreaClipNode.ResNode->AtkEventManager.RegisterEvent(
AtkEventType.MouseWheel,
5,
null,
ScrollingCollisionNode,
ScrollBarNode,
false);
ScrollingCollisionNode.ResNode->AtkEventManager.RegisterEvent(
AtkEventType.MouseWheel,
5,
null,
ScrollingCollisionNode,
ScrollBarNode,
false);
ContentAreaNode.ResNode->AtkEventManager.RegisterEvent(
AtkEventType.MouseWheel,
5,
null,
ScrollingCollisionNode,
ScrollBarNode,
false);
}
public virtual T ContentNode => ContentAreaNode;
public int ScrollPosition {
get => ScrollBarNode.ScrollPosition;
set => ScrollBarNode.ScrollPosition = value;
}
public int ScrollSpeed {
get => ScrollBarNode.ScrollSpeed;
set => ScrollBarNode.ScrollSpeed = value;
}
public required float ContentHeight {
get => ContentAreaNode.Height;
set {
ContentAreaNode.Height = value;
ScrollBarNode.UpdateScrollParams();
}
}
public bool AutoHideScrollBar {
get => ScrollBarNode.HideWhenDisabled;
set => ScrollBarNode.HideWhenDisabled = value;
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
ContentAreaNode.Width = Width - 16.0f;
ScrollingCollisionNode.Size = new Vector2(Width - 16.0f, Height);
ContentAreaClipNode.Size = new Vector2(Width - 16.0f, Height);
ScrollBarNode.Size = new Vector2(8.0f, Height);
ScrollBarNode.UpdateScrollParams();
ScrollBarNode.X = Width - 8.0f;
}
public void FitToContentHeight() {
if (ContentNode is LayoutListNode layoutNode) {
ContentHeight = layoutNode.Nodes.Sum(node => node.IsVisible ? node.Height + layoutNode.ItemSpacing : 0.0f) + layoutNode.FirstItemSpacing;
}
}
}
@@ -0,0 +1,95 @@
using System;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace KamiToolKit.Nodes;
public class SelectableNode : SimpleComponentNode {
private readonly NineGridNode hoveredBackgroundNode;
private readonly NineGridNode selectedBackgroundNode;
public SelectableNode() {
hoveredBackgroundNode = new SimpleNineGridNode {
TexturePath = "ui/uld/ListItemA.tex",
TextureCoordinates = new Vector2(0.0f, 22.0f),
TextureSize = new Vector2(64.0f, 22.0f),
TopOffset = 6,
BottomOffset = 6,
LeftOffset = 16,
RightOffset = 1,
IsVisible = false,
};
hoveredBackgroundNode.AttachNode(this);
selectedBackgroundNode = new SimpleNineGridNode {
TexturePath = "ui/uld/ListItemA.tex",
TextureCoordinates = new Vector2(0.0f, 0.0f),
TextureSize = new Vector2(64.0f, 22.0f),
TopOffset = 6,
BottomOffset = 6,
LeftOffset = 16,
RightOffset = 1,
IsVisible = false,
};
selectedBackgroundNode.AttachNode(this);
CollisionNode.AddEvent(AtkEventType.MouseOver, () => {
if (!IsSelected && EnableHighlight) {
IsHovered = true;
}
});
CollisionNode.AddEvent(AtkEventType.MouseDown, () => {
if (EnableSelection) {
IsSelected = true;
OnClick?.Invoke(this);
}
});
CollisionNode.AddEvent(AtkEventType.MouseOut, () => {
IsHovered = false;
});
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
hoveredBackgroundNode.Size = Size + new Vector2(6.0f, 6.0f);
hoveredBackgroundNode.Position = new Vector2(-3.0f, -3.0f);
selectedBackgroundNode.Size = Size + new Vector2(6.0f, 6.0f);
selectedBackgroundNode.Position = new Vector2(-3.0f, -3.0f);
}
public Action<SelectableNode>? OnClick {
get;
set {
field = value;
CollisionNode.ShowClickableCursor = value is not null && EnableSelection;
}
}
public bool EnableSelection {
get;
set {
field = value;
CollisionNode.ShowClickableCursor = value;
}
} = true;
public bool EnableHighlight { get; set; } = true;
public bool IsHovered {
get => hoveredBackgroundNode.IsVisible;
set => hoveredBackgroundNode.IsVisible = value;
}
public bool IsSelected {
get => selectedBackgroundNode.IsVisible;
set {
selectedBackgroundNode.IsVisible = value;
if (value) {
hoveredBackgroundNode.IsVisible = false;
}
}
}
}
@@ -0,0 +1,83 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Timelines;
namespace KamiToolKit.Nodes;
public unsafe class SliderBackgroundButtonNode : ComponentNode<AtkComponentButton, AtkUldComponentDataButton> {
public readonly NineGridNode BackgroundTexture;
public SliderBackgroundButtonNode() {
SetInternalComponentType(ComponentType.Button);
BackgroundTexture = new SimpleNineGridNode {
TexturePath = "ui/uld/SliderGaugeHorizontalA.tex",
TextureCoordinates = new Vector2(16.0f, 0.0f),
TextureSize = new Vector2(40.0f, 8.0f),
LeftOffset = 8,
RightOffset = 8,
};
BackgroundTexture.AttachNode(this);
Component->ButtonBGNode = BackgroundTexture;
Data->Nodes[0] = 0;
Data->Nodes[1] = BackgroundTexture.NodeId;
BuildTimelines();
InitializeComponentEvents();
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
BackgroundTexture.Size = new Vector2(Width, Height / 2.0f);
BackgroundTexture.Y = Height / 4.0f;
}
private void BuildTimelines() {
AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 20)
.AddFrame(1, alpha: 255)
.EndFrameSet()
.BeginFrameSet(21, 30)
.AddFrame(21, alpha: 127)
.EndFrameSet()
.Build()
);
BackgroundTexture.AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 9)
.AddFrame(1, alpha: 255)
.AddFrame(1, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(10, 19)
.AddFrame(10, alpha: 255)
.AddFrame(12, alpha: 255)
.AddFrame(10, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(12, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(20, 29)
.AddFrame(20, alpha: 255)
.AddFrame(20, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(30, 39)
.AddFrame(30, alpha: 178)
.AddFrame(30, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(50, 50, 50))
.EndFrameSet()
.BeginFrameSet(40, 49)
.AddFrame(40, alpha: 255)
.AddFrame(40, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(50, 59)
.AddFrame(50, alpha: 255)
.AddFrame(52, alpha: 255)
.AddFrame(50, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(52, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.Build()
);
}
}
@@ -0,0 +1,78 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Enums;
using KamiToolKit.Timelines;
namespace KamiToolKit.Nodes;
public class SliderForegroundButtonNode : ComponentNode<AtkComponentButton, AtkUldComponentDataButton> {
public readonly ImageNode HandleNode;
public SliderForegroundButtonNode() {
SetInternalComponentType(ComponentType.Button);
HandleNode = new SimpleImageNode {
TexturePath = "ui/uld/SliderGaugeHorizontalA.tex",
TextureCoordinates = new Vector2(1.0f, 1.0f),
TextureSize = new Vector2(14.0f, 15.0f),
Size = new Vector2(14.0f, 15.0f),
WrapMode = WrapMode.Stretch,
};
HandleNode.AttachNode(this);
BuildTimelines();
InitializeComponentEvents();
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
HandleNode.Size = Size;
}
private void BuildTimelines() {
AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 20)
.AddFrame(1, alpha: 255)
.EndFrameSet()
.BeginFrameSet(21, 30)
.AddFrame(21, alpha: 178)
.EndFrameSet()
.Build()
);
HandleNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 9)
.AddFrame(1, alpha: 255)
.AddFrame(1, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(10, 19)
.AddFrame(10, alpha: 255)
.AddFrame(12, alpha: 255)
.AddFrame(10, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(12, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(20, 29)
.AddFrame(20, alpha: 255)
.AddFrame(20, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(30, 39)
.AddFrame(30, alpha: 255)
.AddFrame(30, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(70, 70, 70))
.EndFrameSet()
.BeginFrameSet(40, 49)
.AddFrame(40, alpha: 255)
.AddFrame(40, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(50, 59)
.AddFrame(50, alpha: 255)
.AddFrame(52, alpha: 255)
.AddFrame(50, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(52, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.Build()
);
}
}
+181
View File
@@ -0,0 +1,181 @@
using System;
using System.Globalization;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Timelines;
namespace KamiToolKit.Nodes;
public unsafe class SliderNode : ComponentNode<AtkComponentSlider, AtkUldComponentDataSlider> {
public readonly NineGridNode ProgressTextureNode;
public readonly SliderBackgroundButtonNode SliderBackgroundButtonNode;
public readonly SliderForegroundButtonNode SliderForegroundButtonNode;
public readonly TextNode ValueNode;
public readonly TextNode FloatValueNode;
public SliderNode() {
SetInternalComponentType(ComponentType.Slider);
SliderBackgroundButtonNode = new SliderBackgroundButtonNode();
SliderBackgroundButtonNode.AttachNode(this);
ProgressTextureNode = new SimpleNineGridNode {
TexturePath = "ui/uld/SliderGaugeHorizontalA.tex",
TextureCoordinates = new Vector2(16.0f, 8.0f),
TextureSize = new Vector2(40.0f, 7.0f),
Height = 7.0f,
Y = 4.0f,
LeftOffset = 8,
RightOffset = 8,
};
ProgressTextureNode.AttachNode(this);
SliderForegroundButtonNode = new SliderForegroundButtonNode {
Size = new Vector2(16.0f, 16.0f),
};
SliderForegroundButtonNode.AttachNode(this);
ValueNode = new TextNode {
Size = new Vector2(24.0f, 16.0f),
FontType = FontType.Axis,
FontSize = 12,
AlignmentType = AlignmentType.TopLeft,
TextFlags = TextFlags.AutoAdjustNodeSize,
};
ValueNode.AttachNode(this);
FloatValueNode = new TextNode {
Size = new Vector2(24.0f, 16.0f),
IsVisible = false,
FontType = FontType.Axis,
FontSize = 12,
AlignmentType = AlignmentType.TopLeft,
TextFlags = TextFlags.AutoAdjustNodeSize,
};
FloatValueNode.AttachNode(this);
Data->Step = 1;
Data->Min = 0;
Data->Max = 100;
Data->OfffsetL = 4;
Data->OffsetR = 50;
Data->Nodes[0] = ProgressTextureNode.NodeId;
Data->Nodes[1] = SliderForegroundButtonNode.NodeId;
Data->Nodes[2] = ValueNode.NodeId;
Data->Nodes[3] = SliderBackgroundButtonNode.NodeId;
BuildTimelines();
InitializeComponentEvents();
Component->SliderSize = 220;
Component->OffsetR = 50;
Component->OffsetL = 4;
AddEvent(AtkEventType.SliderValueUpdate, ValueChangedHandler);
}
public Action<int>? OnValueChanged { get; set; }
public required Range Range {
get => Data->Min .. Data->Max;
set {
Component->SetMaxValue(value.End.Value);
Component->SetMinValue(value.Start.Value);
Value = Math.Clamp(Value, value.Start.Value, value.End.Value);
}
}
public int Step {
get => Component->Steps;
set => Component->Steps = value;
}
public int Value {
get => Component->Value;
set {
Component->SetValue(value);
UpdateFormattedText();
}
}
public int DecimalPlaces {
get;
set {
field = value;
UpdateFormattedText();
}
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
SliderBackgroundButtonNode.Size = new Vector2(Width - 18.0f - 25.0f, Height / 2.0f);
SliderBackgroundButtonNode.Position = new Vector2(0.0f, 4.0f);
ProgressTextureNode.Size = new Vector2(0.0f, Height / 2.0f - 1.0f);
ProgressTextureNode.Position = new Vector2(0.0f, 4.0f);
SliderForegroundButtonNode.Size = new Vector2(Height - 4.0f, Height - 4.0f);
SliderForegroundButtonNode.Position = new Vector2(0.0f, 0.0f);
ValueNode.Size = new Vector2(0.0f, Height);
ValueNode.Position = new Vector2(Width - 18.0f - 20.0f, 0.0f);
FloatValueNode.Size = new Vector2(0.0f, Height);
FloatValueNode.Position = new Vector2(Width - 18.0f - 20.0f, 0.0f);
Component->SliderSize = (short)Width;
}
private void ValueChangedHandler() {
OnValueChanged?.Invoke(Value);
UpdateFormattedText();
}
private void UpdateFormattedText() {
if (DecimalPlaces is not 0) {
var formatInfo = new NumberFormatInfo {
NumberDecimalDigits = DecimalPlaces,
};
FloatValueNode.IsVisible = true;
FloatValueNode.String = string.Format(formatInfo, "{0:F}", Value / MathF.Pow(10, DecimalPlaces));
ValueNode.FontSize = 0;
}
}
private void BuildTimelines() {
AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 30)
.AddLabel(1, 17, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(11, 18, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(21, 7, AtkTimelineJumpBehavior.PlayOnce, 0)
.EndFrameSet()
.Build()
);
ProgressTextureNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 20)
.AddFrame(1, alpha: 255)
.EndFrameSet()
.BeginFrameSet(21, 30)
.AddFrame(21, alpha: 127)
.EndFrameSet()
.Build()
);
ValueNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 20)
.AddFrame(1, alpha: 255)
.EndFrameSet()
.BeginFrameSet(21, 30)
.AddFrame(21, alpha: 153)
.EndFrameSet()
.Build()
);
}
}
+130
View File
@@ -0,0 +1,130 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Timelines;
using Lumina.Text.ReadOnly;
namespace KamiToolKit.Nodes;
public class TabBarNode : SimpleComponentNode {
private readonly List<TabBarRadioButtonNode> radioButtons = [];
public TabBarNode() {
BuildTimelines();
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
RecalculateLayout();
}
public void AddTab(ReadOnlySeString label, Action callback, bool isEnabled = true) {
var newButton = new TabBarRadioButtonNode {
Height = Height,
String = label,
OnClick = callback,
IsEnabled = isEnabled,
MultiplyColor = isEnabled ? Vector3.One : new Vector3(0.6f, 0.6f, 0.6f),
};
newButton.AddEvent(AtkEventType.ButtonClick, () => ClickHandler(newButton));
radioButtons.Add(newButton);
newButton.AttachNode(this);
if (radioButtons.Count is 1) {
newButton.IsSelected = true;
}
RecalculateLayout();
}
private void ClickHandler(TabBarRadioButtonNode button) {
foreach (var radioButton in radioButtons) {
radioButton.IsChecked = false;
radioButton.IsSelected = false;
}
button.IsChecked = true;
button.IsSelected = true;
}
public void SelectTab(ReadOnlySeString label) {
var button = radioButtons.FirstOrDefault(button => button.String == label);
if (button is null) return;
ClickHandler(button);
}
public void DisableTab(ReadOnlySeString label) {
var button = radioButtons.FirstOrDefault(button => button.String == label);
if (button is null) return;
button.IsEnabled = false;
button.MultiplyColor = new Vector3(0.6f, 0.6f, 0.6f);
}
public void EnableTab(ReadOnlySeString label) {
var button = radioButtons.FirstOrDefault(button => button.String == label);
if (button is null) return;
button.IsEnabled = true;
button.MultiplyColor = Vector3.One;
}
public void ToggleTab(ReadOnlySeString label) {
var button = radioButtons.FirstOrDefault(button => button.String == label);
if (button is null) return;
button.IsEnabled = !button.IsEnabled;
if (button.IsEnabled) {
button.MultiplyColor = Vector3.One;
}
else {
button.MultiplyColor = new Vector3(0.6f, 0.6f, 0.6f);
}
}
public void RemoveTab(ReadOnlySeString label) {
var button = radioButtons.FirstOrDefault(button => button.String == label);
if (button is null) return;
button.Dispose();
radioButtons.Remove(button);
RecalculateLayout();
}
public void Clear() {
foreach (var node in radioButtons) {
node.Dispose();
}
radioButtons.Clear();
}
private void RecalculateLayout() {
var step = Width / radioButtons.Count;
foreach (var index in Enumerable.Range(0, radioButtons.Count)) {
var button = radioButtons[index];
button.Width = step + 5.0f;
button.X = step * index - 5.0f;
button.Height = Height;
}
}
private void BuildTimelines() {
AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 20)
.AddLabel(1, 101, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(11, 102, AtkTimelineJumpBehavior.PlayOnce, 0)
.EndFrameSet()
.Build()
);
}
}
@@ -0,0 +1,287 @@
using System;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Timelines;
using Lumina.Text.ReadOnly;
namespace KamiToolKit.Nodes;
public unsafe class TabBarRadioButtonNode : ComponentNode<AtkComponentRadioButton, AtkUldComponentDataRadioButton> {
public readonly TextNode LabelNode;
public readonly NineGridNode SelectedNineGridNode;
public readonly NineGridNode UnselectedNineGridNode;
public TabBarRadioButtonNode() {
SetInternalComponentType(ComponentType.RadioButton);
UnselectedNineGridNode = new SimpleNineGridNode {
Position = new Vector2(-2.0f, -1.0f),
TexturePath = "ui/uld/TabButtonA.tex",
TextureCoordinates = new Vector2(0.0f, 0.0f),
TextureSize = new Vector2(88.0f, 26.0f),
LeftOffset = 16,
RightOffset = 16,
};
UnselectedNineGridNode.AttachNode(this);
SelectedNineGridNode = new SimpleNineGridNode {
Position = new Vector2(-2.0f, -1.0f),
TexturePath = "ui/uld/TabButtonA.tex",
TextureCoordinates = new Vector2(0.0f, 26.0f),
TextureSize = new Vector2(88.0f, 26.0f),
LeftOffset = 16,
RightOffset = 16,
IsVisible = false,
};
SelectedNineGridNode.AttachNode(this);
LabelNode = new TextNode {
Position = new Vector2(13.0f, 2.0f),
AlignmentType = AlignmentType.Center,
TextColor = ColorHelper.GetColor(50),
};
LabelNode.AttachNode(this);
BuildTimelines();
Data->Nodes[0] = LabelNode.NodeId;
Data->Nodes[1] = UnselectedNineGridNode.NodeId;
Data->Nodes[2] = 0;
Data->Nodes[3] = 0;
AddEvent(AtkEventType.ButtonClick, ClickHandler);
InitializeComponentEvents();
}
public Action? OnClick { get; set; }
public ReadOnlySeString String {
get => LabelNode.String;
set => Component->SetText(value);
}
public bool IsSelected {
get => Component->IsSelected;
set {
Component->IsSelected = value;
if (value) {
SelectedNineGridNode.IsVisible = true;
UnselectedNineGridNode.IsVisible = false;
}
else {
SelectedNineGridNode.IsVisible = false;
UnselectedNineGridNode.IsVisible = true;
}
}
}
public bool IsChecked {
get => Component->IsChecked;
set => Component->SetChecked(value);
}
private void ClickHandler() {
OnClick?.Invoke();
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
CollisionNode.Size = Size;
UnselectedNineGridNode.Size = new Vector2(Width + 4.0f, Height + 2.0f);
SelectedNineGridNode.Size = new Vector2(Width + 4.0f, Height + 2.0f);
LabelNode.Size = new Vector2(Width - 25.0f, Height - 4.0f);
}
private void BuildTimelines() {
AddTimeline(new TimelineBuilder()
.BeginFrameSet(11, 20)
.AddFrame(11, new Vector2(525, 0))
.EndFrameSet()
.Build()
);
UnselectedNineGridNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 9)
.AddFrame(1, alpha: 255)
.AddFrame(1, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(10, 19)
.AddFrame(10, alpha: 255)
.AddFrame(12, alpha: 255)
.AddFrame(10, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(12, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(20, 29)
.AddFrame(20, alpha: 255)
.AddFrame(20, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(30, 39)
.AddFrame(30, alpha: 178)
.AddFrame(30, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(50, 50, 50))
.EndFrameSet()
.BeginFrameSet(40, 49)
.AddFrame(40, alpha: 255)
.AddFrame(40, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(50, 59)
.AddFrame(50, alpha: 255)
.AddFrame(52, alpha: 255)
.AddFrame(50, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(52, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(120, 129)
.AddFrame(120, alpha: 255)
.AddFrame(122, alpha: 0)
.AddFrame(120, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(122, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(130, 139)
.AddFrame(130, alpha: 0)
.AddFrame(132, alpha: 255)
.AddFrame(130, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(132, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(140, 149)
.AddFrame(140, alpha: 255)
.AddFrame(142, alpha: 0)
.AddFrame(140, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(142, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(150, 159)
.AddFrame(150, alpha: 0)
.AddFrame(152, alpha: 255)
.AddFrame(150, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(152, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.Build()
);
SelectedNineGridNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(60, 69)
.AddFrame(60, alpha: 255)
.AddFrame(60, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(70, 79)
.AddFrame(70, alpha: 255)
.AddFrame(72, alpha: 255)
.AddFrame(70, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(72, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(80, 89)
.AddFrame(80, alpha: 255)
.AddFrame(80, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(90, 99)
.AddFrame(90, alpha: 178)
.AddFrame(90, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(50, 50, 50))
.EndFrameSet()
.BeginFrameSet(100, 109)
.AddFrame(100, alpha: 255)
.AddFrame(100, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(110, 119)
.AddFrame(110, alpha: 255)
.AddFrame(112, alpha: 255)
.AddFrame(110, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(112, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(120, 129)
.AddFrame(120, alpha: 0)
.AddFrame(122, alpha: 255)
.AddFrame(120, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(122, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(130, 139)
.AddFrame(130, alpha: 255)
.AddFrame(132, alpha: 0)
.AddFrame(130, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(132, addColor: new Vector3(16, 16, 16), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(140, 149)
.AddFrame(140, alpha: 0)
.AddFrame(142, alpha: 255)
.AddFrame(140, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(142, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(150, 159)
.AddFrame(150, alpha: 255)
.AddFrame(152, alpha: 0)
.AddFrame(150, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.AddFrame(152, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.Build()
);
LabelNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 9)
.AddFrame(1, alpha: 255)
.AddFrame(1, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(10, 19)
.AddFrame(10, alpha: 255)
.AddFrame(10, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(20, 29)
.AddFrame(20, alpha: 255)
.AddFrame(20, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(30, 39)
.AddFrame(30, alpha: 153)
.AddFrame(30, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(80, 80, 80))
.EndFrameSet()
.BeginFrameSet(40, 49)
.AddFrame(40, alpha: 255)
.AddFrame(40, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(50, 59)
.AddFrame(50, alpha: 255)
.AddFrame(50, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(60, 69)
.AddFrame(60, alpha: 255)
.AddFrame(60, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(70, 79)
.AddFrame(70, alpha: 255)
.AddFrame(70, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(80, 89)
.AddFrame(80, alpha: 255)
.AddFrame(80, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(90, 99)
.AddFrame(90, alpha: 153)
.AddFrame(90, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(80, 80, 80))
.EndFrameSet()
.BeginFrameSet(100, 109)
.AddFrame(100, alpha: 255)
.AddFrame(100, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(110, 119)
.AddFrame(110, alpha: 255)
.AddFrame(110, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(120, 129)
.AddFrame(120, alpha: 255)
.AddFrame(120, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(130, 139)
.AddFrame(130, alpha: 255)
.AddFrame(130, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(140, 149)
.AddFrame(140, alpha: 255)
.AddFrame(140, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.BeginFrameSet(150, 159)
.AddFrame(150, alpha: 255)
.AddFrame(150, addColor: new Vector3(0, 0, 0), multiplyColor: new Vector3(100, 100, 100))
.EndFrameSet()
.Build()
);
}
}
@@ -0,0 +1,5 @@
namespace KamiToolKit.Nodes;
public class TextButtonListNode : ButtonListNode<string> {
protected override string GetLabelForOption(string option) => option;
}
@@ -0,0 +1,51 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using Lumina.Text.ReadOnly;
namespace KamiToolKit.Nodes;
public unsafe class TextButtonNode : ButtonBase {
public readonly NineGridNode BackgroundNode;
public readonly TextNode LabelNode;
public TextButtonNode() {
BackgroundNode = new SimpleNineGridNode {
TexturePath = "ui/uld/ButtonA.tex",
TextureSize = new Vector2(100.0f, 28.0f),
LeftOffset = 16.0f,
RightOffset = 16.0f,
};
BackgroundNode.AttachNode(this);
LabelNode = new TextNode {
AlignmentType = AlignmentType.Center,
Position = new Vector2(16.0f, 3.0f),
TextColor = ColorHelper.GetColor(50),
};
LabelNode.AttachNode(this);
LoadTimelines();
Data->Nodes[0] = LabelNode.NodeId;
Data->Nodes[1] = BackgroundNode.NodeId;
InitializeComponentEvents();
}
public ReadOnlySeString String {
get => LabelNode.String;
set => LabelNode.String = value;
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
LabelNode.Size = new Vector2(Width - 32.0f, Height - 8.0f);
BackgroundNode.Size = Size;
}
private void LoadTimelines()
=> LoadThreePartTimelines(this, BackgroundNode, LabelNode, new Vector2(16.0f, 3.0f));
}
@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
namespace KamiToolKit.Nodes;
public class TextDropDownNode : DropDownNode<TextButtonListNode, string> {
public TextDropDownNode() {
OptionListNode.OnOptionSelected += OptionSelectedHandler;
}
public Action<string>? OnOptionSelected { get; set; }
public required List<string>? Options {
get => OptionListNode.Options;
set {
OptionListNode.Options = value;
OptionListNode.SelectDefaultOption();
UpdateLabel(OptionListNode.SelectedOption);
}
}
private void OptionSelectedHandler(string option) {
OnOptionSelected?.Invoke(option);
UpdateLabel(option);
Toggle(false);
}
protected override void UpdateLabel(string? option) {
LabelNode.String = option ?? "ERROR: Invalid Default Option";
}
}
@@ -0,0 +1,77 @@
using System.Drawing;
using System.Numerics;
using Dalamud.Interface;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Timelines;
namespace KamiToolKit.Nodes;
public unsafe class TextInputButtonNode : ButtonBase {
public readonly NineGridNode BackgroundNode;
public readonly TextNode LabelNode;
public TextInputButtonNode() {
BackgroundNode = new SimpleNineGridNode {
Size = new Vector2(160.0f, 24.0f),
LeftOffset = 16.0f,
RightOffset = 1.0f,
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.Fill | NodeFlags.EmitsEvents,
TexturePath = "ui/uld/ListItemA.tex",
TextureCoordinates = new Vector2(0.0f, 22.0f),
TextureSize = new Vector2(63.0f, 22.0f),
};
BackgroundNode.AttachNode(this);
LabelNode = new TextNode {
Position = new Vector2(12.0f, 2.0f),
Size = new Vector2(140.0f, 18.0f),
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents,
AlignmentType = AlignmentType.Left,
TextFlags = TextFlags.AutoAdjustNodeSize,
TextColor = KnownColor.White.Vector(),
TextOutlineColor = KnownColor.White.Vector(),
BackgroundColor = KnownColor.Black.Vector(),
};
LabelNode.AttachNode(this);
Data->Nodes[0] = LabelNode.NodeId;
Data->Nodes[1] = BackgroundNode.NodeId;
LoadTimeline();
InitializeComponentEvents();
}
private void LoadTimeline() {
AddTimeline(new TimelineBuilder()
.BeginFrameSet(1, 59)
.AddLabelPair(1, 9, 1)
.AddLabelPair(10, 19, 2)
.AddLabelPair(20, 29, 3)
.AddLabelPair(30, 39, 7)
.AddLabelPair(40, 49, 6)
.AddLabelPair(50, 59, 4)
.EndFrameSet()
.Build());
BackgroundNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(10, 19)
.AddFrame(10, alpha: 0)
.AddFrame(13, alpha: 255)
.EndFrameSet()
.AddFrameSetWithFrame(20, 29, 20, alpha: 255)
.AddFrameSetWithFrame(40, 49, 40, alpha: 255)
.BeginFrameSet(50, 59)
.AddFrame(50, alpha: 255)
.AddFrame(52, alpha: 0)
.EndFrameSet()
.Build());
LabelNode.AddTimeline(new TimelineBuilder()
.AddFrameSetWithFrame(1, 29, 1, alpha: 255, multiplyColor: new Vector3(100.0f))
.AddFrameSetWithFrame(30, 39, 30, alpha: 153, multiplyColor: new Vector3(80.0f))
.AddFrameSetWithFrame(40, 59, 40, alpha: 255, multiplyColor: new Vector3(100.0f))
.Build());
}
}
@@ -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());
}
}
@@ -0,0 +1,97 @@
using System;
using FFXIVClientStructs.FFXIV.Client.System.Input;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Enums;
using Lumina.Text.ReadOnly;
namespace KamiToolKit.Nodes;
public unsafe class TextMultiLineInputNode : TextInputNode {
public TextMultiLineInputNode() {
TextLimitsNode.AlignmentType = AlignmentType.BottomRight;
CurrentTextNode.TextFlags |= TextFlags.MultiLine;
CurrentTextNode.LineSpacing = 14;
Flags |= TextInputFlags.MultiLine;
CollisionNode.AddEvent(AtkEventType.InputReceived, InputComplete);
Component->InputSanitizationFlags = AllowedEntities.UppercaseLetters | AllowedEntities.LowercaseLetters | AllowedEntities.Numbers |
AllowedEntities.SpecialCharacters | AllowedEntities.CharacterList | AllowedEntities.OtherCharacters |
AllowedEntities.Payloads | AllowedEntities.Unknown9;
Component->ComponentTextData.Flags2 = TextInputFlags2.MultiLine | TextInputFlags2.AllowSymbolInput | TextInputFlags2.AllowNumberInput;
Component->ComponentTextData.MaxLine = byte.MaxValue;
Component->ComponentTextData.MaxByte = ushort.MaxValue;
}
public uint MaxLines {
get => Component->ComponentTextData.MaxLine;
set => Component->ComponentTextData.MaxLine = value;
}
public uint MaxBytes {
get => Component->ComponentTextData.MaxByte;
set => Component->ComponentTextData.MaxByte = value;
}
public override ReadOnlySeString String {
get => base.String;
set {
base.String = value;
PlaceholderTextNode.IsVisible = PlaceholderString is not null && value.IsEmpty;
UpdateHeightForContent();
}
}
public override Action<ReadOnlySeString>? OnInputReceived {
get => base.OnInputReceived;
set {
base.OnInputReceived = _ => UpdateHeightForContent();
base.OnInputReceived += value;
}
}
public bool AutoUpdateHeight { get; set; }
public Action<float>? HeightChanged { get; set; }
private void UpdateHeightForContent() {
if (!AutoUpdateHeight) return;
var text = String;
var lineCount = Math.Max(1, text.ToString().Split('\r', '\n').Length);
var lineHeight = CurrentTextNode.LineSpacing;
var contentHeight = Math.Max(Height, lineCount * lineHeight + 20);
var oldHeight = Height;
Height = contentHeight;
if (Math.Abs(contentHeight - oldHeight) > 0.1f) {
HeightChanged?.Invoke(Height);
}
}
private void InputComplete() {
if (UIInputData.Instance()->IsKeyPressed(SeVirtualKey.RETURN)) {
var textInputComponent = Node->GetAsAtkComponentTextInput();
var cursorPos = textInputComponent->CursorPos;
using (var utf8String = new Utf8String()) {
utf8String.SetString("\r");
textInputComponent->WriteString(&utf8String);
}
textInputComponent->CursorPos = cursorPos + 1;
textInputComponent->SelectionStart = cursorPos + 1;
textInputComponent->SelectionEnd = cursorPos + 1;
}
OnInputComplete?.Invoke(Component->EvaluatedString.AsSpan());
}
}
@@ -0,0 +1,184 @@
using System;
using System.Linq;
using FFXIVClientStructs.FFXIV.Client.System.Input;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Enums;
using Lumina.Text.ReadOnly;
namespace KamiToolKit.Nodes;
/// <summary>
/// Needs More Work.
/// </summary>
internal unsafe class TextMultiLineInputNodeScrollable : TextInputNode {
private int startLineIndex;
private bool isProgrammaticTextSet;
private ReadOnlySeString fullText;
private ReadOnlySeString lastDisplayedText;
public TextMultiLineInputNodeScrollable() {
TextLimitsNode.AlignmentType = AlignmentType.BottomRight;
CurrentTextNode.TextFlags |= TextFlags.MultiLine;
CurrentTextNode.LineSpacing = 14;
Flags |= TextInputFlags.MultiLine;
CollisionNode.AddEvent(AtkEventType.InputReceived, InputComplete);
CollisionNode.AddEvent(AtkEventType.MouseWheel, OnMouseScrolled);
Component->InputSanitizationFlags = AllowedEntities.UppercaseLetters | AllowedEntities.LowercaseLetters | AllowedEntities.Numbers |
AllowedEntities.SpecialCharacters | AllowedEntities.CharacterList | AllowedEntities.OtherCharacters |
AllowedEntities.Payloads | AllowedEntities.Unknown9;
Component->ComponentTextData.Flags2 = TextInputFlags2.MultiLine | TextInputFlags2.AllowSymbolInput | TextInputFlags2.AllowNumberInput;
Component->ComponentTextData.MaxLine = byte.MaxValue;
Component->ComponentTextData.MaxByte = ushort.MaxValue;
}
public uint MaxLines {
get => Component->ComponentTextData.MaxLine;
set => Component->ComponentTextData.MaxLine = value;
}
public uint MaxBytes {
get => Component->ComponentTextData.MaxByte;
set => Component->ComponentTextData.MaxByte = value;
}
public override ReadOnlySeString String {
get => fullText;
set {
isProgrammaticTextSet = true;
fullText = value;
UpdateCurrentTextDisplay();
isProgrammaticTextSet = false;
}
}
public override Action<ReadOnlySeString>? OnInputReceived {
get => base.OnInputReceived;
set {
base.OnInputReceived = currentComponentText => {
if (isProgrammaticTextSet) return;
ApplyDisplayChangesToFullText(currentComponentText.ToString());
lastDisplayedText = currentComponentText;
UpdateLineCountDisplay();
};
base.OnInputReceived += value;
}
}
private void OnMouseScrolled(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
var lines = fullText.ToString().Split(['\r', '\n'], StringSplitOptions.None);
var lineHeight = CurrentTextNode.LineSpacing;
var maxVisibleLines = (int)(Height / lineHeight);
var oldStartLineIndex = startLineIndex;
if (atkEventData->IsScrollUp)
startLineIndex = Math.Max(0, startLineIndex - 1);
else if (atkEventData->IsScrollDown)
startLineIndex = Math.Min(Math.Max(0, lines.Length - maxVisibleLines), startLineIndex + 1);
if (oldStartLineIndex != startLineIndex) {
UpdateCurrentTextDisplay();
}
atkEvent->SetEventIsHandled();
}
private void ApplyDisplayChangesToFullText(string newDisplayedText) {
var lines = fullText.ToString().Split(['\r', '\n'], StringSplitOptions.None).ToList();
var oldDisplayLines = lastDisplayedText.ToString().Split(['\r', '\n'], StringSplitOptions.None);
var newDisplayLines = newDisplayedText.Split(['\r', '\n'], StringSplitOptions.None);
if (startLineIndex < lines.Count) {
var removeCount = Math.Min(oldDisplayLines.Length, lines.Count - startLineIndex);
lines.RemoveRange(startLineIndex, removeCount);
lines.InsertRange(startLineIndex, newDisplayLines);
}
else {
lines.AddRange(newDisplayLines);
}
for (var i = lines.Count - 1; i >= 0; i--) {
if (string.IsNullOrEmpty(lines[i]))
lines.RemoveAt(i);
else
break;
}
if (lines.Count == 0)
lines.Add(string.Empty);
fullText = string.Join("\r", lines);
lastDisplayedText = newDisplayedText;
}
private void UpdateLineCountDisplay() {
var lines = fullText.ToString().Split(['\r', '\n'], StringSplitOptions.None);
var lineHeight = CurrentTextNode.LineSpacing;
var totalLines = lines.Length;
var maxVisibleLines = (int)(Height / lineHeight);
if (maxVisibleLines <= 0) return;
startLineIndex = Math.Clamp(startLineIndex, 0, Math.Max(0, totalLines - maxVisibleLines));
var currentEndLine = Math.Min(startLineIndex + maxVisibleLines, totalLines);
var limitText = $"{startLineIndex + 1}-{currentEndLine}/{totalLines}";
TextLimitsNode.String = limitText;
}
private void UpdateCurrentTextDisplay() {
var lines = fullText.ToString().Split(['\r', '\n'], StringSplitOptions.None);
var lineHeight = CurrentTextNode.LineSpacing;
var maxVisibleLines = (int)(Height / lineHeight);
if (maxVisibleLines <= 0) return;
startLineIndex = Math.Clamp(startLineIndex, 0, Math.Max(0, lines.Length - maxVisibleLines));
var displayText = startLineIndex > 0 && startLineIndex < lines.Length
? string.Join("\r", lines.Skip(startLineIndex).Take(maxVisibleLines))
: fullText.ToString();
lastDisplayedText = displayText;
var capturedProgrammaticFlag = isProgrammaticTextSet;
isProgrammaticTextSet = capturedProgrammaticFlag;
Component->SetText(displayText);
UpdateLineCountDisplay();
}
private void InputComplete() {
if (UIInputData.Instance()->IsKeyPressed(SeVirtualKey.RETURN)) {
var textInputComponent = Node->GetAsAtkComponentTextInput();
var cursorPos = textInputComponent->CursorPos;
using (var utf8String = new Utf8String()) {
utf8String.SetString("\r");
textInputComponent->WriteString(&utf8String);
}
textInputComponent->CursorPos = cursorPos + 1;
textInputComponent->SelectionStart = cursorPos + 1;
textInputComponent->SelectionEnd = cursorPos + 1;
}
OnInputComplete?.Invoke(Component->EvaluatedString.AsSpan());
}
}
@@ -0,0 +1,44 @@
using System.Numerics;
using KamiToolKit.Enums;
namespace KamiToolKit.Nodes;
public class TextureButtonNode : ButtonBase {
public readonly SimpleImageNode ImageNode;
public TextureButtonNode() {
ImageNode = new ImGuiImageNode {
WrapMode = WrapMode.Stretch,
};
ImageNode.AttachNode(this);
LoadTimelines();
InitializeComponentEvents();
}
public string TexturePath {
get => ImageNode.TexturePath;
set => ImageNode.TexturePath = value;
}
public Vector2 TextureCoordinates {
get => ImageNode.TextureCoordinates;
set => ImageNode.TextureCoordinates = value;
}
public Vector2 TextureSize {
get => ImageNode.TextureSize;
set => ImageNode.TextureSize = value;
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
ImageNode.Size = Size;
}
private void LoadTimelines()
=> LoadTwoPartTimelines(this, ImageNode);
}
@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace KamiToolKit.Nodes;
public class TreeListNode : SimpleComponentNode {
private readonly SimpleComponentNode childContainer;
private readonly List<TreeListCategoryNode> children = [];
public ReadOnlyCollection<TreeListCategoryNode> CategoryNodes => children.AsReadOnly();
public TreeListNode() {
childContainer = new SimpleComponentNode();
childContainer.AttachNode(this);
}
public float CategoryVerticalSpacing { get; set; } = 4.0f;
public Action<float>? OnLayoutUpdate { get; set; }
protected override void OnSizeChanged() {
base.OnSizeChanged();
childContainer.Width = Width;
}
public void AddCategoryNode(TreeListCategoryNode node) {
RefreshLayout();
children.Add(node);
node.NodeId = (uint)children.Count + 1;
node.Width = childContainer.Width;
node.Y = childContainer.Height;
node.AttachNode(childContainer);
node.ParentTreeListNode = this;
childContainer.Height += node.Height + CategoryVerticalSpacing;
}
public void RefreshLayout() {
childContainer.Height = 0.0f;
foreach (var child in children) {
if (!child.IsVisible) continue;
child.Y = childContainer.Height;
childContainer.Height += child.Height + CategoryVerticalSpacing;
child.UpdateChildrenNodeId();
}
OnLayoutUpdate?.Invoke(childContainer.Height);
}
}
+265
View File
@@ -0,0 +1,265 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Enums;
using KamiToolKit.Timelines;
using Lumina.Text.ReadOnly;
namespace KamiToolKit.Nodes;
public unsafe class WindowNode : WindowNodeBase {
public readonly ImageNode BackgroundImageNode;
public readonly WindowBackgroundNode BackgroundNode;
public readonly WindowBackgroundNode BorderNode;
public readonly TextureButtonNode CloseButtonNode;
public readonly TextureButtonNode ConfigurationButtonNode;
public readonly SimpleNineGridNode DividingLineNode;
public readonly CollisionNode HeaderCollisionNode;
public readonly ResNode HeaderContainerNode;
public readonly TextureButtonNode InformationButtonNode;
public readonly TextNode SubtitleNode;
public readonly TextNode TitleNode;
public WindowNode() {
CollisionNode.NodeId = 13;
Component->ShowFlags = 1;
HeaderCollisionNode = new CollisionNode {
Uses = 2,
NodeId = 12,
Height = 28.0f,
Position = new Vector2(8.0f, 8.0f),
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.HasCollision | NodeFlags.RespondToMouse | NodeFlags.EmitsEvents | NodeFlags.Focusable,
};
HeaderCollisionNode.AttachNode(this);
BackgroundNode = new WindowBackgroundNode(false) {
NodeId = 11,
Position = Vector2.Zero,
Offsets = new Vector4(64.0f, 32.0f, 32.0f, 32.0f),
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.Fill | NodeFlags.EmitsEvents,
PartsRenderType = 19,
};
BackgroundNode.AttachNode(this);
BorderNode = new WindowBackgroundNode(true) {
NodeId = 10,
Position = Vector2.Zero,
Offsets = new Vector4(64.0f, 32.0f, 32.0f, 32.0f),
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.Fill | NodeFlags.EmitsEvents,
PartsRenderType = 7,
};
BorderNode.AttachNode(this);
BackgroundImageNode = new SimpleImageNode {
NodeId = 9,
WrapMode = WrapMode.Stretch,
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents,
TexturePath = "ui/uld/WindowA_Gradation.tex",
TextureCoordinates = new Vector2(6.0f, 2.0f),
TextureSize = new Vector2(24.0f, 24.0f),
};
BackgroundImageNode.AttachNode(this);
HeaderContainerNode = new ResNode {
NodeId = 2,
Size = new Vector2(477.0f, 38.0f),
};
HeaderContainerNode.AttachNode(this);
DividingLineNode = new SimpleNineGridNode {
NodeId = 8,
TexturePath = "ui/uld/WindowA_Line.tex",
TextureCoordinates = Vector2.Zero,
TextureSize = new Vector2(32.0f, 4.0f),
Size = new Vector2(650.0f, 4.0f),
LeftOffset = 12.0f,
RightOffset = 12.0f,
Position = new Vector2(10.0f, 33.0f),
};
DividingLineNode.AttachNode(HeaderContainerNode);
CloseButtonNode = new TextureButtonNode {
NodeId = 7,
Size = new Vector2(28.0f, 28.0f),
Position = new Vector2(449.0f, 6.0f),
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents,
TexturePath = "ui/uld/WindowA_Button.tex",
TextureCoordinates = new Vector2(0.0f, 0.0f),
TextureSize = new Vector2(28.0f, 28.0f),
};
CloseButtonNode.AttachNode(HeaderContainerNode);
ConfigurationButtonNode = new TextureButtonNode {
NodeId = 6,
Size = new Vector2(16.0f, 16.0f),
Position = new Vector2(435.0f, 8.0f),
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents,
TexturePath = "ui/uld/WindowA_Button.tex",
TextureCoordinates = new Vector2(44.0f, 0.0f),
TextureSize = new Vector2(16.0f, 16.0f),
};
ConfigurationButtonNode.AttachNode(HeaderContainerNode);
InformationButtonNode = new TextureButtonNode {
NodeId = 5,
Size = new Vector2(16.0f, 16.0f),
Position = new Vector2(421.0f, 8.0f),
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents,
TexturePath = "ui/uld/WindowA_Button.tex",
TextureCoordinates = new Vector2(28.0f, 0.0f),
TextureSize = new Vector2(16.0f, 16.0f),
};
InformationButtonNode.AttachNode(HeaderContainerNode);
SubtitleNode = new TextNode {
NodeId = 4,
LineSpacing = 12,
AlignmentType = AlignmentType.Left,
FontSize = 12,
FontType = FontType.Axis,
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents,
TextColor = ColorHelper.GetColor(3),
TextOutlineColor = ColorHelper.GetColor(6),
BackgroundColor = Vector4.Zero,
Size = new Vector2(46.0f, 20.0f),
Position = new Vector2(83.0f, 17.0f),
};
SubtitleNode.AttachNode(HeaderContainerNode);
TitleNode = new TextNode {
NodeId = 3,
LineSpacing = 23,
AlignmentType = AlignmentType.Left,
FontSize = 23,
FontType = FontType.TrumpGothic,
TextFlags = TextFlags.AutoAdjustNodeSize,
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents,
TextColor = ColorHelper.GetColor(2),
TextOutlineColor = ColorHelper.GetColor(7),
BackgroundColor = Vector4.Zero,
Size = new Vector2(86.0f, 31.0f),
Position = new Vector2(12.0f, 7.0f),
};
TitleNode.AttachNode(HeaderContainerNode);
Data->ShowCloseButton = 1;
Data->ShowConfigButton = 0;
Data->ShowHelpButton = 0;
Data->ShowHeader = 1;
Data->Nodes[0] = TitleNode.NodeId;
Data->Nodes[1] = SubtitleNode.NodeId;
Data->Nodes[2] = CloseButtonNode.NodeId;
Data->Nodes[3] = ConfigurationButtonNode.NodeId;
Data->Nodes[4] = InformationButtonNode.NodeId;
Data->Nodes[5] = 0;
Data->Nodes[6] = HeaderContainerNode.NodeId;
Data->Nodes[7] = 0;
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents;
LoadTimelines();
InitializeComponentEvents();
}
public AtkUnitBase* OwnerAddon {
get => Component->OwnerUnitBase;
set => Component->OwnerUnitBase = value;
}
public ReadOnlySeString Title {
get => TitleNode.String;
set {
TitleNode.String = value;
TitleNode.IsVisible = true;
}
}
public ReadOnlySeString Subtitle {
get => SubtitleNode.String;
set {
SubtitleNode.String = value;
SubtitleNode.IsVisible = true;
SubtitleNode.X = TitleNode.X + TitleNode.Width + 2.0f;
}
}
public override void SetTitle(string title, string? subtitle = null) {
base.SetTitle(title, subtitle);
SubtitleNode.Position = new Vector2(TitleNode.Bounds.Right + 4.0f, SubtitleNode.Y);
}
public bool ShowCloseButton {
get => CloseButtonNode.IsVisible;
set => CloseButtonNode.IsVisible = value;
}
public bool ShowConfigButton {
get => ConfigurationButtonNode.IsVisible;
set => ConfigurationButtonNode.IsVisible = value;
}
public bool ShowHelpButton {
get => InformationButtonNode.IsVisible;
set => InformationButtonNode.IsVisible = value;
}
public bool ShowHeader {
get => InformationButtonNode.IsVisible;
set => InformationButtonNode.IsVisible = value;
}
public bool Focused {
get => BorderNode.IsVisible;
set => BorderNode.IsVisible = value;
}
public override float HeaderHeight => HeaderContainerNode.Height;
public override Vector2 ContentSize => new(BackgroundImageNode.Width, BackgroundImageNode.Height - HeaderHeight);
public override Vector2 ContentStartPosition => new(BackgroundImageNode.X, BackgroundImageNode.Y + HeaderHeight);
public override ResNode WindowHeaderFocusNode => HeaderContainerNode;
protected override void OnSizeChanged() {
base.OnSizeChanged();
HeaderContainerNode.Width = Width;
HeaderCollisionNode.Width = Width - 14.0f;
BackgroundNode.Size = Size;
BorderNode.Size = Size;
BackgroundImageNode.Size = new Vector2(Width - 8.0f, Height - 16.0f);
BackgroundImageNode.Position = new Vector2(4.0f, 4.0f);
CloseButtonNode.X = Width - 33.0f;
ConfigurationButtonNode.X = Width - 47.0f;
InformationButtonNode.X = Width - 61.0f;
DividingLineNode.Width = Width - 20.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, multiplyColor: new Vector3(100.0f))
.AddFrameSetWithFrame(10, 19, 10, multiplyColor: new Vector3(100.0f))
.AddFrameSetWithFrame(20, 29, 20, multiplyColor: new Vector3(50.0f))
.Build());
BorderNode.AddTimeline(new TimelineBuilder()
.BeginFrameSet(10, 19)
.AddFrame(10, alpha: 0)
.AddFrame(12, alpha: 255)
.EndFrameSet()
.Build());
}
}
@@ -0,0 +1,19 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace KamiToolKit.Nodes;
public abstract unsafe class WindowNodeBase : ComponentNode<AtkComponentWindow, AtkUldComponentDataWindow> {
protected WindowNodeBase() {
SetInternalComponentType(ComponentType.Window);
}
public abstract Vector2 ContentSize { get; }
public abstract Vector2 ContentStartPosition { get; }
public abstract float HeaderHeight { get; }
public abstract ResNode WindowHeaderFocusNode { get; }
public virtual void SetTitle(string title, string? subtitle = null)
=> Component->SetTitle(title, subtitle ?? string.Empty);
}