diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs
index 4464413e63..2a0755b900 100644
--- a/samples/ControlCatalog.NetCore/Program.cs
+++ b/samples/ControlCatalog.NetCore/Program.cs
@@ -111,6 +111,7 @@ namespace ControlCatalog.NetCore
EnableMultiTouch = true,
UseDBusMenu = true,
EnableIme = true,
+ UseCompositor = true
})
.With(new Win32PlatformOptions
{
diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj
index 8e4755b4b7..bcebdd504c 100644
--- a/src/Avalonia.Base/Avalonia.Base.csproj
+++ b/src/Avalonia.Base/Avalonia.Base.csproj
@@ -3,10 +3,13 @@
net6.0;netstandard2.0
Avalonia.Base
Avalonia
- True
+ True
+ true
+ $(BaseIntermediateOutputPath)\GeneratedFiles
+
diff --git a/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs b/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs
index a6a5953827..cefbf642be 100644
--- a/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs
+++ b/src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
diff --git a/src/Avalonia.Base/Collections/Pooled/PooledList.cs b/src/Avalonia.Base/Collections/Pooled/PooledList.cs
index 200a52fb0d..ffda1bedca 100644
--- a/src/Avalonia.Base/Collections/Pooled/PooledList.cs
+++ b/src/Avalonia.Base/Collections/Pooled/PooledList.cs
@@ -1434,7 +1434,7 @@ namespace Avalonia.Collections.Pooled
///
/// Returns the internal buffers to the ArrayPool.
///
- public void Dispose()
+ public virtual void Dispose()
{
ReturnArray();
_size = 0;
diff --git a/src/Avalonia.Base/Matrix.cs b/src/Avalonia.Base/Matrix.cs
index b08a0eb98a..6f00b08d13 100644
--- a/src/Avalonia.Base/Matrix.cs
+++ b/src/Avalonia.Base/Matrix.cs
@@ -1,5 +1,6 @@
using System;
using System.Globalization;
+using System.Numerics;
using Avalonia.Utilities;
namespace Avalonia
@@ -106,6 +107,36 @@ namespace Avalonia
(value1._m31 * value2.M12) + (value1._m32 * value2.M22) + value2._m32);
}
+ public static Matrix operator +(Matrix value1, Matrix value2)
+ {
+ return new Matrix(value1.M11 + value2.M11,
+ value1.M12 + value2.M12,
+ value1.M21 + value2.M21,
+ value1.M22 + value2.M22,
+ value1.M31 + value2.M31,
+ value1.M32 + value2.M32);
+ }
+
+ public static Matrix operator -(Matrix value1, Matrix value2)
+ {
+ return new Matrix(value1.M11 - value2.M11,
+ value1.M12 - value2.M12,
+ value1.M21 - value2.M21,
+ value1.M22 - value2.M22,
+ value1.M31 - value2.M31,
+ value1.M32 - value2.M32);
+ }
+
+ public static Matrix operator *(Matrix value1, double value2)
+ {
+ return new Matrix(value1.M11 * value2,
+ value1.M12 * value2,
+ value1.M21 * value2,
+ value1.M22 * value2,
+ value1.M31 * value2,
+ value1.M32 * value2);
+ }
+
///
/// Negates the given matrix by multiplying all values by -1.
///
@@ -427,6 +458,14 @@ namespace Avalonia
return true;
}
+#if !BUILDTASK
+ public static implicit operator Matrix4x4(Matrix m)
+ {
+ return new Matrix4x4(new Matrix3x2((float)m._m11, (float)m._m12, (float)m._m21, (float)m._m22,
+ (float)m._m31, (float)m._m32));
+ }
+#endif
+
public struct Decomposed
{
public Vector Translate;
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs
new file mode 100644
index 0000000000..180c45022f
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs
@@ -0,0 +1,40 @@
+using Avalonia.Rendering.Composition.Expressions;
+using Avalonia.Rendering.Composition.Server;
+using Avalonia.Rendering.Composition.Transport;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ internal struct AnimatedValueStore where T : struct
+ {
+ private T _direct;
+ private IAnimationInstance _animation;
+ private T? _lastAnimated;
+
+ public T Direct => _direct;
+
+ public T GetAnimated(ServerCompositor compositor)
+ {
+ if (_animation == null)
+ return _direct;
+ var v = _animation.Evaluate(compositor.ServerNow, ExpressionVariant.Create(_direct))
+ .CastOrDefault();
+ _lastAnimated = v;
+ return v;
+ }
+
+ private T LastAnimated => _animation != null ? _lastAnimated ?? _direct : _direct;
+
+ public bool IsAnimation => _animation != null;
+
+ public void SetAnimation(ChangeSet cs, IAnimationInstance animation)
+ {
+ _animation = animation;
+ _animation.Start(cs.Batch.CommitedAt, ExpressionVariant.Create(LastAnimated));
+ }
+
+ public static implicit operator AnimatedValueStore(T value) => new AnimatedValueStore()
+ {
+ _direct = value
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs
new file mode 100644
index 0000000000..9375faaaae
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs
@@ -0,0 +1,63 @@
+// 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
+{
+ public abstract class CompositionAnimation : CompositionObject, ICompositionAnimationBase
+ {
+ private readonly CompositionPropertySet _propertySet;
+ internal CompositionAnimation(Compositor compositor) : base(compositor, null!)
+ {
+ _propertySet = new CompositionPropertySet(compositor);
+ }
+
+ private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException();
+
+ public void ClearAllParameters() => _propertySet.ClearAll();
+
+ public void ClearParameter(string key) => _propertySet.Clear(key);
+
+ void SetVariant(string key, ExpressionVariant value) => _propertySet.Set(key, value);
+
+ public void SetColorParameter(string key, Avalonia.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);
+
+ // TODO: void SetExpressionReferenceParameter(string parameterName, IAnimationObject source)
+
+ public string? Target { get; set; }
+
+ internal abstract IAnimationInstance CreateInstance(ServerObject targetObject,
+ ExpressionVariant? finalValue);
+
+ internal PropertySetSnapshot CreateSnapshot(bool server)
+ => _propertySet.Snapshot(server, 1);
+
+ void ICompositionAnimationBase.InternalOnly()
+ {
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs
new file mode 100644
index 0000000000..833f7e498c
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Rendering.Composition.Transport;
+
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ public class CompositionAnimationGroup : CompositionObject, ICompositionAnimationBase
+ {
+ internal List Animations { get; } = new List();
+ 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!)
+ {
+ }
+
+ private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException();
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs
new file mode 100644
index 0000000000..a6f24c2e35
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs
@@ -0,0 +1,34 @@
+// ReSharper disable CheckNamespace
+using System;
+using Avalonia.Rendering.Composition.Expressions;
+using Avalonia.Rendering.Composition.Server;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ public class ExpressionAnimation : CompositionAnimation
+ {
+ private string? _expression;
+ private Expression? _parsedExpression;
+
+ internal ExpressionAnimation(Compositor compositor) : base(compositor)
+ {
+ }
+
+ 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(true));
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs
new file mode 100644
index 0000000000..47b947b2e9
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs
@@ -0,0 +1,44 @@
+using System;
+using Avalonia.Rendering.Composition.Expressions;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ internal class ExpressionAnimationInstance : IAnimationInstance
+ {
+ private readonly Expression _expression;
+ private readonly IExpressionObject _target;
+ private ExpressionVariant _startingValue;
+ private readonly ExpressionVariant? _finalValue;
+ private readonly PropertySetSnapshot _parameters;
+
+ public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue)
+ {
+ var ctx = new ExpressionEvaluationContext
+ {
+ Parameters = _parameters,
+ Target = _target,
+ ForeignFunctionInterface = BuiltInExpressionFfi.Instance,
+ StartingValue = _startingValue,
+ FinalValue = _finalValue ?? _startingValue,
+ CurrentValue = currentValue
+ };
+ return _expression.Evaluate(ref ctx);
+ }
+
+ public void Start(TimeSpan startedAt, ExpressionVariant startingValue)
+ {
+ _startingValue = startingValue;
+ }
+
+ public ExpressionAnimationInstance(Expression expression,
+ IExpressionObject target,
+ ExpressionVariant? finalValue,
+ PropertySetSnapshot parameters)
+ {
+ _expression = expression;
+ _target = target;
+ _finalValue = finalValue;
+ _parameters = parameters;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs
new file mode 100644
index 0000000000..a0b066ae0c
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs
@@ -0,0 +1,11 @@
+using System;
+using Avalonia.Rendering.Composition.Expressions;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ internal interface IAnimationInstance
+ {
+ ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue);
+ void Start(TimeSpan startedAt, ExpressionVariant startingValue);
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs
new file mode 100644
index 0000000000..bf40fd3ad2
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/ICompositionAnimationBase.cs
@@ -0,0 +1,12 @@
+// ReSharper disable CheckNamespace
+
+using Avalonia.Rendering.Composition.Server;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ public interface ICompositionAnimationBase
+ {
+ internal void InternalOnly();
+ }
+
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs
new file mode 100644
index 0000000000..be91352527
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using Avalonia.Rendering.Composition.Transport;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ public class ImplicitAnimationCollection : CompositionObject, IDictionary
+ {
+ private Dictionary _inner = new Dictionary();
+ private IDictionary _innerface;
+ internal ImplicitAnimationCollection(Compositor compositor) : base(compositor, null!)
+ {
+ _innerface = _inner;
+ }
+
+ private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException();
+
+ public IEnumerator> GetEnumerator() => _inner.GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable) _inner).GetEnumerator();
+
+ void ICollection>.Add(KeyValuePair item) => _innerface.Add(item);
+
+ public void Clear() => _inner.Clear();
+
+ bool ICollection>.Contains(KeyValuePair item) => _innerface.Contains(item);
+
+ void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) => _innerface.CopyTo(array, arrayIndex);
+
+ bool ICollection>.Remove(KeyValuePair item) => _innerface.Remove(item);
+
+ public int Count => _inner.Count;
+
+ bool ICollection>.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 IDictionary.Keys => _innerface.Keys;
+
+ ICollection IDictionary.Values =>
+ _innerface.Values;
+
+ // UWP compat
+ public uint Size => (uint) Count;
+
+ public IReadOnlyDictionary GetView() =>
+ new Dictionary(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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs b/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs
new file mode 100644
index 0000000000..62b790701a
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/Interpolators.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Numerics;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ internal interface IInterpolator
+ {
+ T Interpolate(T from, T to, float progress);
+ }
+
+ class ScalarInterpolator : IInterpolator
+ {
+ public float Interpolate(float @from, float to, float progress) => @from + (to - @from) * progress;
+
+ public static ScalarInterpolator Instance { get; } = new ScalarInterpolator();
+ }
+
+ class Vector2Interpolator : IInterpolator
+ {
+ public Vector2 Interpolate(Vector2 @from, Vector2 to, float progress)
+ => Vector2.Lerp(@from, to, progress);
+
+ public static Vector2Interpolator Instance { get; } = new Vector2Interpolator();
+ }
+
+ class Vector3Interpolator : IInterpolator
+ {
+ public Vector3 Interpolate(Vector3 @from, Vector3 to, float progress)
+ => Vector3.Lerp(@from, to, progress);
+
+ public static Vector3Interpolator Instance { get; } = new Vector3Interpolator();
+ }
+
+ class Vector4Interpolator : IInterpolator
+ {
+ public Vector4 Interpolate(Vector4 @from, Vector4 to, float progress)
+ => Vector4.Lerp(@from, to, progress);
+
+ public static Vector4Interpolator Instance { get; } = new Vector4Interpolator();
+ }
+
+ class QuaternionInterpolator : IInterpolator
+ {
+ public Quaternion Interpolate(Quaternion @from, Quaternion to, float progress)
+ => Quaternion.Lerp(@from, to, progress);
+
+ public static QuaternionInterpolator Instance { get; } = new QuaternionInterpolator();
+ }
+
+ class ColorInterpolator : IInterpolator
+ {
+ 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
+ {
+ public bool Interpolate(bool @from, bool to, float progress) => progress >= 1 ? to : @from;
+
+ public static BooleanInterpolator Instance { get; } = new BooleanInterpolator();
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs
new file mode 100644
index 0000000000..065dfd7a8e
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimation.cs
@@ -0,0 +1,53 @@
+namespace Avalonia.Rendering.Composition.Animations
+{
+ public abstract class KeyFrameAnimation : CompositionAnimation
+ {
+ internal KeyFrameAnimation(Compositor compositor) : base(compositor)
+ {
+ }
+
+ public AnimationDelayBehavior DelayBehavior { get; set; }
+ public System.TimeSpan DelayTime { get; set; }
+ public AnimationDirection Direction { get; set; }
+ public System.TimeSpan Duration { get; set; }
+ public AnimationIterationBehavior IterationBehavior { get; set; }
+ public int IterationCount { get; set; } = 1;
+ public AnimationStopBehavior StopBehavior { get; set; }
+
+ private protected abstract IKeyFrames KeyFrames { get; }
+
+ public void InsertExpressionKeyFrame(float normalizedProgressKey, string value,
+ CompositionEasingFunction easingFunction) =>
+ KeyFrames.InsertExpressionKeyFrame(normalizedProgressKey, value, easingFunction);
+
+ public void InsertExpressionKeyFrame(float normalizedProgressKey, string value)
+ => KeyFrames.InsertExpressionKeyFrame(normalizedProgressKey, value, new LinearEasingFunction(Compositor));
+ }
+
+ public enum AnimationDelayBehavior
+ {
+ SetInitialValueAfterDelay,
+ SetInitialValueBeforeDelay
+ }
+
+ public enum AnimationDirection
+ {
+ Normal,
+ Reverse,
+ Alternate,
+ AlternateReverse
+ }
+
+ public enum AnimationIterationBehavior
+ {
+ Count,
+ Forever
+ }
+
+ public enum AnimationStopBehavior
+ {
+ LeaveCurrentValue,
+ SetToInitialValue,
+ SetToFinalValue
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs
new file mode 100644
index 0000000000..b90a02148d
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs
@@ -0,0 +1,139 @@
+using System;
+using Avalonia.Rendering.Composition.Expressions;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ class KeyFrameAnimationInstance : IAnimationInstance where T : struct
+ {
+ private readonly IInterpolator _interpolator;
+ private readonly ServerKeyFrame[] _keyFrames;
+ private readonly PropertySetSnapshot _snapshot;
+ private readonly ExpressionVariant? _finalValue;
+ private readonly IExpressionObject _target;
+ private readonly AnimationDelayBehavior _delayBehavior;
+ private readonly TimeSpan _delayTime;
+ private readonly AnimationDirection _direction;
+ private readonly TimeSpan _duration;
+ private readonly AnimationIterationBehavior _iterationBehavior;
+ private readonly int _iterationCount;
+ private readonly AnimationStopBehavior _stopBehavior;
+ private TimeSpan _startedAt;
+ private T _startingValue;
+
+ public KeyFrameAnimationInstance(
+ IInterpolator interpolator, ServerKeyFrame[] keyFrames,
+ PropertySetSnapshot snapshot, ExpressionVariant? finalValue,
+ IExpressionObject target,
+ AnimationDelayBehavior delayBehavior, TimeSpan delayTime,
+ AnimationDirection direction, TimeSpan duration,
+ AnimationIterationBehavior iterationBehavior,
+ int iterationCount, AnimationStopBehavior stopBehavior)
+ {
+ _interpolator = interpolator;
+ _keyFrames = keyFrames;
+ _snapshot = snapshot;
+ _finalValue = finalValue;
+ _target = target;
+ _delayBehavior = delayBehavior;
+ _delayTime = delayTime;
+ _direction = direction;
+ _duration = duration;
+ _iterationBehavior = iterationBehavior;
+ _iterationCount = iterationCount;
+ _stopBehavior = stopBehavior;
+ if (_keyFrames.Length == 0)
+ throw new InvalidOperationException("Animation has no key frames");
+ if(_duration.Ticks <= 0)
+ throw new InvalidOperationException("Invalid animation duration");
+ }
+
+ public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue)
+ {
+ var elapsed = now - _startedAt;
+ var starting = ExpressionVariant.Create(_startingValue);
+ var ctx = new ExpressionEvaluationContext
+ {
+ Parameters = _snapshot,
+ Target = _target,
+ CurrentValue = currentValue,
+ FinalValue = _finalValue ?? starting,
+ StartingValue = starting,
+ ForeignFunctionInterface = BuiltInExpressionFfi.Instance
+ };
+
+ 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 == AnimationDirection.Alternate
+ ? !evenIterationNumber
+ : _direction == AnimationDirection.AlternateReverse
+ ? evenIterationNumber
+ : _direction == AnimationDirection.Reverse;
+
+ var iterationProgress = elapsed.TotalSeconds / _duration.TotalSeconds;
+ if (reverse)
+ iterationProgress = 1 - iterationProgress;
+
+ var left = new ServerKeyFrame
+ {
+ 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 = right.EasingFunction.Ease((float) 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 f)
+ {
+ if (f.Expression != null)
+ return f.Expression.Evaluate(ref ctx).CastOrDefault();
+ else
+ return f.Value;
+ }
+
+ public void Start(TimeSpan startedAt, ExpressionVariant startingValue)
+ {
+ _startedAt = startedAt;
+ _startingValue = startingValue.CastOrDefault();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs
new file mode 100644
index 0000000000..d7f2504061
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrames.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Rendering.Composition.Expressions;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ class KeyFrames : List>, 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,
+ CompositionEasingFunction easingFunction)
+ {
+ Validate(normalizedProgressKey);
+ Add(new KeyFrame
+ {
+ NormalizedProgressKey = normalizedProgressKey,
+ Expression = Expression.Parse(value),
+ EasingFunction = easingFunction
+ });
+ }
+
+ public void Insert(float normalizedProgressKey, T value, CompositionEasingFunction easingFunction)
+ {
+ Validate(normalizedProgressKey);
+ Add(new KeyFrame
+ {
+ NormalizedProgressKey = normalizedProgressKey,
+ Value = value,
+ EasingFunction = easingFunction
+ });
+ }
+
+ public ServerKeyFrame[] Snapshot()
+ {
+ var frames = new ServerKeyFrame[Count];
+ for (var c = 0; c < Count; c++)
+ {
+ var f = this[c];
+ frames[c] = new ServerKeyFrame
+ {
+ Expression = f.Expression,
+ Value = f.Value,
+ EasingFunction = f.EasingFunction.Snapshot(),
+ Key = f.NormalizedProgressKey
+ };
+ }
+ return frames;
+ }
+ }
+
+ struct KeyFrame
+ {
+ public float NormalizedProgressKey;
+ public T Value;
+ public Expression Expression;
+ public CompositionEasingFunction EasingFunction;
+ }
+
+ struct ServerKeyFrame
+ {
+ public T Value;
+ public Expression Expression;
+ public IEasingFunction EasingFunction;
+ public float Key;
+ }
+
+
+
+ interface IKeyFrames
+ {
+ public void InsertExpressionKeyFrame(float normalizedProgressKey, string value, CompositionEasingFunction easingFunction);
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs b/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs
new file mode 100644
index 0000000000..ca703dfc6f
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/PropertySetSnapshot.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using Avalonia.Rendering.Composition.Expressions;
+
+namespace Avalonia.Rendering.Composition.Animations
+{
+ internal class PropertySetSnapshot : IExpressionParameterCollection, IExpressionObject
+ {
+ private readonly Dictionary _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 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);
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
new file mode 100644
index 0000000000..07ac54b634
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
@@ -0,0 +1,194 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Runtime.InteropServices;
+using Avalonia.Collections;
+using Avalonia.Media;
+using Avalonia.Rendering.Composition.Drawing;
+using Avalonia.Rendering.Composition.Server;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.Composition;
+
+public class CompositingRenderer : RendererBase, IRendererWithCompositor
+{
+ private readonly IRenderRoot _root;
+ private readonly Compositor _compositor;
+ private readonly IDeferredRendererLock? _rendererLock;
+ CompositionDrawingContext _recorder = new();
+ DrawingContext _recordingContext;
+ private HashSet _dirty = new();
+ private HashSet _recalculateChildren = new();
+ private readonly CompositionTarget _target;
+ private bool _queuedUpdate;
+ private Action _update;
+
+ public CompositingRenderer(IRenderRoot root,
+ Compositor compositor,
+ IDeferredRendererLock? rendererLock = null)
+ {
+ _root = root;
+ _compositor = compositor;
+ _recordingContext = new DrawingContext(_recorder);
+ _rendererLock = rendererLock ?? new ManagedDeferredRendererLock();
+ _target = compositor.CreateCompositionTarget(root.CreateRenderTarget);
+ _target.Root = ((Visual)root!.VisualRoot!).AttachToCompositor(compositor);
+ _update = Update;
+ }
+
+ public bool DrawFps { get; set; }
+ public bool DrawDirtyRects { get; set; }
+ public event EventHandler? SceneInvalidated;
+
+ void QueueUpdate()
+ {
+ if(_queuedUpdate)
+ return;
+ _queuedUpdate = true;
+ Dispatcher.UIThread.Post(_update, DispatcherPriority.Composition);
+ }
+ public void AddDirty(IVisual visual)
+ {
+ _dirty.Add((Visual)visual);
+ QueueUpdate();
+ }
+
+ public IEnumerable HitTest(Point p, IVisual root, Func filter)
+ {
+ var res = _target.TryHitTest(new Vector2((float)p.X, (float)p.Y));
+ if(res == null)
+ yield break;
+ for (var index = res.Count - 1; index >= 0; index--)
+ {
+ var v = res[index];
+ if (v is CompositionDrawListVisual dv)
+ {
+ if (filter == null || filter(dv.Visual))
+ yield return dv.Visual;
+ }
+ }
+ }
+
+ public IVisual? HitTestFirst(Point p, IVisual root, Func filter)
+ {
+ // TODO: Optimize
+ return HitTest(p, root, filter).FirstOrDefault();
+ }
+
+ 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)v.GetVisualChildren();
+ if (compositionChildren.Count == visualChildren.Count)
+ {
+ bool mismatch = false;
+ for(var c=0; c _target.IsEnabled = true;
+
+ public void Stop()
+ {
+ _target.IsEnabled = true;
+ }
+
+ public void Dispose()
+ {
+ Stop();
+ _target.Dispose();
+ // Wait for the composition batch to be applied and rendered to guarantee that
+ // render target is not used anymore and can be safely disposed
+ _compositor.RequestCommitAsync().Wait();
+ }
+
+
+ public Compositor Compositor => _compositor;
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
new file mode 100644
index 0000000000..9f02055412
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
@@ -0,0 +1,44 @@
+using System.Numerics;
+using Avalonia.Rendering.Composition.Drawing;
+using Avalonia.Rendering.Composition.Server;
+using Avalonia.Rendering.Composition.Transport;
+
+namespace Avalonia.Rendering.Composition;
+
+internal class CompositionDrawListVisual : CompositionContainerVisual
+{
+ public Visual Visual { get; }
+
+ private new DrawListVisualChanges Changes => (DrawListVisualChanges)base.Changes;
+ private CompositionDrawList? _drawList;
+ public CompositionDrawList? DrawList
+ {
+ get => _drawList;
+ set
+ {
+ _drawList?.Dispose();
+ _drawList = value;
+ Changes.DrawCommands = value?.Clone();
+ }
+ }
+
+ private protected override IChangeSetPool ChangeSetPool => DrawListVisualChanges.Pool;
+
+ internal CompositionDrawListVisual(Compositor compositor, ServerCompositionContainerVisual server, Visual visual) : base(compositor, server)
+ {
+ Visual = visual;
+ }
+
+ internal override bool HitTest(Vector2 point)
+ {
+ if (DrawList == null)
+ return false;
+ var pt = new Point(point.X, point.Y);
+ if (Visual is ICustomHitTest custom)
+ return custom.HitTest(pt);
+ foreach (var op in DrawList)
+ if (op.Item.HitTest(pt))
+ return true;
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs b/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs
new file mode 100644
index 0000000000..73db243e93
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/CompositionEasingFunction.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Numerics;
+using Avalonia.Rendering.Composition.Transport;
+using Avalonia.Rendering.Composition.Utils;
+
+namespace Avalonia.Rendering.Composition
+{
+ public abstract class CompositionEasingFunction : CompositionObject
+ {
+ internal CompositionEasingFunction(Compositor compositor) : base(compositor, null!)
+ {
+ }
+
+ private protected override IChangeSetPool ChangeSetPool => throw new InvalidOperationException();
+
+ internal abstract IEasingFunction Snapshot();
+ }
+
+ internal interface IEasingFunction
+ {
+ float Ease(float progress);
+ }
+
+ public sealed class DelegateCompositionEasingFunction : CompositionEasingFunction
+ {
+ private readonly Easing _func;
+
+ public delegate float EasingDelegate(float progress);
+
+ internal DelegateCompositionEasingFunction(Compositor compositor, EasingDelegate func) : base(compositor)
+ {
+ _func = new Easing(func);
+ }
+
+ class Easing : IEasingFunction
+ {
+ private readonly EasingDelegate _func;
+
+ public Easing(EasingDelegate func)
+ {
+ _func = func;
+ }
+
+ public float Ease(float progress) => _func(progress);
+ }
+
+ internal override IEasingFunction Snapshot() => _func;
+ }
+
+ public class LinearEasingFunction : CompositionEasingFunction
+ {
+ public LinearEasingFunction(Compositor compositor) : base(compositor)
+ {
+ }
+
+ class Linear : IEasingFunction
+ {
+ public float Ease(float progress) => progress;
+ }
+
+ private static readonly Linear Instance = new Linear();
+ internal override IEasingFunction Snapshot() => Instance;
+ }
+
+ public class CubicBezierEasingFunction : CompositionEasingFunction
+ {
+ private CubicBezier _bezier;
+ public Vector2 ControlPoint1 { get; }
+ public Vector2 ControlPoint2 { get; }
+ //cubic-bezier(0.25, 0.1, 0.25, 1.0)
+ internal CubicBezierEasingFunction(Compositor compositor, Vector2 controlPoint1, Vector2 controlPoint2) : base(compositor)
+ {
+ 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);
+ }
+
+ class EasingFunction : IEasingFunction
+ {
+ private readonly CubicBezier _bezier;
+
+ public EasingFunction(CubicBezier bezier)
+ {
+ _bezier = bezier;
+ }
+
+ public float Ease(float progress) => (float)_bezier.Solve(progress);
+ }
+
+ internal static IEasingFunction Ease { get; } = new EasingFunction(new CubicBezier(0.25, 0.1, 0.25, 1));
+
+ internal override IEasingFunction Snapshot() => new EasingFunction(_bezier);
+ }
+
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionGradientBrush.cs b/src/Avalonia.Base/Rendering/Composition/CompositionGradientBrush.cs
new file mode 100644
index 0000000000..cf222550dd
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/CompositionGradientBrush.cs
@@ -0,0 +1,16 @@
+using Avalonia.Rendering.Composition.Server;
+
+namespace Avalonia.Rendering.Composition
+{
+ public partial class CompositionGradientBrush : CompositionBrush
+ {
+ internal CompositionGradientBrush(Compositor compositor, ServerCompositionGradientBrush server) : base(compositor, server)
+ {
+ ColorStops = new CompositionGradientStopCollection(compositor, server.Stops);
+ }
+
+ public CompositionGradientStopCollection ColorStops { get; }
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs
new file mode 100644
index 0000000000..2417ecaba8
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs
@@ -0,0 +1,125 @@
+using System;
+using Avalonia.Rendering.Composition.Animations;
+using Avalonia.Rendering.Composition.Expressions;
+using Avalonia.Rendering.Composition.Server;
+using Avalonia.Rendering.Composition.Transport;
+
+namespace Avalonia.Rendering.Composition
+{
+ public abstract class CompositionObject : IDisposable, IExpressionObject
+ {
+ public ImplicitAnimationCollection? ImplicitAnimations { get; set; }
+ internal CompositionObject(Compositor compositor, ServerObject server)
+ {
+ Compositor = compositor;
+ Server = server;
+ }
+
+ public Compositor Compositor { get; }
+ internal ServerObject Server { get; }
+ public bool IsDisposed { get; private set; }
+ private ChangeSet? _changes;
+
+ private static void ThrowInvalidOperation() =>
+ throw new InvalidOperationException("There is no server-side counterpart for this object");
+
+ private protected ChangeSet Changes
+ {
+ get
+ {
+ // ReSharper disable once ConditionIsAlwaysTrueOrFalse
+ if (Server == null) ThrowInvalidOperation();
+ var currentBatch = Compositor.CurrentBatch;
+ if (_changes != null && _changes.Batch != currentBatch)
+ _changes = null;
+ if (_changes == null)
+ {
+ _changes = ChangeSetPool.Get(Server!, currentBatch);
+ currentBatch.Changes!.Add(_changes);
+ Compositor.QueueImplicitBatchCommit();
+ }
+
+ return _changes;
+ }
+ }
+
+ private protected abstract IChangeSetPool ChangeSetPool { get; }
+
+ public void Dispose()
+ {
+ Changes.Dispose = true;
+ IsDisposed = true;
+ }
+
+ internal virtual ExpressionVariant GetPropertyForAnimation(string name)
+ {
+ return default;
+ }
+
+ ExpressionVariant IExpressionObject.GetProperty(string name) => GetPropertyForAnimation(name);
+
+ public void StartAnimation(string propertyName, CompositionAnimation animation)
+ => StartAnimation(propertyName, animation, null);
+
+ internal virtual void StartAnimation(string propertyName, CompositionAnimation animation, ExpressionVariant? finalValue = null)
+ {
+ throw new ArgumentException("Unknown property " + propertyName);
+ }
+
+ 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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs
new file mode 100644
index 0000000000..004c2676ff
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs
@@ -0,0 +1,132 @@
+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
+{
+ public class CompositionPropertySet : CompositionObject
+ {
+ private readonly Dictionary _variants = new Dictionary();
+ private readonly Dictionary _objects = new Dictionary();
+
+ internal CompositionPropertySet(Compositor compositor) : base(compositor, null!)
+ {
+ }
+
+ private protected override IChangeSetPool ChangeSetPool => throw new NotSupportedException();
+
+ internal void Set(string key, ExpressionVariant value)
+ {
+ _objects.Remove(key);
+ _variants[key] = value;
+ }
+
+ 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(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(bool server, int allowedNestingLevel)
+ {
+ var dic = new Dictionary(_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.Snapshot(server, 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(server ? (IExpressionObject) o.Value.Server : o.Value);
+ }
+
+ foreach (var v in _variants)
+ dic[v.Key] = v.Value;
+
+ return new PropertySetSnapshot(dic);
+ }
+ }
+
+ public enum CompositionGetValueStatus
+ {
+ Succeeded,
+ TypeMismatch,
+ NotFound
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
new file mode 100644
index 0000000000..a8835ca668
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
@@ -0,0 +1,107 @@
+using System.Collections.Generic;
+using System.Numerics;
+using Avalonia.Collections.Pooled;
+
+namespace Avalonia.Rendering.Composition
+{
+ public partial class CompositionTarget
+ {
+ partial void OnRootChanged()
+ {
+ if (Root != null)
+ Root.Root = this;
+ }
+
+ partial void OnRootChanging()
+ {
+ if (Root != null)
+ Root.Root = null;
+ }
+
+ public PooledList? TryHitTest(Vector2 point)
+ {
+ Server.Readback.NextRead();
+ if (Root == null)
+ return null;
+ var res = new PooledList();
+ HitTestCore(Root, point, res);
+ return res;
+ }
+
+ public Vector2? TryTransformToVisual(CompositionVisual visual, Vector2 point)
+ {
+ if (visual.Root != this)
+ return null;
+ var v = visual;
+ var m = Matrix3x2.Identity;
+ while (v != null)
+ {
+ if (!TryGetInvertedTransform(v, out var cm))
+ return null;
+ m = m * cm;
+ v = v.Parent;
+ }
+
+ return Vector2.Transform(point, m);
+ }
+
+ bool TryGetInvertedTransform(CompositionVisual visual, out Matrix3x2 matrix)
+ {
+ var m = visual.TryGetServerTransform();
+ if (m == null)
+ {
+ matrix = default;
+ return false;
+ }
+
+ // TODO: Use Matrix3x3
+ var m32 = new Matrix3x2(m.Value.M11, m.Value.M12, m.Value.M21, m.Value.M22, m.Value.M41, m.Value.M42);
+
+ return Matrix3x2.Invert(m32, out matrix);
+ }
+
+ bool TryTransformTo(CompositionVisual visual, ref Vector2 v)
+ {
+ if (TryGetInvertedTransform(visual, out var m))
+ {
+ v = Vector2.Transform(v, m);
+ return true;
+ }
+
+ return false;
+ }
+
+ bool HitTestCore(CompositionVisual visual, Vector2 point, PooledList result)
+ {
+ //TODO: Check readback too
+ if (visual.Visible == false)
+ return false;
+ if (!TryTransformTo(visual, ref point))
+ return false;
+ if (point.X >= 0 && point.Y >= 0 && point.X <= visual.Size.X && point.Y <= visual.Size.Y)
+ {
+ bool success = false;
+ // Hit-test the current node
+ if (visual.HitTest(point))
+ {
+ result.Add(visual);
+ success = true;
+ }
+
+ // Inspect children too
+ if(visual is CompositionContainerVisual cv)
+ for (var c = cv.Children.Count - 1; c >= 0; c--)
+ {
+ var ch = cv.Children[c];
+ var hit = HitTestCore(ch, point, result);
+ if (hit)
+ return true;
+ }
+
+ return success;
+ }
+
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs
new file mode 100644
index 0000000000..217d8dd803
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs
@@ -0,0 +1,143 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Numerics;
+using System.Threading.Tasks;
+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
+{
+ public partial class Compositor
+ {
+ private ServerCompositor _server;
+ private Batch _currentBatch;
+ private bool _implicitBatchCommitQueued;
+ private Action _implicitBatchCommit;
+
+ internal Batch CurrentBatch => _currentBatch;
+ internal ServerCompositor Server => _server;
+ internal CompositionEasingFunction DefaultEasing { get; }
+
+ private Compositor(ServerCompositor server)
+ {
+ _server = server;
+ _currentBatch = new Batch();
+ _implicitBatchCommit = ImplicitBatchCommit;
+ DefaultEasing = new CubicBezierEasingFunction(this,
+ new Vector2(0.25f, 0.1f), new Vector2(0.25f, 1f));
+ }
+
+ public CompositionTarget CreateCompositionTarget(Func renderTargetFactory)
+ {
+ return new CompositionTarget(this, new ServerCompositionTarget(_server, renderTargetFactory));
+ }
+
+ public Task RequestCommitAsync()
+ {
+ var batch = CurrentBatch;
+ _currentBatch = new Batch();
+ batch.CommitedAt = Server.Clock.Elapsed;
+ _server.EnqueueBatch(batch);
+ return batch.Completed;
+ }
+
+ public static Compositor Create(IRenderLoop timer)
+ {
+ return new Compositor(new ServerCompositor(timer));
+ }
+
+ public void Dispose()
+ {
+
+ }
+
+ public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server));
+
+ public CompositionSolidColorVisual CreateSolidColorVisual() => new CompositionSolidColorVisual(this,
+ new ServerCompositionSolidColorVisual(_server));
+
+ public CompositionSolidColorVisual CreateSolidColorVisual(Avalonia.Media.Color color)
+ {
+ var v = new CompositionSolidColorVisual(this, new ServerCompositionSolidColorVisual(_server));
+ v.Color = color;
+ return v;
+ }
+
+ public CompositionSpriteVisual CreateSpriteVisual() => new CompositionSpriteVisual(this, new ServerCompositionSpriteVisual(_server));
+
+ public CompositionLinearGradientBrush CreateLinearGradientBrush()
+ => new CompositionLinearGradientBrush(this, new ServerCompositionLinearGradientBrush(_server));
+
+ public CompositionColorGradientStop CreateColorGradientStop()
+ => new CompositionColorGradientStop(this, new ServerCompositionColorGradientStop(_server));
+
+ public CompositionColorGradientStop CreateColorGradientStop(float offset, Avalonia.Media.Color color)
+ {
+ var stop = CreateColorGradientStop();
+ stop.Offset = offset;
+ stop.Color = color;
+ return stop;
+ }
+
+ // We want to make it 100% async later
+ /*
+ public CompositionBitmapSurface LoadBitmapSurface(Stream stream)
+ {
+ var bmp = _server.Backend.LoadCpuMemoryBitmap(stream);
+ return new CompositionBitmapSurface(this, bmp);
+ }
+
+ public async Task LoadBitmapSurfaceAsync(Stream stream)
+ {
+ var bmp = await Task.Run(() => _server.Backend.LoadCpuMemoryBitmap(stream));
+ return new CompositionBitmapSurface(this, bmp);
+ }
+ */
+ public CompositionColorBrush CreateColorBrush(Avalonia.Media.Color color) =>
+ new CompositionColorBrush(this, new ServerCompositionColorBrush(_server)) {Color = color};
+
+ public CompositionSurfaceBrush CreateSurfaceBrush() =>
+ new CompositionSurfaceBrush(this, new ServerCompositionSurfaceBrush(_server));
+
+ /*
+ public CompositionGaussianBlurEffectBrush CreateGaussianBlurEffectBrush() =>
+ new CompositionGaussianBlurEffectBrush(this, new ServerCompositionGaussianBlurEffectBrush(_server));
+
+ public CompositionBackdropBrush CreateBackdropBrush() =>
+ new CompositionBackdropBrush(this, new ServerCompositionBackdropBrush(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);
+
+ internal CustomDrawVisual CreateCustomDrawVisual(ICustomDrawVisualRenderer renderer,
+ ICustomDrawVisualHitTest? hitTest = null) where T : IEquatable =>
+ new CustomDrawVisual(this, renderer, hitTest);
+
+ public void QueueImplicitBatchCommit()
+ {
+ if(_implicitBatchCommitQueued)
+ return;
+ _implicitBatchCommitQueued = true;
+ Dispatcher.UIThread.Post(_implicitBatchCommit, DispatcherPriority.CompositionBatch);
+ }
+
+ private void ImplicitBatchCommit()
+ {
+ _implicitBatchCommitQueued = false;
+ RequestCommitAsync();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/CompositorRenderLoopTask.cs b/src/Avalonia.Base/Rendering/Composition/CompositorRenderLoopTask.cs
new file mode 100644
index 0000000000..074c0a9ccf
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/CompositorRenderLoopTask.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace Avalonia.Rendering.Composition;
+
+partial class Compositor
+{
+ class CompositorRenderLoopTask : IRenderLoopTask
+ {
+ public bool NeedsUpdate { get; }
+ public void Update(TimeSpan time)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void Render()
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs
new file mode 100644
index 0000000000..f650d3e995
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/ContainerVisual.cs
@@ -0,0 +1,20 @@
+using Avalonia.Rendering.Composition.Server;
+
+namespace Avalonia.Rendering.Composition
+{
+ public class CompositionContainerVisual : CompositionVisual
+ {
+ public CompositionVisualCollection Children { get; }
+ internal CompositionContainerVisual(Compositor compositor, ServerCompositionContainerVisual server) : base(compositor, server)
+ {
+ Children = new CompositionVisualCollection(this, server.Children);
+ }
+
+ private protected override void OnRootChanged()
+ {
+ foreach (var ch in Children)
+ ch.Root = Root;
+ base.OnRootChanged();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs b/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs
new file mode 100644
index 0000000000..0505d6a46c
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/CustomDrawVisual.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Numerics;
+using Avalonia.Platform;
+using Avalonia.Rendering.Composition.Server;
+using Avalonia.Rendering.Composition.Transport;
+
+namespace Avalonia.Rendering.Composition
+{
+ internal class CustomDrawVisual : CompositionContainerVisual where TData : IEquatable
+ {
+ private readonly ICustomDrawVisualHitTest? _hitTest;
+
+ internal CustomDrawVisual(Compositor compositor, ICustomDrawVisualRenderer renderer,
+ ICustomDrawVisualHitTest? hitTest) : base(compositor,
+ new ServerCustomDrawVisual(compositor.Server, renderer))
+ {
+ _hitTest = hitTest;
+ }
+
+ private TData? _data;
+
+ static bool Eq(TData? left, TData? right)
+ {
+ if (left == null && right == null)
+ return true;
+ if (left == null)
+ return false;
+ return left.Equals(right);
+ }
+
+ public TData? Data
+ {
+ get => _data;
+ set
+ {
+ if (!Eq(_data, value))
+ {
+ ((CustomDrawVisualChanges) Changes).Data.Value = value;
+ _data = value;
+ }
+ }
+ }
+
+ private protected override IChangeSetPool ChangeSetPool => CustomDrawVisualChanges.Pool;
+ }
+
+ public interface ICustomDrawVisualRenderer
+ {
+ void Render(IDrawingContextImpl canvas, TData? data);
+ }
+
+ public interface ICustomDrawVisualHitTest
+ {
+ bool HitTest(TData data, Vector2 vector2);
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs
new file mode 100644
index 0000000000..aca8ef7c46
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs
@@ -0,0 +1,81 @@
+using System;
+using Avalonia.Collections.Pooled;
+using Avalonia.Rendering.SceneGraph;
+using Avalonia.Utilities;
+
+namespace Avalonia.Rendering.Composition.Drawing;
+
+internal class CompositionDrawList : PooledList>
+{
+ 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);
+ foreach (var r in this)
+ clone.Add(r.Clone());
+ return clone;
+ }
+}
+
+internal class CompositionDrawListBuilder
+{
+ private CompositionDrawList? _operations;
+ private bool _owns;
+
+ public void Reset(CompositionDrawList? previousOperations)
+ {
+ _operations = previousOperations;
+ _owns = false;
+ }
+
+ public CompositionDrawList DrawOperations => _operations ?? new CompositionDrawList();
+
+ 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(DrawOperations.Count);
+ DrawOperations.Add(RefCountable.Create(node));
+ }
+
+ public void TrimTo(int count)
+ {
+ if (count < DrawOperations.Count)
+ DrawOperations.RemoveRange(count, DrawOperations.Count - count);
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs
new file mode 100644
index 0000000000..c8e5d9e064
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs
@@ -0,0 +1,383 @@
+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;
+
+internal class CompositionDrawingContext : IDrawingContextImpl
+{
+ private CompositionDrawListBuilder _builder = new();
+ private int _drawOperationIndex;
+
+ ///
+ public Matrix Transform { get; set; } = Matrix.Identity;
+
+ ///
+ public void Clear(Color color)
+ {
+ // Cannot clear a deferred scene.
+ }
+
+ ///
+ 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!;
+ }
+
+ ///
+ public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry)
+ {
+ var next = NextDrawAs();
+
+ if (next == null || !next.Item.Equals(Transform, brush, pen, geometry))
+ {
+ Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush)));
+ }
+ else
+ {
+ ++_drawOperationIndex;
+ }
+ }
+
+ ///
+ public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect,
+ BitmapInterpolationMode bitmapInterpolationMode)
+ {
+ var next = NextDrawAs();
+
+ if (next == null ||
+ !next.Item.Equals(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode))
+ {
+ Add(new ImageNode(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode));
+ }
+ else
+ {
+ ++_drawOperationIndex;
+ }
+ }
+
+ ///
+ public void DrawBitmap(IRef 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();
+ }
+
+ ///
+ public void DrawLine(IPen pen, Point p1, Point p2)
+ {
+ var next = NextDrawAs();
+
+ if (next == null || !next.Item.Equals(Transform, pen, p1, p2))
+ {
+ Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush)));
+ }
+ else
+ {
+ ++_drawOperationIndex;
+ }
+ }
+
+ ///
+ public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect,
+ BoxShadows boxShadows = default)
+ {
+ var next = NextDrawAs();
+
+ if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows))
+ {
+ Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush)));
+ }
+ else
+ {
+ ++_drawOperationIndex;
+ }
+ }
+
+ ///
+ public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect)
+ {
+ var next = NextDrawAs();
+
+ 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();
+
+ 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();
+ if (next == null || !next.Item.Equals(Transform, custom))
+ Add(new CustomDrawOperation(custom, Transform));
+ else
+ ++_drawOperationIndex;
+ }
+
+ ///
+ public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun)
+ {
+ var next = NextDrawAs();
+
+ 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");
+ }
+
+ ///
+ public void PopClip()
+ {
+ var next = NextDrawAs();
+
+ if (next == null || !next.Item.Equals(null))
+ {
+ Add(new ClipNode());
+ }
+ else
+ {
+ ++_drawOperationIndex;
+ }
+ }
+
+ ///
+ public void PopGeometryClip()
+ {
+ var next = NextDrawAs();
+
+ if (next == null || !next.Item.Equals(null))
+ {
+ Add(new GeometryClipNode());
+ }
+ else
+ {
+ ++_drawOperationIndex;
+ }
+ }
+
+ ///
+ public void PopBitmapBlendMode()
+ {
+ var next = NextDrawAs();
+
+ if (next == null || !next.Item.Equals(null))
+ {
+ Add(new BitmapBlendModeNode());
+ }
+ else
+ {
+ ++_drawOperationIndex;
+ }
+ }
+
+ ///
+ public void PopOpacity()
+ {
+ var next = NextDrawAs();
+
+ if (next == null || !next.Item.Equals(null))
+ {
+ Add(new OpacityNode());
+ }
+ else
+ {
+ ++_drawOperationIndex;
+ }
+ }
+
+ ///
+ public void PopOpacityMask()
+ {
+ var next = NextDrawAs();
+
+ if (next == null || !next.Item.Equals(null, null))
+ {
+ Add(new OpacityMaskNode());
+ }
+ else
+ {
+ ++_drawOperationIndex;
+ }
+ }
+
+ ///
+ public void PushClip(Rect clip)
+ {
+ var next = NextDrawAs();
+
+ if (next == null || !next.Item.Equals(Transform, clip))
+ {
+ Add(new ClipNode(Transform, clip));
+ }
+ else
+ {
+ ++_drawOperationIndex;
+ }
+ }
+
+ ///
+ public void PushClip(RoundedRect clip)
+ {
+ var next = NextDrawAs();
+
+ if (next == null || !next.Item.Equals(Transform, clip))
+ {
+ Add(new ClipNode(Transform, clip));
+ }
+ else
+ {
+ ++_drawOperationIndex;
+ }
+ }
+
+ ///
+ public void PushGeometryClip(IGeometryImpl? clip)
+ {
+ if (clip is null)
+ return;
+
+ var next = NextDrawAs();
+
+ if (next == null || !next.Item.Equals(Transform, clip))
+ {
+ Add(new GeometryClipNode(Transform, clip));
+ }
+ else
+ {
+ ++_drawOperationIndex;
+ }
+ }
+
+ ///
+ public void PushOpacity(double opacity)
+ {
+ var next = NextDrawAs();
+
+ if (next == null || !next.Item.Equals(opacity))
+ {
+ Add(new OpacityNode(opacity));
+ }
+ else
+ {
+ ++_drawOperationIndex;
+ }
+ }
+
+ ///
+ public void PushOpacityMask(IBrush mask, Rect bounds)
+ {
+ var next = NextDrawAs();
+
+ if (next == null || !next.Item.Equals(mask, bounds))
+ {
+ Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask)));
+ }
+ else
+ {
+ ++_drawOperationIndex;
+ }
+ }
+
+ ///
+ public void PushBitmapBlendMode(BitmapBlendingMode blendingMode)
+ {
+ var next = NextDrawAs();
+
+ if (next == null || !next.Item.Equals(blendingMode))
+ {
+ Add(new BitmapBlendModeNode(blendingMode));
+ }
+ else
+ {
+ ++_drawOperationIndex;
+ }
+ }
+
+ private void Add(T node) where T : class, IDrawOperation
+ {
+ if (_drawOperationIndex < _builder!.DrawOperations.Count)
+ {
+ _builder.ReplaceDrawOperation(_drawOperationIndex, node);
+ }
+ else
+ {
+ _builder.AddDrawOperation(node);
+ }
+
+ ++_drawOperationIndex;
+ }
+
+ private IRef? NextDrawAs() where T : class, IDrawOperation
+ {
+ return _drawOperationIndex < _builder!.DrawOperations.Count
+ ? _builder.DrawOperations[_drawOperationIndex] as IRef
+ : null;
+ }
+
+ private IDictionary? CreateChildScene(IBrush? brush)
+ {
+ /*
+ var visualBrush = brush as VisualBrush;
+
+ if (visualBrush != null)
+ {
+ var visual = visualBrush.Visual;
+
+ if (visual != null)
+ {
+ (visual as IVisualBrushInitialize)?.EnsureInitialized();
+ var scene = new Scene(visual);
+ _sceneBuilder.UpdateAll(scene);
+ return new Dictionary { { visualBrush.Visual, scene } };
+ }
+ }*/
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Enums.cs b/src/Avalonia.Base/Rendering/Composition/Enums.cs
new file mode 100644
index 0000000000..e349845cbf
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Enums.cs
@@ -0,0 +1,120 @@
+using System;
+
+namespace Avalonia.Rendering.Composition
+{
+ public enum CompositionBlendMode
+ {
+ /// No regions are enabled. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_clr.svg)
+ Clear,
+
+ /// Only the source will be present. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src.svg)
+ Src,
+
+ /// Only the destination will be present. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst.svg)
+ Dst,
+
+ /// Source is placed over the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-over.svg)
+ SrcOver,
+
+ /// Destination is placed over the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-over.svg)
+ DstOver,
+
+ /// The source that overlaps the destination, replaces the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-in.svg)
+ SrcIn,
+
+ /// Destination which overlaps the source, replaces the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-in.svg)
+ DstIn,
+
+ /// 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)
+ SrcOut,
+
+ /// 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)
+ DstOut,
+
+ /// Source which overlaps the destination, replaces the destination. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_src-atop.svg)
+ SrcATop,
+
+ /// Destination which overlaps the source replaces the source. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_dst-atop.svg)
+ DstATop,
+
+ /// The non-overlapping regions of source and destination are combined. [Porter Duff Compositing Operators] (https://drafts.fxtf.org/compositing-1/examples/PD_xor.svg)
+ Xor,
+
+ /// Display the sum of the source image and destination image. [Porter Duff Compositing Operators]
+ Plus,
+
+ /// Multiplies all components (= alpha and color). [Separable Blend Modes]
+ Modulate,
+
+ /// Multiplies the complements of the backdrop and source CompositionColorvalues, then complements the result. [Separable Blend Modes]
+ Screen,
+
+ /// Multiplies or screens the colors, depending on the backdrop CompositionColorvalue. [Separable Blend Modes]
+ Overlay,
+
+ /// Selects the darker of the backdrop and source colors. [Separable Blend Modes]
+ Darken,
+
+ /// Selects the lighter of the backdrop and source colors. [Separable Blend Modes]
+ Lighten,
+
+ /// Brightens the backdrop CompositionColorto reflect the source color. [Separable Blend Modes]
+ ColorDodge,
+
+ /// Darkens the backdrop CompositionColorto reflect the source color. [Separable Blend Modes]
+ ColorBurn,
+
+ /// Multiplies or screens the colors, depending on the source CompositionColorvalue. [Separable Blend Modes]
+ HardLight,
+
+ /// Darkens or lightens the colors, depending on the source CompositionColorvalue. [Separable Blend Modes]
+ SoftLight,
+
+ /// Subtracts the darker of the two constituent colors from the lighter color. [Separable Blend Modes]
+ Difference,
+
+ /// Produces an effect similar to that of the Difference mode but lower in contrast. [Separable Blend Modes]
+ Exclusion,
+
+ /// The source CompositionColoris multiplied by the destination CompositionColorand replaces the destination [Separable Blend Modes]
+ Multiply,
+
+ /// Creates a CompositionColorwith the hue of the source CompositionColorand the saturation and luminosity of the backdrop color. [Non-Separable Blend Modes]
+ Hue,
+
+ /// Creates a CompositionColorwith the saturation of the source CompositionColorand the hue and luminosity of the backdrop color. [Non-Separable Blend Modes]
+ Saturation,
+
+ /// Creates a CompositionColorwith the hue and saturation of the source CompositionColorand the luminosity of the backdrop color. [Non-Separable Blend Modes]
+ Color,
+
+ /// Creates a CompositionColorwith the luminosity of the source CompositionColorand the hue and saturation of the backdrop color. [Non-Separable Blend Modes]
+ 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
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs
new file mode 100644
index 0000000000..db9a26e301
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Expressions/BuiltInExpressionFfi.cs
@@ -0,0 +1,234 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using Avalonia.Rendering.Composition.Animations;
+using Avalonia.Rendering.Composition.Utils;
+
+namespace Avalonia.Rendering.Composition.Expressions
+{
+ 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 = MathExt.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) => MathExt.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) MathExt.Clamp(a, 0, 255),
+ (byte) MathExt.Clamp(r, 0, 255),
+ (byte) MathExt.Clamp(g, 0, 255),
+ (byte) MathExt.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 arguments, out ExpressionVariant result) =>
+ _registry.Call(name, arguments, out result);
+
+ public static BuiltInExpressionFfi Instance { get; } = new BuiltInExpressionFfi();
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs
new file mode 100644
index 0000000000..002cf37522
--- /dev/null
+++ b/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs
@@ -0,0 +1,181 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using Avalonia.Media;
+
+namespace Avalonia.Rendering.Composition.Expressions
+{
+ internal class DelegateExpressionFfi : IExpressionForeignFunctionInterface, IEnumerable
+ {
+ struct FfiRecord
+ {
+ public VariantType[] Types;
+ public Func, ExpressionVariant> Delegate;
+ }
+
+ private readonly Dictionary>>
+ _registry = new Dictionary>>();
+
+ public bool Call(string name, IReadOnlyList 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