Dalamud/Dalamud/Interface/Utility/ImGuiHelpers.cs
2025-07-17 02:08:49 +02:00

1118 lines
43 KiB
C#

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Reactive.Disposables;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Unicode;
using Dalamud.Bindings.ImGui;
using Dalamud.Configuration.Internal;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface.ImGuiBackend.InputHandler;
using Dalamud.Interface.ImGuiSeStringRenderer;
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Utility.Raii;
using VirtualKey = Dalamud.Game.ClientState.Keys.VirtualKey;
namespace Dalamud.Interface.Utility;
/// <summary>
/// Class containing various helper methods for use with ImGui inside Dalamud.
/// </summary>
public static partial class ImGuiHelpers
{
/// <summary>
/// Gets the main viewport.
/// </summary>
public static ImGuiViewportPtr MainViewport { get; internal set; }
/// <summary>
/// Gets the global Dalamud scale.
/// </summary>
public static float GlobalScale { get; private set; }
/// <summary>
/// Gets a value indicating whether ImGui is initialized and ready for use.<br />
/// This does not necessarily mean you can call drawing functions.
/// </summary>
public static unsafe bool IsImGuiInitialized =>
ImGui.GetCurrentContext().Handle is not null && ImGui.GetIO().Handle is not null;
/// <summary>
/// Gets the global Dalamud scale; even available before drawing is ready.<br />
/// If you are sure that drawing is ready, at the point of using this, use <see cref="GlobalScale"/> instead.
/// </summary>
public static float GlobalScaleSafe =>
IsImGuiInitialized ? ImGui.GetIO().FontGlobalScale : Service<DalamudConfiguration>.Get().GlobalUiScale;
/// <summary>
/// Check if the current ImGui window is on the main viewport.
/// Only valid within a window.
/// </summary>
/// <returns>Whether the window is on the main viewport.</returns>
public static bool CheckIsWindowOnMainViewport() => MainViewport.ID == ImGui.GetWindowViewport().ID;
/// <summary>
/// Gets a <see cref="Vector2"/> that is pre-scaled with the <see cref="GlobalScale"/> multiplier.
/// </summary>
/// <param name="x">Vector2 X/Y parameter.</param>
/// <returns>A scaled Vector2.</returns>
public static Vector2 ScaledVector2(float x) => new Vector2(x, x) * GlobalScale;
/// <summary>
/// Gets a <see cref="Vector2"/> that is pre-scaled with the <see cref="GlobalScale"/> multiplier.
/// </summary>
/// <param name="x">Vector2 X parameter.</param>
/// <param name="y">Vector2 Y parameter.</param>
/// <returns>A scaled Vector2.</returns>
public static Vector2 ScaledVector2(float x, float y) => new Vector2(x, y) * GlobalScale;
/// <summary>
/// Gets a <see cref="Vector4"/> that is pre-scaled with the <see cref="GlobalScale"/> multiplier.
/// </summary>
/// <param name="x">Vector4 X parameter.</param>
/// <param name="y">Vector4 Y parameter.</param>
/// <param name="z">Vector4 Z parameter.</param>
/// <param name="w">Vector4 W parameter.</param>
/// <returns>A scaled Vector2.</returns>
public static Vector4 ScaledVector4(float x, float y, float z, float w) => new Vector4(x, y, z, w) * GlobalScale;
/// <summary>
/// Force the next ImGui window to stay inside the main game window.
/// </summary>
public static void ForceNextWindowMainViewport() => ImGui.SetNextWindowViewport(MainViewport.ID);
/// <summary>
/// Create a dummy scaled by the global Dalamud scale.
/// </summary>
/// <param name="size">The size of the dummy.</param>
public static void ScaledDummy(float size) => ScaledDummy(size, size);
/// <summary>
/// Create a dummy scaled by the global Dalamud scale.
/// </summary>
/// <param name="x">Vector2 X parameter.</param>
/// <param name="y">Vector2 Y parameter.</param>
public static void ScaledDummy(float x, float y) => ScaledDummy(new Vector2(x, y));
/// <summary>
/// Create a dummy scaled by the global Dalamud scale.
/// </summary>
/// <param name="size">The size of the dummy.</param>
public static void ScaledDummy(Vector2 size) => ImGui.Dummy(size * GlobalScale);
/// <summary>
/// Create an indent scaled by the global Dalamud scale.
/// </summary>
/// <param name="size">The size of the indent.</param>
public static void ScaledIndent(float size) => ImGui.Indent(size * GlobalScale);
/// <summary>
/// Use a relative ImGui.SameLine() from your current cursor position, scaled by the Dalamud global scale.
/// </summary>
/// <param name="offset">The offset from your current cursor position.</param>
/// <param name="spacing">The spacing to use.</param>
public static void ScaledRelativeSameLine(float offset, float spacing = -1.0f)
=> ImGui.SameLine(ImGui.GetCursorPosX() + (offset * GlobalScale), spacing);
/// <summary>
/// Set the position of the next window relative to the main viewport.
/// </summary>
/// <param name="position">The position of the next window.</param>
/// <param name="condition">When to set the position.</param>
/// <param name="pivot">The pivot to set the position around.</param>
public static void SetNextWindowPosRelativeMainViewport(Vector2 position, ImGuiCond condition = ImGuiCond.None, Vector2 pivot = default)
=> ImGui.SetNextWindowPos(position + MainViewport.Pos, condition, pivot);
/// <summary>
/// Set the position of a window relative to the main viewport.
/// </summary>
/// <param name="name">The name/ID of the window.</param>
/// <param name="position">The position of the window.</param>
/// <param name="condition">When to set the position.</param>
public static void SetWindowPosRelativeMainViewport(string name, Vector2 position, ImGuiCond condition = ImGuiCond.None)
=> ImGui.SetWindowPos(name, position + MainViewport.Pos, condition);
/// <inheritdoc cref="SetWindowPosRelativeMainViewport(string, Vector2, ImGuiCond)"/>
public static void SetWindowPosRelativeMainViewport(ReadOnlySpan<byte> name, Vector2 position, ImGuiCond condition = ImGuiCond.None)
=> ImGui.SetWindowPos(name, position + MainViewport.Pos, condition);
/// <summary>
/// Creates default color palette for use with color pickers.
/// </summary>
/// <param name="swatchCount">The total number of swatches to use.</param>
/// <returns>Default color palette.</returns>
public static unsafe List<Vector4> DefaultColorPalette(int swatchCount = 32)
{
var colorPalette = new List<Vector4>();
for (var i = 0; i < swatchCount; i++)
{
float r = 0f, g = 0f, b = 0f;
ImGui.ColorConvertHSVtoRGB(i / 31.0f, 0.7f, 0.8f, ref r, ref g, ref b);
colorPalette.Add(new Vector4(r, g, b, 1.0f));
}
return colorPalette;
}
/// <summary>
/// Get the size of a button considering the default frame padding.
/// </summary>
/// <param name="text">Text in the button.</param>
/// <returns><see cref="Vector2"/> with the size of the button.</returns>
public static Vector2 GetButtonSize(string text)
=> ImGui.CalcTextSize(text) + (ImGui.GetStyle().FramePadding * 2);
/// <inheritdoc cref="GetButtonSize(string)"/>
public static Vector2 GetButtonSize(ReadOnlySpan<byte> text)
=> ImGui.CalcTextSize(text) + (ImGui.GetStyle().FramePadding * 2);
/// <summary>
/// Print out text that can be copied when clicked.
/// </summary>
/// <param name="text">The text to show.</param>
/// <param name="textCopy">The text to copy when clicked.</param>
/// <param name="color">The color of the text.</param>
public static void ClickToCopyText(string text, string? textCopy = null, Vector4? color = null)
{
text ??= string.Empty;
textCopy ??= text;
using (var col = new ImRaii.Color())
{
if (color.HasValue)
{
col.Push(ImGuiCol.Text, color.Value);
}
ImGui.TextUnformatted(text);
}
if (ImGui.IsItemHovered())
{
ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);
using (ImRaii.Tooltip())
{
using (ImRaii.PushFont(UiBuilder.IconFont))
{
ImGui.TextUnformatted(FontAwesomeIcon.Copy.ToIconString());
}
ImGui.SameLine();
ImGui.TextUnformatted(textCopy);
}
}
if (ImGui.IsItemClicked())
{
ImGui.SetClipboardText(textCopy);
}
}
/// <inheritdoc cref="ClickToCopyText(string, string?, Vector4?)"/>
public static void ClickToCopyText(ReadOnlySpan<byte> text, ReadOnlySpan<byte> textCopy = default, Vector4? color = null)
{
if (textCopy.IsEmpty)
textCopy = text;
using (var col = new ImRaii.Color())
{
if (color.HasValue)
{
col.Push(ImGuiCol.Text, color.Value);
}
ImGui.TextUnformatted(text);
}
if (ImGui.IsItemHovered())
{
ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);
using (ImRaii.Tooltip())
{
using (ImRaii.PushFont(UiBuilder.IconFont))
{
ImGui.TextUnformatted(FontAwesomeIcon.Copy.ToIconString());
}
ImGui.SameLine();
ImGui.TextUnformatted(textCopy);
}
}
if (ImGui.IsItemClicked())
{
ImGui.SetClipboardText(textCopy);
}
}
/// <summary>Draws a SeString.</summary>
/// <param name="sss">SeString to draw.</param>
/// <param name="style">Initial rendering style.</param>
/// <param name="imGuiId">ImGui ID, if link functionality is desired.</param>
/// <param name="buttonFlags">Button flags to use on link interaction.</param>
/// <returns>Interaction result of the rendered text.</returns>
public static SeStringDrawResult SeStringWrapped(
ReadOnlySpan<byte> sss,
scoped in SeStringDrawParams style = default,
ImGuiId imGuiId = default,
ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault) =>
Service<SeStringRenderer>.Get().Draw(sss, style, imGuiId, buttonFlags);
/// <summary>Creates and caches a SeString from a text macro representation, and then draws it.</summary>
/// <param name="text">SeString text macro representation.
/// Newline characters will be normalized to <see cref="NewLinePayload"/>.</param>
/// <param name="style">Initial rendering style.</param>
/// <param name="imGuiId">ImGui ID, if link functionality is desired.</param>
/// <param name="buttonFlags">Button flags to use on link interaction.</param>
/// <returns>Interaction result of the rendered text.</returns>
public static SeStringDrawResult CompileSeStringWrapped(
string text,
scoped in SeStringDrawParams style = default,
ImGuiId imGuiId = default,
ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault) =>
Service<SeStringRenderer>.Get().CompileAndDrawWrapped(text, style, imGuiId, buttonFlags);
/// <summary>
/// Write unformatted text wrapped.
/// </summary>
/// <param name="text">The text to write.</param>
public static void SafeTextWrapped(string text) => ImGui.TextWrapped(text.Replace("%", "%%"));
/// <inheritdoc cref="SafeTextWrapped(string)"/>
public static void SafeTextWrapped(ReadOnlySpan<byte> text)
{
if (text.Contains((byte)'%'))
ImGui.TextWrapped(Encoding.UTF8.GetString(text).Replace("%", "%%"));
else
ImGui.TextWrapped(text);
}
/// <summary>
/// Write unformatted text wrapped.
/// </summary>
/// <param name="color">The color of the text.</param>
/// <param name="text">The text to write.</param>
public static void SafeTextColoredWrapped(Vector4 color, string text)
{
using (ImRaii.PushColor(ImGuiCol.Text, color))
{
SafeTextWrapped(text);
}
}
/// <inheritdoc cref="SafeTextColoredWrapped(Vector4, string)"/>
public static void SafeTextColoredWrapped(Vector4 color, ReadOnlySpan<byte> text)
{
using (ImRaii.PushColor(ImGuiCol.Text, color))
{
SafeTextWrapped(text);
}
}
/// <summary>
/// Unscales fonts after they have been rendered onto atlas.
/// </summary>
/// <param name="fontPtr">Font to scale.</param>
/// <param name="scale">Scale.</param>
/// <param name="round">If a positive number is given, numbers will be rounded to this.</param>
public static unsafe void AdjustGlyphMetrics(this ImFontPtr fontPtr, float scale, float round = 0f)
{
Func<float, float> rounder = round > 0 ? x => MathF.Round(x / round) * round : x => x;
var font = fontPtr.Handle;
font->FontSize = rounder(font->FontSize * scale);
font->Ascent = rounder(font->Ascent * scale);
font->Descent = font->FontSize - font->Ascent;
if (font->ConfigData != null)
font->ConfigData->SizePixels = rounder(font->ConfigData->SizePixels * scale);
foreach (ref var glyphHotDataReal in new Span<ImFontGlyphHotDataReal>(
(void*)font->IndexedHotData.Data,
font->IndexedHotData.Size))
{
glyphHotDataReal.AdvanceX = rounder(glyphHotDataReal.AdvanceX * scale);
glyphHotDataReal.OccupiedWidth = rounder(glyphHotDataReal.OccupiedWidth * scale);
}
foreach (ref var glyphReal in new Span<ImFontGlyphReal>((void*)font->Glyphs.Data, font->Glyphs.Size))
{
glyphReal.X0 *= scale;
glyphReal.X1 *= scale;
glyphReal.Y0 *= scale;
glyphReal.Y1 *= scale;
glyphReal.AdvanceX = rounder(glyphReal.AdvanceX * scale);
}
foreach (ref var kp in new Span<ImFontKerningPair>((void*)font->KerningPairs.Data, font->KerningPairs.Size))
kp.AdvanceXAdjustment = rounder(kp.AdvanceXAdjustment * scale);
foreach (ref var fkp in new Span<float>((void*)font->FrequentKerningPairs.Data, font->FrequentKerningPairs.Size))
fkp = rounder(fkp * scale);
}
/// <summary>
/// Fills missing glyphs in target font from source font, if both are not null.
/// </summary>
/// <param name="source">Source font.</param>
/// <param name="target">Target font.</param>
/// <param name="missingOnly">Whether to copy missing glyphs only.</param>
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
/// <param name="rangeLow">Low codepoint range to copy.</param>
/// <param name="rangeHigh">High codepoing range to copy.</param>
[Obsolete("Use the non-nullable variant.", true)]
public static void CopyGlyphsAcrossFonts(
ImFontPtr? source,
ImFontPtr? target,
bool missingOnly,
bool rebuildLookupTable = true,
int rangeLow = 32,
int rangeHigh = 0xFFFE) =>
CopyGlyphsAcrossFonts(
source ?? default,
target ?? default,
missingOnly,
rebuildLookupTable,
rangeLow,
rangeHigh);
/// <summary>
/// Fills missing glyphs in target font from source font, if both are not null.
/// </summary>
/// <param name="source">Source font.</param>
/// <param name="target">Target font.</param>
/// <param name="missingOnly">Whether to copy missing glyphs only.</param>
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
/// <param name="rangeLow">Low codepoint range to copy.</param>
/// <param name="rangeHigh">High codepoing range to copy.</param>
public static unsafe void CopyGlyphsAcrossFonts(
ImFontPtr source,
ImFontPtr target,
bool missingOnly,
bool rebuildLookupTable = true,
int rangeLow = 32,
int rangeHigh = 0xFFFE)
{
if (!source.IsNotNullAndLoaded() || !target.IsNotNullAndLoaded())
return;
var changed = false;
var scale = target.FontSize / source.FontSize;
var addedCodepoints = new HashSet<int>();
if (source.Glyphs.Size == 0)
return;
var glyphs = (ImFontGlyphReal*)source.Glyphs.Data;
if (glyphs is null)
throw new InvalidOperationException("Glyphs data is empty but size is >0?");
for (int j = 0, k = source.Glyphs.Size; j < k; j++)
{
var glyph = &glyphs![j];
if (glyph->Codepoint < rangeLow || glyph->Codepoint > rangeHigh)
continue;
var prevGlyphPtr = (ImFontGlyphReal*)target.FindGlyphNoFallback((ushort)glyph->Codepoint);
if ((IntPtr)prevGlyphPtr == IntPtr.Zero)
{
addedCodepoints.Add(glyph->Codepoint);
target.AddGlyph(
target.ConfigData,
(ushort)glyph->Codepoint,
glyph->TextureIndex,
glyph->X0 * scale,
((glyph->Y0 - source.Ascent) * scale) + target.Ascent,
glyph->X1 * scale,
((glyph->Y1 - source.Ascent) * scale) + target.Ascent,
glyph->U0,
glyph->V0,
glyph->U1,
glyph->V1,
glyph->AdvanceX * scale);
target.Mark4KPageUsedAfterGlyphAdd((ushort)glyph->Codepoint);
changed = true;
}
else if (!missingOnly)
{
addedCodepoints.Add(glyph->Codepoint);
prevGlyphPtr->TextureIndex = glyph->TextureIndex;
prevGlyphPtr->X0 = glyph->X0 * scale;
prevGlyphPtr->Y0 = ((glyph->Y0 - source.Ascent) * scale) + target.Ascent;
prevGlyphPtr->X1 = glyph->X1 * scale;
prevGlyphPtr->Y1 = ((glyph->Y1 - source.Ascent) * scale) + target.Ascent;
prevGlyphPtr->U0 = glyph->U0;
prevGlyphPtr->V0 = glyph->V0;
prevGlyphPtr->U1 = glyph->U1;
prevGlyphPtr->V1 = glyph->V1;
prevGlyphPtr->AdvanceX = glyph->AdvanceX * scale;
}
}
if (target.Glyphs.Size == 0)
return;
var kernPairs = source.KerningPairs;
for (int j = 0, k = kernPairs.Size; j < k; j++)
{
if (!addedCodepoints.Contains((int)kernPairs[j].Left))
continue;
if (!addedCodepoints.Contains((int)kernPairs[j].Right))
continue;
target.AddKerningPair(kernPairs[j].Left, kernPairs[j].Right, kernPairs[j].AdvanceXAdjustment);
changed = true;
}
if (changed && rebuildLookupTable)
{
// ImGui resolves ' ' with FindGlyph, which uses FallbackGlyph.
// FallbackGlyph is resolved after resolving ' '.
// On the first call of BuildLookupTable, called from BuildFonts, FallbackGlyph is set to null,
// making FindGlyph return nullptr.
// On our secondary calls of BuildLookupTable, FallbackGlyph is set to some value that is not null,
// making ImGui attempt to treat whatever was there as a ' '.
// This may cause random glyphs to be sized randomly, if not an access violation exception.
target.Handle->FallbackGlyph = null;
target.BuildLookupTable();
}
}
/// <summary>
/// Map a VirtualKey keycode to an ImGuiKey enum value.
/// </summary>
/// <param name="key">The VirtualKey value to retrieve the ImGuiKey counterpart for.</param>
/// <returns>The ImGuiKey that corresponds to this VirtualKey, or <c>ImGuiKey.None</c> otherwise.</returns>
public static ImGuiKey VirtualKeyToImGuiKey(VirtualKey key)
{
return Win32InputHandler.VirtualKeyToImGuiKey((int)key);
}
/// <summary>
/// Map an ImGuiKey enum value to a VirtualKey code.
/// </summary>
/// <param name="key">The ImGuiKey value to retrieve the VirtualKey counterpart for.</param>
/// <returns>The VirtualKey that corresponds to this ImGuiKey, or <c>VirtualKey.NO_KEY</c> otherwise.</returns>
public static VirtualKey ImGuiKeyToVirtualKey(ImGuiKey key)
{
return (VirtualKey)Win32InputHandler.ImGuiKeyToVirtualKey(key);
}
/// <summary>
/// Show centered text.
/// </summary>
/// <param name="text">Text to show.</param>
public static void CenteredText(string text)
{
CenterCursorForText(text);
ImGui.TextUnformatted(text);
}
/// <inheritdoc cref="CenteredText(string)"/>
public static void CenteredText(ReadOnlySpan<byte> text)
{
CenterCursorForText(text);
ImGui.TextUnformatted(text);
}
/// <summary>
/// Center the ImGui cursor for a certain text.
/// </summary>
/// <param name="text">The text to center for.</param>
public static void CenterCursorForText(string text)
=> CenterCursorFor(ImGui.CalcTextSize(text).X);
/// <inheritdoc cref="CenterCursorForText(string)"/>
public static void CenterCursorForText(ReadOnlySpan<byte> text)
=> CenterCursorFor(ImGui.CalcTextSize(text).X);
/// <summary>
/// Center the ImGui cursor for an item with a certain width.
/// </summary>
/// <param name="itemWidth">The width to center for.</param>
public static void CenterCursorFor(float itemWidth) =>
ImGui.SetCursorPosX((int)((ImGui.GetWindowWidth() - itemWidth) / 2));
/// <summary>
/// Starts a new horizontal button group.
/// </summary>
/// <returns>The group.</returns>
public static HorizontalButtonGroup BeginHorizontalButtonGroup() => new();
/// <summary>
/// Allocates memory on the heap using <see cref="ImGui.MemAlloc(nuint)"/><br />
/// Memory must be freed using <see cref="ImGui.MemFree"/>.
/// <br />
/// Note that null is a valid return value when <paramref name="length"/> is 0.
/// </summary>
/// <param name="length">The length of allocated memory.</param>
/// <returns>The allocated memory.</returns>
/// <exception cref="OutOfMemoryException">If <see cref="ImGui.MemAlloc(nuint)"/> returns null.</exception>
public static unsafe void* AllocateMemory(nuint length)
{
var memory = ImGui.MemAlloc(length);
if (memory is null)
{
throw new OutOfMemoryException(
$"Failed to allocate {length} bytes using {nameof(ImGui.MemAlloc)}");
}
return memory;
}
/// <summary>
/// Creates a new instance of <see cref="ImFontGlyphRangesBuilderPtr"/> with a natively backed memory.
/// </summary>
/// <param name="builder">The created instance.</param>
/// <returns>Disposable you can call.</returns>
public static unsafe IDisposable NewFontGlyphRangeBuilderPtrScoped(out ImFontGlyphRangesBuilderPtr builder)
{
builder = new(ImGui.ImFontGlyphRangesBuilder());
var ptr = builder.Handle;
return Disposable.Create(() =>
{
if (ptr != null)
new ImFontGlyphRangesBuilderPtr(ptr).Destroy();
ptr = null;
});
}
/// <summary>
/// Builds ImGui Glyph Ranges for use with <see cref="SafeFontConfig.GlyphRanges"/>.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="addFallbackCodepoints">Add fallback codepoints to the range.</param>
/// <param name="addEllipsisCodepoints">Add ellipsis codepoints to the range.</param>
/// <returns>When disposed, the resource allocated for the range will be freed.</returns>
public static unsafe ushort[] BuildRangesToArray(
this ImFontGlyphRangesBuilderPtr builder,
bool addFallbackCodepoints = true,
bool addEllipsisCodepoints = true)
{
if (addFallbackCodepoints)
builder.AddText(FontAtlasFactory.FallbackCodepoints);
if (addEllipsisCodepoints)
{
builder.AddText(FontAtlasFactory.EllipsisCodepoints);
builder.AddChar('.');
}
ImVector<ushort> outRanges = default;
builder.BuildRanges(&outRanges);
return new ReadOnlySpan<ushort>((void*)outRanges.Data, outRanges.Size).ToArray();
}
/// <inheritdoc cref="CreateImGuiRangesFrom(IEnumerable{UnicodeRange})"/>
public static ushort[] CreateImGuiRangesFrom(params UnicodeRange[] ranges)
=> CreateImGuiRangesFrom((IEnumerable<UnicodeRange>)ranges);
/// <summary>
/// Creates glyph ranges from <see cref="UnicodeRange"/>.<br />
/// Use values from <see cref="UnicodeRanges"/>.
/// </summary>
/// <param name="ranges">The unicode ranges.</param>
/// <returns>The range array that can be used for <see cref="SafeFontConfig.GlyphRanges"/>.</returns>
public static ushort[] CreateImGuiRangesFrom(IEnumerable<UnicodeRange> ranges) =>
ranges
.Select(x => (First: Math.Max(x.FirstCodePoint, 1), Last: x.FirstCodePoint + x.Length))
.Where(x => x.First <= ushort.MaxValue && x.First <= x.Last)
.SelectMany(
x => new[]
{
(ushort)Math.Min(x.First, ushort.MaxValue),
(ushort)Math.Min(x.Last, ushort.MaxValue),
})
.Append((ushort)0)
.ToArray();
/// <summary>
/// Determines whether <paramref name="ptr"/> is not empty and loaded.
/// </summary>
/// <param name="ptr">The pointer.</param>
/// <returns>Whether it is not null and loaded.</returns>
public static unsafe bool IsNotNullAndLoaded(this ImFontPtr ptr) => !ptr.IsNull && ptr.IsLoaded();
/// <summary>
/// If <paramref name="self"/> is default, then returns <paramref name="other"/>.
/// </summary>
/// <param name="self">The self.</param>
/// <param name="other">The other.</param>
/// <returns><paramref name="self"/> if it is not default; otherwise, <paramref name="other"/>.</returns>
public static unsafe ImFontPtr OrElse(this ImFontPtr self, ImFontPtr other) =>
self.IsNull ? other : self;
/// <summary>
/// Mark 4K page as used, after adding a codepoint to a font.
/// </summary>
/// <param name="font">The font.</param>
/// <param name="codepoint">The codepoint.</param>
internal static void Mark4KPageUsedAfterGlyphAdd(this ImFontPtr font, ushort codepoint)
{
// Mark 4K page as used
var pageIndex = unchecked((ushort)(codepoint / 4096));
font.Used4kPagesMap[pageIndex >> 3] |= unchecked((byte)(1 << (pageIndex & 7)));
}
/// <summary>
/// Sets the text for a text input, during the callback.
/// </summary>
/// <param name="data">The callback data.</param>
/// <param name="s">The new text.</param>
internal static unsafe void SetTextFromCallback(ImGuiInputTextCallbackData* data, string s)
{
if (data->BufTextLen != 0)
data->DeleteChars(0, data->BufTextLen);
var len = Encoding.UTF8.GetByteCount(s);
var buf = len < 1024 ? stackalloc byte[len] : new byte[len];
Encoding.UTF8.GetBytes(s, buf);
fixed (byte* pBuf = buf)
data->InsertChars(0, pBuf, pBuf + len);
data->SelectAll();
}
/// <summary>
/// Finds the corresponding ImGui viewport ID for the given window handle.
/// </summary>
/// <param name="hwnd">The window handle.</param>
/// <returns>The viewport ID, or -1 if not found.</returns>
internal static unsafe int FindViewportId(nint hwnd)
{
if (!IsImGuiInitialized)
return -1;
var viewports = new ImVectorWrapper<ImGuiViewportPtr>((ImVector*)Unsafe.AsPointer(ref ImGui.GetPlatformIO().Handle->Viewports));
for (var i = 0; i < viewports.LengthUnsafe; i++)
{
if (viewports.DataUnsafe[i].PlatformHandle == hwnd.ToPointer())
return i;
}
return -1;
}
/// <summary>
/// Clears the stack in the current ImGui context.
/// </summary>
[LibraryImport("cimgui", EntryPoint = "igCustom_ClearStacks")]
internal static partial void ClearStacksOnContext();
/// <summary>
/// Attempts to validate that <paramref name="fontPtr"/> is valid.
/// </summary>
/// <param name="fontPtr">The font pointer.</param>
/// <returns>The exception, if any occurred during validation.</returns>
internal static unsafe Exception? ValidateUnsafe(this ImFontPtr fontPtr)
{
try
{
var font = fontPtr.Handle;
if (font is null)
throw new NullReferenceException("The font is null.");
_ = Marshal.ReadIntPtr((nint)font);
if (font->IndexedHotData.Data != null)
_ = *font->IndexedHotData.Front;
if (font->FrequentKerningPairs.Data != null)
_ = font->FrequentKerningPairs.Data;
if (font->IndexLookup.Data != null)
_ = *font->IndexLookup.Data;
if (font->Glyphs.Data != null)
_ = *font->Glyphs.Data;
if (font->KerningPairs.Data != null)
_ = *font->KerningPairs.Data;
if (font->ConfigDataCount == 0 && font->ConfigData is not null)
throw new InvalidOperationException("ConfigDataCount == 0 but ConfigData is not null?");
if (font->ConfigDataCount != 0 && font->ConfigData is null)
throw new InvalidOperationException("ConfigDataCount != 0 but ConfigData is null?");
if (font->ConfigData is not null)
_ = Marshal.ReadIntPtr((nint)font->ConfigData);
if (font->FallbackGlyph is not null
&& ((nint)font->FallbackGlyph < (nint)font->Glyphs.Data || (nint)font->FallbackGlyph >= (nint)font->Glyphs.Data))
throw new InvalidOperationException("FallbackGlyph is not in range of Glyphs.Data");
if (font->FallbackHotData is not null
&& ((nint)font->FallbackHotData < (nint)font->IndexedHotData.Data
|| (nint)font->FallbackHotData >= (nint)font->IndexedHotData.Data))
throw new InvalidOperationException("FallbackGlyph is not in range of Glyphs.Data");
if (font->ContainerAtlas is not null)
_ = Marshal.ReadIntPtr((nint)font->ContainerAtlas);
}
catch (Exception e)
{
return e;
}
return null;
}
/// <summary>
/// Updates the fallback char of <paramref name="font"/>.
/// </summary>
/// <param name="font">The font.</param>
/// <param name="c">The fallback character.</param>
internal static unsafe void UpdateFallbackChar(this ImFontPtr font, char c)
{
font.FallbackChar = c;
font.Handle->FallbackHotData =
(ImFontGlyphHotData*)((ImFontGlyphHotDataReal*)font.IndexedHotData.Data + font.FallbackChar);
}
/// <summary>
/// Determines if the supplied codepoint is inside the given range,
/// in format of <see cref="ImFontConfig.GlyphRanges"/>.
/// </summary>
/// <param name="codepoint">The codepoint.</param>
/// <param name="rangePtr">The ranges.</param>
/// <returns>Whether it is the case.</returns>
internal static unsafe bool IsCodepointInSuppliedGlyphRangesUnsafe(int codepoint, ushort* rangePtr)
{
if (codepoint is <= 0 or >= ushort.MaxValue)
return false;
while (*rangePtr != 0)
{
var from = *rangePtr++;
var to = *rangePtr++;
if (from <= codepoint && codepoint <= to)
return true;
}
return false;
}
/// <summary>
/// Get data needed for each new frame.
/// </summary>
internal static void NewFrame()
{
GlobalScale = ImGui.GetIO().FontGlobalScale;
}
/// <summary>
/// ImFontGlyph the correct version.
/// </summary>
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "ImGui internals")]
[StructLayout(LayoutKind.Explicit, Size = 40)]
public struct ImFontGlyphReal
{
[FieldOffset(0)]
public uint ColoredVisibleTextureIndexCodepoint;
[FieldOffset(4)]
public float AdvanceX;
[FieldOffset(8)]
public float X0;
[FieldOffset(12)]
public float Y0;
[FieldOffset(16)]
public float X1;
[FieldOffset(20)]
public float Y1;
[FieldOffset(24)]
public float U0;
[FieldOffset(28)]
public float V0;
[FieldOffset(32)]
public float U1;
[FieldOffset(36)]
public float V1;
[FieldOffset(8)]
public Vector2 XY0;
[FieldOffset(16)]
public Vector2 XY1;
[FieldOffset(24)]
public Vector2 UV0;
[FieldOffset(32)]
public Vector2 UV1;
[FieldOffset(8)]
public Vector4 XY;
[FieldOffset(24)]
public Vector4 UV;
private const uint ColoredMask /*****/ = 0b_00000000_00000000_00000000_00000001u;
private const uint VisibleMask /*****/ = 0b_00000000_00000000_00000000_00000010u;
private const uint TextureMask /*****/ = 0b_00000000_00000000_00000111_11111100u;
private const uint CodepointMask /***/ = 0b_11111111_11111111_11111000_00000000u;
private const int ColoredShift = 0;
private const int VisibleShift = 1;
private const int TextureShift = 2;
private const int CodepointShift = 11;
public bool Colored
{
get => (int)((this.ColoredVisibleTextureIndexCodepoint & ColoredMask) >> ColoredShift) != 0;
set => this.ColoredVisibleTextureIndexCodepoint = (this.ColoredVisibleTextureIndexCodepoint & ~ColoredMask) | (value ? 1u << ColoredShift : 0u);
}
public bool Visible
{
get => (int)((this.ColoredVisibleTextureIndexCodepoint & VisibleMask) >> VisibleShift) != 0;
set => this.ColoredVisibleTextureIndexCodepoint = (this.ColoredVisibleTextureIndexCodepoint & ~VisibleMask) | (value ? 1u << VisibleShift : 0u);
}
public int TextureIndex
{
get => (int)(this.ColoredVisibleTextureIndexCodepoint & TextureMask) >> TextureShift;
set => this.ColoredVisibleTextureIndexCodepoint = (this.ColoredVisibleTextureIndexCodepoint & ~TextureMask) | ((uint)value << TextureShift);
}
public int Codepoint
{
get => (int)(this.ColoredVisibleTextureIndexCodepoint & CodepointMask) >> CodepointShift;
set => this.ColoredVisibleTextureIndexCodepoint = (this.ColoredVisibleTextureIndexCodepoint & ~CodepointMask) | ((uint)value << CodepointShift);
}
}
/// <summary>
/// ImFontGlyphHotData the correct version.
/// </summary>
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "ImGui internals")]
public struct ImFontGlyphHotDataReal
{
public float AdvanceX;
public float OccupiedWidth;
public uint KerningPairInfo;
private const uint UseBisectMask /***/ = 0b_00000000_00000000_00000000_00000001u;
private const uint OffsetMask /******/ = 0b_00000000_00001111_11111111_11111110u;
private const uint CountMask /*******/ = 0b_11111111_11110000_00000000_00000000u;
private const int UseBisectShift = 0;
private const int OffsetShift = 1;
private const int CountShift = 20;
public bool UseBisect
{
get => (int)((this.KerningPairInfo & UseBisectMask) >> UseBisectShift) != 0;
set => this.KerningPairInfo = (this.KerningPairInfo & ~UseBisectMask) | (value ? 1u << UseBisectShift : 0u);
}
public bool Offset
{
get => (int)((this.KerningPairInfo & OffsetMask) >> OffsetShift) != 0;
set => this.KerningPairInfo = (this.KerningPairInfo & ~OffsetMask) | (value ? 1u << OffsetShift : 0u);
}
public int Count
{
get => (int)(this.KerningPairInfo & CountMask) >> CountShift;
set => this.KerningPairInfo = (this.KerningPairInfo & ~CountMask) | ((uint)value << CountShift);
}
}
/// <summary>
/// ImFontAtlasCustomRect the correct version.
/// </summary>
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "ImGui internals")]
[StructLayout(LayoutKind.Sequential)]
public unsafe struct ImFontAtlasCustomRectReal
{
public ushort Width;
public ushort Height;
public ushort X;
public ushort Y;
public uint TextureIndexAndGlyphId;
public float GlyphAdvanceX;
public Vector2 GlyphOffset;
public ImFont* Font;
private const uint TextureIndexMask /***/ = 0b_00000000_00000000_00000111_11111100u;
private const uint GlyphIdMask /********/ = 0b_11111111_11111111_11111000_00000000u;
private const int TextureIndexShift = 2;
private const int GlyphIdShift = 11;
public int TextureIndex
{
get => (int)(this.TextureIndexAndGlyphId & TextureIndexMask) >> TextureIndexShift;
set => this.TextureIndexAndGlyphId = (this.TextureIndexAndGlyphId & ~TextureIndexMask) | ((uint)value << TextureIndexShift);
}
public int GlyphId
{
get => (int)(this.TextureIndexAndGlyphId & GlyphIdMask) >> GlyphIdShift;
set => this.TextureIndexAndGlyphId = (this.TextureIndexAndGlyphId & ~GlyphIdMask) | ((uint)value << GlyphIdShift);
}
}
/// <summary>
/// Class helper for creating a horizontal button group.
/// </summary>
public class HorizontalButtonGroup
{
private readonly List<ButtonDef> buttons = [];
/// <summary>
/// Gets or sets a value indicating whether the buttons should be centered horizontally.
/// </summary>
public bool IsCentered { get; set; } = false;
/// <summary>
/// Gets or sets the height of the buttons. If null, the default frame height is used.
/// </summary>
public float? Height { get; set; }
/// <summary>
/// Gets or sets the extra margin to add to the inside of each button, before and after the text.
/// If null, the default margin is used.
/// </summary>
public float? ExtraMargin { get; set; }
/// <summary>
/// Gets or sets the padding between buttons. If null, the default item spacing is used.
/// </summary>
public float? PaddingBetweenButtons { get; set; }
/// <summary>
/// Add a button to the group.
/// </summary>
/// <param name="text">The text of the button.</param>
/// <param name="action">The action to perform when the button is pressed.</param>
/// <returns>The group.</returns>
public HorizontalButtonGroup Add(string text, Action action)
{
this.buttons.Add(new ButtonDef(text, action));
return this;
}
/// <summary>
/// Sets whether the buttons should be centered horizontally.
/// </summary>
/// <param name="centered">The value.</param>
/// <returns>The group.</returns>
public HorizontalButtonGroup SetCentered(bool centered)
{
this.IsCentered = centered;
return this;
}
/// <summary>
/// Sets the height of the buttons.
/// </summary>
/// <param name="height">The height.</param>
/// <returns>The group.</returns>
public HorizontalButtonGroup WithHeight(float height)
{
this.Height = height;
return this;
}
/// <summary>
/// Sets the extra margin to add to the inside of each button, before and after the text.
/// </summary>
/// <param name="extraMargin">The margin.</param>
/// <returns>The group.</returns>
public HorizontalButtonGroup WithExtraMargin(float extraMargin)
{
this.ExtraMargin = extraMargin;
return this;
}
/// <summary>
/// Sets the padding between buttons.
/// </summary>
/// <param name="padding">The padding.</param>
/// <returns>The group.</returns>
public HorizontalButtonGroup WithPaddingBetweenButtons(float padding)
{
this.PaddingBetweenButtons = padding;
return this;
}
/// <summary>
/// Draw the button group at the current location.
/// </summary>
public void Draw()
{
var buttonHeight = this.Height * GlobalScale ?? ImGui.GetFrameHeight();
var buttonCount = this.buttons.Count;
if (buttonCount == 0)
return;
var buttonWidths = new float[buttonCount];
var totalContentWidth = 0f;
var extraMargin = this.ExtraMargin ?? 0f;
for (var i = 0; i < buttonCount; i++)
{
var buttonText = this.buttons[i].Text;
buttonWidths[i] = ImGui.CalcTextSize(buttonText).X + (2 * extraMargin) + (2 * ImGui.GetStyle().FramePadding.X);
totalContentWidth += buttonWidths[i];
}
var buttonPadding = this.PaddingBetweenButtons ?? ImGui.GetStyle().ItemSpacing.X;
if (buttonCount > 1)
totalContentWidth += buttonPadding * (buttonCount - 1);
var startX = ImGui.GetCursorPosX();
if (this.IsCentered)
{
var availWidth = ImGui.GetContentRegionAvail().X;
startX += (availWidth - totalContentWidth) * 0.5f;
ImGui.SetCursorPosX(startX);
}
var originalSpacing = ImGui.GetStyle().ItemSpacing;
if (this.PaddingBetweenButtons.HasValue)
{
var spacing = originalSpacing;
spacing.X = this.PaddingBetweenButtons.Value;
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, spacing);
}
for (var i = 0; i < buttonCount; i++)
{
var buttonDef = this.buttons[i];
if (this.ExtraMargin.HasValue)
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(ImGui.GetStyle().FramePadding.X + extraMargin, ImGui.GetStyle().FramePadding.Y));
if (this.Height.HasValue)
{
if (ImGui.Button(buttonDef.Text, new Vector2(buttonWidths[i], buttonHeight)))
buttonDef.Action?.Invoke();
}
else
{
if (ImGui.Button(buttonDef.Text, new Vector2(buttonWidths[i], -1)))
buttonDef.Action?.Invoke();
}
if (this.ExtraMargin.HasValue)
ImGui.PopStyleVar();
if (i < buttonCount - 1)
ImGui.SameLine();
}
if (this.PaddingBetweenButtons.HasValue)
ImGui.PopStyleVar();
}
private record ButtonDef(string Text, Action Action);
}
}