diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index 3ae1a76ce..e6355cd90 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -59,6 +59,9 @@ public interface IActiveNotification : INotification /// new DateTime Expiry { get; set; } + /// + new bool ShowIndeterminateIfNoExpiry { get; set; } + /// new bool Interactable { get; set; } diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index d8ac95c22..9d6167a95 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -37,6 +37,10 @@ public interface INotification : IDisposable /// (sticky, indeterminate, permanent, or persistent). DateTime Expiry { get; } + /// Gets a value indicating whether to show an indeterminate expiration animation if + /// is set to . + bool ShowIndeterminateIfNoExpiry { get; } + /// Gets a value indicating whether this notification may be interacted. /// /// Set this value to true if you want to respond to user inputs from @@ -58,8 +62,7 @@ public interface INotification : IDisposable /// TimeSpan HoverExtendDuration { get; } - /// Gets the progress for the progress bar of the notification. - /// The progress should either be in the range between 0 and 1 or be a negative value. - /// Specifying a negative value will show an indeterminate progress bar. + /// Gets the progress for the background progress bar of the notification. + /// The progress should be in the range between 0 and 1. float Progress { get; } } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index 53262c08e..a71c35c49 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -50,7 +50,7 @@ internal sealed class ActiveNotification : IActiveNotification this.InitiatorPlugin = initiatorPlugin; this.showEasing = new InCubic(NotificationConstants.ShowAnimationDuration); this.hideEasing = new OutCubic(NotificationConstants.HideAnimationDuration); - this.progressEasing = new InOutCubic(NotificationConstants.ProgressAnimationDuration); + this.progressEasing = new InOutCubic(NotificationConstants.ProgressChangeAnimationDuration); this.showEasing.Start(); this.progressEasing.Start(); @@ -142,6 +142,18 @@ internal sealed class ActiveNotification : IActiveNotification } } + /// + public bool ShowIndeterminateIfNoExpiry + { + get => this.underlyingNotification.ShowIndeterminateIfNoExpiry; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.ShowIndeterminateIfNoExpiry = value; + } + } + /// public bool Interactable { @@ -212,9 +224,6 @@ internal sealed class ActiveNotification : IActiveNotification get { var underlyingProgress = this.underlyingNotification.Progress; - if (underlyingProgress < 0) - return 0f; - if (Math.Abs(underlyingProgress - this.progressBefore) < 0.000001f || this.progressEasing.IsDone) return underlyingProgress; @@ -409,6 +418,7 @@ internal sealed class ActiveNotification : IActiveNotification ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoDocking); + this.DrawWindowBackgroundProgressBar(); this.DrawNotificationMainWindowContent(width); var windowPos = ImGui.GetWindowPos(); var windowSize = ImGui.GetWindowSize(); @@ -440,6 +450,7 @@ internal sealed class ActiveNotification : IActiveNotification ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoDocking); + this.DrawWindowBackgroundProgressBar(); this.DrawNotificationActionWindowContent(interfaceManager, width); windowSize.Y += actionWindowHeight; windowPos.Y -= actionWindowHeight; @@ -517,7 +528,7 @@ internal sealed class ActiveNotification : IActiveNotification this.underlyingNotification.IconSource = newIconSource; this.UpdateIcon(); } - + /// Removes non-Dalamud invocation targets from events. public void RemoveNonDalamudInvocations() { @@ -563,6 +574,49 @@ internal sealed class ActiveNotification : IActiveNotification 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 DrawNotificationMainWindowContent(float width) { var basePos = ImGui.GetCursorPos(); @@ -580,62 +634,61 @@ internal sealed class ActiveNotification : IActiveNotification // Top padding is zero, as the action window will add the padding. ImGui.Dummy(new(NotificationConstants.ScaledWindowPadding)); - float progressL, progressR; + 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; - progressL = midpoint - (length * v); - progressR = midpoint + (length * v); + barL = midpoint - (length * v); + barR = midpoint + (length * v); } else if (this.Expiry == DateTime.MaxValue) { - if (this.Progress >= 0) - { - progressL = 0f; - progressR = this.ProgressEased; - } - else + if (this.ShowIndeterminateIfNoExpiry) { var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds % NotificationConstants.IndeterminateProgressbarLoopDuration) / NotificationConstants.IndeterminateProgressbarLoopDuration); - progressL = Math.Max(elapsed - (1f / 3), 0f) / (2f / 3); - progressR = Math.Min(elapsed, 2f / 3) / (2f / 3); - progressL = MathF.Pow(progressL, 3); - progressR = 1f - MathF.Pow(1f - progressR, 3); + 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; } - - this.prevProgressL = progressL; - this.prevProgressR = progressR; } else if (this.HoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered) { - progressL = 0f; - progressR = 1f; - this.prevProgressL = progressL; - this.prevProgressR = progressR; + barL = 0f; + barR = 1f; + this.prevProgressL = barL; + this.prevProgressR = barR; } else { - progressL = 1f - (float)((this.Expiry - DateTime.Now).TotalMilliseconds / - (this.Expiry - this.ExpiryRelativeToTime).TotalMilliseconds); - progressR = 1f; - this.prevProgressL = progressL; - this.prevProgressR = progressR; + barL = 1f - (float)((this.Expiry - DateTime.Now).TotalMilliseconds / + (this.Expiry - this.ExpiryRelativeToTime).TotalMilliseconds); + barR = 1f; + this.prevProgressL = barL; + this.prevProgressR = barR; } - progressR = Math.Clamp(progressR, 0f, 1f); + 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 * progressL, + windowSize.X * barL, windowSize.Y - NotificationConstants.ScaledExpiryProgressBarHeight), - windowPos + windowSize with { X = windowSize.X * progressR }, + windowPos + windowSize with { X = windowSize.X * barR }, ImGui.GetColorU32(this.DefaultIconColor)); ImGui.PopClipRect(); } diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index 3b452bd2d..9c89dc305 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -20,6 +20,9 @@ public sealed record Notification : INotification /// public DateTime Expiry { get; set; } = DateTime.Now + NotificationConstants.DefaultDisplayDuration; + /// + public bool ShowIndeterminateIfNoExpiry { get; set; } = true; + /// public bool Interactable { get; set; } = true; diff --git a/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs index 62d288836..800531f39 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs @@ -37,14 +37,24 @@ public static class NotificationConstants /// The duration of indeterminate progress bar loop in milliseconds. internal const float IndeterminateProgressbarLoopDuration = 2000f; + /// The duration of the progress wave animation in milliseconds. + internal const float ProgressWaveLoopDuration = 2000f; + + /// The time ratio of a progress wave loop where the animation is idle. + internal const float ProgressWaveIdleTimeRatio = 0.5f; + + /// The time ratio of a non-idle portion of the progress wave loop where the color is the most opaque. + /// + internal const float ProgressWaveLoopMaxColorTimeRatio = 0.7f; + /// Duration of show animation. internal static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300); /// Duration of hide animation. internal static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300); - /// Duration of hide animation. - internal static readonly TimeSpan ProgressAnimationDuration = TimeSpan.FromMilliseconds(200); + /// Duration of progress change animation. + internal static readonly TimeSpan ProgressChangeAnimationDuration = TimeSpan.FromMilliseconds(200); /// Text color for the when. internal static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f); @@ -61,6 +71,12 @@ public static class NotificationConstants /// Text color for the body. internal static readonly Vector4 BodyTextColor = new(0.9f, 0.9f, 0.9f, 1f); + /// Color for the background progress bar (determinate progress only). + internal static readonly Vector4 BackgroundProgressColorMax = new(1f, 1f, 1f, 0.1f); + + /// Color for the background progress bar (determinate progress only). + internal static readonly Vector4 BackgroundProgressColorMin = new(1f, 1f, 1f, 0.05f); + /// Gets the relative time format strings. private static readonly (TimeSpan MinSpan, string? FormatString)[] RelativeFormatStrings = { @@ -94,7 +110,7 @@ public static class NotificationConstants internal static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale); /// Gets the height of the expiry progress bar. - internal static float ScaledExpiryProgressBarHeight => MathF.Round(2 * ImGuiHelpers.GlobalScale); + internal static float ScaledExpiryProgressBarHeight => MathF.Round(3 * ImGuiHelpers.GlobalScale); /// Gets the string format of the initiator name field, if the initiator is unloaded. internal static string UnloadedInitiatorNameFormat => "{0} (unloaded)"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 3b518af84..ae3f16576 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -120,6 +120,8 @@ internal class ImGuiWidget : IDataWindowWidget ImGui.Checkbox("Interactable", ref this.notificationTemplate.Interactable); + ImGui.Checkbox("Show Indeterminate If No Expiry", ref this.notificationTemplate.ShowIndeterminateIfNoExpiry); + ImGui.Checkbox("User Dismissable", ref this.notificationTemplate.UserDismissable); ImGui.Checkbox( @@ -147,6 +149,7 @@ internal class ImGuiWidget : IDataWindowWidget Content = text, Title = title, Type = type, + ShowIndeterminateIfNoExpiry = this.notificationTemplate.ShowIndeterminateIfNoExpiry, Interactable = this.notificationTemplate.Interactable, UserDismissable = this.notificationTemplate.UserDismissable, Expiry = duration == TimeSpan.MaxValue ? DateTime.MaxValue : DateTime.Now + duration, @@ -347,6 +350,7 @@ internal class ImGuiWidget : IDataWindowWidget public bool ManualType; public int TypeInt; public int DurationInt; + public bool ShowIndeterminateIfNoExpiry; public bool Interactable; public bool UserDismissable; public bool ActionBar; @@ -364,6 +368,7 @@ internal class ImGuiWidget : IDataWindowWidget this.ManualType = false; this.TypeInt = (int)NotificationType.None; this.DurationInt = 2; + this.ShowIndeterminateIfNoExpiry = true; this.Interactable = true; this.UserDismissable = true; this.ActionBar = true;