A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

1400 lines
51 KiB

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering.Utilities;
using Avalonia.Skia.Helpers;
using Avalonia.Utilities;
using SkiaSharp;
using ISceneBrush = Avalonia.Media.ISceneBrush;
namespace Avalonia.Skia
{
/// <summary>
/// Skia based drawing context.
/// </summary>
internal partial class DrawingContextImpl : IDrawingContextImpl,
IDrawingContextWithAcrylicLikeSupport,
IDrawingContextImplWithEffects
{
private IDisposable?[]? _disposables;
private readonly Vector _dpi;
private readonly Stack<PaintWrapper> _maskStack = new();
private readonly Stack<double> _opacityStack = new();
private readonly Stack<RenderOptions> _renderOptionsStack = new();
private readonly Matrix? _postTransform;
private double _currentOpacity = 1.0f;
private readonly bool _disableSubpixelTextRendering;
private Matrix _currentTransform;
private bool _disposed;
private GRContext? _grContext;
public GRContext? GrContext => _grContext;
private readonly ISkiaGpu? _gpu;
private readonly SKPaint _strokePaint = SKPaintCache.Shared.Get();
private readonly SKPaint _fillPaint = SKPaintCache.Shared.Get();
private readonly SKPaint _boxShadowPaint = SKPaintCache.Shared.Get();
private static SKShader? s_acrylicNoiseShader;
private readonly ISkiaGpuRenderSession? _session;
private bool _leased;
private bool _useOpacitySaveLayer;
/// <summary>
/// Context create info.
/// </summary>
public struct CreateInfo
{
/// <summary>
/// Canvas to draw to.
/// </summary>
public SKCanvas? Canvas;
/// <summary>
/// Surface to draw to.
/// </summary>
public SKSurface? Surface;
/// <summary>
/// Dpi of drawings.
/// </summary>
public Vector Dpi;
/// <summary>
/// Render text without subpixel antialiasing.
/// </summary>
public bool DisableSubpixelTextRendering;
/// <summary>
/// GPU-accelerated context (optional)
/// </summary>
public GRContext? GrContext;
/// <summary>
/// Skia GPU provider context (optional)
/// </summary>
public ISkiaGpu? Gpu;
public ISkiaGpuRenderSession? CurrentSession;
}
private class SkiaLeaseFeature : ISkiaSharpApiLeaseFeature
{
private readonly DrawingContextImpl _context;
public SkiaLeaseFeature(DrawingContextImpl context)
{
_context = context;
}
public ISkiaSharpApiLease Lease()
{
_context.CheckLease();
return new ApiLease(_context);
}
private class ApiLease : ISkiaSharpApiLease
{
private readonly DrawingContextImpl _context;
private readonly SKMatrix _revertTransform;
private bool _isDisposed;
public ApiLease(DrawingContextImpl context)
{
_revertTransform = context.Canvas.TotalMatrix;
_context = context;
_context._leased = true;
}
public SKCanvas SkCanvas => _context.Canvas;
public GRContext? GrContext => _context.GrContext;
public SKSurface? SkSurface => _context.Surface;
public double CurrentOpacity => _context._currentOpacity;
public void Dispose()
{
if (!_isDisposed)
{
_context.Canvas.CSetMatrix(_revertTransform);
_context._leased = false;
_isDisposed = true;
}
}
}
}
/// <summary>
/// Create new drawing context.
/// </summary>
/// <param name="createInfo">Create info.</param>
/// <param name="disposables">Array of elements to dispose after drawing has finished.</param>
public DrawingContextImpl(CreateInfo createInfo, params IDisposable?[]? disposables)
{
Canvas = createInfo.Canvas ?? createInfo.Surface?.Canvas
?? throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo));
_dpi = createInfo.Dpi;
_disposables = disposables;
_disableSubpixelTextRendering = createInfo.DisableSubpixelTextRendering;
_grContext = createInfo.GrContext;
_gpu = createInfo.Gpu;
if (_grContext != null)
Monitor.Enter(_grContext);
Surface = createInfo.Surface;
_session = createInfo.CurrentSession;
if (!_dpi.NearlyEquals(SkiaPlatform.DefaultDpi))
{
_postTransform =
Matrix.CreateScale(_dpi.X / SkiaPlatform.DefaultDpi.X, _dpi.Y / SkiaPlatform.DefaultDpi.Y);
}
Transform = Matrix.Identity;
var options = AvaloniaLocator.Current.GetService<SkiaOptions>();
if(options != null)
{
_useOpacitySaveLayer = options.UseOpacitySaveLayer;
}
}
/// <summary>
/// Skia canvas.
/// </summary>
public SKCanvas Canvas { get; }
public SKSurface? Surface { get; }
public RenderOptions RenderOptions { get; set; }
private void CheckLease()
{
if (_leased)
throw new InvalidOperationException("The underlying graphics API is currently leased");
}
/// <inheritdoc />
public void Clear(Color color)
{
CheckLease();
Canvas.Clear(color.ToSKColor());
}
/// <inheritdoc />
public void DrawBitmap(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect)
{
CheckLease();
var drawableImage = (IDrawableBitmapImpl)source;
var s = sourceRect.ToSKRect();
var d = destRect.ToSKRect();
var paint = SKPaintCache.Shared.Get();
paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity));
paint.FilterQuality = RenderOptions.BitmapInterpolationMode.ToSKFilterQuality();
paint.BlendMode = RenderOptions.BitmapBlendingMode.ToSKBlendMode();
drawableImage.Draw(this, s, d, paint);
SKPaintCache.Shared.ReturnReset(paint);
}
/// <inheritdoc />
public void DrawBitmap(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
{
CheckLease();
PushOpacityMask(opacityMask, opacityMaskRect);
DrawBitmap(source, 1, new Rect(0, 0, source.PixelSize.Width, source.PixelSize.Height), destRect);
PopOpacityMask();
}
/// <inheritdoc />
public void DrawLine(IPen? pen, Point p1, Point p2)
{
CheckLease();
if (pen is not null
&& TryCreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))) is { } stroke)
{
using (stroke)
{
Canvas.DrawLine((float)p1.X, (float)p1.Y, (float)p2.X, (float)p2.Y, stroke.Paint);
}
}
}
/// <inheritdoc />
public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry)
{
CheckLease();
var impl = (GeometryImpl) geometry;
var size = geometry.Bounds.Size;
if (brush is not null && impl.FillPath != null)
{
using (var fill = CreatePaint(_fillPaint, brush, size))
{
Canvas.DrawPath(impl.FillPath, fill.Paint);
}
}
if (pen is not null
&& impl.StrokePath != null
&& TryCreatePaint(_strokePaint, pen, size.Inflate(new Thickness(pen.Thickness / 2))) is { } stroke)
{
using (stroke)
{
Canvas.DrawPath(impl.StrokePath, stroke.Paint);
}
}
}
private static float SkBlurRadiusToSigma(double radius) {
if (radius <= 0)
return 0.0f;
return 0.288675f * (float)radius + 0.5f;
}
private struct BoxShadowFilter : IDisposable
{
public readonly SKPaint Paint;
private readonly SKImageFilter? _filter;
public readonly SKClipOperation ClipOperation;
private BoxShadowFilter(SKPaint paint, SKImageFilter? filter, SKClipOperation clipOperation)
{
Paint = paint;
_filter = filter;
ClipOperation = clipOperation;
}
public static BoxShadowFilter Create(SKPaint paint, BoxShadow shadow, double opacity)
{
var ac = shadow.Color;
var filter = SkiaCompat.CreateBlur(SkBlurRadiusToSigma(shadow.Blur), SkBlurRadiusToSigma(shadow.Blur));
var color = new SKColor(ac.R, ac.G, ac.B, (byte)(ac.A * opacity));
paint.Reset();
paint.IsAntialias = true;
paint.Color = color;
paint.ImageFilter = filter;
var clipOperation = shadow.IsInset ? SKClipOperation.Intersect : SKClipOperation.Difference;
return new BoxShadowFilter(paint, filter, clipOperation);
}
public void Dispose()
{
Paint?.Reset();
_filter?.Dispose();
}
}
private static SKRect AreaCastingShadowInHole(
SKRect hole_rect,
float shadow_blur,
float shadow_spread,
float offsetX, float offsetY)
{
// Adapted from Chromium
var bounds = hole_rect;
bounds.Inflate(shadow_blur, shadow_blur);
if (shadow_spread < 0)
bounds.Inflate(-shadow_spread, -shadow_spread);
var offset_bounds = bounds;
offset_bounds.Offset(-offsetX, -offsetY);
bounds.Union(offset_bounds);
return bounds;
}
/// <inheritdoc />
public void DrawRectangle(IExperimentalAcrylicMaterial? material, RoundedRect rect)
{
if (rect.Rect.Height <= 0 || rect.Rect.Width <= 0)
return;
CheckLease();
var rc = rect.Rect.ToSKRect();
SKRoundRect? skRoundRect = null;
if (rect.IsRounded)
{
skRoundRect = SKRoundRectCache.Shared.Get();
skRoundRect.SetRectRadii(rc,
new[]
{
rect.RadiiTopLeft.ToSKPoint(),
rect.RadiiTopRight.ToSKPoint(),
rect.RadiiBottomRight.ToSKPoint(),
rect.RadiiBottomLeft.ToSKPoint(),
});
}
if (material != null)
{
using (var paint = CreateAcrylicPaint(_fillPaint, material))
{
if (skRoundRect is not null)
{
Canvas.DrawRoundRect(skRoundRect, paint.Paint);
SKRoundRectCache.Shared.Return(skRoundRect);
}
else
{
Canvas.DrawRect(rc, paint.Paint);
}
}
}
}
/// <inheritdoc />
public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default)
{
if (rect.Rect.Height <= 0 || rect.Rect.Width <= 0)
return;
CheckLease();
// Arbitrary chosen values
// On OSX Skia breaks OpenGL context when asked to draw, e. g. (0, 0, 623, 6666600) rect
if (rect.Rect.Height > 8192 || rect.Rect.Width > 8192)
boxShadows = default;
var rc = rect.Rect.ToSKRect();
var isRounded = rect.IsRounded;
var needRoundRect = rect.IsRounded || (boxShadows.HasInsetShadows);
SKRoundRect? skRoundRect = null;
if (needRoundRect)
{
skRoundRect = SKRoundRectCache.Shared.GetAndSetRadii(rc, rect);
}
foreach (var boxShadow in boxShadows)
{
if (boxShadow != default && !boxShadow.IsInset)
{
using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity))
{
var spread = (float)boxShadow.Spread;
if (boxShadow.IsInset)
spread = -spread;
Canvas.Save();
if (isRounded)
{
var shadowRect = SKRoundRectCache.Shared.GetAndSetRadii(skRoundRect!.Rect, skRoundRect.Radii);
if (spread != 0)
shadowRect.Inflate(spread, spread);
Canvas.ClipRoundRect(skRoundRect,
shadow.ClipOperation, true);
var oldTransform = Transform;
Transform = oldTransform * Matrix.CreateTranslation(boxShadow.OffsetX, boxShadow.OffsetY);
Canvas.DrawRoundRect(shadowRect, shadow.Paint);
Transform = oldTransform;
SKRoundRectCache.Shared.Return(shadowRect);
}
else
{
var shadowRect = rc;
if (spread != 0)
shadowRect.Inflate(spread, spread);
Canvas.ClipRect(rc, shadow.ClipOperation);
var oldTransform = Transform;
Transform = oldTransform * Matrix.CreateTranslation(boxShadow.OffsetX, boxShadow.OffsetY);
Canvas.DrawRect(shadowRect, shadow.Paint);
Transform = oldTransform;
}
Canvas.Restore();
}
}
}
if (brush != null)
{
using (var fill = CreatePaint(_fillPaint, brush, rect.Rect.Size))
{
if (isRounded)
{
Canvas.DrawRoundRect(skRoundRect, fill.Paint);
}
else
{
Canvas.DrawRect(rc, fill.Paint);
}
}
}
foreach (var boxShadow in boxShadows)
{
if (boxShadow != default && boxShadow.IsInset)
{
using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity))
{
var spread = (float)boxShadow.Spread;
var offsetX = (float)boxShadow.OffsetX;
var offsetY = (float)boxShadow.OffsetY;
var outerRect = AreaCastingShadowInHole(rc, (float)boxShadow.Blur, spread, offsetX, offsetY);
Canvas.Save();
var shadowRect = SKRoundRectCache.Shared.GetAndSetRadii(skRoundRect!.Rect, skRoundRect.Radii);
if (spread != 0)
shadowRect.Deflate(spread, spread);
Canvas.ClipRoundRect(skRoundRect,
shadow.ClipOperation, true);
var oldTransform = Transform;
Transform = oldTransform * Matrix.CreateTranslation(boxShadow.OffsetX, boxShadow.OffsetY);
using (var outerRRect = new SKRoundRect(outerRect))
Canvas.DrawRoundRectDifference(outerRRect, shadowRect, shadow.Paint);
Transform = oldTransform;
Canvas.Restore();
SKRoundRectCache.Shared.Return(shadowRect);
}
}
}
if (pen is not null
&& TryCreatePaint(_strokePaint, pen, rect.Rect.Size.Inflate(new Thickness(pen.Thickness / 2))) is { } stroke)
{
using (stroke)
{
if (isRounded)
{
Canvas.DrawRoundRect(skRoundRect, stroke.Paint);
}
else
{
Canvas.DrawRect(rc, stroke.Paint);
}
}
}
if (skRoundRect is not null)
SKRoundRectCache.Shared.Return(skRoundRect);
}
/// <inheritdoc />
public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect)
{
if (rect.Height <= 0 || rect.Width <= 0)
return;
CheckLease();
var rc = rect.ToSKRect();
if (brush != null)
{
using (var fill = CreatePaint(_fillPaint, brush, rect.Size))
{
Canvas.DrawOval(rc, fill.Paint);
}
}
if (pen is not null
&& TryCreatePaint(_strokePaint, pen, rect.Size.Inflate(new Thickness(pen.Thickness / 2))) is { } stroke)
{
using (stroke)
{
Canvas.DrawOval(rc, stroke.Paint);
}
}
}
/// <inheritdoc />
public void DrawGlyphRun(IBrush? foreground, IGlyphRunImpl glyphRun)
{
CheckLease();
if (foreground is null)
{
return;
}
using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Bounds.Size))
{
var glyphRunImpl = (GlyphRunImpl)glyphRun;
var textRenderOptions = RenderOptions;
if (_disableSubpixelTextRendering)
{
switch (textRenderOptions.TextRenderingMode)
{
case TextRenderingMode.Unspecified
when textRenderOptions.EdgeMode == EdgeMode.Antialias || textRenderOptions.EdgeMode == EdgeMode.Unspecified:
case TextRenderingMode.SubpixelAntialias:
{
textRenderOptions = textRenderOptions with { TextRenderingMode = TextRenderingMode.Antialias };
break;
}
}
}
var textBlob = glyphRunImpl.GetTextBlob(textRenderOptions);
Canvas.DrawText(textBlob, (float)glyphRun.BaselineOrigin.X,
(float)glyphRun.BaselineOrigin.Y, paintWrapper.Paint);
}
}
/// <inheritdoc />
public IDrawingContextLayerImpl CreateLayer(Size size)
{
CheckLease();
return CreateRenderTarget(size, true);
}
/// <inheritdoc />
public void PushClip(Rect clip)
{
CheckLease();
Canvas.Save();
Canvas.ClipRect(clip.ToSKRect());
}
public void PushClip(RoundedRect clip)
{
CheckLease();
Canvas.Save();
// Get the rounded rectangle
var rc = clip.Rect.ToSKRect();
// Get a round rect from the cache.
var roundRect = SKRoundRectCache.Shared.Get();
roundRect.SetRectRadii(rc,
new[]
{
clip.RadiiTopLeft.ToSKPoint(), clip.RadiiTopRight.ToSKPoint(),
clip.RadiiBottomRight.ToSKPoint(), clip.RadiiBottomLeft.ToSKPoint(),
});
Canvas.ClipRoundRect(roundRect, antialias:true);
// Should not need to reset as SetRectRadii overrides the values.
SKRoundRectCache.Shared.Return(roundRect);
}
/// <inheritdoc />
public void PopClip()
{
CheckLease();
Canvas.Restore();
}
/// <inheritdoc />
public void PushOpacity(double opacity, Rect? bounds)
{
CheckLease();
_opacityStack.Push(_currentOpacity);
var useOpacitySaveLayer = _useOpacitySaveLayer || RenderOptions.RequiresFullOpacityHandling == true;
if (useOpacitySaveLayer)
{
opacity = _currentOpacity * opacity; //Take current multiplied opacity
_currentOpacity = 1; //Opacity is applied via layering
if (bounds.HasValue)
{
var rect = bounds.Value.ToSKRect();
Canvas.SaveLayer(rect, new SKPaint { ColorF = new SKColorF(0, 0, 0, (float)opacity) });
}
else
{
Canvas.SaveLayer(new SKPaint { ColorF = new SKColorF(0, 0, 0, (float)opacity) });
}
}
else
{
_currentOpacity *= opacity;
}
}
/// <inheritdoc />
public void PopOpacity()
{
CheckLease();
var useOpacitySaveLayer = _useOpacitySaveLayer || RenderOptions.RequiresFullOpacityHandling == true;
if (useOpacitySaveLayer)
{
Canvas.Restore();
}
_currentOpacity = _opacityStack.Pop();
}
/// <inheritdoc />
public void PushRenderOptions(RenderOptions renderOptions)
{
CheckLease();
_renderOptionsStack.Push(RenderOptions);
RenderOptions = RenderOptions.MergeWith(renderOptions);
}
public void PopRenderOptions()
{
RenderOptions = _renderOptionsStack.Pop();
}
/// <inheritdoc />
public virtual void Dispose()
{
if(_disposed)
return;
CheckLease();
try
{
// Return leased paints.
SKPaintCache.Shared.ReturnReset(_strokePaint);
SKPaintCache.Shared.ReturnReset(_fillPaint);
SKPaintCache.Shared.ReturnReset(_boxShadowPaint);
if (_grContext != null)
{
Monitor.Exit(_grContext);
_grContext = null;
}
if (_disposables != null)
{
foreach (var disposable in _disposables)
disposable?.Dispose();
_disposables = null;
}
}
finally
{
_disposed = true;
}
}
/// <inheritdoc />
public void PushGeometryClip(IGeometryImpl clip)
{
CheckLease();
Canvas.Save();
Canvas.ClipPath(((GeometryImpl)clip).FillPath, SKClipOperation.Intersect, true);
}
/// <inheritdoc />
public void PopGeometryClip()
{
CheckLease();
Canvas.Restore();
}
/// <inheritdoc />
public void PushOpacityMask(IBrush mask, Rect bounds)
{
CheckLease();
var paint = SKPaintCache.Shared.Get();
Canvas.SaveLayer(bounds.ToSKRect(), paint);
_maskStack.Push(CreatePaint(paint, mask, bounds.Size));
}
/// <inheritdoc />
public void PopOpacityMask()
{
CheckLease();
var paint = SKPaintCache.Shared.Get();
paint.BlendMode = SKBlendMode.DstIn;
Canvas.SaveLayer(paint);
SKPaintCache.Shared.ReturnReset(paint);
PaintWrapper paintWrapper;
using (paintWrapper = _maskStack.Pop())
{
Canvas.DrawPaint(paintWrapper.Paint);
}
// Return the paint wrapper's paint less the reset since the paint is already reset in the Dispose method above.
SKPaintCache.Shared.Return(paintWrapper.Paint);
Canvas.Restore();
Canvas.Restore();
}
/// <inheritdoc />
public Matrix Transform
{
get { return _currentTransform; }
set
{
CheckLease();
if (_currentTransform == value)
return;
_currentTransform = value;
var transform = value;
if (_postTransform.HasValue)
{
transform *= _postTransform.Value;
}
Canvas.CSetMatrix(transform.ToSKMatrix());
}
}
public object? GetFeature(Type t)
{
if (t == typeof(ISkiaSharpApiLeaseFeature))
return new SkiaLeaseFeature(this);
return null;
}
/// <summary>
/// Configure paint wrapper for using gradient brush.
/// </summary>
/// <param name="paintWrapper">Paint wrapper.</param>
/// <param name="targetSize">Target size.</param>
/// <param name="gradientBrush">Gradient brush.</param>
private static void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Size targetSize, IGradientBrush gradientBrush)
{
var tileMode = gradientBrush.SpreadMethod.ToSKShaderTileMode();
var stopColors = gradientBrush.GradientStops.Select(s => s.Color.ToSKColor()).ToArray();
var stopOffsets = gradientBrush.GradientStops.Select(s => (float)s.Offset).ToArray();
switch (gradientBrush)
{
case ILinearGradientBrush linearGradient:
{
var start = linearGradient.StartPoint.ToPixels(targetSize).ToSKPoint();
var end = linearGradient.EndPoint.ToPixels(targetSize).ToSKPoint();
// would be nice to cache these shaders possibly?
if (linearGradient.Transform is null)
{
using (var shader =
SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode))
{
paintWrapper.Paint.Shader = shader;
}
}
else
{
var transformOrigin = linearGradient.TransformOrigin.ToPixels(targetSize);
var offset = Matrix.CreateTranslation(transformOrigin);
var transform = (-offset) * linearGradient.Transform.Value * (offset);
using (var shader =
SKShader.CreateLinearGradient(start, end, stopColors, stopOffsets, tileMode, transform.ToSKMatrix()))
{
paintWrapper.Paint.Shader = shader;
}
}
break;
}
case IRadialGradientBrush radialGradient:
{
var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint();
var radius = (float)(radialGradient.Radius * targetSize.Width);
var origin = radialGradient.GradientOrigin.ToPixels(targetSize).ToSKPoint();
if (origin.Equals(center))
{
// when the origin is the same as the center the Skia RadialGradient acts the same as D2D
if (radialGradient.Transform is null)
{
using (var shader =
SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode))
{
paintWrapper.Paint.Shader = shader;
}
}
else
{
var transformOrigin = radialGradient.TransformOrigin.ToPixels(targetSize);
var offset = Matrix.CreateTranslation(transformOrigin);
var transform = (-offset) * radialGradient.Transform.Value * (offset);
using (var shader =
SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode, transform.ToSKMatrix()))
{
paintWrapper.Paint.Shader = shader;
}
}
}
else
{
// when the origin is different to the center use a two point ConicalGradient to match the behaviour of D2D
// reverse the order of the stops to match D2D
var reversedColors = new SKColor[stopColors.Length];
Array.Copy(stopColors, reversedColors, stopColors.Length);
Array.Reverse(reversedColors);
// and then reverse the reference point of the stops
var reversedStops = new float[stopOffsets.Length];
for (var i = 0; i < stopOffsets.Length; i++)
{
reversedStops[i] = stopOffsets[i];
if (reversedStops[i] > 0 && reversedStops[i] < 1)
{
reversedStops[i] = Math.Abs(1 - stopOffsets[i]);
}
}
// compose with a background colour of the final stop to match D2D's behaviour of filling with the final color
if (radialGradient.Transform is null)
{
using (var shader = SKShader.CreateCompose(
SKShader.CreateColor(reversedColors[0]),
SKShader.CreateTwoPointConicalGradient(center, radius, origin, 0, reversedColors, reversedStops, tileMode)
))
{
paintWrapper.Paint.Shader = shader;
}
}
else
{
var transformOrigin = radialGradient.TransformOrigin.ToPixels(targetSize);
var offset = Matrix.CreateTranslation(transformOrigin);
var transform = (-offset) * radialGradient.Transform.Value * (offset);
using (var shader = SKShader.CreateCompose(
SKShader.CreateColor(reversedColors[0]),
SKShader.CreateTwoPointConicalGradient(center, radius, origin, 0, reversedColors, reversedStops, tileMode, transform.ToSKMatrix())
))
{
paintWrapper.Paint.Shader = shader;
}
}
}
break;
}
case IConicGradientBrush conicGradient:
{
var center = conicGradient.Center.ToPixels(targetSize).ToSKPoint();
// Skia's default is that angle 0 is from the right hand side of the center point
// but we are matching CSS where the vertical point above the center is 0.
var angle = (float)(conicGradient.Angle - 90);
var rotation = SKMatrix.CreateRotationDegrees(angle, center.X, center.Y);
if (conicGradient.Transform is { })
{
var transformOrigin = conicGradient.TransformOrigin.ToPixels(targetSize);
var offset = Matrix.CreateTranslation(transformOrigin);
var transform = (-offset) * conicGradient.Transform.Value * (offset);
rotation = rotation.PreConcat(transform.ToSKMatrix());
}
using (var shader =
SKShader.CreateSweepGradient(center, stopColors, stopOffsets, rotation))
{
paintWrapper.Paint.Shader = shader;
}
break;
}
}
}
/// <summary>
/// Configure paint wrapper for using tile brush.
/// </summary>
/// <param name="paintWrapper">Paint wrapper.</param>
/// <param name="targetSize">Target size.</param>
/// <param name="tileBrush">Tile brush to use.</param>
/// <param name="tileBrushImage">Tile brush image.</param>
private void ConfigureTileBrush(ref PaintWrapper paintWrapper, Size targetSize, ITileBrush tileBrush, IDrawableBitmapImpl tileBrushImage)
{
var calc = new TileBrushCalculator(tileBrush, tileBrushImage.PixelSize.ToSizeWithDpi(_dpi), targetSize);
var intermediate = CreateRenderTarget(calc.IntermediateSize, false);
paintWrapper.AddDisposable(intermediate);
using (var context = intermediate.CreateDrawingContext())
{
var sourceRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(96));
var targetRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(_dpi));
context.Clear(Colors.Transparent);
context.PushClip(calc.IntermediateClip);
context.Transform = calc.IntermediateTransform;
context.RenderOptions = RenderOptions;
context.DrawBitmap(
tileBrushImage,
1,
sourceRect,
targetRect);
context.PopClip();
}
var tileTransform =
tileBrush.TileMode != TileMode.None
? SKMatrix.CreateTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y)
: SKMatrix.CreateIdentity();
SKShaderTileMode tileX =
tileBrush.TileMode == TileMode.None
? SKShaderTileMode.Clamp
: tileBrush.TileMode == TileMode.FlipX || tileBrush.TileMode == TileMode.FlipXY
? SKShaderTileMode.Mirror
: SKShaderTileMode.Repeat;
SKShaderTileMode tileY =
tileBrush.TileMode == TileMode.None
? SKShaderTileMode.Clamp
: tileBrush.TileMode == TileMode.FlipY || tileBrush.TileMode == TileMode.FlipXY
? SKShaderTileMode.Mirror
: SKShaderTileMode.Repeat;
var image = intermediate.SnapshotImage();
paintWrapper.AddDisposable(image);
var paintTransform = default(SKMatrix);
SKMatrix.Concat(
ref paintTransform,
tileTransform,
SKMatrix.CreateScale((float)(96.0 / _dpi.X), (float)(96.0 / _dpi.Y)));
if (tileBrush.Transform is { })
{
var origin = tileBrush.TransformOrigin.ToPixels(targetSize);
var offset = Matrix.CreateTranslation(origin);
var transform = (-offset) * tileBrush.Transform.Value * (offset);
paintTransform = paintTransform.PreConcat(transform.ToSKMatrix());
}
using (var shader = image.ToShader(tileX, tileY, paintTransform))
{
paintWrapper.Paint.Shader = shader;
}
}
private void ConfigureSceneBrushContent(ref PaintWrapper paintWrapper, ISceneBrushContent content,
Size targetSize)
{
if(content.UseScalableRasterization)
ConfigureSceneBrushContentWithPicture(ref paintWrapper, content, targetSize);
else
ConfigureSceneBrushContentWithSurface(ref paintWrapper, content, targetSize);
}
private void ConfigureSceneBrushContentWithSurface(ref PaintWrapper paintWrapper, ISceneBrushContent content,
Size targetSize)
{
var rect = content.Rect;
var intermediateSize = rect.Size;
if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1)
{
using var intermediate = CreateRenderTarget(intermediateSize, false);
using (var ctx = intermediate.CreateDrawingContext())
{
ctx.RenderOptions = RenderOptions;
ctx.Clear(Colors.Transparent);
content.Render(ctx, rect.TopLeft == default ? null : Matrix.CreateTranslation(-rect.X, -rect.Y));
}
ConfigureTileBrush(ref paintWrapper, targetSize, content.Brush, intermediate);
}
}
private void ConfigureSceneBrushContentWithPicture(ref PaintWrapper paintWrapper, ISceneBrushContent content,
Size targetSize)
{
var rect = content.Rect;
var contentSize = rect.Size;
if (contentSize.Width <= 0 || contentSize.Height <= 0)
{
paintWrapper.Paint.Color = SKColor.Empty;
return;
}
var tileBrush = content.Brush;
var transform = rect.TopLeft == default ? Matrix.Identity : Matrix.CreateTranslation(-rect.X, -rect.Y);
var calc = new TileBrushCalculator(tileBrush, contentSize, targetSize);
transform *= calc.IntermediateTransform;
using var pictureTarget = new PictureRenderTarget(_gpu, _grContext, _dpi);
using (var ctx = pictureTarget.CreateDrawingContext(calc.IntermediateSize))
{
ctx.RenderOptions = RenderOptions;
ctx.PushClip(calc.IntermediateClip);
content.Render(ctx, transform);
ctx.PopClip();
}
using var picture = pictureTarget.GetPicture();
var paintTransform =
tileBrush.TileMode != TileMode.None
? SKMatrix.CreateTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y)
: SKMatrix.CreateIdentity();
SKShaderTileMode tileX =
tileBrush.TileMode == TileMode.None
? SKShaderTileMode.Clamp
: tileBrush.TileMode == TileMode.FlipX || tileBrush.TileMode == TileMode.FlipXY
? SKShaderTileMode.Mirror
: SKShaderTileMode.Repeat;
SKShaderTileMode tileY =
tileBrush.TileMode == TileMode.None
? SKShaderTileMode.Clamp
: tileBrush.TileMode == TileMode.FlipY || tileBrush.TileMode == TileMode.FlipXY
? SKShaderTileMode.Mirror
: SKShaderTileMode.Repeat;
paintTransform = SKMatrix.Concat(paintTransform,
SKMatrix.CreateScale((float)(96.0 / _dpi.X), (float)(96.0 / _dpi.Y)));
if (tileBrush.Transform is { })
{
var origin = tileBrush.TransformOrigin.ToPixels(targetSize);
var offset = Matrix.CreateTranslation(origin);
var brushTransform = (-offset) * tileBrush.Transform.Value * (offset);
paintTransform = paintTransform.PreConcat(brushTransform.ToSKMatrix());
}
using (var shader = picture.ToShader(tileX, tileY, paintTransform,
new SKRect(0, 0, picture.CullRect.Width, picture.CullRect.Height)))
{
paintWrapper.Paint.FilterQuality = SKFilterQuality.None;
paintWrapper.Paint.Shader = shader;
}
}
private static SKColorFilter CreateAlphaColorFilter(double opacity)
{
if (opacity > 1)
opacity = 1;
var c = new byte[256];
var a = new byte[256];
for (var i = 0; i < 256; i++)
{
c[i] = (byte)i;
a[i] = (byte)(i * opacity);
}
return SKColorFilter.CreateTable(a, c, c, c);
}
private static byte Blend(byte leftColor, byte leftAlpha, byte rightColor, byte rightAlpha)
{
var ca = leftColor / 255d;
var aa = leftAlpha / 255d;
var cb = rightColor / 255d;
var ab = rightAlpha / 255d;
var r = (ca * aa + cb * ab * (1 - aa)) / (aa + ab * (1 - aa));
return (byte)(r * 255);
}
private static Color Blend(Color left, Color right)
{
var aa = left.A / 255d;
var ab = right.A / 255d;
return new Color(
(byte)((aa + ab * (1 - aa)) * 255),
Blend(left.R, left.A, right.R, right.A),
Blend(left.G, left.A, right.G, right.A),
Blend(left.B, left.A, right.B, right.A)
);
}
internal PaintWrapper CreateAcrylicPaint (SKPaint paint, IExperimentalAcrylicMaterial material)
{
var paintWrapper = new PaintWrapper(paint);
paint.IsAntialias = true;
var tintOpacity =
material.BackgroundSource == AcrylicBackgroundSource.Digger ?
material.TintOpacity : 1;
const double noiseOpcity = 0.0225;
var tintColor = material.TintColor;
var tint = new SKColor(tintColor.R, tintColor.G, tintColor.B, tintColor.A);
if (s_acrylicNoiseShader == null)
{
using (var stream = typeof(DrawingContextImpl).Assembly.GetManifestResourceStream("Avalonia.Skia.Assets.NoiseAsset_256X256_PNG.png"))
using (var bitmap = SKBitmap.Decode(stream))
{
s_acrylicNoiseShader = SKShader.CreateBitmap(bitmap, SKShaderTileMode.Repeat, SKShaderTileMode.Repeat)
.WithColorFilter(CreateAlphaColorFilter(noiseOpcity));
}
}
using (var backdrop = SKShader.CreateColor(new SKColor(material.MaterialColor.R, material.MaterialColor.G, material.MaterialColor.B, material.MaterialColor.A)))
using (var tintShader = SKShader.CreateColor(tint))
using (var effectiveTint = SKShader.CreateCompose(backdrop, tintShader))
using (var compose = SKShader.CreateCompose(effectiveTint, s_acrylicNoiseShader))
{
paint.Shader = compose;
if (material.BackgroundSource == AcrylicBackgroundSource.Digger)
{
paint.BlendMode = SKBlendMode.Src;
}
return paintWrapper;
}
}
/// <summary>
/// Creates paint wrapper for given brush.
/// </summary>
/// <param name="paint">The paint to wrap.</param>
/// <param name="brush">Source brush.</param>
/// <param name="targetSize">Target size.</param>
/// <returns>Paint wrapper for given brush.</returns>
internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Size targetSize)
{
var paintWrapper = new PaintWrapper(paint);
paint.IsAntialias = RenderOptions.EdgeMode != EdgeMode.Aliased;
double opacity = brush.Opacity * (_useOpacitySaveLayer ? 1 :_currentOpacity);
if (brush is ISolidColorBrush solid)
{
paint.Color = new SKColor(solid.Color.R, solid.Color.G, solid.Color.B, (byte) (solid.Color.A * opacity));
return paintWrapper;
}
paint.Color = new SKColor(255, 255, 255, (byte) (255 * opacity));
if (brush is IGradientBrush gradient)
{
ConfigureGradientBrush(ref paintWrapper, targetSize, gradient);
return paintWrapper;
}
var tileBrush = brush as ITileBrush;
var tileBrushImage = default(IDrawableBitmapImpl);
if (brush is ISceneBrush sceneBrush)
{
using (var content = sceneBrush.CreateContent())
{
if (content != null)
{
ConfigureSceneBrushContent(ref paintWrapper, content, targetSize);
return paintWrapper;
}
else
paint.Color = default;
}
}
else if (brush is ISceneBrushContent sceneBrushContent)
{
ConfigureSceneBrushContent(ref paintWrapper, sceneBrushContent, targetSize);
return paintWrapper;
}
else
{
tileBrushImage = (tileBrush as IImageBrush)?.Source?.Bitmap?.Item as IDrawableBitmapImpl;
}
if (tileBrush != null && tileBrushImage != null)
{
ConfigureTileBrush(ref paintWrapper, targetSize, tileBrush, tileBrushImage);
}
else
{
paint.Color = new SKColor(255, 255, 255, 0);
}
return paintWrapper;
}
/// <summary>
/// Creates paint wrapper for given pen.
/// </summary>
/// <param name="paint">The paint to wrap.</param>
/// <param name="pen">Source pen.</param>
/// <param name="targetSize">Target size.</param>
/// <returns></returns>
private PaintWrapper? TryCreatePaint(SKPaint paint, IPen pen, Size targetSize)
{
// In Skia 0 thickness means - use hairline rendering
// and for us it means - there is nothing rendered.
if (pen.Brush is not { } brush || pen.Thickness == 0d)
{
return null;
}
var rv = CreatePaint(paint, brush, targetSize);
paint.IsStroke = true;
paint.StrokeWidth = (float) pen.Thickness;
// Need to modify dashes due to Skia modifying their lengths
// https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/paths/dots
// TODO: Still something is off, dashes are now present, but don't look the same as D2D ones.
paint.StrokeCap = pen.LineCap.ToSKStrokeCap();
paint.StrokeJoin = pen.LineJoin.ToSKStrokeJoin();
paint.StrokeMiter = (float) pen.MiterLimit;
if (DrawingContextHelper.TryCreateDashEffect(pen, out var dashEffect))
{
paint.PathEffect = dashEffect;
rv.AddDisposable(dashEffect);
}
return rv;
}
/// <summary>
/// Create new render target compatible with this drawing context.
/// </summary>
/// <param name="size">The size of the render target in DIPs.</param>
/// <param name="isLayer">Whether the render target is being created for a layer.</param>
/// <param name="format">Pixel format.</param>
/// <returns></returns>
private SurfaceRenderTarget CreateRenderTarget(Size size, bool isLayer, PixelFormat? format = null)
{
var pixelSize = PixelSize.FromSizeWithDpi(size, _dpi);
var createInfo = new SurfaceRenderTarget.CreateInfo
{
Width = pixelSize.Width,
Height = pixelSize.Height,
Dpi = _dpi,
Format = format,
DisableTextLcdRendering = isLayer ? _disableSubpixelTextRendering : true,
GrContext = _grContext,
Gpu = _gpu,
Session = _session,
DisableManualFbo = !isLayer,
};
return new SurfaceRenderTarget(createInfo);
}
/// <summary>
/// Skia cached paint state.
/// </summary>
private readonly struct PaintState : IDisposable
{
private readonly SKColor _color;
private readonly SKShader _shader;
private readonly SKPaint _paint;
public PaintState(SKPaint paint, SKColor color, SKShader shader)
{
_paint = paint;
_color = color;
_shader = shader;
}
/// <inheritdoc />
public void Dispose()
{
_paint.Color = _color;
_paint.Shader = _shader;
}
}
/// <summary>
/// Skia paint wrapper.
/// </summary>
internal struct PaintWrapper : IDisposable
{
//We are saving memory allocations there
public readonly SKPaint Paint;
private IDisposable? _disposable1;
private IDisposable? _disposable2;
private IDisposable? _disposable3;
public PaintWrapper(SKPaint paint)
{
Paint = paint;
_disposable1 = null;
_disposable2 = null;
_disposable3 = null;
}
public IDisposable ApplyTo(SKPaint paint)
{
var state = new PaintState(paint, paint.Color, paint.Shader);
paint.Color = Paint.Color;
paint.Shader = Paint.Shader;
return state;
}
/// <summary>
/// Add new disposable to a wrapper.
/// </summary>
/// <param name="disposable">Disposable to add.</param>
public void AddDisposable(IDisposable disposable)
{
if (_disposable1 == null)
{
_disposable1 = disposable;
}
else if (_disposable2 == null)
{
_disposable2 = disposable;
}
else if (_disposable3 == null)
{
_disposable3 = disposable;
}
else
{
Debug.Assert(false);
// ReSharper disable once HeuristicUnreachableCode
throw new InvalidOperationException(
"PaintWrapper disposable object limit reached. You need to add extra struct fields to support more disposables.");
}
}
/// <inheritdoc />
public void Dispose()
{
Paint?.Reset();
_disposable1?.Dispose();
_disposable2?.Dispose();
_disposable3?.Dispose();
}
}
}
}