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);
}