From 666feede4c444ede746399bd2e7c085a95ae2e56 Mon Sep 17 00:00:00 2001 From: srkizer Date: Thu, 14 Mar 2024 07:36:43 +0900 Subject: [PATCH 1/4] Suppress DAssetM dispose exceptions (#1707) Whether an asset being unavailable should be an error is decided on Dalamud startup time. This suppresses assets unavailable exceptions on Dispose. --- Dalamud/Storage/Assets/DalamudAssetManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index 69c7c32e8..68be78352 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -75,7 +75,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA .Where(x => x is not DalamudAsset.Empty4X4) .Where(x => x.GetAttribute()?.Required is false) .Select(this.CreateStreamAsync) - .Select(x => x.ToContentDisposedTask())) + .Select(x => x.ToContentDisposedTask(true))) .ContinueWith(r => Log.Verbose($"Optional assets load state: {r}")); } @@ -99,6 +99,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA .Concat(this.fileStreams.Values) .Concat(this.textureWraps.Values) .Where(x => x is not null) + .Select(x => x.ContinueWith(r => { _ = r.Exception; })) .ToArray()); this.scopedFinalizer.Dispose(); } From a26bb58fdbb79032b26b85488a04d49d0e40b8d0 Mon Sep 17 00:00:00 2001 From: srkizer Date: Thu, 14 Mar 2024 08:36:38 +0900 Subject: [PATCH 2/4] Use custom TaskScheduler for Framework.RunOnTick (#1597) * Use custom TaskScheduler for Framework.RunOnTick * TaskSchedulerWidget: add example --- Dalamud/Game/Framework.cs | 262 +++++++----------- .../Data/Widgets/TaskSchedulerWidget.cs | 157 ++++++++++- Dalamud/Plugin/Services/IFramework.cs | 15 + Dalamud/Utility/ThreadBoundTaskScheduler.cs | 90 ++++++ 4 files changed, 353 insertions(+), 171 deletions(-) create mode 100644 Dalamud/Utility/ThreadBoundTaskScheduler.cs diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index ce34f2c06..6520ca5c8 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -41,11 +42,13 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); - private readonly object runOnNextTickTaskListSync = new(); - private List runOnNextTickTaskList = new(); - private List runOnNextTickTaskList2 = new(); + private readonly CancellationTokenSource frameworkDestroy; + private readonly ThreadBoundTaskScheduler frameworkThreadTaskScheduler; - private Thread? frameworkUpdateThread; + private readonly ConcurrentDictionary + tickDelayedTaskCompletionSources = new(); + + private ulong tickCounter; [ServiceManager.ServiceConstructor] private Framework(TargetSigScanner sigScanner, GameLifecycle lifecycle) @@ -56,6 +59,14 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework this.addressResolver = new FrameworkAddressResolver(); this.addressResolver.Setup(sigScanner); + this.frameworkDestroy = new(); + this.frameworkThreadTaskScheduler = new(); + this.FrameworkThreadTaskFactory = new( + this.frameworkDestroy.Token, + TaskCreationOptions.None, + TaskContinuationOptions.None, + this.frameworkThreadTaskScheduler); + this.updateHook = Hook.FromAddress(this.addressResolver.TickAddress, this.HandleFrameworkUpdate); this.destroyHook = Hook.FromAddress(this.addressResolver.DestroyAddress, this.HandleFrameworkDestroy); @@ -92,14 +103,17 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework /// public DateTime LastUpdateUTC { get; private set; } = DateTime.MinValue; + /// + public TaskFactory FrameworkThreadTaskFactory { get; } + /// public TimeSpan UpdateDelta { get; private set; } = TimeSpan.Zero; /// - public bool IsInFrameworkUpdateThread => Thread.CurrentThread == this.frameworkUpdateThread; + public bool IsInFrameworkUpdateThread => this.frameworkThreadTaskScheduler.IsOnBoundThread; /// - public bool IsFrameworkUnloading { get; internal set; } + public bool IsFrameworkUnloading => this.frameworkDestroy.IsCancellationRequested; /// /// Gets the list of update sub-delegates that didn't get updated this frame. @@ -111,6 +125,19 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework /// internal bool DispatchUpdateEvents { get; set; } = true; + /// + public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default) + { + if (this.frameworkDestroy.IsCancellationRequested) + return Task.FromCanceled(this.frameworkDestroy.Token); + if (numTicks <= 0) + return Task.CompletedTask; + + var tcs = new TaskCompletionSource(); + this.tickDelayedTaskCompletionSources[tcs] = (this.tickCounter + (ulong)numTicks, cancellationToken); + return tcs.Task; + } + /// public Task RunOnFrameworkThread(Func func) => this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? Task.FromResult(func()) : this.RunOnTick(func); @@ -157,20 +184,16 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework return Task.FromCanceled(cts.Token); } - var tcs = new TaskCompletionSource(); - lock (this.runOnNextTickTaskListSync) - { - this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc() + if (cancellationToken == default) + cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; + return this.FrameworkThreadTaskFactory.ContinueWhenAll( + new[] { - RemainingTicks = delayTicks, - RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds), - CancellationToken = cancellationToken, - TaskCompletionSource = tcs, - Func = func, - }); - } - - return tcs.Task; + Task.Delay(delay, cancellationToken), + this.DelayTicks(delayTicks, cancellationToken), + }, + _ => func(), + cancellationToken); } /// @@ -186,20 +209,16 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework return Task.FromCanceled(cts.Token); } - var tcs = new TaskCompletionSource(); - lock (this.runOnNextTickTaskListSync) - { - this.runOnNextTickTaskList.Add(new RunOnNextTickTaskAction() + if (cancellationToken == default) + cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; + return this.FrameworkThreadTaskFactory.ContinueWhenAll( + new[] { - RemainingTicks = delayTicks, - RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds), - CancellationToken = cancellationToken, - TaskCompletionSource = tcs, - Action = action, - }); - } - - return tcs.Task; + Task.Delay(delay, cancellationToken), + this.DelayTicks(delayTicks, cancellationToken), + }, + _ => action(), + cancellationToken); } /// @@ -215,20 +234,16 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework return Task.FromCanceled(cts.Token); } - var tcs = new TaskCompletionSource>(); - lock (this.runOnNextTickTaskListSync) - { - this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc>() + if (cancellationToken == default) + cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; + return this.FrameworkThreadTaskFactory.ContinueWhenAll( + new[] { - RemainingTicks = delayTicks, - RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds), - CancellationToken = cancellationToken, - TaskCompletionSource = tcs, - Func = func, - }); - } - - return tcs.Task.ContinueWith(x => x.Result, cancellationToken).Unwrap(); + Task.Delay(delay, cancellationToken), + this.DelayTicks(delayTicks, cancellationToken), + }, + _ => func(), + cancellationToken).Unwrap(); } /// @@ -244,20 +259,16 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework return Task.FromCanceled(cts.Token); } - var tcs = new TaskCompletionSource(); - lock (this.runOnNextTickTaskListSync) - { - this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc() + if (cancellationToken == default) + cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; + return this.FrameworkThreadTaskFactory.ContinueWhenAll( + new[] { - RemainingTicks = delayTicks, - RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds), - CancellationToken = cancellationToken, - TaskCompletionSource = tcs, - Func = func, - }); - } - - return tcs.Task.ContinueWith(x => x.Result, cancellationToken).Unwrap(); + Task.Delay(delay, cancellationToken), + this.DelayTicks(delayTicks, cancellationToken), + }, + _ => func(), + cancellationToken).Unwrap(); } /// @@ -333,23 +344,9 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework } } - private void RunPendingTickTasks() - { - if (this.runOnNextTickTaskList.Count == 0 && this.runOnNextTickTaskList2.Count == 0) - return; - - for (var i = 0; i < 2; i++) - { - lock (this.runOnNextTickTaskListSync) - (this.runOnNextTickTaskList, this.runOnNextTickTaskList2) = (this.runOnNextTickTaskList2, this.runOnNextTickTaskList); - - this.runOnNextTickTaskList2.RemoveAll(x => x.Run()); - } - } - private bool HandleFrameworkUpdate(IntPtr framework) { - this.frameworkUpdateThread ??= Thread.CurrentThread; + this.frameworkThreadTaskScheduler.BoundThread ??= Thread.CurrentThread; ThreadSafety.MarkMainThread(); @@ -381,18 +378,30 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework this.LastUpdate = DateTime.Now; this.LastUpdateUTC = DateTime.UtcNow; + this.tickCounter++; + foreach (var (k, (expiry, ct)) in this.tickDelayedTaskCompletionSources) + { + if (ct.IsCancellationRequested) + k.SetCanceled(ct); + else if (expiry <= this.tickCounter) + k.SetResult(); + else + continue; + + this.tickDelayedTaskCompletionSources.Remove(k, out _); + } if (StatsEnabled) { StatsStopwatch.Restart(); - this.RunPendingTickTasks(); + this.frameworkThreadTaskScheduler.Run(); StatsStopwatch.Stop(); - AddToStats(nameof(this.RunPendingTickTasks), StatsStopwatch.Elapsed.TotalMilliseconds); + AddToStats(nameof(this.frameworkThreadTaskScheduler), StatsStopwatch.Elapsed.TotalMilliseconds); } else { - this.RunPendingTickTasks(); + this.frameworkThreadTaskScheduler.Run(); } if (StatsEnabled && this.Update != null) @@ -404,7 +413,7 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework // Cleanup handlers that are no longer being called foreach (var key in this.NonUpdatedSubDelegates) { - if (key == nameof(this.RunPendingTickTasks)) + if (key == nameof(this.FrameworkThreadTaskFactory)) continue; if (StatsHistory[key].Count > 0) @@ -431,8 +440,11 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework private bool HandleFrameworkDestroy(IntPtr framework) { - this.IsFrameworkUnloading = true; + this.frameworkDestroy.Cancel(); this.DispatchUpdateEvents = false; + foreach (var k in this.tickDelayedTaskCompletionSources.Keys) + k.SetCanceled(this.frameworkDestroy.Token); + this.tickDelayedTaskCompletionSources.Clear(); // All the same, for now... this.lifecycle.SetShuttingDown(); @@ -440,95 +452,12 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework Log.Information("Framework::Destroy!"); Service.Get().Unload(); - this.RunPendingTickTasks(); + this.frameworkThreadTaskScheduler.Run(); ServiceManager.WaitForServiceUnload(); Log.Information("Framework::Destroy OK!"); return this.destroyHook.OriginalDisposeSafe(framework); } - - private abstract class RunOnNextTickTaskBase - { - internal int RemainingTicks { get; set; } - - internal long RunAfterTickCount { get; init; } - - internal CancellationToken CancellationToken { get; init; } - - internal bool Run() - { - if (this.CancellationToken.IsCancellationRequested) - { - this.CancelImpl(); - return true; - } - - if (this.RemainingTicks > 0) - this.RemainingTicks -= 1; - if (this.RemainingTicks > 0) - return false; - - if (this.RunAfterTickCount > Environment.TickCount64) - return false; - - this.RunImpl(); - - return true; - } - - protected abstract void RunImpl(); - - protected abstract void CancelImpl(); - } - - private class RunOnNextTickTaskFunc : RunOnNextTickTaskBase - { - internal TaskCompletionSource TaskCompletionSource { get; init; } - - internal Func Func { get; init; } - - protected override void RunImpl() - { - try - { - this.TaskCompletionSource.SetResult(this.Func()); - } - catch (Exception ex) - { - this.TaskCompletionSource.SetException(ex); - } - } - - protected override void CancelImpl() - { - this.TaskCompletionSource.SetCanceled(); - } - } - - private class RunOnNextTickTaskAction : RunOnNextTickTaskBase - { - internal TaskCompletionSource TaskCompletionSource { get; init; } - - internal Action Action { get; init; } - - protected override void RunImpl() - { - try - { - this.Action(); - this.TaskCompletionSource.SetResult(); - } - catch (Exception ex) - { - this.TaskCompletionSource.SetException(ex); - } - } - - protected override void CancelImpl() - { - this.TaskCompletionSource.SetCanceled(); - } - } } /// @@ -561,7 +490,10 @@ internal class FrameworkPluginScoped : IDisposable, IServiceType, IFramework /// public DateTime LastUpdateUTC => this.frameworkService.LastUpdateUTC; - + + /// + public TaskFactory FrameworkThreadTaskFactory => this.frameworkService.FrameworkThreadTaskFactory; + /// public TimeSpan UpdateDelta => this.frameworkService.UpdateDelta; @@ -579,6 +511,10 @@ internal class FrameworkPluginScoped : IDisposable, IServiceType, IFramework this.Update = null; } + /// + public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default) => + this.frameworkService.DelayTicks(numTicks, cancellationToken); + /// public Task RunOnFrameworkThread(Func func) => this.frameworkService.RunOnFrameworkThread(func); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs index d1ac51ad5..c6d8c4e8b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs @@ -1,13 +1,22 @@ // ReSharper disable MethodSupportsCancellation // Using alternative method of cancelling tasks by throwing exceptions. +using System.IO; +using System.Linq; +using System.Net.Http; using System.Reflection; +using System.Text; using System.Threading; using System.Threading.Tasks; using Dalamud.Game; using Dalamud.Interface.Colors; +using Dalamud.Interface.Components; +using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Logging.Internal; +using Dalamud.Utility; + using ImGuiNET; using Serilog; @@ -18,6 +27,12 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class TaskSchedulerWidget : IDataWindowWidget { + private readonly FileDialogManager fileDialogManager = new(); + private readonly byte[] urlBytes = new byte[2048]; + private readonly byte[] localPathBytes = new byte[2048]; + + private Task? downloadTask = null; + private (long Downloaded, long Total, float Percentage) downloadState; private CancellationTokenSource taskSchedulerCancelSource = new(); /// @@ -33,11 +48,16 @@ internal class TaskSchedulerWidget : IDataWindowWidget public void Load() { this.Ready = true; + Encoding.UTF8.GetBytes( + "https://geo.mirror.pkgbuild.com/iso/2024.01.01/archlinux-2024.01.01-x86_64.iso", + this.urlBytes); } /// public void Draw() { + var framework = Service.Get(); + if (ImGui.Button("Clear list")) { TaskTracker.Clear(); @@ -84,8 +104,7 @@ internal class TaskSchedulerWidget : IDataWindowWidget { Thread.Sleep(200); - string a = null; - a.Contains("dalamud"); // Intentional null exception. + _ = ((string)null)!.Contains("dalamud"); // Intentional null exception. }); } @@ -94,36 +113,156 @@ internal class TaskSchedulerWidget : IDataWindowWidget if (ImGui.Button("ASAP")) { - Task.Run(async () => await Service.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedulerCancelSource.Token)); + _ = framework.RunOnTick(() => Log.Information("Framework.Update - ASAP"), cancellationToken: this.taskSchedulerCancelSource.Token); } ImGui.SameLine(); if (ImGui.Button("In 1s")) { - Task.Run(async () => await Service.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(1))); + _ = framework.RunOnTick(() => Log.Information("Framework.Update - In 1s"), cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(1)); } ImGui.SameLine(); if (ImGui.Button("In 60f")) { - Task.Run(async () => await Service.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedulerCancelSource.Token, delayTicks: 60)); + _ = framework.RunOnTick(() => Log.Information("Framework.Update - In 60f"), cancellationToken: this.taskSchedulerCancelSource.Token, delayTicks: 60); + } + + ImGui.SameLine(); + + if (ImGui.Button("In 1s+120f")) + { + _ = framework.RunOnTick(() => Log.Information("Framework.Update - In 1s+120f"), cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(1), delayTicks: 120); + } + + ImGui.SameLine(); + + if (ImGui.Button("In 2s+60f")) + { + _ = framework.RunOnTick(() => Log.Information("Framework.Update - In 2s+60f"), cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(2), delayTicks: 60); + } + + ImGui.SameLine(); + + if (ImGui.Button("Every 60 frames")) + { + _ = framework.RunOnTick( + async () => + { + for (var i = 0L; ; i++) + { + Log.Information($"Loop #{i}; MainThread={ThreadSafety.IsMainThread}"); + await framework.DelayTicks(60, this.taskSchedulerCancelSource.Token); + } + }, + cancellationToken: this.taskSchedulerCancelSource.Token); } ImGui.SameLine(); if (ImGui.Button("Error in 1s")) { - Task.Run(async () => await Service.Get().RunOnTick(() => throw new Exception("Test Exception"), cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(1))); + _ = framework.RunOnTick(() => throw new Exception("Test Exception"), cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(1)); } ImGui.SameLine(); if (ImGui.Button("As long as it's in Framework Thread")) { - Task.Run(async () => await Service.Get().RunOnFrameworkThread(() => { Log.Information("Task dispatched from non-framework.update thread"); })); - Service.Get().RunOnFrameworkThread(() => { Log.Information("Task dispatched from framework.update thread"); }).Wait(); + Task.Run(async () => await framework.RunOnFrameworkThread(() => { Log.Information("Task dispatched from non-framework.update thread"); })); + framework.RunOnFrameworkThread(() => { Log.Information("Task dispatched from framework.update thread"); }).Wait(); + } + + if (ImGui.CollapsingHeader("Download")) + { + ImGui.InputText("URL", this.urlBytes, (uint)this.urlBytes.Length); + ImGui.InputText("Local Path", this.localPathBytes, (uint)this.localPathBytes.Length); + ImGui.SameLine(); + + if (ImGuiComponents.IconButton("##localpathpicker", FontAwesomeIcon.File)) + { + var defaultFileName = Encoding.UTF8.GetString(this.urlBytes).Split('\0', 2)[0].Split('/').Last(); + this.fileDialogManager.SaveFileDialog( + "Choose a local path", + "*", + defaultFileName, + string.Empty, + (accept, newPath) => + { + if (accept) + { + this.localPathBytes.AsSpan().Clear(); + Encoding.UTF8.GetBytes(newPath, this.localPathBytes.AsSpan()); + } + }); + } + + ImGui.TextUnformatted($"{this.downloadState.Downloaded:##,###}/{this.downloadState.Total:##,###} ({this.downloadState.Percentage:0.00}%)"); + + using var disabled = + ImRaii.Disabled(this.downloadTask?.IsCompleted is false || this.localPathBytes[0] == 0); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Download"); + ImGui.SameLine(); + var downloadUsingGlobalScheduler = ImGui.Button("using default scheduler"); + ImGui.SameLine(); + var downloadUsingFramework = ImGui.Button("using Framework.Update"); + if (downloadUsingGlobalScheduler || downloadUsingFramework) + { + var url = Encoding.UTF8.GetString(this.urlBytes).Split('\0', 2)[0]; + var localPath = Encoding.UTF8.GetString(this.localPathBytes).Split('\0', 2)[0]; + var ct = this.taskSchedulerCancelSource.Token; + this.downloadState = default; + var factory = downloadUsingGlobalScheduler + ? Task.Factory + : framework.FrameworkThreadTaskFactory; + this.downloadState = default; + this.downloadTask = factory.StartNew( + async () => + { + try + { + await using var to = File.Create(localPath); + using var client = new HttpClient(); + using var conn = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct); + this.downloadState.Total = conn.Content.Headers.ContentLength ?? -1L; + await using var from = conn.Content.ReadAsStream(ct); + var buffer = new byte[8192]; + while (true) + { + if (downloadUsingFramework) + ThreadSafety.AssertMainThread(); + if (downloadUsingGlobalScheduler) + ThreadSafety.AssertNotMainThread(); + var len = await from.ReadAsync(buffer, ct); + if (len == 0) + break; + await to.WriteAsync(buffer.AsMemory(0, len), ct); + this.downloadState.Downloaded += len; + if (this.downloadState.Total >= 0) + { + this.downloadState.Percentage = + (100f * this.downloadState.Downloaded) / this.downloadState.Total; + } + } + } + catch (Exception e) + { + Log.Error(e, "Failed to download {from} to {to}.", url, localPath); + try + { + File.Delete(localPath); + } + catch + { + // ignore + } + } + }, + cancellationToken: ct).Unwrap(); + } } if (ImGui.Button("Drown in tasks")) @@ -244,6 +383,8 @@ internal class TaskSchedulerWidget : IDataWindowWidget ImGui.PopStyleColor(1); } + + this.fileDialogManager.Draw(); } private async Task TestTaskInTaskDelay(CancellationToken token) diff --git a/Dalamud/Plugin/Services/IFramework.cs b/Dalamud/Plugin/Services/IFramework.cs index ca33c5867..a93abd252 100644 --- a/Dalamud/Plugin/Services/IFramework.cs +++ b/Dalamud/Plugin/Services/IFramework.cs @@ -29,6 +29,11 @@ public interface IFramework /// public DateTime LastUpdateUTC { get; } + /// + /// Gets a that runs tasks during Framework Update event. + /// + public TaskFactory FrameworkThreadTaskFactory { get; } + /// /// Gets the delta between the last Framework Update and the currently executing one. /// @@ -44,6 +49,14 @@ public interface IFramework /// public bool IsFrameworkUnloading { get; } + /// + /// Returns a task that completes after the given number of ticks. + /// + /// Number of ticks to delay. + /// The cancellation token. + /// A new that gets resolved after specified number of ticks happen. + public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default); + /// /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. /// @@ -65,6 +78,7 @@ public interface IFramework /// Return type. /// Function to call. /// Task representing the pending or already completed function. + [Obsolete($"Use {nameof(RunOnTick)} instead.")] public Task RunOnFrameworkThread(Func> func); /// @@ -72,6 +86,7 @@ public interface IFramework /// /// Function to call. /// Task representing the pending or already completed function. + [Obsolete($"Use {nameof(RunOnTick)} instead.")] public Task RunOnFrameworkThread(Func func); /// diff --git a/Dalamud/Utility/ThreadBoundTaskScheduler.cs b/Dalamud/Utility/ThreadBoundTaskScheduler.cs new file mode 100644 index 000000000..4b6de29ff --- /dev/null +++ b/Dalamud/Utility/ThreadBoundTaskScheduler.cs @@ -0,0 +1,90 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Dalamud.Utility; + +/// +/// A task scheduler that runs tasks on a specific thread. +/// +internal class ThreadBoundTaskScheduler : TaskScheduler +{ + private const byte Scheduled = 0; + private const byte Running = 1; + + private readonly ConcurrentDictionary scheduledTasks = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The thread to bind this task scheduelr to. + public ThreadBoundTaskScheduler(Thread? boundThread = null) + { + this.BoundThread = boundThread; + } + + /// + /// Gets or sets the thread this task scheduler is bound to. + /// + public Thread? BoundThread { get; set; } + + /// + /// Gets a value indicating whether we're on the bound thread. + /// + public bool IsOnBoundThread => Thread.CurrentThread == this.BoundThread; + + /// + /// Runs queued tasks. + /// + public void Run() + { + foreach (var task in this.scheduledTasks.Keys) + { + if (!this.scheduledTasks.TryUpdate(task, Running, Scheduled)) + continue; + + _ = this.TryExecuteTask(task); + } + } + + /// + protected override IEnumerable GetScheduledTasks() + { + return this.scheduledTasks.Keys; + } + + /// + protected override void QueueTask(Task task) + { + this.scheduledTasks[task] = Scheduled; + } + + /// + protected override bool TryDequeue(Task task) + { + if (!this.scheduledTasks.TryRemove(task, out _)) + return false; + return true; + } + + /// + protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) + { + if (!this.IsOnBoundThread) + return false; + + if (taskWasPreviouslyQueued && !this.scheduledTasks.TryUpdate(task, Running, Scheduled)) + return false; + + _ = this.TryExecuteTask(task); + return true; + } + + private new bool TryExecuteTask(Task task) + { + var r = base.TryExecuteTask(task); + this.scheduledTasks.Remove(task, out _); + return r; + } +} From cf4a9e305597501d43722139dfba671ad0d06e77 Mon Sep 17 00:00:00 2001 From: srkizer Date: Thu, 14 Mar 2024 08:57:30 +0900 Subject: [PATCH 3/4] Easier SingleFontChooserDialog ctor, window pos/size/flags, and more docs (#1704) * Make SingleFontChooserDialog ctor less confusing The current constructor expects a new fresh instance of IFontAtlas, which can be easy to miss, resulting in wasted time troubleshooting without enough clues. New constructor is added that directly takes an instance of UiBuilder, and the old constructor has been obsoleted and should be changed to private on api 10. * Add position, size, and window flags conf to SFCD * Improve documentations * Add test for PopupPosition/Size --------- Co-authored-by: goat <16760685+goaaats@users.noreply.github.com> --- .../SingleFontChooserDialog.cs | 243 ++++++++++++++++-- .../Widgets/GamePrebakedFontsTestWidget.cs | 89 +++++-- .../Windows/Settings/Tabs/SettingsTabLook.cs | 5 +- .../Interface/ManagedFontAtlas/IFontAtlas.cs | 20 +- .../Interface/ManagedFontAtlas/IFontHandle.cs | 21 +- .../ManagedFontAtlas/Internals/FontHandle.cs | 9 +- 6 files changed, 327 insertions(+), 60 deletions(-) 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) From 4c18f77f51a2af2de52f8dbb1d262d6c7798f474 Mon Sep 17 00:00:00 2001 From: srkizer Date: Thu, 14 Mar 2024 13:35:14 +0900 Subject: [PATCH 4/4] Fix log window layout (#1706) * Fix log window layout Fixed custom line rendering from not advancing ImGui cursor, and move input boxes around as log window is resized to become narower. * Undo unused change --- .../Internal/Windows/ConsoleWindow.cs | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 1957ab720..0c9c90d0d 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -90,11 +90,6 @@ internal class ConsoleWindow : Window, IDisposable this.Size = new Vector2(500, 400); this.SizeCondition = ImGuiCond.FirstUseEver; - this.SizeConstraints = new WindowSizeConstraints - { - MinimumSize = new Vector2(600.0f, 200.0f), - }; - this.RespectCloseHotkey = false; this.logLinesLimit = configuration.LogLinesLimit; @@ -555,10 +550,24 @@ internal class ConsoleWindow : Window, IDisposable if (ImGui.IsItemHovered()) ImGui.SetTooltip("Kill game"); ImGui.SameLine(); - ImGui.SetCursorPosX( - ImGui.GetContentRegionMax().X - (2 * 200.0f * ImGuiHelpers.GlobalScale) - ImGui.GetStyle().ItemSpacing.X); - ImGui.PushItemWidth(200.0f * ImGuiHelpers.GlobalScale); + var inputWidth = 200.0f * ImGuiHelpers.GlobalScale; + var nextCursorPosX = ImGui.GetContentRegionMax().X - (2 * inputWidth) - ImGui.GetStyle().ItemSpacing.X; + var breakInputLines = nextCursorPosX < 0; + if (ImGui.GetCursorPosX() > nextCursorPosX) + { + ImGui.NewLine(); + inputWidth = ImGui.GetWindowWidth() - (ImGui.GetStyle().WindowPadding.X * 2); + + if (!breakInputLines) + inputWidth = (inputWidth - ImGui.GetStyle().ItemSpacing.X) / 2; + } + else + { + ImGui.SetCursorPosX(nextCursorPosX); + } + + ImGui.PushItemWidth(inputWidth); if (ImGui.InputTextWithHint( "##textHighlight", "regex highlight", @@ -583,8 +592,10 @@ internal class ConsoleWindow : Window, IDisposable log.HighlightMatches = null; } - ImGui.SameLine(); - ImGui.PushItemWidth(200.0f * ImGuiHelpers.GlobalScale); + if (!breakInputLines) + ImGui.SameLine(); + + ImGui.PushItemWidth(inputWidth); if (ImGui.InputTextWithHint( "##textFilter", "regex global filter", @@ -1082,6 +1093,8 @@ internal class ConsoleWindow : Window, IDisposable lastc = currc; } } + + ImGui.Dummy(screenPos - ImGui.GetCursorScreenPos()); } private record LogEntry