From 9fcdacd063a76d81c970c80bd680b930f7417f0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Mon, 9 Feb 2026 11:25:06 +0100 Subject: [PATCH] Add child-only clip support for Border --- .../Composition/CompositionTarget.cs | 21 ++++++++-- .../Server/ServerCompositionDrawListVisual.cs | 13 +++++- .../Rendering/ImmediateRenderer.cs | 17 +++++--- .../VisualTree/IVisualWithChildClip.cs | 15 +++++++ src/Avalonia.Controls/Border.cs | 16 ++++++- src/Avalonia.Controls/BorderVisual.cs | 42 +++++++++++++++---- 6 files changed, 105 insertions(+), 19 deletions(-) create mode 100644 src/Avalonia.Base/VisualTree/IVisualWithChildClip.cs diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index f00eef0d10..f93ce6c933 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -97,15 +97,28 @@ namespace Avalonia.Rendering.Composition if (!TryTransformTo(visual, globalPoint, out var point)) return; - if (visual.ClipToBounds - && (point.X < 0 || point.Y < 0 || point.X > visual.Size.X || point.Y > visual.Size.Y)) - return; + var allowChildren = true; + if (visual.ClipToBounds) + { + 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; + } + } if (visual.Clip?.FillContains(point) == false) return; // Inspect children - if (visual is CompositionContainerVisual cv) + if (allowChildren && visual is CompositionContainerVisual cv) for (var c = cv.Children.Count - 1; c >= 0; c--) { var ch = cv.Children[c]; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index ba9b042ad3..7d8e751a71 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -40,16 +40,25 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua base.DeserializeChangesCore(reader, committedAt); } - protected override void RenderCore(ServerVisualRenderContext context, LtrbRect currentTransformedClip) + protected void RenderOwnContent(ServerVisualRenderContext context, LtrbRect currentTransformedClip) { - if (_renderCommands != null + if (_renderCommands != null && context.ShouldRenderOwnContent(this, currentTransformedClip)) { _renderCommands.Render(context.Canvas); } + } + protected void RenderChildren(ServerVisualRenderContext context, LtrbRect currentTransformedClip) + { base.RenderCore(context, currentTransformedClip); } + + protected override void RenderCore(ServerVisualRenderContext context, LtrbRect currentTransformedClip) + { + RenderOwnContent(context, currentTransformedClip); + RenderChildren(context, currentTransformedClip); + } public void DependencyQueuedInvalidate(IServerRenderResource sender) => ValuesInvalidated(); diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs index f054e77db8..ebc94dc8ff 100644 --- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs @@ -50,17 +50,21 @@ internal class ImmediateRenderer transform = Matrix.CreateTranslation(bounds.Position); } + var childClipProvider = visual.ClipToBounds ? visual as IVisualWithChildClip : null; + RoundedRect childClip = default; + var hasChildClip = childClipProvider != null && childClipProvider.TryGetChildClip(out childClip); + using (visual.TextOptions != default ? context.PushTextOptions(visual.TextOptions) : default(DrawingContext.PushedState?)) 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 + using (!hasChildClip ? visual switch { { ClipToBounds: true } and IVisualWithRoundRectClip roundClipVisual => context.PushClip(new RoundedRect(rect, roundClipVisual.ClipToBoundsRadius)), { ClipToBounds: true } => context.PushClip(rect), _ => default(DrawingContext.PushedState?) - }) + } : 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?)) { @@ -79,12 +83,15 @@ internal class ImmediateRenderer if (visual.ClipToBounds) { totalTransform = Matrix.Identity; - clipRect = rect; + clipRect = hasChildClip ? childClip.Rect : rect; } - foreach (var child in childrenEnumerable) + using (hasChildClip ? context.PushClip(childClip) : default(DrawingContext.PushedState?)) { - Render(context, child, child.Bounds, totalTransform, clipRect); + foreach (var child in childrenEnumerable) + { + Render(context, child, child.Bounds, totalTransform, clipRect); + } } } } diff --git a/src/Avalonia.Base/VisualTree/IVisualWithChildClip.cs b/src/Avalonia.Base/VisualTree/IVisualWithChildClip.cs new file mode 100644 index 0000000000..fd67cb67fc --- /dev/null +++ b/src/Avalonia.Base/VisualTree/IVisualWithChildClip.cs @@ -0,0 +1,15 @@ +using Avalonia.Media; + +namespace Avalonia.VisualTree +{ + /// + /// Provides a clip that should be applied to a visual's children only. + /// + internal interface IVisualWithChildClip + { + /// + /// Attempts to get a clip for the visual's children in local coordinates. + /// + bool TryGetChildClip(out RoundedRect clip); + } +} diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index b816858632..960ef85d81 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -11,7 +11,7 @@ namespace Avalonia.Controls /// A control which decorates a child with a border and background. /// #pragma warning disable CS0618 // Type or member is obsolete - public partial class Border : Decorator, IVisualWithRoundRectClip + public partial class Border : Decorator, IVisualWithRoundRectClip, IVisualWithChildClip #pragma warning restore CS0618 // Type or member is obsolete { /// @@ -178,6 +178,8 @@ namespace Avalonia.Controls /// The drawing context. public sealed override void Render(DrawingContext context) { + if (_borderVisual != null) + _borderVisual.BorderThickness = LayoutThickness; _borderRenderHelper.Render( context, Bounds.Size, @@ -218,5 +220,17 @@ namespace Avalonia.Controls } public CornerRadius ClipToBoundsRadius => CornerRadius; + + bool IVisualWithChildClip.TryGetChildClip(out RoundedRect clip) + { + var bounds = new Rect(Bounds.Size); + var keypoints = GeometryBuilder.CalculateRoundedCornersRectangleWinUI( + bounds, + LayoutThickness, + CornerRadius, + BackgroundSizing.InnerBorderEdge); + clip = keypoints.ToRoundedRect(); + return true; + } } } diff --git a/src/Avalonia.Controls/BorderVisual.cs b/src/Avalonia.Controls/BorderVisual.cs index 3046e7e875..f51cccbe7c 100644 --- a/src/Avalonia.Controls/BorderVisual.cs +++ b/src/Avalonia.Controls/BorderVisual.cs @@ -1,4 +1,6 @@ using System; +using Avalonia; +using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition.Server; @@ -11,6 +13,8 @@ class CompositionBorderVisual : CompositionDrawListVisual { private CornerRadius _cornerRadius; private bool _cornerRadiusChanged; + private Thickness _borderThickness; + private bool _borderThicknessChanged; public CompositionBorderVisual(Compositor compositor, Visual visual) : base(compositor, new ServerBorderVisual(compositor.Server, visual), visual) @@ -31,17 +35,35 @@ class CompositionBorderVisual : CompositionDrawListVisual } } + public Thickness BorderThickness + { + get => _borderThickness; + set + { + if (_borderThickness != value) + { + _borderThicknessChanged = true; + _borderThickness = value; + RegisterForSerialization(); + } + } + } + private protected override void SerializeChangesCore(BatchStreamWriter writer) { base.SerializeChangesCore(writer); writer.Write(_cornerRadiusChanged); if (_cornerRadiusChanged) writer.Write(_cornerRadius); + writer.Write(_borderThicknessChanged); + if (_borderThicknessChanged) + writer.Write(_borderThickness); } class ServerBorderVisual : ServerCompositionDrawListVisual { private CornerRadius _cornerRadius; + private Thickness _borderThickness; public ServerBorderVisual(ServerCompositor compositor, Visual v) : base(compositor, v) { } @@ -49,18 +71,22 @@ class CompositionBorderVisual : CompositionDrawListVisual protected override void RenderCore(ServerVisualRenderContext ctx, LtrbRect currentTransformedClip) { var canvas = ctx.Canvas; + RenderOwnContent(ctx, currentTransformedClip); + if (ClipToBounds) { var clipRect = Root!.SnapToDevicePixels(new Rect(new Size(Size.X, Size.Y))); - if (_cornerRadius == default) - canvas.PushClip(clipRect); - else - canvas.PushClip(new RoundedRect(clipRect, _cornerRadius)); + var keypoints = GeometryBuilder.CalculateRoundedCornersRectangleWinUI( + clipRect, + _borderThickness, + _cornerRadius, + BackgroundSizing.InnerBorderEdge); + canvas.PushClip(keypoints.ToRoundedRect()); } - base.RenderCore(ctx, currentTransformedClip); - - if(ClipToBounds) + RenderChildren(ctx, currentTransformedClip); + + if (ClipToBounds) canvas.PopClip(); } @@ -70,6 +96,8 @@ class CompositionBorderVisual : CompositionDrawListVisual base.DeserializeChangesCore(reader, committedAt); if (reader.Read()) _cornerRadius = reader.Read(); + if (reader.Read()) + _borderThickness = reader.Read(); } protected override bool HandlesClipToBounds => true;