Add Reduced Motion for Notifications (#1732)

When Reduced Motion configuration is on, the expiry progressbar is
removed, and instead a pie on top right is shown, and relative time
update interval increases to 15 seconds. Progress wave animation also is
suppressed.
This commit is contained in:
srkizer 2024-03-21 05:53:20 +09:00 committed by GitHub
parent fb60ac5b4b
commit 95defa200f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 185 additions and 49 deletions

View file

@ -367,6 +367,11 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// </summary> /// </summary>
public bool ShowTsm { get; set; } = true; public bool ShowTsm { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether to reduce motions (animations).
/// </summary>
public bool ReduceMotions { get; set; } = false;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not market board data should be uploaded. /// Gets or sets a value indicating whether or not market board data should be uploaded.
/// </summary> /// </summary>

View file

@ -1,5 +1,6 @@
using System.Numerics; using System.Numerics;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Utility; using Dalamud.Utility;
@ -20,8 +21,8 @@ internal sealed partial class ActiveNotification
var opacity = var opacity =
Math.Clamp( Math.Clamp(
(float)(this.hideEasing.IsRunning (float)(this.hideEasing.IsRunning
? (this.hideEasing.IsDone ? 0 : 1f - this.hideEasing.Value) ? (this.hideEasing.IsDone || ReducedMotions ? 0 : 1f - this.hideEasing.Value)
: (this.showEasing.IsDone ? 1 : this.showEasing.Value)), : (this.showEasing.IsDone || ReducedMotions ? 1 : this.showEasing.Value)),
0f, 0f,
1f); 1f);
if (opacity <= 0) if (opacity <= 0)
@ -97,24 +98,25 @@ internal sealed partial class ActiveNotification
this.lastInterestTime = DateTime.Now; this.lastInterestTime = DateTime.Now;
this.DrawWindowBackgroundProgressBar(); this.DrawWindowBackgroundProgressBar();
this.DrawTopBar(width, actionWindowHeight, isHovered); this.DrawTopBar(width, actionWindowHeight, isHovered, warrantsExtension);
if (!this.underlyingNotification.Minimized && !this.expandoEasing.IsRunning) if (!this.underlyingNotification.Minimized && !this.expandoEasing.IsRunning)
{ {
this.DrawContentAndActions(width, actionWindowHeight); this.DrawContentAndActions(width, actionWindowHeight);
} }
else if (this.expandoEasing.IsRunning) else if (this.expandoEasing.IsRunning)
{ {
var easedValue = ReducedMotions ? 1f : (float)this.expandoEasing.Value;
if (this.underlyingNotification.Minimized) if (this.underlyingNotification.Minimized)
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (1f - (float)this.expandoEasing.Value)); ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (1f - easedValue));
else else
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (float)this.expandoEasing.Value); ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * easedValue);
this.DrawContentAndActions(width, actionWindowHeight); this.DrawContentAndActions(width, actionWindowHeight);
ImGui.PopStyleVar(); ImGui.PopStyleVar();
} }
if (isFocused) if (isFocused)
this.DrawFocusIndicator(); this.DrawFocusIndicator();
this.DrawExpiryBar(this.EffectiveExpiry, warrantsExtension); this.DrawExpiryBar(warrantsExtension);
if (ImGui.IsWindowHovered()) if (ImGui.IsWindowHovered())
{ {
@ -184,24 +186,36 @@ internal sealed partial class ActiveNotification
private void DrawWindowBackgroundProgressBar() private void DrawWindowBackgroundProgressBar()
{ {
var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds % var elapsed = 0f;
NotificationConstants.ProgressWaveLoopDuration) / var colorElapsed = 0f;
NotificationConstants.ProgressWaveLoopDuration); float progress;
elapsed /= NotificationConstants.ProgressWaveIdleTimeRatio;
var colorElapsed = if (ReducedMotions)
elapsed < NotificationConstants.ProgressWaveLoopMaxColorTimeRatio {
? elapsed / NotificationConstants.ProgressWaveLoopMaxColorTimeRatio progress = this.Progress;
: ((NotificationConstants.ProgressWaveLoopMaxColorTimeRatio * 2) - elapsed) / }
NotificationConstants.ProgressWaveLoopMaxColorTimeRatio; else
{
progress = Math.Clamp(this.ProgressEased, 0f, 1f);
elapsed = Math.Clamp(elapsed, 0f, 1f); elapsed =
colorElapsed = Math.Clamp(colorElapsed, 0f, 1f); (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds %
colorElapsed = MathF.Sin(colorElapsed * (MathF.PI / 2f)); NotificationConstants.ProgressWaveLoopDuration) /
NotificationConstants.ProgressWaveLoopDuration);
elapsed /= NotificationConstants.ProgressWaveIdleTimeRatio;
var progress = Math.Clamp(this.ProgressEased, 0f, 1f); colorElapsed = elapsed < NotificationConstants.ProgressWaveLoopMaxColorTimeRatio
if (progress >= 1f) ? elapsed / NotificationConstants.ProgressWaveLoopMaxColorTimeRatio
elapsed = colorElapsed = 0f; : ((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));
if (progress >= 1f)
elapsed = colorElapsed = 0f;
}
var windowPos = ImGui.GetWindowPos(); var windowPos = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize(); var windowSize = ImGui.GetWindowSize();
@ -240,7 +254,7 @@ internal sealed partial class ActiveNotification
ImGui.PopClipRect(); ImGui.PopClipRect();
} }
private void DrawTopBar(float width, float height, bool drawActionButtons) private void DrawTopBar(float width, float height, bool drawActionButtons, bool warrantsExtension)
{ {
var windowPos = ImGui.GetWindowPos(); var windowPos = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize(); var windowSize = ImGui.GetWindowSize();
@ -249,6 +263,10 @@ internal sealed partial class ActiveNotification
using (Service<InterfaceManager>.Get().IconFontHandle?.Push()) using (Service<InterfaceManager>.Get().IconFontHandle?.Push())
{ {
ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false); ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false);
if (!drawActionButtons)
this.DrawExpiryPie(warrantsExtension, new(width - height, 0), new(height));
if (this.UserDismissable) if (this.UserDismissable)
{ {
if (this.DrawIconButton(FontAwesomeIcon.Times, rtOffset, height, drawActionButtons)) if (this.DrawIconButton(FontAwesomeIcon.Times, rtOffset, height, drawActionButtons))
@ -272,7 +290,7 @@ internal sealed partial class ActiveNotification
} }
float relativeOpacity; float relativeOpacity;
if (this.expandoEasing.IsRunning) if (this.expandoEasing.IsRunning && !ReducedMotions)
{ {
relativeOpacity = relativeOpacity =
this.underlyingNotification.Minimized this.underlyingNotification.Minimized
@ -297,36 +315,35 @@ internal sealed partial class ActiveNotification
ImGui.TextUnformatted( ImGui.TextUnformatted(
ImGui.IsWindowHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) ImGui.IsWindowHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem)
? this.CreatedAt.LocAbsolute() ? this.CreatedAt.LocAbsolute()
: this.CreatedAt.LocRelativePastLong()); : ReducedMotions
? this.CreatedAt.LocRelativePastLong(TimeSpan.FromSeconds(15))
: this.CreatedAt.LocRelativePastLong(TimeSpan.FromSeconds(5)));
ImGui.PopStyleColor(); ImGui.PopStyleColor();
ImGui.PopStyleVar(); ImGui.PopStyleVar();
} }
if (relativeOpacity < 1) if (relativeOpacity < 1)
{ {
rtOffset = new(width - NotificationConstants.ScaledWindowPadding, 0);
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * (1f - relativeOpacity)); ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * (1f - relativeOpacity));
var ltOffset = new Vector2(NotificationConstants.ScaledWindowPadding); var agoText =
this.DrawIcon(ltOffset, new(height - (2 * NotificationConstants.ScaledWindowPadding))); ReducedMotions
? this.CreatedAt.LocRelativePastShort(TimeSpan.FromSeconds(15))
ltOffset.X = height; : this.CreatedAt.LocRelativePastShort(TimeSpan.FromSeconds(5));
var agoText = this.CreatedAt.LocRelativePastShort();
var agoSize = ImGui.CalcTextSize(agoText); var agoSize = ImGui.CalcTextSize(agoText);
rtOffset.X -= agoSize.X; ImGui.SetCursorPos(new(width - ((height + agoSize.X) / 2f), NotificationConstants.ScaledWindowPadding));
ImGui.SetCursorPos(rtOffset with { Y = NotificationConstants.ScaledWindowPadding });
ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor);
ImGui.TextUnformatted(agoText); ImGui.TextUnformatted(agoText);
ImGui.PopStyleColor(); ImGui.PopStyleColor();
rtOffset.X -= NotificationConstants.ScaledWindowPadding; this.DrawIcon(
new(NotificationConstants.ScaledWindowPadding),
new(height - (2 * NotificationConstants.ScaledWindowPadding)));
ImGui.PushClipRect( ImGui.PushClipRect(
windowPos + ltOffset with { Y = 0 }, windowPos + new Vector2(height, 0),
windowPos + rtOffset with { Y = height }, windowPos + new Vector2(width - height, height),
true); true);
ImGui.SetCursorPos(ltOffset with { Y = NotificationConstants.ScaledWindowPadding }); ImGui.SetCursorPos(new(height, NotificationConstants.ScaledWindowPadding));
ImGui.TextUnformatted(this.EffectiveMinimizedText); ImGui.TextUnformatted(this.EffectiveMinimizedText);
ImGui.PopClipRect(); ImGui.PopClipRect();
@ -437,12 +454,95 @@ internal sealed partial class ActiveNotification
ImGui.PopTextWrapPos(); ImGui.PopTextWrapPos();
} }
private void DrawExpiryBar(DateTime effectiveExpiry, bool warrantsExtension) private void DrawExpiryPie(bool warrantsExtension, Vector2 offset, Vector2 size)
{ {
if (!Service<DalamudConfiguration>.Get().ReduceMotions)
return;
// circle here; 0 means 0deg; 1 means 360deg
float fillStartCw, fillEndCw;
if (this.DismissReason is not null)
{
fillStartCw = fillEndCw = 0f;
}
else if (warrantsExtension)
{
fillStartCw = fillEndCw = 0f;
}
else if (this.EffectiveExpiry == DateTime.MaxValue)
{
if (this.ShowIndeterminateIfNoExpiry)
{
// draw
var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds %
NotificationConstants.IndeterminatePieLoopDuration) /
NotificationConstants.IndeterminatePieLoopDuration);
fillStartCw = elapsed;
fillEndCw = elapsed + 0.2f + (MathF.Sin(elapsed * MathF.PI) * 0.2f);
}
else
{
// do not draw
fillStartCw = fillEndCw = 0f;
}
}
else
{
fillStartCw = 1f - (float)((this.EffectiveExpiry - DateTime.Now).TotalMilliseconds /
(this.EffectiveExpiry - this.lastInterestTime).TotalMilliseconds);
fillEndCw = 1f;
}
if (fillStartCw > fillEndCw)
(fillStartCw, fillEndCw) = (fillEndCw, fillStartCw);
if (fillStartCw == 0 && fillEndCw == 0)
return;
var radius = Math.Min(size.X, size.Y) / 3f;
var ifrom = fillStartCw * MathF.PI * 2;
var ito = fillEndCw * MathF.PI * 2;
var nseg = MathF.Ceiling(2 * MathF.PI * radius);
var step = (MathF.PI * 2) / nseg;
var center = ImGui.GetWindowPos() + offset + (size / 2);
var color = ImGui.GetColorU32(this.Type.ToColor() * new Vector4(1, 1, 1, 0.2f));
var prevOff = center + (radius * new Vector2(MathF.Sin(ifrom), -MathF.Cos(ifrom)));
Span<Vector2> verts = stackalloc Vector2[(int)MathF.Ceiling(((ito - ifrom) / step) + 3)];
var vertPtr = 0;
verts[vertPtr++] = center;
verts[vertPtr++] = prevOff;
var cur = ifrom + step;
for (; cur < ito; cur += step)
{
var curOff = center + (radius * new Vector2(MathF.Sin(cur), -MathF.Cos(cur)));
if (Vector2.DistanceSquared(prevOff, curOff) >= 1)
verts[vertPtr++] = prevOff = curOff;
}
var lastOff = center + (radius * new Vector2(MathF.Sin(ito), -MathF.Cos(ito)));
if (Vector2.DistanceSquared(prevOff, lastOff) >= 1)
verts[vertPtr++] = lastOff;
unsafe
{
var dlist = ImGui.GetWindowDrawList().NativePtr;
fixed (Vector2* pvert = verts)
ImGuiNative.ImDrawList_AddConvexPolyFilled(dlist, pvert, vertPtr, color);
}
}
private void DrawExpiryBar(bool warrantsExtension)
{
if (Service<DalamudConfiguration>.Get().ReduceMotions)
return;
float barL, barR; float barL, barR;
if (this.DismissReason is not null) if (this.DismissReason is not null)
{ {
var v = this.hideEasing.IsDone ? 0f : 1f - (float)this.hideEasing.Value; var v = this.hideEasing.IsDone || ReducedMotions ? 0f : 1f - (float)this.hideEasing.Value;
var midpoint = (this.prevProgressL + this.prevProgressR) / 2f; var midpoint = (this.prevProgressL + this.prevProgressR) / 2f;
var length = (this.prevProgressR - this.prevProgressL) / 2f; var length = (this.prevProgressR - this.prevProgressL) / 2f;
barL = midpoint - (length * v); barL = midpoint - (length * v);
@ -455,7 +555,7 @@ internal sealed partial class ActiveNotification
this.prevProgressL = barL; this.prevProgressL = barL;
this.prevProgressR = barR; this.prevProgressR = barR;
} }
else if (effectiveExpiry == DateTime.MaxValue) else if (this.EffectiveExpiry == DateTime.MaxValue)
{ {
if (this.ShowIndeterminateIfNoExpiry) if (this.ShowIndeterminateIfNoExpiry)
{ {
@ -477,8 +577,8 @@ internal sealed partial class ActiveNotification
} }
else else
{ {
barL = 1f - (float)((effectiveExpiry - DateTime.Now).TotalMilliseconds / barL = 1f - (float)((this.EffectiveExpiry - DateTime.Now).TotalMilliseconds /
(effectiveExpiry - this.lastInterestTime).TotalMilliseconds); (this.EffectiveExpiry - this.lastInterestTime).TotalMilliseconds);
barR = 1f; barR = 1f;
this.prevProgressL = barL; this.prevProgressL = barL;
this.prevProgressR = barR; this.prevProgressR = barR;

View file

@ -2,6 +2,7 @@ using System.Runtime.Loader;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.Animation; using Dalamud.Interface.Animation;
using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Animation.EasingFunctions;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
@ -187,16 +188,18 @@ internal sealed partial class ActiveNotification : IActiveNotification
set => this.newProgress = value; set => this.newProgress = value;
} }
private static bool ReducedMotions => Service<DalamudConfiguration>.Get().ReduceMotions;
/// <summary>Gets the eased progress.</summary> /// <summary>Gets the eased progress.</summary>
private float ProgressEased private float ProgressEased
{ {
get get
{ {
var underlyingProgress = this.underlyingNotification.Progress; var underlyingProgress = this.underlyingNotification.Progress;
if (Math.Abs(underlyingProgress - this.progressBefore) < 0.000001f || this.progressEasing.IsDone) if (Math.Abs(underlyingProgress - this.progressBefore) < 0.000001f || this.progressEasing.IsDone || ReducedMotions)
return underlyingProgress; return underlyingProgress;
var state = Math.Clamp((float)this.progressEasing.Value, 0f, 1f); var state = ReducedMotions ? 1f : Math.Clamp((float)this.progressEasing.Value, 0f, 1f);
return this.progressBefore + (state * (underlyingProgress - this.progressBefore)); return this.progressBefore + (state * (underlyingProgress - this.progressBefore));
} }
} }

View file

@ -41,6 +41,10 @@ internal static class NotificationConstants
/// <summary>The duration of indeterminate progress bar loop in milliseconds.</summary> /// <summary>The duration of indeterminate progress bar loop in milliseconds.</summary>
public const float IndeterminateProgressbarLoopDuration = 2000f; public const float IndeterminateProgressbarLoopDuration = 2000f;
/// <summary>The duration of indeterminate pie loop in milliseconds.</summary>
/// <remarks>Note that this value is applicable when reduced motion configuration is on.</remarks>
public const float IndeterminatePieLoopDuration = 8000f;
/// <summary>The duration of the progress wave animation in milliseconds.</summary> /// <summary>The duration of the progress wave animation in milliseconds.</summary>
public const float ProgressWaveLoopDuration = 2000f; public const float ProgressWaveLoopDuration = 2000f;

View file

@ -130,6 +130,12 @@ public class SettingsTabLook : SettingsTab
Loc.Localize("DalamudSettingInstallerOpenDefaultHint", "This will allow you to open the Plugin Installer to the \"Installed Plugins\" tab by default, instead of the \"Available Plugins\" tab."), Loc.Localize("DalamudSettingInstallerOpenDefaultHint", "This will allow you to open the Plugin Installer to the \"Installed Plugins\" tab by default, instead of the \"Available Plugins\" tab."),
c => c.PluginInstallerOpen == PluginInstallerWindow.PluginInstallerOpenKind.InstalledPlugins, c => c.PluginInstallerOpen == PluginInstallerWindow.PluginInstallerOpenKind.InstalledPlugins,
(v, c) => c.PluginInstallerOpen = v ? PluginInstallerWindow.PluginInstallerOpenKind.InstalledPlugins : PluginInstallerWindow.PluginInstallerOpenKind.AllPlugins), (v, c) => c.PluginInstallerOpen = v ? PluginInstallerWindow.PluginInstallerOpenKind.InstalledPlugins : PluginInstallerWindow.PluginInstallerOpenKind.AllPlugins),
new SettingsEntry<bool>(
Loc.Localize("DalamudSettingReducedMotion", "Reduce motions"),
Loc.Localize("DalamudSettingReducedMotion", "This will suppress certain animations from Dalamud, such as the notification popup."),
c => c.ReduceMotions,
(v, c) => c.ReduceMotions = v),
}; };
public override string Title => Loc.Localize("DalamudSettingsVisual", "Look & Feel"); public override string Title => Loc.Localize("DalamudSettingsVisual", "Look & Feel");

View file

@ -44,8 +44,9 @@ public static class DateTimeSpanExtensions
/// <summary>Formats an instance of <see cref="DateTime"/> as a localized relative time.</summary> /// <summary>Formats an instance of <see cref="DateTime"/> as a localized relative time.</summary>
/// <param name="when">When.</param> /// <param name="when">When.</param>
/// <param name="floorBy">The alignment unit of time span.</param>
/// <returns>The formatted string.</returns> /// <returns>The formatted string.</returns>
public static string LocRelativePastLong(this DateTime when) public static string LocRelativePastLong(this DateTime when, TimeSpan floorBy)
{ {
var loc = Loc.Localize( var loc = Loc.Localize(
"DateTimeSpanExtensions.RelativeFormatStringsLong", "DateTimeSpanExtensions.RelativeFormatStringsLong",
@ -55,13 +56,17 @@ public static class DateTimeSpanExtensions
if (relativeFormatStringLong?.FormatStringLoc != loc) if (relativeFormatStringLong?.FormatStringLoc != loc)
relativeFormatStringLong ??= new(loc); relativeFormatStringLong ??= new(loc);
return relativeFormatStringLong.Format(DateTime.Now - when); return
floorBy == default
? relativeFormatStringLong.Format(DateTime.Now - when)
: relativeFormatStringLong.Format(Math.Floor((DateTime.Now - when) / floorBy) * floorBy);
} }
/// <summary>Formats an instance of <see cref="DateTime"/> as a localized relative time.</summary> /// <summary>Formats an instance of <see cref="DateTime"/> as a localized relative time.</summary>
/// <param name="when">When.</param> /// <param name="when">When.</param>
/// <param name="floorBy">The alignment unit of time span.</param>
/// <returns>The formatted string.</returns> /// <returns>The formatted string.</returns>
public static string LocRelativePastShort(this DateTime when) public static string LocRelativePastShort(this DateTime when, TimeSpan floorBy)
{ {
var loc = Loc.Localize( var loc = Loc.Localize(
"DateTimeSpanExtensions.RelativeFormatStringsShort", "DateTimeSpanExtensions.RelativeFormatStringsShort",
@ -71,9 +76,22 @@ public static class DateTimeSpanExtensions
if (relativeFormatStringShort?.FormatStringLoc != loc) if (relativeFormatStringShort?.FormatStringLoc != loc)
relativeFormatStringShort = new(loc); relativeFormatStringShort = new(loc);
return relativeFormatStringShort.Format(DateTime.Now - when); return
floorBy == default
? relativeFormatStringShort.Format(DateTime.Now - when)
: relativeFormatStringShort.Format(Math.Floor((DateTime.Now - when) / floorBy) * floorBy);
} }
/// <summary>Formats an instance of <see cref="DateTime"/> as a localized relative time.</summary>
/// <param name="when">When.</param>
/// <returns>The formatted string.</returns>
public static string LocRelativePastLong(this DateTime when) => when.LocRelativePastLong(TimeSpan.FromSeconds(1));
/// <summary>Formats an instance of <see cref="DateTime"/> as a localized relative time.</summary>
/// <param name="when">When.</param>
/// <returns>The formatted string.</returns>
public static string LocRelativePastShort(this DateTime when) => when.LocRelativePastShort(TimeSpan.FromSeconds(1));
private sealed class ParsedRelativeFormatStrings private sealed class ParsedRelativeFormatStrings
{ {
private readonly List<(float MinSeconds, string FormatString)> formatStrings = new(); private readonly List<(float MinSeconds, string FormatString)> formatStrings = new();