diff --git a/Avalonia.sln b/Avalonia.sln
index ac678ba9ba..16fc4d0121 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -199,6 +199,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.UnitTes
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Controls.DataGrid", "src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj", "{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}"
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
GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13
@@ -1842,6 +1846,54 @@ Global
{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.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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
index 6a40f7187d..d9ce776ffd 100644
--- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
+++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
@@ -7,11 +7,12 @@
+
-
+
diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs
index 40321496c0..91f05ca719 100644
--- a/samples/ControlCatalog.NetCore/Program.cs
+++ b/samples/ControlCatalog.NetCore/Program.cs
@@ -6,6 +6,7 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Skia;
using Avalonia.ReactiveUI;
+using Avalonia.Threading;
namespace ControlCatalog.NetCore
{
@@ -32,6 +33,10 @@ namespace ControlCatalog.NetCore
System.Threading.ThreadPool.QueueUserWorkItem(_ => ConsoleSilencer());
return builder.StartLinuxFramebuffer(args);
}
+ else if (args.Contains("--vnc"))
+ {
+ return builder.StartWithHeadlessVncPlatform(null, 5901, args, ShutdownMode.OnMainWindowClose);
+ }
else
return builder.StartWithClassicDesktopLifetime(args);
}
diff --git a/src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj b/src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj
new file mode 100644
index 0000000000..4aac862ae8
--- /dev/null
+++ b/src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj
@@ -0,0 +1,12 @@
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs b/src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs
new file mode 100644
index 0000000000..df68807195
--- /dev/null
+++ b/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 = (InputModifiers)(((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;
+ }
+ }
+}
diff --git a/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs b/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs
new file mode 100644
index 0000000000..bd215296c8
--- /dev/null
+++ b/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(
+ this T builder,
+ string host, int port,
+ string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose)
+ where T : AppBuilderBase, 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);
+ }
+ }
+}
diff --git a/src/Avalonia.Headless/Avalonia.Headless.csproj b/src/Avalonia.Headless/Avalonia.Headless.csproj
new file mode 100644
index 0000000000..b871316a2c
--- /dev/null
+++ b/src/Avalonia.Headless/Avalonia.Headless.csproj
@@ -0,0 +1,8 @@
+
+
+ netstandard2.0
+
+
+
+
+
diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
new file mode 100644
index 0000000000..5857d94182
--- /dev/null
+++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
@@ -0,0 +1,93 @@
+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 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 IEmbeddableWindowImpl CreateEmbeddableWindow() => throw new PlatformNotSupportedException();
+
+ public IPopupImpl CreatePopup() => new HeadlessWindowImpl(true);
+ }
+
+ internal static void Initialize()
+ {
+ AvaloniaLocator.CurrentMutable
+ .Bind().ToConstant(new HeadlessPlatformThreadingInterface())
+ .Bind().ToSingleton()
+ .Bind().ToSingleton()
+ .Bind().ToConstant(new HeadlessPlatformSettingsStub())
+ .Bind().ToSingleton()
+ .Bind().ToSingleton()
+ .Bind().ToConstant(new KeyboardDevice())
+ .Bind().ToConstant(new RenderLoop())
+ .Bind().ToConstant(new RenderTimer(60))
+
+ .Bind().ToConstant(new HeadlessWindowingPlatform())
+ .Bind().ToSingleton();
+ }
+
+
+ public static void ForceRenderTimerTick(int count = 1)
+ {
+ var timer = AvaloniaLocator.Current.GetService() as RenderTimer;
+ for (var c = 0; c < count; c++)
+ timer?.ForceTick();
+
+ }
+ }
+
+ public static class AvaloniaHeadlessPlatformExtensions
+ {
+ public static T UseHeadless(this T builder, bool headlessDrawing = true)
+ where T : AppBuilderBase, new()
+ {
+ if (headlessDrawing)
+ builder.UseRenderingSubsystem(HeadlessPlatformRenderInterface.Initialize, "Headless");
+ return builder.UseWindowingSubsystem(AvaloniaHeadlessPlatform.Initialize, "Headless");
+ }
+ }
+}
diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
new file mode 100644
index 0000000000..381eccef08
--- /dev/null
+++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
@@ -0,0 +1,385 @@
+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().ToConstant(new HeadlessPlatformRenderInterface());
+ }
+
+ public IEnumerable InstalledFontNames { get; } = new[] {"Tahoma"};
+
+ public IFormattedTextImpl CreateFormattedText(string text, Typeface typeface, TextAlignment textAlignment,
+ TextWrapping wrapping, Size constraint, IReadOnlyList 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