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