Browse Source

Headless platform WIP

pull/2847/head
Nikita Tsukanov 7 years ago
parent
commit
bb2c563416
  1. 52
      Avalonia.sln
  2. 3
      samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
  3. 5
      samples/ControlCatalog.NetCore/Program.cs
  4. 12
      src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj
  5. 96
      src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs
  6. 48
      src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs
  7. 8
      src/Avalonia.Headless/Avalonia.Headless.csproj
  8. 93
      src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  9. 385
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  10. 96
      src/Avalonia.Headless/HeadlessPlatformStubs.cs
  11. 86
      src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs
  12. 251
      src/Avalonia.Headless/HeadlessWindowImpl.cs
  13. 17
      src/Avalonia.Headless/IHeadlessWindow.cs

52
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

3
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

@ -7,11 +7,12 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj" />
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Desktop\Avalonia.Desktop.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>

5
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);
}

12
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
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;
}
}
}

48
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
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>

93
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<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 IEmbeddableWindowImpl 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<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");
}
}
}

385
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<IPlatformRenderInterface>().ToConstant(new HeadlessPlatformRenderInterface());
}
public IEnumerable<string> InstalledFontNames { get; } = new[] {"Tahoma"};
public IFormattedTextImpl CreateFormattedText(string text, Typeface typeface, 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));
}
class HeadlessGeometryStub : IGeometryImpl
{
public HeadlessGeometryStub(Rect bounds)
{
Bounds = bounds;
}
public Rect Bounds { get; set; }
public virtual Rect GetRenderBounds(Pen pen) => Bounds.Inflate(pen.Thickness / 2);
public virtual bool FillContains(Point point) => Bounds.Contains(point);
public IGeometryImpl Intersect(IGeometryImpl geometry)
=> new HeadlessGeometryStub(geometry.Bounds.Intersect(Bounds));
public bool StrokeContains(Pen pen, Point point) => false;
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 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 DrawImage(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect,
BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
{
}
public void DrawImage(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
{
}
public void DrawLine(Pen pen, Point p1, Point p2)
{
}
public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry)
{
}
public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0)
{
}
public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
{
}
public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0)
{
}
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)
{
}
}
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];
}
}
}

96
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Platform;
namespace Avalonia.Headless
{
class HeadlessClipboardStub : IClipboard
{
private string _text;
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);
}
}
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, IWindowImpl parent)
{
return Task.Run(() => (string[])null);
}
public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent)
{
return Task.Run(() => (string)null);
}
}
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(new PixelRect(0, 0, 1920, 1280),
new PixelRect(0, 0, 1920, 1280), true),
};
}
}

86
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;
}
}

251
src/Avalonia.Headless/HeadlessWindowImpl.cs

@ -0,0 +1,251 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Avalonia.Controls;
using Avalonia.Controls.Platform.Surfaces;
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)
{
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;
void IHeadlessWindow.KeyPress(Key key, InputModifiers modifiers)
{
Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, RawKeyEventType.KeyDown, key, modifiers));
}
void IHeadlessWindow.KeyRelease(Key key, InputModifiers modifiers)
{
Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, RawKeyEventType.KeyUp, key, modifiers));
}
void IHeadlessWindow.MouseDown(Point point, int button, InputModifiers 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, InputModifiers modifiers)
{
Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot,
RawPointerEventType.Move, point, modifiers));
}
void IHeadlessWindow.MouseUp(Point point, int button, InputModifiers modifiers)
{
Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot,
button == 0 ? RawPointerEventType.LeftButtonUp :
button == 1 ? RawPointerEventType.MiddleButtonUp : RawPointerEventType.RightButtonUp,
point, modifiers));
}
}
}

17
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, InputModifiers modifiers);
void KeyRelease(Key key, InputModifiers modifiers);
void MouseDown(Point point, int button, InputModifiers modifiers = InputModifiers.None);
void MouseMove(Point point, InputModifiers modifiers = InputModifiers.None);
void MouseUp(Point point, int button, InputModifiers modifiers = InputModifiers.None);
}
}
Loading…
Cancel
Save