245 changed files with 13623 additions and 846 deletions
@ -0,0 +1,5 @@ |
|||
<ProjectConfiguration> |
|||
<Settings> |
|||
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely> |
|||
</Settings> |
|||
</ProjectConfiguration> |
|||
@ -0,0 +1,45 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:pages="clr-namespace:ControlCatalog.Pages" |
|||
x:Class="ControlCatalog.Pages.CompositionPage"> |
|||
<StackPanel> |
|||
<TextBlock Classes="h1">Implicit animations</TextBlock> |
|||
<Grid ColumnDefinitions="*,10,40" Margin="0 0 40 0"> |
|||
<ItemsControl x:Name="Items"> |
|||
<ItemsControl.ItemsPanel> |
|||
<ItemsPanelTemplate> |
|||
<WrapPanel/> |
|||
</ItemsPanelTemplate> |
|||
</ItemsControl.ItemsPanel> |
|||
<ItemsControl.DataTemplates> |
|||
<DataTemplate DataType="pages:CompositionPageColorItem"> |
|||
<Border |
|||
pages:CompositionPage.EnableAnimations="True" |
|||
Padding="10" BorderBrush="Gray" BorderThickness="2" |
|||
Background="{Binding ColorBrush}" Width="100" Height="100" Margin="10"> |
|||
<TextBlock Text="{Binding ColorHexValue}"/> |
|||
</Border> |
|||
</DataTemplate> |
|||
</ItemsControl.DataTemplates> |
|||
</ItemsControl> |
|||
<GridSplitter Margin="2" BorderThickness="1" BorderBrush="Gray" |
|||
Background="#e0e0e0" Grid.Column="1" |
|||
ResizeDirection="Columns" ResizeBehavior="PreviousAndNext" |
|||
/> |
|||
<Border Grid.Column="2"> |
|||
<LayoutTransformControl |
|||
HorizontalAlignment="Center" |
|||
|
|||
MinWidth="30"> |
|||
<LayoutTransformControl.LayoutTransform> |
|||
<RotateTransform Angle="90"/> |
|||
</LayoutTransformControl.LayoutTransform> |
|||
|
|||
<TextBlock>Resize me</TextBlock> |
|||
</LayoutTransformControl> |
|||
</Border> |
|||
</Grid> |
|||
|
|||
|
|||
</StackPanel> |
|||
</UserControl> |
|||
@ -0,0 +1,153 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Numerics; |
|||
using System.Threading.Tasks; |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Markup.Xaml; |
|||
using Avalonia.Markup.Xaml.Templates; |
|||
using Avalonia.Media; |
|||
using Avalonia.Rendering.Composition; |
|||
using Avalonia.Rendering.Composition.Animations; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace ControlCatalog.Pages; |
|||
|
|||
public partial class CompositionPage : UserControl |
|||
{ |
|||
private ImplicitAnimationCollection _implicitAnimations; |
|||
|
|||
public CompositionPage() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) |
|||
{ |
|||
base.OnAttachedToVisualTree(e); |
|||
this.FindControl<ItemsControl>("Items").Items = CreateColorItems(); |
|||
} |
|||
|
|||
private List<CompositionPageColorItem> CreateColorItems() |
|||
{ |
|||
var list = new List<CompositionPageColorItem>(); |
|||
|
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 255, 185, 0))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 231, 72, 86))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 120, 215))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 153, 188))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 122, 117, 116))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 118, 118, 118))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 255, 141, 0))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 232, 17, 35))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 99, 177))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 45, 125, 154))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 93, 90, 88))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 76, 74, 72))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 247, 99, 12))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 234, 0, 94))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 142, 140, 216))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 183, 195))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 104, 118, 138))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 105, 121, 126))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 202, 80, 16))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 195, 0, 82))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 107, 105, 214))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 3, 131, 135))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 81, 92, 107))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 74, 84, 89))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 218, 59, 1))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 227, 0, 140))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 135, 100, 184))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 178, 148))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 86, 124, 115))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 100, 124, 100))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 239, 105, 80))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 191, 0, 119))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 116, 77, 169))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 1, 133, 116))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 72, 104, 96))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 82, 94, 84))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 209, 52, 56))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 194, 57, 179))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 177, 70, 194))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 204, 106))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 73, 130, 5))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 132, 117, 69))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 255, 67, 67))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 154, 0, 137))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 136, 23, 152))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 16, 137, 62))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 16, 124, 16))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 126, 115, 95))); |
|||
|
|||
return list; |
|||
} |
|||
|
|||
private void EnsureImplicitAnimations() |
|||
{ |
|||
if (_implicitAnimations == null) |
|||
{ |
|||
var compositor = ElementComposition.GetElementVisual(this)!.Compositor; |
|||
|
|||
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); |
|||
offsetAnimation.Target = "Offset"; |
|||
offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); |
|||
offsetAnimation.Duration = TimeSpan.FromMilliseconds(400); |
|||
|
|||
var rotationAnimation = compositor.CreateScalarKeyFrameAnimation(); |
|||
rotationAnimation.Target = "RotationAngle"; |
|||
rotationAnimation.InsertKeyFrame(.5f, 0.160f); |
|||
rotationAnimation.InsertKeyFrame(1f, 0f); |
|||
rotationAnimation.Duration = TimeSpan.FromMilliseconds(400); |
|||
|
|||
var animationGroup = compositor.CreateAnimationGroup(); |
|||
animationGroup.Add(offsetAnimation); |
|||
animationGroup.Add(rotationAnimation); |
|||
|
|||
_implicitAnimations = compositor.CreateImplicitAnimationCollection(); |
|||
_implicitAnimations["Offset"] = animationGroup; |
|||
} |
|||
} |
|||
|
|||
public static void SetEnableAnimations(Border border, bool value) |
|||
{ |
|||
|
|||
var page = border.FindAncestorOfType<CompositionPage>(); |
|||
if (page == null) |
|||
{ |
|||
border.AttachedToVisualTree += delegate { SetEnableAnimations(border, true); }; |
|||
return; |
|||
} |
|||
|
|||
if (ElementComposition.GetElementVisual(page) == null) |
|||
return; |
|||
|
|||
page.EnsureImplicitAnimations(); |
|||
ElementComposition.GetElementVisual((Visual)border.GetVisualParent()).ImplicitAnimations = |
|||
page._implicitAnimations; |
|||
} |
|||
} |
|||
|
|||
public class CompositionPageColorItem |
|||
{ |
|||
public Color Color { get; private set; } |
|||
|
|||
public SolidColorBrush ColorBrush |
|||
{ |
|||
get { return new SolidColorBrush(Color); } |
|||
} |
|||
|
|||
public String ColorHexValue |
|||
{ |
|||
get { return Color.ToString().Substring(3).ToUpperInvariant(); } |
|||
} |
|||
|
|||
public CompositionPageColorItem(Color color) |
|||
{ |
|||
Color = color; |
|||
} |
|||
} |
|||
@ -0,0 +1,306 @@ |
|||
// ReSharper disable InconsistentNaming
|
|||
// Ported from Chromium project https://github.com/chromium/chromium/blob/374d31b7704475fa59f7b2cb836b3b68afdc3d79/ui/gfx/geometry/cubic_bezier.cc
|
|||
|
|||
using System; |
|||
using Avalonia.Utilities; |
|||
|
|||
// ReSharper disable CompareOfFloatsByEqualityOperator
|
|||
// ReSharper disable CommentTypo
|
|||
// ReSharper disable MemberCanBePrivate.Global
|
|||
// ReSharper disable TooWideLocalVariableScope
|
|||
// ReSharper disable UnusedMember.Global
|
|||
#pragma warning disable 649
|
|||
|
|||
namespace Avalonia.Animation.Easings |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a cubic bezier curve and can compute Y coordinate for a given X
|
|||
/// </summary>
|
|||
internal unsafe struct CubicBezier |
|||
{ |
|||
const int CUBIC_BEZIER_SPLINE_SAMPLES = 11; |
|||
double ax_; |
|||
double bx_; |
|||
double cx_; |
|||
|
|||
double ay_; |
|||
double by_; |
|||
double cy_; |
|||
|
|||
double start_gradient_; |
|||
double end_gradient_; |
|||
|
|||
double range_min_; |
|||
double range_max_; |
|||
private bool monotonically_increasing_; |
|||
|
|||
fixed double spline_samples_[CUBIC_BEZIER_SPLINE_SAMPLES]; |
|||
|
|||
public CubicBezier(double p1x, double p1y, double p2x, double p2y) : this() |
|||
{ |
|||
InitCoefficients(p1x, p1y, p2x, p2y); |
|||
InitGradients(p1x, p1y, p2x, p2y); |
|||
InitRange(p1y, p2y); |
|||
InitSpline(); |
|||
} |
|||
|
|||
public readonly double SampleCurveX(double t) |
|||
{ |
|||
// `ax t^3 + bx t^2 + cx t' expanded using Horner's rule.
|
|||
return ((ax_ * t + bx_) * t + cx_) * t; |
|||
} |
|||
|
|||
readonly double SampleCurveY(double t) |
|||
{ |
|||
return ((ay_ * t + by_) * t + cy_) * t; |
|||
} |
|||
|
|||
readonly double SampleCurveDerivativeX(double t) |
|||
{ |
|||
return (3.0 * ax_ * t + 2.0 * bx_) * t + cx_; |
|||
} |
|||
|
|||
readonly double SampleCurveDerivativeY(double t) |
|||
{ |
|||
return (3.0 * ay_ * t + 2.0 * by_) * t + cy_; |
|||
} |
|||
|
|||
public readonly double SolveWithEpsilon(double x, double epsilon) |
|||
{ |
|||
if (x < 0.0) |
|||
return 0.0 + start_gradient_ * x; |
|||
if (x > 1.0) |
|||
return 1.0 + end_gradient_ * (x - 1.0); |
|||
return SampleCurveY(SolveCurveX(x, epsilon)); |
|||
} |
|||
|
|||
void InitCoefficients(double p1x, |
|||
double p1y, |
|||
double p2x, |
|||
double p2y) |
|||
{ |
|||
// Calculate the polynomial coefficients, implicit first and last control
|
|||
// points are (0,0) and (1,1).
|
|||
cx_ = 3.0 * p1x; |
|||
bx_ = 3.0 * (p2x - p1x) - cx_; |
|||
ax_ = 1.0 - cx_ - bx_; |
|||
|
|||
cy_ = 3.0 * p1y; |
|||
by_ = 3.0 * (p2y - p1y) - cy_; |
|||
ay_ = 1.0 - cy_ - by_; |
|||
|
|||
#if DEBUG
|
|||
// Bezier curves with x-coordinates outside the range [0,1] for internal
|
|||
// control points may have multiple values for t for a given value of x.
|
|||
// In this case, calls to SolveCurveX may produce ambiguous results.
|
|||
monotonically_increasing_ = p1x >= 0 && p1x <= 1 && p2x >= 0 && p2x <= 1; |
|||
#endif
|
|||
} |
|||
|
|||
void InitGradients(double p1x, |
|||
double p1y, |
|||
double p2x, |
|||
double p2y) |
|||
{ |
|||
// End-point gradients are used to calculate timing function results
|
|||
// outside the range [0, 1].
|
|||
//
|
|||
// There are four possibilities for the gradient at each end:
|
|||
// (1) the closest control point is not horizontally coincident with regard to
|
|||
// (0, 0) or (1, 1). In this case the line between the end point and
|
|||
// the control point is tangent to the bezier at the end point.
|
|||
// (2) the closest control point is coincident with the end point. In
|
|||
// this case the line between the end point and the far control
|
|||
// point is tangent to the bezier at the end point.
|
|||
// (3) both internal control points are coincident with an endpoint. There
|
|||
// are two special case that fall into this category:
|
|||
// CubicBezier(0, 0, 0, 0) and CubicBezier(1, 1, 1, 1). Both are
|
|||
// equivalent to linear.
|
|||
// (4) the closest control point is horizontally coincident with the end
|
|||
// point, but vertically distinct. In this case the gradient at the
|
|||
// end point is Infinite. However, this causes issues when
|
|||
// interpolating. As a result, we break down to a simple case of
|
|||
// 0 gradient under these conditions.
|
|||
|
|||
if (p1x > 0) |
|||
start_gradient_ = p1y / p1x; |
|||
else if (p1y == 0 && p2x > 0) |
|||
start_gradient_ = p2y / p2x; |
|||
else if (p1y == 0 && p2y == 0) |
|||
start_gradient_ = 1; |
|||
else |
|||
start_gradient_ = 0; |
|||
|
|||
if (p2x < 1) |
|||
end_gradient_ = (p2y - 1) / (p2x - 1); |
|||
else if (p2y == 1 && p1x < 1) |
|||
end_gradient_ = (p1y - 1) / (p1x - 1); |
|||
else if (p2y == 1 && p1y == 1) |
|||
end_gradient_ = 1; |
|||
else |
|||
end_gradient_ = 0; |
|||
} |
|||
|
|||
const double kBezierEpsilon = 1e-7; |
|||
|
|||
void InitRange(double p1y, double p2y) |
|||
{ |
|||
range_min_ = 0; |
|||
range_max_ = 1; |
|||
if (0 <= p1y && p1y < 1 && 0 <= p2y && p2y <= 1) |
|||
return; |
|||
|
|||
double epsilon = kBezierEpsilon; |
|||
|
|||
// Represent the function's derivative in the form at^2 + bt + c
|
|||
// as in sampleCurveDerivativeY.
|
|||
// (Technically this is (dy/dt)*(1/3), which is suitable for finding zeros
|
|||
// but does not actually give the slope of the curve.)
|
|||
double a = 3.0 * ay_; |
|||
double b = 2.0 * by_; |
|||
double c = cy_; |
|||
|
|||
// Check if the derivative is constant.
|
|||
if (Math.Abs(a) < epsilon && Math.Abs(b) < epsilon) |
|||
return; |
|||
|
|||
// Zeros of the function's derivative.
|
|||
double t1; |
|||
double t2 = 0; |
|||
|
|||
if (Math.Abs(a) < epsilon) |
|||
{ |
|||
// The function's derivative is linear.
|
|||
t1 = -c / b; |
|||
} |
|||
else |
|||
{ |
|||
// The function's derivative is a quadratic. We find the zeros of this
|
|||
// quadratic using the quadratic formula.
|
|||
double discriminant = b * b - 4 * a * c; |
|||
if (discriminant < 0) |
|||
return; |
|||
double discriminant_sqrt = Math.Sqrt(discriminant); |
|||
t1 = (-b + discriminant_sqrt) / (2 * a); |
|||
t2 = (-b - discriminant_sqrt) / (2 * a); |
|||
} |
|||
|
|||
double sol1 = 0; |
|||
double sol2 = 0; |
|||
|
|||
// If the solution is in the range [0,1] then we include it, otherwise we
|
|||
// ignore it.
|
|||
|
|||
// An interesting fact about these beziers is that they are only
|
|||
// actually evaluated in [0,1]. After that we take the tangent at that point
|
|||
// and linearly project it out.
|
|||
if (0 < t1 && t1 < 1) |
|||
sol1 = SampleCurveY(t1); |
|||
|
|||
if (0 < t2 && t2 < 1) |
|||
sol2 = SampleCurveY(t2); |
|||
|
|||
range_min_ = Math.Min(Math.Min(range_min_, sol1), sol2); |
|||
range_max_ = Math.Max(Math.Max(range_max_, sol1), sol2); |
|||
} |
|||
|
|||
void InitSpline() |
|||
{ |
|||
double delta_t = 1.0 / (CUBIC_BEZIER_SPLINE_SAMPLES - 1); |
|||
for (int i = 0; i < CUBIC_BEZIER_SPLINE_SAMPLES; i++) |
|||
{ |
|||
spline_samples_[i] = SampleCurveX(i * delta_t); |
|||
} |
|||
} |
|||
|
|||
const int kMaxNewtonIterations = 4; |
|||
|
|||
|
|||
public readonly double SolveCurveX(double x, double epsilon) |
|||
{ |
|||
if (x < 0 || x > 1) |
|||
throw new ArgumentException(); |
|||
|
|||
double t0 = 0; |
|||
double t1 = 0; |
|||
double t2 = x; |
|||
double x2 = 0; |
|||
double d2; |
|||
int i; |
|||
|
|||
#if DEBUG
|
|||
if (!monotonically_increasing_) |
|||
throw new InvalidOperationException(); |
|||
#endif
|
|||
|
|||
// Linear interpolation of spline curve for initial guess.
|
|||
double delta_t = 1.0 / (CUBIC_BEZIER_SPLINE_SAMPLES - 1); |
|||
for (i = 1; i < CUBIC_BEZIER_SPLINE_SAMPLES; i++) |
|||
{ |
|||
if (x <= spline_samples_[i]) |
|||
{ |
|||
t1 = delta_t * i; |
|||
t0 = t1 - delta_t; |
|||
t2 = t0 + (t1 - t0) * (x - spline_samples_[i - 1]) / |
|||
(spline_samples_[i] - spline_samples_[i - 1]); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
// Perform a few iterations of Newton's method -- normally very fast.
|
|||
// See https://en.wikipedia.org/wiki/Newton%27s_method.
|
|||
double newton_epsilon = Math.Min(kBezierEpsilon, epsilon); |
|||
for (i = 0; i < kMaxNewtonIterations; i++) |
|||
{ |
|||
x2 = SampleCurveX(t2) - x; |
|||
if (Math.Abs(x2) < newton_epsilon) |
|||
return t2; |
|||
d2 = SampleCurveDerivativeX(t2); |
|||
if (Math.Abs(d2) < kBezierEpsilon) |
|||
break; |
|||
t2 = t2 - x2 / d2; |
|||
} |
|||
|
|||
if (Math.Abs(x2) < epsilon) |
|||
return t2; |
|||
|
|||
// Fall back to the bisection method for reliability.
|
|||
while (t0 < t1) |
|||
{ |
|||
x2 = SampleCurveX(t2); |
|||
if (Math.Abs(x2 - x) < epsilon) |
|||
return t2; |
|||
if (x > x2) |
|||
t0 = t2; |
|||
else |
|||
t1 = t2; |
|||
t2 = (t1 + t0) * .5; |
|||
} |
|||
|
|||
// Failure.
|
|||
return t2; |
|||
} |
|||
|
|||
public readonly double Solve(double x) |
|||
{ |
|||
return SolveWithEpsilon(x, kBezierEpsilon); |
|||
} |
|||
|
|||
public readonly double SlopeWithEpsilon(double x, double epsilon) |
|||
{ |
|||
x = MathUtilities.Clamp(x, 0.0, 1.0); |
|||
double t = SolveCurveX(x, epsilon); |
|||
double dx = SampleCurveDerivativeX(t); |
|||
double dy = SampleCurveDerivativeY(t); |
|||
return dy / dx; |
|||
} |
|||
|
|||
public readonly double Slope(double x) |
|||
{ |
|||
return SlopeWithEpsilon(x, kBezierEpsilon); |
|||
} |
|||
|
|||
public readonly double RangeMin => range_min_; |
|||
public readonly double RangeMax => range_max_; |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Animation.Easings; |
|||
|
|||
public class CubicBezierEasing : IEasing |
|||
{ |
|||
private CubicBezier _bezier; |
|||
//cubic-bezier(0.25, 0.1, 0.25, 1.0)
|
|||
internal CubicBezierEasing(Point controlPoint1, Point controlPoint2) |
|||
{ |
|||
ControlPoint1 = controlPoint1; |
|||
ControlPoint2 = controlPoint2; |
|||
if (controlPoint1.X < 0 || controlPoint1.X > 1 || controlPoint2.X < 0 || controlPoint2.X > 1) |
|||
throw new ArgumentException(); |
|||
_bezier = new CubicBezier(controlPoint1.X, controlPoint1.Y, controlPoint2.X, controlPoint2.Y); |
|||
} |
|||
|
|||
public Point ControlPoint2 { get; set; } |
|||
public Point ControlPoint1 { get; set; } |
|||
|
|||
internal static IEasing Ease { get; } = new CubicBezierEasing(new Point(0.25, 0.1), new Point(0.25, 1)); |
|||
|
|||
double IEasing.Ease(double progress) |
|||
{ |
|||
return _bezier.Solve(progress); |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Internal interface for listening to changes in <see cref="Classes"/> in a more
|
|||
/// performant manner than subscribing to CollectionChanged.
|
|||
/// </summary>
|
|||
internal interface IClassesChangedListener |
|||
{ |
|||
/// <summary>
|
|||
/// Notifies the listener that the <see cref="Classes"/> collection has changed.
|
|||
/// </summary>
|
|||
void Changed(); |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
using System; |
|||
using Avalonia.Metadata; |
|||
|
|||
namespace Avalonia.Platform; |
|||
|
|||
[Unstable] |
|||
public interface IPlatformGpu |
|||
{ |
|||
IPlatformGpuContext PrimaryContext { get; } |
|||
} |
|||
|
|||
[Unstable] |
|||
public interface IPlatformGpuContext : IDisposable |
|||
{ |
|||
IDisposable EnsureCurrent(); |
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Rendering.Composition.Expressions; |
|||
using Avalonia.Rendering.Composition.Server; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Animations; |
|||
|
|||
|
|||
/// <summary>
|
|||
/// The base class for both key-frame and expression animation instances
|
|||
/// Is responsible for activation tracking and for subscribing to properties used in dependencies
|
|||
/// </summary>
|
|||
internal abstract class AnimationInstanceBase : IAnimationInstance |
|||
{ |
|||
private List<(ServerObject obj, CompositionProperty member)>? _trackedObjects; |
|||
protected PropertySetSnapshot Parameters { get; } |
|||
public ServerObject TargetObject { get; } |
|||
protected CompositionProperty Property { get; private set; } = null!; |
|||
private bool _invalidated; |
|||
|
|||
public AnimationInstanceBase(ServerObject target, PropertySetSnapshot parameters) |
|||
{ |
|||
Parameters = parameters; |
|||
TargetObject = target; |
|||
} |
|||
|
|||
protected void Initialize(CompositionProperty property, HashSet<(string name, string member)> trackedObjects) |
|||
{ |
|||
if (trackedObjects.Count > 0) |
|||
{ |
|||
_trackedObjects = new (); |
|||
foreach (var t in trackedObjects) |
|||
{ |
|||
var obj = Parameters.GetObjectParameter(t.name); |
|||
if (obj is ServerObject tracked) |
|||
{ |
|||
var off = tracked.GetCompositionProperty(t.member); |
|||
if (off == null) |
|||
#if DEBUG
|
|||
throw new InvalidCastException("Attempting to subscribe to unknown field"); |
|||
#else
|
|||
continue; |
|||
#endif
|
|||
_trackedObjects.Add((tracked, off)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
Property = property; |
|||
} |
|||
|
|||
public abstract void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property); |
|||
protected abstract ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue); |
|||
|
|||
public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) |
|||
{ |
|||
_invalidated = false; |
|||
return EvaluateCore(now, currentValue); |
|||
} |
|||
|
|||
public virtual void Activate() |
|||
{ |
|||
if (_trackedObjects != null) |
|||
foreach (var tracked in _trackedObjects) |
|||
tracked.obj.SubscribeToInvalidation(tracked.member, this); |
|||
} |
|||
|
|||
public virtual void Deactivate() |
|||
{ |
|||
if (_trackedObjects != null) |
|||
foreach (var tracked in _trackedObjects) |
|||
tracked.obj.UnsubscribeFromInvalidation(tracked.member, this); |
|||
} |
|||
|
|||
public void Invalidate() |
|||
{ |
|||
if (_invalidated) |
|||
return; |
|||
_invalidated = true; |
|||
TargetObject.NotifyAnimatedValueChanged(Property); |
|||
} |
|||
} |
|||
@ -0,0 +1,75 @@ |
|||
// ReSharper disable InconsistentNaming
|
|||
// ReSharper disable CheckNamespace
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Numerics; |
|||
using Avalonia.Rendering.Composition.Expressions; |
|||
using Avalonia.Rendering.Composition.Server; |
|||
using Avalonia.Rendering.Composition.Transport; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Animations |
|||
{ |
|||
/// <summary>
|
|||
/// This is the base class for ExpressionAnimation and KeyFrameAnimation.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Use the <see cref="CompositionObject.StartAnimation"/> method to start the animation.
|
|||
/// Value parameters (as opposed to reference parameters which are set using <see cref="SetReferenceParameter"/>)
|
|||
/// are copied and "embedded" into an expression at the time CompositionObject.StartAnimation is called.
|
|||
/// Changing the value of the variable after <see cref="CompositionObject.StartAnimation"/> is called will not affect
|
|||
/// the value of the ExpressionAnimation.
|
|||
/// See the remarks section of ExpressionAnimation for additional information.
|
|||
/// </remarks>
|
|||
public abstract class CompositionAnimation : CompositionObject, ICompositionAnimationBase |
|||
{ |
|||
private readonly CompositionPropertySet _propertySet; |
|||
internal CompositionAnimation(Compositor compositor) : base(compositor, null!) |
|||
{ |
|||
_propertySet = new CompositionPropertySet(compositor); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Clears all of the parameters of the animation.
|
|||
/// </summary>
|
|||
public void ClearAllParameters() => _propertySet.ClearAll(); |
|||
|
|||
/// <summary>
|
|||
/// Clears a parameter from the animation.
|
|||
/// </summary>
|
|||
public void ClearParameter(string key) => _propertySet.Clear(key); |
|||
|
|||
void SetVariant(string key, ExpressionVariant value) => _propertySet.Set(key, value); |
|||
|
|||
public void SetColorParameter(string key, Media.Color value) => SetVariant(key, value); |
|||
|
|||
public void SetMatrix3x2Parameter(string key, Matrix3x2 value) => SetVariant(key, value); |
|||
|
|||
public void SetMatrix4x4Parameter(string key, Matrix4x4 value) => SetVariant(key, value); |
|||
|
|||
public void SetQuaternionParameter(string key, Quaternion value) => SetVariant(key, value); |
|||
|
|||
public void SetReferenceParameter(string key, CompositionObject compositionObject) => |
|||
_propertySet.Set(key, compositionObject); |
|||
|
|||
public void SetScalarParameter(string key, float value) => SetVariant(key, value); |
|||
|
|||
public void SetVector2Parameter(string key, Vector2 value) => SetVariant(key, value); |
|||
|
|||
public void SetVector3Parameter(string key, Vector3 value) => SetVariant(key, value); |
|||
|
|||
public void SetVector4Parameter(string key, Vector4 value) => SetVariant(key, value); |
|||
|
|||
public string? Target { get; set; } |
|||
|
|||
internal abstract IAnimationInstance CreateInstance(ServerObject targetObject, |
|||
ExpressionVariant? finalValue); |
|||
|
|||
internal PropertySetSnapshot CreateSnapshot() => _propertySet.Snapshot(); |
|||
|
|||
void ICompositionAnimationBase.InternalOnly() |
|||
{ |
|||
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Rendering.Composition.Transport; |
|||
|
|||
|
|||
namespace Avalonia.Rendering.Composition.Animations |
|||
{ |
|||
public class CompositionAnimationGroup : CompositionObject, ICompositionAnimationBase |
|||
{ |
|||
internal List<CompositionAnimation> Animations { get; } = new List<CompositionAnimation>(); |
|||
void ICompositionAnimationBase.InternalOnly() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void Add(CompositionAnimation value) => Animations.Add(value); |
|||
public void Remove(CompositionAnimation value) => Animations.Remove(value); |
|||
public void RemoveAll() => Animations.Clear(); |
|||
|
|||
public CompositionAnimationGroup(Compositor compositor) : base(compositor, null!) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
// ReSharper disable CheckNamespace
|
|||
using System; |
|||
using Avalonia.Rendering.Composition.Expressions; |
|||
using Avalonia.Rendering.Composition.Server; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Animations |
|||
{ |
|||
/// <summary>
|
|||
/// A Composition Animation that uses a mathematical equation to calculate the value for an animating property every frame.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// The core of ExpressionAnimations allows a developer to define a mathematical equation that can be used to calculate the value
|
|||
/// of a targeted animating property each frame.
|
|||
/// This contrasts <see cref="KeyFrameAnimation"/>s, which use an interpolator to define how the animating
|
|||
/// property changes over time. The mathematical equation can be defined using references to properties
|
|||
/// of Composition objects, mathematical functions and operators and Input.
|
|||
/// Use the <see cref="CompositionObject.StartAnimation"/> method to start the animation.
|
|||
/// </remarks>
|
|||
public class ExpressionAnimation : CompositionAnimation |
|||
{ |
|||
private string? _expression; |
|||
private Expression? _parsedExpression; |
|||
|
|||
internal ExpressionAnimation(Compositor compositor) : base(compositor) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The mathematical equation specifying how the animated value is calculated each frame.
|
|||
/// The Expression is the core of an <see cref="ExpressionAnimation"/> and represents the equation
|
|||
/// the system will use to calculate the value of the animation property each frame.
|
|||
/// The equation is set on this property in the form of a string.
|
|||
/// Although expressions can be defined by simple mathematical equations such as "2+2",
|
|||
/// the real power lies in creating mathematical relationships where the input values can change frame over frame.
|
|||
/// </summary>
|
|||
public string? Expression |
|||
{ |
|||
get => _expression; |
|||
set |
|||
{ |
|||
_expression = value; |
|||
_parsedExpression = null; |
|||
} |
|||
} |
|||
|
|||
private Expression ParsedExpression => _parsedExpression ??= ExpressionParser.Parse(_expression.AsSpan()); |
|||
|
|||
internal override IAnimationInstance CreateInstance( |
|||
ServerObject targetObject, ExpressionVariant? finalValue) |
|||
=> new ExpressionAnimationInstance(ParsedExpression, |
|||
targetObject, finalValue, CreateSnapshot()); |
|||
} |
|||
} |
|||
@ -0,0 +1,49 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Rendering.Composition.Expressions; |
|||
using Avalonia.Rendering.Composition.Server; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Animations |
|||
{ |
|||
|
|||
/// <summary>
|
|||
/// Server-side counterpart of <see cref="ExpressionAnimation"/> with values baked-in.
|
|||
/// </summary>
|
|||
internal class ExpressionAnimationInstance : AnimationInstanceBase, IAnimationInstance |
|||
{ |
|||
private readonly Expression _expression; |
|||
private ExpressionVariant _startingValue; |
|||
private readonly ExpressionVariant? _finalValue; |
|||
|
|||
protected override ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue) |
|||
{ |
|||
var ctx = new ExpressionEvaluationContext |
|||
{ |
|||
Parameters = Parameters, |
|||
Target = TargetObject, |
|||
ForeignFunctionInterface = BuiltInExpressionFfi.Instance, |
|||
StartingValue = _startingValue, |
|||
FinalValue = _finalValue ?? _startingValue, |
|||
CurrentValue = currentValue |
|||
}; |
|||
return _expression.Evaluate(ref ctx); |
|||
} |
|||
|
|||
public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property) |
|||
{ |
|||
_startingValue = startingValue; |
|||
var hs = new HashSet<(string, string)>(); |
|||
_expression.CollectReferences(hs); |
|||
base.Initialize(property, hs); |
|||
} |
|||
|
|||
public ExpressionAnimationInstance(Expression expression, |
|||
ServerObject target, |
|||
ExpressionVariant? finalValue, |
|||
PropertySetSnapshot parameters) : base(target, parameters) |
|||
{ |
|||
_expression = expression; |
|||
_finalValue = finalValue; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
using System; |
|||
using Avalonia.Rendering.Composition.Expressions; |
|||
using Avalonia.Rendering.Composition.Server; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Animations |
|||
{ |
|||
internal interface IAnimationInstance |
|||
{ |
|||
ServerObject TargetObject { get; } |
|||
ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue); |
|||
void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property); |
|||
void Activate(); |
|||
void Deactivate(); |
|||
void Invalidate(); |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
// ReSharper disable CheckNamespace
|
|||
|
|||
using Avalonia.Rendering.Composition.Server; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Animations |
|||
{ |
|||
/// <summary>
|
|||
/// Base class for composition animations.
|
|||
/// </summary>
|
|||
public interface ICompositionAnimationBase |
|||
{ |
|||
internal void InternalOnly(); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using Avalonia.Rendering.Composition.Transport; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Animations |
|||
{ |
|||
/// <summary>
|
|||
/// A collection of animations triggered when a condition is met.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Implicit animations let you drive animations by specifying trigger conditions rather than requiring the manual definition of animation behavior.
|
|||
/// They help decouple animation start logic from core app logic. You define animations and the events that should trigger these animations.
|
|||
/// Currently the only available trigger is animated property change.
|
|||
///
|
|||
/// When expression is used in ImplicitAnimationCollection a special keyword `this.FinalValue` will represent
|
|||
/// the final value of the animated property that was changed
|
|||
/// </remarks>
|
|||
public class ImplicitAnimationCollection : CompositionObject, IDictionary<string, ICompositionAnimationBase> |
|||
{ |
|||
private Dictionary<string, ICompositionAnimationBase> _inner = new Dictionary<string, ICompositionAnimationBase>(); |
|||
private IDictionary<string, ICompositionAnimationBase> _innerface; |
|||
internal ImplicitAnimationCollection(Compositor compositor) : base(compositor, null!) |
|||
{ |
|||
_innerface = _inner; |
|||
} |
|||
|
|||
public IEnumerator<KeyValuePair<string, ICompositionAnimationBase>> GetEnumerator() => _inner.GetEnumerator(); |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable) _inner).GetEnumerator(); |
|||
|
|||
void ICollection<KeyValuePair<string, ICompositionAnimationBase>>.Add(KeyValuePair<string, ICompositionAnimationBase> item) => _innerface.Add(item); |
|||
|
|||
public void Clear() => _inner.Clear(); |
|||
|
|||
bool ICollection<KeyValuePair<string, ICompositionAnimationBase>>.Contains(KeyValuePair<string, ICompositionAnimationBase> item) => _innerface.Contains(item); |
|||
|
|||
void ICollection<KeyValuePair<string, ICompositionAnimationBase>>.CopyTo(KeyValuePair<string, ICompositionAnimationBase>[] array, int arrayIndex) => _innerface.CopyTo(array, arrayIndex); |
|||
|
|||
bool ICollection<KeyValuePair<string, ICompositionAnimationBase>>.Remove(KeyValuePair<string, ICompositionAnimationBase> item) => _innerface.Remove(item); |
|||
|
|||
public int Count => _inner.Count; |
|||
|
|||
bool ICollection<KeyValuePair<string, ICompositionAnimationBase>>.IsReadOnly => _innerface.IsReadOnly; |
|||
|
|||
public void Add(string key, ICompositionAnimationBase value) => _inner.Add(key, value); |
|||
|
|||
public bool ContainsKey(string key) => _inner.ContainsKey(key); |
|||
|
|||
public bool Remove(string key) => _inner.Remove(key); |
|||
|
|||
public bool TryGetValue(string key, [MaybeNullWhen(false)] out ICompositionAnimationBase value) => |
|||
_inner.TryGetValue(key, out value); |
|||
|
|||
public ICompositionAnimationBase this[string key] |
|||
{ |
|||
get => _inner[key]; |
|||
set => _inner[key] = value; |
|||
} |
|||
|
|||
ICollection<string> IDictionary<string, ICompositionAnimationBase>.Keys => _innerface.Keys; |
|||
|
|||
ICollection<ICompositionAnimationBase> IDictionary<string, ICompositionAnimationBase>.Values => |
|||
_innerface.Values; |
|||
|
|||
// UWP compat
|
|||
public uint Size => (uint) Count; |
|||
|
|||
public IReadOnlyDictionary<string, ICompositionAnimationBase> GetView() => |
|||
new Dictionary<string, ICompositionAnimationBase>(this); |
|||
|
|||
public bool HasKey(string key) => ContainsKey(key); |
|||
public void Insert(string key, ICompositionAnimationBase animation) => Add(key, animation); |
|||
|
|||
public ICompositionAnimationBase? Lookup(string key) |
|||
{ |
|||
_inner.TryGetValue(key, out var rv); |
|||
return rv; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,76 @@ |
|||
using System; |
|||
using System.Numerics; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Animations |
|||
{ |
|||
/// <summary>
|
|||
/// An interface to define interpolation logic for a particular type
|
|||
/// </summary>
|
|||
internal interface IInterpolator<T> |
|||
{ |
|||
T Interpolate(T from, T to, float progress); |
|||
} |
|||
|
|||
class ScalarInterpolator : IInterpolator<float> |
|||
{ |
|||
public float Interpolate(float @from, float to, float progress) => @from + (to - @from) * progress; |
|||
|
|||
public static ScalarInterpolator Instance { get; } = new ScalarInterpolator(); |
|||
} |
|||
|
|||
class Vector2Interpolator : IInterpolator<Vector2> |
|||
{ |
|||
public Vector2 Interpolate(Vector2 @from, Vector2 to, float progress) |
|||
=> Vector2.Lerp(@from, to, progress); |
|||
|
|||
public static Vector2Interpolator Instance { get; } = new Vector2Interpolator(); |
|||
} |
|||
|
|||
class Vector3Interpolator : IInterpolator<Vector3> |
|||
{ |
|||
public Vector3 Interpolate(Vector3 @from, Vector3 to, float progress) |
|||
=> Vector3.Lerp(@from, to, progress); |
|||
|
|||
public static Vector3Interpolator Instance { get; } = new Vector3Interpolator(); |
|||
} |
|||
|
|||
class Vector4Interpolator : IInterpolator<Vector4> |
|||
{ |
|||
public Vector4 Interpolate(Vector4 @from, Vector4 to, float progress) |
|||
=> Vector4.Lerp(@from, to, progress); |
|||
|
|||
public static Vector4Interpolator Instance { get; } = new Vector4Interpolator(); |
|||
} |
|||
|
|||
class QuaternionInterpolator : IInterpolator<Quaternion> |
|||
{ |
|||
public Quaternion Interpolate(Quaternion @from, Quaternion to, float progress) |
|||
=> Quaternion.Lerp(@from, to, progress); |
|||
|
|||
public static QuaternionInterpolator Instance { get; } = new QuaternionInterpolator(); |
|||
} |
|||
|
|||
class ColorInterpolator : IInterpolator<Avalonia.Media.Color> |
|||
{ |
|||
static byte Lerp(float a, float b, float p) => (byte) Math.Max(0, Math.Min(255, (p * (b - a) + a))); |
|||
|
|||
public static Avalonia.Media.Color |
|||
LerpRGB(Avalonia.Media.Color to, Avalonia.Media.Color from, float progress) => |
|||
new Avalonia.Media.Color(Lerp(to.A, @from.A, progress), |
|||
Lerp(to.R, @from.R, progress), |
|||
Lerp(to.G, @from.G, progress), |
|||
Lerp(to.B, @from.B, progress)); |
|||
|
|||
public Avalonia.Media.Color Interpolate(Avalonia.Media.Color @from, Avalonia.Media.Color to, float progress) |
|||
=> LerpRGB(@from, to, progress); |
|||
|
|||
public static ColorInterpolator Instance { get; } = new ColorInterpolator(); |
|||
} |
|||
|
|||
class BooleanInterpolator : IInterpolator<bool> |
|||
{ |
|||
public bool Interpolate(bool @from, bool to, float progress) => progress >= 1 ? to : @from; |
|||
|
|||
public static BooleanInterpolator Instance { get; } = new BooleanInterpolator(); |
|||
} |
|||
} |
|||
@ -0,0 +1,134 @@ |
|||
using System; |
|||
using Avalonia.Animation; |
|||
using Avalonia.Animation.Easings; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Animations |
|||
{ |
|||
|
|||
/// <summary>
|
|||
/// A time-based animation with one or more key frames.
|
|||
/// These frames are markers, allowing developers to specify values at specific times for the animating property.
|
|||
/// KeyFrame animations can be further customized by specifying how the animation interpolates between keyframes.
|
|||
/// </summary>
|
|||
public abstract class KeyFrameAnimation : CompositionAnimation |
|||
{ |
|||
private TimeSpan _duration = TimeSpan.FromMilliseconds(1); |
|||
|
|||
internal KeyFrameAnimation(Compositor compositor) : base(compositor) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The delay behavior of the key frame animation.
|
|||
/// </summary>
|
|||
public AnimationDelayBehavior DelayBehavior { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Delay before the animation starts after <see cref="CompositionObject.StartAnimation"/> is called.
|
|||
/// </summary>
|
|||
public System.TimeSpan DelayTime { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The direction the animation is playing.
|
|||
/// The Direction property allows you to drive your animation from start to end or end to start or alternate
|
|||
/// between start and end or end to start if animation has an <see cref="IterationCount"/> greater than one.
|
|||
/// This gives an easy way for customizing animation definitions.
|
|||
/// </summary>
|
|||
public PlaybackDirection Direction { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The duration of the animation.
|
|||
/// Minimum allowed value is 1ms and maximum allowed value is 24 days.
|
|||
/// </summary>
|
|||
public TimeSpan Duration |
|||
{ |
|||
get => _duration; |
|||
set |
|||
{ |
|||
if (_duration < TimeSpan.FromMilliseconds(1) || _duration > TimeSpan.FromDays(1)) |
|||
throw new ArgumentException("Minimum allowed value is 1ms and maximum allowed value is 24 days."); |
|||
_duration = value; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The iteration behavior for the key frame animation.
|
|||
/// </summary>
|
|||
public AnimationIterationBehavior IterationBehavior { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The number of times to repeat the key frame animation.
|
|||
/// </summary>
|
|||
public int IterationCount { get; set; } = 1; |
|||
|
|||
/// <summary>
|
|||
/// Specifies how to set the property value when animation is stopped
|
|||
/// </summary>
|
|||
public AnimationStopBehavior StopBehavior { get; set; } |
|||
|
|||
private protected abstract IKeyFrames KeyFrames { get; } |
|||
|
|||
/// <summary>
|
|||
/// Inserts an expression keyframe.
|
|||
/// </summary>
|
|||
/// <param name="normalizedProgressKey">
|
|||
/// The time the key frame should occur at, expressed as a percentage of the animation Duration. Allowed value is from 0.0 to 1.0.
|
|||
/// </param>
|
|||
/// <param name="value">The expression used to calculate the value of the key frame.</param>
|
|||
/// <param name="easingFunction">The easing function to use when interpolating between frames.</param>
|
|||
public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, |
|||
Easing? easingFunction = null) => |
|||
KeyFrames.InsertExpressionKeyFrame(normalizedProgressKey, value, easingFunction ?? Compositor.DefaultEasing); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Specifies the animation delay behavior.
|
|||
/// </summary>
|
|||
public enum AnimationDelayBehavior |
|||
{ |
|||
/// <summary>
|
|||
/// If a DelayTime is specified, it delays starting the animation according to delay time and after delay
|
|||
/// has expired it applies animation to the object property.
|
|||
/// </summary>
|
|||
SetInitialValueAfterDelay, |
|||
/// <summary>
|
|||
/// Applies the initial value of the animation (i.e. the value at Keyframe 0) to the object before the delay time
|
|||
/// is elapsed (when there is a DelayTime specified), it then delays starting the animation according to the DelayTime.
|
|||
/// </summary>
|
|||
SetInitialValueBeforeDelay |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Specifies if the animation should loop.
|
|||
/// </summary>
|
|||
public enum AnimationIterationBehavior |
|||
{ |
|||
/// <summary>
|
|||
/// The animation should loop the specified number of times.
|
|||
/// </summary>
|
|||
Count, |
|||
/// <summary>
|
|||
/// The animation should loop forever.
|
|||
/// </summary>
|
|||
Forever |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Specifies the behavior of an animation when it stops.
|
|||
/// </summary>
|
|||
public enum AnimationStopBehavior |
|||
{ |
|||
/// <summary>
|
|||
/// Leave the animation at its current value.
|
|||
/// </summary>
|
|||
LeaveCurrentValue, |
|||
/// <summary>
|
|||
/// Reset the animation to its initial value.
|
|||
/// </summary>
|
|||
SetToInitialValue, |
|||
/// <summary>
|
|||
/// Set the animation to its final value.
|
|||
/// </summary>
|
|||
SetToFinalValue |
|||
} |
|||
} |
|||
@ -0,0 +1,178 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Animation; |
|||
using Avalonia.Rendering.Composition.Expressions; |
|||
using Avalonia.Rendering.Composition.Server; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Animations |
|||
{ |
|||
/// <summary>
|
|||
/// Server-side counterpart of KeyFrameAnimation with values baked-in
|
|||
/// </summary>
|
|||
class KeyFrameAnimationInstance<T> : AnimationInstanceBase, IAnimationInstance where T : struct |
|||
{ |
|||
private readonly IInterpolator<T> _interpolator; |
|||
private readonly ServerKeyFrame<T>[] _keyFrames; |
|||
private readonly ExpressionVariant? _finalValue; |
|||
private readonly AnimationDelayBehavior _delayBehavior; |
|||
private readonly TimeSpan _delayTime; |
|||
private readonly PlaybackDirection _direction; |
|||
private readonly TimeSpan _duration; |
|||
private readonly AnimationIterationBehavior _iterationBehavior; |
|||
private readonly int _iterationCount; |
|||
private readonly AnimationStopBehavior _stopBehavior; |
|||
private TimeSpan _startedAt; |
|||
private T _startingValue; |
|||
private readonly TimeSpan _totalDuration; |
|||
private bool _finished; |
|||
|
|||
public KeyFrameAnimationInstance( |
|||
IInterpolator<T> interpolator, ServerKeyFrame<T>[] keyFrames, |
|||
PropertySetSnapshot snapshot, ExpressionVariant? finalValue, |
|||
ServerObject target, |
|||
AnimationDelayBehavior delayBehavior, TimeSpan delayTime, |
|||
PlaybackDirection direction, TimeSpan duration, |
|||
AnimationIterationBehavior iterationBehavior, |
|||
int iterationCount, AnimationStopBehavior stopBehavior) : base(target, snapshot) |
|||
{ |
|||
_interpolator = interpolator; |
|||
_keyFrames = keyFrames; |
|||
_finalValue = finalValue; |
|||
_delayBehavior = delayBehavior; |
|||
_delayTime = delayTime; |
|||
_direction = direction; |
|||
_duration = duration; |
|||
_iterationBehavior = iterationBehavior; |
|||
_iterationCount = iterationCount; |
|||
_stopBehavior = stopBehavior; |
|||
if (_iterationBehavior == AnimationIterationBehavior.Count) |
|||
_totalDuration = delayTime.Add(TimeSpan.FromTicks(iterationCount * _duration.Ticks)); |
|||
if (_keyFrames.Length == 0) |
|||
throw new InvalidOperationException("Animation has no key frames"); |
|||
if(_duration.Ticks <= 0) |
|||
throw new InvalidOperationException("Invalid animation duration"); |
|||
} |
|||
|
|||
|
|||
protected override ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue) |
|||
{ |
|||
var starting = ExpressionVariant.Create(_startingValue); |
|||
var ctx = new ExpressionEvaluationContext |
|||
{ |
|||
Parameters = Parameters, |
|||
Target = TargetObject, |
|||
CurrentValue = currentValue, |
|||
FinalValue = _finalValue ?? starting, |
|||
StartingValue = starting, |
|||
ForeignFunctionInterface = BuiltInExpressionFfi.Instance |
|||
}; |
|||
var elapsed = now - _startedAt; |
|||
var res = EvaluateImpl(elapsed, currentValue, ref ctx); |
|||
|
|||
if (_iterationBehavior == AnimationIterationBehavior.Count |
|||
&& !_finished |
|||
&& elapsed > _totalDuration) |
|||
{ |
|||
// Active check?
|
|||
TargetObject.Compositor.RemoveFromClock(this); |
|||
_finished = true; |
|||
} |
|||
return res; |
|||
} |
|||
|
|||
private ExpressionVariant EvaluateImpl(TimeSpan elapsed, ExpressionVariant currentValue, ref ExpressionEvaluationContext ctx) |
|||
{ |
|||
if (elapsed < _delayTime) |
|||
{ |
|||
if (_delayBehavior == AnimationDelayBehavior.SetInitialValueBeforeDelay) |
|||
return ExpressionVariant.Create(GetKeyFrame(ref ctx, _keyFrames[0])); |
|||
return currentValue; |
|||
} |
|||
|
|||
elapsed -= _delayTime; |
|||
var iterationNumber = elapsed.Ticks / _duration.Ticks; |
|||
if (_iterationBehavior == AnimationIterationBehavior.Count |
|||
&& iterationNumber >= _iterationCount) |
|||
return ExpressionVariant.Create(GetKeyFrame(ref ctx, _keyFrames[_keyFrames.Length - 1])); |
|||
|
|||
|
|||
var evenIterationNumber = iterationNumber % 2 == 0; |
|||
elapsed = TimeSpan.FromTicks(elapsed.Ticks % _duration.Ticks); |
|||
|
|||
var reverse = |
|||
_direction == PlaybackDirection.Alternate |
|||
? !evenIterationNumber |
|||
: _direction == PlaybackDirection.AlternateReverse |
|||
? evenIterationNumber |
|||
: _direction == PlaybackDirection.Reverse; |
|||
|
|||
var iterationProgress = elapsed.TotalSeconds / _duration.TotalSeconds; |
|||
if (reverse) |
|||
iterationProgress = 1 - iterationProgress; |
|||
|
|||
var left = new ServerKeyFrame<T> |
|||
{ |
|||
Value = _startingValue |
|||
}; |
|||
var right = _keyFrames[_keyFrames.Length - 1]; |
|||
for (var c = 0; c < _keyFrames.Length; c++) |
|||
{ |
|||
var kf = _keyFrames[c]; |
|||
if (kf.Key < iterationProgress) |
|||
{ |
|||
// this is the last frame
|
|||
if (c == _keyFrames.Length - 1) |
|||
return ExpressionVariant.Create(GetKeyFrame(ref ctx, kf)); |
|||
|
|||
left = kf; |
|||
right = _keyFrames[c + 1]; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
var keyProgress = Math.Max(0, Math.Min(1, (iterationProgress - left.Key) / (right.Key - left.Key))); |
|||
|
|||
var easedKeyProgress = (float)right.EasingFunction.Ease(keyProgress); |
|||
if (float.IsNaN(easedKeyProgress) || float.IsInfinity(easedKeyProgress)) |
|||
return currentValue; |
|||
|
|||
return ExpressionVariant.Create(_interpolator.Interpolate( |
|||
GetKeyFrame(ref ctx, left), |
|||
GetKeyFrame(ref ctx, right), |
|||
easedKeyProgress |
|||
)); |
|||
} |
|||
|
|||
T GetKeyFrame(ref ExpressionEvaluationContext ctx, ServerKeyFrame<T> f) |
|||
{ |
|||
if (f.Expression != null) |
|||
return f.Expression.Evaluate(ref ctx).CastOrDefault<T>(); |
|||
else |
|||
return f.Value; |
|||
} |
|||
|
|||
public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property) |
|||
{ |
|||
_startedAt = startedAt; |
|||
_startingValue = startingValue.CastOrDefault<T>(); |
|||
var hs = new HashSet<(string, string)>(); |
|||
|
|||
// TODO: Update subscriptions based on the current keyframe rather than keeping subscriptions to all of them
|
|||
foreach (var frame in _keyFrames) |
|||
frame.Expression?.CollectReferences(hs); |
|||
Initialize(property, hs); |
|||
} |
|||
|
|||
public override void Activate() |
|||
{ |
|||
TargetObject.Compositor.AddToClock(this); |
|||
base.Activate(); |
|||
} |
|||
|
|||
public override void Deactivate() |
|||
{ |
|||
TargetObject.Compositor.RemoveFromClock(this); |
|||
base.Deactivate(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,89 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Animation.Easings; |
|||
using Avalonia.Rendering.Composition.Expressions; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Animations |
|||
{ |
|||
|
|||
/// <summary>
|
|||
/// Collection of composition animation key frames
|
|||
/// </summary>
|
|||
/// <typeparam name="T"></typeparam>
|
|||
class KeyFrames<T> : List<KeyFrame<T>>, IKeyFrames |
|||
{ |
|||
void Validate(float key) |
|||
{ |
|||
if (key < 0 || key > 1) |
|||
throw new ArgumentException("Key frame key"); |
|||
if (Count > 0 && this[Count - 1].NormalizedProgressKey > key) |
|||
throw new ArgumentException("Key frame key " + key + " is less than the previous one"); |
|||
} |
|||
|
|||
public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, IEasing easingFunction) |
|||
{ |
|||
Validate(normalizedProgressKey); |
|||
Add(new KeyFrame<T> |
|||
{ |
|||
NormalizedProgressKey = normalizedProgressKey, |
|||
Expression = Expression.Parse(value), |
|||
EasingFunction = easingFunction |
|||
}); |
|||
} |
|||
|
|||
public void Insert(float normalizedProgressKey, T value, IEasing easingFunction) |
|||
{ |
|||
Validate(normalizedProgressKey); |
|||
Add(new KeyFrame<T> |
|||
{ |
|||
NormalizedProgressKey = normalizedProgressKey, |
|||
Value = value, |
|||
EasingFunction = easingFunction |
|||
}); |
|||
} |
|||
|
|||
public ServerKeyFrame<T>[] Snapshot() |
|||
{ |
|||
var frames = new ServerKeyFrame<T>[Count]; |
|||
for (var c = 0; c < Count; c++) |
|||
{ |
|||
var f = this[c]; |
|||
frames[c] = new ServerKeyFrame<T> |
|||
{ |
|||
Expression = f.Expression, |
|||
Value = f.Value, |
|||
EasingFunction = f.EasingFunction, |
|||
Key = f.NormalizedProgressKey |
|||
}; |
|||
} |
|||
return frames; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Composition animation key frame
|
|||
/// </summary>
|
|||
struct KeyFrame<T> |
|||
{ |
|||
public float NormalizedProgressKey; |
|||
public T Value; |
|||
public Expression Expression; |
|||
public IEasing EasingFunction; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Server-side composition animation key frame
|
|||
/// </summary>
|
|||
struct ServerKeyFrame<T> |
|||
{ |
|||
public T Value; |
|||
public Expression? Expression; |
|||
public IEasing EasingFunction; |
|||
public float Key; |
|||
} |
|||
|
|||
interface IKeyFrames |
|||
{ |
|||
public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, IEasing easingFunction); |
|||
} |
|||
} |
|||
@ -0,0 +1,49 @@ |
|||
using System.Collections.Generic; |
|||
using Avalonia.Rendering.Composition.Expressions; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Animations |
|||
{ |
|||
/// <summary>
|
|||
/// A snapshot of properties used by an animation
|
|||
/// </summary>
|
|||
internal class PropertySetSnapshot : IExpressionParameterCollection, IExpressionObject |
|||
{ |
|||
private readonly Dictionary<string, Value> _dic; |
|||
|
|||
public struct Value |
|||
{ |
|||
public ExpressionVariant Variant; |
|||
public IExpressionObject Object; |
|||
|
|||
public Value(IExpressionObject o) |
|||
{ |
|||
Object = o; |
|||
Variant = default; |
|||
} |
|||
|
|||
public static implicit operator Value(ExpressionVariant v) => new Value |
|||
{ |
|||
Variant = v |
|||
}; |
|||
} |
|||
|
|||
public PropertySetSnapshot(Dictionary<string, Value> dic) |
|||
{ |
|||
_dic = dic; |
|||
} |
|||
|
|||
public ExpressionVariant GetParameter(string name) |
|||
{ |
|||
_dic.TryGetValue(name, out var v); |
|||
return v.Variant; |
|||
} |
|||
|
|||
public IExpressionObject GetObjectParameter(string name) |
|||
{ |
|||
_dic.TryGetValue(name, out var v); |
|||
return v.Object; |
|||
} |
|||
|
|||
public ExpressionVariant GetProperty(string name) => GetParameter(name); |
|||
} |
|||
} |
|||
@ -0,0 +1,278 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Numerics; |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia.Collections; |
|||
using Avalonia.Collections.Pooled; |
|||
using Avalonia.Media; |
|||
using Avalonia.Rendering.Composition.Drawing; |
|||
using Avalonia.Rendering.Composition.Server; |
|||
using Avalonia.Threading; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Rendering.Composition; |
|||
|
|||
/// <summary>
|
|||
/// A renderer that utilizes <see cref="Avalonia.Rendering.Composition.Compositor"/> to render the visual tree
|
|||
/// </summary>
|
|||
public class CompositingRenderer : IRendererWithCompositor |
|||
{ |
|||
private readonly IRenderRoot _root; |
|||
private readonly Compositor _compositor; |
|||
CompositionDrawingContext _recorder = new(); |
|||
DrawingContext _recordingContext; |
|||
private HashSet<Visual> _dirty = new(); |
|||
private HashSet<Visual> _recalculateChildren = new(); |
|||
private readonly CompositionTarget _target; |
|||
private bool _queuedUpdate; |
|||
private Action _update; |
|||
private Action _invalidateScene; |
|||
|
|||
/// <summary>
|
|||
/// Asks the renderer to only draw frames on the render thread. Makes Paint to wait until frame is rendered.
|
|||
/// </summary>
|
|||
public bool RenderOnlyOnRenderThread { get; set; } = true; |
|||
|
|||
public CompositingRenderer(IRenderRoot root, |
|||
Compositor compositor) |
|||
{ |
|||
_root = root; |
|||
_compositor = compositor; |
|||
_recordingContext = new DrawingContext(_recorder); |
|||
_target = compositor.CreateCompositionTarget(root.CreateRenderTarget); |
|||
_target.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor); |
|||
_update = Update; |
|||
_invalidateScene = InvalidateScene; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool DrawFps |
|||
{ |
|||
get => _target.DrawFps; |
|||
set => _target.DrawFps = value; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool DrawDirtyRects |
|||
{ |
|||
get => _target.DrawDirtyRects; |
|||
set => _target.DrawDirtyRects = value; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public event EventHandler<SceneInvalidatedEventArgs>? SceneInvalidated; |
|||
|
|||
void QueueUpdate() |
|||
{ |
|||
if(_queuedUpdate) |
|||
return; |
|||
_queuedUpdate = true; |
|||
Dispatcher.UIThread.Post(_update, DispatcherPriority.Composition); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void AddDirty(IVisual visual) |
|||
{ |
|||
_dirty.Add((Visual)visual); |
|||
QueueUpdate(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public IEnumerable<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool>? filter) |
|||
{ |
|||
var res = _target.TryHitTest(p, filter); |
|||
if(res == null) |
|||
yield break; |
|||
for (var index = res.Count - 1; index >= 0; index--) |
|||
{ |
|||
var v = res[index]; |
|||
if (v is CompositionDrawListVisual dv) |
|||
{ |
|||
if (filter == null || filter(dv.Visual)) |
|||
yield return dv.Visual; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public IVisual? HitTestFirst(Point p, IVisual root, Func<IVisual, bool>? filter) |
|||
{ |
|||
// TODO: Optimize
|
|||
return HitTest(p, root, filter).FirstOrDefault(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void RecalculateChildren(IVisual visual) |
|||
{ |
|||
_recalculateChildren.Add((Visual)visual); |
|||
QueueUpdate(); |
|||
} |
|||
|
|||
private void SyncChildren(Visual v) |
|||
{ |
|||
//TODO: Optimize by moving that logic to Visual itself
|
|||
if(v.CompositionVisual == null) |
|||
return; |
|||
var compositionChildren = v.CompositionVisual.Children; |
|||
var visualChildren = (AvaloniaList<IVisual>)v.GetVisualChildren(); |
|||
|
|||
PooledList<(IVisual visual, int index)>? sortedChildren = null; |
|||
if (v.HasNonUniformZIndexChildren && visualChildren.Count > 1) |
|||
{ |
|||
sortedChildren = new (visualChildren.Count); |
|||
for (var c = 0; c < visualChildren.Count; c++) |
|||
sortedChildren.Add((visualChildren[c], c)); |
|||
|
|||
// Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements.
|
|||
sortedChildren.Sort(static (lhs, rhs) => |
|||
{ |
|||
var result = lhs.visual.ZIndex.CompareTo(rhs.visual.ZIndex); |
|||
return result == 0 ? lhs.index.CompareTo(rhs.index) : result; |
|||
}); |
|||
} |
|||
|
|||
if (compositionChildren.Count == visualChildren.Count) |
|||
{ |
|||
bool mismatch = false; |
|||
if (v.HasNonUniformZIndexChildren) |
|||
{ |
|||
|
|||
|
|||
} |
|||
|
|||
if (sortedChildren != null) |
|||
for (var c = 0; c < visualChildren.Count; c++) |
|||
{ |
|||
if (!ReferenceEquals(compositionChildren[c], ((Visual)sortedChildren[c].visual).CompositionVisual)) |
|||
{ |
|||
mismatch = true; |
|||
break; |
|||
} |
|||
} |
|||
else |
|||
for (var c = 0; c < visualChildren.Count; c++) |
|||
if (!ReferenceEquals(compositionChildren[c], ((Visual)visualChildren[c]).CompositionVisual)) |
|||
{ |
|||
mismatch = true; |
|||
break; |
|||
} |
|||
|
|||
|
|||
if (!mismatch) |
|||
{ |
|||
sortedChildren?.Dispose(); |
|||
return; |
|||
} |
|||
} |
|||
|
|||
compositionChildren.Clear(); |
|||
if (sortedChildren != null) |
|||
{ |
|||
foreach (var ch in sortedChildren) |
|||
{ |
|||
var compositionChild = ((Visual)ch.visual).CompositionVisual; |
|||
if (compositionChild != null) |
|||
compositionChildren.Add(compositionChild); |
|||
} |
|||
sortedChildren.Dispose(); |
|||
} |
|||
else |
|||
foreach (var ch in v.GetVisualChildren()) |
|||
{ |
|||
var compositionChild = ((Visual)ch).CompositionVisual; |
|||
if (compositionChild != null) |
|||
compositionChildren.Add(compositionChild); |
|||
} |
|||
} |
|||
|
|||
private void InvalidateScene() => |
|||
SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize))); |
|||
|
|||
private void Update() |
|||
{ |
|||
_queuedUpdate = false; |
|||
foreach (var visual in _dirty) |
|||
{ |
|||
var comp = visual.CompositionVisual; |
|||
if(comp == null) |
|||
continue; |
|||
|
|||
// TODO: Optimize all of that by moving to the Visual itself, so we won't have to recalculate every time
|
|||
comp.Offset = new Vector3((float)visual.Bounds.Left, (float)visual.Bounds.Top, 0); |
|||
comp.Size = new Vector2((float)visual.Bounds.Width, (float)visual.Bounds.Height); |
|||
comp.Visible = visual.IsVisible; |
|||
comp.Opacity = (float)visual.Opacity; |
|||
comp.ClipToBounds = visual.ClipToBounds; |
|||
comp.Clip = visual.Clip?.PlatformImpl; |
|||
comp.OpacityMask = visual.OpacityMask; |
|||
|
|||
var renderTransform = Matrix.Identity; |
|||
|
|||
if (visual.HasMirrorTransform) |
|||
renderTransform = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0); |
|||
|
|||
if (visual.RenderTransform != null) |
|||
{ |
|||
var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); |
|||
var offset = Matrix.CreateTranslation(origin); |
|||
renderTransform *= (-offset) * visual.RenderTransform.Value * (offset); |
|||
} |
|||
|
|||
|
|||
|
|||
comp.TransformMatrix = MatrixUtils.ToMatrix4x4(renderTransform); |
|||
|
|||
_recorder.BeginUpdate(comp.DrawList); |
|||
visual.Render(_recordingContext); |
|||
comp.DrawList = _recorder.EndUpdate(); |
|||
|
|||
SyncChildren(visual); |
|||
} |
|||
foreach(var v in _recalculateChildren) |
|||
if (!_dirty.Contains(v)) |
|||
SyncChildren(v); |
|||
_dirty.Clear(); |
|||
_recalculateChildren.Clear(); |
|||
_target.Size = _root.ClientSize; |
|||
_target.Scaling = _root.RenderScaling; |
|||
Compositor.InvokeOnNextCommit(_invalidateScene); |
|||
} |
|||
|
|||
public void Resized(Size size) |
|||
{ |
|||
} |
|||
|
|||
public void Paint(Rect rect) |
|||
{ |
|||
Update(); |
|||
_target.RequestRedraw(); |
|||
if(RenderOnlyOnRenderThread && Compositor.Loop.RunsInBackground) |
|||
Compositor.RequestCommitAsync().Wait(); |
|||
else |
|||
_target.ImmediateUIThreadRender(); |
|||
} |
|||
|
|||
public void Start() => _target.IsEnabled = true; |
|||
|
|||
public void Stop() |
|||
{ |
|||
_target.IsEnabled = false; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
Stop(); |
|||
_target.Dispose(); |
|||
|
|||
// Wait for the composition batch to be applied and rendered to guarantee that
|
|||
// render target is not used anymore and can be safely disposed
|
|||
if (Compositor.Loop.RunsInBackground) |
|||
_compositor.RequestCommitAsync().Wait(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The associated <see cref="Avalonia.Rendering.Composition.Compositor"/> object
|
|||
/// </summary>
|
|||
public Compositor Compositor => _compositor; |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
using System; |
|||
using System.Numerics; |
|||
using Avalonia.Rendering.Composition.Drawing; |
|||
using Avalonia.Rendering.Composition.Server; |
|||
using Avalonia.Rendering.Composition.Transport; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Rendering.Composition; |
|||
|
|||
|
|||
/// <summary>
|
|||
/// A composition visual that holds a list of drawing commands issued by <see cref="Avalonia.Visual"/>
|
|||
/// </summary>
|
|||
internal class CompositionDrawListVisual : CompositionContainerVisual |
|||
{ |
|||
/// <summary>
|
|||
/// The associated <see cref="Avalonia.Visual"/>
|
|||
/// </summary>
|
|||
public Visual Visual { get; } |
|||
|
|||
private bool _drawListChanged; |
|||
private CompositionDrawList? _drawList; |
|||
|
|||
/// <summary>
|
|||
/// The list of drawing commands
|
|||
/// </summary>
|
|||
public CompositionDrawList? DrawList |
|||
{ |
|||
get => _drawList; |
|||
set |
|||
{ |
|||
_drawList?.Dispose(); |
|||
_drawList = value; |
|||
_drawListChanged = true; |
|||
RegisterForSerialization(); |
|||
} |
|||
} |
|||
|
|||
private protected override void SerializeChangesCore(BatchStreamWriter writer) |
|||
{ |
|||
writer.Write((byte)(_drawListChanged ? 1 : 0)); |
|||
if (_drawListChanged) |
|||
{ |
|||
writer.WriteObject(DrawList?.Clone()); |
|||
_drawListChanged = false; |
|||
} |
|||
base.SerializeChangesCore(writer); |
|||
} |
|||
|
|||
internal CompositionDrawListVisual(Compositor compositor, ServerCompositionDrawListVisual server, Visual visual) : base(compositor, server) |
|||
{ |
|||
Visual = visual; |
|||
} |
|||
|
|||
internal override bool HitTest(Point pt, Func<IVisual, bool>? filter) |
|||
{ |
|||
if (DrawList == null) |
|||
return false; |
|||
if (filter != null && !filter(Visual)) |
|||
return false; |
|||
if (Visual is ICustomHitTest custom) |
|||
return custom.HitTest(pt); |
|||
foreach (var op in DrawList) |
|||
if (op.Item.HitTest(pt)) |
|||
return true; |
|||
return false; |
|||
} |
|||
} |
|||
@ -0,0 +1,141 @@ |
|||
using System; |
|||
using Avalonia.Rendering.Composition.Animations; |
|||
using Avalonia.Rendering.Composition.Expressions; |
|||
using Avalonia.Rendering.Composition.Server; |
|||
using Avalonia.Rendering.Composition.Transport; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Rendering.Composition |
|||
{ |
|||
/// <summary>
|
|||
/// Base class of the composition API representing a node in the visual tree structure.
|
|||
/// Composition objects are the visual tree structure on which all other features of the composition API use and build on.
|
|||
/// The API allows developers to define and create one or many <see cref="CompositionVisual" /> objects each representing a single node in a Visual tree.
|
|||
/// </summary>
|
|||
public abstract class CompositionObject : IDisposable |
|||
{ |
|||
/// <summary>
|
|||
/// The collection of implicit animations attached to this object.
|
|||
/// </summary>
|
|||
public ImplicitAnimationCollection? ImplicitAnimations { get; set; } |
|||
|
|||
private protected InlineDictionary<CompositionProperty, IAnimationInstance> PendingAnimations; |
|||
internal CompositionObject(Compositor compositor, ServerObject server) |
|||
{ |
|||
Compositor = compositor; |
|||
Server = server; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The associated Compositor
|
|||
/// </summary>
|
|||
public Compositor Compositor { get; } |
|||
internal ServerObject Server { get; } |
|||
public bool IsDisposed { get; private set; } |
|||
private bool _registeredForSerialization; |
|||
|
|||
private static void ThrowInvalidOperation() => |
|||
throw new InvalidOperationException("There is no server-side counterpart for this object"); |
|||
|
|||
public void Dispose() |
|||
{ |
|||
RegisterForSerialization(); |
|||
IsDisposed = true; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Connects an animation with the specified property of the object and starts the animation.
|
|||
/// </summary>
|
|||
public void StartAnimation(string propertyName, CompositionAnimation animation) |
|||
=> StartAnimation(propertyName, animation, null); |
|||
|
|||
internal virtual void StartAnimation(string propertyName, CompositionAnimation animation, ExpressionVariant? finalValue) |
|||
{ |
|||
throw new ArgumentException("Unknown property " + propertyName); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Starts an animation group.
|
|||
/// The StartAnimationGroup method on CompositionObject lets you start CompositionAnimationGroup.
|
|||
/// All the animations in the group will be started at the same time on the object.
|
|||
/// </summary>
|
|||
public void StartAnimationGroup(ICompositionAnimationBase grp) |
|||
{ |
|||
if (grp is CompositionAnimation animation) |
|||
{ |
|||
if(animation.Target == null) |
|||
throw new ArgumentException("Animation Target can't be null"); |
|||
StartAnimation(animation.Target, animation); |
|||
} |
|||
else if (grp is CompositionAnimationGroup group) |
|||
{ |
|||
foreach (var a in group.Animations) |
|||
{ |
|||
if (a.Target == null) |
|||
throw new ArgumentException("Animation Target can't be null"); |
|||
StartAnimation(a.Target, a); |
|||
} |
|||
} |
|||
} |
|||
|
|||
bool StartAnimationGroupPart(CompositionAnimation animation, string target, ExpressionVariant finalValue) |
|||
{ |
|||
if(animation.Target == null) |
|||
throw new ArgumentException("Animation Target can't be null"); |
|||
if (animation.Target == target) |
|||
{ |
|||
StartAnimation(animation.Target, animation, finalValue); |
|||
return true; |
|||
} |
|||
else |
|||
{ |
|||
StartAnimation(animation.Target, animation); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
internal bool StartAnimationGroup(ICompositionAnimationBase grp, string target, ExpressionVariant finalValue) |
|||
{ |
|||
if (grp is CompositionAnimation animation) |
|||
return StartAnimationGroupPart(animation, target, finalValue); |
|||
if (grp is CompositionAnimationGroup group) |
|||
{ |
|||
var matched = false; |
|||
foreach (var a in group.Animations) |
|||
{ |
|||
if (a.Target == null) |
|||
throw new ArgumentException("Animation Target can't be null"); |
|||
if (StartAnimationGroupPart(a, target, finalValue)) |
|||
matched = true; |
|||
} |
|||
|
|||
return matched; |
|||
} |
|||
|
|||
throw new ArgumentException(); |
|||
} |
|||
|
|||
protected void RegisterForSerialization() |
|||
{ |
|||
if (Server == null) |
|||
throw new InvalidOperationException("The object doesn't have an associated server counterpart"); |
|||
|
|||
if(_registeredForSerialization) |
|||
return; |
|||
_registeredForSerialization = true; |
|||
Compositor.RegisterForSerialization(this); |
|||
} |
|||
|
|||
internal void SerializeChanges(BatchStreamWriter writer) |
|||
{ |
|||
_registeredForSerialization = false; |
|||
SerializeChangesCore(writer); |
|||
} |
|||
|
|||
private protected virtual void SerializeChangesCore(BatchStreamWriter writer) |
|||
{ |
|||
if (Server is IDisposable) |
|||
writer.Write((byte)(IsDisposed ? 1 : 0)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,147 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Numerics; |
|||
using Avalonia.Rendering.Composition.Animations; |
|||
using Avalonia.Rendering.Composition.Expressions; |
|||
using Avalonia.Rendering.Composition.Transport; |
|||
|
|||
namespace Avalonia.Rendering.Composition |
|||
{ |
|||
/// <summary>
|
|||
/// <see cref="CompositionPropertySet"/>s are <see cref="CompositionObject"/>s that allow storage of key values pairs
|
|||
/// that can be shared across the application and are not tied to the lifetime of another composition object.
|
|||
/// <see cref="CompositionPropertySet"/>s are most commonly used with animations, where they maintain key-value pairs
|
|||
/// that are referenced to drive portions of composition animations. <see cref="CompositionPropertySet"/>s
|
|||
/// provide the ability to insert key-value pairs or retrieve a value for a given key.
|
|||
/// <see cref="CompositionPropertySet"/> does not support a delete function – ensure you use <see cref="CompositionPropertySet"/>
|
|||
/// to store values that will be shared across the application.
|
|||
/// </summary>
|
|||
public class CompositionPropertySet : CompositionObject |
|||
{ |
|||
private readonly Dictionary<string, ExpressionVariant> _variants = new Dictionary<string, ExpressionVariant>(); |
|||
private readonly Dictionary<string, CompositionObject> _objects = new Dictionary<string, CompositionObject>(); |
|||
|
|||
internal CompositionPropertySet(Compositor compositor) : base(compositor, null!) |
|||
{ |
|||
} |
|||
|
|||
internal void Set(string key, ExpressionVariant value) |
|||
{ |
|||
_objects.Remove(key); |
|||
_variants[key] = value; |
|||
} |
|||
|
|||
/* |
|||
For INTERNAL USE by CompositionAnimation ONLY, we DON'T support expression |
|||
paths like SomeParam.SomePropertyObject.SomeValue |
|||
*/ |
|||
internal void Set(string key, CompositionObject obj) |
|||
{ |
|||
_objects[key] = obj ?? throw new ArgumentNullException(nameof(obj)); |
|||
_variants.Remove(key); |
|||
} |
|||
|
|||
public void InsertColor(string propertyName, Avalonia.Media.Color value) => Set(propertyName, value); |
|||
|
|||
public void InsertMatrix3x2(string propertyName, Matrix3x2 value) => Set(propertyName, value); |
|||
|
|||
public void InsertMatrix4x4(string propertyName, Matrix4x4 value) => Set(propertyName, value); |
|||
|
|||
public void InsertQuaternion(string propertyName, Quaternion value) => Set(propertyName, value); |
|||
|
|||
public void InsertScalar(string propertyName, float value) => Set(propertyName, value); |
|||
public void InsertVector2(string propertyName, Vector2 value) => Set(propertyName, value); |
|||
|
|||
public void InsertVector3(string propertyName, Vector3 value) => Set(propertyName, value); |
|||
|
|||
public void InsertVector4(string propertyName, Vector4 value) => Set(propertyName, value); |
|||
|
|||
|
|||
CompositionGetValueStatus TryGetVariant<T>(string key, out T value) where T : struct |
|||
{ |
|||
value = default; |
|||
if (!_variants.TryGetValue(key, out var v)) |
|||
return _objects.ContainsKey(key) |
|||
? CompositionGetValueStatus.TypeMismatch |
|||
: CompositionGetValueStatus.NotFound; |
|||
|
|||
return v.TryCast(out value) ? CompositionGetValueStatus.Succeeded : CompositionGetValueStatus.TypeMismatch; |
|||
} |
|||
|
|||
public CompositionGetValueStatus TryGetColor(string propertyName, out Avalonia.Media.Color value) |
|||
=> TryGetVariant(propertyName, out value); |
|||
|
|||
public CompositionGetValueStatus TryGetMatrix3x2(string propertyName, out Matrix3x2 value) |
|||
=> TryGetVariant(propertyName, out value); |
|||
|
|||
public CompositionGetValueStatus TryGetMatrix4x4(string propertyName, out Matrix4x4 value) |
|||
=> TryGetVariant(propertyName, out value); |
|||
|
|||
public CompositionGetValueStatus TryGetQuaternion(string propertyName, out Quaternion value) |
|||
=> TryGetVariant(propertyName, out value); |
|||
|
|||
|
|||
public CompositionGetValueStatus TryGetScalar(string propertyName, out float value) |
|||
=> TryGetVariant(propertyName, out value); |
|||
|
|||
public CompositionGetValueStatus TryGetVector2(string propertyName, out Vector2 value) |
|||
=> TryGetVariant(propertyName, out value); |
|||
|
|||
public CompositionGetValueStatus TryGetVector3(string propertyName, out Vector3 value) |
|||
=> TryGetVariant(propertyName, out value); |
|||
|
|||
public CompositionGetValueStatus TryGetVector4(string propertyName, out Vector4 value) |
|||
=> TryGetVariant(propertyName, out value); |
|||
|
|||
|
|||
public void InsertBoolean(string propertyName, bool value) => Set(propertyName, value); |
|||
|
|||
public CompositionGetValueStatus TryGetBoolean(string propertyName, out bool value) |
|||
=> TryGetVariant(propertyName, out value); |
|||
|
|||
internal void ClearAll() |
|||
{ |
|||
_objects.Clear(); |
|||
_variants.Clear(); |
|||
} |
|||
|
|||
internal void Clear(string key) |
|||
{ |
|||
_objects.Remove(key); |
|||
_variants.Remove(key); |
|||
} |
|||
|
|||
internal PropertySetSnapshot Snapshot() => |
|||
SnapshotCore(1); |
|||
|
|||
private PropertySetSnapshot SnapshotCore(int allowedNestingLevel) |
|||
{ |
|||
var dic = new Dictionary<string, PropertySetSnapshot.Value>(_objects.Count + _variants.Count); |
|||
foreach (var o in _objects) |
|||
{ |
|||
if (o.Value is CompositionPropertySet ps) |
|||
{ |
|||
if (allowedNestingLevel <= 0) |
|||
throw new InvalidOperationException("PropertySet depth limit reached"); |
|||
dic[o.Key] = new PropertySetSnapshot.Value(ps.SnapshotCore(allowedNestingLevel - 1)); |
|||
} |
|||
else if (o.Value.Server == null) |
|||
throw new InvalidOperationException($"Object of type {o.Value.GetType()} is not allowed"); |
|||
else |
|||
dic[o.Key] = new PropertySetSnapshot.Value(o.Value.Server); |
|||
} |
|||
|
|||
foreach (var v in _variants) |
|||
dic[v.Key] = v.Value; |
|||
|
|||
return new PropertySetSnapshot(dic); |
|||
} |
|||
} |
|||
|
|||
public enum CompositionGetValueStatus |
|||
{ |
|||
Succeeded, |
|||
TypeMismatch, |
|||
NotFound |
|||
} |
|||
} |
|||
@ -0,0 +1,138 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Numerics; |
|||
using Avalonia.Collections.Pooled; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Rendering.Composition |
|||
{ |
|||
/// <summary>
|
|||
/// Represents the composition output (e. g. a window, embedded control, entire screen)
|
|||
/// </summary>
|
|||
public partial class CompositionTarget |
|||
{ |
|||
partial void OnRootChanged() |
|||
{ |
|||
if (Root != null) |
|||
Root.Root = this; |
|||
} |
|||
|
|||
partial void OnRootChanging() |
|||
{ |
|||
if (Root != null) |
|||
Root.Root = null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Attempts to perform a hit-tst
|
|||
/// </summary>
|
|||
/// <param name="point"></param>
|
|||
/// <param name="filter"></param>
|
|||
/// <returns></returns>
|
|||
public PooledList<CompositionVisual>? TryHitTest(Point point, Func<IVisual, bool>? filter) |
|||
{ |
|||
Server.Readback.NextRead(); |
|||
if (Root == null) |
|||
return null; |
|||
var res = new PooledList<CompositionVisual>(); |
|||
HitTestCore(Root, point, res, filter); |
|||
return res; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Attempts to transform a point to a particular CompositionVisual coordinate space
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
public Point? TryTransformToVisual(CompositionVisual visual, Point point) |
|||
{ |
|||
if (visual.Root != this) |
|||
return null; |
|||
var v = visual; |
|||
var m = Matrix.Identity; |
|||
while (v != null) |
|||
{ |
|||
if (!TryGetInvertedTransform(v, out var cm)) |
|||
return null; |
|||
m = m * cm; |
|||
v = v.Parent; |
|||
} |
|||
|
|||
return point * m; |
|||
} |
|||
|
|||
bool TryGetInvertedTransform(CompositionVisual visual, out Matrix matrix) |
|||
{ |
|||
var m = visual.TryGetServerTransform(); |
|||
if (m == null) |
|||
{ |
|||
matrix = default; |
|||
return false; |
|||
} |
|||
|
|||
var m33 = MatrixUtils.ToMatrix(m.Value); |
|||
return m33.TryInvert(out matrix); |
|||
} |
|||
|
|||
bool TryTransformTo(CompositionVisual visual, ref Point v) |
|||
{ |
|||
if (TryGetInvertedTransform(visual, out var m)) |
|||
{ |
|||
v = v * m; |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
bool HitTestCore(CompositionVisual visual, Point point, PooledList<CompositionVisual> result, |
|||
Func<IVisual, bool>? filter) |
|||
{ |
|||
//TODO: Check readback too
|
|||
if (visual.Visible == false) |
|||
return false; |
|||
if (!TryTransformTo(visual, ref point)) |
|||
return false; |
|||
|
|||
if (visual.ClipToBounds |
|||
&& (point.X < 0 || point.Y < 0 || point.X > visual.Size.X || point.Y > visual.Size.Y)) |
|||
return false; |
|||
if (visual.Clip?.FillContains(point) == false) |
|||
return false; |
|||
|
|||
bool success = false; |
|||
// Hit-test the current node
|
|||
if (visual.HitTest(point, filter)) |
|||
{ |
|||
result.Add(visual); |
|||
success = true; |
|||
} |
|||
|
|||
// Inspect children too
|
|||
if (visual is CompositionContainerVisual cv) |
|||
for (var c = cv.Children.Count - 1; c >= 0; c--) |
|||
{ |
|||
var ch = cv.Children[c]; |
|||
var hit = HitTestCore(ch, point, result, filter); |
|||
if (hit) |
|||
return true; |
|||
} |
|||
|
|||
return success; |
|||
|
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Registers the composition target for explicit redraw
|
|||
/// </summary>
|
|||
public void RequestRedraw() => RegisterForSerialization(); |
|||
|
|||
/// <summary>
|
|||
/// Performs composition directly on the UI thread
|
|||
/// </summary>
|
|||
internal void ImmediateUIThreadRender() |
|||
{ |
|||
Compositor.RequestCommitAsync(); |
|||
Compositor.Server.Render(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,141 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Numerics; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Animation.Easings; |
|||
using Avalonia.Media; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering.Composition.Animations; |
|||
using Avalonia.Rendering.Composition.Server; |
|||
using Avalonia.Rendering.Composition.Transport; |
|||
using Avalonia.Threading; |
|||
|
|||
|
|||
namespace Avalonia.Rendering.Composition |
|||
{ |
|||
/// <summary>
|
|||
/// The Compositor class manages communication between UI-thread and render-thread parts of the composition engine.
|
|||
/// It also serves as a factory to create UI-thread parts of various composition objects
|
|||
/// </summary>
|
|||
public partial class Compositor |
|||
{ |
|||
internal IRenderLoop Loop { get; } |
|||
private ServerCompositor _server; |
|||
private bool _implicitBatchCommitQueued; |
|||
private Action _implicitBatchCommit; |
|||
private BatchStreamObjectPool<object?> _batchObjectPool = new(); |
|||
private BatchStreamMemoryPool _batchMemoryPool = new(); |
|||
private List<CompositionObject> _objectsForSerialization = new(); |
|||
internal ServerCompositor Server => _server; |
|||
internal IEasing DefaultEasing { get; } |
|||
private List<Action>? _invokeOnNextCommit; |
|||
private readonly Stack<List<Action>> _invokeListPool = new(); |
|||
|
|||
/// <summary>
|
|||
/// Creates a new compositor on a specified render loop that would use a particular GPU
|
|||
/// </summary>
|
|||
/// <param name="loop"></param>
|
|||
/// <param name="gpu"></param>
|
|||
public Compositor(IRenderLoop loop, IPlatformGpu? gpu) |
|||
{ |
|||
Loop = loop; |
|||
_server = new ServerCompositor(loop, gpu, _batchObjectPool, _batchMemoryPool); |
|||
_implicitBatchCommit = ImplicitBatchCommit; |
|||
|
|||
DefaultEasing = new CubicBezierEasing(new Point(0.25f, 0.1f), new Point(0.25f, 1f)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a new CompositionTarget
|
|||
/// </summary>
|
|||
/// <param name="renderTargetFactory">A factory method to create IRenderTarget to be called from the render thread</param>
|
|||
/// <returns></returns>
|
|||
public CompositionTarget CreateCompositionTarget(Func<IRenderTarget> renderTargetFactory) |
|||
{ |
|||
return new CompositionTarget(this, new ServerCompositionTarget(_server, renderTargetFactory)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Requests pending changes in the composition objects to be serialized and sent to the render thread
|
|||
/// </summary>
|
|||
/// <returns>A task that completes when sent changes are applied and rendered on the render thread</returns>
|
|||
public Task RequestCommitAsync() |
|||
{ |
|||
Dispatcher.UIThread.VerifyAccess(); |
|||
var batch = new Batch(); |
|||
|
|||
using (var writer = new BatchStreamWriter(batch.Changes, _batchMemoryPool, _batchObjectPool)) |
|||
{ |
|||
foreach (var obj in _objectsForSerialization) |
|||
{ |
|||
writer.WriteObject(obj.Server); |
|||
obj.SerializeChanges(writer); |
|||
#if DEBUG_COMPOSITOR_SERIALIZATION
|
|||
writer.Write(BatchStreamDebugMarkers.ObjectEndMagic); |
|||
writer.WriteObject(BatchStreamDebugMarkers.ObjectEndMarker); |
|||
#endif
|
|||
} |
|||
_objectsForSerialization.Clear(); |
|||
} |
|||
|
|||
batch.CommitedAt = Server.Clock.Elapsed; |
|||
_server.EnqueueBatch(batch); |
|||
if (_invokeOnNextCommit != null) |
|||
ScheduleCommitCallbacks(batch.Completed); |
|||
|
|||
return batch.Completed; |
|||
} |
|||
|
|||
async void ScheduleCommitCallbacks(Task task) |
|||
{ |
|||
var list = _invokeOnNextCommit; |
|||
_invokeOnNextCommit = null; |
|||
await task; |
|||
foreach (var i in list!) |
|||
i(); |
|||
list.Clear(); |
|||
_invokeListPool.Push(list); |
|||
} |
|||
|
|||
public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server)); |
|||
|
|||
public ExpressionAnimation CreateExpressionAnimation() => new ExpressionAnimation(this); |
|||
|
|||
public ExpressionAnimation CreateExpressionAnimation(string expression) => new ExpressionAnimation(this) |
|||
{ |
|||
Expression = expression |
|||
}; |
|||
|
|||
public ImplicitAnimationCollection CreateImplicitAnimationCollection() => new ImplicitAnimationCollection(this); |
|||
|
|||
public CompositionAnimationGroup CreateAnimationGroup() => new CompositionAnimationGroup(this); |
|||
|
|||
private void QueueImplicitBatchCommit() |
|||
{ |
|||
if(_implicitBatchCommitQueued) |
|||
return; |
|||
_implicitBatchCommitQueued = true; |
|||
Dispatcher.UIThread.Post(_implicitBatchCommit, DispatcherPriority.CompositionBatch); |
|||
} |
|||
|
|||
private void ImplicitBatchCommit() |
|||
{ |
|||
_implicitBatchCommitQueued = false; |
|||
RequestCommitAsync(); |
|||
} |
|||
|
|||
internal void RegisterForSerialization(CompositionObject compositionObject) |
|||
{ |
|||
Dispatcher.UIThread.VerifyAccess(); |
|||
_objectsForSerialization.Add(compositionObject); |
|||
QueueImplicitBatchCommit(); |
|||
} |
|||
|
|||
internal void InvokeOnNextCommit(Action action) |
|||
{ |
|||
_invokeOnNextCommit ??= _invokeListPool.Count > 0 ? _invokeListPool.Pop() : new(); |
|||
_invokeOnNextCommit.Add(action); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
using Avalonia.Rendering.Composition.Server; |
|||
|
|||
namespace Avalonia.Rendering.Composition |
|||
{ |
|||
/// <summary>
|
|||
/// A node in the visual tree that can have children.
|
|||
/// </summary>
|
|||
public partial class CompositionContainerVisual : CompositionVisual |
|||
{ |
|||
public CompositionVisualCollection Children { get; private set; } = null!; |
|||
|
|||
partial void InitializeDefaultsExtra() |
|||
{ |
|||
Children = new CompositionVisualCollection(this, Server.Children); |
|||
} |
|||
|
|||
private protected override void OnRootChangedCore() |
|||
{ |
|||
foreach (var ch in Children) |
|||
ch.Root = Root; |
|||
base.OnRootChangedCore(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,102 @@ |
|||
using System; |
|||
using Avalonia.Collections.Pooled; |
|||
using Avalonia.Rendering.Composition.Server; |
|||
using Avalonia.Rendering.SceneGraph; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Drawing; |
|||
|
|||
/// <summary>
|
|||
/// A list of serialized drawing commands
|
|||
/// </summary>
|
|||
internal class CompositionDrawList : PooledList<IRef<IDrawOperation>> |
|||
{ |
|||
public Size? Size { get; set; } |
|||
|
|||
public CompositionDrawList() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public CompositionDrawList(int capacity) : base(capacity) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public override void Dispose() |
|||
{ |
|||
foreach(var item in this) |
|||
item.Dispose(); |
|||
base.Dispose(); |
|||
} |
|||
|
|||
public CompositionDrawList Clone() |
|||
{ |
|||
var clone = new CompositionDrawList(Count) { Size = Size }; |
|||
foreach (var r in this) |
|||
clone.Add(r.Clone()); |
|||
return clone; |
|||
} |
|||
|
|||
public void Render(CompositorDrawingContextProxy canvas) |
|||
{ |
|||
foreach (var cmd in this) |
|||
{ |
|||
canvas.VisualBrushDrawList = (cmd.Item as BrushDrawOperation)?.Aux as CompositionDrawList; |
|||
cmd.Item.Render(canvas); |
|||
} |
|||
|
|||
canvas.VisualBrushDrawList = null; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// An helper class for building <see cref="CompositionDrawList"/>
|
|||
/// </summary>
|
|||
internal class CompositionDrawListBuilder |
|||
{ |
|||
private CompositionDrawList? _operations; |
|||
private bool _owns; |
|||
|
|||
public void Reset(CompositionDrawList? previousOperations) |
|||
{ |
|||
_operations = previousOperations; |
|||
_owns = false; |
|||
} |
|||
|
|||
public int Count => _operations?.Count ?? 0; |
|||
public CompositionDrawList? DrawOperations => _operations; |
|||
|
|||
void MakeWritable(int atIndex) |
|||
{ |
|||
if(_owns) |
|||
return; |
|||
_owns = true; |
|||
var newOps = new CompositionDrawList(_operations?.Count ?? Math.Max(1, atIndex)); |
|||
if (_operations != null) |
|||
{ |
|||
for (var c = 0; c < atIndex; c++) |
|||
newOps.Add(_operations[c].Clone()); |
|||
} |
|||
|
|||
_operations = newOps; |
|||
} |
|||
|
|||
public void ReplaceDrawOperation(int index, IDrawOperation node) |
|||
{ |
|||
MakeWritable(index); |
|||
DrawOperations!.Add(RefCountable.Create(node)); |
|||
} |
|||
|
|||
public void AddDrawOperation(IDrawOperation node) |
|||
{ |
|||
MakeWritable(Count); |
|||
DrawOperations!.Add(RefCountable.Create(node)); |
|||
} |
|||
|
|||
public void TrimTo(int count) |
|||
{ |
|||
if (count < Count) |
|||
_operations!.RemoveRange(count, _operations.Count - count); |
|||
} |
|||
} |
|||
@ -0,0 +1,391 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Numerics; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Imaging; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering.Composition.Drawing; |
|||
using Avalonia.Rendering.SceneGraph; |
|||
using Avalonia.Utilities; |
|||
using Avalonia.VisualTree; |
|||
namespace Avalonia.Rendering.Composition; |
|||
|
|||
/// <summary>
|
|||
/// An IDrawingContextImpl implementation that builds <see cref="CompositionDrawList"/>
|
|||
/// </summary>
|
|||
internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport |
|||
{ |
|||
private CompositionDrawListBuilder _builder = new(); |
|||
private int _drawOperationIndex; |
|||
|
|||
/// <inheritdoc/>
|
|||
public Matrix Transform { get; set; } = Matrix.Identity; |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Clear(Color color) |
|||
{ |
|||
// Cannot clear a deferred scene.
|
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Dispose() |
|||
{ |
|||
// Nothing to do here since we allocate no unmanaged resources.
|
|||
} |
|||
|
|||
public void BeginUpdate(CompositionDrawList? list) |
|||
{ |
|||
_builder.Reset(list); |
|||
_drawOperationIndex = 0; |
|||
} |
|||
|
|||
public CompositionDrawList EndUpdate() |
|||
{ |
|||
_builder.TrimTo(_drawOperationIndex); |
|||
return _builder.DrawOperations!; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) |
|||
{ |
|||
var next = NextDrawAs<GeometryNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(Transform, brush, pen, geometry)) |
|||
{ |
|||
Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush))); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationIndex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, |
|||
BitmapInterpolationMode bitmapInterpolationMode) |
|||
{ |
|||
var next = NextDrawAs<ImageNode>(); |
|||
|
|||
if (next == null || |
|||
!next.Item.Equals(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)) |
|||
{ |
|||
Add(new ImageNode(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationIndex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) |
|||
{ |
|||
// This method is currently only used to composite layers so shouldn't be called here.
|
|||
throw new NotSupportedException(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void DrawLine(IPen pen, Point p1, Point p2) |
|||
{ |
|||
var next = NextDrawAs<LineNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(Transform, pen, p1, p2)) |
|||
{ |
|||
Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush))); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationIndex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, |
|||
BoxShadows boxShadows = default) |
|||
{ |
|||
var next = NextDrawAs<RectangleNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows)) |
|||
{ |
|||
Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush))); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationIndex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) |
|||
{ |
|||
var next = NextDrawAs<ExperimentalAcrylicNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(Transform, material, rect)) |
|||
{ |
|||
Add(new ExperimentalAcrylicNode(Transform, material, rect)); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationIndex; |
|||
} |
|||
} |
|||
|
|||
public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) |
|||
{ |
|||
var next = NextDrawAs<EllipseNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(Transform, brush, pen, rect)) |
|||
{ |
|||
Add(new EllipseNode(Transform, brush, pen, rect, CreateChildScene(brush))); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationIndex; |
|||
} |
|||
} |
|||
|
|||
public void Custom(ICustomDrawOperation custom) |
|||
{ |
|||
var next = NextDrawAs<CustomDrawOperation>(); |
|||
if (next == null || !next.Item.Equals(Transform, custom)) |
|||
Add(new CustomDrawOperation(custom, Transform)); |
|||
else |
|||
++_drawOperationIndex; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) |
|||
{ |
|||
var next = NextDrawAs<GlyphRunNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(Transform, foreground, glyphRun)) |
|||
{ |
|||
Add(new GlyphRunNode(Transform, foreground, glyphRun, CreateChildScene(foreground))); |
|||
} |
|||
|
|||
else |
|||
{ |
|||
++_drawOperationIndex; |
|||
} |
|||
} |
|||
|
|||
public IDrawingContextLayerImpl CreateLayer(Size size) |
|||
{ |
|||
throw new NotSupportedException("Creating layers on a deferred drawing context not supported"); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void PopClip() |
|||
{ |
|||
var next = NextDrawAs<ClipNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(null)) |
|||
{ |
|||
Add(new ClipNode()); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationIndex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void PopGeometryClip() |
|||
{ |
|||
var next = NextDrawAs<GeometryClipNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(null)) |
|||
{ |
|||
Add(new GeometryClipNode()); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationIndex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void PopBitmapBlendMode() |
|||
{ |
|||
var next = NextDrawAs<BitmapBlendModeNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(null)) |
|||
{ |
|||
Add(new BitmapBlendModeNode()); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationIndex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void PopOpacity() |
|||
{ |
|||
var next = NextDrawAs<OpacityNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(null)) |
|||
{ |
|||
Add(new OpacityNode()); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationIndex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void PopOpacityMask() |
|||
{ |
|||
var next = NextDrawAs<OpacityMaskNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(null, null)) |
|||
{ |
|||
Add(new OpacityMaskNode()); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationIndex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void PushClip(Rect clip) |
|||
{ |
|||
var next = NextDrawAs<ClipNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(Transform, clip)) |
|||
{ |
|||
Add(new ClipNode(Transform, clip)); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationIndex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public void PushClip(RoundedRect clip) |
|||
{ |
|||
var next = NextDrawAs<ClipNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(Transform, clip)) |
|||
{ |
|||
Add(new ClipNode(Transform, clip)); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationIndex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void PushGeometryClip(IGeometryImpl? clip) |
|||
{ |
|||
if (clip is null) |
|||
return; |
|||
|
|||
var next = NextDrawAs<GeometryClipNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(Transform, clip)) |
|||
{ |
|||
Add(new GeometryClipNode(Transform, clip)); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationIndex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void PushOpacity(double opacity) |
|||
{ |
|||
var next = NextDrawAs<OpacityNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(opacity)) |
|||
{ |
|||
Add(new OpacityNode(opacity)); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationIndex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void PushOpacityMask(IBrush mask, Rect bounds) |
|||
{ |
|||
var next = NextDrawAs<OpacityMaskNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(mask, bounds)) |
|||
{ |
|||
Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask))); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationIndex; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) |
|||
{ |
|||
var next = NextDrawAs<BitmapBlendModeNode>(); |
|||
|
|||
if (next == null || !next.Item.Equals(blendingMode)) |
|||
{ |
|||
Add(new BitmapBlendModeNode(blendingMode)); |
|||
} |
|||
else |
|||
{ |
|||
++_drawOperationIndex; |
|||
} |
|||
} |
|||
|
|||
private void Add<T>(T node) where T : class, IDrawOperation |
|||
{ |
|||
if (_drawOperationIndex < _builder.Count) |
|||
{ |
|||
_builder.ReplaceDrawOperation(_drawOperationIndex, node); |
|||
} |
|||
else |
|||
{ |
|||
_builder.AddDrawOperation(node); |
|||
} |
|||
|
|||
++_drawOperationIndex; |
|||
} |
|||
|
|||
private IRef<T>? NextDrawAs<T>() where T : class, IDrawOperation |
|||
{ |
|||
return _drawOperationIndex < _builder.Count |
|||
? _builder.DrawOperations![_drawOperationIndex] as IRef<T> |
|||
: null; |
|||
} |
|||
|
|||
private IDisposable? CreateChildScene(IBrush? brush) |
|||
{ |
|||
if (brush is VisualBrush visualBrush) |
|||
{ |
|||
var visual = visualBrush.Visual; |
|||
|
|||
if (visual != null) |
|||
{ |
|||
// TODO: This is a temporary solution to make visual brush to work like it does with DeferredRenderer
|
|||
// We should directly reference the corresponding CompositionVisual (which should
|
|||
// be attached to the same composition target) like UWP does.
|
|||
// Render-able visuals shouldn't be dangling unattached
|
|||
(visual as IVisualBrushInitialize)?.EnsureInitialized(); |
|||
|
|||
var recorder = new CompositionDrawingContext(); |
|||
recorder.BeginUpdate(null); |
|||
ImmediateRenderer.Render(visual, new DrawingContext(recorder)); |
|||
var drawList = recorder.EndUpdate(); |
|||
drawList.Size = visual.Bounds.Size; |
|||
|
|||
return drawList; |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
namespace Avalonia.Rendering.Composition; |
|||
|
|||
/// <summary>
|
|||
/// Enables access to composition visual objects that back XAML elements in the XAML composition tree.
|
|||
/// </summary>
|
|||
public static class ElementComposition |
|||
{ |
|||
/// <summary>
|
|||
/// Gets CompositionVisual that backs a Visual
|
|||
/// </summary>
|
|||
/// <param name="visual"></param>
|
|||
/// <returns></returns>
|
|||
public static CompositionVisual? GetElementVisual(Visual visual) => visual.CompositionVisual; |
|||
} |
|||
@ -0,0 +1,120 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Rendering.Composition |
|||
{ |
|||
public enum CompositionBlendMode |
|||
{ |
|||
/// <summary>No regions are enabled. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_clr.svg)</summary>
|
|||
Clear, |
|||
|
|||
/// <summary>Only the source will be present. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src.svg)</summary>
|
|||
Src, |
|||
|
|||
/// <summary>Only the destination will be present. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst.svg)</summary>
|
|||
Dst, |
|||
|
|||
/// <summary>Source is placed over the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-over.svg)</summary>
|
|||
SrcOver, |
|||
|
|||
/// <summary>Destination is placed over the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-over.svg)</summary>
|
|||
DstOver, |
|||
|
|||
/// <summary>The source that overlaps the destination, replaces the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-in.svg)</summary>
|
|||
SrcIn, |
|||
|
|||
/// <summary>Destination which overlaps the source, replaces the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-in.svg)</summary>
|
|||
DstIn, |
|||
|
|||
/// <summary>Source is placed, where it falls outside of the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-out.svg)</summary>
|
|||
SrcOut, |
|||
|
|||
/// <summary>Destination is placed, where it falls outside of the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-out.svg)</summary>
|
|||
DstOut, |
|||
|
|||
/// <summary>Source which overlaps the destination, replaces the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-atop.svg)</summary>
|
|||
SrcATop, |
|||
|
|||
/// <summary>Destination which overlaps the source replaces the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-atop.svg)</summary>
|
|||
DstATop, |
|||
|
|||
/// <summary>The non-overlapping regions of source and destination are combined. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_xor.svg)</summary>
|
|||
Xor, |
|||
|
|||
/// <summary>Display the sum of the source image and destination image. [Porter Duff Compositing Operators]</summary>
|
|||
Plus, |
|||
|
|||
/// <summary>Multiplies all components (= alpha and color). [Separable Blend Modes]</summary>
|
|||
Modulate, |
|||
|
|||
/// <summary>Multiplies the complements of the backdrop and source CompositionColorvalues, then complements the result. [Separable Blend Modes]</summary>
|
|||
Screen, |
|||
|
|||
/// <summary>Multiplies or screens the colors, depending on the backdrop CompositionColorvalue. [Separable Blend Modes]</summary>
|
|||
Overlay, |
|||
|
|||
/// <summary>Selects the darker of the backdrop and source colors. [Separable Blend Modes]</summary>
|
|||
Darken, |
|||
|
|||
/// <summary>Selects the lighter of the backdrop and source colors. [Separable Blend Modes]</summary>
|
|||
Lighten, |
|||
|
|||
/// <summary>Brightens the backdrop CompositionColorto reflect the source color. [Separable Blend Modes]</summary>
|
|||
ColorDodge, |
|||
|
|||
/// <summary>Darkens the backdrop CompositionColorto reflect the source color. [Separable Blend Modes]</summary>
|
|||
ColorBurn, |
|||
|
|||
/// <summary>Multiplies or screens the colors, depending on the source CompositionColorvalue. [Separable Blend Modes]</summary>
|
|||
HardLight, |
|||
|
|||
/// <summary>Darkens or lightens the colors, depending on the source CompositionColorvalue. [Separable Blend Modes]</summary>
|
|||
SoftLight, |
|||
|
|||
/// <summary>Subtracts the darker of the two constituent colors from the lighter color. [Separable Blend Modes]</summary>
|
|||
Difference, |
|||
|
|||
/// <summary>Produces an effect similar to that of the Difference mode but lower in contrast. [Separable Blend Modes]</summary>
|
|||
Exclusion, |
|||
|
|||
/// <summary>The source CompositionColoris multiplied by the destination CompositionColorand replaces the destination [Separable Blend Modes]</summary>
|
|||
Multiply, |
|||
|
|||
/// <summary>Creates a CompositionColorwith the hue of the source CompositionColorand the saturation and luminosity of the backdrop color. [Non-Separable Blend Modes]</summary>
|
|||
Hue, |
|||
|
|||
/// <summary>Creates a CompositionColorwith the saturation of the source CompositionColorand the hue and luminosity of the backdrop color. [Non-Separable Blend Modes]</summary>
|
|||
Saturation, |
|||
|
|||
/// <summary>Creates a CompositionColorwith the hue and saturation of the source CompositionColorand the luminosity of the backdrop color. [Non-Separable Blend Modes]</summary>
|
|||
Color, |
|||
|
|||
/// <summary>Creates a CompositionColorwith the luminosity of the source CompositionColorand the hue and saturation of the backdrop color. [Non-Separable Blend Modes]</summary>
|
|||
Luminosity, |
|||
} |
|||
|
|||
public enum CompositionGradientExtendMode |
|||
{ |
|||
Clamp, |
|||
Wrap, |
|||
Mirror |
|||
} |
|||
|
|||
[Flags] |
|||
public enum CompositionTileMode |
|||
{ |
|||
None = 0, |
|||
TileX = 1, |
|||
TileY = 2, |
|||
FlipX = 4, |
|||
FlipY = 8, |
|||
Tile = TileX | TileY, |
|||
Flip = FlipX | FlipY |
|||
} |
|||
|
|||
public enum CompositionStretch |
|||
{ |
|||
None = 0, |
|||
Fill = 1, |
|||
//TODO: Uniform, UniformToFill
|
|||
} |
|||
} |
|||
@ -0,0 +1,237 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Numerics; |
|||
using Avalonia.Rendering.Composition.Animations; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Expressions |
|||
{ |
|||
/// <summary>
|
|||
/// Built-in functions for Foreign Function Interface available from composition animation expressions
|
|||
/// </summary>
|
|||
internal class BuiltInExpressionFfi : IExpressionForeignFunctionInterface |
|||
{ |
|||
private readonly DelegateExpressionFfi _registry; |
|||
|
|||
static float Lerp(float a, float b, float p) => p * (b - a) + a; |
|||
|
|||
static Matrix3x2 Inverse(Matrix3x2 m) |
|||
{ |
|||
Matrix3x2.Invert(m, out var r); |
|||
return r; |
|||
} |
|||
|
|||
static Matrix4x4 Inverse(Matrix4x4 m) |
|||
{ |
|||
Matrix4x4.Invert(m, out var r); |
|||
return r; |
|||
} |
|||
|
|||
static float SmoothStep(float edge0, float edge1, float x) |
|||
{ |
|||
var t = MathUtilities.Clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); |
|||
return t * t * (3.0f - 2.0f * t); |
|||
} |
|||
|
|||
static Vector2 SmoothStep(Vector2 edge0, Vector2 edge1, Vector2 x) |
|||
{ |
|||
return new Vector2( |
|||
SmoothStep(edge0.X, edge1.X, x.X), |
|||
SmoothStep(edge0.Y, edge1.Y, x.Y) |
|||
|
|||
); |
|||
} |
|||
static Vector3 SmoothStep(Vector3 edge0, Vector3 edge1, Vector3 x) |
|||
{ |
|||
return new Vector3( |
|||
SmoothStep(edge0.X, edge1.X, x.X), |
|||
SmoothStep(edge0.Y, edge1.Y, x.Y), |
|||
SmoothStep(edge0.Z, edge1.Z, x.Z) |
|||
|
|||
); |
|||
} |
|||
|
|||
static Vector4 SmoothStep(Vector4 edge0, Vector4 edge1, Vector4 x) |
|||
{ |
|||
return new Vector4( |
|||
SmoothStep(edge0.X, edge1.X, x.X), |
|||
SmoothStep(edge0.Y, edge1.Y, x.Y), |
|||
SmoothStep(edge0.Z, edge1.Z, x.Z), |
|||
SmoothStep(edge0.W, edge1.W, x.W) |
|||
); |
|||
} |
|||
|
|||
private BuiltInExpressionFfi() |
|||
{ |
|||
_registry = new DelegateExpressionFfi |
|||
{ |
|||
{"Abs", (float f) => Math.Abs(f)}, |
|||
{"Abs", (Vector2 v) => Vector2.Abs(v)}, |
|||
{"Abs", (Vector3 v) => Vector3.Abs(v)}, |
|||
{"Abs", (Vector4 v) => Vector4.Abs(v)}, |
|||
|
|||
{"ACos", (float f) => (float) Math.Acos(f)}, |
|||
{"ASin", (float f) => (float) Math.Asin(f)}, |
|||
{"ATan", (float f) => (float) Math.Atan(f)}, |
|||
{"Ceil", (float f) => (float) Math.Ceiling(f)}, |
|||
|
|||
{"Clamp", (float a1, float a2, float a3) => MathUtilities.Clamp(a1, a2, a3)}, |
|||
{"Clamp", (Vector2 a1, Vector2 a2, Vector2 a3) => Vector2.Clamp(a1, a2, a3)}, |
|||
{"Clamp", (Vector3 a1, Vector3 a2, Vector3 a3) => Vector3.Clamp(a1, a2, a3)}, |
|||
{"Clamp", (Vector4 a1, Vector4 a2, Vector4 a3) => Vector4.Clamp(a1, a2, a3)}, |
|||
|
|||
{"Concatenate", (Quaternion a1, Quaternion a2) => Quaternion.Concatenate(a1, a2)}, |
|||
{"Cos", (float a) => (float) Math.Cos(a)}, |
|||
|
|||
/* |
|||
TODO: |
|||
ColorHsl(Float h, Float s, Float l) |
|||
ColorLerpHSL(Color colorTo, CompositionColorcolorFrom, Float progress) |
|||
*/ |
|||
|
|||
{ |
|||
"ColorLerp", (Avalonia.Media.Color to, Avalonia.Media.Color from, float progress) => |
|||
ColorInterpolator.LerpRGB(to, from, progress) |
|||
}, |
|||
{ |
|||
"ColorLerpRGB", (Avalonia.Media.Color to, Avalonia.Media.Color from, float progress) => |
|||
ColorInterpolator.LerpRGB(to, from, progress) |
|||
}, |
|||
{ |
|||
"ColorRGB", (float a, float r, float g, float b) => Avalonia.Media.Color.FromArgb( |
|||
(byte) MathUtilities.Clamp(a, 0, 255), |
|||
(byte) MathUtilities.Clamp(r, 0, 255), |
|||
(byte) MathUtilities.Clamp(g, 0, 255), |
|||
(byte) MathUtilities.Clamp(b, 0, 255) |
|||
) |
|||
}, |
|||
|
|||
{"Distance", (Vector2 a1, Vector2 a2) => Vector2.Distance(a1, a2)}, |
|||
{"Distance", (Vector3 a1, Vector3 a2) => Vector3.Distance(a1, a2)}, |
|||
{"Distance", (Vector4 a1, Vector4 a2) => Vector4.Distance(a1, a2)}, |
|||
|
|||
{"DistanceSquared", (Vector2 a1, Vector2 a2) => Vector2.DistanceSquared(a1, a2)}, |
|||
{"DistanceSquared", (Vector3 a1, Vector3 a2) => Vector3.DistanceSquared(a1, a2)}, |
|||
{"DistanceSquared", (Vector4 a1, Vector4 a2) => Vector4.DistanceSquared(a1, a2)}, |
|||
|
|||
{"Floor", (float v) => (float) Math.Floor(v)}, |
|||
|
|||
{"Inverse", (Matrix3x2 v) => Inverse(v)}, |
|||
{"Inverse", (Matrix4x4 v) => Inverse(v)}, |
|||
|
|||
|
|||
{"Length", (Vector2 a1) => a1.Length()}, |
|||
{"Length", (Vector3 a1) => a1.Length()}, |
|||
{"Length", (Vector4 a1) => a1.Length()}, |
|||
{"Length", (Quaternion a1) => a1.Length()}, |
|||
|
|||
{"LengthSquared", (Vector2 a1) => a1.LengthSquared()}, |
|||
{"LengthSquared", (Vector3 a1) => a1.LengthSquared()}, |
|||
{"LengthSquared", (Vector4 a1) => a1.LengthSquared()}, |
|||
{"LengthSquared", (Quaternion a1) => a1.LengthSquared()}, |
|||
|
|||
{"Lerp", (float a1, float a2, float a3) => Lerp(a1, a2, a3)}, |
|||
{"Lerp", (Vector2 a1, Vector2 a2, float a3) => Vector2.Lerp(a1, a2, a3)}, |
|||
{"Lerp", (Vector3 a1, Vector3 a2, float a3) => Vector3.Lerp(a1, a2, a3)}, |
|||
{"Lerp", (Vector4 a1, Vector4 a2, float a3) => Vector4.Lerp(a1, a2, a3)}, |
|||
|
|||
|
|||
{"Ln", (float f) => (float) Math.Log(f)}, |
|||
{"Log10", (float f) => (float) Math.Log10(f)}, |
|||
|
|||
{"Matrix3x2.CreateFromScale", (Vector2 v) => Matrix3x2.CreateScale(v)}, |
|||
{"Matrix3x2.CreateFromTranslation", (Vector2 v) => Matrix3x2.CreateTranslation(v)}, |
|||
{"Matrix3x2.CreateRotation", (float v) => Matrix3x2.CreateRotation(v)}, |
|||
{"Matrix3x2.CreateScale", (Vector2 v) => Matrix3x2.CreateScale(v)}, |
|||
{"Matrix3x2.CreateSkew", (float a1, float a2, Vector2 a3) => Matrix3x2.CreateSkew(a1, a2, a3)}, |
|||
{"Matrix3x2.CreateTranslation", (Vector2 v) => Matrix3x2.CreateScale(v)}, |
|||
{ |
|||
"Matrix3x2", (float m11, float m12, float m21, float m22, float m31, float m32) => |
|||
new Matrix3x2(m11, m12, m21, m22, m31, m32) |
|||
}, |
|||
{"Matrix4x4.CreateFromAxisAngle", (Vector3 v, float angle) => Matrix4x4.CreateFromAxisAngle(v, angle)}, |
|||
{"Matrix4x4.CreateFromScale", (Vector3 v) => Matrix4x4.CreateScale(v)}, |
|||
{"Matrix4x4.CreateFromTranslation", (Vector3 v) => Matrix4x4.CreateTranslation(v)}, |
|||
{"Matrix4x4.CreateScale", (Vector3 v) => Matrix4x4.CreateScale(v)}, |
|||
{"Matrix4x4.CreateTranslation", (Vector3 v) => Matrix4x4.CreateScale(v)}, |
|||
{"Matrix4x4", (Matrix3x2 m) => new Matrix4x4(m)}, |
|||
{ |
|||
"Matrix4x4", |
|||
(float m11, float m12, float m13, float m14, |
|||
float m21, float m22, float m23, float m24, |
|||
float m31, float m32, float m33, float m34, |
|||
float m41, float m42, float m43, float m44) => |
|||
new Matrix4x4( |
|||
m11, m12, m13, m14, |
|||
m21, m22, m23, m24, |
|||
m31, m32, m33, m34, |
|||
m41, m42, m43, m44) |
|||
}, |
|||
|
|||
|
|||
{"Max", (float a1, float a2) => Math.Max(a1, a2)}, |
|||
{"Max", (Vector2 a1, Vector2 a2) => Vector2.Max(a1, a2)}, |
|||
{"Max", (Vector3 a1, Vector3 a2) => Vector3.Max(a1, a2)}, |
|||
{"Max", (Vector4 a1, Vector4 a2) => Vector4.Max(a1, a2)}, |
|||
|
|||
|
|||
{"Min", (float a1, float a2) => Math.Min(a1, a2)}, |
|||
{"Min", (Vector2 a1, Vector2 a2) => Vector2.Min(a1, a2)}, |
|||
{"Min", (Vector3 a1, Vector3 a2) => Vector3.Min(a1, a2)}, |
|||
{"Min", (Vector4 a1, Vector4 a2) => Vector4.Min(a1, a2)}, |
|||
|
|||
{"Mod", (float a, float b) => a % b}, |
|||
|
|||
{"Normalize", (Quaternion a) => Quaternion.Normalize(a)}, |
|||
{"Normalize", (Vector2 a) => Vector2.Normalize(a)}, |
|||
{"Normalize", (Vector3 a) => Vector3.Normalize(a)}, |
|||
{"Normalize", (Vector4 a) => Vector4.Normalize(a)}, |
|||
|
|||
{"Pow", (float a, float b) => (float) Math.Pow(a, b)}, |
|||
{"Quaternion.CreateFromAxisAngle", (Vector3 a, float b) => Quaternion.CreateFromAxisAngle(a, b)}, |
|||
{"Quaternion", (float a, float b, float c, float d) => new Quaternion(a, b, c, d)}, |
|||
|
|||
{"Round", (float a) => (float) Math.Round(a)}, |
|||
|
|||
{"Scale", (Matrix3x2 a, float b) => a * b}, |
|||
{"Scale", (Matrix4x4 a, float b) => a * b}, |
|||
{"Scale", (Vector2 a, float b) => a * b}, |
|||
{"Scale", (Vector3 a, float b) => a * b}, |
|||
{"Scale", (Vector4 a, float b) => a * b}, |
|||
|
|||
{"Sin", (float a) => (float) Math.Sin(a)}, |
|||
|
|||
{"SmoothStep", (float a1, float a2, float a3) => SmoothStep(a1, a2, a3)}, |
|||
{"SmoothStep", (Vector2 a1, Vector2 a2, Vector2 a3) => SmoothStep(a1, a2, a3)}, |
|||
{"SmoothStep", (Vector3 a1, Vector3 a2, Vector3 a3) => SmoothStep(a1, a2, a3)}, |
|||
{"SmoothStep", (Vector4 a1, Vector4 a2, Vector4 a3) => SmoothStep(a1, a2, a3)}, |
|||
|
|||
// I have no idea how to do a spherical interpolation for a scalar value, so we are doing a linear one
|
|||
{"Slerp", (float a1, float a2, float a3) => Lerp(a1, a2, a3)}, |
|||
{"Slerp", (Quaternion a1, Quaternion a2, float a3) => Quaternion.Slerp(a1, a2, a3)}, |
|||
|
|||
{"Sqrt", (float a) => (float) Math.Sqrt(a)}, |
|||
{"Square", (float a) => a * a}, |
|||
{"Tan", (float a) => (float) Math.Tan(a)}, |
|||
|
|||
{"ToRadians", (float a) => (float) (a * Math.PI / 180)}, |
|||
{"ToDegrees", (float a) => (float) (a * 180d / Math.PI)}, |
|||
|
|||
{"Transform", (Vector2 a, Matrix3x2 b) => Vector2.Transform(a, b)}, |
|||
{"Transform", (Vector3 a, Matrix4x4 b) => Vector3.Transform(a, b)}, |
|||
|
|||
{"Vector2", (float a, float b) => new Vector2(a, b)}, |
|||
{"Vector3", (float a, float b, float c) => new Vector3(a, b, c)}, |
|||
{"Vector3", (Vector2 v2, float z) => new Vector3(v2, z)}, |
|||
{"Vector4", (float a, float b, float c, float d) => new Vector4(a, b, c, d)}, |
|||
{"Vector4", (Vector2 v2, float z, float w) => new Vector4(v2, z, w)}, |
|||
{"Vector4", (Vector3 v3, float w) => new Vector4(v3, w)}, |
|||
}; |
|||
} |
|||
|
|||
public bool Call(string name, IReadOnlyList<ExpressionVariant> arguments, out ExpressionVariant result) => |
|||
_registry.Call(name, arguments, out result); |
|||
|
|||
public static BuiltInExpressionFfi Instance { get; } = new BuiltInExpressionFfi(); |
|||
} |
|||
} |
|||
@ -0,0 +1,184 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Numerics; |
|||
using Avalonia.Media; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Expressions |
|||
{ |
|||
/// <summary>
|
|||
/// Foreign function interface for composition animations based on calling delegates
|
|||
/// </summary>
|
|||
internal class DelegateExpressionFfi : IExpressionForeignFunctionInterface, IEnumerable |
|||
{ |
|||
struct FfiRecord |
|||
{ |
|||
public VariantType[] Types; |
|||
public Func<IReadOnlyList<ExpressionVariant>, ExpressionVariant> Delegate; |
|||
} |
|||
|
|||
private readonly Dictionary<string, Dictionary<int, List<FfiRecord>>> |
|||
_registry = new Dictionary<string, Dictionary<int, List<FfiRecord>>>(); |
|||
|
|||
public bool Call(string name, IReadOnlyList<ExpressionVariant> arguments, out ExpressionVariant result) |
|||
{ |
|||
result = default; |
|||
if (!_registry.TryGetValue(name, out var nameGroup)) |
|||
return false; |
|||
if (!nameGroup.TryGetValue(arguments.Count, out var countGroup)) |
|||
return false; |
|||
foreach (var record in countGroup) |
|||
{ |
|||
var match = true; |
|||
for (var c = 0; c < arguments.Count; c++) |
|||
{ |
|||
if (record.Types[c] != arguments[c].Type) |
|||
{ |
|||
match = false; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (match) |
|||
{ |
|||
result = record.Delegate(arguments); |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
// Stub for collection initializer
|
|||
IEnumerator IEnumerable.GetEnumerator() => Array.Empty<object>().GetEnumerator(); |
|||
|
|||
void Add(string name, Func<IReadOnlyList<ExpressionVariant>, ExpressionVariant> cb, |
|||
params Type[] types) |
|||
{ |
|||
if (!_registry.TryGetValue(name, out var nameGroup)) |
|||
_registry[name] = nameGroup = |
|||
new Dictionary<int, List<FfiRecord>>(); |
|||
if (!nameGroup.TryGetValue(types.Length, out var countGroup)) |
|||
nameGroup[types.Length] = countGroup = new List<FfiRecord>(); |
|||
|
|||
countGroup.Add(new FfiRecord |
|||
{ |
|||
Types = types.Select(t => TypeMap[t]).ToArray(), |
|||
Delegate = cb |
|||
}); |
|||
} |
|||
|
|||
static readonly Dictionary<Type, VariantType> TypeMap = new Dictionary<Type, VariantType> |
|||
{ |
|||
[typeof(bool)] = VariantType.Boolean, |
|||
[typeof(float)] = VariantType.Scalar, |
|||
[typeof(Vector2)] = VariantType.Vector2, |
|||
[typeof(Vector3)] = VariantType.Vector3, |
|||
[typeof(Vector4)] = VariantType.Vector4, |
|||
[typeof(Matrix3x2)] = VariantType.Matrix3x2, |
|||
[typeof(Matrix4x4)] = VariantType.Matrix4x4, |
|||
[typeof(Quaternion)] = VariantType.Quaternion, |
|||
[typeof(Color)] = VariantType.Color |
|||
}; |
|||
|
|||
public void Add<T1>(string name, Func<T1, ExpressionVariant> cb) where T1 : struct |
|||
{ |
|||
Add(name, args => cb(args[0].CastOrDefault<T1>()), typeof(T1)); |
|||
} |
|||
|
|||
public void Add<T1, T2>(string name, Func<T1, T2, ExpressionVariant> cb) where T1 : struct where T2 : struct |
|||
{ |
|||
Add(name, args => cb(args[0].CastOrDefault<T1>(), args[1].CastOrDefault<T2>()), typeof(T1), typeof(T2)); |
|||
} |
|||
|
|||
|
|||
public void Add<T1, T2, T3>(string name, Func<T1, T2, T3, ExpressionVariant> cb) |
|||
where T1 : struct where T2 : struct where T3 : struct |
|||
{ |
|||
Add(name, args => cb(args[0].CastOrDefault<T1>(), args[1].CastOrDefault<T2>(), args[2].CastOrDefault<T3>()), typeof(T1), typeof(T2), |
|||
typeof(T3)); |
|||
} |
|||
|
|||
public void Add<T1, T2, T3, T4>(string name, Func<T1, T2, T3, T4, ExpressionVariant> cb) |
|||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct |
|||
{ |
|||
Add(name, args => cb( |
|||
args[0].CastOrDefault<T1>(), |
|||
args[1].CastOrDefault<T2>(), |
|||
args[2].CastOrDefault<T3>(), |
|||
args[3].CastOrDefault<T4>()), |
|||
typeof(T1), typeof(T2), typeof(T3), typeof(T4)); |
|||
} |
|||
|
|||
public void Add<T1, T2, T3, T4, T5>(string name, Func<T1, T2, T3, T4, T5, ExpressionVariant> cb) |
|||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct |
|||
{ |
|||
Add(name, args => cb( |
|||
args[0].CastOrDefault<T1>(), |
|||
args[1].CastOrDefault<T2>(), |
|||
args[2].CastOrDefault<T3>(), |
|||
args[3].CastOrDefault<T4>(), |
|||
args[4].CastOrDefault<T5>()), |
|||
typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5)); |
|||
} |
|||
|
|||
public void Add<T1, T2, T3, T4, T5, T6>(string name, Func<T1, T2, T3, T4, T5, T6, ExpressionVariant> cb) |
|||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct where T6 : struct |
|||
{ |
|||
Add(name, args => cb( |
|||
args[0].CastOrDefault<T1>(), |
|||
args[1].CastOrDefault<T2>(), |
|||
args[2].CastOrDefault<T3>(), |
|||
args[3].CastOrDefault<T4>(), |
|||
args[4].CastOrDefault<T5>(), |
|||
args[4].CastOrDefault<T6>()), |
|||
typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6)); |
|||
} |
|||
|
|||
|
|||
public void Add<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>(string name, |
|||
Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, ExpressionVariant> cb) |
|||
where T1 : struct |
|||
where T2 : struct |
|||
where T3 : struct |
|||
where T4 : struct |
|||
where T5 : struct |
|||
where T6 : struct |
|||
where T7 : struct |
|||
where T8 : struct |
|||
where T9 : struct |
|||
where T10 : struct |
|||
where T11 : struct |
|||
where T12 : struct |
|||
where T13 : struct |
|||
where T14 : struct |
|||
where T15 : struct |
|||
where T16 : struct |
|||
{ |
|||
Add(name, args => cb( |
|||
args[0].CastOrDefault<T1>(), |
|||
args[1].CastOrDefault<T2>(), |
|||
args[2].CastOrDefault<T3>(), |
|||
args[3].CastOrDefault<T4>(), |
|||
args[4].CastOrDefault<T5>(), |
|||
args[4].CastOrDefault<T6>(), |
|||
args[4].CastOrDefault<T7>(), |
|||
args[4].CastOrDefault<T8>(), |
|||
args[4].CastOrDefault<T9>(), |
|||
args[4].CastOrDefault<T10>(), |
|||
args[4].CastOrDefault<T11>(), |
|||
args[4].CastOrDefault<T12>(), |
|||
args[4].CastOrDefault<T13>(), |
|||
args[4].CastOrDefault<T14>(), |
|||
args[4].CastOrDefault<T15>(), |
|||
args[4].CastOrDefault<T16>() |
|||
), |
|||
typeof(T1), typeof(T2), typeof(T3), typeof(T4), |
|||
typeof(T5), typeof(T6), typeof(T7), typeof(T8), |
|||
typeof(T9), typeof(T10), typeof(T11), typeof(T12), |
|||
typeof(T13), typeof(T14), typeof(T15), typeof(T16) |
|||
); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,377 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using System.Reflection; |
|||
using Avalonia.Rendering.Composition.Server; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Expressions |
|||
{ |
|||
/// <summary>
|
|||
/// A parsed composition expression
|
|||
/// </summary>
|
|||
internal abstract class Expression |
|||
{ |
|||
public abstract ExpressionType Type { get; } |
|||
public static Expression Parse(string expression) |
|||
{ |
|||
return ExpressionParser.Parse(expression.AsSpan()); |
|||
} |
|||
|
|||
public abstract ExpressionVariant Evaluate(ref ExpressionEvaluationContext context); |
|||
|
|||
public virtual void CollectReferences(HashSet<(string parameter, string property)> references) |
|||
{ |
|||
|
|||
} |
|||
|
|||
protected abstract string Print(); |
|||
public override string ToString() => Print(); |
|||
|
|||
internal static string OperatorName(ExpressionType t) |
|||
{ |
|||
var attr = typeof(ExpressionType).GetMember(t.ToString())[0] |
|||
.GetCustomAttribute<PrettyPrintStringAttribute>(); |
|||
if (attr != null) |
|||
return attr.Name; |
|||
return t.ToString(); |
|||
} |
|||
} |
|||
|
|||
internal class PrettyPrintStringAttribute : Attribute |
|||
{ |
|||
public string Name { get; } |
|||
|
|||
public PrettyPrintStringAttribute(string name) |
|||
{ |
|||
Name = name; |
|||
} |
|||
} |
|||
|
|||
internal enum ExpressionType |
|||
{ |
|||
// Binary operators
|
|||
[PrettyPrintString("+")] |
|||
Add, |
|||
[PrettyPrintString("-")] |
|||
Subtract, |
|||
[PrettyPrintString("/")] |
|||
Divide, |
|||
[PrettyPrintString("*")] |
|||
Multiply, |
|||
[PrettyPrintString(">")] |
|||
MoreThan, |
|||
[PrettyPrintString("<")] |
|||
LessThan, |
|||
[PrettyPrintString(">=")] |
|||
MoreThanOrEqual, |
|||
[PrettyPrintString("<=")] |
|||
LessThanOrEqual, |
|||
[PrettyPrintString("&&")] |
|||
LogicalAnd, |
|||
[PrettyPrintString("||")] |
|||
LogicalOr, |
|||
[PrettyPrintString("%")] |
|||
Remainder, |
|||
[PrettyPrintString("==")] |
|||
Equals, |
|||
[PrettyPrintString("!=")] |
|||
NotEquals, |
|||
// Unary operators
|
|||
[PrettyPrintString("!")] |
|||
Not, |
|||
[PrettyPrintString("-")] |
|||
UnaryMinus, |
|||
// The rest
|
|||
MemberAccess, |
|||
Parameter, |
|||
FunctionCall, |
|||
Keyword, |
|||
Constant, |
|||
ConditionalExpression |
|||
} |
|||
|
|||
internal enum ExpressionKeyword |
|||
{ |
|||
StartingValue, |
|||
CurrentValue, |
|||
FinalValue, |
|||
Target, |
|||
Pi, |
|||
True, |
|||
False |
|||
} |
|||
|
|||
internal class ConditionalExpression : Expression |
|||
{ |
|||
public Expression Condition { get; } |
|||
public Expression TruePart { get; } |
|||
public Expression FalsePart { get; } |
|||
public override ExpressionType Type => ExpressionType.ConditionalExpression; |
|||
|
|||
public ConditionalExpression(Expression condition, Expression truePart, Expression falsePart) |
|||
{ |
|||
Condition = condition; |
|||
TruePart = truePart; |
|||
FalsePart = falsePart; |
|||
} |
|||
|
|||
public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) |
|||
{ |
|||
var cond = Condition.Evaluate(ref context); |
|||
if (cond.Type == VariantType.Boolean && cond.Boolean) |
|||
return TruePart.Evaluate(ref context); |
|||
return FalsePart.Evaluate(ref context); |
|||
} |
|||
|
|||
public override void CollectReferences(HashSet<(string parameter, string property)> references) |
|||
{ |
|||
Condition.CollectReferences(references); |
|||
TruePart.CollectReferences(references); |
|||
FalsePart.CollectReferences(references); |
|||
} |
|||
|
|||
protected override string Print() => $"({Condition}) ? ({TruePart}) : ({FalsePart})"; |
|||
} |
|||
|
|||
internal class ConstantExpression : Expression |
|||
{ |
|||
public float Constant { get; } |
|||
public override ExpressionType Type => ExpressionType.Constant; |
|||
|
|||
public ConstantExpression(float constant) |
|||
{ |
|||
Constant = constant; |
|||
} |
|||
|
|||
public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) => Constant; |
|||
|
|||
protected override string Print() => Constant.ToString(CultureInfo.InvariantCulture); |
|||
} |
|||
|
|||
internal class FunctionCallExpression : Expression |
|||
{ |
|||
public string Name { get; } |
|||
public List<Expression> Parameters { get; } |
|||
public override ExpressionType Type => ExpressionType.FunctionCall; |
|||
|
|||
public FunctionCallExpression(string name, List<Expression> parameters) |
|||
{ |
|||
Name = name; |
|||
Parameters = parameters; |
|||
} |
|||
|
|||
public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) |
|||
{ |
|||
if (context.ForeignFunctionInterface == null) |
|||
return default; |
|||
var args = new List<ExpressionVariant>(); |
|||
foreach (var expr in Parameters) |
|||
args.Add(expr.Evaluate(ref context)); |
|||
if (!context.ForeignFunctionInterface.Call(Name, args, out var res)) |
|||
return default; |
|||
return res; |
|||
} |
|||
|
|||
public override void CollectReferences(HashSet<(string parameter, string property)> references) |
|||
{ |
|||
foreach(var arg in Parameters) |
|||
arg.CollectReferences(references); |
|||
} |
|||
|
|||
protected override string Print() |
|||
{ |
|||
return Name + "( (" + string.Join("), (", Parameters) + ") )"; |
|||
} |
|||
} |
|||
|
|||
internal class MemberAccessExpression : Expression |
|||
{ |
|||
public override ExpressionType Type => ExpressionType.MemberAccess; |
|||
public Expression Target { get; } |
|||
public string Member { get; } |
|||
|
|||
public MemberAccessExpression(Expression target, string member) |
|||
{ |
|||
Target = target; |
|||
Member = string.Intern(member); |
|||
} |
|||
|
|||
public override void CollectReferences(HashSet<(string parameter, string property)> references) |
|||
{ |
|||
Target.CollectReferences(references); |
|||
if (Target is ParameterExpression pe) |
|||
references.Add((pe.Name, Member)); |
|||
} |
|||
|
|||
public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) |
|||
{ |
|||
if (Target is KeywordExpression ke |
|||
&& ke.Keyword == ExpressionKeyword.Target) |
|||
{ |
|||
return context.Target.GetProperty(Member); |
|||
} |
|||
|
|||
if (Target is ParameterExpression pe) |
|||
{ |
|||
var obj = context.Parameters?.GetObjectParameter(pe.Name); |
|||
if (obj != null) |
|||
{ |
|||
return obj.GetProperty(Member); |
|||
} |
|||
} |
|||
// Those are considered immutable
|
|||
return Target.Evaluate(ref context).GetProperty(Member); |
|||
} |
|||
|
|||
protected override string Print() |
|||
{ |
|||
return "(" + Target.ToString() + ")." + Member; |
|||
} |
|||
} |
|||
|
|||
internal class ParameterExpression : Expression |
|||
{ |
|||
public string Name { get; } |
|||
public override ExpressionType Type => ExpressionType.Parameter; |
|||
|
|||
public ParameterExpression(string name) |
|||
{ |
|||
Name = name; |
|||
} |
|||
public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) |
|||
{ |
|||
return context.Parameters?.GetParameter(Name) ?? default; |
|||
} |
|||
|
|||
protected override string Print() |
|||
{ |
|||
return "{" + Name + "}"; |
|||
} |
|||
} |
|||
|
|||
internal class KeywordExpression : Expression |
|||
{ |
|||
public override ExpressionType Type => ExpressionType.Keyword; |
|||
public ExpressionKeyword Keyword { get; } |
|||
|
|||
public KeywordExpression(ExpressionKeyword keyword) |
|||
{ |
|||
Keyword = keyword; |
|||
} |
|||
|
|||
public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) |
|||
{ |
|||
if (Keyword == ExpressionKeyword.StartingValue) |
|||
return context.StartingValue; |
|||
if (Keyword == ExpressionKeyword.CurrentValue) |
|||
return context.CurrentValue; |
|||
if (Keyword == ExpressionKeyword.FinalValue) |
|||
return context.FinalValue; |
|||
if (Keyword == ExpressionKeyword.Target) |
|||
// should be handled by MemberAccess
|
|||
return default; |
|||
if (Keyword == ExpressionKeyword.True) |
|||
return true; |
|||
if (Keyword == ExpressionKeyword.False) |
|||
return false; |
|||
if (Keyword == ExpressionKeyword.Pi) |
|||
return (float) Math.PI; |
|||
return default; |
|||
} |
|||
|
|||
protected override string Print() |
|||
{ |
|||
return "[" + Keyword + "]"; |
|||
} |
|||
} |
|||
|
|||
internal class UnaryExpression : Expression |
|||
{ |
|||
public Expression Parameter { get; } |
|||
public override ExpressionType Type { get; } |
|||
public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) |
|||
{ |
|||
if (Type == ExpressionType.Not) |
|||
return !Parameter.Evaluate(ref context); |
|||
if (Type == ExpressionType.UnaryMinus) |
|||
return -Parameter.Evaluate(ref context); |
|||
return default; |
|||
} |
|||
|
|||
public override void CollectReferences(HashSet<(string parameter, string property)> references) |
|||
{ |
|||
Parameter.CollectReferences(references); |
|||
} |
|||
|
|||
protected override string Print() |
|||
{ |
|||
return OperatorName(Type) + Parameter; |
|||
} |
|||
|
|||
public UnaryExpression(Expression parameter, ExpressionType type) |
|||
{ |
|||
Parameter = parameter; |
|||
Type = type; |
|||
} |
|||
} |
|||
|
|||
internal class BinaryExpression : Expression |
|||
{ |
|||
public Expression Left { get; } |
|||
public Expression Right { get; } |
|||
public override ExpressionType Type { get; } |
|||
public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) |
|||
{ |
|||
var left = Left.Evaluate(ref context); |
|||
var right = Right.Evaluate(ref context); |
|||
if (Type == ExpressionType.Add) |
|||
return left + right; |
|||
if (Type == ExpressionType.Subtract) |
|||
return left - right; |
|||
if (Type == ExpressionType.Multiply) |
|||
return left * right; |
|||
if (Type == ExpressionType.Divide) |
|||
return left / right; |
|||
if (Type == ExpressionType.Remainder) |
|||
return left % right; |
|||
if (Type == ExpressionType.MoreThan) |
|||
return left > right; |
|||
if (Type == ExpressionType.LessThan) |
|||
return left < right; |
|||
if (Type == ExpressionType.MoreThanOrEqual) |
|||
return left > right; |
|||
if (Type == ExpressionType.LessThanOrEqual) |
|||
return left < right; |
|||
if (Type == ExpressionType.LogicalAnd) |
|||
return left.And(right); |
|||
if (Type == ExpressionType.LogicalOr) |
|||
return left.Or(right); |
|||
if (Type == ExpressionType.Equals) |
|||
return left.EqualsTo(right); |
|||
if (Type == ExpressionType.NotEquals) |
|||
return left.NotEqualsTo(right); |
|||
return default; |
|||
} |
|||
|
|||
public override void CollectReferences(HashSet<(string parameter, string property)> references) |
|||
{ |
|||
Left.CollectReferences(references); |
|||
Right.CollectReferences(references); |
|||
} |
|||
|
|||
protected override string Print() |
|||
{ |
|||
return "(" + Left + OperatorName(Type) + Right + ")"; |
|||
} |
|||
|
|||
public BinaryExpression(Expression left, Expression right, ExpressionType type) |
|||
{ |
|||
Left = left; |
|||
Right = right; |
|||
Type = type; |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
using System.Collections.Generic; |
|||
using Avalonia.Rendering.Composition.Server; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Expressions |
|||
{ |
|||
internal struct ExpressionEvaluationContext |
|||
{ |
|||
public ExpressionVariant StartingValue { get; set; } |
|||
public ExpressionVariant CurrentValue { get; set; } |
|||
public ExpressionVariant FinalValue { get; set; } |
|||
public IExpressionObject Target { get; set; } |
|||
public IExpressionParameterCollection Parameters { get; set; } |
|||
public IExpressionForeignFunctionInterface ForeignFunctionInterface { get; set; } |
|||
} |
|||
|
|||
internal interface IExpressionObject |
|||
{ |
|||
ExpressionVariant GetProperty(string name); |
|||
} |
|||
|
|||
internal interface IExpressionParameterCollection |
|||
{ |
|||
public ExpressionVariant GetParameter(string name); |
|||
|
|||
public IExpressionObject GetObjectParameter(string name); |
|||
} |
|||
|
|||
internal interface IExpressionForeignFunctionInterface |
|||
{ |
|||
bool Call(string name, IReadOnlyList<ExpressionVariant> arguments, out ExpressionVariant result); |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Expressions |
|||
{ |
|||
internal class ExpressionParseException : Exception |
|||
{ |
|||
public int Position { get; } |
|||
|
|||
public ExpressionParseException(string message, int position) : base(message) |
|||
{ |
|||
Position = position; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,298 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.Linq; |
|||
|
|||
// ReSharper disable StringLiteralTypo
|
|||
|
|||
namespace Avalonia.Rendering.Composition.Expressions |
|||
{ |
|||
internal class ExpressionParser |
|||
{ |
|||
public static Expression Parse(ReadOnlySpan<char> s) |
|||
{ |
|||
var p = new TokenParser(s); |
|||
var parsed = ParseTillTerminator(ref p, "", false, false, out _); |
|||
p.SkipWhitespace(); |
|||
if (p.Length != 0) |
|||
throw new ExpressionParseException("Unexpected data ", p.Position); |
|||
return parsed; |
|||
} |
|||
|
|||
private static ReadOnlySpan<char> Dot => ".".AsSpan(); |
|||
static bool TryParseAtomic(ref TokenParser parser, |
|||
[MaybeNullWhen(returnValue: false)] out Expression expr) |
|||
{ |
|||
// We can parse keywords, parameter names and constants
|
|||
expr = null; |
|||
if (parser.TryParseKeywordLowerCase("this.startingvalue")) |
|||
expr = new KeywordExpression(ExpressionKeyword.StartingValue); |
|||
else if(parser.TryParseKeywordLowerCase("this.currentvalue")) |
|||
expr = new KeywordExpression(ExpressionKeyword.CurrentValue); |
|||
else if(parser.TryParseKeywordLowerCase("this.finalvalue")) |
|||
expr = new KeywordExpression(ExpressionKeyword.FinalValue); |
|||
else if(parser.TryParseKeywordLowerCase("pi")) |
|||
expr = new KeywordExpression(ExpressionKeyword.Pi); |
|||
else if(parser.TryParseKeywordLowerCase("true")) |
|||
expr = new KeywordExpression(ExpressionKeyword.True); |
|||
else if(parser.TryParseKeywordLowerCase("false")) |
|||
expr = new KeywordExpression(ExpressionKeyword.False); |
|||
else if (parser.TryParseKeywordLowerCase("this.target")) |
|||
expr = new KeywordExpression(ExpressionKeyword.Target); |
|||
|
|||
if (expr != null) |
|||
return true; |
|||
|
|||
if (parser.TryParseIdentifier(out var identifier)) |
|||
{ |
|||
expr = new ParameterExpression(identifier.ToString()); |
|||
return true; |
|||
} |
|||
|
|||
if(parser.TryParseFloat(out var scalar)) |
|||
{ |
|||
expr = new ConstantExpression(scalar); |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
|
|||
} |
|||
|
|||
static bool TryParseOperator(ref TokenParser parser, out ExpressionType op) |
|||
{ |
|||
op = (ExpressionType) (-1); |
|||
if (parser.TryConsume("||")) |
|||
op = ExpressionType.LogicalOr; |
|||
else if (parser.TryConsume("&&")) |
|||
op = ExpressionType.LogicalAnd; |
|||
else if (parser.TryConsume(">=")) |
|||
op = ExpressionType.MoreThanOrEqual; |
|||
else if (parser.TryConsume("<=")) |
|||
op = ExpressionType.LessThanOrEqual; |
|||
else if (parser.TryConsume("==")) |
|||
op = ExpressionType.Equals; |
|||
else if (parser.TryConsume("!=")) |
|||
op = ExpressionType.NotEquals; |
|||
else if (parser.TryConsumeAny("+-/*><%".AsSpan(), out var sop)) |
|||
{ |
|||
#pragma warning disable CS8509
|
|||
op = sop switch |
|||
#pragma warning restore CS8509
|
|||
{ |
|||
'+' => ExpressionType.Add, |
|||
'-' => ExpressionType.Subtract, |
|||
'/' => ExpressionType.Divide, |
|||
'*' => ExpressionType.Multiply, |
|||
'<' => ExpressionType.LessThan, |
|||
'>' => ExpressionType.MoreThan, |
|||
'%' => ExpressionType.Remainder |
|||
}; |
|||
} |
|||
else |
|||
return false; |
|||
|
|||
return true; |
|||
} |
|||
|
|||
|
|||
struct ExpressionOperatorGroup |
|||
{ |
|||
private List<Expression> _expressions; |
|||
private List<ExpressionType> _operators; |
|||
private Expression? _first; |
|||
|
|||
public bool NotEmpty => !Empty; |
|||
public bool Empty => _expressions == null && _first == null; |
|||
|
|||
public void AppendFirst(Expression expr) |
|||
{ |
|||
if (NotEmpty) |
|||
throw new InvalidOperationException(); |
|||
_first = expr; |
|||
} |
|||
|
|||
public void AppendWithOperator(Expression expr, ExpressionType op) |
|||
{ |
|||
if (_expressions == null) |
|||
{ |
|||
if (_first == null) |
|||
throw new InvalidOperationException(); |
|||
_expressions = new List<Expression>(); |
|||
_expressions.Add(_first); |
|||
_first = null; |
|||
_operators = new List<ExpressionType>(); |
|||
} |
|||
_expressions.Add(expr); |
|||
_operators.Add(op); |
|||
} |
|||
|
|||
// https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/
|
|||
private static readonly ExpressionType[][] OperatorPrecedenceGroups = new[] |
|||
{ |
|||
// multiplicative
|
|||
new[] {ExpressionType.Multiply, ExpressionType.Divide, ExpressionType.Remainder}, |
|||
// additive
|
|||
new[] {ExpressionType.Add, ExpressionType.Subtract}, |
|||
// relational
|
|||
new[] {ExpressionType.MoreThan, ExpressionType.MoreThanOrEqual, ExpressionType.LessThan, ExpressionType.LessThanOrEqual}, |
|||
// equality
|
|||
new[] {ExpressionType.Equals, ExpressionType.NotEquals}, |
|||
// conditional AND
|
|||
new[] {ExpressionType.LogicalAnd}, |
|||
// conditional OR
|
|||
new[]{ ExpressionType.LogicalOr}, |
|||
}; |
|||
|
|||
private static readonly ExpressionType[][] OperatorPrecedenceGroupsReversed = |
|||
OperatorPrecedenceGroups.Reverse().ToArray(); |
|||
|
|||
// a*b+c [a,b,c] [*,+], call with (0, 2)
|
|||
// ToExpression(a*b) + ToExpression(c)
|
|||
// a+b*c -> ToExpression(a) + ToExpression(b*c)
|
|||
Expression ToExpression(int from, int to) |
|||
{ |
|||
if (to - from == 0) |
|||
return _expressions[from]; |
|||
|
|||
if (to - from == 1) |
|||
return new BinaryExpression(_expressions[from], _expressions[to], _operators[from]); |
|||
|
|||
foreach (var grp in OperatorPrecedenceGroupsReversed) |
|||
{ |
|||
for (var c = from; c < to; c++) |
|||
{ |
|||
var currentOperator = _operators[c]; |
|||
foreach(var operatorFromGroup in grp) |
|||
if (currentOperator == operatorFromGroup) |
|||
{ |
|||
// We are dividing the expression right here
|
|||
var left = ToExpression(from, c); |
|||
var right = ToExpression(c + 1, to); |
|||
return new BinaryExpression(left, right, currentOperator); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// We shouldn't ever get here, if we are, there is something wrong in the code
|
|||
throw new ExpressionParseException("Expression parsing algorithm bug in ToExpression", 0); |
|||
} |
|||
|
|||
public Expression ToExpression() |
|||
{ |
|||
if (_expressions == null) |
|||
return _first ?? throw new InvalidOperationException(); |
|||
return ToExpression(0, _expressions.Count - 1); |
|||
} |
|||
} |
|||
|
|||
static Expression ParseTillTerminator(ref TokenParser parser, string terminatorChars, |
|||
bool throwOnTerminator, |
|||
bool throwOnEnd, |
|||
out char? token) |
|||
{ |
|||
ExpressionOperatorGroup left = default; |
|||
token = null; |
|||
while (true) |
|||
{ |
|||
if (parser.TryConsumeAny(terminatorChars.AsSpan(), out var consumedToken)) |
|||
{ |
|||
if (throwOnTerminator || left.Empty) |
|||
throw new ExpressionParseException($"Unexpected '{token}'", parser.Position - 1); |
|||
token = consumedToken; |
|||
return left.ToExpression(); |
|||
} |
|||
parser.SkipWhitespace(); |
|||
if (parser.Length == 0) |
|||
{ |
|||
if (throwOnEnd || left.Empty) |
|||
throw new ExpressionParseException("Unexpected end of expression", parser.Position); |
|||
return left.ToExpression(); |
|||
} |
|||
|
|||
ExpressionType? op = null; |
|||
if (left.NotEmpty) |
|||
{ |
|||
if (parser.TryConsume('?')) |
|||
{ |
|||
var truePart = ParseTillTerminator(ref parser, ":", |
|||
false, true, out _); |
|||
// pass through the current parsing rules to consume the rest
|
|||
var falsePart = ParseTillTerminator(ref parser, terminatorChars, throwOnTerminator, throwOnEnd, |
|||
out token); |
|||
|
|||
return new ConditionalExpression(left.ToExpression(), truePart, falsePart); |
|||
} |
|||
|
|||
// We expect a binary operator here
|
|||
if (!TryParseOperator(ref parser, out var sop)) |
|||
throw new ExpressionParseException("Unexpected token", parser.Position); |
|||
op = sop; |
|||
} |
|||
|
|||
// We expect an expression to be parsed (either due to expecting a binary operator or parsing the first part
|
|||
var applyNegation = false; |
|||
while (parser.TryConsume('!')) |
|||
applyNegation = !applyNegation; |
|||
|
|||
var applyUnaryMinus = false; |
|||
while (parser.TryConsume('-')) |
|||
applyUnaryMinus = !applyUnaryMinus; |
|||
|
|||
Expression? parsed; |
|||
|
|||
if (parser.TryConsume('(')) |
|||
parsed = ParseTillTerminator(ref parser, ")", false, true, out _); |
|||
else if (parser.TryParseCall(out var functionName)) |
|||
{ |
|||
var parameterList = new List<Expression>(); |
|||
while (true) |
|||
{ |
|||
parameterList.Add(ParseTillTerminator(ref parser, ",)", false, true, out var closingToken)); |
|||
if (closingToken == ')') |
|||
break; |
|||
if (closingToken != ',') |
|||
throw new ExpressionParseException("Unexpected end of the expression", parser.Position); |
|||
} |
|||
|
|||
parsed = new FunctionCallExpression(functionName.ToString(), parameterList); |
|||
} |
|||
else if (TryParseAtomic(ref parser, out parsed)) |
|||
{ |
|||
// do nothing
|
|||
} |
|||
else |
|||
throw new ExpressionParseException("Unexpected token", parser.Position); |
|||
|
|||
|
|||
// Parse any following member accesses
|
|||
while (parser.TryConsume('.')) |
|||
{ |
|||
if(!parser.TryParseIdentifier(out var memberName)) |
|||
throw new ExpressionParseException("Unexpected token", parser.Position); |
|||
|
|||
parsed = new MemberAccessExpression(parsed, memberName.ToString()); |
|||
} |
|||
|
|||
// Apply ! operator
|
|||
if (applyNegation) |
|||
parsed = new UnaryExpression(parsed, ExpressionType.Not); |
|||
|
|||
if (applyUnaryMinus) |
|||
{ |
|||
if(parsed is ConstantExpression constexpr) |
|||
parsed = new ConstantExpression(-constexpr.Constant); |
|||
else parsed = new UnaryExpression(parsed, ExpressionType.UnaryMinus); |
|||
} |
|||
|
|||
if (left.Empty) |
|||
left.AppendFirst(parsed); |
|||
else |
|||
left.AppendWithOperator(parsed, op!.Value); |
|||
} |
|||
|
|||
|
|||
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Rendering.Composition.Server; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Expressions; |
|||
|
|||
internal class ExpressionTrackedObjects : IEnumerable<IExpressionObject> |
|||
{ |
|||
private List<IExpressionObject> _list = new(); |
|||
private HashSet<IExpressionObject> _hashSet = new(); |
|||
|
|||
public void Add(IExpressionObject obj, string member) |
|||
{ |
|||
if (_hashSet.Add(obj)) |
|||
_list.Add(obj); |
|||
} |
|||
|
|||
public void Clear() |
|||
{ |
|||
_list.Clear(); |
|||
_hashSet.Clear(); |
|||
} |
|||
|
|||
IEnumerator<IExpressionObject> IEnumerable<IExpressionObject>.GetEnumerator() |
|||
{ |
|||
return _list.GetEnumerator(); |
|||
} |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() |
|||
{ |
|||
return ((IEnumerable)_list).GetEnumerator(); |
|||
} |
|||
|
|||
public List<IExpressionObject>.Enumerator GetEnumerator() => _list.GetEnumerator(); |
|||
|
|||
public struct Pool |
|||
{ |
|||
private Stack<ExpressionTrackedObjects> _stack = new(); |
|||
|
|||
public Pool() |
|||
{ |
|||
} |
|||
|
|||
public ExpressionTrackedObjects Get() |
|||
{ |
|||
if (_stack.Count > 0) |
|||
return _stack.Pop(); |
|||
return new ExpressionTrackedObjects(); |
|||
} |
|||
|
|||
public void Return(ExpressionTrackedObjects obj) |
|||
{ |
|||
_stack.Clear(); |
|||
_stack.Push(obj); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,730 @@ |
|||
using System; |
|||
using System.Globalization; |
|||
using System.Numerics; |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia.Media; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Expressions |
|||
{ |
|||
internal enum VariantType |
|||
{ |
|||
Invalid, |
|||
Boolean, |
|||
Scalar, |
|||
Double, |
|||
Vector2, |
|||
Vector3, |
|||
Vector4, |
|||
AvaloniaMatrix, |
|||
Matrix3x2, |
|||
Matrix4x4, |
|||
Quaternion, |
|||
Color |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// A VARIANT type used in expression animations. Can represent multiple value types
|
|||
/// </summary>
|
|||
[StructLayout(LayoutKind.Explicit)] |
|||
internal struct ExpressionVariant |
|||
{ |
|||
[FieldOffset(0)] public VariantType Type; |
|||
|
|||
[FieldOffset(4)] public bool Boolean; |
|||
[FieldOffset(4)] public float Scalar; |
|||
[FieldOffset(4)] public double Double; |
|||
[FieldOffset(4)] public Vector2 Vector2; |
|||
[FieldOffset(4)] public Vector3 Vector3; |
|||
[FieldOffset(4)] public Vector4 Vector4; |
|||
[FieldOffset(4)] public Matrix AvaloniaMatrix; |
|||
[FieldOffset(4)] public Matrix3x2 Matrix3x2; |
|||
[FieldOffset(4)] public Matrix4x4 Matrix4x4; |
|||
[FieldOffset(4)] public Quaternion Quaternion; |
|||
[FieldOffset(4)] public Color Color; |
|||
|
|||
|
|||
public ExpressionVariant GetProperty(string property) |
|||
{ |
|||
if (Type == VariantType.Vector2) |
|||
{ |
|||
if (ReferenceEquals(property, "X")) |
|||
return Vector2.X; |
|||
if (ReferenceEquals(property, "Y")) |
|||
return Vector2.Y; |
|||
return default; |
|||
} |
|||
|
|||
if (Type == VariantType.Vector3) |
|||
{ |
|||
if (ReferenceEquals(property, "X")) |
|||
return Vector3.X; |
|||
if (ReferenceEquals(property, "Y")) |
|||
return Vector3.Y; |
|||
if (ReferenceEquals(property, "Z")) |
|||
return Vector3.Z; |
|||
if(ReferenceEquals(property, "XY")) |
|||
return new Vector2(Vector3.X, Vector3.Y); |
|||
if(ReferenceEquals(property, "YX")) |
|||
return new Vector2(Vector3.Y, Vector3.X); |
|||
if(ReferenceEquals(property, "XZ")) |
|||
return new Vector2(Vector3.X, Vector3.Z); |
|||
if(ReferenceEquals(property, "ZX")) |
|||
return new Vector2(Vector3.Z, Vector3.X); |
|||
if(ReferenceEquals(property, "YZ")) |
|||
return new Vector2(Vector3.Y, Vector3.Z); |
|||
if(ReferenceEquals(property, "ZY")) |
|||
return new Vector2(Vector3.Z, Vector3.Y); |
|||
return default; |
|||
} |
|||
|
|||
if (Type == VariantType.Vector4) |
|||
{ |
|||
if (ReferenceEquals(property, "X")) |
|||
return Vector4.X; |
|||
if (ReferenceEquals(property, "Y")) |
|||
return Vector4.Y; |
|||
if (ReferenceEquals(property, "Z")) |
|||
return Vector4.Z; |
|||
if (ReferenceEquals(property, "W")) |
|||
return Vector4.W; |
|||
return default; |
|||
} |
|||
|
|||
if (Type == VariantType.Matrix3x2) |
|||
{ |
|||
if (ReferenceEquals(property, "M11")) |
|||
return Matrix3x2.M11; |
|||
if (ReferenceEquals(property, "M12")) |
|||
return Matrix3x2.M12; |
|||
if (ReferenceEquals(property, "M21")) |
|||
return Matrix3x2.M21; |
|||
if (ReferenceEquals(property, "M22")) |
|||
return Matrix3x2.M22; |
|||
if (ReferenceEquals(property, "M31")) |
|||
return Matrix3x2.M31; |
|||
if (ReferenceEquals(property, "M32")) |
|||
return Matrix3x2.M32; |
|||
return default; |
|||
} |
|||
|
|||
if (Type == VariantType.AvaloniaMatrix) |
|||
{ |
|||
if (ReferenceEquals(property, "M11")) |
|||
return AvaloniaMatrix.M11; |
|||
if (ReferenceEquals(property, "M12")) |
|||
return AvaloniaMatrix.M12; |
|||
if (ReferenceEquals(property, "M21")) |
|||
return AvaloniaMatrix.M21; |
|||
if (ReferenceEquals(property, "M22")) |
|||
return AvaloniaMatrix.M22; |
|||
if (ReferenceEquals(property, "M31")) |
|||
return AvaloniaMatrix.M31; |
|||
if (ReferenceEquals(property, "M32")) |
|||
return AvaloniaMatrix.M32; |
|||
return default; |
|||
} |
|||
|
|||
if (Type == VariantType.Matrix4x4) |
|||
{ |
|||
if (ReferenceEquals(property, "M11")) |
|||
return Matrix4x4.M11; |
|||
if (ReferenceEquals(property, "M12")) |
|||
return Matrix4x4.M12; |
|||
if (ReferenceEquals(property, "M13")) |
|||
return Matrix4x4.M13; |
|||
if (ReferenceEquals(property, "M14")) |
|||
return Matrix4x4.M14; |
|||
if (ReferenceEquals(property, "M21")) |
|||
return Matrix4x4.M21; |
|||
if (ReferenceEquals(property, "M22")) |
|||
return Matrix4x4.M22; |
|||
if (ReferenceEquals(property, "M23")) |
|||
return Matrix4x4.M23; |
|||
if (ReferenceEquals(property, "M24")) |
|||
return Matrix4x4.M24; |
|||
if (ReferenceEquals(property, "M31")) |
|||
return Matrix4x4.M31; |
|||
if (ReferenceEquals(property, "M32")) |
|||
return Matrix4x4.M32; |
|||
if (ReferenceEquals(property, "M33")) |
|||
return Matrix4x4.M33; |
|||
if (ReferenceEquals(property, "M34")) |
|||
return Matrix4x4.M34; |
|||
if (ReferenceEquals(property, "M41")) |
|||
return Matrix4x4.M41; |
|||
if (ReferenceEquals(property, "M42")) |
|||
return Matrix4x4.M42; |
|||
if (ReferenceEquals(property, "M43")) |
|||
return Matrix4x4.M43; |
|||
if (ReferenceEquals(property, "M44")) |
|||
return Matrix4x4.M44; |
|||
return default; |
|||
} |
|||
|
|||
if (Type == VariantType.Quaternion) |
|||
{ |
|||
if (ReferenceEquals(property, "X")) |
|||
return Quaternion.X; |
|||
if (ReferenceEquals(property, "Y")) |
|||
return Quaternion.Y; |
|||
if (ReferenceEquals(property, "Z")) |
|||
return Quaternion.Z; |
|||
if (ReferenceEquals(property, "W")) |
|||
return Quaternion.W; |
|||
return default; |
|||
} |
|||
|
|||
if (Type == VariantType.Color) |
|||
{ |
|||
if (ReferenceEquals(property, "A")) |
|||
return Color.A; |
|||
if (ReferenceEquals(property, "R")) |
|||
return Color.R; |
|||
if (ReferenceEquals(property, "G")) |
|||
return Color.G; |
|||
if (ReferenceEquals(property, "B")) |
|||
return Color.B; |
|||
return default; |
|||
} |
|||
|
|||
return default; |
|||
} |
|||
|
|||
public static implicit operator ExpressionVariant(bool value) => |
|||
new ExpressionVariant |
|||
{ |
|||
Type = VariantType.Boolean, |
|||
Boolean = value |
|||
}; |
|||
|
|||
public static implicit operator ExpressionVariant(float scalar) => |
|||
new ExpressionVariant |
|||
{ |
|||
Type = VariantType.Scalar, |
|||
Scalar = scalar |
|||
}; |
|||
|
|||
public static implicit operator ExpressionVariant(double d) => |
|||
new ExpressionVariant |
|||
{ |
|||
Type = VariantType.Double, |
|||
Double = d |
|||
}; |
|||
|
|||
|
|||
public static implicit operator ExpressionVariant(Vector2 value) => |
|||
new ExpressionVariant |
|||
{ |
|||
Type = VariantType.Vector2, |
|||
Vector2 = value |
|||
}; |
|||
|
|||
|
|||
public static implicit operator ExpressionVariant(Vector3 value) => |
|||
new ExpressionVariant |
|||
{ |
|||
Type = VariantType.Vector3, |
|||
Vector3 = value |
|||
}; |
|||
|
|||
|
|||
public static implicit operator ExpressionVariant(Vector4 value) => |
|||
new ExpressionVariant |
|||
{ |
|||
Type = VariantType.Vector4, |
|||
Vector4 = value |
|||
}; |
|||
|
|||
public static implicit operator ExpressionVariant(Matrix3x2 value) => |
|||
new ExpressionVariant |
|||
{ |
|||
Type = VariantType.Matrix3x2, |
|||
Matrix3x2 = value |
|||
}; |
|||
|
|||
public static implicit operator ExpressionVariant(Matrix value) => |
|||
new ExpressionVariant |
|||
{ |
|||
Type = VariantType.Matrix3x2, |
|||
AvaloniaMatrix = value |
|||
}; |
|||
|
|||
public static implicit operator ExpressionVariant(Matrix4x4 value) => |
|||
new ExpressionVariant |
|||
{ |
|||
Type = VariantType.Matrix4x4, |
|||
Matrix4x4 = value |
|||
}; |
|||
|
|||
public static implicit operator ExpressionVariant(Quaternion value) => |
|||
new ExpressionVariant |
|||
{ |
|||
Type = VariantType.Quaternion, |
|||
Quaternion = value |
|||
}; |
|||
|
|||
public static implicit operator ExpressionVariant(Avalonia.Media.Color value) => |
|||
new ExpressionVariant |
|||
{ |
|||
Type = VariantType.Color, |
|||
Color = value |
|||
}; |
|||
|
|||
public static ExpressionVariant operator +(ExpressionVariant left, ExpressionVariant right) |
|||
{ |
|||
if (left.Type != right.Type || left.Type == VariantType.Invalid) |
|||
return default; |
|||
|
|||
if (left.Type == VariantType.Scalar) |
|||
return left.Scalar + right.Scalar; |
|||
|
|||
if (left.Type == VariantType.Double) |
|||
return left.Double + right.Double; |
|||
|
|||
if (left.Type == VariantType.Vector2) |
|||
return left.Vector2 + right.Vector2; |
|||
|
|||
if (left.Type == VariantType.Vector3) |
|||
return left.Vector3 + right.Vector3; |
|||
|
|||
if (left.Type == VariantType.Vector4) |
|||
return left.Vector4 + right.Vector4; |
|||
|
|||
if (left.Type == VariantType.Matrix3x2) |
|||
return left.Matrix3x2 + right.Matrix3x2; |
|||
|
|||
if (left.Type == VariantType.Matrix4x4) |
|||
return left.Matrix4x4 + right.Matrix4x4; |
|||
|
|||
if (left.Type == VariantType.Quaternion) |
|||
return left.Quaternion + right.Quaternion; |
|||
|
|||
return default; |
|||
} |
|||
|
|||
public static ExpressionVariant operator -(ExpressionVariant left, ExpressionVariant right) |
|||
{ |
|||
if (left.Type != right.Type || left.Type == VariantType.Invalid) |
|||
return default; |
|||
|
|||
if (left.Type == VariantType.Scalar) |
|||
return left.Scalar - right.Scalar; |
|||
|
|||
if (left.Type == VariantType.Double) |
|||
return left.Double - right.Double; |
|||
|
|||
if (left.Type == VariantType.Vector2) |
|||
return left.Vector2 - right.Vector2; |
|||
|
|||
if (left.Type == VariantType.Vector3) |
|||
return left.Vector3 - right.Vector3; |
|||
|
|||
if (left.Type == VariantType.Vector4) |
|||
return left.Vector4 - right.Vector4; |
|||
|
|||
if (left.Type == VariantType.Matrix3x2) |
|||
return left.Matrix3x2 - right.Matrix3x2; |
|||
|
|||
if (left.Type == VariantType.Matrix4x4) |
|||
return left.Matrix4x4 - right.Matrix4x4; |
|||
|
|||
if (left.Type == VariantType.Quaternion) |
|||
return left.Quaternion - right.Quaternion; |
|||
|
|||
return default; |
|||
} |
|||
|
|||
public static ExpressionVariant operator -(ExpressionVariant left) |
|||
{ |
|||
|
|||
if (left.Type == VariantType.Scalar) |
|||
return -left.Scalar; |
|||
|
|||
if (left.Type == VariantType.Double) |
|||
return -left.Double; |
|||
|
|||
if (left.Type == VariantType.Vector2) |
|||
return -left.Vector2; |
|||
|
|||
if (left.Type == VariantType.Vector3) |
|||
return -left.Vector3; |
|||
|
|||
if (left.Type == VariantType.Vector4) |
|||
return -left.Vector4; |
|||
|
|||
if (left.Type == VariantType.Matrix3x2) |
|||
return -left.Matrix3x2; |
|||
|
|||
if (left.Type == VariantType.AvaloniaMatrix) |
|||
return -left.AvaloniaMatrix; |
|||
|
|||
if (left.Type == VariantType.Matrix4x4) |
|||
return -left.Matrix4x4; |
|||
|
|||
if (left.Type == VariantType.Quaternion) |
|||
return -left.Quaternion; |
|||
|
|||
return default; |
|||
} |
|||
|
|||
public static ExpressionVariant operator *(ExpressionVariant left, ExpressionVariant right) |
|||
{ |
|||
if (left.Type == VariantType.Invalid || right.Type == VariantType.Invalid) |
|||
return default; |
|||
|
|||
if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) |
|||
return left.Scalar * right.Scalar; |
|||
|
|||
if (left.Type == VariantType.Double && right.Type == VariantType.Double) |
|||
return left.Double * right.Double; |
|||
|
|||
if (left.Type == VariantType.Vector2 && right.Type == VariantType.Vector2) |
|||
return left.Vector2 * right.Vector2; |
|||
|
|||
if (left.Type == VariantType.Vector2 && right.Type == VariantType.Scalar) |
|||
return left.Vector2 * right.Scalar; |
|||
|
|||
if (left.Type == VariantType.Vector3 && right.Type == VariantType.Vector3) |
|||
return left.Vector3 * right.Vector3; |
|||
|
|||
if (left.Type == VariantType.Vector3 && right.Type == VariantType.Scalar) |
|||
return left.Vector3 * right.Scalar; |
|||
|
|||
if (left.Type == VariantType.Vector4 && right.Type == VariantType.Vector4) |
|||
return left.Vector4 * right.Vector4; |
|||
|
|||
if (left.Type == VariantType.Vector4 && right.Type == VariantType.Scalar) |
|||
return left.Vector4 * right.Scalar; |
|||
|
|||
if (left.Type == VariantType.Matrix3x2 && right.Type == VariantType.Matrix3x2) |
|||
return left.Matrix3x2 * right.Matrix3x2; |
|||
|
|||
if (left.Type == VariantType.Matrix3x2 && right.Type == VariantType.Scalar) |
|||
return left.Matrix3x2 * right.Scalar; |
|||
|
|||
if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.AvaloniaMatrix) |
|||
return left.AvaloniaMatrix * right.AvaloniaMatrix; |
|||
|
|||
if (left.Type == VariantType.Matrix4x4 && right.Type == VariantType.Matrix4x4) |
|||
return left.Matrix4x4 * right.Matrix4x4; |
|||
|
|||
if (left.Type == VariantType.Matrix4x4 && right.Type == VariantType.Scalar) |
|||
return left.Matrix4x4 * right.Scalar; |
|||
|
|||
if (left.Type == VariantType.Quaternion && right.Type == VariantType.Quaternion) |
|||
return left.Quaternion * right.Quaternion; |
|||
|
|||
if (left.Type == VariantType.Quaternion && right.Type == VariantType.Scalar) |
|||
return left.Quaternion * right.Scalar; |
|||
|
|||
return default; |
|||
} |
|||
|
|||
public static ExpressionVariant operator /(ExpressionVariant left, ExpressionVariant right) |
|||
{ |
|||
if (left.Type == VariantType.Invalid || right.Type == VariantType.Invalid) |
|||
return default; |
|||
|
|||
if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) |
|||
return left.Scalar / right.Scalar; |
|||
|
|||
if (left.Type == VariantType.Double && right.Type == VariantType.Double) |
|||
return left.Double / right.Double; |
|||
|
|||
if (left.Type == VariantType.Vector2 && right.Type == VariantType.Vector2) |
|||
return left.Vector2 / right.Vector2; |
|||
|
|||
if (left.Type == VariantType.Vector2 && right.Type == VariantType.Scalar) |
|||
return left.Vector2 / right.Scalar; |
|||
|
|||
if (left.Type == VariantType.Vector3 && right.Type == VariantType.Vector3) |
|||
return left.Vector3 / right.Vector3; |
|||
|
|||
if (left.Type == VariantType.Vector3 && right.Type == VariantType.Scalar) |
|||
return left.Vector3 / right.Scalar; |
|||
|
|||
if (left.Type == VariantType.Vector4 && right.Type == VariantType.Vector4) |
|||
return left.Vector4 / right.Vector4; |
|||
|
|||
if (left.Type == VariantType.Vector4 && right.Type == VariantType.Scalar) |
|||
return left.Vector4 / right.Scalar; |
|||
|
|||
if (left.Type == VariantType.Quaternion && right.Type == VariantType.Quaternion) |
|||
return left.Quaternion / right.Quaternion; |
|||
|
|||
return default; |
|||
} |
|||
|
|||
public ExpressionVariant EqualsTo(ExpressionVariant right) |
|||
{ |
|||
if (Type != right.Type || Type == VariantType.Invalid) |
|||
return default; |
|||
|
|||
if (Type == VariantType.Scalar) |
|||
return Scalar == right.Scalar; |
|||
|
|||
|
|||
if (Type == VariantType.Double) |
|||
return Double == right.Double; |
|||
|
|||
if (Type == VariantType.Vector2) |
|||
return Vector2 == right.Vector2; |
|||
|
|||
if (Type == VariantType.Vector3) |
|||
return Vector3 == right.Vector3; |
|||
|
|||
if (Type == VariantType.Vector4) |
|||
return Vector4 == right.Vector4; |
|||
|
|||
if (Type == VariantType.Boolean) |
|||
return Boolean == right.Boolean; |
|||
|
|||
if (Type == VariantType.Matrix3x2) |
|||
return Matrix3x2 == right.Matrix3x2; |
|||
|
|||
if (Type == VariantType.AvaloniaMatrix) |
|||
return AvaloniaMatrix == right.AvaloniaMatrix; |
|||
|
|||
if (Type == VariantType.Matrix4x4) |
|||
return Matrix4x4 == right.Matrix4x4; |
|||
|
|||
if (Type == VariantType.Quaternion) |
|||
return Quaternion == right.Quaternion; |
|||
|
|||
return default; |
|||
} |
|||
|
|||
public ExpressionVariant NotEqualsTo(ExpressionVariant right) |
|||
{ |
|||
var r = EqualsTo(right); |
|||
if (r.Type == VariantType.Boolean) |
|||
return !r.Boolean; |
|||
return default; |
|||
} |
|||
|
|||
public static ExpressionVariant operator !(ExpressionVariant v) |
|||
{ |
|||
if (v.Type == VariantType.Boolean) |
|||
return !v.Boolean; |
|||
return default; |
|||
} |
|||
|
|||
public static ExpressionVariant operator %(ExpressionVariant left, ExpressionVariant right) |
|||
{ |
|||
if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) |
|||
return left.Scalar % right.Scalar; |
|||
if (left.Type == VariantType.Double && right.Type == VariantType.Double) |
|||
return left.Double % right.Double; |
|||
return default; |
|||
} |
|||
|
|||
public static ExpressionVariant operator <(ExpressionVariant left, ExpressionVariant right) |
|||
{ |
|||
if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) |
|||
return left.Scalar < right.Scalar; |
|||
if (left.Type == VariantType.Double && right.Type == VariantType.Double) |
|||
return left.Double < right.Double; |
|||
return default; |
|||
} |
|||
|
|||
public static ExpressionVariant operator >(ExpressionVariant left, ExpressionVariant right) |
|||
{ |
|||
if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar) |
|||
return left.Scalar > right.Scalar; |
|||
|
|||
if (left.Type == VariantType.Double && right.Type == VariantType.Double) |
|||
return left.Double > right.Double; |
|||
return default; |
|||
} |
|||
|
|||
public ExpressionVariant And(ExpressionVariant right) |
|||
{ |
|||
if (Type == VariantType.Boolean && right.Type == VariantType.Boolean) |
|||
return Boolean && right.Boolean; |
|||
return default; |
|||
} |
|||
|
|||
public ExpressionVariant Or(ExpressionVariant right) |
|||
{ |
|||
if (Type == VariantType.Boolean && right.Type == VariantType.Boolean) |
|||
return Boolean && right.Boolean; |
|||
return default; |
|||
} |
|||
|
|||
public bool TryCast<T>(out T res) where T : struct |
|||
{ |
|||
if (typeof(T) == typeof(bool)) |
|||
{ |
|||
if (Type == VariantType.Boolean) |
|||
{ |
|||
res = (T) (object) Boolean; |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
if (typeof(T) == typeof(float)) |
|||
{ |
|||
if (Type == VariantType.Scalar) |
|||
{ |
|||
res = (T) (object) Scalar; |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
if (typeof(T) == typeof(double)) |
|||
{ |
|||
if (Type == VariantType.Double) |
|||
{ |
|||
res = (T) (object) Double; |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
if (typeof(T) == typeof(Vector2)) |
|||
{ |
|||
if (Type == VariantType.Vector2) |
|||
{ |
|||
res = (T) (object) Vector2; |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
if (typeof(T) == typeof(Vector3)) |
|||
{ |
|||
if (Type == VariantType.Vector3) |
|||
{ |
|||
res = (T) (object) Vector3; |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
if (typeof(T) == typeof(Vector4)) |
|||
{ |
|||
if (Type == VariantType.Vector4) |
|||
{ |
|||
res = (T) (object) Vector4; |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
if (typeof(T) == typeof(Matrix3x2)) |
|||
{ |
|||
if (Type == VariantType.Matrix3x2) |
|||
{ |
|||
res = (T) (object) Matrix3x2; |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
if (typeof(T) == typeof(Matrix)) |
|||
{ |
|||
if (Type == VariantType.AvaloniaMatrix) |
|||
{ |
|||
res = (T) (object) Matrix3x2; |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
if (typeof(T) == typeof(Matrix4x4)) |
|||
{ |
|||
if (Type == VariantType.Matrix4x4) |
|||
{ |
|||
res = (T) (object) Matrix4x4; |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
if (typeof(T) == typeof(Quaternion)) |
|||
{ |
|||
if (Type == VariantType.Quaternion) |
|||
{ |
|||
res = (T) (object) Quaternion; |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
if (typeof(T) == typeof(Avalonia.Media.Color)) |
|||
{ |
|||
if (Type == VariantType.Color) |
|||
{ |
|||
res = (T) (object) Color; |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
res = default(T); |
|||
return false; |
|||
} |
|||
|
|||
public static ExpressionVariant Create<T>(T v) where T : struct |
|||
{ |
|||
if (typeof(T) == typeof(bool)) |
|||
return (bool) (object) v; |
|||
|
|||
if (typeof(T) == typeof(float)) |
|||
return (float) (object) v; |
|||
|
|||
if (typeof(T) == typeof(Vector2)) |
|||
return (Vector2) (object) v; |
|||
|
|||
if (typeof(T) == typeof(Vector3)) |
|||
return (Vector3) (object) v; |
|||
|
|||
if (typeof(T) == typeof(Vector4)) |
|||
return (Vector4) (object) v; |
|||
|
|||
if (typeof(T) == typeof(Matrix3x2)) |
|||
return (Matrix3x2) (object) v; |
|||
|
|||
if (typeof(T) == typeof(Matrix)) |
|||
return (Matrix) (object) v; |
|||
|
|||
if (typeof(T) == typeof(Matrix4x4)) |
|||
return (Matrix4x4) (object) v; |
|||
|
|||
if (typeof(T) == typeof(Quaternion)) |
|||
return (Quaternion) (object) v; |
|||
|
|||
if (typeof(T) == typeof(Avalonia.Media.Color)) |
|||
return (Avalonia.Media.Color) (object) v; |
|||
|
|||
throw new ArgumentException("Invalid variant type: " + typeof(T)); |
|||
} |
|||
|
|||
public T CastOrDefault<T>() where T : struct |
|||
{ |
|||
TryCast<T>(out var r); |
|||
return r; |
|||
} |
|||
|
|||
public override string ToString() |
|||
{ |
|||
if (Type == VariantType.Boolean) |
|||
return Boolean.ToString(); |
|||
if (Type == VariantType.Scalar) |
|||
return Scalar.ToString(CultureInfo.InvariantCulture); |
|||
if (Type == VariantType.Double) |
|||
return Double.ToString(CultureInfo.InvariantCulture); |
|||
if (Type == VariantType.Vector2) |
|||
return Vector2.ToString(); |
|||
if (Type == VariantType.Vector3) |
|||
return Vector3.ToString(); |
|||
if (Type == VariantType.Vector4) |
|||
return Vector4.ToString(); |
|||
if (Type == VariantType.Quaternion) |
|||
return Quaternion.ToString(); |
|||
if (Type == VariantType.Matrix3x2) |
|||
return Matrix3x2.ToString(); |
|||
if (Type == VariantType.AvaloniaMatrix) |
|||
return AvaloniaMatrix.ToString(); |
|||
if (Type == VariantType.Matrix4x4) |
|||
return Matrix4x4.ToString(); |
|||
if (Type == VariantType.Color) |
|||
return Color.ToString(); |
|||
if (Type == VariantType.Invalid) |
|||
return "Invalid"; |
|||
return "Unknown"; |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,259 @@ |
|||
using System; |
|||
using System.Globalization; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Expressions |
|||
{ |
|||
/// <summary>
|
|||
/// Helper class for composition expression parser
|
|||
/// </summary>
|
|||
internal ref struct TokenParser |
|||
{ |
|||
private ReadOnlySpan<char> _s; |
|||
public int Position { get; private set; } |
|||
public TokenParser(ReadOnlySpan<char> s) |
|||
{ |
|||
_s = s; |
|||
Position = 0; |
|||
} |
|||
|
|||
public void SkipWhitespace() |
|||
{ |
|||
while (true) |
|||
{ |
|||
if (_s.Length > 0 && char.IsWhiteSpace(_s[0])) |
|||
Advance(1); |
|||
else |
|||
return; |
|||
} |
|||
} |
|||
|
|||
static bool IsAlphaNumeric(char ch) => (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || |
|||
(ch >= 'A' && ch <= 'Z'); |
|||
|
|||
public bool TryConsume(char c) |
|||
{ |
|||
SkipWhitespace(); |
|||
if (_s.Length == 0 || _s[0] != c) |
|||
return false; |
|||
|
|||
Advance(1); |
|||
return true; |
|||
} |
|||
public bool TryConsume(string s) |
|||
{ |
|||
SkipWhitespace(); |
|||
if (_s.Length < s.Length) |
|||
return false; |
|||
for (var c = 0; c < s.Length; c++) |
|||
{ |
|||
if (_s[c] != s[c]) |
|||
return false; |
|||
} |
|||
|
|||
Advance(s.Length); |
|||
return true; |
|||
} |
|||
|
|||
public bool TryConsumeAny(ReadOnlySpan<char> chars, out char token) |
|||
{ |
|||
SkipWhitespace(); |
|||
token = default; |
|||
if (_s.Length == 0) |
|||
return false; |
|||
|
|||
foreach (var c in chars) |
|||
{ |
|||
if (c == _s[0]) |
|||
{ |
|||
token = c; |
|||
Advance(1); |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
|
|||
public bool TryParseKeyword(string keyword) |
|||
{ |
|||
SkipWhitespace(); |
|||
if (keyword.Length > _s.Length) |
|||
return false; |
|||
for(var c=0; c<keyword.Length;c++) |
|||
if (keyword[c] != _s[c]) |
|||
return false; |
|||
|
|||
if (_s.Length > keyword.Length && IsAlphaNumeric(_s[keyword.Length])) |
|||
return false; |
|||
|
|||
Advance(keyword.Length); |
|||
return true; |
|||
} |
|||
|
|||
public bool TryParseKeywordLowerCase(string keywordInLowerCase) |
|||
{ |
|||
SkipWhitespace(); |
|||
if (keywordInLowerCase.Length > _s.Length) |
|||
return false; |
|||
for(var c=0; c<keywordInLowerCase.Length;c++) |
|||
if (keywordInLowerCase[c] != char.ToLowerInvariant(_s[c])) |
|||
return false; |
|||
|
|||
if (_s.Length > keywordInLowerCase.Length && IsAlphaNumeric(_s[keywordInLowerCase.Length])) |
|||
return false; |
|||
|
|||
Advance(keywordInLowerCase.Length); |
|||
return true; |
|||
} |
|||
|
|||
public void Advance(int c) |
|||
{ |
|||
_s = _s.Slice(c); |
|||
Position += c; |
|||
} |
|||
|
|||
public int Length => _s.Length; |
|||
|
|||
public bool TryParseIdentifier(ReadOnlySpan<char> extraValidChars, out ReadOnlySpan<char> res) |
|||
{ |
|||
res = ReadOnlySpan<char>.Empty; |
|||
SkipWhitespace(); |
|||
if (_s.Length == 0) |
|||
return false; |
|||
var first = _s[0]; |
|||
if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z'))) |
|||
return false; |
|||
int len = 1; |
|||
for (var c = 1; c < _s.Length; c++) |
|||
{ |
|||
var ch = _s[c]; |
|||
if (IsAlphaNumeric(ch)) |
|||
len++; |
|||
else |
|||
{ |
|||
var found = false; |
|||
foreach(var vc in extraValidChars) |
|||
if (vc == ch) |
|||
{ |
|||
found = true; |
|||
break; |
|||
} |
|||
|
|||
if (found) |
|||
len++; |
|||
else |
|||
break; |
|||
} |
|||
} |
|||
|
|||
res = _s.Slice(0, len); |
|||
Advance(len); |
|||
return true; |
|||
} |
|||
|
|||
public bool TryParseIdentifier(out ReadOnlySpan<char> res) |
|||
{ |
|||
res = ReadOnlySpan<char>.Empty; |
|||
SkipWhitespace(); |
|||
if (_s.Length == 0) |
|||
return false; |
|||
var first = _s[0]; |
|||
if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z'))) |
|||
return false; |
|||
int len = 1; |
|||
for (var c = 1; c < _s.Length; c++) |
|||
{ |
|||
var ch = _s[c]; |
|||
if (IsAlphaNumeric(ch)) |
|||
len++; |
|||
else |
|||
break; |
|||
} |
|||
|
|||
res = _s.Slice(0, len); |
|||
Advance(len); |
|||
return true; |
|||
} |
|||
|
|||
public bool TryParseCall(out ReadOnlySpan<char> res) |
|||
{ |
|||
res = ReadOnlySpan<char>.Empty; |
|||
SkipWhitespace(); |
|||
if (_s.Length == 0) |
|||
return false; |
|||
var first = _s[0]; |
|||
if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z'))) |
|||
return false; |
|||
int len = 1; |
|||
for (var c = 1; c < _s.Length; c++) |
|||
{ |
|||
var ch = _s[c]; |
|||
if ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch<= 'Z') || ch == '.') |
|||
len++; |
|||
else |
|||
break; |
|||
} |
|||
|
|||
res = _s.Slice(0, len); |
|||
|
|||
// Find '('
|
|||
for (var c = len; c < _s.Length; c++) |
|||
{ |
|||
if(char.IsWhiteSpace(_s[c])) |
|||
continue; |
|||
if(_s[c]=='(') |
|||
{ |
|||
Advance(c + 1); |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
|
|||
} |
|||
|
|||
return false; |
|||
|
|||
} |
|||
|
|||
|
|||
public bool TryParseFloat(out float res) |
|||
{ |
|||
res = 0; |
|||
SkipWhitespace(); |
|||
if (_s.Length == 0) |
|||
return false; |
|||
|
|||
var len = 0; |
|||
var dotCount = 0; |
|||
for (var c = 0; c < _s.Length; c++) |
|||
{ |
|||
var ch = _s[c]; |
|||
if (ch >= '0' && ch <= '9') |
|||
len = c + 1; |
|||
else if (ch == '.' && dotCount == 0) |
|||
{ |
|||
len = c + 1; |
|||
dotCount++; |
|||
} |
|||
else |
|||
break; |
|||
} |
|||
|
|||
var span = _s.Slice(0, len); |
|||
|
|||
#if NETSTANDARD2_0
|
|||
if (!float.TryParse(span.ToString(), NumberStyles.Number, CultureInfo.InvariantCulture, out res)) |
|||
return false; |
|||
#else
|
|||
if (!float.TryParse(span, NumberStyles.Number, CultureInfo.InvariantCulture, out res)) |
|||
return false; |
|||
#endif
|
|||
Advance(len); |
|||
return true; |
|||
} |
|||
|
|||
public override string ToString() => _s.ToString(); |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
using System.Numerics; |
|||
|
|||
namespace Avalonia.Rendering.Composition |
|||
{ |
|||
static class MatrixUtils |
|||
{ |
|||
public static Matrix4x4 ComputeTransform(Vector2 size, Vector2 anchorPoint, Vector3 centerPoint, |
|||
Matrix4x4 transformMatrix, Vector3 scale, float rotationAngle, Quaternion orientation, Vector3 offset) |
|||
{ |
|||
// The math here follows the *observed* UWP behavior since there are no docs on how it's supposed to work
|
|||
|
|||
var anchor = size * anchorPoint; |
|||
var mat = Matrix4x4.CreateTranslation(-anchor.X, -anchor.Y, 0); |
|||
|
|||
var center = new Vector3(centerPoint.X, centerPoint.Y, centerPoint.Z); |
|||
|
|||
if (!transformMatrix.IsIdentity) |
|||
mat = transformMatrix * mat; |
|||
|
|||
|
|||
if (scale != new Vector3(1, 1, 1)) |
|||
mat *= Matrix4x4.CreateScale(scale, center); |
|||
|
|||
//TODO: RotationAxis support
|
|||
if (rotationAngle != 0) |
|||
mat *= Matrix4x4.CreateRotationZ(rotationAngle, center); |
|||
|
|||
if (orientation != Quaternion.Identity) |
|||
{ |
|||
if (centerPoint != default) |
|||
{ |
|||
mat *= Matrix4x4.CreateTranslation(-center) |
|||
* Matrix4x4.CreateFromQuaternion(orientation) |
|||
* Matrix4x4.CreateTranslation(center); |
|||
} |
|||
else |
|||
mat *= Matrix4x4.CreateFromQuaternion(orientation); |
|||
} |
|||
|
|||
if (offset != default) |
|||
mat *= Matrix4x4.CreateTranslation(offset); |
|||
|
|||
return mat; |
|||
} |
|||
|
|||
public static Matrix4x4 ToMatrix4x4(Matrix matrix) => |
|||
new Matrix4x4( |
|||
(float)matrix.M11, (float)matrix.M12, 0, (float)matrix.M13, |
|||
(float)matrix.M21, (float)matrix.M22, 0, (float)matrix.M23, |
|||
0, 0, 1, 0, |
|||
(float)matrix.M31, (float)matrix.M32, 0, (float)matrix.M33 |
|||
); |
|||
|
|||
public static Matrix ToMatrix(Matrix4x4 matrix44) => |
|||
new Matrix( |
|||
matrix44.M11, |
|||
matrix44.M12, |
|||
matrix44.M14, |
|||
matrix44.M21, |
|||
matrix44.M22, |
|||
matrix44.M24, |
|||
matrix44.M41, |
|||
matrix44.M42, |
|||
matrix44.M44); |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
using System.Collections.Generic; |
|||
using System.Threading; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Server; |
|||
|
|||
internal class CompositionProperty |
|||
{ |
|||
private static volatile int s_NextId = 1; |
|||
public int Id { get; private set; } |
|||
|
|||
public static CompositionProperty Register() => new() |
|||
{ |
|||
Id = Interlocked.Increment(ref s_NextId) |
|||
}; |
|||
} |
|||
@ -0,0 +1,179 @@ |
|||
using System.Numerics; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Imaging; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering.Composition.Drawing; |
|||
using Avalonia.Rendering.SceneGraph; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Server; |
|||
|
|||
/// <summary>
|
|||
/// A bunch of hacks to make the existing rendering operations and IDrawingContext
|
|||
/// to work with composition rendering infrastructure.
|
|||
/// 1) Keeps and applies the transform of the current visual since drawing operations think that
|
|||
/// they have information about the full render transform (they are not)
|
|||
/// 2) Keeps the draw list for the VisualBrush contents of the current drawing operation.
|
|||
/// </summary>
|
|||
internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport |
|||
{ |
|||
private IDrawingContextImpl _impl; |
|||
private readonly VisualBrushRenderer _visualBrushRenderer; |
|||
|
|||
public CompositorDrawingContextProxy(IDrawingContextImpl impl, VisualBrushRenderer visualBrushRenderer) |
|||
{ |
|||
_impl = impl; |
|||
_visualBrushRenderer = visualBrushRenderer; |
|||
} |
|||
|
|||
// This is a hack to make it work with the current way of handling visual brushes
|
|||
public CompositionDrawList? VisualBrushDrawList |
|||
{ |
|||
get => _visualBrushRenderer.VisualBrushDrawList; |
|||
set => _visualBrushRenderer.VisualBrushDrawList = value; |
|||
} |
|||
|
|||
public Matrix PostTransform { get; set; } = Matrix.Identity; |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_impl.Dispose(); |
|||
} |
|||
|
|||
Matrix _transform; |
|||
public Matrix Transform |
|||
{ |
|||
get => _transform; |
|||
set => _impl.Transform = (_transform = value) * PostTransform; |
|||
} |
|||
|
|||
public void Clear(Color color) |
|||
{ |
|||
_impl.Clear(color); |
|||
} |
|||
|
|||
public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, |
|||
BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) |
|||
{ |
|||
_impl.DrawBitmap(source, opacity, sourceRect, destRect, bitmapInterpolationMode); |
|||
} |
|||
|
|||
public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) |
|||
{ |
|||
_impl.DrawBitmap(source, opacityMask, opacityMaskRect, destRect); |
|||
} |
|||
|
|||
public void DrawLine(IPen pen, Point p1, Point p2) |
|||
{ |
|||
_impl.DrawLine(pen, p1, p2); |
|||
} |
|||
|
|||
public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) |
|||
{ |
|||
_impl.DrawGeometry(brush, pen, geometry); |
|||
} |
|||
|
|||
public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default) |
|||
{ |
|||
_impl.DrawRectangle(brush, pen, rect, boxShadows); |
|||
} |
|||
|
|||
public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) |
|||
{ |
|||
_impl.DrawEllipse(brush, pen, rect); |
|||
} |
|||
|
|||
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) |
|||
{ |
|||
_impl.DrawGlyphRun(foreground, glyphRun); |
|||
} |
|||
|
|||
public IDrawingContextLayerImpl CreateLayer(Size size) |
|||
{ |
|||
return _impl.CreateLayer(size); |
|||
} |
|||
|
|||
public void PushClip(Rect clip) |
|||
{ |
|||
_impl.PushClip(clip); |
|||
} |
|||
|
|||
public void PushClip(RoundedRect clip) |
|||
{ |
|||
_impl.PushClip(clip); |
|||
} |
|||
|
|||
public void PopClip() |
|||
{ |
|||
_impl.PopClip(); |
|||
} |
|||
|
|||
public void PushOpacity(double opacity) |
|||
{ |
|||
_impl.PushOpacity(opacity); |
|||
} |
|||
|
|||
public void PopOpacity() |
|||
{ |
|||
_impl.PopOpacity(); |
|||
} |
|||
|
|||
public void PushOpacityMask(IBrush mask, Rect bounds) |
|||
{ |
|||
_impl.PushOpacityMask(mask, bounds); |
|||
} |
|||
|
|||
public void PopOpacityMask() |
|||
{ |
|||
_impl.PopOpacityMask(); |
|||
} |
|||
|
|||
public void PushGeometryClip(IGeometryImpl clip) |
|||
{ |
|||
_impl.PushGeometryClip(clip); |
|||
} |
|||
|
|||
public void PopGeometryClip() |
|||
{ |
|||
_impl.PopGeometryClip(); |
|||
} |
|||
|
|||
public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) |
|||
{ |
|||
_impl.PushBitmapBlendMode(blendingMode); |
|||
} |
|||
|
|||
public void PopBitmapBlendMode() |
|||
{ |
|||
_impl.PopBitmapBlendMode(); |
|||
} |
|||
|
|||
public void Custom(ICustomDrawOperation custom) |
|||
{ |
|||
_impl.Custom(custom); |
|||
} |
|||
|
|||
public class VisualBrushRenderer : IVisualBrushRenderer |
|||
{ |
|||
public CompositionDrawList? VisualBrushDrawList { get; set; } |
|||
public Size GetRenderTargetSize(IVisualBrush brush) |
|||
{ |
|||
return VisualBrushDrawList?.Size ?? Size.Empty; |
|||
} |
|||
|
|||
public void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) |
|||
{ |
|||
if (VisualBrushDrawList != null) |
|||
{ |
|||
foreach (var cmd in VisualBrushDrawList) |
|||
cmd.Item.Render(context); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) |
|||
{ |
|||
if (_impl is IDrawingContextWithAcrylicLikeSupport acrylic) |
|||
acrylic.DrawRectangle(material, rect); |
|||
} |
|||
} |
|||
@ -0,0 +1,76 @@ |
|||
using System; |
|||
using System.Diagnostics; |
|||
using System.Globalization; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.TextFormatting; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Server; |
|||
|
|||
/// <summary>
|
|||
/// An FPS counter helper that can draw itself on the render thread
|
|||
/// </summary>
|
|||
internal class FpsCounter |
|||
{ |
|||
private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); |
|||
private int _framesThisSecond; |
|||
private int _totalFrames; |
|||
private int _fps; |
|||
private TimeSpan _lastFpsUpdate; |
|||
const int FirstChar = 32; |
|||
const int LastChar = 126; |
|||
// ASCII chars
|
|||
private GlyphRun[] _runs = new GlyphRun[LastChar - FirstChar + 1]; |
|||
|
|||
public FpsCounter(GlyphTypeface typeface) |
|||
{ |
|||
for (var c = FirstChar; c <= LastChar; c++) |
|||
{ |
|||
var s = new string((char)c, 1); |
|||
var glyph = typeface.GetGlyph((uint)(s[0])); |
|||
_runs[c - FirstChar] = new GlyphRun(typeface, 18, new ReadOnlySlice<char>(s.AsMemory()), new ushort[] { glyph }); |
|||
} |
|||
} |
|||
|
|||
public void FpsTick() => _framesThisSecond++; |
|||
|
|||
public void RenderFps(IDrawingContextImpl context, string aux) |
|||
{ |
|||
var now = _stopwatch.Elapsed; |
|||
var elapsed = now - _lastFpsUpdate; |
|||
|
|||
++_framesThisSecond; |
|||
++_totalFrames; |
|||
|
|||
if (elapsed.TotalSeconds > 1) |
|||
{ |
|||
_fps = (int)(_framesThisSecond / elapsed.TotalSeconds); |
|||
_framesThisSecond = 0; |
|||
_lastFpsUpdate = now; |
|||
} |
|||
|
|||
var fpsLine = $"Frame #{_totalFrames:00000000} FPS: {_fps:000} " + aux; |
|||
double width = 0; |
|||
double height = 0; |
|||
foreach (var ch in fpsLine) |
|||
{ |
|||
var run = _runs[ch - FirstChar]; |
|||
width += run.Size.Width; |
|||
height = Math.Max(height, run.Size.Height); |
|||
} |
|||
|
|||
var rect = new Rect(0, 0, width + 3, height + 3); |
|||
|
|||
context.DrawRectangle(Brushes.Black, null, rect); |
|||
|
|||
double offset = 0; |
|||
foreach (var ch in fpsLine) |
|||
{ |
|||
var run = _runs[ch - FirstChar]; |
|||
context.Transform = Matrix.CreateTranslation(offset, 0); |
|||
context.DrawGlyphRun(Brushes.White, run); |
|||
offset += run.Size.Width; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
namespace Avalonia.Rendering.Composition.Server |
|||
{ |
|||
/// <summary>
|
|||
/// A helper class used to manage the current slots for writing data from the render thread
|
|||
/// and reading it from the UI thread.
|
|||
/// Used mostly by hit-testing which needs to know the last transform of the visual
|
|||
/// </summary>
|
|||
internal class ReadbackIndices |
|||
{ |
|||
private readonly object _lock = new object(); |
|||
public int ReadIndex { get; private set; } = 0; |
|||
public int WriteIndex { get; private set; } = 1; |
|||
public int WrittenIndex { get; private set; } = 0; |
|||
public ulong ReadRevision { get; private set; } |
|||
public ulong LastWrittenRevision { get; private set; } |
|||
|
|||
public void NextRead() |
|||
{ |
|||
lock (_lock) |
|||
{ |
|||
if (ReadRevision < LastWrittenRevision) |
|||
{ |
|||
ReadIndex = WrittenIndex; |
|||
ReadRevision = LastWrittenRevision; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void CompleteWrite(ulong writtenRevision) |
|||
{ |
|||
lock (_lock) |
|||
{ |
|||
for (var c = 0; c < 3; c++) |
|||
{ |
|||
if (c != WriteIndex && c != ReadIndex) |
|||
{ |
|||
WrittenIndex = WriteIndex; |
|||
LastWrittenRevision = writtenRevision; |
|||
WriteIndex = c; |
|||
return; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
using System.Numerics; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Server |
|||
{ |
|||
/// <summary>
|
|||
/// Server-side counterpart of <see cref="CompositionContainerVisual"/>.
|
|||
/// Mostly propagates update and render calls, but is also responsible
|
|||
/// for updating adorners in deferred manner
|
|||
/// </summary>
|
|||
internal partial class ServerCompositionContainerVisual : ServerCompositionVisual |
|||
{ |
|||
public ServerCompositionVisualCollection Children { get; private set; } = null!; |
|||
|
|||
protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) |
|||
{ |
|||
base.RenderCore(canvas, currentTransformedClip); |
|||
|
|||
foreach (var ch in Children) |
|||
{ |
|||
ch.Render(canvas, currentTransformedClip); |
|||
} |
|||
} |
|||
|
|||
public override void Update(ServerCompositionTarget root) |
|||
{ |
|||
base.Update(root); |
|||
foreach (var child in Children) |
|||
{ |
|||
if (child.AdornedVisual != null) |
|||
root.EnqueueAdornerUpdate(child); |
|||
else |
|||
child.Update(root); |
|||
} |
|||
|
|||
IsDirtyComposition = false; |
|||
} |
|||
|
|||
partial void Initialize() |
|||
{ |
|||
Children = new ServerCompositionVisualCollection(Compositor); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,75 @@ |
|||
using System; |
|||
using System.Numerics; |
|||
using Avalonia.Collections.Pooled; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering.Composition.Drawing; |
|||
using Avalonia.Rendering.Composition.Transport; |
|||
using Avalonia.Rendering.SceneGraph; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Server; |
|||
|
|||
/// <summary>
|
|||
/// Server-side counterpart of <see cref="CompositionDrawListVisual"/>
|
|||
/// </summary>
|
|||
internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisual |
|||
{ |
|||
#if DEBUG
|
|||
// This is needed for debugging purposes so we could see inspect the associated visual from debugger
|
|||
public readonly Visual UiVisual; |
|||
#endif
|
|||
private CompositionDrawList? _renderCommands; |
|||
|
|||
public ServerCompositionDrawListVisual(ServerCompositor compositor, Visual v) : base(compositor) |
|||
{ |
|||
#if DEBUG
|
|||
UiVisual = v; |
|||
#endif
|
|||
} |
|||
|
|||
Rect? _contentBounds; |
|||
|
|||
public override Rect OwnContentBounds |
|||
{ |
|||
get |
|||
{ |
|||
if (_contentBounds == null) |
|||
{ |
|||
var rect = Rect.Empty; |
|||
if(_renderCommands!=null) |
|||
foreach (var cmd in _renderCommands) |
|||
rect = rect.Union(cmd.Item.Bounds); |
|||
_contentBounds = rect; |
|||
} |
|||
|
|||
return _contentBounds.Value; |
|||
} |
|||
} |
|||
|
|||
protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) |
|||
{ |
|||
if (reader.Read<byte>() == 1) |
|||
{ |
|||
_renderCommands?.Dispose(); |
|||
_renderCommands = reader.ReadObject<CompositionDrawList?>(); |
|||
_contentBounds = null; |
|||
} |
|||
base.DeserializeChangesCore(reader, commitedAt); |
|||
} |
|||
|
|||
protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) |
|||
{ |
|||
if (_renderCommands != null) |
|||
{ |
|||
_renderCommands.Render(canvas); |
|||
} |
|||
base.RenderCore(canvas, currentTransformedClip); |
|||
} |
|||
|
|||
#if DEBUG
|
|||
public override string ToString() |
|||
{ |
|||
return UiVisual.GetType().ToString(); |
|||
} |
|||
#endif
|
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
namespace Avalonia.Rendering.Composition.Server |
|||
{ |
|||
internal abstract class ServerCompositionSurface : ServerObject |
|||
{ |
|||
protected ServerCompositionSurface(ServerCompositor compositor) : base(compositor) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,220 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Numerics; |
|||
using System.Threading; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Imaging; |
|||
using Avalonia.Media.Immutable; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering.Composition.Transport; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Server |
|||
{ |
|||
/// <summary>
|
|||
/// Server-side counterpart of the <see cref="CompositionTarget"/>
|
|||
/// That's the place where we update visual transforms, track dirty rects and actually do rendering
|
|||
/// </summary>
|
|||
internal partial class ServerCompositionTarget : IDisposable |
|||
{ |
|||
private readonly ServerCompositor _compositor; |
|||
private readonly Func<IRenderTarget> _renderTargetFactory; |
|||
private static long s_nextId = 1; |
|||
public long Id { get; } |
|||
public ulong Revision { get; private set; } |
|||
private IRenderTarget? _renderTarget; |
|||
private FpsCounter _fpsCounter = new FpsCounter(Typeface.Default.GlyphTypeface); |
|||
private Rect _dirtyRect; |
|||
private Random _random = new(); |
|||
private Size _layerSize; |
|||
private IDrawingContextLayerImpl? _layer; |
|||
private bool _redrawRequested; |
|||
private bool _disposed; |
|||
private HashSet<ServerCompositionVisual> _attachedVisuals = new(); |
|||
private Queue<ServerCompositionVisual> _adornerUpdateQueue = new(); |
|||
|
|||
|
|||
public ReadbackIndices Readback { get; } = new(); |
|||
public int RenderedVisuals { get; set; } |
|||
|
|||
public ServerCompositionTarget(ServerCompositor compositor, Func<IRenderTarget> renderTargetFactory) : |
|||
base(compositor) |
|||
{ |
|||
_compositor = compositor; |
|||
_renderTargetFactory = renderTargetFactory; |
|||
Id = Interlocked.Increment(ref s_nextId); |
|||
} |
|||
|
|||
partial void OnIsEnabledChanged() |
|||
{ |
|||
if (IsEnabled) |
|||
{ |
|||
_compositor.AddCompositionTarget(this); |
|||
foreach (var v in _attachedVisuals) |
|||
v.Activate(); |
|||
} |
|||
else |
|||
{ |
|||
_compositor.RemoveCompositionTarget(this); |
|||
foreach (var v in _attachedVisuals) |
|||
v.Deactivate(); |
|||
} |
|||
} |
|||
|
|||
partial void DeserializeChangesExtra(BatchStreamReader c) |
|||
{ |
|||
_redrawRequested = true; |
|||
} |
|||
|
|||
public void Render() |
|||
{ |
|||
if (_disposed) |
|||
{ |
|||
Compositor.RemoveCompositionTarget(this); |
|||
return; |
|||
} |
|||
|
|||
if (Root == null) |
|||
return; |
|||
_renderTarget ??= _renderTargetFactory(); |
|||
|
|||
Compositor.UpdateServerTime(); |
|||
|
|||
if(_dirtyRect.IsEmpty && !_redrawRequested) |
|||
return; |
|||
|
|||
Revision++; |
|||
|
|||
// Update happens in a separate phase to extend dirty rect if needed
|
|||
Root.Update(this); |
|||
|
|||
while (_adornerUpdateQueue.Count > 0) |
|||
{ |
|||
var adorner = _adornerUpdateQueue.Dequeue(); |
|||
adorner.Update(this); |
|||
} |
|||
|
|||
Readback.CompleteWrite(Revision); |
|||
|
|||
_redrawRequested = false; |
|||
using (var targetContext = _renderTarget.CreateDrawingContext(null)) |
|||
{ |
|||
var layerSize = Size * Scaling; |
|||
if (layerSize != _layerSize || _layer == null) |
|||
{ |
|||
_layer?.Dispose(); |
|||
_layer = null; |
|||
_layer = targetContext.CreateLayer(Size); |
|||
_layerSize = layerSize; |
|||
} |
|||
|
|||
if (!_dirtyRect.IsEmpty) |
|||
{ |
|||
var visualBrushHelper = new CompositorDrawingContextProxy.VisualBrushRenderer(); |
|||
using (var context = _layer.CreateDrawingContext(visualBrushHelper)) |
|||
{ |
|||
context.PushClip(_dirtyRect); |
|||
context.Clear(Colors.Transparent); |
|||
Root.Render(new CompositorDrawingContextProxy(context, visualBrushHelper), _dirtyRect); |
|||
context.PopClip(); |
|||
} |
|||
} |
|||
|
|||
targetContext.Clear(Colors.Transparent); |
|||
targetContext.Transform = Matrix.Identity; |
|||
if (_layer.CanBlit) |
|||
_layer.Blit(targetContext); |
|||
else |
|||
targetContext.DrawBitmap(RefCountable.CreateUnownedNotClonable(_layer), 1, |
|||
new Rect(_layerSize), |
|||
new Rect(Size), BitmapInterpolationMode.LowQuality); |
|||
|
|||
|
|||
if (DrawDirtyRects) |
|||
{ |
|||
targetContext.DrawRectangle(new ImmutableSolidColorBrush( |
|||
new Color(30, (byte)_random.Next(255), (byte)_random.Next(255), |
|||
(byte)_random.Next(255))) |
|||
, null, _dirtyRect); |
|||
} |
|||
|
|||
if (DrawFps) |
|||
{ |
|||
var nativeMem = ByteSizeHelper.ToString((ulong)( |
|||
(Compositor.BatchMemoryPool.CurrentUsage + Compositor.BatchMemoryPool.CurrentPool) * |
|||
Compositor.BatchMemoryPool.BufferSize), false); |
|||
var managedMem = ByteSizeHelper.ToString((ulong)( |
|||
(Compositor.BatchObjectPool.CurrentUsage + Compositor.BatchObjectPool.CurrentPool) * |
|||
Compositor.BatchObjectPool.ArraySize * |
|||
IntPtr.Size), false); |
|||
_fpsCounter.RenderFps(targetContext, $"M:{managedMem} / N:{nativeMem} R:{RenderedVisuals:0000}"); |
|||
} |
|||
RenderedVisuals = 0; |
|||
|
|||
_dirtyRect = Rect.Empty; |
|||
} |
|||
} |
|||
|
|||
public Rect SnapToDevicePixels(Rect rect) => SnapToDevicePixels(rect, Scaling); |
|||
|
|||
private static Rect SnapToDevicePixels(Rect rect, double scale) |
|||
{ |
|||
return new Rect( |
|||
new Point( |
|||
Math.Floor(rect.X * scale) / scale, |
|||
Math.Floor(rect.Y * scale) / scale), |
|||
new Point( |
|||
Math.Ceiling(rect.Right * scale) / scale, |
|||
Math.Ceiling(rect.Bottom * scale) / scale)); |
|||
} |
|||
|
|||
public void AddDirtyRect(Rect rect) |
|||
{ |
|||
if(rect.IsEmpty) |
|||
return; |
|||
var snapped = SnapToDevicePixels(rect, Scaling); |
|||
_dirtyRect = _dirtyRect.Union(snapped); |
|||
_redrawRequested = true; |
|||
} |
|||
|
|||
public void Invalidate() |
|||
{ |
|||
_redrawRequested = true; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if(_disposed) |
|||
return; |
|||
_disposed = true; |
|||
using (_compositor.GpuContext?.EnsureCurrent()) |
|||
{ |
|||
if (_layer != null) |
|||
{ |
|||
_layer.Dispose(); |
|||
_layer = null; |
|||
} |
|||
|
|||
_renderTarget?.Dispose(); |
|||
_renderTarget = null; |
|||
} |
|||
_compositor.RemoveCompositionTarget(this); |
|||
} |
|||
|
|||
public void AddVisual(ServerCompositionVisual visual) |
|||
{ |
|||
if (_attachedVisuals.Add(visual) && IsEnabled) |
|||
visual.Activate(); |
|||
} |
|||
|
|||
public void RemoveVisual(ServerCompositionVisual visual) |
|||
{ |
|||
if (_attachedVisuals.Remove(visual) && IsEnabled) |
|||
visual.Deactivate(); |
|||
if(visual.IsVisibleInFrame) |
|||
AddDirtyRect(visual.TransformedOwnContentBounds); |
|||
} |
|||
|
|||
public void EnqueueAdornerUpdate(ServerCompositionVisual visual) => _adornerUpdateQueue.Enqueue(visual); |
|||
} |
|||
} |
|||
@ -0,0 +1,76 @@ |
|||
namespace Avalonia.Rendering.Composition.Server; |
|||
|
|||
partial class ServerCompositionVisual |
|||
{ |
|||
protected bool IsDirtyComposition; |
|||
private bool _combinedTransformDirty; |
|||
private bool _clipSizeDirty; |
|||
|
|||
private const CompositionVisualChangedFields CompositionFieldsMask |
|||
= CompositionVisualChangedFields.Opacity |
|||
| CompositionVisualChangedFields.OpacityAnimated |
|||
| CompositionVisualChangedFields.OpacityMaskBrush |
|||
| CompositionVisualChangedFields.Clip |
|||
| CompositionVisualChangedFields.ClipToBounds |
|||
| CompositionVisualChangedFields.ClipToBoundsAnimated |
|||
| CompositionVisualChangedFields.Size |
|||
| CompositionVisualChangedFields.SizeAnimated; |
|||
|
|||
private const CompositionVisualChangedFields CombinedTransformFieldsMask = |
|||
CompositionVisualChangedFields.Size |
|||
| CompositionVisualChangedFields.SizeAnimated |
|||
| CompositionVisualChangedFields.AnchorPoint |
|||
| CompositionVisualChangedFields.AnchorPointAnimated |
|||
| CompositionVisualChangedFields.CenterPoint |
|||
| CompositionVisualChangedFields.CenterPointAnimated |
|||
| CompositionVisualChangedFields.AdornedVisual |
|||
| CompositionVisualChangedFields.TransformMatrix |
|||
| CompositionVisualChangedFields.Scale |
|||
| CompositionVisualChangedFields.ScaleAnimated |
|||
| CompositionVisualChangedFields.RotationAngle |
|||
| CompositionVisualChangedFields.RotationAngleAnimated |
|||
| CompositionVisualChangedFields.Orientation |
|||
| CompositionVisualChangedFields.OrientationAnimated |
|||
| CompositionVisualChangedFields.Offset |
|||
| CompositionVisualChangedFields.OffsetAnimated; |
|||
|
|||
private const CompositionVisualChangedFields ClipSizeDirtyMask = |
|||
CompositionVisualChangedFields.Size |
|||
| CompositionVisualChangedFields.SizeAnimated |
|||
| CompositionVisualChangedFields.ClipToBounds |
|||
| CompositionVisualChangedFields.ClipToBoundsAnimated; |
|||
|
|||
partial void OnFieldsDeserialized(CompositionVisualChangedFields changed) |
|||
{ |
|||
if ((changed & CompositionFieldsMask) != 0) |
|||
IsDirtyComposition = true; |
|||
if ((changed & CombinedTransformFieldsMask) != 0) |
|||
_combinedTransformDirty = true; |
|||
if ((changed & ClipSizeDirtyMask) != 0) |
|||
_clipSizeDirty = true; |
|||
} |
|||
|
|||
public override void NotifyAnimatedValueChanged(CompositionProperty offset) |
|||
{ |
|||
base.NotifyAnimatedValueChanged(offset); |
|||
if (offset == s_IdOfClipToBoundsProperty |
|||
|| offset == s_IdOfOpacityProperty |
|||
|| offset == s_IdOfSizeProperty) |
|||
IsDirtyComposition = true; |
|||
|
|||
if (offset == s_IdOfSizeProperty |
|||
|| offset == s_IdOfAnchorPointProperty |
|||
|| offset == s_IdOfCenterPointProperty |
|||
|| offset == s_IdOfAdornedVisualProperty |
|||
|| offset == s_IdOfTransformMatrixProperty |
|||
|| offset == s_IdOfScaleProperty |
|||
|| offset == s_IdOfRotationAngleProperty |
|||
|| offset == s_IdOfOrientationProperty |
|||
|| offset == s_IdOfOffsetProperty) |
|||
_combinedTransformDirty = true; |
|||
|
|||
if (offset == s_IdOfClipToBoundsProperty |
|||
|| offset == s_IdOfSizeProperty) |
|||
_clipSizeDirty = true; |
|||
} |
|||
} |
|||
@ -0,0 +1,246 @@ |
|||
using System; |
|||
using System.Numerics; |
|||
using Avalonia.Media; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering.Composition.Animations; |
|||
using Avalonia.Rendering.Composition.Transport; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Server |
|||
{ |
|||
/// <summary>
|
|||
/// Server-side <see cref="CompositionVisual"/> counterpart.
|
|||
/// Is responsible for computing the transformation matrix, for applying various visual
|
|||
/// properties before calling visual-specific drawing code and for notifying the
|
|||
/// <see cref="ServerCompositionTarget"/> for new dirty rects
|
|||
/// </summary>
|
|||
partial class ServerCompositionVisual : ServerObject |
|||
{ |
|||
private bool _isDirtyForUpdate; |
|||
private Rect _oldOwnContentBounds; |
|||
private bool _isBackface; |
|||
private Rect? _transformedClipBounds; |
|||
private Rect _combinedTransformedClipBounds; |
|||
|
|||
protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void Render(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) |
|||
{ |
|||
if(Visible == false || IsVisibleInFrame == false) |
|||
return; |
|||
if(Opacity == 0) |
|||
return; |
|||
|
|||
currentTransformedClip = currentTransformedClip.Intersect(_combinedTransformedClipBounds); |
|||
if(currentTransformedClip.IsEmpty) |
|||
return; |
|||
|
|||
Root!.RenderedVisuals++; |
|||
|
|||
var transform = GlobalTransformMatrix; |
|||
canvas.PostTransform = MatrixUtils.ToMatrix(transform); |
|||
canvas.Transform = Matrix.Identity; |
|||
if (Opacity != 1) |
|||
canvas.PushOpacity(Opacity); |
|||
var boundsRect = new Rect(new Size(Size.X, Size.Y)); |
|||
if(ClipToBounds) |
|||
canvas.PushClip(Root!.SnapToDevicePixels(boundsRect)); |
|||
if (Clip != null) |
|||
canvas.PushGeometryClip(Clip); |
|||
if(OpacityMaskBrush != null) |
|||
canvas.PushOpacityMask(OpacityMaskBrush, boundsRect); |
|||
|
|||
RenderCore(canvas, currentTransformedClip); |
|||
|
|||
// Hack to force invalidation of SKMatrix
|
|||
canvas.PostTransform = MatrixUtils.ToMatrix(transform); |
|||
canvas.Transform = Matrix.Identity; |
|||
|
|||
if (OpacityMaskBrush != null) |
|||
canvas.PopOpacityMask(); |
|||
if (Clip != null) |
|||
canvas.PopGeometryClip(); |
|||
if (ClipToBounds) |
|||
canvas.PopClip(); |
|||
if(Opacity != 1) |
|||
canvas.PopOpacity(); |
|||
} |
|||
|
|||
private ReadbackData _readback0, _readback1, _readback2; |
|||
|
|||
/// <summary>
|
|||
/// Obtains "readback" data - the data that is sent from the render thread to the UI thread
|
|||
/// in non-blocking manner. Used mostly by hit-testing
|
|||
/// </summary>
|
|||
public ref ReadbackData GetReadback(int idx) |
|||
{ |
|||
if (idx == 0) |
|||
return ref _readback0; |
|||
if (idx == 1) |
|||
return ref _readback1; |
|||
return ref _readback2; |
|||
} |
|||
|
|||
public Matrix4x4 CombinedTransformMatrix { get; private set; } = Matrix4x4.Identity; |
|||
public Matrix4x4 GlobalTransformMatrix { get; private set; } |
|||
|
|||
public virtual void Update(ServerCompositionTarget root) |
|||
{ |
|||
if(Parent == null && Root == null) |
|||
return; |
|||
|
|||
var wasVisible = IsVisibleInFrame; |
|||
|
|||
// Calculate new parent-relative transform
|
|||
if (_combinedTransformDirty) |
|||
{ |
|||
CombinedTransformMatrix = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, |
|||
// HACK: Ignore RenderTransform set by the adorner layer
|
|||
AdornedVisual != null ? Matrix4x4.Identity : TransformMatrix, |
|||
Scale, RotationAngle, Orientation, Offset); |
|||
_combinedTransformDirty = false; |
|||
} |
|||
|
|||
var parentTransform = (AdornedVisual ?? Parent)?.GlobalTransformMatrix ?? Matrix4x4.Identity; |
|||
|
|||
var newTransform = CombinedTransformMatrix * parentTransform; |
|||
|
|||
// Check if visual was moved and recalculate face orientation
|
|||
var positionChanged = false; |
|||
if (GlobalTransformMatrix != newTransform) |
|||
{ |
|||
_isBackface = Vector3.Transform( |
|||
new Vector3(0, 0, float.PositiveInfinity), GlobalTransformMatrix).Z <= 0; |
|||
positionChanged = true; |
|||
} |
|||
|
|||
var oldTransformedContentBounds = TransformedOwnContentBounds; |
|||
var oldCombinedTransformedClipBounds = _combinedTransformedClipBounds; |
|||
|
|||
var dirtyOldBounds = false; |
|||
if (_parent?.IsDirtyComposition == true) |
|||
{ |
|||
IsDirtyComposition = true; |
|||
_isDirtyForUpdate = true; |
|||
dirtyOldBounds = true; |
|||
} |
|||
|
|||
GlobalTransformMatrix = newTransform; |
|||
|
|||
var ownBounds = OwnContentBounds; |
|||
if (ownBounds != _oldOwnContentBounds || positionChanged) |
|||
{ |
|||
_oldOwnContentBounds = ownBounds; |
|||
if (ownBounds.IsEmpty) |
|||
TransformedOwnContentBounds = default; |
|||
else |
|||
TransformedOwnContentBounds = |
|||
ownBounds.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)); |
|||
} |
|||
|
|||
if (_clipSizeDirty || positionChanged) |
|||
{ |
|||
_transformedClipBounds = ClipToBounds |
|||
? new Rect(new Size(Size.X, Size.Y)) |
|||
.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)) |
|||
: null; |
|||
|
|||
_clipSizeDirty = false; |
|||
} |
|||
|
|||
_combinedTransformedClipBounds = Parent?._combinedTransformedClipBounds ?? new Rect(Root!.Size); |
|||
if (_transformedClipBounds != null) |
|||
_combinedTransformedClipBounds = _combinedTransformedClipBounds.Intersect(_transformedClipBounds.Value); |
|||
|
|||
EffectiveOpacity = Opacity * (Parent?.EffectiveOpacity ?? 1); |
|||
|
|||
IsVisibleInFrame = Visible && EffectiveOpacity > 0.04 && !_isBackface && |
|||
!_combinedTransformedClipBounds.IsEmpty; |
|||
|
|||
if (wasVisible != IsVisibleInFrame) |
|||
_isDirtyForUpdate = true; |
|||
|
|||
// Invalidate previous rect and queue new rect based on visibility
|
|||
if (positionChanged) |
|||
{ |
|||
if (wasVisible) |
|||
dirtyOldBounds = true; |
|||
|
|||
if (IsVisibleInFrame) |
|||
_isDirtyForUpdate = true; |
|||
} |
|||
|
|||
// Invalidate new bounds
|
|||
if (IsVisibleInFrame && _isDirtyForUpdate) |
|||
{ |
|||
dirtyOldBounds = true; |
|||
AddDirtyRect(TransformedOwnContentBounds.Intersect(_combinedTransformedClipBounds)); |
|||
} |
|||
|
|||
if (dirtyOldBounds && wasVisible) |
|||
AddDirtyRect(oldTransformedContentBounds.Intersect(oldCombinedTransformedClipBounds)); |
|||
|
|||
|
|||
_isDirtyForUpdate = false; |
|||
|
|||
// Update readback indices
|
|||
var i = Root!.Readback; |
|||
ref var readback = ref GetReadback(i.WriteIndex); |
|||
readback.Revision = root.Revision; |
|||
readback.Matrix = CombinedTransformMatrix; |
|||
readback.TargetId = Root.Id; |
|||
readback.Visible = IsVisibleInFrame; |
|||
} |
|||
|
|||
void AddDirtyRect(Rect rc) |
|||
{ |
|||
if(rc == Rect.Empty) |
|||
return; |
|||
Root?.AddDirtyRect(rc); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Data that can be read from the UI thread
|
|||
/// </summary>
|
|||
public struct ReadbackData |
|||
{ |
|||
public Matrix4x4 Matrix; |
|||
public ulong Revision; |
|||
public long TargetId; |
|||
public bool Visible; |
|||
} |
|||
|
|||
partial void DeserializeChangesExtra(BatchStreamReader c) |
|||
{ |
|||
ValuesInvalidated(); |
|||
} |
|||
|
|||
partial void OnRootChanging() |
|||
{ |
|||
if (Root != null) |
|||
Root.RemoveVisual(this); |
|||
} |
|||
|
|||
partial void OnRootChanged() |
|||
{ |
|||
if (Root != null) |
|||
Root.AddVisual(this); |
|||
} |
|||
|
|||
protected override void ValuesInvalidated() |
|||
{ |
|||
_isDirtyForUpdate = true; |
|||
Root?.Invalidate(); |
|||
} |
|||
|
|||
public bool IsVisibleInFrame { get; set; } |
|||
public double EffectiveOpacity { get; set; } |
|||
public Rect TransformedOwnContentBounds { get; set; } |
|||
public virtual Rect OwnContentBounds => new Rect(0, 0, Size.X, Size.Y); |
|||
} |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,140 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering.Composition.Animations; |
|||
using Avalonia.Rendering.Composition.Expressions; |
|||
using Avalonia.Rendering.Composition.Transport; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Server |
|||
{ |
|||
/// <summary>
|
|||
/// Server-side counterpart of the <see cref="Compositor"/>.
|
|||
/// 1) manages deserialization of changes received from the UI thread
|
|||
/// 2) triggers animation ticks
|
|||
/// 3) asks composition targets to render themselves
|
|||
/// </summary>
|
|||
internal class ServerCompositor : IRenderLoopTask |
|||
{ |
|||
private readonly IRenderLoop _renderLoop; |
|||
private readonly Queue<Batch> _batches = new Queue<Batch>(); |
|||
public long LastBatchId { get; private set; } |
|||
public Stopwatch Clock { get; } = Stopwatch.StartNew(); |
|||
public TimeSpan ServerNow { get; private set; } |
|||
private List<ServerCompositionTarget> _activeTargets = new(); |
|||
private HashSet<IAnimationInstance> _activeAnimations = new(); |
|||
private List<IAnimationInstance> _animationsToUpdate = new(); |
|||
internal BatchStreamObjectPool<object?> BatchObjectPool; |
|||
internal BatchStreamMemoryPool BatchMemoryPool; |
|||
private object _lock = new object(); |
|||
public IPlatformGpuContext? GpuContext { get; } |
|||
|
|||
public ServerCompositor(IRenderLoop renderLoop, IPlatformGpu? platformGpu, |
|||
BatchStreamObjectPool<object?> batchObjectPool, BatchStreamMemoryPool batchMemoryPool) |
|||
{ |
|||
GpuContext = platformGpu?.PrimaryContext; |
|||
_renderLoop = renderLoop; |
|||
BatchObjectPool = batchObjectPool; |
|||
BatchMemoryPool = batchMemoryPool; |
|||
_renderLoop.Add(this); |
|||
} |
|||
|
|||
public void EnqueueBatch(Batch batch) |
|||
{ |
|||
lock (_batches) |
|||
_batches.Enqueue(batch); |
|||
} |
|||
|
|||
internal void UpdateServerTime() => ServerNow = Clock.Elapsed; |
|||
|
|||
List<Batch> _reusableToCompleteList = new(); |
|||
void ApplyPendingBatches() |
|||
{ |
|||
while (true) |
|||
{ |
|||
Batch batch; |
|||
lock (_batches) |
|||
{ |
|||
if(_batches.Count == 0) |
|||
break; |
|||
batch = _batches.Dequeue(); |
|||
} |
|||
|
|||
using (var stream = new BatchStreamReader(batch.Changes, BatchMemoryPool, BatchObjectPool)) |
|||
{ |
|||
while (!stream.IsObjectEof) |
|||
{ |
|||
var target = (ServerObject)stream.ReadObject()!; |
|||
target.DeserializeChanges(stream, batch); |
|||
#if DEBUG_COMPOSITOR_SERIALIZATION
|
|||
if (stream.ReadObject() != BatchStreamDebugMarkers.ObjectEndMarker) |
|||
throw new InvalidOperationException( |
|||
$"Object {target.GetType()} failed to deserialize properly on object stream"); |
|||
if(stream.Read<Guid>() != BatchStreamDebugMarkers.ObjectEndMagic) |
|||
throw new InvalidOperationException( |
|||
$"Object {target.GetType()} failed to deserialize properly on data stream"); |
|||
#endif
|
|||
} |
|||
} |
|||
|
|||
_reusableToCompleteList.Add(batch); |
|||
LastBatchId = batch.SequenceId; |
|||
} |
|||
} |
|||
|
|||
void CompletePendingBatches() |
|||
{ |
|||
foreach(var batch in _reusableToCompleteList) |
|||
batch.Complete(); |
|||
_reusableToCompleteList.Clear(); |
|||
} |
|||
|
|||
bool IRenderLoopTask.NeedsUpdate => false; |
|||
|
|||
void IRenderLoopTask.Update(TimeSpan time) |
|||
{ |
|||
} |
|||
|
|||
public void Render() |
|||
{ |
|||
lock (_lock) |
|||
{ |
|||
RenderCore(); |
|||
} |
|||
} |
|||
|
|||
private void RenderCore() |
|||
{ |
|||
ApplyPendingBatches(); |
|||
|
|||
foreach(var animation in _activeAnimations) |
|||
_animationsToUpdate.Add(animation); |
|||
|
|||
foreach(var animation in _animationsToUpdate) |
|||
animation.Invalidate(); |
|||
|
|||
_animationsToUpdate.Clear(); |
|||
|
|||
foreach (var t in _activeTargets) |
|||
t.Render(); |
|||
|
|||
CompletePendingBatches(); |
|||
} |
|||
|
|||
public void AddCompositionTarget(ServerCompositionTarget target) |
|||
{ |
|||
_activeTargets.Add(target); |
|||
} |
|||
|
|||
public void RemoveCompositionTarget(ServerCompositionTarget target) |
|||
{ |
|||
_activeTargets.Remove(target); |
|||
} |
|||
|
|||
public void AddToClock(IAnimationInstance animationInstance) => |
|||
_activeAnimations.Add(animationInstance); |
|||
|
|||
public void RemoveFromClock(IAnimationInstance animationInstance) => |
|||
_activeAnimations.Remove(animationInstance); |
|||
} |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Rendering.Composition.Transport; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Server |
|||
{ |
|||
/// <summary>
|
|||
/// A server-side list container capable of receiving changes from the UI thread
|
|||
/// Right now it's quite dumb since it always receives the full list
|
|||
/// </summary>
|
|||
class ServerList<T> : ServerObject where T : ServerObject |
|||
{ |
|||
public List<T> List { get; } = new List<T>(); |
|||
|
|||
protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) |
|||
{ |
|||
if (reader.Read<byte>() == 1) |
|||
{ |
|||
List.Clear(); |
|||
var count = reader.Read<int>(); |
|||
for (var c = 0; c < count; c++) |
|||
List.Add(reader.ReadObject<T>()); |
|||
} |
|||
base.DeserializeChangesCore(reader, commitedAt); |
|||
} |
|||
|
|||
public override long LastChangedBy |
|||
{ |
|||
get |
|||
{ |
|||
var seq = base.LastChangedBy; |
|||
foreach (var i in List) |
|||
seq = Math.Max(i.LastChangedBy, seq); |
|||
return seq; |
|||
} |
|||
} |
|||
|
|||
public List<T>.Enumerator GetEnumerator() => List.GetEnumerator(); |
|||
|
|||
public ServerList(ServerCompositor compositor) : base(compositor) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,180 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia.Rendering.Composition.Animations; |
|||
using Avalonia.Rendering.Composition.Expressions; |
|||
using Avalonia.Rendering.Composition.Transport; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Server |
|||
{ |
|||
/// <summary>
|
|||
/// Server-side <see cref="CompositionObject" /> counterpart.
|
|||
/// Is responsible for animation activation and invalidation
|
|||
/// </summary>
|
|||
internal abstract class ServerObject : IExpressionObject |
|||
{ |
|||
public ServerCompositor Compositor { get; } |
|||
|
|||
public virtual long LastChangedBy => ItselfLastChangedBy; |
|||
public long ItselfLastChangedBy { get; private set; } |
|||
private uint _activationCount; |
|||
public bool IsActive => _activationCount != 0; |
|||
private InlineDictionary<CompositionProperty, ServerObjectSubscriptionStore> _subscriptions; |
|||
private InlineDictionary<CompositionProperty, IAnimationInstance> _animations; |
|||
|
|||
private class ServerObjectSubscriptionStore |
|||
{ |
|||
public bool IsValid; |
|||
public RefTrackingDictionary<IAnimationInstance>? Subscribers; |
|||
|
|||
public void Invalidate() |
|||
{ |
|||
if (IsValid) |
|||
return; |
|||
IsValid = false; |
|||
if (Subscribers != null) |
|||
foreach (var sub in Subscribers) |
|||
sub.Key.Invalidate(); |
|||
} |
|||
} |
|||
|
|||
public ServerObject(ServerCompositor compositor) |
|||
{ |
|||
Compositor = compositor; |
|||
} |
|||
|
|||
public virtual ExpressionVariant GetPropertyForAnimation(string name) |
|||
{ |
|||
return default; |
|||
} |
|||
|
|||
ExpressionVariant IExpressionObject.GetProperty(string name) => GetPropertyForAnimation(name); |
|||
|
|||
public void Activate() |
|||
{ |
|||
_activationCount++; |
|||
if (_activationCount == 1) |
|||
Activated(); |
|||
} |
|||
|
|||
public void Deactivate() |
|||
{ |
|||
#if DEBUG
|
|||
if (_activationCount == 0) |
|||
throw new InvalidOperationException(); |
|||
#endif
|
|||
_activationCount--; |
|||
if (_activationCount == 0) |
|||
Deactivated(); |
|||
} |
|||
|
|||
protected void Activated() |
|||
{ |
|||
foreach(var kp in _animations) |
|||
kp.Value.Activate(); |
|||
} |
|||
|
|||
protected void Deactivated() |
|||
{ |
|||
foreach(var kp in _animations) |
|||
kp.Value.Deactivate(); |
|||
} |
|||
|
|||
void InvalidateSubscriptions(CompositionProperty property) |
|||
{ |
|||
if(_subscriptions.TryGetValue(property, out var subs)) |
|||
subs.Invalidate(); |
|||
} |
|||
|
|||
protected void SetValue<T>(CompositionProperty prop, out T field, T value) |
|||
{ |
|||
field = value; |
|||
InvalidateSubscriptions(prop); |
|||
} |
|||
|
|||
protected T GetValue<T>(CompositionProperty prop, ref T field) |
|||
{ |
|||
if (_subscriptions.TryGetValue(prop, out var subs)) |
|||
subs.IsValid = true; |
|||
return field; |
|||
} |
|||
|
|||
protected void SetAnimatedValue<T>(CompositionProperty prop, ref T field, |
|||
TimeSpan commitedAt, IAnimationInstance animation) where T : struct |
|||
{ |
|||
if (IsActive && _animations.TryGetValue(prop, out var oldAnimation)) |
|||
oldAnimation.Deactivate(); |
|||
_animations[prop] = animation; |
|||
|
|||
animation.Initialize(commitedAt, ExpressionVariant.Create(field), prop); |
|||
if(IsActive) |
|||
animation.Activate(); |
|||
|
|||
InvalidateSubscriptions(prop); |
|||
} |
|||
|
|||
protected void SetAnimatedValue<T>(CompositionProperty property, out T field, T value) |
|||
{ |
|||
if (_animations.TryGetAndRemoveValue(property, out var animation) && IsActive) |
|||
animation.Deactivate(); |
|||
field = value; |
|||
InvalidateSubscriptions(property); |
|||
} |
|||
|
|||
protected T GetAnimatedValue<T>(CompositionProperty property, ref T field) where T : struct |
|||
{ |
|||
if (_subscriptions.TryGetValue(property, out var subscriptions)) |
|||
subscriptions.IsValid = true; |
|||
|
|||
if (_animations.TryGetValue(property, out var animation)) |
|||
field = animation.Evaluate(Compositor.ServerNow, ExpressionVariant.Create(field)) |
|||
.CastOrDefault<T>(); |
|||
|
|||
return field; |
|||
} |
|||
|
|||
public virtual void NotifyAnimatedValueChanged(CompositionProperty prop) |
|||
{ |
|||
InvalidateSubscriptions(prop); |
|||
ValuesInvalidated(); |
|||
} |
|||
|
|||
protected virtual void ValuesInvalidated() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void SubscribeToInvalidation(CompositionProperty member, IAnimationInstance animation) |
|||
{ |
|||
if (!_subscriptions.TryGetValue(member, out var store)) |
|||
_subscriptions[member] = store = new ServerObjectSubscriptionStore(); |
|||
if (store.Subscribers == null) |
|||
store.Subscribers = new(); |
|||
store.Subscribers.AddRef(animation); |
|||
} |
|||
|
|||
public void UnsubscribeFromInvalidation(CompositionProperty member, IAnimationInstance animation) |
|||
{ |
|||
if(_subscriptions.TryGetValue(member, out var store)) |
|||
store.Subscribers?.ReleaseRef(animation); |
|||
} |
|||
|
|||
public virtual CompositionProperty? GetCompositionProperty(string fieldName) => null; |
|||
|
|||
protected virtual void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) |
|||
{ |
|||
if (this is IDisposable disp |
|||
&& reader.Read<byte>() == 1) |
|||
disp.Dispose(); |
|||
} |
|||
|
|||
public void DeserializeChanges(BatchStreamReader reader, Batch batch) |
|||
{ |
|||
DeserializeChangesCore(reader, batch.CommitedAt); |
|||
ValuesInvalidated(); |
|||
ItselfLastChangedBy = batch.SequenceId; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
using System; |
|||
using System.Collections.Concurrent; |
|||
using System.Collections.Generic; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Transport |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a group of serialized changes from the UI thread to be atomically applied at the render thread
|
|||
/// </summary>
|
|||
internal class Batch |
|||
{ |
|||
private static long _nextSequenceId = 1; |
|||
private static ConcurrentBag<BatchStreamData> _pool = new(); |
|||
public long SequenceId { get; } |
|||
|
|||
public Batch() |
|||
{ |
|||
SequenceId = Interlocked.Increment(ref _nextSequenceId); |
|||
if (!_pool.TryTake(out var lst)) |
|||
lst = new BatchStreamData(); |
|||
Changes = lst; |
|||
} |
|||
private TaskCompletionSource<int> _tcs = new TaskCompletionSource<int>(); |
|||
public BatchStreamData Changes { get; private set; } |
|||
public TimeSpan CommitedAt { get; set; } |
|||
|
|||
public void Complete() |
|||
{ |
|||
_pool.Add(Changes); |
|||
Changes = null!; |
|||
|
|||
_tcs.TrySetResult(0); |
|||
} |
|||
|
|||
public Task Completed => _tcs.Task; |
|||
} |
|||
} |
|||
@ -0,0 +1,184 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Runtime.CompilerServices; |
|||
using Avalonia.Rendering.Composition.Animations; |
|||
using Avalonia.Rendering.Composition.Server; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Transport; |
|||
|
|||
/// <summary>
|
|||
/// The batch data is separated into 2 "streams":
|
|||
/// - objects: CLR reference types that are references to either server-side or common objects
|
|||
/// - structs: blittable types like int, Matrix, Color
|
|||
/// Each "stream" consists of memory segments that are pooled
|
|||
/// </summary>
|
|||
internal class BatchStreamData |
|||
{ |
|||
public Queue<BatchStreamSegment<object?[]>> Objects { get; } = new(); |
|||
public Queue<BatchStreamSegment<IntPtr>> Structs { get; } = new(); |
|||
} |
|||
|
|||
public struct BatchStreamSegment<TData> |
|||
{ |
|||
public TData Data { get; set; } |
|||
public int ElementCount { get; set; } |
|||
} |
|||
|
|||
internal class BatchStreamWriter : IDisposable |
|||
{ |
|||
private readonly BatchStreamData _output; |
|||
private readonly BatchStreamMemoryPool _memoryPool; |
|||
private readonly BatchStreamObjectPool<object?> _objectPool; |
|||
|
|||
private BatchStreamSegment<object?[]?> _currentObjectSegment; |
|||
private BatchStreamSegment<IntPtr> _currentDataSegment; |
|||
|
|||
public BatchStreamWriter(BatchStreamData output, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool<object?> objectPool) |
|||
{ |
|||
_output = output; |
|||
_memoryPool = memoryPool; |
|||
_objectPool = objectPool; |
|||
} |
|||
|
|||
void CommitDataSegment() |
|||
{ |
|||
if (_currentDataSegment.Data != IntPtr.Zero) |
|||
_output.Structs.Enqueue(_currentDataSegment); |
|||
_currentDataSegment = new (); |
|||
} |
|||
|
|||
void NextDataSegment() |
|||
{ |
|||
CommitDataSegment(); |
|||
_currentDataSegment.Data = _memoryPool.Get(); |
|||
} |
|||
|
|||
void CommitObjectSegment() |
|||
{ |
|||
if (_currentObjectSegment.Data != null) |
|||
_output.Objects.Enqueue(_currentObjectSegment!); |
|||
_currentObjectSegment = new(); |
|||
} |
|||
|
|||
void NextObjectSegment() |
|||
{ |
|||
CommitObjectSegment(); |
|||
_currentObjectSegment.Data = _objectPool.Get(); |
|||
} |
|||
|
|||
public unsafe void Write<T>(T item) where T : unmanaged |
|||
{ |
|||
var size = Unsafe.SizeOf<T>(); |
|||
if (_currentDataSegment.Data == IntPtr.Zero || _currentDataSegment.ElementCount + size > _memoryPool.BufferSize) |
|||
NextDataSegment(); |
|||
Unsafe.WriteUnaligned<T>((byte*)_currentDataSegment.Data + _currentDataSegment.ElementCount, item); |
|||
_currentDataSegment.ElementCount += size; |
|||
} |
|||
|
|||
public void WriteObject(object? item) |
|||
{ |
|||
if (_currentObjectSegment.Data == null || |
|||
_currentObjectSegment.ElementCount >= _currentObjectSegment.Data.Length) |
|||
NextObjectSegment(); |
|||
_currentObjectSegment.Data![_currentObjectSegment.ElementCount] = item; |
|||
_currentObjectSegment.ElementCount++; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
CommitDataSegment(); |
|||
CommitObjectSegment(); |
|||
} |
|||
} |
|||
|
|||
internal class BatchStreamReader : IDisposable |
|||
{ |
|||
private readonly BatchStreamData _input; |
|||
private readonly BatchStreamMemoryPool _memoryPool; |
|||
private readonly BatchStreamObjectPool<object?> _objectPool; |
|||
|
|||
private BatchStreamSegment<object?[]?> _currentObjectSegment; |
|||
private BatchStreamSegment<IntPtr> _currentDataSegment; |
|||
private int _memoryOffset, _objectOffset; |
|||
|
|||
public BatchStreamReader(BatchStreamData input, BatchStreamMemoryPool memoryPool, BatchStreamObjectPool<object?> objectPool) |
|||
{ |
|||
_input = input; |
|||
_memoryPool = memoryPool; |
|||
_objectPool = objectPool; |
|||
} |
|||
|
|||
public unsafe T Read<T>() where T : unmanaged |
|||
{ |
|||
var size = Unsafe.SizeOf<T>(); |
|||
if (_currentDataSegment.Data == IntPtr.Zero) |
|||
{ |
|||
if (_input.Structs.Count == 0) |
|||
throw new EndOfStreamException(); |
|||
_currentDataSegment = _input.Structs.Dequeue(); |
|||
_memoryOffset = 0; |
|||
} |
|||
|
|||
if (_memoryOffset + size > _currentDataSegment.ElementCount) |
|||
throw new InvalidOperationException("Attempted to read more memory then left in the current segment"); |
|||
|
|||
var rv = Unsafe.ReadUnaligned<T>((byte*)_currentDataSegment.Data + _memoryOffset); |
|||
_memoryOffset += size; |
|||
if (_memoryOffset == _currentDataSegment.ElementCount) |
|||
{ |
|||
_memoryPool.Return(_currentDataSegment.Data); |
|||
_currentDataSegment = new(); |
|||
} |
|||
|
|||
return rv; |
|||
} |
|||
|
|||
public T ReadObject<T>() where T : class? => (T)ReadObject()!; |
|||
|
|||
public object? ReadObject() |
|||
{ |
|||
if (_currentObjectSegment.Data == null) |
|||
{ |
|||
if (_input.Objects.Count == 0) |
|||
throw new EndOfStreamException(); |
|||
_currentObjectSegment = _input.Objects.Dequeue()!; |
|||
_objectOffset = 0; |
|||
} |
|||
|
|||
var rv = _currentObjectSegment.Data![_objectOffset]; |
|||
_objectOffset++; |
|||
if (_objectOffset == _currentObjectSegment.ElementCount) |
|||
{ |
|||
_objectPool.Return(_currentObjectSegment.Data); |
|||
_currentObjectSegment = new(); |
|||
} |
|||
|
|||
return rv; |
|||
} |
|||
|
|||
public bool IsObjectEof => _currentObjectSegment.Data == null && _input.Objects.Count == 0; |
|||
|
|||
public bool IsStructEof => _currentDataSegment.Data == IntPtr.Zero && _input.Structs.Count == 0; |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (_currentDataSegment.Data != IntPtr.Zero) |
|||
{ |
|||
_memoryPool.Return(_currentDataSegment.Data); |
|||
_currentDataSegment = new(); |
|||
} |
|||
|
|||
while (_input.Structs.Count > 0) |
|||
_memoryPool.Return(_input.Structs.Dequeue().Data); |
|||
|
|||
if (_currentObjectSegment.Data != null) |
|||
{ |
|||
_objectPool.Return(_currentObjectSegment.Data); |
|||
_currentObjectSegment = new(); |
|||
} |
|||
|
|||
while (_input.Objects.Count > 0) |
|||
_objectPool.Return(_input.Objects.Dequeue().Data); |
|||
} |
|||
} |
|||
@ -0,0 +1,152 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Runtime.ConstrainedExecution; |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Transport; |
|||
|
|||
/// <summary>
|
|||
/// A pool that keeps a number of elements that was used in the last 10 seconds
|
|||
/// </summary>
|
|||
internal abstract class BatchStreamPoolBase<T> : IDisposable |
|||
{ |
|||
readonly Stack<T> _pool = new(); |
|||
bool _disposed; |
|||
int _usage; |
|||
readonly int[] _usageStatistics = new int[10]; |
|||
int _usageStatisticsSlot; |
|||
|
|||
public int CurrentUsage => _usage; |
|||
public int CurrentPool => _pool.Count; |
|||
|
|||
public BatchStreamPoolBase(bool needsFinalize, Action<Func<bool>>? startTimer = null) |
|||
{ |
|||
if(!needsFinalize) |
|||
GC.SuppressFinalize(needsFinalize); |
|||
|
|||
var updateRef = new WeakReference<BatchStreamPoolBase<T>>(this); |
|||
StartUpdateTimer(startTimer, updateRef); |
|||
} |
|||
|
|||
static void StartUpdateTimer(Action<Func<bool>>? startTimer, WeakReference<BatchStreamPoolBase<T>> updateRef) |
|||
{ |
|||
Func<bool> timerProc = () => |
|||
{ |
|||
if (updateRef.TryGetTarget(out var target)) |
|||
{ |
|||
target.UpdateStatistics(); |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
}; |
|||
if (startTimer != null) |
|||
startTimer(timerProc); |
|||
else |
|||
DispatcherTimer.Run(timerProc, TimeSpan.FromSeconds(1)); |
|||
} |
|||
|
|||
private void UpdateStatistics() |
|||
{ |
|||
lock (_pool) |
|||
{ |
|||
var maximumUsage = _usageStatistics.Max(); |
|||
var recentlyUsedPooledSlots = maximumUsage - _usage; |
|||
var keepSlots = Math.Max(recentlyUsedPooledSlots, 10); |
|||
while (keepSlots < _pool.Count) |
|||
DestroyItem(_pool.Pop()); |
|||
|
|||
_usageStatisticsSlot = (_usageStatisticsSlot + 1) % _usageStatistics.Length; |
|||
_usageStatistics[_usageStatisticsSlot] = 0; |
|||
} |
|||
} |
|||
|
|||
protected abstract T CreateItem(); |
|||
|
|||
protected virtual void DestroyItem(T item) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public T Get() |
|||
{ |
|||
lock (_pool) |
|||
{ |
|||
_usage++; |
|||
if (_usageStatistics[_usageStatisticsSlot] < _usage) |
|||
_usageStatistics[_usageStatisticsSlot] = _usage; |
|||
|
|||
if (_pool.Count != 0) |
|||
return _pool.Pop(); |
|||
} |
|||
|
|||
return CreateItem(); |
|||
} |
|||
|
|||
public void Return(T item) |
|||
{ |
|||
lock (_pool) |
|||
{ |
|||
_usage--; |
|||
if (!_disposed) |
|||
{ |
|||
_pool.Push(item); |
|||
return; |
|||
} |
|||
} |
|||
|
|||
DestroyItem(item); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
lock (_pool) |
|||
{ |
|||
_disposed = true; |
|||
foreach (var item in _pool) |
|||
DestroyItem(item); |
|||
_pool.Clear(); |
|||
} |
|||
} |
|||
|
|||
~BatchStreamPoolBase() |
|||
{ |
|||
Dispose(); |
|||
} |
|||
} |
|||
|
|||
internal sealed class BatchStreamObjectPool<T> : BatchStreamPoolBase<T[]> where T : class? |
|||
{ |
|||
public int ArraySize { get; } |
|||
|
|||
public BatchStreamObjectPool(int arraySize = 128, Action<Func<bool>>? startTimer = null) : base(false, startTimer) |
|||
{ |
|||
ArraySize = arraySize; |
|||
} |
|||
|
|||
protected override T[] CreateItem() |
|||
{ |
|||
return new T[ArraySize]; |
|||
} |
|||
|
|||
protected override void DestroyItem(T[] item) |
|||
{ |
|||
Array.Clear(item, 0, item.Length); |
|||
} |
|||
} |
|||
|
|||
internal sealed class BatchStreamMemoryPool : BatchStreamPoolBase<IntPtr> |
|||
{ |
|||
public int BufferSize { get; } |
|||
|
|||
public BatchStreamMemoryPool(int bufferSize = 1024, Action<Func<bool>>? startTimer = null) : base(true, startTimer) |
|||
{ |
|||
BufferSize = bufferSize; |
|||
} |
|||
|
|||
protected override IntPtr CreateItem() => Marshal.AllocHGlobal(BufferSize); |
|||
|
|||
protected override void DestroyItem(IntPtr item) => Marshal.FreeHGlobal(item); |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Transport; |
|||
|
|||
internal class BatchStreamDebugMarkers |
|||
{ |
|||
public static object ObjectEndMarker = new object(); |
|||
public static Guid ObjectEndMagic = Guid.NewGuid(); |
|||
} |
|||
@ -0,0 +1,98 @@ |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Rendering.Composition.Server; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Transport |
|||
{ |
|||
/// <summary>
|
|||
/// A helper class used from generated UI-thread-side collections of composition objects.
|
|||
/// </summary>
|
|||
// NOTE: This should probably be a base class since TServer isn't used anymore and it was the reason why
|
|||
// it couldn't be exposed as a base class
|
|||
class ServerListProxyHelper<TClient, TServer> : IList<TClient> |
|||
where TServer : ServerObject |
|||
where TClient : CompositionObject |
|||
{ |
|||
private readonly IRegisterForSerialization _parent; |
|||
private bool _changed; |
|||
|
|||
public interface IRegisterForSerialization |
|||
{ |
|||
void RegisterForSerialization(); |
|||
} |
|||
|
|||
public ServerListProxyHelper(IRegisterForSerialization parent) |
|||
{ |
|||
_parent = parent; |
|||
} |
|||
|
|||
private readonly List<TClient> _list = new List<TClient>(); |
|||
|
|||
IEnumerator<TClient> IEnumerable<TClient>.GetEnumerator() => GetEnumerator(); |
|||
public List<TClient>.Enumerator GetEnumerator() => _list.GetEnumerator(); |
|||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
|
|||
public void Add(TClient item) => Insert(_list.Count, item); |
|||
|
|||
public void Clear() |
|||
{ |
|||
_list.Clear(); |
|||
_changed = true; |
|||
_parent.RegisterForSerialization(); |
|||
} |
|||
|
|||
public bool Contains(TClient item) => _list.Contains(item); |
|||
|
|||
public void CopyTo(TClient[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex); |
|||
|
|||
public bool Remove(TClient item) |
|||
{ |
|||
var idx = _list.IndexOf(item); |
|||
if (idx == -1) |
|||
return false; |
|||
RemoveAt(idx); |
|||
return true; |
|||
} |
|||
|
|||
public int Count => _list.Count; |
|||
public bool IsReadOnly => false; |
|||
public int IndexOf(TClient item) => _list.IndexOf(item); |
|||
|
|||
public void Insert(int index, TClient item) |
|||
{ |
|||
_list.Insert(index, item); |
|||
_changed = true; |
|||
_parent.RegisterForSerialization(); |
|||
} |
|||
|
|||
public void RemoveAt(int index) |
|||
{ |
|||
_list.RemoveAt(index); |
|||
_changed = true; |
|||
_parent.RegisterForSerialization(); |
|||
} |
|||
|
|||
public TClient this[int index] |
|||
{ |
|||
get => _list[index]; |
|||
set |
|||
{ |
|||
_list[index] = value; |
|||
_changed = true; |
|||
_parent.RegisterForSerialization(); |
|||
} |
|||
} |
|||
|
|||
public void Serialize(BatchStreamWriter writer) |
|||
{ |
|||
writer.Write((byte)(_changed ? 1 : 0)); |
|||
if (_changed) |
|||
{ |
|||
writer.Write(_list.Count); |
|||
foreach (var el in _list) |
|||
writer.WriteObject(el.Server); |
|||
} |
|||
_changed = false; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
using System; |
|||
using System.Numerics; |
|||
using Avalonia.Media; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Rendering.Composition |
|||
{ |
|||
/// <summary>
|
|||
/// The base visual object in the composition visual hierarchy.
|
|||
/// </summary>
|
|||
public abstract partial class CompositionVisual |
|||
{ |
|||
private IBrush? _opacityMask; |
|||
|
|||
private protected virtual void OnRootChangedCore() |
|||
{ |
|||
} |
|||
|
|||
partial void OnRootChanged() => OnRootChangedCore(); |
|||
|
|||
partial void OnParentChanged() => Root = Parent?.Root; |
|||
|
|||
public IBrush? OpacityMask |
|||
{ |
|||
get => _opacityMask; |
|||
set |
|||
{ |
|||
if (_opacityMask == value) |
|||
return; |
|||
OpacityMaskBrush = (_opacityMask = value)?.ToImmutable(); |
|||
} |
|||
} |
|||
|
|||
internal Matrix4x4? TryGetServerTransform() |
|||
{ |
|||
if (Root == null) |
|||
return null; |
|||
var i = Root.Server.Readback; |
|||
ref var readback = ref Server.GetReadback(i.ReadIndex); |
|||
|
|||
// CompositionVisual wasn't visible or wasn't even attached to the composition target during the lat frame
|
|||
if (!readback.Visible || readback.Revision < i.ReadRevision) |
|||
return null; |
|||
|
|||
// CompositionVisual was reparented (potential race here)
|
|||
if (readback.TargetId != Root.Server.Id) |
|||
return null; |
|||
|
|||
return readback.Matrix; |
|||
} |
|||
|
|||
internal object? Tag { get; set; } |
|||
|
|||
internal virtual bool HitTest(Point point, Func<IVisual, bool>? filter) => true; |
|||
} |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
using System; |
|||
using Avalonia.Rendering.Composition.Server; |
|||
|
|||
namespace Avalonia.Rendering.Composition |
|||
{ |
|||
/// <summary>
|
|||
/// A collection of CompositionVisual objects
|
|||
/// </summary>
|
|||
public partial class CompositionVisualCollection : CompositionObject |
|||
{ |
|||
private CompositionVisual _owner; |
|||
internal CompositionVisualCollection(CompositionVisual parent, ServerCompositionVisualCollection server) : base(parent.Compositor, server) |
|||
{ |
|||
_owner = parent; |
|||
InitializeDefaults(); |
|||
} |
|||
|
|||
public void InsertAbove(CompositionVisual newChild, CompositionVisual sibling) |
|||
{ |
|||
var idx = _list.IndexOf(sibling); |
|||
if (idx == -1) |
|||
throw new InvalidOperationException(); |
|||
|
|||
Insert(idx + 1, newChild); |
|||
} |
|||
|
|||
public void InsertBelow(CompositionVisual newChild, CompositionVisual sibling) |
|||
{ |
|||
var idx = _list.IndexOf(sibling); |
|||
if (idx == -1) |
|||
throw new InvalidOperationException(); |
|||
Insert(idx, newChild); |
|||
} |
|||
|
|||
public void InsertAtTop(CompositionVisual newChild) => Insert(_list.Count, newChild); |
|||
|
|||
public void InsertAtBottom(CompositionVisual newChild) => Insert(0, newChild); |
|||
|
|||
public void RemoveAll() => Clear(); |
|||
|
|||
partial void OnAdded(CompositionVisual item) => item.Parent = _owner; |
|||
|
|||
partial void OnBeforeReplace(CompositionVisual oldItem, CompositionVisual newItem) |
|||
{ |
|||
if (oldItem != newItem) |
|||
OnBeforeAdded(newItem); |
|||
} |
|||
|
|||
partial void OnReplace(CompositionVisual oldItem, CompositionVisual newItem) |
|||
{ |
|||
if (oldItem != newItem) |
|||
{ |
|||
OnRemoved(oldItem); |
|||
OnAdded(newItem); |
|||
} |
|||
} |
|||
|
|||
partial void OnRemoved(CompositionVisual item) => item.Parent = null; |
|||
|
|||
partial void OnBeforeClear() |
|||
{ |
|||
foreach (var i in this) |
|||
i.Parent = null; |
|||
} |
|||
|
|||
partial void OnBeforeAdded(CompositionVisual item) |
|||
{ |
|||
if (item.Parent != null) |
|||
throw new InvalidOperationException("Visual already has a parent"); |
|||
item.Parent = item; |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue