From 6bf1376515df449ba6ec15d45980c838281141d2 Mon Sep 17 00:00:00 2001 From: goat Date: Mon, 6 Mar 2023 20:52:21 +0100 Subject: [PATCH] chore: add raii, tables code from OtterGui into new Dalamud.Interface assembly --- Dalamud.Interface/ArrayExtensions.cs | 85 +++++++ Dalamud.Interface/Dalamud.Interface.csproj | 17 ++ Dalamud.Interface/ImGuiClip.cs | 144 +++++++++++ Dalamud.Interface/ImGuiTable.cs | 41 +++ Dalamud.Interface/InterfaceHelpers.cs | 6 + Dalamud.Interface/Raii/Color.cs | 66 +++++ Dalamud.Interface/Raii/EndObjects.cs | 239 ++++++++++++++++++ Dalamud.Interface/Raii/Font.cs | 51 ++++ Dalamud.Interface/Raii/Id.cs | 66 +++++ Dalamud.Interface/Raii/Indent.cs | 70 +++++ Dalamud.Interface/Raii/Style.cs | 147 +++++++++++ .../StableInsertionSortExtension.cs | 29 +++ Dalamud.Interface/Table/Column.cs | 34 +++ Dalamud.Interface/Table/ColumnFlags.cs | 65 +++++ Dalamud.Interface/Table/ColumnSelect.cs | 47 ++++ Dalamud.Interface/Table/ColumnString.cs | 56 ++++ Dalamud.Interface/Table/Table.cs | 172 +++++++++++++ Dalamud.sln | 14 + Dalamud/Dalamud.csproj | 1 + Dalamud/GlobalSuppressions.cs | 1 + Dalamud/Interface/ImGuiHelpers.cs | 1 + targets/Dalamud.Plugin.targets | 4 + 22 files changed, 1356 insertions(+) create mode 100644 Dalamud.Interface/ArrayExtensions.cs create mode 100644 Dalamud.Interface/Dalamud.Interface.csproj create mode 100644 Dalamud.Interface/ImGuiClip.cs create mode 100644 Dalamud.Interface/ImGuiTable.cs create mode 100644 Dalamud.Interface/InterfaceHelpers.cs create mode 100644 Dalamud.Interface/Raii/Color.cs create mode 100644 Dalamud.Interface/Raii/EndObjects.cs create mode 100644 Dalamud.Interface/Raii/Font.cs create mode 100644 Dalamud.Interface/Raii/Id.cs create mode 100644 Dalamud.Interface/Raii/Indent.cs create mode 100644 Dalamud.Interface/Raii/Style.cs create mode 100644 Dalamud.Interface/StableInsertionSortExtension.cs create mode 100644 Dalamud.Interface/Table/Column.cs create mode 100644 Dalamud.Interface/Table/ColumnFlags.cs create mode 100644 Dalamud.Interface/Table/ColumnSelect.cs create mode 100644 Dalamud.Interface/Table/ColumnString.cs create mode 100644 Dalamud.Interface/Table/Table.cs diff --git a/Dalamud.Interface/ArrayExtensions.cs b/Dalamud.Interface/ArrayExtensions.cs new file mode 100644 index 000000000..68bf52a29 --- /dev/null +++ b/Dalamud.Interface/ArrayExtensions.cs @@ -0,0 +1,85 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Dalamud.Interface; + +internal static class ArrayExtensions +{ + /// Iterate over enumerables with additional index. + public static IEnumerable<(T Value, int Index)> WithIndex(this IEnumerable list) + => list.Select((x, i) => (x, i)); + + /// Remove an added index from an indexed enumerable. + public static IEnumerable WithoutIndex(this IEnumerable<(T Value, int Index)> list) + => list.Select(x => x.Value); + + /// Remove the value and only keep the index from an indexed enumerable. + public static IEnumerable WithoutValue(this IEnumerable<(T Value, int Index)> list) + => list.Select(x => x.Index); + + + // Find the index of the first object fulfilling predicate's criteria in the given list. + // Returns -1 if no such object is found. + public static int IndexOf(this IEnumerable array, Predicate predicate) + { + var i = 0; + foreach (var obj in array) + { + if (predicate(obj)) + return i; + + ++i; + } + + return -1; + } + + // Find the index of the first occurrence of needle in the given list. + // Returns -1 if needle is not contained in the list. + public static int IndexOf(this IEnumerable array, T needle) where T : notnull + { + var i = 0; + foreach (var obj in array) + { + if (needle.Equals(obj)) + return i; + + ++i; + } + + return -1; + } + + // Find the first object fulfilling predicate's criteria in the given list, if one exists. + // Returns true if an object is found, false otherwise. + public static bool FindFirst(this IEnumerable array, Predicate predicate, [NotNullWhen(true)] out T? result) + { + foreach (var obj in array) + { + if (predicate(obj)) + { + result = obj!; + return true; + } + } + + result = default; + return false; + } + + // Find the first occurrence of needle in the given list and return the value contained in the list in result. + // Returns true if an object is found, false otherwise. + public static bool FindFirst(this IEnumerable array, T needle, [NotNullWhen(true)] out T? result) where T : notnull + { + foreach (var obj in array) + { + if (obj.Equals(needle)) + { + result = obj; + return true; + } + } + + result = default; + return false; + } +} diff --git a/Dalamud.Interface/Dalamud.Interface.csproj b/Dalamud.Interface/Dalamud.Interface.csproj new file mode 100644 index 000000000..1dd8468be --- /dev/null +++ b/Dalamud.Interface/Dalamud.Interface.csproj @@ -0,0 +1,17 @@ + + + + net7.0-windows + x64 + x64;AnyCPU + enable + enable + true + Dalamud.Interface + + + + + + + diff --git a/Dalamud.Interface/ImGuiClip.cs b/Dalamud.Interface/ImGuiClip.cs new file mode 100644 index 000000000..dc1845a35 --- /dev/null +++ b/Dalamud.Interface/ImGuiClip.cs @@ -0,0 +1,144 @@ +using System.Numerics; +using Dalamud.Interface.Raii; +using ImGuiNET; + +namespace Dalamud.Interface; + +public static class ImGuiClip +{ + // Get the number of skipped items of a given height necessary for the current scroll bar, + // and apply the dummy of the appropriate height, removing one item spacing. + // The height has to contain the spacing. + public static int GetNecessarySkips(float height) + { + var curY = ImGui.GetScrollY(); + var skips = (int)(curY / height); + if (skips > 0) + ImGui.Dummy(new Vector2(1, skips * height - ImGui.GetStyle().ItemSpacing.Y)); + + return skips; + } + + // Draw the dummy for the remaining items computed by ClippedDraw, + // removing one item spacing. + public static void DrawEndDummy(int remainder, float height) + { + if (remainder > 0) + ImGui.Dummy(new Vector2(1, remainder * height - ImGui.GetStyle().ItemSpacing.Y)); + } + + // Draw a clipped random-access collection of consistent height lineHeight. + // Uses ImGuiListClipper and thus handles start- and end-dummies itself. + public static void ClippedDraw(IReadOnlyList data, Action draw, float lineHeight) + { + ImGuiListClipperPtr clipper; + unsafe + { + clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper()); + } + + clipper.Begin(data.Count, lineHeight); + while (clipper.Step()) + { + for (var actualRow = clipper.DisplayStart; actualRow < clipper.DisplayEnd; actualRow++) + { + if (actualRow >= data.Count) + return; + + if (actualRow < 0) + continue; + + draw(data[actualRow]); + } + } + + clipper.End(); + clipper.Destroy(); + } + + // Draw a clipped random-access collection of consistent height lineHeight. + // Uses ImGuiListClipper and thus handles start- and end-dummies itself, but acts on type and index. + public static void ClippedDraw(IReadOnlyList data, Action draw, float lineHeight) + { + ImGuiListClipperPtr clipper; + unsafe + { + clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper()); + } + + clipper.Begin(data.Count, lineHeight); + while (clipper.Step()) + { + for (var actualRow = clipper.DisplayStart; actualRow < clipper.DisplayEnd; actualRow++) + { + if (actualRow >= data.Count) + return; + + if (actualRow < 0) + continue; + + draw(data[actualRow], actualRow); + } + } + + clipper.End(); + clipper.Destroy(); + } + + // Draw non-random-access data without storing state. + // Use GetNecessarySkips first and use its return value for skips. + // startIndex can be set if using multiple separate chunks of data with different filter or draw functions (of the same height). + // Returns either the non-negative remaining objects in data that could not be drawn due to being out of the visible area, + // if count was given this will be subtracted instead of counted, + // or the bitwise-inverse of the next startIndex for subsequent collections, if there is still room for more visible objects. + public static int ClippedDraw(IEnumerable data, int skips, Action draw, int? count = null, int startIndex = 0) + { + if (count != null && count.Value + startIndex <= skips) + return ~(count.Value + startIndex); + + using var it = data.GetEnumerator(); + var visible = false; + var idx = startIndex; + while (it.MoveNext()) + { + if (idx >= skips) + { + using var group = ImRaii.Group(); + draw(it.Current); + group.Dispose(); + if (!ImGui.IsItemVisible()) + { + if (visible) + { + if (count != null) + return Math.Max(0, count.Value - idx + startIndex - 1); + + var remainder = 0; + while (it.MoveNext()) + ++remainder; + + return remainder; + } + } + else + { + visible = true; + } + } + + ++idx; + } + + return ~idx; + } + + + // Draw non-random-access data that gets filtered without storing state. + // Use GetNecessarySkips first and use its return value for skips. + // checkFilter should return true for items that should be displayed and false for those that should be skipped. + // startIndex can be set if using multiple separate chunks of data with different filter or draw functions (of the same height). + // Returns either the non-negative remaining objects in data that could not be drawn due to being out of the visible area, + // or the bitwise-inverse of the next startIndex for subsequent collections, if there is still room for more visible objects. + public static int FilteredClippedDraw(IEnumerable data, int skips, Func checkFilter, Action draw, int startIndex = 0) + => ClippedDraw(data.Where(checkFilter), skips, draw, null, startIndex); +} diff --git a/Dalamud.Interface/ImGuiTable.cs b/Dalamud.Interface/ImGuiTable.cs new file mode 100644 index 000000000..5ea6a2c9a --- /dev/null +++ b/Dalamud.Interface/ImGuiTable.cs @@ -0,0 +1,41 @@ +using Dalamud.Interface.Raii; +using ImGuiNET; + +namespace Dalamud.Interface; + +public static class ImGuiTable +{ + // Draw a simple table with the given data using the drawRow action. + // Headers and thus columns and column count are defined by columnTitles. + public static void DrawTable(string label, IEnumerable data, Action drawRow, ImGuiTableFlags flags = ImGuiTableFlags.None, + params string[] columnTitles) + { + if (columnTitles.Length == 0) + return; + + using var table = ImRaii.Table(label, columnTitles.Length, flags); + if (!table) + return; + + foreach (var title in columnTitles) + { + ImGui.TableNextColumn(); + ImGui.TableHeader(title); + } + + foreach (var datum in data) + { + ImGui.TableNextRow(); + drawRow(datum); + } + } + + // Draw a simple table with the given data using the drawRow action inside a collapsing header. + // Headers and thus columns and column count are defined by columnTitles. + public static void DrawTabbedTable(string label, IEnumerable data, Action drawRow, ImGuiTableFlags flags = ImGuiTableFlags.None, + params string[] columnTitles) + { + if (ImGui.CollapsingHeader(label)) + DrawTable($"{label}##Table", data, drawRow, flags, columnTitles); + } +} diff --git a/Dalamud.Interface/InterfaceHelpers.cs b/Dalamud.Interface/InterfaceHelpers.cs new file mode 100644 index 000000000..26f09bedb --- /dev/null +++ b/Dalamud.Interface/InterfaceHelpers.cs @@ -0,0 +1,6 @@ +namespace Dalamud.Interface; + +public static class InterfaceHelpers +{ + public static float GlobalScale = 1.0f; +} diff --git a/Dalamud.Interface/Raii/Color.cs b/Dalamud.Interface/Raii/Color.cs new file mode 100644 index 000000000..388e6e737 --- /dev/null +++ b/Dalamud.Interface/Raii/Color.cs @@ -0,0 +1,66 @@ +using System.Numerics; +using ImGuiNET; + +namespace Dalamud.Interface.Raii; + +// Push an arbitrary amount of colors into an object that are all popped when it is disposed. +// If condition is false, no color is pushed. +public static partial class ImRaii +{ + public static Color PushColor(ImGuiCol idx, uint color, bool condition = true) + => new Color().Push(idx, color, condition); + + public static Color PushColor(ImGuiCol idx, Vector4 color, bool condition = true) + => new Color().Push(idx, color, condition); + + // Push colors that revert all current color changes made temporarily. + public static Color DefaultColors() + { + var ret = new Color(); + var reverseStack = Color.Stack.GroupBy(p => p.Item1).Select(p => (p.Key, p.First().Item2)).ToArray(); + foreach (var (idx, val) in reverseStack) + ret.Push(idx, val); + return ret; + } + + public sealed class Color : IDisposable + { + internal static readonly List<(ImGuiCol, uint)> Stack = new(); + private int _count; + + public Color Push(ImGuiCol idx, uint color, bool condition = true) + { + if (condition) + { + Stack.Add((idx, ImGui.GetColorU32(idx))); + ImGui.PushStyleColor(idx, color); + ++this._count; + } + + return this; + } + + public Color Push(ImGuiCol idx, Vector4 color, bool condition = true) + { + if (condition) + { + Stack.Add((idx, ImGui.GetColorU32(idx))); + ImGui.PushStyleColor(idx, color); + ++this._count; + } + + return this; + } + + public void Pop(int num = 1) + { + num = Math.Min(num, this._count); + this._count -= num; + ImGui.PopStyleColor(num); + Stack.RemoveRange(Stack.Count - num, num); + } + + public void Dispose() + => this.Pop(this._count); + } +} diff --git a/Dalamud.Interface/Raii/EndObjects.cs b/Dalamud.Interface/Raii/EndObjects.cs new file mode 100644 index 000000000..1edc9f518 --- /dev/null +++ b/Dalamud.Interface/Raii/EndObjects.cs @@ -0,0 +1,239 @@ +using System.Numerics; +using ImGuiNET; + +namespace Dalamud.Interface.Raii; + +// Most ImGui widgets with IDisposable interface that automatically destroys them +// when created with using variables. +public static partial class ImRaii +{ + private static int _disabledCount = 0; + + public static IEndObject Child(string strId) + => new EndUnconditionally(ImGui.EndChild, ImGui.BeginChild(strId)); + + public static IEndObject Child(string strId, Vector2 size) + => new EndUnconditionally(ImGui.EndChild, ImGui.BeginChild(strId, size)); + + public static IEndObject Child(string strId, Vector2 size, bool border) + => new EndUnconditionally(ImGui.EndChild, ImGui.BeginChild(strId, size, border)); + + public static IEndObject Child(string strId, Vector2 size, bool border, ImGuiWindowFlags flags) + => new EndUnconditionally(ImGui.EndChild, ImGui.BeginChild(strId, size, border, flags)); + + public static IEndObject DragDropTarget() + => new EndConditionally(ImGui.EndDragDropTarget, ImGui.BeginDragDropTarget()); + + public static IEndObject DragDropSource() + => new EndConditionally(ImGui.EndDragDropSource, ImGui.BeginDragDropSource()); + + public static IEndObject DragDropSource(ImGuiDragDropFlags flags) + => new EndConditionally(ImGui.EndDragDropSource, ImGui.BeginDragDropSource(flags)); + + public static IEndObject Popup(string id) + => new EndConditionally(ImGui.EndPopup, ImGui.BeginPopup(id)); + + public static IEndObject Popup(string id, ImGuiWindowFlags flags) + => new EndConditionally(ImGui.EndPopup, ImGui.BeginPopup(id, flags)); + + public static IEndObject ContextPopup(string id) + => new EndConditionally(ImGui.EndPopup, ImGui.BeginPopupContextWindow(id)); + + public static IEndObject ContextPopup(string id, ImGuiPopupFlags flags) + => new EndConditionally(ImGui.EndPopup, ImGui.BeginPopupContextWindow(id, flags)); + + public static IEndObject Combo(string label, string previewValue) + => new EndConditionally(ImGui.EndCombo, ImGui.BeginCombo(label, previewValue)); + + public static IEndObject Combo(string label, string previewValue, ImGuiComboFlags flags) + => new EndConditionally(ImGui.EndCombo, ImGui.BeginCombo(label, previewValue, flags)); + + public static IEndObject Group() + { + ImGui.BeginGroup(); + return new EndUnconditionally(ImGui.EndGroup, true); + } + + public static IEndObject Tooltip() + { + ImGui.BeginTooltip(); + return new EndUnconditionally(ImGui.EndTooltip, true); + } + + public static IEndObject ListBox(string label) + => new EndConditionally(ImGui.EndListBox, ImGui.BeginListBox(label)); + + public static IEndObject ListBox(string label, Vector2 size) + => new EndConditionally(ImGui.EndListBox, ImGui.BeginListBox(label, size)); + + public static IEndObject Table(string table, int numColumns) + => new EndConditionally(ImGui.EndTable, ImGui.BeginTable(table, numColumns)); + + public static IEndObject Table(string table, int numColumns, ImGuiTableFlags flags) + => new EndConditionally(ImGui.EndTable, ImGui.BeginTable(table, numColumns, flags)); + + public static IEndObject Table(string table, int numColumns, ImGuiTableFlags flags, Vector2 outerSize) + => new EndConditionally(ImGui.EndTable, ImGui.BeginTable(table, numColumns, flags, outerSize)); + + public static IEndObject Table(string table, int numColumns, ImGuiTableFlags flags, Vector2 outerSize, float innerWidth) + => new EndConditionally(ImGui.EndTable, ImGui.BeginTable(table, numColumns, flags, outerSize, innerWidth)); + + public static IEndObject TabBar(string label) + => new EndConditionally(ImGui.EndTabBar, ImGui.BeginTabBar(label)); + + public static IEndObject TabBar(string label, ImGuiTabBarFlags flags) + => new EndConditionally(ImGui.EndTabBar, ImGui.BeginTabBar(label, flags)); + + public static IEndObject TabItem(string label) + => new EndConditionally(ImGui.EndTabItem, ImGui.BeginTabItem(label)); + + public static unsafe IEndObject TabItem(byte* label, ImGuiTabItemFlags flags) + => new EndConditionally(ImGuiNative.igEndTabItem, ImGuiNative.igBeginTabItem(label, null, flags) != 0); + + public static IEndObject TabItem(string label, ref bool open) + => new EndConditionally(ImGui.EndTabItem, ImGui.BeginTabItem(label, ref open)); + + public static IEndObject TabItem(string label, ref bool open, ImGuiTabItemFlags flags) + => new EndConditionally(ImGui.EndTabItem, ImGui.BeginTabItem(label, ref open, flags)); + + public static IEndObject TreeNode(string label) + => new EndConditionally(ImGui.TreePop, ImGui.TreeNodeEx(label)); + + public static IEndObject TreeNode(string label, ImGuiTreeNodeFlags flags) + => new EndConditionally(flags.HasFlag(ImGuiTreeNodeFlags.NoTreePushOnOpen) ? Nop : ImGui.TreePop, ImGui.TreeNodeEx(label, flags)); + + public static IEndObject Disabled() + { + ImGui.BeginDisabled(); + ++_disabledCount; + return DisabledEnd(); + } + + public static IEndObject Disabled(bool disabled) + { + if (!disabled) + return new EndConditionally(Nop, false); + + ImGui.BeginDisabled(); + ++_disabledCount; + return DisabledEnd(); + } + + public static IEndObject Enabled() + { + var oldCount = _disabledCount; + if (oldCount == 0) + return new EndConditionally(Nop, false); + + void Restore() + { + _disabledCount += oldCount; + while (--oldCount >= 0) + ImGui.BeginDisabled(); + } + + for (; _disabledCount > 0; --_disabledCount) + ImGui.EndDisabled(); + + return new EndUnconditionally(Restore, true); + } + + private static IEndObject DisabledEnd() + => new EndUnconditionally(() => + { + --_disabledCount; + ImGui.EndDisabled(); + }, true); + + /* Only in OtterGui for now + public static IEndObject FramedGroup(string label) + { + Widget.BeginFramedGroup(label, Vector2.Zero); + return new EndUnconditionally(Widget.EndFramedGroup, true); + } + + public static IEndObject FramedGroup(string label, Vector2 minSize, string description = "") + { + Widget.BeginFramedGroup(label, minSize, description); + return new EndUnconditionally(Widget.EndFramedGroup, true); + } + */ + + // Exported interface for RAII. + public interface IEndObject : IDisposable + { + public bool Success { get; } + + public static bool operator true(IEndObject i) + => i.Success; + + public static bool operator false(IEndObject i) + => !i.Success; + + public static bool operator !(IEndObject i) + => !i.Success; + + public static bool operator &(IEndObject i, bool value) + => i.Success && value; + + public static bool operator |(IEndObject i, bool value) + => i.Success || value; + + // Empty end object. + public static readonly IEndObject Empty = new EndConditionally(Nop, false); + } + + // Use end-function regardless of success. + // Used by Child, Group and Tooltip. + private struct EndUnconditionally : IEndObject + { + private Action EndAction { get; } + public bool Success { get; } + public bool Disposed { get; private set; } + + public EndUnconditionally(Action endAction, bool success) + { + this.EndAction = endAction; + this.Success = success; + this.Disposed = false; + } + + public void Dispose() + { + if (this.Disposed) + return; + + this.EndAction(); + this.Disposed = true; + } + } + + // Use end-function only on success. + private struct EndConditionally : IEndObject + { + private Action EndAction { get; } + public bool Success { get; } + public bool Disposed { get; private set; } + + public EndConditionally(Action endAction, bool success) + { + this.EndAction = endAction; + this.Success = success; + this.Disposed = false; + } + + public void Dispose() + { + if (this.Disposed) + return; + + if (this.Success) + this.EndAction(); + this.Disposed = true; + } + } + + // Used to avoid tree pops when flag for no push is set. + private static void Nop() + { } +} diff --git a/Dalamud.Interface/Raii/Font.cs b/Dalamud.Interface/Raii/Font.cs new file mode 100644 index 000000000..cdecf457c --- /dev/null +++ b/Dalamud.Interface/Raii/Font.cs @@ -0,0 +1,51 @@ +using ImGuiNET; + +namespace Dalamud.Interface.Raii; + +// Push an arbitrary amount of fonts into an object that are all popped when it is disposed. +// If condition is false, no font is pushed. +public static partial class ImRaii +{ + public static Font PushFont(ImFontPtr font, bool condition = true) + => condition ? new Font().Push(font) : new Font(); + + // Push the default font if any other font is currently pushed. + public static Font DefaultFont() + => new Font().Push(Font.DefaultPushed, Font.FontPushCounter > 0); + + public sealed class Font : IDisposable + { + internal static int FontPushCounter = 0; + internal static ImFontPtr DefaultPushed; + + private int _count; + + public Font() + => this._count = 0; + + public Font Push(ImFontPtr font, bool condition = true) + { + if (condition) + { + if (FontPushCounter++ == 0) + DefaultPushed = ImGui.GetFont(); + ImGui.PushFont(font); + ++this._count; + } + + return this; + } + + public void Pop(int num = 1) + { + num = Math.Min(num, this._count); + this._count -= num; + FontPushCounter -= num; + while (num-- > 0) + ImGui.PopFont(); + } + + public void Dispose() + => this.Pop(this._count); + } +} diff --git a/Dalamud.Interface/Raii/Id.cs b/Dalamud.Interface/Raii/Id.cs new file mode 100644 index 000000000..1248b92f3 --- /dev/null +++ b/Dalamud.Interface/Raii/Id.cs @@ -0,0 +1,66 @@ +using ImGuiNET; + +namespace Dalamud.Interface.Raii; + +// Push an arbitrary amount of ids into an object that are all popped when it is disposed. +// If condition is false, no id is pushed. +public static partial class ImRaii +{ + public static Id PushId(string id, bool enabled = true) + => enabled ? new Id().Push(id) : new Id(); + + public static Id PushId(int id, bool enabled = true) + => enabled ? new Id().Push(id) : new Id(); + + public static Id PushId(IntPtr id, bool enabled = true) + => enabled ? new Id().Push(id) : new Id(); + + public sealed class Id : IDisposable + { + private int _count; + + public Id Push(string id, bool condition = true) + { + if (condition) + { + ImGui.PushID(id); + ++this._count; + } + + return this; + } + + public Id Push(int id, bool condition = true) + { + if (condition) + { + ImGui.PushID(id); + ++this._count; + } + + return this; + } + + public Id Push(IntPtr id, bool condition = true) + { + if (condition) + { + ImGui.PushID(id); + ++this._count; + } + + return this; + } + + public void Pop(int num = 1) + { + num = Math.Min(num, this._count); + this._count -= num; + while (num-- > 0) + ImGui.PopID(); + } + + public void Dispose() + => this.Pop(this._count); + } +} diff --git a/Dalamud.Interface/Raii/Indent.cs b/Dalamud.Interface/Raii/Indent.cs new file mode 100644 index 000000000..99eab8783 --- /dev/null +++ b/Dalamud.Interface/Raii/Indent.cs @@ -0,0 +1,70 @@ +using ImGuiNET; + +namespace Dalamud.Interface.Raii; + +public static partial class ImRaii +{ + public static Indent PushIndent(float f, bool scaled = true, bool condition = true) + => new Indent().Push(f, scaled, condition); + + public static Indent PushIndent(int i = 1, bool condition = true) + => new Indent().Push(i, condition); + + public sealed class Indent : IDisposable + { + public float Indentation { get; private set; } + + public Indent Push(float indent, bool scaled = true, bool condition = true) + { + if (condition) + { + if (scaled) + indent *= InterfaceHelpers.GlobalScale; + + IndentInternal(indent); + this.Indentation += indent; + } + + return this; + } + + public Indent Push(int i = 1, bool condition = true) + { + if (condition) + { + var spacing = i * ImGui.GetStyle().IndentSpacing; + IndentInternal(spacing); + this.Indentation += spacing; + } + + return this; + } + + public void Pop(float indent, bool scaled = true) + { + if (scaled) + indent *= InterfaceHelpers.GlobalScale; + + IndentInternal(-indent); + this.Indentation -= indent; + } + + public void Pop(int i) + { + var spacing = i * ImGui.GetStyle().IndentSpacing; + IndentInternal(-spacing); + this.Indentation -= spacing; + } + + private static void IndentInternal(float indent) + { + if (indent < 0) + ImGui.Unindent(-indent); + else if (indent > 0) + ImGui.Indent(indent); + } + + public void Dispose() + => this.Pop(this.Indentation, false); + } +} diff --git a/Dalamud.Interface/Raii/Style.cs b/Dalamud.Interface/Raii/Style.cs new file mode 100644 index 000000000..2f1fea538 --- /dev/null +++ b/Dalamud.Interface/Raii/Style.cs @@ -0,0 +1,147 @@ +using System.Numerics; +using ImGuiNET; + +namespace Dalamud.Interface.Raii; + +// Push an arbitrary amount of styles into an object that are all popped when it is disposed. +// If condition is false, no style is pushed. +// In debug mode, checks that the type of the value given for the style is valid. +public static partial class ImRaii +{ + public static Style PushStyle(ImGuiStyleVar idx, float value, bool condition = true) + => new Style().Push(idx, value, condition); + + public static Style PushStyle(ImGuiStyleVar idx, Vector2 value, bool condition = true) + => new Style().Push(idx, value, condition); + + // Push styles that revert all current style changes made temporarily. + public static Style DefaultStyle() + { + var ret = new Style(); + var reverseStack = Style.Stack.GroupBy(p => p.Item1).Select(p => (p.Key, p.First().Item2)).ToArray(); + foreach (var (idx, val) in reverseStack) + { + if (float.IsNaN(val.Y)) + ret.Push(idx, val.X); + else + ret.Push(idx, val); + } + + return ret; + } + + public sealed class Style : IDisposable + { + internal static readonly List<(ImGuiStyleVar, Vector2)> Stack = new(); + + private int _count; + + [System.Diagnostics.Conditional("DEBUG")] + private static void CheckStyleIdx(ImGuiStyleVar idx, Type type) + { + var shouldThrow = idx switch + { + ImGuiStyleVar.Alpha => type != typeof(float), + ImGuiStyleVar.WindowPadding => type != typeof(Vector2), + ImGuiStyleVar.WindowRounding => type != typeof(float), + ImGuiStyleVar.WindowBorderSize => type != typeof(float), + ImGuiStyleVar.WindowMinSize => type != typeof(Vector2), + ImGuiStyleVar.WindowTitleAlign => type != typeof(Vector2), + ImGuiStyleVar.ChildRounding => type != typeof(float), + ImGuiStyleVar.ChildBorderSize => type != typeof(float), + ImGuiStyleVar.PopupRounding => type != typeof(float), + ImGuiStyleVar.PopupBorderSize => type != typeof(float), + ImGuiStyleVar.FramePadding => type != typeof(Vector2), + ImGuiStyleVar.FrameRounding => type != typeof(float), + ImGuiStyleVar.FrameBorderSize => type != typeof(float), + ImGuiStyleVar.ItemSpacing => type != typeof(Vector2), + ImGuiStyleVar.ItemInnerSpacing => type != typeof(Vector2), + ImGuiStyleVar.IndentSpacing => type != typeof(float), + ImGuiStyleVar.CellPadding => type != typeof(Vector2), + ImGuiStyleVar.ScrollbarSize => type != typeof(float), + ImGuiStyleVar.ScrollbarRounding => type != typeof(float), + ImGuiStyleVar.GrabMinSize => type != typeof(float), + ImGuiStyleVar.GrabRounding => type != typeof(float), + ImGuiStyleVar.TabRounding => type != typeof(float), + ImGuiStyleVar.ButtonTextAlign => type != typeof(Vector2), + ImGuiStyleVar.SelectableTextAlign => type != typeof(Vector2), + ImGuiStyleVar.DisabledAlpha => type != typeof(float), + _ => throw new ArgumentOutOfRangeException(nameof(idx), idx, null), + }; + + if (shouldThrow) + throw new ArgumentException($"Unable to push {type} to {idx}."); + } + + public static Vector2 GetStyle(ImGuiStyleVar idx) + { + var style = ImGui.GetStyle(); + return idx switch + { + ImGuiStyleVar.Alpha => new Vector2(style.Alpha, float.NaN), + ImGuiStyleVar.WindowPadding => style.WindowPadding, + ImGuiStyleVar.WindowRounding => new Vector2(style.WindowRounding, float.NaN), + ImGuiStyleVar.WindowBorderSize => new Vector2(style.WindowBorderSize, float.NaN), + ImGuiStyleVar.WindowMinSize => style.WindowMinSize, + ImGuiStyleVar.WindowTitleAlign => style.WindowTitleAlign, + ImGuiStyleVar.ChildRounding => new Vector2(style.ChildRounding, float.NaN), + ImGuiStyleVar.ChildBorderSize => new Vector2(style.ChildBorderSize, float.NaN), + ImGuiStyleVar.PopupRounding => new Vector2(style.PopupRounding, float.NaN), + ImGuiStyleVar.PopupBorderSize => new Vector2(style.PopupBorderSize, float.NaN), + ImGuiStyleVar.FramePadding => style.FramePadding, + ImGuiStyleVar.FrameRounding => new Vector2(style.FrameRounding, float.NaN), + ImGuiStyleVar.FrameBorderSize => new Vector2(style.FrameBorderSize, float.NaN), + ImGuiStyleVar.ItemSpacing => style.ItemSpacing, + ImGuiStyleVar.ItemInnerSpacing => style.ItemInnerSpacing, + ImGuiStyleVar.IndentSpacing => new Vector2(style.IndentSpacing, float.NaN), + ImGuiStyleVar.CellPadding => style.CellPadding, + ImGuiStyleVar.ScrollbarSize => new Vector2(style.ScrollbarSize, float.NaN), + ImGuiStyleVar.ScrollbarRounding => new Vector2(style.ScrollbarRounding, float.NaN), + ImGuiStyleVar.GrabMinSize => new Vector2(style.GrabMinSize, float.NaN), + ImGuiStyleVar.GrabRounding => new Vector2(style.GrabRounding, float.NaN), + ImGuiStyleVar.TabRounding => new Vector2(style.TabRounding, float.NaN), + ImGuiStyleVar.ButtonTextAlign => style.ButtonTextAlign, + ImGuiStyleVar.SelectableTextAlign => style.SelectableTextAlign, + ImGuiStyleVar.DisabledAlpha => new Vector2(style.DisabledAlpha, float.NaN), + _ => throw new ArgumentOutOfRangeException(nameof(idx), idx, null), + }; + } + + public Style Push(ImGuiStyleVar idx, float value, bool condition = true) + { + if (!condition) + return this; + + CheckStyleIdx(idx, typeof(float)); + Stack.Add((idx, GetStyle(idx))); + ImGui.PushStyleVar(idx, value); + ++this._count; + + return this; + } + + public Style Push(ImGuiStyleVar idx, Vector2 value, bool condition = true) + { + if (!condition) + return this; + + CheckStyleIdx(idx, typeof(Vector2)); + Stack.Add((idx, GetStyle(idx))); + ImGui.PushStyleVar(idx, value); + ++this._count; + + return this; + } + + public void Pop(int num = 1) + { + num = Math.Min(num, this._count); + this._count -= num; + ImGui.PopStyleVar(num); + Stack.RemoveRange(Stack.Count - num, num); + } + + public void Dispose() + => this.Pop(this._count); + } +} diff --git a/Dalamud.Interface/StableInsertionSortExtension.cs b/Dalamud.Interface/StableInsertionSortExtension.cs new file mode 100644 index 000000000..d2884f838 --- /dev/null +++ b/Dalamud.Interface/StableInsertionSortExtension.cs @@ -0,0 +1,29 @@ +using System.Runtime.CompilerServices; + +namespace Dalamud.Interface; + +internal static class StableInsertionSortExtension +{ + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static void StableSort(this IList list, Func selector) + { + var tmpList = new List(list.Count); + tmpList.AddRange(list.OrderBy(selector)); + for (var i = 0; i < tmpList.Count; ++i) + list[i] = tmpList[i]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static void StableSort(this IList list, Comparison comparer) + { + var tmpList = new List<(T, int)>(list.Count); + tmpList.AddRange(list.WithIndex()); + tmpList.Sort((a, b) => + { + var ret = comparer(a.Item1, b.Item1); + return ret != 0 ? ret : a.Item2.CompareTo(b.Item2); + }); + for (var i = 0; i < tmpList.Count; ++i) + list[i] = tmpList[i].Item1; + } +} diff --git a/Dalamud.Interface/Table/Column.cs b/Dalamud.Interface/Table/Column.cs new file mode 100644 index 000000000..7460ec189 --- /dev/null +++ b/Dalamud.Interface/Table/Column.cs @@ -0,0 +1,34 @@ +using ImGuiNET; + +namespace Dalamud.Interface.Table; + +public class Column +{ + public string Label = string.Empty; + public ImGuiTableColumnFlags Flags = ImGuiTableColumnFlags.NoResize; + + public virtual float Width + => -1f; + + public string FilterLabel + => $"##{this.Label}Filter"; + + public virtual bool DrawFilter() + { + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(this.Label); + return false; + } + + public virtual bool FilterFunc(TItem item) + => true; + + public virtual int Compare(TItem lhs, TItem rhs) + => 0; + + public virtual void DrawColumn(TItem item, int idx) + { } + + public int CompareInv(TItem lhs, TItem rhs) + => this.Compare(rhs, lhs); +} diff --git a/Dalamud.Interface/Table/ColumnFlags.cs b/Dalamud.Interface/Table/ColumnFlags.cs new file mode 100644 index 000000000..815ddcf76 --- /dev/null +++ b/Dalamud.Interface/Table/ColumnFlags.cs @@ -0,0 +1,65 @@ +using ImGuiNET; +using ImRaii = Dalamud.Interface.Raii.ImRaii; + +namespace Dalamud.Interface.Table; + +public class ColumnFlags : Column where T : struct, Enum +{ + public T AllFlags = default; + + protected virtual IReadOnlyList Values + => Enum.GetValues(); + + protected virtual string[] Names + => Enum.GetNames(); + + public virtual T FilterValue + => default; + + protected virtual void SetValue(T value, bool enable) + { } + + public override bool DrawFilter() + { + using var id = ImRaii.PushId(this.FilterLabel); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0); + ImGui.SetNextItemWidth(-Table.ArrowWidth * InterfaceHelpers.GlobalScale); + var all = this.FilterValue.HasFlag(this.AllFlags); + using var color = ImRaii.PushColor(ImGuiCol.FrameBg, 0x803030A0, !all); + using var combo = ImRaii.Combo(string.Empty, this.Label, ImGuiComboFlags.NoArrowButton); + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + this.SetValue(this.AllFlags, true); + return true; + } + + if (!all && ImGui.IsItemHovered()) + ImGui.SetTooltip("Right-click to clear filters."); + + if (!combo) + return false; + + color.Pop(); + + var ret = false; + if (ImGui.Checkbox("Enable All", ref all)) + { + this.SetValue(this.AllFlags, all); + ret = true; + } + + using var indent = ImRaii.PushIndent(10f); + for (var i = 0; i < this.Names.Length; ++i) + { + var tmp = this.FilterValue.HasFlag(this.Values[i]); + if (!ImGui.Checkbox(this.Names[i], ref tmp)) + continue; + + this.SetValue(this.Values[i], tmp); + ret = true; + } + + return ret; + } +} diff --git a/Dalamud.Interface/Table/ColumnSelect.cs b/Dalamud.Interface/Table/ColumnSelect.cs new file mode 100644 index 000000000..5ef276b06 --- /dev/null +++ b/Dalamud.Interface/Table/ColumnSelect.cs @@ -0,0 +1,47 @@ +using ImGuiNET; +using ImRaii = Dalamud.Interface.Raii.ImRaii; + +namespace Dalamud.Interface.Table; + +public class ColumnSelect : Column where T : struct, Enum, IEquatable +{ + public ColumnSelect(T initialValue) + => this.FilterValue = initialValue; + + protected virtual IReadOnlyList Values + => Enum.GetValues(); + + protected virtual string[] Names + => Enum.GetNames(); + + protected virtual void SetValue(T value) + => this.FilterValue = value; + + public T FilterValue; + protected int Idx = -1; + + public override bool DrawFilter() + { + using var id = ImRaii.PushId(this.FilterLabel); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0); + ImGui.SetNextItemWidth(-Table.ArrowWidth * InterfaceHelpers.GlobalScale); + using var combo = ImRaii.Combo(string.Empty, this.Idx < 0 ? this.Label : this.Names[this.Idx]); + if(!combo) + return false; + + var ret = false; + for (var i = 0; i < this.Names.Length; ++i) + { + if (this.FilterValue.Equals(this.Values[i])) + this.Idx = i; + if (!ImGui.Selectable(this.Names[i], this.Idx == i) || this.Idx == i) + continue; + + this.Idx = i; + this.SetValue(this.Values[i]); + ret = true; + } + + return ret; + } +} diff --git a/Dalamud.Interface/Table/ColumnString.cs b/Dalamud.Interface/Table/ColumnString.cs new file mode 100644 index 000000000..dcd43b23c --- /dev/null +++ b/Dalamud.Interface/Table/ColumnString.cs @@ -0,0 +1,56 @@ +using System.Text.RegularExpressions; +using Dalamud.Interface.Raii; +using ImGuiNET; + +namespace Dalamud.Interface.Table; + +public class ColumnString : Column +{ + public ColumnString() + => this.Flags &= ~ImGuiTableColumnFlags.NoResize; + + public string FilterValue = string.Empty; + protected Regex? FilterRegex; + + public virtual string ToName(TItem item) + => item!.ToString() ?? string.Empty; + + public override int Compare(TItem lhs, TItem rhs) + => string.Compare(this.ToName(lhs), this.ToName(rhs), StringComparison.InvariantCulture); + + public override bool DrawFilter() + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0); + + ImGui.SetNextItemWidth(-Table.ArrowWidth * InterfaceHelpers.GlobalScale); + var tmp = this.FilterValue; + if (!ImGui.InputTextWithHint(this.FilterLabel, this.Label, ref tmp, 256) || tmp == this.FilterValue) + return false; + + this.FilterValue = tmp; + try + { + this.FilterRegex = new Regex(this.FilterValue, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + } + catch + { + this.FilterRegex = null; + } + + return true; + } + + public override bool FilterFunc(TItem item) + { + var name = this.ToName(item); + if (this.FilterValue.Length == 0) + return true; + + return this.FilterRegex?.IsMatch(name) ?? name.Contains(this.FilterValue, StringComparison.OrdinalIgnoreCase); + } + + public override void DrawColumn(TItem item, int _) + { + ImGui.TextUnformatted(this.ToName(item)); + } +} diff --git a/Dalamud.Interface/Table/Table.cs b/Dalamud.Interface/Table/Table.cs new file mode 100644 index 000000000..74fb0bc5c --- /dev/null +++ b/Dalamud.Interface/Table/Table.cs @@ -0,0 +1,172 @@ +using System.Numerics; +using ImGuiNET; +using ImRaii = Dalamud.Interface.Raii.ImRaii; + +namespace Dalamud.Interface.Table; + +public static class Table +{ + public const float ArrowWidth = 10; +} + +public class Table +{ + protected bool FilterDirty = true; + protected bool SortDirty = true; + protected readonly ICollection Items; + internal readonly List<(T, int)> FilteredItems; + + protected readonly string Label; + protected readonly Column[] Headers; + + protected float ItemHeight { get; set; } + public float ExtraHeight { get; set; } = 0; + + private int _currentIdx = 0; + + protected bool Sortable + { + get => this.Flags.HasFlag(ImGuiTableFlags.Sortable); + set => this.Flags = value ? this.Flags | ImGuiTableFlags.Sortable : this.Flags & ~ImGuiTableFlags.Sortable; + } + + protected int SortIdx = -1; + + public ImGuiTableFlags Flags = ImGuiTableFlags.RowBg + | ImGuiTableFlags.Sortable + | ImGuiTableFlags.BordersOuter + | ImGuiTableFlags.ScrollY + | ImGuiTableFlags.ScrollX + | ImGuiTableFlags.PreciseWidths + | ImGuiTableFlags.BordersInnerV + | ImGuiTableFlags.NoBordersInBodyUntilResize; + + public int TotalItems + => this.Items.Count; + + public int CurrentItems + => this.FilteredItems.Count; + + public int TotalColumns + => this.Headers.Length; + + public int VisibleColumns { get; private set; } + + public Table(string label, ICollection items, params Column[] headers) + { + this.Label = label; + this.Items = items; + this.Headers = headers; + this.FilteredItems = new List<(T, int)>(this.Items.Count); + this.VisibleColumns = this.Headers.Length; + } + + public void Draw(float itemHeight) + { + this.ItemHeight = itemHeight; + using var id = ImRaii.PushId(this.Label); + this.UpdateFilter(); + this.DrawTableInternal(); + } + + protected virtual void DrawFilters() + => throw new NotImplementedException(); + + protected virtual void PreDraw() + { } + + private void SortInternal() + { + if (!this.Sortable) + return; + + var sortSpecs = ImGui.TableGetSortSpecs(); + this.SortDirty |= sortSpecs.SpecsDirty; + + if (!this.SortDirty) + return; + + this.SortIdx = sortSpecs.Specs.ColumnIndex; + + if (this.Headers.Length <= this.SortIdx) + this.SortIdx = 0; + + if (sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) + this.FilteredItems.StableSort((a, b) => this.Headers[this.SortIdx].Compare(a.Item1, b.Item1)); + else if (sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) + this.FilteredItems.StableSort((a, b) => this.Headers[this.SortIdx].CompareInv(a.Item1, b.Item1)); + else + this.SortIdx = -1; + this.SortDirty = false; + sortSpecs.SpecsDirty = false; + } + + private void UpdateFilter() + { + if (!this.FilterDirty) + return; + + this.FilteredItems.Clear(); + var idx = 0; + foreach (var item in this.Items) + { + if (this.Headers.All(header => header.FilterFunc(item))) + this.FilteredItems.Add((item, idx)); + idx++; + } + + this.FilterDirty = false; + this.SortDirty = true; + } + + private void DrawItem((T, int) pair) + { + var column = 0; + using var id = ImRaii.PushId(this._currentIdx); + this._currentIdx = pair.Item2; + foreach (var header in this.Headers) + { + id.Push(column++); + if (ImGui.TableNextColumn()) + header.DrawColumn(pair.Item1, pair.Item2); + id.Pop(); + } + } + + private void DrawTableInternal() + { + using var table = ImRaii.Table("Table", this.Headers.Length, this.Flags, + ImGui.GetContentRegionAvail() - this.ExtraHeight * Vector2.UnitY * InterfaceHelpers.GlobalScale); + if (!table) + return; + + this.PreDraw(); + ImGui.TableSetupScrollFreeze(1, 1); + + foreach (var header in this.Headers) + ImGui.TableSetupColumn(header.Label, header.Flags, header.Width); + + ImGui.TableNextRow(ImGuiTableRowFlags.Headers); + var i = 0; + this.VisibleColumns = 0; + foreach (var header in this.Headers) + { + using var id = ImRaii.PushId(i); + if (ImGui.TableGetColumnFlags(i).HasFlag(ImGuiTableColumnFlags.IsEnabled)) + ++this.VisibleColumns; + if (!ImGui.TableSetColumnIndex(i++)) + continue; + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + ImGui.TableHeader(string.Empty); + ImGui.SameLine(); + style.Pop(); + if (header.DrawFilter()) + this.FilterDirty = true; + } + + this.SortInternal(); + this._currentIdx = 0; + ImGuiClip.ClippedDraw(this.FilteredItems, this.DrawItem, this.ItemHeight); + } +} diff --git a/Dalamud.sln b/Dalamud.sln index 443f38496..20442e52d 100644 --- a/Dalamud.sln +++ b/Dalamud.sln @@ -38,6 +38,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs.InteropS EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DalamudCrashHandler", "DalamudCrashHandler\DalamudCrashHandler.vcxproj", "{317A264C-920B-44A1-8A34-F3A6827B0705}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.Interface", "Dalamud.Interface\Dalamud.Interface.csproj", "{757C997D-AA58-4241-8299-243C56514917}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -202,6 +204,18 @@ Global {317A264C-920B-44A1-8A34-F3A6827B0705}.Release|x64.Build.0 = Release|x64 {317A264C-920B-44A1-8A34-F3A6827B0705}.Release|x86.ActiveCfg = Release|x64 {317A264C-920B-44A1-8A34-F3A6827B0705}.Release|x86.Build.0 = Release|x64 + {757C997D-AA58-4241-8299-243C56514917}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {757C997D-AA58-4241-8299-243C56514917}.Debug|Any CPU.Build.0 = Debug|Any CPU + {757C997D-AA58-4241-8299-243C56514917}.Debug|x64.ActiveCfg = Debug|Any CPU + {757C997D-AA58-4241-8299-243C56514917}.Debug|x64.Build.0 = Debug|Any CPU + {757C997D-AA58-4241-8299-243C56514917}.Debug|x86.ActiveCfg = Debug|Any CPU + {757C997D-AA58-4241-8299-243C56514917}.Debug|x86.Build.0 = Debug|Any CPU + {757C997D-AA58-4241-8299-243C56514917}.Release|Any CPU.ActiveCfg = Release|Any CPU + {757C997D-AA58-4241-8299-243C56514917}.Release|Any CPU.Build.0 = Release|Any CPU + {757C997D-AA58-4241-8299-243C56514917}.Release|x64.ActiveCfg = Release|Any CPU + {757C997D-AA58-4241-8299-243C56514917}.Release|x64.Build.0 = Release|Any CPU + {757C997D-AA58-4241-8299-243C56514917}.Release|x86.ActiveCfg = Release|Any CPU + {757C997D-AA58-4241-8299-243C56514917}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index fa8066063..de78291f4 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -87,6 +87,7 @@ + diff --git a/Dalamud/GlobalSuppressions.cs b/Dalamud/GlobalSuppressions.cs index b60cac636..5c3b40ebc 100644 --- a/Dalamud/GlobalSuppressions.cs +++ b/Dalamud/GlobalSuppressions.cs @@ -13,4 +13,5 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1512:Single-line comments should not be followed by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")] [assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515:Single-line comment should be preceded by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")] [assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1127:Generic type constraints should be on their own line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")] +[assembly: SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1028:Code should not contain trailing whitespace", Justification = "I don't care anymore", Scope = "namespaceanddescendants", Target = "~N:Dalamud")] [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "We don't do those yet")] diff --git a/Dalamud/Interface/ImGuiHelpers.cs b/Dalamud/Interface/ImGuiHelpers.cs index bb17c4423..396a3b007 100644 --- a/Dalamud/Interface/ImGuiHelpers.cs +++ b/Dalamud/Interface/ImGuiHelpers.cs @@ -298,6 +298,7 @@ public static class ImGuiHelpers internal static void NewFrame() { GlobalScale = ImGui.GetIO().FontGlobalScale; + InterfaceHelpers.GlobalScale = GlobalScale; } /// diff --git a/targets/Dalamud.Plugin.targets b/targets/Dalamud.Plugin.targets index 3fc165454..9234f5064 100644 --- a/targets/Dalamud.Plugin.targets +++ b/targets/Dalamud.Plugin.targets @@ -26,6 +26,10 @@ $(DalamudLibPath)Dalamud.dll false + + $(DalamudLibPath)Dalamud.Interface.dll + false + $(DalamudLibPath)ImGui.NET.dll false