From bf8690fc601b440b2487b7acf7162f313aa32a2f Mon Sep 17 00:00:00 2001 From: ItsBexy Date: Mon, 2 Sep 2024 21:01:49 -0600 Subject: [PATCH] Update Addon Inspector This updates the Addon Inspector with lots of new features and functionality. - Features from Caraxi's fork of UiDebug have been incorporated, such as the Element Selector UI and address search. - Any addon or node can now pop out into its own window. - Revised the visual style of node field/property information. - Color values are now visually displayed. - Any nodes or components that are referenced by fields within the addon will now show that field name in the inspector. - Added editors for nodes, allowing complete control over most of their properties. - Improved texture display for Image nodes (and Image node variant types). The active part of the texture is now highlighted, and the boundaries of other parts can be shown via mouseover. - Highlighting of node bounds onscreen is now more accurate, factoring in rotation (including when using the Element Selector). - Display of animation timelines has been revamped, showing a table of keyframes for each animation. A standalone SamplePlugin-based version is available here: https://github.com/ItsBexy/UiDebug2 --- .../UiDebug2/Browsing/AddonTree.FieldNames.cs | 196 ++++++++ .../Internal/UiDebug2/Browsing/AddonTree.cs | 242 +++++++++ .../Internal/UiDebug2/Browsing/Events.cs | 68 +++ .../Browsing/NodeTree.ClippingMask.cs | 35 ++ .../UiDebug2/Browsing/NodeTree.Collision.cs | 24 + .../UiDebug2/Browsing/NodeTree.Component.cs | 283 +++++++++++ .../UiDebug2/Browsing/NodeTree.Counter.cs | 36 ++ .../UiDebug2/Browsing/NodeTree.Editor.cs | 384 ++++++++++++++ .../UiDebug2/Browsing/NodeTree.Image.cs | 317 ++++++++++++ .../Browsing/NodeTree.NineGrid.Offsets.cs | 69 +++ .../UiDebug2/Browsing/NodeTree.NineGrid.cs | 89 ++++ .../UiDebug2/Browsing/NodeTree.Res.cs | 402 +++++++++++++++ .../UiDebug2/Browsing/NodeTree.Text.cs | 126 +++++ .../Browsing/TimelineTree.KeyGroupColumn.cs | 90 ++++ .../UiDebug2/Browsing/TimelineTree.cs | 381 ++++++++++++++ .../Internal/UiDebug2/ElementSelector.cs | 475 ++++++++++++++++++ .../Internal/UiDebug2/Popout.Addon.cs | 50 ++ .../Internal/UiDebug2/Popout.Node.cs | 69 +++ .../Internal/UiDebug2/UiDebug2.Sidebar.cs | 214 ++++++++ .../Interface/Internal/UiDebug2/UiDebug2.cs | 112 +++++ .../Internal/UiDebug2/Utility/Gui.cs | 204 ++++++++ .../Internal/UiDebug2/Utility/NodeBounds.cs | 170 +++++++ .../Data/Widgets/AddonInspectorWidget.cs | 6 +- 23 files changed, 4039 insertions(+), 3 deletions(-) create mode 100644 Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.FieldNames.cs create mode 100644 Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.cs create mode 100644 Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs create mode 100644 Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.ClippingMask.cs create mode 100644 Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Collision.cs create mode 100644 Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs create mode 100644 Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Counter.cs create mode 100644 Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs create mode 100644 Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs create mode 100644 Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.Offsets.cs create mode 100644 Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.cs create mode 100644 Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs create mode 100644 Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs create mode 100644 Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.KeyGroupColumn.cs create mode 100644 Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs create mode 100644 Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs create mode 100644 Dalamud/Interface/Internal/UiDebug2/Popout.Addon.cs create mode 100644 Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs create mode 100644 Dalamud/Interface/Internal/UiDebug2/UiDebug2.Sidebar.cs create mode 100644 Dalamud/Interface/Internal/UiDebug2/UiDebug2.cs create mode 100644 Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs create mode 100644 Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs 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) {