diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 1ed3896dd3..a5cdeefb0e 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -53,6 +53,7 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register( nameof(PlacementConstraintAdjustment), PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY | + PopupPositionerConstraintAdjustment.SlideX | PopupPositionerConstraintAdjustment.SlideY | PopupPositionerConstraintAdjustment.ResizeX | PopupPositionerConstraintAdjustment.ResizeY); /// diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 268171d467..63cbfb2dbe 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -47,6 +47,8 @@ namespace Avalonia.Headless } public IStreamGeometryImpl CreateStreamGeometry() => new HeadlessStreamingGeometryStub(); + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => throw new NotImplementedException(); + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => throw new NotImplementedException(); public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => new HeadlessRenderTarget(); diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index 39a4c3004c..e3f9f9a070 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -67,6 +67,8 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Double Avaloni InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetPointAndTangentAtDistance(System.Double, Avalonia.Point, Avalonia.Point)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetPointAtDistance(System.Double, Avalonia.Point)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetSegment(System.Double, System.Double, System.Boolean, Avalonia.Platform.IGeometryImpl)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGeometryImpl Avalonia.Platform.IPlatformRenderInterface.CreateCombinedGeometry(Avalonia.Media.GeometryCombineMode, Avalonia.Media.Geometry, Avalonia.Media.Geometry)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGeometryImpl Avalonia.Platform.IPlatformRenderInterface.CreateGeometryGroup(Avalonia.Media.FillRule, System.Collections.Generic.IReadOnlyList)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun, System.Double)' is present in the contract but not in the implementation. MembersMustExist : Member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun, System.Double)' does not exist in the implementation but it does exist in the contract. @@ -74,4 +76,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWr InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmap(System.String)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToHeight(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToWidth(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract. -Total Issues: 75 +Total Issues: 77 diff --git a/src/Avalonia.Visuals/Media/CombinedGeometry.cs b/src/Avalonia.Visuals/Media/CombinedGeometry.cs new file mode 100644 index 0000000000..2202030b7a --- /dev/null +++ b/src/Avalonia.Visuals/Media/CombinedGeometry.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Platform; + +#nullable enable + +namespace Avalonia.Media +{ + public enum GeometryCombineMode + { + /// + /// The two regions are combined by taking the union of both. The resulting geometry is + /// geometry A + geometry B. + /// + Union, + + /// + /// The two regions are combined by taking their intersection. The new area consists of the + /// overlapping region between the two geometries. + /// + Intersect, + + /// + /// 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. + /// + Xor, + + /// + /// 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. + /// + Exclude, + } + + /// + /// Represents a 2-D geometric shape defined by the combination of two Geometry objects. + /// + public class CombinedGeometry : Geometry + { + /// + /// Defines the property. + /// + public static readonly StyledProperty Geometry1Property = + AvaloniaProperty.Register(nameof(Geometry1)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty Geometry2Property = + AvaloniaProperty.Register(nameof(Geometry2)); + /// + /// Defines the property. + /// + public static readonly StyledProperty GeometryCombineModeProperty = + AvaloniaProperty.Register(nameof(GeometryCombineMode)); + + /// + /// Initializes a new instance of the class. + /// + public CombinedGeometry() + { + } + + /// + /// Initializes a new instance of the class with the + /// specified objects. + /// + /// The first geometry to combine. + /// The second geometry to combine. + public CombinedGeometry(Geometry geometry1, Geometry geometry2) + { + Geometry1 = geometry1; + Geometry2 = geometry2; + } + + /// + /// Initializes a new instance of the class with the + /// specified objects and . + /// + /// The method by which geometry1 and geometry2 are combined. + /// The first geometry to combine. + /// The second geometry to combine. + public CombinedGeometry(GeometryCombineMode combineMode, Geometry? geometry1, Geometry? geometry2) + { + Geometry1 = geometry1; + Geometry2 = geometry2; + GeometryCombineMode = combineMode; + } + + /// + /// Initializes a new instance of the class with the + /// specified objects, and + /// . + /// + /// The method by which geometry1 and geometry2 are combined. + /// The first geometry to combine. + /// The second geometry to combine. + /// The transform applied to the geometry. + public CombinedGeometry( + GeometryCombineMode combineMode, + Geometry? geometry1, + Geometry? geometry2, + Transform? transform) + { + Geometry1 = geometry1; + Geometry2 = geometry2; + GeometryCombineMode = combineMode; + Transform = transform; + } + + /// + /// Gets or sets the first object of this + /// object. + /// + public Geometry? Geometry1 + { + get => GetValue(Geometry1Property); + set => SetValue(Geometry1Property, value); + } + + /// + /// Gets or sets the second object of this + /// object. + /// + public Geometry? Geometry2 + { + get => GetValue(Geometry2Property); + set => SetValue(Geometry2Property, value); + } + + /// + /// Gets or sets the method by which the two geometries (specified by the + /// and properties) are combined. The + /// default value is . + /// + 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(); + 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; + } + } +} diff --git a/src/Avalonia.Visuals/Media/GeometryCollection.cs b/src/Avalonia.Visuals/Media/GeometryCollection.cs new file mode 100644 index 0000000000..0bd02d5438 --- /dev/null +++ b/src/Avalonia.Visuals/Media/GeometryCollection.cs @@ -0,0 +1,37 @@ +using System.Collections; +using System.Collections.Generic; +using Avalonia.Animation; + +#nullable enable + +namespace Avalonia.Media +{ + public class GeometryCollection : Animatable, IList, IReadOnlyList + { + private List _inner; + + public GeometryCollection() => _inner = new List(); + public GeometryCollection(IEnumerable collection) => _inner = new List(collection); + public GeometryCollection(int capacity) => _inner = new List(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 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(); + } +} diff --git a/src/Avalonia.Visuals/Media/GeometryGroup.cs b/src/Avalonia.Visuals/Media/GeometryGroup.cs new file mode 100644 index 0000000000..edbe63d4bb --- /dev/null +++ b/src/Avalonia.Visuals/Media/GeometryGroup.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Metadata; +using Avalonia.Platform; + +#nullable enable + +namespace Avalonia.Media +{ + /// + /// Represents a composite geometry, composed of other objects. + /// + public class GeometryGroup : Geometry + { + public static readonly DirectProperty ChildrenProperty = + AvaloniaProperty.RegisterDirect ( + nameof(Children), + o => o.Children, + (o, v) => o.Children = v); + + public static readonly StyledProperty FillRuleProperty = + AvaloniaProperty.Register(nameof(FillRule)); + + private GeometryCollection? _children; + private bool _childrenSet; + + /// + /// Gets or sets the collection that contains the child geometries. + /// + [Content] + public GeometryCollection? Children + { + get => _children ??= (!_childrenSet ? new GeometryCollection() : null); + set + { + SetAndRaise(ChildrenProperty, ref _children, value); + _childrenSet = true; + } + } + + /// + /// Gets or sets how the intersecting areas of the objects contained in this + /// are combined. The default is . + /// + 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(); + return factory.CreateGeometryGroup(FillRule, _children); + } + + return null; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ChildrenProperty || change.Property == FillRuleProperty) + { + InvalidateGeometry(); + } + } + } +} diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index 9fefcb6645..8b9d0833db 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -496,12 +496,18 @@ namespace Avalonia.Media private bool ReadBool(ref ReadOnlySpan span) { - if (!ReadArgument(ref span, out var boolValue) || boolValue.Length != 1) + span = SkipWhitespace(span); + + if (span.IsEmpty) { throw new InvalidDataException("Invalid bool rule."); } - switch (boolValue[0]) + var c = span[0]; + + span = span.Slice(1); + + switch (c) { case '0': return false; diff --git a/src/Avalonia.Visuals/Media/RotateTransform.cs b/src/Avalonia.Visuals/Media/RotateTransform.cs index 653d38eb45..126bb7c274 100644 --- a/src/Avalonia.Visuals/Media/RotateTransform.cs +++ b/src/Avalonia.Visuals/Media/RotateTransform.cs @@ -14,6 +14,18 @@ namespace Avalonia.Media public static readonly StyledProperty AngleProperty = AvaloniaProperty.Register(nameof(Angle)); + /// + /// Defines the property. + /// + public static readonly StyledProperty CenterXProperty = + AvaloniaProperty.Register(nameof(CenterX)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty CenterYProperty = + AvaloniaProperty.Register(nameof(CenterY)); + /// /// Initializes a new instance of the class. /// @@ -32,18 +44,52 @@ namespace Avalonia.Media Angle = angle; } + /// + /// Initializes a new instance of the class. + /// + /// The angle, in degrees. + /// The x-coordinate of the center point for the rotation. + /// The y-coordinate of the center point for the rotation. + public RotateTransform(double angle, double centerX, double centerY) + : this() + { + Angle = angle; + CenterX = centerX; + CenterY = centerY; + } + /// /// Gets or sets the angle of rotation, in degrees. /// public double Angle { - get { return GetValue(AngleProperty); } - set { SetValue(AngleProperty, value); } + get => GetValue(AngleProperty); + set => SetValue(AngleProperty, value); + } + + /// + /// Gets or sets the x-coordinate of the rotation center point. The default is 0. + /// + public double CenterX + { + get => GetValue(CenterXProperty); + set => SetValue(CenterXProperty, value); + } + + /// + /// Gets or sets the y-coordinate of the rotation center point. The default is 0. + /// + public double CenterY + { + get => GetValue(CenterYProperty); + set => SetValue(CenterYProperty, value); } /// /// Gets the transform's . /// - public override Matrix Value => Matrix.CreateRotation(Matrix.ToRadians(Angle)); + public override Matrix Value => Matrix.CreateTranslation(-CenterX, -CenterY) * + Matrix.CreateRotation(Matrix.ToRadians(Angle)) * + Matrix.CreateTranslation(CenterX, CenterY); } } diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index de67aca5a8..772f1ac9f3 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -59,6 +59,23 @@ namespace Avalonia.Platform /// An . IStreamGeometryImpl CreateStreamGeometry(); + /// + /// Creates a geometry group implementation. + /// + /// The fill rule. + /// The geometries to group. + /// A combined geometry. + IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children); + + /// + /// Creates a geometry group implementation. + /// + /// The combine mode + /// The first geometry. + /// The second geometry. + /// A combined geometry. + IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2); + /// /// Creates a renderer. /// diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs b/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs deleted file mode 100644 index 87c7b64c26..0000000000 --- a/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs +++ /dev/null @@ -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; - } -} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevBackBuffer.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevBackBuffer.cs new file mode 100644 index 0000000000..7afad13bb6 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevBackBuffer.cs @@ -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; + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs index 61f00b2795..f3f9a12ac8 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs @@ -14,6 +14,7 @@ namespace Avalonia.LinuxFramebuffer private fb_var_screeninfo _varInfo; private IntPtr _mappedLength; private IntPtr _mappedAddress; + private FbDevBackBuffer _backBuffer; public double Scaling { get; set; } /// @@ -146,7 +147,9 @@ namespace Avalonia.LinuxFramebuffer { if (_fd <= 0) throw new ObjectDisposedException("LinuxFramebuffer"); - return new LockedFramebuffer(_fd, _fixedInfo, _varInfo, _mappedAddress, new Vector(96, 96) * Scaling); + return (_backBuffer ??= + new FbDevBackBuffer(_fd, _fixedInfo, _varInfo, _mappedAddress)) + .Lock(new Vector(96, 96) * Scaling); } @@ -165,6 +168,8 @@ namespace Avalonia.LinuxFramebuffer public void Dispose() { + _backBuffer?.Dispose(); + _backBuffer = null; ReleaseUnmanagedResources(); GC.SuppressFinalize(this); } diff --git a/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs b/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs new file mode 100644 index 0000000000..40d7e10ae3 --- /dev/null +++ b/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Avalonia.Media; +using SkiaSharp; + +#nullable enable + +namespace Avalonia.Skia +{ + /// + /// A Skia implementation of a . + /// + 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; } + } +} diff --git a/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs new file mode 100644 index 0000000000..d6f19612c1 --- /dev/null +++ b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using Avalonia.Media; +using SkiaSharp; + +#nullable enable + +namespace Avalonia.Skia +{ + /// + /// A Skia implementation of a . + /// + internal class GeometryGroupImpl : GeometryImpl + { + public GeometryGroupImpl(FillRule fillRule, IReadOnlyList 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; } + } +} diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 7bc83ec85b..e2175f1145 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -62,6 +62,16 @@ namespace Avalonia.Skia return new StreamGeometryImpl(); } + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + { + return new GeometryGroupImpl(fillRule, children); + } + + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + { + return new CombinedGeometryImpl(combineMode, g1, g2); + } + /// public IBitmapImpl LoadBitmap(string fileName) { diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index f50167b39a..eef4416101 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -175,6 +175,8 @@ namespace Avalonia.Direct2D1 public IGeometryImpl CreateLineGeometry(Point p1, Point p2) => new LineGeometryImpl(p1, p2); public IGeometryImpl CreateRectangleGeometry(Rect rect) => new RectangleGeometryImpl(rect); public IStreamGeometryImpl CreateStreamGeometry() => new StreamGeometryImpl(); + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children); + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2); /// public IBitmapImpl LoadBitmap(string fileName) diff --git a/src/Windows/Avalonia.Direct2D1/Media/CombinedGeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/CombinedGeometryImpl.cs new file mode 100644 index 0000000000..5a13c10bbc --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/CombinedGeometryImpl.cs @@ -0,0 +1,36 @@ +using SharpDX.Direct2D1; +using AM = Avalonia.Media; + +namespace Avalonia.Direct2D1.Media +{ + /// + /// A Direct2D implementation of a . + /// + internal class CombinedGeometryImpl : GeometryImpl + { + /// + /// Initializes a new instance of the class. + /// + 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; + } + } +} diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs new file mode 100644 index 0000000000..352708bf03 --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using SharpDX.Direct2D1; +using AM = Avalonia.Media; + +namespace Avalonia.Direct2D1.Media +{ + /// + /// A Direct2D implementation of a . + /// + internal class GeometryGroupImpl : GeometryImpl + { + /// + /// Initializes a new instance of the class. + /// + public GeometryGroupImpl(AM.FillRule fillRule, IReadOnlyList geometry) + : base(CreateGeometry(fillRule, geometry)) + { + } + + private static Geometry CreateGeometry(AM.FillRule fillRule, IReadOnlyList 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); + } + } +} diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index 876a0de643..3e11c74e1c 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -36,6 +36,16 @@ namespace Avalonia.Benchmarks return new MockStreamGeometryImpl(); } + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + { + throw new NotImplementedException(); + } + + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + { + throw new NotImplementedException(); + } + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) { throw new NotImplementedException(); diff --git a/tests/Avalonia.RenderTests/Media/CombinedGeometryTests.cs b/tests/Avalonia.RenderTests/Media/CombinedGeometryTests.cs new file mode 100644 index 0000000000..9c5c0248cf --- /dev/null +++ b/tests/Avalonia.RenderTests/Media/CombinedGeometryTests.cs @@ -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(); + } + } +} diff --git a/tests/Avalonia.RenderTests/Media/GeometryGroupTests.cs b/tests/Avalonia.RenderTests/Media/GeometryGroupTests.cs new file mode 100644 index 0000000000..9ebbd30e05 --- /dev/null +++ b/tests/Avalonia.RenderTests/Media/GeometryGroupTests.cs @@ -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(); + } + } +} diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index 74366f9e26..1f632034be 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -52,6 +52,16 @@ namespace Avalonia.UnitTests return new MockStreamGeometryImpl(); } + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + { + return Mock.Of(); + } + + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + { + return Mock.Of(); + } + public IWriteableBitmapImpl CreateWriteableBitmap( PixelSize size, Vector dpi, diff --git a/tests/Avalonia.Visuals.UnitTests/Media/GeometryGroupTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/GeometryGroupTests.cs new file mode 100644 index 0000000000..8f80238903 --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Media/GeometryGroupTests.cs @@ -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); + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs index c5ad705654..ba8c490829 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs @@ -297,5 +297,28 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.Equal(new Point(20, 20), figure.StartPoint); } } + + [Fact] + public void Should_Parse_Flags_Without_Separator() + { + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) + { + parser.Parse("a.898.898 0 01.27.188"); + + var figure = pathGeometry.Figures[0]; + + var segments = figure.Segments; + + Assert.NotNull(segments); + + Assert.Equal(1, segments.Count); + + var arcSegment = segments[0]; + + Assert.IsType(arcSegment); + } + } } } diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index 51ea1e893f..229bb8aef3 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -37,6 +37,16 @@ namespace Avalonia.Visuals.UnitTests.VisualTree return new MockStreamGeometry(); } + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + { + throw new NotImplementedException(); + } + + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + { + throw new NotImplementedException(); + } + public IBitmapImpl LoadBitmap(Stream stream) { throw new NotImplementedException(); diff --git a/tests/TestFiles/Direct2D1/Media/CombinedGeometry/Geometry1_Transform.expected.png b/tests/TestFiles/Direct2D1/Media/CombinedGeometry/Geometry1_Transform.expected.png new file mode 100644 index 0000000000..34976f3de6 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/CombinedGeometry/Geometry1_Transform.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Exclude.expected.png b/tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Exclude.expected.png new file mode 100644 index 0000000000..2c4aa99eeb Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Exclude.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Intersect.expected.png b/tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Intersect.expected.png new file mode 100644 index 0000000000..abb3cea270 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Intersect.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Union.expected.png b/tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Union.expected.png new file mode 100644 index 0000000000..f431ef4403 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Union.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Xor.expected.png b/tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Xor.expected.png new file mode 100644 index 0000000000..4dd547c4bf Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Xor.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/GeometryGroup/Child_Transform.expected.png b/tests/TestFiles/Direct2D1/Media/GeometryGroup/Child_Transform.expected.png new file mode 100644 index 0000000000..d8e4c54924 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/GeometryGroup/Child_Transform.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_Stroke_EvenOdd.expected.png b/tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_Stroke_EvenOdd.expected.png new file mode 100644 index 0000000000..4dd547c4bf Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_Stroke_EvenOdd.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_Stroke_NonZero.expected.png b/tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_Stroke_NonZero.expected.png new file mode 100644 index 0000000000..3ab700dc04 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_Stroke_NonZero.expected.png differ diff --git a/tests/TestFiles/Skia/Media/CombinedGeometry/Geometry1_Transform.expected.png b/tests/TestFiles/Skia/Media/CombinedGeometry/Geometry1_Transform.expected.png new file mode 100644 index 0000000000..2b98a79049 Binary files /dev/null and b/tests/TestFiles/Skia/Media/CombinedGeometry/Geometry1_Transform.expected.png differ diff --git a/tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Exclude.expected.png b/tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Exclude.expected.png new file mode 100644 index 0000000000..2c4aa99eeb Binary files /dev/null and b/tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Exclude.expected.png differ diff --git a/tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Intersect.expected.png b/tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Intersect.expected.png new file mode 100644 index 0000000000..abb3cea270 Binary files /dev/null and b/tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Intersect.expected.png differ diff --git a/tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Union.expected.png b/tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Union.expected.png new file mode 100644 index 0000000000..f431ef4403 Binary files /dev/null and b/tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Union.expected.png differ diff --git a/tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Xor.expected.png b/tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Xor.expected.png new file mode 100644 index 0000000000..4dd547c4bf Binary files /dev/null and b/tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Xor.expected.png differ diff --git a/tests/TestFiles/Skia/Media/GeometryGroup/Child_Transform.expected.png b/tests/TestFiles/Skia/Media/GeometryGroup/Child_Transform.expected.png new file mode 100644 index 0000000000..7182602fce Binary files /dev/null and b/tests/TestFiles/Skia/Media/GeometryGroup/Child_Transform.expected.png differ diff --git a/tests/TestFiles/Skia/Media/GeometryGroup/FillRule_Stroke_EvenOdd.expected.png b/tests/TestFiles/Skia/Media/GeometryGroup/FillRule_Stroke_EvenOdd.expected.png new file mode 100644 index 0000000000..80b91d1209 Binary files /dev/null and b/tests/TestFiles/Skia/Media/GeometryGroup/FillRule_Stroke_EvenOdd.expected.png differ diff --git a/tests/TestFiles/Skia/Media/GeometryGroup/FillRule_Stroke_NonZero.expected.png b/tests/TestFiles/Skia/Media/GeometryGroup/FillRule_Stroke_NonZero.expected.png new file mode 100644 index 0000000000..a101525cb3 Binary files /dev/null and b/tests/TestFiles/Skia/Media/GeometryGroup/FillRule_Stroke_NonZero.expected.png differ