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