diff --git a/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs b/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs
index c830ca2c49..27aca436b8 100644
--- a/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs
+++ b/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs
@@ -2,5 +2,7 @@ namespace Avalonia.Rendering.Composition;
internal interface ICompositionTargetDebugEvents
{
+ int RenderedVisuals { get; }
+ void IncrementRenderedVisuals();
void RectInvalidated(Rect rc);
}
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs
index c1037d5c67..51414c2250 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs
@@ -38,6 +38,7 @@ partial class ServerCompositionVisual
CompositionVisualChangedFields.Size
| CompositionVisualChangedFields.SizeAnimated
| CompositionVisualChangedFields.ClipToBounds
+ | CompositionVisualChangedFields.Clip
| CompositionVisualChangedFields.ClipToBoundsAnimated;
partial void OnFieldsDeserialized(CompositionVisualChangedFields changed)
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
index 45515a37e2..aeb228282e 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
@@ -38,6 +38,7 @@ namespace Avalonia.Rendering.Composition.Server
return;
Root!.RenderedVisuals++;
+ Root!.DebugEvents?.IncrementRenderedVisuals();
var boundsRect = new Rect(new Size(Size.X, Size.Y));
@@ -182,11 +183,24 @@ namespace Avalonia.Rendering.Composition.Server
if (_clipSizeDirty || positionChanged)
{
- _transformedClipBounds = ClipToBounds
- ? new Rect(new Size(Size.X, Size.Y))
- .TransformToAABB(GlobalTransformMatrix)
- : null;
+ Rect? transformedVisualBounds = null;
+ Rect? transformedClipBounds = null;
+ if (ClipToBounds)
+ transformedVisualBounds = new Rect(new Size(Size.X, Size.Y)).TransformToAABB(GlobalTransformMatrix);
+
+ if (Clip != null)
+ transformedClipBounds = Clip.Bounds.TransformToAABB(GlobalTransformMatrix);
+
+ if (transformedVisualBounds != null && transformedClipBounds != null)
+ _transformedClipBounds = transformedVisualBounds.Value.Intersect(transformedClipBounds.Value);
+ else if (transformedVisualBounds != null)
+ _transformedClipBounds = transformedVisualBounds;
+ else if (transformedClipBounds != null)
+ _transformedClipBounds = transformedClipBounds;
+ else
+ _transformedClipBounds = null;
+
_clipSizeDirty = false;
}
diff --git a/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs
new file mode 100644
index 0000000000..c158ff4e75
--- /dev/null
+++ b/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs
@@ -0,0 +1,68 @@
+using Avalonia.Controls;
+using Avalonia.Media;
+using Xunit;
+
+namespace Avalonia.Base.UnitTests.Rendering;
+///
+/// Test class that verifies how clipping influences rendering in the compositor
+///
+public class CompositorInvalidationClippingTests : CompositorTestsBase
+{
+ [Fact]
+ // Test case: When the ClipToBounds is false, all visuals should be rendered
+ public void Siblings_Should_Be_Rendered_On_Invalidate_Without_ClipToBounds()
+ {
+ AssertRenderedVisuals(clipToBounds: false, clipGeometry: false, expectedRenderedVisualsCount: 4);
+ }
+
+ [Fact]
+ // Test case: When the ClipToBounds is true, only visuals within the clipped boundary should be rendered
+ public void Siblings_Should_Not_Be_Rendered_On_Invalidate_With_ClipToBounds()
+ {
+ AssertRenderedVisuals(clipToBounds: true, clipGeometry: false, expectedRenderedVisualsCount: 3);
+ }
+
+ [Fact]
+ // Test case: When the Clip is used, only visuals within the clip geometry should be rendered
+ public void Siblings_Should_Not_Be_Rendered_On_Invalidate_With_Clip()
+ {
+ AssertRenderedVisuals(clipToBounds: false, clipGeometry: true, expectedRenderedVisualsCount: 3);
+ }
+
+ private void AssertRenderedVisuals(bool clipToBounds, bool clipGeometry, int expectedRenderedVisualsCount)
+ {
+ using (var s = new CompositorCanvas())
+ {
+ //#1 visual is top level
+ //#2 visual is s.Canvas
+
+ //#3 visual is border1
+ s.Canvas.Children.Add(new Border()
+ {
+ [Canvas.LeftProperty] = 0, [Canvas.TopProperty] = 0,
+ Width = 20, Height = 10,
+ Background = Brushes.Red,
+ ClipToBounds = clipToBounds,
+ Clip = clipGeometry ? new RectangleGeometry(new Rect(new Size(20, 10))) : null
+ });
+
+ //#4 visual is border2
+ s.Canvas.Children.Add(new Border()
+ {
+ [Canvas.LeftProperty] = 30, [Canvas.TopProperty] = 50,
+ Width = 20, Height = 10,
+ Background = Brushes.Red,
+ ClipToBounds = clipToBounds,
+ Clip = clipGeometry ? new RectangleGeometry(new Rect(new Size(20, 10))) : null
+ });
+ s.RunJobs();
+ s.Events.Reset();
+
+ //invalidate border1
+ s.Canvas.Children[0].IsVisible = false;
+ s.RunJobs();
+
+ s.AssertRenderedVisuals(expectedRenderedVisualsCount);
+ }
+ }
+}
diff --git a/tests/Avalonia.UnitTests/CompositorTestServices.cs b/tests/Avalonia.UnitTests/CompositorTestServices.cs
index 53fd610a17..00645259a5 100644
--- a/tests/Avalonia.UnitTests/CompositorTestServices.cs
+++ b/tests/Avalonia.UnitTests/CompositorTestServices.cs
@@ -89,6 +89,13 @@ public class CompositorTestServices : IDisposable
Events.Rects.Clear();
}
+ public void AssertRenderedVisuals(int renderVisuals)
+ {
+ RunJobs();
+ Assert.Equal(Events.RenderedVisuals, renderVisuals);
+ Events.Rects.Clear();
+ }
+
public void AssertHitTest(double x, double y, Func filter, params object[] expected)
=> AssertHitTest(new Point(x, y), filter, expected);
@@ -110,6 +117,13 @@ public class CompositorTestServices : IDisposable
{
public List Rects = new();
+ public int RenderedVisuals { get; private set; }
+
+ public void IncrementRenderedVisuals()
+ {
+ RenderedVisuals++;
+ }
+
public void RectInvalidated(Rect rc)
{
Rects.Add(rc);
@@ -118,6 +132,7 @@ public class CompositorTestServices : IDisposable
public void Reset()
{
Rects.Clear();
+ RenderedVisuals = 0;
}
}
@@ -218,4 +233,4 @@ public class DispatcherCompositorScheduler : ICompositorScheduler
{
Dispatcher.UIThread.Post(() => compositor.Commit(), DispatcherPriority.UiThreadRender);
}
-}
\ No newline at end of file
+}