@ -0,0 +1,20 @@ |
|||||
|
namespace Avalonia.Controls.Templates |
||||
|
{ |
||||
|
public class TemplateResult<T> |
||||
|
{ |
||||
|
public T Result { get; } |
||||
|
public INameScope NameScope { get; } |
||||
|
|
||||
|
public TemplateResult(T result, INameScope nameScope) |
||||
|
{ |
||||
|
Result = result; |
||||
|
NameScope = nameScope; |
||||
|
} |
||||
|
|
||||
|
public void Deconstruct(out T result, out INameScope scope) |
||||
|
{ |
||||
|
result = Result; |
||||
|
scope = NameScope; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,170 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Text; |
||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
public enum GeometryCombineMode |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The two regions are combined by taking the union of both. The resulting geometry is
|
||||
|
/// geometry A + geometry B.
|
||||
|
/// </summary>
|
||||
|
Union, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The two regions are combined by taking their intersection. The new area consists of the
|
||||
|
/// overlapping region between the two geometries.
|
||||
|
/// </summary>
|
||||
|
Intersect, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The two regions are combined by taking the area that exists in the first region but not
|
||||
|
/// the second and the area that exists in the second region but not the first. The new
|
||||
|
/// region consists of (A-B) + (B-A), where A and B are geometries.
|
||||
|
/// </summary>
|
||||
|
Xor, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The second region is excluded from the first. Given two geometries, A and B, the area of
|
||||
|
/// geometry B is removed from the area of geometry A, producing a region that is A-B.
|
||||
|
/// </summary>
|
||||
|
Exclude, |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents a 2-D geometric shape defined by the combination of two Geometry objects.
|
||||
|
/// </summary>
|
||||
|
public class CombinedGeometry : Geometry |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="Geometry1"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<Geometry?> Geometry1Property = |
||||
|
AvaloniaProperty.Register<CombinedGeometry, Geometry?>(nameof(Geometry1)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="Geometry2"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<Geometry?> Geometry2Property = |
||||
|
AvaloniaProperty.Register<CombinedGeometry, Geometry?>(nameof(Geometry2)); |
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="GeometryCombineMode"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<GeometryCombineMode> GeometryCombineModeProperty = |
||||
|
AvaloniaProperty.Register<CombinedGeometry, GeometryCombineMode>(nameof(GeometryCombineMode)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="CombinedGeometry"/> class.
|
||||
|
/// </summary>
|
||||
|
public CombinedGeometry() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="CombinedGeometry"/> class with the
|
||||
|
/// specified <see cref="Geometry"/> objects.
|
||||
|
/// </summary>
|
||||
|
/// <param name="geometry1">The first geometry to combine.</param>
|
||||
|
/// <param name="geometry2">The second geometry to combine.</param>
|
||||
|
public CombinedGeometry(Geometry geometry1, Geometry geometry2) |
||||
|
{ |
||||
|
Geometry1 = geometry1; |
||||
|
Geometry2 = geometry2; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="CombinedGeometry"/> class with the
|
||||
|
/// specified <see cref="Geometry"/> objects and <see cref="GeometryCombineMode"/>.
|
||||
|
/// </summary>
|
||||
|
/// <param name="combineMode">The method by which geometry1 and geometry2 are combined.</param>
|
||||
|
/// <param name="geometry1">The first geometry to combine.</param>
|
||||
|
/// <param name="geometry2">The second geometry to combine.</param>
|
||||
|
public CombinedGeometry(GeometryCombineMode combineMode, Geometry? geometry1, Geometry? geometry2) |
||||
|
{ |
||||
|
Geometry1 = geometry1; |
||||
|
Geometry2 = geometry2; |
||||
|
GeometryCombineMode = combineMode; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="CombinedGeometry"/> class with the
|
||||
|
/// specified <see cref="Geometry"/> objects, <see cref="GeometryCombineMode"/> and
|
||||
|
/// <see cref="Transform"/>.
|
||||
|
/// </summary>
|
||||
|
/// <param name="combineMode">The method by which geometry1 and geometry2 are combined.</param>
|
||||
|
/// <param name="geometry1">The first geometry to combine.</param>
|
||||
|
/// <param name="geometry2">The second geometry to combine.</param>
|
||||
|
/// <param name="transform">The transform applied to the geometry.</param>
|
||||
|
public CombinedGeometry( |
||||
|
GeometryCombineMode combineMode, |
||||
|
Geometry? geometry1, |
||||
|
Geometry? geometry2, |
||||
|
Transform? transform) |
||||
|
{ |
||||
|
Geometry1 = geometry1; |
||||
|
Geometry2 = geometry2; |
||||
|
GeometryCombineMode = combineMode; |
||||
|
Transform = transform; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the first <see cref="Geometry"/> object of this
|
||||
|
/// <see cref="CombinedGeometry"/> object.
|
||||
|
/// </summary>
|
||||
|
public Geometry? Geometry1 |
||||
|
{ |
||||
|
get => GetValue(Geometry1Property); |
||||
|
set => SetValue(Geometry1Property, value); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the second <see cref="Geometry"/> object of this
|
||||
|
/// <see cref="CombinedGeometry"/> object.
|
||||
|
/// </summary>
|
||||
|
public Geometry? Geometry2 |
||||
|
{ |
||||
|
get => GetValue(Geometry2Property); |
||||
|
set => SetValue(Geometry2Property, value); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the method by which the two geometries (specified by the
|
||||
|
/// <see cref="Geometry1"/> and <see cref="Geometry2"/> properties) are combined. The
|
||||
|
/// default value is <see cref="GeometryCombineMode.Union"/>.
|
||||
|
/// </summary>
|
||||
|
public GeometryCombineMode GeometryCombineMode |
||||
|
{ |
||||
|
get => GetValue(GeometryCombineModeProperty); |
||||
|
set => SetValue(GeometryCombineModeProperty, value); |
||||
|
} |
||||
|
|
||||
|
public override Geometry Clone() |
||||
|
{ |
||||
|
return new CombinedGeometry(GeometryCombineMode, Geometry1, Geometry2, Transform); |
||||
|
} |
||||
|
|
||||
|
protected override IGeometryImpl? CreateDefiningGeometry() |
||||
|
{ |
||||
|
var g1 = Geometry1; |
||||
|
var g2 = Geometry2; |
||||
|
|
||||
|
if (g1 is object && g2 is object) |
||||
|
{ |
||||
|
var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>(); |
||||
|
return factory.CreateCombinedGeometry(GeometryCombineMode, g1, g2); |
||||
|
} |
||||
|
else if (GeometryCombineMode == GeometryCombineMode.Intersect) |
||||
|
return null; |
||||
|
else if (g1 is object) |
||||
|
return g1.PlatformImpl; |
||||
|
else if (g2 is object) |
||||
|
return g2.PlatformImpl; |
||||
|
else |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
using System.Collections; |
||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Animation; |
||||
|
|
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
public class GeometryCollection : Animatable, IList<Geometry>, IReadOnlyList<Geometry> |
||||
|
{ |
||||
|
private List<Geometry> _inner; |
||||
|
|
||||
|
public GeometryCollection() => _inner = new List<Geometry>(); |
||||
|
public GeometryCollection(IEnumerable<Geometry> collection) => _inner = new List<Geometry>(collection); |
||||
|
public GeometryCollection(int capacity) => _inner = new List<Geometry>(capacity); |
||||
|
|
||||
|
public Geometry this[int index] |
||||
|
{ |
||||
|
get => _inner[index]; |
||||
|
set => _inner[index] = value; |
||||
|
} |
||||
|
|
||||
|
public int Count => _inner.Count; |
||||
|
public bool IsReadOnly => false; |
||||
|
|
||||
|
public void Add(Geometry item) => _inner.Add(item); |
||||
|
public void Clear() => _inner.Clear(); |
||||
|
public bool Contains(Geometry item) => _inner.Contains(item); |
||||
|
public void CopyTo(Geometry[] array, int arrayIndex) => _inner.CopyTo(array, arrayIndex); |
||||
|
public IEnumerator<Geometry> GetEnumerator() => _inner.GetEnumerator(); |
||||
|
public int IndexOf(Geometry item) => _inner.IndexOf(item); |
||||
|
public void Insert(int index, Geometry item) => _inner.Insert(index, item); |
||||
|
public bool Remove(Geometry item) => _inner.Remove(item); |
||||
|
public void RemoveAt(int index) => _inner.RemoveAt(index); |
||||
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,80 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using Avalonia.Metadata; |
||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Media |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents a composite geometry, composed of other <see cref="Geometry"/> objects.
|
||||
|
/// </summary>
|
||||
|
public class GeometryGroup : Geometry |
||||
|
{ |
||||
|
public static readonly DirectProperty<GeometryGroup, GeometryCollection?> ChildrenProperty = |
||||
|
AvaloniaProperty.RegisterDirect<GeometryGroup, GeometryCollection?> ( |
||||
|
nameof(Children), |
||||
|
o => o.Children, |
||||
|
(o, v) => o.Children = v); |
||||
|
|
||||
|
public static readonly StyledProperty<FillRule> FillRuleProperty = |
||||
|
AvaloniaProperty.Register<GeometryGroup, FillRule>(nameof(FillRule)); |
||||
|
|
||||
|
private GeometryCollection? _children; |
||||
|
private bool _childrenSet; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the collection that contains the child geometries.
|
||||
|
/// </summary>
|
||||
|
[Content] |
||||
|
public GeometryCollection? Children |
||||
|
{ |
||||
|
get => _children ??= (!_childrenSet ? new GeometryCollection() : null); |
||||
|
set |
||||
|
{ |
||||
|
SetAndRaise(ChildrenProperty, ref _children, value); |
||||
|
_childrenSet = true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets how the intersecting areas of the objects contained in this
|
||||
|
/// <see cref="GeometryGroup"/> are combined. The default is <see cref="FillRule.EvenOdd"/>.
|
||||
|
/// </summary>
|
||||
|
public FillRule FillRule |
||||
|
{ |
||||
|
get => GetValue(FillRuleProperty); |
||||
|
set => SetValue(FillRuleProperty, value); |
||||
|
} |
||||
|
|
||||
|
public override Geometry Clone() |
||||
|
{ |
||||
|
var result = new GeometryGroup { FillRule = FillRule, Transform = Transform }; |
||||
|
if (_children?.Count > 0) |
||||
|
result.Children = new GeometryCollection(_children); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
protected override IGeometryImpl? CreateDefiningGeometry() |
||||
|
{ |
||||
|
if (_children?.Count > 0) |
||||
|
{ |
||||
|
var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>(); |
||||
|
return factory.CreateGeometryGroup(FillRule, _children); |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) |
||||
|
{ |
||||
|
base.OnPropertyChanged(change); |
||||
|
|
||||
|
if (change.Property == ChildrenProperty || change.Property == FillRuleProperty) |
||||
|
{ |
||||
|
InvalidateGeometry(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
namespace Avalonia.LinuxFramebuffer |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Platform-specific options which apply to the Linux framebuffer.
|
||||
|
/// </summary>
|
||||
|
public class LinuxFramebufferPlatformOptions |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets or sets the number of frames per second at which the renderer should run.
|
||||
|
/// Default 60.
|
||||
|
/// </summary>
|
||||
|
public int Fps { get; set; } = 60; |
||||
|
} |
||||
|
} |
||||
@ -1,46 +0,0 @@ |
|||||
using System; |
|
||||
using System.Runtime.InteropServices; |
|
||||
using Avalonia.Platform; |
|
||||
|
|
||||
namespace Avalonia.LinuxFramebuffer |
|
||||
{ |
|
||||
unsafe class LockedFramebuffer : ILockedFramebuffer |
|
||||
{ |
|
||||
private readonly int _fb; |
|
||||
private readonly fb_fix_screeninfo _fixedInfo; |
|
||||
private fb_var_screeninfo _varInfo; |
|
||||
private readonly IntPtr _address; |
|
||||
|
|
||||
public LockedFramebuffer(int fb, fb_fix_screeninfo fixedInfo, fb_var_screeninfo varInfo, IntPtr address, Vector dpi) |
|
||||
{ |
|
||||
_fb = fb; |
|
||||
_fixedInfo = fixedInfo; |
|
||||
_varInfo = varInfo; |
|
||||
_address = address; |
|
||||
Dpi = dpi; |
|
||||
//Use double buffering to avoid flicker
|
|
||||
Address = Marshal.AllocHGlobal(RowBytes * Size.Height); |
|
||||
} |
|
||||
|
|
||||
|
|
||||
void VSync() |
|
||||
{ |
|
||||
NativeUnsafeMethods.ioctl(_fb, FbIoCtl.FBIO_WAITFORVSYNC, null); |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
VSync(); |
|
||||
NativeUnsafeMethods.memcpy(_address, Address, new IntPtr(RowBytes * Size.Height)); |
|
||||
|
|
||||
Marshal.FreeHGlobal(Address); |
|
||||
Address = IntPtr.Zero; |
|
||||
} |
|
||||
|
|
||||
public IntPtr Address { get; private set; } |
|
||||
public PixelSize Size => new PixelSize((int)_varInfo.xres, (int) _varInfo.yres); |
|
||||
public int RowBytes => (int) _fixedInfo.line_length; |
|
||||
public Vector Dpi { get; } |
|
||||
public PixelFormat Format => _varInfo.bits_per_pixel == 16 ? PixelFormat.Rgb565 : _varInfo.blue.offset == 16 ? PixelFormat.Rgba8888 : PixelFormat.Bgra8888; |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,70 @@ |
|||||
|
using System; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Threading; |
||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
namespace Avalonia.LinuxFramebuffer.Output |
||||
|
{ |
||||
|
internal unsafe class FbDevBackBuffer : IDisposable |
||||
|
{ |
||||
|
private readonly int _fb; |
||||
|
private readonly fb_fix_screeninfo _fixedInfo; |
||||
|
private readonly fb_var_screeninfo _varInfo; |
||||
|
private readonly IntPtr _targetAddress; |
||||
|
private readonly object _lock = new object(); |
||||
|
|
||||
|
public FbDevBackBuffer(int fb, fb_fix_screeninfo fixedInfo, fb_var_screeninfo varInfo, IntPtr targetAddress) |
||||
|
{ |
||||
|
_fb = fb; |
||||
|
_fixedInfo = fixedInfo; |
||||
|
_varInfo = varInfo; |
||||
|
_targetAddress = targetAddress; |
||||
|
Address = Marshal.AllocHGlobal(RowBytes * Size.Height); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
if (Address != IntPtr.Zero) |
||||
|
{ |
||||
|
Marshal.FreeHGlobal(Address); |
||||
|
Address = IntPtr.Zero; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public ILockedFramebuffer Lock(Vector dpi) |
||||
|
{ |
||||
|
Monitor.Enter(_lock); |
||||
|
try |
||||
|
{ |
||||
|
return new LockedFramebuffer(Address, |
||||
|
new PixelSize((int)_varInfo.xres, (int)_varInfo.yres), |
||||
|
(int)_fixedInfo.line_length, dpi, |
||||
|
_varInfo.bits_per_pixel == 16 ? PixelFormat.Rgb565 |
||||
|
: _varInfo.blue.offset == 16 ? PixelFormat.Rgba8888 |
||||
|
: PixelFormat.Bgra8888, |
||||
|
() => |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
NativeUnsafeMethods.ioctl(_fb, FbIoCtl.FBIO_WAITFORVSYNC, null); |
||||
|
NativeUnsafeMethods.memcpy(_targetAddress, Address, new IntPtr(RowBytes * Size.Height)); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
Monitor.Exit(_lock); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
Monitor.Exit(_lock); |
||||
|
throw; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public IntPtr Address { get; private set; } |
||||
|
public PixelSize Size => new PixelSize((int)_varInfo.xres, (int) _varInfo.yres); |
||||
|
public int RowBytes => (int) _fixedInfo.line_length; |
||||
|
} |
||||
|
} |
||||
@ -1 +1 @@ |
|||||
Subproject commit f4ac681b91a9dc7a7a095d1050a683de23d86b72 |
Subproject commit 8e20d65eb5f1efbae08e49b18f39bfdce32df7b3 |
||||
@ -0,0 +1,35 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Media; |
||||
|
using SkiaSharp; |
||||
|
|
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Skia |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A Skia implementation of a <see cref="Avalonia.Media.GeometryGroup"/>.
|
||||
|
/// </summary>
|
||||
|
internal class CombinedGeometryImpl : GeometryImpl |
||||
|
{ |
||||
|
public CombinedGeometryImpl(GeometryCombineMode combineMode, Geometry g1, Geometry g2) |
||||
|
{ |
||||
|
var path1 = ((GeometryImpl)g1.PlatformImpl).EffectivePath; |
||||
|
var path2 = ((GeometryImpl)g2.PlatformImpl).EffectivePath; |
||||
|
var op = combineMode switch |
||||
|
{ |
||||
|
GeometryCombineMode.Intersect => SKPathOp.Intersect, |
||||
|
GeometryCombineMode.Xor => SKPathOp.Xor, |
||||
|
GeometryCombineMode.Exclude => SKPathOp.Difference, |
||||
|
_ => SKPathOp.Union, |
||||
|
}; |
||||
|
|
||||
|
var path = path1.Op(path2, op); |
||||
|
|
||||
|
EffectivePath = path; |
||||
|
Bounds = path.Bounds.ToAvaloniaRect(); |
||||
|
} |
||||
|
|
||||
|
public override Rect Bounds { get; } |
||||
|
public override SKPath EffectivePath { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Media; |
||||
|
using SkiaSharp; |
||||
|
|
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Skia |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A Skia implementation of a <see cref="Avalonia.Media.GeometryGroup"/>.
|
||||
|
/// </summary>
|
||||
|
internal class GeometryGroupImpl : GeometryImpl |
||||
|
{ |
||||
|
public GeometryGroupImpl(FillRule fillRule, IReadOnlyList<Geometry> children) |
||||
|
{ |
||||
|
var path = new SKPath |
||||
|
{ |
||||
|
FillType = fillRule == FillRule.NonZero ? SKPathFillType.Winding : SKPathFillType.EvenOdd, |
||||
|
}; |
||||
|
|
||||
|
var count = children.Count; |
||||
|
|
||||
|
for (var i = 0; i < count; ++i) |
||||
|
{ |
||||
|
if (children[i]?.PlatformImpl is GeometryImpl child) |
||||
|
path.AddPath(child.EffectivePath); |
||||
|
} |
||||
|
|
||||
|
EffectivePath = path; |
||||
|
Bounds = path.Bounds.ToAvaloniaRect(); |
||||
|
} |
||||
|
|
||||
|
public override Rect Bounds { get; } |
||||
|
public override SKPath EffectivePath { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
using SharpDX.Direct2D1; |
||||
|
using AM = Avalonia.Media; |
||||
|
|
||||
|
namespace Avalonia.Direct2D1.Media |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A Direct2D implementation of a <see cref="Avalonia.Media.CombinedGeometry"/>.
|
||||
|
/// </summary>
|
||||
|
internal class CombinedGeometryImpl : GeometryImpl |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
|
||||
|
/// </summary>
|
||||
|
public CombinedGeometryImpl( |
||||
|
AM.GeometryCombineMode combineMode, |
||||
|
AM.Geometry geometry1, |
||||
|
AM.Geometry geometry2) |
||||
|
: base(CreateGeometry(combineMode, geometry1, geometry2)) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
private static Geometry CreateGeometry( |
||||
|
AM.GeometryCombineMode combineMode, |
||||
|
AM.Geometry geometry1, |
||||
|
AM.Geometry geometry2) |
||||
|
{ |
||||
|
var g1 = ((GeometryImpl)geometry1.PlatformImpl).Geometry; |
||||
|
var g2 = ((GeometryImpl)geometry2.PlatformImpl).Geometry; |
||||
|
var dest = new PathGeometry(Direct2D1Platform.Direct2D1Factory); |
||||
|
using var sink = dest.Open(); |
||||
|
g1.Combine(g2, (CombineMode)combineMode, sink); |
||||
|
sink.Close(); |
||||
|
return dest; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using SharpDX.Direct2D1; |
||||
|
using AM = Avalonia.Media; |
||||
|
|
||||
|
namespace Avalonia.Direct2D1.Media |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A Direct2D implementation of a <see cref="Avalonia.Media.GeometryGroup"/>.
|
||||
|
/// </summary>
|
||||
|
internal class GeometryGroupImpl : GeometryImpl |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
|
||||
|
/// </summary>
|
||||
|
public GeometryGroupImpl(AM.FillRule fillRule, IReadOnlyList<AM.Geometry> geometry) |
||||
|
: base(CreateGeometry(fillRule, geometry)) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
private static Geometry CreateGeometry(AM.FillRule fillRule, IReadOnlyList<AM.Geometry> children) |
||||
|
{ |
||||
|
var count = children.Count; |
||||
|
var c = new Geometry[count]; |
||||
|
|
||||
|
for (var i = 0; i < count; ++i) |
||||
|
{ |
||||
|
c[i] = ((GeometryImpl)children[i].PlatformImpl).Geometry; |
||||
|
} |
||||
|
|
||||
|
return new GeometryGroup(Direct2D1Platform.Direct2D1Factory, (FillMode)fillRule, c); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
using System; |
||||
|
using System.Collections; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Collections.Specialized; |
||||
|
using System.Text; |
||||
|
using Avalonia.Collections; |
||||
|
using Avalonia.Diagnostics; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Controls.UnitTests |
||||
|
{ |
||||
|
public class ItemsSourceViewTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Only_Subscribes_To_Source_CollectionChanged_When_CollectionChanged_Subscribed() |
||||
|
{ |
||||
|
var source = new AvaloniaList<string>(); |
||||
|
var target = new ItemsSourceView<string>(source); |
||||
|
var debug = (INotifyCollectionChangedDebug)source; |
||||
|
|
||||
|
Assert.Null(debug.GetCollectionChangedSubscribers()); |
||||
|
|
||||
|
void Handler(object sender, NotifyCollectionChangedEventArgs e) { } |
||||
|
target.CollectionChanged += Handler; |
||||
|
|
||||
|
Assert.NotNull(debug.GetCollectionChangedSubscribers()); |
||||
|
Assert.Equal(1, debug.GetCollectionChangedSubscribers().Length); |
||||
|
|
||||
|
target.CollectionChanged -= Handler; |
||||
|
|
||||
|
Assert.Null(debug.GetCollectionChangedSubscribers()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Cannot_Wrap_An_ItemsSourceView_In_Another() |
||||
|
{ |
||||
|
var source = new ItemsSourceView<string>(new string[0]); |
||||
|
Assert.Throws<ArgumentException>(() => new ItemsSourceView<string>(source)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Cannot_Create_ItemsSourceView_With_Collection_That_Implements_INCC_But_Not_List() |
||||
|
{ |
||||
|
var source = new InvalidCollection(); |
||||
|
Assert.Throws<ArgumentException>(() => new ItemsSourceView<string>(source)); |
||||
|
} |
||||
|
|
||||
|
private class InvalidCollection : INotifyCollectionChanged, IEnumerable<string> |
||||
|
{ |
||||
|
public event NotifyCollectionChangedEventHandler CollectionChanged; |
||||
|
|
||||
|
public IEnumerator<string> GetEnumerator() |
||||
|
{ |
||||
|
yield break; |
||||
|
} |
||||
|
|
||||
|
IEnumerator IEnumerable.GetEnumerator() |
||||
|
{ |
||||
|
yield break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,62 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Controls.Presenters; |
||||
|
using Avalonia.Markup.Xaml.Templates; |
||||
|
using Avalonia.Metadata; |
||||
|
using Avalonia.UnitTests; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Markup.Xaml.UnitTests.Xaml |
||||
|
{ |
||||
|
public class SampleTemplatedObject : StyledElement |
||||
|
{ |
||||
|
[Content] public List<SampleTemplatedObject> Content { get; set; } = new List<SampleTemplatedObject>(); |
||||
|
public string Foo { get; set; } |
||||
|
} |
||||
|
|
||||
|
public class SampleTemplatedObjectTemplate |
||||
|
{ |
||||
|
[Content] |
||||
|
[TemplateContent(TemplateResultType = typeof(SampleTemplatedObject))] |
||||
|
public object Content { get; set; } |
||||
|
} |
||||
|
|
||||
|
public class SampleTemplatedObjectContainer |
||||
|
{ |
||||
|
public SampleTemplatedObjectTemplate Template { get; set; } |
||||
|
} |
||||
|
|
||||
|
public class GenericTemplateTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void DataTemplate_Can_Be_Empty() |
||||
|
{ |
||||
|
using (UnitTestApplication.Start(TestServices.StyledWindow)) |
||||
|
{ |
||||
|
var xaml = @"
|
||||
|
<s:SampleTemplatedObjectContainer xmlns='https://github.com/avaloniaui'
|
||||
|
xmlns:sys='clr-namespace:System;assembly=netstandard' |
||||
|
xmlns:s='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml' |
||||
|
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
|
||||
|
<s:SampleTemplatedObjectContainer.Template> |
||||
|
<s:SampleTemplatedObjectTemplate> |
||||
|
<s:SampleTemplatedObject x:Name='root'> |
||||
|
<s:SampleTemplatedObject x:Name='child1' Foo='foo' /> |
||||
|
<s:SampleTemplatedObject x:Name='child2' Foo='bar' /> |
||||
|
</s:SampleTemplatedObject> |
||||
|
</s:SampleTemplatedObjectTemplate> |
||||
|
</s:SampleTemplatedObjectContainer.Template> |
||||
|
</s:SampleTemplatedObjectContainer>";
|
||||
|
var container = |
||||
|
(SampleTemplatedObjectContainer)AvaloniaRuntimeXamlLoader.Load(xaml, |
||||
|
typeof(GenericTemplateTests).Assembly); |
||||
|
var res = TemplateContent.Load<SampleTemplatedObject>(container.Template.Content); |
||||
|
Assert.Equal(res.Result, res.NameScope.Find("root")); |
||||
|
Assert.Equal(res.Result.Content[0], res.NameScope.Find("child1")); |
||||
|
Assert.Equal(res.Result.Content[1], res.NameScope.Find("child2")); |
||||
|
Assert.Equal("foo", res.Result.Content[0].Foo); |
||||
|
Assert.Equal("bar", res.Result.Content[1].Foo); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,89 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Controls.Shapes; |
||||
|
using Avalonia.Media; |
||||
|
using Xunit; |
||||
|
|
||||
|
#if AVALONIA_SKIA
|
||||
|
namespace Avalonia.Skia.RenderTests |
||||
|
#else
|
||||
|
namespace Avalonia.Direct2D1.RenderTests.Media |
||||
|
#endif
|
||||
|
{ |
||||
|
public class CombinedGeometryTests : TestBase |
||||
|
{ |
||||
|
public CombinedGeometryTests() |
||||
|
: base(@"Media\CombinedGeometry") |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(Avalonia.Media.GeometryCombineMode.Union)] |
||||
|
[InlineData(Avalonia.Media.GeometryCombineMode.Intersect)] |
||||
|
[InlineData(Avalonia.Media.GeometryCombineMode.Xor)] |
||||
|
[InlineData(Avalonia.Media.GeometryCombineMode.Exclude)] |
||||
|
public async Task GeometryCombineMode(GeometryCombineMode mode) |
||||
|
{ |
||||
|
var target = new Border |
||||
|
{ |
||||
|
Width = 200, |
||||
|
Height = 200, |
||||
|
Background = Brushes.White, |
||||
|
Child = new Path |
||||
|
{ |
||||
|
Data = new CombinedGeometry |
||||
|
{ |
||||
|
GeometryCombineMode = mode, |
||||
|
Geometry1 = new RectangleGeometry(new Rect(25, 25, 100, 100)), |
||||
|
Geometry2 = new EllipseGeometry |
||||
|
{ |
||||
|
Center = new Point(125, 125), |
||||
|
RadiusX = 50, |
||||
|
RadiusY = 50, |
||||
|
} |
||||
|
}, |
||||
|
Fill = Brushes.Blue, |
||||
|
Stroke = Brushes.Red, |
||||
|
StrokeThickness = 1, |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
var testName = $"{nameof(GeometryCombineMode)}_{mode}"; |
||||
|
await RenderToFile(target, testName); |
||||
|
CompareImages(testName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Geometry1_Transform() |
||||
|
{ |
||||
|
var target = new Border |
||||
|
{ |
||||
|
Width = 200, |
||||
|
Height = 200, |
||||
|
Background = Brushes.White, |
||||
|
Child = new Path |
||||
|
{ |
||||
|
Data = new CombinedGeometry |
||||
|
{ |
||||
|
Geometry1 = new RectangleGeometry(new Rect(25, 25, 100, 100)) |
||||
|
{ |
||||
|
Transform = new RotateTransform(45, 75, 75) |
||||
|
}, |
||||
|
Geometry2 = new EllipseGeometry |
||||
|
{ |
||||
|
Center = new Point(125, 125), |
||||
|
RadiusX = 50, |
||||
|
RadiusY = 50, |
||||
|
} |
||||
|
}, |
||||
|
Fill = Brushes.Blue, |
||||
|
Stroke = Brushes.Red, |
||||
|
StrokeThickness = 1, |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
await RenderToFile(target); |
||||
|
CompareImages(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,95 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Controls.Shapes; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Media.Imaging; |
||||
|
using Xunit; |
||||
|
|
||||
|
#if AVALONIA_SKIA
|
||||
|
namespace Avalonia.Skia.RenderTests |
||||
|
#else
|
||||
|
namespace Avalonia.Direct2D1.RenderTests.Media |
||||
|
#endif
|
||||
|
{ |
||||
|
public class GeometryGroupTests : TestBase |
||||
|
{ |
||||
|
public GeometryGroupTests() |
||||
|
: base(@"Media\GeometryGroup") |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(FillRule.EvenOdd)] |
||||
|
[InlineData(FillRule.NonZero)] |
||||
|
public async Task FillRule_Stroke(FillRule fillRule) |
||||
|
{ |
||||
|
var target = new Border |
||||
|
{ |
||||
|
Width = 200, |
||||
|
Height = 200, |
||||
|
Background = Brushes.White, |
||||
|
Child = new Path |
||||
|
{ |
||||
|
Data = new GeometryGroup |
||||
|
{ |
||||
|
FillRule = fillRule, |
||||
|
Children = |
||||
|
{ |
||||
|
new RectangleGeometry(new Rect(25, 25, 100, 100)), |
||||
|
new EllipseGeometry |
||||
|
{ |
||||
|
Center = new Point(125, 125), |
||||
|
RadiusX = 50, |
||||
|
RadiusY = 50, |
||||
|
}, |
||||
|
} |
||||
|
}, |
||||
|
Fill = Brushes.Blue, |
||||
|
Stroke = Brushes.Red, |
||||
|
StrokeThickness = 1, |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
var testName = $"{nameof(FillRule_Stroke)}_{fillRule}"; |
||||
|
await RenderToFile(target, testName); |
||||
|
CompareImages(testName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Child_Transform() |
||||
|
{ |
||||
|
var target = new Border |
||||
|
{ |
||||
|
Width = 200, |
||||
|
Height = 200, |
||||
|
Background = Brushes.White, |
||||
|
Child = new Path |
||||
|
{ |
||||
|
Data = new GeometryGroup |
||||
|
{ |
||||
|
Children = |
||||
|
{ |
||||
|
new RectangleGeometry(new Rect(25, 25, 100, 100)) |
||||
|
{ |
||||
|
Transform = new RotateTransform(45, 75, 75) |
||||
|
}, |
||||
|
new EllipseGeometry |
||||
|
{ |
||||
|
Center = new Point(125, 125), |
||||
|
RadiusX = 50, |
||||
|
RadiusY = 50, |
||||
|
}, |
||||
|
} |
||||
|
}, |
||||
|
Fill = Brushes.Blue, |
||||
|
Stroke = Brushes.Red, |
||||
|
StrokeThickness = 1, |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
await RenderToFile(target); |
||||
|
CompareImages(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
using Avalonia.Media; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Visuals.UnitTests.Media |
||||
|
{ |
||||
|
public class GeometryGroupTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Children_Should_Have_Initial_Collection() |
||||
|
{ |
||||
|
var target = new GeometryGroup(); |
||||
|
|
||||
|
Assert.NotNull(target.Children); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Children_Can_Be_Set_To_Null() |
||||
|
{ |
||||
|
var target = new GeometryGroup(); |
||||
|
|
||||
|
target.Children = null; |
||||
|
|
||||
|
Assert.Null(target.Children); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.6 KiB |