diff --git a/src/Avalonia.HtmlRenderer/Adapters/AvaloniaAdapter.cs b/src/Avalonia.HtmlRenderer/Adapters/AvaloniaAdapter.cs index 12c040fab3..aab65fcbc7 100644 --- a/src/Avalonia.HtmlRenderer/Adapters/AvaloniaAdapter.cs +++ b/src/Avalonia.HtmlRenderer/Adapters/AvaloniaAdapter.cs @@ -80,7 +80,7 @@ namespace TheArtOfDev.HtmlRenderer.Avalonia.Adapters { StartPoint = new RelativePoint(x, y, RelativeUnit.Relative), EndPoint = new RelativePoint(1 - x, 1 - y, RelativeUnit.Relative), - GradientStops = + GradientStops = new[] { new GradientStop(startColor, 0), new GradientStop(endColor, 1) diff --git a/src/Avalonia.Visuals/Avalonia.Visuals.csproj b/src/Avalonia.Visuals/Avalonia.Visuals.csproj index 57393e5448..9c3830769b 100644 --- a/src/Avalonia.Visuals/Avalonia.Visuals.csproj +++ b/src/Avalonia.Visuals/Avalonia.Visuals.csproj @@ -181,7 +181,7 @@ - + diff --git a/src/Avalonia.Visuals/Media/GradientBrush.cs b/src/Avalonia.Visuals/Media/GradientBrush.cs index 9059df73d7..2ea3c78064 100644 --- a/src/Avalonia.Visuals/Media/GradientBrush.cs +++ b/src/Avalonia.Visuals/Media/GradientBrush.cs @@ -7,28 +7,46 @@ using Avalonia.Metadata; namespace Avalonia.Media { + /// + /// Base class for brushes that draw with a gradient. + /// public abstract class GradientBrush : Brush, IGradientBrush { + /// + /// Defines the property. + /// public static readonly StyledProperty SpreadMethodProperty = AvaloniaProperty.Register(nameof(SpreadMethod)); - public static readonly StyledProperty> GradientStopsProperty = - AvaloniaProperty.Register>(nameof(Opacity)); + /// + /// Defines the property. + /// + public static readonly StyledProperty> GradientStopsProperty = + AvaloniaProperty.Register>(nameof(Opacity)); + /// + /// Initializes a new instance of the class. + /// public GradientBrush() { this.GradientStops = new List(); } + /// + /// Gets or sets the brush's spread method that defines how to draw a gradient that + /// doesn't fill the bounds of the destination control. + /// public GradientSpreadMethod SpreadMethod { get { return GetValue(SpreadMethodProperty); } set { SetValue(SpreadMethodProperty, value); } } - // TODO: We shouldn't be returning a concrete List<> here + /// + /// Gets or sets the brush's gradient stops. + /// [Content] - public List GradientStops + public IReadOnlyList GradientStops { get { return GetValue(GradientStopsProperty); } set { SetValue(GradientStopsProperty, value); } diff --git a/src/Avalonia.Visuals/Media/IGradientBrush.cs b/src/Avalonia.Visuals/Media/IGradientBrush.cs index a9079ecfda..ce064c4a1f 100644 --- a/src/Avalonia.Visuals/Media/IGradientBrush.cs +++ b/src/Avalonia.Visuals/Media/IGradientBrush.cs @@ -3,7 +3,7 @@ namespace Avalonia.Media { /// - /// A brush that draws with a linear gradient. + /// A brush that draws with a gradient. /// public interface IGradientBrush : IBrush { diff --git a/src/Avalonia.Visuals/Media/IRadialGradientBrush.cs b/src/Avalonia.Visuals/Media/IRadialGradientBrush.cs index a9d902dabe..cadf53cc18 100644 --- a/src/Avalonia.Visuals/Media/IRadialGradientBrush.cs +++ b/src/Avalonia.Visuals/Media/IRadialGradientBrush.cs @@ -11,7 +11,8 @@ RelativePoint Center { get; } /// - /// Gets the location of the two-dimensional focal point that defines the beginning of the gradient. + /// Gets the location of the two-dimensional focal point that defines the beginning of the + /// gradient. /// RelativePoint GradientOrigin { get; } diff --git a/src/Avalonia.Visuals/Media/ImageBush.cs b/src/Avalonia.Visuals/Media/ImageBrush.cs similarity index 100% rename from src/Avalonia.Visuals/Media/ImageBush.cs rename to src/Avalonia.Visuals/Media/ImageBrush.cs diff --git a/src/Avalonia.Visuals/Media/Imaging/IImageBrush.cs b/src/Avalonia.Visuals/Media/Imaging/IImageBrush.cs new file mode 100644 index 0000000000..aaa481bd28 --- /dev/null +++ b/src/Avalonia.Visuals/Media/Imaging/IImageBrush.cs @@ -0,0 +1,15 @@ +using Avalonia.Media.Imaging; + +namespace Avalonia.Media +{ + /// + /// Paints an area with an . + /// + public interface IImageBrush : ITileBrush + { + /// + /// Gets the image to draw. + /// + IBitmap Source { get; } + } +} \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/RadialGradientBrush.cs b/src/Avalonia.Visuals/Media/RadialGradientBrush.cs index a78cd86afe..203b5767df 100644 --- a/src/Avalonia.Visuals/Media/RadialGradientBrush.cs +++ b/src/Avalonia.Visuals/Media/RadialGradientBrush.cs @@ -44,7 +44,8 @@ namespace Avalonia.Media } /// - /// Gets or sets the location of the two-dimensional focal point that defines the beginning of the gradient. + /// Gets or sets the location of the two-dimensional focal point that defines the beginning + /// of the gradient. /// public RelativePoint GradientOrigin { @@ -53,7 +54,8 @@ namespace Avalonia.Media } /// - /// Gets or sets the horizontal and vertical radius of the outermost circle of the radial gradient. + /// Gets or sets the horizontal and vertical radius of the outermost circle of the radial + /// gradient. /// // TODO: This appears to always be relative so should use a RelativeSize struct or something. public double Radius diff --git a/src/Gtk/Avalonia.Cairo/Media/ImageBrushImpl.cs b/src/Gtk/Avalonia.Cairo/Media/ImageBrushImpl.cs index 4e037ce210..94aa4e6b25 100644 --- a/src/Gtk/Avalonia.Cairo/Media/ImageBrushImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/ImageBrushImpl.cs @@ -4,6 +4,7 @@ using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering.Utilities; using Gdk; +using Avalonia.Media; using global::Cairo; namespace Avalonia.Cairo.Media diff --git a/src/Gtk/Avalonia.Cairo/Media/LinearGradientBrushImpl.cs b/src/Gtk/Avalonia.Cairo/Media/LinearGradientBrushImpl.cs index 5354d4899a..1e82da4059 100644 --- a/src/Gtk/Avalonia.Cairo/Media/LinearGradientBrushImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/LinearGradientBrushImpl.cs @@ -1,11 +1,12 @@ using System; +using Avalonia.Media; using global::Cairo; namespace Avalonia.Cairo { public class LinearGradientBrushImpl : BrushImpl { - public LinearGradientBrushImpl(Avalonia.Media.ILinearGradientBrush brush, Size destinationSize) + public LinearGradientBrushImpl(ILinearGradientBrush brush, Size destinationSize) { var start = brush.StartPoint.ToPixels(destinationSize); var end = brush.EndPoint.ToPixels(destinationSize); diff --git a/src/Gtk/Avalonia.Cairo/Media/RadialGradientBrushImpl.cs b/src/Gtk/Avalonia.Cairo/Media/RadialGradientBrushImpl.cs index 51d028b935..03385f8d86 100644 --- a/src/Gtk/Avalonia.Cairo/Media/RadialGradientBrushImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/RadialGradientBrushImpl.cs @@ -1,11 +1,12 @@ using System; +using Avalonia.Media; using global::Cairo; namespace Avalonia.Cairo { public class RadialGradientBrushImpl : BrushImpl { - public RadialGradientBrushImpl(Avalonia.Media.IRadialGradientBrush brush, Size destinationSize) + public RadialGradientBrushImpl(IRadialGradientBrush brush, Size destinationSize) { var center = brush.Center.ToPixels(destinationSize); var gradientOrigin = brush.GradientOrigin.ToPixels(destinationSize); diff --git a/src/Gtk/Avalonia.Cairo/Media/TileBrushes.cs b/src/Gtk/Avalonia.Cairo/Media/TileBrushes.cs new file mode 100644 index 0000000000..eb78c9d2f3 --- /dev/null +++ b/src/Gtk/Avalonia.Cairo/Media/TileBrushes.cs @@ -0,0 +1,55 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Cairo; +using Avalonia.Cairo.Media.Imaging; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.RenderHelpers; + +namespace Avalonia.Cairo.Media +{ + internal static class TileBrushes + { + public static SurfacePattern CreateTileBrush(ITileBrush brush, Size targetSize) + { + var helper = new TileBrushImplHelper(brush, targetSize); + if (!helper.IsValid) + return null; + + using (var intermediate = new ImageSurface(Format.ARGB32, (int)helper.IntermediateSize.Width, (int)helper.IntermediateSize.Height)) + using (var ctx = new RenderTarget(intermediate).CreateDrawingContext()) + { + helper.DrawIntermediate(ctx); + + var result = new SurfacePattern(intermediate); + + if ((brush.TileMode & TileMode.FlipXY) != 0) + { + // TODO: Currently always FlipXY as that's all cairo supports natively. + // Support separate FlipX and FlipY by drawing flipped images to intermediate + // surface. + result.Extend = Extend.Reflect; + } + else + { + result.Extend = Extend.Repeat; + } + + if (brush.TileMode != TileMode.None) + { + var matrix = result.Matrix; + matrix.InitTranslate(-helper.DestinationRect.X, -helper.DestinationRect.Y); + result.Matrix = matrix; + } + + return result; + } + } + + + } +} diff --git a/src/Gtk/Avalonia.Cairo/Media/VisualBrushImpl.cs b/src/Gtk/Avalonia.Cairo/Media/VisualBrushImpl.cs new file mode 100644 index 0000000000..e820c50420 --- /dev/null +++ b/src/Gtk/Avalonia.Cairo/Media/VisualBrushImpl.cs @@ -0,0 +1,15 @@ +using System; +using Avalonia.Media; +using global::Cairo; + +namespace Avalonia.Cairo.Media +{ + public class VisualBrushImpl : BrushImpl + { + public VisualBrushImpl(IVisualBrush brush, Size destinationSize) + { + this.PlatformBrush = TileBrushes.CreateTileBrush(brush, destinationSize); + } + } +} + diff --git a/src/Shared/RenderHelpers/TileBrushImplHelper.cs b/src/Shared/RenderHelpers/TileBrushImplHelper.cs index d1d3073be6..b6d09101c9 100644 --- a/src/Shared/RenderHelpers/TileBrushImplHelper.cs +++ b/src/Shared/RenderHelpers/TileBrushImplHelper.cs @@ -105,7 +105,7 @@ namespace Avalonia.RenderHelpers /// - /// Calculates a translate based on a , a source and destination + /// Calculates a translate based on an , a source and destination /// rectangle and a scale. /// /// The brush. diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 26207c23d2..216f94dc3e 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -341,11 +341,11 @@ namespace Avalonia.Direct2D1.Media /// The Direct2D brush wrapper. public BrushImpl CreateBrush(IBrush brush, Size destinationSize) { - var solidColorBrush = brush as Avalonia.Media.ISolidColorBrush; - var linearGradientBrush = brush as Avalonia.Media.ILinearGradientBrush; - var radialGradientBrush = brush as Avalonia.Media.IRadialGradientBrush; - var imageBrush = brush as Avalonia.Media.IImageBrush; - var visualBrush = brush as Avalonia.Media.IVisualBrush; + var solidColorBrush = brush as ISolidColorBrush; + var linearGradientBrush = brush as ILinearGradientBrush; + var radialGradientBrush = brush as IRadialGradientBrush; + var imageBrush = brush as IImageBrush; + var visualBrush = brush as IVisualBrush; if (solidColorBrush != null) { diff --git a/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs index 2eb585e6ef..5369f84f46 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs @@ -2,13 +2,14 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Linq; +using Avalonia.Media; namespace Avalonia.Direct2D1.Media { public class LinearGradientBrushImpl : BrushImpl { public LinearGradientBrushImpl( - Avalonia.Media.ILinearGradientBrush brush, + ILinearGradientBrush brush, SharpDX.Direct2D1.RenderTarget target, Size destinationSize) { diff --git a/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs index be972c0173..45932a4760 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs @@ -2,13 +2,14 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Linq; +using Avalonia.Media; namespace Avalonia.Direct2D1.Media { public class RadialGradientBrushImpl : BrushImpl { public RadialGradientBrushImpl( - Avalonia.Media.IRadialGradientBrush brush, + IRadialGradientBrush brush, SharpDX.Direct2D1.RenderTarget target, Size destinationSize) { diff --git a/src/Windows/Avalonia.Direct2D1/Media/TileBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TileBrushImpl.cs new file mode 100644 index 0000000000..b0c4a2c83b --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/TileBrushImpl.cs @@ -0,0 +1,78 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Media; +using Avalonia.RenderHelpers; +using SharpDX.Direct2D1; + +namespace Avalonia.Direct2D1.Media +{ + public sealed class TileBrushImpl : BrushImpl + { + public TileBrushImpl( + ITileBrush brush, + SharpDX.Direct2D1.RenderTarget target, + Size targetSize) + { + var helper = new TileBrushImplHelper(brush, targetSize); + if (!helper.IsValid) + return; + + using (var intermediate = new BitmapRenderTarget(target, CompatibleRenderTargetOptions.None, helper.IntermediateSize.ToSharpDX())) + { + using (var ctx = new RenderTarget(intermediate).CreateDrawingContext()) + { + intermediate.Clear(null); + helper.DrawIntermediate(ctx); + } + + PlatformBrush = new BitmapBrush( + target, + intermediate.Bitmap, + GetBitmapBrushProperties(brush), + GetBrushProperties(brush, helper.DestinationRect)); + } + } + + private static BrushProperties GetBrushProperties(ITileBrush brush, Rect destinationRect) + { + var tileTransform = + brush.TileMode != TileMode.None ? + Matrix.CreateTranslation(destinationRect.X, destinationRect.Y) : + Matrix.Identity; + + return new BrushProperties + { + Opacity = (float)brush.Opacity, + Transform = tileTransform.ToDirect2D(), + }; + } + + private static BitmapBrushProperties GetBitmapBrushProperties(ITileBrush brush) + { + var tileMode = brush.TileMode; + + return new BitmapBrushProperties + { + ExtendModeX = GetExtendModeX(tileMode), + ExtendModeY = GetExtendModeY(tileMode), + }; + } + + private static ExtendMode GetExtendModeX(TileMode tileMode) + { + return (tileMode & TileMode.FlipX) != 0 ? ExtendMode.Mirror : ExtendMode.Wrap; + } + + private static ExtendMode GetExtendModeY(TileMode tileMode) + { + return (tileMode & TileMode.FlipY) != 0 ? ExtendMode.Mirror : ExtendMode.Wrap; + } + + public override void Dispose() + { + ((BitmapBrush)PlatformBrush)?.Bitmap.Dispose(); + base.Dispose(); + } + } +} diff --git a/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs index 776910ca5a..e7889672b9 100644 --- a/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs @@ -42,7 +42,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media { StartPoint = new RelativePoint(0, 0.5, RelativeUnit.Relative), EndPoint = new RelativePoint(1, 0.5, RelativeUnit.Relative), - GradientStops = + GradientStops = new[] { new GradientStop { Color = Colors.Red, Offset = 0 }, new GradientStop { Color = Colors.Blue, Offset = 1 } @@ -73,7 +73,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media { StartPoint = new RelativePoint(0.5, 0, RelativeUnit.Relative), EndPoint = new RelativePoint(0.5, 1, RelativeUnit.Relative), - GradientStops = + GradientStops = new[] { new GradientStop { Color = Colors.Red, Offset = 0 }, new GradientStop { Color = Colors.Blue, Offset = 1 } diff --git a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs index af4c17b328..383c8ee0f8 100644 --- a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs @@ -40,7 +40,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media { Background = new RadialGradientBrush { - GradientStops = + GradientStops = new[] { new GradientStop { Color = Colors.Red, Offset = 0 }, new GradientStop { Color = Colors.Blue, Offset = 1 }