diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs
index 79c0d331cd..78b89d6cb1 100644
--- a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs
+++ b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs
@@ -20,9 +20,9 @@ public static class HeadlessWindowExtensions
/// Bitmap with last rendered frame. Null, if nothing was rendered.
public static WriteableBitmap? CaptureRenderedFrame(this TopLevel topLevel)
{
- Dispatcher.UIThread.RunJobs();
- AvaloniaHeadlessPlatform.ForceRenderTimerTick();
- return topLevel.GetLastRenderedFrame();
+ WriteableBitmap? bitmap = null;
+ topLevel.RunJobsOnImpl(w => bitmap = w.GetLastRenderedFrame());
+ return bitmap;
}
///
@@ -114,6 +114,15 @@ public static class HeadlessWindowExtensions
DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None) =>
RunJobsOnImpl(topLevel, w => w.DragDrop(point, type, data, effects, modifiers));
+ ///
+ /// Changes the render scaling (DPI) of the headless window/toplevel.
+ /// This simulates a DPI change, triggering scaling changed notifications and a layout pass.
+ ///
+ /// The target headless top level.
+ /// The new render scaling factor. Must be greater than zero.
+ public static void SetRenderScaling(this TopLevel topLevel, double scaling) =>
+ RunJobsOnImpl(topLevel, w => w.SetRenderScaling(scaling));
+
private static void RunJobsOnImpl(this TopLevel topLevel, Action action)
{
RunJobsAndRender();
diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs
index 275dc7f48a..999a20644f 100644
--- a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs
+++ b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs
@@ -49,7 +49,7 @@ namespace Avalonia.Headless
public Size ClientSize { get; set; }
public Size? FrameSize => null;
- public double RenderScaling { get; } = 1;
+ public double RenderScaling { get; private set; } = 1;
public double DesktopScaling => RenderScaling;
public IPlatformRenderSurface[] Surfaces { get; }
public Action? Input { get; set; }
@@ -358,6 +358,20 @@ namespace Avalonia.Headless
Input?.Invoke(new RawDragEvent(device, type, InputRoot!, point, data, effects, modifiers));
}
+ void IHeadlessWindow.SetRenderScaling(double scaling)
+ {
+ if (scaling <= 0)
+ throw new ArgumentOutOfRangeException(nameof(scaling), "Scaling must be greater than zero.");
+
+ if (RenderScaling == scaling)
+ return;
+
+ var oldScaledSize = ClientSize;
+ RenderScaling = scaling;
+ ScalingChanged?.Invoke(scaling);
+ Resize(oldScaledSize, WindowResizeReason.DpiChange);
+ }
+
void IWindowImpl.Move(PixelPoint point)
{
Position = point;
diff --git a/src/Headless/Avalonia.Headless/IHeadlessWindow.cs b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs
index 30c2390f64..44ac0a5ace 100644
--- a/src/Headless/Avalonia.Headless/IHeadlessWindow.cs
+++ b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs
@@ -16,5 +16,6 @@ namespace Avalonia.Headless
void MouseUp(Point point, MouseButton button, RawInputModifiers modifiers = RawInputModifiers.None);
void MouseWheel(Point point, Vector delta, RawInputModifiers modifiers = RawInputModifiers.None);
void DragDrop(Point point, RawDragEventType type, IDataTransfer data, DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None);
+ void SetRenderScaling(double scaling);
}
}
diff --git a/tests/Avalonia.Headless.UnitTests/RenderingTests.cs b/tests/Avalonia.Headless.UnitTests/RenderingTests.cs
index 1541b74fd9..24db2d2285 100644
--- a/tests/Avalonia.Headless.UnitTests/RenderingTests.cs
+++ b/tests/Avalonia.Headless.UnitTests/RenderingTests.cs
@@ -169,4 +169,66 @@ public class RenderingTests
AssertHelper.Equal(100, snapshot.Size.Width);
AssertHelper.Equal(100, snapshot.Size.Height);
}
+
+#if NUNIT
+ [AvaloniaTest]
+#elif XUNIT
+ [AvaloniaFact]
+#endif
+ public void Should_Change_Render_Scaling()
+ {
+ var window = new Window
+ {
+ Content = new Border
+ {
+ Background = Brushes.Red
+ },
+ Width = 100,
+ Height = 100,
+ };
+
+ window.Show();
+
+ var frameBefore = window.CaptureRenderedFrame();
+ AssertHelper.NotNull(frameBefore);
+
+ var sizeBefore = frameBefore!.PixelSize;
+
+ window.SetRenderScaling(2.0);
+
+ AssertHelper.Equal(2.0, window.RenderScaling);
+
+ var frameAfter = window.CaptureRenderedFrame();
+ AssertHelper.NotNull(frameAfter);
+
+ var sizeAfter = frameAfter!.PixelSize;
+
+ AssertHelper.Equal(sizeBefore.Width * 2, sizeAfter.Width);
+ AssertHelper.Equal(sizeBefore.Height * 2, sizeAfter.Height);
+ }
+
+#if NUNIT
+ [AvaloniaTest]
+#elif XUNIT
+ [AvaloniaFact]
+#endif
+ public void Should_Keep_Client_Size_After_Scaling_Change()
+ {
+ var window = new Window
+ {
+ Width = 200,
+ Height = 150
+ };
+
+ window.Show();
+ window.CaptureRenderedFrame();
+
+ var clientSizeBefore = window.ClientSize;
+
+ window.SetRenderScaling(2.0);
+ window.CaptureRenderedFrame();
+
+ AssertHelper.Equal(clientSizeBefore.Width, window.ClientSize.Width);
+ AssertHelper.Equal(clientSizeBefore.Height, window.ClientSize.Height);
+ }
}