From 21ad94b98085767674204e7da71dd2b403759c38 Mon Sep 17 00:00:00 2001 From: SKProCH <29896317+SKProCH@users.noreply.github.com> Date: Tue, 26 Dec 2023 06:52:25 +0300 Subject: [PATCH] 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 Co-authored-by: Tim <47110241+timunie@users.noreply.github.com> --- src/Avalonia.Base/Media/Pen.cs | 90 ++++++++++++++++--- src/Avalonia.Controls/Shapes/Shape.cs | 63 +++++++------ .../Utils/BorderRenderHelper.cs | 34 ++----- .../Avalonia.Base.UnitTests/Media/PenTests.cs | 83 ++++++++++++++++- 4 files changed, 196 insertions(+), 74 deletions(-) diff --git a/src/Avalonia.Base/Media/Pen.cs b/src/Avalonia.Base/Media/Pen.cs index ad0bc66276..26001df4f7 100644 --- a/src/Avalonia.Base/Media/Pen.cs +++ b/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 class. /// public Pen() - { - } + { } /// /// Initializes a new instance of the 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) - { - } + { } /// /// Initializes a new instance of the class. @@ -178,6 +179,69 @@ namespace Avalonia.Media MiterLimit); } + /// + /// Smart reuse and update pen properties. + /// + /// Old pen to modify. + /// The brush used to draw. + /// The stroke thickness. + /// The stroke dask array. + /// The stroke dask offset. + /// The line cap. + /// The line join. + /// The miter limit. + /// If a new instance was created and visual invalidation required. + internal static bool TryModifyOrCreate(ref IPen? pen, + IBrush? brush, + double thickness, + IList? 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 _resource; - + IPen ICompositionRenderResource.GetForCompositor(Compositor c) => _resource.GetForCompositor(c); void ICompositionRenderResource.AddRefOnCompositor(Compositor c) diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index a65ac774ef..c10e4d766d 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/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(StretchProperty, StrokeThicknessProperty); - - AffectsRender(FillProperty, StrokeProperty, StrokeDashArrayProperty, StrokeDashOffsetProperty, - StrokeThicknessProperty, StrokeLineCapProperty, StrokeJoinProperty); - } + private IPen? _strokePen; /// /// Gets a value that represents the 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) diff --git a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs index 799cc47d0c..4df4fd738a 100644 --- a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs +++ b/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? 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? 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); } } diff --git a/tests/Avalonia.Base.UnitTests/Media/PenTests.cs b/tests/Avalonia.Base.UnitTests/Media/PenTests.cs index 156a25c678..a20f112939 100644 --- a/tests/Avalonia.Base.UnitTests/Media/PenTests.cs +++ b/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(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(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); + } } }