mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
1118 lines
43 KiB
C#
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);
|
|
}
|
|
}
|