diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.FieldNames.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.FieldNames.cs
new file mode 100644
index 000000000..e72de2b23
--- /dev/null
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.FieldNames.cs
@@ -0,0 +1,196 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+using FFXIVClientStructs.Attributes;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+using static System.Reflection.BindingFlags;
+using static Dalamud.Interface.Internal.UiDebug2.UiDebug2;
+
+namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
+
+///
+public unsafe partial class AddonTree
+{
+ private static readonly Dictionary AddonTypeDict = [];
+
+ ///
+ /// Gets or sets a collection of names corresponding to pointers documented within the addon struct.
+ ///
+ internal Dictionary> FieldNames { get; set; } = [];
+
+ private object? GetAddonObj(AtkUnitBase* addon)
+ {
+ if (addon == null)
+ {
+ return null;
+ }
+
+ if (!AddonTypeDict.ContainsKey(this.AddonName))
+ {
+ AddonTypeDict.Add(this.AddonName, null);
+
+ foreach (var a in AppDomain.CurrentDomain.GetAssemblies())
+ {
+ try
+ {
+ foreach (var t in from t in a.GetTypes()
+ where t.IsPublic
+ let xivAddonAttr = (Addon?)t.GetCustomAttribute(typeof(Addon), false)
+ where xivAddonAttr != null
+ where xivAddonAttr.AddonIdentifiers.Contains(this.AddonName)
+ select t)
+ {
+ AddonTypeDict[this.AddonName] = t;
+ break;
+ }
+ }
+ catch
+ {
+ // ignored
+ }
+ }
+ }
+
+ return AddonTypeDict.TryGetValue(this.AddonName, out var result) && result != null ? Marshal.PtrToStructure(new(addon), result) : *addon;
+ }
+
+ private void PopulateFieldNames(nint ptr)
+ {
+ this.PopulateFieldNames(this.GetAddonObj((AtkUnitBase*)ptr), ptr);
+ }
+
+ private void PopulateFieldNames(object? obj, nint baseAddr, List? path = null)
+ {
+ if (obj == null)
+ {
+ return;
+ }
+
+ path ??= [];
+ var baseType = obj.GetType();
+
+ foreach (var field in baseType.GetFields(Static | Public | NonPublic | Instance))
+ {
+ if (field.GetCustomAttribute(typeof(FieldOffsetAttribute)) is FieldOffsetAttribute offset)
+ {
+ try
+ {
+ var fieldAddr = baseAddr + offset.Value;
+ var name = field.Name[0] == '_' ? char.ToUpperInvariant(field.Name[1]) + field.Name[2..] : field.Name;
+ var fieldType = field.FieldType;
+
+ if (!field.IsStatic && fieldType.IsPointer)
+ {
+ var pointer = (nint)Pointer.Unbox((Pointer)field.GetValue(obj)!);
+ var itemType = fieldType.GetElementType();
+ ParsePointer(fieldAddr, pointer, itemType, name);
+ }
+ else if (fieldType.IsExplicitLayout)
+ {
+ ParseExplicitField(fieldAddr, field, fieldType, name);
+ }
+ else if (fieldType.Name.Contains("FixedSizeArray"))
+ {
+ ParseFixedSizeArray(fieldAddr, fieldType, name);
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Warning($"Failed to parse field at {offset.Value:X} in {baseType}!\n{ex}");
+ }
+ }
+ }
+
+ void ParseExplicitField(nint fieldAddr, FieldInfo field, MemberInfo fieldType, string name)
+ {
+ try
+ {
+ if (this.FieldNames.TryAdd(fieldAddr, new List(path!) { name }) && fieldType.DeclaringType == baseType)
+ {
+ this.PopulateFieldNames(field.GetValue(obj), fieldAddr, new List(path) { name });
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Warning($"Failed to parse explicit field: {fieldType} {name} in {baseType}!\n{ex}");
+ }
+ }
+
+ void ParseFixedSizeArray(nint fieldAddr, Type fieldType, string name)
+ {
+ try
+ {
+ var spanLength = (int)(fieldType.CustomAttributes.ToArray()[0].ConstructorArguments[0].Value ?? 0);
+
+ if (spanLength <= 0)
+ {
+ return;
+ }
+
+ var itemType = fieldType.UnderlyingSystemType.GenericTypeArguments[0];
+
+ if (!itemType.IsGenericType)
+ {
+ var size = Marshal.SizeOf(itemType);
+ for (var i = 0; i < spanLength; i++)
+ {
+ var itemAddr = fieldAddr + (size * i);
+ var itemName = $"{name}[{i}]";
+
+ this.FieldNames.TryAdd(itemAddr, new List(path!) { itemName });
+
+ var item = Marshal.PtrToStructure(itemAddr, itemType);
+ if (itemType.DeclaringType == baseType)
+ {
+ this.PopulateFieldNames(item, itemAddr, new List(path) { name });
+ }
+ }
+ }
+ else if (itemType.Name.Contains("Pointer"))
+ {
+ itemType = itemType.GenericTypeArguments[0];
+
+ for (var i = 0; i < spanLength; i++)
+ {
+ var itemAddr = fieldAddr + (0x08 * i);
+ var pointer = Marshal.ReadIntPtr(itemAddr);
+ ParsePointer(itemAddr, pointer, itemType, $"{name}[{i}]");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Warning($"Failed to parse fixed size array: {fieldType} {name} in {baseType}!\n{ex}");
+ }
+ }
+
+ void ParsePointer(nint fieldAddr, nint pointer, Type? itemType, string name)
+ {
+ try
+ {
+ if (pointer == 0)
+ {
+ return;
+ }
+
+ this.FieldNames.TryAdd(fieldAddr, new List(path!) { name });
+ this.FieldNames.TryAdd(pointer, new List(path) { name });
+
+ if (itemType?.DeclaringType != baseType || itemType.IsPointer)
+ {
+ return;
+ }
+
+ var item = Marshal.PtrToStructure(pointer, itemType);
+ this.PopulateFieldNames(item, pointer, new List(path) { name });
+ }
+ catch (Exception ex)
+ {
+ Log.Warning($"Failed to parse pointer: {itemType}* {name} in {baseType}!\n{ex}");
+ }
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs
new file mode 100644
index 000000000..6d6377530
--- /dev/null
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs
@@ -0,0 +1,242 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+
+using Dalamud.Interface.Components;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using ImGuiNET;
+
+using static Dalamud.Interface.FontAwesomeIcon;
+using static Dalamud.Interface.Internal.UiDebug2.ElementSelector;
+using static Dalamud.Interface.Internal.UiDebug2.UiDebug2;
+using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui;
+using static Dalamud.Utility.Util;
+
+namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
+
+///
+/// A class representing an , allowing it to be browsed within an ImGui window.
+///
+public unsafe partial class AddonTree : IDisposable
+{
+ private readonly nint initialPtr;
+
+ private AddonPopoutWindow? window;
+
+ private AddonTree(string name, nint ptr)
+ {
+ this.AddonName = name;
+ this.initialPtr = ptr;
+ this.PopulateFieldNames(ptr);
+ }
+
+ ///
+ /// Gets the name of the addon this tree represents.
+ ///
+ internal string AddonName { get; init; }
+
+ ///
+ /// Gets or sets a collection of trees representing nodes within this addon.
+ ///
+ internal Dictionary NodeTrees { get; set; } = [];
+
+ ///
+ public void Dispose()
+ {
+ foreach (var nodeTree in this.NodeTrees)
+ {
+ nodeTree.Value.Dispose();
+ }
+
+ this.NodeTrees.Clear();
+ this.FieldNames.Clear();
+ AddonTrees.Remove(this.AddonName);
+ if (this.window != null && PopoutWindows.Windows.Contains(this.window))
+ {
+ PopoutWindows.RemoveWindow(this.window);
+ this.window?.Dispose();
+ }
+
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Gets an instance of for the given addon name (or creates one if none are found).
+ /// The tree can then be drawn within the Addon Inspector and browsed.
+ ///
+ /// The name of the addon.
+ /// The for the named addon. Returns null if it does not exist, or if it is not at the expected address.
+ internal static AddonTree? GetOrCreate(string? name)
+ {
+ if (name == null)
+ {
+ return null;
+ }
+
+ try
+ {
+ var ptr = GameGui.GetAddonByName(name);
+
+ if ((AtkUnitBase*)ptr != null)
+ {
+ if (AddonTrees.TryGetValue(name, out var tree))
+ {
+ if (tree.initialPtr == ptr)
+ {
+ return tree;
+ }
+
+ tree.Dispose();
+ }
+
+ var newTree = new AddonTree(name, ptr);
+ AddonTrees.Add(name, newTree);
+ return newTree;
+ }
+ }
+ catch
+ {
+ Log.Warning("Couldn't get addon!");
+ }
+
+ return null;
+ }
+
+ ///
+ /// Draws this AddonTree within a window.
+ ///
+ internal void Draw()
+ {
+ if (!this.ValidateAddon(out var addon))
+ {
+ return;
+ }
+
+ var isVisible = addon->IsVisible;
+
+ ImGui.Text($"{this.AddonName}");
+ ImGui.SameLine();
+
+ ImGui.SameLine();
+ ImGui.TextColored(isVisible ? new(0.1f, 1f, 0.1f, 1f) : new(0.6f, 0.6f, 0.6f, 1), isVisible ? "Visible" : "Not Visible");
+
+ ImGui.SameLine(ImGui.GetWindowWidth() - 100);
+
+ if (ImGuiComponents.IconButton($"##vis{(nint)addon:X}", isVisible ? Eye : EyeSlash, isVisible ? new(0.0f, 0.8f, 0.2f, 1f) : new Vector4(0.6f, 0.6f, 0.6f, 1)))
+ {
+ addon->IsVisible = !isVisible;
+ }
+
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip("Toggle Visibility");
+ }
+
+ ImGui.SameLine();
+ if (ImGuiComponents.IconButton("pop", this.window?.IsOpen == true ? Times : ArrowUpRightFromSquare, null))
+ {
+ this.TogglePopout();
+ }
+
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip("Toggle Popout Window");
+ }
+
+ ImGui.Separator();
+
+ PrintFieldValuePair("Address", $"{(nint)addon:X}");
+
+ var uldManager = addon->UldManager;
+ PrintFieldValuePairs(
+ ("X", $"{addon->X}"),
+ ("Y", $"{addon->X}"),
+ ("Scale", $"{addon->Scale}"),
+ ("Widget Count", $"{uldManager.ObjectCount}"));
+
+ ImGui.Separator();
+
+ var addonObj = this.GetAddonObj(addon);
+ if (addonObj != null)
+ {
+ ShowStruct(addonObj, (ulong)addon);
+ }
+
+ ImGui.Dummy(new(25 * ImGui.GetIO().FontGlobalScale));
+ ImGui.Separator();
+
+ ResNodeTree.PrintNodeList(uldManager.NodeList, uldManager.NodeListCount, this);
+
+ ImGui.Dummy(new(25 * ImGui.GetIO().FontGlobalScale));
+ ImGui.Separator();
+
+ ResNodeTree.PrintNodeListAsTree(addon->CollisionNodeList, (int)addon->CollisionNodeListCount, "Collision List", this, new(0.5F, 0.7F, 1F, 1F));
+
+ if (SearchResults.Length > 0 && Countdown <= 0)
+ {
+ SearchResults = [];
+ }
+ }
+
+ ///
+ /// Checks whether a given exists somewhere within this 's associated (or any of its s).
+ ///
+ /// The node to check.
+ /// true if the node was found.
+ internal bool ContainsNode(AtkResNode* node) => this.ValidateAddon(out var addon) && FindNode(node, addon);
+
+ private static bool FindNode(AtkResNode* node, AtkUnitBase* addon) => addon != null && FindNode(node, addon->UldManager);
+
+ private static bool FindNode(AtkResNode* node, AtkComponentNode* compNode) => compNode != null && FindNode(node, compNode->Component->UldManager);
+
+ private static bool FindNode(AtkResNode* node, AtkUldManager uldManager) => FindNode(node, uldManager.NodeList, uldManager.NodeListCount);
+
+ private static bool FindNode(AtkResNode* node, AtkResNode** list, int count)
+ {
+ for (var i = 0; i < count; i++)
+ {
+ var listNode = list[i];
+ if (listNode == node)
+ {
+ return true;
+ }
+
+ if ((int)listNode->Type >= 1000 && FindNode(node, (AtkComponentNode*)listNode))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Checks whether the addon exists at the expected address. If the addon is null or has a new address, disposes this instance of .
+ ///
+ /// The addon, if successfully found.
+ /// true if the addon is found.
+ private bool ValidateAddon(out AtkUnitBase* addon)
+ {
+ addon = (AtkUnitBase*)GameGui.GetAddonByName(this.AddonName);
+ if (addon == null || (nint)addon != this.initialPtr)
+ {
+ this.Dispose();
+ return false;
+ }
+
+ return true;
+ }
+
+ private void TogglePopout()
+ {
+ if (this.window == null)
+ {
+ this.window = new AddonPopoutWindow(this, $"{this.AddonName}###addonPopout{this.initialPtr}");
+ PopoutWindows.AddWindow(this.window);
+ }
+ else
+ {
+ this.window.IsOpen = !this.window.IsOpen;
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs
new file mode 100644
index 000000000..bd1efe6c7
--- /dev/null
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs
@@ -0,0 +1,68 @@
+using Dalamud.Interface.Internal.UiDebug2.Utility;
+
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using ImGuiNET;
+
+using static ImGuiNET.ImGuiTableColumnFlags;
+using static ImGuiNET.ImGuiTableFlags;
+
+namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
+
+///
+/// Class that prints the events table for a node, where applicable.
+///
+public static class Events
+{
+ ///
+ /// Prints out each for a given node.
+ ///
+ /// The node to print events for.
+ internal static unsafe void PrintEvents(AtkResNode* node)
+ {
+ var evt = node->AtkEventManager.Event;
+ if (evt == null)
+ {
+ return;
+ }
+
+ if (ImGui.TreeNode($"Events##{(nint)node:X}eventTree"))
+ {
+ if (ImGui.BeginTable($"##{(nint)node:X}eventTable", 7, Resizable | SizingFixedFit | Borders | RowBg))
+ {
+ ImGui.TableSetupColumn("#", WidthFixed);
+ ImGui.TableSetupColumn("Type", WidthFixed);
+ ImGui.TableSetupColumn("Param", WidthFixed);
+ ImGui.TableSetupColumn("Flags", WidthFixed);
+ ImGui.TableSetupColumn("Unk29", WidthFixed);
+ ImGui.TableSetupColumn("Target", WidthFixed);
+ ImGui.TableSetupColumn("Listener", WidthFixed);
+
+ ImGui.TableHeadersRow();
+
+ var i = 0;
+ while (evt != null)
+ {
+ ImGui.TableNextColumn();
+ ImGui.Text($"{i++}");
+ ImGui.TableNextColumn();
+ ImGui.Text($"{evt->Type}");
+ ImGui.TableNextColumn();
+ ImGui.Text($"{evt->Param}");
+ ImGui.TableNextColumn();
+ ImGui.Text($"{evt->Flags}");
+ ImGui.TableNextColumn();
+ ImGui.Text($"{evt->Unk29}");
+ ImGui.TableNextColumn();
+ Gui.ClickToCopyText($"{(nint)evt->Target:X}");
+ ImGui.TableNextColumn();
+ Gui.ClickToCopyText($"{(nint)evt->Listener:X}");
+ evt = evt->NextEvent;
+ }
+
+ ImGui.EndTable();
+ }
+
+ ImGui.TreePop();
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.ClippingMask.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.ClippingMask.cs
new file mode 100644
index 000000000..cfba1a2bc
--- /dev/null
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.ClippingMask.cs
@@ -0,0 +1,35 @@
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+using static Dalamud.Utility.Util;
+
+namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
+
+///
+/// A tree for an that can be printed and browsed via ImGui.
+///
+internal unsafe class ClippingMaskNodeTree : ImageNodeTree
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The node to create a tree for.
+ /// The tree representing the containing addon.
+ internal ClippingMaskNodeTree(AtkResNode* node, AddonTree addonTree)
+ : base(node, addonTree)
+ {
+ }
+
+ ///
+ private protected override uint PartId => this.CmNode->PartId;
+
+ ///
+ private protected override AtkUldPartsList* PartsList => this.CmNode->PartsList;
+
+ private AtkClippingMaskNode* CmNode => (AtkClippingMaskNode*)this.Node;
+
+ ///
+ private protected override void PrintNodeObject() => ShowStruct(this.CmNode);
+
+ ///
+ private protected override void PrintFieldsForNodeType(bool isEditorOpen = false) => this.DrawTextureAndParts();
+}
diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Collision.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Collision.cs
new file mode 100644
index 000000000..c447afac9
--- /dev/null
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Collision.cs
@@ -0,0 +1,24 @@
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+using static Dalamud.Utility.Util;
+
+namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
+
+///
+/// A tree for an that can be printed and browsed via ImGui.
+///
+internal unsafe class CollisionNodeTree : ResNodeTree
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The node to create a tree for.
+ /// The tree representing the containing addon.
+ internal CollisionNodeTree(AtkResNode* node, AddonTree addonTree)
+ : base(node, addonTree)
+ {
+ }
+
+ ///
+ private protected override void PrintNodeObject() => ShowStruct((AtkCollisionNode*)this.Node);
+}
diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs
new file mode 100644
index 000000000..2c95924c5
--- /dev/null
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs
@@ -0,0 +1,283 @@
+using System.Runtime.InteropServices;
+
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using ImGuiNET;
+
+using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui;
+using static Dalamud.Utility.Util;
+using static FFXIVClientStructs.FFXIV.Component.GUI.ComponentType;
+
+namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
+
+///
+/// A tree for an that can be printed and browsed via ImGui.
+///
+internal unsafe class ComponentNodeTree : ResNodeTree
+{
+ private readonly AtkUldManager* uldManager;
+
+ private readonly ComponentType componentType;
+
+ private readonly AtkComponentBase* component;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The node to create a tree for.
+ /// The tree representing the containing addon.
+ internal ComponentNodeTree(AtkResNode* node, AddonTree addonTree)
+ : base(node, addonTree)
+ {
+ this.component = ((AtkComponentNode*)node)->Component;
+ this.uldManager = &this.component->UldManager;
+ this.NodeType = 0;
+ this.componentType = ((AtkUldComponentInfo*)this.uldManager->Objects)->ComponentType;
+ }
+
+ ///
+ private protected override string GetHeaderText()
+ {
+ var childCount = (int)this.uldManager->NodeListCount;
+ return $"{this.componentType} Component Node{(childCount > 0 ? $" [+{childCount}]" : string.Empty)} (Node: {(nint)this.Node:X} / Comp: {(nint)this.component:X})";
+ }
+
+ ///
+ private protected override void PrintNodeObject()
+ {
+ base.PrintNodeObject();
+ this.PrintComponentObject();
+ ImGui.SameLine();
+ ImGui.NewLine();
+ this.PrintComponentDataObject();
+ ImGui.SameLine();
+ ImGui.NewLine();
+ }
+
+ ///
+ private protected override void PrintChildNodes()
+ {
+ base.PrintChildNodes();
+ var count = this.uldManager->NodeListCount;
+ PrintNodeListAsTree(this.uldManager->NodeList, count, $"Node List [{count}]:", this.AddonTree, new(0f, 0.5f, 0.8f, 1f));
+ }
+
+ ///
+ private protected override void PrintFieldNames()
+ {
+ this.PrintFieldName((nint)this.Node, new(0, 0.85F, 1, 1));
+ this.PrintFieldName((nint)this.component, new(0f, 0.5f, 0.8f, 1f));
+ }
+
+ ///
+ private protected override void PrintFieldsForNodeType(bool isEditorOpen = false)
+ {
+ if (this.component == null)
+ {
+ return;
+ }
+
+ switch (this.componentType)
+ {
+ case TextInput:
+ var textInputComponent = (AtkComponentTextInput*)this.component;
+ ImGui.Text(
+ $"InputBase Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.UnkText1.StringPtr))}");
+ ImGui.Text(
+ $"InputBase Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.UnkText2.StringPtr))}");
+ ImGui.Text(
+ $"Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText01.StringPtr))}");
+ ImGui.Text(
+ $"Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText02.StringPtr))}");
+ ImGui.Text(
+ $"Text3: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText03.StringPtr))}");
+ ImGui.Text(
+ $"Text4: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText04.StringPtr))}");
+ ImGui.Text(
+ $"Text5: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText05.StringPtr))}");
+ break;
+ case List:
+ case TreeList:
+ var l = (AtkComponentList*)this.component;
+ if (ImGui.SmallButton("Inc.Selected"))
+ {
+ l->SelectedItemIndex++;
+ }
+
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void PrintComponentObject()
+ {
+ PrintFieldValuePair("Component", $"{(nint)this.component:X}");
+
+ ImGui.SameLine();
+
+ switch (this.componentType)
+ {
+ case Button:
+ ShowStruct((AtkComponentButton*)this.component);
+ break;
+ case Slider:
+ ShowStruct((AtkComponentSlider*)this.component);
+ break;
+ case Window:
+ ShowStruct((AtkComponentWindow*)this.component);
+ break;
+ case CheckBox:
+ ShowStruct((AtkComponentCheckBox*)this.component);
+ break;
+ case GaugeBar:
+ ShowStruct((AtkComponentGaugeBar*)this.component);
+ break;
+ case RadioButton:
+ ShowStruct((AtkComponentRadioButton*)this.component);
+ break;
+ case TextInput:
+ ShowStruct((AtkComponentTextInput*)this.component);
+ break;
+ case Icon:
+ ShowStruct((AtkComponentIcon*)this.component);
+ break;
+ case NumericInput:
+ ShowStruct((AtkComponentNumericInput*)this.component);
+ break;
+ case List:
+ ShowStruct((AtkComponentList*)this.component);
+ break;
+ case TreeList:
+ ShowStruct((AtkComponentTreeList*)this.component);
+ break;
+ case DropDownList:
+ ShowStruct((AtkComponentDropDownList*)this.component);
+ break;
+ case ScrollBar:
+ ShowStruct((AtkComponentScrollBar*)this.component);
+ break;
+ case ListItemRenderer:
+ ShowStruct((AtkComponentListItemRenderer*)this.component);
+ break;
+ case IconText:
+ ShowStruct((AtkComponentIconText*)this.component);
+ break;
+ case ComponentType.DragDrop:
+ ShowStruct((AtkComponentDragDrop*)this.component);
+ break;
+ case GuildLeveCard:
+ ShowStruct((AtkComponentGuildLeveCard*)this.component);
+ break;
+ case TextNineGrid:
+ ShowStruct((AtkComponentTextNineGrid*)this.component);
+ break;
+ case JournalCanvas:
+ ShowStruct((AtkComponentJournalCanvas*)this.component);
+ break;
+ case HoldButton:
+ ShowStruct((AtkComponentHoldButton*)this.component);
+ break;
+ case Portrait:
+ ShowStruct((AtkComponentPortrait*)this.component);
+ break;
+ default:
+ ShowStruct(this.component);
+ break;
+ }
+ }
+
+ private void PrintComponentDataObject()
+ {
+ var componentData = this.component->UldManager.ComponentData;
+ PrintFieldValuePair("Data", $"{(nint)componentData:X}");
+
+ if (componentData != null)
+ {
+ ImGui.SameLine();
+ switch (this.componentType)
+ {
+ case Base:
+ ShowStruct(componentData);
+ break;
+ case Button:
+ ShowStruct((AtkUldComponentDataButton*)componentData);
+ break;
+ case Window:
+ ShowStruct((AtkUldComponentDataWindow*)componentData);
+ break;
+ case CheckBox:
+ ShowStruct((AtkUldComponentDataCheckBox*)componentData);
+ break;
+ case RadioButton:
+ ShowStruct((AtkUldComponentDataRadioButton*)componentData);
+ break;
+ case GaugeBar:
+ ShowStruct((AtkUldComponentDataGaugeBar*)componentData);
+ break;
+ case Slider:
+ ShowStruct((AtkUldComponentDataSlider*)componentData);
+ break;
+ case TextInput:
+ ShowStruct((AtkUldComponentDataTextInput*)componentData);
+ break;
+ case NumericInput:
+ ShowStruct((AtkUldComponentDataNumericInput*)componentData);
+ break;
+ case List:
+ ShowStruct((AtkUldComponentDataList*)componentData);
+ break;
+ case DropDownList:
+ ShowStruct((AtkUldComponentDataDropDownList*)componentData);
+ break;
+ case Tab:
+ ShowStruct((AtkUldComponentDataTab*)componentData);
+ break;
+ case TreeList:
+ ShowStruct((AtkUldComponentDataTreeList*)componentData);
+ break;
+ case ScrollBar:
+ ShowStruct((AtkUldComponentDataScrollBar*)componentData);
+ break;
+ case ListItemRenderer:
+ ShowStruct((AtkUldComponentDataListItemRenderer*)componentData);
+ break;
+ case Icon:
+ ShowStruct((AtkUldComponentDataIcon*)componentData);
+ break;
+ case IconText:
+ ShowStruct((AtkUldComponentDataIconText*)componentData);
+ break;
+ case ComponentType.DragDrop:
+ ShowStruct((AtkUldComponentDataDragDrop*)componentData);
+ break;
+ case GuildLeveCard:
+ ShowStruct((AtkUldComponentDataGuildLeveCard*)componentData);
+ break;
+ case TextNineGrid:
+ ShowStruct((AtkUldComponentDataTextNineGrid*)componentData);
+ break;
+ case JournalCanvas:
+ ShowStruct((AtkUldComponentDataJournalCanvas*)componentData);
+ break;
+ case Multipurpose:
+ ShowStruct((AtkUldComponentDataMultipurpose*)componentData);
+ break;
+ case Map:
+ ShowStruct((AtkUldComponentDataMap*)componentData);
+ break;
+ case Preview:
+ ShowStruct((AtkUldComponentDataPreview*)componentData);
+ break;
+ case HoldButton:
+ ShowStruct((AtkUldComponentDataHoldButton*)componentData);
+ break;
+ case Portrait:
+ ShowStruct((AtkUldComponentDataPortrait*)componentData);
+ break;
+ default:
+ ShowStruct(componentData);
+ break;
+ }
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Counter.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Counter.cs
new file mode 100644
index 000000000..ff40db37a
--- /dev/null
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Counter.cs
@@ -0,0 +1,36 @@
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui;
+using static Dalamud.Utility.Util;
+
+namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
+
+///
+/// A tree for an that can be printed and browsed via ImGui.
+///
+internal unsafe partial class CounterNodeTree : ResNodeTree
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The node to create a tree for.
+ /// The tree representing the containing addon.
+ internal CounterNodeTree(AtkResNode* node, AddonTree addonTree)
+ : base(node, addonTree)
+ {
+ }
+
+ private AtkCounterNode* CntNode => (AtkCounterNode*)this.Node;
+
+ ///
+ private protected override void PrintNodeObject() => ShowStruct(this.CntNode);
+
+ ///
+ private protected override void PrintFieldsForNodeType(bool isEditorOpen = false)
+ {
+ if (!isEditorOpen)
+ {
+ PrintFieldValuePairs(("Text", ((AtkCounterNode*)this.Node)->NodeText.ToString()));
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs
new file mode 100644
index 000000000..ad7c09165
--- /dev/null
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs
@@ -0,0 +1,384 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+
+using Dalamud.Interface.Internal.UiDebug2.Utility;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using ImGuiNET;
+
+using static Dalamud.Interface.ColorHelpers;
+using static Dalamud.Interface.FontAwesomeIcon;
+using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui;
+using static Dalamud.Interface.Utility.ImGuiHelpers;
+using static ImGuiNET.ImGuiColorEditFlags;
+using static ImGuiNET.ImGuiInputTextFlags;
+using static ImGuiNET.ImGuiTableColumnFlags;
+using static ImGuiNET.ImGuiTableFlags;
+
+namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
+
+///
+internal unsafe partial class ResNodeTree
+{
+ ///
+ /// Sets up the table for the node editor, if the "Edit" checkbox is ticked.
+ ///
+ private protected void DrawNodeEditorTable()
+ {
+ ImGui.BeginTable($"###Editor{(nint)this.Node}", 2, SizingStretchProp | NoHostExtendX);
+
+ this.DrawEditorRows();
+
+ ImGui.EndTable();
+ }
+
+ ///
+ /// Draws each row in the node editor table.
+ ///
+ private protected virtual void DrawEditorRows()
+ {
+ var pos = new Vector2(this.Node->X, this.Node->Y);
+ var size = new Vector2(this.Node->Width, this.Node->Height);
+ var scale = new Vector2(this.Node->ScaleX, this.Node->ScaleY);
+ var origin = new Vector2(this.Node->OriginX, this.Node->OriginY);
+ var angle = (float)((this.Node->Rotation * (180 / Math.PI)) + 360);
+
+ var rgba = RgbaUintToVector4(this.Node->Color.RGBA);
+ var mult = new Vector3(this.Node->MultiplyRed, this.Node->MultiplyGreen, this.Node->MultiplyBlue) / 255f;
+ var add = new Vector3(this.Node->AddRed, this.Node->AddGreen, this.Node->AddBlue);
+
+ var hov = false;
+
+ ImGui.TableSetupColumn("Labels", WidthFixed);
+ ImGui.TableSetupColumn("Editors", WidthFixed);
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.Text("Position:");
+
+ ImGui.TableNextColumn();
+ ImGui.SetNextItemWidth(150);
+ if (ImGui.DragFloat2($"##{(nint)this.Node:X}position", ref pos, 1, default, default, "%.0f"))
+ {
+ this.Node->X = pos.X;
+ this.Node->Y = pos.Y;
+ this.Node->DrawFlags |= 0xD;
+ }
+
+ hov |= SplitTooltip("X", "Y") || ImGui.IsItemActive();
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.Text("Size:");
+ ImGui.TableNextColumn();
+ ImGui.SetNextItemWidth(150);
+ if (ImGui.DragFloat2($"##{(nint)this.Node:X}size", ref size, 1, 0, default, "%.0f"))
+ {
+ this.Node->Width = (ushort)Math.Max(size.X, 0);
+ this.Node->Height = (ushort)Math.Max(size.Y, 0);
+ this.Node->DrawFlags |= 0xD;
+ }
+
+ hov |= SplitTooltip("Width", "Height") || ImGui.IsItemActive();
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.Text("Scale:");
+ ImGui.TableNextColumn();
+ ImGui.SetNextItemWidth(150);
+ if (ImGui.DragFloat2($"##{(nint)this.Node:X}scale", ref scale, 0.05f))
+ {
+ this.Node->ScaleX = scale.X;
+ this.Node->ScaleY = scale.Y;
+ this.Node->DrawFlags |= 0xD;
+ }
+
+ hov |= SplitTooltip("ScaleX", "ScaleY") || ImGui.IsItemActive();
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.Text("Origin:");
+ ImGui.TableNextColumn();
+ ImGui.SetNextItemWidth(150);
+ if (ImGui.DragFloat2($"##{(nint)this.Node:X}origin", ref origin, 1, default, default, "%.0f"))
+ {
+ this.Node->OriginX = origin.X;
+ this.Node->OriginY = origin.Y;
+ this.Node->DrawFlags |= 0xD;
+ }
+
+ hov |= SplitTooltip("OriginX", "OriginY") || ImGui.IsItemActive();
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.Text("Rotation:");
+ ImGui.TableNextColumn();
+ ImGui.SetNextItemWidth(150);
+ while (angle > 180)
+ {
+ angle -= 360;
+ }
+
+ if (ImGui.DragFloat($"##{(nint)this.Node:X}rotation", ref angle, 0.05f, default, default, "%.2f°"))
+ {
+ this.Node->Rotation = (float)(angle / (180 / Math.PI));
+ this.Node->DrawFlags |= 0xD;
+ }
+
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip("Rotation (deg)");
+ hov = true;
+ }
+
+ hov |= ImGui.IsItemActive();
+
+ if (hov)
+ {
+ Vector4 brightYellow = new(1, 1, 0.5f, 0.8f);
+ new NodeBounds(this.Node).Draw(brightYellow);
+ new NodeBounds(origin, this.Node).Draw(brightYellow);
+ }
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.Text("RGBA:");
+ ImGui.TableNextColumn();
+ ImGui.SetNextItemWidth(150);
+ if (ImGui.ColorEdit4($"##{(nint)this.Node:X}RGBA", ref rgba, DisplayHex))
+ {
+ this.Node->Color = new() { RGBA = RgbaVector4ToUint(rgba) };
+ }
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.Text("Multiply:");
+ ImGui.TableNextColumn();
+ ImGui.SetNextItemWidth(150);
+ if (ImGui.ColorEdit3($"##{(nint)this.Node:X}multiplyRGB", ref mult, DisplayHex))
+ {
+ this.Node->MultiplyRed = (byte)(mult.X * 255);
+ this.Node->MultiplyGreen = (byte)(mult.Y * 255);
+ this.Node->MultiplyBlue = (byte)(mult.Z * 255);
+ }
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.Text("Add:");
+ ImGui.TableNextColumn();
+ ImGui.SetNextItemWidth(124);
+
+ if (ImGui.DragFloat3($"##{(nint)this.Node:X}addRGB", ref add, 1, -255, 255, "%.0f"))
+ {
+ this.Node->AddRed = (short)add.X;
+ this.Node->AddGreen = (short)add.Y;
+ this.Node->AddBlue = (short)add.Z;
+ }
+
+ SplitTooltip("+/- Red", "+/- Green", "+/- Blue");
+
+ var addTransformed = (add / 510f) + new Vector3(0.5f);
+
+ ImGui.SameLine();
+ ImGui.SetCursorPosX(ImGui.GetCursorPosX() - (4 * GlobalScale));
+ if (ImGui.ColorEdit3($"##{(nint)this.Node:X}addRGBPicker", ref addTransformed, NoAlpha | NoInputs))
+ {
+ this.Node->AddRed = (short)Math.Floor((addTransformed.X * 510f) - 255f);
+ this.Node->AddGreen = (short)Math.Floor((addTransformed.Y * 510f) - 255f);
+ this.Node->AddBlue = (short)Math.Floor((addTransformed.Z * 510f) - 255f);
+ }
+ }
+}
+
+///
+internal unsafe partial class CounterNodeTree
+{
+ ///
+ private protected override void DrawEditorRows()
+ {
+ base.DrawEditorRows();
+
+ var str = this.CntNode->NodeText.ToString();
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.Text("Counter:");
+ ImGui.TableNextColumn();
+
+ ImGui.SetNextItemWidth(150);
+ if (ImGui.InputText($"##{(nint)this.Node:X}counterEdit", ref str, 512, EnterReturnsTrue))
+ {
+ this.CntNode->SetText(str);
+ }
+ }
+}
+
+///
+internal unsafe partial class ImageNodeTree
+{
+ private static int TexDisplayStyle { get; set; }
+
+ ///
+ private protected override void DrawEditorRows()
+ {
+ base.DrawEditorRows();
+
+ var partId = (int)this.PartId;
+ var partcount = this.ImgNode->PartsList->PartCount;
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.Text("Part Id:");
+ ImGui.TableNextColumn();
+ ImGui.SetNextItemWidth(150);
+ if (ImGui.InputInt($"##partId{(nint)this.Node:X}", ref partId, 1, 1))
+ {
+ if (partId < 0)
+ {
+ partId = 0;
+ }
+
+ if (partId >= partcount)
+ {
+ partId = (int)(partcount - 1);
+ }
+
+ this.ImgNode->PartId = (ushort)partId;
+ }
+ }
+}
+
+///
+internal unsafe partial class NineGridNodeTree
+{
+ ///
+ private protected override void DrawEditorRows()
+ {
+ base.DrawEditorRows();
+
+ var lr = new Vector2(this.Offsets.Left, this.Offsets.Right);
+ var tb = new Vector2(this.Offsets.Top, this.Offsets.Bottom);
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.Text("Ninegrid Offsets:");
+ ImGui.TableNextColumn();
+ ImGui.SetNextItemWidth(150);
+ if (ImGui.DragFloat2($"##{(nint)this.Node:X}ngOffsetLR", ref lr, 1, 0))
+ {
+ this.NgNode->LeftOffset = (short)Math.Max(0, lr.X);
+ this.NgNode->RightOffset = (short)Math.Max(0, lr.Y);
+ }
+
+ SplitTooltip("Left", "Right");
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.TableNextColumn();
+ ImGui.SetNextItemWidth(150);
+ if (ImGui.DragFloat2($"##{(nint)this.Node:X}ngOffsetTB", ref tb, 1, 0))
+ {
+ this.NgNode->TopOffset = (short)Math.Max(0, tb.X);
+ this.NgNode->BottomOffset = (short)Math.Max(0, tb.Y);
+ }
+
+ SplitTooltip("Top", "Bottom");
+ }
+}
+
+///
+internal unsafe partial class TextNodeTree
+{
+ private static readonly List FontList = Enum.GetValues().ToList();
+
+ private static readonly string[] FontNames = Enum.GetNames();
+
+ ///
+ private protected override void DrawEditorRows()
+ {
+ base.DrawEditorRows();
+
+ var text = this.TxtNode->NodeText.ToString();
+ var fontIndex = FontList.IndexOf(this.TxtNode->FontType);
+ int fontSize = this.TxtNode->FontSize;
+ var alignment = this.TxtNode->AlignmentType;
+ var textColor = RgbaUintToVector4(this.TxtNode->TextColor.RGBA);
+ var edgeColor = RgbaUintToVector4(this.TxtNode->EdgeColor.RGBA);
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.Text("Text:");
+ ImGui.TableNextColumn();
+ ImGui.SetNextItemWidth(Math.Max(ImGui.GetWindowContentRegionMax().X - ImGui.GetCursorPosX() - 50f, 150));
+ if (ImGui.InputText($"##{(nint)this.Node:X}textEdit", ref text, 512, EnterReturnsTrue))
+ {
+ this.TxtNode->SetText(text);
+ }
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.Text("Font:");
+ ImGui.TableNextColumn();
+ ImGui.SetNextItemWidth(150);
+ if (ImGui.Combo($"##{(nint)this.Node:X}fontType", ref fontIndex, FontNames, FontList.Count))
+ {
+ this.TxtNode->FontType = FontList[fontIndex];
+ }
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.Text("Font Size:");
+ ImGui.TableNextColumn();
+ ImGui.SetNextItemWidth(150);
+ if (ImGui.InputInt($"##{(nint)this.Node:X}fontSize", ref fontSize, 1, 10))
+ {
+ this.TxtNode->FontSize = (byte)fontSize;
+ }
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.Text("Alignment:");
+ ImGui.TableNextColumn();
+ if (InputAlignment($"##{(nint)this.Node:X}alignment", ref alignment))
+ {
+ this.TxtNode->AlignmentType = alignment;
+ }
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.Text("Text Color:");
+ ImGui.TableNextColumn();
+ ImGui.SetNextItemWidth(150);
+ if (ImGui.ColorEdit4($"##{(nint)this.Node:X}TextRGB", ref textColor, DisplayHex))
+ {
+ this.TxtNode->TextColor = new() { RGBA = RgbaVector4ToUint(textColor) };
+ }
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.Text("Edge Color:");
+ ImGui.TableNextColumn();
+ ImGui.SetNextItemWidth(150);
+ if (ImGui.ColorEdit4($"##{(nint)this.Node:X}EdgeRGB", ref edgeColor, DisplayHex))
+ {
+ this.TxtNode->EdgeColor = new() { RGBA = RgbaVector4ToUint(edgeColor) };
+ }
+ }
+
+ private static bool InputAlignment(string label, ref AlignmentType alignment)
+ {
+ var hAlign = (int)alignment % 3;
+ var vAlign = ((int)alignment - hAlign) / 3;
+
+ var hAlignInput = IconSelectInput($"{label}H", ref hAlign, new() { 0, 1, 2 }, new() { AlignLeft, AlignCenter, AlignRight });
+ var vAlignInput = IconSelectInput($"{label}V", ref vAlign, new() { 0, 1, 2 }, new() { ArrowsUpToLine, GripLines, ArrowsDownToLine });
+
+ if (hAlignInput || vAlignInput)
+ {
+ alignment = (AlignmentType)((vAlign * 3) + hAlign);
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs
new file mode 100644
index 000000000..f3cc69618
--- /dev/null
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs
@@ -0,0 +1,317 @@
+using System.Numerics;
+using System.Runtime.InteropServices;
+
+using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using ImGuiNET;
+
+using static Dalamud.Interface.ColorHelpers;
+using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui;
+using static Dalamud.Utility.Util;
+using static FFXIVClientStructs.FFXIV.Component.GUI.TextureType;
+using static ImGuiNET.ImGuiTableColumnFlags;
+using static ImGuiNET.ImGuiTableFlags;
+
+namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
+
+///
+/// A tree for an that can be printed and browsed via ImGui.
+///
+internal unsafe partial class ImageNodeTree : ResNodeTree
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The node to create a tree for.
+ /// The tree representing the containing addon.
+ internal ImageNodeTree(AtkResNode* node, AddonTree addonTree)
+ : base(node, addonTree)
+ {
+ }
+
+ ///
+ /// Gets the part ID that this node uses.
+ ///
+ private protected virtual uint PartId => this.ImgNode->PartId;
+
+ ///
+ /// Gets the parts list that this node uses.
+ ///
+ private protected virtual AtkUldPartsList* PartsList => this.ImgNode->PartsList;
+
+ ///
+ /// Gets or sets a summary of pertinent data about this 's texture. Updated each time is called.
+ ///
+ private protected TextureData TexData { get; set; }
+
+ private AtkImageNode* ImgNode => (AtkImageNode*)this.Node;
+
+ ///
+ /// Draws the texture inside the window, in either of two styles.
+ /// Full Imagepresents the texture in full as a spritesheet.
+ /// Parts Listpresents the individual parts as rows in a table.
+ ///
+ private protected void DrawTextureAndParts()
+ {
+ this.TexData = new TextureData(this.PartsList, this.PartId);
+
+ if (this.TexData.Texture == null)
+ {
+ return;
+ }
+
+ if (NestedTreePush($"Texture##texture{(nint)this.TexData.Texture->D3D11ShaderResourceView:X}", out _))
+ {
+ PrintFieldValuePairs(
+ ("Texture Type", $"{this.TexData.TexType}"),
+ ("Part ID", $"{this.TexData.PartId}"),
+ ("Part Count", $"{this.TexData.PartCount}"));
+
+ if (this.TexData.Path != null)
+ {
+ PrintFieldValuePairs(("Texture Path", this.TexData.Path));
+ }
+
+ if (ImGui.RadioButton("Full Image##textureDisplayStyle0", TexDisplayStyle == 0))
+ {
+ TexDisplayStyle = 0;
+ }
+
+ ImGui.SameLine();
+ if (ImGui.RadioButton("Parts List##textureDisplayStyle1", TexDisplayStyle == 1))
+ {
+ TexDisplayStyle = 1;
+ }
+
+ ImGui.NewLine();
+
+ if (TexDisplayStyle == 1)
+ {
+ this.PrintPartsTable();
+ }
+ else
+ {
+ this.DrawFullTexture();
+ }
+
+ ImGui.TreePop();
+ }
+ }
+
+ ///
+ /// Draws an outline of a given part within the texture.
+ ///
+ /// The part ID.
+ /// The absolute position of the cursor onscreen.
+ /// The relative position of the cursor within the window.
+ /// The color of the outline.
+ /// Whether this outline requires the user to mouse over it.
+ private protected virtual void DrawPartOutline(uint partId, Vector2 cursorScreenPos, Vector2 cursorLocalPos, Vector4 col, bool reqHover = false)
+ {
+ var part = this.TexData.PartsList->Parts[partId];
+
+ var hrFactor = this.TexData.HiRes ? 2f : 1f;
+
+ var uv = new Vector2(part.U, part.V) * hrFactor;
+ var wh = new Vector2(part.Width, part.Height) * hrFactor;
+
+ var partBegin = cursorScreenPos + uv;
+ var partEnd = partBegin + wh;
+
+ if (reqHover && !ImGui.IsMouseHoveringRect(partBegin, partEnd))
+ {
+ return;
+ }
+
+ var savePos = ImGui.GetCursorPos();
+
+ ImGui.GetWindowDrawList().AddRect(partBegin, partEnd, RgbaVector4ToUint(col));
+
+ ImGui.SetCursorPos(cursorLocalPos + uv + new Vector2(0, -20));
+ ImGui.TextColored(col, $"[#{partId}]\t{part.U}, {part.V}\t{part.Width}x{part.Height}");
+ ImGui.SetCursorPos(savePos);
+ }
+
+ ///
+ private protected override void PrintNodeObject() => ShowStruct(this.ImgNode);
+
+ ///
+ private protected override void PrintFieldsForNodeType(bool isEditorOpen = false)
+ {
+ PrintFieldValuePairs(
+ ("Wrap", $"{this.ImgNode->WrapMode}"),
+ ("Image Flags", $"0x{this.ImgNode->Flags:X}"));
+ this.DrawTextureAndParts();
+ }
+
+ private static void PrintPartCoords(float u, float v, float w, float h, bool asFloat = false, bool lineBreak = false)
+ {
+ ImGui.TextDisabled($"{u}, {v},{(lineBreak ? "\n" : " ")}{w}, {h}");
+
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip("Click to copy as Vector2\nShift-click to copy as Vector4");
+ }
+
+ var suffix = asFloat ? "f" : string.Empty;
+
+ if (ImGui.IsItemClicked())
+ {
+ ImGui.SetClipboardText(ImGui.IsKeyDown(ImGuiKey.ModShift)
+ ? $"new Vector4({u}{suffix}, {v}{suffix}, {w}{suffix}, {h}{suffix})"
+ : $"new Vector2({u}{suffix}, {v}{suffix});\nnew Vector2({w}{suffix}, {h}{suffix})");
+ }
+ }
+
+ private void DrawFullTexture()
+ {
+ var cursorScreenPos = ImGui.GetCursorScreenPos();
+ var cursorLocalPos = ImGui.GetCursorPos();
+
+ ImGui.Image(new(this.TexData.Texture->D3D11ShaderResourceView), new(this.TexData.Texture->Width, this.TexData.Texture->Height));
+
+ for (uint p = 0; p < this.TexData.PartsList->PartCount; p++)
+ {
+ if (p == this.TexData.PartId)
+ {
+ continue;
+ }
+
+ this.DrawPartOutline(p, cursorScreenPos, cursorLocalPos, new(0.6f, 0.6f, 0.6f, 1), true);
+ }
+
+ this.DrawPartOutline(this.TexData.PartId, cursorScreenPos, cursorLocalPos, new(0, 0.85F, 1, 1));
+ }
+
+ private void PrintPartsTable()
+ {
+ ImGui.BeginTable($"partsTable##{(nint)this.TexData.Texture->D3D11ShaderResourceView:X}", 3, Borders | RowBg | Reorderable);
+ ImGui.TableSetupColumn("Part ID", WidthFixed);
+ ImGui.TableSetupColumn("Part Texture", WidthFixed);
+ ImGui.TableSetupColumn("Coordinates", WidthFixed);
+
+ ImGui.TableHeadersRow();
+
+ var tWidth = this.TexData.Texture->Width;
+ var tHeight = this.TexData.Texture->Height;
+ var textureSize = new Vector2(tWidth, tHeight);
+
+ for (ushort i = 0; i < this.TexData.PartCount; i++)
+ {
+ ImGui.TableNextColumn();
+
+ if (i == this.TexData.PartId)
+ {
+ ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0, 0.85F, 1, 1));
+ }
+
+ ImGui.Text($"#{i.ToString().PadLeft(this.TexData.PartCount.ToString().Length, '0')}");
+
+ if (i == this.TexData.PartId)
+ {
+ ImGui.PopStyleColor(1);
+ }
+
+ ImGui.TableNextColumn();
+
+ var part = this.TexData.PartsList->Parts[i];
+ var hiRes = this.TexData.HiRes;
+
+ var u = hiRes ? part.U * 2f : part.U;
+ var v = hiRes ? part.V * 2f : part.V;
+ var width = hiRes ? part.Width * 2f : part.Width;
+ var height = hiRes ? part.Height * 2f : part.Height;
+
+ ImGui.Image(new(this.TexData.Texture->D3D11ShaderResourceView), new(width, height), new Vector2(u, v) / textureSize, new Vector2(u + width, v + height) / textureSize);
+
+ ImGui.TableNextColumn();
+
+ ImGui.TextColored(!hiRes ? new(1) : new(0.6f, 0.6f, 0.6f, 1), "Standard:\t");
+ ImGui.SameLine();
+ var cursX = ImGui.GetCursorPosX();
+
+ PrintPartCoords(u / 2f, v / 2f, width / 2f, height / 2f);
+
+ ImGui.TextColored(hiRes ? new(1) : new(0.6f, 0.6f, 0.6f, 1), "Hi-Res:\t");
+ ImGui.SameLine();
+ ImGui.SetCursorPosX(cursX);
+
+ PrintPartCoords(u, v, width, height);
+
+ ImGui.Text("UV:\t");
+ ImGui.SameLine();
+ ImGui.SetCursorPosX(cursX);
+
+ PrintPartCoords(u / tWidth, v / tWidth, (u + width) / tWidth, (v + height) / tHeight, true, true);
+ }
+
+ ImGui.EndTable();
+ }
+
+ ///
+ /// A summary of pertinent data about a node's texture.
+ ///
+ protected struct TextureData
+ {
+ /// The texture's partslist.
+ public AtkUldPartsList* PartsList;
+
+ /// The number of parts in the texture.
+ public uint PartCount;
+
+ /// The part ID the node is using.
+ public uint PartId;
+
+ /// The texture itself.
+ public Texture* Texture = null;
+
+ /// The type of texture.
+ public TextureType TexType = 0;
+
+ /// The texture's file path (if , otherwise this value is null).
+ public string? Path = null;
+
+ /// Whether this is a high-resolution texture.
+ public bool HiRes = false;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The texture's parts list.
+ /// The part ID being used by the node.
+ public TextureData(AtkUldPartsList* partsList, uint partId)
+ {
+ this.PartsList = partsList;
+ this.PartCount = this.PartsList->PartCount;
+ this.PartId = partId >= this.PartCount ? 0 : partId;
+
+ if (this.PartsList == null)
+ {
+ return;
+ }
+
+ var asset = this.PartsList->Parts[this.PartId].UldAsset;
+
+ if (asset == null)
+ {
+ return;
+ }
+
+ this.TexType = asset->AtkTexture.TextureType;
+
+ if (this.TexType == Resource)
+ {
+ var resource = asset->AtkTexture.Resource;
+ this.Texture = resource->KernelTextureObject;
+ this.Path = Marshal.PtrToStringAnsi(new(resource->TexFileResourceHandle->ResourceHandle.FileName.BufferPtr));
+ }
+ else
+ {
+ this.Texture = this.TexType == KernelTexture ? asset->AtkTexture.KernelTexture : null;
+ this.Path = null;
+ }
+
+ this.HiRes = this.Path?.Contains("_hr1") ?? false;
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.Offsets.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.Offsets.cs
new file mode 100644
index 000000000..237303155
--- /dev/null
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.Offsets.cs
@@ -0,0 +1,69 @@
+using System.Numerics;
+
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui;
+
+namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
+
+///
+internal unsafe partial class NineGridNodeTree
+{
+ ///
+ /// A struct representing the four offsets of an .
+ ///
+ internal struct NineGridOffsets
+ {
+ /// Top offset.
+ internal int Top;
+
+ /// Left offset.
+ internal int Left;
+
+ /// Right offset.
+ internal int Right;
+
+ /// Bottom offset.
+ internal int Bottom;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The top offset.
+ /// The right offset.
+ /// The bottom offset.
+ /// The left offset.
+ internal NineGridOffsets(int top, int right, int bottom, int left)
+ {
+ this.Top = top;
+ this.Right = right;
+ this.Left = left;
+ this.Bottom = bottom;
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The node using these offsets.
+ internal NineGridOffsets(AtkNineGridNode* ngNode)
+ : this(ngNode->TopOffset, ngNode->RightOffset, ngNode->BottomOffset, ngNode->LeftOffset)
+ {
+ }
+
+ private NineGridOffsets(Vector4 v)
+ : this((int)v.X, (int)v.Y, (int)v.Z, (int)v.W)
+ {
+ }
+
+ public static implicit operator NineGridOffsets(Vector4 v) => new(v);
+
+ public static implicit operator Vector4(NineGridOffsets v) => new(v.Top, v.Right, v.Bottom, v.Left);
+
+ public static NineGridOffsets operator *(float n, NineGridOffsets a) => n * (Vector4)a;
+
+ public static NineGridOffsets operator *(NineGridOffsets a, float n) => n * a;
+
+ /// Prints the offsets in ImGui.
+ internal readonly void Print() => PrintFieldValuePairs(("Top", $"{this.Top}"), ("Bottom", $"{this.Bottom}"), ("Left", $"{this.Left}"), ("Right", $"{this.Right}"));
+ }
+}
diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.cs
new file mode 100644
index 000000000..3c66d44c3
--- /dev/null
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.cs
@@ -0,0 +1,89 @@
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using ImGuiNET;
+
+using static Dalamud.Interface.ColorHelpers;
+using static Dalamud.Utility.Util;
+
+using Vector2 = System.Numerics.Vector2;
+using Vector4 = System.Numerics.Vector4;
+
+namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
+
+///
+/// A tree for an that can be printed and browsed via ImGui.
+///
+internal unsafe partial class NineGridNodeTree : ImageNodeTree
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The node to create a tree for.
+ /// The tree representing the containing addon.
+ internal NineGridNodeTree(AtkResNode* node, AddonTree addonTree)
+ : base(node, addonTree)
+ {
+ }
+
+ ///
+ private protected override uint PartId => this.NgNode->PartId;
+
+ ///
+ private protected override AtkUldPartsList* PartsList => this.NgNode->PartsList;
+
+ private AtkNineGridNode* NgNode => (AtkNineGridNode*)this.Node;
+
+ private NineGridOffsets Offsets => new(this.NgNode);
+
+ ///
+ private protected override void DrawPartOutline(
+ uint partId, Vector2 cursorScreenPos, Vector2 cursorLocalPos, Vector4 col, bool reqHover = false)
+ {
+ var part = this.TexData.PartsList->Parts[partId];
+
+ var hrFactor = this.TexData.HiRes ? 2f : 1f;
+ var uv = new Vector2(part.U, part.V) * hrFactor;
+ var wh = new Vector2(part.Width, part.Height) * hrFactor;
+ var partBegin = cursorScreenPos + uv;
+ var partEnd = cursorScreenPos + uv + wh;
+
+ var savePos = ImGui.GetCursorPos();
+
+ if (!reqHover || ImGui.IsMouseHoveringRect(partBegin, partEnd))
+ {
+ var adjustedOffsets = this.Offsets * hrFactor;
+ var ngBegin1 = partBegin with { X = partBegin.X + adjustedOffsets.Left };
+ var ngEnd1 = partEnd with { X = partEnd.X - adjustedOffsets.Right };
+
+ var ngBegin2 = partBegin with { Y = partBegin.Y + adjustedOffsets.Top };
+ var ngEnd2 = partEnd with { Y = partEnd.Y - adjustedOffsets.Bottom };
+
+ var ngCol = RgbaVector4ToUint(col with { W = 0.75f * col.W });
+
+ ImGui.GetWindowDrawList()
+ .AddRect(partBegin, partEnd, RgbaVector4ToUint(col));
+ ImGui.GetWindowDrawList().AddRect(ngBegin1, ngEnd1, ngCol);
+ ImGui.GetWindowDrawList().AddRect(ngBegin2, ngEnd2, ngCol);
+
+ ImGui.SetCursorPos(cursorLocalPos + uv + new Vector2(0, -20));
+ ImGui.TextColored(col, $"[#{partId}]\t{part.U}, {part.V}\t{part.Width}x{part.Height}");
+ }
+
+ ImGui.SetCursorPos(savePos);
+ }
+
+ ///
+ private protected override void PrintNodeObject() => ShowStruct(this.NgNode);
+
+ ///
+ private protected override void PrintFieldsForNodeType(bool isEditorOpen = false)
+ {
+ if (!isEditorOpen)
+ {
+ ImGui.Text("NineGrid Offsets:\t");
+ ImGui.SameLine();
+ this.Offsets.Print();
+ }
+
+ this.DrawTextureAndParts();
+ }
+}
diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs
new file mode 100644
index 000000000..222ca30d4
--- /dev/null
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs
@@ -0,0 +1,402 @@
+using System.Linq;
+using System.Numerics;
+
+using Dalamud.Interface.Components;
+using Dalamud.Interface.Internal.UiDebug2.Utility;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using ImGuiNET;
+
+using static Dalamud.Interface.ColorHelpers;
+using static Dalamud.Interface.FontAwesomeIcon;
+using static Dalamud.Interface.Internal.UiDebug2.Browsing.Events;
+using static Dalamud.Interface.Internal.UiDebug2.ElementSelector;
+using static Dalamud.Interface.Internal.UiDebug2.UiDebug2;
+using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui;
+using static Dalamud.Utility.Util;
+using static FFXIVClientStructs.FFXIV.Component.GUI.NodeFlags;
+
+namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
+
+///
+/// A tree for an that can be printed and browsed via ImGui.
+///
+/// As with the structs they represent, this class serves as the base class for other types of NodeTree.
+internal unsafe partial class ResNodeTree : IDisposable
+{
+ private NodePopoutWindow? window;
+
+ private bool editorOpen;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The node to create a tree for.
+ /// The tree representing the containing addon.
+ private protected ResNodeTree(AtkResNode* node, AddonTree addonTree)
+ {
+ this.Node = node;
+ this.AddonTree = addonTree;
+ this.NodeType = node->Type;
+ this.AddonTree.NodeTrees.Add((nint)this.Node, this);
+ }
+
+ ///
+ /// Gets or sets the this tree represents.
+ ///
+ protected internal AtkResNode* Node { get; set; }
+
+ ///
+ /// Gets the containing this tree.
+ ///
+ protected internal AddonTree AddonTree { get; private set; }
+
+ ///
+ /// Gets this node's type.
+ ///
+ private protected NodeType NodeType { get; init; }
+
+ ///
+ /// Clears this NodeTree's popout window, if it has one.
+ ///
+ public void Dispose()
+ {
+ if (this.window != null && PopoutWindows.Windows.Contains(this.window))
+ {
+ PopoutWindows.RemoveWindow(this.window);
+ this.window.Dispose();
+ }
+ }
+
+ ///
+ /// Gets an instance of (or one of its inheriting types) for the given node. If no instance exists, one is created.
+ ///
+ /// The node to get a tree for.
+ /// The tree for the node's containing addon.
+ /// An existing or newly-created instance of .
+ internal static ResNodeTree GetOrCreate(AtkResNode* node, AddonTree addonTree) =>
+ addonTree.NodeTrees.TryGetValue((nint)node, out var nodeTree) ? nodeTree
+ : (int)node->Type > 1000
+ ? new ComponentNodeTree(node, addonTree)
+ : node->Type switch
+ {
+ NodeType.Text => new TextNodeTree(node, addonTree),
+ NodeType.Image => new ImageNodeTree(node, addonTree),
+ NodeType.NineGrid => new NineGridNodeTree(node, addonTree),
+ NodeType.ClippingMask => new ClippingMaskNodeTree(node, addonTree),
+ NodeType.Counter => new CounterNodeTree(node, addonTree),
+ NodeType.Collision => new CollisionNodeTree(node, addonTree),
+ _ => new ResNodeTree(node, addonTree),
+ };
+
+ ///
+ /// Prints a list of NodeTree for a given list of nodes.
+ ///
+ /// The address of the start of the list.
+ /// The number of nodes in the list.
+ /// The tree for the containing addon.
+ internal static void PrintNodeList(AtkResNode** nodeList, int count, AddonTree addonTree)
+ {
+ for (uint j = 0; j < count; j++)
+ {
+ GetOrCreate(nodeList[j], addonTree).Print(j);
+ }
+ }
+
+ ///
+ /// Calls , but outputs the results as a collapsible tree.
+ ///
+ /// The address of the start of the list.
+ /// The number of nodes in the list.
+ /// The heading text of the tree.
+ /// The tree for the containing addon.
+ /// The text color of the heading.
+ internal static void PrintNodeListAsTree(AtkResNode** nodeList, int count, string label, AddonTree addonTree, Vector4 color)
+ {
+ if (count <= 0)
+ {
+ return;
+ }
+
+ ImGui.PushStyleColor(ImGuiCol.Text, color);
+ var treeOpened = NestedTreePush($"{label}##{(nint)nodeList:X}", color, out var lineStart);
+ ImGui.PopStyleColor();
+
+ if (treeOpened)
+ {
+ PrintNodeList(nodeList, count, addonTree);
+ NestedTreePop(lineStart, color);
+ }
+ }
+
+ ///
+ /// Prints this tree in the window.
+ ///
+ /// The index of the tree within its containing node or addon, if applicable.
+ /// Whether the tree should default to being open.
+ internal void Print(uint? index, bool forceOpen = false)
+ {
+ if (SearchResults.Length > 0 && SearchResults[0] == (nint)this.Node)
+ {
+ this.PrintWithHighlights(index);
+ }
+ else
+ {
+ this.PrintTree(index, forceOpen);
+ }
+ }
+
+ ///
+ /// Prints out the tree's header text.
+ ///
+ internal void WriteTreeHeading()
+ {
+ ImGui.Text(this.GetHeaderText());
+ this.PrintFieldNames();
+ }
+
+ ///
+ /// If the given pointer has been identified as a field within the addon struct, this method prints that field's name.
+ ///
+ /// The pointer to check.
+ /// The text color to use.
+ private protected void PrintFieldName(nint ptr, Vector4 color)
+ {
+ if (this.AddonTree.FieldNames.TryGetValue(ptr, out var result))
+ {
+ ImGui.SameLine();
+ ImGui.TextColored(color, string.Join(".", result));
+ }
+ }
+
+ ///
+ /// Builds a string that will serve as the header text for the tree. Indicates the node type, the number of direct children it contains, and its pointer.
+ ///
+ /// The resulting header text string.
+ private protected virtual string GetHeaderText()
+ {
+ var count = this.GetDirectChildCount();
+ return $"{this.NodeType} Node{(count > 0 ? $" [+{count}]" : string.Empty)} ({(nint)this.Node:X})";
+ }
+
+ ///
+ /// Prints the node struct.
+ ///
+ private protected virtual void PrintNodeObject()
+ {
+ ShowStruct(this.Node);
+ ImGui.SameLine();
+ ImGui.NewLine();
+ }
+
+ ///
+ /// Prints any field names for the node.
+ ///
+ private protected virtual void PrintFieldNames() => this.PrintFieldName((nint)this.Node, new(0, 0.85F, 1, 1));
+
+ ///
+ /// Prints all direct children of this node.
+ ///
+ private protected virtual void PrintChildNodes()
+ {
+ var prevNode = this.Node->ChildNode;
+ while (prevNode != null)
+ {
+ GetOrCreate(prevNode, this.AddonTree).Print(null);
+ prevNode = prevNode->PrevSiblingNode;
+ }
+ }
+
+ ///
+ /// Prints any specific fields pertaining to the specific type of node.
+ ///
+ /// Whether the "Edit" box is currently checked.
+ private protected virtual void PrintFieldsForNodeType(bool isEditorOpen = false)
+ {
+ }
+
+ private int GetDirectChildCount()
+ {
+ var count = 0;
+ if (this.Node->ChildNode != null)
+ {
+ count++;
+
+ var prev = this.Node->ChildNode;
+ while (prev->PrevSiblingNode != null)
+ {
+ prev = prev->PrevSiblingNode;
+ count++;
+ }
+ }
+
+ return count;
+ }
+
+ private void PrintWithHighlights(uint? index)
+ {
+ if (!Scrolled)
+ {
+ ImGui.SetScrollHereY();
+ Scrolled = true;
+ }
+
+ var start = ImGui.GetCursorScreenPos() - new Vector2(5);
+ this.PrintTree(index, true);
+ var end = new Vector2(ImGui.GetMainViewport().WorkSize.X, ImGui.GetCursorScreenPos().Y + 5);
+
+ ImGui.GetWindowDrawList().AddRectFilled(start, end, RgbaVector4ToUint(new Vector4(1, 1, 0.2f, 1) { W = Countdown / 200f }));
+ }
+
+ private void PrintTree(uint? index, bool forceOpen = false)
+ {
+ var visible = this.Node->NodeFlags.HasFlag(Visible);
+
+ var displayColor = !visible ? new Vector4(0.8f, 0.8f, 0.8f, 1) :
+ this.Node->Color.A == 0 ? new(0.015f, 0.575f, 0.355f, 1) :
+ new(0.1f, 1f, 0.1f, 1f);
+
+ if (forceOpen || SearchResults.Contains((nint)this.Node))
+ {
+ ImGui.SetNextItemOpen(true, ImGuiCond.Always);
+ }
+
+ ImGui.PushStyleColor(ImGuiCol.Text, displayColor);
+
+ var treePush = NestedTreePush($"{(index == null ? string.Empty : $"[{index}] ")}[#{this.Node->NodeId}]###{(nint)this.Node:X}nodeTree", displayColor, out var lineStart);
+
+ if (ImGui.IsItemHovered())
+ {
+ new NodeBounds(this.Node).Draw(visible ? new(0.1f, 1f, 0.1f, 1f) : new(1f, 0f, 0.2f, 1f));
+ }
+
+ ImGui.SameLine();
+ this.WriteTreeHeading();
+
+ ImGui.PopStyleColor();
+
+ if (treePush)
+ {
+ try
+ {
+ PrintFieldValuePair("Node", $"{(nint)this.Node:X}");
+
+ ImGui.SameLine();
+ this.PrintNodeObject();
+
+ PrintFieldValuePairs(
+ ("NodeID", $"{this.Node->NodeId}"),
+ ("Type", $"{this.Node->Type}"));
+
+ this.DrawBasicControls();
+
+ if (this.editorOpen)
+ {
+ this.DrawNodeEditorTable();
+ }
+ else
+ {
+ this.PrintResNodeFields();
+ }
+
+ this.PrintFieldsForNodeType(this.editorOpen);
+ PrintEvents(this.Node);
+ new TimelineTree(this.Node).Print();
+
+ this.PrintChildNodes();
+ }
+ catch (Exception ex)
+ {
+ ImGui.TextDisabled($"Couldn't display node!\n\n{ex}");
+ }
+
+ NestedTreePop(lineStart, displayColor);
+ }
+ }
+
+ private void DrawBasicControls()
+ {
+ ImGui.SameLine();
+ var y = ImGui.GetCursorPosY();
+
+ ImGui.SetCursorPosY(y - 2);
+ var isVisible = this.Node->NodeFlags.HasFlag(Visible);
+ if (ImGuiComponents.IconButton("vis", isVisible ? Eye : EyeSlash, isVisible ? new Vector4(0.0f, 0.8f, 0.2f, 1f) : new(0.6f, 0.6f, 0.6f, 1)))
+ {
+ if (isVisible)
+ {
+ this.Node->NodeFlags &= ~Visible;
+ }
+ else
+ {
+ this.Node->NodeFlags |= Visible;
+ }
+ }
+
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip("Toggle Visibility");
+ }
+
+ ImGui.SameLine();
+ ImGui.SetCursorPosY(y - 2);
+ ImGui.Checkbox($"Edit###editCheckBox{(nint)this.Node}", ref this.editorOpen);
+
+ ImGui.SameLine();
+ ImGui.SetCursorPosY(y - 2);
+ if (ImGuiComponents.IconButton($"###{(nint)this.Node}popoutButton", this.window?.IsOpen == true ? Times : ArrowUpRightFromSquare, null))
+ {
+ this.TogglePopout();
+ }
+
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip("Toggle Popout Window");
+ }
+ }
+
+ private void TogglePopout()
+ {
+ if (this.window != null)
+ {
+ this.window.IsOpen = !this.window.IsOpen;
+ }
+ else
+ {
+ this.window = new NodePopoutWindow(this, $"{this.AddonTree.AddonName}: {this.GetHeaderText()}###nodePopout{(nint)this.Node}");
+ PopoutWindows.AddWindow(this.window);
+ }
+ }
+
+ private void PrintResNodeFields()
+ {
+ PrintFieldValuePairs(
+ ("X", $"{this.Node->X}"),
+ ("Y", $"{this.Node->Y}"),
+ ("Width", $"{this.Node->Width}"),
+ ("Height", $"{this.Node->Height}"),
+ ("Priority", $"{this.Node->Priority}"),
+ ("Depth", $"{this.Node->Depth}"),
+ ("DrawFlags", $"0x{this.Node->DrawFlags:X}"));
+
+ PrintFieldValuePairs(
+ ("ScaleX", $"{this.Node->ScaleX:F2}"),
+ ("ScaleY", $"{this.Node->ScaleY:F2}"),
+ ("OriginX", $"{this.Node->OriginX}"),
+ ("OriginY", $"{this.Node->OriginY}"),
+ ("Rotation", $"{this.Node->Rotation * (180d / Math.PI):F1}° / {this.Node->Rotation:F7}rad "));
+
+ var color = this.Node->Color;
+ var add = new Vector3(this.Node->AddRed, this.Node->AddGreen, this.Node->AddBlue);
+ var multiply = new Vector3(this.Node->MultiplyRed, this.Node->MultiplyGreen, this.Node->MultiplyBlue);
+
+ PrintColor(RgbaUintToVector4(color.RGBA) with { W = 1 }, $"RGB: {SwapEndianness(color.RGBA) >> 8:X6}");
+ ImGui.SameLine();
+ PrintColor(color, $"Alpha: {color.A}");
+ ImGui.SameLine();
+ PrintColor((add / new Vector3(510f)) + new Vector3(0.5f), $"Add: {add.X} {add.Y} {add.Z}");
+ ImGui.SameLine();
+ PrintColor(multiply / 255f, $"Multiply: {multiply.X} {multiply.Y} {multiply.Z}");
+
+ PrintFieldValuePairs(("Flags", $"0x{(uint)this.Node->NodeFlags:X} ({this.Node->NodeFlags})"));
+ }
+}
diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs
new file mode 100644
index 000000000..8572a5495
--- /dev/null
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs
@@ -0,0 +1,126 @@
+using System.Numerics;
+using System.Runtime.InteropServices;
+
+using Dalamud.Game.Text.SeStringHandling;
+using Dalamud.Game.Text.SeStringHandling.Payloads;
+using Dalamud.Interface.ImGuiSeStringRenderer;
+using Dalamud.Interface.Utility;
+using FFXIVClientStructs.FFXIV.Client.System.String;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using ImGuiNET;
+
+using static Dalamud.Interface.ColorHelpers;
+using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui;
+using static Dalamud.Utility.Util;
+
+namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
+
+///
+/// A tree for an that can be printed and browsed via ImGui.
+///
+internal unsafe partial class TextNodeTree : ResNodeTree
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The node to create a tree for.
+ /// The tree representing the containing addon.
+ internal TextNodeTree(AtkResNode* node, AddonTree addonTree)
+ : base(node, addonTree)
+ {
+ }
+
+ private AtkTextNode* TxtNode => (AtkTextNode*)this.Node;
+
+ private Utf8String NodeText => this.TxtNode->NodeText;
+
+ ///
+ private protected override void PrintNodeObject() => ShowStruct(this.TxtNode);
+
+ ///
+ private protected override void PrintFieldsForNodeType(bool isEditorOpen = false)
+ {
+ if (isEditorOpen)
+ {
+ return;
+ }
+
+ ImGui.TextColored(new(1), "Text:");
+ ImGui.SameLine();
+
+#pragma warning disable
+ try
+ {
+ var style = new SeStringDrawParams()
+ {
+ Color = TxtNode->TextColor.RGBA,
+ EdgeColor = TxtNode->EdgeColor.RGBA,
+ ForceEdgeColor = true,
+ EdgeStrength = 1f
+ };
+
+ ImGuiHelpers.SeStringWrapped(NodeText.AsSpan(), style);
+ }
+ catch
+ {
+ ImGui.Text(Marshal.PtrToStringAnsi(new(NodeText.StringPtr)) ?? "");
+ }
+#pragma warning restore
+
+ PrintFieldValuePairs(
+ ("Font", $"{this.TxtNode->FontType}"),
+ ("Font Size", $"{this.TxtNode->FontSize}"),
+ ("Alignment", $"{this.TxtNode->AlignmentType}"));
+
+ PrintColor(this.TxtNode->TextColor, $"Text Color: {SwapEndianness(this.TxtNode->TextColor.RGBA):X8}");
+ ImGui.SameLine();
+ PrintColor(this.TxtNode->EdgeColor, $"Edge Color: {SwapEndianness(this.TxtNode->EdgeColor.RGBA):X8}");
+
+ this.PrintPayloads();
+ }
+
+ private void PrintPayloads()
+ {
+ if (ImGui.TreeNode($"Text Payloads##{(nint)this.Node:X}"))
+ {
+ var utf8String = this.NodeText;
+ var seStringBytes = new byte[utf8String.BufUsed];
+ for (var i = 0L; i < utf8String.BufUsed; i++)
+ {
+ seStringBytes[i] = utf8String.StringPtr[i];
+ }
+
+ var seString = SeString.Parse(seStringBytes);
+ for (var i = 0; i < seString.Payloads.Count; i++)
+ {
+ var payload = seString.Payloads[i];
+ ImGui.Text($"[{i}]");
+ ImGui.SameLine();
+ switch (payload.Type)
+ {
+ case PayloadType.RawText when payload is TextPayload tp:
+ {
+ ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
+ ImGui.Text("Raw Text: '");
+ ImGui.SameLine();
+ ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0.6f, 0.6f, 0.6f, 1));
+ ImGui.Text(tp.Text);
+ ImGui.PopStyleColor();
+ ImGui.SameLine();
+ ImGui.PopStyleVar();
+ ImGui.Text("'");
+ break;
+ }
+
+ default:
+ {
+ ImGui.Text(payload.ToString());
+ break;
+ }
+ }
+ }
+
+ ImGui.TreePop();
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.KeyGroupColumn.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.KeyGroupColumn.cs
new file mode 100644
index 000000000..179197128
--- /dev/null
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.KeyGroupColumn.cs
@@ -0,0 +1,90 @@
+using System.Collections.Generic;
+
+using ImGuiNET;
+
+namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
+
+///
+public partial struct TimelineTree
+{
+ ///
+ /// An interface for retrieving and printing the contents of a given column in an animation timeline table.
+ ///
+ public interface IKeyGroupColumn
+ {
+ /// Gets the column's name/heading.
+ public string Name { get; }
+
+ /// Gets the number of cells in the column.
+ public int Count { get; }
+
+ /// Gets the column's width.
+ public float Width { get; }
+
+ ///
+ /// Calls this column's print function for a given row.
+ ///
+ /// The row number.
+ public void PrintValueAt(int i);
+ }
+
+ ///
+ /// A column within an animation timeline table, representing a particular KeyGroup.
+ ///
+ /// The value type of the KeyGroup.
+ public struct KeyGroupColumn : IKeyGroupColumn
+ {
+ /// The values of each cell in the column.
+ public List Values;
+
+ /// The method that should be used to format and print values in this KeyGroup.
+ public Action PrintFunc;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The column's name/heading.
+ /// The method that should be used to format and print values in this KeyGroup.
+ internal KeyGroupColumn(string name, Action? printFunc = null)
+ {
+ this.Name = name;
+ this.PrintFunc = printFunc ?? PlainTextCell;
+ this.Values = [];
+ this.Width = 50;
+ }
+
+ ///
+ public string Name { get; set; }
+
+ ///
+ public float Width { get; init; }
+
+ ///
+ public readonly int Count => this.Values.Count;
+
+ ///
+ /// The default print function, if none is specified.
+ ///
+ /// The value to print.
+ public static void PlainTextCell(T value) => ImGui.Text($"{value}");
+
+ ///
+ /// Adds a value to this column.
+ ///
+ /// The value to add.
+ public readonly void Add(T val) => this.Values.Add(val);
+
+ ///
+ public readonly void PrintValueAt(int i)
+ {
+ if (this.Values.Count > i)
+ {
+ this.PrintFunc.Invoke(this.Values[i]);
+ }
+ else
+ {
+ ImGui.TextDisabled("...");
+ }
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs
new file mode 100644
index 000000000..22fb61872
--- /dev/null
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs
@@ -0,0 +1,381 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+
+using Dalamud.Interface.Utility;
+using FFXIVClientStructs.FFXIV.Client.Graphics;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using ImGuiNET;
+
+using static Dalamud.Interface.ColorHelpers;
+using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui;
+using static Dalamud.Utility.Util;
+using static FFXIVClientStructs.FFXIV.Component.GUI.NodeType;
+using static ImGuiNET.ImGuiTableColumnFlags;
+using static ImGuiNET.ImGuiTableFlags;
+
+// ReSharper disable SuggestBaseTypeForParameter
+namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
+
+///
+/// A struct allowing a node's animation timeline to be printed and browsed.
+///
+public unsafe partial struct TimelineTree
+{
+ private AtkResNode* node;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The node whose timelines are to be displayed.
+ internal TimelineTree(AtkResNode* node)
+ {
+ this.node = node;
+ }
+
+ private AtkTimeline* NodeTimeline => this.node->Timeline;
+
+ private AtkTimelineResource* Resource => this.NodeTimeline->Resource;
+
+ private AtkTimelineAnimation* ActiveAnimation => this.NodeTimeline->ActiveAnimation;
+
+ ///
+ /// Prints out this timeline tree within a window.
+ ///
+ internal void Print()
+ {
+ if (this.NodeTimeline == null)
+ {
+ return;
+ }
+
+ var count = this.Resource->AnimationCount;
+
+ if (count > 0)
+ {
+ if (NestedTreePush($"Timeline##{(nint)this.node:X}timeline", out _))
+ {
+ PrintFieldValuePair("Timeline", $"{(nint)this.NodeTimeline:X}");
+
+ ImGui.SameLine();
+
+ ShowStruct(this.NodeTimeline);
+
+ PrintFieldValuePairs(
+ ("Id", $"{this.NodeTimeline->Resource->Id}"),
+ ("Parent Time", $"{this.NodeTimeline->ParentFrameTime:F2} ({this.NodeTimeline->ParentFrameTime * 30:F0})"),
+ ("Frame Time", $"{this.NodeTimeline->FrameTime:F2} ({this.NodeTimeline->FrameTime * 30:F0})"));
+
+ PrintFieldValuePairs(("Active Label Id", $"{this.NodeTimeline->ActiveLabelId}"), ("Duration", $"{this.NodeTimeline->LabelFrameIdxDuration}"), ("End Frame", $"{this.NodeTimeline->LabelEndFrameIdx}"));
+ ImGui.TextColored(new(0.6f, 0.6f, 0.6f, 1), "Animation List");
+
+ for (var a = 0; a < count; a++)
+ {
+ var animation = this.Resource->Animations[a];
+ var isActive = this.ActiveAnimation != null && animation.Equals(*this.ActiveAnimation);
+ this.PrintAnimation(animation, a, isActive, (nint)(this.NodeTimeline->Resource->Animations + (a * sizeof(AtkTimelineAnimation))));
+ }
+
+ ImGui.TreePop();
+ }
+ }
+ }
+
+ private static void GetFrameColumn(Span keyGroups, List columns, ushort endFrame)
+ {
+ for (var i = 0; i < keyGroups.Length; i++)
+ {
+ if (keyGroups[i].Type != AtkTimelineKeyGroupType.None)
+ {
+ var keyGroup = keyGroups[i];
+ var idColumn = new KeyGroupColumn("Frame");
+
+ for (var f = 0; f < keyGroup.KeyFrameCount; f++)
+ {
+ idColumn.Add(keyGroup.KeyFrames[f].FrameIdx);
+ }
+
+ if (idColumn.Values.Last() != endFrame)
+ {
+ idColumn.Add(endFrame);
+ }
+
+ columns.Add(idColumn);
+ break;
+ }
+ }
+ }
+
+ private static void GetPosColumns(AtkTimelineKeyGroup keyGroup, List columns)
+ {
+ if (keyGroup.KeyFrameCount <= 0)
+ {
+ return;
+ }
+
+ var xColumn = new KeyGroupColumn("X");
+ var yColumn = new KeyGroupColumn("Y");
+
+ for (var f = 0; f < keyGroup.KeyFrameCount; f++)
+ {
+ var (x, y) = keyGroup.KeyFrames[f].Value.Float2;
+
+ xColumn.Add(x);
+ yColumn.Add(y);
+ }
+
+ columns.Add(xColumn);
+ columns.Add(yColumn);
+ }
+
+ private static void GetRotationColumn(AtkTimelineKeyGroup keyGroup, List columns)
+ {
+ if (keyGroup.KeyFrameCount <= 0)
+ {
+ return;
+ }
+
+ var rotColumn = new KeyGroupColumn("Rotation", static r => ImGui.Text($"{r * (180d / Math.PI):F1}°"));
+
+ for (var f = 0; f < keyGroup.KeyFrameCount; f++)
+ {
+ rotColumn.Add(keyGroup.KeyFrames[f].Value.Float);
+ }
+
+ columns.Add(rotColumn);
+ }
+
+ private static void GetScaleColumns(AtkTimelineKeyGroup keyGroup, List columns)
+ {
+ if (keyGroup.KeyFrameCount <= 0)
+ {
+ return;
+ }
+
+ var scaleXColumn = new KeyGroupColumn("ScaleX");
+ var scaleYColumn = new KeyGroupColumn("ScaleY");
+
+ for (var f = 0; f < keyGroup.KeyFrameCount; f++)
+ {
+ var (scaleX, scaleY) = keyGroup.KeyFrames[f].Value.Float2;
+
+ scaleXColumn.Add(scaleX);
+ scaleYColumn.Add(scaleY);
+ }
+
+ columns.Add(scaleXColumn);
+ columns.Add(scaleYColumn);
+ }
+
+ private static void GetAlphaColumn(AtkTimelineKeyGroup keyGroup, List columns)
+ {
+ if (keyGroup.KeyFrameCount <= 0)
+ {
+ return;
+ }
+
+ var alphaColumn = new KeyGroupColumn("Alpha", PrintAlpha);
+
+ for (var f = 0; f < keyGroup.KeyFrameCount; f++)
+ {
+ alphaColumn.Add(keyGroup.KeyFrames[f].Value.Byte);
+ }
+
+ columns.Add(alphaColumn);
+ }
+
+ private static void GetTintColumns(AtkTimelineKeyGroup keyGroup, List columns)
+ {
+ if (keyGroup.KeyFrameCount <= 0)
+ {
+ return;
+ }
+
+ var addRGBColumn = new KeyGroupColumn("Add", PrintAddCell) { Width = 110 };
+ var multiplyRGBColumn = new KeyGroupColumn("Multiply", PrintMultiplyCell) { Width = 110 };
+
+ for (var f = 0; f < keyGroup.KeyFrameCount; f++)
+ {
+ var nodeTint = keyGroup.KeyFrames[f].Value.NodeTint;
+
+ addRGBColumn.Add(new Vector3(nodeTint.AddR, nodeTint.AddG, nodeTint.AddB));
+ multiplyRGBColumn.Add(nodeTint.MultiplyRGB);
+ }
+
+ columns.Add(addRGBColumn);
+ columns.Add(multiplyRGBColumn);
+ }
+
+ private static void GetTextColorColumn(AtkTimelineKeyGroup keyGroup, List columns)
+ {
+ if (keyGroup.KeyFrameCount <= 0)
+ {
+ return;
+ }
+
+ var textColorColumn = new KeyGroupColumn("Text Color", PrintRGB);
+
+ for (var f = 0; f < keyGroup.KeyFrameCount; f++)
+ {
+ textColorColumn.Add(keyGroup.KeyFrames[f].Value.RGB);
+ }
+
+ columns.Add(textColorColumn);
+ }
+
+ private static void GetPartIdColumn(AtkTimelineKeyGroup keyGroup, List columns)
+ {
+ if (keyGroup.KeyFrameCount <= 0)
+ {
+ return;
+ }
+
+ var partColumn = new KeyGroupColumn("Part ID");
+
+ for (var f = 0; f < keyGroup.KeyFrameCount; f++)
+ {
+ partColumn.Add(keyGroup.KeyFrames[f].Value.UShort);
+ }
+
+ columns.Add(partColumn);
+ }
+
+ private static void GetEdgeColumn(AtkTimelineKeyGroup keyGroup, List columns)
+ {
+ if (keyGroup.KeyFrameCount <= 0)
+ {
+ return;
+ }
+
+ var edgeColorColumn = new KeyGroupColumn("Edge Color", PrintRGB);
+
+ for (var f = 0; f < keyGroup.KeyFrameCount; f++)
+ {
+ edgeColorColumn.Add(keyGroup.KeyFrames[f].Value.RGB);
+ }
+
+ columns.Add(edgeColorColumn);
+ }
+
+ private static void GetLabelColumn(AtkTimelineKeyGroup keyGroup, List columns)
+ {
+ if (keyGroup.KeyFrameCount <= 0)
+ {
+ return;
+ }
+
+ var labelColumn = new KeyGroupColumn("Label");
+
+ for (var f = 0; f < keyGroup.KeyFrameCount; f++)
+ {
+ labelColumn.Add(keyGroup.KeyFrames[f].Value.Label.LabelId);
+ }
+
+ columns.Add(labelColumn);
+ }
+
+ private static void PrintRGB(ByteColor c) => PrintColor(c, $"0x{SwapEndianness(c.RGBA):X8}");
+
+ private static void PrintAlpha(byte b) => PrintColor(new Vector4(b / 255f), PadEvenly($"{b}", 25));
+
+ private static void PrintAddCell(Vector3 add)
+ {
+ var fmt = PadEvenly($"{PadEvenly($"{add.X}", 30)}{PadEvenly($"{add.Y}", 30)}{PadEvenly($"{add.Z}", 30)}", 100);
+ PrintColor(new Vector4((add / new Vector3(510f)) + new Vector3(0.5f), 1), fmt);
+ }
+
+ private static void PrintMultiplyCell(ByteColor byteColor)
+ {
+ var multiply = new Vector3(byteColor.R, byteColor.G, byteColor.B);
+ var fmt = PadEvenly($"{PadEvenly($"{multiply.X}", 25)}{PadEvenly($"{multiply.Y}", 25)}{PadEvenly($"{multiply.Z}", 25)}", 100);
+ PrintColor(multiply / 255f, fmt);
+ }
+
+ private static string PadEvenly(string str, float size)
+ {
+ while (ImGui.CalcTextSize(str).X < size * ImGuiHelpers.GlobalScale)
+ {
+ str = $" {str} ";
+ }
+
+ return str;
+ }
+
+ private void PrintAnimation(AtkTimelineAnimation animation, int a, bool isActive, nint address)
+ {
+ var columns = this.BuildColumns(animation);
+
+ ImGui.PushStyleColor(ImGuiCol.Text, isActive ? new Vector4(1, 0.65F, 0.4F, 1) : new(1));
+ var treePush = ImGui.TreeNode($"[#{a}] [Frames {animation.StartFrameIdx}-{animation.EndFrameIdx}] {(isActive ? " (Active)" : string.Empty)}###{(nint)this.node}animTree{a}");
+ ImGui.PopStyleColor();
+
+ if (treePush)
+ {
+ PrintFieldValuePair("Animation", $"{address:X}");
+
+ ShowStruct((AtkTimelineAnimation*)address);
+
+ if (columns.Count > 0)
+ {
+ ImGui.BeginTable($"##{(nint)this.node}animTable{a}", columns.Count, Borders | SizingFixedFit | RowBg | NoHostExtendX);
+
+ foreach (var c in columns)
+ {
+ ImGui.TableSetupColumn(c.Name, WidthFixed, c.Width);
+ }
+
+ ImGui.TableHeadersRow();
+
+ var rows = columns.Select(static c => c.Count).Max();
+
+ for (var i = 0; i < rows; i++)
+ {
+ ImGui.TableNextRow();
+
+ foreach (var c in columns)
+ {
+ ImGui.TableNextColumn();
+ c.PrintValueAt(i);
+ }
+ }
+
+ ImGui.EndTable();
+ }
+
+ ImGui.TreePop();
+ }
+ }
+
+ private List BuildColumns(AtkTimelineAnimation animation)
+ {
+ var keyGroups = animation.KeyGroups;
+ var columns = new List();
+
+ GetFrameColumn(keyGroups, columns, animation.EndFrameIdx);
+
+ GetPosColumns(keyGroups[0], columns);
+
+ GetRotationColumn(keyGroups[1], columns);
+
+ GetScaleColumns(keyGroups[2], columns);
+
+ GetAlphaColumn(keyGroups[3], columns);
+
+ GetTintColumns(keyGroups[4], columns);
+
+ if (this.node->Type is Image or NineGrid or ClippingMask)
+ {
+ GetPartIdColumn(keyGroups[5], columns);
+ }
+ else if (this.node->Type == Text)
+ {
+ GetTextColorColumn(keyGroups[5], columns);
+ }
+
+ GetEdgeColumn(keyGroups[6], columns);
+
+ GetLabelColumn(keyGroups[7], columns);
+
+ return columns;
+ }
+}
diff --git a/Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs b/Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs
new file mode 100644
index 000000000..c6cb99037
--- /dev/null
+++ b/Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs
@@ -0,0 +1,475 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Numerics;
+
+using Dalamud.Interface.Components;
+using Dalamud.Interface.Internal.UiDebug2.Browsing;
+using Dalamud.Interface.Internal.UiDebug2.Utility;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using ImGuiNET;
+
+using static System.Globalization.NumberFormatInfo;
+
+using static Dalamud.Interface.FontAwesomeIcon;
+using static Dalamud.Interface.Internal.UiDebug2.UiDebug2;
+using static Dalamud.Interface.UiBuilder;
+using static Dalamud.Interface.Utility.ImGuiHelpers;
+using static FFXIVClientStructs.FFXIV.Component.GUI.NodeFlags;
+using static ImGuiNET.ImGuiCol;
+using static ImGuiNET.ImGuiWindowFlags;
+
+#pragma warning disable CS0659
+
+namespace Dalamud.Interface.Internal.UiDebug2;
+
+///
+/// A tool that enables the user to select UI elements within the inspector by mousing over them onscreen.
+///
+internal unsafe class ElementSelector : IDisposable
+{
+ private const int UnitListCount = 18;
+
+ private readonly UiDebug2 uiDebug2;
+
+ private string addressSearchInput = string.Empty;
+
+ private int index;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The instance of this Element Selector belongs to.
+ internal ElementSelector(UiDebug2 uiDebug2)
+ {
+ this.uiDebug2 = uiDebug2;
+ }
+
+ ///
+ /// Gets or sets the results retrieved by the Element Selector.
+ ///
+ internal static nint[] SearchResults { get; set; } = [];
+
+ ///
+ /// Gets or sets a value governing the highlighting of nodes when found via search.
+ ///
+ internal static float Countdown { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the window has scrolled down to the position of the search result.
+ ///
+ internal static bool Scrolled { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the mouseover UI is currently active.
+ ///
+ internal bool Active { get; set; }
+
+ ///
+ public void Dispose()
+ {
+ this.Active = false;
+ }
+
+ ///
+ /// Draws the Element Selector and Address Search interface at the bottom of the sidebar.
+ ///
+ internal void DrawInterface()
+ {
+ ImGui.BeginChild("###sidebar_elementSelector", new(250, 0), true);
+
+ ImGui.PushFont(IconFont);
+ ImGui.PushStyleColor(Text, this.Active ? new Vector4(1, 1, 0.2f, 1) : new(1));
+ if (ImGui.Button($"{(char)ObjectUngroup}"))
+ {
+ this.Active = !this.Active;
+ }
+
+ if (Countdown > 0)
+ {
+ Countdown -= 1;
+ if (Countdown < 0)
+ {
+ Countdown = 0;
+ }
+ }
+
+ ImGui.PopStyleColor();
+ ImGui.PopFont();
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip("Element Selector");
+ }
+
+ ImGui.SameLine();
+
+ ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 32);
+ ImGui.InputTextWithHint("###addressSearchInput", "Address Search", ref this.addressSearchInput, 18, ImGuiInputTextFlags.AutoSelectAll);
+ ImGui.SameLine();
+
+ if (ImGuiComponents.IconButton("###elemSelectorAddrSearch", Search) && nint.TryParse(this.addressSearchInput, NumberStyles.HexNumber | NumberStyles.AllowHexSpecifier, InvariantInfo, out var address))
+ {
+ this.PerformSearch(address);
+ }
+
+ ImGui.EndChild();
+ }
+
+ ///
+ /// Draws the Element Selector's search output within the main window.
+ ///
+ internal void DrawSelectorOutput()
+ {
+ ImGui.GetIO().WantCaptureKeyboard = true;
+ ImGui.GetIO().WantCaptureMouse = true;
+ ImGui.GetIO().WantTextInput = true;
+ if (ImGui.IsKeyPressed(ImGuiKey.Escape))
+ {
+ this.Active = false;
+ return;
+ }
+
+ ImGui.Text("ELEMENT SELECTOR");
+ ImGui.TextDisabled("Use the mouse to hover and identify UI elements, then click to jump to them in the inspector");
+ ImGui.TextDisabled("Use the scrollwheel to choose between overlapping elements");
+ ImGui.TextDisabled("Press ESCAPE to cancel");
+ ImGui.Spacing();
+
+ var mousePos = ImGui.GetMousePos() - MainViewport.Pos;
+ var addonResults = GetAtkUnitBaseAtPosition(mousePos);
+
+ ImGui.PushStyleColor(WindowBg, new Vector4(0.5f));
+ ImGui.BeginChild("noClick", new(800, 2000), false, NoInputs | NoBackground | NoScrollWithMouse);
+ ImGui.BeginGroup();
+
+ Gui.PrintFieldValuePair("Mouse Position", $"{mousePos.X}, {mousePos.Y}");
+ ImGui.Spacing();
+ ImGui.Text("RESULTS:\n");
+
+ var i = 0;
+ foreach (var a in addonResults)
+ {
+ var name = a.Addon->NameString;
+ ImGui.Text($"[Addon] {name}");
+ ImGui.Indent(15);
+ foreach (var n in a.Nodes)
+ {
+ var nSelected = i++ == this.index;
+
+ PrintNodeHeaderOnly(n.Node, nSelected, a.Addon);
+
+ if (nSelected && ImGui.IsMouseClicked(ImGuiMouseButton.Left))
+ {
+ this.Active = false;
+
+ this.uiDebug2.SelectedAddonName = a.Addon->NameString;
+
+ var ptrList = new List { (nint)n.Node };
+
+ var nextNode = n.Node->ParentNode;
+ while (nextNode != null)
+ {
+ ptrList.Add((nint)nextNode);
+ nextNode = nextNode->ParentNode;
+ }
+
+ SearchResults = [.. ptrList];
+ Countdown = 100;
+ Scrolled = false;
+ }
+
+ if (nSelected)
+ {
+ n.NodeBounds.DrawFilled(new(1, 1, 0.2f, 1));
+ }
+ }
+
+ ImGui.Indent(-15);
+ }
+
+ if (i != 0)
+ {
+ this.index -= (int)ImGui.GetIO().MouseWheel;
+ while (this.index < 0)
+ {
+ this.index += i;
+ }
+
+ while (this.index >= i)
+ {
+ this.index -= i;
+ }
+ }
+
+ ImGui.EndGroup();
+ ImGui.EndChild();
+ ImGui.PopStyleColor();
+ }
+
+ private static List GetAtkUnitBaseAtPosition(Vector2 position)
+ {
+ var addonResults = new List();
+ var unitListBaseAddr = GetUnitListBaseAddr();
+ if (unitListBaseAddr == null)
+ {
+ return addonResults;
+ }
+
+ foreach (var unit in UnitListOptions)
+ {
+ var unitManager = &unitListBaseAddr[unit.Index];
+
+ var safeCount = Math.Min(unitManager->Count, unitManager->Entries.Length);
+
+ for (var i = 0; i < safeCount; i++)
+ {
+ var addon = unitManager->Entries[i].Value;
+
+ if (addon == null || addon->RootNode == null)
+ {
+ continue;
+ }
+
+ if (!addon->IsVisible || !addon->RootNode->NodeFlags.HasFlag(Visible))
+ {
+ continue;
+ }
+
+ var addonResult = new AddonResult(addon, []);
+
+ if (addonResults.Contains(addonResult))
+ {
+ continue;
+ }
+
+ if (addon->X > position.X || addon->Y > position.Y)
+ {
+ continue;
+ }
+
+ if (addon->X + addon->RootNode->Width < position.X)
+ {
+ continue;
+ }
+
+ if (addon->Y + addon->RootNode->Height < position.Y)
+ {
+ continue;
+ }
+
+ addonResult.Nodes.AddRange(GetNodeAtPosition(&addon->UldManager, position, true));
+ addonResults.Add(addonResult);
+ }
+ }
+
+ return [.. addonResults.OrderBy(static w => w.Area)];
+ }
+
+ private static List GetNodeAtPosition(AtkUldManager* uldManager, Vector2 position, bool reverse)
+ {
+ var nodeResults = new List();
+ for (var i = 0; i < uldManager->NodeListCount; i++)
+ {
+ var node = uldManager->NodeList[i];
+
+ var bounds = new NodeBounds(node);
+
+ if (!bounds.ContainsPoint(position))
+ {
+ continue;
+ }
+
+ if ((int)node->Type >= 1000)
+ {
+ var compNode = (AtkComponentNode*)node;
+ nodeResults.AddRange(GetNodeAtPosition(&compNode->Component->UldManager, position, false));
+ }
+
+ nodeResults.Add(new() { NodeBounds = bounds, Node = node });
+ }
+
+ if (reverse)
+ {
+ nodeResults.Reverse();
+ }
+
+ return nodeResults;
+ }
+
+ private static bool FindByAddress(AtkUnitBase* atkUnitBase, nint address)
+ {
+ if (atkUnitBase->RootNode == null)
+ {
+ return false;
+ }
+
+ if (!FindByAddress(atkUnitBase->RootNode, address, out var path))
+ {
+ return false;
+ }
+
+ Scrolled = false;
+ SearchResults = path?.ToArray() ?? [];
+ Countdown = 100;
+ return true;
+ }
+
+ private static bool FindByAddress(AtkResNode* node, nint address, out List? path)
+ {
+ if (node == null)
+ {
+ path = null;
+ return false;
+ }
+
+ if ((nint)node == address)
+ {
+ path = [(nint)node];
+ return true;
+ }
+
+ if ((int)node->Type >= 1000)
+ {
+ var cNode = (AtkComponentNode*)node;
+
+ if (cNode->Component != null)
+ {
+ if ((nint)cNode->Component == address)
+ {
+ path = [(nint)node];
+ return true;
+ }
+
+ if (FindByAddress(cNode->Component->UldManager.RootNode, address, out path) && path != null)
+ {
+ path.Add((nint)node);
+ return true;
+ }
+ }
+ }
+
+ if (FindByAddress(node->ChildNode, address, out path) && path != null)
+ {
+ path.Add((nint)node);
+ return true;
+ }
+
+ if (FindByAddress(node->PrevSiblingNode, address, out path) && path != null)
+ {
+ return true;
+ }
+
+ path = null;
+ return false;
+ }
+
+ private static void PrintNodeHeaderOnly(AtkResNode* node, bool selected, AtkUnitBase* addon)
+ {
+ if (addon == null)
+ {
+ return;
+ }
+
+ if (node == null)
+ {
+ return;
+ }
+
+ var tree = AddonTree.GetOrCreate(addon->NameString);
+ if (tree == null)
+ {
+ return;
+ }
+
+ ImGui.PushStyleColor(Text, selected ? new Vector4(1, 1, 0.2f, 1) : new(0.6f, 0.6f, 0.6f, 1));
+ ResNodeTree.GetOrCreate(node, tree).WriteTreeHeading();
+ ImGui.PopStyleColor();
+ }
+
+ private void PerformSearch(nint address)
+ {
+ var unitListBaseAddr = GetUnitListBaseAddr();
+ if (unitListBaseAddr == null)
+ {
+ return;
+ }
+
+ for (var i = 0; i < UnitListCount; i++)
+ {
+ var unitManager = &unitListBaseAddr[i];
+ var safeCount = Math.Min(unitManager->Count, unitManager->Entries.Length);
+
+ for (var j = 0; j < safeCount; j++)
+ {
+ var addon = unitManager->Entries[j].Value;
+ if ((nint)addon == address || FindByAddress(addon, address))
+ {
+ this.uiDebug2.SelectedAddonName = addon->NameString;
+ return;
+ }
+ }
+ }
+ }
+
+ ///
+ /// An found by the Element Selector.
+ ///
+ internal struct AddonResult
+ {
+ /// The addon itself.
+ internal AtkUnitBase* Addon;
+
+ /// A list of nodes discovered within this addon by the Element Selector.
+ internal List Nodes;
+
+ /// The calculated area of the addon's root node.
+ internal float Area;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The addon found.
+ /// A list for documenting nodes found within the addon.
+ public AddonResult(AtkUnitBase* addon, List nodes)
+ {
+ this.Addon = addon;
+ this.Nodes = nodes;
+ var rootNode = addon->RootNode;
+ this.Area = rootNode != null ? rootNode->Width * rootNode->Height * rootNode->ScaleY * rootNode->ScaleX : 0;
+ }
+
+ ///
+ public override readonly bool Equals(object? obj)
+ {
+ if (obj is not AddonResult ar)
+ {
+ return false;
+ }
+
+ return (nint)this.Addon == (nint)ar.Addon;
+ }
+ }
+
+ ///
+ /// An found by the Element Selector.
+ ///
+ internal struct NodeResult
+ {
+ /// The node itself.
+ internal AtkResNode* Node;
+
+ /// A struct representing the perimeter of the node.
+ internal NodeBounds NodeBounds;
+
+ ///
+ public override readonly bool Equals(object? obj)
+ {
+ if (obj is not NodeResult nr)
+ {
+ return false;
+ }
+
+ return nr.Node == this.Node;
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/UiDebug2/Popout.Addon.cs b/Dalamud/Interface/Internal/UiDebug2/Popout.Addon.cs
new file mode 100644
index 000000000..3aef3b6a4
--- /dev/null
+++ b/Dalamud/Interface/Internal/UiDebug2/Popout.Addon.cs
@@ -0,0 +1,50 @@
+using System.Numerics;
+
+using Dalamud.Interface.Internal.UiDebug2.Browsing;
+using Dalamud.Interface.Windowing;
+using ImGuiNET;
+
+namespace Dalamud.Interface.Internal.UiDebug2;
+
+///
+/// A popout window for an .
+///
+internal class AddonPopoutWindow : Window, IDisposable
+{
+ private readonly AddonTree addonTree;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The AddonTree this popout will show.
+ /// the window's name.
+ public AddonPopoutWindow(AddonTree tree, string name)
+ : base(name)
+ {
+ this.addonTree = tree;
+ this.PositionCondition = ImGuiCond.Once;
+
+ var pos = ImGui.GetMousePos() + new Vector2(50, -50);
+ var workSize = ImGui.GetMainViewport().WorkSize;
+ var pos2 = new Vector2(Math.Min(workSize.X - 750, pos.X), Math.Min(workSize.Y - 250, pos.Y));
+
+ this.Position = pos2;
+ this.SizeCondition = ImGuiCond.Once;
+ this.Size = new(700, 200);
+ this.IsOpen = true;
+ this.SizeConstraints = new() { MinimumSize = new(100, 100) };
+ }
+
+ ///
+ public override void Draw()
+ {
+ ImGui.BeginChild($"{this.WindowName}child", new(-1, -1), true);
+ this.addonTree.Draw();
+ ImGui.EndChild();
+ }
+
+ ///
+ public void Dispose()
+ {
+ }
+}
diff --git a/Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs b/Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs
new file mode 100644
index 000000000..b293b734e
--- /dev/null
+++ b/Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs
@@ -0,0 +1,69 @@
+using System.Numerics;
+
+using Dalamud.Interface.Internal.UiDebug2.Browsing;
+using Dalamud.Interface.Windowing;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using ImGuiNET;
+
+using static Dalamud.Interface.Internal.UiDebug2.UiDebug2;
+
+namespace Dalamud.Interface.Internal.UiDebug2;
+
+///
+/// A popout window for a .
+///
+internal unsafe class NodePopoutWindow : Window, IDisposable
+{
+ private readonly ResNodeTree resNodeTree;
+
+ private bool firstDraw = true;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The node tree this window will show.
+ /// The name of the window.
+ public NodePopoutWindow(ResNodeTree nodeTree, string windowName)
+ : base(windowName)
+ {
+ this.resNodeTree = nodeTree;
+
+ var pos = ImGui.GetMousePos() + new Vector2(50, -50);
+ var workSize = ImGui.GetMainViewport().WorkSize;
+ var pos2 = new Vector2(Math.Min(workSize.X - 750, pos.X), Math.Min(workSize.Y - 250, pos.Y));
+
+ this.Position = pos2;
+ this.IsOpen = true;
+ this.PositionCondition = ImGuiCond.Once;
+ this.SizeCondition = ImGuiCond.Once;
+ this.Size = new(700, 200);
+ this.SizeConstraints = new() { MinimumSize = new(100, 100) };
+ }
+
+ private AddonTree AddonTree => this.resNodeTree.AddonTree;
+
+ private AtkResNode* Node => this.resNodeTree.Node;
+
+ ///
+ public override void Draw()
+ {
+ if (this.Node != null && this.AddonTree.ContainsNode(this.Node))
+ {
+ ImGui.BeginChild($"{(nint)this.Node:X}popoutChild", new(-1, -1), true);
+ ResNodeTree.GetOrCreate(this.Node, this.AddonTree).Print(null, this.firstDraw);
+ ImGui.EndChild();
+ this.firstDraw = false;
+ }
+ else
+ {
+ Log.Warning($"Popout closed ({this.WindowName}); Node or Addon no longer exists.");
+ this.IsOpen = false;
+ this.Dispose();
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ }
+}
diff --git a/Dalamud/Interface/Internal/UiDebug2/UiDebug2.Sidebar.cs b/Dalamud/Interface/Internal/UiDebug2/UiDebug2.Sidebar.cs
new file mode 100644
index 000000000..d2510d16b
--- /dev/null
+++ b/Dalamud/Interface/Internal/UiDebug2/UiDebug2.Sidebar.cs
@@ -0,0 +1,214 @@
+using System.Collections.Generic;
+using System.Numerics;
+
+using Dalamud.Interface.Components;
+using FFXIVClientStructs.FFXIV.Client.UI;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using ImGuiNET;
+
+using static System.StringComparison;
+using static Dalamud.Interface.FontAwesomeIcon;
+
+namespace Dalamud.Interface.Internal.UiDebug2;
+
+///
+internal unsafe partial class UiDebug2
+{
+ ///
+ /// All unit lists to check for addons.
+ ///
+ internal static readonly List UnitListOptions =
+ [
+ new(13, "Loaded"),
+ new(14, "Focused"),
+ new(0, "Depth Layer 1"),
+ new(1, "Depth Layer 2"),
+ new(2, "Depth Layer 3"),
+ new(3, "Depth Layer 4"),
+ new(4, "Depth Layer 5"),
+ new(5, "Depth Layer 6"),
+ new(6, "Depth Layer 7"),
+ new(7, "Depth Layer 8"),
+ new(8, "Depth Layer 9"),
+ new(9, "Depth Layer 10"),
+ new(10, "Depth Layer 11"),
+ new(11, "Depth Layer 12"),
+ new(12, "Depth Layer 13"),
+ new(15, "Units 16"),
+ new(16, "Units 17"),
+ new(17, "Units 18"),
+ ];
+
+ private string addonNameSearch = string.Empty;
+
+ private bool visFilter;
+
+ ///
+ /// Gets the base address for all unit lists.
+ ///
+ /// The address, if found.
+ internal static AtkUnitList* GetUnitListBaseAddr() => &((UIModule*)GameGui.GetUIModule())->GetRaptureAtkModule()->RaptureAtkUnitManager.AtkUnitManager.DepthLayerOneList;
+
+ private void DrawSidebar()
+ {
+ ImGui.BeginGroup();
+
+ this.DrawNameSearch();
+ this.DrawAddonSelectionList();
+ this.elementSelector.DrawInterface();
+
+ ImGui.EndGroup();
+ }
+
+ private void DrawNameSearch()
+ {
+ ImGui.BeginChild("###sidebar_nameSearch", new(250, 40), true);
+ var atkUnitBaseSearch = this.addonNameSearch;
+
+ Vector4? defaultColor = this.visFilter ? new(0.0f, 0.8f, 0.2f, 1f) : new Vector4(0.6f, 0.6f, 0.6f, 1);
+ if (ImGuiComponents.IconButton("filter", LowVision, defaultColor))
+ {
+ this.visFilter = !this.visFilter;
+ }
+
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip("Filter by visibility");
+ }
+
+ ImGui.SameLine();
+
+ ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
+ if (ImGui.InputTextWithHint("###atkUnitBaseSearch", "Filter by name", ref atkUnitBaseSearch, 0x20))
+ {
+ this.addonNameSearch = atkUnitBaseSearch;
+ }
+
+ ImGui.EndChild();
+ }
+
+ private void DrawAddonSelectionList()
+ {
+ ImGui.BeginChild("###sideBar_addonList", new(250, -44), true, ImGuiWindowFlags.AlwaysVerticalScrollbar);
+
+ var unitListBaseAddr = GetUnitListBaseAddr();
+
+ foreach (var unit in UnitListOptions)
+ {
+ this.DrawUnitListOption(unitListBaseAddr, unit);
+ }
+
+ ImGui.EndChild();
+ }
+
+ private void DrawUnitListOption(AtkUnitList* unitListBaseAddr, UnitListOption unit)
+ {
+ var atkUnitList = &unitListBaseAddr[unit.Index];
+ var safeLength = Math.Min(atkUnitList->Count, atkUnitList->Entries.Length);
+
+ var options = new List();
+ var totalCount = 0;
+ var matchCount = 0;
+ var anyVisible = false;
+
+ var usingFilter = this.visFilter || !string.IsNullOrEmpty(this.addonNameSearch);
+
+ for (var i = 0; i < safeLength; i++)
+ {
+ var addon = atkUnitList->Entries[i].Value;
+
+ if (addon == null)
+ {
+ continue;
+ }
+
+ totalCount++;
+
+ if (this.visFilter && !addon->IsVisible)
+ {
+ continue;
+ }
+
+ if (!string.IsNullOrEmpty(this.addonNameSearch) && !addon->NameString.Contains(this.addonNameSearch, InvariantCultureIgnoreCase))
+ {
+ continue;
+ }
+
+ matchCount++;
+ anyVisible |= addon->IsVisible;
+ options.Add(new AddonOption(addon->NameString, addon->IsVisible));
+ }
+
+ if (matchCount == 0)
+ {
+ return;
+ }
+
+ ImGui.PushStyleColor(ImGuiCol.Text, anyVisible ? new Vector4(1) : new Vector4(0.6f, 0.6f, 0.6f, 1));
+ var countStr = $"{(usingFilter ? $"{matchCount}/" : string.Empty)}{totalCount}";
+ var treePush = ImGui.TreeNodeEx($"{unit.Name} [{countStr}]###unitListTree{unit.Index}");
+ ImGui.PopStyleColor();
+
+ if (treePush)
+ {
+ foreach (var option in options)
+ {
+ ImGui.PushStyleColor(ImGuiCol.Text, option.Visible ? new Vector4(0.1f, 1f, 0.1f, 1f) : new Vector4(0.6f, 0.6f, 0.6f, 1));
+ if (ImGui.Selectable($"{option.Name}##select{option.Name}", this.SelectedAddonName == option.Name))
+ {
+ this.SelectedAddonName = option.Name;
+ }
+
+ ImGui.PopStyleColor();
+ }
+
+ ImGui.TreePop();
+ }
+ }
+
+ ///
+ /// A struct representing a unit list that can be browed in the sidebar.
+ ///
+ internal struct UnitListOption
+ {
+ /// The index of the unit list.
+ internal uint Index;
+
+ /// The name of the unit list.
+ internal string Name;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The index of the unit list.
+ /// The name of the unit list.
+ internal UnitListOption(uint i, string name)
+ {
+ this.Index = i;
+ this.Name = name;
+ }
+ }
+
+ ///
+ /// A struct representing an addon that can be selected in the sidebar.
+ ///
+ internal struct AddonOption
+ {
+ /// The name of the addon.
+ internal string Name;
+
+ /// Whether the addon is visible.
+ internal bool Visible;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The name of the addon.
+ /// Whether the addon is visible.
+ internal AddonOption(string name, bool visible)
+ {
+ this.Name = name;
+ this.Visible = visible;
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/UiDebug2/UiDebug2.cs b/Dalamud/Interface/Internal/UiDebug2/UiDebug2.cs
new file mode 100644
index 000000000..396a84ac9
--- /dev/null
+++ b/Dalamud/Interface/Internal/UiDebug2/UiDebug2.cs
@@ -0,0 +1,112 @@
+using System.Collections.Generic;
+
+using Dalamud.Game.Gui;
+using Dalamud.Interface.Internal.UiDebug2.Browsing;
+using Dalamud.Interface.Windowing;
+using Dalamud.Logging.Internal;
+using Dalamud.Plugin.Services;
+
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+using ImGuiNET;
+
+using static ImGuiNET.ImGuiWindowFlags;
+
+namespace Dalamud.Interface.Internal.UiDebug2;
+
+// Original version by aers https://github.com/aers/FFXIVUIDebug
+// Also incorporates features from Caraxi's fork https://github.com/Caraxi/SimpleTweaksPlugin/blob/main/Debugging/UIDebug.cs
+
+///
+/// A tool for browsing the contents and structure of UI elements.
+///
+internal partial class UiDebug2 : IDisposable
+{
+ private readonly ElementSelector elementSelector;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal UiDebug2()
+ {
+ this.elementSelector = new(this);
+
+ GameGui = Service.Get();
+
+ Log = new ModuleLog("UiDebug2");
+ }
+
+ ///
+ internal static ModuleLog Log { get; set; } = null!;
+
+ ///
+ internal static IGameGui GameGui { get; set; } = null!;
+
+ ///
+ /// Gets a collection of instances, each representing an .
+ ///
+ internal static Dictionary AddonTrees { get; } = [];
+
+ ///
+ /// Gets or sets a window system to handle any popout windows for addons or nodes.
+ ///
+ internal static WindowSystem PopoutWindows { get; set; } = new("UiDebugPopouts");
+
+ ///
+ /// Gets or sets the name of the currently-selected .
+ ///
+ internal string? SelectedAddonName { get; set; }
+
+ ///
+ /// Clears all windows and s.
+ ///
+ public void Dispose()
+ {
+ foreach (var a in AddonTrees)
+ {
+ a.Value.Dispose();
+ }
+
+ AddonTrees.Clear();
+ PopoutWindows.RemoveAllWindows();
+ this.elementSelector.Dispose();
+ }
+
+ ///
+ /// Draws the UiDebug tool's interface and contents.
+ ///
+ internal void Draw()
+ {
+ PopoutWindows.Draw();
+ this.DrawSidebar();
+ this.DrawMainPanel();
+ }
+
+ private void DrawMainPanel()
+ {
+ ImGui.SameLine();
+ ImGui.BeginChild("###uiDebugMainPanel", new(-1, -1), true, HorizontalScrollbar);
+
+ if (this.elementSelector.Active)
+ {
+ this.elementSelector.DrawSelectorOutput();
+ }
+ else
+ {
+ if (this.SelectedAddonName != null)
+ {
+ var addonTree = AddonTree.GetOrCreate(this.SelectedAddonName);
+
+ if (addonTree == null)
+ {
+ this.SelectedAddonName = null;
+ return;
+ }
+
+ addonTree.Draw();
+ }
+ }
+
+ ImGui.EndChild();
+ }
+}
diff --git a/Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs b/Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs
new file mode 100644
index 000000000..37b2f92cd
--- /dev/null
+++ b/Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs
@@ -0,0 +1,204 @@
+using System.Collections.Generic;
+using System.Numerics;
+
+using Dalamud.Interface.Components;
+using Dalamud.Interface.Utility;
+using FFXIVClientStructs.FFXIV.Client.Graphics;
+using ImGuiNET;
+
+using static Dalamud.Interface.ColorHelpers;
+using static ImGuiNET.ImGuiCol;
+
+namespace Dalamud.Interface.Internal.UiDebug2.Utility;
+
+///
+/// Miscellaneous ImGui tools used by .
+///
+internal static class Gui
+{
+ ///
+ /// Begins a tree node that also displays a colored line to the left while open.
+ ///
+ /// The label of the tree.
+ /// The color of the heading text.
+ /// A value representing where to begin drawing the left-side line.
+ /// Whether this tree should default to being open.
+ /// true if the tree is open.
+ internal static bool NestedTreePush(string label, Vector4 color, out Vector2 lineStart, bool defOpen = false)
+ {
+ ImGui.PushStyleColor(Text, color);
+ var result = NestedTreePush(label, out lineStart, defOpen);
+ ImGui.PopStyleColor();
+ return result;
+ }
+
+ ///
+ internal static bool NestedTreePush(string label, out Vector2 lineStart, bool defOpen = false)
+ {
+ var imGuiTreeNodeFlags = ImGuiTreeNodeFlags.SpanFullWidth;
+
+ if (defOpen)
+ {
+ imGuiTreeNodeFlags |= ImGuiTreeNodeFlags.DefaultOpen;
+ }
+
+ var treeNodeEx = ImGui.TreeNodeEx(label, imGuiTreeNodeFlags);
+ lineStart = ImGui.GetCursorScreenPos() + new Vector2(-10, 2);
+ return treeNodeEx;
+ }
+
+ ///
+ /// Completes a NestedTree.
+ ///
+ /// The starting position calculated when the tree was pushed.
+ /// The color of the left-side line.
+ internal static void NestedTreePop(Vector2 lineStart, Vector4? color = null)
+ {
+ var lineEnd = lineStart with { Y = ImGui.GetCursorScreenPos().Y - 7 };
+
+ if (lineStart.Y < lineEnd.Y)
+ {
+ ImGui.GetWindowDrawList().AddLine(lineStart, lineEnd, RgbaVector4ToUint(color ?? new(1)), 1);
+ }
+
+ ImGui.TreePop();
+ }
+
+ ///
+ /// A radio-button-esque input that uses Fontawesome icon buttons.
+ ///
+ /// The type of value being set.
+ /// The label for the inputs.
+ /// The value being set.
+ /// A list of all options to create buttons for.
+ /// A list of the icons to use for each option.
+ /// true if a button is clicked.
+ internal static unsafe bool IconSelectInput(string label, ref T val, List options, List icons)
+ {
+ var ret = false;
+ for (var i = 0; i < options.Count; i++)
+ {
+ var option = options[i];
+ var icon = icons[i];
+
+ if (i > 0)
+ {
+ ImGui.SameLine();
+ ImGui.SetCursorPosX(ImGui.GetCursorPosX() - ((ImGui.GetFontSize() / -6f) + 7f));
+ }
+
+ var color = *ImGui.GetStyleColorVec4(val is not null && val.Equals(option) ? ButtonActive : Button);
+
+ if (ImGuiComponents.IconButton($"{label}{option}{i}", icon, color))
+ {
+ val = option;
+ ret = true;
+ }
+ }
+
+ return ret;
+ }
+
+ ///
+ /// Prints field name and its value.
+ ///
+ /// The name of the field.
+ /// The value of the field.
+ /// Whether to enable click-to-copy.
+ internal static void PrintFieldValuePair(string fieldName, string value, bool copy = true)
+ {
+ ImGui.Text($"{fieldName}:");
+ ImGui.SameLine();
+ if (copy)
+ {
+ ClickToCopyText(value);
+ }
+ else
+ {
+ ImGui.TextColored(new(0.6f, 0.6f, 0.6f, 1), value);
+ }
+ }
+
+ ///
+ /// Prints a set of fields and their values.
+ ///
+ /// Tuples of fieldnames and values to display.
+ internal static void PrintFieldValuePairs(params (string FieldName, string Value)[] pairs)
+ {
+ for (var i = 0; i < pairs.Length; i++)
+ {
+ if (i != 0)
+ {
+ ImGui.SameLine();
+ }
+
+ PrintFieldValuePair(pairs[i].FieldName, pairs[i].Value, false);
+ }
+ }
+
+ ///
+ internal static void PrintColor(ByteColor color, string fmt) => PrintColor(RgbaUintToVector4(color.RGBA), fmt);
+
+ ///
+ internal static void PrintColor(Vector3 color, string fmt) => PrintColor(new Vector4(color, 1), fmt);
+
+ ///
+ /// Prints a text string representing a color, with a backdrop in that color.
+ ///
+ /// The color value.
+ /// The text string to print.
+ /// Colors the text itself either white or black, depending on the luminosity of the background color.
+ internal static void PrintColor(Vector4 color, string fmt)
+ {
+ static double Luminosity(Vector4 vector4) =>
+ Math.Pow(
+ (Math.Pow(vector4.X, 2) * 0.299f) +
+ (Math.Pow(vector4.Y, 2) * 0.587f) +
+ (Math.Pow(vector4.Z, 2) * 0.114f),
+ 0.5f) * vector4.W;
+
+ ImGui.PushStyleColor(Text, Luminosity(color) < 0.5f ? new Vector4(1) : new(0, 0, 0, 1));
+ ImGui.PushStyleColor(Button, color);
+ ImGui.PushStyleColor(ButtonActive, color);
+ ImGui.PushStyleColor(ButtonHovered, color);
+ ImGui.SmallButton(fmt);
+ ImGui.PopStyleColor(4);
+ }
+
+ ///
+ internal static void ClickToCopyText(string text, string? textCopy = null)
+ {
+ ImGui.PushStyleColor(Text, new Vector4(0.6f, 0.6f, 0.6f, 1));
+ ImGuiHelpers.ClickToCopyText(text, textCopy);
+ ImGui.PopStyleColor();
+
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip($"{textCopy ?? text}");
+ }
+ }
+
+ ///
+ /// Draws a tooltip that changes based on the cursor's x-position within the hovered item.
+ ///
+ /// The text for each section.
+ /// true if the item is hovered.
+ internal static bool SplitTooltip(params string[] tooltips)
+ {
+ if (!ImGui.IsItemHovered())
+ {
+ return false;
+ }
+
+ var mouseX = ImGui.GetMousePos().X;
+ var minX = ImGui.GetItemRectMin().X;
+ var maxX = ImGui.GetItemRectMax().X;
+ var prog = (mouseX - minX) / (maxX - minX);
+
+ var index = (int)Math.Floor(prog * tooltips.Length);
+
+ ImGui.SetTooltip(tooltips[index]);
+
+ return true;
+ }
+}
diff --git a/Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs b/Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs
new file mode 100644
index 000000000..82bf8f96c
--- /dev/null
+++ b/Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs
@@ -0,0 +1,170 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+
+using Dalamud.Interface.Utility;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using ImGuiNET;
+
+using static System.Math;
+
+namespace Dalamud.Interface.Internal.UiDebug2.Utility;
+
+///
+/// A struct representing the perimeter of an , accounting for all transformations.
+///
+public unsafe struct NodeBounds
+{
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The node to calculate the bounds of.
+ internal NodeBounds(AtkResNode* node)
+ {
+ if (node == null)
+ {
+ return;
+ }
+
+ var w = node->Width;
+ var h = node->Height;
+ this.Points = w == 0 && h == 0 ?
+ new() { new(0) } :
+ new() { new(0), new(w, 0), new(w, h), new(0, h) };
+
+ this.TransformPoints(node);
+ }
+
+ ///
+ /// Initializes a new instance of the struct, containing only a single given point.
+ ///
+ /// The point onscreen.
+ /// The node used to calculate transformations.
+ internal NodeBounds(Vector2 point, AtkResNode* node)
+ {
+ this.Points = [point];
+ this.TransformPoints(node);
+ }
+
+ private List Points { get; set; } = [];
+
+ ///
+ /// Draws the bounds onscreen.
+ ///
+ /// The color of line to use.
+ /// The thickness of line to use.
+ /// If there is only a single point to draw, it will be indicated with a circle and dot.
+ internal readonly void Draw(Vector4 col, int thickness = 1)
+ {
+ if (this.Points == null || this.Points.Count == 0)
+ {
+ return;
+ }
+
+ if (this.Points.Count == 1)
+ {
+ ImGui.GetBackgroundDrawList().AddCircle(this.Points[0], 10, ColorHelpers.RgbaVector4ToUint(col with { W = col.W / 2 }), 12, thickness);
+ ImGui.GetBackgroundDrawList().AddCircle(this.Points[0], thickness, ColorHelpers.RgbaVector4ToUint(col), 12, thickness + 1);
+ }
+ else
+ {
+ var path = new ImVectorWrapper(this.Points.Count);
+ foreach (var p in this.Points)
+ {
+ path.Add(p);
+ }
+
+ ImGui.GetBackgroundDrawList()
+ .AddPolyline(ref path[0], path.Length, ColorHelpers.RgbaVector4ToUint(col), ImDrawFlags.Closed, thickness);
+
+ path.Dispose();
+ }
+ }
+
+ ///
+ /// Draws the bounds onscreen, filled in.
+ ///
+ /// The fill and border color.
+ /// The border thickness.
+ internal readonly void DrawFilled(Vector4 col, int thickness = 1)
+ {
+ if (this.Points == null || this.Points.Count == 0)
+ {
+ return;
+ }
+
+ if (this.Points.Count == 1)
+ {
+ ImGui.GetBackgroundDrawList()
+ .AddCircleFilled(this.Points[0], 10, ColorHelpers.RgbaVector4ToUint(col with { W = col.W / 2 }), 12);
+ ImGui.GetBackgroundDrawList().AddCircle(this.Points[0], 10, ColorHelpers.RgbaVector4ToUint(col), 12, thickness);
+ }
+ else
+ {
+ var path = new ImVectorWrapper(this.Points.Count);
+ foreach (var p in this.Points)
+ {
+ path.Add(p);
+ }
+
+ ImGui.GetBackgroundDrawList()
+ .AddConvexPolyFilled(ref path[0], path.Length, ColorHelpers.RgbaVector4ToUint(col with { W = col.W / 2 }));
+ ImGui.GetBackgroundDrawList()
+ .AddPolyline(ref path[0], path.Length, ColorHelpers.RgbaVector4ToUint(col), ImDrawFlags.Closed, thickness);
+
+ path.Dispose();
+ }
+ }
+
+ ///
+ /// Checks whether the bounds contain a given point.
+ ///
+ /// The point to check.
+ /// True if the point exists within the bounds.
+ internal readonly bool ContainsPoint(Vector2 p)
+ {
+ var count = this.Points.Count;
+ var inside = false;
+
+ for (var i = 0; i < count; i++)
+ {
+ var p1 = this.Points[i];
+ var p2 = this.Points[(i + 1) % count];
+
+ if (p.Y > Min(p1.Y, p2.Y) &&
+ p.Y <= Max(p1.Y, p2.Y) &&
+ p.X <= Max(p1.X, p2.X) &&
+ (p1.X.Equals(p2.X) || p.X <= ((p.Y - p1.Y) * (p2.X - p1.X) / (p2.Y - p1.Y)) + p1.X))
+ {
+ inside = !inside;
+ }
+ }
+
+ return inside;
+ }
+
+ private static Vector2 TransformPoint(Vector2 p, Vector2 o, float r, Vector2 s)
+ {
+ var cosR = (float)Cos(r);
+ var sinR = (float)Sin(r);
+ var d = (p - o) * s;
+
+ return new(o.X + (d.X * cosR) - (d.Y * sinR),
+ o.Y + (d.X * sinR) + (d.Y * cosR));
+ }
+
+ private void TransformPoints(AtkResNode* transformNode)
+ {
+ while (transformNode != null)
+ {
+ var offset = new Vector2(transformNode->X, transformNode->Y);
+ var origin = offset + new Vector2(transformNode->OriginX, transformNode->OriginY);
+ var rotation = transformNode->Rotation;
+ var scale = new Vector2(transformNode->ScaleX, transformNode->ScaleY);
+
+ this.Points = this.Points.Select(b => TransformPoint(b + offset, origin, rotation, scale)).ToList();
+
+ transformNode = transformNode->ParentNode;
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs
index d4bea2931..33db51c75 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs
@@ -1,11 +1,11 @@
-namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
+namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
///
/// Widget for displaying addon inspector.
///
internal class AddonInspectorWidget : IDataWindowWidget
{
- private UiDebug? addonInspector;
+ private UiDebug2.UiDebug2? addonInspector;
///
public string[]? CommandShortcuts { get; init; } = { "ai", "addoninspector" };
@@ -19,7 +19,7 @@ internal class AddonInspectorWidget : IDataWindowWidget
///
public void Load()
{
- this.addonInspector = new UiDebug();
+ this.addonInspector = new UiDebug2.UiDebug2();
if (this.addonInspector is not null)
{