1.9.0.0 (Testing) Release
This commit is contained in:
+333
-96
@@ -1,130 +1,149 @@
|
||||
using Craftimizer.Utils;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using ImGuiNET;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Craftimizer.Plugin;
|
||||
|
||||
internal static class ImGuiUtils
|
||||
{
|
||||
private static readonly Stack<(Vector2 Min, Vector2 Max)> GroupPanelLabelStack = new();
|
||||
private static readonly Stack<(Vector2 Min, Vector2 Max, float TopPadding)> GroupPanelLabelStack = new();
|
||||
|
||||
// Adapted from https://github.com/ocornut/imgui/issues/1496#issuecomment-655048353
|
||||
public static void BeginGroupPanel(float width = -1, bool addPadding = true)
|
||||
// width = -1 -> size to parent
|
||||
// width = 0 -> size to content
|
||||
// returns available width (better since it accounts for the right side padding)
|
||||
// ^ only useful if width = -1
|
||||
public static float BeginGroupPanel(string name, float width)
|
||||
{
|
||||
// container group
|
||||
ImGui.BeginGroup();
|
||||
|
||||
var itemSpacing = ImGui.GetStyle().ItemSpacing;
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero);
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
|
||||
|
||||
var frameHeight = ImGui.GetFrameHeight();
|
||||
width = width < 0 ? ImGui.GetContentRegionAvail().X - (2 * itemSpacing.X) : width;
|
||||
var fullWidth = width > 0 ? width + (2 * itemSpacing.X) : 0;
|
||||
{
|
||||
using var noPadding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero);
|
||||
using var noSpacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
|
||||
|
||||
ImGui.BeginGroup();
|
||||
ImGui.Dummy(new Vector2(width < 0 ? ImGui.GetContentRegionAvail().X : width, 0));
|
||||
ImGui.Dummy(new Vector2(frameHeight * 0.5f, 0));
|
||||
ImGui.SameLine(0, 0);
|
||||
// inner group
|
||||
ImGui.BeginGroup();
|
||||
ImGui.Dummy(new Vector2(fullWidth, 0));
|
||||
ImGui.Dummy(new Vector2(itemSpacing.X, 0)); // shifts next group by is.x
|
||||
ImGui.SameLine(0, 0);
|
||||
|
||||
ImGui.BeginGroup();
|
||||
ImGui.Dummy(new Vector2(frameHeight * 0.5f, 0));
|
||||
GroupPanelLabelStack.Push((ImGui.GetItemRectMin(), ImGui.GetItemRectMax()));
|
||||
ImGui.SameLine(0, 0);
|
||||
ImGui.Dummy(new Vector2(0f, frameHeight * (addPadding ? 1 : .5f) + itemSpacing.Y));
|
||||
// label group
|
||||
ImGui.BeginGroup();
|
||||
ImGui.Dummy(new Vector2(frameHeight / 2, 0)); // shifts text by fh/2
|
||||
ImGui.SameLine(0, 0);
|
||||
var textFrameHeight = ImGui.GetFrameHeight();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted(name);
|
||||
GroupPanelLabelStack.Push((ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), textFrameHeight / 2f)); // push rect to stack
|
||||
ImGui.SameLine(0, 0);
|
||||
ImGui.Dummy(new Vector2(0f, textFrameHeight + itemSpacing.Y)); // shifts content by fh + is.y
|
||||
|
||||
ImGui.BeginGroup();
|
||||
// content group
|
||||
ImGui.BeginGroup();
|
||||
}
|
||||
|
||||
ImGui.PopStyleVar(2);
|
||||
|
||||
ImGui.PushItemWidth(MathF.Max(0, ImGui.CalcItemWidth() - frameHeight));
|
||||
}
|
||||
|
||||
public static void BeginGroupPanel(string name, float width = -1, bool addPadding = true)
|
||||
{
|
||||
ImGui.BeginGroup();
|
||||
|
||||
var itemSpacing = ImGui.GetStyle().ItemSpacing;
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero);
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
|
||||
|
||||
var frameHeight = ImGui.GetFrameHeight();
|
||||
|
||||
ImGui.BeginGroup();
|
||||
ImGui.Dummy(new Vector2(width < 0 ? ImGui.GetContentRegionAvail().X : width, 0));
|
||||
ImGui.Dummy(new Vector2(frameHeight * 0.5f, 0));
|
||||
ImGui.SameLine(0, 0);
|
||||
|
||||
ImGui.BeginGroup();
|
||||
ImGui.Dummy(new Vector2(frameHeight * 0.5f, 0));
|
||||
ImGui.SameLine(0, 0);
|
||||
ImGui.TextUnformatted(name);
|
||||
GroupPanelLabelStack.Push((ImGui.GetItemRectMin(), ImGui.GetItemRectMax()));
|
||||
ImGui.SameLine(0, 0);
|
||||
ImGui.Dummy(new Vector2(0f, frameHeight * (addPadding ? 1 : .5f) + itemSpacing.Y));
|
||||
|
||||
ImGui.BeginGroup();
|
||||
|
||||
ImGui.PopStyleVar(2);
|
||||
|
||||
ImGui.PushItemWidth(MathF.Max(0, ImGui.CalcItemWidth() - frameHeight));
|
||||
return width;
|
||||
}
|
||||
|
||||
public static void EndGroupPanel()
|
||||
{
|
||||
ImGui.PopItemWidth();
|
||||
|
||||
var itemSpacing = ImGui.GetStyle().ItemSpacing;
|
||||
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero);
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
|
||||
|
||||
var frameHeight = ImGui.GetFrameHeight();
|
||||
|
||||
ImGui.EndGroup();
|
||||
|
||||
ImGui.EndGroup();
|
||||
|
||||
ImGui.SameLine(0, 0);
|
||||
ImGui.Dummy(new Vector2(frameHeight * 0.5f, 0));
|
||||
ImGui.Dummy(new Vector2(0f, frameHeight * 0.5f - itemSpacing.Y));
|
||||
|
||||
ImGui.EndGroup();
|
||||
|
||||
var itemMin = ImGui.GetItemRectMin();
|
||||
var itemMax = ImGui.GetItemRectMax();
|
||||
var labelRect = GroupPanelLabelStack.Pop();
|
||||
|
||||
var halfFrame = new Vector2(frameHeight * 0.25f, frameHeight) * 0.5f;
|
||||
(Vector2 Min, Vector2 Max) frameRect = (itemMin + halfFrame, itemMax - new Vector2(halfFrame.X, 0));
|
||||
labelRect.Min.X -= itemSpacing.X;
|
||||
labelRect.Max.X += itemSpacing.X;
|
||||
for (var i = 0; i < 4; ++i)
|
||||
{
|
||||
var (minClip, maxClip) = i switch
|
||||
{
|
||||
0 => (new Vector2(float.NegativeInfinity), new Vector2(labelRect.Min.X, float.PositiveInfinity)),
|
||||
1 => (new Vector2(labelRect.Max.X, float.NegativeInfinity), new Vector2(float.PositiveInfinity)),
|
||||
2 => (new Vector2(labelRect.Min.X, float.NegativeInfinity), new Vector2(labelRect.Max.X, labelRect.Min.Y)),
|
||||
3 => (new Vector2(labelRect.Min.X, labelRect.Max.Y), new Vector2(labelRect.Max.X, float.PositiveInfinity)),
|
||||
_ => (Vector2.Zero, Vector2.Zero)
|
||||
};
|
||||
using var noPadding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero);
|
||||
using var noSpacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
|
||||
|
||||
ImGui.PushClipRect(minClip, maxClip, true);
|
||||
ImGui.GetWindowDrawList().AddRect(
|
||||
frameRect.Min, frameRect.Max,
|
||||
ImGui.GetColorU32(ImGuiCol.Border),
|
||||
halfFrame.X);
|
||||
ImGui.PopClipRect();
|
||||
// content group
|
||||
ImGui.EndGroup();
|
||||
|
||||
// label group
|
||||
ImGui.EndGroup();
|
||||
|
||||
ImGui.SameLine(0, 0);
|
||||
// shifts full size by is (for rect placement)
|
||||
ImGui.Dummy(new(itemSpacing.X, 0));
|
||||
ImGui.Dummy(new(0, itemSpacing.Y * 2)); // * 2 for some reason (otherwise the bottom is too skinny)
|
||||
|
||||
// inner group
|
||||
ImGui.EndGroup();
|
||||
|
||||
var labelRect = GroupPanelLabelStack.Pop();
|
||||
var innerMin = ImGui.GetItemRectMin() + new Vector2(0, labelRect.TopPadding);
|
||||
var innerMax = ImGui.GetItemRectMax();
|
||||
|
||||
(Vector2 Min, Vector2 Max) frameRect = (innerMin, innerMax);
|
||||
// add itemspacing padding on the label's sides
|
||||
labelRect.Min.X -= itemSpacing.X / 2;
|
||||
labelRect.Max.X += itemSpacing.X / 2;
|
||||
for (var i = 0; i < 4; ++i)
|
||||
{
|
||||
var (minClip, maxClip) = i switch
|
||||
{
|
||||
0 => (new Vector2(float.NegativeInfinity), new Vector2(labelRect.Min.X, float.PositiveInfinity)),
|
||||
1 => (new Vector2(labelRect.Max.X, float.NegativeInfinity), new Vector2(float.PositiveInfinity)),
|
||||
2 => (new Vector2(labelRect.Min.X, float.NegativeInfinity), new Vector2(labelRect.Max.X, labelRect.Min.Y)),
|
||||
3 => (new Vector2(labelRect.Min.X, labelRect.Max.Y), new Vector2(labelRect.Max.X, float.PositiveInfinity)),
|
||||
_ => (Vector2.Zero, Vector2.Zero)
|
||||
};
|
||||
|
||||
ImGui.PushClipRect(minClip, maxClip, true);
|
||||
ImGui.GetWindowDrawList().AddRect(
|
||||
frameRect.Min, frameRect.Max,
|
||||
ImGui.GetColorU32(ImGuiCol.Border),
|
||||
itemSpacing.X);
|
||||
ImGui.PopClipRect();
|
||||
}
|
||||
|
||||
ImGui.Dummy(Vector2.Zero);
|
||||
}
|
||||
|
||||
ImGui.PopStyleVar(2);
|
||||
|
||||
ImGui.Dummy(Vector2.Zero);
|
||||
|
||||
ImGui.EndGroup();
|
||||
}
|
||||
|
||||
private struct EndUnconditionally : ImRaii.IEndObject, IDisposable
|
||||
{
|
||||
private Action EndAction { get; }
|
||||
|
||||
public bool Success { get; }
|
||||
|
||||
public bool Disposed { get; private set; }
|
||||
|
||||
public EndUnconditionally(Action endAction, bool success)
|
||||
{
|
||||
EndAction = endAction;
|
||||
Success = success;
|
||||
Disposed = false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!Disposed)
|
||||
{
|
||||
EndAction();
|
||||
Disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static ImRaii.IEndObject GroupPanel(string name, float width, out float internalWidth)
|
||||
{
|
||||
internalWidth = BeginGroupPanel(name, width);
|
||||
return new EndUnconditionally(EndGroupPanel, true);
|
||||
}
|
||||
|
||||
private static Vector2 UnitCircle(float theta)
|
||||
{
|
||||
var (s, c) = MathF.SinCos(theta);
|
||||
@@ -156,7 +175,7 @@ internal static class ImGuiUtils
|
||||
|
||||
var offset = ImGui.GetCursorScreenPos() + new Vector2(radius);
|
||||
|
||||
var segments = ImGui.GetWindowDrawList()._CalcCircleAutoSegmentCount(radius * 2);
|
||||
var segments = ImGui.GetWindowDrawList()._CalcCircleAutoSegmentCount(radius);
|
||||
var incrementAngle = MathF.Tau / segments;
|
||||
var isFullCircle = (endAngle - startAngle) % MathF.Tau == 0;
|
||||
|
||||
@@ -215,6 +234,216 @@ internal static class ImGuiUtils
|
||||
Arc(MathF.PI / 2, MathF.PI / 2 - MathF.Tau * Math.Clamp(value, 0, 1), radiusInner, radiusOuter, backgroundColor, filledColor);
|
||||
}
|
||||
|
||||
private sealed class SearchableComboData<T> where T : class
|
||||
{
|
||||
public readonly ImmutableArray<T> items;
|
||||
public List<T> filteredItems;
|
||||
public T selectedItem;
|
||||
public string input;
|
||||
public bool wasTextActive;
|
||||
public bool wasPopupActive;
|
||||
public CancellationTokenSource? cts;
|
||||
public Task? task;
|
||||
|
||||
private readonly Func<T, string> getString;
|
||||
|
||||
public SearchableComboData(IEnumerable<T> items, T selectedItem, Func<T, string> getString)
|
||||
{
|
||||
this.items = items.ToImmutableArray();
|
||||
filteredItems = new() { selectedItem };
|
||||
this.selectedItem = selectedItem;
|
||||
this.getString = getString;
|
||||
input = GetString(selectedItem);
|
||||
}
|
||||
|
||||
public void SetItem(T selectedItem)
|
||||
{
|
||||
if (this.selectedItem != selectedItem)
|
||||
{
|
||||
input = GetString(selectedItem);
|
||||
this.selectedItem = selectedItem;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetString(T item) => getString(item);
|
||||
|
||||
public void Filter()
|
||||
{
|
||||
cts?.Cancel();
|
||||
var inp = input;
|
||||
cts = new();
|
||||
var token = cts.Token;
|
||||
task = Task.Run(() => FilterTask(inp, token), token)
|
||||
.ContinueWith(t =>
|
||||
{
|
||||
if (cts.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
t.Exception!.Flatten().Handle(ex => ex is TaskCanceledException or OperationCanceledException);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Filtering recipes failed");
|
||||
}
|
||||
}, TaskContinuationOptions.OnlyOnFaulted);
|
||||
}
|
||||
|
||||
private void FilterTask(string input, CancellationToken token)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
filteredItems = items.ToList();
|
||||
return;
|
||||
}
|
||||
var matcher = new FuzzyMatcher(input.ToLowerInvariant(), MatchMode.FuzzyParts);
|
||||
var query = items.AsParallel().Select(i => (Item: i, Score: matcher.Matches(getString(i).ToLowerInvariant())))
|
||||
.Where(t => t.Score > 0)
|
||||
.OrderByDescending(t => t.Score)
|
||||
.Select(t => t.Item);
|
||||
token.ThrowIfCancellationRequested();
|
||||
filteredItems = query.ToList();
|
||||
}
|
||||
}
|
||||
private static readonly Dictionary<uint, object> ComboData = new();
|
||||
|
||||
private static SearchableComboData<T> GetComboData<T>(uint comboKey, IEnumerable<T> items, T selectedItem, Func<T, string> getString) where T : class =>
|
||||
(SearchableComboData<T>)(
|
||||
ComboData.TryGetValue(comboKey, out var data)
|
||||
? data
|
||||
: ComboData[comboKey] = new SearchableComboData<T>(items, selectedItem, getString));
|
||||
|
||||
// https://github.com/ocornut/imgui/issues/718#issuecomment-1563162222
|
||||
public static bool SearchableCombo<T>(string id, ref T selectedItem, IEnumerable<T> items, ImFontPtr selectableFont, float width, Func<T, string> getString, Func<T, string> getId, Action<T> draw) where T : class
|
||||
{
|
||||
var comboKey = ImGui.GetID(id);
|
||||
var data = GetComboData(comboKey, items, selectedItem, getString);
|
||||
data.SetItem(selectedItem);
|
||||
|
||||
using var pushId = ImRaii.PushId(id);
|
||||
|
||||
width = width == 0 ? ImGui.GetContentRegionAvail().X : width;
|
||||
var availableSpace = Math.Min(ImGui.GetContentRegionAvail().X, width);
|
||||
ImGui.SetNextItemWidth(availableSpace);
|
||||
var isInputTextEnterPressed = ImGui.InputText("##input", ref data.input, 256, ImGuiInputTextFlags.EnterReturnsTrue);
|
||||
var min = ImGui.GetItemRectMin();
|
||||
var size = ImGui.GetItemRectSize();
|
||||
size.X = Math.Min(size.X, availableSpace);
|
||||
|
||||
var isInputTextActivated = ImGui.IsItemActivated();
|
||||
|
||||
if (isInputTextActivated)
|
||||
{
|
||||
ImGui.SetNextWindowPos(min - ImGui.GetStyle().WindowPadding);
|
||||
ImGui.OpenPopup("##popup");
|
||||
data.wasTextActive = false;
|
||||
}
|
||||
|
||||
using (var popup = ImRaii.Popup("##popup", ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoSavedSettings))
|
||||
{
|
||||
if (popup)
|
||||
{
|
||||
data.wasPopupActive = true;
|
||||
|
||||
if (isInputTextActivated)
|
||||
{
|
||||
ImGui.SetKeyboardFocusHere(0);
|
||||
data.Filter();
|
||||
}
|
||||
ImGui.SetNextItemWidth(size.X);
|
||||
if (ImGui.InputText("##input_popup", ref data.input, 256))
|
||||
data.Filter();
|
||||
var isActive = ImGui.IsItemActive();
|
||||
if (!isActive && data.wasTextActive && ImGui.IsKeyPressed(ImGuiKey.Enter))
|
||||
isInputTextEnterPressed = true;
|
||||
data.wasTextActive = isActive;
|
||||
|
||||
using (var scrollingRegion = ImRaii.Child("scrollingRegion", new Vector2(size.X, size.Y * 10), false, ImGuiWindowFlags.HorizontalScrollbar))
|
||||
{
|
||||
T? _selectedItem = default;
|
||||
var height = ImGui.GetTextLineHeight();
|
||||
var r = ListClip(data.filteredItems, height, t =>
|
||||
{
|
||||
var name = getString(t);
|
||||
using (var selectFont = ImRaii.PushFont(selectableFont))
|
||||
{
|
||||
if (ImGui.Selectable($"##{getId(t)}"))
|
||||
{
|
||||
_selectedItem = t;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
ImGui.SameLine(0, ImGui.GetStyle().ItemSpacing.X / 2f);
|
||||
draw(t);
|
||||
return false;
|
||||
});
|
||||
if (r)
|
||||
{
|
||||
selectedItem = _selectedItem!;
|
||||
data.SetItem(selectedItem);
|
||||
ImGui.CloseCurrentPopup();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isInputTextEnterPressed || ImGui.IsKeyPressed(ImGuiKey.Escape))
|
||||
{
|
||||
if (isInputTextEnterPressed && data.filteredItems.Count > 0)
|
||||
{
|
||||
selectedItem = data.filteredItems[0];
|
||||
data.SetItem(selectedItem);
|
||||
}
|
||||
ImGui.CloseCurrentPopup();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (data.wasPopupActive)
|
||||
{
|
||||
data.wasPopupActive = false;
|
||||
data.input = getString(selectedItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool ListClip<T>(IReadOnlyList<T> data, float lineHeight, Predicate<T> func)
|
||||
{
|
||||
ImGuiListClipperPtr imGuiListClipperPtr;
|
||||
unsafe
|
||||
{
|
||||
imGuiListClipperPtr = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper());
|
||||
}
|
||||
try
|
||||
{
|
||||
imGuiListClipperPtr.Begin(data.Count, lineHeight);
|
||||
while (imGuiListClipperPtr.Step())
|
||||
{
|
||||
for (var i = imGuiListClipperPtr.DisplayStart; i <= imGuiListClipperPtr.DisplayEnd; i++)
|
||||
{
|
||||
if (i >= data.Count)
|
||||
return false;
|
||||
|
||||
if (i >= 0)
|
||||
{
|
||||
if (func(data[i]))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
imGuiListClipperPtr.End();
|
||||
imGuiListClipperPtr.Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IconButtonSized(FontAwesomeIcon icon, Vector2 size)
|
||||
{
|
||||
ImGui.PushFont(UiBuilder.IconFont);
|
||||
@@ -254,6 +483,14 @@ internal static class ImGuiUtils
|
||||
ImGui.SetCursorPosX(ImGui.GetCursorPos().X + (availWidth - width) / 2);
|
||||
}
|
||||
|
||||
public static void AlignRight(float width, float availWidth = default)
|
||||
{
|
||||
if (availWidth == default)
|
||||
availWidth = ImGui.GetContentRegionAvail().X;
|
||||
if (availWidth > width)
|
||||
ImGui.SetCursorPosX(ImGui.GetCursorPos().X + availWidth - width);
|
||||
}
|
||||
|
||||
public static void AlignMiddle(Vector2 size, Vector2 availSize = default)
|
||||
{
|
||||
if (availSize == default)
|
||||
@@ -271,9 +508,9 @@ internal static class ImGuiUtils
|
||||
ImGui.TextUnformatted(text);
|
||||
}
|
||||
|
||||
public static void TextMiddle(string text, Vector2 availSize = default)
|
||||
public static void TextRight(string text, float availWidth = default)
|
||||
{
|
||||
AlignMiddle(ImGui.CalcTextSize(text), availSize);
|
||||
AlignRight(ImGui.CalcTextSize(text).X, availWidth);
|
||||
ImGui.TextUnformatted(text);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user