Dalamud/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs

1305 lines
48 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Bindings.ImGui;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.Colors;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Utility;
using Dalamud.Utility;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
namespace Dalamud.Interface.ImGuiFontChooserDialog;
/// <summary>
/// A dialog for choosing a font and its size.
/// </summary>
[SuppressMessage(
"StyleCop.CSharp.LayoutRules",
"SA1519:Braces should not be omitted from multi-line child statement",
Justification = "Multiple fixed blocks")]
public sealed class SingleFontChooserDialog : IDisposable
{
private const float MinFontSizePt = 1;
private const float MaxFontSizePt = 127;
private static readonly List<IFontId> EmptyIFontList = new();
private static readonly (string Name, float Value)[] FontSizeList =
{
("9.6", 9.6f),
("10", 10f),
("12", 12f),
("14", 14f),
("16", 16f),
("18", 18f),
("18.4", 18.4f),
("20", 20),
("23", 23),
("34", 34),
("36", 36),
("40", 40),
("45", 45),
("46", 46),
("68", 68),
("90", 90),
};
private static int counterStatic;
private readonly int counter;
private readonly byte[] fontPreviewText = new byte[2048];
private readonly TaskCompletionSource<SingleFontSpec> tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly IFontAtlas atlas;
private string popupImGuiName;
private string title;
private bool firstDraw = true;
private bool firstDrawAfterRefresh;
private int setFocusOn = -1;
private bool useAdvancedOptions;
private AdvancedOptionsUiState advUiState;
private Task<List<IFontFamilyId>>? fontFamilies;
private int selectedFamilyIndex = -1;
private int selectedFontIndex = -1;
private int selectedFontWeight = (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL;
private int selectedFontStretch = (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL;
private int selectedFontStyle = (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL;
private string familySearch = string.Empty;
private string fontSearch = string.Empty;
private string fontSizeSearch = "12";
private IFontHandle? fontHandle;
private SingleFontSpec selectedFont;
private bool popupPositionChanged;
private bool popupSizeChanged;
private Vector2 popupPosition = new(float.NaN);
private Vector2 popupSize = new(float.NaN);
/// <summary>Initializes a new instance of the <see cref="SingleFontChooserDialog"/> class.</summary>
/// <param name="uiBuilder">The relevant instance of UiBuilder.</param>
/// <param name="isGlobalScaled">Whether the fonts in the atlas is global scaled.</param>
/// <param name="debugAtlasName">Atlas name for debugging purposes.</param>
/// <remarks>
/// <para>The passed <see cref="UiBuilder"/> is only used for creating a temporary font atlas. It will not
/// automatically register a hander for <see cref="UiBuilder.Draw"/>.</para>
/// <para>Consider using <see cref="CreateAuto"/> for automatic registration and unregistration of
/// <see cref="Draw"/> event handler in addition to automatic disposal of this class and the temporary font atlas
/// for this font chooser dialog.</para>
/// </remarks>
public SingleFontChooserDialog(UiBuilder uiBuilder, bool isGlobalScaled = true, string? debugAtlasName = null)
: this(uiBuilder.CreateFontAtlas(FontAtlasAutoRebuildMode.Async, isGlobalScaled, debugAtlasName))
{
}
/// <summary>Initializes a new instance of the <see cref="SingleFontChooserDialog"/> class.</summary>
/// <param name="factory">An instance of <see cref="FontAtlasFactory"/>.</param>
/// <param name="debugAtlasName">The temporary atlas name.</param>
internal SingleFontChooserDialog(FontAtlasFactory factory, string debugAtlasName)
: this(factory.CreateFontAtlas(debugAtlasName, FontAtlasAutoRebuildMode.Async))
{
}
/// <summary>Initializes a new instance of the <see cref="SingleFontChooserDialog"/> class.</summary>
/// <param name="newAsyncAtlas">A new instance of <see cref="IFontAtlas"/> created using
/// <see cref="FontAtlasAutoRebuildMode.Async"/> as its auto-rebuild mode.</param>
/// <remarks>The passed instance of <paramref see="newAsyncAtlas"/> will be disposed after use. If you pass an atlas
/// that is already being used, then all the font handles under the passed atlas will be invalidated upon disposing
/// this font chooser. Consider using <see cref="SingleFontChooserDialog(UiBuilder, bool, string?)"/> for automatic
/// handling of font atlas derived from a <see cref="UiBuilder"/>, or even <see cref="CreateAuto"/> for automatic
/// registration and unregistration of <see cref="Draw"/> event handler in addition to automatic disposal of this
/// class and the temporary font atlas for this font chooser dialog.</remarks>
private SingleFontChooserDialog(IFontAtlas newAsyncAtlas)
{
this.counter = Interlocked.Increment(ref counterStatic);
this.title = "Choose a font...";
this.popupImGuiName = $"{this.title}##{nameof(SingleFontChooserDialog)}[{this.counter}]";
this.atlas = newAsyncAtlas;
this.selectedFont = new() { FontId = DalamudDefaultFontAndFamilyId.Instance };
Encoding.UTF8.GetBytes("Font preview.\n0123456789!\n遍角次亮采之门门上插刀、直字拐弯、天上平板、船顶漏雨。\n다람쥐 헌 쳇바퀴에 타고파", this.fontPreviewText);
}
/// <summary>Called when the selected font spec has changed.</summary>
public event Action<SingleFontSpec>? SelectedFontSpecChanged;
/// <summary>
/// Gets or sets the title of this font chooser dialog popup.
/// </summary>
public string Title
{
get => this.title;
set
{
this.title = value;
this.popupImGuiName = $"{this.title}##{nameof(SingleFontChooserDialog)}[{this.counter}]";
}
}
/// <summary>
/// Gets or sets the preview text. A text too long may be truncated on assignment.
/// </summary>
public string PreviewText
{
get
{
var n = this.fontPreviewText.AsSpan().IndexOf((byte)0);
return n < 0
? Encoding.UTF8.GetString(this.fontPreviewText)
: Encoding.UTF8.GetString(this.fontPreviewText, 0, n);
}
set => Encoding.UTF8.GetBytes(value, this.fontPreviewText);
}
/// <summary>
/// Gets the task that resolves upon choosing a font or cancellation.
/// </summary>
public Task<SingleFontSpec> ResultTask => this.tcs.Task;
/// <summary>
/// Gets or sets the selected family and font.
/// </summary>
public SingleFontSpec SelectedFont
{
get => this.selectedFont;
set
{
this.selectedFont = value;
var familyName = value.FontId.Family.ToString() ?? string.Empty;
var fontName = value.FontId.ToString() ?? string.Empty;
this.familySearch = this.ExtractName(value.FontId.Family);
this.fontSearch = this.ExtractName(value.FontId);
if (this.fontFamilies?.IsCompletedSuccessfully is true)
this.UpdateSelectedFamilyAndFontIndices(this.fontFamilies.Result, familyName, fontName);
this.fontSizeSearch = $"{value.SizePt:0.##}";
this.advUiState = new(value);
this.useAdvancedOptions |= Math.Abs(value.LineHeight - 1f) > 0.000001;
this.useAdvancedOptions |= value.GlyphOffset != default;
this.useAdvancedOptions |= value.LetterSpacing != 0f;
this.SelectedFontSpecChanged?.Invoke(this.selectedFont);
}
}
/// <summary>
/// Gets or sets the font family exclusion filter predicate.
/// </summary>
public Predicate<IFontFamilyId>? FontFamilyExcludeFilter { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to ignore the global scale on preview text input.
/// </summary>
public bool IgnorePreviewGlobalScale { get; set; }
/// <summary>Gets or sets a value indicating whether this popup should be modal, blocking everything behind from
/// being interacted.</summary>
/// <remarks>If <c>true</c>, then <see cref="ImGui.BeginPopupModal(ImU8String, ref bool, ImGuiWindowFlags)"/> will be
/// used. Otherwise, <see cref="ImGui.Begin(ImU8String, ref bool, ImGuiWindowFlags)"/> will be used.</remarks>
public bool IsModal { get; set; } = true;
/// <summary>Gets or sets the window flags.</summary>
public ImGuiWindowFlags WindowFlags { get; set; }
/// <summary>Gets or sets the popup window position.</summary>
/// <remarks>
/// <para>Setting the position only works before the first call to <see cref="Draw"/>.</para>
/// <para>If any of the coordinates are <see cref="float.NaN"/>, default position will be used.</para>
/// <para>The position will be clamped into the work area of the selected monitor.</para>
/// </remarks>
public Vector2 PopupPosition
{
get => this.popupPosition;
set
{
this.popupPositionChanged = true;
this.popupPosition = value;
}
}
/// <summary>Gets or sets the popup window size.</summary>
/// <remarks>
/// <para>Setting the size only works before the first call to <see cref="Draw"/>.</para>
/// <para>If any of the coordinates are <see cref="float.NaN"/>, default size will be used.</para>
/// <para>The size will be clamped into the work area of the selected monitor.</para>
/// </remarks>
public Vector2 PopupSize
{
get => this.popupSize;
set
{
this.popupSizeChanged = true;
this.popupSize = value;
}
}
/// <summary>Creates a new instance of <see cref="SingleFontChooserDialog"/> that will automatically draw and
/// dispose itself as needed; calling <see cref="Draw"/> and <see cref="Dispose"/> are handled automatically.
/// </summary>
/// <param name="uiBuilder">An instance of <see cref="UiBuilder"/>.</param>
/// <returns>The new instance of <see cref="SingleFontChooserDialog"/>.</returns>
public static SingleFontChooserDialog CreateAuto(UiBuilder uiBuilder)
{
var fcd = new SingleFontChooserDialog(uiBuilder);
uiBuilder.Draw += fcd.Draw;
fcd.tcs.Task.ContinueWith(
r =>
{
_ = r.Exception;
uiBuilder.Draw -= fcd.Draw;
fcd.Dispose();
});
return fcd;
}
/// <summary>Gets the default popup size before clamping to monitor work area.</summary>
/// <returns>The default popup size.</returns>
public static Vector2 GetDefaultPopupSizeNonClamped()
{
ThreadSafety.AssertMainThread();
return new Vector2(40, 30) * ImGui.GetTextLineHeight();
}
/// <inheritdoc/>
public void Dispose()
{
this.fontHandle?.Dispose();
this.atlas.Dispose();
}
/// <summary>
/// Cancels this dialog.
/// </summary>
public void Cancel()
{
this.tcs.SetCanceled();
ImGui.GetIO().WantCaptureKeyboard = false;
ImGui.GetIO().WantTextInput = false;
}
/// <summary>Sets <see cref="PopupSize"/> and <see cref="PopupPosition"/> to be at the center of the current window
/// being drawn.</summary>
/// <param name="preferredPopupSize">The preferred popup size.</param>
public void SetPopupPositionAndSizeToCurrentWindowCenter(Vector2 preferredPopupSize)
{
ThreadSafety.AssertMainThread();
this.PopupSize = preferredPopupSize;
this.PopupPosition = ImGui.GetWindowPos() + ((ImGui.GetWindowSize() - preferredPopupSize) / 2);
}
/// <summary>Sets <see cref="PopupSize"/> and <see cref="PopupPosition"/> to be at the center of the current window
/// being drawn.</summary>
public void SetPopupPositionAndSizeToCurrentWindowCenter() =>
this.SetPopupPositionAndSizeToCurrentWindowCenter(GetDefaultPopupSizeNonClamped());
/// <summary>
/// Draws this dialog.
/// </summary>
public void Draw()
{
const float popupMinWidth = 320;
const float popupMinHeight = 240;
ImGui.GetIO().WantCaptureKeyboard = true;
ImGui.GetIO().WantTextInput = true;
if (ImGui.IsKeyPressed(ImGuiKey.Escape))
{
this.Cancel();
return;
}
if (this.firstDraw)
{
if (this.IsModal)
ImGui.OpenPopup(this.popupImGuiName);
}
if (this.firstDraw || this.popupPositionChanged || this.popupSizeChanged)
{
var preferProvidedSize = !float.IsNaN(this.popupSize.X) && !float.IsNaN(this.popupSize.Y);
var size = preferProvidedSize ? this.popupSize : GetDefaultPopupSizeNonClamped();
size.X = Math.Max(size.X, popupMinWidth);
size.Y = Math.Max(size.Y, popupMinHeight);
var preferProvidedPos = !float.IsNaN(this.popupPosition.X) && !float.IsNaN(this.popupPosition.Y);
var monitorLocatorPos = preferProvidedPos ? this.popupPosition + (size / 2) : ImGui.GetMousePos();
var monitors = ImGui.GetPlatformIO().Monitors;
var preferredMonitor = 0;
var preferredDistance = GetDistanceFromMonitor(monitorLocatorPos, monitors[0]);
for (var i = 1; i < monitors.Size; i++)
{
var distance = GetDistanceFromMonitor(monitorLocatorPos, monitors[i]);
if (distance < preferredDistance)
{
preferredMonitor = i;
preferredDistance = distance;
}
}
var lt = monitors[preferredMonitor].WorkPos;
var workSize = monitors[preferredMonitor].WorkSize;
size.X = Math.Min(size.X, workSize.X);
size.Y = Math.Min(size.Y, workSize.Y);
var rb = (lt + workSize) - size;
var pos =
preferProvidedPos
? new(Math.Clamp(this.PopupPosition.X, lt.X, rb.X), Math.Clamp(this.PopupPosition.Y, lt.Y, rb.Y))
: (lt + rb) / 2;
ImGui.SetNextWindowSize(size, ImGuiCond.Always);
ImGui.SetNextWindowPos(pos, ImGuiCond.Always);
this.popupPositionChanged = this.popupSizeChanged = false;
}
ImGui.SetNextWindowSizeConstraints(new(popupMinWidth, popupMinHeight), new(float.MaxValue));
if (this.IsModal)
{
var open = true;
if (!ImGui.BeginPopupModal(this.popupImGuiName, ref open, this.WindowFlags) || !open)
{
this.Cancel();
return;
}
}
else
{
var open = true;
if (!ImGui.Begin(this.popupImGuiName, ref open, this.WindowFlags) || !open)
{
ImGui.End();
this.Cancel();
return;
}
}
var framePad = ImGui.GetStyle().FramePadding;
var windowPad = ImGui.GetStyle().WindowPadding;
var baseOffset = ImGui.GetCursorPos() - windowPad;
var actionSize = Vector2.Zero;
actionSize = Vector2.Max(actionSize, ImGui.CalcTextSize("OK"u8));
actionSize = Vector2.Max(actionSize, ImGui.CalcTextSize("Cancel"u8));
actionSize = Vector2.Max(actionSize, ImGui.CalcTextSize("Refresh"u8));
actionSize = Vector2.Max(actionSize, ImGui.CalcTextSize("Reset"u8));
actionSize += framePad * 2;
var bodySize = ImGui.GetContentRegionAvail();
ImGui.SetCursorPos(baseOffset + windowPad);
if (ImGui.BeginChild(
"##choicesBlock"u8,
bodySize with { X = bodySize.X - windowPad.X - actionSize.X },
false,
ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse))
{
this.DrawChoices();
}
ImGui.EndChild();
ImGui.SetCursorPos(baseOffset + windowPad + new Vector2(bodySize.X - actionSize.X, 0));
if (ImGui.BeginChild("##actionsBlock"u8, bodySize with { X = actionSize.X }))
{
this.DrawActionButtons(actionSize);
}
ImGui.EndChild();
this.popupPosition = ImGui.GetWindowPos();
this.popupSize = ImGui.GetWindowSize();
if (this.IsModal)
ImGui.EndPopup();
else
ImGui.End();
this.firstDraw = false;
this.firstDrawAfterRefresh = false;
}
private static float GetDistanceFromMonitor(Vector2 point, ImGuiPlatformMonitor monitor)
{
var lt = monitor.MainPos;
var rb = monitor.MainPos + monitor.MainSize;
var xoff =
point.X < lt.X
? lt.X - point.X
: point.X > rb.X
? point.X - rb.X
: 0;
var yoff =
point.Y < lt.Y
? lt.Y - point.Y
: point.Y > rb.Y
? point.Y - rb.Y
: 0;
return MathF.Sqrt((xoff * xoff) + (yoff * yoff));
}
private void DrawChoices()
{
var lineHeight = ImGui.GetTextLineHeight();
var previewHeight = (ImGui.GetFrameHeightWithSpacing() - lineHeight) +
Math.Max(lineHeight, this.selectedFont.LineHeightPx * 2);
var advancedOptionsHeight = ImGui.GetFrameHeightWithSpacing() * (this.useAdvancedOptions ? 4 : 1);
var tableSize = ImGui.GetContentRegionAvail() -
new Vector2(0, ImGui.GetStyle().WindowPadding.Y + previewHeight + advancedOptionsHeight);
if (ImGui.BeginChild(
"##tableContainer"u8,
tableSize,
false,
ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)
&& ImGui.BeginTable("##table"u8, 3, ImGuiTableFlags.None))
{
ImGui.PushStyleColor(ImGuiCol.TableHeaderBg, Vector4.Zero);
ImGui.PushStyleColor(ImGuiCol.HeaderHovered, Vector4.Zero);
ImGui.PushStyleColor(ImGuiCol.HeaderActive, Vector4.Zero);
ImGui.TableSetupColumn(
"Font:##familyColumn"u8,
ImGuiTableColumnFlags.WidthStretch,
0.4f);
ImGui.TableSetupColumn(
"Style:##fontColumn"u8,
ImGuiTableColumnFlags.WidthStretch,
0.4f);
ImGui.TableSetupColumn(
"Size:##sizeColumn"u8,
ImGuiTableColumnFlags.WidthStretch,
0.2f);
ImGui.TableHeadersRow();
ImGui.PopStyleColor(3);
ImGui.TableNextRow();
var pad = (int)MathF.Round(8 * ImGuiHelpers.GlobalScale);
ImGui.PushStyleVar(ImGuiStyleVar.CellPadding, new Vector2(pad));
ImGui.TableNextColumn();
var changed = this.DrawFamilyListColumn();
ImGui.TableNextColumn();
changed |= this.DrawFontListColumn(changed);
ImGui.TableNextColumn();
changed |= this.DrawSizeListColumn();
if (changed)
{
this.fontHandle?.Dispose();
this.fontHandle = null;
}
ImGui.PopStyleVar();
ImGui.EndTable();
}
ImGui.EndChild();
ImGui.Checkbox("Show advanced options"u8, ref this.useAdvancedOptions);
if (this.useAdvancedOptions)
{
if (this.DrawAdvancedOptions())
{
this.fontHandle?.Dispose();
this.fontHandle = null;
}
}
if (this.fontHandle is null)
{
if (this.IgnorePreviewGlobalScale)
{
this.fontHandle = this.selectedFont.CreateFontHandle(
this.atlas,
tk => tk.OnPreBuild(e => e.SetFontScaleMode(e.Font, FontScaleMode.UndoGlobalScale)));
}
else
{
this.fontHandle = this.selectedFont.CreateFontHandle(this.atlas);
}
this.SelectedFontSpecChanged?.InvokeSafely(this.selectedFont);
}
if (this.fontHandle is null)
{
ImGui.SetCursorPos(ImGui.GetCursorPos() + ImGui.GetStyle().FramePadding);
ImGui.Text("Select a font."u8);
}
else if (this.fontHandle.LoadException is { } loadException)
{
ImGui.SetCursorPos(ImGui.GetCursorPos() + ImGui.GetStyle().FramePadding);
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
ImGui.Text(loadException.Message);
ImGui.PopStyleColor();
}
else if (!this.fontHandle.Available)
{
ImGui.SetCursorPos(ImGui.GetCursorPos() + ImGui.GetStyle().FramePadding);
ImGui.Text("Loading font..."u8);
}
else
{
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
using (this.fontHandle?.Push())
{
ImGui.InputTextMultiline(
"##fontPreviewText"u8,
this.fontPreviewText,
ImGui.GetContentRegionAvail());
}
}
}
private unsafe bool DrawFamilyListColumn()
{
if (this.fontFamilies?.IsCompleted is not true)
{
ImGui.SetScrollY(0);
ImGui.Text("Loading..."u8);
return false;
}
if (!this.fontFamilies.IsCompletedSuccessfully)
{
ImGui.SetScrollY(0);
ImGui.Text("Error: " + this.fontFamilies.Exception);
return false;
}
var families = this.fontFamilies.Result;
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
if (this.setFocusOn == 0)
{
this.setFocusOn = -1;
ImGui.SetKeyboardFocusHere();
}
var changed = false;
if (ImGui.InputText(
"##familySearch"u8,
ref this.familySearch,
255,
ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CallbackHistory,
(ref ImGuiInputTextCallbackData data) =>
{
if (families.Count == 0)
return 0;
var baseIndex = this.selectedFamilyIndex;
if (data.SelectionStart == 0 && data.SelectionEnd == data.BufTextLen)
{
switch (data.EventKey)
{
case ImGuiKey.DownArrow:
this.selectedFamilyIndex = (this.selectedFamilyIndex + 1) % families.Count;
changed = true;
break;
case ImGuiKey.UpArrow:
this.selectedFamilyIndex =
(this.selectedFamilyIndex + families.Count - 1) % families.Count;
changed = true;
break;
}
if (changed)
{
ImGuiHelpers.SetTextFromCallback(
ref data,
this.ExtractName(families[this.selectedFamilyIndex]));
}
}
else
{
switch (data.EventKey)
{
case ImGuiKey.DownArrow:
this.selectedFamilyIndex = families.FindIndex(
baseIndex + 1,
x => this.TestName(x, this.familySearch));
if (this.selectedFamilyIndex < 0)
{
this.selectedFamilyIndex = families.FindIndex(
0,
baseIndex + 1,
x => this.TestName(x, this.familySearch));
}
changed = true;
break;
case ImGuiKey.UpArrow:
if (baseIndex > 0)
{
this.selectedFamilyIndex = families.FindLastIndex(
baseIndex - 1,
x => this.TestName(x, this.familySearch));
}
if (this.selectedFamilyIndex < 0)
{
if (baseIndex < 0)
baseIndex = 0;
this.selectedFamilyIndex = families.FindLastIndex(
families.Count - 1,
families.Count - baseIndex,
x => this.TestName(x, this.familySearch));
}
changed = true;
break;
}
}
return 0;
}))
{
if (!string.IsNullOrWhiteSpace(this.familySearch) && !changed)
{
this.selectedFamilyIndex = families.FindIndex(x => this.TestName(x, this.familySearch));
changed = true;
}
}
if (ImGui.BeginChild("##familyList"u8, ImGui.GetContentRegionAvail()))
{
var clipper = ImGui.ImGuiListClipper();
var lineHeight = ImGui.GetTextLineHeightWithSpacing();
if ((changed || this.firstDrawAfterRefresh) && this.selectedFamilyIndex != -1)
{
ImGui.SetScrollY(
(lineHeight * this.selectedFamilyIndex) -
((ImGui.GetContentRegionAvail().Y - lineHeight) / 2));
}
clipper.Begin(families.Count, lineHeight);
while (clipper.Step())
{
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
{
if (i < 0)
{
ImGui.Text(" "u8);
continue;
}
var selected = this.selectedFamilyIndex == i;
if (ImGui.Selectable(
this.ExtractName(families[i]),
ref selected,
ImGuiSelectableFlags.DontClosePopups))
{
this.selectedFamilyIndex = families.IndexOf(families[i]);
this.familySearch = this.ExtractName(families[i]);
this.setFocusOn = 0;
changed = true;
}
}
}
clipper.Destroy();
}
if (changed && this.selectedFamilyIndex >= 0)
{
var family = families[this.selectedFamilyIndex];
using var matchingFont = default(ComPtr<IDWriteFont>);
this.selectedFontIndex = family.FindBestMatch(
this.selectedFontWeight,
this.selectedFontStretch,
this.selectedFontStyle);
this.selectedFont = this.selectedFont with { FontId = family.Fonts[this.selectedFontIndex] };
}
ImGui.EndChild();
return changed;
}
private unsafe bool DrawFontListColumn(bool changed)
{
if (this.fontFamilies?.IsCompleted is not true)
{
ImGui.Text("Loading..."u8);
return changed;
}
if (!this.fontFamilies.IsCompletedSuccessfully)
{
ImGui.Text("Error: " + this.fontFamilies.Exception);
return changed;
}
var families = this.fontFamilies.Result;
var family = this.selectedFamilyIndex >= 0
&& this.selectedFamilyIndex < families.Count
? families[this.selectedFamilyIndex]
: null;
var fonts = family?.Fonts ?? EmptyIFontList;
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
if (this.setFocusOn == 1)
{
this.setFocusOn = -1;
ImGui.SetKeyboardFocusHere();
}
if (ImGui.InputText(
"##fontSearch"u8,
ref this.fontSearch,
255,
ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CallbackHistory,
(ref ImGuiInputTextCallbackData data) =>
{
if (fonts.Count == 0)
return 0;
var baseIndex = this.selectedFontIndex;
if (data.SelectionStart == 0 && data.SelectionEnd == data.BufTextLen)
{
switch (data.EventKey)
{
case ImGuiKey.DownArrow:
this.selectedFontIndex = (this.selectedFontIndex + 1) % fonts.Count;
changed = true;
break;
case ImGuiKey.UpArrow:
this.selectedFontIndex = (this.selectedFontIndex + fonts.Count - 1) % fonts.Count;
changed = true;
break;
}
if (changed)
{
ImGuiHelpers.SetTextFromCallback(
ref data,
this.ExtractName(fonts[this.selectedFontIndex]));
}
}
else
{
switch (data.EventKey)
{
case ImGuiKey.DownArrow:
this.selectedFontIndex = fonts.FindIndex(
baseIndex + 1,
x => this.TestName(x, this.fontSearch));
if (this.selectedFontIndex < 0)
{
this.selectedFontIndex = fonts.FindIndex(
0,
baseIndex + 1,
x => this.TestName(x, this.fontSearch));
}
changed = true;
break;
case ImGuiKey.UpArrow:
if (baseIndex > 0)
{
this.selectedFontIndex = fonts.FindLastIndex(
baseIndex - 1,
x => this.TestName(x, this.fontSearch));
}
if (this.selectedFontIndex < 0)
{
if (baseIndex < 0)
baseIndex = 0;
this.selectedFontIndex = fonts.FindLastIndex(
fonts.Count - 1,
fonts.Count - baseIndex,
x => this.TestName(x, this.fontSearch));
}
changed = true;
break;
}
}
return 0;
}))
{
if (!string.IsNullOrWhiteSpace(this.fontSearch) && !changed)
{
this.selectedFontIndex = fonts.FindIndex(x => this.TestName(x, this.fontSearch));
changed = true;
}
}
if (ImGui.BeginChild("##fontList"u8))
{
var clipper = ImGui.ImGuiListClipper();
var lineHeight = ImGui.GetTextLineHeightWithSpacing();
if ((changed || this.firstDrawAfterRefresh) && this.selectedFontIndex != -1)
{
ImGui.SetScrollY(
(lineHeight * this.selectedFontIndex) -
((ImGui.GetContentRegionAvail().Y - lineHeight) / 2));
}
clipper.Begin(fonts.Count, lineHeight);
while (clipper.Step())
{
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
{
if (i < 0)
{
ImGui.Text(" "u8);
continue;
}
var selected = this.selectedFontIndex == i;
if (ImGui.Selectable(
this.ExtractName(fonts[i]),
ref selected,
ImGuiSelectableFlags.DontClosePopups))
{
this.selectedFontIndex = fonts.IndexOf(fonts[i]);
this.fontSearch = this.ExtractName(fonts[i]);
this.setFocusOn = 1;
changed = true;
}
}
}
clipper.Destroy();
}
ImGui.EndChild();
if (changed && family is not null && this.selectedFontIndex >= 0)
{
var font = family.Fonts[this.selectedFontIndex];
this.selectedFontWeight = font.Weight;
this.selectedFontStretch = font.Stretch;
this.selectedFontStyle = font.Style;
int fontNo = 0;
if (family is DalamudAssetFontAndFamilyId { Asset: DalamudAsset.NotoSansCJKRegular or DalamudAsset.NotoSansCJKMedium })
{
var dalamudConfiguration = Service<DalamudConfiguration>.Get();
fontNo = dalamudConfiguration.EffectiveLanguage switch
{
"jp" => 0,
"tw" => 1,
"zh" => 2,
"ko" => 3,
_ => 0,
};
}
this.selectedFont = this.selectedFont with { FontId = font, FontNo = fontNo };
}
return changed;
}
private unsafe bool DrawSizeListColumn()
{
var changed = false;
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
if (this.setFocusOn == 2)
{
this.setFocusOn = -1;
ImGui.SetKeyboardFocusHere();
}
if (ImGui.InputText(
"##fontSizeSearch"u8,
ref this.fontSizeSearch,
255,
ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CallbackHistory |
ImGuiInputTextFlags.CharsDecimal,
(ref ImGuiInputTextCallbackData data) =>
{
switch (data.EventKey)
{
case ImGuiKey.DownArrow:
this.selectedFont = this.selectedFont with
{
SizePt = Math.Min(MaxFontSizePt, MathF.Floor(this.selectedFont.SizePt) + 1),
};
changed = true;
break;
case ImGuiKey.UpArrow:
this.selectedFont = this.selectedFont with
{
SizePt = Math.Max(MinFontSizePt, MathF.Ceiling(this.selectedFont.SizePt) - 1),
};
changed = true;
break;
}
if (changed)
ImGuiHelpers.SetTextFromCallback(ref data, $"{this.selectedFont.SizePt:0.##}");
return 0;
}))
{
if (float.TryParse(this.fontSizeSearch, out var fontSizePt1))
{
this.selectedFont = this.selectedFont with { SizePt = fontSizePt1 };
changed = true;
}
}
if (ImGui.BeginChild("##fontSizeList"u8))
{
var clipper = ImGui.ImGuiListClipper();
var lineHeight = ImGui.GetTextLineHeightWithSpacing();
if (changed && this.selectedFontIndex != -1)
{
ImGui.SetScrollY(
(lineHeight * this.selectedFontIndex) -
((ImGui.GetContentRegionAvail().Y - lineHeight) / 2));
}
clipper.Begin(FontSizeList.Length, lineHeight);
while (clipper.Step())
{
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
{
if (i < 0)
{
ImGui.Text(" "u8);
continue;
}
var selected = Equals(FontSizeList[i].Value, this.selectedFont.SizePt);
if (ImGui.Selectable(
FontSizeList[i].Name,
ref selected,
ImGuiSelectableFlags.DontClosePopups))
{
this.selectedFont = this.selectedFont with { SizePt = FontSizeList[i].Value };
this.setFocusOn = 2;
changed = true;
}
}
}
clipper.Destroy();
}
ImGui.EndChild();
if (this.selectedFont.SizePt < MinFontSizePt)
{
this.selectedFont = this.selectedFont with { SizePt = MinFontSizePt };
changed = true;
}
if (this.selectedFont.SizePt > MaxFontSizePt)
{
this.selectedFont = this.selectedFont with { SizePt = MaxFontSizePt };
changed = true;
}
if (changed)
this.fontSizeSearch = $"{this.selectedFont.SizePt:0.##}";
return changed;
}
private bool DrawAdvancedOptions()
{
var changed = false;
if (!ImGui.BeginTable("##advancedOptions"u8, 4))
return false;
var labelWidth = ImGui.CalcTextSize("Letter Spacing:"u8).X;
labelWidth = Math.Max(labelWidth, ImGui.CalcTextSize("Offset:"u8).X);
labelWidth = Math.Max(labelWidth, ImGui.CalcTextSize("Line Height:"u8).X);
labelWidth += ImGui.GetStyle().FramePadding.X;
var inputWidth = ImGui.CalcTextSize("000.000"u8).X + (ImGui.GetStyle().FramePadding.X * 2);
ImGui.TableSetupColumn(
"##inputLabelColumn"u8,
ImGuiTableColumnFlags.WidthFixed,
labelWidth);
ImGui.TableSetupColumn(
"##input1Column"u8,
ImGuiTableColumnFlags.WidthFixed,
inputWidth);
ImGui.TableSetupColumn(
"##input2Column"u8,
ImGuiTableColumnFlags.WidthFixed,
inputWidth);
ImGui.TableSetupColumn(
"##fillerColumn"u8,
ImGuiTableColumnFlags.WidthStretch,
1f);
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.Text("Offset:"u8);
ImGui.TableNextColumn();
if (FloatInputText(
"##glyphOffsetXInput",
ref this.advUiState.OffsetXText,
this.selectedFont.GlyphOffset.X) is { } newGlyphOffsetX)
{
changed = true;
this.selectedFont = this.selectedFont with
{
GlyphOffset = this.selectedFont.GlyphOffset with { X = newGlyphOffsetX },
};
}
ImGui.TableNextColumn();
if (FloatInputText(
"##glyphOffsetYInput",
ref this.advUiState.OffsetYText,
this.selectedFont.GlyphOffset.Y) is { } newGlyphOffsetY)
{
changed = true;
this.selectedFont = this.selectedFont with
{
GlyphOffset = this.selectedFont.GlyphOffset with { Y = newGlyphOffsetY },
};
}
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.Text("Letter Spacing:"u8);
ImGui.TableNextColumn();
if (FloatInputText(
"##letterSpacingXInput",
ref this.advUiState.LetterSpacingText,
this.selectedFont.LetterSpacing) is { } newLetterSpacing)
{
changed = true;
this.selectedFont = this.selectedFont with { LetterSpacing = newLetterSpacing };
}
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.Text("Line Height:"u8);
ImGui.TableNextColumn();
if (FloatInputText(
"##lineHeightInput",
ref this.advUiState.LineHeightText,
this.selectedFont.LineHeight,
0.05f,
0.1f,
3f) is { } newLineHeight)
{
changed = true;
this.selectedFont = this.selectedFont with { LineHeight = newLineHeight };
}
ImGui.EndTable();
return changed;
static unsafe float? FloatInputText(
string label, ref string buf, float value, float step = 1f, float min = -127, float max = 127)
{
var stylePushed = value < min || value > max || !float.TryParse(buf, out _);
if (stylePushed)
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
var changed2 = false;
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
var changed1 = ImGui.InputText(
label,
ref buf,
255,
ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CallbackHistory |
ImGuiInputTextFlags.CharsDecimal,
(ref ImGuiInputTextCallbackData data) =>
{
switch (data.EventKey)
{
case ImGuiKey.DownArrow:
changed2 = true;
value = Math.Min(max, (MathF.Round(value / step) * step) + step);
ImGuiHelpers.SetTextFromCallback(ref data, $"{value:0.##}");
break;
case ImGuiKey.UpArrow:
changed2 = true;
value = Math.Max(min, (MathF.Round(value / step) * step) - step);
ImGuiHelpers.SetTextFromCallback(ref data, $"{value:0.##}");
break;
}
return 0;
});
if (stylePushed)
ImGui.PopStyleColor();
if (!changed1 && !changed2)
return null;
if (!float.TryParse(buf, out var parsed))
return null;
if (min > parsed || parsed > max)
return null;
return parsed;
}
}
private void DrawActionButtons(Vector2 buttonSize)
{
if (this.fontHandle?.Available is not true
|| this.FontFamilyExcludeFilter?.Invoke(this.selectedFont.FontId.Family) is true)
{
ImGui.BeginDisabled();
ImGui.Button("OK"u8, buttonSize);
ImGui.EndDisabled();
}
else if (ImGui.Button("OK"u8, buttonSize))
{
this.tcs.SetResult(this.selectedFont);
}
if (ImGui.Button("Cancel"u8, buttonSize))
{
this.Cancel();
}
var doRefresh = false;
var isFirst = false;
if (this.fontFamilies?.IsCompleted is not true)
{
isFirst = doRefresh = this.fontFamilies is null;
ImGui.BeginDisabled();
ImGui.Button("Refresh"u8, buttonSize);
ImGui.EndDisabled();
}
else if (ImGui.Button("Refresh"u8, buttonSize))
{
doRefresh = true;
}
if (doRefresh)
{
this.fontFamilies =
this.fontFamilies?.ContinueWith(_ => RefreshBody())
?? Task.Run(RefreshBody);
this.fontFamilies.ContinueWith(_ => this.firstDrawAfterRefresh = true);
List<IFontFamilyId> RefreshBody()
{
var familyName = this.selectedFont.FontId.Family.ToString() ?? string.Empty;
var fontName = this.selectedFont.FontId.ToString() ?? string.Empty;
var newFonts = new List<IFontFamilyId> { DalamudDefaultFontAndFamilyId.Instance };
newFonts.AddRange(IFontFamilyId.ListDalamudFonts());
newFonts.AddRange(IFontFamilyId.ListGameFonts());
var systemFonts = IFontFamilyId.ListSystemFonts(!isFirst);
systemFonts.Sort(
(a, b) => string.Compare(
this.ExtractName(a),
this.ExtractName(b),
StringComparison.CurrentCultureIgnoreCase));
newFonts.AddRange(systemFonts);
if (this.FontFamilyExcludeFilter is not null)
newFonts.RemoveAll(this.FontFamilyExcludeFilter);
this.UpdateSelectedFamilyAndFontIndices(newFonts, familyName, fontName);
return newFonts;
}
}
if (this.useAdvancedOptions)
{
if (ImGui.Button("Reset"u8, buttonSize))
{
this.selectedFont = this.selectedFont with
{
LineHeight = 1f,
GlyphOffset = default,
LetterSpacing = default,
};
this.advUiState = new(this.selectedFont);
this.fontHandle?.Dispose();
this.fontHandle = null;
}
}
}
private void UpdateSelectedFamilyAndFontIndices(
IReadOnlyList<IFontFamilyId> fonts,
string familyName,
string fontName)
{
this.selectedFamilyIndex = fonts.FindIndex(x => x.ToString() == familyName);
if (this.selectedFamilyIndex == -1)
{
this.selectedFontIndex = -1;
}
else
{
this.selectedFontIndex = -1;
var family = fonts[this.selectedFamilyIndex];
for (var i = 0; i < family.Fonts.Count; i++)
{
if (family.Fonts[i].ToString() == fontName)
{
this.selectedFontIndex = i;
break;
}
}
if (this.selectedFontIndex == -1)
this.selectedFontIndex = 0;
this.selectedFont = this.selectedFont with
{
FontId = fonts[this.selectedFamilyIndex].Fonts[this.selectedFontIndex],
};
}
}
private string ExtractName(IObjectWithLocalizableName what) =>
what.GetLocalizedName(Service<DalamudConfiguration>.Get().EffectiveLanguage);
// Note: EffectiveLanguage can be incorrect but close enough for now
private bool TestName(IObjectWithLocalizableName what, string search) =>
this.ExtractName(what).Contains(search, StringComparison.CurrentCultureIgnoreCase);
private struct AdvancedOptionsUiState
{
public string OffsetXText;
public string OffsetYText;
public string LetterSpacingText;
public string LineHeightText;
public AdvancedOptionsUiState(SingleFontSpec spec)
{
this.OffsetXText = $"{spec.GlyphOffset.X:0.##}";
this.OffsetYText = $"{spec.GlyphOffset.Y:0.##}";
this.LetterSpacingText = $"{spec.LetterSpacing:0.##}";
this.LineHeightText = $"{spec.LineHeight:0.##}";
}
}
}