using System; using System.Collections.Generic; using Avalonia.Platform; using Avalonia.Rendering.SceneGraph; using Avalonia.Threading; using Avalonia.Utilities; using Avalonia.Media.Imaging; using System.ComponentModel; namespace Avalonia.Media { public abstract class DrawingContext : IDisposable { private static ThreadSafeObjectPool> StateStackPool { get; } = ThreadSafeObjectPool>.Default; private Stack? _states; internal DrawingContext() { } public void Dispose() { if (_states != null) { while (_states.Count > 0) _states.Pop().Dispose(); StateStackPool.ReturnAndSetNull(ref _states); } DisposeCore(); } protected abstract void DisposeCore(); /// /// Draws an image. /// /// The image. /// The rect in the output to draw to. public virtual void DrawImage(IImage source, Rect rect) { _ = source ?? throw new ArgumentNullException(nameof(source)); DrawImage(source, new Rect(source.Size), rect); } /// /// Draws an image. /// /// The image. /// The rect in the image to draw. /// The rect in the output to draw to. /// The bitmap interpolation mode. public virtual void DrawImage(IImage source, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default) { _ = source ?? throw new ArgumentNullException(nameof(source)); source.Draw(this, sourceRect, destRect, bitmapInterpolationMode); } /// /// Draws a platform-specific bitmap impl. /// /// The bitmap image. /// The opacity to draw with. /// The rect in the image to draw. /// The rect in the output to draw to. /// The bitmap interpolation mode. internal abstract void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default); /// /// Draws a line. /// /// The stroke pen. /// The first point of the line. /// The second point of the line. public void DrawLine(IPen pen, Point p1, Point p2) { if (PenIsVisible(pen)) DrawLineCore(pen, p1, p2); } protected abstract void DrawLineCore(IPen pen, Point p1, Point p2); /// /// Draws a geometry. /// /// The fill brush. /// The stroke pen. /// The geometry. public void DrawGeometry(IBrush? brush, IPen? pen, Geometry geometry) { if ((brush != null || PenIsVisible(pen)) && geometry.PlatformImpl != null) DrawGeometryCore(brush, pen, geometry.PlatformImpl); } /// /// Draws a geometry. /// /// The fill brush. /// The stroke pen. /// The geometry. public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) { if ((brush != null || PenIsVisible(pen))) DrawGeometryCore(brush, pen, geometry); } protected abstract void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry); /// /// Draws a rectangle with the specified Brush and Pen. /// /// The brush used to fill the rectangle, or null for no fill. /// The pen used to stroke the rectangle, or null for no stroke. /// The rectangle bounds. /// The radius in the X dimension of the rounded corners. /// This value will be clamped to the range of 0 to Width/2 /// /// The radius in the Y dimension of the rounded corners. /// This value will be clamped to the range of 0 to Height/2 /// /// Box shadow effect parameters /// /// The brush and the pen can both be null. If the brush is null, then no fill is performed. /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. /// public void DrawRectangle(IBrush? brush, IPen? pen, Rect rect, double radiusX = 0, double radiusY = 0, BoxShadows boxShadows = default) { if (brush == null && !PenIsVisible(pen) && boxShadows.Count == 0) return; if (!MathUtilities.IsZero(radiusX)) { radiusX = Math.Min(radiusX, rect.Width / 2); } if (!MathUtilities.IsZero(radiusY)) { radiusY = Math.Min(radiusY, rect.Height / 2); } DrawRectangleCore(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadows); } /// /// Draws a rectangle with the specified Brush and Pen. /// /// The brush used to fill the rectangle, or null for no fill. /// The pen used to stroke the rectangle, or null for no stroke. /// The rectangle bounds. /// Box shadow effect parameters /// /// The brush and the pen can both be null. If the brush is null, then no fill is performed. /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. /// public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rrect, BoxShadows boxShadows = default) { if (brush == null && !PenIsVisible(pen) && boxShadows.Count == 0) return; DrawRectangleCore(brush, pen, rrect, boxShadows); } protected abstract void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect, BoxShadows boxShadows = default); /// /// Draws the outline of a rectangle. /// /// The pen. /// The rectangle bounds. /// The corner radius. public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f) => DrawRectangle(null, pen, rect, cornerRadius, cornerRadius); /// /// Draws a filled rectangle. /// /// The brush. /// The rectangle bounds. /// The corner radius. public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f) => DrawRectangle(brush, null, rect, cornerRadius, cornerRadius); /// /// Draws an ellipse with the specified Brush and Pen. /// /// The brush used to fill the ellipse, or null for no fill. /// The pen used to stroke the ellipse, or null for no stroke. /// The location of the center of the ellipse. /// The horizontal radius of the ellipse. /// The vertical radius of the ellipse. /// /// The brush and the pen can both be null. If the brush is null, then no fill is performed. /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. /// public void DrawEllipse(IBrush? brush, IPen? pen, Point center, double radiusX, double radiusY) { if (brush != null || PenIsVisible(pen)) { var originX = center.X - radiusX; var originY = center.Y - radiusY; var width = radiusX * 2; var height = radiusY * 2; DrawEllipseCore(brush, pen, new Rect(originX, originY, width, height)); } } /// /// Draws an ellipse with the specified Brush and Pen. /// /// The brush used to fill the ellipse, or null for no fill. /// The pen used to stroke the ellipse, or null for no stroke. /// The bounding rect. /// /// The brush and the pen can both be null. If the brush is null, then no fill is performed. /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. /// public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) { if (brush != null || PenIsVisible(pen)) DrawEllipseCore(brush, pen, rect); } protected abstract void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect); /// /// Draws a custom drawing operation /// /// custom operation public abstract void Custom(ICustomDrawOperation custom); /// /// Draws text. /// /// The upper-left corner of the text. /// The text. public virtual void DrawText(FormattedText text, Point origin) { _ = text ?? throw new ArgumentNullException(nameof(text)); text.Draw(this, origin); } /// /// Draws a glyph run. /// /// The foreground brush. /// The glyph run. public abstract void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun); public record struct PushedState : IDisposable { private readonly DrawingContext _context; private readonly int _level; public PushedState(DrawingContext context) { _context = context; _level = _context._states!.Count; } public void Dispose() { if(_context?._states == null) return; if(_context._states.Count != _level) throw new InvalidOperationException("Wrong Push/Pop state order"); _context._states.Pop().Dispose(); } } private readonly record struct RestoreState : IDisposable { private readonly DrawingContext _context; private readonly PushedStateType _type; public enum PushedStateType { None, Transform, Opacity, Clip, GeometryClip, OpacityMask, BitmapBlendMode } public RestoreState(DrawingContext context, PushedStateType type) { _context = context; _type = type; } public void Dispose() { if (_type == PushedStateType.None) return; if (_context._states is null) throw new ObjectDisposedException(nameof(DrawingContext)); if (_type == PushedStateType.Transform) _context.PopTransformCore(); else if (_type == PushedStateType.Clip) _context.PopClipCore(); else if (_type == PushedStateType.Opacity) _context.PopOpacityCore(); else if (_type == PushedStateType.GeometryClip) _context.PopGeometryClipCore(); else if (_type == PushedStateType.OpacityMask) _context.PopOpacityMaskCore(); else if (_type == PushedStateType.BitmapBlendMode) _context.PopBitmapBlendModeCore(); } } /// /// Pushes a clip rectangle. /// /// The clip rectangle. /// A disposable used to undo the clip rectangle. public PushedState PushClip(RoundedRect clip) { PushClipCore(clip); _states ??= StateStackPool.Get(); _states.Push(new RestoreState(this, RestoreState.PushedStateType.Clip)); return new PushedState(this); } protected abstract void PushClipCore(RoundedRect rect); /// /// Pushes a clip rectangle. /// /// The clip rectangle. /// A disposable used to undo the clip rectangle. public PushedState PushClip(Rect clip) { PushClipCore(clip); _states ??= StateStackPool.Get(); _states.Push(new RestoreState(this, RestoreState.PushedStateType.Clip)); return new PushedState(this); } protected abstract void PushClipCore(Rect rect); /// /// Pushes a clip geometry. /// /// The clip geometry. /// A disposable used to undo the clip geometry. public PushedState PushGeometryClip(Geometry clip) { PushGeometryClipCore(clip); _states ??= StateStackPool.Get(); _states.Push(new RestoreState(this, RestoreState.PushedStateType.GeometryClip)); return new PushedState(this); } protected abstract void PushGeometryClipCore(Geometry clip); /// /// Pushes an opacity value. /// /// The opacity. /// The bounds. /// A disposable used to undo the opacity. public PushedState PushOpacity(double opacity, Rect bounds) { PushOpacityCore(opacity, bounds); _states ??= StateStackPool.Get(); _states.Push(new RestoreState(this, RestoreState.PushedStateType.Opacity)); return new PushedState(this); } protected abstract void PushOpacityCore(double opacity, Rect bounds); /// /// Pushes an opacity mask. /// /// The opacity mask. /// /// The size of the brush's target area. TODO: Are we sure this is needed? /// /// A disposable to undo the opacity mask. public PushedState PushOpacityMask(IBrush mask, Rect bounds) { PushOpacityMaskCore(mask, bounds); _states ??= StateStackPool.Get(); _states.Push(new RestoreState(this, RestoreState.PushedStateType.OpacityMask)); return new PushedState(this); } protected abstract void PushOpacityMaskCore(IBrush mask, Rect bounds); public PushedState PushBitmapBlendMode(BitmapBlendingMode blendingMode) { PushBitmapBlendMode(blendingMode); _states ??= StateStackPool.Get(); _states.Push(new RestoreState(this, RestoreState.PushedStateType.BitmapBlendMode)); return new PushedState(this); } protected abstract void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode); /// /// Pushes a matrix transformation. /// /// The matrix /// A disposable used to undo the transformation. public PushedState PushTransform(Matrix matrix) { PushTransformCore(matrix); _states ??= StateStackPool.Get(); _states.Push(new RestoreState(this, RestoreState.PushedStateType.Transform)); return new PushedState(this); } [Obsolete("Use PushTransform"), EditorBrowsable(EditorBrowsableState.Never)] public PushedState PushPreTransform(Matrix matrix) => PushTransform(matrix); [Obsolete("Use PushTransform"), EditorBrowsable(EditorBrowsableState.Never)] public PushedState PushPostTransform(Matrix matrix) => PushTransform(matrix); [Obsolete("Use PushTransform"), EditorBrowsable(EditorBrowsableState.Never)] public PushedState PushTransformContainer() => PushTransform(Matrix.Identity); protected abstract void PushTransformCore(Matrix matrix); protected abstract void PopClipCore(); protected abstract void PopGeometryClipCore(); protected abstract void PopOpacityCore(); protected abstract void PopOpacityMaskCore(); protected abstract void PopBitmapBlendModeCore(); protected abstract void PopTransformCore(); private static bool PenIsVisible(IPen? pen) { return pen?.Brush != null && pen.Thickness > 0; } } }