diff --git a/samples/ControlCatalog/Pages/BorderPage.xaml b/samples/ControlCatalog/Pages/BorderPage.xaml index c30056d5e5..8133d0e408 100644 --- a/samples/ControlCatalog/Pages/BorderPage.xaml +++ b/samples/ControlCatalog/Pages/BorderPage.xaml @@ -29,6 +29,13 @@ Padding="16"> Rounded Corners + + + + diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index b355310244..c8337df99c 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -1,13 +1,14 @@ using Avalonia.Controls.Utils; using Avalonia.Layout; using Avalonia.Media; +using Avalonia.VisualTree; namespace Avalonia.Controls { /// /// A control which decorates a child with a border and background. /// - public partial class Border : Decorator + public partial class Border : Decorator, IVisualWithRoundRectClip { /// /// Defines the property. @@ -129,5 +130,7 @@ namespace Avalonia.Controls { return LayoutHelper.ArrangeChild(Child, finalSize, Padding, BorderThickness); } + + public CornerRadius ClipToBoundsRadius => CornerRadius; } } diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index b1cf1aecc9..ba7191d7a6 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -283,6 +283,12 @@ namespace Avalonia.Media } + public PushedState PushClip(RoundedRect clip) + { + PlatformImpl.PushClip(clip); + return new PushedState(this, PushedState.PushedStateType.Clip); + } + /// /// Pushes a clip rectangle. /// diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index 660d10c088..c87946b3ea 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -107,6 +107,12 @@ namespace Avalonia.Platform /// The clip rectangle. void PushClip(Rect clip); + /// + /// Pushes a clip rounded rectangle. + /// + /// The clip rounded rectangle + void PushClip(RoundedRect clip); + /// /// Pops the latest pushed clip rectangle. /// diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs index c1c4b6bc99..b1f51b941a 100644 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs @@ -289,7 +289,11 @@ namespace Avalonia.Rendering using (context.PushPostTransform(m)) using (context.PushOpacity(opacity)) - using (clipToBounds ? context.PushClip(bounds) : default(DrawingContext.PushedState)) + using (clipToBounds + ? visual is IVisualWithRoundRectClip roundClipVisual + ? context.PushClip(new RoundedRect(bounds, roundClipVisual.ClipToBoundsRadius)) + : context.PushClip(bounds) + : default(DrawingContext.PushedState)) using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default(DrawingContext.PushedState)) using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default(DrawingContext.PushedState)) using (context.PushTransformContainer()) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs index 34f042e334..ada04bfefd 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs @@ -16,6 +16,16 @@ namespace Avalonia.Rendering.SceneGraph { Clip = clip; } + + /// + /// Initializes a new instance of the class that represents a + /// clip push. + /// + /// The clip to push. + public ClipNode(RoundedRect clip) + { + Clip = clip; + } /// /// Initializes a new instance of the class that represents a @@ -31,7 +41,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Gets the clip to be pushed or null if the operation represents a pop. /// - public Rect? Clip { get; } + public RoundedRect? Clip { get; } /// public bool HitTest(Point p) => false; @@ -45,7 +55,7 @@ namespace Avalonia.Rendering.SceneGraph /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(Rect? clip) => Clip == clip; + public bool Equals(RoundedRect? clip) => Clip == clip; /// public void Render(IDrawingContextImpl context) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 6ad71ac111..dfb21a0289 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -163,7 +163,7 @@ namespace Avalonia.Rendering.SceneGraph ++_drawOperationindex; } } - + public void Custom(ICustomDrawOperation custom) { var next = NextDrawAs(); @@ -283,6 +283,21 @@ namespace Avalonia.Rendering.SceneGraph } } + /// + public void PushClip(RoundedRect clip) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(clip)) + { + Add(new ClipNode(clip)); + } + else + { + ++_drawOperationindex; + } + } + /// public void PushGeometryClip(IGeometryImpl clip) { @@ -368,7 +383,7 @@ namespace Avalonia.Rendering.SceneGraph { using (var refCounted = RefCountable.Create(node)) { - Add(refCounted); + Add(refCounted); } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs index 8b77c37c1f..6d12b5bca4 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs @@ -26,6 +26,11 @@ namespace Avalonia.Rendering.SceneGraph /// Matrix Transform { get; } + /// + /// Gets the corner radius of visual. Contents are clipped to this radius. + /// + CornerRadius ClipToBoundsRadius { get; } + /// /// Gets the bounds of the node's geometry in global coordinates. /// diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index 263a491933..5da44c5943 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -163,6 +163,10 @@ namespace Avalonia.Rendering.SceneGraph var visual = node.Visual; var opacity = visual.Opacity; var clipToBounds = visual.ClipToBounds; + var clipToBoundsRadius = visual is IVisualWithRoundRectClip roundRectClip ? + roundRectClip.ClipToBoundsRadius : + default; + var bounds = new Rect(visual.Bounds.Size); var contextImpl = (DeferredDrawingContextImpl)context.PlatformImpl; @@ -201,6 +205,7 @@ namespace Avalonia.Rendering.SceneGraph node.ClipBounds = clipBounds; node.ClipToBounds = clipToBounds; node.LayoutBounds = globalBounds; + node.ClipToBoundsRadius = clipToBoundsRadius; node.GeometryClip = visual.Clip?.PlatformImpl; node.Opacity = opacity; diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index 6f566ff6d6..8fb6b2542a 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -46,6 +46,9 @@ namespace Avalonia.Rendering.SceneGraph /// public IVisualNode Parent { get; } + /// + public CornerRadius ClipToBoundsRadius { get; set; } + /// public Matrix Transform { get; set; } @@ -262,6 +265,7 @@ namespace Avalonia.Rendering.SceneGraph { Transform = Transform, ClipBounds = ClipBounds, + ClipToBoundsRadius = ClipToBoundsRadius, ClipToBounds = ClipToBounds, LayoutBounds = LayoutBounds, GeometryClip = GeometryClip, @@ -301,7 +305,10 @@ namespace Avalonia.Rendering.SceneGraph if (ClipToBounds) { context.Transform = Matrix.Identity; - context.PushClip(ClipBounds); + if (ClipToBoundsRadius.IsEmpty) + context.PushClip(ClipBounds); + else + context.PushClip(new RoundedRect(ClipBounds, ClipToBoundsRadius)); } context.Transform = Transform; diff --git a/src/Avalonia.Visuals/RoundedRect.cs b/src/Avalonia.Visuals/RoundedRect.cs index ad860240f2..3452bc1ff8 100644 --- a/src/Avalonia.Visuals/RoundedRect.cs +++ b/src/Avalonia.Visuals/RoundedRect.cs @@ -27,6 +27,10 @@ namespace Avalonia } } + public static bool operator ==(RoundedRect left, RoundedRect right) => left.Equals(right); + + public static bool operator !=(RoundedRect left, RoundedRect right) => !left.Equals(right); + public Rect Rect { get; } public Vector RadiiTopLeft { get; } public Vector RadiiTopRight { get; } @@ -74,6 +78,13 @@ namespace Avalonia } + public RoundedRect(in Rect bounds, in CornerRadius radius) : this(bounds, + radius.TopLeft, radius.TopRight, + radius.BottomRight, radius.BottomLeft) + { + + } + public static implicit operator RoundedRect(Rect r) => new RoundedRect(r); public bool IsRounded => RadiiTopLeft != default || RadiiTopRight != default || RadiiBottomRight != default || diff --git a/src/Avalonia.Visuals/VisualTree/IVisualWithRoundRectClip.cs b/src/Avalonia.Visuals/VisualTree/IVisualWithRoundRectClip.cs new file mode 100644 index 0000000000..9ace215d03 --- /dev/null +++ b/src/Avalonia.Visuals/VisualTree/IVisualWithRoundRectClip.cs @@ -0,0 +1,15 @@ +using System; + +namespace Avalonia.VisualTree +{ + [Obsolete("Internal API, will be removed in future versions, you've been warned")] + public interface IVisualWithRoundRectClip + { + /// + /// Gets a value indicating the corner radius of control's clip bounds + /// + [Obsolete("Internal API, will be removed in future versions, you've been warned")] + CornerRadius ClipToBoundsRadius { get; } + + } +} diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 26fdb08a4b..ae756f4eab 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -389,6 +389,12 @@ namespace Avalonia.Skia Canvas.ClipRect(clip.ToSKRect()); } + public void PushClip(RoundedRect clip) + { + Canvas.Save(); + Canvas.ClipRoundRect(clip.ToSKRoundRect()); + } + /// public void PopClip() { diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs index 6375f74c59..ec7e0a67ed 100644 --- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs +++ b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs @@ -40,6 +40,21 @@ namespace Avalonia.Skia return new SKRect((float)r.X, (float)r.Y, (float)r.Right, (float)r.Bottom); } + public static SKRoundRect ToSKRoundRect(this RoundedRect r) + { + var rc = r.Rect.ToSKRect(); + var result = new SKRoundRect(); + + result.SetRectRadii(rc, + new[] + { + r.RadiiTopLeft.ToSKPoint(), r.RadiiTopRight.ToSKPoint(), + r.RadiiBottomRight.ToSKPoint(), r.RadiiBottomLeft.ToSKPoint(), + }); + + return result; + } + public static Rect ToAvaloniaRect(this SKRect r) { return new Rect(r.Left, r.Top, r.Right - r.Left, r.Bottom - r.Top); diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 9b7ba4844a..e0de40525f 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -359,6 +359,12 @@ namespace Avalonia.Direct2D1.Media _deviceContext.PushAxisAlignedClip(clip.ToSharpDX(), AntialiasMode.PerPrimitive); } + public void PushClip(RoundedRect clip) + { + //TODO: radius + _deviceContext.PushAxisAlignedClip(clip.Rect.ToDirect2D(), AntialiasMode.PerPrimitive); + } + public void PopClip() { _deviceContext.PopAxisAlignedClip();