This commit is contained in:
Soreepeong 2024-02-27 23:20:08 +09:00
parent e44180d4a2
commit a7d5380796
24 changed files with 1056 additions and 1412 deletions

View file

@ -1,5 +1,7 @@
using System.Threading;
using Dalamud.Interface.Internal;
namespace Dalamud.Interface.ImGuiNotification;
/// <summary>Represents an active notification.</summary>
@ -20,20 +22,6 @@ public interface IActiveNotification : INotification
/// </remarks>
event Action<IActiveNotification> Click;
/// <summary>Invoked when the mouse enters the notification window.</summary>
/// <remarks>
/// Note that this function may be called even after <see cref="Dismiss"/> has been invoked.
/// Refer to <see cref="IsDismissed"/>.
/// </remarks>
event Action<IActiveNotification> MouseEnter;
/// <summary>Invoked when the mouse leaves the notification window.</summary>
/// <remarks>
/// Note that this function may be called even after <see cref="Dismiss"/> has been invoked.
/// Refer to <see cref="IsDismissed"/>.
/// </remarks>
event Action<IActiveNotification> MouseLeave;
/// <summary>Invoked upon drawing the action bar of the notification.</summary>
/// <remarks>
/// Note that this function may be called even after <see cref="Dismiss"/> has been invoked.
@ -44,16 +32,13 @@ public interface IActiveNotification : INotification
/// <summary>Gets the ID of this notification.</summary>
long Id { get; }
/// <summary>Gets the time of creating this notification.</summary>
DateTime CreatedAt { get; }
/// <summary>Gets the effective expiry time.</summary>
/// <remarks>Contains <see cref="DateTime.MaxValue"/> if the notification does not expire.</remarks>
DateTime EffectiveExpiry { get; }
/// <summary>Gets a value indicating whether the mouse cursor is on the notification window.</summary>
bool IsHovered { get; }
/// <summary>Gets a value indicating whether the notification window is focused.</summary>
bool IsFocused { get; }
/// <summary>Gets a value indicating whether the notification has been dismissed.</summary>
/// <remarks>This includes when the hide animation is being played.</remarks>
bool IsDismissed { get; }
@ -66,9 +51,16 @@ public interface IActiveNotification : INotification
/// <remarks>This does not override <see cref="INotification.HardExpiry"/>.</remarks>
void ExtendBy(TimeSpan extension);
/// <summary>Loads the icon again using the same <see cref="INotification.IconSource"/>.</summary>
/// <remarks>If <see cref="IsDismissed"/> is <c>true</c>, then this function is a no-op.</remarks>
void UpdateIcon();
/// <summary>Sets the icon from <see cref="IDalamudTextureWrap"/>, overriding the icon .</summary>
/// <param name="textureWrap">The new texture wrap to use, or null to clear and revert back to the icon specified
/// from <see cref="INotification.Icon"/>.</param>
/// <remarks>
/// <para>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.</para>
/// <para>If <see cref="IsDismissed"/> is <c>true</c>, then calling this function will simply dispose the passed
/// <paramref name="textureWrap"/> without actually updating the icon.</para>
/// </remarks>
void SetIconTexture(IDalamudTextureWrap? textureWrap);
/// <summary>Generates a new value to use for <see cref="Id"/>.</summary>
/// <returns>The new value.</returns>

View file

@ -1,9 +1,10 @@
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Plugin.Services;
namespace Dalamud.Interface.ImGuiNotification;
/// <summary>Represents a notification.</summary>
public interface INotification : IDisposable
public interface INotification
{
/// <summary>Gets or sets the content body of the notification.</summary>
string Content { get; set; }
@ -18,22 +19,13 @@ public interface INotification : IDisposable
NotificationType Type { get; set; }
/// <summary>Gets or sets the icon source.</summary>
/// <remarks>
/// <para>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 <see cref="INotification"/>. <b>Even if the assignment throws an
/// exception</b>, 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.</para>
/// <para>The assigned value will be disposed upon the <see cref="IDisposable.Dispose"/> call on this instance of
/// <see cref="INotification"/>, unless the same value is assigned, in which case it will do nothing.</para>
/// <para>If this <see cref="INotification"/> is an <see cref="IActiveNotification"/>, then updating this property
/// will change the icon being displayed (calls <see cref="IActiveNotification.UpdateIcon"/>), unless
/// <see cref="IActiveNotification.IsDismissed"/> is <c>true</c>.</para>
/// </remarks>
INotificationIconSource? IconSource { get; set; }
/// <remarks>Use <see cref="IActiveNotification.SetIconTexture"/> to use a texture, after calling
/// <see cref="INotificationManager.AddNotification"/>.</remarks>
INotificationIcon? Icon { get; set; }
/// <summary>Gets or sets the hard expiry.</summary>
/// <remarks>
/// Setting this value will override <see cref="InitialDuration"/> and <see cref="DurationSinceLastInterest"/>, in that
/// Setting this value will override <see cref="InitialDuration"/> and <see cref="ExtensionDurationSinceLastInterest"/>, in that
/// the notification will be dismissed when this expiry expires.<br />
/// Set to <see cref="DateTime.MaxValue"/> to make only <see cref="InitialDuration"/> take effect.<br />
/// If neither <see cref="HardExpiry"/> nor <see cref="InitialDuration"/> is not MaxValue, then the notification
@ -45,7 +37,8 @@ public interface INotification : IDisposable
/// <summary>Gets or sets the initial duration.</summary>
/// <remarks>Set to <see cref="TimeSpan.MaxValue"/> to make only <see cref="HardExpiry"/> take effect.</remarks>
/// <remarks>Updating this value will reset the dismiss timer.</remarks>
/// <remarks>Updating this value will reset the dismiss timer, but the remaining duration will still be calculated
/// based on <see cref="IActiveNotification.CreatedAt"/>.</remarks>
TimeSpan InitialDuration { get; set; }
/// <summary>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.<br />
/// Updating this value will reset the dismiss timer.
/// </remarks>
TimeSpan DurationSinceLastInterest { get; set; }
TimeSpan ExtensionDurationSinceLastInterest { get; set; }
/// <summary>Gets or sets a value indicating whether to show an indeterminate expiration animation if
/// <see cref="HardExpiry"/> is set to <see cref="DateTime.MaxValue"/>.</summary>

View file

@ -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;
/// <summary>Icon source for <see cref="INotification"/>.</summary>
/// <remarks>Plugins implementing this interface are left to their own on managing the resources contained by the
/// instance of their implementation of <see cref="INotificationIcon"/>. In other words, they should not expect to have
/// <see cref="IDisposable.Dispose"/> called if their implementation is an <see cref="IDisposable"/>. Dalamud will not
/// call <see cref="IDisposable.Dispose"/> on any instance of <see cref="INotificationIcon"/>. On plugin unloads, the
/// icon may be reverted back to the default, if the instance of <see cref="INotificationIcon"/> is not provided by
/// Dalamud.</remarks>
public interface INotificationIcon
{
/// <summary>Gets a new instance of <see cref="INotificationIcon"/> that will source the icon from an
/// <see cref="SeIconChar"/>.</summary>
/// <param name="iconChar">The icon character.</param>
/// <returns>A new instance of <see cref="INotificationIcon"/> that should be disposed after use.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIcon From(SeIconChar iconChar) => new SeIconCharNotificationIcon(iconChar);
/// <summary>Gets a new instance of <see cref="INotificationIcon"/> that will source the icon from an
/// <see cref="FontAwesomeIcon"/>.</summary>
/// <param name="iconChar">The icon character.</param>
/// <returns>A new instance of <see cref="INotificationIcon"/> that should be disposed after use.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIcon From(FontAwesomeIcon iconChar) => new FontAwesomeIconNotificationIcon(iconChar);
/// <summary>Gets a new instance of <see cref="INotificationIcon"/> that will source the icon from a texture
/// file shipped as a part of the game resources.</summary>
/// <param name="gamePath">The path to a texture file in the game virtual file system.</param>
/// <returns>A new instance of <see cref="INotificationIcon"/> that should be disposed after use.</returns>
/// <remarks>If any errors are thrown, the default icon will be displayed instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIcon FromGame(string gamePath) => new GamePathNotificationIcon(gamePath);
/// <summary>Gets a new instance of <see cref="INotificationIcon"/> that will source the icon from an image
/// file from the file system.</summary>
/// <param name="filePath">The path to an image file in the file system.</param>
/// <returns>A new instance of <see cref="INotificationIcon"/> that should be disposed after use.</returns>
/// <remarks>If any errors are thrown, the default icon will be displayed instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIcon FromFile(string filePath) => new FilePathNotificationIcon(filePath);
/// <summary>Draws the icon.</summary>
/// <param name="minCoord">The coordinates of the top left of the icon area.</param>
/// <param name="maxCoord">The coordinates of the bottom right of the icon area.</param>
/// <param name="color">The foreground color.</param>
/// <returns><c>true</c> if anything has been drawn.</returns>
bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color);
}

View file

@ -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;
/// <summary>Icon source for <see cref="INotification"/>.</summary>
/// <remarks>Plugins should NOT implement this interface.</remarks>
public interface INotificationIconSource : ICloneable, IDisposable
{
/// <summary>The internal interface.</summary>
internal interface IInternal : INotificationIconSource
{
/// <summary>Materializes the icon resource.</summary>
/// <returns>The materialized resource.</returns>
INotificationMaterializedIcon Materialize();
}
/// <summary>Gets a new instance of <see cref="INotificationIconSource"/> that will source the icon from an
/// <see cref="SeIconChar"/>.</summary>
/// <param name="iconChar">The icon character.</param>
/// <returns>A new instance of <see cref="INotificationIconSource"/> that should be disposed after use.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIconSource From(SeIconChar iconChar) => new SeIconCharIconSource(iconChar);
/// <summary>Gets a new instance of <see cref="INotificationIconSource"/> that will source the icon from an
/// <see cref="FontAwesomeIcon"/>.</summary>
/// <param name="iconChar">The icon character.</param>
/// <returns>A new instance of <see cref="INotificationIconSource"/> that should be disposed after use.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIconSource From(FontAwesomeIcon iconChar) => new FontAwesomeIconIconSource(iconChar);
/// <summary>Gets a new instance of <see cref="INotificationIconSource"/> that will source the icon from an
/// <see cref="IDalamudTextureWrap"/>.</summary>
/// <param name="wrap">The texture wrap.</param>
/// <param name="takeOwnership">
/// If <c>true</c>, this class will own the passed <paramref name="wrap"/>, and you <b>must not</b> call
/// <see cref="IDisposable.Dispose"/> on the passed wrap.
/// If <c>false</c>, this class will create a new reference of the passed wrap, and you <b>should</b> call
/// <see cref="IDisposable.Dispose"/> on the passed wrap.
/// In both cases, the returned object must be disposed after use.</param>
/// <returns>A new instance of <see cref="INotificationIconSource"/> that should be disposed after use.</returns>
/// <remarks>If any errors are thrown or <paramref name="wrap"/> is <c>null</c>, the default icon will be displayed
/// instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIconSource From(IDalamudTextureWrap? wrap, bool takeOwnership = true) =>
new TextureWrapIconSource(wrap, takeOwnership);
/// <summary>Gets a new instance of <see cref="INotificationIconSource"/> that will source the icon from an
/// <see cref="Func{TResult}"/> returning a <see cref="Task{TResult}"/> resulting in an
/// <see cref="IDalamudTextureWrap"/>.</summary>
/// <param name="wrapTaskFunc">The function that returns a task that results a texture wrap.</param>
/// <returns>A new instance of <see cref="INotificationIconSource"/> that should be disposed after use.</returns>
/// <remarks>If any errors are thrown or <paramref name="wrapTaskFunc"/> is <c>null</c>, the default icon will be
/// displayed instead.<br />
/// Use <see cref="Task.FromResult{TResult}"/> if you will have a wrap available without waiting.<br />
/// <paramref name="wrapTaskFunc"/> should not contain a reference to a resource; if it does, the resource will be
/// released when all instances of <see cref="INotificationIconSource"/> derived from the returned object are freed
/// by the garbage collector, which will result in non-deterministic resource releases.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIconSource From(Func<Task<IDalamudTextureWrap?>?>? wrapTaskFunc) =>
new TextureWrapTaskIconSource(wrapTaskFunc);
/// <summary>Gets a new instance of <see cref="INotificationIconSource"/> that will source the icon from a texture
/// file shipped as a part of the game resources.</summary>
/// <param name="gamePath">The path to a texture file in the game virtual file system.</param>
/// <returns>A new instance of <see cref="INotificationIconSource"/> that should be disposed after use.</returns>
/// <remarks>If any errors are thrown, the default icon will be displayed instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIconSource FromGame(string gamePath) => new GamePathIconSource(gamePath);
/// <summary>Gets a new instance of <see cref="INotificationIconSource"/> that will source the icon from an image
/// file from the file system.</summary>
/// <param name="filePath">The path to an image file in the file system.</param>
/// <returns>A new instance of <see cref="INotificationIconSource"/> that should be disposed after use.</returns>
/// <remarks>If any errors are thrown, the default icon will be displayed instead.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIconSource FromFile(string filePath) => new FilePathIconSource(filePath);
/// <inheritdoc cref="ICloneable.Clone"/>
new INotificationIconSource Clone();
/// <inheritdoc/>
object ICloneable.Clone() => this.Clone();
}

View file

@ -1,16 +0,0 @@
using System.Numerics;
using Dalamud.Plugin.Internal.Types;
namespace Dalamud.Interface.ImGuiNotification;
/// <summary>Represents a materialized icon.</summary>
internal interface INotificationMaterializedIcon : IDisposable
{
/// <summary>Draws the icon.</summary>
/// <param name="minCoord">The coordinates of the top left of the icon area.</param>
/// <param name="maxCoord">The coordinates of the bottom right of the icon area.</param>
/// <param name="color">The foreground color.</param>
/// <param name="initiatorPlugin">The initiator plugin.</param>
void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin);
}

View file

@ -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;
/// <summary>Represents an active notification.</summary>
internal sealed partial class ActiveNotification
{
/// <summary>Draws this notification.</summary>
/// <param name="width">The maximum width of the notification window.</param>
/// <param name="offsetY">The offset from the bottom.</param>
/// <returns>The height of the notification.</returns>
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;
}
/// <summary>Calculates the effective expiry, taking ImGui window state into account.</summary>
/// <param name="warrantsExtension">Notification will not dismiss while this paramter is <c>true</c>.</param>
/// <returns>The calculated effective expiry.</returns>
/// <remarks>Expected to be called BETWEEN <see cref="ImGui.Begin(string)"/> and <see cref="ImGui.End"/>.</remarks>
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<InterfaceManager>.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<NotificationManager>.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();
}
}

View file

@ -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;
/// <summary>Represents an active notification.</summary>
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;
/// <summary>Gets the time of starting to count the timer for the expiration.</summary>
private DateTime lastInterestTime;
/// <summary>Gets the extended expiration time from <see cref="ExtendBy"/>.</summary>
private DateTime extendedExpiry;
/// <summary>The icon texture to use if specified; otherwise, icon will be used from <see cref="Icon"/>.</summary>
private IDalamudTextureWrap? iconTextureWrap;
/// <summary>The plugin that initiated this notification.</summary>
private LocalPlugin? initiatorPlugin;
/// <summary>Whether <see cref="initiatorPlugin"/> has been unloaded.</summary>
private bool isInitiatorUnloaded;
/// <summary>The progress before for the progress bar animation with <see cref="progressEasing"/>.</summary>
private float progressBefore;
@ -36,10 +48,10 @@ internal sealed class ActiveNotification : IActiveNotification
/// <summary>Used for calculating correct dismissal progressbar animation (right edge).</summary>
private float prevProgressR;
/// <summary>New progress value to be updated on next call to <see cref="UpdateAnimations"/>.</summary>
/// <summary>New progress value to be updated on next call to <see cref="UpdateOrDisposeInternal"/>.</summary>
private float? newProgress;
/// <summary>New minimized value to be updated on next call to <see cref="UpdateAnimations"/>.</summary>
/// <summary>New minimized value to be updated on next call to <see cref="UpdateOrDisposeInternal"/>.</summary>
private bool? newMinimized;
/// <summary>Initializes a new instance of the <see cref="ActiveNotification"/> class.</summary>
@ -47,28 +59,16 @@ internal sealed class ActiveNotification : IActiveNotification
/// <param name="initiatorPlugin">The initiator plugin. Use <c>null</c> if originated by Dalamud.</param>
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.");
}
}
/// <inheritdoc/>
@ -80,23 +80,11 @@ internal sealed class ActiveNotification : IActiveNotification
/// <inheritdoc/>
public event Action<IActiveNotification>? DrawActions;
/// <inheritdoc/>
public event Action<IActiveNotification>? MouseEnter;
/// <inheritdoc/>
public event Action<IActiveNotification>? MouseLeave;
/// <inheritdoc/>
public long Id { get; } = IActiveNotification.CreateNewId();
/// <summary>Gets the time of creating this notification.</summary>
public DateTime CreatedAt { get; } = DateTime.Now;
/// <summary>Gets the time of starting to count the timer for the expiration.</summary>
public DateTime LastInterestTime { get; private set; } = DateTime.Now;
/// <summary>Gets the extended expiration time from <see cref="ExtendBy"/>.</summary>
public DateTime ExtendedExpiry { get; private set; } = DateTime.Now;
/// <inheritdoc/>
public DateTime CreatedAt { get; }
/// <inheritdoc/>
public string Content
@ -147,19 +135,14 @@ internal sealed class ActiveNotification : IActiveNotification
}
/// <inheritdoc/>
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;
}
}
/// <inheritdoc/>
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;
}
}
/// <inheritdoc/>
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; }
/// <inheritdoc/>
public bool ShowIndeterminateIfNoExpiry
@ -286,24 +236,9 @@ internal sealed class ActiveNotification : IActiveNotification
}
}
/// <inheritdoc/>
public bool IsHovered { get; private set; }
/// <inheritdoc/>
public bool IsFocused { get; private set; }
/// <inheritdoc/>
public bool IsDismissed => this.hideEasing.IsRunning;
/// <summary>Gets a value indicating whether <see cref="InitiatorPlugin"/> has been unloaded.</summary>
public bool IsInitiatorUnloaded { get; private set; }
/// <summary>Gets or sets the plugin that initiated this notification.</summary>
public LocalPlugin? InitiatorPlugin { get; set; }
/// <summary>Gets or sets the icon of this notification.</summary>
public INotificationMaterializedIcon? MaterializedIcon { get; set; }
/// <summary>Gets the eased progress.</summary>
private float ProgressEased
{
@ -318,61 +253,17 @@ internal sealed class ActiveNotification : IActiveNotification
}
}
/// <summary>Gets the default color of the notification.</summary>
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,
};
/// <summary>Gets the default icon of the notification.</summary>
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,
};
/// <summary>Gets the default title of the notification.</summary>
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,
};
/// <summary>Gets the string for the initiator field.</summary>
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;
/// <summary>Gets the effective text to display when minimized.</summary>
private string EffectiveMinimizedText => (this.MinimizedText ?? this.Content).ReplaceLineEndings(" ");
/// <inheritdoc/>
public void Dispose()
{
this.ClearMaterializedIcon();
this.underlyingNotification.Dispose();
this.Dismiss = null;
this.Click = null;
this.DrawActions = null;
this.InitiatorPlugin = null;
}
/// <inheritdoc/>
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}");
}
}
/// <summary>Updates animations.</summary>
/// <returns><c>true</c> if the notification is over.</returns>
public bool UpdateAnimations()
/// <inheritdoc/>
public void ExtendBy(TimeSpan extension)
{
var newExpiry = DateTime.Now + extension;
if (this.extendedExpiry < newExpiry)
this.extendedExpiry = newExpiry;
}
/// <inheritdoc/>
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();
}
}
/// <summary>Removes non-Dalamud invocation targets from events.</summary>
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>(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;
}
}
/// <summary>Updates the state of this notification, and release the relevant resource if this notification is no
/// longer in use.</summary>
/// <returns><c>true</c> if the notification is over and relevant resources are released.</returns>
/// <remarks>Intended to be called from the main thread only.</remarks>
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;
}
/// <summary>Draws this notification.</summary>
/// <param name="maxWidth">The maximum width of the notification window.</param>
/// <param name="offsetY">The offset from the bottom.</param>
/// <returns>The height of the notification.</returns>
public float Draw(float maxWidth, float offsetY)
/// <summary>Clears the resources associated with this instance of <see cref="ActiveNotification"/>.</summary>
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<InterfaceManager>.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;
}
/// <inheritdoc/>
public void ExtendBy(TimeSpan extension)
{
var newExpiry = DateTime.Now + extension;
if (this.ExtendedExpiry < newExpiry)
this.ExtendedExpiry = newExpiry;
}
/// <inheritdoc/>
public void UpdateIcon()
{
if (this.IsDismissed)
return;
this.ClearMaterializedIcon();
this.MaterializedIcon = (this.IconSource as INotificationIconSource.IInternal)?.Materialize();
}
/// <summary>Removes non-Dalamud invocation targets from events.</summary>
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>(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<NotificationManager>.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;
}
}

View file

@ -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;
/// <summary>Represents the use of a texture from a file as the icon of a notification.</summary>
/// <remarks>If there was no texture loaded for any reason, the plugin icon will be displayed instead.</remarks>
internal class FilePathIconSource : INotificationIconSource.IInternal
{
/// <summary>Initializes a new instance of the <see cref="FilePathIconSource"/> class.</summary>
/// <param name="filePath">The path to a .tex file inside the game resources.</param>
public FilePathIconSource(string filePath) => this.FilePath = filePath;
/// <summary>Gets the path to a .tex file inside the game resources.</summary>
public string FilePath { get; }
/// <inheritdoc/>
public INotificationIconSource Clone() => this;
/// <inheritdoc/>
public void Dispose()
{
}
/// <inheritdoc/>
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<TextureManager>.Get().GetTextureFromFile(this.fileInfo),
minCoord,
maxCoord,
initiatorPlugin);
}
}

View file

@ -1,46 +0,0 @@
using System.Numerics;
using Dalamud.Plugin.Internal.Types;
namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource;
/// <summary>Represents the use of <see cref="FontAwesomeIcon"/> as the icon of a notification.</summary>
internal class FontAwesomeIconIconSource : INotificationIconSource.IInternal
{
/// <summary>Initializes a new instance of the <see cref="FontAwesomeIconIconSource"/> class.</summary>
/// <param name="iconChar">The character.</param>
public FontAwesomeIconIconSource(FontAwesomeIcon iconChar) => this.IconChar = iconChar;
/// <summary>Gets the icon character.</summary>
public FontAwesomeIcon IconChar { get; }
/// <inheritdoc/>
public INotificationIconSource Clone() => this;
/// <inheritdoc/>
public void Dispose()
{
}
/// <inheritdoc/>
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<NotificationManager>.Get().IconFontAwesomeFontHandle,
this.iconChar,
minCoord,
maxCoord,
color);
}
}

View file

@ -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;
/// <summary>Represents the use of a game-shipped texture as the icon of a notification.</summary>
/// <remarks>If there was no texture loaded for any reason, the plugin icon will be displayed instead.</remarks>
internal class GamePathIconSource : INotificationIconSource.IInternal
{
/// <summary>Initializes a new instance of the <see cref="GamePathIconSource"/> class.</summary>
/// <param name="gamePath">The path to a .tex file inside the game resources.</param>
/// <remarks>Use <see cref="ITextureProvider.GetIconPath"/> to get the game path from icon IDs.</remarks>
public GamePathIconSource(string gamePath) => this.GamePath = gamePath;
/// <summary>Gets the path to a .tex file inside the game resources.</summary>
public string GamePath { get; }
/// <inheritdoc/>
public INotificationIconSource Clone() => this;
/// <inheritdoc/>
public void Dispose()
{
}
/// <inheritdoc/>
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<TextureManager>.Get().GetTextureFromGame(this.gamePath),
minCoord,
maxCoord,
initiatorPlugin);
}
}

View file

@ -1,47 +0,0 @@
using System.Numerics;
using Dalamud.Game.Text;
using Dalamud.Plugin.Internal.Types;
namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource;
/// <summary>Represents the use of <see cref="SeIconChar"/> as the icon of a notification.</summary>
internal class SeIconCharIconSource : INotificationIconSource.IInternal
{
/// <summary>Initializes a new instance of the <see cref="SeIconCharIconSource"/> class.</summary>
/// <param name="c">The character.</param>
public SeIconCharIconSource(SeIconChar c) => this.IconChar = c;
/// <summary>Gets the icon character.</summary>
public SeIconChar IconChar { get; }
/// <inheritdoc/>
public INotificationIconSource Clone() => this;
/// <inheritdoc/>
public void Dispose()
{
}
/// <inheritdoc/>
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<NotificationManager>.Get().IconAxisFontHandle,
this.iconChar,
minCoord,
maxCoord,
color);
}
}

View file

@ -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;
/// <summary>Represents the use of future <see cref="IDalamudTextureWrap"/> as the icon of a notification.</summary>
/// <remarks>If there was no texture loaded for any reason, the plugin icon will be displayed instead.</remarks>
internal class TextureWrapIconSource : INotificationIconSource.IInternal
{
private IDalamudTextureWrap? wrap;
/// <summary>Initializes a new instance of the <see cref="TextureWrapIconSource"/> class.</summary>
/// <param name="wrap">The texture wrap to handle over the ownership.</param>
/// <param name="takeOwnership">
/// If <c>true</c>, this class will own the passed <paramref name="wrap"/>, and you <b>must not</b> call
/// <see cref="IDisposable.Dispose"/> on the passed wrap.
/// If <c>false</c>, this class will create a new reference of the passed wrap, and you <b>should</b> call
/// <see cref="IDisposable.Dispose"/> on the passed wrap.
/// In both cases, this class must be disposed after use.</param>
public TextureWrapIconSource(IDalamudTextureWrap? wrap, bool takeOwnership) =>
this.wrap = takeOwnership ? wrap : wrap?.CreateWrapSharingLowLevelResource();
/// <summary>Gets the underlying texture wrap.</summary>
public IDalamudTextureWrap? Wrap => this.wrap;
/// <inheritdoc/>
public INotificationIconSource Clone() => new TextureWrapIconSource(this.wrap, false);
/// <inheritdoc/>
public void Dispose()
{
if (Interlocked.Exchange(ref this.wrap, null) is { } w)
w.Dispose();
}
/// <inheritdoc/>
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);
}
}

View file

@ -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;
/// <summary>Represents the use of future <see cref="IDalamudTextureWrap"/> as the icon of a notification.</summary>
/// <remarks>If there was no texture loaded for any reason, the plugin icon will be displayed instead.</remarks>
internal class TextureWrapTaskIconSource : INotificationIconSource.IInternal
{
/// <summary>Gets the default materialized icon, for the purpose of displaying the plugin icon.</summary>
internal static readonly INotificationMaterializedIcon DefaultMaterializedIcon = new MaterializedIcon(null);
/// <summary>Initializes a new instance of the <see cref="TextureWrapTaskIconSource"/> class.</summary>
/// <param name="taskFunc">The function.</param>
public TextureWrapTaskIconSource(Func<Task<IDalamudTextureWrap?>?>? taskFunc) =>
this.TextureWrapTaskFunc = taskFunc;
/// <summary>Gets the function that returns a task resulting in a new instance of <see cref="IDalamudTextureWrap"/>.
/// </summary>
/// <remarks>Dalamud will take ownership of the result. Do not call <see cref="IDisposable.Dispose"/>.</remarks>
public Func<Task<IDalamudTextureWrap?>?>? TextureWrapTaskFunc { get; }
/// <inheritdoc/>
public INotificationIconSource Clone() => this;
/// <inheritdoc/>
public void Dispose()
{
}
/// <inheritdoc/>
public INotificationMaterializedIcon Materialize() =>
new MaterializedIcon(this.TextureWrapTaskFunc);
private sealed class MaterializedIcon : INotificationMaterializedIcon
{
private Task<IDalamudTextureWrap>? task;
public MaterializedIcon(Func<Task<IDalamudTextureWrap?>?>? 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);
}
}

View file

@ -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;
/// <summary>Constants for drawing notification windows.</summary>
public static class NotificationConstants
internal static class NotificationConstants
{
// .............................[..]
// ..when.......................[XX]
@ -20,69 +22,74 @@ public static class NotificationConstants
// .. action buttons ..
// .................................
/// <summary>Default duration of the notification.</summary>
public static readonly TimeSpan DefaultDisplayDuration = TimeSpan.FromSeconds(3);
/// <summary>Default duration of the notification, after the mouse cursor leaves the notification window.</summary>
public static readonly TimeSpan DefaultHoverExtendDuration = TimeSpan.FromSeconds(3);
/// <summary>The string to show in place of this_plugin if the notification is shown by Dalamud.</summary>
internal const string DefaultInitiator = "Dalamud";
public const string DefaultInitiator = "Dalamud";
/// <summary>The string to measure size of, to decide the width of notification windows.</summary>
public const string NotificationWidthMeasurementString =
"The width of this text will decide the width\n" +
"of the notification window.";
/// <summary>The ratio of maximum notification window width w.r.t. main viewport width.</summary>
public const float MaxNotificationWindowWidthWrtMainViewportWidth = 2f / 3;
/// <summary>The size of the icon.</summary>
internal const float IconSize = 32;
public const float IconSize = 32;
/// <summary>The background opacity of a notification window.</summary>
internal const float BackgroundOpacity = 0.82f;
public const float BackgroundOpacity = 0.82f;
/// <summary>The duration of indeterminate progress bar loop in milliseconds.</summary>
internal const float IndeterminateProgressbarLoopDuration = 2000f;
public const float IndeterminateProgressbarLoopDuration = 2000f;
/// <summary>The duration of the progress wave animation in milliseconds.</summary>
internal const float ProgressWaveLoopDuration = 2000f;
public const float ProgressWaveLoopDuration = 2000f;
/// <summary>The time ratio of a progress wave loop where the animation is idle.</summary>
internal const float ProgressWaveIdleTimeRatio = 0.5f;
public const float ProgressWaveIdleTimeRatio = 0.5f;
/// <summary>The time ratio of a non-idle portion of the progress wave loop where the color is the most opaque.
/// </summary>
internal const float ProgressWaveLoopMaxColorTimeRatio = 0.7f;
public const float ProgressWaveLoopMaxColorTimeRatio = 0.7f;
/// <summary>Default duration of the notification.</summary>
public static readonly TimeSpan DefaultDuration = TimeSpan.FromSeconds(3);
/// <summary>Duration of show animation.</summary>
internal static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300);
public static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300);
/// <summary>Duration of hide animation.</summary>
internal static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300);
public static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300);
/// <summary>Duration of progress change animation.</summary>
internal static readonly TimeSpan ProgressChangeAnimationDuration = TimeSpan.FromMilliseconds(200);
public static readonly TimeSpan ProgressChangeAnimationDuration = TimeSpan.FromMilliseconds(200);
/// <summary>Duration of expando animation.</summary>
internal static readonly TimeSpan ExpandoAnimationDuration = TimeSpan.FromMilliseconds(300);
public static readonly TimeSpan ExpandoAnimationDuration = TimeSpan.FromMilliseconds(300);
/// <summary>Text color for the rectangular border when the notification is focused.</summary>
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);
/// <summary>Text color for the when.</summary>
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);
/// <summary>Text color for the close button [X].</summary>
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);
/// <summary>Text color for the title.</summary>
internal static readonly Vector4 TitleTextColor = new(1f, 1f, 1f, 1f);
public static readonly Vector4 TitleTextColor = new(1f, 1f, 1f, 1f);
/// <summary>Text color for the name of the initiator.</summary>
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);
/// <summary>Text color for the body.</summary>
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);
/// <summary>Color for the background progress bar (determinate progress only).</summary>
internal static readonly Vector4 BackgroundProgressColorMax = new(1f, 1f, 1f, 0.1f);
public static readonly Vector4 BackgroundProgressColorMax = new(1f, 1f, 1f, 0.1f);
/// <summary>Color for the background progress bar (determinate progress only).</summary>
internal static readonly Vector4 BackgroundProgressColorMin = new(1f, 1f, 1f, 0.05f);
public static readonly Vector4 BackgroundProgressColorMin = new(1f, 1f, 1f, 0.05f);
/// <summary>Gets the relative time format strings.</summary>
private static readonly (TimeSpan MinSpan, string? FormatString)[] RelativeFormatStrings =
@ -110,35 +117,35 @@ public static class NotificationConstants
};
/// <summary>Gets the scaled padding of the window (dot(.) in the above diagram).</summary>
internal static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale);
public static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale);
/// <summary>Gets the distance from the right bottom border of the viewport
/// to the right bottom border of a notification window.
/// </summary>
internal static float ScaledViewportEdgeMargin => MathF.Round(20 * ImGuiHelpers.GlobalScale);
public static float ScaledViewportEdgeMargin => MathF.Round(20 * ImGuiHelpers.GlobalScale);
/// <summary>Gets the scaled gap between two notification windows.</summary>
internal static float ScaledWindowGap => MathF.Round(10 * ImGuiHelpers.GlobalScale);
public static float ScaledWindowGap => MathF.Round(10 * ImGuiHelpers.GlobalScale);
/// <summary>Gets the scaled gap between components.</summary>
internal static float ScaledComponentGap => MathF.Round(5 * ImGuiHelpers.GlobalScale);
public static float ScaledComponentGap => MathF.Round(5 * ImGuiHelpers.GlobalScale);
/// <summary>Gets the scaled size of the icon.</summary>
internal static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale);
public static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale);
/// <summary>Gets the height of the expiry progress bar.</summary>
internal static float ScaledExpiryProgressBarHeight => MathF.Round(3 * ImGuiHelpers.GlobalScale);
public static float ScaledExpiryProgressBarHeight => MathF.Round(3 * ImGuiHelpers.GlobalScale);
/// <summary>Gets the thickness of the focus indicator rectangle.</summary>
internal static float FocusIndicatorThickness => MathF.Round(3 * ImGuiHelpers.GlobalScale);
public static float FocusIndicatorThickness => MathF.Round(3 * ImGuiHelpers.GlobalScale);
/// <summary>Gets the string format of the initiator name field, if the initiator is unloaded.</summary>
internal static string UnloadedInitiatorNameFormat => "{0} (unloaded)";
public static string UnloadedInitiatorNameFormat => "{0} (unloaded)";
/// <summary>Formats an instance of <see cref="DateTime"/> as a relative time.</summary>
/// <param name="when">When.</param>
/// <returns>The formatted string.</returns>
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
/// <summary>Formats an instance of <see cref="DateTime"/> as an absolute time.</summary>
/// <param name="when">When.</param>
/// <returns>The formatted string.</returns>
internal static string FormatAbsoluteDateTime(this DateTime when) => $"{when:G}";
public static string FormatAbsoluteDateTime(this DateTime when) => $"{when:G}";
/// <summary>Formats an instance of <see cref="DateTime"/> as a relative time.</summary>
/// <param name="when">When.</param>
/// <returns>The formatted string.</returns>
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 "???";
}
/// <summary>Gets the color corresponding to the notification type.</summary>
/// <param name="type">The notification type.</param>
/// <returns>The corresponding color.</returns>
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,
};
/// <summary>Gets the <see cref="FontAwesomeIcon"/> char value corresponding to the notification type.</summary>
/// <param name="type">The notification type.</param>
/// <returns>The corresponding char, or null.</returns>
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',
};
/// <summary>Gets the localized title string corresponding to the notification type.</summary>
/// <param name="type">The notification type.</param>
/// <returns>The corresponding title.</returns>
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,
};
}

View file

@ -0,0 +1,34 @@
using System.IO;
using System.Numerics;
using Dalamud.Interface.Internal;
namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon;
/// <summary>Represents the use of a texture from a file as the icon of a notification.</summary>
/// <remarks>If there was no texture loaded for any reason, the plugin icon will be displayed instead.</remarks>
internal class FilePathNotificationIcon : INotificationIcon
{
private readonly FileInfo fileInfo;
/// <summary>Initializes a new instance of the <see cref="FilePathNotificationIcon"/> class.</summary>
/// <param name="filePath">The path to a .tex file inside the game resources.</param>
public FilePathNotificationIcon(string filePath) => this.fileInfo = new(filePath);
/// <inheritdoc/>
public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) =>
NotificationUtilities.DrawIconFrom(
minCoord,
maxCoord,
Service<TextureManager>.Get().GetTextureFromFile(this.fileInfo));
/// <inheritdoc/>
public override bool Equals(object? obj) =>
obj is FilePathNotificationIcon r && r.fileInfo.FullName == this.fileInfo.FullName;
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.fileInfo.FullName);
/// <inheritdoc/>
public override string ToString() => $"{nameof(FilePathNotificationIcon)}({this.fileInfo.FullName})";
}

View file

@ -0,0 +1,31 @@
using System.Numerics;
namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon;
/// <summary>Represents the use of <see cref="FontAwesomeIcon"/> as the icon of a notification.</summary>
internal class FontAwesomeIconNotificationIcon : INotificationIcon
{
private readonly char iconChar;
/// <summary>Initializes a new instance of the <see cref="FontAwesomeIconNotificationIcon"/> class.</summary>
/// <param name="iconChar">The character.</param>
public FontAwesomeIconNotificationIcon(FontAwesomeIcon iconChar) => this.iconChar = (char)iconChar;
/// <inheritdoc/>
public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) =>
NotificationUtilities.DrawIconFrom(
minCoord,
maxCoord,
this.iconChar,
Service<NotificationManager>.Get().IconFontAwesomeFontHandle,
color);
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is FontAwesomeIconNotificationIcon r && r.iconChar == this.iconChar;
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.iconChar);
/// <inheritdoc/>
public override string ToString() => $"{nameof(FontAwesomeIconNotificationIcon)}({this.iconChar})";
}

View file

@ -0,0 +1,34 @@
using System.Numerics;
using Dalamud.Interface.Internal;
using Dalamud.Plugin.Services;
namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon;
/// <summary>Represents the use of a game-shipped texture as the icon of a notification.</summary>
/// <remarks>If there was no texture loaded for any reason, the plugin icon will be displayed instead.</remarks>
internal class GamePathNotificationIcon : INotificationIcon
{
private readonly string gamePath;
/// <summary>Initializes a new instance of the <see cref="GamePathNotificationIcon"/> class.</summary>
/// <param name="gamePath">The path to a .tex file inside the game resources.</param>
/// <remarks>Use <see cref="ITextureProvider.GetIconPath"/> to get the game path from icon IDs.</remarks>
public GamePathNotificationIcon(string gamePath) => this.gamePath = gamePath;
/// <inheritdoc/>
public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) =>
NotificationUtilities.DrawIconFrom(
minCoord,
maxCoord,
Service<TextureManager>.Get().GetTextureFromGame(this.gamePath));
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is GamePathNotificationIcon r && r.gamePath == this.gamePath;
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.gamePath);
/// <inheritdoc/>
public override string ToString() => $"{nameof(GamePathNotificationIcon)}({this.gamePath})";
}

View file

@ -0,0 +1,33 @@
using System.Numerics;
using Dalamud.Game.Text;
namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon;
/// <summary>Represents the use of <see cref="SeIconChar"/> as the icon of a notification.</summary>
internal class SeIconCharNotificationIcon : INotificationIcon
{
private readonly SeIconChar iconChar;
/// <summary>Initializes a new instance of the <see cref="SeIconCharNotificationIcon"/> class.</summary>
/// <param name="c">The character.</param>
public SeIconCharNotificationIcon(SeIconChar c) => this.iconChar = c;
/// <inheritdoc/>
public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) =>
NotificationUtilities.DrawIconFrom(
minCoord,
maxCoord,
(char)this.iconChar,
Service<NotificationManager>.Get().IconAxisFontHandle,
color);
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is SeIconCharNotificationIcon r && r.iconChar == this.iconChar;
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.iconChar);
/// <inheritdoc/>
public override string ToString() => $"{nameof(SeIconCharNotificationIcon)}({this.iconChar})";
}

View file

@ -11,6 +11,8 @@ using Dalamud.IoC.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
using ImGuiNET;
namespace Dalamud.Interface.ImGuiNotification.Internal;
/// <summary>Class handling notifications/toasts in ImGui.</summary>
@ -41,6 +43,7 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos
/// <summary>Gets the handle to FontAwesome fonts, sized for use as an icon.</summary>
public IFontHandle IconFontAwesomeFontHandle { get; }
/// <summary>Gets the private atlas for use with notification windows.</summary>
private IFontAtlas PrivateAtlas { get; }
/// <inheritdoc/>
@ -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();
}
/// <inheritdoc/>
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
/// <summary>Adds a notification originating from a plugin.</summary>
/// <param name="notification">The notification.</param>
/// <param name="disposeNotification">Dispose <paramref name="notification"/> when this function returns.</param>
/// <param name="plugin">The source plugin.</param>
/// <returns>The added notification.</returns>
/// <remarks><paramref name="disposeNotification"/> will be honored even on exceptions.</remarks>
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);
});
/// <summary>Draw all currently queued notifications.</summary>
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;
/// <inheritdoc/>
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;

View file

@ -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;
/// <summary>Represents a blueprint for a notification.</summary>
public sealed record Notification : INotification
{
private INotificationIconSource? iconSource;
/// <summary>Initializes a new instance of the <see cref="Notification"/> class.</summary>
public Notification()
{
}
/// <summary>Initializes a new instance of the <see cref="Notification"/> class.</summary>
/// <param name="notification">The instance of <see cref="INotification"/> to copy from.</param>
public Notification(INotification notification) => this.CopyValuesFrom(notification);
/// <summary>Initializes a new instance of the <see cref="Notification"/> class.</summary>
/// <param name="notification">The instance of <see cref="Notification"/> to copy from.</param>
public Notification(Notification notification) => this.CopyValuesFrom(notification);
/// <summary>
/// Gets the default value for <see cref="InitialDuration"/> and <see cref="ExtensionDurationSinceLastInterest"/>.
/// </summary>
public static TimeSpan DefaultDuration => NotificationConstants.DefaultDuration;
/// <inheritdoc/>
public string Content { get; set; } = string.Empty;
@ -35,25 +24,16 @@ public sealed record Notification : INotification
public NotificationType Type { get; set; } = NotificationType.None;
/// <inheritdoc/>
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; }
/// <inheritdoc/>
public DateTime HardExpiry { get; set; } = DateTime.MaxValue;
/// <inheritdoc/>
public TimeSpan InitialDuration { get; set; } = NotificationConstants.DefaultDisplayDuration;
public TimeSpan InitialDuration { get; set; } = DefaultDuration;
/// <inheritdoc/>
public TimeSpan DurationSinceLastInterest { get; set; } = NotificationConstants.DefaultHoverExtendDuration;
public TimeSpan ExtensionDurationSinceLastInterest { get; set; } = DefaultDuration;
/// <inheritdoc/>
public bool ShowIndeterminateIfNoExpiry { get; set; } = true;
@ -66,29 +46,4 @@ public sealed record Notification : INotification
/// <inheritdoc/>
public float Progress { get; set; } = 1f;
/// <inheritdoc/>
public void Dispose()
{
// Assign to the property; it will take care of disposing
this.IconSource = null;
}
/// <summary>Copy values from the given instance of <see cref="INotification"/>.</summary>
/// <param name="copyFrom">The instance of <see cref="INotification"/> to copy from.</param>
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;
}
}

View file

@ -17,44 +17,47 @@ namespace Dalamud.Interface.ImGuiNotification;
/// <summary>Utilities for implementing stuff under <see cref="ImGuiNotification"/>.</summary>
public static class NotificationUtilities
{
/// <inheritdoc cref="INotificationIconSource.From(SeIconChar)"/>
/// <inheritdoc cref="INotificationIcon.From(SeIconChar)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIconSource ToIconSource(this SeIconChar iconChar) =>
INotificationIconSource.From(iconChar);
public static INotificationIcon ToIconSource(this SeIconChar iconChar) =>
INotificationIcon.From(iconChar);
/// <inheritdoc cref="INotificationIconSource.From(FontAwesomeIcon)"/>
/// <inheritdoc cref="INotificationIcon.From(FontAwesomeIcon)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIconSource ToIconSource(this FontAwesomeIcon iconChar) =>
INotificationIconSource.From(iconChar);
public static INotificationIcon ToIconSource(this FontAwesomeIcon iconChar) =>
INotificationIcon.From(iconChar);
/// <inheritdoc cref="INotificationIconSource.From(IDalamudTextureWrap,bool)"/>
/// <inheritdoc cref="INotificationIcon.FromFile(string)"/>
[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);
/// <inheritdoc cref="INotificationIconSource.FromFile(string)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static INotificationIconSource ToIconSource(this FileInfo fileInfo) =>
INotificationIconSource.FromFile(fileInfo.FullName);
/// <summary>Draws an icon string.</summary>
/// <param name="fontHandleLarge">The font handle to use.</param>
/// <param name="c">The icon character.</param>
/// <summary>Draws an icon from an <see cref="IFontHandle"/> and a <see cref="char"/>.</summary>
/// <param name="minCoord">The coordinates of the top left of the icon area.</param>
/// <param name="maxCoord">The coordinates of the bottom right of the icon area.</param>
/// <param name="c">The icon character.</param>
/// <param name="fontHandle">The font handle to use.</param>
/// <param name="color">The foreground color.</param>
internal static unsafe void DrawIconString(
IFontHandle fontHandleLarge,
char c,
/// <returns><c>true</c> if anything has been drawn.</returns>
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;
}
/// <summary>Draws the given texture, or the icon of the plugin if texture is <c>null</c>.</summary>
/// <param name="texture">The texture.</param>
/// <summary>Draws an icon from an instance of <see cref="IDalamudTextureWrap"/>.</summary>
/// <param name="minCoord">The coordinates of the top left of the icon area.</param>
/// <param name="maxCoord">The coordinates of the bottom right of the icon area.</param>
/// <param name="initiatorPlugin">The initiator plugin.</param>
internal static void DrawTexture(
IDalamudTextureWrap? texture,
Vector2 minCoord,
Vector2 maxCoord,
LocalPlugin? initiatorPlugin)
/// <param name="texture">The texture.</param>
/// <returns><c>true</c> if anything has been drawn.</returns>
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;
}
}
/// <summary>Draws an icon from an instance of <see cref="LocalPlugin"/>.</summary>
/// <param name="minCoord">The coordinates of the top left of the icon area.</param>
/// <param name="maxCoord">The coordinates of the bottom right of the icon area.</param>
/// <param name="plugin">The plugin. Dalamud icon will be drawn if <c>null</c> is given.</param>
/// <returns><c>true</c> if anything has been drawn.</returns>
internal static bool DrawIconFrom(Vector2 minCoord, Vector2 maxCoord, LocalPlugin? plugin)
{
var dam = Service<DalamudAssetManager>.Get();
if (plugin is null)
return false;
if (!Service<PluginImageCache>.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<DalamudAssetManager>.Get();
if (initiatorPlugin is null)
{
texture = dam.GetDalamudTextureWrap(DalamudAsset.LogoSmall);
}
else
{
if (!Service<PluginImageCache>.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);
/// <summary>Draws the Dalamud logo as an icon.</summary>
/// <param name="minCoord">The coordinates of the top left of the icon area.</param>
/// <param name="maxCoord">The coordinates of the bottom right of the icon area.</param>
internal static void DrawIconFromDalamudLogo(Vector2 minCoord, Vector2 maxCoord)
{
var dam = Service<DalamudAssetManager>.Get();
var texture = dam.GetDalamudTextureWrap(DalamudAsset.LogoSmall);
DrawIconFrom(minCoord, maxCoord, texture);
}
}

View file

@ -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<DalamudAssetManager>.Get().GetDalamudTextureWrap(
Enum.Parse<DalamudAsset>(
NotificationTemplate.AssetSources[
this.notificationTemplate.IconSourceAssetInt])),
false),
4 => INotificationIconSource.From(
() =>
Service<DalamudAssetManager>.Get().GetDalamudTextureWrapAsync(
Enum.Parse<DalamudAsset>(
NotificationTemplate.AssetSources[
this.notificationTemplate.IconSourceAssetInt]))),
5 => INotificationIconSource.FromGame(this.notificationTemplate.IconSourceText),
6 => INotificationIconSource.FromFile(this.notificationTemplate.IconSourceText),
7 => INotificationIconSource.From(
Service<TextureManager>.Get().GetTextureFromGame(this.notificationTemplate.IconSourceText),
false),
8 => INotificationIconSource.From(
Service<TextureManager>.Get().GetTextureFromFile(
new(this.notificationTemplate.IconSourceText)),
false),
3 => INotificationIcon.FromGame(this.notificationTemplate.IconSourceText),
4 => INotificationIcon.FromFile(this.notificationTemplate.IconSourceText),
_ => null,
},
},
true);
});
var dam = Service<DalamudAssetManager>.Get();
var tm = Service<TextureManager>.Get();
switch (this.notificationTemplate.IconSourceInt)
{
case 5:
n.SetIconTexture(
dam.GetDalamudTextureWrap(
Enum.Parse<DalamudAsset>(
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),
};

View file

@ -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 _);

View file

@ -2,21 +2,11 @@ using Dalamud.Interface.ImGuiNotification;
namespace Dalamud.Plugin.Services;
/// <summary>
/// Manager for notifications provided by Dalamud using ImGui.
/// </summary>
/// <summary>Manager for notifications provided by Dalamud using ImGui.</summary>
public interface INotificationManager
{
/// <summary>
/// Adds a notification.
/// </summary>
/// <summary>Adds a notification.</summary>
/// <param name="notification">The new notification.</param>
/// <param name="disposeNotification">
/// Dispose <paramref name="notification"/> when this function returns, even if the function throws an exception.
/// Set to <c>false</c> to reuse <paramref name="notification"/> for multiple calls to this function, in which case,
/// you should call <see cref="IDisposable.Dispose"/> on the value supplied to <paramref name="notification"/> at a
/// later time.
/// </param>
/// <returns>The added notification.</returns>
IActiveNotification AddNotification(Notification notification, bool disposeNotification = true);
IActiveNotification AddNotification(Notification notification);
}