File: Platform\iOS\MauiCALayer.cs
Web Access
Project: src\src\Core\src\Core.csproj (Microsoft.Maui)
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using CoreAnimation;
using CoreGraphics;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Graphics.Platform;
using UIKit;
 
namespace Microsoft.Maui.Platform
{
	public class MauiCALayer : CALayer, IAutoSizableCALayer
	{
		CGRect _bounds;
		[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "IShape is a non-NSObject in MAUI.")]
		IShape? _shape;
 
		UIColor? _backgroundColor;
		Paint? _background;
 
		float _strokeThickness;
		UIColor? _strokeColor;
		Paint? _stroke;
 
		CGLineCap _strokeLineCap;
		CGLineJoin _strokeLineJoin;
 
		nfloat[]? _strokeDash;
		nfloat _strokeDashOffset;
 
		nfloat _strokeMiterLimit;
 
		[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Proven safe in CALayerAutosizeObserver_DoesNotLeak test.")]
		CALayerAutosizeObserver? _boundsObserver;
 
		public MauiCALayer()
		{
			_bounds = new CGRect();
			ContentsScale = UIScreen.MainScreen.Scale;
		}
 
		protected override void Dispose(bool disposing)
		{
			_boundsObserver?.Dispose();
			_boundsObserver = null;
			base.Dispose(disposing);
		}
 
		public override void RemoveFromSuperLayer()
		{
			_boundsObserver?.Dispose();
			_boundsObserver = null;
			base.RemoveFromSuperLayer();
		}
 
		void IAutoSizableCALayer.AutoSizeToSuperLayer()
		{
			_boundsObserver?.Dispose();
			_boundsObserver = CALayerAutosizeObserver.Attach(this);
		}
 
		public override void AddAnimation(CAAnimation animation, string? key)
		{
			// Do nothing, we don't want animations here
		}
 
		public override void LayoutSublayers()
		{
			base.LayoutSublayers();
 
			// If the super layer's frame is zero, indicating an off-screen rendering scenario, 
			// the bounds are intentionally kept at zero to avoid incorrect initial drawing 
			// caused by bounds matching the screen size.
			var bounds = SuperLayer?.Frame == CGRect.Empty ? CGRect.Empty : Bounds;
 
			if (bounds.Equals(_bounds))
			{
				return;
			}
 
			_bounds = new CGRect(bounds.Location, bounds.Size);
		}
 
		public override void DrawInContext(CGContext ctx)
		{
			base.DrawInContext(ctx);
 
			var clipPath = GetClipPath();
 
			if (clipPath! != null!)
				ctx.AddPath(clipPath);
 
			ctx.Clip();
 
			DrawBackground(ctx);
			DrawBorder(ctx);
		}
 
		public void SetBorderShape(IShape? shape)
		{
			_shape = shape;
 
			SetNeedsDisplay();
		}
 
		public void SetBackground(Paint? paint)
		{
			if (paint is SolidPaint solidPaint)
				SetBackground(solidPaint);
			else if (paint is LinearGradientPaint linearGradientPaint)
				SetBackground(linearGradientPaint);
			else if (paint is RadialGradientPaint radialGradientPaint)
				SetBackground(radialGradientPaint);
			else if (paint is ImagePaint imagePaint)
				SetBackground(imagePaint);
			else if (paint is PatternPaint patternPaint)
				SetBackground(patternPaint);
			else
				SetDefaultBackgroundColor();
		}
 
		public void SetBackground(SolidPaint solidPaint)
		{
			if (solidPaint.Color == null)
				SetDefaultBackgroundColor();
			else
				_backgroundColor = solidPaint.Color.ToPlatform();
 
			_background = null;
 
			SetNeedsDisplay();
		}
 
		public void SetBackground(LinearGradientPaint linearGradientPaint)
		{
			_backgroundColor = null;
			_background = linearGradientPaint;
 
			SetNeedsDisplay();
		}
 
		public void SetBackground(RadialGradientPaint radialGradientPaint)
		{
			_backgroundColor = null;
			_background = radialGradientPaint;
 
			SetNeedsDisplay();
		}
 
		public void SetBackground(ImagePaint imagePaint)
		{
			throw new NotImplementedException();
		}
 
		public void SetBackground(PatternPaint patternPaint)
		{
			throw new NotImplementedException();
		}
 
		public void SetBorderBrush(Paint? paint)
		{
			if (paint is SolidPaint solidPaint)
				SetBorderBrush(solidPaint);
 
			if (paint is LinearGradientPaint linearGradientPaint)
				SetBorderBrush(linearGradientPaint);
 
			if (paint is RadialGradientPaint radialGradientPaint)
				SetBorderBrush(radialGradientPaint);
 
			if (paint is ImagePaint imagePaint)
				SetBorderBrush(imagePaint);
 
			if (paint is PatternPaint patternPaint)
				SetBorderBrush(patternPaint);
		}
 
		public void SetBorderBrush(SolidPaint solidPaint)
		{
			_strokeColor = solidPaint.Color == null
				? UIColor.Clear
				: solidPaint.Color.ToPlatform();
 
			_stroke = null;
 
			SetNeedsDisplay();
		}
 
		public void SetBorderBrush(LinearGradientPaint linearGradientPaint)
		{
			_strokeColor = null;
			_stroke = linearGradientPaint;
 
			SetNeedsDisplay();
		}
 
		public void SetBorderBrush(RadialGradientPaint radialGradientPaint)
		{
			_strokeColor = null;
			_stroke = radialGradientPaint;
 
			SetNeedsDisplay();
		}
 
		public void SetBorderBrush(ImagePaint imagePaint)
		{
			throw new NotImplementedException();
		}
 
		public void SetBorderBrush(PatternPaint patternPaint)
		{
			throw new NotImplementedException();
		}
 
		public void SetBorderWidth(double borderWidth)
		{
			_strokeThickness = (float)borderWidth;
 
			SetNeedsDisplay();
		}
 
		public void SetBorderDash(float[]? borderDashArray, double borderDashOffset)
		{
			_strokeDashOffset = (float)borderDashOffset;
 
			if (borderDashArray != null && borderDashArray.Length > 0)
			{
				nfloat[] dashArray;
				double[] array;
 
				if (borderDashArray.Length % 2 == 0)
				{
					array = new double[borderDashArray.Length];
					dashArray = new nfloat[borderDashArray.Length];
					borderDashArray.CopyTo(array, 0);
				}
				else
				{
					array = new double[2 * borderDashArray.Length];
					dashArray = new nfloat[2 * borderDashArray.Length];
					borderDashArray.CopyTo(array, 0);
					borderDashArray.CopyTo(array, borderDashArray.Length);
				}
 
				double thickness = _strokeThickness;
 
				for (int i = 0; i < array.Length; i++)
					dashArray[i] = new nfloat(thickness * array[i]);
 
				_strokeDash = dashArray;
			}
 
			SetNeedsDisplay();
		}
 
		public void SetBorderMiterLimit(float strokeMiterLimit)
		{
			_strokeMiterLimit = strokeMiterLimit;
 
			SetNeedsDisplay();
		}
 
		public void SetBorderLineJoin(LineJoin lineJoin)
		{
			CGLineJoin iLineJoin = CGLineJoin.Miter;
 
			switch (lineJoin)
			{
				case LineJoin.Miter:
					iLineJoin = CGLineJoin.Miter;
					break;
				case LineJoin.Bevel:
					iLineJoin = CGLineJoin.Bevel;
					break;
				case LineJoin.Round:
					iLineJoin = CGLineJoin.Round;
					break;
			}
 
			_strokeLineJoin = iLineJoin;
 
			SetNeedsDisplay();
		}
 
		public void SetBorderLineCap(LineCap lineCap)
		{
			CGLineCap iLineCap = CGLineCap.Butt;
 
			switch (lineCap)
			{
				case LineCap.Butt:
					iLineCap = CGLineCap.Butt;
					break;
				case LineCap.Square:
					iLineCap = CGLineCap.Square;
					break;
				case LineCap.Round:
					iLineCap = CGLineCap.Round;
					break;
			}
 
			_strokeLineCap = iLineCap;
 
			SetNeedsDisplay();
		}
 
		CGPath? GetClipPath()
		{
			if (_shape != null)
			{
				var bounds = _bounds.ToRectangle();
				var path = _shape.PathForBounds(bounds);
				return path?.AsCGPath();
			}
 
			return null;
		}
 
		void SetDefaultBackgroundColor()
		{
			_backgroundColor = UIColor.Clear;
		}
 
		void DrawBackground(CGContext ctx)
		{
			if (_background != null)
				DrawGradientPaint(ctx, _background);
			else if (_backgroundColor != null)
			{
				ctx.SetFillColor(_backgroundColor.CGColor);
				var clipPath = GetClipPath();
 
				if (clipPath! != null!)
					ctx.AddPath(clipPath);
 
				ctx.DrawPath(CGPathDrawingMode.Fill);
			}
		}
 
		void DrawBorder(CGContext ctx)
		{
			if (_strokeThickness <= 0)
				return;
 
			if (IsBorderDashed())
				ctx.SetLineDash(_strokeDashOffset * _strokeThickness, _strokeDash);
 
			// The Stroke is inner and we are clipping the outer, for that reason, we use the double to get the correct value.
			ctx.SetLineWidth(2 * _strokeThickness);
 
			ctx.SetLineCap(_strokeLineCap);
			ctx.SetLineJoin(_strokeLineJoin);
			ctx.SetMiterLimit(_strokeMiterLimit * _strokeThickness / 4);
 
			var clipPath = GetClipPath();
 
			if (clipPath! != null!)
				ctx.AddPath(clipPath);
 
			if (_stroke != null)
			{
				ctx.ReplacePathWithStrokedPath();
				ctx.Clip();
 
				DrawGradientPaint(ctx, _stroke);
			}
			else if (_strokeColor != null)
			{
				ctx.SetStrokeColor(_strokeColor.CGColor);
				ctx.DrawPath(CGPathDrawingMode.Stroke);
			}
		}
 
		void DrawGradientPaint(CGContext graphics, Paint paint)
		{
			if (paint == null)
				return;
 
			if (paint is GradientPaint gradientPaint)
			{
				using (CGColorSpace rgb = CGColorSpace.CreateDeviceRGB())
				{
					CGColor[] colors = new CGColor[gradientPaint.GradientStops.Length];
					nfloat[] locations = new nfloat[gradientPaint.GradientStops.Length];
 
					for (int index = 0; index < gradientPaint.GradientStops.Length; index++)
					{
						Graphics.Color color = gradientPaint.GradientStops[index].Color;
						colors[index] = new CGColor(new nfloat(color.Red), new nfloat(color.Green), new nfloat(color.Blue), new nfloat(color.Alpha));
						locations[index] = new nfloat(gradientPaint.GradientStops[index].Offset);
					}
 
					CGGradient gradient = new CGGradient(rgb, colors, locations);
 
					if (gradientPaint is LinearGradientPaint linearGradientPaint)
					{
						graphics.DrawLinearGradient(
							gradient,
							new CGPoint(_bounds.Left + linearGradientPaint.StartPoint.X * _bounds.Width, _bounds.Top + linearGradientPaint.StartPoint.Y * _bounds.Height),
							new CGPoint(_bounds.Left + linearGradientPaint.EndPoint.X * _bounds.Width, _bounds.Top + linearGradientPaint.EndPoint.Y * _bounds.Height),
							CGGradientDrawingOptions.DrawsBeforeStartLocation | CGGradientDrawingOptions.DrawsAfterEndLocation);
					}
 
					if (gradientPaint is RadialGradientPaint radialGradientPaint)
					{
						graphics.DrawRadialGradient(
							gradient,
							new CGPoint(radialGradientPaint.Center.X * _bounds.Width + _bounds.Left, radialGradientPaint.Center.Y * _bounds.Height + _bounds.Top),
							0.0f,
							new CGPoint(radialGradientPaint.Center.X * _bounds.Width + _bounds.Left, radialGradientPaint.Center.Y * _bounds.Height + _bounds.Top),
							(nfloat)(radialGradientPaint.Radius * Math.Max(_bounds.Height, _bounds.Width)),
							CGGradientDrawingOptions.DrawsBeforeStartLocation | CGGradientDrawingOptions.DrawsAfterEndLocation);
					}
				}
			}
		}
 
		bool IsBorderDashed()
		{
			return _strokeDash != null && _strokeDashOffset > 0;
		}
	}
}