From a7d53807961e1df5939a536011d7b4d7cd4b2ed5 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 27 Feb 2024 23:20:08 +0900 Subject: [PATCH] Cleanup --- .../ImGuiNotification/IActiveNotification.cs | 38 +- .../ImGuiNotification/INotification.cs | 25 +- .../ImGuiNotification/INotificationIcon.cs | 54 ++ .../INotificationIconSource.cs | 88 -- .../INotificationMaterializedIcon.cs | 16 - .../Internal/ActiveNotification.ImGui.cs | 494 +++++++++++ .../Internal/ActiveNotification.cs | 818 +++--------------- .../Internal/IconSource/FilePathIconSource.cs | 49 -- .../IconSource/FontAwesomeIconIconSource.cs | 46 - .../Internal/IconSource/GamePathIconSource.cs | 50 -- .../IconSource/SeIconCharIconSource.cs | 47 - .../IconSource/TextureWrapIconSource.cs | 62 -- .../IconSource/TextureWrapTaskIconSource.cs | 71 -- .../{ => Internal}/NotificationConstants.cs | 122 ++- .../FilePathNotificationIcon.cs | 34 + .../FontAwesomeIconNotificationIcon.cs | 31 + .../GamePathNotificationIcon.cs | 34 + .../SeIconCharNotificationIcon.cs | 33 + .../Internal/NotificationManager.cs | 39 +- .../ImGuiNotification/Notification.cs | 61 +- .../NotificationUtilities.cs | 156 ++-- .../Windows/Data/Widgets/ImGuiWidget.cs | 83 +- Dalamud/Interface/UiBuilder.cs | 1 - .../Plugin/Services/INotificationManager.cs | 16 +- 24 files changed, 1056 insertions(+), 1412 deletions(-) create mode 100644 Dalamud/Interface/ImGuiNotification/INotificationIcon.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs create mode 100644 Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/Internal/IconSource/FilePathIconSource.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/Internal/IconSource/GamePathIconSource.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapIconSource.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapTaskIconSource.cs rename Dalamud/Interface/ImGuiNotification/{ => Internal}/NotificationConstants.cs (51%) create mode 100644 Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FilePathNotificationIcon.cs create mode 100644 Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FontAwesomeIconNotificationIcon.cs create mode 100644 Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs create mode 100644 Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/SeIconCharNotificationIcon.cs diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index dd4101c92..340c052cd 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -1,5 +1,7 @@ using System.Threading; +using Dalamud.Interface.Internal; + namespace Dalamud.Interface.ImGuiNotification; /// Represents an active notification. @@ -20,20 +22,6 @@ public interface IActiveNotification : INotification /// event Action Click; - /// Invoked when the mouse enters the notification window. - /// - /// Note that this function may be called even after has been invoked. - /// Refer to . - /// - event Action MouseEnter; - - /// Invoked when the mouse leaves the notification window. - /// - /// Note that this function may be called even after has been invoked. - /// Refer to . - /// - event Action MouseLeave; - /// Invoked upon drawing the action bar of the notification. /// /// Note that this function may be called even after has been invoked. @@ -44,16 +32,13 @@ public interface IActiveNotification : INotification /// Gets the ID of this notification. long Id { get; } + /// Gets the time of creating this notification. + DateTime CreatedAt { get; } + /// Gets the effective expiry time. /// Contains if the notification does not expire. DateTime EffectiveExpiry { get; } - /// Gets a value indicating whether the mouse cursor is on the notification window. - bool IsHovered { get; } - - /// Gets a value indicating whether the notification window is focused. - bool IsFocused { get; } - /// Gets a value indicating whether the notification has been dismissed. /// This includes when the hide animation is being played. bool IsDismissed { get; } @@ -66,9 +51,16 @@ public interface IActiveNotification : INotification /// This does not override . void ExtendBy(TimeSpan extension); - /// Loads the icon again using the same . - /// If is true, then this function is a no-op. - void UpdateIcon(); + /// Sets the icon from , overriding the icon . + /// The new texture wrap to use, or null to clear and revert back to the icon specified + /// from . + /// + /// The texture passed will be disposed when the notification is dismissed or a new different texture is set + /// via another call to this function. You do not have to dispose it yourself. + /// If is true, then calling this function will simply dispose the passed + /// without actually updating the icon. + /// + void SetIconTexture(IDalamudTextureWrap? textureWrap); /// Generates a new value to use for . /// The new value. diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index 349d66f72..e6861726f 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -1,9 +1,10 @@ using Dalamud.Interface.Internal.Notifications; +using Dalamud.Plugin.Services; namespace Dalamud.Interface.ImGuiNotification; /// Represents a notification. -public interface INotification : IDisposable +public interface INotification { /// Gets or sets the content body of the notification. string Content { get; set; } @@ -18,22 +19,13 @@ public interface INotification : IDisposable NotificationType Type { get; set; } /// Gets or sets the icon source. - /// - /// Assigning a new value that does not equal to the previous value will dispose the old value. The ownership - /// of the new value is transferred to this . Even if the assignment throws an - /// exception, the ownership is transferred, causing the value to be disposed. Assignment should not throw an - /// exception though, so wrapping the assignment in try...catch block is not required. - /// The assigned value will be disposed upon the call on this instance of - /// , unless the same value is assigned, in which case it will do nothing. - /// If this is an , then updating this property - /// will change the icon being displayed (calls ), unless - /// is true. - /// - INotificationIconSource? IconSource { get; set; } + /// Use to use a texture, after calling + /// . + INotificationIcon? Icon { get; set; } /// Gets or sets the hard expiry. /// - /// Setting this value will override and , in that + /// Setting this value will override and , in that /// the notification will be dismissed when this expiry expires.
/// Set to to make only take effect.
/// If neither nor is not MaxValue, then the notification @@ -45,7 +37,8 @@ public interface INotification : IDisposable /// Gets or sets the initial duration. /// Set to to make only take effect. - /// Updating this value will reset the dismiss timer. + /// Updating this value will reset the dismiss timer, but the remaining duration will still be calculated + /// based on . TimeSpan InitialDuration { get; set; } /// Gets or sets the new duration for this notification once the mouse cursor leaves the window and the @@ -55,7 +48,7 @@ public interface INotification : IDisposable /// notification or focusing on it will not make the notification stay.
/// Updating this value will reset the dismiss timer. /// - TimeSpan DurationSinceLastInterest { get; set; } + TimeSpan ExtensionDurationSinceLastInterest { get; set; } /// Gets or sets a value indicating whether to show an indeterminate expiration animation if /// is set to . diff --git a/Dalamud/Interface/ImGuiNotification/INotificationIcon.cs b/Dalamud/Interface/ImGuiNotification/INotificationIcon.cs new file mode 100644 index 000000000..94c746b4f --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/INotificationIcon.cs @@ -0,0 +1,54 @@ +using System.Numerics; +using System.Runtime.CompilerServices; + +using Dalamud.Game.Text; +using Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon; + +namespace Dalamud.Interface.ImGuiNotification; + +/// Icon source for . +/// Plugins implementing this interface are left to their own on managing the resources contained by the +/// instance of their implementation of . In other words, they should not expect to have +/// called if their implementation is an . Dalamud will not +/// call on any instance of . On plugin unloads, the +/// icon may be reverted back to the default, if the instance of is not provided by +/// Dalamud. +public interface INotificationIcon +{ + /// Gets a new instance of that will source the icon from an + /// . + /// The icon character. + /// A new instance of that should be disposed after use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIcon From(SeIconChar iconChar) => new SeIconCharNotificationIcon(iconChar); + + /// Gets a new instance of that will source the icon from an + /// . + /// The icon character. + /// A new instance of that should be disposed after use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIcon From(FontAwesomeIcon iconChar) => new FontAwesomeIconNotificationIcon(iconChar); + + /// Gets a new instance of that will source the icon from a texture + /// file shipped as a part of the game resources. + /// The path to a texture file in the game virtual file system. + /// A new instance of that should be disposed after use. + /// If any errors are thrown, the default icon will be displayed instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIcon FromGame(string gamePath) => new GamePathNotificationIcon(gamePath); + + /// Gets a new instance of that will source the icon from an image + /// file from the file system. + /// The path to an image file in the file system. + /// A new instance of that should be disposed after use. + /// If any errors are thrown, the default icon will be displayed instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIcon FromFile(string filePath) => new FilePathNotificationIcon(filePath); + + /// Draws the icon. + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + /// The foreground color. + /// true if anything has been drawn. + bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color); +} diff --git a/Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs b/Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs deleted file mode 100644 index 1fee67098..000000000 --- a/Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -using Dalamud.Game.Text; -using Dalamud.Interface.ImGuiNotification.Internal.IconSource; -using Dalamud.Interface.Internal; - -namespace Dalamud.Interface.ImGuiNotification; - -/// Icon source for . -/// Plugins should NOT implement this interface. -public interface INotificationIconSource : ICloneable, IDisposable -{ - /// The internal interface. - internal interface IInternal : INotificationIconSource - { - /// Materializes the icon resource. - /// The materialized resource. - INotificationMaterializedIcon Materialize(); - } - - /// Gets a new instance of that will source the icon from an - /// . - /// The icon character. - /// A new instance of that should be disposed after use. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource From(SeIconChar iconChar) => new SeIconCharIconSource(iconChar); - - /// Gets a new instance of that will source the icon from an - /// . - /// The icon character. - /// A new instance of that should be disposed after use. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource From(FontAwesomeIcon iconChar) => new FontAwesomeIconIconSource(iconChar); - - /// Gets a new instance of that will source the icon from an - /// . - /// The texture wrap. - /// - /// If true, this class will own the passed , and you must not call - /// on the passed wrap. - /// If false, this class will create a new reference of the passed wrap, and you should call - /// on the passed wrap. - /// In both cases, the returned object must be disposed after use. - /// A new instance of that should be disposed after use. - /// If any errors are thrown or is null, the default icon will be displayed - /// instead. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource From(IDalamudTextureWrap? wrap, bool takeOwnership = true) => - new TextureWrapIconSource(wrap, takeOwnership); - - /// Gets a new instance of that will source the icon from an - /// returning a resulting in an - /// . - /// The function that returns a task that results a texture wrap. - /// A new instance of that should be disposed after use. - /// If any errors are thrown or is null, the default icon will be - /// displayed instead.
- /// Use if you will have a wrap available without waiting.
- /// should not contain a reference to a resource; if it does, the resource will be - /// released when all instances of derived from the returned object are freed - /// by the garbage collector, which will result in non-deterministic resource releases.
- [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource From(Func?>? wrapTaskFunc) => - new TextureWrapTaskIconSource(wrapTaskFunc); - - /// Gets a new instance of that will source the icon from a texture - /// file shipped as a part of the game resources. - /// The path to a texture file in the game virtual file system. - /// A new instance of that should be disposed after use. - /// If any errors are thrown, the default icon will be displayed instead. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource FromGame(string gamePath) => new GamePathIconSource(gamePath); - - /// Gets a new instance of that will source the icon from an image - /// file from the file system. - /// The path to an image file in the file system. - /// A new instance of that should be disposed after use. - /// If any errors are thrown, the default icon will be displayed instead. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource FromFile(string filePath) => new FilePathIconSource(filePath); - - /// - new INotificationIconSource Clone(); - - /// - object ICloneable.Clone() => this.Clone(); -} diff --git a/Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs b/Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs deleted file mode 100644 index 0657a94a4..000000000 --- a/Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Numerics; - -using Dalamud.Plugin.Internal.Types; - -namespace Dalamud.Interface.ImGuiNotification; - -/// Represents a materialized icon. -internal interface INotificationMaterializedIcon : IDisposable -{ - /// Draws the icon. - /// The coordinates of the top left of the icon area. - /// The coordinates of the bottom right of the icon area. - /// The foreground color. - /// The initiator plugin. - void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin); -} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs new file mode 100644 index 000000000..99b924923 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs @@ -0,0 +1,494 @@ +using System.Numerics; + +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ImGuiNotification.Internal; + +/// Represents an active notification. +internal sealed partial class ActiveNotification +{ + /// Draws this notification. + /// The maximum width of the notification window. + /// The offset from the bottom. + /// The height of the notification. + public float Draw(float width, float offsetY) + { + var opacity = + Math.Clamp( + (float)(this.hideEasing.IsRunning + ? (this.hideEasing.IsDone ? 0 : 1f - this.hideEasing.Value) + : (this.showEasing.IsDone ? 1 : this.showEasing.Value)), + 0f, + 1f); + if (opacity <= 0) + return 0; + + var actionWindowHeight = + // Content + ImGui.GetTextLineHeight() + + // Top and bottom padding + (NotificationConstants.ScaledWindowPadding * 2); + + var viewport = ImGuiHelpers.MainViewport; + var viewportPos = viewport.WorkPos; + var viewportSize = viewport.WorkSize; + + ImGui.PushID(this.Id.GetHashCode()); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity); + ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f); + ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(NotificationConstants.ScaledWindowPadding)); + unsafe + { + ImGui.PushStyleColor( + ImGuiCol.WindowBg, + *ImGui.GetStyleColorVec4(ImGuiCol.WindowBg) * new Vector4( + 1f, + 1f, + 1f, + NotificationConstants.BackgroundOpacity)); + } + + ImGuiHelpers.ForceNextWindowMainViewport(); + ImGui.SetNextWindowPos( + (viewportPos + viewportSize) - + new Vector2(NotificationConstants.ScaledViewportEdgeMargin) - + new Vector2(0, offsetY), + ImGuiCond.Always, + Vector2.One); + ImGui.SetNextWindowSizeConstraints( + new(width, actionWindowHeight), + new( + width, + !this.underlyingNotification.Minimized || this.expandoEasing.IsRunning + ? float.MaxValue + : actionWindowHeight)); + ImGui.Begin( + $"##NotifyMainWindow{this.Id}", + ImGuiWindowFlags.AlwaysAutoResize | + ImGuiWindowFlags.NoDecoration | + ImGuiWindowFlags.NoNav | + ImGuiWindowFlags.NoMove | + ImGuiWindowFlags.NoFocusOnAppearing | + ImGuiWindowFlags.NoDocking); + + var isTakingKeyboardInput = ImGui.IsWindowFocused() && ImGui.GetIO().WantTextInput; + var isHovered = ImGui.IsWindowHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem); + var warrantsExtension = + this.ExtensionDurationSinceLastInterest > TimeSpan.Zero + && (isHovered || isTakingKeyboardInput); + + this.EffectiveExpiry = this.CalculateEffectiveExpiry(ref warrantsExtension); + + if (!this.IsDismissed && DateTime.Now > this.EffectiveExpiry) + this.DismissNow(NotificationDismissReason.Timeout); + + if (this.ExtensionDurationSinceLastInterest > TimeSpan.Zero && warrantsExtension) + this.lastInterestTime = DateTime.Now; + + this.DrawWindowBackgroundProgressBar(); + this.DrawTopBar(width, actionWindowHeight, isHovered); + if (!this.underlyingNotification.Minimized && !this.expandoEasing.IsRunning) + { + this.DrawContentArea(width, actionWindowHeight); + } + else if (this.expandoEasing.IsRunning) + { + if (this.underlyingNotification.Minimized) + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (1f - (float)this.expandoEasing.Value)); + else + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (float)this.expandoEasing.Value); + this.DrawContentArea(width, actionWindowHeight); + ImGui.PopStyleVar(); + } + + if (isTakingKeyboardInput) + this.DrawKeyboardInputIndicator(); + this.DrawExpiryBar(this.EffectiveExpiry, warrantsExtension); + + if (ImGui.IsWindowHovered()) + { + if (this.Click is null) + { + if (this.UserDismissable && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + this.DismissNow(NotificationDismissReason.Manual); + } + else + { + if (ImGui.IsMouseClicked(ImGuiMouseButton.Left) + || ImGui.IsMouseClicked(ImGuiMouseButton.Right) + || ImGui.IsMouseClicked(ImGuiMouseButton.Middle)) + this.Click.InvokeSafely(this); + } + } + + var windowSize = ImGui.GetWindowSize(); + ImGui.End(); + + ImGui.PopStyleColor(); + ImGui.PopStyleVar(3); + ImGui.PopID(); + + return windowSize.Y; + } + + /// Calculates the effective expiry, taking ImGui window state into account. + /// Notification will not dismiss while this paramter is true. + /// The calculated effective expiry. + /// Expected to be called BETWEEN and . + private DateTime CalculateEffectiveExpiry(ref bool warrantsExtension) + { + DateTime expiry; + var initialDuration = this.InitialDuration; + var expiryInitial = + initialDuration == TimeSpan.MaxValue + ? DateTime.MaxValue + : this.CreatedAt + initialDuration; + + var extendDuration = this.ExtensionDurationSinceLastInterest; + if (warrantsExtension) + { + expiry = DateTime.MaxValue; + } + else + { + var expiryExtend = + extendDuration == TimeSpan.MaxValue + ? DateTime.MaxValue + : this.lastInterestTime + extendDuration; + + expiry = expiryInitial > expiryExtend ? expiryInitial : expiryExtend; + if (expiry < this.extendedExpiry) + expiry = this.extendedExpiry; + } + + var he = this.HardExpiry; + if (he < expiry) + { + expiry = he; + warrantsExtension = false; + } + + return expiry; + } + + private void DrawWindowBackgroundProgressBar() + { + var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds % + NotificationConstants.ProgressWaveLoopDuration) / + NotificationConstants.ProgressWaveLoopDuration); + elapsed /= NotificationConstants.ProgressWaveIdleTimeRatio; + + var colorElapsed = + elapsed < NotificationConstants.ProgressWaveLoopMaxColorTimeRatio + ? elapsed / NotificationConstants.ProgressWaveLoopMaxColorTimeRatio + : ((NotificationConstants.ProgressWaveLoopMaxColorTimeRatio * 2) - elapsed) / + NotificationConstants.ProgressWaveLoopMaxColorTimeRatio; + + elapsed = Math.Clamp(elapsed, 0f, 1f); + colorElapsed = Math.Clamp(colorElapsed, 0f, 1f); + colorElapsed = MathF.Sin(colorElapsed * (MathF.PI / 2f)); + + var progress = Math.Clamp(this.ProgressEased, 0f, 1f); + if (progress >= 1f) + elapsed = colorElapsed = 0f; + + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + var rb = windowPos + windowSize; + var midp = windowPos + windowSize with { X = windowSize.X * progress * elapsed }; + var rp = windowPos + windowSize with { X = windowSize.X * progress }; + + ImGui.PushClipRect(windowPos, rb, false); + ImGui.GetWindowDrawList().AddRectFilled( + windowPos, + midp, + ImGui.GetColorU32( + Vector4.Lerp( + NotificationConstants.BackgroundProgressColorMin, + NotificationConstants.BackgroundProgressColorMax, + colorElapsed))); + ImGui.GetWindowDrawList().AddRectFilled( + midp with { Y = 0 }, + rp, + ImGui.GetColorU32(NotificationConstants.BackgroundProgressColorMin)); + ImGui.PopClipRect(); + } + + private void DrawKeyboardInputIndicator() + { + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + ImGui.PushClipRect(windowPos, windowPos + windowSize, false); + ImGui.GetWindowDrawList().AddRect( + windowPos, + windowPos + windowSize, + ImGui.GetColorU32(NotificationConstants.FocusBorderColor * new Vector4(1f, 1f, 1f, ImGui.GetStyle().Alpha)), + 0f, + ImDrawFlags.None, + NotificationConstants.FocusIndicatorThickness); + ImGui.PopClipRect(); + } + + private void DrawTopBar(float width, float height, bool drawActionButtons) + { + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + + var rtOffset = new Vector2(width, 0); + using (Service.Get().IconFontHandle?.Push()) + { + ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false); + if (this.UserDismissable) + { + if (this.DrawIconButton(FontAwesomeIcon.Times, rtOffset, height, drawActionButtons)) + this.DismissNow(NotificationDismissReason.Manual); + rtOffset.X -= height; + } + + if (this.underlyingNotification.Minimized) + { + if (this.DrawIconButton(FontAwesomeIcon.ChevronDown, rtOffset, height, drawActionButtons)) + this.Minimized = false; + } + else + { + if (this.DrawIconButton(FontAwesomeIcon.ChevronUp, rtOffset, height, drawActionButtons)) + this.Minimized = true; + } + + rtOffset.X -= height; + ImGui.PopClipRect(); + } + + float relativeOpacity; + if (this.expandoEasing.IsRunning) + { + relativeOpacity = + this.underlyingNotification.Minimized + ? 1f - (float)this.expandoEasing.Value + : (float)this.expandoEasing.Value; + } + else + { + relativeOpacity = this.underlyingNotification.Minimized ? 0f : 1f; + } + + if (drawActionButtons) + ImGui.PushClipRect(windowPos, windowPos + rtOffset with { Y = height }, false); + else + ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false); + + if (relativeOpacity > 0) + { + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * relativeOpacity); + ImGui.SetCursorPos(new(NotificationConstants.ScaledWindowPadding)); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); + ImGui.TextUnformatted( + ImGui.IsWindowHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) + ? this.CreatedAt.FormatAbsoluteDateTime() + : this.CreatedAt.FormatRelativeDateTime()); + ImGui.PopStyleColor(); + ImGui.PopStyleVar(); + } + + if (relativeOpacity < 1) + { + rtOffset = new(width - NotificationConstants.ScaledWindowPadding, 0); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * (1f - relativeOpacity)); + + var ltOffset = new Vector2(NotificationConstants.ScaledWindowPadding); + this.DrawIcon(ltOffset, new(height - (2 * NotificationConstants.ScaledWindowPadding))); + + ltOffset.X = height; + + var agoText = this.CreatedAt.FormatRelativeDateTimeShort(); + var agoSize = ImGui.CalcTextSize(agoText); + rtOffset.X -= agoSize.X; + ImGui.SetCursorPos(rtOffset with { Y = NotificationConstants.ScaledWindowPadding }); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); + ImGui.TextUnformatted(agoText); + ImGui.PopStyleColor(); + + rtOffset.X -= NotificationConstants.ScaledWindowPadding; + + ImGui.PushClipRect( + windowPos + ltOffset with { Y = 0 }, + windowPos + rtOffset with { Y = height }, + true); + ImGui.SetCursorPos(ltOffset with { Y = NotificationConstants.ScaledWindowPadding }); + ImGui.TextUnformatted(this.EffectiveMinimizedText); + ImGui.PopClipRect(); + + ImGui.PopStyleVar(); + } + + ImGui.PopClipRect(); + } + + private bool DrawIconButton(FontAwesomeIcon icon, Vector2 rt, float size, bool drawActionButtons) + { + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); + if (!drawActionButtons) + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0f); + ImGui.PushStyleColor(ImGuiCol.Button, 0); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor); + + ImGui.SetCursorPos(rt - new Vector2(size, 0)); + var r = ImGui.Button(icon.ToIconString(), new(size)); + + ImGui.PopStyleColor(2); + if (!drawActionButtons) + ImGui.PopStyleVar(); + ImGui.PopStyleVar(); + return r; + } + + private void DrawContentArea(float width, float actionWindowHeight) + { + var textColumnX = (NotificationConstants.ScaledWindowPadding * 2) + NotificationConstants.ScaledIconSize; + var textColumnWidth = width - textColumnX - NotificationConstants.ScaledWindowPadding; + var textColumnOffset = new Vector2(textColumnX, actionWindowHeight); + + this.DrawIcon( + new(NotificationConstants.ScaledWindowPadding, actionWindowHeight), + new(NotificationConstants.ScaledIconSize)); + + textColumnOffset.Y += this.DrawTitle(textColumnOffset, textColumnWidth); + textColumnOffset.Y += NotificationConstants.ScaledComponentGap; + + this.DrawContentBody(textColumnOffset, textColumnWidth); + } + + private void DrawIcon(Vector2 minCoord, Vector2 size) + { + var maxCoord = minCoord + size; + var iconColor = this.Type.ToColor(); + + if (NotificationUtilities.DrawIconFrom(minCoord, maxCoord, this.iconTextureWrap)) + return; + + if (this.Icon?.DrawIcon(minCoord, maxCoord, iconColor) is true) + return; + + if (NotificationUtilities.DrawIconFrom( + minCoord, + maxCoord, + this.Type.ToChar(), + Service.Get().IconFontAwesomeFontHandle, + iconColor)) + return; + + if (NotificationUtilities.DrawIconFrom(minCoord, maxCoord, this.initiatorPlugin)) + return; + + NotificationUtilities.DrawIconFromDalamudLogo(minCoord, maxCoord); + } + + private float DrawTitle(Vector2 minCoord, float width) + { + ImGui.PushTextWrapPos(minCoord.X + width); + + ImGui.SetCursorPos(minCoord); + if ((this.Title ?? this.Type.ToTitle()) is { } title) + { + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.TitleTextColor); + ImGui.TextUnformatted(title); + ImGui.PopStyleColor(); + } + + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BlameTextColor); + ImGui.SetCursorPos(minCoord with { Y = ImGui.GetCursorPosY() }); + ImGui.TextUnformatted(this.InitiatorString); + ImGui.PopStyleColor(); + + ImGui.PopTextWrapPos(); + return ImGui.GetCursorPosY() - minCoord.Y; + } + + private void DrawContentBody(Vector2 minCoord, float width) + { + ImGui.SetCursorPos(minCoord); + ImGui.PushTextWrapPos(minCoord.X + width); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BodyTextColor); + ImGui.TextUnformatted(this.Content); + ImGui.PopStyleColor(); + ImGui.PopTextWrapPos(); + if (this.DrawActions is not null) + { + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); + try + { + this.DrawActions.Invoke(this); + } + catch + { + // ignore + } + } + } + + private void DrawExpiryBar(DateTime effectiveExpiry, bool warrantsExtension) + { + float barL, barR; + if (this.IsDismissed) + { + var v = this.hideEasing.IsDone ? 0f : 1f - (float)this.hideEasing.Value; + var midpoint = (this.prevProgressL + this.prevProgressR) / 2f; + var length = (this.prevProgressR - this.prevProgressL) / 2f; + barL = midpoint - (length * v); + barR = midpoint + (length * v); + } + else if (warrantsExtension) + { + barL = 0f; + barR = 1f; + this.prevProgressL = barL; + this.prevProgressR = barR; + } + else if (effectiveExpiry == DateTime.MaxValue) + { + if (this.ShowIndeterminateIfNoExpiry) + { + var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds % + NotificationConstants.IndeterminateProgressbarLoopDuration) / + NotificationConstants.IndeterminateProgressbarLoopDuration); + barL = Math.Max(elapsed - (1f / 3), 0f) / (2f / 3); + barR = Math.Min(elapsed, 2f / 3) / (2f / 3); + barL = MathF.Pow(barL, 3); + barR = 1f - MathF.Pow(1f - barR, 3); + this.prevProgressL = barL; + this.prevProgressR = barR; + } + else + { + this.prevProgressL = barL = 0f; + this.prevProgressR = barR = 1f; + } + } + else + { + barL = 1f - (float)((effectiveExpiry - DateTime.Now).TotalMilliseconds / + (effectiveExpiry - this.lastInterestTime).TotalMilliseconds); + barR = 1f; + this.prevProgressL = barL; + this.prevProgressR = barR; + } + + barR = Math.Clamp(barR, 0f, 1f); + + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + ImGui.PushClipRect(windowPos, windowPos + windowSize, false); + ImGui.GetWindowDrawList().AddRectFilled( + windowPos + new Vector2( + windowSize.X * barL, + windowSize.Y - NotificationConstants.ScaledExpiryProgressBarHeight), + windowPos + windowSize with { X = windowSize.X * barR }, + ImGui.GetColorU32(this.Type.ToColor())); + ImGui.PopClipRect(); + } +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index 8591695a6..357752f6e 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -1,24 +1,21 @@ using System.Numerics; using System.Runtime.Loader; +using System.Threading; using Dalamud.Interface.Animation; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; -using Dalamud.Interface.ImGuiNotification.Internal.IconSource; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Interface.Utility; using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; -using ImGuiNET; - using Serilog; namespace Dalamud.Interface.ImGuiNotification.Internal; /// Represents an active notification. -internal sealed class ActiveNotification : IActiveNotification +internal sealed partial class ActiveNotification : IActiveNotification { private readonly Notification underlyingNotification; @@ -27,6 +24,21 @@ internal sealed class ActiveNotification : IActiveNotification private readonly Easing progressEasing; private readonly Easing expandoEasing; + /// Gets the time of starting to count the timer for the expiration. + private DateTime lastInterestTime; + + /// Gets the extended expiration time from . + private DateTime extendedExpiry; + + /// The icon texture to use if specified; otherwise, icon will be used from . + private IDalamudTextureWrap? iconTextureWrap; + + /// The plugin that initiated this notification. + private LocalPlugin? initiatorPlugin; + + /// Whether has been unloaded. + private bool isInitiatorUnloaded; + /// The progress before for the progress bar animation with . private float progressBefore; @@ -36,10 +48,10 @@ internal sealed class ActiveNotification : IActiveNotification /// Used for calculating correct dismissal progressbar animation (right edge). private float prevProgressR; - /// New progress value to be updated on next call to . + /// New progress value to be updated on next call to . private float? newProgress; - /// New minimized value to be updated on next call to . + /// New minimized value to be updated on next call to . private bool? newMinimized; /// Initializes a new instance of the class. @@ -47,28 +59,16 @@ internal sealed class ActiveNotification : IActiveNotification /// The initiator plugin. Use null if originated by Dalamud. public ActiveNotification(Notification underlyingNotification, LocalPlugin? initiatorPlugin) { - this.underlyingNotification = underlyingNotification with - { - IconSource = underlyingNotification.IconSource?.Clone(), - }; - this.InitiatorPlugin = initiatorPlugin; + this.underlyingNotification = underlyingNotification with { }; + this.initiatorPlugin = initiatorPlugin; this.showEasing = new InCubic(NotificationConstants.ShowAnimationDuration); this.hideEasing = new OutCubic(NotificationConstants.HideAnimationDuration); this.progressEasing = new InOutCubic(NotificationConstants.ProgressChangeAnimationDuration); this.expandoEasing = new InOutCubic(NotificationConstants.ExpandoAnimationDuration); + this.CreatedAt = this.lastInterestTime = this.extendedExpiry = DateTime.Now; this.showEasing.Start(); this.progressEasing.Start(); - try - { - this.UpdateIcon(); - } - catch (Exception e) - { - // Ignore the one caused from ctor only; other UpdateIcon calls are from plugins, and they should handle the - // error accordingly. - Log.Error(e, $"{nameof(ActiveNotification)}#{this.Id} ctor: {nameof(this.UpdateIcon)} failed and ignored."); - } } /// @@ -80,23 +80,11 @@ internal sealed class ActiveNotification : IActiveNotification /// public event Action? DrawActions; - /// - public event Action? MouseEnter; - - /// - public event Action? MouseLeave; - /// public long Id { get; } = IActiveNotification.CreateNewId(); - /// Gets the time of creating this notification. - public DateTime CreatedAt { get; } = DateTime.Now; - - /// Gets the time of starting to count the timer for the expiration. - public DateTime LastInterestTime { get; private set; } = DateTime.Now; - - /// Gets the extended expiration time from . - public DateTime ExtendedExpiry { get; private set; } = DateTime.Now; + /// + public DateTime CreatedAt { get; } /// public string Content @@ -147,19 +135,14 @@ internal sealed class ActiveNotification : IActiveNotification } /// - public INotificationIconSource? IconSource + public INotificationIcon? Icon { - get => this.underlyingNotification.IconSource; + get => this.underlyingNotification.Icon; set { if (this.IsDismissed) - { - value?.Dispose(); return; - } - - this.underlyingNotification.IconSource = value; - this.UpdateIcon(); + this.underlyingNotification.Icon = value; } } @@ -172,7 +155,7 @@ internal sealed class ActiveNotification : IActiveNotification if (this.underlyingNotification.HardExpiry == value || this.IsDismissed) return; this.underlyingNotification.HardExpiry = value; - this.LastInterestTime = DateTime.Now; + this.lastInterestTime = DateTime.Now; } } @@ -185,58 +168,25 @@ internal sealed class ActiveNotification : IActiveNotification if (this.IsDismissed) return; this.underlyingNotification.InitialDuration = value; - this.LastInterestTime = DateTime.Now; + this.lastInterestTime = DateTime.Now; } } /// - public TimeSpan DurationSinceLastInterest + public TimeSpan ExtensionDurationSinceLastInterest { - get => this.underlyingNotification.DurationSinceLastInterest; + get => this.underlyingNotification.ExtensionDurationSinceLastInterest; set { if (this.IsDismissed) return; - this.underlyingNotification.DurationSinceLastInterest = value; - this.LastInterestTime = DateTime.Now; + this.underlyingNotification.ExtensionDurationSinceLastInterest = value; + this.lastInterestTime = DateTime.Now; } } /// - public DateTime EffectiveExpiry - { - get - { - var initialDuration = this.InitialDuration; - var expiryInitial = - initialDuration == TimeSpan.MaxValue - ? DateTime.MaxValue - : this.CreatedAt + initialDuration; - - DateTime expiry; - var hoverExtendDuration = this.DurationSinceLastInterest; - if (hoverExtendDuration > TimeSpan.Zero && (this.IsHovered || this.IsFocused)) - { - expiry = DateTime.MaxValue; - } - else - { - var expiryExtend = - hoverExtendDuration == TimeSpan.MaxValue - ? DateTime.MaxValue - : this.LastInterestTime + hoverExtendDuration; - - expiry = expiryInitial > expiryExtend ? expiryInitial : expiryExtend; - if (expiry < this.ExtendedExpiry) - expiry = this.ExtendedExpiry; - } - - var he = this.HardExpiry; - if (he < expiry) - expiry = he; - return expiry; - } - } + public DateTime EffectiveExpiry { get; private set; } /// public bool ShowIndeterminateIfNoExpiry @@ -286,24 +236,9 @@ internal sealed class ActiveNotification : IActiveNotification } } - /// - public bool IsHovered { get; private set; } - - /// - public bool IsFocused { get; private set; } - /// public bool IsDismissed => this.hideEasing.IsRunning; - /// Gets a value indicating whether has been unloaded. - public bool IsInitiatorUnloaded { get; private set; } - - /// Gets or sets the plugin that initiated this notification. - public LocalPlugin? InitiatorPlugin { get; set; } - - /// Gets or sets the icon of this notification. - public INotificationMaterializedIcon? MaterializedIcon { get; set; } - /// Gets the eased progress. private float ProgressEased { @@ -318,61 +253,17 @@ internal sealed class ActiveNotification : IActiveNotification } } - /// Gets the default color of the notification. - private Vector4 DefaultIconColor => this.Type switch - { - NotificationType.None => ImGuiColors.DalamudWhite, - NotificationType.Success => ImGuiColors.HealerGreen, - NotificationType.Warning => ImGuiColors.DalamudOrange, - NotificationType.Error => ImGuiColors.DalamudRed, - NotificationType.Info => ImGuiColors.TankBlue, - _ => ImGuiColors.DalamudWhite, - }; - - /// Gets the default icon of the notification. - private char? DefaultIconChar => this.Type switch - { - NotificationType.None => null, - NotificationType.Success => FontAwesomeIcon.CheckCircle.ToIconChar(), - NotificationType.Warning => FontAwesomeIcon.ExclamationCircle.ToIconChar(), - NotificationType.Error => FontAwesomeIcon.TimesCircle.ToIconChar(), - NotificationType.Info => FontAwesomeIcon.InfoCircle.ToIconChar(), - _ => null, - }; - - /// Gets the default title of the notification. - private string? DefaultTitle => this.Type switch - { - NotificationType.None => null, - NotificationType.Success => NotificationType.Success.ToString(), - NotificationType.Warning => NotificationType.Warning.ToString(), - NotificationType.Error => NotificationType.Error.ToString(), - NotificationType.Info => NotificationType.Info.ToString(), - _ => null, - }; - /// Gets the string for the initiator field. private string InitiatorString => - this.InitiatorPlugin is not { } initiatorPlugin + this.initiatorPlugin is not { } plugin ? NotificationConstants.DefaultInitiator - : this.IsInitiatorUnloaded - ? NotificationConstants.UnloadedInitiatorNameFormat.Format(initiatorPlugin.Name) - : initiatorPlugin.Name; + : this.isInitiatorUnloaded + ? NotificationConstants.UnloadedInitiatorNameFormat.Format(plugin.Name) + : plugin.Name; /// Gets the effective text to display when minimized. private string EffectiveMinimizedText => (this.MinimizedText ?? this.Content).ReplaceLineEndings(" "); - /// - public void Dispose() - { - this.ClearMaterializedIcon(); - this.underlyingNotification.Dispose(); - this.Dismiss = null; - this.Click = null; - this.DrawActions = null; - this.InitiatorPlugin = null; - } - /// public void DismissNow() => this.DismissNow(NotificationDismissReason.Programmatical); @@ -392,13 +283,78 @@ internal sealed class ActiveNotification : IActiveNotification { Log.Error( e, - $"{nameof(this.Dismiss)} error; notification is owned by {this.InitiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator}"); + $"{nameof(this.Dismiss)} error; notification is owned by {this.initiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator}"); } } - /// Updates animations. - /// true if the notification is over. - public bool UpdateAnimations() + /// + public void ExtendBy(TimeSpan extension) + { + var newExpiry = DateTime.Now + extension; + if (this.extendedExpiry < newExpiry) + this.extendedExpiry = newExpiry; + } + + /// + public void SetIconTexture(IDalamudTextureWrap? textureWrap) + { + if (this.IsDismissed) + { + textureWrap?.Dispose(); + return; + } + + // After replacing, if the old texture is not the old texture, then dispose the old texture. + if (Interlocked.Exchange(ref this.iconTextureWrap, textureWrap) is { } wrapToDispose && + wrapToDispose != textureWrap) + { + wrapToDispose.Dispose(); + } + } + + /// Removes non-Dalamud invocation targets from events. + internal void RemoveNonDalamudInvocations() + { + var dalamudContext = AssemblyLoadContext.GetLoadContext(typeof(NotificationManager).Assembly); + this.Dismiss = RemoveNonDalamudInvocationsCore(this.Dismiss); + this.Click = RemoveNonDalamudInvocationsCore(this.Click); + this.DrawActions = RemoveNonDalamudInvocationsCore(this.DrawActions); + + if (this.Icon is { } previousIcon && !IsOwnedByDalamud(previousIcon.GetType())) + this.Icon = null; + + this.isInitiatorUnloaded = true; + this.UserDismissable = true; + this.ExtensionDurationSinceLastInterest = NotificationConstants.DefaultDuration; + + var newMaxExpiry = DateTime.Now + NotificationConstants.DefaultDuration; + if (this.EffectiveExpiry > newMaxExpiry) + this.HardExpiry = newMaxExpiry; + + return; + + bool IsOwnedByDalamud(Type t) => AssemblyLoadContext.GetLoadContext(t.Assembly) == dalamudContext; + + T? RemoveNonDalamudInvocationsCore(T? @delegate) where T : Delegate + { + if (@delegate is null) + return null; + + foreach (var il in @delegate.GetInvocationList()) + { + if (il.Target is { } target && !IsOwnedByDalamud(target.GetType())) + @delegate = (T)Delegate.Remove(@delegate, il); + } + + return @delegate; + } + } + + /// Updates the state of this notification, and release the relevant resource if this notification is no + /// longer in use. + /// true if the notification is over and relevant resources are released. + /// Intended to be called from the main thread only. + internal bool UpdateOrDisposeInternal() { this.showEasing.Update(); this.hideEasing.Update(); @@ -435,555 +391,21 @@ internal sealed class ActiveNotification : IActiveNotification this.newMinimized = null; } - return this.hideEasing.IsRunning && this.hideEasing.IsDone; + if (!this.hideEasing.IsRunning || !this.hideEasing.IsDone) + return false; + + this.DisposeInternal(); + return true; } - /// Draws this notification. - /// The maximum width of the notification window. - /// The offset from the bottom. - /// The height of the notification. - public float Draw(float maxWidth, float offsetY) + /// Clears the resources associated with this instance of . + internal void DisposeInternal() { - var effectiveExpiry = this.EffectiveExpiry; - if (!this.IsDismissed && DateTime.Now > effectiveExpiry) - this.DismissNow(NotificationDismissReason.Timeout); - - var opacity = - Math.Clamp( - (float)(this.hideEasing.IsRunning - ? (this.hideEasing.IsDone ? 0 : 1f - this.hideEasing.Value) - : (this.showEasing.IsDone ? 1 : this.showEasing.Value)), - 0f, - 1f); - if (opacity <= 0) - return 0; - - var interfaceManager = Service.Get(); - var unboundedWidth = ImGui.CalcTextSize(this.Content).X; - float closeButtonHorizontalSpaceReservation; - using (interfaceManager.IconFontHandle?.Push()) - { - closeButtonHorizontalSpaceReservation = ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X; - closeButtonHorizontalSpaceReservation += NotificationConstants.ScaledWindowPadding; - } - - unboundedWidth = Math.Max( - unboundedWidth, - ImGui.CalcTextSize(this.Title ?? this.DefaultTitle ?? string.Empty).X); - unboundedWidth = Math.Max( - unboundedWidth, - ImGui.CalcTextSize(this.InitiatorString).X); - unboundedWidth = Math.Max( - unboundedWidth, - ImGui.CalcTextSize(this.CreatedAt.FormatAbsoluteDateTime()).X + closeButtonHorizontalSpaceReservation); - unboundedWidth = Math.Max( - unboundedWidth, - ImGui.CalcTextSize(this.CreatedAt.FormatRelativeDateTime()).X + closeButtonHorizontalSpaceReservation); - - unboundedWidth += NotificationConstants.ScaledWindowPadding * 3; - unboundedWidth += NotificationConstants.ScaledIconSize; - - var actionWindowHeight = - // Content - ImGui.GetTextLineHeight() + - // Top and bottom padding - (NotificationConstants.ScaledWindowPadding * 2); - - var width = Math.Min(maxWidth, unboundedWidth); - - var viewport = ImGuiHelpers.MainViewport; - var viewportPos = viewport.WorkPos; - var viewportSize = viewport.WorkSize; - - ImGui.PushID(this.Id.GetHashCode()); - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity); - ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f); - ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(NotificationConstants.ScaledWindowPadding)); - unsafe - { - ImGui.PushStyleColor( - ImGuiCol.WindowBg, - *ImGui.GetStyleColorVec4(ImGuiCol.WindowBg) * new Vector4( - 1f, - 1f, - 1f, - NotificationConstants.BackgroundOpacity)); - } - - ImGuiHelpers.ForceNextWindowMainViewport(); - ImGui.SetNextWindowPos( - (viewportPos + viewportSize) - - new Vector2(NotificationConstants.ScaledViewportEdgeMargin) - - new Vector2(0, offsetY), - ImGuiCond.Always, - Vector2.One); - ImGui.SetNextWindowSizeConstraints( - new(width, actionWindowHeight), - new( - width, - !this.underlyingNotification.Minimized || this.expandoEasing.IsRunning - ? float.MaxValue - : actionWindowHeight)); - ImGui.Begin( - $"##NotifyMainWindow{this.Id}", - ImGuiWindowFlags.AlwaysAutoResize | - ImGuiWindowFlags.NoDecoration | - ImGuiWindowFlags.NoNav | - ImGuiWindowFlags.NoMove | - ImGuiWindowFlags.NoFocusOnAppearing | - ImGuiWindowFlags.NoDocking); - this.IsFocused = ImGui.IsWindowFocused(); - if (this.IsFocused) - this.LastInterestTime = DateTime.Now; - - this.DrawWindowBackgroundProgressBar(); - this.DrawFocusIndicator(); - this.DrawTopBar(interfaceManager, width, actionWindowHeight); - if (!this.underlyingNotification.Minimized && !this.expandoEasing.IsRunning) - { - this.DrawContentArea(width, actionWindowHeight); - } - else if (this.expandoEasing.IsRunning) - { - if (this.underlyingNotification.Minimized) - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (1f - (float)this.expandoEasing.Value)); - else - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (float)this.expandoEasing.Value); - this.DrawContentArea(width, actionWindowHeight); - ImGui.PopStyleVar(); - } - - this.DrawExpiryBar(effectiveExpiry); - - var windowPos = ImGui.GetWindowPos(); - var windowSize = ImGui.GetWindowSize(); - var hovered = ImGui.IsWindowHovered(); - ImGui.End(); - - ImGui.PopStyleColor(); - ImGui.PopStyleVar(3); - ImGui.PopID(); - - if (windowPos.X <= ImGui.GetIO().MousePos.X - && windowPos.Y <= ImGui.GetIO().MousePos.Y - && ImGui.GetIO().MousePos.X < windowPos.X + windowSize.X - && ImGui.GetIO().MousePos.Y < windowPos.Y + windowSize.Y) - { - if (!this.IsHovered) - { - this.IsHovered = true; - this.MouseEnter.InvokeSafely(this); - } - - if (this.DurationSinceLastInterest > TimeSpan.Zero) - this.LastInterestTime = DateTime.Now; - - if (hovered) - { - if (this.Click is null) - { - if (this.UserDismissable && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) - this.DismissNow(NotificationDismissReason.Manual); - } - else - { - if (ImGui.IsMouseClicked(ImGuiMouseButton.Left) - || ImGui.IsMouseClicked(ImGuiMouseButton.Right) - || ImGui.IsMouseClicked(ImGuiMouseButton.Middle)) - this.Click.InvokeSafely(this); - } - } - } - else if (this.IsHovered) - { - this.IsHovered = false; - this.MouseLeave.InvokeSafely(this); - } - - return windowSize.Y; - } - - /// - public void ExtendBy(TimeSpan extension) - { - var newExpiry = DateTime.Now + extension; - if (this.ExtendedExpiry < newExpiry) - this.ExtendedExpiry = newExpiry; - } - - /// - public void UpdateIcon() - { - if (this.IsDismissed) - return; - this.ClearMaterializedIcon(); - this.MaterializedIcon = (this.IconSource as INotificationIconSource.IInternal)?.Materialize(); - } - - /// Removes non-Dalamud invocation targets from events. - public void RemoveNonDalamudInvocations() - { - var dalamudContext = AssemblyLoadContext.GetLoadContext(typeof(NotificationManager).Assembly); - this.Dismiss = RemoveNonDalamudInvocationsCore(this.Dismiss); - this.Click = RemoveNonDalamudInvocationsCore(this.Click); - this.DrawActions = RemoveNonDalamudInvocationsCore(this.DrawActions); - this.MouseEnter = RemoveNonDalamudInvocationsCore(this.MouseEnter); - this.MouseLeave = RemoveNonDalamudInvocationsCore(this.MouseLeave); - - this.IsInitiatorUnloaded = true; - this.UserDismissable = true; - this.DurationSinceLastInterest = NotificationConstants.DefaultHoverExtendDuration; - - var newMaxExpiry = DateTime.Now + NotificationConstants.DefaultDisplayDuration; - if (this.EffectiveExpiry > newMaxExpiry) - this.HardExpiry = newMaxExpiry; - - return; - - T? RemoveNonDalamudInvocationsCore(T? @delegate) where T : Delegate - { - if (@delegate is null) - return null; - - foreach (var il in @delegate.GetInvocationList()) - { - if (il.Target is { } target && - AssemblyLoadContext.GetLoadContext(target.GetType().Assembly) != dalamudContext) - { - @delegate = (T)Delegate.Remove(@delegate, il); - } - } - - return @delegate; - } - } - - private void ClearMaterializedIcon() - { - this.MaterializedIcon?.Dispose(); - this.MaterializedIcon = null; - } - - private void DrawWindowBackgroundProgressBar() - { - var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds % - NotificationConstants.ProgressWaveLoopDuration) / - NotificationConstants.ProgressWaveLoopDuration); - elapsed /= NotificationConstants.ProgressWaveIdleTimeRatio; - - var colorElapsed = - elapsed < NotificationConstants.ProgressWaveLoopMaxColorTimeRatio - ? elapsed / NotificationConstants.ProgressWaveLoopMaxColorTimeRatio - : ((NotificationConstants.ProgressWaveLoopMaxColorTimeRatio * 2) - elapsed) / - NotificationConstants.ProgressWaveLoopMaxColorTimeRatio; - - elapsed = Math.Clamp(elapsed, 0f, 1f); - colorElapsed = Math.Clamp(colorElapsed, 0f, 1f); - colorElapsed = MathF.Sin(colorElapsed * (MathF.PI / 2f)); - - var progress = Math.Clamp(this.ProgressEased, 0f, 1f); - if (progress >= 1f) - elapsed = colorElapsed = 0f; - - var windowPos = ImGui.GetWindowPos(); - var windowSize = ImGui.GetWindowSize(); - var rb = windowPos + windowSize; - var midp = windowPos + windowSize with { X = windowSize.X * progress * elapsed }; - var rp = windowPos + windowSize with { X = windowSize.X * progress }; - - ImGui.PushClipRect(windowPos, rb, false); - ImGui.GetWindowDrawList().AddRectFilled( - windowPos, - midp, - ImGui.GetColorU32( - Vector4.Lerp( - NotificationConstants.BackgroundProgressColorMin, - NotificationConstants.BackgroundProgressColorMax, - colorElapsed))); - ImGui.GetWindowDrawList().AddRectFilled( - midp with { Y = 0 }, - rp, - ImGui.GetColorU32(NotificationConstants.BackgroundProgressColorMin)); - ImGui.PopClipRect(); - } - - private void DrawFocusIndicator() - { - if (!this.IsFocused) - return; - var windowPos = ImGui.GetWindowPos(); - var windowSize = ImGui.GetWindowSize(); - ImGui.PushClipRect(windowPos, windowPos + windowSize, false); - ImGui.GetWindowDrawList().AddRect( - windowPos, - windowPos + windowSize, - ImGui.GetColorU32(NotificationConstants.FocusBorderColor * new Vector4(1f, 1f, 1f, ImGui.GetStyle().Alpha)), - 0f, - ImDrawFlags.None, - NotificationConstants.FocusIndicatorThickness); - ImGui.PopClipRect(); - } - - private void DrawTopBar(InterfaceManager interfaceManager, float width, float height) - { - var windowPos = ImGui.GetWindowPos(); - var windowSize = ImGui.GetWindowSize(); - - var rtOffset = new Vector2(width, 0); - using (interfaceManager.IconFontHandle?.Push()) - { - ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false); - if (this.UserDismissable) - { - if (this.DrawIconButton(FontAwesomeIcon.Times, rtOffset, height)) - this.DismissNow(NotificationDismissReason.Manual); - rtOffset.X -= height; - } - - if (this.underlyingNotification.Minimized) - { - if (this.DrawIconButton(FontAwesomeIcon.ChevronDown, rtOffset, height)) - this.Minimized = false; - } - else - { - if (this.DrawIconButton(FontAwesomeIcon.ChevronUp, rtOffset, height)) - this.Minimized = true; - } - - rtOffset.X -= height; - ImGui.PopClipRect(); - } - - float relativeOpacity; - if (this.expandoEasing.IsRunning) - { - relativeOpacity = - this.underlyingNotification.Minimized - ? 1f - (float)this.expandoEasing.Value - : (float)this.expandoEasing.Value; - } - else - { - relativeOpacity = this.underlyingNotification.Minimized ? 0f : 1f; - } - - if (this.IsHovered || this.IsFocused) - ImGui.PushClipRect(windowPos, windowPos + rtOffset with { Y = height }, false); - else - ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false); - - if (relativeOpacity > 0) - { - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * relativeOpacity); - ImGui.SetCursorPos(new(NotificationConstants.ScaledWindowPadding)); - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); - ImGui.TextUnformatted( - this.IsHovered || this.IsFocused - ? this.CreatedAt.FormatAbsoluteDateTime() - : this.CreatedAt.FormatRelativeDateTime()); - ImGui.PopStyleColor(); - ImGui.PopStyleVar(); - } - - if (relativeOpacity < 1) - { - rtOffset = new(width - NotificationConstants.ScaledWindowPadding, 0); - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * (1f - relativeOpacity)); - - var ltOffset = new Vector2(NotificationConstants.ScaledWindowPadding); - this.DrawIcon(ltOffset, new(height - (2 * NotificationConstants.ScaledWindowPadding))); - - ltOffset.X = height; - - var agoText = this.CreatedAt.FormatRelativeDateTimeShort(); - var agoSize = ImGui.CalcTextSize(agoText); - rtOffset.X -= agoSize.X; - ImGui.SetCursorPos(rtOffset with { Y = NotificationConstants.ScaledWindowPadding }); - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); - ImGui.TextUnformatted(agoText); - ImGui.PopStyleColor(); - - rtOffset.X -= NotificationConstants.ScaledWindowPadding; - - ImGui.PushClipRect( - windowPos + ltOffset with { Y = 0 }, - windowPos + rtOffset with { Y = height }, - true); - ImGui.SetCursorPos(ltOffset with { Y = NotificationConstants.ScaledWindowPadding }); - ImGui.TextUnformatted(this.EffectiveMinimizedText); - ImGui.PopClipRect(); - - ImGui.PopStyleVar(); - } - - ImGui.PopClipRect(); - } - - private bool DrawIconButton(FontAwesomeIcon icon, Vector2 rt, float size) - { - ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); - var alphaPush = !this.IsHovered && !this.IsFocused; - if (alphaPush) - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0f); - ImGui.PushStyleColor(ImGuiCol.Button, 0); - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor); - - ImGui.SetCursorPos(rt - new Vector2(size, 0)); - var r = ImGui.Button(icon.ToIconString(), new(size)); - - ImGui.PopStyleColor(2); - if (alphaPush) - ImGui.PopStyleVar(); - ImGui.PopStyleVar(); - return r; - } - - private void DrawContentArea(float width, float actionWindowHeight) - { - var textColumnX = (NotificationConstants.ScaledWindowPadding * 2) + NotificationConstants.ScaledIconSize; - var textColumnWidth = width - textColumnX - NotificationConstants.ScaledWindowPadding; - var textColumnOffset = new Vector2(textColumnX, actionWindowHeight); - - this.DrawIcon( - new(NotificationConstants.ScaledWindowPadding, actionWindowHeight), - new(NotificationConstants.ScaledIconSize)); - - textColumnOffset.Y += this.DrawTitle(textColumnOffset, textColumnWidth); - textColumnOffset.Y += NotificationConstants.ScaledComponentGap; - - this.DrawContentBody(textColumnOffset, textColumnWidth); - } - - private void DrawIcon(Vector2 minCoord, Vector2 size) - { - var maxCoord = minCoord + size; - if (this.MaterializedIcon is not null) - { - this.MaterializedIcon.DrawIcon(minCoord, maxCoord, this.DefaultIconColor, this.InitiatorPlugin); - return; - } - - var defaultIconChar = this.DefaultIconChar; - if (defaultIconChar is not null) - { - NotificationUtilities.DrawIconString( - Service.Get().IconFontAwesomeFontHandle, - defaultIconChar.Value, - minCoord, - maxCoord, - this.DefaultIconColor); - return; - } - - TextureWrapTaskIconSource.DefaultMaterializedIcon.DrawIcon( - minCoord, - maxCoord, - this.DefaultIconColor, - this.InitiatorPlugin); - } - - private float DrawTitle(Vector2 minCoord, float width) - { - ImGui.PushTextWrapPos(minCoord.X + width); - - ImGui.SetCursorPos(minCoord); - if ((this.Title ?? this.DefaultTitle) is { } title) - { - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.TitleTextColor); - ImGui.TextUnformatted(title); - ImGui.PopStyleColor(); - } - - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BlameTextColor); - ImGui.SetCursorPos(minCoord with { Y = ImGui.GetCursorPosY() }); - ImGui.TextUnformatted(this.InitiatorString); - ImGui.PopStyleColor(); - - ImGui.PopTextWrapPos(); - return ImGui.GetCursorPosY() - minCoord.Y; - } - - private void DrawContentBody(Vector2 minCoord, float width) - { - ImGui.SetCursorPos(minCoord); - ImGui.PushTextWrapPos(minCoord.X + width); - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BodyTextColor); - ImGui.TextUnformatted(this.Content); - ImGui.PopStyleColor(); - ImGui.PopTextWrapPos(); - if (this.DrawActions is not null) - { - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); - try - { - this.DrawActions.Invoke(this); - } - catch - { - // ignore - } - } - } - - private void DrawExpiryBar(DateTime effectiveExpiry) - { - float barL, barR; - if (this.IsDismissed) - { - var v = this.hideEasing.IsDone ? 0f : 1f - (float)this.hideEasing.Value; - var midpoint = (this.prevProgressL + this.prevProgressR) / 2f; - var length = (this.prevProgressR - this.prevProgressL) / 2f; - barL = midpoint - (length * v); - barR = midpoint + (length * v); - } - else if (this.DurationSinceLastInterest > TimeSpan.Zero && (this.IsHovered || this.IsFocused)) - { - barL = 0f; - barR = 1f; - this.prevProgressL = barL; - this.prevProgressR = barR; - } - else if (effectiveExpiry == DateTime.MaxValue) - { - if (this.ShowIndeterminateIfNoExpiry) - { - var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds % - NotificationConstants.IndeterminateProgressbarLoopDuration) / - NotificationConstants.IndeterminateProgressbarLoopDuration); - barL = Math.Max(elapsed - (1f / 3), 0f) / (2f / 3); - barR = Math.Min(elapsed, 2f / 3) / (2f / 3); - barL = MathF.Pow(barL, 3); - barR = 1f - MathF.Pow(1f - barR, 3); - this.prevProgressL = barL; - this.prevProgressR = barR; - } - else - { - this.prevProgressL = barL = 0f; - this.prevProgressR = barR = 1f; - } - } - else - { - barL = 1f - (float)((effectiveExpiry - DateTime.Now).TotalMilliseconds / - (effectiveExpiry - this.LastInterestTime).TotalMilliseconds); - barR = 1f; - this.prevProgressL = barL; - this.prevProgressR = barR; - } - - barR = Math.Clamp(barR, 0f, 1f); - - var windowPos = ImGui.GetWindowPos(); - var windowSize = ImGui.GetWindowSize(); - ImGui.PushClipRect(windowPos, windowPos + windowSize, false); - ImGui.GetWindowDrawList().AddRectFilled( - windowPos + new Vector2( - windowSize.X * barL, - windowSize.Y - NotificationConstants.ScaledExpiryProgressBarHeight), - windowPos + windowSize with { X = windowSize.X * barR }, - ImGui.GetColorU32(this.DefaultIconColor)); - ImGui.PopClipRect(); + if (Interlocked.Exchange(ref this.iconTextureWrap, null) is { } wrapToDispose) + wrapToDispose.Dispose(); + this.Dismiss = null; + this.Click = null; + this.DrawActions = null; + this.initiatorPlugin = null; } } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FilePathIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FilePathIconSource.cs deleted file mode 100644 index a741931a5..000000000 --- a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FilePathIconSource.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.IO; -using System.Numerics; - -using Dalamud.Interface.Internal; -using Dalamud.Plugin.Internal.Types; - -namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; - -/// Represents the use of a texture from a file as the icon of a notification. -/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. -internal class FilePathIconSource : INotificationIconSource.IInternal -{ - /// Initializes a new instance of the class. - /// The path to a .tex file inside the game resources. - public FilePathIconSource(string filePath) => this.FilePath = filePath; - - /// Gets the path to a .tex file inside the game resources. - public string FilePath { get; } - - /// - public INotificationIconSource Clone() => this; - - /// - public void Dispose() - { - } - - /// - public INotificationMaterializedIcon Materialize() => - new MaterializedIcon(this.FilePath); - - private sealed class MaterializedIcon : INotificationMaterializedIcon - { - private readonly FileInfo fileInfo; - - public MaterializedIcon(string filePath) => this.fileInfo = new(filePath); - - public void Dispose() - { - } - - public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => - NotificationUtilities.DrawTexture( - Service.Get().GetTextureFromFile(this.fileInfo), - minCoord, - maxCoord, - initiatorPlugin); - } -} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs deleted file mode 100644 index cfe790851..000000000 --- a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Numerics; - -using Dalamud.Plugin.Internal.Types; - -namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; - -/// Represents the use of as the icon of a notification. -internal class FontAwesomeIconIconSource : INotificationIconSource.IInternal -{ - /// Initializes a new instance of the class. - /// The character. - public FontAwesomeIconIconSource(FontAwesomeIcon iconChar) => this.IconChar = iconChar; - - /// Gets the icon character. - public FontAwesomeIcon IconChar { get; } - - /// - public INotificationIconSource Clone() => this; - - /// - public void Dispose() - { - } - - /// - public INotificationMaterializedIcon Materialize() => new MaterializedIcon(this.IconChar); - - private sealed class MaterializedIcon : INotificationMaterializedIcon - { - private readonly char iconChar; - - public MaterializedIcon(FontAwesomeIcon c) => this.iconChar = c.ToIconChar(); - - public void Dispose() - { - } - - public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => - NotificationUtilities.DrawIconString( - Service.Get().IconFontAwesomeFontHandle, - this.iconChar, - minCoord, - maxCoord, - color); - } -} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/GamePathIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/GamePathIconSource.cs deleted file mode 100644 index 974e60ee7..000000000 --- a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/GamePathIconSource.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Numerics; - -using Dalamud.Interface.Internal; -using Dalamud.Plugin.Internal.Types; -using Dalamud.Plugin.Services; - -namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; - -/// Represents the use of a game-shipped texture as the icon of a notification. -/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. -internal class GamePathIconSource : INotificationIconSource.IInternal -{ - /// Initializes a new instance of the class. - /// The path to a .tex file inside the game resources. - /// Use to get the game path from icon IDs. - public GamePathIconSource(string gamePath) => this.GamePath = gamePath; - - /// Gets the path to a .tex file inside the game resources. - public string GamePath { get; } - - /// - public INotificationIconSource Clone() => this; - - /// - public void Dispose() - { - } - - /// - public INotificationMaterializedIcon Materialize() => - new MaterializedIcon(this.GamePath); - - private sealed class MaterializedIcon : INotificationMaterializedIcon - { - private readonly string gamePath; - - public MaterializedIcon(string gamePath) => this.gamePath = gamePath; - - public void Dispose() - { - } - - public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => - NotificationUtilities.DrawTexture( - Service.Get().GetTextureFromGame(this.gamePath), - minCoord, - maxCoord, - initiatorPlugin); - } -} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs deleted file mode 100644 index 19fe8e948..000000000 --- a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Numerics; - -using Dalamud.Game.Text; -using Dalamud.Plugin.Internal.Types; - -namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; - -/// Represents the use of as the icon of a notification. -internal class SeIconCharIconSource : INotificationIconSource.IInternal -{ - /// Initializes a new instance of the class. - /// The character. - public SeIconCharIconSource(SeIconChar c) => this.IconChar = c; - - /// Gets the icon character. - public SeIconChar IconChar { get; } - - /// - public INotificationIconSource Clone() => this; - - /// - public void Dispose() - { - } - - /// - public INotificationMaterializedIcon Materialize() => new MaterializedIcon(this.IconChar); - - private sealed class MaterializedIcon : INotificationMaterializedIcon - { - private readonly char iconChar; - - public MaterializedIcon(SeIconChar c) => this.iconChar = c.ToIconChar(); - - public void Dispose() - { - } - - public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => - NotificationUtilities.DrawIconString( - Service.Get().IconAxisFontHandle, - this.iconChar, - minCoord, - maxCoord, - color); - } -} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapIconSource.cs deleted file mode 100644 index a10b09bce..000000000 --- a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapIconSource.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Numerics; -using System.Threading; - -using Dalamud.Interface.Internal; -using Dalamud.Plugin.Internal.Types; - -namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; - -/// Represents the use of future as the icon of a notification. -/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. -internal class TextureWrapIconSource : INotificationIconSource.IInternal -{ - private IDalamudTextureWrap? wrap; - - /// Initializes a new instance of the class. - /// The texture wrap to handle over the ownership. - /// - /// If true, this class will own the passed , and you must not call - /// on the passed wrap. - /// If false, this class will create a new reference of the passed wrap, and you should call - /// on the passed wrap. - /// In both cases, this class must be disposed after use. - public TextureWrapIconSource(IDalamudTextureWrap? wrap, bool takeOwnership) => - this.wrap = takeOwnership ? wrap : wrap?.CreateWrapSharingLowLevelResource(); - - /// Gets the underlying texture wrap. - public IDalamudTextureWrap? Wrap => this.wrap; - - /// - public INotificationIconSource Clone() => new TextureWrapIconSource(this.wrap, false); - - /// - public void Dispose() - { - if (Interlocked.Exchange(ref this.wrap, null) is { } w) - w.Dispose(); - } - - /// - public INotificationMaterializedIcon Materialize() => - new MaterializedIcon(this.wrap?.CreateWrapSharingLowLevelResource()); - - private sealed class MaterializedIcon : INotificationMaterializedIcon - { - private IDalamudTextureWrap? wrap; - - public MaterializedIcon(IDalamudTextureWrap? wrap) => this.wrap = wrap; - - public void Dispose() - { - if (Interlocked.Exchange(ref this.wrap, null) is { } w) - w.Dispose(); - } - - public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => - NotificationUtilities.DrawTexture( - this.wrap, - minCoord, - maxCoord, - initiatorPlugin); - } -} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapTaskIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapTaskIconSource.cs deleted file mode 100644 index 4039b6955..000000000 --- a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapTaskIconSource.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Numerics; -using System.Threading.Tasks; - -using Dalamud.Interface.Internal; -using Dalamud.Plugin.Internal.Types; -using Dalamud.Utility; - -using Serilog; - -namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; - -/// Represents the use of future as the icon of a notification. -/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. -internal class TextureWrapTaskIconSource : INotificationIconSource.IInternal -{ - /// Gets the default materialized icon, for the purpose of displaying the plugin icon. - internal static readonly INotificationMaterializedIcon DefaultMaterializedIcon = new MaterializedIcon(null); - - /// Initializes a new instance of the class. - /// The function. - public TextureWrapTaskIconSource(Func?>? taskFunc) => - this.TextureWrapTaskFunc = taskFunc; - - /// Gets the function that returns a task resulting in a new instance of . - /// - /// Dalamud will take ownership of the result. Do not call . - public Func?>? TextureWrapTaskFunc { get; } - - /// - public INotificationIconSource Clone() => this; - - /// - public void Dispose() - { - } - - /// - public INotificationMaterializedIcon Materialize() => - new MaterializedIcon(this.TextureWrapTaskFunc); - - private sealed class MaterializedIcon : INotificationMaterializedIcon - { - private Task? task; - - public MaterializedIcon(Func?>? taskFunc) - { - try - { - this.task = taskFunc?.Invoke(); - } - catch (Exception e) - { - Log.Error(e, $"{nameof(TextureWrapTaskIconSource)}: failed to materialize the icon texture."); - this.task = null; - } - } - - public void Dispose() - { - this.task?.ToContentDisposedTask(true); - this.task = null; - } - - public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => - NotificationUtilities.DrawTexture( - this.task?.IsCompletedSuccessfully is true ? this.task.Result : null, - minCoord, - maxCoord, - initiatorPlugin); - } -} diff --git a/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs similarity index 51% rename from Dalamud/Interface/ImGuiNotification/NotificationConstants.cs rename to Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs index d02ff47f5..f88eac53a 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs @@ -1,12 +1,14 @@ using System.Diagnostics; using System.Numerics; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Utility; -namespace Dalamud.Interface.ImGuiNotification; +namespace Dalamud.Interface.ImGuiNotification.Internal; /// Constants for drawing notification windows. -public static class NotificationConstants +internal static class NotificationConstants { // .............................[..] // ..when.......................[XX] @@ -20,69 +22,74 @@ public static class NotificationConstants // .. action buttons .. // ................................. - /// Default duration of the notification. - public static readonly TimeSpan DefaultDisplayDuration = TimeSpan.FromSeconds(3); - - /// Default duration of the notification, after the mouse cursor leaves the notification window. - public static readonly TimeSpan DefaultHoverExtendDuration = TimeSpan.FromSeconds(3); - /// The string to show in place of this_plugin if the notification is shown by Dalamud. - internal const string DefaultInitiator = "Dalamud"; + public const string DefaultInitiator = "Dalamud"; + + /// The string to measure size of, to decide the width of notification windows. + public const string NotificationWidthMeasurementString = + "The width of this text will decide the width\n" + + "of the notification window."; + + /// The ratio of maximum notification window width w.r.t. main viewport width. + public const float MaxNotificationWindowWidthWrtMainViewportWidth = 2f / 3; /// The size of the icon. - internal const float IconSize = 32; + public const float IconSize = 32; /// The background opacity of a notification window. - internal const float BackgroundOpacity = 0.82f; + public const float BackgroundOpacity = 0.82f; /// The duration of indeterminate progress bar loop in milliseconds. - internal const float IndeterminateProgressbarLoopDuration = 2000f; + public const float IndeterminateProgressbarLoopDuration = 2000f; /// The duration of the progress wave animation in milliseconds. - internal const float ProgressWaveLoopDuration = 2000f; + public const float ProgressWaveLoopDuration = 2000f; /// The time ratio of a progress wave loop where the animation is idle. - internal const float ProgressWaveIdleTimeRatio = 0.5f; + public const float ProgressWaveIdleTimeRatio = 0.5f; /// The time ratio of a non-idle portion of the progress wave loop where the color is the most opaque. /// - internal const float ProgressWaveLoopMaxColorTimeRatio = 0.7f; + public const float ProgressWaveLoopMaxColorTimeRatio = 0.7f; + + /// Default duration of the notification. + public static readonly TimeSpan DefaultDuration = TimeSpan.FromSeconds(3); /// Duration of show animation. - internal static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300); + public static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300); /// Duration of hide animation. - internal static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300); + public static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300); /// Duration of progress change animation. - internal static readonly TimeSpan ProgressChangeAnimationDuration = TimeSpan.FromMilliseconds(200); + public static readonly TimeSpan ProgressChangeAnimationDuration = TimeSpan.FromMilliseconds(200); /// Duration of expando animation. - internal static readonly TimeSpan ExpandoAnimationDuration = TimeSpan.FromMilliseconds(300); + public static readonly TimeSpan ExpandoAnimationDuration = TimeSpan.FromMilliseconds(300); /// Text color for the rectangular border when the notification is focused. - internal static readonly Vector4 FocusBorderColor = new(0.4f, 0.4f, 0.4f, 1f); + public static readonly Vector4 FocusBorderColor = new(0.4f, 0.4f, 0.4f, 1f); /// Text color for the when. - internal static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f); + public static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f); /// Text color for the close button [X]. - internal static readonly Vector4 CloseTextColor = new(0.8f, 0.8f, 0.8f, 1f); + public static readonly Vector4 CloseTextColor = new(0.8f, 0.8f, 0.8f, 1f); /// Text color for the title. - internal static readonly Vector4 TitleTextColor = new(1f, 1f, 1f, 1f); + public static readonly Vector4 TitleTextColor = new(1f, 1f, 1f, 1f); /// Text color for the name of the initiator. - internal static readonly Vector4 BlameTextColor = new(0.8f, 0.8f, 0.8f, 1f); + public static readonly Vector4 BlameTextColor = new(0.8f, 0.8f, 0.8f, 1f); /// Text color for the body. - internal static readonly Vector4 BodyTextColor = new(0.9f, 0.9f, 0.9f, 1f); + public static readonly Vector4 BodyTextColor = new(0.9f, 0.9f, 0.9f, 1f); /// Color for the background progress bar (determinate progress only). - internal static readonly Vector4 BackgroundProgressColorMax = new(1f, 1f, 1f, 0.1f); + public static readonly Vector4 BackgroundProgressColorMax = new(1f, 1f, 1f, 0.1f); /// Color for the background progress bar (determinate progress only). - internal static readonly Vector4 BackgroundProgressColorMin = new(1f, 1f, 1f, 0.05f); + public static readonly Vector4 BackgroundProgressColorMin = new(1f, 1f, 1f, 0.05f); /// Gets the relative time format strings. private static readonly (TimeSpan MinSpan, string? FormatString)[] RelativeFormatStrings = @@ -110,35 +117,35 @@ public static class NotificationConstants }; /// Gets the scaled padding of the window (dot(.) in the above diagram). - internal static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale); + public static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale); /// Gets the distance from the right bottom border of the viewport /// to the right bottom border of a notification window. /// - internal static float ScaledViewportEdgeMargin => MathF.Round(20 * ImGuiHelpers.GlobalScale); + public static float ScaledViewportEdgeMargin => MathF.Round(20 * ImGuiHelpers.GlobalScale); /// Gets the scaled gap between two notification windows. - internal static float ScaledWindowGap => MathF.Round(10 * ImGuiHelpers.GlobalScale); + public static float ScaledWindowGap => MathF.Round(10 * ImGuiHelpers.GlobalScale); /// Gets the scaled gap between components. - internal static float ScaledComponentGap => MathF.Round(5 * ImGuiHelpers.GlobalScale); + public static float ScaledComponentGap => MathF.Round(5 * ImGuiHelpers.GlobalScale); /// Gets the scaled size of the icon. - internal static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale); + public static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale); /// Gets the height of the expiry progress bar. - internal static float ScaledExpiryProgressBarHeight => MathF.Round(3 * ImGuiHelpers.GlobalScale); + public static float ScaledExpiryProgressBarHeight => MathF.Round(3 * ImGuiHelpers.GlobalScale); /// Gets the thickness of the focus indicator rectangle. - internal static float FocusIndicatorThickness => MathF.Round(3 * ImGuiHelpers.GlobalScale); + public static float FocusIndicatorThickness => MathF.Round(3 * ImGuiHelpers.GlobalScale); /// Gets the string format of the initiator name field, if the initiator is unloaded. - internal static string UnloadedInitiatorNameFormat => "{0} (unloaded)"; + public static string UnloadedInitiatorNameFormat => "{0} (unloaded)"; /// Formats an instance of as a relative time. /// When. /// The formatted string. - internal static string FormatRelativeDateTime(this DateTime when) + public static string FormatRelativeDateTime(this DateTime when) { var ts = DateTime.Now - when; foreach (var (minSpan, formatString) in RelativeFormatStrings) @@ -156,12 +163,12 @@ public static class NotificationConstants /// Formats an instance of as an absolute time. /// When. /// The formatted string. - internal static string FormatAbsoluteDateTime(this DateTime when) => $"{when:G}"; + public static string FormatAbsoluteDateTime(this DateTime when) => $"{when:G}"; /// Formats an instance of as a relative time. /// When. /// The formatted string. - internal static string FormatRelativeDateTimeShort(this DateTime when) + public static string FormatRelativeDateTimeShort(this DateTime when) { var ts = DateTime.Now - when; foreach (var (minSpan, formatString) in RelativeFormatStringsShort) @@ -174,4 +181,43 @@ public static class NotificationConstants Debug.Assert(false, "must not reach here"); return "???"; } + + /// Gets the color corresponding to the notification type. + /// The notification type. + /// The corresponding color. + public static Vector4 ToColor(this NotificationType type) => type switch + { + NotificationType.None => ImGuiColors.DalamudWhite, + NotificationType.Success => ImGuiColors.HealerGreen, + NotificationType.Warning => ImGuiColors.DalamudOrange, + NotificationType.Error => ImGuiColors.DalamudRed, + NotificationType.Info => ImGuiColors.TankBlue, + _ => ImGuiColors.DalamudWhite, + }; + + /// Gets the char value corresponding to the notification type. + /// The notification type. + /// The corresponding char, or null. + public static char ToChar(this NotificationType type) => type switch + { + NotificationType.None => '\0', + NotificationType.Success => FontAwesomeIcon.CheckCircle.ToIconChar(), + NotificationType.Warning => FontAwesomeIcon.ExclamationCircle.ToIconChar(), + NotificationType.Error => FontAwesomeIcon.TimesCircle.ToIconChar(), + NotificationType.Info => FontAwesomeIcon.InfoCircle.ToIconChar(), + _ => '\0', + }; + + /// Gets the localized title string corresponding to the notification type. + /// The notification type. + /// The corresponding title. + public static string? ToTitle(this NotificationType type) => type switch + { + NotificationType.None => null, + NotificationType.Success => NotificationType.Success.ToString(), + NotificationType.Warning => NotificationType.Warning.ToString(), + NotificationType.Error => NotificationType.Error.ToString(), + NotificationType.Info => NotificationType.Info.ToString(), + _ => null, + }; } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FilePathNotificationIcon.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FilePathNotificationIcon.cs new file mode 100644 index 000000000..3aa712160 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FilePathNotificationIcon.cs @@ -0,0 +1,34 @@ +using System.IO; +using System.Numerics; + +using Dalamud.Interface.Internal; + +namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon; + +/// Represents the use of a texture from a file as the icon of a notification. +/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. +internal class FilePathNotificationIcon : INotificationIcon +{ + private readonly FileInfo fileInfo; + + /// Initializes a new instance of the class. + /// The path to a .tex file inside the game resources. + public FilePathNotificationIcon(string filePath) => this.fileInfo = new(filePath); + + /// + public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) => + NotificationUtilities.DrawIconFrom( + minCoord, + maxCoord, + Service.Get().GetTextureFromFile(this.fileInfo)); + + /// + public override bool Equals(object? obj) => + obj is FilePathNotificationIcon r && r.fileInfo.FullName == this.fileInfo.FullName; + + /// + public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.fileInfo.FullName); + + /// + public override string ToString() => $"{nameof(FilePathNotificationIcon)}({this.fileInfo.FullName})"; +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FontAwesomeIconNotificationIcon.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FontAwesomeIconNotificationIcon.cs new file mode 100644 index 000000000..0acfdee4c --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FontAwesomeIconNotificationIcon.cs @@ -0,0 +1,31 @@ +using System.Numerics; + +namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon; + +/// Represents the use of as the icon of a notification. +internal class FontAwesomeIconNotificationIcon : INotificationIcon +{ + private readonly char iconChar; + + /// Initializes a new instance of the class. + /// The character. + public FontAwesomeIconNotificationIcon(FontAwesomeIcon iconChar) => this.iconChar = (char)iconChar; + + /// + public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) => + NotificationUtilities.DrawIconFrom( + minCoord, + maxCoord, + this.iconChar, + Service.Get().IconFontAwesomeFontHandle, + color); + + /// + public override bool Equals(object? obj) => obj is FontAwesomeIconNotificationIcon r && r.iconChar == this.iconChar; + + /// + public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.iconChar); + + /// + public override string ToString() => $"{nameof(FontAwesomeIconNotificationIcon)}({this.iconChar})"; +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs new file mode 100644 index 000000000..c1db8820c --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs @@ -0,0 +1,34 @@ +using System.Numerics; + +using Dalamud.Interface.Internal; +using Dalamud.Plugin.Services; + +namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon; + +/// Represents the use of a game-shipped texture as the icon of a notification. +/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. +internal class GamePathNotificationIcon : INotificationIcon +{ + private readonly string gamePath; + + /// Initializes a new instance of the class. + /// The path to a .tex file inside the game resources. + /// Use to get the game path from icon IDs. + public GamePathNotificationIcon(string gamePath) => this.gamePath = gamePath; + + /// + public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) => + NotificationUtilities.DrawIconFrom( + minCoord, + maxCoord, + Service.Get().GetTextureFromGame(this.gamePath)); + + /// + public override bool Equals(object? obj) => obj is GamePathNotificationIcon r && r.gamePath == this.gamePath; + + /// + public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.gamePath); + + /// + public override string ToString() => $"{nameof(GamePathNotificationIcon)}({this.gamePath})"; +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/SeIconCharNotificationIcon.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/SeIconCharNotificationIcon.cs new file mode 100644 index 000000000..3bbd8dd81 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/SeIconCharNotificationIcon.cs @@ -0,0 +1,33 @@ +using System.Numerics; + +using Dalamud.Game.Text; + +namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon; + +/// Represents the use of as the icon of a notification. +internal class SeIconCharNotificationIcon : INotificationIcon +{ + private readonly SeIconChar iconChar; + + /// Initializes a new instance of the class. + /// The character. + public SeIconCharNotificationIcon(SeIconChar c) => this.iconChar = c; + + /// + public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) => + NotificationUtilities.DrawIconFrom( + minCoord, + maxCoord, + (char)this.iconChar, + Service.Get().IconAxisFontHandle, + color); + + /// + public override bool Equals(object? obj) => obj is SeIconCharNotificationIcon r && r.iconChar == this.iconChar; + + /// + public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.iconChar); + + /// + public override string ToString() => $"{nameof(SeIconCharNotificationIcon)}({this.iconChar})"; +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs index b457539a3..5ee9fed3e 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs @@ -11,6 +11,8 @@ using Dalamud.IoC.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; +using ImGuiNET; + namespace Dalamud.Interface.ImGuiNotification.Internal; /// Class handling notifications/toasts in ImGui. @@ -41,6 +43,7 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos /// Gets the handle to FontAwesome fonts, sized for use as an icon. public IFontHandle IconFontAwesomeFontHandle { get; } + /// Gets the private atlas for use with notification windows. private IFontAtlas PrivateAtlas { get; } /// @@ -48,17 +51,16 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos { this.PrivateAtlas.Dispose(); foreach (var n in this.pendingNotifications) - n.Dispose(); + n.DisposeInternal(); foreach (var n in this.notifications) - n.Dispose(); + n.DisposeInternal(); this.pendingNotifications.Clear(); this.notifications.Clear(); } /// - public IActiveNotification AddNotification(Notification notification, bool disposeNotification = true) + public IActiveNotification AddNotification(Notification notification) { - using var disposer = disposeNotification ? notification : null; var an = new ActiveNotification(notification, null); this.pendingNotifications.Add(an); return an; @@ -66,13 +68,10 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos /// Adds a notification originating from a plugin. /// The notification. - /// Dispose when this function returns. /// The source plugin. /// The added notification. - /// will be honored even on exceptions. - public IActiveNotification AddNotification(Notification notification, bool disposeNotification, LocalPlugin plugin) + public IActiveNotification AddNotification(Notification notification, LocalPlugin plugin) { - using var disposer = disposeNotification ? notification : null; var an = new ActiveNotification(notification, plugin); this.pendingNotifications.Add(an); return an; @@ -92,8 +91,7 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos Content = content, Title = title, Type = type, - }, - true); + }); /// Draw all currently queued notifications. public void Draw() @@ -104,19 +102,14 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos while (this.pendingNotifications.TryTake(out var newNotification)) this.notifications.Add(newNotification); - var maxWidth = Math.Max(320 * ImGuiHelpers.GlobalScale, viewportSize.X / 3); + var width = ImGui.CalcTextSize(NotificationConstants.NotificationWidthMeasurementString).X; + width += NotificationConstants.ScaledWindowPadding * 3; + width += NotificationConstants.ScaledIconSize; + width = Math.Min(width, viewportSize.X * NotificationConstants.MaxNotificationWindowWidthWrtMainViewportWidth); - this.notifications.RemoveAll( - static x => - { - if (!x.UpdateAnimations()) - return false; - - x.Dispose(); - return true; - }); + this.notifications.RemoveAll(static x => x.UpdateOrDisposeInternal()); foreach (var tn in this.notifications) - height += tn.Draw(maxWidth, height) + NotificationConstants.ScaledWindowGap; + height += tn.Draw(width, height) + NotificationConstants.ScaledWindowGap; } } @@ -140,9 +133,9 @@ internal class NotificationManagerPluginScoped : INotificationManager, IServiceT this.localPlugin = localPlugin; /// - public IActiveNotification AddNotification(Notification notification, bool disposeNotification = true) + public IActiveNotification AddNotification(Notification notification) { - var an = this.notificationManagerService.AddNotification(notification, disposeNotification, this.localPlugin); + var an = this.notificationManagerService.AddNotification(notification, this.localPlugin); _ = this.notifications.TryAdd(an, 0); an.Dismiss += (a, unused) => this.notifications.TryRemove(an, out _); return an; diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index 33a3ad974..612533cb8 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -1,5 +1,4 @@ -using System.Threading; - +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; @@ -7,20 +6,10 @@ namespace Dalamud.Interface.ImGuiNotification; /// Represents a blueprint for a notification. public sealed record Notification : INotification { - private INotificationIconSource? iconSource; - - /// Initializes a new instance of the class. - public Notification() - { - } - - /// Initializes a new instance of the class. - /// The instance of to copy from. - public Notification(INotification notification) => this.CopyValuesFrom(notification); - - /// Initializes a new instance of the class. - /// The instance of to copy from. - public Notification(Notification notification) => this.CopyValuesFrom(notification); + /// + /// Gets the default value for and . + /// + public static TimeSpan DefaultDuration => NotificationConstants.DefaultDuration; /// public string Content { get; set; } = string.Empty; @@ -35,25 +24,16 @@ public sealed record Notification : INotification public NotificationType Type { get; set; } = NotificationType.None; /// - public INotificationIconSource? IconSource - { - get => this.iconSource; - set - { - var prevSource = Interlocked.Exchange(ref this.iconSource, value); - if (prevSource != value) - prevSource?.Dispose(); - } - } + public INotificationIcon? Icon { get; set; } /// public DateTime HardExpiry { get; set; } = DateTime.MaxValue; /// - public TimeSpan InitialDuration { get; set; } = NotificationConstants.DefaultDisplayDuration; + public TimeSpan InitialDuration { get; set; } = DefaultDuration; /// - public TimeSpan DurationSinceLastInterest { get; set; } = NotificationConstants.DefaultHoverExtendDuration; + public TimeSpan ExtensionDurationSinceLastInterest { get; set; } = DefaultDuration; /// public bool ShowIndeterminateIfNoExpiry { get; set; } = true; @@ -66,29 +46,4 @@ public sealed record Notification : INotification /// public float Progress { get; set; } = 1f; - - /// - public void Dispose() - { - // Assign to the property; it will take care of disposing - this.IconSource = null; - } - - /// Copy values from the given instance of . - /// The instance of to copy from. - private void CopyValuesFrom(INotification copyFrom) - { - this.Content = copyFrom.Content; - this.Title = copyFrom.Title; - this.MinimizedText = copyFrom.MinimizedText; - this.Type = copyFrom.Type; - this.IconSource = copyFrom.IconSource?.Clone(); - this.HardExpiry = copyFrom.HardExpiry; - this.InitialDuration = copyFrom.InitialDuration; - this.DurationSinceLastInterest = copyFrom.DurationSinceLastInterest; - this.ShowIndeterminateIfNoExpiry = copyFrom.ShowIndeterminateIfNoExpiry; - this.Minimized = copyFrom.Minimized; - this.UserDismissable = copyFrom.UserDismissable; - this.Progress = copyFrom.Progress; - } } diff --git a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs index 016e9b793..e82b95b75 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs @@ -17,44 +17,47 @@ namespace Dalamud.Interface.ImGuiNotification; /// Utilities for implementing stuff under . public static class NotificationUtilities { - /// + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource ToIconSource(this SeIconChar iconChar) => - INotificationIconSource.From(iconChar); + public static INotificationIcon ToIconSource(this SeIconChar iconChar) => + INotificationIcon.From(iconChar); - /// + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource ToIconSource(this FontAwesomeIcon iconChar) => - INotificationIconSource.From(iconChar); + public static INotificationIcon ToIconSource(this FontAwesomeIcon iconChar) => + INotificationIcon.From(iconChar); - /// + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource ToIconSource(this IDalamudTextureWrap? wrap, bool takeOwnership = true) => - INotificationIconSource.From(wrap, takeOwnership); + public static INotificationIcon ToIconSource(this FileInfo fileInfo) => + INotificationIcon.FromFile(fileInfo.FullName); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource ToIconSource(this FileInfo fileInfo) => - INotificationIconSource.FromFile(fileInfo.FullName); - - /// Draws an icon string. - /// The font handle to use. - /// The icon character. + /// Draws an icon from an and a . /// The coordinates of the top left of the icon area. /// The coordinates of the bottom right of the icon area. + /// The icon character. + /// The font handle to use. /// The foreground color. - internal static unsafe void DrawIconString( - IFontHandle fontHandleLarge, - char c, + /// true if anything has been drawn. + internal static unsafe bool DrawIconFrom( Vector2 minCoord, Vector2 maxCoord, + char c, + IFontHandle fontHandle, Vector4 color) { + if (c is '\0' or char.MaxValue) + return false; + var smallerDim = Math.Max(maxCoord.Y - minCoord.Y, maxCoord.X - minCoord.X); - using (fontHandleLarge.Push()) + using (fontHandle.Push()) { var font = ImGui.GetFont(); - ref readonly var glyph = ref *(ImGuiHelpers.ImFontGlyphReal*)font.FindGlyph(c).NativePtr; + var glyphPtr = (ImGuiHelpers.ImFontGlyphReal*)font.FindGlyphNoFallback(c).NativePtr; + if (glyphPtr is null) + return false; + + ref readonly var glyph = ref *glyphPtr; var size = glyph.XY1 - glyph.XY0; var smallerSizeDim = Math.Min(size.X, size.Y); var scale = smallerSizeDim > smallerDim ? smallerDim / smallerSizeDim : 1f; @@ -69,67 +72,72 @@ public static class NotificationUtilities glyph.UV1, ImGui.GetColorU32(color with { W = color.W * ImGui.GetStyle().Alpha })); } + + return true; } - /// Draws the given texture, or the icon of the plugin if texture is null. - /// The texture. + /// Draws an icon from an instance of . /// The coordinates of the top left of the icon area. /// The coordinates of the bottom right of the icon area. - /// The initiator plugin. - internal static void DrawTexture( - IDalamudTextureWrap? texture, - Vector2 minCoord, - Vector2 maxCoord, - LocalPlugin? initiatorPlugin) + /// The texture. + /// true if anything has been drawn. + internal static bool DrawIconFrom(Vector2 minCoord, Vector2 maxCoord, IDalamudTextureWrap? texture) { - var handle = nint.Zero; - var size = Vector2.Zero; - if (texture is not null) + if (texture is null) + return false; + try { - try + var handle = texture.ImGuiHandle; + var size = texture.Size; + if (size.X > maxCoord.X - minCoord.X) + size *= (maxCoord.X - minCoord.X) / size.X; + if (size.Y > maxCoord.Y - minCoord.Y) + size *= (maxCoord.Y - minCoord.Y) / size.Y; + ImGui.SetCursorPos(((minCoord + maxCoord) - size) / 2); + ImGui.Image(handle, size); + return true; + } + catch + { + return false; + } + } + + /// Draws an icon from an instance of . + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + /// The plugin. Dalamud icon will be drawn if null is given. + /// true if anything has been drawn. + internal static bool DrawIconFrom(Vector2 minCoord, Vector2 maxCoord, LocalPlugin? plugin) + { + var dam = Service.Get(); + if (plugin is null) + return false; + + if (!Service.Get().TryGetIcon( + plugin, + plugin.Manifest, + plugin.IsThirdParty, + out var texture) || texture is null) + { + texture = plugin switch { - handle = texture.ImGuiHandle; - size = texture.Size; - } - catch - { - // must have been disposed or something; ignore the texture - } + { IsDev: true } => dam.GetDalamudTextureWrap(DalamudAsset.DevPluginIcon), + { IsThirdParty: true } => dam.GetDalamudTextureWrap(DalamudAsset.ThirdInstalledIcon), + _ => dam.GetDalamudTextureWrap(DalamudAsset.InstalledIcon), + }; } - if (handle == nint.Zero) - { - var dam = Service.Get(); - if (initiatorPlugin is null) - { - texture = dam.GetDalamudTextureWrap(DalamudAsset.LogoSmall); - } - else - { - if (!Service.Get().TryGetIcon( - initiatorPlugin, - initiatorPlugin.Manifest, - initiatorPlugin.IsThirdParty, - out texture) || texture is null) - { - texture = initiatorPlugin switch - { - { IsDev: true } => dam.GetDalamudTextureWrap(DalamudAsset.DevPluginIcon), - { IsThirdParty: true } => dam.GetDalamudTextureWrap(DalamudAsset.ThirdInstalledIcon), - _ => dam.GetDalamudTextureWrap(DalamudAsset.InstalledIcon), - }; - } - } + return DrawIconFrom(minCoord, maxCoord, texture); + } - handle = texture.ImGuiHandle; - size = texture.Size; - } - - if (size.X > maxCoord.X - minCoord.X) - size *= (maxCoord.X - minCoord.X) / size.X; - if (size.Y > maxCoord.Y - minCoord.Y) - size *= (maxCoord.Y - minCoord.Y) / size.Y; - ImGui.SetCursorPos(((minCoord + maxCoord) - size) / 2); - ImGui.Image(handle, size); + /// Draws the Dalamud logo as an icon. + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + internal static void DrawIconFromDalamudLogo(Vector2 minCoord, Vector2 maxCoord) + { + var dam = Service.Get(); + var texture = dam.GetDalamudTextureWrap(DalamudAsset.LogoSmall); + DrawIconFrom(minCoord, maxCoord, texture); } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index dcd193496..6c94a2273 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -116,7 +116,7 @@ internal class ImGuiWidget : IDataWindowWidget NotificationTemplate.InitialDurationTitles.Length); ImGui.Combo( - "Hover Extend Duration", + "Extension Duration", ref this.notificationTemplate.HoverExtendDurationInt, NotificationTemplate.HoverExtendDurationTitles, NotificationTemplate.HoverExtendDurationTitles.Length); @@ -166,7 +166,7 @@ internal class ImGuiWidget : IDataWindowWidget this.notificationTemplate.InitialDurationInt == 0 ? TimeSpan.MaxValue : NotificationTemplate.Durations[this.notificationTemplate.InitialDurationInt], - DurationSinceLastInterest = + ExtensionDurationSinceLastInterest = this.notificationTemplate.HoverExtendDurationInt == 0 ? TimeSpan.Zero : NotificationTemplate.Durations[this.notificationTemplate.HoverExtendDurationInt], @@ -179,41 +179,40 @@ internal class ImGuiWidget : IDataWindowWidget 4 => -1f, _ => 0.5f, }, - IconSource = this.notificationTemplate.IconSourceInt switch + Icon = this.notificationTemplate.IconSourceInt switch { - 1 => INotificationIconSource.From( + 1 => INotificationIcon.From( (SeIconChar)(this.notificationTemplate.IconSourceText.Length == 0 ? 0 : this.notificationTemplate.IconSourceText[0])), - 2 => INotificationIconSource.From( + 2 => INotificationIcon.From( (FontAwesomeIcon)(this.notificationTemplate.IconSourceText.Length == 0 ? 0 : this.notificationTemplate.IconSourceText[0])), - 3 => INotificationIconSource.From( - Service.Get().GetDalamudTextureWrap( - Enum.Parse( - NotificationTemplate.AssetSources[ - this.notificationTemplate.IconSourceAssetInt])), - false), - 4 => INotificationIconSource.From( - () => - Service.Get().GetDalamudTextureWrapAsync( - Enum.Parse( - NotificationTemplate.AssetSources[ - this.notificationTemplate.IconSourceAssetInt]))), - 5 => INotificationIconSource.FromGame(this.notificationTemplate.IconSourceText), - 6 => INotificationIconSource.FromFile(this.notificationTemplate.IconSourceText), - 7 => INotificationIconSource.From( - Service.Get().GetTextureFromGame(this.notificationTemplate.IconSourceText), - false), - 8 => INotificationIconSource.From( - Service.Get().GetTextureFromFile( - new(this.notificationTemplate.IconSourceText)), - false), + 3 => INotificationIcon.FromGame(this.notificationTemplate.IconSourceText), + 4 => INotificationIcon.FromFile(this.notificationTemplate.IconSourceText), _ => null, }, - }, - true); + }); + + var dam = Service.Get(); + var tm = Service.Get(); + switch (this.notificationTemplate.IconSourceInt) + { + case 5: + n.SetIconTexture( + dam.GetDalamudTextureWrap( + Enum.Parse( + NotificationTemplate.AssetSources[this.notificationTemplate.IconSourceAssetInt]))); + break; + case 6: + n.SetIconTexture(tm.GetTextureFromGame(this.notificationTemplate.IconSourceText)); + break; + case 7: + n.SetIconTexture(tm.GetTextureFromFile(new(this.notificationTemplate.IconSourceText))); + break; + } + switch (this.notificationTemplate.ProgressMode) { case 2: @@ -237,8 +236,8 @@ internal class ImGuiWidget : IDataWindowWidget n.Progress = i / 10f; } - n.ExtendBy(NotificationConstants.DefaultDisplayDuration); - n.InitialDuration = NotificationConstants.DefaultDisplayDuration; + n.ExtendBy(NotificationConstants.DefaultDuration); + n.InitialDuration = NotificationConstants.DefaultDuration; }); break; } @@ -251,6 +250,10 @@ internal class ImGuiWidget : IDataWindowWidget n.Click += _ => nclick++; n.DrawActions += an => { + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted($"{nclick}"); + + ImGui.SameLine(); if (ImGui.Button("Update")) { NewRandom(out title, out type, out progress); @@ -260,18 +263,11 @@ internal class ImGuiWidget : IDataWindowWidget } ImGui.SameLine(); - ImGui.InputText("##input", ref testString, 255); - - if (an.IsHovered) - { - ImGui.SameLine(); - if (ImGui.Button("Dismiss")) - an.DismissNow(); - } - - ImGui.AlignTextToFramePadding(); + if (ImGui.Button("Dismiss")) + an.DismissNow(); + ImGui.SameLine(); - ImGui.TextUnformatted($"Clicked {nclick} time(s)"); + ImGui.InputText("##input", ref testString, 255); }; } } @@ -315,10 +311,9 @@ internal class ImGuiWidget : IDataWindowWidget "None (use Type)", "SeIconChar", "FontAwesomeIcon", - "TextureWrap from DalamudAssets", - "TextureWrapTask from DalamudAssets", "GamePath", "FilePath", + "TextureWrap from DalamudAssets", "TextureWrap from GamePath", "TextureWrap from FilePath", }; @@ -367,7 +362,7 @@ internal class ImGuiWidget : IDataWindowWidget { TimeSpan.Zero, TimeSpan.FromSeconds(1), - NotificationConstants.DefaultDisplayDuration, + NotificationConstants.DefaultDuration, TimeSpan.FromSeconds(10), }; diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 417d77e7d..3a90d52c1 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -581,7 +581,6 @@ public sealed class UiBuilder : IDisposable Type = type, InitialDuration = TimeSpan.FromMilliseconds(msDelay), }, - true, this.localPlugin); _ = this.notifications.TryAdd(an, 0); an.Dismiss += (a, unused) => this.notifications.TryRemove(an, out _); diff --git a/Dalamud/Plugin/Services/INotificationManager.cs b/Dalamud/Plugin/Services/INotificationManager.cs index 441cc31f7..7d9ccd0b0 100644 --- a/Dalamud/Plugin/Services/INotificationManager.cs +++ b/Dalamud/Plugin/Services/INotificationManager.cs @@ -2,21 +2,11 @@ using Dalamud.Interface.ImGuiNotification; namespace Dalamud.Plugin.Services; -/// -/// Manager for notifications provided by Dalamud using ImGui. -/// +/// Manager for notifications provided by Dalamud using ImGui. public interface INotificationManager { - /// - /// Adds a notification. - /// + /// Adds a notification. /// The new notification. - /// - /// Dispose when this function returns, even if the function throws an exception. - /// Set to false to reuse for multiple calls to this function, in which case, - /// you should call on the value supplied to at a - /// later time. - /// /// The added notification. - IActiveNotification AddNotification(Notification notification, bool disposeNotification = true); + IActiveNotification AddNotification(Notification notification); }