浏览代码

Merge pull request #2847 from AvaloniaUI/headless-platform

Headless platform
danwalmsley 5 年之前
父节点
当前提交
ae182c12da

+ 52 - 0
Avalonia.sln

@@ -207,6 +207,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NativeEmbedSample", "sample
 EndProject
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Fluent", "src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj", "{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}"
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Fluent", "src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj", "{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}"
 EndProject
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless", "src\Avalonia.Headless\Avalonia.Headless.csproj", "{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.Vnc", "src\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj", "{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}"
+EndProject
 Global
 Global
 	GlobalSection(SharedMSBuildProjectFiles) = preSolution
 	GlobalSection(SharedMSBuildProjectFiles) = preSolution
 		src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13
 		src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13
@@ -1826,6 +1830,54 @@ Global
 		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhone.Build.0 = Release|Any CPU
 		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhone.Build.0 = Release|Any CPU
 		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
 		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
 		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
 		{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+		{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+		{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+		{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+		{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+		{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+		{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+		{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+		{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhone.Build.0 = Debug|Any CPU
+		{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+		{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhone.Build.0 = Debug|Any CPU
+		{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|Any CPU.Build.0 = Release|Any CPU
+		{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhone.ActiveCfg = Release|Any CPU
+		{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhone.Build.0 = Release|Any CPU
+		{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+		{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+		{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+		{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+		{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+		{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+		{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+		{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+		{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+		{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhone.Build.0 = Debug|Any CPU
+		{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+		{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhone.Build.0 = Debug|Any CPU
+		{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|Any CPU.Build.0 = Release|Any CPU
+		{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhone.ActiveCfg = Release|Any CPU
+		{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhone.Build.0 = Release|Any CPU
+		{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+		{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
 		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
 		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
 		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
 		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
 		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
 		{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU

+ 2 - 1
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

@@ -7,12 +7,13 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
+    <ProjectReference Include="..\..\src\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Dialogs\Avalonia.Dialogs.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Dialogs\Avalonia.Dialogs.csproj" />
     <ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
     <ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
     <ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
     <ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Desktop\Avalonia.Desktop.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Desktop\Avalonia.Desktop.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.X11\Avalonia.X11.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.X11\Avalonia.X11.csproj" />
-    <PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2019013001"/>
+    <PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2019013001" />
   </ItemGroup>
   </ItemGroup>
 
 
 
 

+ 54 - 1
samples/ControlCatalog.NetCore/Program.cs

@@ -3,9 +3,16 @@ using System.Diagnostics;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
+using System.Threading.Tasks;
 using Avalonia;
 using Avalonia;
-using Avalonia.Dialogs;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Headless;
+using Avalonia.LogicalTree;
+using Avalonia.Skia;
 using Avalonia.ReactiveUI;
 using Avalonia.ReactiveUI;
+using Avalonia.Threading;
+using Avalonia.Dialogs;
 
 
 namespace ControlCatalog.NetCore
 namespace ControlCatalog.NetCore
 {
 {
@@ -40,6 +47,52 @@ namespace ControlCatalog.NetCore
                 SilenceConsole();
                 SilenceConsole();
                 return builder.StartLinuxFbDev(args, scaling: GetScaling());
                 return builder.StartLinuxFbDev(args, scaling: GetScaling());
             }
             }
+            else if (args.Contains("--vnc"))
+            {
+                return builder.StartWithHeadlessVncPlatform(null, 5901, args, ShutdownMode.OnMainWindowClose);
+            }
+            else if (args.Contains("--full-headless"))
+            {
+                return builder
+                    .UseHeadless(true)
+                    .AfterSetup(_ =>
+                    {
+                        DispatcherTimer.RunOnce(async () =>
+                        {
+                            var window = ((IClassicDesktopStyleApplicationLifetime)Application.Current.ApplicationLifetime)
+                                .MainWindow;
+                            var tc = window.GetLogicalDescendants().OfType<TabControl>().First();
+                            foreach (var page in tc.Items.Cast<TabItem>().ToList())
+                            {
+                                // Skip DatePicker because of some layout bug in grid
+                                if (page.Header.ToString() == "DatePicker")
+                                    continue;
+                                Console.WriteLine("Selecting " + page.Header);
+                                tc.SelectedItem = page;
+                                await Task.Delay(500);
+                            }
+                            Console.WriteLine("Selecting the first page");
+                            tc.SelectedItem = tc.Items.OfType<object>().First();
+                            await Task.Delay(500);
+                            Console.WriteLine("Clicked through all pages, triggering GC");
+                            for (var c = 0; c < 3; c++)
+                            {
+                                GC.Collect(2, GCCollectionMode.Forced);
+                                await Task.Delay(500);
+                            }
+
+                            void FormatMem(string metric, long bytes)
+                            {
+                                Console.WriteLine(metric + ": " + bytes / 1024 / 1024 + "MB");
+                            }
+
+                            FormatMem("GC allocated bytes", GC.GetTotalMemory(true));
+                            FormatMem("WorkingSet64", Process.GetCurrentProcess().WorkingSet64);
+
+                        }, TimeSpan.FromSeconds(1));
+                    })
+                    .StartWithClassicDesktopLifetime(args);
+            }
             else if (args.Contains("--drm"))
             else if (args.Contains("--drm"))
             {
             {
                 SilenceConsole();
                 SilenceConsole();

+ 12 - 0
src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj

@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>netstandard2.0</TargetFramework>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <ProjectReference Include="..\Avalonia.Headless\Avalonia.Headless.csproj" />
+      <PackageReference Include="Quamotion.RemoteViewing" Version="1.1.21" />
+    </ItemGroup>
+
+</Project>

+ 96 - 0
src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs

@@ -0,0 +1,96 @@
+using System;
+using System.Runtime.InteropServices;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Threading;
+using RemoteViewing.Vnc;
+using RemoteViewing.Vnc.Server;
+
+namespace Avalonia.Headless.Vnc
+{
+    public class HeadlessVncFramebufferSource : IVncFramebufferSource
+    {
+        public IHeadlessWindow Window { get; set; }
+        private object _lock = new object();
+        public VncFramebuffer _framebuffer = new VncFramebuffer("Avalonia", 1, 1, VncPixelFormat.RGB32);
+
+        private VncButton _previousButtons;
+        public HeadlessVncFramebufferSource(VncServerSession session, Window window)
+        {
+            Window = (IHeadlessWindow)window.PlatformImpl;
+            session.PointerChanged += (_, args) =>
+            {
+                var pt = new Point(args.X, args.Y);
+                    
+                var buttons = (VncButton)args.PressedButtons;
+
+                int TranslateButton(VncButton vncButton) =>
+                    vncButton == VncButton.Left ? 0 : vncButton == VncButton.Right ? 1 : 2;
+
+                var modifiers = (RawInputModifiers)(((int)buttons & 7) << 4);
+                
+                Dispatcher.UIThread.Post(() =>
+                {
+                    Window?.MouseMove(pt);
+                    foreach (var btn in CheckedButtons)
+                        if (_previousButtons.HasFlag(btn) && !buttons.HasFlag(btn))
+                            Window?.MouseUp(pt, TranslateButton(btn), modifiers);
+                    
+                    foreach (var btn in CheckedButtons)
+                        if (!_previousButtons.HasFlag(btn) && buttons.HasFlag(btn))
+                            Window?.MouseDown(pt, TranslateButton(btn), modifiers);
+                    _previousButtons = buttons;
+                }, DispatcherPriority.Input);
+            };
+            
+        }
+
+        [Flags]
+        enum VncButton
+        {
+            Left = 1,
+            Middle = 2,
+            Right = 4,
+            ScrollUp = 8,
+            ScrollDown = 16
+        }
+        
+
+        private static VncButton[] CheckedButtons = new[] {VncButton.Left, VncButton.Middle, VncButton.Right}; 
+
+        public VncFramebuffer Capture()
+        {
+            lock (_lock)
+            {
+                using (var bmpRef = Window.GetLastRenderedFrame())
+                {
+                    if (bmpRef?.Item == null)
+                        return _framebuffer;
+                    var bmp = bmpRef.Item;
+                    if (bmp.PixelSize.Width != _framebuffer.Width || bmp.PixelSize.Height != _framebuffer.Height)
+                    {
+                        _framebuffer = new VncFramebuffer("Avalonia", bmp.PixelSize.Width, bmp.PixelSize.Height,
+                            VncPixelFormat.RGB32);
+                    }
+
+                    using (var fb = bmp.Lock())
+                    {
+                        var buf = _framebuffer.GetBuffer();
+                        if (_framebuffer.Stride == fb.RowBytes)
+                            Marshal.Copy(fb.Address, buf, 0, buf.Length);
+                        else
+                            for (var y = 0; y < fb.Size.Height; y++)
+                            {
+                                var sourceStart = fb.RowBytes * y;
+                                var dstStart = _framebuffer.Stride * y;
+                                var row = fb.Size.Width * 4;
+                                Marshal.Copy(new IntPtr(sourceStart + fb.Address.ToInt64()), buf, dstStart, row);
+                            }
+                    }
+                }
+            }
+
+            return _framebuffer;
+        }
+    }
+}

+ 48 - 0
src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs

@@ -0,0 +1,48 @@
+using System.Net;
+using System.Net.Sockets;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Headless;
+using Avalonia.Headless.Vnc;
+using RemoteViewing.Vnc;
+using RemoteViewing.Vnc.Server;
+
+namespace Avalonia
+{
+    public static class HeadlessVncPlatformExtensions
+    {
+        public static int StartWithHeadlessVncPlatform<T>(
+            this T builder,
+            string host, int port,
+            string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose)
+            where T : AppBuilderBase<T>, new()
+        {
+            var tcpServer = new TcpListener(host == null ? IPAddress.Loopback : IPAddress.Parse(host), port);
+            tcpServer.Start();    
+            return builder
+                .UseHeadless(false)
+                .AfterSetup(_ =>
+                {
+                    var lt = ((IClassicDesktopStyleApplicationLifetime)builder.Instance.ApplicationLifetime);
+                    lt.Startup += async delegate
+                    {
+                        while (true)
+                        {
+                            var client = await tcpServer.AcceptTcpClientAsync();
+                            var options = new VncServerSessionOptions
+                            {
+                                AuthenticationMethod = AuthenticationMethod.None
+                            };
+                            var session = new VncServerSession();
+                            
+                            session.SetFramebufferSource(new HeadlessVncFramebufferSource(
+                                session, lt.MainWindow));
+                            session.Connect(client.GetStream(), options);
+                        }
+                        
+                    };
+                })
+                .StartWithClassicDesktopLifetime(args, shutdownMode);
+        }
+    }
+}

+ 8 - 0
src/Avalonia.Headless/Avalonia.Headless.csproj

@@ -0,0 +1,8 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+  </PropertyGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
+  </ItemGroup>
+</Project>

+ 94 - 0
src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs

@@ -0,0 +1,94 @@
+using System;
+using System.Diagnostics;
+using System.Reactive.Disposables;
+using Avalonia.Controls;
+using Avalonia.Controls.Platform;
+using Avalonia.Input;
+using Avalonia.Input.Platform;
+using Avalonia.Platform;
+using Avalonia.Rendering;
+using Avalonia.Threading;
+
+namespace Avalonia.Headless
+{
+    public static class AvaloniaHeadlessPlatform
+    {
+        class RenderTimer : DefaultRenderTimer
+        {
+            private readonly int _framesPerSecond;
+            private Action _forceTick; 
+            protected override IDisposable StartCore(Action<TimeSpan> tick)
+            {
+                bool cancelled = false;
+                var st = Stopwatch.StartNew();
+                _forceTick = () => tick(st.Elapsed);
+                DispatcherTimer.Run(() =>
+                {
+                    if (cancelled)
+                        return false;
+                    tick(st.Elapsed);
+                    return !cancelled;
+                }, TimeSpan.FromSeconds(1.0 / _framesPerSecond), DispatcherPriority.Render);
+                return Disposable.Create(() =>
+                {
+                    _forceTick = null;
+                    cancelled = true;
+                });
+            }
+
+            public RenderTimer(int framesPerSecond) : base(framesPerSecond)
+            {
+                _framesPerSecond = framesPerSecond;
+            }
+
+            public void ForceTick() => _forceTick?.Invoke();
+        }
+
+        class HeadlessWindowingPlatform : IWindowingPlatform
+        {
+            public IWindowImpl CreateWindow() => new HeadlessWindowImpl(false);
+
+            public IWindowImpl CreateEmbeddableWindow() => throw new PlatformNotSupportedException();
+
+            public IPopupImpl CreatePopup() => new HeadlessWindowImpl(true);
+        }
+        
+        internal static void Initialize()
+        {
+            AvaloniaLocator.CurrentMutable
+                .Bind<IPlatformThreadingInterface>().ToConstant(new HeadlessPlatformThreadingInterface())
+                .Bind<IClipboard>().ToSingleton<HeadlessClipboardStub>()
+                .Bind<IStandardCursorFactory>().ToSingleton<HeadlessCursorFactoryStub>()
+                .Bind<IPlatformSettings>().ToConstant(new HeadlessPlatformSettingsStub())
+                .Bind<ISystemDialogImpl>().ToSingleton<HeadlessSystemDialogsStub>()
+                .Bind<IPlatformIconLoader>().ToSingleton<HeadlessIconLoaderStub>()
+                .Bind<IKeyboardDevice>().ToConstant(new KeyboardDevice())
+                .Bind<IRenderLoop>().ToConstant(new RenderLoop())
+                .Bind<IRenderTimer>().ToConstant(new RenderTimer(60))
+                .Bind<IFontManagerImpl>().ToSingleton<HeadlessFontManagerStub>()
+                .Bind<ITextShaperImpl>().ToSingleton<HeadlessTextShaperStub>()
+                .Bind<IWindowingPlatform>().ToConstant(new HeadlessWindowingPlatform())
+                .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();
+        }
+
+
+        public static void ForceRenderTimerTick(int count = 1)
+        {
+            var timer = AvaloniaLocator.Current.GetService<IRenderTimer>() as RenderTimer;
+            for (var c = 0; c < count; c++)
+                timer?.ForceTick();
+
+        }
+    }
+    
+    public static class AvaloniaHeadlessPlatformExtensions
+    {
+        public static T UseHeadless<T>(this T builder, bool headlessDrawing = true) 
+            where T : AppBuilderBase<T>, new()
+        {
+            if (headlessDrawing)
+                builder.UseRenderingSubsystem(HeadlessPlatformRenderInterface.Initialize, "Headless");
+            return builder.UseWindowingSubsystem(AvaloniaHeadlessPlatform.Initialize, "Headless");
+        }
+    }
+}

+ 434 - 0
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@@ -0,0 +1,434 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.InteropServices;
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.Rendering;
+using Avalonia.Rendering.SceneGraph;
+using Avalonia.Utilities;
+using Avalonia.Visuals.Media.Imaging;
+
+namespace Avalonia.Headless
+{
+    internal class HeadlessPlatformRenderInterface : IPlatformRenderInterface
+    {
+        public static void Initialize()
+        {
+            AvaloniaLocator.CurrentMutable
+                .Bind<IPlatformRenderInterface>().ToConstant(new HeadlessPlatformRenderInterface());
+        }
+
+        public IEnumerable<string> InstalledFontNames { get; } = new[] { "Tahoma" };
+
+        public bool SupportsIndividualRoundRects => throw new NotImplementedException();
+
+        public IFormattedTextImpl CreateFormattedText(string text, Typeface typeface, double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, IReadOnlyList<FormattedTextStyleSpan> spans)
+        {
+            return new HeadlessFormattedTextStub(text, constraint);
+        }
+
+        public IGeometryImpl CreateEllipseGeometry(Rect rect) => new HeadlessGeometryStub(rect);
+
+        public IGeometryImpl CreateLineGeometry(Point p1, Point p2)
+        {
+            var tl = new Point(Math.Min(p1.X, p2.X), Math.Min(p1.Y, p2.Y));
+            var br = new Point(Math.Max(p1.X, p2.X), Math.Max(p1.Y, p2.Y));
+            return new HeadlessGeometryStub(new Rect(tl, br));
+        }
+
+        public IGeometryImpl CreateRectangleGeometry(Rect rect)
+        {
+            return new HeadlessGeometryStub(rect);
+        }
+
+        public IStreamGeometryImpl CreateStreamGeometry() => new HeadlessStreamingGeometryStub();
+
+        public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces) => new HeadlessRenderTarget();
+
+        public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi)
+        {
+            return new HeadlessBitmapStub(size, dpi);
+        }
+
+        public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? format = null)
+        {
+            return new HeadlessBitmapStub(size, dpi);
+        }
+
+        public IBitmapImpl LoadBitmap(string fileName)
+        {
+            return new HeadlessBitmapStub(new Size(1, 1), new Vector(96, 96));
+        }
+
+        public IBitmapImpl LoadBitmap(Stream stream)
+        {
+            return new HeadlessBitmapStub(new Size(1, 1), new Vector(96, 96));
+        }
+
+        public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride)
+        {
+            return new HeadlessBitmapStub(new Size(1, 1), new Vector(96, 96));
+        }        
+
+        public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
+        {
+            return new HeadlessBitmapStub(new Size(width, width), new Vector(96, 96));
+        }
+
+        public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
+        {
+            return new HeadlessBitmapStub(new Size(height, height), new Vector(96, 96));
+        }
+
+        public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
+        {
+            return new HeadlessBitmapStub(destinationSize, new Vector(96, 96));
+        }
+
+        public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
+        {
+            width = 100;
+            return new HeadlessGlyphRunStub();
+        }
+
+        class HeadlessGeometryStub : IGeometryImpl
+        {
+            public HeadlessGeometryStub(Rect bounds)
+            {
+                Bounds = bounds;
+            }
+
+            public Rect Bounds { get; set; }
+            public virtual bool FillContains(Point point) => Bounds.Contains(point);
+
+            public Rect GetRenderBounds(IPen pen)
+            {
+                if(pen is null)
+                {
+                    return Bounds;
+                }
+
+                return Bounds.Inflate(pen.Thickness / 2);
+            }
+
+            public bool StrokeContains(IPen pen, Point point)
+            {
+                return false;
+            }
+
+            public IGeometryImpl Intersect(IGeometryImpl geometry)
+                => new HeadlessGeometryStub(geometry.Bounds.Intersect(Bounds));
+
+            public ITransformedGeometryImpl WithTransform(Matrix transform) =>
+                new HeadlessTransformedGeometryStub(this, transform);
+        }
+
+        class HeadlessTransformedGeometryStub : HeadlessGeometryStub, ITransformedGeometryImpl
+        {
+            public HeadlessTransformedGeometryStub(IGeometryImpl b, Matrix transform) : this(Fix(b, transform))
+            {
+
+            }
+
+            static (IGeometryImpl, Matrix, Rect) Fix(IGeometryImpl b, Matrix transform)
+            {
+                if (b is HeadlessTransformedGeometryStub transformed)
+                {
+                    b = transformed.SourceGeometry;
+                    transform = transformed.Transform * transform;
+                }
+
+                return (b, transform, b.Bounds.TransformToAABB(transform));
+            }
+
+            private HeadlessTransformedGeometryStub((IGeometryImpl b, Matrix transform, Rect bounds) fix) : base(fix.bounds)
+            {
+                SourceGeometry = fix.b;
+                Transform = fix.transform;
+            }
+
+
+            public IGeometryImpl SourceGeometry { get; }
+            public Matrix Transform { get; }
+        }
+
+        class HeadlessGlyphRunStub : IGlyphRunImpl
+        {
+            public void Dispose()
+            {
+            }
+        }
+
+        class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl
+        {
+            public HeadlessStreamingGeometryStub() : base(Rect.Empty)
+            {
+            }
+
+            public IStreamGeometryImpl Clone()
+            {
+                return this;
+            }
+
+            public IStreamGeometryContextImpl Open()
+            {
+                return new HeadlessStreamingGeometryContextStub(this);
+            }
+
+            class HeadlessStreamingGeometryContextStub : IStreamGeometryContextImpl
+            {
+                private readonly HeadlessStreamingGeometryStub _parent;
+                private double _x1, _y1, _x2, _y2;
+                public HeadlessStreamingGeometryContextStub(HeadlessStreamingGeometryStub parent)
+                {
+                    _parent = parent;
+                }
+
+                void Track(Point pt)
+                {
+                    if (_x1 > pt.X)
+                        _x1 = pt.X;
+                    if (_x2 < pt.X)
+                        _x2 = pt.X;
+                    if (_y1 > pt.Y)
+                        _y1 = pt.Y;
+                    if (_y2 < pt.Y)
+                        _y2 = pt.Y;
+                }
+
+                public void Dispose()
+                {
+                    _parent.Bounds = new Rect(_x1, _y1, _x2 - _x1, _y2 - _y1);
+                }
+
+                public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
+                    => Track(point);
+
+                public void BeginFigure(Point startPoint, bool isFilled = true) => Track(startPoint);
+
+                public void CubicBezierTo(Point point1, Point point2, Point point3)
+                {
+                    Track(point1);
+                    Track(point2);
+                    Track(point3);
+                }
+
+                public void QuadraticBezierTo(Point control, Point endPoint)
+                {
+                    Track(control);
+                    Track(endPoint);
+                }
+
+                public void LineTo(Point point) => Track(point);
+
+                public void EndFigure(bool isClosed)
+                {
+                    Dispose();
+                }
+
+                public void SetFillRule(FillRule fillRule)
+                {
+
+                }
+            }
+        }
+
+        class HeadlessBitmapStub : IBitmapImpl, IRenderTargetBitmapImpl, IWriteableBitmapImpl
+        {
+            public Size Size { get; }
+
+            public HeadlessBitmapStub(Size size, Vector dpi)
+            {
+                Size = size;
+                Dpi = dpi;
+                var pixel = Size * (Dpi / 96);
+                PixelSize = new PixelSize(Math.Max(1, (int)pixel.Width), Math.Max(1, (int)pixel.Height));
+            }
+
+            public HeadlessBitmapStub(PixelSize size, Vector dpi)
+            {
+                PixelSize = size;
+                Dpi = dpi;
+                Size = PixelSize.ToSizeWithDpi(dpi);
+            }
+
+            public void Dispose()
+            {
+
+            }
+
+            public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
+            {
+                return new HeadlessDrawingContextStub();
+            }
+
+            public Vector Dpi { get; }
+            public PixelSize PixelSize { get; }
+            public int Version { get; set; }
+            public void Save(string fileName)
+            {
+
+            }
+
+            public void Save(Stream stream)
+            {
+
+            }
+
+            public ILockedFramebuffer Lock()
+            {
+                Version++;
+                var mem = Marshal.AllocHGlobal(PixelSize.Width * PixelSize.Height * 4);
+                return new LockedFramebuffer(mem, PixelSize, PixelSize.Width * 4, Dpi, PixelFormat.Rgba8888,
+                    () => Marshal.FreeHGlobal(mem));
+            }
+        }
+
+        class HeadlessDrawingContextStub : IDrawingContextImpl
+        {
+            public void Dispose()
+            {
+
+            }
+
+            public Matrix Transform { get; set; }
+            public void Clear(Color color)
+            {
+
+            }
+
+            public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
+            {
+
+            }
+
+            public IRenderTargetBitmapImpl CreateLayer(Size size)
+            {
+                return new HeadlessBitmapStub(size, new Vector(96, 96));
+            }
+
+            public void PushClip(Rect clip)
+            {
+
+            }
+
+            public void PopClip()
+            {
+
+            }
+
+            public void PushOpacity(double opacity)
+            {
+
+            }
+
+            public void PopOpacity()
+            {
+
+            }
+
+            public void PushOpacityMask(IBrush mask, Rect bounds)
+            {
+
+            }
+
+            public void PopOpacityMask()
+            {
+
+            }
+
+            public void PushGeometryClip(IGeometryImpl clip)
+            {
+
+            }
+
+            public void PopGeometryClip()
+            {
+
+            }
+
+            public void Custom(ICustomDrawOperation custom)
+            {
+
+            }
+
+            public void DrawLine(IPen pen, Point p1, Point p2)
+            {
+                throw new NotImplementedException();
+            }
+
+            public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
+            {
+            }
+
+            public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0)
+            {
+            }
+
+            public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
+            {
+                
+            }
+
+            public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
+            {
+                
+            }
+
+            public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadow = default)
+            {
+                
+            }
+
+            public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
+            {
+                
+            }
+
+            public void PushClip(RoundedRect clip)
+            {
+                
+            }
+        }
+
+        class HeadlessRenderTarget : IRenderTarget
+        {
+            public void Dispose()
+            {
+
+            }
+
+            public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
+            {
+                return new HeadlessDrawingContextStub();
+            }
+        }
+
+        class HeadlessFormattedTextStub : IFormattedTextImpl
+        {
+            public HeadlessFormattedTextStub(string text, Size constraint)
+            {
+                Text = text;
+                Constraint = constraint;
+                Bounds = new Rect(Constraint.Constrain(new Size(50, 50)));
+            }
+
+            public Size Constraint { get; }
+            public Rect Bounds { get; }
+            public string Text { get; }
+
+
+            public IEnumerable<FormattedTextLine> GetLines()
+            {
+                return new[] { new FormattedTextLine(Text.Length, 10) };
+            }
+
+            public TextHitTestResult HitTestPoint(Point point) => new TextHitTestResult();
+
+            public Rect HitTestTextPosition(int index) => new Rect();
+
+            public IEnumerable<Rect> HitTestTextRange(int index, int length) => new Rect[length];
+        }
+    }
+}

+ 201 - 0
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@@ -0,0 +1,201 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Controls.Platform;
+using Avalonia.Input;
+using Avalonia.Input.Platform;
+using Avalonia.Media;
+using Avalonia.Media.Fonts;
+using Avalonia.Media.TextFormatting;
+using Avalonia.Platform;
+using Avalonia.Utilities;
+
+namespace Avalonia.Headless
+{
+    class HeadlessClipboardStub : IClipboard
+    {
+        private string _text;
+        private IDataObject _data;
+
+        public Task<string> GetTextAsync()
+        {
+            return Task.Run(() => _text);
+        }
+
+        public Task SetTextAsync(string text)
+        {
+            return Task.Run(() => _text = text);
+        }
+
+        public Task ClearAsync()
+        {
+            return Task.Run(() => _text = null);
+        }
+
+        public Task SetDataObjectAsync(IDataObject data)
+        {
+            return Task.Run(() => _data = data);
+        }
+
+        public Task<string[]> GetFormatsAsync()
+        {
+            throw new NotImplementedException();
+        }
+
+        public async Task<object> GetDataAsync(string format)
+        {
+            return await Task.Run(() => _data);
+        }
+    }
+
+    class HeadlessCursorFactoryStub : IStandardCursorFactory
+    {
+        
+        public IPlatformHandle GetCursor(StandardCursorType cursorType)
+        {
+            return new PlatformHandle(new IntPtr((int)cursorType), "STUB");
+        }
+    }
+
+    class HeadlessPlatformSettingsStub : IPlatformSettings
+    {
+        public Size DoubleClickSize { get; } = new Size(2, 2);
+        public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500);
+    }
+
+    class HeadlessSystemDialogsStub : ISystemDialogImpl
+    {
+        public Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent)
+        {
+            return Task.Run(() => (string[])null);
+        }
+
+        public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
+        {
+            return Task.Run(() => (string)null);
+        }
+    }
+
+    class HeadlessGlyphTypefaceImpl : IGlyphTypefaceImpl
+    {
+        public short DesignEmHeight => 10;
+
+        public int Ascent => 5;
+
+        public int Descent => 5;
+
+        public int LineGap => 2;
+
+        public int UnderlinePosition => 5;
+
+        public int UnderlineThickness => 5;
+
+        public int StrikethroughPosition => 5;
+
+        public int StrikethroughThickness => 2;
+
+        public bool IsFixedPitch => true;
+
+        public void Dispose()
+        {            
+        }
+
+        public ushort GetGlyph(uint codepoint)
+        {
+            return 1;
+        }
+
+        public int GetGlyphAdvance(ushort glyph)
+        {
+            return 1;
+        }
+
+        public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs)
+        {
+            return glyphs.ToArray().Select(x => (int)x).ToArray();
+        }
+
+        public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints)
+        {
+            return codepoints.ToArray().Select(x => (ushort)x).ToArray();
+        }
+    }
+
+    class HeadlessTextShaperStub : ITextShaperImpl
+    {
+        public GlyphRun ShapeText(ReadOnlySlice<char> text, Typeface typeface, double fontRenderingEmSize, CultureInfo culture)
+        {
+            return new GlyphRun(new GlyphTypeface(typeface), 10,
+                new ReadOnlySlice<ushort>(new ushort[] { 1, 2, 3 }),
+                new ReadOnlySlice<double>(new double[] { 1, 2, 3 }),
+                new ReadOnlySlice<Vector>(new Vector[] { new Vector(1, 1), new Vector(2, 2), new Vector(3, 3) }),
+                text,
+                new ReadOnlySlice<ushort>(new ushort[] { 1, 2, 3 }));
+        }
+    }
+
+    class HeadlessFontManagerStub : IFontManagerImpl
+    {
+        public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
+        {
+            return new HeadlessGlyphTypefaceImpl();
+        }
+
+        public string GetDefaultFontFamilyName()
+        {
+            return "Arial";
+        }
+
+        public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
+        {
+            return new List<string> { "Arial" };
+        }
+
+        public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey)
+        {
+            fontKey = new FontKey("Arial", fontWeight, fontStyle);
+            return true;
+        }
+    }
+
+    class HeadlessIconLoaderStub : IPlatformIconLoader
+    {
+
+        class IconStub : IWindowIconImpl
+        {
+            public void Save(Stream outputStream)
+            {
+                
+            }
+        }
+        public IWindowIconImpl LoadIcon(string fileName)
+        {
+            return new IconStub();
+        }
+
+        public IWindowIconImpl LoadIcon(Stream stream)
+        {
+            return new IconStub();
+        }
+
+        public IWindowIconImpl LoadIcon(IBitmapImpl bitmap)
+        {
+            return new IconStub();
+        }
+    }
+
+    class HeadlessScreensStub : IScreenImpl
+    {
+        public int ScreenCount { get; } = 1;
+
+        public IReadOnlyList<Screen> AllScreens { get; } = new[]
+        {
+            new Screen(1, new PixelRect(0, 0, 1920, 1280),
+                new PixelRect(0, 0, 1920, 1280), true),
+        };
+    }
+}

+ 86 - 0
src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs

@@ -0,0 +1,86 @@
+using System;
+using System.Reactive.Disposables;
+using System.Threading;
+using Avalonia.Platform;
+using Avalonia.Threading;
+
+namespace Avalonia.Headless
+{
+    class HeadlessPlatformThreadingInterface : IPlatformThreadingInterface
+    {
+        public HeadlessPlatformThreadingInterface()
+        {
+            _thread = Thread.CurrentThread;
+        }
+        
+        private AutoResetEvent _event = new AutoResetEvent(false);
+        private Thread _thread;
+        private object _lock = new object();
+        private DispatcherPriority? _signaledPriority;
+
+        public void RunLoop(CancellationToken cancellationToken)
+        {
+            while (!cancellationToken.IsCancellationRequested)
+            {
+                DispatcherPriority? signaled = null;
+                lock (_lock)
+                {
+                    signaled = _signaledPriority;
+                    _signaledPriority = null;
+                }
+                if(signaled.HasValue)
+                    Signaled?.Invoke(signaled);
+                WaitHandle.WaitAny(new[] {cancellationToken.WaitHandle, _event}, TimeSpan.FromMilliseconds(20));
+            }
+        }
+
+        public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
+        {
+            var cancelled = false;
+            var enqueued = false;
+            var l = new object();
+            var timer = new Timer(_ =>
+            {
+                lock (l)
+                {
+                    if (cancelled || enqueued)
+                        return;
+                    enqueued = true;
+                    Dispatcher.UIThread.Post(() =>
+                    {
+                        lock (l)
+                        {
+                            enqueued = false;
+                            if (cancelled)
+                                return;
+                            tick();
+                        }
+                    }, priority);
+                }
+            }, null, interval, interval);
+            return Disposable.Create(() =>
+            {
+                lock (l)
+                {
+                    timer.Dispose();
+                    cancelled = true;
+                }
+            });
+        }
+
+        public void Signal(DispatcherPriority priority)
+        {
+            lock (_lock)
+            {
+                if (_signaledPriority == null || _signaledPriority.Value > priority)
+                {
+                    _signaledPriority = priority;
+                }
+                _event.Set();
+            }
+        }
+
+        public bool CurrentThreadIsLoopThread => _thread == Thread.CurrentThread;
+        public event Action<DispatcherPriority?> Signaled;
+    }
+}

+ 340 - 0
src/Avalonia.Headless/HeadlessWindowImpl.cs

@@ -0,0 +1,340 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using Avalonia.Controls;
+using Avalonia.Controls.Platform.Surfaces;
+using Avalonia.Controls.Primitives.PopupPositioning;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+using Avalonia.Rendering;
+using Avalonia.Threading;
+using Avalonia.Utilities;
+
+namespace Avalonia.Headless
+{
+    class HeadlessWindowImpl : IWindowImpl, IPopupImpl, IFramebufferPlatformSurface, IHeadlessWindow
+    {
+        private IKeyboardDevice _keyboard;
+        private Stopwatch _st = Stopwatch.StartNew();
+        private Pointer _mousePointer;
+        private WriteableBitmap _lastRenderedFrame;
+        private object _sync = new object();
+        public bool IsPopup { get; }
+
+        public HeadlessWindowImpl(bool isPopup)
+        {
+            IsPopup = isPopup;
+            Surfaces = new object[] { this };
+            _keyboard = AvaloniaLocator.Current.GetService<IKeyboardDevice>();
+            _mousePointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
+            MouseDevice = new MouseDevice(_mousePointer);
+            ClientSize = new Size(1024, 768);
+        }
+
+        public void Dispose()
+        {
+            Closed?.Invoke();
+            _lastRenderedFrame?.Dispose();
+            _lastRenderedFrame = null;
+        }
+
+        public Size ClientSize { get; set; }
+        public double Scaling { get; } = 1;
+        public IEnumerable<object> Surfaces { get; }
+        public Action<RawInputEventArgs> Input { get; set; }
+        public Action<Rect> Paint { get; set; }
+        public Action<Size> Resized { get; set; }
+        public Action<double> ScalingChanged { get; set; }
+
+        public IRenderer CreateRenderer(IRenderRoot root)
+            => new DeferredRenderer(root, AvaloniaLocator.Current.GetService<IRenderLoop>());
+
+        public void Invalidate(Rect rect)
+        {
+        }
+
+        public void SetInputRoot(IInputRoot inputRoot)
+        {
+            InputRoot = inputRoot;
+        }
+
+        public IInputRoot InputRoot { get; set; }
+
+        public Point PointToClient(PixelPoint point) => point.ToPoint(Scaling);
+
+        public PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, Scaling);
+
+        public void SetCursor(IPlatformHandle cursor)
+        {
+
+        }
+
+        public Action Closed { get; set; }
+        public IMouseDevice MouseDevice { get; }
+
+        public void Show()
+        {
+            Dispatcher.UIThread.Post(() => Activated?.Invoke(), DispatcherPriority.Input);
+        }
+
+        public void Hide()
+        {
+            Dispatcher.UIThread.Post(() => Deactivated?.Invoke(), DispatcherPriority.Input);
+        }
+
+        public void BeginMoveDrag()
+        {
+
+        }
+
+        public void BeginResizeDrag(WindowEdge edge)
+        {
+
+        }
+
+        public PixelPoint Position { get; set; }
+        public Action<PixelPoint> PositionChanged { get; set; }
+        public void Activate()
+        {
+            Dispatcher.UIThread.Post(() => Activated?.Invoke(), DispatcherPriority.Input);
+        }
+
+        public Action Deactivated { get; set; }
+        public Action Activated { get; set; }
+        public IPlatformHandle Handle { get; } = new PlatformHandle(IntPtr.Zero, "STUB");
+        public Size MaxClientSize { get; } = new Size(1920, 1280);
+        public void Resize(Size clientSize)
+        {
+            // Emulate X11 behavior here
+            if (IsPopup)
+                DoResize(clientSize);
+            else
+                Dispatcher.UIThread.Post(() =>
+                {
+                    DoResize(clientSize);
+                });
+        }
+
+        void DoResize(Size clientSize)
+        {
+            // Uncomment this check and experience a weird bug in layout engine
+            if (ClientSize != clientSize)
+            {
+                ClientSize = clientSize;
+                Resized?.Invoke(clientSize);
+            }
+        }
+
+        public void SetMinMaxSize(Size minSize, Size maxSize)
+        {
+
+        }
+
+        public void SetTopmost(bool value)
+        {
+
+        }
+
+        public IScreenImpl Screen { get; } = new HeadlessScreensStub();
+        public WindowState WindowState { get; set; }
+        public Action<WindowState> WindowStateChanged { get; set; }
+        public void SetTitle(string title)
+        {
+
+        }
+
+        public void ShowDialog(IWindowImpl parent)
+        {
+            Show();
+        }
+
+        public void SetSystemDecorations(bool enabled)
+        {
+
+        }
+
+        public void SetIcon(IWindowIconImpl icon)
+        {
+
+        }
+
+        public void ShowTaskbarIcon(bool value)
+        {
+
+        }
+
+        public void CanResize(bool value)
+        {
+
+        }
+
+        public Func<bool> Closing { get; set; }
+
+        class FramebufferProxy : ILockedFramebuffer
+        {
+            private readonly ILockedFramebuffer _fb;
+            private readonly Action _onDispose;
+            private bool _disposed;
+
+            public FramebufferProxy(ILockedFramebuffer fb, Action onDispose)
+            {
+                _fb = fb;
+                _onDispose = onDispose;
+            }
+            public void Dispose()
+            {
+                if (_disposed)
+                    return;
+                _disposed = true;
+                _fb.Dispose();
+                _onDispose();
+            }
+
+            public IntPtr Address => _fb.Address;
+            public PixelSize Size => _fb.Size;
+            public int RowBytes => _fb.RowBytes;
+            public Vector Dpi => _fb.Dpi;
+            public PixelFormat Format => _fb.Format;
+        }
+
+        public ILockedFramebuffer Lock()
+        {
+            var bmp = new WriteableBitmap(PixelSize.FromSize(ClientSize, Scaling), new Vector(96, 96) * Scaling);
+            var fb = bmp.Lock();
+            return new FramebufferProxy(fb, () =>
+            {
+                lock (_sync)
+                {
+                    _lastRenderedFrame?.Dispose();
+                    _lastRenderedFrame = bmp;
+                }
+            });
+        }
+
+        public IRef<IWriteableBitmapImpl> GetLastRenderedFrame()
+        {
+            lock (_sync)
+                return _lastRenderedFrame?.PlatformImpl?.CloneAs<IWriteableBitmapImpl>();
+        }
+
+        private ulong Timestamp => (ulong)_st.ElapsedMilliseconds;
+
+        // TODO: Hook recent Popup changes. 
+        IPopupPositioner IPopupImpl.PopupPositioner => null;
+
+        public Size MaxAutoSizeHint => new Size(1920, 1080);
+
+        public Action<WindowTransparencyLevel> TransparencyLevelChanged { get; set; }
+
+        public WindowTransparencyLevel TransparencyLevel => WindowTransparencyLevel.None;
+
+        public Action GotInputWhenDisabled { get; set; }
+
+        public bool IsClientAreaExtendedToDecorations => false;
+
+        public Action<bool> ExtendClientAreaToDecorationsChanged { get; set; }
+
+        public bool NeedsManagedDecorations => false;
+
+        public Thickness ExtendedMargins => new Thickness();
+
+        public Thickness OffScreenMargin => new Thickness();
+
+        public Action LostFocus { get; set; }
+
+        void IHeadlessWindow.KeyPress(Key key, RawInputModifiers modifiers)
+        {
+            Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot, RawKeyEventType.KeyDown, key, modifiers));
+        }
+
+        void IHeadlessWindow.KeyRelease(Key key, RawInputModifiers modifiers)
+        {
+            Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot, RawKeyEventType.KeyUp, key, modifiers));
+        }
+
+        void IHeadlessWindow.MouseDown(Point point, int button, RawInputModifiers modifiers)
+        {
+            Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot,
+                button == 0 ? RawPointerEventType.LeftButtonDown :
+                button == 1 ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.RightButtonDown,
+                point, modifiers));
+        }
+
+        void IHeadlessWindow.MouseMove(Point point, RawInputModifiers modifiers)
+        {
+            Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot,
+                RawPointerEventType.Move, point, modifiers));
+        }
+
+        void IHeadlessWindow.MouseUp(Point point, int button, RawInputModifiers modifiers)
+        {
+            Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot,
+                button == 0 ? RawPointerEventType.LeftButtonUp :
+                button == 1 ? RawPointerEventType.MiddleButtonUp : RawPointerEventType.RightButtonUp,
+                point, modifiers));
+        }
+
+        void IWindowImpl.Move(PixelPoint point)
+        {
+
+        }
+
+        public IPopupImpl CreatePopup()
+        {
+            // TODO: Hook recent Popup changes. 
+            return null;
+        }
+
+        public void SetWindowManagerAddShadowHint(bool enabled)
+        {
+            
+        }
+
+        public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel)
+        {
+            
+        }
+
+        public void SetParent(IWindowImpl parent)
+        {
+            
+        }
+
+        public void SetEnabled(bool enable)
+        {
+            
+        }
+
+        public void SetSystemDecorations(SystemDecorations enabled)
+        {
+            
+        }
+
+        public void BeginMoveDrag(PointerPressedEventArgs e)
+        {
+            
+        }
+
+        public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e)
+        {
+            
+        }
+
+        public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint)
+        {
+            
+        }
+
+        public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints)
+        {
+            
+        }
+
+        public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight)
+        {
+            
+        }
+    }
+}

+ 17 - 0
src/Avalonia.Headless/IHeadlessWindow.cs

@@ -0,0 +1,17 @@
+using Avalonia.Input;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+using Avalonia.Utilities;
+
+namespace Avalonia.Headless
+{
+    public interface IHeadlessWindow
+    {
+        IRef<IWriteableBitmapImpl> GetLastRenderedFrame();
+        void KeyPress(Key key, RawInputModifiers modifiers);
+        void KeyRelease(Key key, RawInputModifiers modifiers);
+        void MouseDown(Point point, int button, RawInputModifiers modifiers = RawInputModifiers.None);
+        void MouseMove(Point point, RawInputModifiers modifiers = RawInputModifiers.None);
+        void MouseUp(Point point, int button, RawInputModifiers modifiers = RawInputModifiers.None);
+    }
+}