From e36584918d1fd4873c5e5539e5f27336cd4efb7e Mon Sep 17 00:00:00 2001 From: Denis Pleshakov Date: Fri, 5 Dec 2025 17:51:49 +0300 Subject: [PATCH] [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 --- .../Media/Imaging/RenderTargetBitmap.cs | 2 +- src/Avalonia.Base/Media/VisualBrush.cs | 4 +- .../Rendering/ImmediateRenderer.cs | 165 +++++++----------- .../RenderTests_Culling.cs | 41 ++++- 4 files changed, 108 insertions(+), 104 deletions(-) diff --git a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs index 9f41177223..26229b5ecb 100644 --- a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs +++ b/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); } /// diff --git a/src/Avalonia.Base/Media/VisualBrush.cs b/src/Avalonia.Base/Media/VisualBrush.cs index 3365689f1b..05a363b00e 100644 --- a/src/Avalonia.Base/Media/VisualBrush.cs +++ b/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; diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs index 1d2244dfbb..d308f532da 100644 --- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs +++ b/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; + +/// +/// 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 +/// +internal class ImmediateRenderer { /// - /// 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. /// - internal class ImmediateRenderer + /// The visual. + /// The drawing context. + 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) { - /// - /// Renders a visual to a drawing context. - /// - /// The visual. - /// The drawing context. - 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.VisualChildren; + IEnumerable 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); } } } diff --git a/tests/Avalonia.Base.UnitTests/RenderTests_Culling.cs b/tests/Avalonia.Base.UnitTests/RenderTests_Culling.cs index f91b4b613c..a5e3ef9315 100644 --- a/tests/Avalonia.Base.UnitTests/RenderTests_Culling.cs +++ b/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()