Browse Source
* Add render data opcode enum First step of the Drawing/Nodes binary stream refactor. * Add render data resource table * Add render data stream writer and reader Encodes the opcode stream field-by-field via BinaryPrimitives to avoid unsafe and blittability assumptions. * Add RenderDataStream with recording and replay * Add hit-test walker to RenderDataStream * Add bounds walker to RenderDataStream * Add render data stream serialization * Switch render data to the binary stream * Move node level test coverage to the stream * Delete the render data node classes * Small optimization, stackalloc the render data walker scope stacks * Optimize serialization for blittable types * Encode render data via payload structs, add render data visitor * Rename RenderDataResources.Add to AppendDeserialized * Add effect support to render data stream * Simplify render data read/write with MemoryMarshalpull/21591/head
committed by
GitHub
37 changed files with 2642 additions and 1183 deletions
@ -0,0 +1,30 @@ |
|||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Media.Imaging; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Rendering.SceneGraph; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Rendering.Composition.Drawing; |
||||
|
|
||||
|
internal interface IRenderDataVisitor<TScope> where TScope : unmanaged |
||||
|
{ |
||||
|
bool StopVisiting { get; } |
||||
|
|
||||
|
void OnDrawLine(IPen? serverPen, IPen? clientPen, Point p1, Point p2); |
||||
|
void OnDrawRectangle(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, RoundedRect rect, BoxShadows boxShadows); |
||||
|
void OnDrawEllipse(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, Rect rect); |
||||
|
void OnDrawGeometry(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, IGeometryImpl? geometry); |
||||
|
void OnDrawGlyphRun(IBrush? serverBrush, IRef<IGlyphRunImpl>? glyphRun); |
||||
|
void OnDrawBitmap(IRef<IBitmapImpl>? bitmap, double opacity, Rect sourceRect, Rect destRect); |
||||
|
void OnDrawCustom(ICustomDrawOperation? operation); |
||||
|
|
||||
|
TScope OnPushClip(RoundedRect clip); |
||||
|
TScope OnPushGeometryClip(IGeometryImpl? geometry); |
||||
|
TScope OnPushOpacity(double opacity); |
||||
|
TScope OnPushOpacityMask(IBrush? brush, Rect bounds); |
||||
|
TScope OnPushTransform(Matrix matrix); |
||||
|
TScope OnPushRenderOptions(RenderOptions options); |
||||
|
TScope OnPushTextOptions(TextOptions options); |
||||
|
TScope OnPushEffect(IEffect? effect, Rect bounds); |
||||
|
void OnPop(in TScope scope); |
||||
|
} |
||||
@ -1,28 +0,0 @@ |
|||||
using System; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.Utilities; |
|
||||
|
|
||||
namespace Avalonia.Rendering.Composition.Drawing.Nodes; |
|
||||
|
|
||||
class RenderDataBitmapNode : IRenderDataItem, IDisposable |
|
||||
{ |
|
||||
public IRef<IBitmapImpl>? Bitmap { get; set; } |
|
||||
public double Opacity { get; set; } |
|
||||
public Rect SourceRect { get; set; } |
|
||||
public Rect DestRect { get; set; } |
|
||||
|
|
||||
public bool HitTest(Point p) => DestRect.Contains(p); |
|
||||
|
|
||||
public void Invoke(ref RenderDataNodeRenderContext context) |
|
||||
{ |
|
||||
if (Bitmap != null) |
|
||||
context.Context.DrawBitmap(Bitmap.Item, Opacity, SourceRect, DestRect); |
|
||||
} |
|
||||
|
|
||||
public Rect? Bounds => DestRect; |
|
||||
public void Dispose() |
|
||||
{ |
|
||||
Bitmap?.Dispose(); |
|
||||
Bitmap = null; |
|
||||
} |
|
||||
} |
|
||||
@ -1,61 +0,0 @@ |
|||||
using System; |
|
||||
using Avalonia.Media; |
|
||||
using Avalonia.Platform; |
|
||||
|
|
||||
namespace Avalonia.Rendering.Composition.Drawing.Nodes; |
|
||||
|
|
||||
class RenderDataEllipseNode :RenderDataBrushAndPenNode |
|
||||
{ |
|
||||
public Rect Rect { get; set; } |
|
||||
|
|
||||
bool Contains(double dx, double dy, double radiusX, double radiusY) |
|
||||
{ |
|
||||
var rx2 = radiusX * radiusX; |
|
||||
var ry2 = radiusY * radiusY; |
|
||||
|
|
||||
var distance = ry2 * dx * dx + rx2 * dy * dy; |
|
||||
|
|
||||
return distance <= rx2 * ry2; |
|
||||
} |
|
||||
|
|
||||
public override bool HitTest(Point p) |
|
||||
{ |
|
||||
var center = Rect.Center; |
|
||||
|
|
||||
var strokeThickness = ClientPen?.Thickness ?? 0; |
|
||||
|
|
||||
var rx = Rect.Width / 2 + strokeThickness / 2; |
|
||||
var ry = Rect.Height / 2 + strokeThickness / 2; |
|
||||
|
|
||||
var dx = p.X - center.X; |
|
||||
var dy = p.Y - center.Y; |
|
||||
|
|
||||
if (Math.Abs(dx) > rx || Math.Abs(dy) > ry) |
|
||||
{ |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
if (ServerBrush != null) |
|
||||
{ |
|
||||
return Contains(dx, dy, rx, ry); |
|
||||
} |
|
||||
else if (strokeThickness > 0) |
|
||||
{ |
|
||||
bool inStroke = Contains(dx, dy, rx, ry); |
|
||||
|
|
||||
rx = Rect.Width / 2 - strokeThickness / 2; |
|
||||
ry = Rect.Height / 2 - strokeThickness / 2; |
|
||||
|
|
||||
bool inInner = Contains(dx, dy, rx, ry); |
|
||||
|
|
||||
return inStroke && !inInner; |
|
||||
} |
|
||||
|
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
public override void Invoke(ref RenderDataNodeRenderContext context) => |
|
||||
context.Context.DrawEllipse(ServerBrush, ServerPen, Rect); |
|
||||
|
|
||||
public override Rect? Bounds => Rect.Inflate(ServerPen?.Thickness ?? 0); |
|
||||
} |
|
||||
@ -1,29 +0,0 @@ |
|||||
using System.Diagnostics; |
|
||||
using Avalonia.Media; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.Rendering.SceneGraph; |
|
||||
|
|
||||
namespace Avalonia.Rendering.Composition.Drawing.Nodes; |
|
||||
|
|
||||
class RenderDataGeometryNode : RenderDataBrushAndPenNode |
|
||||
{ |
|
||||
public IGeometryImpl? Geometry { get; set; } |
|
||||
|
|
||||
public override bool HitTest(Point p) |
|
||||
{ |
|
||||
if (Geometry == null) |
|
||||
return false; |
|
||||
|
|
||||
return (ServerBrush != null // null check is safe
|
|
||||
&& Geometry.FillContains(p)) || |
|
||||
(ClientPen != null && Geometry.StrokeContains(ClientPen, p)); |
|
||||
} |
|
||||
|
|
||||
public override void Invoke(ref RenderDataNodeRenderContext context) |
|
||||
{ |
|
||||
Debug.Assert(Geometry != null); |
|
||||
context.Context.DrawGeometry(ServerBrush, ServerPen, Geometry!); |
|
||||
} |
|
||||
|
|
||||
public override Rect? Bounds => Geometry?.GetRenderBounds(ServerPen) ?? default; |
|
||||
} |
|
||||
@ -1,35 +0,0 @@ |
|||||
using System; |
|
||||
using System.Diagnostics; |
|
||||
using Avalonia.Media; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.Utilities; |
|
||||
|
|
||||
namespace Avalonia.Rendering.Composition.Drawing.Nodes; |
|
||||
|
|
||||
class RenderDataGlyphRunNode : IRenderDataItemWithServerResources, IDisposable |
|
||||
{ |
|
||||
public IBrush? ServerBrush { get; set; } |
|
||||
// Dispose only happens once, so it's safe to have one reference
|
|
||||
public IRef<IGlyphRunImpl>? GlyphRun { get; set; } |
|
||||
|
|
||||
public bool HitTest(Point p) => GlyphRun?.Item.Bounds.ContainsExclusive(p) ?? false; |
|
||||
|
|
||||
public void Invoke(ref RenderDataNodeRenderContext context) |
|
||||
{ |
|
||||
Debug.Assert(GlyphRun!.Item != null); |
|
||||
context.Context.DrawGlyphRun(ServerBrush, GlyphRun.Item); |
|
||||
} |
|
||||
|
|
||||
public Rect? Bounds => GlyphRun?.Item?.Bounds ?? default; |
|
||||
|
|
||||
public void Collect(IRenderDataServerResourcesCollector collector) |
|
||||
{ |
|
||||
collector.AddRenderDataServerResource(ServerBrush); |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
GlyphRun?.Dispose(); |
|
||||
GlyphRun = null; |
|
||||
} |
|
||||
} |
|
||||
@ -1,65 +0,0 @@ |
|||||
using System; |
|
||||
using Avalonia.Media; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.Rendering.SceneGraph; |
|
||||
|
|
||||
namespace Avalonia.Rendering.Composition.Drawing.Nodes; |
|
||||
|
|
||||
class RenderDataLineNode : IRenderDataItemWithServerResources |
|
||||
{ |
|
||||
public IPen? ServerPen { get; set; } |
|
||||
public IPen? ClientPen { get; set; } |
|
||||
public Point P1 { get; set; } |
|
||||
public Point P2 { get; set; } |
|
||||
|
|
||||
public bool HitTest(Point p) |
|
||||
{ |
|
||||
if (ClientPen == null) |
|
||||
return false; |
|
||||
var halfThickness = ClientPen.Thickness / 2; |
|
||||
var minX = Math.Min(P1.X, P2.X) - halfThickness; |
|
||||
var maxX = Math.Max(P1.X, P2.X) + halfThickness; |
|
||||
var minY = Math.Min(P1.Y, P2.Y) - halfThickness; |
|
||||
var maxY = Math.Max(P1.Y, P2.Y) + halfThickness; |
|
||||
|
|
||||
if (p.X < minX || p.X > maxX || p.Y < minY || p.Y > maxY) |
|
||||
return false; |
|
||||
|
|
||||
var a = P1; |
|
||||
var b = P2; |
|
||||
|
|
||||
//If dot1 or dot2 is negative, then the angle between the perpendicular and the segment is obtuse.
|
|
||||
//The distance from a point to a straight line is defined as the
|
|
||||
//length of the vector formed by the point and the closest point of the segment
|
|
||||
|
|
||||
Vector ap = p - a; |
|
||||
var dot1 = Vector.Dot(b - a, ap); |
|
||||
|
|
||||
if (dot1 < 0) |
|
||||
return ap.Length <= ClientPen.Thickness / 2; |
|
||||
|
|
||||
Vector bp = p - b; |
|
||||
var dot2 = Vector.Dot(a - b, bp); |
|
||||
|
|
||||
if (dot2 < 0) |
|
||||
return bp.Length <= halfThickness; |
|
||||
|
|
||||
var bXaX = b.X - a.X; |
|
||||
var bYaY = b.Y - a.Y; |
|
||||
|
|
||||
var distance = (bXaX * (p.Y - a.Y) - bYaY * (p.X - a.X)) / |
|
||||
(Math.Sqrt(bXaX * bXaX + bYaY * bYaY)); |
|
||||
|
|
||||
return Math.Abs(distance) <= halfThickness; |
|
||||
} |
|
||||
|
|
||||
|
|
||||
public void Invoke(ref RenderDataNodeRenderContext context) |
|
||||
=> context.Context.DrawLine(ServerPen, P1, P2); |
|
||||
|
|
||||
public Rect? Bounds => LineBoundsHelper.CalculateBounds(P1, P2, ServerPen!); |
|
||||
public void Collect(IRenderDataServerResourcesCollector collector) |
|
||||
{ |
|
||||
collector.AddRenderDataServerResource(ServerPen); |
|
||||
} |
|
||||
} |
|
||||
@ -1,294 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using Avalonia.Media; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.Rendering.SceneGraph; |
|
||||
using Avalonia.Threading; |
|
||||
using Avalonia.Utilities; |
|
||||
|
|
||||
namespace Avalonia.Rendering.Composition.Drawing.Nodes; |
|
||||
|
|
||||
enum RenderDataPopNodeType |
|
||||
{ |
|
||||
Transform, |
|
||||
Clip, |
|
||||
GeometryClip, |
|
||||
Opacity, |
|
||||
OpacityMask, |
|
||||
Effect |
|
||||
} |
|
||||
|
|
||||
interface IRenderDataServerResourcesCollector |
|
||||
{ |
|
||||
void AddRenderDataServerResource(object? obj); |
|
||||
} |
|
||||
|
|
||||
interface IRenderDataItemWithServerResources : IRenderDataItem |
|
||||
{ |
|
||||
void Collect(IRenderDataServerResourcesCollector collector); |
|
||||
} |
|
||||
|
|
||||
struct RenderDataNodeRenderContext : IDisposable |
|
||||
{ |
|
||||
private Stack<Matrix>? _stack; |
|
||||
private static readonly ThreadSafeObjectPool<Stack<Matrix>> s_matrixStackPool = new(); |
|
||||
|
|
||||
public RenderDataNodeRenderContext(IDrawingContextImpl context) |
|
||||
{ |
|
||||
Context = context; |
|
||||
} |
|
||||
public IDrawingContextImpl Context { get; } |
|
||||
|
|
||||
public Stack<Matrix> MatrixStack |
|
||||
{ |
|
||||
get => _stack ??= s_matrixStackPool.Get(); |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
if (_stack != null) |
|
||||
{ |
|
||||
_stack.Clear(); |
|
||||
s_matrixStackPool.ReturnAndSetNull(ref _stack); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
interface IRenderDataItem |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Renders the node to a drawing context.
|
|
||||
/// </summary>
|
|
||||
/// <param name="context">The drawing context.</param>
|
|
||||
void Invoke(ref RenderDataNodeRenderContext context); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the bounds of the visible content in the node in global coordinates.
|
|
||||
/// </summary>
|
|
||||
Rect? Bounds { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Hit test the geometry in this node.
|
|
||||
/// </summary>
|
|
||||
/// <param name="p">The point in global coordinates.</param>
|
|
||||
/// <returns>True if the point hits the node's geometry; otherwise false.</returns>
|
|
||||
/// <remarks>
|
|
||||
/// This method does not recurse to childs, if you want
|
|
||||
/// to hit test children they must be hit tested manually.
|
|
||||
/// </remarks>
|
|
||||
bool HitTest(Point p); |
|
||||
} |
|
||||
|
|
||||
class RenderDataCustomNode : IRenderDataItem, IDisposable |
|
||||
{ |
|
||||
public ICustomDrawOperation? Operation { get; set; } |
|
||||
public bool HitTest(Point p) => Operation?.HitTest(p) ?? false; |
|
||||
public void Invoke(ref RenderDataNodeRenderContext context) => Operation?.Render(new(context.Context, false)); |
|
||||
|
|
||||
public Rect? Bounds => Operation?.Bounds; |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
Operation?.Dispose(); |
|
||||
Operation = null; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
abstract class RenderDataPushNode : IRenderDataItem, IDisposable |
|
||||
{ |
|
||||
public PooledInlineList<IRenderDataItem> Children; |
|
||||
public abstract void Push(ref RenderDataNodeRenderContext context); |
|
||||
public abstract void Pop(ref RenderDataNodeRenderContext context); |
|
||||
public void Invoke(ref RenderDataNodeRenderContext context) |
|
||||
{ |
|
||||
if (Children.Count == 0) |
|
||||
return; |
|
||||
Push(ref context); |
|
||||
foreach (var ch in Children) |
|
||||
ch.Invoke(ref context); |
|
||||
Pop(ref context); |
|
||||
} |
|
||||
|
|
||||
public virtual Rect? Bounds |
|
||||
{ |
|
||||
get |
|
||||
{ |
|
||||
if (Children.Count == 0) |
|
||||
return null; |
|
||||
Rect? union = null; |
|
||||
foreach (var i in Children) |
|
||||
union = Rect.Union(union, i.Bounds); |
|
||||
return union; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public virtual bool HitTest(Point p) |
|
||||
{ |
|
||||
if (Children.Count == 0) |
|
||||
return false; |
|
||||
foreach(var ch in Children) |
|
||||
if (ch.HitTest(p)) |
|
||||
return true; |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
if (Children.Count > 0) |
|
||||
{ |
|
||||
foreach(var ch in Children) |
|
||||
if (ch is IDisposable disposable) |
|
||||
disposable.Dispose(); |
|
||||
Children.Dispose(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
class RenderDataClipNode : RenderDataPushNode |
|
||||
{ |
|
||||
public RoundedRect Rect { get; set; } |
|
||||
public override void Push(ref RenderDataNodeRenderContext context) => |
|
||||
context.Context.PushClip(Rect); |
|
||||
|
|
||||
public override void Pop(ref RenderDataNodeRenderContext context) => |
|
||||
context.Context.PopClip(); |
|
||||
|
|
||||
public override bool HitTest(Point p) |
|
||||
{ |
|
||||
if (!Rect.Rect.Contains(p)) |
|
||||
return false; |
|
||||
return base.HitTest(p); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
class RenderDataGeometryClipNode : RenderDataPushNode |
|
||||
{ |
|
||||
public IGeometryImpl? Geometry { get; set; } |
|
||||
public bool Contains(Point p) => Geometry?.FillContains(p) ?? false; |
|
||||
|
|
||||
public override void Push(ref RenderDataNodeRenderContext context) |
|
||||
{ |
|
||||
if (Geometry != null) |
|
||||
context.Context.PushGeometryClip(Geometry); |
|
||||
} |
|
||||
|
|
||||
public override void Pop(ref RenderDataNodeRenderContext context) |
|
||||
{ |
|
||||
if (Geometry != null) |
|
||||
context.Context.PopGeometryClip(); |
|
||||
} |
|
||||
|
|
||||
public override bool HitTest(Point p) |
|
||||
{ |
|
||||
if (Geometry != null && !Geometry.FillContains(p)) |
|
||||
return false; |
|
||||
return base.HitTest(p); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
class RenderDataOpacityNode : RenderDataPushNode |
|
||||
{ |
|
||||
public double Opacity { get; set; } |
|
||||
public override void Push(ref RenderDataNodeRenderContext context) |
|
||||
{ |
|
||||
if (Opacity != 1) |
|
||||
context.Context.PushOpacity(Opacity, null); |
|
||||
} |
|
||||
|
|
||||
public override void Pop(ref RenderDataNodeRenderContext context) |
|
||||
{ |
|
||||
if (Opacity != 1) |
|
||||
context.Context.PopOpacity(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
abstract class RenderDataBrushAndPenNode : IRenderDataItemWithServerResources |
|
||||
{ |
|
||||
public IBrush? ServerBrush { get; set; } |
|
||||
public IPen? ServerPen { get; set; } |
|
||||
public IPen? ClientPen { get; set; } |
|
||||
|
|
||||
public void Collect(IRenderDataServerResourcesCollector collector) |
|
||||
{ |
|
||||
collector.AddRenderDataServerResource(ServerBrush); |
|
||||
collector.AddRenderDataServerResource(ServerPen); |
|
||||
} |
|
||||
|
|
||||
public abstract void Invoke(ref RenderDataNodeRenderContext context); |
|
||||
public abstract Rect? Bounds { get; } |
|
||||
public abstract bool HitTest(Point p); |
|
||||
} |
|
||||
|
|
||||
class RenderDataRenderOptionsNode : RenderDataPushNode |
|
||||
{ |
|
||||
public RenderOptions RenderOptions { get; set; } |
|
||||
|
|
||||
public override void Push(ref RenderDataNodeRenderContext context) |
|
||||
{ |
|
||||
context.Context.PushRenderOptions(RenderOptions); |
|
||||
} |
|
||||
|
|
||||
public override void Pop(ref RenderDataNodeRenderContext context) |
|
||||
{ |
|
||||
context.Context.PopRenderOptions(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
class RenderDataTextOptionsNode : RenderDataPushNode |
|
||||
{ |
|
||||
public TextOptions TextOptions { get; set; } |
|
||||
|
|
||||
public override void Push(ref RenderDataNodeRenderContext context) |
|
||||
{ |
|
||||
context.Context.PushTextOptions(TextOptions); |
|
||||
} |
|
||||
|
|
||||
public override void Pop(ref RenderDataNodeRenderContext context) |
|
||||
{ |
|
||||
context.Context.PopTextOptions(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// A render data node that pushes an effect.
|
|
||||
/// </summary>
|
|
||||
class RenderDataEffectNode : RenderDataPushNode |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Gets or sets the effect to push.
|
|
||||
/// </summary>
|
|
||||
public IEffect? Effect { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets the bounds of the effect.
|
|
||||
/// </summary>
|
|
||||
public Rect BoundsRect { get; set; } |
|
||||
|
|
||||
/// <inheritdoc />
|
|
||||
public override Rect? Bounds |
|
||||
{ |
|
||||
get |
|
||||
{ |
|
||||
var bounds = base.Bounds; |
|
||||
if (bounds is null) |
|
||||
return null; |
|
||||
return Effect is { } effect |
|
||||
? bounds.Value.Inflate(effect.GetEffectOutputPadding()) |
|
||||
: bounds; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc />
|
|
||||
public override void Push(ref RenderDataNodeRenderContext context) |
|
||||
{ |
|
||||
if (Effect != null && context.Context is IDrawingContextImplWithEffects effectImpl) |
|
||||
effectImpl.PushEffect(BoundsRect, Effect); |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc />
|
|
||||
public override void Pop(ref RenderDataNodeRenderContext context) |
|
||||
{ |
|
||||
if (Effect != null && context.Context is IDrawingContextImplWithEffects effectImpl) |
|
||||
effectImpl.PopEffect(); |
|
||||
} |
|
||||
} |
|
||||
@ -1,27 +0,0 @@ |
|||||
namespace Avalonia.Rendering.Composition.Drawing.Nodes; |
|
||||
|
|
||||
class RenderDataPushMatrixNode : RenderDataPushNode |
|
||||
{ |
|
||||
public Matrix Matrix { get; set; } |
|
||||
|
|
||||
public override void Push(ref RenderDataNodeRenderContext context) |
|
||||
{ |
|
||||
var current = context.Context.Transform; |
|
||||
context.MatrixStack.Push(current); |
|
||||
context.Context.Transform = Matrix * current; |
|
||||
} |
|
||||
|
|
||||
public override void Pop(ref RenderDataNodeRenderContext context) |
|
||||
{ |
|
||||
context.Context.Transform = context.MatrixStack.Pop(); |
|
||||
} |
|
||||
|
|
||||
public override bool HitTest(Point p) |
|
||||
{ |
|
||||
if (Matrix.TryInvert(out var inverted)) |
|
||||
return base.HitTest(p.Transform(inverted)); |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
public override Rect? Bounds => base.Bounds?.TransformToAABB(Matrix); |
|
||||
} |
|
||||
@ -1,25 +0,0 @@ |
|||||
using Avalonia.Media; |
|
||||
using Avalonia.Platform; |
|
||||
|
|
||||
namespace Avalonia.Rendering.Composition.Drawing.Nodes; |
|
||||
|
|
||||
class RenderDataOpacityMaskNode : RenderDataPushNode, IRenderDataItemWithServerResources |
|
||||
{ |
|
||||
public IBrush? ServerBrush { get; set; } |
|
||||
|
|
||||
public Rect BoundsRect { get; set; } |
|
||||
|
|
||||
public void Collect(IRenderDataServerResourcesCollector collector) |
|
||||
{ |
|
||||
collector.AddRenderDataServerResource(ServerBrush); |
|
||||
} |
|
||||
|
|
||||
public override void Push(ref RenderDataNodeRenderContext context) |
|
||||
{ |
|
||||
if (ServerBrush != null) |
|
||||
context.Context.PushOpacityMask(ServerBrush, BoundsRect); |
|
||||
} |
|
||||
|
|
||||
public override void Pop(ref RenderDataNodeRenderContext context) => |
|
||||
context.Context.PopOpacityMask(); |
|
||||
} |
|
||||
@ -1,46 +0,0 @@ |
|||||
using Avalonia.Media; |
|
||||
|
|
||||
namespace Avalonia.Rendering.Composition.Drawing.Nodes; |
|
||||
|
|
||||
class RenderDataRectangleNode : RenderDataBrushAndPenNode |
|
||||
{ |
|
||||
public RoundedRect Rect { get; set; } |
|
||||
public BoxShadows BoxShadows { get; set; } |
|
||||
|
|
||||
public override bool HitTest(Point p) |
|
||||
{ |
|
||||
var strokeThicknessAdjustment = (ClientPen?.Thickness / 2) ?? 0; |
|
||||
|
|
||||
if (Rect.IsRounded) |
|
||||
{ |
|
||||
var outerRoundedRect = Rect.Inflate(strokeThicknessAdjustment, strokeThicknessAdjustment); |
|
||||
if (outerRoundedRect.ContainsExclusive(p)) |
|
||||
{ |
|
||||
if (ServerBrush != null) // it's safe to check for null
|
|
||||
return true; |
|
||||
|
|
||||
var innerRoundedRect = Rect.Deflate(strokeThicknessAdjustment, strokeThicknessAdjustment); |
|
||||
return !innerRoundedRect.ContainsExclusive(p); |
|
||||
} |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
var outerRect = Rect.Rect.Inflate(strokeThicknessAdjustment); |
|
||||
if (outerRect.ContainsExclusive(p)) |
|
||||
{ |
|
||||
if (ServerBrush != null) // it's safe to check for null
|
|
||||
return true; |
|
||||
|
|
||||
var innerRect = Rect.Rect.Deflate(strokeThicknessAdjustment); |
|
||||
return !innerRect.ContainsExclusive(p); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
public override void Invoke(ref RenderDataNodeRenderContext context) => |
|
||||
context.Context.DrawRectangle(ServerBrush, ServerPen, Rect, BoxShadows); |
|
||||
|
|
||||
public override Rect? Bounds => BoxShadows.TransformBounds(Rect.Rect).Inflate((ServerPen?.Thickness ?? 0) / 2); |
|
||||
} |
|
||||
@ -0,0 +1,22 @@ |
|||||
|
namespace Avalonia.Rendering.Composition.Drawing; |
||||
|
|
||||
|
internal enum RenderDataOpcode : byte |
||||
|
{ |
||||
|
Invalid = 0, |
||||
|
DrawLine, |
||||
|
DrawRectangle, |
||||
|
DrawEllipse, |
||||
|
DrawGeometry, |
||||
|
DrawGlyphRun, |
||||
|
DrawBitmap, |
||||
|
DrawCustom, |
||||
|
PushClip, |
||||
|
PushGeometryClip, |
||||
|
PushOpacity, |
||||
|
PushOpacityMask, |
||||
|
PushTransform, |
||||
|
PushRenderOptions, |
||||
|
PushTextOptions, |
||||
|
PushEffect, |
||||
|
Pop |
||||
|
} |
||||
@ -0,0 +1,132 @@ |
|||||
|
using Avalonia.Media; |
||||
|
|
||||
|
namespace Avalonia.Rendering.Composition.Drawing; |
||||
|
|
||||
|
internal interface IRenderDataPayload<TSelf> where TSelf : unmanaged, IRenderDataPayload<TSelf> |
||||
|
{ |
||||
|
static abstract RenderDataOpcode Opcode { get; } |
||||
|
} |
||||
|
|
||||
|
internal struct DrawLinePayload : IRenderDataPayload<DrawLinePayload> |
||||
|
{ |
||||
|
public static RenderDataOpcode Opcode => RenderDataOpcode.DrawLine; |
||||
|
|
||||
|
public int ServerPen; |
||||
|
public int ClientPen; |
||||
|
public Point P1; |
||||
|
public Point P2; |
||||
|
} |
||||
|
|
||||
|
internal struct DrawRectanglePayload : IRenderDataPayload<DrawRectanglePayload> |
||||
|
{ |
||||
|
public static RenderDataOpcode Opcode => RenderDataOpcode.DrawRectangle; |
||||
|
|
||||
|
public int ServerBrush; |
||||
|
public int ServerPen; |
||||
|
public int ClientPen; |
||||
|
public RoundedRect Rect; |
||||
|
public int BoxShadowCount; |
||||
|
} |
||||
|
|
||||
|
internal struct DrawEllipsePayload : IRenderDataPayload<DrawEllipsePayload> |
||||
|
{ |
||||
|
public static RenderDataOpcode Opcode => RenderDataOpcode.DrawEllipse; |
||||
|
|
||||
|
public int ServerBrush; |
||||
|
public int ServerPen; |
||||
|
public int ClientPen; |
||||
|
public Rect Rect; |
||||
|
} |
||||
|
|
||||
|
internal struct DrawGeometryPayload : IRenderDataPayload<DrawGeometryPayload> |
||||
|
{ |
||||
|
public static RenderDataOpcode Opcode => RenderDataOpcode.DrawGeometry; |
||||
|
|
||||
|
public int ServerBrush; |
||||
|
public int ServerPen; |
||||
|
public int ClientPen; |
||||
|
public int Geometry; |
||||
|
} |
||||
|
|
||||
|
internal struct DrawGlyphRunPayload : IRenderDataPayload<DrawGlyphRunPayload> |
||||
|
{ |
||||
|
public static RenderDataOpcode Opcode => RenderDataOpcode.DrawGlyphRun; |
||||
|
|
||||
|
public int ServerBrush; |
||||
|
public int GlyphRun; |
||||
|
} |
||||
|
|
||||
|
internal struct DrawBitmapPayload : IRenderDataPayload<DrawBitmapPayload> |
||||
|
{ |
||||
|
public static RenderDataOpcode Opcode => RenderDataOpcode.DrawBitmap; |
||||
|
|
||||
|
public int Bitmap; |
||||
|
public double Opacity; |
||||
|
public Rect SourceRect; |
||||
|
public Rect DestRect; |
||||
|
} |
||||
|
|
||||
|
internal struct DrawCustomPayload : IRenderDataPayload<DrawCustomPayload> |
||||
|
{ |
||||
|
public static RenderDataOpcode Opcode => RenderDataOpcode.DrawCustom; |
||||
|
|
||||
|
public int Operation; |
||||
|
} |
||||
|
|
||||
|
internal struct PushClipPayload : IRenderDataPayload<PushClipPayload> |
||||
|
{ |
||||
|
public static RenderDataOpcode Opcode => RenderDataOpcode.PushClip; |
||||
|
|
||||
|
public RoundedRect Clip; |
||||
|
} |
||||
|
|
||||
|
internal struct PushGeometryClipPayload : IRenderDataPayload<PushGeometryClipPayload> |
||||
|
{ |
||||
|
public static RenderDataOpcode Opcode => RenderDataOpcode.PushGeometryClip; |
||||
|
|
||||
|
public int Geometry; |
||||
|
} |
||||
|
|
||||
|
internal struct PushOpacityPayload : IRenderDataPayload<PushOpacityPayload> |
||||
|
{ |
||||
|
public static RenderDataOpcode Opcode => RenderDataOpcode.PushOpacity; |
||||
|
|
||||
|
public double Opacity; |
||||
|
} |
||||
|
|
||||
|
internal struct PushOpacityMaskPayload : IRenderDataPayload<PushOpacityMaskPayload> |
||||
|
{ |
||||
|
public static RenderDataOpcode Opcode => RenderDataOpcode.PushOpacityMask; |
||||
|
|
||||
|
public int Brush; |
||||
|
public Rect Bounds; |
||||
|
} |
||||
|
|
||||
|
internal struct PushTransformPayload : IRenderDataPayload<PushTransformPayload> |
||||
|
{ |
||||
|
public static RenderDataOpcode Opcode => RenderDataOpcode.PushTransform; |
||||
|
|
||||
|
public Matrix Matrix; |
||||
|
} |
||||
|
|
||||
|
internal struct PushRenderOptionsPayload : IRenderDataPayload<PushRenderOptionsPayload> |
||||
|
{ |
||||
|
public static RenderDataOpcode Opcode => RenderDataOpcode.PushRenderOptions; |
||||
|
|
||||
|
public RenderOptions Options; |
||||
|
} |
||||
|
|
||||
|
internal struct PushTextOptionsPayload : IRenderDataPayload<PushTextOptionsPayload> |
||||
|
{ |
||||
|
public static RenderDataOpcode Opcode => RenderDataOpcode.PushTextOptions; |
||||
|
|
||||
|
public TextOptions Options; |
||||
|
} |
||||
|
|
||||
|
internal struct PushEffectPayload : IRenderDataPayload<PushEffectPayload> |
||||
|
{ |
||||
|
public static RenderDataOpcode Opcode => RenderDataOpcode.PushEffect; |
||||
|
|
||||
|
public int Effect; |
||||
|
public Rect Bounds; |
||||
|
} |
||||
@ -0,0 +1,42 @@ |
|||||
|
using System; |
||||
|
using System.Diagnostics; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using System.Runtime.InteropServices; |
||||
|
|
||||
|
namespace Avalonia.Rendering.Composition.Drawing; |
||||
|
|
||||
|
internal ref struct RenderDataReader |
||||
|
{ |
||||
|
private readonly ReadOnlySpan<byte> _buffer; |
||||
|
private int _position; |
||||
|
|
||||
|
public RenderDataReader(ReadOnlySpan<byte> buffer) |
||||
|
{ |
||||
|
_buffer = buffer; |
||||
|
_position = 0; |
||||
|
} |
||||
|
|
||||
|
public int Position => _position; |
||||
|
|
||||
|
public bool IsAtEnd => _position >= _buffer.Length; |
||||
|
|
||||
|
public ReadOnlySpan<byte> Take(int count) |
||||
|
{ |
||||
|
var span = _buffer.Slice(_position, count); |
||||
|
_position += count; |
||||
|
return span; |
||||
|
} |
||||
|
|
||||
|
public T Read<T>() where T : unmanaged |
||||
|
=> MemoryMarshal.Read<T>(Take(Unsafe.SizeOf<T>())); |
||||
|
|
||||
|
public T Peek<T>() where T : unmanaged |
||||
|
=> MemoryMarshal.Read<T>(_buffer.Slice(_position, Unsafe.SizeOf<T>())); |
||||
|
|
||||
|
public T ReadPayload<T>() where T : unmanaged, IRenderDataPayload<T> |
||||
|
{ |
||||
|
var opcode = Read<RenderDataOpcode>(); |
||||
|
Debug.Assert(opcode == T.Opcode); |
||||
|
return Read<T>(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,54 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Collections.Pooled; |
||||
|
|
||||
|
namespace Avalonia.Rendering.Composition.Drawing; |
||||
|
|
||||
|
internal struct RenderDataResources : IDisposable |
||||
|
{ |
||||
|
public const int NullHandle = -1; |
||||
|
|
||||
|
private PooledList<object?>? _resources; |
||||
|
private Dictionary<object, int>? _internMap; |
||||
|
|
||||
|
public int Count => _resources?.Count ?? 0; |
||||
|
|
||||
|
// Recording path: dedupes by reference equality so a resource reused across many draws gets one slot.
|
||||
|
public int Intern(object? resource) |
||||
|
{ |
||||
|
if (resource is null) |
||||
|
return NullHandle; |
||||
|
|
||||
|
_resources ??= new PooledList<object?>(); |
||||
|
_internMap ??= new Dictionary<object, int>(ReferenceEqualityComparer.Instance); |
||||
|
|
||||
|
if (_internMap.TryGetValue(resource, out var handle)) |
||||
|
return handle; |
||||
|
|
||||
|
handle = _resources.Count; |
||||
|
_resources.Add(resource); |
||||
|
_internMap.Add(resource, handle); |
||||
|
return handle; |
||||
|
} |
||||
|
|
||||
|
// Deserialize path: appends without deduping since the wire format is already deduped.
|
||||
|
public int AppendDeserialized(object? resource) |
||||
|
{ |
||||
|
if (resource is null) |
||||
|
return NullHandle; |
||||
|
|
||||
|
_resources ??= new PooledList<object?>(); |
||||
|
var handle = _resources.Count; |
||||
|
_resources.Add(resource); |
||||
|
return handle; |
||||
|
} |
||||
|
|
||||
|
public object? this[int handle] => handle == NullHandle ? null : _resources![handle]; |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
_resources?.Dispose(); |
||||
|
_resources = null; |
||||
|
_internMap = null; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,93 @@ |
|||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Media.Imaging; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Rendering.SceneGraph; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Rendering.Composition.Drawing; |
||||
|
|
||||
|
internal partial class RenderDataStream |
||||
|
{ |
||||
|
internal struct BoundsScope |
||||
|
{ |
||||
|
public Rect? SavedBounds; |
||||
|
public bool IsTransform; |
||||
|
public Matrix Matrix; |
||||
|
public Thickness EffectPadding; |
||||
|
} |
||||
|
|
||||
|
internal struct BoundsVisitor : IRenderDataVisitor<BoundsScope> |
||||
|
{ |
||||
|
public Rect? Current; |
||||
|
|
||||
|
public bool StopVisiting => false; |
||||
|
|
||||
|
public void OnDrawLine(IPen? serverPen, IPen? clientPen, Point p1, Point p2) |
||||
|
{ |
||||
|
if (serverPen != null) |
||||
|
Current = Rect.Union(Current, LineBoundsHelper.CalculateBounds(p1, p2, serverPen)); |
||||
|
} |
||||
|
|
||||
|
public void OnDrawRectangle(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, RoundedRect rect, |
||||
|
BoxShadows boxShadows) |
||||
|
{ |
||||
|
var bounds = boxShadows.TransformBounds(rect.Rect) |
||||
|
.Inflate((serverPen?.Thickness ?? 0) / 2); |
||||
|
Current = Rect.Union(Current, bounds); |
||||
|
} |
||||
|
|
||||
|
public void OnDrawEllipse(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, Rect rect) |
||||
|
=> Current = Rect.Union(Current, rect.Inflate(serverPen?.Thickness ?? 0)); |
||||
|
|
||||
|
public void OnDrawGeometry(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, IGeometryImpl? geometry) |
||||
|
=> Current = Rect.Union(Current, geometry?.GetRenderBounds(serverPen) ?? default); |
||||
|
|
||||
|
public void OnDrawGlyphRun(IBrush? serverBrush, IRef<IGlyphRunImpl>? glyphRun) |
||||
|
=> Current = Rect.Union(Current, glyphRun?.Item?.Bounds ?? default); |
||||
|
|
||||
|
public void OnDrawBitmap(IRef<IBitmapImpl>? bitmap, double opacity, Rect sourceRect, Rect destRect) |
||||
|
=> Current = Rect.Union(Current, destRect); |
||||
|
|
||||
|
public void OnDrawCustom(ICustomDrawOperation? operation) |
||||
|
=> Current = Rect.Union(Current, operation?.Bounds); |
||||
|
|
||||
|
private BoundsScope EnterChildScope(bool isTransform = false, Matrix matrix = default, |
||||
|
Thickness effectPadding = default) |
||||
|
{ |
||||
|
var scope = new BoundsScope |
||||
|
{ |
||||
|
SavedBounds = Current, IsTransform = isTransform, Matrix = matrix, EffectPadding = effectPadding |
||||
|
}; |
||||
|
Current = null; |
||||
|
return scope; |
||||
|
} |
||||
|
|
||||
|
public BoundsScope OnPushClip(RoundedRect clip) => EnterChildScope(); |
||||
|
public BoundsScope OnPushGeometryClip(IGeometryImpl? geometry) => EnterChildScope(); |
||||
|
public BoundsScope OnPushOpacity(double opacity) => EnterChildScope(); |
||||
|
public BoundsScope OnPushOpacityMask(IBrush? brush, Rect bounds) => EnterChildScope(); |
||||
|
public BoundsScope OnPushTransform(Matrix matrix) => EnterChildScope(true, matrix); |
||||
|
public BoundsScope OnPushRenderOptions(RenderOptions options) => EnterChildScope(); |
||||
|
public BoundsScope OnPushTextOptions(TextOptions options) => EnterChildScope(); |
||||
|
|
||||
|
public BoundsScope OnPushEffect(IEffect? effect, Rect bounds) |
||||
|
=> EnterChildScope(effectPadding: effect.GetEffectOutputPadding()); |
||||
|
|
||||
|
public void OnPop(in BoundsScope scope) |
||||
|
{ |
||||
|
var childUnion = Current; |
||||
|
if (scope.IsTransform) |
||||
|
childUnion = childUnion?.TransformToAABB(scope.Matrix); |
||||
|
else if (childUnion.HasValue && !scope.EffectPadding.Equals(default)) |
||||
|
childUnion = childUnion.Value.Inflate(scope.EffectPadding); |
||||
|
Current = Rect.Union(scope.SavedBounds, childUnion); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public Rect? CalculateBounds() |
||||
|
{ |
||||
|
var visitor = new BoundsVisitor(); |
||||
|
Visit<BoundsVisitor, BoundsScope>(ref visitor); |
||||
|
return visitor.Current; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,250 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Media.Imaging; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Rendering.SceneGraph; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Rendering.Composition.Drawing; |
||||
|
|
||||
|
internal partial class RenderDataStream |
||||
|
{ |
||||
|
internal struct HitTestScope |
||||
|
{ |
||||
|
public bool SavedLive; |
||||
|
public bool RestorePoint; |
||||
|
public Point SavedPoint; |
||||
|
} |
||||
|
|
||||
|
internal struct HitTestVisitor : IRenderDataVisitor<HitTestScope> |
||||
|
{ |
||||
|
public bool StopVisiting { get; private set; } |
||||
|
public bool HitFound; |
||||
|
public Point Current; |
||||
|
public bool Live; |
||||
|
|
||||
|
public HitTestVisitor(Point point) |
||||
|
{ |
||||
|
StopVisiting = false; |
||||
|
HitFound = false; |
||||
|
Current = point; |
||||
|
Live = true; |
||||
|
} |
||||
|
|
||||
|
private void Hit() |
||||
|
{ |
||||
|
HitFound = true; |
||||
|
StopVisiting = true; |
||||
|
} |
||||
|
|
||||
|
public void OnDrawLine(IPen? serverPen, IPen? clientPen, Point p1, Point p2) |
||||
|
{ |
||||
|
if (Live && HitTestLine(clientPen, p1, p2, Current)) |
||||
|
Hit(); |
||||
|
} |
||||
|
|
||||
|
public void OnDrawRectangle(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, RoundedRect rect, |
||||
|
BoxShadows boxShadows) |
||||
|
{ |
||||
|
if (Live && HitTestRectangle(serverBrush, clientPen, rect, Current)) |
||||
|
Hit(); |
||||
|
} |
||||
|
|
||||
|
public void OnDrawEllipse(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, Rect rect) |
||||
|
{ |
||||
|
if (Live && HitTestEllipse(serverBrush, clientPen, rect, Current)) |
||||
|
Hit(); |
||||
|
} |
||||
|
|
||||
|
public void OnDrawGeometry(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, IGeometryImpl? geometry) |
||||
|
{ |
||||
|
if (Live && geometry != null && |
||||
|
((serverBrush != null && geometry.FillContains(Current)) || |
||||
|
(clientPen != null && geometry.StrokeContains(clientPen, Current)))) |
||||
|
Hit(); |
||||
|
} |
||||
|
|
||||
|
public void OnDrawGlyphRun(IBrush? serverBrush, IRef<IGlyphRunImpl>? glyphRun) |
||||
|
{ |
||||
|
if (Live && glyphRun != null && glyphRun.Item.Bounds.ContainsExclusive(Current)) |
||||
|
Hit(); |
||||
|
} |
||||
|
|
||||
|
public void OnDrawBitmap(IRef<IBitmapImpl>? bitmap, double opacity, Rect sourceRect, Rect destRect) |
||||
|
{ |
||||
|
if (Live && destRect.Contains(Current)) |
||||
|
Hit(); |
||||
|
} |
||||
|
|
||||
|
public void OnDrawCustom(ICustomDrawOperation? operation) |
||||
|
{ |
||||
|
if (Live && operation != null && operation.HitTest(Current)) |
||||
|
Hit(); |
||||
|
} |
||||
|
|
||||
|
public HitTestScope OnPushClip(RoundedRect clip) |
||||
|
{ |
||||
|
var scope = new HitTestScope { SavedLive = Live }; |
||||
|
if (Live && !clip.Rect.Contains(Current)) |
||||
|
Live = false; |
||||
|
return scope; |
||||
|
} |
||||
|
|
||||
|
public HitTestScope OnPushGeometryClip(IGeometryImpl? geometry) |
||||
|
{ |
||||
|
var scope = new HitTestScope { SavedLive = Live }; |
||||
|
if (Live && geometry != null && !geometry.FillContains(Current)) |
||||
|
Live = false; |
||||
|
return scope; |
||||
|
} |
||||
|
|
||||
|
public HitTestScope OnPushOpacity(double opacity) |
||||
|
=> new HitTestScope { SavedLive = Live }; |
||||
|
|
||||
|
public HitTestScope OnPushOpacityMask(IBrush? brush, Rect bounds) |
||||
|
=> new HitTestScope { SavedLive = Live }; |
||||
|
|
||||
|
public HitTestScope OnPushTransform(Matrix matrix) |
||||
|
{ |
||||
|
var scope = new HitTestScope { SavedLive = Live }; |
||||
|
if (Live) |
||||
|
{ |
||||
|
if (matrix.TryInvert(out var inverted)) |
||||
|
{ |
||||
|
scope.RestorePoint = true; |
||||
|
scope.SavedPoint = Current; |
||||
|
Current = Current.Transform(inverted); |
||||
|
} |
||||
|
else |
||||
|
Live = false; |
||||
|
} |
||||
|
return scope; |
||||
|
} |
||||
|
|
||||
|
public HitTestScope OnPushRenderOptions(RenderOptions options) |
||||
|
=> new HitTestScope { SavedLive = Live }; |
||||
|
|
||||
|
public HitTestScope OnPushTextOptions(TextOptions options) |
||||
|
=> new HitTestScope { SavedLive = Live }; |
||||
|
|
||||
|
public HitTestScope OnPushEffect(IEffect? effect, Rect bounds) |
||||
|
=> new HitTestScope { SavedLive = Live }; |
||||
|
|
||||
|
public void OnPop(in HitTestScope scope) |
||||
|
{ |
||||
|
Live = scope.SavedLive; |
||||
|
if (scope.RestorePoint) |
||||
|
Current = scope.SavedPoint; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public bool HitTest(Point point) |
||||
|
{ |
||||
|
var visitor = new HitTestVisitor(point); |
||||
|
Visit<HitTestVisitor, HitTestScope>(ref visitor); |
||||
|
return visitor.HitFound; |
||||
|
} |
||||
|
|
||||
|
private static bool HitTestLine(IPen? clientPen, Point p1, Point p2, Point p) |
||||
|
{ |
||||
|
if (clientPen == null) |
||||
|
return false; |
||||
|
|
||||
|
var halfThickness = clientPen.Thickness / 2; |
||||
|
var minX = Math.Min(p1.X, p2.X) - halfThickness; |
||||
|
var maxX = Math.Max(p1.X, p2.X) + halfThickness; |
||||
|
var minY = Math.Min(p1.Y, p2.Y) - halfThickness; |
||||
|
var maxY = Math.Max(p1.Y, p2.Y) + halfThickness; |
||||
|
|
||||
|
if (p.X < minX || p.X > maxX || p.Y < minY || p.Y > maxY) |
||||
|
return false; |
||||
|
|
||||
|
Vector ap = p - p1; |
||||
|
var dot1 = Vector.Dot(p2 - p1, ap); |
||||
|
if (dot1 < 0) |
||||
|
return ap.Length <= halfThickness; |
||||
|
|
||||
|
Vector bp = p - p2; |
||||
|
var dot2 = Vector.Dot(p1 - p2, bp); |
||||
|
if (dot2 < 0) |
||||
|
return bp.Length <= halfThickness; |
||||
|
|
||||
|
var bXaX = p2.X - p1.X; |
||||
|
var bYaY = p2.Y - p1.Y; |
||||
|
var distance = (bXaX * (p.Y - p1.Y) - bYaY * (p.X - p1.X)) / |
||||
|
Math.Sqrt(bXaX * bXaX + bYaY * bYaY); |
||||
|
return Math.Abs(distance) <= halfThickness; |
||||
|
} |
||||
|
|
||||
|
private static bool HitTestRectangle(IBrush? serverBrush, IPen? clientPen, RoundedRect rect, Point p) |
||||
|
{ |
||||
|
var strokeThicknessAdjustment = (clientPen?.Thickness / 2) ?? 0; |
||||
|
|
||||
|
if (rect.IsRounded) |
||||
|
{ |
||||
|
var outer = rect.Inflate(strokeThicknessAdjustment, strokeThicknessAdjustment); |
||||
|
if (outer.ContainsExclusive(p)) |
||||
|
{ |
||||
|
if (serverBrush != null) |
||||
|
return true; |
||||
|
|
||||
|
var inner = rect.Deflate(strokeThicknessAdjustment, strokeThicknessAdjustment); |
||||
|
return !inner.ContainsExclusive(p); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var outer = rect.Rect.Inflate(strokeThicknessAdjustment); |
||||
|
if (outer.ContainsExclusive(p)) |
||||
|
{ |
||||
|
if (serverBrush != null) |
||||
|
return true; |
||||
|
|
||||
|
var inner = rect.Rect.Deflate(strokeThicknessAdjustment); |
||||
|
return !inner.ContainsExclusive(p); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
private static bool HitTestEllipse(IBrush? serverBrush, IPen? clientPen, Rect rect, Point p) |
||||
|
{ |
||||
|
var center = rect.Center; |
||||
|
var strokeThickness = clientPen?.Thickness ?? 0; |
||||
|
|
||||
|
var rx = rect.Width / 2 + strokeThickness / 2; |
||||
|
var ry = rect.Height / 2 + strokeThickness / 2; |
||||
|
|
||||
|
var dx = p.X - center.X; |
||||
|
var dy = p.Y - center.Y; |
||||
|
|
||||
|
if (Math.Abs(dx) > rx || Math.Abs(dy) > ry) |
||||
|
return false; |
||||
|
|
||||
|
if (serverBrush != null) |
||||
|
return EllipseContains(dx, dy, rx, ry); |
||||
|
|
||||
|
if (strokeThickness > 0) |
||||
|
{ |
||||
|
var inStroke = EllipseContains(dx, dy, rx, ry); |
||||
|
|
||||
|
rx = rect.Width / 2 - strokeThickness / 2; |
||||
|
ry = rect.Height / 2 - strokeThickness / 2; |
||||
|
|
||||
|
var inInner = EllipseContains(dx, dy, rx, ry); |
||||
|
|
||||
|
return inStroke && !inInner; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
private static bool EllipseContains(double dx, double dy, double radiusX, double radiusY) |
||||
|
{ |
||||
|
var rx2 = radiusX * radiusX; |
||||
|
var ry2 = radiusY * radiusY; |
||||
|
var distance = ry2 * dx * dx + rx2 * dy * dy; |
||||
|
return distance <= rx2 * ry2; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,159 @@ |
|||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Media.Imaging; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Rendering.SceneGraph; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Rendering.Composition.Drawing; |
||||
|
|
||||
|
internal partial class RenderDataStream |
||||
|
{ |
||||
|
internal struct ReplayScope |
||||
|
{ |
||||
|
public RenderDataOpcode Kind; |
||||
|
public bool Active; |
||||
|
public Matrix SavedTransform; |
||||
|
} |
||||
|
|
||||
|
internal struct ReplayVisitor : IRenderDataVisitor<ReplayScope> |
||||
|
{ |
||||
|
private readonly IDrawingContextImpl _context; |
||||
|
|
||||
|
public ReplayVisitor(IDrawingContextImpl context) |
||||
|
{ |
||||
|
_context = context; |
||||
|
} |
||||
|
|
||||
|
public bool StopVisiting => false; |
||||
|
|
||||
|
public void OnDrawLine(IPen? serverPen, IPen? clientPen, Point p1, Point p2) |
||||
|
=> _context.DrawLine(serverPen, p1, p2); |
||||
|
|
||||
|
public void OnDrawRectangle(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, RoundedRect rect, |
||||
|
BoxShadows boxShadows) |
||||
|
=> _context.DrawRectangle(serverBrush, serverPen, rect, boxShadows); |
||||
|
|
||||
|
public void OnDrawEllipse(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, Rect rect) |
||||
|
=> _context.DrawEllipse(serverBrush, serverPen, rect); |
||||
|
|
||||
|
public void OnDrawGeometry(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, IGeometryImpl? geometry) |
||||
|
{ |
||||
|
if (geometry != null) |
||||
|
_context.DrawGeometry(serverBrush, serverPen, geometry); |
||||
|
} |
||||
|
|
||||
|
public void OnDrawGlyphRun(IBrush? serverBrush, IRef<IGlyphRunImpl>? glyphRun) |
||||
|
{ |
||||
|
if (glyphRun != null) |
||||
|
_context.DrawGlyphRun(serverBrush, glyphRun.Item); |
||||
|
} |
||||
|
|
||||
|
public void OnDrawBitmap(IRef<IBitmapImpl>? bitmap, double opacity, Rect sourceRect, Rect destRect) |
||||
|
{ |
||||
|
if (bitmap != null) |
||||
|
_context.DrawBitmap(bitmap.Item, opacity, sourceRect, destRect); |
||||
|
} |
||||
|
|
||||
|
public void OnDrawCustom(ICustomDrawOperation? operation) |
||||
|
=> operation?.Render(new ImmediateDrawingContext(_context, false)); |
||||
|
|
||||
|
public ReplayScope OnPushClip(RoundedRect clip) |
||||
|
{ |
||||
|
_context.PushClip(clip); |
||||
|
return new ReplayScope { Kind = RenderDataOpcode.PushClip, Active = true }; |
||||
|
} |
||||
|
|
||||
|
public ReplayScope OnPushGeometryClip(IGeometryImpl? geometry) |
||||
|
{ |
||||
|
if (geometry != null) |
||||
|
_context.PushGeometryClip(geometry); |
||||
|
return new ReplayScope |
||||
|
{ Kind = RenderDataOpcode.PushGeometryClip, Active = geometry != null }; |
||||
|
} |
||||
|
|
||||
|
public ReplayScope OnPushOpacity(double opacity) |
||||
|
{ |
||||
|
if (opacity != 1) |
||||
|
_context.PushOpacity(opacity, null); |
||||
|
return new ReplayScope { Kind = RenderDataOpcode.PushOpacity, Active = opacity != 1 }; |
||||
|
} |
||||
|
|
||||
|
public ReplayScope OnPushOpacityMask(IBrush? brush, Rect bounds) |
||||
|
{ |
||||
|
if (brush != null) |
||||
|
_context.PushOpacityMask(brush, bounds); |
||||
|
return new ReplayScope { Kind = RenderDataOpcode.PushOpacityMask, Active = brush != null }; |
||||
|
} |
||||
|
|
||||
|
public ReplayScope OnPushTransform(Matrix matrix) |
||||
|
{ |
||||
|
var saved = _context.Transform; |
||||
|
_context.Transform = matrix * saved; |
||||
|
return new ReplayScope |
||||
|
{ Kind = RenderDataOpcode.PushTransform, Active = true, SavedTransform = saved }; |
||||
|
} |
||||
|
|
||||
|
public ReplayScope OnPushRenderOptions(RenderOptions options) |
||||
|
{ |
||||
|
_context.PushRenderOptions(options); |
||||
|
return new ReplayScope { Kind = RenderDataOpcode.PushRenderOptions, Active = true }; |
||||
|
} |
||||
|
|
||||
|
public ReplayScope OnPushTextOptions(TextOptions options) |
||||
|
{ |
||||
|
_context.PushTextOptions(options); |
||||
|
return new ReplayScope { Kind = RenderDataOpcode.PushTextOptions, Active = true }; |
||||
|
} |
||||
|
|
||||
|
public ReplayScope OnPushEffect(IEffect? effect, Rect bounds) |
||||
|
{ |
||||
|
var active = false; |
||||
|
if (effect != null && _context is IDrawingContextImplWithEffects effectImpl) |
||||
|
{ |
||||
|
effectImpl.PushEffect(bounds, effect); |
||||
|
active = true; |
||||
|
} |
||||
|
return new ReplayScope { Kind = RenderDataOpcode.PushEffect, Active = active }; |
||||
|
} |
||||
|
|
||||
|
public void OnPop(in ReplayScope scope) |
||||
|
{ |
||||
|
if (!scope.Active) |
||||
|
return; |
||||
|
|
||||
|
switch (scope.Kind) |
||||
|
{ |
||||
|
case RenderDataOpcode.PushClip: |
||||
|
_context.PopClip(); |
||||
|
break; |
||||
|
case RenderDataOpcode.PushGeometryClip: |
||||
|
_context.PopGeometryClip(); |
||||
|
break; |
||||
|
case RenderDataOpcode.PushOpacity: |
||||
|
_context.PopOpacity(); |
||||
|
break; |
||||
|
case RenderDataOpcode.PushOpacityMask: |
||||
|
_context.PopOpacityMask(); |
||||
|
break; |
||||
|
case RenderDataOpcode.PushTransform: |
||||
|
_context.Transform = scope.SavedTransform; |
||||
|
break; |
||||
|
case RenderDataOpcode.PushRenderOptions: |
||||
|
_context.PopRenderOptions(); |
||||
|
break; |
||||
|
case RenderDataOpcode.PushTextOptions: |
||||
|
_context.PopTextOptions(); |
||||
|
break; |
||||
|
case RenderDataOpcode.PushEffect: |
||||
|
((IDrawingContextImplWithEffects)_context).PopEffect(); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Replay(IDrawingContextImpl context) |
||||
|
{ |
||||
|
var visitor = new ReplayVisitor(context); |
||||
|
Visit<ReplayVisitor, ReplayScope>(ref visitor); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,177 @@ |
|||||
|
using System; |
||||
|
using System.Buffers; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Media.Imaging; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Rendering.SceneGraph; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Rendering.Composition.Drawing; |
||||
|
|
||||
|
internal partial class RenderDataStream |
||||
|
{ |
||||
|
private const int MaxStackScopeDepth = 64; |
||||
|
|
||||
|
public void Visit<TVisitor, TScope>(ref TVisitor visitor) |
||||
|
where TVisitor : struct, IRenderDataVisitor<TScope> |
||||
|
where TScope : unmanaged |
||||
|
{ |
||||
|
var reader = new RenderDataReader(_writer.Written); |
||||
|
TScope[]? rented = null; |
||||
|
scoped Span<TScope> scopes; |
||||
|
if (_maxDepth == 0) |
||||
|
scopes = default; |
||||
|
else if (_maxDepth <= MaxStackScopeDepth) |
||||
|
scopes = stackalloc TScope[_maxDepth]; |
||||
|
else |
||||
|
scopes = rented = ArrayPool<TScope>.Shared.Rent(_maxDepth); |
||||
|
var depth = 0; |
||||
|
try |
||||
|
{ |
||||
|
while (!visitor.StopVisiting && !reader.IsAtEnd) |
||||
|
{ |
||||
|
switch (reader.Peek<RenderDataOpcode>()) |
||||
|
{ |
||||
|
case RenderDataOpcode.DrawLine: |
||||
|
{ |
||||
|
var p = reader.ReadPayload<DrawLinePayload>(); |
||||
|
visitor.OnDrawLine( |
||||
|
(IPen?)_resources[p.ServerPen], |
||||
|
(IPen?)_resources[p.ClientPen], |
||||
|
p.P1, p.P2); |
||||
|
break; |
||||
|
} |
||||
|
case RenderDataOpcode.DrawRectangle: |
||||
|
{ |
||||
|
var p = reader.ReadPayload<DrawRectanglePayload>(); |
||||
|
var shadows = ReadBoxShadows(ref reader, p.BoxShadowCount); |
||||
|
visitor.OnDrawRectangle( |
||||
|
(IBrush?)_resources[p.ServerBrush], |
||||
|
(IPen?)_resources[p.ServerPen], |
||||
|
(IPen?)_resources[p.ClientPen], |
||||
|
p.Rect, shadows); |
||||
|
break; |
||||
|
} |
||||
|
case RenderDataOpcode.DrawEllipse: |
||||
|
{ |
||||
|
var p = reader.ReadPayload<DrawEllipsePayload>(); |
||||
|
visitor.OnDrawEllipse( |
||||
|
(IBrush?)_resources[p.ServerBrush], |
||||
|
(IPen?)_resources[p.ServerPen], |
||||
|
(IPen?)_resources[p.ClientPen], |
||||
|
p.Rect); |
||||
|
break; |
||||
|
} |
||||
|
case RenderDataOpcode.DrawGeometry: |
||||
|
{ |
||||
|
var p = reader.ReadPayload<DrawGeometryPayload>(); |
||||
|
visitor.OnDrawGeometry( |
||||
|
(IBrush?)_resources[p.ServerBrush], |
||||
|
(IPen?)_resources[p.ServerPen], |
||||
|
(IPen?)_resources[p.ClientPen], |
||||
|
(IGeometryImpl?)_resources[p.Geometry]); |
||||
|
break; |
||||
|
} |
||||
|
case RenderDataOpcode.DrawGlyphRun: |
||||
|
{ |
||||
|
var p = reader.ReadPayload<DrawGlyphRunPayload>(); |
||||
|
visitor.OnDrawGlyphRun( |
||||
|
(IBrush?)_resources[p.ServerBrush], |
||||
|
(IRef<IGlyphRunImpl>?)_resources[p.GlyphRun]); |
||||
|
break; |
||||
|
} |
||||
|
case RenderDataOpcode.DrawBitmap: |
||||
|
{ |
||||
|
var p = reader.ReadPayload<DrawBitmapPayload>(); |
||||
|
visitor.OnDrawBitmap( |
||||
|
(IRef<IBitmapImpl>?)_resources[p.Bitmap], |
||||
|
p.Opacity, p.SourceRect, p.DestRect); |
||||
|
break; |
||||
|
} |
||||
|
case RenderDataOpcode.DrawCustom: |
||||
|
{ |
||||
|
var p = reader.ReadPayload<DrawCustomPayload>(); |
||||
|
visitor.OnDrawCustom((ICustomDrawOperation?)_resources[p.Operation]); |
||||
|
break; |
||||
|
} |
||||
|
case RenderDataOpcode.PushClip: |
||||
|
{ |
||||
|
var p = reader.ReadPayload<PushClipPayload>(); |
||||
|
scopes[depth++] = visitor.OnPushClip(p.Clip); |
||||
|
break; |
||||
|
} |
||||
|
case RenderDataOpcode.PushGeometryClip: |
||||
|
{ |
||||
|
var p = reader.ReadPayload<PushGeometryClipPayload>(); |
||||
|
scopes[depth++] = visitor.OnPushGeometryClip((IGeometryImpl?)_resources[p.Geometry]); |
||||
|
break; |
||||
|
} |
||||
|
case RenderDataOpcode.PushOpacity: |
||||
|
{ |
||||
|
var p = reader.ReadPayload<PushOpacityPayload>(); |
||||
|
scopes[depth++] = visitor.OnPushOpacity(p.Opacity); |
||||
|
break; |
||||
|
} |
||||
|
case RenderDataOpcode.PushOpacityMask: |
||||
|
{ |
||||
|
var p = reader.ReadPayload<PushOpacityMaskPayload>(); |
||||
|
scopes[depth++] = visitor.OnPushOpacityMask( |
||||
|
(IBrush?)_resources[p.Brush], p.Bounds); |
||||
|
break; |
||||
|
} |
||||
|
case RenderDataOpcode.PushTransform: |
||||
|
{ |
||||
|
var p = reader.ReadPayload<PushTransformPayload>(); |
||||
|
scopes[depth++] = visitor.OnPushTransform(p.Matrix); |
||||
|
break; |
||||
|
} |
||||
|
case RenderDataOpcode.PushRenderOptions: |
||||
|
{ |
||||
|
var p = reader.ReadPayload<PushRenderOptionsPayload>(); |
||||
|
scopes[depth++] = visitor.OnPushRenderOptions(p.Options); |
||||
|
break; |
||||
|
} |
||||
|
case RenderDataOpcode.PushTextOptions: |
||||
|
{ |
||||
|
var p = reader.ReadPayload<PushTextOptionsPayload>(); |
||||
|
scopes[depth++] = visitor.OnPushTextOptions(p.Options); |
||||
|
break; |
||||
|
} |
||||
|
case RenderDataOpcode.PushEffect: |
||||
|
{ |
||||
|
var p = reader.ReadPayload<PushEffectPayload>(); |
||||
|
scopes[depth++] = visitor.OnPushEffect( |
||||
|
(IEffect?)_resources[p.Effect], p.Bounds); |
||||
|
break; |
||||
|
} |
||||
|
case RenderDataOpcode.Pop: |
||||
|
{ |
||||
|
reader.Read<RenderDataOpcode>(); |
||||
|
visitor.OnPop(in scopes[--depth]); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
if (rented != null) |
||||
|
ArrayPool<TScope>.Shared.Return(rented); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static BoxShadows ReadBoxShadows(ref RenderDataReader reader, int count) |
||||
|
{ |
||||
|
if (count == 0) |
||||
|
return default; |
||||
|
|
||||
|
var first = reader.Read<BoxShadow>(); |
||||
|
if (count == 1) |
||||
|
return new BoxShadows(first); |
||||
|
|
||||
|
var rest = new BoxShadow[count - 1]; |
||||
|
for (var i = 0; i < rest.Length; i++) |
||||
|
rest[i] = reader.Read<BoxShadow>(); |
||||
|
return new BoxShadows(first, rest); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,226 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Rendering.Composition.Transport; |
||||
|
using Avalonia.Rendering.SceneGraph; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Rendering.Composition.Drawing; |
||||
|
|
||||
|
internal partial class RenderDataStream : IDisposable |
||||
|
{ |
||||
|
private RenderDataWriter _writer; |
||||
|
private RenderDataResources _resources; |
||||
|
private int _depth; |
||||
|
private int _maxDepth; |
||||
|
|
||||
|
public ReadOnlySpan<byte> Opcodes => _writer.Written; |
||||
|
|
||||
|
public int OpcodeLength => _writer.Length; |
||||
|
|
||||
|
public int Depth => _depth; |
||||
|
|
||||
|
public int ResourceCount => _resources.Count; |
||||
|
|
||||
|
public object? GetResource(int handle) => _resources[handle]; |
||||
|
|
||||
|
public void Rewind(int length, int depth) |
||||
|
{ |
||||
|
_writer.Rewind(length); |
||||
|
_depth = depth; |
||||
|
} |
||||
|
|
||||
|
private void EnterScope() |
||||
|
{ |
||||
|
_depth++; |
||||
|
if (_depth > _maxDepth) |
||||
|
_maxDepth = _depth; |
||||
|
} |
||||
|
|
||||
|
public void DisposeResources() |
||||
|
{ |
||||
|
for (var i = 0; i < _resources.Count; i++) |
||||
|
{ |
||||
|
switch (_resources[i]) |
||||
|
{ |
||||
|
case IRef<IBitmapImpl> bitmap: |
||||
|
bitmap.Dispose(); |
||||
|
break; |
||||
|
case IRef<IGlyphRunImpl> glyphRun: |
||||
|
glyphRun.Dispose(); |
||||
|
break; |
||||
|
case ICustomDrawOperation operation: |
||||
|
operation.Dispose(); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void DrawLine(IPen? serverPen, IPen? clientPen, Point p1, Point p2) |
||||
|
{ |
||||
|
_writer.WritePayload(new DrawLinePayload |
||||
|
{ |
||||
|
ServerPen = _resources.Intern(serverPen), |
||||
|
ClientPen = _resources.Intern(clientPen), |
||||
|
P1 = p1, |
||||
|
P2 = p2 |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public void DrawRectangle(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, RoundedRect rect, |
||||
|
BoxShadows boxShadows) |
||||
|
{ |
||||
|
_writer.WritePayload(new DrawRectanglePayload |
||||
|
{ |
||||
|
ServerBrush = _resources.Intern(serverBrush), |
||||
|
ServerPen = _resources.Intern(serverPen), |
||||
|
ClientPen = _resources.Intern(clientPen), |
||||
|
Rect = rect, |
||||
|
BoxShadowCount = boxShadows.Count |
||||
|
}); |
||||
|
for (var i = 0; i < boxShadows.Count; i++) |
||||
|
_writer.Write(boxShadows[i]); |
||||
|
} |
||||
|
|
||||
|
public void DrawEllipse(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, Rect rect) |
||||
|
{ |
||||
|
_writer.WritePayload(new DrawEllipsePayload |
||||
|
{ |
||||
|
ServerBrush = _resources.Intern(serverBrush), |
||||
|
ServerPen = _resources.Intern(serverPen), |
||||
|
ClientPen = _resources.Intern(clientPen), |
||||
|
Rect = rect |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public void DrawGeometry(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, IGeometryImpl? geometry) |
||||
|
{ |
||||
|
_writer.WritePayload(new DrawGeometryPayload |
||||
|
{ |
||||
|
ServerBrush = _resources.Intern(serverBrush), |
||||
|
ServerPen = _resources.Intern(serverPen), |
||||
|
ClientPen = _resources.Intern(clientPen), |
||||
|
Geometry = _resources.Intern(geometry) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public void DrawGlyphRun(IBrush? serverBrush, IRef<IGlyphRunImpl>? glyphRun) |
||||
|
{ |
||||
|
_writer.WritePayload(new DrawGlyphRunPayload |
||||
|
{ |
||||
|
ServerBrush = _resources.Intern(serverBrush), |
||||
|
GlyphRun = _resources.Intern(glyphRun) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public void DrawBitmap(IRef<IBitmapImpl>? bitmap, double opacity, Rect sourceRect, Rect destRect) |
||||
|
{ |
||||
|
_writer.WritePayload(new DrawBitmapPayload |
||||
|
{ |
||||
|
Bitmap = _resources.Intern(bitmap), |
||||
|
Opacity = opacity, |
||||
|
SourceRect = sourceRect, |
||||
|
DestRect = destRect |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public void DrawCustom(ICustomDrawOperation? operation) |
||||
|
{ |
||||
|
_writer.WritePayload(new DrawCustomPayload |
||||
|
{ |
||||
|
Operation = _resources.Intern(operation) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public void PushClip(RoundedRect clip) |
||||
|
{ |
||||
|
_writer.WritePayload(new PushClipPayload { Clip = clip }); |
||||
|
EnterScope(); |
||||
|
} |
||||
|
|
||||
|
public void PushGeometryClip(IGeometryImpl? geometry) |
||||
|
{ |
||||
|
_writer.WritePayload(new PushGeometryClipPayload { Geometry = _resources.Intern(geometry) }); |
||||
|
EnterScope(); |
||||
|
} |
||||
|
|
||||
|
public void PushOpacity(double opacity) |
||||
|
{ |
||||
|
_writer.WritePayload(new PushOpacityPayload { Opacity = opacity }); |
||||
|
EnterScope(); |
||||
|
} |
||||
|
|
||||
|
public void PushOpacityMask(IBrush? serverBrush, Rect bounds) |
||||
|
{ |
||||
|
_writer.WritePayload(new PushOpacityMaskPayload |
||||
|
{ |
||||
|
Brush = _resources.Intern(serverBrush), |
||||
|
Bounds = bounds |
||||
|
}); |
||||
|
EnterScope(); |
||||
|
} |
||||
|
|
||||
|
public void PushTransform(Matrix matrix) |
||||
|
{ |
||||
|
_writer.WritePayload(new PushTransformPayload { Matrix = matrix }); |
||||
|
EnterScope(); |
||||
|
} |
||||
|
|
||||
|
public void PushRenderOptions(RenderOptions renderOptions) |
||||
|
{ |
||||
|
_writer.WritePayload(new PushRenderOptionsPayload { Options = renderOptions }); |
||||
|
EnterScope(); |
||||
|
} |
||||
|
|
||||
|
public void PushTextOptions(TextOptions textOptions) |
||||
|
{ |
||||
|
_writer.WritePayload(new PushTextOptionsPayload { Options = textOptions }); |
||||
|
EnterScope(); |
||||
|
} |
||||
|
|
||||
|
public void PushEffect(IImmutableEffect? effect, Rect bounds) |
||||
|
{ |
||||
|
_writer.WritePayload(new PushEffectPayload |
||||
|
{ |
||||
|
Effect = _resources.Intern(effect), |
||||
|
Bounds = bounds |
||||
|
}); |
||||
|
EnterScope(); |
||||
|
} |
||||
|
|
||||
|
public void Pop() |
||||
|
{ |
||||
|
_writer.WriteOpcode(RenderDataOpcode.Pop); |
||||
|
_depth--; |
||||
|
} |
||||
|
|
||||
|
public void SerializeTo(BatchStreamWriter writer) |
||||
|
{ |
||||
|
var opcodes = _writer.Written; |
||||
|
writer.Write(_maxDepth); |
||||
|
writer.Write(_resources.Count); |
||||
|
for (var i = 0; i < _resources.Count; i++) |
||||
|
writer.WriteObject(_resources[i]); |
||||
|
writer.Write(opcodes.Length); |
||||
|
writer.Write(opcodes); |
||||
|
} |
||||
|
|
||||
|
public void DeserializeFrom(BatchStreamReader reader) |
||||
|
{ |
||||
|
_maxDepth = reader.Read<int>(); |
||||
|
|
||||
|
var resourceCount = reader.Read<int>(); |
||||
|
for (var i = 0; i < resourceCount; i++) |
||||
|
_resources.AppendDeserialized(reader.ReadObject()); |
||||
|
|
||||
|
var byteCount = reader.Read<int>(); |
||||
|
if (byteCount > 0) |
||||
|
reader.Read(_writer.Reserve(byteCount)); |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
_writer.Dispose(); |
||||
|
_resources.Dispose(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
using System; |
||||
|
using System.Buffers; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using System.Runtime.InteropServices; |
||||
|
|
||||
|
namespace Avalonia.Rendering.Composition.Drawing; |
||||
|
|
||||
|
internal struct RenderDataWriter : IDisposable |
||||
|
{ |
||||
|
private byte[]? _buffer; |
||||
|
private int _length; |
||||
|
|
||||
|
public int Length => _length; |
||||
|
|
||||
|
public ReadOnlySpan<byte> Written => _buffer is null ? default : _buffer.AsSpan(0, _length); |
||||
|
|
||||
|
private Span<byte> Advance(int size) |
||||
|
{ |
||||
|
var required = _length + size; |
||||
|
if (_buffer is null) |
||||
|
_buffer = ArrayPool<byte>.Shared.Rent(Math.Max(required, 256)); |
||||
|
else if (_buffer.Length < required) |
||||
|
{ |
||||
|
var grown = ArrayPool<byte>.Shared.Rent(Math.Max(required, _buffer.Length * 2)); |
||||
|
Array.Copy(_buffer, grown, _length); |
||||
|
ArrayPool<byte>.Shared.Return(_buffer); |
||||
|
_buffer = grown; |
||||
|
} |
||||
|
|
||||
|
var span = _buffer.AsSpan(_length, size); |
||||
|
_length += size; |
||||
|
return span; |
||||
|
} |
||||
|
|
||||
|
public Span<byte> Reserve(int count) => Advance(count); |
||||
|
|
||||
|
public void Rewind(int length) => _length = length; |
||||
|
|
||||
|
public void Write<T>(T value) where T : unmanaged |
||||
|
=> MemoryMarshal.Write(Advance(Unsafe.SizeOf<T>()), in value); |
||||
|
|
||||
|
public void WriteOpcode(RenderDataOpcode opcode) => Write(opcode); |
||||
|
|
||||
|
public void WritePayload<T>(T payload) where T : unmanaged, IRenderDataPayload<T> |
||||
|
{ |
||||
|
Write(T.Opcode); |
||||
|
Write(payload); |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
if (_buffer != null) |
||||
|
ArrayPool<byte>.Shared.Return(_buffer); |
||||
|
_buffer = null; |
||||
|
_length = 0; |
||||
|
} |
||||
|
} |
||||
@ -1,50 +0,0 @@ |
|||||
using Avalonia.Media; |
|
||||
using Avalonia.Rendering.Composition.Drawing.Nodes; |
|
||||
using Xunit; |
|
||||
|
|
||||
namespace Avalonia.Base.UnitTests.Rendering; |
|
||||
|
|
||||
public class RenderDataEffectNodeTests |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Regression test: RenderDataEffectNode.Bounds was returning BoundsRect even when
|
|
||||
/// there were no children, causing incorrect dirty rect tracking.
|
|
||||
/// </summary>
|
|
||||
[Fact] |
|
||||
public void Bounds_Should_Return_Null_When_No_Children() |
|
||||
{ |
|
||||
var effect = new BlurEffect { Radius = 10 }.ToImmutable(); |
|
||||
var node = new RenderDataEffectNode |
|
||||
{ |
|
||||
Effect = effect, |
|
||||
BoundsRect = new Rect(0, 0, 200, 200) |
|
||||
}; |
|
||||
|
|
||||
Assert.Null(node.Bounds); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// RenderDataEffectNode.Bounds should expand child bounds by the effect output padding
|
|
||||
/// so that dirty rects include effect output (blur/shadow extending beyond geometry bounds).
|
|
||||
/// </summary>
|
|
||||
[Fact] |
|
||||
public void Bounds_Should_Inflate_Child_Bounds_By_Effect_Output_Padding() |
|
||||
{ |
|
||||
var effect = new BlurEffect { Radius = 10 }.ToImmutable(); |
|
||||
var childBounds = new Rect(10, 10, 100, 100); |
|
||||
var node = new RenderDataEffectNode { Effect = effect }; |
|
||||
node.Children.Add(new MockRenderDataItem { MockBounds = childBounds }); |
|
||||
|
|
||||
var expectedBounds = childBounds.Inflate(effect.GetEffectOutputPadding()); |
|
||||
|
|
||||
Assert.Equal(expectedBounds, node.Bounds); |
|
||||
} |
|
||||
|
|
||||
private class MockRenderDataItem : IRenderDataItem |
|
||||
{ |
|
||||
public Rect? MockBounds { get; set; } |
|
||||
public Rect? Bounds => MockBounds; |
|
||||
public bool HitTest(Point p) => false; |
|
||||
public void Invoke(ref RenderDataNodeRenderContext context) { } |
|
||||
} |
|
||||
} |
|
||||
@ -1,67 +0,0 @@ |
|||||
using System.Collections.Generic; |
|
||||
using Avalonia.Media; |
|
||||
using Avalonia.Rendering.Composition.Drawing.Nodes; |
|
||||
using Avalonia.Rendering.SceneGraph; |
|
||||
using Xunit; |
|
||||
|
|
||||
namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph |
|
||||
{ |
|
||||
public class LineNodeTests |
|
||||
{ |
|
||||
static RenderDataLineNode LineNode(IPen pen, Point p1, Point p2) => new RenderDataLineNode |
|
||||
{ |
|
||||
P1 = p1, |
|
||||
P2 = p2, |
|
||||
ServerPen = pen, |
|
||||
ClientPen = pen |
|
||||
}; |
|
||||
|
|
||||
[Fact] |
|
||||
public void HitTest_Should_Be_True() |
|
||||
{ |
|
||||
var lineNode = LineNode( |
|
||||
new Pen(Brushes.Black, 3), |
|
||||
new Point(15, 10), |
|
||||
new Point(150, 73)); |
|
||||
|
|
||||
var pointsInside = new List<Point>() |
|
||||
{ |
|
||||
new Point(14, 8.9), |
|
||||
new Point(15, 10), |
|
||||
new Point(30, 15.5), |
|
||||
new Point(30, 18.5), |
|
||||
new Point(150, 73), |
|
||||
new Point(151, 71.9), |
|
||||
}; |
|
||||
|
|
||||
foreach (var point in pointsInside) |
|
||||
{ |
|
||||
Assert.True(lineNode.HitTest(point)); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void HitTest_Should_Be_False() |
|
||||
{ |
|
||||
var lineNode = LineNode( |
|
||||
new Pen(Brushes.Black, 3), |
|
||||
new Point(15, 10), |
|
||||
new Point(150, 73)); |
|
||||
|
|
||||
var pointsOutside = new List<Point>() |
|
||||
{ |
|
||||
new Point(14, 8), |
|
||||
new Point(14, 8.8), |
|
||||
new Point(30, 15.3), |
|
||||
new Point(30, 18.7), |
|
||||
new Point(151, 71.8), |
|
||||
new Point(155, 75), |
|
||||
}; |
|
||||
|
|
||||
foreach (var point in pointsOutside) |
|
||||
{ |
|
||||
Assert.False(lineNode.HitTest(point)); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,103 @@ |
|||||
|
using Avalonia.Rendering.Composition.Drawing; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Base.UnitTests.Rendering.SceneGraph |
||||
|
{ |
||||
|
public class RenderDataResourcesTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Intern_Null_Returns_Null_Handle() |
||||
|
{ |
||||
|
var resources = new RenderDataResources(); |
||||
|
Assert.Equal(RenderDataResources.NullHandle, resources.Intern(null)); |
||||
|
Assert.Equal(0, resources.Count); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Intern_Same_Reference_Returns_Same_Handle() |
||||
|
{ |
||||
|
var resources = new RenderDataResources(); |
||||
|
var obj = new object(); |
||||
|
|
||||
|
var first = resources.Intern(obj); |
||||
|
var second = resources.Intern(obj); |
||||
|
|
||||
|
Assert.Equal(first, second); |
||||
|
Assert.Equal(1, resources.Count); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Intern_Distinct_References_Return_Distinct_Handles() |
||||
|
{ |
||||
|
var resources = new RenderDataResources(); |
||||
|
var a = resources.Intern(new object()); |
||||
|
var b = resources.Intern(new object()); |
||||
|
|
||||
|
Assert.NotEqual(a, b); |
||||
|
Assert.Equal(2, resources.Count); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Intern_Equal_But_Distinct_References_Return_Distinct_Handles() |
||||
|
{ |
||||
|
var resources = new RenderDataResources(); |
||||
|
var a = resources.Intern(new EqualByValue()); |
||||
|
var b = resources.Intern(new EqualByValue()); |
||||
|
|
||||
|
Assert.NotEqual(a, b); |
||||
|
Assert.Equal(2, resources.Count); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Indexer_Returns_Interned_Resource() |
||||
|
{ |
||||
|
var resources = new RenderDataResources(); |
||||
|
var obj = new object(); |
||||
|
|
||||
|
var handle = resources.Intern(obj); |
||||
|
|
||||
|
Assert.Same(obj, resources[handle]); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Indexer_Null_Handle_Returns_Null() |
||||
|
{ |
||||
|
var resources = new RenderDataResources(); |
||||
|
resources.Intern(new object()); |
||||
|
|
||||
|
Assert.Null(resources[RenderDataResources.NullHandle]); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void AppendDeserialized_Appends_Without_Deduplication() |
||||
|
{ |
||||
|
var resources = new RenderDataResources(); |
||||
|
var obj = new object(); |
||||
|
|
||||
|
var first = resources.AppendDeserialized(obj); |
||||
|
var second = resources.AppendDeserialized(obj); |
||||
|
|
||||
|
Assert.NotEqual(first, second); |
||||
|
Assert.Equal(2, resources.Count); |
||||
|
Assert.Same(obj, resources[first]); |
||||
|
Assert.Same(obj, resources[second]); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Dispose_Resets_The_Table() |
||||
|
{ |
||||
|
var resources = new RenderDataResources(); |
||||
|
resources.Intern(new object()); |
||||
|
|
||||
|
resources.Dispose(); |
||||
|
|
||||
|
Assert.Equal(0, resources.Count); |
||||
|
} |
||||
|
|
||||
|
private sealed class EqualByValue |
||||
|
{ |
||||
|
public override bool Equals(object? obj) => obj is EqualByValue; |
||||
|
public override int GetHashCode() => 1; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,177 @@ |
|||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Media.Immutable; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Rendering.Composition.Drawing; |
||||
|
using Avalonia.Rendering.SceneGraph; |
||||
|
using Avalonia.Utilities; |
||||
|
using Moq; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Base.UnitTests.Rendering.SceneGraph |
||||
|
{ |
||||
|
public class RenderDataStreamBoundsTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Empty_Stream_Has_Null_Bounds() |
||||
|
{ |
||||
|
using var stream = new RenderDataStream(); |
||||
|
Assert.Null(stream.CalculateBounds()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Filled_Rectangle_Bounds_Are_The_Rectangle() |
||||
|
{ |
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.DrawRectangle(Brushes.Black, null, null, |
||||
|
new RoundedRect(new Rect(0, 0, 10, 10)), default); |
||||
|
|
||||
|
Assert.Equal(new Rect(0, 0, 10, 10), stream.CalculateBounds()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Stroked_Rectangle_Bounds_Are_Inflated_By_Half_Thickness() |
||||
|
{ |
||||
|
var pen = new ImmutablePen(Brushes.Black, 4); |
||||
|
|
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.DrawRectangle(null, pen, pen, |
||||
|
new RoundedRect(new Rect(10, 10, 20, 20)), default); |
||||
|
|
||||
|
Assert.Equal(new Rect(8, 8, 24, 24), stream.CalculateBounds()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Bounds_Are_The_Union_Of_All_Draws() |
||||
|
{ |
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.DrawRectangle(Brushes.Black, null, null, |
||||
|
new RoundedRect(new Rect(0, 0, 10, 10)), default); |
||||
|
stream.DrawRectangle(Brushes.Black, null, null, |
||||
|
new RoundedRect(new Rect(20, 20, 10, 10)), default); |
||||
|
|
||||
|
Assert.Equal(new Rect(0, 0, 30, 30), stream.CalculateBounds()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Stroked_Ellipse_Bounds_Are_Inflated_By_Thickness() |
||||
|
{ |
||||
|
var pen = new ImmutablePen(Brushes.Black, 4); |
||||
|
|
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.DrawEllipse(null, pen, pen, new Rect(0, 0, 10, 10)); |
||||
|
|
||||
|
Assert.Equal(new Rect(-4, -4, 18, 18), stream.CalculateBounds()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Bitmap_Bounds_Are_The_Destination_Rect() |
||||
|
{ |
||||
|
using var bitmap = RefCountable.Create(Mock.Of<IBitmapImpl>()); |
||||
|
|
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.DrawBitmap(bitmap, 1, new Rect(0, 0, 10, 10), new Rect(5, 5, 20, 20)); |
||||
|
|
||||
|
Assert.Equal(new Rect(5, 5, 20, 20), stream.CalculateBounds()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Custom_Operation_Bounds_Are_Used() |
||||
|
{ |
||||
|
var operation = new Mock<ICustomDrawOperation>(); |
||||
|
operation.Setup(x => x.Bounds).Returns(new Rect(1, 2, 3, 4)); |
||||
|
|
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.DrawCustom(operation.Object); |
||||
|
|
||||
|
Assert.Equal(new Rect(1, 2, 3, 4), stream.CalculateBounds()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Line_Bounds_Cover_The_Segment() |
||||
|
{ |
||||
|
var pen = new ImmutablePen(Brushes.Black, 2); |
||||
|
|
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.DrawLine(pen, pen, new Point(0, 0), new Point(100, 0)); |
||||
|
|
||||
|
var bounds = Assert.NotNull(stream.CalculateBounds()); |
||||
|
Assert.True(bounds.Contains(new Point(50, 0))); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Transform_Is_Applied_To_Bounds() |
||||
|
{ |
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.PushTransform(Matrix.CreateTranslation(50, 50)); |
||||
|
stream.DrawRectangle(Brushes.Black, null, null, |
||||
|
new RoundedRect(new Rect(0, 0, 10, 10)), default); |
||||
|
stream.Pop(); |
||||
|
|
||||
|
Assert.Equal(new Rect(50, 50, 10, 10), stream.CalculateBounds()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Nested_Transforms_Compose_For_Bounds() |
||||
|
{ |
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.PushTransform(Matrix.CreateTranslation(20, 20)); |
||||
|
stream.PushTransform(Matrix.CreateScale(2, 2)); |
||||
|
stream.DrawRectangle(Brushes.Black, null, null, |
||||
|
new RoundedRect(new Rect(0, 0, 10, 10)), default); |
||||
|
stream.Pop(); |
||||
|
stream.Pop(); |
||||
|
|
||||
|
Assert.Equal(new Rect(20, 20, 20, 20), stream.CalculateBounds()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Clip_Does_Not_Restrict_Bounds() |
||||
|
{ |
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.PushClip(new RoundedRect(new Rect(0, 0, 5, 5))); |
||||
|
stream.DrawRectangle(Brushes.Black, null, null, |
||||
|
new RoundedRect(new Rect(0, 0, 100, 100)), default); |
||||
|
stream.Pop(); |
||||
|
|
||||
|
Assert.Equal(new Rect(0, 0, 100, 100), stream.CalculateBounds()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Opacity_Push_Is_Transparent_To_Bounds() |
||||
|
{ |
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.PushOpacity(0.5); |
||||
|
stream.DrawRectangle(Brushes.Black, null, null, |
||||
|
new RoundedRect(new Rect(0, 0, 10, 10)), default); |
||||
|
stream.Pop(); |
||||
|
|
||||
|
Assert.Equal(new Rect(0, 0, 10, 10), stream.CalculateBounds()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Empty_Push_Scope_Contributes_Nothing() |
||||
|
{ |
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.DrawRectangle(Brushes.Black, null, null, |
||||
|
new RoundedRect(new Rect(0, 0, 10, 10)), default); |
||||
|
stream.PushTransform(Matrix.CreateTranslation(1000, 1000)); |
||||
|
stream.Pop(); |
||||
|
|
||||
|
Assert.Equal(new Rect(0, 0, 10, 10), stream.CalculateBounds()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Bounds_Handles_Deeply_Nested_Scopes() |
||||
|
{ |
||||
|
using var stream = new RenderDataStream(); |
||||
|
for (var i = 0; i < 100; i++) |
||||
|
stream.PushOpacity(0.5); |
||||
|
stream.DrawRectangle(Brushes.Black, null, null, |
||||
|
new RoundedRect(new Rect(0, 0, 10, 10)), default); |
||||
|
for (var i = 0; i < 100; i++) |
||||
|
stream.Pop(); |
||||
|
|
||||
|
Assert.Equal(new Rect(0, 0, 10, 10), stream.CalculateBounds()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Rendering.Composition.Drawing; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Base.UnitTests.Rendering.SceneGraph; |
||||
|
|
||||
|
public class RenderDataStreamEffectTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Effect_Inflates_Child_Bounds_By_Padding() |
||||
|
{ |
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.PushEffect(new ImmutableBlurEffect(5), new Rect(0, 0, 100, 100)); |
||||
|
stream.DrawRectangle(null, null, null, new RoundedRect(new Rect(0, 0, 100, 100)), default); |
||||
|
stream.Pop(); |
||||
|
|
||||
|
var padding = ((IEffect)new ImmutableBlurEffect(5)).GetEffectOutputPadding(); |
||||
|
Assert.Equal(new Rect(0, 0, 100, 100).Inflate(padding), stream.CalculateBounds()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Empty_Effect_Scope_Has_Null_Bounds() |
||||
|
{ |
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.PushEffect(new ImmutableBlurEffect(5), new Rect(0, 0, 100, 100)); |
||||
|
stream.Pop(); |
||||
|
Assert.Null(stream.CalculateBounds()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,208 @@ |
|||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Media.Immutable; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Rendering.Composition.Drawing; |
||||
|
using Avalonia.Rendering.SceneGraph; |
||||
|
using Avalonia.Utilities; |
||||
|
using Moq; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Base.UnitTests.Rendering.SceneGraph |
||||
|
{ |
||||
|
public class RenderDataStreamHitTestTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Filled_Rectangle_Is_Hit_Inside_And_Missed_Outside() |
||||
|
{ |
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.DrawRectangle(Brushes.Black, null, null, |
||||
|
new RoundedRect(new Rect(0, 0, 100, 100)), default); |
||||
|
|
||||
|
Assert.True(stream.HitTest(new Point(50, 50))); |
||||
|
Assert.False(stream.HitTest(new Point(150, 150))); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Stroked_Rectangle_Is_Hit_On_Border_And_Missed_In_Hollow_Center() |
||||
|
{ |
||||
|
var pen = new ImmutablePen(Brushes.Black, 4); |
||||
|
|
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.DrawRectangle(null, pen, pen, |
||||
|
new RoundedRect(new Rect(0, 0, 100, 100)), default); |
||||
|
|
||||
|
Assert.True(stream.HitTest(new Point(0, 50))); |
||||
|
Assert.False(stream.HitTest(new Point(50, 50))); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Line_Is_Hit_Along_Its_Length() |
||||
|
{ |
||||
|
var pen = new ImmutablePen(Brushes.Black, 4); |
||||
|
|
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.DrawLine(pen, pen, new Point(0, 0), new Point(100, 0)); |
||||
|
|
||||
|
Assert.True(stream.HitTest(new Point(50, 1))); |
||||
|
Assert.False(stream.HitTest(new Point(50, 50))); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Filled_Ellipse_Is_Hit_At_Center_And_Missed_At_Corner() |
||||
|
{ |
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.DrawEllipse(Brushes.Black, null, null, new Rect(0, 0, 100, 100)); |
||||
|
|
||||
|
Assert.True(stream.HitTest(new Point(50, 50))); |
||||
|
Assert.False(stream.HitTest(new Point(2, 2))); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Geometry_Is_Hit_Via_FillContains() |
||||
|
{ |
||||
|
var geometry = new Mock<IGeometryImpl>(); |
||||
|
geometry.Setup(x => x.FillContains(new Point(5, 5))).Returns(true); |
||||
|
geometry.Setup(x => x.FillContains(new Point(50, 50))).Returns(false); |
||||
|
|
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.DrawGeometry(Brushes.Black, null, null, geometry.Object); |
||||
|
|
||||
|
Assert.True(stream.HitTest(new Point(5, 5))); |
||||
|
Assert.False(stream.HitTest(new Point(50, 50))); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Bitmap_Is_Hit_Within_Its_Destination_Rect() |
||||
|
{ |
||||
|
using var bitmap = RefCountable.Create(Mock.Of<IBitmapImpl>()); |
||||
|
|
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.DrawBitmap(bitmap, 1, new Rect(0, 0, 10, 10), new Rect(20, 20, 30, 30)); |
||||
|
|
||||
|
Assert.True(stream.HitTest(new Point(25, 25))); |
||||
|
Assert.False(stream.HitTest(new Point(5, 5))); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Custom_Operation_Hit_Test_Is_Delegated() |
||||
|
{ |
||||
|
var operation = new Mock<ICustomDrawOperation>(); |
||||
|
operation.Setup(x => x.HitTest(new Point(5, 5))).Returns(true); |
||||
|
|
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.DrawCustom(operation.Object); |
||||
|
|
||||
|
Assert.True(stream.HitTest(new Point(5, 5))); |
||||
|
Assert.False(stream.HitTest(new Point(99, 99))); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Clip_Restricts_The_Hit_Region() |
||||
|
{ |
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.PushClip(new RoundedRect(new Rect(0, 0, 10, 10))); |
||||
|
stream.DrawRectangle(Brushes.Black, null, null, |
||||
|
new RoundedRect(new Rect(0, 0, 100, 100)), default); |
||||
|
stream.Pop(); |
||||
|
|
||||
|
Assert.True(stream.HitTest(new Point(5, 5))); |
||||
|
Assert.False(stream.HitTest(new Point(50, 50))); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Geometry_Clip_Restricts_The_Hit_Region() |
||||
|
{ |
||||
|
var geometry = new Mock<IGeometryImpl>(); |
||||
|
geometry.Setup(x => x.FillContains(new Point(5, 5))).Returns(true); |
||||
|
geometry.Setup(x => x.FillContains(new Point(50, 50))).Returns(false); |
||||
|
|
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.PushGeometryClip(geometry.Object); |
||||
|
stream.DrawRectangle(Brushes.Black, null, null, |
||||
|
new RoundedRect(new Rect(0, 0, 100, 100)), default); |
||||
|
stream.Pop(); |
||||
|
|
||||
|
Assert.True(stream.HitTest(new Point(5, 5))); |
||||
|
Assert.False(stream.HitTest(new Point(50, 50))); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Transform_Maps_Hit_Test_Coordinates() |
||||
|
{ |
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.PushTransform(Matrix.CreateTranslation(50, 50)); |
||||
|
stream.DrawRectangle(Brushes.Black, null, null, |
||||
|
new RoundedRect(new Rect(0, 0, 10, 10)), default); |
||||
|
stream.Pop(); |
||||
|
|
||||
|
Assert.True(stream.HitTest(new Point(55, 55))); |
||||
|
Assert.False(stream.HitTest(new Point(5, 5))); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Nested_Transforms_Compose() |
||||
|
{ |
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.PushTransform(Matrix.CreateTranslation(20, 20)); |
||||
|
stream.PushTransform(Matrix.CreateScale(2, 2)); |
||||
|
stream.DrawRectangle(Brushes.Black, null, null, |
||||
|
new RoundedRect(new Rect(0, 0, 10, 10)), default); |
||||
|
stream.Pop(); |
||||
|
stream.Pop(); |
||||
|
|
||||
|
Assert.True(stream.HitTest(new Point(30, 30))); |
||||
|
Assert.False(stream.HitTest(new Point(5, 5))); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Singular_Transform_Excludes_Its_Scope() |
||||
|
{ |
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.PushTransform(new Matrix()); |
||||
|
stream.DrawRectangle(Brushes.Black, null, null, |
||||
|
new RoundedRect(new Rect(0, 0, 100, 100)), default); |
||||
|
stream.Pop(); |
||||
|
|
||||
|
Assert.False(stream.HitTest(new Point(50, 50))); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Opacity_Push_Is_Transparent_To_Hit_Testing() |
||||
|
{ |
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.PushOpacity(0.5); |
||||
|
stream.DrawRectangle(Brushes.Black, null, null, |
||||
|
new RoundedRect(new Rect(0, 0, 100, 100)), default); |
||||
|
stream.Pop(); |
||||
|
|
||||
|
Assert.True(stream.HitTest(new Point(50, 50))); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Scope_State_Is_Restored_After_Pop() |
||||
|
{ |
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.PushTransform(Matrix.CreateTranslation(1000, 1000)); |
||||
|
stream.Pop(); |
||||
|
stream.DrawRectangle(Brushes.Black, null, null, |
||||
|
new RoundedRect(new Rect(0, 0, 10, 10)), default); |
||||
|
|
||||
|
Assert.True(stream.HitTest(new Point(5, 5))); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Hit_Test_Handles_Deeply_Nested_Scopes() |
||||
|
{ |
||||
|
using var stream = new RenderDataStream(); |
||||
|
for (var i = 0; i < 100; i++) |
||||
|
stream.PushOpacity(0.5); |
||||
|
stream.DrawRectangle(Brushes.Black, null, null, |
||||
|
new RoundedRect(new Rect(0, 0, 10, 10)), default); |
||||
|
for (var i = 0; i < 100; i++) |
||||
|
stream.Pop(); |
||||
|
|
||||
|
Assert.True(stream.HitTest(new Point(5, 5))); |
||||
|
Assert.False(stream.HitTest(new Point(50, 50))); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,55 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Rendering.Composition.Drawing; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Base.UnitTests.Rendering.SceneGraph |
||||
|
{ |
||||
|
public class RenderDataStreamLineHitTestTests |
||||
|
{ |
||||
|
private static RenderDataStream LineStream(IPen pen, Point p1, Point p2) |
||||
|
{ |
||||
|
var stream = new RenderDataStream(); |
||||
|
stream.DrawLine(pen, pen, p1, p2); |
||||
|
return stream; |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void HitTest_Should_Be_True() |
||||
|
{ |
||||
|
using var stream = LineStream(new Pen(Brushes.Black, 3), new Point(15, 10), new Point(150, 73)); |
||||
|
|
||||
|
var pointsInside = new List<Point> |
||||
|
{ |
||||
|
new Point(14, 8.9), |
||||
|
new Point(15, 10), |
||||
|
new Point(30, 15.5), |
||||
|
new Point(30, 18.5), |
||||
|
new Point(150, 73), |
||||
|
new Point(151, 71.9), |
||||
|
}; |
||||
|
|
||||
|
foreach (var point in pointsInside) |
||||
|
Assert.True(stream.HitTest(point)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void HitTest_Should_Be_False() |
||||
|
{ |
||||
|
using var stream = LineStream(new Pen(Brushes.Black, 3), new Point(15, 10), new Point(150, 73)); |
||||
|
|
||||
|
var pointsOutside = new List<Point> |
||||
|
{ |
||||
|
new Point(14, 8), |
||||
|
new Point(14, 8.8), |
||||
|
new Point(30, 15.3), |
||||
|
new Point(30, 18.7), |
||||
|
new Point(151, 71.8), |
||||
|
new Point(155, 75), |
||||
|
}; |
||||
|
|
||||
|
foreach (var point in pointsOutside) |
||||
|
Assert.False(stream.HitTest(point)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,98 @@ |
|||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Rendering.Composition.Drawing; |
||||
|
using Avalonia.Rendering.Composition.Transport; |
||||
|
using Moq; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Base.UnitTests.Rendering.SceneGraph |
||||
|
{ |
||||
|
public class RenderDataStreamSerializationTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Round_Trip_Preserves_Bounds() |
||||
|
{ |
||||
|
using var source = new RenderDataStream(); |
||||
|
source.DrawRectangle(Brushes.Black, null, null, |
||||
|
new RoundedRect(new Rect(0, 0, 10, 10)), default); |
||||
|
source.PushTransform(Matrix.CreateTranslation(40, 40)); |
||||
|
source.DrawRectangle(Brushes.Black, null, null, |
||||
|
new RoundedRect(new Rect(0, 0, 10, 10)), default); |
||||
|
source.Pop(); |
||||
|
|
||||
|
using var result = RoundTrip(source); |
||||
|
|
||||
|
Assert.Equal(source.CalculateBounds(), result.CalculateBounds()); |
||||
|
Assert.Equal(new Rect(0, 0, 50, 50), result.CalculateBounds()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Round_Trip_Preserves_Hit_Testing() |
||||
|
{ |
||||
|
using var source = new RenderDataStream(); |
||||
|
source.PushClip(new RoundedRect(new Rect(0, 0, 10, 10))); |
||||
|
source.DrawRectangle(Brushes.Black, null, null, |
||||
|
new RoundedRect(new Rect(0, 0, 100, 100)), default); |
||||
|
source.Pop(); |
||||
|
|
||||
|
using var result = RoundTrip(source); |
||||
|
|
||||
|
Assert.True(result.HitTest(new Point(5, 5))); |
||||
|
Assert.False(result.HitTest(new Point(50, 50))); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Round_Trip_Preserves_Resource_References() |
||||
|
{ |
||||
|
var brush = Mock.Of<IBrush>(); |
||||
|
using var source = new RenderDataStream(); |
||||
|
source.DrawRectangle(brush, null, null, |
||||
|
new RoundedRect(new Rect(0, 0, 10, 10)), default); |
||||
|
|
||||
|
using var result = RoundTrip(source); |
||||
|
|
||||
|
var context = new Mock<IDrawingContextImpl>(); |
||||
|
result.Replay(context.Object); |
||||
|
context.Verify(x => x.DrawRectangle(brush, null, |
||||
|
It.IsAny<RoundedRect>(), It.IsAny<BoxShadows>()), Times.Once); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Round_Trip_Of_Empty_Stream_Produces_Empty_Stream() |
||||
|
{ |
||||
|
using var source = new RenderDataStream(); |
||||
|
using var result = RoundTrip(source); |
||||
|
|
||||
|
Assert.Null(result.CalculateBounds()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Round_Trip_Spanning_Multiple_Stream_Segments() |
||||
|
{ |
||||
|
using var source = new RenderDataStream(); |
||||
|
for (var i = 0; i < 50; i++) |
||||
|
source.DrawRectangle(Brushes.Black, null, null, |
||||
|
new RoundedRect(new Rect(i, i, 1, 1)), default); |
||||
|
|
||||
|
using var result = RoundTrip(source); |
||||
|
|
||||
|
Assert.Equal(source.CalculateBounds(), result.CalculateBounds()); |
||||
|
} |
||||
|
|
||||
|
private static RenderDataStream RoundTrip(RenderDataStream source) |
||||
|
{ |
||||
|
var data = new BatchStreamData(); |
||||
|
var memoryPool = new BatchStreamMemoryPool(false, 64, _ => { }); |
||||
|
var objectPool = new BatchStreamObjectPool<object?>(false, 8, _ => { }); |
||||
|
|
||||
|
using (var writer = new BatchStreamWriter(data, memoryPool, objectPool)) |
||||
|
source.SerializeTo(writer); |
||||
|
|
||||
|
var result = new RenderDataStream(); |
||||
|
using (var reader = new BatchStreamReader(data, memoryPool, objectPool)) |
||||
|
result.DeserializeFrom(reader); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,228 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Rendering.Composition.Drawing; |
||||
|
using Avalonia.Rendering.SceneGraph; |
||||
|
using Avalonia.Utilities; |
||||
|
using Moq; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Base.UnitTests.Rendering.SceneGraph |
||||
|
{ |
||||
|
public class RenderDataStreamTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Replay_Forwards_Line() |
||||
|
{ |
||||
|
var pen = Mock.Of<IPen>(); |
||||
|
var context = new Mock<IDrawingContextImpl>(); |
||||
|
|
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.DrawLine(pen, pen, new Point(1, 2), new Point(3, 4)); |
||||
|
stream.Replay(context.Object); |
||||
|
|
||||
|
context.Verify(x => x.DrawLine(pen, new Point(1, 2), new Point(3, 4)), Times.Once); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Replay_Forwards_Rectangle_With_Box_Shadows() |
||||
|
{ |
||||
|
var brush = Mock.Of<IBrush>(); |
||||
|
var pen = Mock.Of<IPen>(); |
||||
|
var rect = new RoundedRect(new Rect(0, 0, 10, 20)); |
||||
|
var shadows = new BoxShadows( |
||||
|
new BoxShadow { Blur = 1 }, |
||||
|
new[] { new BoxShadow { Blur = 2 } }); |
||||
|
var context = new Mock<IDrawingContextImpl>(); |
||||
|
|
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.DrawRectangle(brush, pen, pen, rect, shadows); |
||||
|
stream.Replay(context.Object); |
||||
|
|
||||
|
context.Verify(x => x.DrawRectangle(brush, pen, rect, |
||||
|
It.Is<BoxShadows>(s => s.Count == 2)), Times.Once); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Replay_Forwards_Custom_Operation() |
||||
|
{ |
||||
|
var operation = new Mock<ICustomDrawOperation>(); |
||||
|
var context = new Mock<IDrawingContextImpl>(); |
||||
|
|
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.DrawCustom(operation.Object); |
||||
|
stream.Replay(context.Object); |
||||
|
|
||||
|
operation.Verify(x => x.Render(It.IsAny<ImmediateDrawingContext>()), Times.Once); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Replay_Pop_Dispatches_To_Matching_Pop_In_Lifo_Order() |
||||
|
{ |
||||
|
var calls = new List<string>(); |
||||
|
var context = RecordingContext(calls); |
||||
|
|
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.PushClip(new RoundedRect(new Rect(0, 0, 10, 10))); |
||||
|
stream.PushOpacity(0.5); |
||||
|
stream.Pop(); |
||||
|
stream.Pop(); |
||||
|
stream.Replay(context.Object); |
||||
|
|
||||
|
Assert.Equal(new[] { "PushClip", "PushOpacity", "PopOpacity", "PopClip" }, calls); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Replay_Applies_And_Restores_Transform() |
||||
|
{ |
||||
|
var context = new Mock<IDrawingContextImpl>(); |
||||
|
context.SetupProperty(x => x.Transform); |
||||
|
context.Object.Transform = Matrix.Identity; |
||||
|
var matrix = Matrix.CreateTranslation(5, 7); |
||||
|
|
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.PushTransform(matrix); |
||||
|
stream.Pop(); |
||||
|
|
||||
|
stream.Replay(context.Object); |
||||
|
|
||||
|
Assert.Equal(Matrix.Identity, context.Object.Transform); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Replay_Skips_Opacity_One_Push() |
||||
|
{ |
||||
|
var calls = new List<string>(); |
||||
|
var context = RecordingContext(calls); |
||||
|
|
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.PushOpacity(1); |
||||
|
stream.Pop(); |
||||
|
stream.Replay(context.Object); |
||||
|
|
||||
|
Assert.Empty(calls); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Replay_Skips_Null_Geometry_Clip() |
||||
|
{ |
||||
|
var calls = new List<string>(); |
||||
|
var context = RecordingContext(calls); |
||||
|
|
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.PushGeometryClip(null); |
||||
|
stream.Pop(); |
||||
|
stream.Replay(context.Object); |
||||
|
|
||||
|
Assert.Empty(calls); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Replay_Walks_Nested_Pushes_In_Order() |
||||
|
{ |
||||
|
var calls = new List<string>(); |
||||
|
var context = RecordingContext(calls); |
||||
|
|
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.PushClip(new RoundedRect(new Rect(0, 0, 10, 10))); |
||||
|
stream.PushGeometryClip(Mock.Of<IGeometryImpl>()); |
||||
|
stream.Pop(); |
||||
|
stream.Pop(); |
||||
|
stream.Replay(context.Object); |
||||
|
|
||||
|
Assert.Equal( |
||||
|
new[] { "PushClip", "PushGeometryClip", "PopGeometryClip", "PopClip" }, |
||||
|
calls); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Replay_Forwards_Render_Options() |
||||
|
{ |
||||
|
var options = new RenderOptions { EdgeMode = EdgeMode.Aliased }; |
||||
|
var context = new Mock<IDrawingContextImpl>(); |
||||
|
|
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.PushRenderOptions(options); |
||||
|
stream.Pop(); |
||||
|
stream.Replay(context.Object); |
||||
|
|
||||
|
context.Verify(x => x.PushRenderOptions(options), Times.Once); |
||||
|
context.Verify(x => x.PopRenderOptions(), Times.Once); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Replay_Forwards_Text_Options() |
||||
|
{ |
||||
|
var options = new TextOptions { TextRenderingMode = TextRenderingMode.Antialias }; |
||||
|
var context = new Mock<IDrawingContextImpl>(); |
||||
|
|
||||
|
using var stream = new RenderDataStream(); |
||||
|
stream.PushTextOptions(options); |
||||
|
stream.Pop(); |
||||
|
stream.Replay(context.Object); |
||||
|
|
||||
|
context.Verify(x => x.PushTextOptions(options), Times.Once); |
||||
|
context.Verify(x => x.PopTextOptions(), Times.Once); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Dispose_Resources_Disposes_Owned_Resources() |
||||
|
{ |
||||
|
var bitmap = RefCountable.Create(Mock.Of<IBitmapImpl>()); |
||||
|
var glyphRun = RefCountable.Create(Mock.Of<IGlyphRunImpl>()); |
||||
|
var operation = new Mock<ICustomDrawOperation>(); |
||||
|
|
||||
|
using (var stream = new RenderDataStream()) |
||||
|
{ |
||||
|
stream.DrawBitmap(bitmap.Clone(), 1, new Rect(0, 0, 1, 1), new Rect(0, 0, 1, 1)); |
||||
|
stream.DrawGlyphRun(null, glyphRun.Clone()); |
||||
|
stream.DrawCustom(operation.Object); |
||||
|
|
||||
|
Assert.Equal(2, bitmap.RefCount); |
||||
|
Assert.Equal(2, glyphRun.RefCount); |
||||
|
|
||||
|
stream.DisposeResources(); |
||||
|
} |
||||
|
|
||||
|
Assert.Equal(1, bitmap.RefCount); |
||||
|
Assert.Equal(1, glyphRun.RefCount); |
||||
|
operation.Verify(x => x.Dispose(), Times.Once); |
||||
|
|
||||
|
bitmap.Dispose(); |
||||
|
glyphRun.Dispose(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Replay_Handles_Deeply_Nested_Scopes() |
||||
|
{ |
||||
|
var context = new Mock<IDrawingContextImpl>(); |
||||
|
|
||||
|
using var stream = new RenderDataStream(); |
||||
|
for (var i = 0; i < 100; i++) |
||||
|
stream.PushOpacity(0.5); |
||||
|
stream.DrawRectangle(Brushes.Black, null, null, |
||||
|
new RoundedRect(new Rect(0, 0, 10, 10)), default); |
||||
|
for (var i = 0; i < 100; i++) |
||||
|
stream.Pop(); |
||||
|
|
||||
|
stream.Replay(context.Object); |
||||
|
|
||||
|
context.Verify(x => x.DrawRectangle(Brushes.Black, null, |
||||
|
It.IsAny<RoundedRect>(), It.IsAny<BoxShadows>()), Times.Once); |
||||
|
} |
||||
|
|
||||
|
private static Mock<IDrawingContextImpl> RecordingContext(List<string> calls) |
||||
|
{ |
||||
|
var context = new Mock<IDrawingContextImpl>(); |
||||
|
context.Setup(x => x.PushClip(It.IsAny<RoundedRect>())).Callback(() => calls.Add("PushClip")); |
||||
|
context.Setup(x => x.PopClip()).Callback(() => calls.Add("PopClip")); |
||||
|
context.Setup(x => x.PushGeometryClip(It.IsAny<IGeometryImpl>())) |
||||
|
.Callback(() => calls.Add("PushGeometryClip")); |
||||
|
context.Setup(x => x.PopGeometryClip()).Callback(() => calls.Add("PopGeometryClip")); |
||||
|
context.Setup(x => x.PushOpacity(It.IsAny<double>(), It.IsAny<Rect?>())) |
||||
|
.Callback(() => calls.Add("PushOpacity")); |
||||
|
context.Setup(x => x.PopOpacity()).Callback(() => calls.Add("PopOpacity")); |
||||
|
return context; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,237 @@ |
|||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Media.Imaging; |
||||
|
using Avalonia.Rendering.Composition.Drawing; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Base.UnitTests.Rendering.SceneGraph |
||||
|
{ |
||||
|
public class RenderDataWriterReaderTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Primitives_Round_Trip() |
||||
|
{ |
||||
|
var writer = new RenderDataWriter(); |
||||
|
try |
||||
|
{ |
||||
|
writer.Write<byte>(200); |
||||
|
writer.WriteOpcode(RenderDataOpcode.DrawGeometry); |
||||
|
writer.Write(-123456); |
||||
|
writer.Write(4000000000u); |
||||
|
writer.Write(3.14159); |
||||
|
writer.Write(true); |
||||
|
writer.Write(false); |
||||
|
|
||||
|
var reader = new RenderDataReader(writer.Written); |
||||
|
Assert.Equal(200, reader.Read<byte>()); |
||||
|
Assert.Equal(RenderDataOpcode.DrawGeometry, reader.Read<RenderDataOpcode>()); |
||||
|
Assert.Equal(-123456, reader.Read<int>()); |
||||
|
Assert.Equal(4000000000u, reader.Read<uint>()); |
||||
|
Assert.Equal(3.14159, reader.Read<double>()); |
||||
|
Assert.True(reader.Read<bool>()); |
||||
|
Assert.False(reader.Read<bool>()); |
||||
|
Assert.True(reader.IsAtEnd); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
writer.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Geometric_Structs_Round_Trip() |
||||
|
{ |
||||
|
var point = new Point(1, 2); |
||||
|
var vector = new Vector(3, 4); |
||||
|
var rect = new Rect(5, 6, 7, 8); |
||||
|
var roundedRect = new RoundedRect(new Rect(1, 2, 30, 40), |
||||
|
new Vector(1, 1), new Vector(2, 2), new Vector(3, 3), new Vector(4, 4)); |
||||
|
var matrix = new Matrix(1, 2, 3, 4, 5, 6, 7, 8, 9); |
||||
|
|
||||
|
var writer = new RenderDataWriter(); |
||||
|
try |
||||
|
{ |
||||
|
writer.Write(point); |
||||
|
writer.Write(vector); |
||||
|
writer.Write(rect); |
||||
|
writer.Write(roundedRect); |
||||
|
writer.Write(matrix); |
||||
|
|
||||
|
var reader = new RenderDataReader(writer.Written); |
||||
|
Assert.Equal(point, reader.Read<Point>()); |
||||
|
Assert.Equal(vector, reader.Read<Vector>()); |
||||
|
Assert.Equal(rect, reader.Read<Rect>()); |
||||
|
|
||||
|
var readRounded = reader.Read<RoundedRect>(); |
||||
|
Assert.Equal(roundedRect.Rect, readRounded.Rect); |
||||
|
Assert.Equal(roundedRect.RadiiTopLeft, readRounded.RadiiTopLeft); |
||||
|
Assert.Equal(roundedRect.RadiiTopRight, readRounded.RadiiTopRight); |
||||
|
Assert.Equal(roundedRect.RadiiBottomRight, readRounded.RadiiBottomRight); |
||||
|
Assert.Equal(roundedRect.RadiiBottomLeft, readRounded.RadiiBottomLeft); |
||||
|
|
||||
|
Assert.Equal(matrix, reader.Read<Matrix>()); |
||||
|
Assert.True(reader.IsAtEnd); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
writer.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Color_And_BoxShadow_Round_Trip() |
||||
|
{ |
||||
|
var color = Color.FromArgb(10, 20, 30, 40); |
||||
|
var shadow = new BoxShadow |
||||
|
{ |
||||
|
OffsetX = 1.5, |
||||
|
OffsetY = -2.5, |
||||
|
Blur = 3, |
||||
|
Spread = 4, |
||||
|
Color = Color.FromArgb(255, 1, 2, 3), |
||||
|
IsInset = true |
||||
|
}; |
||||
|
|
||||
|
var writer = new RenderDataWriter(); |
||||
|
try |
||||
|
{ |
||||
|
writer.Write(color); |
||||
|
writer.Write(shadow); |
||||
|
|
||||
|
var reader = new RenderDataReader(writer.Written); |
||||
|
Assert.Equal(color, reader.Read<Color>()); |
||||
|
|
||||
|
var readShadow = reader.Read<BoxShadow>(); |
||||
|
Assert.Equal(shadow.OffsetX, readShadow.OffsetX); |
||||
|
Assert.Equal(shadow.OffsetY, readShadow.OffsetY); |
||||
|
Assert.Equal(shadow.Blur, readShadow.Blur); |
||||
|
Assert.Equal(shadow.Spread, readShadow.Spread); |
||||
|
Assert.Equal(shadow.Color, readShadow.Color); |
||||
|
Assert.Equal(shadow.IsInset, readShadow.IsInset); |
||||
|
Assert.True(reader.IsAtEnd); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
writer.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void RenderOptions_And_TextOptions_Round_Trip() |
||||
|
{ |
||||
|
var renderOptions = new RenderOptions |
||||
|
{ |
||||
|
#pragma warning disable CS0618
|
||||
|
TextRenderingMode = TextRenderingMode.Antialias, |
||||
|
#pragma warning restore CS0618
|
||||
|
BitmapInterpolationMode = BitmapInterpolationMode.HighQuality, |
||||
|
EdgeMode = EdgeMode.Aliased, |
||||
|
BitmapBlendingMode = BitmapBlendingMode.Plus, |
||||
|
RequiresFullOpacityHandling = true |
||||
|
}; |
||||
|
var textOptions = new TextOptions |
||||
|
{ |
||||
|
TextRenderingMode = TextRenderingMode.SubpixelAntialias, |
||||
|
TextHintingMode = TextHintingMode.Light, |
||||
|
BaselinePixelAlignment = BaselinePixelAlignment.Aligned |
||||
|
}; |
||||
|
|
||||
|
var writer = new RenderDataWriter(); |
||||
|
try |
||||
|
{ |
||||
|
writer.Write(renderOptions); |
||||
|
writer.Write(textOptions); |
||||
|
|
||||
|
var reader = new RenderDataReader(writer.Written); |
||||
|
Assert.Equal(renderOptions, reader.Read<RenderOptions>()); |
||||
|
Assert.Equal(textOptions, reader.Read<TextOptions>()); |
||||
|
Assert.True(reader.IsAtEnd); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
writer.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(null)] |
||||
|
[InlineData(true)] |
||||
|
[InlineData(false)] |
||||
|
public void Nullable_Boolean_Round_Trips_All_Three_States(bool? value) |
||||
|
{ |
||||
|
var writer = new RenderDataWriter(); |
||||
|
try |
||||
|
{ |
||||
|
writer.Write(new RenderOptions { RequiresFullOpacityHandling = value }); |
||||
|
|
||||
|
var reader = new RenderDataReader(writer.Written); |
||||
|
Assert.Equal(value, reader.Read<RenderOptions>().RequiresFullOpacityHandling); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
writer.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Length_Tracks_Written_Bytes() |
||||
|
{ |
||||
|
var writer = new RenderDataWriter(); |
||||
|
try |
||||
|
{ |
||||
|
Assert.Equal(0, writer.Length); |
||||
|
writer.Write<byte>(1); |
||||
|
Assert.Equal(1, writer.Length); |
||||
|
writer.Write(2); |
||||
|
Assert.Equal(5, writer.Length); |
||||
|
writer.Write(3d); |
||||
|
Assert.Equal(13, writer.Length); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
writer.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Writer_Grows_Buffer_To_Fit_Large_Payloads() |
||||
|
{ |
||||
|
var writer = new RenderDataWriter(); |
||||
|
try |
||||
|
{ |
||||
|
for (var i = 0; i < 1000; i++) |
||||
|
writer.Write(i); |
||||
|
|
||||
|
Assert.Equal(4000, writer.Length); |
||||
|
|
||||
|
var reader = new RenderDataReader(writer.Written); |
||||
|
for (var i = 0; i < 1000; i++) |
||||
|
Assert.Equal(i, reader.Read<int>()); |
||||
|
Assert.True(reader.IsAtEnd); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
writer.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Payload_Auto_Prepends_Opcode() |
||||
|
{ |
||||
|
var writer = new RenderDataWriter(); |
||||
|
try |
||||
|
{ |
||||
|
writer.WritePayload(new PushOpacityPayload { Opacity = 0.5 }); |
||||
|
|
||||
|
var reader = new RenderDataReader(writer.Written); |
||||
|
Assert.Equal(RenderDataOpcode.PushOpacity, reader.Read<RenderDataOpcode>()); |
||||
|
var payload = reader.Read<PushOpacityPayload>(); |
||||
|
Assert.Equal(0.5, payload.Opacity); |
||||
|
Assert.True(reader.IsAtEnd); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
writer.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue