Browse Source

Fixes Border and Shape border re-rendering when changing Brush value (#13980)

* Adds the ImmutablePenWithDynamicBrush and fixes Border and Shape border re-rendering when changing Brush value

* Removes the ImmutablePenWithDynamicBrush, rollback to Pen

* Cache and reuse Pen

* Move all Pen's updating logic to Pen.TryModifyOrCreate

* Add some tests for Pen.TryModifyOrCreate

* Add proper handling for DashStyle

* Invert condition in Pen.TryModifyOrCreate, fixes the logic

---------

Co-authored-by: Max Katz <maxkatz6@outlook.com>
Co-authored-by: Tim <47110241+timunie@users.noreply.github.com>
pull/14050/head
SKProCH 2 years ago
committed by GitHub
parent
commit
21ad94b980
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 90
      src/Avalonia.Base/Media/Pen.cs
  2. 63
      src/Avalonia.Controls/Shapes/Shape.cs
  3. 34
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  4. 83
      tests/Avalonia.Base.UnitTests/Media/PenTests.cs

90
src/Avalonia.Base/Media/Pen.cs

@ -1,4 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Collections;
using Avalonia.Media.Immutable;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Drawing;
@ -56,8 +59,7 @@ namespace Avalonia.Media
/// Initializes a new instance of the <see cref="Pen"/> class.
/// </summary>
public Pen()
{
}
{ }
/// <summary>
/// Initializes a new instance of the <see cref="Pen"/> class.
@ -75,8 +77,7 @@ namespace Avalonia.Media
PenLineCap lineCap = PenLineCap.Flat,
PenLineJoin lineJoin = PenLineJoin.Miter,
double miterLimit = 10.0) : this(new SolidColorBrush(color), thickness, dashStyle, lineCap, lineJoin, miterLimit)
{
}
{ }
/// <summary>
/// Initializes a new instance of the <see cref="Pen"/> class.
@ -178,6 +179,69 @@ namespace Avalonia.Media
MiterLimit);
}
/// <summary>
/// Smart reuse and update pen properties.
/// </summary>
/// <param name="pen">Old pen to modify.</param>
/// <param name="brush">The brush used to draw.</param>
/// <param name="thickness">The stroke thickness.</param>
/// <param name="strokeDashArray">The stroke dask array.</param>
/// <param name="strokeDaskOffset">The stroke dask offset.</param>
/// <param name="lineCap">The line cap.</param>
/// <param name="lineJoin">The line join.</param>
/// <param name="miterLimit">The miter limit.</param>
/// <returns>If a new instance was created and visual invalidation required.</returns>
internal static bool TryModifyOrCreate(ref IPen? pen,
IBrush? brush,
double thickness,
IList<double>? strokeDashArray = null,
double strokeDaskOffset = default,
PenLineCap lineCap = PenLineCap.Flat,
PenLineJoin lineJoin = PenLineJoin.Miter,
double miterLimit = 10.0)
{
var previousPen = pen;
if (brush is null)
{
pen = null;
return previousPen is not null;
}
IDashStyle? dashStyle = null;
if (strokeDashArray is { Count: > 0 })
{
// strokeDashArray can be IList (instead of AvaloniaList) in future
// So, if it supports notification - create a mutable DashStyle
dashStyle = strokeDashArray is INotifyCollectionChanged
? new DashStyle(strokeDashArray, strokeDaskOffset)
: new ImmutableDashStyle(strokeDashArray, strokeDaskOffset);
}
if (brush is IImmutableBrush immutableBrush && dashStyle is null or ImmutableDashStyle)
{
pen = new ImmutablePen(
immutableBrush,
thickness,
(ImmutableDashStyle?)dashStyle,
lineCap,
lineJoin,
miterLimit);
return true;
}
var mutablePen = previousPen as Pen ?? new Pen();
mutablePen.Brush = brush;
mutablePen.Thickness = thickness;
mutablePen.LineCap = lineCap;
mutablePen.LineJoin = lineJoin;
mutablePen.DashStyle = dashStyle;
mutablePen.MiterLimit = miterLimit;
pen = mutablePen;
return !Equals(previousPen, pen);
}
void RegisterForSerialization()
{
_resource.RegisterForInvalidationOnAllCompositors(this);
@ -186,21 +250,21 @@ namespace Avalonia.Media
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
RegisterForSerialization();
if (change.Property == BrushProperty)
if (change.Property == BrushProperty)
_resource.ProcessPropertyChangeNotification(change);
if(change.Property == DashStyleProperty)
if (change.Property == DashStyleProperty)
UpdateDashStyleSubscription();
base.OnPropertyChanged(change);
}
void UpdateDashStyleSubscription()
{
var newValue = _resource.IsAttached ? DashStyle as DashStyle : null;
if(ReferenceEquals(_subscribedToDashes, newValue))
if (ReferenceEquals(_subscribedToDashes, newValue))
return;
if (_subscribedToDashes != null && _weakSubscriber != null)
@ -221,9 +285,9 @@ namespace Avalonia.Media
_subscribedToDashes = newValue;
}
}
private CompositorResourceHolder<ServerCompositionSimplePen> _resource;
IPen ICompositionRenderResource<IPen>.GetForCompositor(Compositor c) => _resource.GetForCompositor(c);
void ICompositionRenderResource.AddRefOnCompositor(Compositor c)

63
src/Avalonia.Controls/Shapes/Shape.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Avalonia.Collections;
using Avalonia.Media;
using Avalonia.Media.Immutable;
@ -62,14 +63,7 @@ namespace Avalonia.Controls.Shapes
private Matrix _transform = Matrix.Identity;
private Geometry? _definingGeometry;
private Geometry? _renderedGeometry;
static Shape()
{
AffectsMeasure<Shape>(StretchProperty, StrokeThicknessProperty);
AffectsRender<Shape>(FillProperty, StrokeProperty, StrokeDashArrayProperty, StrokeDashOffsetProperty,
StrokeThicknessProperty, StrokeLineCapProperty, StrokeJoinProperty);
}
private IPen? _strokePen;
/// <summary>
/// Gets a value that represents the <see cref="Geometry"/> of the shape.
@ -199,30 +193,7 @@ namespace Avalonia.Controls.Shapes
if (geometry != null)
{
var stroke = Stroke;
ImmutablePen? pen = null;
if (stroke != null)
{
var strokeDashArray = StrokeDashArray;
ImmutableDashStyle? dashStyle = null;
if (strokeDashArray != null && strokeDashArray.Count > 0)
{
dashStyle = new ImmutableDashStyle(strokeDashArray, StrokeDashOffset);
}
pen = new ImmutablePen(
stroke.ToImmutable(),
StrokeThickness,
dashStyle,
StrokeLineCap,
StrokeJoin);
}
context.DrawGeometry(Fill, pen, geometry);
context.DrawGeometry(Fill, _strokePen, geometry);
}
}
@ -266,6 +237,34 @@ namespace Avalonia.Controls.Shapes
InvalidateMeasure();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == StrokeProperty
|| change.Property == StrokeThicknessProperty
|| change.Property == StrokeDashArrayProperty
|| change.Property == StrokeDashOffsetProperty
|| change.Property == StrokeLineCapProperty
|| change.Property == StrokeJoinProperty)
{
if (change.Property == StrokeProperty
|| change.Property == StrokeThicknessProperty)
{
InvalidateMeasure();
}
if (!Pen.TryModifyOrCreate(ref _strokePen, Stroke, StrokeThickness, StrokeDashArray, StrokeDashOffset, StrokeLineCap, StrokeJoin))
{
InvalidateVisual();
}
}
else if (change.Property == FillProperty)
{
InvalidateVisual();
}
}
protected override Size MeasureOverride(Size availableSize)
{
if (DefiningGeometry is null)

34
src/Avalonia.Controls/Utils/BorderRenderHelper.cs

@ -17,6 +17,7 @@ namespace Avalonia.Controls.Utils
private Thickness _borderThickness;
private CornerRadius _cornerRadius;
private bool _initialized;
private IPen? _cachedPen;
void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius)
@ -87,22 +88,17 @@ namespace Avalonia.Controls.Utils
public void Render(DrawingContext context,
Size finalSize, Thickness borderThickness, CornerRadius cornerRadius,
IBrush? background, IBrush? borderBrush, BoxShadows boxShadows, double borderDashOffset = 0,
PenLineCap borderLineCap = PenLineCap.Flat, PenLineJoin borderLineJoin = PenLineJoin.Miter,
AvaloniaList<double>? borderDashArray = null)
IBrush? background, IBrush? borderBrush, BoxShadows boxShadows)
{
if (_size != finalSize
|| _borderThickness != borderThickness
|| _cornerRadius != cornerRadius
|| !_initialized)
Update(finalSize, borderThickness, cornerRadius);
RenderCore(context, background, borderBrush, boxShadows, borderDashOffset, borderLineCap, borderLineJoin,
borderDashArray);
RenderCore(context, background, borderBrush, boxShadows);
}
void RenderCore(DrawingContext context, IBrush? background, IBrush? borderBrush, BoxShadows boxShadows,
double borderDashOffset, PenLineCap borderLineCap, PenLineJoin borderLineJoin,
AvaloniaList<double>? borderDashArray)
void RenderCore(DrawingContext context, IBrush? background, IBrush? borderBrush, BoxShadows boxShadows)
{
if (_useComplexRendering)
{
@ -121,26 +117,8 @@ namespace Avalonia.Controls.Utils
else
{
var borderThickness = _borderThickness.Top;
IPen? pen = null;
ImmutableDashStyle? dashStyle = null;
if (borderDashArray != null && borderDashArray.Count > 0)
{
dashStyle = new ImmutableDashStyle(borderDashArray, borderDashOffset);
}
if (borderBrush != null && borderThickness > 0)
{
pen = new ImmutablePen(
borderBrush.ToImmutable(),
borderThickness,
dashStyle,
borderLineCap,
borderLineJoin);
}
Pen.TryModifyOrCreate(ref _cachedPen, borderBrush, borderThickness);
var rect = new Rect(_size);
if (!MathUtilities.IsZero(borderThickness))
@ -148,7 +126,7 @@ namespace Avalonia.Controls.Utils
var rrect = new RoundedRect(rect, _cornerRadius.TopLeft, _cornerRadius.TopRight,
_cornerRadius.BottomRight, _cornerRadius.BottomLeft);
context.DrawRectangle(background, pen, rrect, boxShadows);
context.DrawRectangle(background, _cachedPen, rrect, boxShadows);
}
}

83
tests/Avalonia.Base.UnitTests/Media/PenTests.cs

@ -1,4 +1,5 @@
using System;
#nullable enable
using System;
using Avalonia.Collections;
using Avalonia.Media;
using Avalonia.Media.Immutable;
@ -116,5 +117,85 @@ namespace Avalonia.Base.UnitTests.Media
Assert.True(Equals(target1, target2));
}
[Fact]
public void TryModifyOrCreate_Should_Return_True_When_Previous_Exists_And_Assign_Null_When_Brush_Is_Null()
{
IPen? target = new ImmutablePen(
brush: new ImmutableSolidColorBrush(Colors.Red),
thickness: 2,
dashStyle: new ImmutableDashStyle(new[] { 0.1, 0.2 }, 5),
lineCap: PenLineCap.Round,
lineJoin: PenLineJoin.Round,
miterLimit: 21);
var result = Pen.TryModifyOrCreate(ref target, null, 2);
Assert.True(result);
Assert.Null(target);
}
[Fact]
public void TryModifyOrCreate_Should_Return_False_When_Previous_Not_Exists_And_Assign_Null_When_Brush_Is_Null()
{
IPen? target = null;
var result = Pen.TryModifyOrCreate(ref target, null, 2);
Assert.False(result);
Assert.Null(target);
}
[Fact]
public void TryModifyOrCreate_Should_Return_True_When_Previous_Immutable_And_Assign_Mutable_When_Brush_Is_Mutable()
{
IPen? target = new ImmutablePen(
brush: new ImmutableSolidColorBrush(Colors.Red),
thickness: 2,
dashStyle: new ImmutableDashStyle(new[] { 0.1, 0.2 }, 5),
lineCap: PenLineCap.Round,
lineJoin: PenLineJoin.Round,
miterLimit: 21);
var result = Pen.TryModifyOrCreate(ref target, new SolidColorBrush(Colors.Blue), 2);
Assert.True(result);
Assert.IsType<Pen>(target);
}
[Fact]
public void TryModifyOrCreate_Should_Return_True_When_Previous_Immutable_And_Assign_Immutable_When_Brush_Is_Immutable()
{
IPen? target = new ImmutablePen(
brush: new ImmutableSolidColorBrush(Colors.Red),
thickness: 2,
dashStyle: new ImmutableDashStyle(new[] { 0.1, 0.2 }, 5),
lineCap: PenLineCap.Round,
lineJoin: PenLineJoin.Round,
miterLimit: 21);
var result = Pen.TryModifyOrCreate(ref target, new ImmutableSolidColorBrush(Colors.Blue), 2);
Assert.True(result);
Assert.IsType<ImmutablePen>(target);
}
[Fact]
public void TryModifyOrCreate_Should_Return_False_When_Previous_Mutable_And_Modify_Mutable_When_Brush_Is_Mutable()
{
var oldPen = new Pen(
brush: new SolidColorBrush(Colors.Red),
thickness: 2,
dashStyle: new ImmutableDashStyle(new[] { 0.1, 0.2 }, 5),
lineCap: PenLineCap.Round,
lineJoin: PenLineJoin.Round,
miterLimit: 21);
IPen? target = oldPen;
var result = Pen.TryModifyOrCreate(ref target, new SolidColorBrush(Colors.Blue), 2);
Assert.False(result);
Assert.Same(oldPen, target);
}
}
}

Loading…
Cancel
Save