Skip to content

Commit f635154

Browse files
authored
Respect OS "reduce motion" setting for Avalonia UI animations (#4904)
1 parent bb53804 commit f635154

4 files changed

Lines changed: 123 additions & 0 deletions

File tree

src/UniGetUI.Avalonia/Infrastructure/DirectionalSlideTransition.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ public sealed class DirectionalSlideTransition : IPageTransition
2929

3030
public async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
3131
{
32+
// Honor the OS "reduce motion" preference: swap pages instantly, no slide.
33+
if (MotionPreference.ReducedMotion)
34+
{
35+
if (from is not null) { from.IsVisible = false; from.RenderTransform = null; }
36+
to?.RenderTransform = null;
37+
return;
38+
}
39+
3240
double sign = Reverse ? -1d : 1d;
3341
double width = (to ?? from)?.GetVisualParent()?.Bounds.Width
3442
?? (to ?? from)?.Bounds.Width ?? 0d;

src/UniGetUI.Avalonia/Infrastructure/EntrancePageTransition.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ public sealed class EntrancePageTransition : IPageTransition
2222

2323
public async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
2424
{
25+
// Honor "reduce motion": hide the outgoing page and show the incoming one instantly, no overlap.
26+
if (MotionPreference.ReducedMotion)
27+
{
28+
from?.Opacity = 0;
29+
to?.Opacity = 1;
30+
return;
31+
}
32+
2533
// Drop the outgoing page immediately so only the incoming page animates in.
2634
from?.Opacity = 0;
2735

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.Runtime.InteropServices;
4+
using System.Runtime.Versioning;
5+
using UniGetUI.Core.Logging;
6+
7+
namespace UniGetUI.Avalonia.Infrastructure;
8+
9+
/// <summary>
10+
/// Reads the OS "reduce motion / show animations" accessibility preference so UI
11+
/// transitions can honor it (Windows: Settings → Accessibility → Visual effects →
12+
/// Animation effects; macOS: Accessibility → Display → Reduce motion). Windows is read
13+
/// live on each access (cheap P/Invoke); macOS/Linux are read once and cached because
14+
/// they spawn a process. Defaults to "animations on" if the preference can't be read.
15+
/// </summary>
16+
internal static class MotionPreference
17+
{
18+
private static bool? _cachedUnix;
19+
20+
/// <summary>True when the user has asked the OS to minimize animations.</summary>
21+
public static bool ReducedMotion
22+
{
23+
get
24+
{
25+
if (OperatingSystem.IsWindows())
26+
return GetWindowsReducedMotion();
27+
return _cachedUnix ??= GetUnixReducedMotion();
28+
}
29+
}
30+
31+
[SupportedOSPlatform("windows")]
32+
private static bool GetWindowsReducedMotion()
33+
{
34+
try
35+
{
36+
// Same signal WinUI's UISettings.AnimationsEnabled reads; pvParam is a BOOL.
37+
int enabled = 1;
38+
if (NativeMethods.SystemParametersInfoW(NativeMethods.SPI_GETCLIENTAREAANIMATION, 0, ref enabled, 0))
39+
return enabled == 0;
40+
}
41+
catch (Exception ex)
42+
{
43+
Logger.Warn("Could not read SPI_GETCLIENTAREAANIMATION; assuming animations enabled");
44+
Logger.Warn(ex);
45+
}
46+
return false;
47+
}
48+
49+
private static bool GetUnixReducedMotion()
50+
{
51+
try
52+
{
53+
if (OperatingSystem.IsMacOS())
54+
return ReadCommand("/usr/bin/defaults", ["read", "com.apple.universalaccess", "reduceMotion"]) is "1";
55+
56+
if (OperatingSystem.IsLinux())
57+
return string.Equals(
58+
ReadCommand("/usr/bin/gsettings", ["get", "org.gnome.desktop.interface", "gtk-enable-animations"]),
59+
"false", StringComparison.OrdinalIgnoreCase);
60+
}
61+
catch (Exception ex)
62+
{
63+
Logger.Warn("Could not read OS reduce-motion preference; assuming animations enabled");
64+
Logger.Warn(ex);
65+
}
66+
return false;
67+
}
68+
69+
private static string? ReadCommand(string fileName, string[] args)
70+
{
71+
var psi = new ProcessStartInfo
72+
{
73+
FileName = fileName,
74+
UseShellExecute = false,
75+
RedirectStandardOutput = true,
76+
RedirectStandardError = true,
77+
CreateNoWindow = true,
78+
};
79+
foreach (var arg in args)
80+
psi.ArgumentList.Add(arg);
81+
82+
using var proc = Process.Start(psi);
83+
if (proc is null)
84+
return null;
85+
86+
string output = proc.StandardOutput.ReadToEnd();
87+
if (!proc.WaitForExit(2000))
88+
{
89+
try { proc.Kill(); } catch { /* best effort */ }
90+
return null;
91+
}
92+
return proc.ExitCode == 0 ? output.Trim() : null;
93+
}
94+
95+
private static class NativeMethods
96+
{
97+
public const uint SPI_GETCLIENTAREAANIMATION = 0x1042;
98+
99+
[DllImport("user32.dll", SetLastError = true)]
100+
[return: MarshalAs(UnmanagedType.Bool)]
101+
public static extern bool SystemParametersInfoW(uint uiAction, uint uiParam, ref int pvParam, uint fWinIni);
102+
}
103+
}

src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ public PackageDetailsWindow(IPackage package, OperationType operation)
4545
InitializeComponent();
4646
UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this);
4747

48+
// Honor the OS "reduce motion" preference: drop the screenshot slide animation.
49+
if (MotionPreference.ReducedMotion)
50+
ScreenshotsCarousel.PageTransition = null;
51+
4852
_vm.CloseRequested += (_, _) => Close();
4953
_vm.DetailsLoaded += (_, _) =>
5054
{

0 commit comments

Comments
 (0)