diff --git a/src/SamplesApp/UITests.Shared/UITests.Shared.projitems b/src/SamplesApp/UITests.Shared/UITests.Shared.projitems index 64187ccf22b5..693e4d4dd1c4 100644 --- a/src/SamplesApp/UITests.Shared/UITests.Shared.projitems +++ b/src/SamplesApp/UITests.Shared/UITests.Shared.projitems @@ -5350,6 +5350,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile @@ -8797,6 +8801,9 @@ Path_Custom.xaml + + Path_Clipping.xaml + Path_Geometries.xaml @@ -9841,4 +9848,4 @@ - \ No newline at end of file + diff --git a/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Shapes/Path_Clipping.xaml b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Shapes/Path_Clipping.xaml new file mode 100644 index 000000000000..65d4e5bc2e97 --- /dev/null +++ b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Shapes/Path_Clipping.xaml @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Shapes/Path_Clipping.xaml.cs b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Shapes/Path_Clipping.xaml.cs new file mode 100644 index 000000000000..bf6cf426267b --- /dev/null +++ b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Shapes/Path_Clipping.xaml.cs @@ -0,0 +1,15 @@ +using Uno.UI.Samples.Controls; +using Microsoft.UI.Xaml.Controls; + +namespace UITests.Windows_UI_Xaml_Shapes +{ + [Sample("Shapes", IsManualTest = true)] + public sealed partial class Path_Clipping : Page + { + public Path_Clipping() + { + this.InitializeComponent(); + Loaded += (_, _) => sv.ScrollToVerticalOffset(9999); + } + } +} diff --git a/src/Uno.UI.Composition/Composition/CompositionMaskBrush.skia.cs b/src/Uno.UI.Composition/Composition/CompositionMaskBrush.skia.cs index 39fa3c0bed23..55853839b80a 100644 --- a/src/Uno.UI.Composition/Composition/CompositionMaskBrush.skia.cs +++ b/src/Uno.UI.Composition/Composition/CompositionMaskBrush.skia.cs @@ -14,7 +14,6 @@ public partial class CompositionMaskBrush : CompositionBrush, IOnlineBrush { private SKPaint? _sourcePaint; private SKPaint? _maskPaint; - private SKPaint? _resultPaint; bool IOnlineBrush.IsOnline { @@ -36,10 +35,13 @@ internal override void UpdatePaint(SKPaint paint, SKRect bounds) void IOnlineBrush.Paint(in Visual.PaintingSession session, SKRect bounds) { - _resultPaint ??= new SKPaint() { IsAntialias = true }; + using (SkiaHelper.GetTempSKPaint(out var resultPaint)) + { + resultPaint.IsAntialias = true; - UpdatePaint(_resultPaint, bounds); - session.Canvas?.DrawRect(bounds, _resultPaint); + UpdatePaint(resultPaint, bounds); + session.Canvas?.DrawRect(bounds, resultPaint); + } } private protected override void DisposeInternal() @@ -48,7 +50,6 @@ private protected override void DisposeInternal() _sourcePaint?.Dispose(); _maskPaint?.Dispose(); - _resultPaint?.Dispose(); } } } diff --git a/src/Uno.UI.Composition/Composition/CompositionSpriteShape.skia.cs b/src/Uno.UI.Composition/Composition/CompositionSpriteShape.skia.cs index 81cdb203a298..327198534c56 100644 --- a/src/Uno.UI.Composition/Composition/CompositionSpriteShape.skia.cs +++ b/src/Uno.UI.Composition/Composition/CompositionSpriteShape.skia.cs @@ -3,14 +3,13 @@ using Windows.Foundation; using SkiaSharp; using Uno; +using Uno.Disposables; using Uno.Extensions; namespace Microsoft.UI.Composition { public partial class CompositionSpriteShape : CompositionShape { - private SKPaint? _strokePaint; - private SKPaint? _fillPaint; private SKPath? _geometryWithTransformations; internal override void Paint(in Visual.PaintingSession session) @@ -19,7 +18,7 @@ internal override void Paint(in Visual.PaintingSession session) { if (FillBrush is { } fill) { - var fillPaint = TryCreateAndClearFillPaint(session.Filters.OpacityColorFilter); + using var fillPaintDisposable = GetTempFillPaint(session.Filters.OpacityColorFilter, out var fillPaint); if (Compositor.TryGetEffectiveBackgroundColor(this, out var colorFromTransition)) { @@ -52,8 +51,8 @@ internal override void Paint(in Visual.PaintingSession session) if (StrokeBrush is { } stroke && StrokeThickness > 0) { - var fillPaint = TryCreateAndClearFillPaint(session.Filters.OpacityColorFilter); - var strokePaint = TryCreateAndClearStrokePaint(session.Filters.OpacityColorFilter); + using var fillPaintDisposable = GetTempFillPaint(session.Filters.OpacityColorFilter, out var fillPaint); + using var strokePaintDisposable = GetTempStrokePaint(session.Filters.OpacityColorFilter, out var strokePaint); // Set stroke thickness strokePaint.StrokeWidth = StrokeThickness; @@ -78,59 +77,45 @@ internal override void Paint(in Visual.PaintingSession session) // On Windows, the stroke is simply 1px, it doesn't scale with the height. // So, to get a correct stroke geometry, we must apply the transformations first. - // Get the stroke geometry, after scaling has been applied. - var strokeGeometry = strokePaint.GetFillPath(geometryWithTransformations); + using (SkiaHelper.GetTempSKPath(out var strokeFillPath)) + { + // Get the stroke geometry, after scaling has been applied. + strokePaint.GetFillPath(geometryWithTransformations, strokeFillPath); - stroke.UpdatePaint(fillPaint, strokeGeometry.Bounds); + stroke.UpdatePaint(fillPaint, strokeFillPath.Bounds); - session.Canvas.DrawPath(strokeGeometry, fillPaint); + session.Canvas.DrawPath(strokeFillPath, fillPaint); + } } } } - private SKPaint TryCreateAndClearStrokePaint(SKColorFilter? colorFilter) - => TryCreateAndClearPaint(ref _strokePaint, true, colorFilter, CompositionConfiguration.UseBrushAntialiasing); + private DisposableStruct GetTempStrokePaint(SKColorFilter? colorFilter, out SKPaint paint) + => GetTempPaint(out paint, true, colorFilter, CompositionConfiguration.UseBrushAntialiasing); - private SKPaint TryCreateAndClearFillPaint(SKColorFilter? colorFilter) - => TryCreateAndClearPaint(ref _fillPaint, false, colorFilter, CompositionConfiguration.UseBrushAntialiasing); + private DisposableStruct GetTempFillPaint(SKColorFilter? colorFilter, out SKPaint paint) + => GetTempPaint(out paint, false, colorFilter, CompositionConfiguration.UseBrushAntialiasing); - private static SKPaint TryCreateAndClearPaint(ref SKPaint? paint, bool isStroke, SKColorFilter? colorFilter, bool isHighQuality = false) + private static DisposableStruct GetTempPaint(out SKPaint paint, bool isStroke, SKColorFilter? colorFilter, bool isHighQuality = false) { - if (paint == null) - { - // Initialization - paint = new SKPaint(); - paint.IsStroke = isStroke; - paint.IsAntialias = true; - paint.IsAutohinted = true; + var disposable = SkiaHelper.GetTempSKPaint(out paint); + paint.IsAntialias = true; + paint.ColorFilter = colorFilter; - if (isHighQuality) - { - paint.FilterQuality = SKFilterQuality.High; - } - } - else - { - // Cleanup - // - Brushes can change, we cant leave color and shader garbage - // from last rendering around for the next pass. - paint.Color = SKColors.White; // Transparent color wouldn't draw anything - if (paint.Shader is { } shader) - { - shader.Dispose(); - paint.Shader = null; - } + paint.IsStroke = isStroke; - if (paint.PathEffect is { } pathEffect) - { - pathEffect.Dispose(); - paint.PathEffect = null; - } - } + // uno-specific defaults + paint.Color = SKColors.White; // Transparent color wouldn't draw anything + paint.IsAutohinted = true; + // paint.IsAntialias = true; // IMPORTANT: don't set this to true by default. It breaks canvas clipping on Linux for some reason. paint.ColorFilter = colorFilter; + if (isHighQuality) + { + paint.FilterQuality = SKFilterQuality.High; + } - return paint; + return disposable; } private protected override void OnPropertyChangedCore(string? propertyName, bool isSubPropertyChange) @@ -175,14 +160,20 @@ internal override bool HitTest(Point point) return true; } - if (StrokeBrush is { } stroke && StrokeThickness > 0) + if (StrokeBrush is { } && StrokeThickness > 0) { - var strokePaint = TryCreateAndClearStrokePaint(null); - strokePaint.StrokeWidth = StrokeThickness; - var strokeGeometry = strokePaint.GetFillPath(geometryWithTransformations); - if (strokeGeometry.Contains((float)point.X, (float)point.Y)) + using (GetTempStrokePaint(null, out var strokePaint)) { - return true; + strokePaint.StrokeWidth = StrokeThickness; + + using (SkiaHelper.GetTempSKPath(out var hitTestStrokeFillPath)) + { + strokePaint.GetFillPath(geometryWithTransformations, hitTestStrokeFillPath); + if (hitTestStrokeFillPath.Contains((float)point.X, (float)point.Y)) + { + return true; + } + } } } } diff --git a/src/Uno.UI.Composition/Composition/ShapeVisual.skia.cs b/src/Uno.UI.Composition/Composition/ShapeVisual.skia.cs index 8254ad7e9636..a0bb43f90f3d 100644 --- a/src/Uno.UI.Composition/Composition/ShapeVisual.skia.cs +++ b/src/Uno.UI.Composition/Composition/ShapeVisual.skia.cs @@ -1,5 +1,6 @@ #nullable enable +using System; using System.Numerics; using Windows.Foundation; using SkiaSharp; @@ -12,9 +13,12 @@ public partial class ShapeVisual private protected override void ApplyPrePaintingClipping(in SKCanvas canvas) { base.ApplyPrePaintingClipping(in canvas); - if (GetViewBoxPathInElementCoordinateSpace() is { } path) + using (SkiaHelper.GetTempSKPath(out var prePaintingClipPath)) { - canvas.ClipPath(path, antialias: true); + if (GetViewBoxPathInElementCoordinateSpace(prePaintingClipPath)) + { + canvas.ClipPath(prePaintingClipPath, antialias: true); + } } } @@ -32,28 +36,28 @@ internal override void Paint(in PaintingSession session) base.Paint(in session); } - internal SKPath? GetViewBoxPathInElementCoordinateSpace() + /// true if a ViewBox exists + internal bool GetViewBoxPathInElementCoordinateSpace(SKPath dst) { if (ViewBox is not { } viewBox) { - return null; + return false; } - var shape = new SKPath(); + dst.Rewind(); var clipRect = new SKRect(viewBox.Offset.X, viewBox.Offset.Y, viewBox.Offset.X + viewBox.Size.X, viewBox.Offset.Y + viewBox.Size.Y); - shape.AddRect(clipRect); + dst.AddRect(clipRect); if (viewBox.IsAncestorClip) { Matrix4x4.Invert(TotalMatrix, out var totalMatrixInverted); var childToParentTransform = Parent!.TotalMatrix * totalMatrixInverted; if (!childToParentTransform.IsIdentity) { - - shape.Transform(childToParentTransform.ToSKMatrix()); + dst.Transform(childToParentTransform.ToSKMatrix()); } } - return shape; + return true; } /// This does NOT take the clipping into account. diff --git a/src/Uno.UI.Composition/Composition/SkiaHelper.skia.cs b/src/Uno.UI.Composition/Composition/SkiaHelper.skia.cs new file mode 100644 index 000000000000..bd650dfc4e61 --- /dev/null +++ b/src/Uno.UI.Composition/Composition/SkiaHelper.skia.cs @@ -0,0 +1,33 @@ +#nullable enable + +using SkiaSharp; +using Microsoft.CodeAnalysis.PooledObjects; +using Uno.Disposables; +using SKPaint = SkiaSharp.SKPaint; + +namespace Microsoft.UI.Composition +{ + internal static class SkiaHelper + { + private static readonly ObjectPool _paintPool = new(() => new SKPaint(), 8); + private static readonly ObjectPool _pathPool = new(() => new SKPath(), 8); + + public static DisposableStruct GetTempSKPath(out SKPath path) + { + path = _pathPool.Allocate(); + // Note the difference between Rewind and Reset + // https://api.skia.org/classSkPath.html#a8dc858ee4c95a59b3dd4bdd3f7b85fdc : "Use rewind() instead of reset() if SkPath storage will be reused and performance is critical." + path.Rewind(); + return new DisposableStruct(p => _pathPool.Free(p), path); + } + + public static DisposableStruct GetTempSKPaint(out SKPaint paint) + { + paint = _paintPool.Allocate(); + // https://api.skia.org/classSkPaint.html#a6c7118c97a0e8819d75aa757afbc4c49 + // "This is equivalent to replacing SkPaint with the result of SkPaint()." + paint.Reset(); + return new DisposableStruct(p => _paintPool.Free(p), paint); + } + } +} diff --git a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.skia.cs b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.skia.cs index 1917c0cf030f..f65a17b05111 100644 --- a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.skia.cs +++ b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.skia.cs @@ -188,11 +188,13 @@ partial void SetupInlines() { var canvas = t.canvas; var rect = t.rect; - canvas.DrawRect(new SKRect((float)rect.Left, (float)rect.Top, (float)rect.Right, (float)rect.Bottom), new SKPaint + + using (SkiaHelper.GetTempSKPaint(out var paint)) { - Color = SelectionHighlightColor.Color.ToSKColor(), - Style = SKPaintStyle.Fill - }); + paint.Color = SelectionHighlightColor.Color.ToSKColor(); + paint.Style = SKPaintStyle.Fill; + canvas.DrawRect(new SKRect((float)rect.Left, (float)rect.Top, (float)rect.Right, (float)rect.Bottom), paint); + } }; _inlines.RenderSelection = IsTextSelectionEnabled; diff --git a/src/Uno.UI/UI/Xaml/Controls/TextBox/TextBox.skia.cs b/src/Uno.UI/UI/Xaml/Controls/TextBox/TextBox.skia.cs index 310476ac58c7..2a0aec65e9d9 100644 --- a/src/Uno.UI/UI/Xaml/Controls/TextBox/TextBox.skia.cs +++ b/src/Uno.UI/UI/Xaml/Controls/TextBox/TextBox.skia.cs @@ -239,11 +239,13 @@ private void UpdateTextBoxView() var caretRect = args.rect; var compositor = _visual.Compositor; var brush = DefaultBrushes.TextForegroundBrush.GetOrCreateCompositionBrush(compositor); - var caretPaint = new SKPaint(); // we create a new caret everytime to dodge the complications that occur when trying to "reset" an SKPaint. - brush.UpdatePaint(caretPaint, caretRect.ToSKRect()); - args.canvas.DrawRect( - new SKRect((float)caretRect.Left, (float)caretRect.Top, (float)caretRect.Right, - (float)caretRect.Bottom), caretPaint); + using (SkiaHelper.GetTempSKPaint(out var caretPaint)) + { + brush.UpdatePaint(caretPaint, caretRect.ToSKRect()); + args.canvas.DrawRect( + new SKRect((float)caretRect.Left, (float)caretRect.Top, (float)caretRect.Right, + (float)caretRect.Bottom), caretPaint); + } } if ((CaretMode == CaretDisplayMode.CaretWithThumbsOnlyEndShowing && args.endCaret) || diff --git a/src/Uno.UI/UI/Xaml/Documents/Inline.skia.cs b/src/Uno.UI/UI/Xaml/Documents/Inline.skia.cs index fbd772e967ce..fb265d7cc915 100644 --- a/src/Uno.UI/UI/Xaml/Documents/Inline.skia.cs +++ b/src/Uno.UI/UI/Xaml/Documents/Inline.skia.cs @@ -10,22 +10,6 @@ namespace Microsoft.UI.Xaml.Documents partial class Inline { private FontDetails? _fontInfo; - private SKPaint? _paint; - - internal SKPaint Paint - { - get - { - var paint = _paint ??= new SKPaint() - { - TextEncoding = SKTextEncoding.Utf16, - IsStroke = false, - IsAntialias = true, - }; - - return paint; - } - } internal FontDetails FontInfo { diff --git a/src/Uno.UI/UI/Xaml/Documents/InlineCollection.skia.cs b/src/Uno.UI/UI/Xaml/Documents/InlineCollection.skia.cs index 2d070fdee36a..1fdf463fe978 100644 --- a/src/Uno.UI/UI/Xaml/Documents/InlineCollection.skia.cs +++ b/src/Uno.UI/UI/Xaml/Documents/InlineCollection.skia.cs @@ -499,7 +499,11 @@ internal void Draw(in Visual.PaintingSession session) var segment = segmentSpan.Segment; var inline = segment.Inline; var fontInfo = segment.FallbackFont ?? inline.FontInfo; - var paint = inline.Paint; + + using var paintDisposable = SkiaHelper.GetTempSKPaint(out var paint); + paint.TextEncoding = SKTextEncoding.Utf16; + paint.IsStroke = false; + paint.IsAntialias = true; if (inline.Foreground is SolidColorBrush scb) { diff --git a/src/Uno.UI/UI/Xaml/Documents/Run.skia.cs b/src/Uno.UI/UI/Xaml/Documents/Run.skia.cs index 7b05a00e6e9a..8ccefd2cc24f 100644 --- a/src/Uno.UI/UI/Xaml/Documents/Run.skia.cs +++ b/src/Uno.UI/UI/Xaml/Documents/Run.skia.cs @@ -166,7 +166,6 @@ private List GetSegments() var fontInfo = FontInfo; var defaultTypeface = fontInfo.SKFont.Typeface; var defaultFont = fontInfo.Font; - var paint = Paint; var fontSize = fontInfo.SKFontSize; defaultFont.GetScale(out int defaultFontScale, out _); diff --git a/src/Uno.UI/UI/Xaml/Media/VisualTreeHelper.cs b/src/Uno.UI/UI/Xaml/Media/VisualTreeHelper.cs index ec4af5e05f56..a897299dcd4d 100644 --- a/src/Uno.UI/UI/Xaml/Media/VisualTreeHelper.cs +++ b/src/Uno.UI/UI/Xaml/Media/VisualTreeHelper.cs @@ -517,9 +517,14 @@ internal static (UIElement? element, Branch? stale) SearchDownForTopMostElementA // The maximum region where the current element and its children might draw themselves // This is expressed in the window (absolute) coordinate space. - var clippingBounds = element.Visual.GetViewBoxPathInElementCoordinateSpace() is { } path - ? transformToElement.Transform(path.TightBounds.ToRect()) - : Rect.Infinite; + Rect clippingBounds; + using (SkiaHelper.GetTempSKPath(out var viewBoxPath)) + { + clippingBounds = element.Visual.GetViewBoxPathInElementCoordinateSpace(viewBoxPath) + ? transformToElement.Transform(viewBoxPath.TightBounds.ToRect()) + : Rect.Infinite; + } + if (element.Visual.Clip?.GetBounds(element.Visual) is { } clip) {