Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 71 additions & 7 deletions RadialActions/Interop/WpfUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace RadialActions;

/// <summary>
/// Utility methods for WPF window positioning.
/// Utility methods for WPF window positioning and display information.
/// </summary>
public static class WpfUtil
{
Expand All @@ -19,8 +19,11 @@ public static class WpfUtil
[DllImport("user32.dll")]
private static extern IntPtr MonitorFromPoint(POINT pt, uint dwFlags);

[DllImport("user32.dll")]
private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFOEX lpmi);

[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern bool EnumDisplaySettings(string lpszDeviceName, int iModeNum, ref DEVMODE lpDevMode);

[DllImport("shcore.dll")]
private static extern int GetDpiForMonitor(IntPtr hmonitor, MonitorDpiType dpiType, out uint dpiX, out uint dpiY);
Expand All @@ -41,13 +44,52 @@ private struct RECT
public int Bottom;
}

[StructLayout(LayoutKind.Sequential)]
private struct MONITORINFO
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct MONITORINFOEX
{
public int cbSize;
public RECT rcMonitor;
public RECT rcWork;
public uint dwFlags;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string szDevice;
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct DEVMODE
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string dmDeviceName;
public ushort dmSpecVersion;
public ushort dmDriverVersion;
public ushort dmSize;
public ushort dmDriverExtra;
public uint dmFields;
public int dmPositionX;
public int dmPositionY;
public uint dmDisplayOrientation;
public uint dmDisplayFixedOutput;
public short dmColor;
public short dmDuplex;
public short dmYResolution;
public short dmTTOption;
public short dmCollate;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string dmFormName;
public ushort dmLogPixels;
public uint dmBitsPerPel;
public uint dmPelsWidth;
public uint dmPelsHeight;
public uint dmDisplayFlags;
public uint dmDisplayFrequency;
public uint dmICMMethod;
public uint dmICMIntent;
public uint dmMediaType;
public uint dmDitherType;
public uint dmReserved1;
public uint dmReserved2;
public uint dmPanningWidth;
public uint dmPanningHeight;
}

private enum MonitorDpiType
Expand All @@ -58,6 +100,7 @@ private enum MonitorDpiType
private const uint SWP_NOZORDER = 0x0004;
private const uint SWP_NOSIZE = 0x0001;
private const uint MONITOR_DEFAULTTONEAREST = 2;
private const int ENUM_CURRENT_SETTINGS = -1;
private const int DefaultDpi = 96;

/// <summary>
Expand Down Expand Up @@ -145,13 +188,34 @@ internal static int ScaleDipToPixels(double dipValue, double dpiScale)
return (int)Math.Round(dipValue * dpiScale);
}

private static bool TryGetMonitorInfo(POINT point, out IntPtr monitor, out MONITORINFO monitorInfo)
private static bool TryGetMonitorInfo(POINT point, out IntPtr monitor, out MONITORINFOEX monitorInfo)
{
monitor = MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST);
monitorInfo = new MONITORINFO { cbSize = Marshal.SizeOf<MONITORINFO>() };
monitorInfo = new MONITORINFOEX { cbSize = Marshal.SizeOf<MONITORINFOEX>() };
return monitor != IntPtr.Zero && GetMonitorInfo(monitor, ref monitorInfo);
}

/// <summary>
/// Gets the refresh rate in Hz of the display the cursor is currently on, or 0 if it can't be determined.
/// </summary>
public static int GetCursorMonitorRefreshRate()
{
if (!GetCursorPos(out var cursorPosition) || !TryGetMonitorInfo(cursorPosition, out _, out var monitorInfo))
{
return 0;
}

var devMode = new DEVMODE { dmSize = (ushort)Marshal.SizeOf<DEVMODE>() };

// A frequency of 0 or 1 means the hardware default, not a real rate.
if (EnumDisplaySettings(monitorInfo.szDevice, ENUM_CURRENT_SETTINGS, ref devMode) && devMode.dmDisplayFrequency > 1)
{
return (int)devMode.dmDisplayFrequency;
}

return 0;
}

private static Size GetWindowSize(Window window)
{
var width = window.ActualWidth;
Expand Down
12 changes: 11 additions & 1 deletion RadialActions/Pie/PieAnimationService.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
using System.Windows;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;

namespace RadialActions;

internal sealed class PieAnimationService
{
/// <summary>
/// Desired frame rate in Hz for animations, matching the refresh rate of the display the menu is on. Null uses the WPF default of about 60.
/// </summary>
public int? DesiredFrameRate { get; set; }

public void ApplyBrushColor(
SolidColorBrush brush,
Color color,
Expand Down Expand Up @@ -43,6 +48,7 @@ public void AnimateBrushColor(
EasingFunction = easingFunction,
};

Timeline.SetDesiredFrameRate(colorAnimation, DesiredFrameRate);
brush.BeginAnimation(SolidColorBrush.ColorProperty, colorAnimation, HandoffBehavior.SnapshotAndReplace);
}

Expand Down Expand Up @@ -73,6 +79,7 @@ public void AnimateOpacity(
opacityAnimation.Completed += (_, _) => onCompleted();
}

Timeline.SetDesiredFrameRate(opacityAnimation, DesiredFrameRate);
element.BeginAnimation(UIElement.OpacityProperty, opacityAnimation, HandoffBehavior.SnapshotAndReplace);
}

Expand Down Expand Up @@ -103,6 +110,7 @@ public void AnimateRotationAngle(
rotationAnimation.Completed += (_, _) => onCompleted();
}

Timeline.SetDesiredFrameRate(rotationAnimation, DesiredFrameRate);
transform.BeginAnimation(RotateTransform.AngleProperty, rotationAnimation, HandoffBehavior.SnapshotAndReplace);
}

Expand Down Expand Up @@ -133,6 +141,7 @@ public void AnimateClickDown(UIElement target, Duration duration, IEasingFunctio
FillBehavior = FillBehavior.HoldEnd,
};

Timeline.SetDesiredFrameRate(scaleAnimation, DesiredFrameRate);
scaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, scaleAnimation, HandoffBehavior.SnapshotAndReplace);
scaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, scaleAnimation, HandoffBehavior.SnapshotAndReplace);
}
Expand Down Expand Up @@ -161,6 +170,7 @@ public void AnimateClickUp(UIElement target, Duration duration, IEasingFunction
EasingFunction = easingFunction,
};

Timeline.SetDesiredFrameRate(scaleAnimation, DesiredFrameRate);
scaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, scaleAnimation, HandoffBehavior.SnapshotAndReplace);
scaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, scaleAnimation, HandoffBehavior.SnapshotAndReplace);
}
Expand Down
9 changes: 9 additions & 0 deletions RadialActions/Pie/PieControl.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ private sealed class DragReorderState
public required int TargetSlot { get; set; }
}

/// <summary>
/// Desired frame rate in Hz for pie animations, matching the refresh rate of the display the menu is on. Null uses the WPF default of about 60.
/// </summary>
public int? AnimationFrameRate
{
get => _animationService.DesiredFrameRate;
set => _animationService.DesiredFrameRate = value;
}

public PieControl()
{
InitializeComponent();
Expand Down
17 changes: 17 additions & 0 deletions RadialActions/Services/MenuService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public void ShowMenu(bool atCursor)
_window.CenterOnScreen();
}

SyncAnimationFrameRate();

if (!_window.IsVisible)
{
_window.Opacity = 0;
Expand All @@ -54,6 +56,21 @@ public void ShowMenu(bool atCursor)
BeginFadeIn();
}

/// <summary>
/// Points the animation clock at the refresh rate of the display the menu is opening on, so animations aren't capped at the WPF default of about 60 fps.
/// Re-queried on every show because the menu can open on a different display each time and rates change with hotplug or settings.
/// </summary>
private void SyncAnimationFrameRate()
{
var refreshRate = WpfUtil.GetCursorMonitorRefreshRate();
int? desiredFrameRate = refreshRate > 0 ? refreshRate : null;
Log.Debug($"Display refresh rate: {refreshRate} Hz");

Timeline.SetDesiredFrameRate(_fadeInStoryboard, desiredFrameRate);
Timeline.SetDesiredFrameRate(_fadeOutStoryboard, desiredFrameRate);
_pieMenu.AnimationFrameRate = desiredFrameRate;
}

public void HideMenu(bool animate = true)
{
if (!_window.IsVisible || _isFadingOut)
Expand Down