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().GetEnumerator(); + + void Add(string name, Func, ExpressionVariant> cb, + params Type[] types) + { + if (!_registry.TryGetValue(name, out var nameGroup)) + _registry[name] = nameGroup = + new Dictionary>(); + if (!nameGroup.TryGetValue(types.Length, out var countGroup)) + nameGroup[types.Length] = countGroup = new List(); + + countGroup.Add(new FfiRecord + { + Types = types.Select(t => TypeMap[t]).ToArray(), + Delegate = cb + }); + } + + static readonly Dictionary TypeMap = new Dictionary + { + [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(string name, Func cb) where T1 : struct + { + Add(name, args => cb(args[0].CastOrDefault()), typeof(T1)); + } + + public void Add(string name, Func cb) where T1 : struct where T2 : struct + { + Add(name, args => cb(args[0].CastOrDefault(), args[1].CastOrDefault()), typeof(T1), typeof(T2)); + } + + + public void Add(string name, Func cb) + where T1 : struct where T2 : struct where T3 : struct + { + Add(name, args => cb(args[0].CastOrDefault(), args[1].CastOrDefault(), args[2].CastOrDefault()), typeof(T1), typeof(T2), + typeof(T3)); + } + + public void Add(string name, Func cb) + where T1 : struct where T2 : struct where T3 : struct where T4 : struct + { + Add(name, args => cb( + args[0].CastOrDefault(), + args[1].CastOrDefault(), + args[2].CastOrDefault(), + args[3].CastOrDefault()), + typeof(T1), typeof(T2), typeof(T3), typeof(T4)); + } + + public void Add(string name, Func cb) + where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct + { + Add(name, args => cb( + args[0].CastOrDefault(), + args[1].CastOrDefault(), + args[2].CastOrDefault(), + args[3].CastOrDefault(), + args[4].CastOrDefault()), + typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5)); + } + + public void Add(string name, Func 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(), + args[1].CastOrDefault(), + args[2].CastOrDefault(), + args[3].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault()), + typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6)); + } + + + public void Add(string name, + Func 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(), + args[1].CastOrDefault(), + args[2].CastOrDefault(), + args[3].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault(), + args[4].CastOrDefault() + ), + 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) + ); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs new file mode 100644 index 0000000000..5577d2b52a --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs @@ -0,0 +1,331 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; + +namespace Avalonia.Rendering.Composition.Expressions +{ + 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); + + protected abstract string Print(); + public override string ToString() => Print(); + + internal static string OperatorName(ExpressionType t) + { + var attr = typeof(ExpressionType).GetMember(t.ToString())[0] + .GetCustomAttribute(); + 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); + } + + 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 Parameters { get; } + public override ExpressionType Type => ExpressionType.FunctionCall; + + public FunctionCallExpression(string name, List parameters) + { + Name = name; + Parameters = parameters; + } + + public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) + { + if (context.ForeignFunctionInterface == null) + return default; + var args = new List(); + foreach (var expr in Parameters) + args.Add(expr.Evaluate(ref context)); + if (!context.ForeignFunctionInterface.Call(Name, args, out var res)) + return default; + return res; + } + + 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 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); + } + + 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; + } + + 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; + } + + protected override string Print() + { + return "(" + Left + OperatorName(Type) + Right + ")"; + } + + public BinaryExpression(Expression left, Expression right, ExpressionType type) + { + Left = left; + Right = right; + Type = type; + } + } + + + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs new file mode 100644 index 0000000000..a7ddabd70d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +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 arguments, out ExpressionVariant result); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParseException.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParseException.cs new file mode 100644 index 0000000000..6a207a3bf7 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParseException.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs new file mode 100644 index 0000000000..5924bb8f1b --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs @@ -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 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 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 _expressions; + private List _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(); + _expressions.Add(_first); + _first = null; + _operators = new List(); + } + _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(); + 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); + } + + + + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs new file mode 100644 index 0000000000..8c6af5cb0c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs @@ -0,0 +1,739 @@ +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 + } + + [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.AvaloniaMatrix) + return left.AvaloniaMatrix + right.AvaloniaMatrix; + + 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.AvaloniaMatrix) + return left.AvaloniaMatrix - right.AvaloniaMatrix; + + 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.AvaloniaMatrix && right.Type == VariantType.Scalar) + return left.AvaloniaMatrix * (double)right.Scalar; + + if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.Double) + return left.AvaloniaMatrix * right.Double; + + 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(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 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() where T : struct + { + TryCast(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"; + } + } + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs new file mode 100644 index 0000000000..1050c7274c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs @@ -0,0 +1,256 @@ +using System; +using System.Globalization; + +namespace Avalonia.Rendering.Composition.Expressions +{ + internal ref struct TokenParser + { + private ReadOnlySpan _s; + public int Position { get; private set; } + public TokenParser(ReadOnlySpan 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 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 && 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 && 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 extraValidChars, out ReadOnlySpan res) + { + res = ReadOnlySpan.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 res) + { + res = ReadOnlySpan.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 res) + { + res = ReadOnlySpan.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(); + + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/ICompositionSurface.cs b/src/Avalonia.Base/Rendering/Composition/ICompositionSurface.cs new file mode 100644 index 0000000000..9ef31c30e0 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/ICompositionSurface.cs @@ -0,0 +1,9 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition +{ + public interface ICompositionSurface + { + internal ServerCompositionSurface Server { get; } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs b/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs new file mode 100644 index 0000000000..5e91bcb3d4 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs @@ -0,0 +1,46 @@ +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; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs new file mode 100644 index 0000000000..f5dfa92897 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -0,0 +1,142 @@ +using System.Numerics; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal class CompositorDrawingContextProxy : IDrawingContextImpl +{ + private IDrawingContextImpl _impl; + + public CompositorDrawingContextProxy(IDrawingContextImpl impl) + { + _impl = impl; + } + + public Matrix PreTransform { get; set; } = Matrix.Identity; + + public void Dispose() + { + _impl.Dispose(); + } + + Matrix _transform; + public Matrix Transform + { + get => _transform; + set => _impl.Transform = PreTransform * (_transform = value); + } + + public void Clear(Color color) + { + _impl.Clear(color); + } + + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) + { + _impl.DrawBitmap(source, opacity, sourceRect, destRect, bitmapInterpolationMode); + } + + public void DrawBitmap(IRef 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 Matrix CutTransform(Matrix4x4 transform) => new Matrix(transform.M11, transform.M12, transform.M21, + transform.M22, transform.M41, + transform.M42); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs new file mode 100644 index 0000000000..372fa4d9ce --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs @@ -0,0 +1,46 @@ +namespace Avalonia.Rendering.Composition.Server +{ + internal class ReadbackIndices + { + private readonly object _lock = new object(); + public int ReadIndex { get; private set; } = 0; + public int WriteIndex { get; private set; } = -1; + public ulong ReadRevision { get; private set; } + public ulong WriteRevision { get; private set; } + private ulong[] _revisions = new ulong[3]; + + + public void NextRead() + { + lock (_lock) + { + for (var c = 0; c < 3; c++) + { + if (c != WriteIndex && c != ReadIndex && _revisions[c] > ReadRevision) + { + ReadIndex = c; + ReadRevision = _revisions[c]; + return; + } + } + } + } + + public void NextWrite(ulong revision) + { + lock (_lock) + { + for (var c = 0; c < 3; c++) + { + if (c != WriteIndex && c != ReadIndex) + { + WriteIndex = c; + WriteRevision = revision; + _revisions[c] = revision; + return; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBrush.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBrush.cs new file mode 100644 index 0000000000..eb041aaf88 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBrush.cs @@ -0,0 +1,7 @@ +namespace Avalonia.Rendering.Composition.Server +{ + internal abstract partial class ServerCompositionBrush : ServerObject + { + + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs new file mode 100644 index 0000000000..397c968d04 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -0,0 +1,41 @@ +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; + +internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisual +{ + private CompositionDrawList? _renderCommands; + + public ServerCompositionDrawListVisual(ServerCompositor compositor) : base(compositor) + { + } + + protected override void ApplyCore(ChangeSet changes) + { + var ch = (DrawListVisualChanges)changes; + if (ch.DrawCommandsIsSet) + { + _renderCommands?.Dispose(); + _renderCommands = ch.AcquireDrawCommands(); + } + base.ApplyCore(changes); + } + + protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + if (_renderCommands != null) + { + foreach (var cmd in _renderCommands) + { + cmd.Item.Render(canvas); + } + } + base.RenderCore(canvas, transform); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionGradientBrush.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionGradientBrush.cs new file mode 100644 index 0000000000..0948b9692f --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionGradientBrush.cs @@ -0,0 +1,15 @@ +using System; + +namespace Avalonia.Rendering.Composition.Server +{ + internal abstract partial class ServerCompositionGradientBrush : ServerCompositionBrush + { + public ServerCompositionGradientStopCollection Stops { get; } + public ServerCompositionGradientBrush(ServerCompositor compositor) : base(compositor) + { + Stops = new ServerCompositionGradientStopCollection(compositor); + } + + public override long LastChangedBy => Math.Max(base.LastChangedBy, (long)Stops.LastChangedBy); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs new file mode 100644 index 0000000000..c421cdcfb0 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionLinearGradientBrush.cs @@ -0,0 +1,22 @@ +using Avalonia.Media; + +namespace Avalonia.Rendering.Composition.Server +{ + internal partial class ServerCompositionLinearGradientBrush + { + /* + protected override void UpdateBackendBrush(ICbBrush brush) + { + var stopColors = new Color[Stops.List.Count]; + var offsets = new float[Stops.List.Count]; + for (var c = 0; c < Stops.List.Count; c++) + { + stopColors[c] = Stops.List[c].Color; + offsets[c] = Stops.List[c].Offset; + } + + ((ICbLinearGradientBrush) brush).Update(StartPoint, EndPoint, stopColors, offsets, ExtendMode); + }*/ + + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs new file mode 100644 index 0000000000..462a193a86 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Rendering.Composition.Server +{ + internal abstract class ServerCompositionSurface : ServerObject + { + protected ServerCompositionSurface(ServerCompositor compositor) : base(compositor) + { + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs new file mode 100644 index 0000000000..493529e111 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -0,0 +1,53 @@ +using System; +using System.Numerics; +using System.Threading; +using Avalonia.Media; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server +{ + internal partial class ServerCompositionTarget + { + private readonly ServerCompositor _compositor; + private readonly Func _renderTargetFactory; + private static long s_nextId = 1; + public long Id { get; } + private ulong _frame = 1; + private IRenderTarget? _renderTarget; + + public ReadbackIndices Readback { get; } = new(); + + public ServerCompositionTarget(ServerCompositor compositor, Func renderTargetFactory) : + base(compositor) + { + _compositor = compositor; + _renderTargetFactory = renderTargetFactory; + Id = Interlocked.Increment(ref s_nextId); + } + + partial void OnIsEnabledChanged() + { + if (IsEnabled) + _compositor.AddCompositionTarget(this); + else + _compositor.RemoveCompositionTarget(this); + } + + public void Render() + { + if (Root == null) + return; + _renderTarget ??= _renderTargetFactory(); + + Compositor.UpdateServerTime(); + using (var context = _renderTarget.CreateDrawingContext(null)) + { + context.Clear(Colors.Transparent); + Root.Render(new CompositorDrawingContextProxy(context), Root.CombinedTransformMatrix); + } + + Readback.NextWrite(_frame); + _frame++; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs new file mode 100644 index 0000000000..e56f85acdf --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + internal class ServerCompositor : IRenderLoopTask + { + private readonly IRenderLoop _renderLoop; + private readonly Queue _batches = new Queue(); + public long LastBatchId { get; private set; } + public Stopwatch Clock { get; } = Stopwatch.StartNew(); + public TimeSpan ServerNow { get; private set; } + private List _activeTargets = new(); + + public ServerCompositor(IRenderLoop renderLoop) + { + _renderLoop = renderLoop; + _renderLoop.Add(this); + } + + public void EnqueueBatch(Batch batch) + { + lock (_batches) + _batches.Enqueue(batch); + } + + internal void UpdateServerTime() => ServerNow = Clock.Elapsed; + + List _reusableToCompleteList = new(); + void ApplyPendingBatches() + { + while (true) + { + Batch batch; + lock (_batches) + { + if(_batches.Count == 0) + break; + batch = _batches.Dequeue(); + } + + foreach (var change in batch.Changes) + { + if (change.Dispose) + { + //TODO + } + change.Target!.Apply(change); + change.Reset(); + } + + _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) + { + } + + void IRenderLoopTask.Render() + { + ApplyPendingBatches(); + foreach (var t in _activeTargets) + t.Render(); + + CompletePendingBatches(); + } + + public void AddCompositionTarget(ServerCompositionTarget target) + { + _activeTargets.Add(target); + } + + public void RemoveCompositionTarget(ServerCompositionTarget target) + { + _activeTargets.Remove(target); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs new file mode 100644 index 0000000000..ac112b846f --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs @@ -0,0 +1,29 @@ +using System.Numerics; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server +{ + internal class ServerCompositionContainerVisual : ServerCompositionVisual + { + public ServerCompositionVisualCollection Children { get; } + + + protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + base.RenderCore(canvas, transform); + + foreach (var ch in Children) + { + var t = transform; + + t = ch.CombinedTransformMatrix * t; + ch.Render(canvas, t); + } + } + + public ServerCompositionContainerVisual(ServerCompositor compositor) : base(compositor) + { + Children = new ServerCompositionVisualCollection(compositor); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs new file mode 100644 index 0000000000..5f3eb051a4 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomDrawVisual.cs @@ -0,0 +1,31 @@ +using System.Numerics; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + class ServerCustomDrawVisual : ServerCompositionContainerVisual + { + private readonly ICustomDrawVisualRenderer _renderer; + private TData? _data; + public ServerCustomDrawVisual(ServerCompositor compositor, ICustomDrawVisualRenderer renderer) : base(compositor) + { + _renderer = renderer; + } + + protected override void ApplyCore(ChangeSet changes) + { + var c = (CustomDrawVisualChanges) changes; + if (c.Data.IsSet) + _data = c.Data.Value; + + base.ApplyCore(changes); + } + + protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + _renderer.Render(canvas, _data); + base.RenderCore(canvas, transform); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs new file mode 100644 index 0000000000..09ef119e6b --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + class ServerList : ServerObject where T : ServerObject + { + public List List { get; } = new List(); + protected override void ApplyCore(ChangeSet changes) + { + var c = (ListChangeSet) changes; + if (c.HasListChanges) + { + foreach (var lc in c.ListChanges) + { + if(lc.Action == ListChangeAction.Clear) + List.Clear(); + if(lc.Action == ListChangeAction.RemoveAt) + List.RemoveAt(lc.Index); + if(lc.Action == ListChangeAction.InsertAt) + List.Insert(lc.Index, lc.Added!); + if (lc.Action == ListChangeAction.ReplaceAt) + List[lc.Index] = lc.Added!; + } + } + } + + public override long LastChangedBy + { + get + { + var seq = base.LastChangedBy; + foreach (var i in List) + seq = Math.Max(i.LastChangedBy, seq); + return seq; + } + } + + public List.Enumerator GetEnumerator() => List.GetEnumerator(); + + public ServerList(ServerCompositor compositor) : base(compositor) + { + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs new file mode 100644 index 0000000000..072377cd7e --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -0,0 +1,36 @@ +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + internal abstract class ServerObject : IExpressionObject + { + public ServerCompositor Compositor { get; } + + public virtual long LastChangedBy => ItselfLastChangedBy; + public long ItselfLastChangedBy { get; private set; } + + public ServerObject(ServerCompositor compositor) + { + Compositor = compositor; + } + + protected virtual void ApplyCore(ChangeSet changes) + { + + } + + public void Apply(ChangeSet changes) + { + ApplyCore(changes); + ItselfLastChangedBy = changes.Batch!.SequenceId; + } + + public virtual ExpressionVariant GetPropertyForAnimation(string name) + { + return default; + } + + ExpressionVariant IExpressionObject.GetProperty(string name) => GetPropertyForAnimation(name); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs new file mode 100644 index 0000000000..60569867de --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerSolidColorVisual.cs @@ -0,0 +1,16 @@ +using System.Numerics; +using Avalonia.Media.Immutable; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server +{ + internal partial class ServerCompositionSolidColorVisual + { + protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + canvas.Transform = canvas.CutTransform(transform); + canvas.DrawRectangle(new ImmutableSolidColorBrush(Color), null, new RoundedRect(new Rect(new Size(Size)))); + base.RenderCore(canvas, transform); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs new file mode 100644 index 0000000000..c658dc8ae3 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerSpriteVisual.cs @@ -0,0 +1,20 @@ +using System.Numerics; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server +{ + internal partial class ServerCompositionSpriteVisual + { + + protected override void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + if (Brush != null) + { + //SetTransform(canvas, transform); + //canvas.FillRect((Vector2)Size, (ICbBrush)Brush.Brush!); + } + + base.RenderCore(canvas, transform); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs new file mode 100644 index 0000000000..37e188fb47 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -0,0 +1,92 @@ +using System.Numerics; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server +{ + unsafe partial class ServerCompositionVisual : ServerObject + { + protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + + } + + public void Render(CompositorDrawingContextProxy canvas, Matrix4x4 transform) + { + if(Visible == false) + return; + if(Opacity == 0) + return; + canvas.PreTransform = canvas.CutTransform(transform); + canvas.Transform = Matrix.Identity; + if (Opacity != 1) + canvas.PushOpacity(Opacity); + if(ClipToBounds) + canvas.PushClip(new Rect(new Size(Size.X, Size.Y))); + if (Clip != null) + canvas.PushGeometryClip(Clip); + + RenderCore(canvas, transform); + + if (Clip != null) + canvas.PopGeometryClip(); + if (ClipToBounds) + canvas.PopClip(); + if(Opacity != 1) + canvas.PopOpacity(); + } + + private ReadbackData _readback0, _readback1, _readback2; + + + 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 + { + if (Root == null) + return default; + + var res = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, + Scale, RotationAngle, Orientation, Offset); + var i = Root.Readback; + ref var readback = ref GetReadback(i.WriteIndex); + readback.Revision = i.WriteRevision; + readback.Matrix = res; + readback.TargetId = Root.Id; + + return res; + } + } + + public struct ReadbackData + { + public Matrix4x4 Matrix; + public bool Visible; + public ulong Revision; + public long TargetId; + } + + partial void ApplyChangesExtra(CompositionVisualChanges c) + { + if (c.Parent.IsSet) + Parent = c.Parent.Value; + if (c.Root.IsSet) + Root = c.Root.Value; + } + + public ServerCompositionTarget? Root { get; private set; } + + public ServerCompositionVisual? Parent { get; private set; } + } + + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs new file mode 100644 index 0000000000..7b64c01d09 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Avalonia.Rendering.Composition.Transport +{ + internal class Batch + { + private static long _nextSequenceId = 1; + private static ConcurrentBag> _pool = new ConcurrentBag>(); + public long SequenceId { get; } + + public Batch() + { + SequenceId = Interlocked.Increment(ref _nextSequenceId); + if (!_pool.TryTake(out var lst)) + lst = new List(); + Changes = lst; + } + private TaskCompletionSource _tcs = new TaskCompletionSource(); + public List Changes { get; private set; } + public TimeSpan CommitedAt { get; set; } + + public void Complete() + { + Changes.Clear(); + _pool.Add(Changes); + Changes = null!; + + _tcs.TrySetResult(0); + } + + public Task Completed => _tcs.Task; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs b/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs new file mode 100644 index 0000000000..cbee350ab3 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/Change.cs @@ -0,0 +1,82 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Rendering.Composition.Animations; + +namespace Avalonia.Rendering.Composition.Transport +{ + struct Change + { + private T? _value; + + public bool IsSet { get; private set; } + + public T? Value + { + get + { + if(!IsSet) + throw new InvalidOperationException(); + return _value; + } + set + { + IsSet = true; + _value = value; + } + } + + public void Reset() + { + _value = default; + IsSet = false; + } + } + + struct AnimatedChange + { + private T? _value; + private IAnimationInstance? _animation; + + public bool IsValue { get; private set; } + public bool IsAnimation { get; private set; } + + public T Value + { + get + { + if(!IsValue) + throw new InvalidOperationException(); + return _value!; + } + set + { + IsAnimation = false; + _animation = null; + IsValue = true; + _value = value; + } + } + + public IAnimationInstance Animation + { + get + { + if(!IsAnimation) + throw new InvalidOperationException(); + return _animation!; + } + set + { + IsValue = false; + _value = default; + IsAnimation = true; + _animation = value; + } + } + + public void Reset() + { + this = default; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs new file mode 100644 index 0000000000..898885dce6 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSet.cs @@ -0,0 +1,36 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + internal abstract class ChangeSet + { + private readonly IChangeSetPool _pool; + public Batch Batch = null!; + public ServerObject? Target; + public bool Dispose; + + public ChangeSet(IChangeSetPool pool) + { + _pool = pool; + } + + public virtual void Reset() + { + Batch = null!; + Target = null; + Dispose = false; + } + + public void Return() + { + _pool.Return(this); + } + } + + internal class CompositionObjectChanges : ChangeSet + { + public CompositionObjectChanges(IChangeSetPool pool) : base(pool) + { + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs new file mode 100644 index 0000000000..ea97cd7d44 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ChangeSetPool.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Concurrent; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + interface IChangeSetPool + { + void Return(ChangeSet changes); + ChangeSet Get(ServerObject target, Batch batch); + } + + class ChangeSetPool : IChangeSetPool where T : ChangeSet + { + private readonly Func _factory; + private readonly ConcurrentBag _pool = new ConcurrentBag(); + + public ChangeSetPool(Func factory) + { + _factory = factory; + } + + public void Return(T changes) + { + changes.Reset(); + _pool.Add(changes); + } + + void IChangeSetPool.Return(ChangeSet changes) => Return((T) changes); + ChangeSet IChangeSetPool.Get(ServerObject target, Batch batch) => Get(target, batch); + + public T Get(ServerObject target, Batch batch) + { + if (!_pool.TryTake(out var res)) + res = _factory(this); + res.Target = target; + res.Batch = batch; + res.Dispose = false; + return res; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs new file mode 100644 index 0000000000..aed041b62e --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/CustomDrawVisualChanges.cs @@ -0,0 +1,20 @@ +namespace Avalonia.Rendering.Composition.Transport +{ + class CustomDrawVisualChanges : CompositionVisualChanges + { + public CustomDrawVisualChanges(IChangeSetPool pool) : base(pool) + { + } + + public Change Data; + + public override void Reset() + { + Data.Reset(); + base.Reset(); + } + + public new static ChangeSetPool> Pool { get; } = + new ChangeSetPool>(pool => new CustomDrawVisualChanges(pool)); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs new file mode 100644 index 0000000000..215c03b229 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/DrawListVisualChanges.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using Avalonia.Collections.Pooled; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Transport; + +internal class DrawListVisualChanges : CompositionVisualChanges +{ + private CompositionDrawList? _drawCommands; + + public DrawListVisualChanges(IChangeSetPool pool) : base(pool) + { + } + + public CompositionDrawList? DrawCommands + { + get => _drawCommands; + set + { + _drawCommands?.Dispose(); + _drawCommands = value; + DrawCommandsIsSet = true; + } + } + + public bool DrawCommandsIsSet { get; private set; } + + public CompositionDrawList? AcquireDrawCommands() + { + var rv = _drawCommands; + _drawCommands = null; + DrawCommandsIsSet = false; + return rv; + } + + public override void Reset() + { + _drawCommands?.Dispose(); + _drawCommands = null; + DrawCommandsIsSet = false; + base.Reset(); + } + + public new static ChangeSetPool Pool { get; } = + new ChangeSetPool(pool => new(pool)); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs new file mode 100644 index 0000000000..ee6e4231f8 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ListChange.cs @@ -0,0 +1,19 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + internal class ListChange where T : ServerObject + { + public int Index; + public ListChangeAction Action; + public T? Added; + } + + internal enum ListChangeAction + { + InsertAt, + RemoveAt, + Clear, + ReplaceAt + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs new file mode 100644 index 0000000000..9bb101a080 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ListChangeSet.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + class ListChangeSet : ChangeSet where T : ServerObject + { + private List>? _listChanges; + public List> ListChanges => _listChanges ??= new List>(); + public bool HasListChanges => _listChanges != null; + + public override void Reset() + { + _listChanges?.Clear(); + base.Reset(); + } + + public ListChangeSet(IChangeSetPool pool) : base(pool) + { + } + + public static readonly ChangeSetPool> Pool = + new ChangeSetPool>(pool => new ListChangeSet(pool)); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs b/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs new file mode 100644 index 0000000000..1add3aa990 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/ServerListProxyHelper.cs @@ -0,0 +1,92 @@ +using System.Collections; +using System.Collections.Generic; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + class ServerListProxyHelper : IList + where TServer : ServerObject + where TClient : CompositionObject + { + private readonly IGetChanges _parent; + private readonly List _list = new List(); + + public interface IGetChanges + { + ListChangeSet GetChanges(); + } + + public ServerListProxyHelper(IGetChanges parent) + { + _parent = parent; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public List.Enumerator GetEnumerator() => _list.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void Add(TClient item) => Insert(_list.Count, item); + + public void Clear() + { + _list.Clear(); + _parent.GetChanges().ListChanges.Add(new ListChange + { + Action = ListChangeAction.Clear + }); + } + + 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); + _parent.GetChanges().ListChanges.Add(new ListChange + { + Action = ListChangeAction.InsertAt, + Index = index, + Added = (TServer) item.Server + }); + } + + public void RemoveAt(int index) + { + _list.RemoveAt(index); + _parent.GetChanges().ListChanges.Add(new ListChange + { + Action = ListChangeAction.RemoveAt, + Index = index + }); + } + + public TClient this[int index] + { + get => _list[index]; + set + { + _list[index] = value; + _parent.GetChanges().ListChanges.Add(new ListChange + { + Action = ListChangeAction.ReplaceAt, + Index = index, + Added = (TServer) value.Server + }); + } + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs b/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs new file mode 100644 index 0000000000..c87fb96967 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Transport/VisualChanges.cs @@ -0,0 +1,16 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition.Transport +{ + partial class CompositionVisualChanges + { + public Change Parent; + public Change Root; + + partial void ResetExtra() + { + Parent.Reset(); + Root.Reset(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Utils/CubicBezier.cs b/src/Avalonia.Base/Rendering/Composition/Utils/CubicBezier.cs new file mode 100644 index 0000000000..8c85d7978b --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Utils/CubicBezier.cs @@ -0,0 +1,302 @@ +// ReSharper disable InconsistentNaming +// Ported from Chromium project https://github.com/chromium/chromium/blob/374d31b7704475fa59f7b2cb836b3b68afdc3d79/ui/gfx/geometry/cubic_bezier.cc + +using System; + +// ReSharper disable CompareOfFloatsByEqualityOperator +// ReSharper disable CommentTypo +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable TooWideLocalVariableScope +// ReSharper disable UnusedMember.Global +#pragma warning disable 649 + +namespace Avalonia.Rendering.Composition.Utils +{ + 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 = MathExt.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_; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Utils/MathExt.cs b/src/Avalonia.Base/Rendering/Composition/Utils/MathExt.cs new file mode 100644 index 0000000000..0be19a8e9d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Utils/MathExt.cs @@ -0,0 +1,23 @@ +using System; + +namespace Avalonia.Rendering.Composition.Utils +{ + static class MathExt + { + public static float Clamp(float value, float min, float max) + { + var amax = Math.Max(min, max); + var amin = Math.Min(min, max); + return Math.Min(Math.Max(value, amin), amax); + } + + public static double Clamp(double value, double min, double max) + { + var amax = Math.Max(min, max); + var amin = Math.Min(min, max); + return Math.Min(Math.Max(value, amin), amax); + } + + + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Visual.cs b/src/Avalonia.Base/Rendering/Composition/Visual.cs new file mode 100644 index 0000000000..1e6d7f8abb --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Visual.cs @@ -0,0 +1,64 @@ +using System.Numerics; + +namespace Avalonia.Rendering.Composition +{ + public abstract partial class CompositionVisual + { + private CompositionVisual? _parent; + private CompositionTarget? _root; + + public CompositionVisual? Parent + { + get => _parent; + internal set + { + if (_parent == value) + return; + _parent = value; + Changes.Parent.Value = value?.Server; + Root = _parent?.Root; + } + } + + // TODO: hide behind private-ish API + public CompositionTarget? Root + { + get => _root; + internal set + { + var changed = _root != value; + _root = value; + Changes.Root.Value = value?.Server; + if (changed) + OnRootChanged(); + } + } + + private protected virtual void OnRootChanged() + { + } + + + internal Matrix4x4? TryGetServerTransform() + { + if (Root == null) + return null; + var i = Root.Server.Readback; + ref var readback = ref Server.GetReadback(i.ReadIndex); + + // CompositionVisual wasn't visible + if (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(Vector2 point) => true; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs new file mode 100644 index 0000000000..35f33c3b38 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/VisualCollection.cs @@ -0,0 +1,64 @@ +using System; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition +{ + 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 OnBeforeAdded(CompositionVisual item) + { + if (item.Parent != null) + throw new InvalidOperationException("Visual already has a parent"); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/IRenderer.cs b/src/Avalonia.Base/Rendering/IRenderer.cs index e998f78d5c..8d6aabf440 100644 --- a/src/Avalonia.Base/Rendering/IRenderer.cs +++ b/src/Avalonia.Base/Rendering/IRenderer.cs @@ -1,6 +1,7 @@ using System; using Avalonia.VisualTree; using System.Collections.Generic; +using Avalonia.Rendering.Composition; namespace Avalonia.Rendering { @@ -87,4 +88,9 @@ namespace Avalonia.Rendering /// void Stop(); } + + public interface IRendererWithCompositor : IRenderer + { + Compositor Compositor { get; } + } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 5225b85020..3f495c619c 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Numerics; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Utilities; diff --git a/src/Avalonia.Base/Size.cs b/src/Avalonia.Base/Size.cs index 69c3ae7319..5f20206200 100644 --- a/src/Avalonia.Base/Size.cs +++ b/src/Avalonia.Base/Size.cs @@ -52,6 +52,17 @@ namespace Avalonia _width = width; _height = height; } + +#if !BUILDTASK + /// + /// Initializes a new instance of the structure. + /// + /// The vector to take values from. + public Size(System.Numerics.Vector2 vector2) : this(vector2.X, vector2.Y) + { + + } +#endif /// /// Gets the aspect ratio of the size. diff --git a/src/Avalonia.Base/Threading/DispatcherPriority.cs b/src/Avalonia.Base/Threading/DispatcherPriority.cs index a93e4f406d..b4bf603f74 100644 --- a/src/Avalonia.Base/Threading/DispatcherPriority.cs +++ b/src/Avalonia.Base/Threading/DispatcherPriority.cs @@ -62,10 +62,20 @@ namespace Avalonia.Threading /// public static readonly DispatcherPriority Render = new(5); + /// + /// The job will be processed with the same priority as composition batch commit. + /// + public static readonly DispatcherPriority CompositionBatch = new(6); + + /// + /// The job will be processed with the same priority as composition updates. + /// + public static readonly DispatcherPriority Composition = new(7); + /// /// The job will be processed with the same priority as render. /// - public static readonly DispatcherPriority Layout = new(6); + public static readonly DispatcherPriority Layout = new(8); /// /// The job will be processed with the same priority as data binding. @@ -75,7 +85,7 @@ namespace Avalonia.Threading /// /// The job will be processed before other asynchronous operations. /// - public static readonly DispatcherPriority Send = new(7); + public static readonly DispatcherPriority Send = new(9); /// /// Maximum possible priority diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 4fd21f02f9..64341be0c7 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -1,3 +1,7 @@ + + +#nullable enable + using System; using System.Collections; using System.Collections.Specialized; @@ -8,11 +12,10 @@ using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Server; using Avalonia.Utilities; using Avalonia.VisualTree; - -#nullable enable - namespace Avalonia { /// @@ -288,6 +291,8 @@ namespace Avalonia /// protected IRenderRoot? VisualRoot => _visualRoot ?? (this as IRenderRoot); + internal CompositionDrawListVisual? CompositionVisual { get; private set; } + /// /// Gets a value indicating whether this control is attached to a visual root. /// @@ -432,6 +437,10 @@ namespace Avalonia } EnableTransitions(); + if (_visualRoot.Renderer is IRendererWithCompositor compositingRenderer) + { + AttachToCompositor(compositingRenderer.Compositor); + } OnAttachedToVisualTree(e); AttachedToVisualTree?.Invoke(this, e); InvalidateVisual(); @@ -452,6 +461,14 @@ namespace Avalonia } } + internal CompositionVisual AttachToCompositor(Compositor compositor) + { + if (CompositionVisual == null || CompositionVisual.Compositor != compositor) + CompositionVisual = new CompositionDrawListVisual(compositor, + new ServerCompositionDrawListVisual(compositor.Server), this); + return CompositionVisual; + } + /// /// Calls the method /// for this control and all of its visual descendants. @@ -564,7 +581,7 @@ namespace Avalonia { newValue.Changed += sender.RenderTransformChanged; } - + sender.InvalidateVisual(); } } diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml new file mode 100644 index 0000000000..cf864dac2d --- /dev/null +++ b/src/Avalonia.Base/composition-schema.xml @@ -0,0 +1,70 @@ + + + System.Numerics + Avalonia.Rendering.Composition.Server + Avalonia.Rendering.Composition.Transport + Avalonia.Rendering.Composition.Animations + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index addc248d58..984f76adb6 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Numerics; using System.Runtime.InteropServices; using Avalonia.Media; using Avalonia.Platform; @@ -344,6 +345,7 @@ namespace Avalonia.Headless } public Matrix Transform { get; set; } + public void Clear(Color color) { diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs new file mode 100644 index 0000000000..f079a339df --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/CompositionRoslynGenerator.cs @@ -0,0 +1,21 @@ +using System.IO; +using System.Xml.Serialization; +using Microsoft.CodeAnalysis; + +namespace Avalonia.SourceGenerator.CompositionGenerator; + +[Generator(LanguageNames.CSharp)] +public class CompositionRoslynGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var schema = context.AdditionalTextsProvider.Where(static file => file.Path.EndsWith("composition-schema.xml")); + var configs = schema.Select((t, _) => + (GConfig)new XmlSerializer(typeof(GConfig)).Deserialize(new StringReader(t.GetText().ToString()))); + context.RegisterSourceOutput(configs, (spc, config) => + { + var generator = new Generator(spc, config); + generator.Generate(); + }); + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs new file mode 100644 index 0000000000..8b6aca33cd --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Config.cs @@ -0,0 +1,116 @@ +#nullable disable +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + + + [XmlRoot("NComposition")] + public class GConfig + { + [XmlElement("Using")] + public List Usings { get; set; } = new List(); + + [XmlElement(typeof(GManualClass), ElementName = "Manual")] + public List ManualClasses { get; set; } = new List(); + + [XmlElement(typeof(GClass), ElementName = "Object")] + [XmlElement(typeof(GBrush), ElementName = "Brush")] + [XmlElement(typeof(GList), ElementName = "List")] + public List Classes { get; set; } = new List(); + + [XmlElement(typeof(GAnimationType), ElementName = "KeyFrameAnimation")] + public List KeyFrameAnimations { get; set; } = new List(); + } + + public class GUsing + { + [XmlText] + public string Name { get; set; } + } + + public class GManualClass + { + [XmlAttribute] + public string Name { get; set; } + + [XmlAttribute] + public string ServerName { get; set; } + } + + public class GImplements + { + [XmlAttribute] + public string Name { get; set; } + [XmlAttribute] + public string ServerName { get; set; } + } + + public class GClass + { + [XmlAttribute] + public string Name { get; set; } + + [XmlAttribute] + public string Inherits { get; set; } + + [XmlAttribute] + public string ChangesBase { get; set; } + + [XmlAttribute] + public string ServerBase { get; set; } + + [XmlAttribute] + public bool CustomCtor { get; set; } + + [XmlAttribute] + public bool CustomServerCtor { get; set; } + + [XmlElement(typeof(GImplements), ElementName = "Implements")] + public List Implements { get; set; } = new List(); + + [XmlAttribute] + public bool Abstract { get; set; } + + [XmlElement(typeof(GProperty), ElementName = "Property")] + public List Properties { get; set; } = new List(); + } + + public class GBrush : GClass + { + [XmlAttribute] + public bool CustomUpdate { get; set; } + + public GBrush() + { + Inherits = "CompositionBrush"; + } + } + + public class GList : GClass + { + [XmlAttribute] + public string ItemType { get; set; } + } + + public class GProperty + { + [XmlAttribute] + public string Name { get; set; } + [XmlAttribute] + public string Type { get; set; } + [XmlAttribute] + public string DefaultValue { get; set; } + [XmlAttribute] + public bool Animated { get; set; } + } + + public class GAnimationType + { + [XmlAttribute] + public string Name { get; set; } + [XmlAttribute] + public string Type { get; set; } + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs new file mode 100644 index 0000000000..43a4a4afa7 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Extensions.cs @@ -0,0 +1,90 @@ +using System; +using System.Linq; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + public static class Extensions + { + public static ClassDeclarationSyntax AddModifiers(this ClassDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static MethodDeclarationSyntax AddModifiers(this MethodDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static PropertyDeclarationSyntax AddModifiers(this PropertyDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static ConstructorDeclarationSyntax AddModifiers(this ConstructorDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static AccessorDeclarationSyntax AddModifiers(this AccessorDeclarationSyntax cl, params SyntaxKind[] modifiers) + { + if (modifiers == null) + return cl; + return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); + } + + public static string WithLowerFirst(this string s) + { + if (string.IsNullOrEmpty(s)) + return s; + return char.ToLowerInvariant(s[0]) + s.Substring(1); + } + + public static ExpressionSyntax MemberAccess(params string[] identifiers) + { + if (identifiers == null || identifiers.Length == 0) + throw new ArgumentException(); + var expr = (ExpressionSyntax)IdentifierName(identifiers[0]); + for (var c = 1; c < identifiers.Length; c++) + expr = MemberAccess(expr, identifiers[c]); + return expr; + } + + public static ExpressionSyntax MemberAccess(ExpressionSyntax expr, params string[] identifiers) + { + foreach (var i in identifiers) + expr = MemberAccess(expr, i); + return expr; + } + + public static MemberAccessExpressionSyntax MemberAccess(ExpressionSyntax expr, string identifier) => + MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, expr, IdentifierName(identifier)); + + public static ExpressionSyntax ConditionalMemberAccess(ExpressionSyntax expr, string member, bool checkNull) + { + if (checkNull) + return ConditionalAccessExpression(expr, MemberBindingExpression(IdentifierName(member))); + return MemberAccess(expr, member); + } + + public static ClassDeclarationSyntax WithBaseType(this ClassDeclarationSyntax cl, string bt) + { + return cl.AddBaseListTypes(SimpleBaseType(SyntaxFactory.ParseTypeName(bt))); + } + + public static string StripPrefix(this string s, string prefix) => string.IsNullOrEmpty(s) + ? s + : s.StartsWith(prefix) + ? s.Substring(prefix.Length) + : s; + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs new file mode 100644 index 0000000000..7d5146c5f5 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.KeyFrameAnimation.cs @@ -0,0 +1,59 @@ +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + public partial class Generator + { + void GenerateAnimations() + { + var code = $@"using System.Numerics; +using Avalonia.Rendering.Composition.Animations; +using Avalonia.Rendering.Composition.Expressions; + +namespace Avalonia.Rendering.Composition +{{ +"; + + foreach (var a in _config.KeyFrameAnimations) + { + var name = a.Name ?? a.Type; + + code += $@" + public class {name}KeyFrameAnimation : KeyFrameAnimation + {{ + public {name}KeyFrameAnimation(Compositor compositor) : base(compositor) + {{ + }} + + internal override IAnimationInstance CreateInstance(Avalonia.Rendering.Composition.Server.ServerObject targetObject, ExpressionVariant? finalValue) + {{ + return new KeyFrameAnimationInstance<{a.Type}>({name}Interpolator.Instance, _keyFrames.Snapshot(), CreateSnapshot(true), + finalValue?.CastOrDefault<{a.Type}>(), targetObject, + DelayBehavior, DelayTime, Direction, Duration, IterationBehavior, + IterationCount, StopBehavior); + }} + + private KeyFrames<{a.Type}> _keyFrames = new KeyFrames<{a.Type}>(); + private protected override IKeyFrames KeyFrames => _keyFrames; + + public void InsertKeyFrame(float normalizedProgressKey, {a.Type} value, CompositionEasingFunction easingFunction) + {{ + _keyFrames.Insert(normalizedProgressKey, value, easingFunction); + }} + + public void InsertKeyFrame(float normalizedProgressKey, {a.Type} value) + {{ + _keyFrames.Insert(normalizedProgressKey, value, Compositor.DefaultEasing); + }} + }} + + public partial class Compositor + {{ + public {name}KeyFrameAnimation Create{name}KeyFrameAnimation() => new {name}KeyFrameAnimation(this); + }} +"; + } + + code += "}"; + _output.AddSource("CompositionAnimations.cs", code); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs new file mode 100644 index 0000000000..593386f713 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.ListProxy.cs @@ -0,0 +1,121 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + public partial class Generator + { + private const string ListProxyTemplate = @" +class Template +{ + private ServerListProxyHelper _list = null!; + + ListChangeSet + ServerListProxyHelper.IGetChanges. + GetChanges() => Changes; + + public List.Enumerator GetEnumerator() => _list.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => + GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable) _list).GetEnumerator(); + + public void Add(ItemTypeName item) + { + OnBeforeAdded(item); + _list.Add(item); + OnAdded(item); + } + + public void Clear() + { + OnBeforeClear(); + _list.Clear(); + OnClear(); + } + + public bool Contains(ItemTypeName item) => _list.Contains(item); + + public void CopyTo(ItemTypeName[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex); + + public bool Remove(ItemTypeName item) + { + var removed = _list.Remove(item); + if(removed) + OnRemoved(item); + return removed; + } + + public int Count => _list.Count; + + public bool IsReadOnly => _list.IsReadOnly; + + public int IndexOf(ItemTypeName item) => _list.IndexOf(item); + + public void Insert(int index, ItemTypeName item) + { + OnBeforeAdded(item); + _list.Insert(index, item); + OnAdded(item); + } + + public void RemoveAt(int index) + { + var item = _list[index]; + _list.RemoveAt(index); + OnRemoved(item); + } + + public ItemTypeName this[int index] + { + get => _list[index]; + set + { + var old = _list[index]; + OnBeforeReplace(old, value); + _list[index] = value; + OnReplace(old, value); + } + } + + partial void OnBeforeAdded(ItemTypeName item); + partial void OnAdded(ItemTypeName item); + partial void OnRemoved(ItemTypeName item); + partial void OnBeforeClear(); + partial void OnBeforeReplace(ItemTypeName oldItem, ItemTypeName newItem); + partial void OnReplace(ItemTypeName oldItem, ItemTypeName newItem); + partial void OnClear(); +} +"; + + private ClassDeclarationSyntax AppendListProxy(GList list, ClassDeclarationSyntax cl) + { + + var itemType = list.ItemType; + var serverItemType = ServerName(itemType); + + cl = cl.AddBaseListTypes(SimpleBaseType( + ParseTypeName("ServerListProxyHelper<" + itemType + ", " + serverItemType + ">.IGetChanges")), + SimpleBaseType(ParseTypeName("IList<" + itemType + ">")) + ); + var code = ListProxyTemplate.Replace("ListTypeName", list.Name) + .Replace("ItemTypeName", itemType); + + var parsed = ParseCompilationUnit(code); + var parsedClass = (ClassDeclarationSyntax)parsed.Members.First(); + + cl = cl.AddMembers(parsedClass.Members.ToArray()); + + var defs = cl.Members.OfType().First(m => m.Identifier.Text == "InitializeDefaults"); + + cl = cl.ReplaceNode(defs.Body, defs.Body.AddStatements( + + ParseStatement($"_list = new ServerListProxyHelper<{itemType}, {serverItemType}>(this);"))); + + return cl; + } + + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.Utils.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.Utils.cs new file mode 100644 index 0000000000..b53c247991 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.Utils.cs @@ -0,0 +1,66 @@ +using System.IO; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + public partial class Generator + { + static void CleanDirectory(string path) + { + Directory.CreateDirectory(path); + Directory.Delete(path, true); + Directory.CreateDirectory(path); + } + + CompilationUnitSyntax Unit() + => CompilationUnit().WithUsings(List(new[] + { + "System", + "System.Text", + "System.Collections", + "System.Collections.Generic" + } + .Concat(_config.Usings + .Select(x => x.Name)).Select(u => UsingDirective(IdentifierName(u))))); + + void SaveTo(CompilationUnitSyntax unit, params string[] path) + { + var text = @" +#nullable enable +#pragma warning disable CS0108, CS0114 + +" + + + unit.NormalizeWhitespace().ToFullString(); + _output.AddSource(string.Join("_", path), text); + } + + + SyntaxToken Semicolon() => Token(SyntaxKind.SemicolonToken); + + + FieldDeclarationSyntax DeclareConstant(string type, string name, LiteralExpressionSyntax value) + => FieldDeclaration( + VariableDeclaration(ParseTypeName(type), + SingletonSeparatedList( + VariableDeclarator(name).WithInitializer(EqualsValueClause(value)) + )) + ).WithSemicolonToken(Semicolon()) + .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.ConstKeyword))); + + FieldDeclarationSyntax DeclareField(string type, string name, params SyntaxKind[] modifiers) => + DeclareField(type, name, null, modifiers); + + FieldDeclarationSyntax DeclareField(string type, string name, EqualsValueClauseSyntax initializer, + params SyntaxKind[] modifiers) => + FieldDeclaration( + VariableDeclaration(ParseTypeName(type), + SingletonSeparatedList( + VariableDeclarator(name).WithInitializer(initializer)))) + .WithSemicolonToken(Semicolon()) + .WithModifiers(TokenList(modifiers.Select(x => Token(x)))); + } +} \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs new file mode 100644 index 0000000000..43ef4a96e8 --- /dev/null +++ b/src/Avalonia.SourceGenerator/CompositionGenerator/Generator.cs @@ -0,0 +1,504 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Avalonia.SourceGenerator.CompositionGenerator.Extensions; +namespace Avalonia.SourceGenerator.CompositionGenerator +{ + partial class Generator + { + private readonly SourceProductionContext _output; + private readonly GConfig _config; + private readonly HashSet _objects; + private readonly HashSet _brushes; + private readonly Dictionary _manuals; + public Generator(SourceProductionContext output, GConfig config) + { + _output = output; + _config = config; + _manuals = _config.ManualClasses.ToDictionary(x => x.Name); + _objects = new HashSet(_config.ManualClasses.Select(x => x.Name) + .Concat(_config.Classes.Select(x => x.Name))); + _brushes = new HashSet(_config.Classes.OfType().Select(x => x.Name)) {"CompositionBrush"}; + } + + + + public void Generate() + { + foreach (var cl in _config.Classes) + GenerateClass(cl); + + GenerateAnimations(); + } + + + + string ServerName(string c) => c != null ? ("Server" + c) : "ServerObject"; + string ChangesName(string c) => c != null ? (c + "Changes") : "ChangeSet"; + + void GenerateClass(GClass cl) + { + var list = cl as GList; + + var unit = Unit(); + + var clientNs = NamespaceDeclaration(IdentifierName("Avalonia.Rendering.Composition")); + var serverNs = NamespaceDeclaration(IdentifierName("Avalonia.Rendering.Composition.Server")); + var transportNs = NamespaceDeclaration(IdentifierName("Avalonia.Rendering.Composition.Transport")); + + var inherits = cl.Inherits ?? "CompositionObject"; + var abstractModifier = cl.Abstract ? new[] {SyntaxKind.AbstractKeyword} : null; + + var client = ClassDeclaration(cl.Name) + .AddModifiers(abstractModifier) + .AddModifiers(SyntaxKind.PublicKeyword, SyntaxKind.UnsafeKeyword, SyntaxKind.PartialKeyword) + .WithBaseType(inherits); + + var serverName = ServerName(cl.Name); + var serverBase = cl.ServerBase ?? ServerName(cl.Inherits); + if (list != null) + serverBase = "ServerList<" + ServerName(list.ItemType) + ">"; + + var server = ClassDeclaration(serverName) + .AddModifiers(abstractModifier) + .AddModifiers(SyntaxKind.UnsafeKeyword, SyntaxKind.PartialKeyword) + .WithBaseType(serverBase); + + string changesName = ChangesName(cl.Name); + var changesBase = ChangesName(cl.ChangesBase ?? cl.Inherits); + + if (list != null) + changesBase = "ListChangeSet<" + ServerName(list.ItemType) + ">"; + + var changeSetPoolType = "ChangeSetPool<" + changesName + ">"; + var transport = ClassDeclaration(changesName) + .AddModifiers(SyntaxKind.UnsafeKeyword, SyntaxKind.PartialKeyword) + .WithBaseType(changesBase) + .AddMembers(DeclareField(changeSetPoolType, "Pool", + EqualsValueClause( + ParseExpression($"new {changeSetPoolType}(pool => new {changesName}(pool))") + ), + SyntaxKind.PublicKeyword, + SyntaxKind.StaticKeyword, SyntaxKind.ReadOnlyKeyword)) + .AddMembers(ParseMemberDeclaration($"public {changesName}(IChangeSetPool pool) : base(pool){{}}")); + + client = client + .AddMembers( + PropertyDeclaration(ParseTypeName("IChangeSetPool"), "ChangeSetPool") + .AddModifiers(SyntaxKind.PrivateKeyword, SyntaxKind.ProtectedKeyword, + SyntaxKind.OverrideKeyword) + .WithExpressionBody( + ArrowExpressionClause(MemberAccess(changesName, "Pool"))) + .WithSemicolonToken(Semicolon())) + .AddMembers(PropertyDeclaration(ParseTypeName(changesName), "Changes") + .AddModifiers(SyntaxKind.PrivateKeyword, SyntaxKind.NewKeyword) + .WithExpressionBody(ArrowExpressionClause(CastExpression(ParseTypeName(changesName), + MemberAccess(BaseExpression(), "Changes")))) + .WithSemicolonToken(Semicolon())); + + if (!cl.CustomCtor) + { + client = client.AddMembers(PropertyDeclaration(ParseTypeName(serverName), "Server") + .AddModifiers(SyntaxKind.InternalKeyword, SyntaxKind.NewKeyword) + .AddAccessorListAccessors(AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithSemicolonToken(Semicolon()))); + client = client.AddMembers( + ConstructorDeclaration(cl.Name) + .AddModifiers(SyntaxKind.InternalKeyword) + .WithParameterList(ParameterList(SeparatedList(new[] + { + Parameter(Identifier("compositor")).WithType(ParseTypeName("Compositor")), + Parameter(Identifier("server")).WithType(ParseTypeName(serverName)), + }))) + .WithInitializer(ConstructorInitializer(SyntaxKind.BaseConstructorInitializer, + ArgumentList(SeparatedList(new[] + { + Argument(IdentifierName("compositor")), + Argument(IdentifierName("server")), + })))).WithBody(Block( + ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName("Server"), + CastExpression(ParseTypeName(serverName), IdentifierName("server")))), + ExpressionStatement(InvocationExpression(IdentifierName("InitializeDefaults"))) + ))); + } + + if (!cl.CustomServerCtor) + { + server = server.AddMembers( + ConstructorDeclaration(serverName) + .AddModifiers(SyntaxKind.InternalKeyword) + .WithParameterList(ParameterList(SeparatedList(new[] + { + Parameter(Identifier("compositor")).WithType(ParseTypeName("ServerCompositor")), + }))) + .WithInitializer(ConstructorInitializer(SyntaxKind.BaseConstructorInitializer, + ArgumentList(SeparatedList(new[] + { + Argument(IdentifierName("compositor")), + })))).WithBody(Block())); + } + + + var changesVarName = "c"; + var changesVar = IdentifierName(changesVarName); + + server = server.AddMembers( + MethodDeclaration(ParseTypeName("void"), "ApplyChangesExtra") + .AddParameterListParameters(Parameter(Identifier("c")).WithType(ParseTypeName(changesName))) + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + + transport = transport.AddMembers( + MethodDeclaration(ParseTypeName("void"), "ResetExtra") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + + var applyMethodBody = Block( + ExpressionStatement(InvocationExpression(MemberAccess(IdentifierName("base"), "ApplyCore"), + ArgumentList(SeparatedList(new[] {Argument(IdentifierName("changes"))})))), + LocalDeclarationStatement(VariableDeclaration(ParseTypeName("var")) + .WithVariables(SingletonSeparatedList( + VariableDeclarator(changesVarName) + .WithInitializer(EqualsValueClause(CastExpression(ParseTypeName(changesName), + IdentifierName("changes"))))))), + ExpressionStatement(InvocationExpression(IdentifierName("ApplyChangesExtra")) + .AddArgumentListArguments(Argument(IdentifierName("c")))) + ); + + var resetBody = Block(); + var startAnimationBody = Block(); + var getPropertyBody = Block(); + var serverGetPropertyBody = Block(); + + var defaultsMethodBody = Block(); + + foreach (var prop in cl.Properties) + { + var fieldName = "_" + prop.Name.WithLowerFirst(); + var propType = ParseTypeName(prop.Type); + var filteredPropertyType = prop.Type.TrimEnd('?'); + var isObject = _objects.Contains(filteredPropertyType); + var isNullable = prop.Type.EndsWith("?"); + + + + + client = client + .AddMembers(DeclareField(prop.Type, fieldName)) + .AddMembers(PropertyDeclaration(propType, prop.Name) + .AddModifiers(SyntaxKind.PublicKeyword) + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, + Block(ReturnStatement(IdentifierName(fieldName)))), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, + Block( + ParseStatement("var changed = false;"), + IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, + IdentifierName(fieldName), + IdentifierName("value")), + Block( + ParseStatement("On" + prop.Name + "Changing();"), + ParseStatement("changed = true;"), + GeneratePropertySetterAssignment(prop, fieldName, isObject, isNullable)) + ), + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(fieldName), IdentifierName("value"))), + ParseStatement($"if(changed) On" + prop.Name + "Changed();") + )) + )) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changed") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + + + var animatedServer = prop.Animated; + + var serverPropertyType = ((isObject ? "Server" : "") + prop.Type); + if (_manuals.TryGetValue(filteredPropertyType, out var manual) && manual.ServerName != null) + serverPropertyType = manual.ServerName + (isNullable ? "?" : ""); + + + transport = transport + .AddMembers(DeclareField((animatedServer ? "Animated" : "") + "Change<" + serverPropertyType + ">", + prop.Name, SyntaxKind.PublicKeyword)); + + if (animatedServer) + server = server.AddMembers( + DeclareField("AnimatedValueStore<" + serverPropertyType + ">", fieldName), + PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) + .AddModifiers(SyntaxKind.PublicKeyword) + .WithExpressionBody(ArrowExpressionClause( + InvocationExpression(MemberAccess(fieldName, "GetAnimated"), + ArgumentList(SingletonSeparatedList(Argument(IdentifierName("Compositor"))))))) + .WithSemicolonToken(Semicolon()) + ); + else + { + server = server + .AddMembers(DeclareField(serverPropertyType, fieldName)) + .AddMembers(PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) + .AddModifiers(SyntaxKind.PublicKeyword) + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, + Block(ReturnStatement(IdentifierName(fieldName)))), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, + Block( + ParseStatement("var changed = false;"), + IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, + IdentifierName(fieldName), + IdentifierName("value")), + Block( + ParseStatement("On" + prop.Name + "Changing();"), + ParseStatement($"changed = true;")) + ), + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(fieldName), IdentifierName("value"))), + ParseStatement($"if(changed) On" + prop.Name + "Changed();") + )) + )) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changed") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())) + .AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing") + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + } + + if (animatedServer) + applyMethodBody = applyMethodBody.AddStatements( + IfStatement(MemberAccess(changesVar, prop.Name, "IsValue"), + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(fieldName), MemberAccess(changesVar, prop.Name, "Value")))), + IfStatement(MemberAccess(changesVar, prop.Name, "IsAnimation"), + ExpressionStatement( + InvocationExpression(MemberAccess(fieldName, "SetAnimation"), + ArgumentList(SeparatedList(new[] + { + Argument(changesVar), + Argument(MemberAccess(changesVar, prop.Name, "Animation")) + }))))) + ); + else + applyMethodBody = applyMethodBody.AddStatements( + IfStatement(MemberAccess(changesVar, prop.Name, "IsSet"), + ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(prop.Name), MemberAccess(changesVar, prop.Name, "Value")))) + + ); + + + resetBody = resetBody.AddStatements( + ExpressionStatement(InvocationExpression(MemberAccess(prop.Name, "Reset")))); + + if (animatedServer) + startAnimationBody = ApplyStartAnimation(startAnimationBody, prop, fieldName); + + getPropertyBody = ApplyGetProperty(getPropertyBody, prop); + serverGetPropertyBody = ApplyGetProperty(getPropertyBody, prop); + + if (prop.DefaultValue != null) + { + defaultsMethodBody = defaultsMethodBody.AddStatements( + ExpressionStatement( + AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(prop.Name), ParseExpression(prop.DefaultValue)))); + } + } + + if (cl is GBrush brush && !cl.Abstract) + { + var brushName = brush.Name.StripPrefix("Composition"); + /* + server = server.AddMembers( + MethodDeclaration(ParseTypeName("ICbBrush"), "CreateBackendBrush") + .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) + .WithExpressionBody(ArrowExpressionClause( + InvocationExpression(MemberAccess("Compositor", "Backend", "Create" + brushName)) + )).WithSemicolonToken(Semicolon()) + ); + if (!brush.CustomUpdate) + server = server.AddMembers( + MethodDeclaration(ParseTypeName("void"), "UpdateBackendBrush") + .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) + .AddParameterListParameters(Parameter(Identifier("brush")) + .WithType(ParseTypeName("ICbBrush"))) + .AddBodyStatements( + ExpressionStatement( + InvocationExpression( + MemberAccess( + ParenthesizedExpression( + CastExpression(ParseTypeName("ICb" + brushName), IdentifierName("brush"))), "Update"), + ArgumentList(SeparatedList(cl.Properties.Select(x => + { + if(x.Type.TrimEnd('?') == "ICompositionSurface") + return Argument( + ConditionalAccessExpression(IdentifierName(x.Name), + MemberBindingExpression(IdentifierName("BackendSurface"))) + ); + if (_brushes.Contains(x.Type)) + return Argument( + ConditionalAccessExpression(IdentifierName(x.Name), + MemberBindingExpression(IdentifierName("Brush"))) + ); + return Argument(IdentifierName(x.Name)); + })))) + ))); + +*/ + } + + server = server.AddMembers( + MethodDeclaration(ParseTypeName("void"), "ApplyCore") + .AddModifiers(SyntaxKind.ProtectedKeyword, SyntaxKind.OverrideKeyword) + .AddParameterListParameters( + Parameter(Identifier("changes")).WithType(ParseTypeName("ChangeSet"))) + .WithBody(applyMethodBody)); + + client = client.AddMembers( + MethodDeclaration(ParseTypeName("void"), "InitializeDefaults").WithBody(defaultsMethodBody)); + + transport = transport.AddMembers(MethodDeclaration(ParseTypeName("void"), "Reset") + .AddModifiers(SyntaxKind.PublicKeyword, SyntaxKind.OverrideKeyword) + .WithBody(resetBody.AddStatements( + ExpressionStatement(InvocationExpression(IdentifierName("ResetExtra"))), + ExpressionStatement(InvocationExpression(MemberAccess("base", "Reset")))))); + + if (list != null) + client = AppendListProxy(list, client); + + if (startAnimationBody.Statements.Count != 0) + client = WithStartAnimation(client, startAnimationBody); + + client = WithGetProperty(client, getPropertyBody, false); + server = WithGetProperty(server, serverGetPropertyBody, true); + + if(cl.Implements.Count > 0) + foreach (var impl in cl.Implements) + { + client = client.WithBaseList(client.BaseList.AddTypes(SimpleBaseType(ParseTypeName(impl.Name)))); + if (impl.ServerName != null) + server = server.WithBaseList( + server.BaseList.AddTypes(SimpleBaseType(ParseTypeName(impl.ServerName)))); + + client = client.AddMembers( + ParseMemberDeclaration($"{impl.ServerName} {impl.Name}.Server => Server;")); + } + + + SaveTo(unit.AddMembers(clientNs.AddMembers(client)), + cl.Name + ".generated.cs"); + SaveTo(unit.AddMembers(serverNs.AddMembers(server)), + "Server", "Server" + cl.Name + ".generated.cs"); + SaveTo(unit.AddMembers(transportNs.AddMembers(transport)), + "Transport", cl.Name + "Changes.generated.cs"); + } + + StatementSyntax GeneratePropertySetterAssignment(GProperty prop, string fieldName, bool isObject, bool isNullable) + { + var normalChangesAssignment = (StatementSyntax)ExpressionStatement(AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + MemberAccess((ExpressionSyntax) IdentifierName("Changes"), prop.Name, + "Value"), + isObject + ? + ConditionalMemberAccess(IdentifierName("value"), "Server", isNullable) + : IdentifierName("value"))); + if (!prop.Animated) + return normalChangesAssignment; + + var code = $@" +{{ + if(animation is CompositionAnimation a) + Changes.{prop.Name}.Animation = a.CreateInstance(this.Server, value); + else + {{ + var saved = Changes.{prop.Name}; + if(!StartAnimationGroup(animation, ""{prop.Name}"", value)) + Changes.{prop.Name}.Value = value; + }} +}} + +"; + + return IfStatement( + ParseExpression( + $"ImplicitAnimations != null && ImplicitAnimations.TryGetValue(\"{prop.Name}\", out var animation) == true"), + ParseStatement(code), + ElseClause(normalChangesAssignment) + ); + } + + BlockSyntax ApplyStartAnimation(BlockSyntax body, GProperty prop, string fieldName) + { + var code = $@" +if (propertyName == ""{prop.Name}"") +{{ +var current = {fieldName}; +var server = animation.CreateInstance(this.Server, finalValue); +Changes.{prop.Name}.Animation = server; +return; +}} +"; + return body.AddStatements(ParseStatement(code)); + } + + private static HashSet VariantPropertyTypes = new HashSet + { + "bool", + "float", + "Vector2", + "Vector3", + "Vector4", + "Matrix3x2", + "Matrix4x4", + "Quaternion", + "CompositionColor" + }; + + BlockSyntax ApplyGetProperty(BlockSyntax body, GProperty prop) + { + if (VariantPropertyTypes.Contains(prop.Type)) + return body.AddStatements( + ParseStatement($"if(name == \"{prop.Name}\")\n return {prop.Name};\n") + ); + + return body; + } + + ClassDeclarationSyntax WithGetProperty(ClassDeclarationSyntax cl, BlockSyntax body, bool server) + { + if (body.Statements.Count == 0) + return cl; + body = body.AddStatements( + ParseStatement("return base.GetPropertyForAnimation(name);")); + var method = ((MethodDeclarationSyntax) ParseMemberDeclaration( + $"{(server ? "public" : "internal")} override Avalonia.Rendering.Composition.Expressions.ExpressionVariant GetPropertyForAnimation(string name){{}}")) + .WithBody(body); + + return cl.AddMembers(method); + } + + ClassDeclarationSyntax WithStartAnimation(ClassDeclarationSyntax cl, BlockSyntax body) + { + body = body.AddStatements( + ExpressionStatement(InvocationExpression(MemberAccess("base", "StartAnimation"), + ArgumentList(SeparatedList(new[] + { + Argument(IdentifierName("propertyName")), + Argument(IdentifierName("animation")), + Argument(IdentifierName("finalValue")), + })))) + ); + return cl.AddMembers( + ((MethodDeclarationSyntax) ParseMemberDeclaration( + "internal override void StartAnimation(string propertyName, CompositionAnimation animation, Avalonia.Rendering.Composition.Expressions.ExpressionVariant? finalValue){}")) + .WithBody(body)); + + + } + + } +} \ No newline at end of file diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index ec3f29c806..7a64b39575 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -13,6 +13,7 @@ using Avalonia.OpenGL; using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.X11; using Avalonia.X11.Glx; using Avalonia.X11.NativeDialogs; @@ -29,6 +30,7 @@ namespace Avalonia.X11 public XI2Manager XI2; public X11Info Info { get; private set; } public IX11Screens X11Screens { get; private set; } + public Compositor Compositor { get; private set; } public IScreenImpl Screens { get; private set; } public X11PlatformOptions Options { get; private set; } public IntPtr OrphanedWindow { get; private set; } @@ -101,7 +103,9 @@ namespace Avalonia.X11 GlxPlatformOpenGlInterface.TryInitialize(Info, Options.GlProfiles); } - + if (options.UseCompositor) + Compositor = Compositor.Create(AvaloniaLocator.Current.GetService()!); + } public IntPtr DeferredDisplay { get; set; } @@ -222,6 +226,8 @@ namespace Avalonia /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. /// public bool UseDeferredRendering { get; set; } = true; + + public bool UseCompositor { get; set; } /// /// Determines whether to use IME. diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 066156a652..37cfeb0624 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -18,6 +18,7 @@ using Avalonia.OpenGL; using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.X11.Glx; using static Avalonia.X11.XLib; @@ -360,13 +361,15 @@ namespace Avalonia.X11 if (customRendererFactory != null) return customRendererFactory.Create(root, loop); - - return _platform.Options.UseDeferredRendering ? - new DeferredRenderer(root, loop) - { - RenderOnlyOnRenderThread = true - } : - (IRenderer)new X11ImmediateRendererProxy(root, loop); + + return _platform.Options.UseDeferredRendering + ? _platform.Options.UseCompositor + ? new CompositingRenderer(root, this._platform.Compositor) + : new DeferredRenderer(root, loop) + { + RenderOnlyOnRenderThread = true + } + : (IRenderer)new X11ImmediateRendererProxy(root, loop); } void OnEvent(ref XEvent ev) diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index a259d8fab9..1243c90214 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Numerics; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; @@ -73,6 +74,12 @@ namespace Avalonia.Direct2D1.Media set { _deviceContext.Transform = value.ToDirect2D(); } } + public Matrix4x4 Transform4x4 + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + /// public void Clear(Color color) {