@ -1,6 +1,6 @@ |
|||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|||
<ItemGroup> |
|||
<PackageReference Include="SkiaSharp" Version="1.57.1" /> |
|||
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="Avalonia.Skia.Linux.Natives" Version="1.57.1.3" /> |
|||
<PackageReference Include="SkiaSharp" Version="1.60.0" /> |
|||
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="Avalonia.Skia.Linux.Natives" Version="1.60.0.1" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
|
|||
@ -1,142 +0,0 @@ |
|||
using System; |
|||
using System.IO; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Skia |
|||
{ |
|||
class BitmapImpl : IRenderTargetBitmapImpl, IWriteableBitmapImpl |
|||
{ |
|||
private Vector _dpi; |
|||
|
|||
public SKBitmap Bitmap { get; private set; } |
|||
|
|||
public BitmapImpl(SKBitmap bm) |
|||
{ |
|||
Bitmap = bm; |
|||
PixelHeight = bm.Height; |
|||
PixelWidth = bm.Width; |
|||
_dpi = new Vector(96, 96); |
|||
} |
|||
|
|||
static void ReleaseProc(IntPtr address, object ctx) |
|||
{ |
|||
((IUnmanagedBlob) ctx).Dispose(); |
|||
} |
|||
|
|||
private static readonly SKBitmapReleaseDelegate ReleaseDelegate = ReleaseProc; |
|||
|
|||
public BitmapImpl(int width, int height, Vector dpi, PixelFormat? fmt = null) |
|||
{ |
|||
PixelHeight = height; |
|||
PixelWidth = width; |
|||
_dpi = dpi; |
|||
var colorType = fmt?.ToSkColorType() ?? SKImageInfo.PlatformColorType; |
|||
var runtimePlatform = AvaloniaLocator.Current?.GetService<IRuntimePlatform>(); |
|||
var runtime = runtimePlatform?.GetRuntimeInfo(); |
|||
if (runtime?.IsDesktop == true && runtime?.OperatingSystem == OperatingSystemType.Linux) |
|||
colorType = SKColorType.Bgra8888; |
|||
|
|||
if (runtimePlatform != null) |
|||
{ |
|||
Bitmap = new SKBitmap(); |
|||
var nfo = new SKImageInfo(width, height, colorType, SKAlphaType.Premul); |
|||
var plat = AvaloniaLocator.Current.GetService<IRuntimePlatform>(); |
|||
var blob = plat.AllocBlob(nfo.BytesSize); |
|||
Bitmap.InstallPixels(nfo, blob.Address, nfo.RowBytes, null, ReleaseDelegate, blob); |
|||
|
|||
} |
|||
else |
|||
Bitmap = new SKBitmap(width, height, colorType, SKAlphaType.Premul); |
|||
Bitmap.Erase(SKColor.Empty); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
Bitmap.Dispose(); |
|||
} |
|||
|
|||
public int PixelWidth { get; private set; } |
|||
public int PixelHeight { get; private set; } |
|||
|
|||
class BitmapDrawingContext : DrawingContextImpl |
|||
{ |
|||
private readonly SKSurface _surface; |
|||
|
|||
public BitmapDrawingContext(SKBitmap bitmap, Vector dpi, IVisualBrushRenderer visualBrushRenderer) |
|||
: this(CreateSurface(bitmap), dpi, visualBrushRenderer) |
|||
{ |
|||
CanUseLcdRendering = false; |
|||
} |
|||
|
|||
private static SKSurface CreateSurface(SKBitmap bitmap) |
|||
{ |
|||
IntPtr length; |
|||
var rv = SKSurface.Create(bitmap.Info, bitmap.GetPixels(out length), bitmap.RowBytes); |
|||
if (rv == null) |
|||
throw new Exception("Unable to create Skia surface"); |
|||
return rv; |
|||
} |
|||
|
|||
public BitmapDrawingContext(SKSurface surface, Vector dpi, IVisualBrushRenderer visualBrushRenderer) |
|||
: base(surface.Canvas, dpi, visualBrushRenderer) |
|||
{ |
|||
_surface = surface; |
|||
} |
|||
|
|||
public override void Dispose() |
|||
{ |
|||
base.Dispose(); |
|||
_surface.Dispose(); |
|||
} |
|||
} |
|||
|
|||
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) |
|||
{ |
|||
return new BitmapDrawingContext(Bitmap, _dpi, visualBrushRenderer); |
|||
} |
|||
|
|||
public void Save(Stream stream) |
|||
{ |
|||
IntPtr length; |
|||
using (var image = SKImage.FromPixels(Bitmap.Info, Bitmap.GetPixels(out length), Bitmap.RowBytes)) |
|||
using (var data = image.Encode()) |
|||
{ |
|||
data.SaveTo(stream); |
|||
} |
|||
} |
|||
|
|||
public void Save(string fileName) |
|||
{ |
|||
using (var stream = File.Create(fileName)) |
|||
Save(stream); |
|||
} |
|||
|
|||
class BitmapFramebuffer : ILockedFramebuffer |
|||
{ |
|||
private SKBitmap _bmp; |
|||
|
|||
public BitmapFramebuffer(SKBitmap bmp) |
|||
{ |
|||
_bmp = bmp; |
|||
_bmp.LockPixels(); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_bmp.UnlockPixels(); |
|||
_bmp = null; |
|||
} |
|||
|
|||
public IntPtr Address => _bmp.GetPixels(); |
|||
public int Width => _bmp.Width; |
|||
public int Height => _bmp.Height; |
|||
public int RowBytes => _bmp.RowBytes; |
|||
public Vector Dpi { get; } = new Vector(96, 96); |
|||
public PixelFormat Format => _bmp.ColorType.ToPixelFormat(); |
|||
} |
|||
|
|||
public ILockedFramebuffer Lock() => new BitmapFramebuffer(Bitmap); |
|||
} |
|||
} |
|||
@ -1,82 +1,199 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
using System.Reactive.Disposables; |
|||
using Avalonia.Controls.Platform.Surfaces; |
|||
using Avalonia.Media; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering; |
|||
using Avalonia.Skia.Helpers; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Skia |
|||
{ |
|||
/// <summary>
|
|||
/// Skia render target that renders to a framebuffer surface. No gpu acceleration available.
|
|||
/// </summary>
|
|||
public class FramebufferRenderTarget : IRenderTarget |
|||
{ |
|||
private readonly IFramebufferPlatformSurface _surface; |
|||
private readonly IFramebufferPlatformSurface _platformSurface; |
|||
private SKImageInfo _currentImageInfo; |
|||
private IntPtr _currentFramebufferAddress; |
|||
private SKSurface _framebufferSurface; |
|||
private PixelFormatConversionShim _conversionShim; |
|||
private IDisposable _preFramebufferCopyHandler; |
|||
|
|||
public FramebufferRenderTarget(IFramebufferPlatformSurface surface) |
|||
/// <summary>
|
|||
/// Create new framebuffer render target using a target surface.
|
|||
/// </summary>
|
|||
/// <param name="platformSurface">Target surface.</param>
|
|||
public FramebufferRenderTarget(IFramebufferPlatformSurface platformSurface) |
|||
{ |
|||
_surface = surface; |
|||
_platformSurface = platformSurface; |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public void Dispose() |
|||
{ |
|||
//Nothing to do here, since we don't own framebuffer
|
|||
FreeSurface(); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) |
|||
{ |
|||
var framebuffer = _platformSurface.Lock(); |
|||
var framebufferImageInfo = new SKImageInfo(framebuffer.Width, framebuffer.Height, |
|||
framebuffer.Format.ToSkColorType(), SKAlphaType.Premul); |
|||
|
|||
CreateSurface(framebufferImageInfo, framebuffer); |
|||
|
|||
var canvas = _framebufferSurface.Canvas; |
|||
|
|||
canvas.RestoreToCount(-1); |
|||
canvas.Save(); |
|||
canvas.ResetMatrix(); |
|||
|
|||
var createInfo = new DrawingContextImpl.CreateInfo |
|||
{ |
|||
Canvas = canvas, |
|||
Dpi = framebuffer.Dpi, |
|||
VisualBrushRenderer = visualBrushRenderer, |
|||
DisableTextLcdRendering = true |
|||
}; |
|||
|
|||
return new DrawingContextImpl(createInfo, _preFramebufferCopyHandler, framebuffer); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Check if two images info are compatible.
|
|||
/// </summary>
|
|||
/// <param name="currentImageInfo">Current.</param>
|
|||
/// <param name="desiredImageInfo">Desired.</param>
|
|||
/// <returns>True, if images are compatible.</returns>
|
|||
private static bool AreImageInfosCompatible(SKImageInfo currentImageInfo, SKImageInfo desiredImageInfo) |
|||
{ |
|||
return currentImageInfo.Width == desiredImageInfo.Width && |
|||
currentImageInfo.Height == desiredImageInfo.Height && |
|||
currentImageInfo.ColorType == desiredImageInfo.ColorType; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Create Skia surface backed by given framebuffer.
|
|||
/// </summary>
|
|||
/// <param name="desiredImageInfo">Desired image info.</param>
|
|||
/// <param name="framebuffer">Backing framebuffer.</param>
|
|||
private void CreateSurface(SKImageInfo desiredImageInfo, ILockedFramebuffer framebuffer) |
|||
{ |
|||
if (_framebufferSurface != null && AreImageInfosCompatible(_currentImageInfo, desiredImageInfo) && _currentFramebufferAddress == framebuffer.Address) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
FreeSurface(); |
|||
|
|||
_currentFramebufferAddress = framebuffer.Address; |
|||
|
|||
var surface = SKSurface.Create(desiredImageInfo, _currentFramebufferAddress, framebuffer.RowBytes); |
|||
|
|||
// If surface cannot be created - try to create a compatibilty shim first
|
|||
if (surface == null) |
|||
{ |
|||
_conversionShim = new PixelFormatConversionShim(desiredImageInfo, framebuffer.Address); |
|||
_preFramebufferCopyHandler = _conversionShim.SurfaceCopyHandler; |
|||
|
|||
surface = _conversionShim.Surface; |
|||
} |
|||
|
|||
_framebufferSurface = surface ?? throw new Exception("Unable to create a surface for pixel format " + |
|||
framebuffer.Format + |
|||
" or pixel format translator"); |
|||
_currentImageInfo = desiredImageInfo; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Free Skia surface.
|
|||
/// </summary>
|
|||
private void FreeSurface() |
|||
{ |
|||
_conversionShim?.Dispose(); |
|||
_conversionShim = null; |
|||
_preFramebufferCopyHandler = null; |
|||
|
|||
if (_conversionShim != null) |
|||
{ |
|||
_framebufferSurface?.Dispose(); |
|||
} |
|||
|
|||
_framebufferSurface = null; |
|||
_currentFramebufferAddress = IntPtr.Zero; |
|||
} |
|||
|
|||
class PixelFormatShim : IDisposable |
|||
/// <summary>
|
|||
/// Converts non-compatible pixel formats using bitmap copies.
|
|||
/// </summary>
|
|||
private class PixelFormatConversionShim : IDisposable |
|||
{ |
|||
private readonly SKImageInfo _nfo; |
|||
private readonly IntPtr _fb; |
|||
private readonly int _rowBytes; |
|||
private SKBitmap _bitmap; |
|||
private readonly SKBitmap _bitmap; |
|||
private readonly SKImageInfo _destinationInfo; |
|||
private readonly IntPtr _framebufferAddress; |
|||
|
|||
public PixelFormatShim(SKImageInfo nfo, IntPtr fb, int rowBytes) |
|||
public PixelFormatConversionShim(SKImageInfo destinationInfo, IntPtr framebufferAddress) |
|||
{ |
|||
_nfo = nfo; |
|||
_fb = fb; |
|||
_rowBytes = rowBytes; |
|||
_destinationInfo = destinationInfo; |
|||
_framebufferAddress = framebufferAddress; |
|||
|
|||
// Create bitmap using default platform settings
|
|||
_bitmap = new SKBitmap(destinationInfo.Width, destinationInfo.Height); |
|||
|
|||
if (!_bitmap.CanCopyTo(destinationInfo.ColorType)) |
|||
{ |
|||
_bitmap.Dispose(); |
|||
|
|||
throw new Exception( |
|||
$"Unable to create pixel format shim for conversion from {_bitmap.ColorType} to {destinationInfo.ColorType}"); |
|||
} |
|||
|
|||
Surface = SKSurface.Create(_bitmap.Info, _bitmap.GetPixels(), _bitmap.RowBytes); |
|||
|
|||
|
|||
_bitmap = new SKBitmap(nfo.Width, nfo.Height); |
|||
if (!_bitmap.CanCopyTo(nfo.ColorType)) |
|||
if (Surface == null) |
|||
{ |
|||
_bitmap.Dispose(); |
|||
|
|||
throw new Exception( |
|||
$"Unable to create pixel format shim for conversion from {_bitmap.ColorType} to {nfo.ColorType}"); |
|||
$"Unable to create pixel format shim surface for conversion from {_bitmap.ColorType} to {destinationInfo.ColorType}"); |
|||
} |
|||
|
|||
SurfaceCopyHandler = Disposable.Create(CopySurface); |
|||
} |
|||
|
|||
public SKSurface CreateSurface() => SKSurface.Create(_bitmap.Info, _bitmap.GetPixels(), _bitmap.RowBytes); |
|||
/// <summary>
|
|||
/// Skia surface.
|
|||
/// </summary>
|
|||
public SKSurface Surface { get; } |
|||
|
|||
/// <summary>
|
|||
/// Handler to start conversion via surface copy.
|
|||
/// </summary>
|
|||
public IDisposable SurfaceCopyHandler { get; } |
|||
|
|||
/// <inheritdoc />
|
|||
public void Dispose() |
|||
{ |
|||
using (var tmp = _bitmap.Copy(_nfo.ColorType)) |
|||
tmp.CopyPixelsTo(_fb, _nfo.BytesPerPixel * _nfo.Height * _rowBytes, _rowBytes); |
|||
Surface.Dispose(); |
|||
_bitmap.Dispose(); |
|||
} |
|||
|
|||
} |
|||
|
|||
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) |
|||
{ |
|||
var fb = _surface.Lock(); |
|||
PixelFormatShim shim = null; |
|||
SKImageInfo framebuffer = new SKImageInfo(fb.Width, fb.Height, fb.Format.ToSkColorType(), |
|||
SKAlphaType.Premul); |
|||
var surface = SKSurface.Create(framebuffer, fb.Address, fb.RowBytes) ?? |
|||
(shim = new PixelFormatShim(framebuffer, fb.Address, fb.RowBytes)) |
|||
.CreateSurface(); |
|||
if (surface == null) |
|||
throw new Exception("Unable to create a surface for pixel format " + fb.Format + |
|||
" or pixel format translator"); |
|||
var canvas = surface.Canvas; |
|||
|
|||
|
|||
|
|||
canvas.RestoreToCount(0); |
|||
canvas.Save(); |
|||
canvas.ResetMatrix(); |
|||
return new DrawingContextImpl(canvas, fb.Dpi, visualBrushRenderer, canvas, surface, shim, fb); |
|||
/// <summary>
|
|||
/// Convert and copy surface to a framebuffer.
|
|||
/// </summary>
|
|||
private void CopySurface() |
|||
{ |
|||
using (var snapshot = Surface.Snapshot()) |
|||
{ |
|||
snapshot.ReadPixels(_destinationInfo, _framebufferAddress, _destinationInfo.RowBytes, 0, 0, |
|||
SKImageCachingHint.Disallow); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Skia.Helpers |
|||
{ |
|||
/// <summary>
|
|||
/// Helps with saving images to stream/file.
|
|||
/// </summary>
|
|||
public static class ImageSavingHelper |
|||
{ |
|||
/// <summary>
|
|||
/// Save Skia image to a file.
|
|||
/// </summary>
|
|||
/// <param name="image">Image to save</param>
|
|||
/// <param name="fileName">Target file.</param>
|
|||
public static void SaveImage(SKImage image, string fileName) |
|||
{ |
|||
if (image == null) throw new ArgumentNullException(nameof(image)); |
|||
if (fileName == null) throw new ArgumentNullException(nameof(fileName)); |
|||
|
|||
using (var stream = File.Create(fileName)) |
|||
{ |
|||
SaveImage(image, stream); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Save Skia image to a stream.
|
|||
/// </summary>
|
|||
/// <param name="image">Image to save</param>
|
|||
/// <param name="stream">Target stream.</param>
|
|||
public static void SaveImage(SKImage image, Stream stream) |
|||
{ |
|||
if (image == null) throw new ArgumentNullException(nameof(image)); |
|||
if (stream == null) throw new ArgumentNullException(nameof(stream)); |
|||
|
|||
using (var data = image.Encode()) |
|||
{ |
|||
data.SaveTo(stream); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using Avalonia.Platform; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Skia.Helpers |
|||
{ |
|||
/// <summary>
|
|||
/// Helps with resolving pixel formats to Skia color types.
|
|||
/// </summary>
|
|||
public static class PixelFormatHelper |
|||
{ |
|||
/// <summary>
|
|||
/// Resolve given format to Skia color type.
|
|||
/// </summary>
|
|||
/// <param name="format">Format to resolve.</param>
|
|||
/// <returns>Resolved color type.</returns>
|
|||
public static SKColorType ResolveColorType(PixelFormat? format) |
|||
{ |
|||
var colorType = format?.ToSkColorType() ?? SKImageInfo.PlatformColorType; |
|||
|
|||
// TODO: This looks like some leftover hack
|
|||
var runtimePlatform = AvaloniaLocator.Current?.GetService<IRuntimePlatform>(); |
|||
var runtime = runtimePlatform?.GetRuntimeInfo(); |
|||
|
|||
if (runtime?.IsDesktop == true && runtime.Value.OperatingSystem == OperatingSystemType.Linux) |
|||
{ |
|||
colorType = SKColorType.Bgra8888; |
|||
} |
|||
|
|||
return colorType; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using Avalonia.Platform; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Skia |
|||
{ |
|||
/// <summary>
|
|||
/// Extended bitmap implementation that allows for drawing it's contents.
|
|||
/// </summary>
|
|||
internal interface IDrawableBitmapImpl : IBitmapImpl |
|||
{ |
|||
/// <summary>
|
|||
/// Draw bitmap to a drawing context.
|
|||
/// </summary>
|
|||
/// <param name="context">Drawing context.</param>
|
|||
/// <param name="sourceRect">Source rect.</param>
|
|||
/// <param name="destRect">Destination rect.</param>
|
|||
/// <param name="paint">Paint to use.</param>
|
|||
void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint); |
|||
} |
|||
} |
|||
@ -0,0 +1,92 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Skia.Helpers; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Skia |
|||
{ |
|||
/// <summary>
|
|||
/// Immutable Skia bitmap.
|
|||
/// </summary>
|
|||
public class ImmutableBitmap : IDrawableBitmapImpl |
|||
{ |
|||
private readonly SKImage _image; |
|||
|
|||
/// <summary>
|
|||
/// Create immutable bitmap from given stream.
|
|||
/// </summary>
|
|||
/// <param name="stream">Stream containing encoded data.</param>
|
|||
public ImmutableBitmap(Stream stream) |
|||
{ |
|||
using (var skiaStream = new SKManagedStream(stream)) |
|||
{ |
|||
_image = SKImage.FromEncodedData(SKData.Create(skiaStream)); |
|||
|
|||
if (_image == null) |
|||
{ |
|||
throw new ArgumentException("Unable to load bitmap from provided data"); |
|||
} |
|||
|
|||
PixelWidth = _image.Width; |
|||
PixelHeight = _image.Height; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Create immutable bitmap from given pixel data copy.
|
|||
/// </summary>
|
|||
/// <param name="width">Width of data pixels.</param>
|
|||
/// <param name="height">Height of data pixels.</param>
|
|||
/// <param name="stride">Stride of data pixels.</param>
|
|||
/// <param name="format">Format of data pixels.</param>
|
|||
/// <param name="data">Data pixels.</param>
|
|||
public ImmutableBitmap(int width, int height, int stride, PixelFormat format, IntPtr data) |
|||
{ |
|||
var imageInfo = new SKImageInfo(width, height, format.ToSkColorType(), SKAlphaType.Premul); |
|||
|
|||
_image = SKImage.FromPixelCopy(imageInfo, data, stride); |
|||
|
|||
if (_image == null) |
|||
{ |
|||
throw new ArgumentException("Unable to create bitmap from provided data"); |
|||
} |
|||
|
|||
PixelWidth = width; |
|||
PixelHeight = height; |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public int PixelWidth { get; } |
|||
|
|||
/// <inheritdoc />
|
|||
public int PixelHeight { get; } |
|||
|
|||
/// <inheritdoc />
|
|||
public void Dispose() |
|||
{ |
|||
_image.Dispose(); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public void Save(string fileName) |
|||
{ |
|||
ImageSavingHelper.SaveImage(_image, fileName); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public void Save(Stream stream) |
|||
{ |
|||
ImageSavingHelper.SaveImage(_image, stream); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) |
|||
{ |
|||
context.Canvas.DrawImage(_image, sourceRect, destRect, paint); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using Avalonia.Controls; |
|||
using Avalonia.Skia; |
|||
|
|||
// ReSharper disable once CheckNamespace
|
|||
namespace Avalonia |
|||
{ |
|||
/// <summary>
|
|||
/// Skia appication extensions.
|
|||
/// </summary>
|
|||
public static class SkiaApplicationExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Enable Skia renderer.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">Builder type.</typeparam>
|
|||
/// <param name="builder">Builder.</param>
|
|||
/// <param name="preferredBackendType">Preferred backend type.</param>
|
|||
/// <returns>Configure builder.</returns>
|
|||
public static T UseSkia<T>(this T builder) where T : AppBuilderBase<T>, new() |
|||
{ |
|||
builder.UseRenderingSubsystem(() => SkiaPlatform.Initialize(), "Skia"); |
|||
return builder; |
|||
} |
|||
} |
|||
} |
|||
@ -1,47 +1,31 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Logging; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering; |
|||
|
|||
namespace Avalonia |
|||
{ |
|||
public static class SkiaApplicationExtensions |
|||
{ |
|||
public static T UseSkia<T>(this T builder) where T : AppBuilderBase<T>, new() |
|||
{ |
|||
builder.UseRenderingSubsystem(Skia.SkiaPlatform.Initialize, "Skia"); |
|||
return builder; |
|||
} |
|||
} |
|||
} |
|||
|
|||
namespace Avalonia.Skia |
|||
{ |
|||
/// <summary>
|
|||
/// Skia platform initializer.
|
|||
/// </summary>
|
|||
public static class SkiaPlatform |
|||
{ |
|||
private static bool s_forceSoftwareRendering; |
|||
|
|||
/// <summary>
|
|||
/// Initialize Skia platform.
|
|||
/// </summary>
|
|||
public static void Initialize() |
|||
{ |
|||
var renderInterface = new PlatformRenderInterface(); |
|||
|
|||
AvaloniaLocator.CurrentMutable |
|||
.Bind<IPlatformRenderInterface>().ToConstant(renderInterface); |
|||
} |
|||
|
|||
public static bool ForceSoftwareRendering |
|||
{ |
|||
get { return s_forceSoftwareRendering; } |
|||
set |
|||
{ |
|||
s_forceSoftwareRendering = value; |
|||
|
|||
// TODO: I left this property here as place holder. Do we still need the ability to Force software rendering?
|
|||
// Is it even possible with SkiaSharp? Perhaps kekekes can answer as part of the HW accel work.
|
|||
//
|
|||
throw new NotImplementedException(); |
|||
} |
|||
} |
|||
/// <summary>
|
|||
/// Default DPI.
|
|||
/// </summary>
|
|||
public static Vector DefaultDpi => new Vector(96.0f, 96.0f); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,169 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering; |
|||
using Avalonia.Skia.Helpers; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Skia |
|||
{ |
|||
/// <summary>
|
|||
/// Skia render target that writes to a surface.
|
|||
/// </summary>
|
|||
public class SurfaceRenderTarget : IRenderTargetBitmapImpl, IDrawableBitmapImpl |
|||
{ |
|||
private readonly Vector _dpi; |
|||
private readonly SKSurface _surface; |
|||
private readonly SKCanvas _canvas; |
|||
private readonly bool _disableLcdRendering; |
|||
|
|||
/// <summary>
|
|||
/// Create new surface render target.
|
|||
/// </summary>
|
|||
/// <param name="createInfo">Create info.</param>
|
|||
public SurfaceRenderTarget(CreateInfo createInfo) |
|||
{ |
|||
PixelWidth = createInfo.Width; |
|||
PixelHeight = createInfo.Height; |
|||
_dpi = createInfo.Dpi; |
|||
_disableLcdRendering = createInfo.DisableTextLcdRendering; |
|||
|
|||
_surface = CreateSurface(PixelWidth, PixelHeight, createInfo.Format); |
|||
|
|||
_canvas = _surface?.Canvas; |
|||
|
|||
if (_surface == null || _canvas == null) |
|||
{ |
|||
throw new InvalidOperationException("Failed to create Skia render target surface"); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Create backing Skia surface.
|
|||
/// </summary>
|
|||
/// <param name="width">Width.</param>
|
|||
/// <param name="height">Height.</param>
|
|||
/// <param name="format">Format.</param>
|
|||
/// <returns></returns>
|
|||
private static SKSurface CreateSurface(int width, int height, PixelFormat? format) |
|||
{ |
|||
var imageInfo = MakeImageInfo(width, height, format); |
|||
|
|||
return SKSurface.Create(imageInfo); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public void Dispose() |
|||
{ |
|||
_canvas.Dispose(); |
|||
_surface.Dispose(); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) |
|||
{ |
|||
_canvas.RestoreToCount(-1); |
|||
_canvas.ResetMatrix(); |
|||
|
|||
var createInfo = new DrawingContextImpl.CreateInfo |
|||
{ |
|||
Canvas = _canvas, |
|||
Dpi = _dpi, |
|||
VisualBrushRenderer = visualBrushRenderer, |
|||
DisableTextLcdRendering = _disableLcdRendering |
|||
}; |
|||
|
|||
return new DrawingContextImpl(createInfo); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public int PixelWidth { get; } |
|||
|
|||
/// <inheritdoc />
|
|||
public int PixelHeight { get; } |
|||
|
|||
/// <inheritdoc />
|
|||
public void Save(string fileName) |
|||
{ |
|||
using (var image = SnapshotImage()) |
|||
{ |
|||
ImageSavingHelper.SaveImage(image, fileName); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public void Save(Stream stream) |
|||
{ |
|||
using (var image = SnapshotImage()) |
|||
{ |
|||
ImageSavingHelper.SaveImage(image, stream); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) |
|||
{ |
|||
using (var image = SnapshotImage()) |
|||
{ |
|||
context.Canvas.DrawImage(image, sourceRect, destRect, paint); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Create Skia image snapshot from a surface.
|
|||
/// </summary>
|
|||
/// <returns>Image snapshot.</returns>
|
|||
public SKImage SnapshotImage() |
|||
{ |
|||
return _surface.Snapshot(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Create image info for given parameters.
|
|||
/// </summary>
|
|||
/// <param name="width">Width.</param>
|
|||
/// <param name="height">Height.</param>
|
|||
/// <param name="format">Format.</param>
|
|||
/// <returns></returns>
|
|||
private static SKImageInfo MakeImageInfo(int width, int height, PixelFormat? format) |
|||
{ |
|||
var colorType = PixelFormatHelper.ResolveColorType(format); |
|||
|
|||
return new SKImageInfo(width, height, colorType, SKAlphaType.Premul); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Create info of a surface render target.
|
|||
/// </summary>
|
|||
public struct CreateInfo |
|||
{ |
|||
/// <summary>
|
|||
/// Width of a render target.
|
|||
/// </summary>
|
|||
public int Width; |
|||
|
|||
/// <summary>
|
|||
/// Height of a render target.
|
|||
/// </summary>
|
|||
public int Height; |
|||
|
|||
/// <summary>
|
|||
/// Dpi used when rendering to a surface.
|
|||
/// </summary>
|
|||
public Vector Dpi; |
|||
|
|||
/// <summary>
|
|||
/// Pixel format of a render target.
|
|||
/// </summary>
|
|||
public PixelFormat? Format; |
|||
|
|||
/// <summary>
|
|||
/// Render text without Lcd rendering.
|
|||
/// </summary>
|
|||
public bool DisableTextLcdRendering; |
|||
} |
|||
} |
|||
} |
|||
@ -1,59 +1,43 @@ |
|||
using System; |
|||
using Avalonia.Media; |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using Avalonia.Platform; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Skia |
|||
{ |
|||
class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl |
|||
/// <summary>
|
|||
/// A Skia implementation of a <see cref="ITransformedGeometryImpl"/>.
|
|||
/// </summary>
|
|||
public class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="TransformedGeometryImpl"/> class.
|
|||
/// </summary>
|
|||
/// <param name="source">Source geometry.</param>
|
|||
/// <param name="transform">Transform of new geometry.</param>
|
|||
public TransformedGeometryImpl(GeometryImpl source, Matrix transform) |
|||
{ |
|||
SourceGeometry = source; |
|||
Transform = transform; |
|||
EffectivePath = source.EffectivePath.Clone(); |
|||
EffectivePath.Transform(transform.ToSKMatrix()); |
|||
|
|||
var transformedPath = source.EffectivePath.Clone(); |
|||
transformedPath.Transform(transform.ToSKMatrix()); |
|||
|
|||
EffectivePath = transformedPath; |
|||
Bounds = transformedPath.TightBounds.ToAvaloniaRect(); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public override SKPath EffectivePath { get; } |
|||
|
|||
/// <inheritdoc />
|
|||
public IGeometryImpl SourceGeometry { get; } |
|||
|
|||
/// <inheritdoc />
|
|||
public Matrix Transform { get; } |
|||
|
|||
public override Rect Bounds => SourceGeometry.Bounds.TransformToAABB(Transform); |
|||
|
|||
public override bool FillContains(Point point) |
|||
{ |
|||
// TODO: Not supported by SkiaSharp yet, so use expanded Rect
|
|||
return GetRenderBounds(0).Contains(point); |
|||
} |
|||
|
|||
public override Rect GetRenderBounds(Pen pen) |
|||
{ |
|||
return GetRenderBounds(pen.Thickness); |
|||
} |
|||
|
|||
public override IGeometryImpl Intersect(IGeometryImpl geometry) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
public override bool StrokeContains(Pen pen, Point point) |
|||
{ |
|||
// TODO: Not supported by SkiaSharp yet, so use expanded Rect
|
|||
return GetRenderBounds(0).Contains(point); |
|||
} |
|||
|
|||
public override ITransformedGeometryImpl WithTransform(Matrix transform) |
|||
{ |
|||
return new TransformedGeometryImpl(this, transform); |
|||
} |
|||
|
|||
public Rect GetRenderBounds(double strokeThickness) |
|||
{ |
|||
// TODO: Calculate properly.
|
|||
return Bounds.Inflate(strokeThickness); |
|||
} |
|||
/// <inheritdoc />
|
|||
public override Rect Bounds { get; } |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,151 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Skia.Helpers; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Skia |
|||
{ |
|||
/// <summary>
|
|||
/// Skia based writeable bitmap.
|
|||
/// </summary>
|
|||
public class WriteableBitmapImpl : IWriteableBitmapImpl, IDrawableBitmapImpl |
|||
{ |
|||
private static readonly SKBitmapReleaseDelegate s_releaseDelegate = ReleaseProc; |
|||
private readonly SKBitmap _bitmap; |
|||
|
|||
/// <summary>
|
|||
/// Create new writeable bitmap.
|
|||
/// </summary>
|
|||
/// <param name="width">Width.</param>
|
|||
/// <param name="height">Height.</param>
|
|||
/// <param name="format">Format.</param>
|
|||
public WriteableBitmapImpl(int width, int height, PixelFormat? format = null) |
|||
{ |
|||
PixelHeight = height; |
|||
PixelWidth = width; |
|||
|
|||
var colorType = PixelFormatHelper.ResolveColorType(format); |
|||
|
|||
var runtimePlatform = AvaloniaLocator.Current?.GetService<IRuntimePlatform>(); |
|||
|
|||
if (runtimePlatform != null) |
|||
{ |
|||
_bitmap = new SKBitmap(); |
|||
|
|||
var nfo = new SKImageInfo(width, height, colorType, SKAlphaType.Premul); |
|||
var blob = runtimePlatform.AllocBlob(nfo.BytesSize); |
|||
|
|||
_bitmap.InstallPixels(nfo, blob.Address, nfo.RowBytes, null, s_releaseDelegate, blob); |
|||
} |
|||
else |
|||
{ |
|||
_bitmap = new SKBitmap(width, height, colorType, SKAlphaType.Premul); |
|||
} |
|||
|
|||
_bitmap.Erase(SKColor.Empty); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public int PixelWidth { get; } |
|||
|
|||
/// <inheritdoc />
|
|||
public int PixelHeight { get; } |
|||
|
|||
/// <inheritdoc />
|
|||
public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) |
|||
{ |
|||
context.Canvas.DrawBitmap(_bitmap, sourceRect, destRect, paint); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public void Dispose() |
|||
{ |
|||
_bitmap.Dispose(); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public void Save(Stream stream) |
|||
{ |
|||
using (var image = GetSnapshot()) |
|||
{ |
|||
ImageSavingHelper.SaveImage(image, stream); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public void Save(string fileName) |
|||
{ |
|||
using (var image = GetSnapshot()) |
|||
{ |
|||
ImageSavingHelper.SaveImage(image, fileName); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public ILockedFramebuffer Lock() => new BitmapFramebuffer(_bitmap); |
|||
|
|||
/// <summary>
|
|||
/// Get snapshot as image.
|
|||
/// </summary>
|
|||
/// <returns>Image snapshot.</returns>
|
|||
public SKImage GetSnapshot() |
|||
{ |
|||
return SKImage.FromPixels(_bitmap.Info, _bitmap.GetPixels(), _bitmap.RowBytes); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Release given unmanaged blob.
|
|||
/// </summary>
|
|||
/// <param name="address">Blob address.</param>
|
|||
/// <param name="ctx">Blob.</param>
|
|||
private static void ReleaseProc(IntPtr address, object ctx) |
|||
{ |
|||
((IUnmanagedBlob)ctx).Dispose(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Framebuffer for bitmap.
|
|||
/// </summary>
|
|||
private class BitmapFramebuffer : ILockedFramebuffer |
|||
{ |
|||
private SKBitmap _bitmap; |
|||
|
|||
/// <summary>
|
|||
/// Create framebuffer from given bitmap.
|
|||
/// </summary>
|
|||
/// <param name="bitmap">Bitmap.</param>
|
|||
public BitmapFramebuffer(SKBitmap bitmap) |
|||
{ |
|||
_bitmap = bitmap; |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public void Dispose() |
|||
{ |
|||
_bitmap = null; |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public IntPtr Address => _bitmap.GetPixels(); |
|||
|
|||
/// <inheritdoc />
|
|||
public int Width => _bitmap.Width; |
|||
|
|||
/// <inheritdoc />
|
|||
public int Height => _bitmap.Height; |
|||
|
|||
/// <inheritdoc />
|
|||
public int RowBytes => _bitmap.RowBytes; |
|||
|
|||
/// <inheritdoc />
|
|||
public Vector Dpi { get; } = SkiaPlatform.DefaultDpi; |
|||
|
|||
/// <inheritdoc />
|
|||
public PixelFormat Format => _bitmap.ColorType.ToPixelFormat(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,42 +1,22 @@ |
|||
TODO: |
|||
|
|||
BitmapImpl |
|||
- constructor from Width/Height |
|||
- Save |
|||
|
|||
StreamGeometryImpl |
|||
- Hit testing in Geometry missing as SkiaSharp does not expose this |
|||
|
|||
DrawingContextImpl |
|||
- Alpha support missing as SkiaSharp does not expose this |
|||
- Gradient Shader caching? |
|||
- TileBrushes |
|||
- Pen Dash styles |
|||
|
|||
Formatted Text Rendering |
|||
- minor polish |
|||
- Minor polish |
|||
|
|||
RenderTarget |
|||
- Figure out a cleaner implementation across all platforms |
|||
- HW acceleration |
|||
Linux |
|||
- Need gpu platform implementation |
|||
|
|||
App Bootstrapping |
|||
- Cleanup the testapplications across all platforms |
|||
- Add a cleaner Fluent API for the subsystems |
|||
- ie. app.UseDirect2D() (via platform specific extension methods) |
|||
macOS |
|||
- Need gpu platform implementation |
|||
|
|||
Android |
|||
- Not tested at all yet |
|||
|
|||
iOS |
|||
- Get GLView working again. See HW above |
|||
|
|||
Win32 |
|||
- Cleanup the unmanaged methods (BITMAPINFO) if possible |
|||
- Not tested at all yet |
|||
|
|||
General |
|||
- Cleanup/eliminate obsolete files |
|||
- Finish cleanup of the many Test Applications |
|||
- Get Skia Unit Tests passing |
|||
|
|||
|
|||
- Get Skia Unit Tests passing (most of the issues are related to antialiasing) |
|||
@ -0,0 +1,22 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0"> |
|||
<PropertyGroup> |
|||
<TargetFrameworks>netcoreapp2.0</TargetFrameworks> |
|||
</PropertyGroup> |
|||
<Import Project="..\..\build\UnitTests.NetCore.targets" /> |
|||
<Import Project="..\..\build\Moq.props" /> |
|||
<Import Project="..\..\build\XUnit.props" /> |
|||
<Import Project="..\..\build\Rx.props" /> |
|||
<Import Project="..\..\build\Microsoft.Reactive.Testing.props" /> |
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\Avalonia.Animation\Avalonia.Animation.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Input\Avalonia.Input.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Interactivity\Avalonia.Interactivity.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Layout\Avalonia.Layout.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Visuals\Avalonia.Visuals.csproj" /> |
|||
<ProjectReference Include="..\..\src\Avalonia.Styling\Avalonia.Styling.csproj" /> |
|||
<ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" /> |
|||
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
@ -0,0 +1,79 @@ |
|||
using Avalonia.Controls.Shapes; |
|||
using Avalonia.Layout; |
|||
using Avalonia.Media; |
|||
using Avalonia.Rendering; |
|||
using Avalonia.UnitTests; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Skia.UnitTests |
|||
{ |
|||
public class HitTesting |
|||
{ |
|||
[Fact] |
|||
public void Hit_Test_Should_Respect_Fill() |
|||
{ |
|||
using (AvaloniaLocator.EnterScope()) |
|||
{ |
|||
SkiaPlatform.Initialize(); |
|||
|
|||
var root = new TestRoot |
|||
{ |
|||
Width = 100, |
|||
Height = 100, |
|||
Child = new Ellipse |
|||
{ |
|||
Width = 100, |
|||
Height = 100, |
|||
Fill = Brushes.Red, |
|||
HorizontalAlignment = HorizontalAlignment.Center, |
|||
VerticalAlignment = VerticalAlignment.Center |
|||
} |
|||
}; |
|||
|
|||
root.Renderer = new DeferredRenderer(root, null); |
|||
root.Measure(Size.Infinity); |
|||
root.Arrange(new Rect(root.DesiredSize)); |
|||
|
|||
var outsideResult = root.Renderer.HitTest(new Point(10, 10), root, null); |
|||
var insideResult = root.Renderer.HitTest(new Point(50, 50), root, null); |
|||
|
|||
Assert.Empty(outsideResult); |
|||
Assert.Equal(new[] {root.Child}, insideResult); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Hit_Test_Should_Respect_Stroke() |
|||
{ |
|||
using (AvaloniaLocator.EnterScope()) |
|||
{ |
|||
SkiaPlatform.Initialize(); |
|||
|
|||
var root = new TestRoot |
|||
{ |
|||
Width = 100, |
|||
Height = 100, |
|||
Child = new Ellipse |
|||
{ |
|||
Width = 100, |
|||
Height = 100, |
|||
Stroke = Brushes.Red, |
|||
StrokeThickness = 5, |
|||
HorizontalAlignment = HorizontalAlignment.Center, |
|||
VerticalAlignment = VerticalAlignment.Center |
|||
} |
|||
}; |
|||
|
|||
root.Renderer = new DeferredRenderer(root, null); |
|||
root.Measure(Size.Infinity); |
|||
root.Arrange(new Rect(root.DesiredSize)); |
|||
|
|||
var outsideResult = root.Renderer.HitTest(new Point(50, 50), root, null); |
|||
var insideResult = root.Renderer.HitTest(new Point(1, 50), root, null); |
|||
|
|||
Assert.Empty(outsideResult); |
|||
Assert.Equal(new[] { root.Child }, insideResult); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System.Reflection; |
|||
using Xunit; |
|||
|
|||
// Don't run tests in parallel.
|
|||
[assembly: CollectionBehavior(DisableTestParallelization = true)] |
|||
|
Before Width: | Height: | Size: 652 B After Width: | Height: | Size: 642 B |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 1.1 KiB |