13 changed files with 1151 additions and 1 deletions
@ -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> |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netstandard2.0</TargetFramework> |
||||
|
</PropertyGroup> |
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" /> |
||||
|
</ItemGroup> |
||||
|
</Project> |
||||
@ -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"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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), |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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…
Reference in new issue