Browse Source

[Fix] ImmediateRenderer (#20174)

* Fixed invalid renering of controls with non-zero bounds position.
Fixed text clipping of text elements inside LayoutTransformControl.

* Fixed rendering of VisualBrush.

* Fixed control clipping.

* Fixed opacity mask state.

* Reworked to skip rendering of elements out of clip.

* Code cleanup.

* Added unit test.

* Parameters order.

---------

Co-authored-by: PleshakovDV <pleshakovdv@ugpa.ru>
pull/20166/head
Denis Pleshakov 2 months ago
committed by GitHub
parent
commit
f878d94c4a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs
  2. 4
      src/Avalonia.Base/Media/VisualBrush.cs
  3. 165
      src/Avalonia.Base/Rendering/ImmediateRenderer.cs
  4. 41
      tests/Avalonia.Base.UnitTests/RenderTests_Culling.cs

2
src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs

@ -47,7 +47,7 @@ namespace Avalonia.Media.Imaging
public void Render(Visual visual)
{
using (var ctx = CreateDrawingContext())
ImmediateRenderer.Render(visual, ctx);
ImmediateRenderer.Render(ctx, visual);
}
/// <summary>

4
src/Avalonia.Base/Media/VisualBrush.cs

@ -56,7 +56,7 @@ namespace Avalonia.Media
initialize.EnsureInitialized();
using var recorder = new RenderDataDrawingContext(null);
ImmediateRenderer.Render(recorder, Visual, Visual.Bounds);
ImmediateRenderer.Render(recorder, Visual);
return recorder.GetImmediateSceneBrushContent(this, new(Visual.Bounds.Size), true);
}
@ -130,7 +130,7 @@ namespace Avalonia.Media
initialize.EnsureInitialized();
using var recorder = new RenderDataDrawingContext(c);
ImmediateRenderer.Render(recorder, Visual, Visual.Bounds);
ImmediateRenderer.Render(recorder, Visual);
var renderData = recorder.GetRenderResults();
if (renderData == null)
return null;

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

@ -1,124 +1,89 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Logging;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Rendering
namespace Avalonia.Rendering;
/// <summary>
/// This class is used to render the visual tree into a DrawingContext by doing
/// a simple tree traversal.
/// It's currently used mostly for RenderTargetBitmap.Render and VisualBrush
/// </summary>
internal class ImmediateRenderer
{
/// <summary>
/// This class is used to render the visual tree into a DrawingContext by doing
/// a simple tree traversal.
/// It's currently used mostly for RenderTargetBitmap.Render and VisualBrush
/// Renders a visual to a drawing context.
/// </summary>
internal class ImmediateRenderer
/// <param name="visual">The visual.</param>
/// <param name="context">The drawing context.</param>
public static void Render(DrawingContext context, Visual visual)
=> Render(context, visual, new Rect(visual.Bounds.Size));
public static void Render(DrawingContext context, Visual visual, Rect clipRect)
{
/// <summary>
/// Renders a visual to a drawing context.
/// </summary>
/// <param name="visual">The visual.</param>
/// <param name="context">The drawing context.</param>
public static void Render(Visual visual, DrawingContext context)
using (context.PushTransform(Matrix.CreateTranslation(-clipRect.Position.X, -clipRect.Position.Y)))
using (context.PushClip(clipRect))
{
Render(context, visual, visual.Bounds);
Render(context, visual, new Rect(visual.Bounds.Size), Matrix.Identity, new Rect(clipRect.Size));
}
}
private static Rect GetTransformedBounds(Visual visual)
private static void Render(DrawingContext context, Visual visual, Rect bounds, Matrix parentTransform, Rect clipRect)
{
if (!visual.IsVisible || visual.Opacity is not (var opacity and > 0))
{
if (visual.RenderTransform == null)
{
return visual.Bounds;
}
else
{
var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height));
var offset = Matrix.CreateTranslation(visual.Bounds.Position + origin);
var m = (-offset) * visual.RenderTransform.Value * (offset);
return visual.Bounds.TransformToAABB(m);
}
return;
}
var rect = new Rect(bounds.Size);
Matrix transform;
public static void Render(DrawingContext context, Visual visual, Rect clipRect)
if (visual.RenderTransform?.Value is { } rt)
{
using(visual.RenderOptions != default ? context.PushRenderOptions(visual.RenderOptions) : (DrawingContext.PushedState?)null)
{
var opacity = visual.Opacity;
var clipToBounds = visual.ClipToBounds;
var bounds = new Rect(visual.Bounds.Size);
if (visual.IsVisible && opacity > 0)
{
var m = Matrix.CreateTranslation(visual.Bounds.Position);
var renderTransform = Matrix.Identity;
// this should be calculated BEFORE renderTransform
if (visual.HasMirrorTransform)
{
var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0);
renderTransform *= mirrorMatrix;
}
if (visual.RenderTransform != null)
{
var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height));
var offset = Matrix.CreateTranslation(origin);
var finalTransform = (-offset) * visual.RenderTransform.Value * (offset);
renderTransform *= finalTransform;
}
m = renderTransform * m;
if (clipToBounds)
{
if (visual.RenderTransform != null)
{
clipRect = new Rect(visual.Bounds.Size);
}
else
{
clipRect = clipRect.Intersect(new Rect(visual.Bounds.Size));
}
}
var origin = visual.RenderTransformOrigin.ToPixels(visual.Bounds.Size);
var offset = Matrix.CreateTranslation(origin);
transform = (-offset) * rt * (offset) * Matrix.CreateTranslation(bounds.Position);
}
else
{
transform = Matrix.CreateTranslation(bounds.Position);
}
using (context.PushTransform(m))
using (context.PushOpacity(opacity))
using (clipToBounds
#pragma warning disable CS0618 // Type or member is obsolete
? visual is IVisualWithRoundRectClip roundClipVisual
? context.PushClip(new RoundedRect(bounds, roundClipVisual.ClipToBoundsRadius))
: context.PushClip(bounds)
: default)
#pragma warning restore CS0618 // Type or member is obsolete
using (visual.RenderOptions != default ? context.PushRenderOptions(visual.RenderOptions) : default(DrawingContext.PushedState?))
using (context.PushTransform(transform))
using (visual.HasMirrorTransform ? context.PushTransform(new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0)) : default(DrawingContext.PushedState?))
using (context.PushOpacity(opacity))
using (visual switch
{
{ ClipToBounds: true } and IVisualWithRoundRectClip roundClipVisual => context.PushClip(new RoundedRect(rect, roundClipVisual.ClipToBoundsRadius)),
{ ClipToBounds: true } => context.PushClip(rect),
_ => default(DrawingContext.PushedState?)
})
using (visual.Clip is { } clip ? context.PushGeometryClip(clip) : default(DrawingContext.PushedState?))
using (visual.OpacityMask is { } opctMask ? context.PushOpacityMask(opctMask, rect) : default(DrawingContext.PushedState?))
{
var totalTransform = transform * parentTransform;
var visualBounds = rect.TransformToAABB(totalTransform);
using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default)
using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default)
using (context.PushTransform(Matrix.Identity))
{
visual.Render(context);
if (visualBounds.Intersects(clipRect))
{
visual.Render(context);
}
var childrenEnumerable = visual.HasNonUniformZIndexChildren
? visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance)
: (IEnumerable<Visual>)visual.VisualChildren;
IEnumerable<Visual> childrenEnumerable = visual.HasNonUniformZIndexChildren
? visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance)
: visual.VisualChildren;
foreach (var child in childrenEnumerable)
{
var childBounds = GetTransformedBounds(child);
if (visual.ClipToBounds)
{
totalTransform = Matrix.Identity;
clipRect = rect;
}
if (!child.ClipToBounds || clipRect.Intersects(childBounds))
{
var childClipRect = child.RenderTransform == null
? clipRect.Translate(-childBounds.Position)
: clipRect;
Render(context, child, childClipRect);
}
}
}
}
foreach (var child in childrenEnumerable)
{
Render(context, child, child.Bounds, totalTransform, clipRect);
}
}
}

41
tests/Avalonia.Base.UnitTests/RenderTests_Culling.cs

@ -109,6 +109,45 @@ namespace Avalonia.Base.UnitTests
}
}
[Fact]
public void Transformed_Child_Control_With_ClipToBounds_True_Should_Be_Rendered()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
TestControl target;
var container = new Panel
{
Width = 100,
Height = 20,
ClipToBounds = true,
Children =
{
new Panel
{
Width = 100,
Height = 20,
RenderTransform = new TranslateTransform(0, 30),
Children =
{
(target = new TestControl
{
Width = 100,
Height = 20,
ClipToBounds = true,
RenderTransform = new TranslateTransform(0, -30),
})
}
}
}
};
Render(container);
Assert.True(target.Rendered);
}
}
[Fact]
public void RenderTransform_Should_Be_Respected()
{
@ -176,7 +215,7 @@ namespace Avalonia.Base.UnitTests
var ctx = CreateDrawingContext();
control.Measure(Size.Infinity);
control.Arrange(new Rect(control.DesiredSize));
ImmediateRenderer.Render(control, ctx);
ImmediateRenderer.Render(ctx, control);
}
private DrawingContext CreateDrawingContext()

Loading…
Cancel
Save