diff --git a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs index ca75e5ce0..9420fe42c 100644 --- a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs +++ b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs @@ -9,6 +9,7 @@ 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; @@ -84,11 +85,22 @@ public sealed class SingleFontChooserDialog : IDisposable private IFontHandle? fontHandle; private SingleFontSpec selectedFont; - /// - /// Initializes a new instance of the class. - /// + private bool popupPositionChanged; + private bool popupSizeChanged; + private Vector2 popupPosition = new(float.NaN); + private Vector2 popupSize = new(float.NaN); + + /// Initializes a new instance of the class. /// A new instance of created using /// as its auto-rebuild mode. + /// The passed instance of 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 for automatic + /// handling of font atlas derived from a , or even for automatic + /// registration and unregistration of event handler in addition to automatic disposal of this + /// class and the temporary font atlas for this font chooser dialog. + [Obsolete("See remarks, and use the other constructor.", false)] + [Api10ToDo("Make private.")] public SingleFontChooserDialog(IFontAtlas newAsyncAtlas) { this.counter = Interlocked.Increment(ref counterStatic); @@ -99,6 +111,39 @@ public sealed class SingleFontChooserDialog : IDisposable Encoding.UTF8.GetBytes("Font preview.\n0123456789!", this.fontPreviewText); } +#pragma warning disable CS0618 // Type or member is obsolete + // TODO: Api10ToDo; Remove this pragma warning disable line + + /// Initializes a new instance of the class. + /// The relevant instance of UiBuilder. + /// Whether the fonts in the atlas is global scaled. + /// Atlas name for debugging purposes. + /// + /// The passed is only used for creating a temporary font atlas. It will not + /// automatically register a hander for . + /// Consider using for automatic registration and unregistration of + /// event handler in addition to automatic disposal of this class and the temporary font atlas + /// for this font chooser dialog. + /// + public SingleFontChooserDialog(UiBuilder uiBuilder, bool isGlobalScaled = true, string? debugAtlasName = null) + : this(uiBuilder.CreateFontAtlas(FontAtlasAutoRebuildMode.Async, isGlobalScaled, debugAtlasName)) + { + } + + /// Initializes a new instance of the class. + /// An instance of . + /// The temporary atlas name. + internal SingleFontChooserDialog(FontAtlasFactory factory, string debugAtlasName) + : this(factory.CreateFontAtlas(debugAtlasName, FontAtlasAutoRebuildMode.Async)) + { + } + +#pragma warning restore CS0618 // Type or member is obsolete + // TODO: Api10ToDo; Remove this pragma warning restore line + + /// Called when the selected font spec has changed. + public event Action? SelectedFontSpecChanged; + /// /// Gets or sets the title of this font chooser dialog popup. /// @@ -153,6 +198,8 @@ public sealed class SingleFontChooserDialog : IDisposable this.useAdvancedOptions |= Math.Abs(value.LineHeight - 1f) > 0.000001; this.useAdvancedOptions |= value.GlyphOffset != default; this.useAdvancedOptions |= value.LetterSpacing != 0f; + + this.SelectedFontSpecChanged?.Invoke(this.selectedFont); } } @@ -166,15 +213,55 @@ public sealed class SingleFontChooserDialog : IDisposable /// public bool IgnorePreviewGlobalScale { get; set; } - /// - /// Creates a new instance of that will automatically draw and dispose itself as - /// needed. + /// Gets or sets a value indicating whether this popup should be modal, blocking everything behind from + /// being interacted. + /// If true, then will be + /// used. Otherwise, will be used. + public bool IsModal { get; set; } = true; + + /// Gets or sets the window flags. + public ImGuiWindowFlags WindowFlags { get; set; } + + /// Gets or sets the popup window position. + /// + /// Setting the position only works before the first call to . + /// If any of the coordinates are , default position will be used. + /// The position will be clamped into the work area of the selected monitor. + /// + public Vector2 PopupPosition + { + get => this.popupPosition; + set + { + this.popupPositionChanged = true; + this.popupPosition = value; + } + } + + /// Gets or sets the popup window size. + /// + /// Setting the size only works before the first call to . + /// If any of the coordinates are , default size will be used. + /// The size will be clamped into the work area of the selected monitor. + /// + public Vector2 PopupSize + { + get => this.popupSize; + set + { + this.popupSizeChanged = true; + this.popupSize = value; + } + } + + /// Creates a new instance of that will automatically draw and + /// dispose itself as needed; calling and are handled automatically. /// /// An instance of . /// The new instance of . public static SingleFontChooserDialog CreateAuto(UiBuilder uiBuilder) { - var fcd = new SingleFontChooserDialog(uiBuilder.CreateFontAtlas(FontAtlasAutoRebuildMode.Async)); + var fcd = new SingleFontChooserDialog(uiBuilder); uiBuilder.Draw += fcd.Draw; fcd.tcs.Task.ContinueWith( r => @@ -187,6 +274,14 @@ public sealed class SingleFontChooserDialog : IDisposable return fcd; } + /// Gets the default popup size before clamping to monitor work area. + /// The default popup size. + public static Vector2 GetDefaultPopupSizeNonClamped() + { + ThreadSafety.AssertMainThread(); + return new Vector2(40, 30) * ImGui.GetTextLineHeight(); + } + /// public void Dispose() { @@ -204,13 +299,28 @@ public sealed class SingleFontChooserDialog : IDisposable ImGui.GetIO().WantTextInput = false; } + /// Sets and to be at the center of the current window + /// being drawn. + /// The preferred popup size. + public void SetPopupPositionAndSizeToCurrentWindowCenter(Vector2 preferredPopupSize) + { + ThreadSafety.AssertMainThread(); + this.PopupSize = preferredPopupSize; + this.PopupPosition = ImGui.GetWindowPos() + ((ImGui.GetWindowSize() - preferredPopupSize) / 2); + } + + /// Sets and to be at the center of the current window + /// being drawn. + public void SetPopupPositionAndSizeToCurrentWindowCenter() => + this.SetPopupPositionAndSizeToCurrentWindowCenter(GetDefaultPopupSizeNonClamped()); + /// /// Draws this dialog. /// public void Draw() { - if (this.firstDraw) - ImGui.OpenPopup(this.popupImGuiName); + const float popupMinWidth = 320; + const float popupMinHeight = 240; ImGui.GetIO().WantCaptureKeyboard = true; ImGui.GetIO().WantTextInput = true; @@ -220,12 +330,70 @@ public sealed class SingleFontChooserDialog : IDisposable return; } - var open = true; - ImGui.SetNextWindowSize(new(640, 480), ImGuiCond.Appearing); - if (!ImGui.BeginPopupModal(this.popupImGuiName, ref open) || !open) + if (this.firstDraw) { - this.Cancel(); - return; + 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; @@ -261,12 +429,36 @@ public sealed class SingleFontChooserDialog : IDisposable ImGui.EndChild(); - ImGui.EndPopup(); + 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, ImGuiPlatformMonitorPtr 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(); @@ -338,15 +530,20 @@ public sealed class SingleFontChooserDialog : IDisposable } } - if (this.IgnorePreviewGlobalScale) + if (this.fontHandle is null) { - 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); + 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) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs index 8bb999557..469ef3dc3 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs @@ -44,6 +44,8 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable private bool useBold; private bool useMinimumBuild; + private SingleFontChooserDialog? chooserDialog; + /// public string[]? CommandShortcuts { get; init; } @@ -126,32 +128,75 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable if (ImGui.Button("Test Lock")) Task.Run(this.TestLock); - ImGui.SameLine(); if (ImGui.Button("Choose Editor Font")) { - var fcd = new SingleFontChooserDialog( - Service.Get().CreateFontAtlas( - $"{nameof(GamePrebakedFontsTestWidget)}:EditorFont", - FontAtlasAutoRebuildMode.Async)); - fcd.SelectedFont = this.fontSpec; - fcd.IgnorePreviewGlobalScale = !this.atlasScaleMode; - Service.Get().Draw += fcd.Draw; - fcd.ResultTask.ContinueWith( - r => Service.Get().RunOnFrameworkThread( - () => - { - Service.Get().Draw -= fcd.Draw; - fcd.Dispose(); + if (this.chooserDialog is null) + { + DoNext(); + } + else + { + this.chooserDialog.Cancel(); + this.chooserDialog.ResultTask.ContinueWith(_ => Service.Get().RunOnFrameworkThread(DoNext)); + this.chooserDialog = null; + } - _ = r.Exception; - if (!r.IsCompletedSuccessfully) - return; + void DoNext() + { + var fcd = new SingleFontChooserDialog( + Service.Get(), + $"{nameof(GamePrebakedFontsTestWidget)}:EditorFont"); + this.chooserDialog = fcd; + fcd.SelectedFont = this.fontSpec; + fcd.IgnorePreviewGlobalScale = !this.atlasScaleMode; + fcd.IsModal = false; + Service.Get().Draw += fcd.Draw; + var prevSpec = this.fontSpec; + fcd.SelectedFontSpecChanged += spec => + { + this.fontSpec = spec; + Log.Information("Selected font: {font}", this.fontSpec); + this.fontDialogHandle?.Dispose(); + this.fontDialogHandle = null; + }; + fcd.ResultTask.ContinueWith( + r => Service.Get().RunOnFrameworkThread( + () => + { + Service.Get().Draw -= fcd.Draw; + fcd.Dispose(); - this.fontSpec = r.Result; - Log.Information("Selected font: {font}", this.fontSpec); - this.fontDialogHandle?.Dispose(); - this.fontDialogHandle = null; - })); + _ = r.Exception; + var spec = r.IsCompletedSuccessfully ? r.Result : prevSpec; + if (this.fontSpec != spec) + { + this.fontSpec = spec; + this.fontDialogHandle?.Dispose(); + this.fontDialogHandle = null; + } + + this.chooserDialog = null; + })); + } + } + + if (this.chooserDialog is not null) + { + ImGui.SameLine(); + ImGui.TextUnformatted($"{this.chooserDialog.PopupPosition}, {this.chooserDialog.PopupSize}"); + + ImGui.SameLine(); + if (ImGui.Button("Random Location")) + { + var monitors = ImGui.GetPlatformIO().Monitors; + var monitor = monitors[Random.Shared.Next() % monitors.Size]; + this.chooserDialog.PopupPosition = monitor.WorkPos + (monitor.WorkSize * new Vector2( + Random.Shared.NextSingle(), + Random.Shared.NextSingle())); + this.chooserDialog.PopupSize = monitor.WorkSize * new Vector2( + Random.Shared.NextSingle(), + Random.Shared.NextSingle()); + } } this.privateAtlas ??= diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index ea6400121..5ccace850 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -12,7 +12,6 @@ using Dalamud.Interface.GameFonts; using Dalamud.Interface.ImGuiFontChooserDialog; using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.Settings.Widgets; -using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Utility; @@ -199,10 +198,10 @@ public class SettingsTabLook : SettingsTab if (ImGui.Button(Loc.Localize("DalamudSettingChooseDefaultFont", "Choose Default Font"))) { var faf = Service.Get(); - var fcd = new SingleFontChooserDialog( - faf.CreateFontAtlas($"{nameof(SettingsTabLook)}:Default", FontAtlasAutoRebuildMode.Async)); + var fcd = new SingleFontChooserDialog(faf, $"{nameof(SettingsTabLook)}:Default"); fcd.SelectedFont = (SingleFontSpec)this.defaultFontSpec; fcd.FontFamilyExcludeFilter = x => x is DalamudDefaultFontAndFamilyId; + fcd.SetPopupPositionAndSizeToCurrentWindowCenter(); interfaceManager.Draw += fcd.Draw; fcd.ResultTask.ContinueWith( r => Service.Get().RunOnFrameworkThread( diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs index 0445499c8..a79ab099d 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs @@ -82,21 +82,25 @@ public interface IFontAtlas : IDisposable /// public IDisposable SuppressAutoRebuild(); - /// - /// Creates a new from game's built-in fonts. - /// + /// Creates a new from game's built-in fonts. /// Font to use. /// Handle to a font that may or may not be ready yet. + /// This function does not throw. will be populated instead, if + /// the build procedure has failed. can be used regardless of the state of the font + /// handle. public IFontHandle NewGameFontHandle(GameFontStyle style); - /// - /// Creates a new IFontHandle using your own callbacks. - /// + /// Creates a new IFontHandle using your own callbacks. /// Callback for . /// Handle to a font that may or may not be ready yet. /// - /// Consider calling to support - /// glyphs that are not supplied by the game by default; this mostly affects Chinese and Korean language users. + /// Consider calling to + /// support glyphs that are not supplied by the game by default; this mostly affects Chinese and Korean language + /// users. + /// This function does not throw, even if would throw exceptions. + /// Instead, if it fails, the returned handle will contain an property + /// containing the exception happened during the build process. can be used even if + /// the build process has not been completed yet or failed. /// /// /// On initialization: diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index 70799bb9c..0a9e9072e 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -58,10 +58,27 @@ public interface IFontHandle : IDisposable /// A disposable object that will pop the font on dispose. /// If called outside of the main thread. /// - /// This function uses , and may do extra things. + /// This function uses , and may do extra things. /// Use or to undo this operation. - /// Do not use . + /// Do not use . /// + /// + /// Push a font with `using` clause. + /// + /// using (fontHandle.Push()) + /// ImGui.TextUnformatted("Test"); + /// + /// Push a font with a matching call to . + /// + /// fontHandle.Push(); + /// ImGui.TextUnformatted("Test 2"); + /// + /// Push a font between two choices. + /// + /// using ((someCondition ? myFontHandle : dalamudPluginInterface.UiBuilder.MonoFontHandle).Push()) + /// ImGui.TextUnformatted("Test 3"); + /// + /// IDisposable Push(); /// diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs index 47254a5c9..89d968158 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs @@ -136,13 +136,18 @@ internal abstract class FontHandle : IFontHandle /// An instance of that must be disposed after use on success; /// null with populated on failure. /// - /// Still may be thrown. public ILockedImFont? TryLock(out string? errorMessage) { IFontHandleSubstance? prevSubstance = default; while (true) { - var substance = this.Manager.Substance; + if (this.manager is not { } nonDisposedManager) + { + errorMessage = "The font handle has been disposed."; + return null; + } + + var substance = nonDisposedManager.Substance; // Does the associated IFontAtlas have a built substance? if (substance is null)