diff --git a/src/Avalonia.Visuals/Media/Brush.cs b/src/Avalonia.Visuals/Media/Brush.cs
index eef6e1a43c..8ba7c1be04 100644
--- a/src/Avalonia.Visuals/Media/Brush.cs
+++ b/src/Avalonia.Visuals/Media/Brush.cs
@@ -10,7 +10,7 @@ namespace Avalonia.Media
/// Describes how an area is painted.
///
[TypeConverter(typeof(BrushConverter))]
- public abstract class Brush : AvaloniaObject, IBrush
+ public abstract class Brush : AvaloniaObject, IMutableBrush
{
///
/// Defines the property.
@@ -18,6 +18,9 @@ namespace Avalonia.Media
public static readonly StyledProperty OpacityProperty =
AvaloniaProperty.Register(nameof(Opacity), 1.0);
+ ///
+ public event EventHandler Changed;
+
///
/// Gets or sets the opacity of the brush.
///
@@ -50,5 +53,36 @@ namespace Avalonia.Media
throw new FormatException($"Invalid brush string: '{s}'.");
}
+
+ ///
+ public abstract IBrush ToImmutable();
+
+ ///
+ /// Marks a property as affecting the brush's visual representation.
+ ///
+ /// The properties.
+ ///
+ /// After a call to this method in a brush's static constructor, any change to the
+ /// property will cause the event to be raised on the brush.
+ ///
+ protected static void AffectsRender(params AvaloniaProperty[] properties)
+ where T : Brush
+ {
+ void Invalidate(AvaloniaPropertyChangedEventArgs e)
+ {
+ (e.Sender as T)?.RaiseChanged(EventArgs.Empty);
+ }
+
+ foreach (var property in properties)
+ {
+ property.Changed.Subscribe(Invalidate);
+ }
+ }
+
+ ///
+ /// Raises the event.
+ ///
+ /// The event args.
+ protected void RaiseChanged(EventArgs e) => Changed?.Invoke(this, e);
}
}
diff --git a/src/Avalonia.Visuals/Media/GradientBrush.cs b/src/Avalonia.Visuals/Media/GradientBrush.cs
index 41c3afc8c3..c123813cee 100644
--- a/src/Avalonia.Visuals/Media/GradientBrush.cs
+++ b/src/Avalonia.Visuals/Media/GradientBrush.cs
@@ -1,7 +1,11 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using System;
using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using Avalonia.Collections;
using Avalonia.Metadata;
namespace Avalonia.Media
@@ -20,35 +24,74 @@ namespace Avalonia.Media
///
/// Defines the property.
///
- public static readonly StyledProperty> GradientStopsProperty =
- AvaloniaProperty.Register>(nameof(GradientStops));
+ public static readonly StyledProperty GradientStopsProperty =
+ AvaloniaProperty.Register(nameof(GradientStops));
+
+ private IDisposable _gradientStopsSubscription;
+
+ static GradientBrush()
+ {
+ GradientStopsProperty.Changed.Subscribe(GradientStopsChanged);
+ AffectsRender(SpreadMethodProperty);
+ }
///
/// Initializes a new instance of the class.
///
public GradientBrush()
{
- this.GradientStops = new List();
+ this.GradientStops = new GradientStops();
}
- ///
- /// 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); }
}
- ///
- /// Gets or sets the brush's gradient stops.
- ///
+ ///
[Content]
- public IList GradientStops
+ public GradientStops GradientStops
{
get { return GetValue(GradientStopsProperty); }
set { SetValue(GradientStopsProperty, value); }
}
+
+ ///
+ IReadOnlyList IGradientBrush.GradientStops => GradientStops;
+
+ private static void GradientStopsChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Sender is GradientBrush brush)
+ {
+ var oldValue = (GradientStops)e.OldValue;
+ var newValue = (GradientStops)e.NewValue;
+
+ if (oldValue != null)
+ {
+ oldValue.CollectionChanged -= brush.GradientStopsChanged;
+ brush._gradientStopsSubscription.Dispose();
+ }
+
+ if (newValue != null)
+ {
+ newValue.CollectionChanged += brush.GradientStopsChanged;
+ brush._gradientStopsSubscription = newValue.TrackItemPropertyChanged(brush.GradientStopChanged);
+ }
+
+ brush.RaiseChanged(EventArgs.Empty);
+ }
+ }
+
+ private void GradientStopsChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ RaiseChanged(EventArgs.Empty);
+ }
+
+ private void GradientStopChanged(Tuple
/// The brush from which this brush's properties should be copied.
- public ImmutableLinearGradientBrush(ILinearGradientBrush source)
+ public ImmutableLinearGradientBrush(LinearGradientBrush source)
: base(source)
{
StartPoint = source.StartPoint;
diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableRadialGradientBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableRadialGradientBrush.cs
index f36a1cd2de..e26fbab5f5 100644
--- a/src/Avalonia.Visuals/Media/Immutable/ImmutableRadialGradientBrush.cs
+++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableRadialGradientBrush.cs
@@ -21,7 +21,7 @@ namespace Avalonia.Media.Immutable
/// The horizontal and vertical radius of the outermost circle of the radial gradient.
///
public ImmutableRadialGradientBrush(
- IList gradientStops,
+ IReadOnlyList gradientStops,
double opacity = 1,
GradientSpreadMethod spreadMethod = GradientSpreadMethod.Pad,
RelativePoint? center = null,
@@ -38,7 +38,7 @@ namespace Avalonia.Media.Immutable
/// Initializes a new instance of the class.
///
/// The brush from which this brush's properties should be copied.
- public ImmutableRadialGradientBrush(IRadialGradientBrush source)
+ public ImmutableRadialGradientBrush(RadialGradientBrush source)
: base(source)
{
Center = source.Center;
diff --git a/src/Avalonia.Visuals/Media/LinearGradientBrush.cs b/src/Avalonia.Visuals/Media/LinearGradientBrush.cs
index d092bebf0f..14adc0e0cd 100644
--- a/src/Avalonia.Visuals/Media/LinearGradientBrush.cs
+++ b/src/Avalonia.Visuals/Media/LinearGradientBrush.cs
@@ -1,12 +1,14 @@
// 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.Immutable;
+
namespace Avalonia.Media
{
///
/// A brush that draws with a linear gradient.
///
- public sealed class LinearGradientBrush : GradientBrush, ILinearGradientBrush, IMutableBrush
+ public sealed class LinearGradientBrush : GradientBrush, ILinearGradientBrush
{
///
/// Defines the property.
@@ -24,6 +26,11 @@ namespace Avalonia.Media
nameof(EndPoint),
RelativePoint.BottomRight);
+ static LinearGradientBrush()
+ {
+ AffectsRender(StartPointProperty, EndPointProperty);
+ }
+
///
/// Gets or sets the start point for the gradient.
///
@@ -43,9 +50,9 @@ namespace Avalonia.Media
}
///
- IBrush IMutableBrush.ToImmutable()
+ public override IBrush ToImmutable()
{
- return new Immutable.ImmutableLinearGradientBrush(this);
+ return new ImmutableLinearGradientBrush(this);
}
}
}
diff --git a/src/Avalonia.Visuals/Media/RadialGradientBrush.cs b/src/Avalonia.Visuals/Media/RadialGradientBrush.cs
index 003e2e05f9..589cd83ca1 100644
--- a/src/Avalonia.Visuals/Media/RadialGradientBrush.cs
+++ b/src/Avalonia.Visuals/Media/RadialGradientBrush.cs
@@ -1,12 +1,14 @@
// 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.Immutable;
+
namespace Avalonia.Media
{
///
/// Paints an area with a radial gradient.
///
- public sealed class RadialGradientBrush : GradientBrush, IRadialGradientBrush, IMutableBrush
+ public sealed class RadialGradientBrush : GradientBrush, IRadialGradientBrush
{
///
/// Defines the property.
@@ -63,9 +65,9 @@ namespace Avalonia.Media
}
///
- IBrush IMutableBrush.ToImmutable()
+ public override IBrush ToImmutable()
{
- return new Immutable.ImmutableRadialGradientBrush(this);
+ return new ImmutableRadialGradientBrush(this);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Visuals/Media/SolidColorBrush.cs b/src/Avalonia.Visuals/Media/SolidColorBrush.cs
index d84e407cb4..32b87df56b 100644
--- a/src/Avalonia.Visuals/Media/SolidColorBrush.cs
+++ b/src/Avalonia.Visuals/Media/SolidColorBrush.cs
@@ -1,12 +1,14 @@
// 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.Immutable;
+
namespace Avalonia.Media
{
///
/// Fills an area with a solid color.
///
- public class SolidColorBrush : Brush, ISolidColorBrush, IMutableBrush
+ public class SolidColorBrush : Brush, ISolidColorBrush
{
///
/// Defines the property.
@@ -14,6 +16,11 @@ namespace Avalonia.Media
public static readonly StyledProperty ColorProperty =
AvaloniaProperty.Register(nameof(Color));
+ static SolidColorBrush()
+ {
+ AffectsRender(ColorProperty);
+ }
+
///
/// Initializes a new instance of the class.
///
@@ -75,9 +82,9 @@ namespace Avalonia.Media
}
///
- IBrush IMutableBrush.ToImmutable()
+ public override IBrush ToImmutable()
{
- return new Immutable.ImmutableSolidColorBrush(this);
+ return new ImmutableSolidColorBrush(this);
}
}
}
diff --git a/src/Avalonia.Visuals/Media/TileBrush.cs b/src/Avalonia.Visuals/Media/TileBrush.cs
index 2033754137..47f20fa285 100644
--- a/src/Avalonia.Visuals/Media/TileBrush.cs
+++ b/src/Avalonia.Visuals/Media/TileBrush.cs
@@ -79,6 +79,13 @@ namespace Avalonia.Media
static TileBrush()
{
+ AffectsRender(
+ AlignmentXProperty,
+ AlignmentYProperty,
+ DestinationRectProperty,
+ SourceRectProperty,
+ StretchProperty,
+ TileModeProperty);
RenderOptions.BitmapInterpolationModeProperty.OverrideDefaultValue(BitmapInterpolationMode.Default);
}
diff --git a/src/Avalonia.Visuals/Media/VisualBrush.cs b/src/Avalonia.Visuals/Media/VisualBrush.cs
index 435f4ba1b1..963ba8f4a1 100644
--- a/src/Avalonia.Visuals/Media/VisualBrush.cs
+++ b/src/Avalonia.Visuals/Media/VisualBrush.cs
@@ -1,6 +1,7 @@
// 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.Immutable;
using Avalonia.VisualTree;
namespace Avalonia.Media
@@ -8,7 +9,7 @@ namespace Avalonia.Media
///
/// Paints an area with an .
///
- public class VisualBrush : TileBrush, IVisualBrush, IMutableBrush
+ public class VisualBrush : TileBrush, IVisualBrush
{
///
/// Defines the property.
@@ -16,6 +17,11 @@ namespace Avalonia.Media
public static readonly StyledProperty VisualProperty =
AvaloniaProperty.Register(nameof(Visual));
+ static VisualBrush()
+ {
+ AffectsRender(VisualProperty);
+ }
+
///
/// Initializes a new instance of the class.
///
@@ -42,9 +48,9 @@ namespace Avalonia.Media
}
///
- IBrush IMutableBrush.ToImmutable()
+ public override IBrush ToImmutable()
{
- return new Immutable.ImmutableVisualBrush(this);
+ return new ImmutableVisualBrush(this);
}
}
}
diff --git a/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs b/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs
index 8356e78cc3..6a01536b12 100644
--- a/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs
+++ b/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs
@@ -124,7 +124,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
{
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
- GradientStops = new[]
+ GradientStops =
{
new GradientStop(Color.FromUInt32(0xffffffff), 0),
new GradientStop(Color.FromUInt32(0x00ffffff), 1)
diff --git a/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs
index 656e77fc31..a0d6f1e423 100644
--- a/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs
+++ b/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs
@@ -36,7 +36,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
StartPoint = new RelativePoint(0, 0.5, RelativeUnit.Relative),
EndPoint = new RelativePoint(1, 0.5, RelativeUnit.Relative),
- GradientStops = new[]
+ GradientStops =
{
new GradientStop { Color = Colors.Red, Offset = 0 },
new GradientStop { Color = Colors.Blue, Offset = 1 }
@@ -63,7 +63,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
StartPoint = new RelativePoint(0.5, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0.5, 1, RelativeUnit.Relative),
- GradientStops = new[]
+ GradientStops =
{
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 0017feb106..bd1d26ce70 100644
--- a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs
+++ b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs
@@ -34,7 +34,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
Background = new RadialGradientBrush
{
- GradientStops = new[]
+ GradientStops =
{
new GradientStop { Color = Colors.Red, Offset = 0 },
new GradientStop { Color = Colors.Blue, Offset = 1 }
diff --git a/tests/Avalonia.RenderTests/OpacityMaskTests.cs b/tests/Avalonia.RenderTests/OpacityMaskTests.cs
index 4edf4daa13..2f01b03db6 100644
--- a/tests/Avalonia.RenderTests/OpacityMaskTests.cs
+++ b/tests/Avalonia.RenderTests/OpacityMaskTests.cs
@@ -29,7 +29,7 @@ namespace Avalonia.Direct2D1.RenderTests
{
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
- GradientStops = new List
+ GradientStops =
{
new GradientStop(Color.FromUInt32(0xffffffff), 0),
new GradientStop(Color.FromUInt32(0x00ffffff), 1)
@@ -65,7 +65,7 @@ namespace Avalonia.Direct2D1.RenderTests
{
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
- GradientStops = new List
+ GradientStops =
{
new GradientStop(Color.FromUInt32(0xffffffff), 0),
new GradientStop(Color.FromUInt32(0x00ffffff), 1)
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/ImageBrushTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/ImageBrushTests.cs
new file mode 100644
index 0000000000..f843a6e333
--- /dev/null
+++ b/tests/Avalonia.Visuals.UnitTests/Media/ImageBrushTests.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using Moq;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Media
+{
+ public class ImageBrushTests
+ {
+ [Fact]
+ public void Changing_Source_Raises_Changed()
+ {
+ var bitmap1 = Mock.Of();
+ var bitmap2 = Mock.Of();
+ var target = new ImageBrush(bitmap1);
+ var raised = false;
+
+ target.Changed += (s, e) => raised = true;
+ target.Source = bitmap2;
+
+ Assert.True(raised);
+ }
+ }
+}
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/LinearGradientBrushTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/LinearGradientBrushTests.cs
new file mode 100644
index 0000000000..62f53108e6
--- /dev/null
+++ b/tests/Avalonia.Visuals.UnitTests/Media/LinearGradientBrushTests.cs
@@ -0,0 +1,86 @@
+using System;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using Moq;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Media
+{
+ public class LinearGradientBrushTests
+ {
+ [Fact]
+ public void Changing_StartPoint_Raises_Changed()
+ {
+ var bitmap1 = Mock.Of();
+ var bitmap2 = Mock.Of();
+ var target = new LinearGradientBrush();
+ var raised = false;
+
+ target.StartPoint = new RelativePoint();
+ target.Changed += (s, e) => raised = true;
+ target.StartPoint = new RelativePoint(10, 10, RelativeUnit.Absolute);
+
+ Assert.True(raised);
+ }
+
+ [Fact]
+ public void Changing_EndPoint_Raises_Changed()
+ {
+ var bitmap1 = Mock.Of();
+ var bitmap2 = Mock.Of();
+ var target = new LinearGradientBrush();
+ var raised = false;
+
+ target.EndPoint = new RelativePoint();
+ target.Changed += (s, e) => raised = true;
+ target.EndPoint = new RelativePoint(10, 10, RelativeUnit.Absolute);
+
+ Assert.True(raised);
+ }
+
+ [Fact]
+ public void Changing_GradientStops_Raises_Changed()
+ {
+ var bitmap1 = Mock.Of();
+ var bitmap2 = Mock.Of();
+ var target = new LinearGradientBrush();
+ var raised = false;
+
+ target.GradientStops = new GradientStops { new GradientStop(Colors.Red, 0) };
+ target.Changed += (s, e) => raised = true;
+ target.GradientStops = new GradientStops { new GradientStop(Colors.Green, 0) };
+
+ Assert.True(raised);
+ }
+
+ [Fact]
+ public void Adding_GradientStop_Raises_Changed()
+ {
+ var bitmap1 = Mock.Of();
+ var bitmap2 = Mock.Of();
+ var target = new LinearGradientBrush();
+ var raised = false;
+
+ target.GradientStops = new GradientStops { new GradientStop(Colors.Red, 0) };
+ target.Changed += (s, e) => raised = true;
+ target.GradientStops.Add(new GradientStop(Colors.Green, 1));
+
+ Assert.True(raised);
+ }
+
+ [Fact]
+ public void Changing_GradientStop_Offset_Raises_Changed()
+ {
+ var bitmap1 = Mock.Of();
+ var bitmap2 = Mock.Of();
+ var target = new LinearGradientBrush();
+ var raised = false;
+
+ target.GradientStops = new GradientStops { new GradientStop(Colors.Red, 0) };
+ target.Changed += (s, e) => raised = true;
+ target.GradientStops[0].Offset = 0.5;
+
+ Assert.True(raised);
+ }
+ }
+}
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/SolidColorBrushTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/SolidColorBrushTests.cs
new file mode 100644
index 0000000000..4e87b7081d
--- /dev/null
+++ b/tests/Avalonia.Visuals.UnitTests/Media/SolidColorBrushTests.cs
@@ -0,0 +1,21 @@
+using System;
+using Avalonia.Media;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Media
+{
+ public class SolidColorBrushTests
+ {
+ [Fact]
+ public void Changing_Color_Raises_Changed()
+ {
+ var target = new SolidColorBrush(Colors.Red);
+ var raised = false;
+
+ target.Changed += (s, e) => raised = true;
+ target.Color = Colors.Green;
+
+ Assert.True(raised);
+ }
+ }
+}