using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Components;
using Dalamud.Interface.Internal.UiDebug.Browsing;
using Dalamud.Interface.Internal.UiDebug.Utility;
using Dalamud.Interface.Utility.Raii;
using FFXIVClientStructs.FFXIV.Component.GUI;
using static System.Globalization.NumberFormatInfo;
using static Dalamud.Bindings.ImGui.ImGuiCol;
using static Dalamud.Bindings.ImGui.ImGuiWindowFlags;
using static Dalamud.Interface.FontAwesomeIcon;
using static Dalamud.Interface.Internal.UiDebug.UiDebug;
using static Dalamud.Interface.UiBuilder;
using static Dalamud.Interface.Utility.ImGuiHelpers;
using static FFXIVClientStructs.FFXIV.Component.GUI.NodeFlags;
// ReSharper disable StructLacksIEquatable.Global
#pragma warning disable CS0659
namespace Dalamud.Interface.Internal.UiDebug;
///
/// 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 UiDebug uiDebug;
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(UiDebug uiDebug)
{
this.uiDebug = uiDebug;
}
///
/// 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()
{
using var ch = ImRaii.Child("###sidebar_elementSelector"u8, new(250, -1), true);
if (ch.Success)
{
using (ImRaii.PushFont(IconFont))
{
using (ImRaii.PushColor(Text, new Vector4(1, 1, 0.2f, 1), this.Active))
{
if (ImGui.Button($"{(char)ObjectUngroup}"))
{
this.Active = !this.Active;
}
if (Countdown > 0)
{
Countdown -= 1;
if (Countdown < 0)
{
Countdown = 0;
}
}
}
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Element Selector"u8);
}
ImGui.SameLine();
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 32);
ImGui.InputTextWithHint(
"###addressSearchInput"u8,
"Address Search"u8,
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);
}
}
}
///
/// 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"u8);
ImGui.TextDisabled("Use the mouse to hover and identify UI elements, then click to jump to them in the inspector"u8);
ImGui.TextDisabled("Use the scrollwheel to choose between overlapping elements"u8);
ImGui.TextDisabled("Press ESCAPE to cancel"u8);
ImGui.Spacing();
var mousePos = ImGui.GetMousePos() - MainViewport.Pos;
var addonResults = GetAtkUnitBaseAtPosition(mousePos);
using (ImRaii.PushColor(WindowBg, new Vector4(0.5f)))
{
using var ch = ImRaii.Child("noClick"u8, new(800, 2000), false, NoInputs | NoBackground | NoScrollWithMouse);
if (ch.Success)
{
using var gr = ImRaii.Group();
Gui.PrintFieldValuePair("Mouse Position", $"{mousePos.X}, {mousePos.Y}");
ImGui.Spacing();
ImGui.Text("RESULTS:\n"u8);
var i = 0;
foreach (var a in addonResults)
{
var name = a.Addon->NameString;
ImGui.Text($"[Addon] {name}");
using var indent = ImRaii.PushIndent(15.0f);
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.uiDebug.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));
}
}
}
if (i != 0)
{
this.index -= (int)ImGui.GetIO().MouseWheel;
while (this.index < 0)
{
this.index += i;
}
while (this.index >= i)
{
this.index -= i;
}
}
}
}
}
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;
}
using (ImRaii.PushColor(Text, selected ? new Vector4(1, 1, 0.2f, 1) : new(0.6f, 0.6f, 0.6f, 1)))
{
ResNodeTree.GetOrCreate(node, tree).WriteTreeHeading();
}
}
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.uiDebug.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;
}
}
}