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