From 74c20f1fdc3f55d46d701d966911b85ca8c40d82 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 16 Mar 2026 23:37:04 +0900 Subject: [PATCH] New HeadlessWindow.SetRenderScaling API (#20888) * Add `void IHeadlessWindow.SetRenderScaling` API * Add tests * Enforce Window * Try to fix failing test --- .../HeadlessWindowExtensions.cs | 15 ++++- .../Avalonia.Headless/HeadlessWindowImpl.cs | 16 ++++- .../Avalonia.Headless/IHeadlessWindow.cs | 1 + .../RenderingTests.cs | 62 +++++++++++++++++++ 4 files changed, 90 insertions(+), 4 deletions(-) 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); + } }