committed by
GitHub
283 changed files with 15117 additions and 1094 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(); |
||||
|
} |
||||
|
} |
||||
@ -1,11 +1,11 @@ |
|||||
namespace Avalonia.Markup.Xaml |
namespace Avalonia.Metadata |
||||
{ |
{ |
||||
public interface IAddChild |
public interface IAddChild |
||||
{ |
{ |
||||
void AddChild(object child); |
void AddChild(object child); |
||||
} |
} |
||||
|
|
||||
public interface IAddChild<T> : IAddChild |
public interface IAddChild<T> |
||||
{ |
{ |
||||
void AddChild(T child); |
void AddChild(T child); |
||||
} |
} |
||||
@ -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 bool _queuedUpdate; |
||||
|
private Action _update; |
||||
|
private Action _invalidateScene; |
||||
|
|
||||
|
internal CompositionTarget CompositionTarget; |
||||
|
|
||||
|
/// <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); |
||||
|
CompositionTarget = compositor.CreateCompositionTarget(root.CreateRenderTarget); |
||||
|
CompositionTarget.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor); |
||||
|
_update = Update; |
||||
|
_invalidateScene = InvalidateScene; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public bool DrawFps |
||||
|
{ |
||||
|
get => CompositionTarget.DrawFps; |
||||
|
set => CompositionTarget.DrawFps = value; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public bool DrawDirtyRects |
||||
|
{ |
||||
|
get => CompositionTarget.DrawDirtyRects; |
||||
|
set => CompositionTarget.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 = CompositionTarget.TryHitTest(p, filter); |
||||
|
if(res == null) |
||||
|
yield break; |
||||
|
foreach(var v in res) |
||||
|
{ |
||||
|
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(); |
||||
|
CompositionTarget.Size = _root.ClientSize; |
||||
|
CompositionTarget.Scaling = _root.RenderScaling; |
||||
|
Compositor.InvokeOnNextCommit(_invalidateScene); |
||||
|
} |
||||
|
|
||||
|
public void Resized(Size size) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public void Paint(Rect rect) |
||||
|
{ |
||||
|
Update(); |
||||
|
CompositionTarget.RequestRedraw(); |
||||
|
if(RenderOnlyOnRenderThread && Compositor.Loop.RunsInBackground) |
||||
|
Compositor.RequestCommitAsync().Wait(); |
||||
|
else |
||||
|
CompositionTarget.ImmediateUIThreadRender(); |
||||
|
} |
||||
|
|
||||
|
public void Start() => CompositionTarget.IsEnabled = true; |
||||
|
|
||||
|
public void Stop() |
||||
|
{ |
||||
|
CompositionTarget.IsEnabled = false; |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
Stop(); |
||||
|
CompositionTarget.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,75 @@ |
|||||
|
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) |
||||
|
{ |
||||
|
var custom = Visual as ICustomHitTest; |
||||
|
if (DrawList == null && custom == null) |
||||
|
return false; |
||||
|
if (filter != null && !filter(Visual)) |
||||
|
return false; |
||||
|
if (custom != null) |
||||
|
{ |
||||
|
// Simulate the old behavior
|
||||
|
// TODO: Change behavior once legacy renderers are removed
|
||||
|
pt += new Point(Offset.X, Offset.Y); |
||||
|
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,130 @@ |
|||||
|
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.TryGetServerGlobalTransform(); |
||||
|
if (m == null) |
||||
|
{ |
||||
|
matrix = default; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
var m33 = MatrixUtils.ToMatrix(m.Value); |
||||
|
return m33.TryInvert(out matrix); |
||||
|
} |
||||
|
|
||||
|
bool TryTransformTo(CompositionVisual visual, Point globalPoint, out Point v) |
||||
|
{ |
||||
|
v = default; |
||||
|
if (TryGetInvertedTransform(visual, out var m)) |
||||
|
{ |
||||
|
v = globalPoint * m; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
void HitTestCore(CompositionVisual visual, Point globalPoint, PooledList<CompositionVisual> result, |
||||
|
Func<IVisual, bool>? filter) |
||||
|
{ |
||||
|
if (visual.Visible == false) |
||||
|
return; |
||||
|
if (!TryTransformTo(visual, globalPoint, out var point)) |
||||
|
return; |
||||
|
|
||||
|
if (visual.ClipToBounds |
||||
|
&& (point.X < 0 || point.Y < 0 || point.X > visual.Size.X || point.Y > visual.Size.Y)) |
||||
|
return; |
||||
|
|
||||
|
if (visual.Clip?.FillContains(point) == false) |
||||
|
return; |
||||
|
|
||||
|
// Inspect children
|
||||
|
if (visual is CompositionContainerVisual cv) |
||||
|
for (var c = cv.Children.Count - 1; c >= 0; c--) |
||||
|
{ |
||||
|
var ch = cv.Children[c]; |
||||
|
HitTestCore(ch, globalPoint, result, filter); |
||||
|
} |
||||
|
|
||||
|
// Hit-test the current node
|
||||
|
if (visual.HitTest(point, filter)) |
||||
|
result.Add(visual); |
||||
|
} |
||||
|
|
||||
|
/// <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,6 @@ |
|||||
|
namespace Avalonia.Rendering.Composition; |
||||
|
|
||||
|
internal interface ICompositionTargetDebugEvents |
||||
|
{ |
||||
|
void RectInvalidated(Rect rc); |
||||
|
} |
||||
@ -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,221 @@ |
|||||
|
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 ICompositionTargetDebugEvents? DebugEvents { get; set; } |
||||
|
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); |
||||
|
DebugEvents?.RectInvalidated(rect); |
||||
|
_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,237 @@ |
|||||
|
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; |
||||
|
|
||||
|
if (_parent?.IsDirtyComposition == true) |
||||
|
{ |
||||
|
IsDirtyComposition = true; |
||||
|
_isDirtyForUpdate = true; |
||||
|
} |
||||
|
|
||||
|
var invalidateOldBounds = _isDirtyForUpdate; |
||||
|
var invalidateNewBounds = _isDirtyForUpdate; |
||||
|
|
||||
|
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 = _parent?.IsVisibleInFrame != false && Visible && EffectiveOpacity > 0.04 && !_isBackface && |
||||
|
!_combinedTransformedClipBounds.IsEmpty; |
||||
|
|
||||
|
if (wasVisible != IsVisibleInFrame || positionChanged) |
||||
|
{ |
||||
|
invalidateOldBounds |= wasVisible; |
||||
|
invalidateNewBounds |= IsVisibleInFrame; |
||||
|
} |
||||
|
|
||||
|
// Invalidate new bounds
|
||||
|
if (invalidateNewBounds) |
||||
|
AddDirtyRect(TransformedOwnContentBounds.Intersect(_combinedTransformedClipBounds)); |
||||
|
|
||||
|
if (invalidateOldBounds) |
||||
|
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 = GlobalTransformMatrix; |
||||
|
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,156 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using Avalonia.Platform; |
||||
|
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; |
||||
|
bool _reclaimImmediately; |
||||
|
|
||||
|
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); |
||||
|
if (AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>() == null) |
||||
|
_reclaimImmediately = true; |
||||
|
else |
||||
|
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 && !_reclaimImmediately) |
||||
|
{ |
||||
|
_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? TryGetServerGlobalTransform() |
||||
|
{ |
||||
|
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