Browse Source

Merge 956dcb3163 into 658afb8717

pull/20790/merge
Tim 20 hours ago
committed by GitHub
parent
commit
fadac2cc57
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 33
      src/Avalonia.Base/Media/DrawingContext.cs
  2. 45
      src/Avalonia.Base/Media/DrawingGroup.cs
  3. 18
      src/Avalonia.Base/Media/PlatformDrawingContext.cs
  4. 33
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs
  5. 10
      src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs
  6. 1
      src/Avalonia.Base/Rendering/ImmediateRenderer.cs
  7. 99
      tests/Avalonia.Base.UnitTests/Media/DrawingGroupTests.cs
  8. 14
      tests/Avalonia.Controls.UnitTests/Shapes/ShapeTests.cs
  9. 32
      tests/Avalonia.RenderTests/Media/DrawingContextTests.cs
  10. 55
      tests/Avalonia.RenderTests/Media/RenderTargetBitmapTests.cs
  11. BIN
      tests/TestFiles/Skia/Media/DrawingContext/Should_Render_DrawingGroup_With_Effect.expected.png
  12. BIN
      tests/TestFiles/Skia/Media/RenderTargetBitmap/RenderTargetBitmap_DropShadowEffect.expected.png

33
src/Avalonia.Base/Media/DrawingContext.cs

@ -285,7 +285,8 @@ namespace Avalonia.Media
GeometryClip,
OpacityMask,
RenderOptions,
TextOptions
TextOptions,
Effect
}
public RestoreState(DrawingContext context, PushedStateType type)
@ -314,6 +315,8 @@ namespace Avalonia.Media
_context.PopRenderOptionsCore();
else if (_type == PushedStateType.TextOptions)
_context.PopTextOptionsCore();
else if (_type == PushedStateType.Effect)
_context.PopEffectCore();
}
}
@ -433,10 +436,31 @@ namespace Avalonia.Media
return new PushedState(this);
}
/// <summary>
/// Pushes an effect.
/// </summary>
/// <param name="effect">The effect.</param>
/// <param name="bounds">The bounds of the effect.</param>
/// <returns>A disposable used to undo the effect.</returns>
public PushedState PushEffect(IEffect effect, Rect bounds)
{
PushEffectCore(effect, bounds);
_states ??= StateStackPool.Get();
_states.Push(new RestoreState(this, RestoreState.PushedStateType.Effect));
return new PushedState(this);
}
protected abstract void PushTextOptionsCore(TextOptions textOptions);
protected abstract void PushTransformCore(Matrix matrix);
/// <summary>
/// Pushes an effect.
/// </summary>
/// <param name="effect">The effect.</param>
/// <param name="bounds">The bounds of the effect.</param>
protected abstract void PushEffectCore(IEffect effect, Rect bounds);
protected abstract void PopClipCore();
protected abstract void PopGeometryClipCore();
protected abstract void PopOpacityCore();
@ -444,6 +468,11 @@ namespace Avalonia.Media
protected abstract void PopTransformCore();
protected abstract void PopRenderOptionsCore();
protected abstract void PopTextOptionsCore();
/// <summary>
/// Pops an effect.
/// </summary>
protected abstract void PopEffectCore();
private static bool PenIsVisible(IPen? pen)
{

45
src/Avalonia.Base/Media/DrawingGroup.cs

@ -21,6 +21,12 @@ namespace Avalonia.Media
public static readonly StyledProperty<IBrush?> OpacityMaskProperty =
AvaloniaProperty.Register<DrawingGroup, IBrush?>(nameof(OpacityMask));
/// <summary>
/// Defines the <see cref="Effect"/> property.
/// </summary>
public static readonly StyledProperty<IEffect?> EffectProperty =
AvaloniaProperty.Register<DrawingGroup, IEffect?>(nameof(Effect));
public static readonly DirectProperty<DrawingGroup, DrawingCollection> ChildrenProperty =
AvaloniaProperty.RegisterDirect<DrawingGroup, DrawingCollection>(
nameof(Children),
@ -53,8 +59,18 @@ namespace Avalonia.Media
set => SetValue(OpacityMaskProperty, value);
}
/// <summary>
/// Gets or sets the effect to apply to the drawing group.
/// </summary>
public IEffect? Effect
{
get => GetValue(EffectProperty);
set => SetValue(EffectProperty, value);
}
internal RenderOptions? RenderOptions { get; set; }
internal TextOptions? TextOptions { get; set; }
internal Rect? EffectBounds { get; set; }
/// <summary>
/// Gets or sets the collection that contains the child geometries.
@ -73,13 +89,24 @@ namespace Avalonia.Media
internal override void DrawCore(DrawingContext context)
{
var bounds = GetBounds();
// Compute effect bounds in local coordinate space (pre-transform)
// GetBounds() already includes transform, so we need to inverse-transform
// to get the local bounds, or compute bounds without applying transform
var localBounds = new Rect();
foreach (var drawing in Children)
{
localBounds = localBounds.Union(drawing.GetBounds());
}
var effectPadding = Effect?.GetEffectOutputPadding() ?? default;
var effectBounds = EffectBounds ?? localBounds.Inflate(effectPadding);
using (context.PushTransform(Transform?.Value ?? Matrix.Identity))
using (context.PushOpacity(Opacity))
using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default)
using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, bounds) : default)
using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, localBounds) : default)
using (RenderOptions != null ? context.PushRenderOptions(RenderOptions.Value) : default)
using (TextOptions != null ? context.PushTextOptions(TextOptions.Value) : default)
using (Effect != null ? context.PushEffect(Effect, effectBounds) : default)
{
foreach (var drawing in Children)
{
@ -336,6 +363,17 @@ namespace Avalonia.Media
drawingGroup.TextOptions = textOptions;
}
/// <inheritdoc />
protected override void PushEffectCore(IEffect effect, Rect bounds)
{
// Instantiate a new drawing group and set it as the _currentDrawingGroup
var drawingGroup = PushNewDrawingGroup();
// Set the effect on the new DrawingGroup
drawingGroup.Effect = effect;
drawingGroup.EffectBounds = bounds;
}
protected override void PopClipCore() => Pop();
protected override void PopGeometryClipCore() => Pop();
@ -350,6 +388,9 @@ namespace Avalonia.Media
protected override void PopTextOptionsCore() => Pop();
/// <inheritdoc />
protected override void PopEffectCore() => Pop();
/// <summary>
/// Creates a new DrawingGroup for a Push* call by setting the
/// _currentDrawingGroup to a newly instantiated DrawingGroup,

18
src/Avalonia.Base/Media/PlatformDrawingContext.cs

@ -88,6 +88,15 @@ internal sealed class PlatformDrawingContext : DrawingContext
protected override void PushTextOptionsCore(TextOptions textOptions) => _impl.PushTextOptions(textOptions);
/// <inheritdoc />
protected override void PushEffectCore(IEffect effect, Rect bounds)
{
if (_impl is IDrawingContextImplWithEffects effectImpl)
{
effectImpl.PushEffect(bounds, effect);
}
}
protected override void PopClipCore() => _impl.PopClip();
protected override void PopGeometryClipCore() => _impl.PopGeometryClip();
@ -104,6 +113,15 @@ internal sealed class PlatformDrawingContext : DrawingContext
protected override void PopTextOptionsCore() => _impl.PopTextOptions();
/// <inheritdoc />
protected override void PopEffectCore()
{
if (_impl is IDrawingContextImplWithEffects effectImpl)
{
effectImpl.PopEffect();
}
}
protected override void DisposeCore()
{
if (_ownsImpl)

33
src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs

@ -14,7 +14,8 @@ enum RenderDataPopNodeType
Clip,
GeometryClip,
Opacity,
OpacityMask
OpacityMask,
Effect
}
interface IRenderDataServerResourcesCollector
@ -247,3 +248,33 @@ class RenderDataTextOptionsNode : RenderDataPushNode
context.Context.PopTextOptions();
}
}
/// <summary>
/// A render data node that pushes an effect.
/// </summary>
class RenderDataEffectNode : RenderDataPushNode
{
/// <summary>
/// Gets or sets the effect to push.
/// </summary>
public IEffect? Effect { get; set; }
/// <summary>
/// Gets or sets the bounds of the effect.
/// </summary>
public Rect BoundsRect { get; set; }
/// <inheritdoc />
public override void Push(ref RenderDataNodeRenderContext context)
{
if (Effect != null && context.Context is IDrawingContextImplWithEffects effectImpl)
effectImpl.PushEffect(BoundsRect, Effect);
}
/// <inheritdoc />
public override void Pop(ref RenderDataNodeRenderContext context)
{
if (Effect != null && context.Context is IDrawingContextImplWithEffects effectImpl)
effectImpl.PopEffect();
}
}

10
src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs

@ -269,6 +269,13 @@ internal class RenderDataDrawingContext : DrawingContext
TextOptions = textOptions
});
/// <inheritdoc />
protected override void PushEffectCore(IEffect effect, Rect bounds) => Push(new RenderDataEffectNode()
{
Effect = effect.ToImmutable(),
BoundsRect = bounds
});
protected override void PopClipCore() => Pop<RenderDataClipNode>();
protected override void PopGeometryClipCore() => Pop<RenderDataGeometryClipNode>();
@ -283,6 +290,9 @@ internal class RenderDataDrawingContext : DrawingContext
protected override void PopTextOptionsCore() => Pop<RenderDataTextOptionsNode>();
/// <inheritdoc />
protected override void PopEffectCore() => Pop<RenderDataEffectNode>();
internal override void DrawBitmap(IRef<IBitmapImpl>? source, double opacity, Rect sourceRect, Rect destRect)
{
if (source == null || sourceRect.IsEmpty() || destRect.IsEmpty())

1
src/Avalonia.Base/Rendering/ImmediateRenderer.cs

@ -63,6 +63,7 @@ internal class ImmediateRenderer
})
using (visual.Clip is { } clip ? context.PushGeometryClip(clip) : default(DrawingContext.PushedState?))
using (visual.OpacityMask is { } opctMask ? context.PushOpacityMask(opctMask, rect) : default(DrawingContext.PushedState?))
using (visual.Effect is { } effect ? context.PushEffect(effect, rect.Inflate(effect.GetEffectOutputPadding())) : default(DrawingContext.PushedState?))
{
var totalTransform = transform * parentTransform;
var visualBounds = rect.TransformToAABB(totalTransform);

99
tests/Avalonia.Base.UnitTests/Media/DrawingGroupTests.cs

@ -0,0 +1,99 @@
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.UnitTests;
using Avalonia.Utilities;
using Moq;
using Xunit;
namespace Avalonia.Base.UnitTests.Media;
public class DrawingGroupTests
{
[Fact]
public void PushEffect_Should_Store_Provided_Bounds()
{
using (UnitTestApplication.Start(new TestServices(renderInterface: Mock.Of<IPlatformRenderInterface>())))
{
var group = new DrawingGroup();
var effect = new BlurEffect { Radius = 10 };
var bounds = new Rect(10, 10, 100, 100);
using (var context = group.Open())
{
using (context.PushEffect(effect, bounds))
{
context.DrawRectangle(Brushes.Red, null, new Rect(20, 20, 50, 50));
}
}
// The Open() call adds a child DrawingGroup to the root group when PushEffect is called.
Assert.Single(group.Children);
var childGroup = Assert.IsType<DrawingGroup>(group.Children[0]);
Assert.Equal(effect, childGroup.Effect);
Assert.Equal(bounds, childGroup.EffectBounds);
}
}
[Fact]
public void Drawing_With_Effect_Should_Use_Stored_Bounds()
{
using (UnitTestApplication.Start(new TestServices(renderInterface: Mock.Of<IPlatformRenderInterface>())))
{
var effect = new BlurEffect { Radius = 10 };
var bounds = new Rect(10, 10, 100, 100);
var group = new DrawingGroup
{
Effect = effect,
EffectBounds = bounds
};
group.Children.Add(new GeometryDrawing { Brush = Brushes.Red, Geometry = new RectangleGeometry { Rect = new Rect(20, 20, 50, 50) } });
var mockContext = new MockDrawingContext();
group.Draw(mockContext);
Assert.Equal(effect, mockContext.Effect);
Assert.Equal(bounds, mockContext.Bounds);
}
}
private class MockDrawingContext : DrawingContext
{
public IEffect? Effect { get; private set; }
public Rect Bounds { get; private set; }
protected override void PushEffectCore(IEffect effect, Rect bounds)
{
Effect = effect;
Bounds = bounds;
}
protected override void PopEffectCore() { }
// Implementing required abstract members
protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect) { }
protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry) { }
protected override void DrawLineCore(IPen pen, Point p1, Point p2) { }
protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect, BoxShadows boxShadows = default) { }
protected override void PushClipCore(Rect rect) { }
protected override void PushClipCore(RoundedRect rect) { }
protected override void PushGeometryClipCore(Geometry clip) { }
protected override void PushOpacityCore(double opacity) { }
protected override void PushOpacityMaskCore(IBrush mask, Rect bounds) { }
protected override void PushTransformCore(Matrix matrix) { }
protected override void PopClipCore() { }
protected override void PopGeometryClipCore() { }
protected override void PopOpacityCore() { }
protected override void PopOpacityMaskCore() { }
protected override void PopTransformCore() { }
protected override void DisposeCore() { }
internal override void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect) { }
public override void Custom(ICustomDrawOperation custom) { }
public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun) { }
protected override void PushRenderOptionsCore(RenderOptions renderOptions) { }
protected override void PushTextOptionsCore(TextOptions textOptions) { }
protected override void PopRenderOptionsCore() { }
protected override void PopTextOptionsCore() { }
}
}

14
tests/Avalonia.Controls.UnitTests/Shapes/ShapeTests.cs

@ -181,8 +181,12 @@ public class ShapeTests : ScopedTestBase
protected override void PushRenderOptionsCore(RenderOptions renderOptions)
{
}
protected override void PushTextOptionsCore(TextOptions textOptions)
protected override void PushTextOptionsCore(TextOptions textOptions)
{
}
protected override void PushEffectCore(IEffect effect, Rect bounds)
{
}
@ -213,10 +217,14 @@ public class ShapeTests : ScopedTestBase
protected override void PopRenderOptionsCore()
{
}
protected override void PopTextOptionsCore()
{
}
protected override void PopEffectCore()
{
}
protected override void DisposeCore()
{

32
tests/Avalonia.RenderTests/Media/DrawingContextTests.cs

@ -1,6 +1,7 @@
using System.Globalization;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using Xunit;
#pragma warning disable CS0649
@ -29,6 +30,37 @@ public class DrawingContextTests : TestBase
CompareImages(skipImmediate: true);
}
[Fact]
public async Task Should_Render_DrawingGroup_With_Effect()
{
var group = new DrawingGroup();
using (var context = group.Open())
{
context.DrawRectangle(Brushes.White, null, new Rect(0, 0, 100, 100));
using (context.PushEffect(new DropShadowEffect { BlurRadius = 10, Color = Colors.Black, Opacity = 1 }, new Rect(0, 0, 100, 100)))
{
context.DrawRectangle(Brushes.Red, null, new Rect(20, 20, 60, 60));
}
}
var target = new Border
{
Width = 100,
Height = 100,
Background = Brushes.White,
Child = new Image {
Source = new DrawingImage { Drawing = group },
Stretch = Stretch.None,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center,
ClipToBounds = false
}
};
await RenderToFile(target);
CompareImages();
}
internal class RenderControl : Control
{
private static readonly Typeface s_typeface = new Typeface(TestFontFamily);

55
tests/Avalonia.RenderTests/Media/RenderTargetBitmapTests.cs

@ -0,0 +1,55 @@
#if AVALONIA_SKIA
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Skia.RenderTests;
public class RenderTargetBitmapTests : TestBase
{
public RenderTargetBitmapTests() : base(@"Media\RenderTargetBitmap")
{
}
[Fact]
public async Task RenderTargetBitmap_DropShadowEffect()
{
var root = new Grid
{
Width = 300,
Height = 300,
Children =
{
new Canvas
{
Width = 300,
Height = 300,
Background = Brushes.White,
Children =
{
new Rectangle
{
Fill = Brushes.Red,
Width = 200,
Height = 200,
Margin = new Thickness(50),
Effect = new DropShadowEffect
{
Color = Colors.Black,
BlurRadius = 30
}
}
}
}
}
};
await RenderToFile(root);
CompareImages();
}
}
#endif

BIN
tests/TestFiles/Skia/Media/DrawingContext/Should_Render_DrawingGroup_With_Effect.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
tests/TestFiles/Skia/Media/RenderTargetBitmap/RenderTargetBitmap_DropShadowEffect.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Loading…
Cancel
Save