From 05dbca21254fe0ee102fd5aa712f517b07f3ab9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Tue, 10 Feb 2026 22:59:56 +0100 Subject: [PATCH] Plumb child layout clips through rendering --- .../Composition/CompositionDrawListVisual.cs | 37 ++++++++++ .../Composition/CompositionTarget.cs | 26 +++---- .../Server/ServerCompositionDrawListVisual.cs | 14 ++++ .../ServerCompositionVisual.Render.cs | 70 ++++++++++++++++++- .../Rendering/ImmediateRenderer.cs | 20 ++++-- src/Avalonia.Base/Visual.Composition.cs | 12 ++++ src/Avalonia.Controls/BorderVisual.cs | 20 ------ 7 files changed, 161 insertions(+), 38 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs index 6f4bb3b882..ba3cd283d4 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -1,3 +1,4 @@ +using Avalonia.Platform; using Avalonia.Rendering.Composition.Drawing; using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Transport; @@ -16,6 +17,10 @@ internal class CompositionDrawListVisual : CompositionContainerVisual private bool _drawListChanged; private CompositionRenderData? _drawList; + private bool _childClipChanged; + private bool _hasChildClip; + private RoundedRect _childClip; + private IGeometryImpl? _childClipGeometry; /// /// The list of drawing commands @@ -36,6 +41,30 @@ internal class CompositionDrawListVisual : CompositionContainerVisual } } + internal void SetChildClip(RoundedRect clip, IGeometryImpl? geometryClip) + { + if (_hasChildClip && _childClip.Equals(clip) && ReferenceEquals(_childClipGeometry, geometryClip)) + return; + + _hasChildClip = true; + _childClip = clip; + _childClipGeometry = geometryClip; + _childClipChanged = true; + RegisterForSerialization(); + } + + internal void ClearChildClip() + { + if (!_hasChildClip && _childClipGeometry == null) + return; + + _hasChildClip = false; + _childClip = default; + _childClipGeometry = null; + _childClipChanged = true; + RegisterForSerialization(); + } + private protected override void SerializeChangesCore(BatchStreamWriter writer) { writer.Write((byte)(_drawListChanged ? 1 : 0)); @@ -44,6 +73,14 @@ internal class CompositionDrawListVisual : CompositionContainerVisual writer.WriteObject(DrawList?.Server); _drawListChanged = false; } + writer.Write((byte)(_childClipChanged ? 1 : 0)); + if (_childClipChanged) + { + writer.Write(_hasChildClip); + writer.Write(_childClip); + writer.WriteObject(_childClipGeometry); + _childClipChanged = false; + } base.SerializeChangesCore(writer); } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index 6e5bdf339c..c97612b6d2 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Numerics; +using Avalonia.Media; using Avalonia.Collections.Pooled; using Avalonia.VisualTree; @@ -72,20 +73,19 @@ namespace Avalonia.Rendering.Composition var point = parentPoint.Transform(invMatrix); var allowChildren = true; - if (visual.ClipToBounds) + if (visual is CompositionDrawListVisual { Visual: Visual childClipProvider } + && childClipProvider.TryGetChildClip(out var childClip, out var childClipGeometry)) { - if (visual is CompositionDrawListVisual { Visual: IVisualWithChildClip childClipProvider } - && childClipProvider.TryGetChildClip(out var childClip)) - { - if (point.X < 0 || point.Y < 0 || point.X > visual.Size.X || point.Y > visual.Size.Y) - return; - if (!childClip.ContainsExclusive(point)) - allowChildren = false; - } - else if (point.X < 0 || point.Y < 0 || point.X > visual.Size.X || point.Y > visual.Size.Y) - { - return; - } + var clipContains = childClipGeometry == null + ? childClip.ContainsExclusive(point) + : childClipGeometry.FillContains(point); + if (!clipContains) + allowChildren = false; + } + else if (visual.ClipToBounds && + (point.X < 0 || point.Y < 0 || point.X > visual.Size.X || point.Y > visual.Size.Y)) + { + return; } if (visual.Clip?.FillContains(point) == false) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index 98fd9e4873..510d984ac6 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -19,6 +19,9 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua public readonly Visual UiVisual; #endif private ServerCompositionRenderData? _renderCommands; + private bool _hasChildClip; + private RoundedRect _childClip; + private IGeometryImpl? _childClipGeometry; public ServerCompositionDrawListVisual(ServerCompositor compositor, Visual v) : base(compositor) { @@ -29,6 +32,10 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua public override LtrbRect? ComputeOwnContentBounds() => _renderCommands?.Bounds; + public bool HasChildClip => _hasChildClip; + public RoundedRect ChildClip => _childClip; + public IGeometryImpl? ChildClipGeometry => _childClipGeometry; + protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt) { if (reader.Read() == 1) @@ -38,6 +45,13 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua _renderCommands?.AddObserver(this); InvalidateContent(); } + if (reader.Read() == 1) + { + _hasChildClip = reader.Read(); + _childClip = reader.Read(); + _childClipGeometry = reader.ReadObject(); + InvalidateContent(); + } base.DeserializeChangesCore(reader, committedAt); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Render.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Render.cs index d2dd0346d6..560e953840 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Render.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Render.cs @@ -16,6 +16,7 @@ partial class ServerCompositionVisual private readonly bool _renderChildren; private TreeWalkContext _walkContext; private Stack _opacityStack; + private Stack _childClipSwapStack; private double _opacity; private bool _fullSkip; private bool _usedCache; @@ -51,6 +52,7 @@ partial class ServerCompositionVisual _opacity = 1; _opacityStack = pools.DoubleStackPool.Rent(); + _childClipSwapStack = pools.IntStackPool.Rent(); _skipNextVisualTransform = skipRootVisualTransform; _renderingToBitmapCache = renderingToBitmapCache; } @@ -122,6 +124,7 @@ partial class ServerCompositionVisual { VisitedVisuals++; var bitmapCacheRoot = _renderingToBitmapCache && visual == _rootVisual; + ServerCompositionDrawListVisual? childClipVisual = null; if (!bitmapCacheRoot) // Skip those for the root visual if we are rendering to bitmap cache { @@ -137,6 +140,9 @@ partial class ServerCompositionVisual if (visual.AdornedVisual != null) AdornerHelper_RenderPreGraphPushAdornerClip(visual); + if (visual is ServerCompositionDrawListVisual drawListVisual && drawListVisual.HasChildClip) + childClipVisual = drawListVisual; + // If caching is enabled, draw from cache and skip rendering if (visual.Cache != null) { @@ -144,6 +150,7 @@ partial class ServerCompositionVisual VisitedVisuals += visited; RenderedVisuals += rendered; _usedCache = true; + _childClipSwapStack.Push(0); visitChildren = false; return; } @@ -162,7 +169,39 @@ partial class ServerCompositionVisual effects.PushEffect(visual._subTreeBounds!.Value.ToRect(), visual.Effect); visual.RenderCore(_publicContext, _walkContext.Clip); - + + var childClipSwapState = 0; + if (!bitmapCacheRoot && childClipVisual != null) + { + var clipGeometry = visual.Clip; + var hasClipGeometry = clipGeometry != null; + var useGeometryClip = childClipVisual.ChildClipGeometry != null; + var hasBoundsClip = visual.ClipToBounds; + + if (hasClipGeometry) + _canvas.PopGeometryClip(); + if (hasBoundsClip) + _canvas.PopClip(); + + if (useGeometryClip) + { + if (hasBoundsClip) + visual.PushClipToBounds(_canvas); + _canvas.PushGeometryClip(childClipVisual.ChildClipGeometry!); + } + else + { + _canvas.PushClip(childClipVisual.ChildClip); + } + + if (hasClipGeometry) + _canvas.PushGeometryClip(clipGeometry!); + + childClipSwapState = 1 | (useGeometryClip ? 2 : 0) | (hasClipGeometry ? 4 : 0); + } + + _childClipSwapStack.Push(childClipSwapState); + visitChildren = _renderChildren; } @@ -175,6 +214,34 @@ partial class ServerCompositionVisual } var bitmapCacheRoot = _renderingToBitmapCache && visual == _rootVisual; + + var childClipSwapState = _childClipSwapStack.Pop(); + if (childClipSwapState != 0) + { + var hasClipGeometry = (childClipSwapState & 4) != 0; + var useGeometryClip = (childClipSwapState & 2) != 0; + var hasBoundsClip = visual.ClipToBounds; + + if (hasClipGeometry) + _canvas.PopGeometryClip(); + + if (useGeometryClip) + { + _canvas.PopGeometryClip(); + if (hasBoundsClip) + _canvas.PopClip(); + } + else + { + _canvas.PopClip(); + } + + if (hasBoundsClip) + visual.PushClipToBounds(_canvas); + + if (hasClipGeometry) + _canvas.PushGeometryClip(visual.Clip!); + } // If we've used cache, those never got pushed in PreSubgraph if (!_usedCache) @@ -225,6 +292,7 @@ partial class ServerCompositionVisual { _walkContext.Dispose(); _pools.DoubleStackPool.Return(ref _opacityStack); + _pools.IntStackPool.Return(ref _childClipSwapStack); AdornerHelper_Dispose(); } } diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs index ebc94dc8ff..008cd4664e 100644 --- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs @@ -50,9 +50,12 @@ internal class ImmediateRenderer transform = Matrix.CreateTranslation(bounds.Position); } - var childClipProvider = visual.ClipToBounds ? visual as IVisualWithChildClip : null; + var childClipProvider = visual; RoundedRect childClip = default; - var hasChildClip = childClipProvider != null && childClipProvider.TryGetChildClip(out childClip); + Geometry? childClipGeometry = null; + var hasChildClip = childClipProvider != null && + childClipProvider.TryGetChildClip(out childClip, out childClipGeometry); + var useGeometryClip = hasChildClip && childClipGeometry != null; using (visual.TextOptions != default ? context.PushTextOptions(visual.TextOptions) : default(DrawingContext.PushedState?)) using (visual.RenderOptions != default ? context.PushRenderOptions(visual.RenderOptions) : default(DrawingContext.PushedState?)) @@ -83,10 +86,19 @@ internal class ImmediateRenderer if (visual.ClipToBounds) { totalTransform = Matrix.Identity; - clipRect = hasChildClip ? childClip.Rect : rect; + clipRect = hasChildClip + ? (useGeometryClip ? rect : childClip.Rect) + : rect; } - using (hasChildClip ? context.PushClip(childClip) : default(DrawingContext.PushedState?)) + using (hasChildClip && useGeometryClip && visual.ClipToBounds + ? context.PushClip(rect) + : default(DrawingContext.PushedState?)) + using (hasChildClip + ? (useGeometryClip + ? context.PushGeometryClip(childClipGeometry!) + : context.PushClip(childClip)) + : default(DrawingContext.PushedState?)) { foreach (var child in childrenEnumerable) { diff --git a/src/Avalonia.Base/Visual.Composition.cs b/src/Avalonia.Base/Visual.Composition.cs index be2d42782d..1176e869b3 100644 --- a/src/Avalonia.Base/Visual.Composition.cs +++ b/src/Avalonia.Base/Visual.Composition.cs @@ -140,6 +140,18 @@ public partial class Visual comp.Opacity = (float)Opacity; comp.ClipToBounds = ClipToBounds; comp.Clip = Clip?.PlatformImpl; + + if (comp is CompositionDrawListVisual drawListVisual) + { + if (TryGetChildClip(out var childClip, out var childClipGeometry)) + { + drawListVisual.SetChildClip(childClip, childClipGeometry?.PlatformImpl); + } + else + { + drawListVisual.ClearChildClip(); + } + } if (!Equals(comp.OpacityMask, OpacityMask)) comp.OpacityMask = OpacityMask; diff --git a/src/Avalonia.Controls/BorderVisual.cs b/src/Avalonia.Controls/BorderVisual.cs index 7048c86fb7..d77ff1e6ec 100644 --- a/src/Avalonia.Controls/BorderVisual.cs +++ b/src/Avalonia.Controls/BorderVisual.cs @@ -70,13 +70,6 @@ class CompositionBorderVisual : CompositionDrawListVisual protected override void RenderCore(ServerVisualRenderContext ctx, LtrbRect currentTransformedClip) { - if (ClipToBounds && Clip == null) - { - base.RenderCore(ctx, currentTransformedClip); - ReplaceBoundsClipWithChildClip(ctx.Canvas); - return; - } - base.RenderCore(ctx, currentTransformedClip); } protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt) @@ -94,19 +87,6 @@ class CompositionBorderVisual : CompositionDrawListVisual canvas.PushClip(new Rect(0, 0, Size.X, Size.Y)); } - private void ReplaceBoundsClipWithChildClip(IDrawingContextImpl canvas) - { - canvas.PopClip(); - - var clipRect = new Rect(new Size(Size.X, Size.Y)); - var keypoints = GeometryBuilder.CalculateRoundedCornersRectangleWinUI( - clipRect, - _borderThickness, - _cornerRadius, - BackgroundSizing.InnerBorderEdge); - canvas.PushClip(keypoints.ToRoundedRect()); - } - } }