Files
HellionChat/HellionChat/Util/SearchSelector.cs
T
JonKazama-Hellion 699d4ede1d chore: housekeeping — linter & formatter setup
Add .prettierrc.json, .markdownlint.json, .yamllint.yaml, .gitattributes
Run CSharpier, Prettier and markdownlint across the entire codebase.
No logic changes — formatting, using order and line endings only.
2026-05-10 13:01:00 +02:00

202 lines
6.4 KiB
C#

using System.Collections;
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
namespace HellionChat.Util;
// Modified from: https://github.com/UnknownX7/Hypostasis/blob/master/ImGui/ExcelSheet.cs
public static class SearchSelector
{
private static string[]? FilteredSearchSheet;
private static string SheetSearchText = null!;
private static string PrevSearchId = null!;
private static Type PrevSearchType = null!;
public record SelectorOptions
{
public Func<string, string> FormatRow { get; init; } = row => row.ToString();
public Func<string, string, bool>? SearchPredicate { get; init; } = null;
public Func<string, bool, bool>? DrawSelectable { get; init; } = null;
public string[] FilteredSheet { get; init; } = [];
public Vector2? Size { get; init; } = null;
}
public record SelectorPopupOptions : SelectorOptions
{
public ImGuiPopupFlags PopupFlags { get; init; } = ImGuiPopupFlags.None;
public bool CloseOnSelection { get; init; } = false;
public Func<string, bool> IsSelected { get; init; } = _ => false;
}
private static void SearchInput(
string id,
IEnumerable<string> filteredSheet,
Func<string, string, bool> searchPredicate
)
{
if (ImGui.IsWindowAppearing() && ImGui.IsWindowFocused() && !ImGui.IsAnyItemActive())
{
if (id != PrevSearchId)
{
if (typeof(string) != PrevSearchType)
{
SheetSearchText = string.Empty;
PrevSearchType = typeof(string);
}
FilteredSearchSheet = null;
PrevSearchId = id;
}
ImGui.SetKeyboardFocusHere(0);
}
if (
ImGui.InputTextWithHint(
"##ExcelSheetSearch",
"Search",
ref SheetSearchText,
128,
ImGuiInputTextFlags.AutoSelectAll
)
)
FilteredSearchSheet = null;
FilteredSearchSheet ??= filteredSheet
.Where(s => searchPredicate(s, SheetSearchText))
.ToArray();
}
public static bool SelectorPopup(
string id,
out string selected,
SelectorPopupOptions? options = null,
bool close = false
)
{
options ??= new SelectorPopupOptions();
var sheet = options.FilteredSheet;
selected = string.Empty;
if (close)
return false;
ImGui.SetNextWindowSize(options.Size ?? new Vector2(0, 250 * ImGuiHelpers.GlobalScale));
using var popup = ImRaii.ContextPopupItem(id, options.PopupFlags);
if (!popup.Success)
return false;
SearchInput(
id,
sheet,
options.SearchPredicate
?? (
(row, s) =>
options
.FormatRow(row)
.Contains(s, StringComparison.CurrentCultureIgnoreCase)
)
);
using var child = ImRaii.Child("SearchList", Vector2.Zero, true);
if (!child.Success)
return false;
var ret = false;
var drawSelectable =
options.DrawSelectable
?? ((row, selected) => ImGui.Selectable(options.FormatRow(row), selected));
using (var clipper = new ListClipper(FilteredSearchSheet!.Length))
{
foreach (var i in clipper.Rows)
{
var searched = FilteredSearchSheet[i];
// Mix the row index into the ImGui ID so each Selectable in
// the loop has a distinct ID — using the same id for every
// row collapsed all rows to a single ID-stack entry and made
// selection ambiguous.
using var pushedId = ImRaii.PushId($"{id}##{i}");
if (!drawSelectable(searched, options.IsSelected(searched)))
continue;
selected = searched;
ret = true;
}
}
// ImGui issue #273849, children keep popups from closing automatically
if (ret && options.CloseOnSelection)
ImGui.CloseCurrentPopup();
return ret;
}
}
public unsafe class ListClipper : IEnumerable<(int, int)>, IDisposable
{
private ImGuiListClipperPtr Clipper;
private readonly int CurrentRows;
private readonly int CurrentColumns;
private readonly bool TwoDimensional;
private readonly int ItemRemainder;
public int FirstRow { get; private set; } = -1;
public int CurrentRow { get; private set; }
public int DisplayEnd => Clipper.DisplayEnd;
public IEnumerable<int> Rows
{
get
{
while (Clipper.Step()) // Supposedly this calls End()
{
if (Clipper.ItemsHeight > 0 && FirstRow < 0)
FirstRow = (int)(ImGui.GetScrollY() / Clipper.ItemsHeight);
for (var i = Clipper.DisplayStart; i < Clipper.DisplayEnd; i++)
{
CurrentRow = i;
yield return TwoDimensional ? i : i * CurrentColumns;
}
}
}
}
private IEnumerable<int> Columns
{
get
{
var cols =
(ItemRemainder == 0 || CurrentRows != DisplayEnd || CurrentRow != DisplayEnd - 1)
? CurrentColumns
: ItemRemainder;
for (var j = 0; j < cols; j++)
yield return j;
}
}
public ListClipper(int items, int cols = 1, bool twoD = false, float itemHeight = 0)
{
TwoDimensional = twoD;
CurrentColumns = cols;
CurrentRows = TwoDimensional ? items : (int)MathF.Ceiling((float)items / CurrentColumns);
ItemRemainder = !TwoDimensional ? items % CurrentColumns : 0;
Clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper());
Clipper.Begin(CurrentRows, itemHeight);
}
public IEnumerator<(int, int)> GetEnumerator() =>
(from i in Rows from j in Columns select (i, j)).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public void Dispose()
{
Clipper.Destroy(); // This also calls End() but I'm calling it anyway just in case
GC.SuppressFinalize(this);
}
}