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)
{