diff --git a/src/Skyline.Apple/AppKitWindowBackend.cs b/src/Skyline.Apple/AppKitWindowBackend.cs index 91524de..33ac103 100644 --- a/src/Skyline.Apple/AppKitWindowBackend.cs +++ b/src/Skyline.Apple/AppKitWindowBackend.cs @@ -112,6 +112,8 @@ private void UpdateDrawableSize() } } + public double RefreshRate => (_window.Screen ?? NSScreen.MainScreen)?.MaximumFramesPerSecond ?? 60; + public string Title { get => _window.Title; diff --git a/src/Skyline/AppWindow.cs b/src/Skyline/AppWindow.cs index 962a94a..56e956d 100644 --- a/src/Skyline/AppWindow.cs +++ b/src/Skyline/AppWindow.cs @@ -158,6 +158,13 @@ public string? ClipboardText set => _backend.ClipboardText = value; } + /// + /// The display's refresh rate in Hz. Pace animation to this. On macOS it + /// is the window's screen rate; on GLFW it is the window's monitor rate + /// when known (set in fullscreen), otherwise 60. + /// + public double RefreshRate => _backend.RefreshRate; + public FrameInfo CurrentFrame => Frame(0); /// True when this window has been adopted by an . Hosted windows render on the host's thread, so use AppHost.Run, not . diff --git a/src/Skyline/GlfwWindowBackend.cs b/src/Skyline/GlfwWindowBackend.cs index 3d0d94f..bb234c0 100644 --- a/src/Skyline/GlfwWindowBackend.cs +++ b/src/Skyline/GlfwWindowBackend.cs @@ -72,6 +72,9 @@ public unsafe GlfwWindowBackend(AppWindowOptions options) public (int Width, int Height) FramebufferSize => (_window.FramebufferSize.X, _window.FramebufferSize.Y); public (int Width, int Height) LogicalSize => (_window.Size.X, _window.Size.Y); + // The window's monitor when GLFW knows it (set in fullscreen), else 60. + public double RefreshRate => _window.Monitor?.VideoMode.RefreshRate ?? 60; + public string Title { get => _window.Title; diff --git a/src/Skyline/IWindowBackend.cs b/src/Skyline/IWindowBackend.cs index e1b01c9..f562b30 100644 --- a/src/Skyline/IWindowBackend.cs +++ b/src/Skyline/IWindowBackend.cs @@ -17,6 +17,9 @@ internal interface IWindowBackend : IDisposable /// Window size in logical points. (int Width, int Height) LogicalSize { get; } + /// The display's refresh rate in Hz, for pacing animation. + double RefreshRate { get; } + string Title { get; set; } string? ClipboardText { get; set; } diff --git a/tests/Skyline.Tests/WindowBackendFactoryTests.cs b/tests/Skyline.Tests/WindowBackendFactoryTests.cs index a7b3ff2..167ea51 100644 --- a/tests/Skyline.Tests/WindowBackendFactoryTests.cs +++ b/tests/Skyline.Tests/WindowBackendFactoryTests.cs @@ -14,6 +14,7 @@ private sealed class FakeBackend : IWindowBackend { public (int Width, int Height) FramebufferSize => (200, 100); public (int Width, int Height) LogicalSize => (100, 50); + public double RefreshRate => 60; public string Title { get; set; } = "fake"; public string? ClipboardText { get; set; } public bool IsClosing => false; diff --git a/tests/Skyline.WindowedTests/Program.cs b/tests/Skyline.WindowedTests/Program.cs index 8694c73..ed1b848 100644 --- a/tests/Skyline.WindowedTests/Program.cs +++ b/tests/Skyline.WindowedTests/Program.cs @@ -38,6 +38,7 @@ void Check(bool condition, string what) Check(frame.Dpr > 0, "CurrentFrame has a device pixel ratio"); Check(Math.Abs(frame.LogicalWidth - frame.PixelWidth / frame.Dpr) < 0.01, "logical width derives from Dpr"); Check(win.Surface is not null, "native surface source is exposed"); +Check(win.RefreshRate > 0, "RefreshRate reports a positive display rate"); win.ClipboardText = "skyline-test"; Check(win.ClipboardText == "skyline-test", "clipboard round-trips"); @@ -58,6 +59,7 @@ void Check(bool condition, string what) }); var cf = chromed.CurrentFrame; Check(cf.PixelWidth > 0 && cf.PixelHeight > 0, $"{chrome} window constructs with a real surface"); + Check(chromed.RefreshRate > 0, $"{chrome} GLFW window reports a refresh rate"); } var soloResized = false;