diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs deleted file mode 100644 index 92aa1f35bb..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Avalonia.Rendering.Composition.Expressions; -using Avalonia.Rendering.Composition.Server; -using Avalonia.Rendering.Composition.Transport; -using Avalonia.Utilities; - -namespace Avalonia.Rendering.Composition.Animations -{ - /// - /// This is the first element of both animated and non-animated value stores. - /// It's used to propagate property invalidation to subscribers - /// - - internal struct ServerObjectSubscriptionStore - { - public bool IsValid; - public RefTrackingDictionary? Subscribers; - - public void Invalidate() - { - if (IsValid) - return; - IsValid = false; - if (Subscribers != null) - foreach (var sub in Subscribers) - sub.Key.Invalidate(); - } - } - - /// - /// The value store for non-animated values that can still be referenced by animations. - /// Simply stores the value and notifies subscribers - /// - internal struct ServerValueStore - { - public ServerObjectSubscriptionStore Subscriptions; - private T _value; - public T Value - { - set - { - _value = value; - Subscriptions.Invalidate(); - } - get - { - Subscriptions.IsValid = true; - return _value; - } - } - } - - /// - /// Value store for potentially animated values. Can hold both direct value and animation instance. - /// Is also responsible for activating/deactivating the animation when container object is activated/deactivated - /// - /// - [StructLayout(LayoutKind.Sequential)] - internal struct ServerAnimatedValueStore where T : struct - { - public ServerObjectSubscriptionStore Subscriptions; - private IAnimationInstance? _animation; - private T _direct; - private T? _lastAnimated; - - public T GetAnimated(ServerCompositor compositor) - { - Subscriptions.IsValid = true; - if (_animation == null) - return _direct; - var v = _animation.Evaluate(compositor.ServerNow, ExpressionVariant.Create(_direct)) - .CastOrDefault(); - _lastAnimated = v; - return v; - } - - public void Activate(ServerObject parent) - { - if (_animation != null) - _animation.Activate(); - } - - public void Deactivate(ServerObject parent) - { - if (_animation != null) - _animation.Deactivate(); - } - - private T LastAnimated => _animation != null ? _lastAnimated ?? _direct : _direct; - - public bool IsAnimation => _animation != null; - - public void SetAnimation(ServerObject target, TimeSpan commitedAt, IAnimationInstance animation, int storeOffset) - { - _direct = default; - if (_animation != null) - { - if (target.IsActive) - _animation.Deactivate(); - } - - _animation = animation; - _animation.Initialize(commitedAt, ExpressionVariant.Create(LastAnimated), storeOffset); - if (target.IsActive) - _animation.Activate(); - - Subscriptions.Invalidate(); - } - - public void SetValue(ServerObject target, T value) - { - if (_animation != null) - { - if (target.IsActive) - _animation.Deactivate(); - } - - _animation = null; - _direct = value; - Subscriptions.Invalidate(); - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs index 35aa8de1bc..80e64118ee 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs @@ -12,10 +12,10 @@ namespace Avalonia.Rendering.Composition.Animations; /// internal abstract class AnimationInstanceBase : IAnimationInstance { - private List<(ServerObject obj, int member)>? _trackedObjects; + private List<(ServerObject obj, CompositionProperty member)>? _trackedObjects; protected PropertySetSnapshot Parameters { get; } public ServerObject TargetObject { get; } - protected int StoreOffset { get; private set; } + protected CompositionProperty Property { get; private set; } = null!; private bool _invalidated; public AnimationInstanceBase(ServerObject target, PropertySetSnapshot parameters) @@ -24,7 +24,7 @@ internal abstract class AnimationInstanceBase : IAnimationInstance TargetObject = target; } - protected void Initialize(int storeOffset, HashSet<(string name, string member)> trackedObjects) + protected void Initialize(CompositionProperty property, HashSet<(string name, string member)> trackedObjects) { if (trackedObjects.Count > 0) { @@ -34,22 +34,22 @@ internal abstract class AnimationInstanceBase : IAnimationInstance var obj = Parameters.GetObjectParameter(t.name); if (obj is ServerObject tracked) { - var off = tracked.GetFieldOffset(t.member); + var off = tracked.GetCompositionProperty(t.member); if (off == null) #if DEBUG throw new InvalidCastException("Attempting to subscribe to unknown field"); #else continue; #endif - _trackedObjects.Add((tracked, off.Value)); + _trackedObjects.Add((tracked, off)); } } } - StoreOffset = storeOffset; + Property = property; } - public abstract void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, int storeOffset); + public abstract void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property); protected abstract ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue); public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) @@ -77,6 +77,6 @@ internal abstract class AnimationInstanceBase : IAnimationInstance if (_invalidated) return; _invalidated = true; - TargetObject.NotifyAnimatedValueChanged(StoreOffset); + TargetObject.NotifyAnimatedValueChanged(Property); } } \ 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 index 445cef9a08..764bac9931 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs @@ -29,12 +29,12 @@ namespace Avalonia.Rendering.Composition.Animations return _expression.Evaluate(ref ctx); } - public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, int storeOffset) + public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property) { _startingValue = startingValue; var hs = new HashSet<(string, string)>(); _expression.CollectReferences(hs); - base.Initialize(storeOffset, hs); + base.Initialize(property, hs); } public ExpressionAnimationInstance(Expression expression, diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs index 05d1b50953..4e1972f2c6 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs @@ -8,7 +8,7 @@ namespace Avalonia.Rendering.Composition.Animations { ServerObject TargetObject { get; } ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue); - void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, int storeOffset); + void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property); void Activate(); void Deactivate(); void Invalidate(); diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs index 7268780298..0c0fcfaf2b 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs @@ -151,7 +151,7 @@ namespace Avalonia.Rendering.Composition.Animations return f.Value; } - public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, int storeOffset) + public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property) { _startedAt = startedAt; _startingValue = startingValue.CastOrDefault(); @@ -160,7 +160,7 @@ namespace Avalonia.Rendering.Composition.Animations // TODO: Update subscriptions based on the current keyframe rather than keeping subscriptions to all of them foreach (var frame in _keyFrames) frame.Expression?.CollectReferences(hs); - Initialize(storeOffset, hs); + Initialize(property, hs); } public override void Activate() diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs index 0ed9a3c75d..f529ee9cff 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs @@ -3,6 +3,7 @@ using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Transport; +using Avalonia.Utilities; namespace Avalonia.Rendering.Composition { @@ -17,6 +18,8 @@ namespace Avalonia.Rendering.Composition /// The collection of implicit animations attached to this object. /// public ImplicitAnimationCollection? ImplicitAnimations { get; set; } + + private protected InlineDictionary PendingAnimations; internal CompositionObject(Compositor compositor, ServerObject server) { Compositor = compositor; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/CompositionProperty.cs b/src/Avalonia.Base/Rendering/Composition/Server/CompositionProperty.cs new file mode 100644 index 0000000000..282c0e113d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/CompositionProperty.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading; + +namespace Avalonia.Rendering.Composition.Server; + +internal class CompositionProperty +{ + private static volatile int s_NextId = 1; + public int Id { get; private set; } + + public static CompositionProperty Register() => new() + { + Id = Interlocked.Increment(ref s_NextId) + }; +} \ 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 index 4c358605fc..c6b468a32f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -21,7 +21,25 @@ namespace Avalonia.Rendering.Composition.Server public long ItselfLastChangedBy { get; private set; } private uint _activationCount; public bool IsActive => _activationCount != 0; + private InlineDictionary _subscriptions; + private InlineDictionary _animations; + + private class ServerObjectSubscriptionStore + { + public bool IsValid; + public RefTrackingDictionary? Subscribers; + public void Invalidate() + { + if (IsValid) + return; + IsValid = false; + if (Subscribers != null) + foreach (var sub in Subscribers) + sub.Key.Invalidate(); + } + } + public ServerObject(ServerCompositor compositor) { Compositor = compositor; @@ -62,46 +80,62 @@ namespace Avalonia.Rendering.Composition.Server } - [StructLayout(LayoutKind.Sequential)] - protected class OffsetDummy + void InvalidateSubscriptions(CompositionProperty property) { -#pragma warning disable CS0649 - public FillerStruct Filler; -#pragma warning restore CS0649 + if(_subscriptions.TryGetValue(property, out var subs)) + subs.Invalidate(); } - - [StructLayout(LayoutKind.Sequential)] - protected unsafe struct FillerStruct + + protected void SetValue(CompositionProperty prop, out T field, T value) { - public fixed byte FillerData[8192]; + field = value; + InvalidateSubscriptions(prop); } - private static readonly object s_OffsetDummy = new OffsetDummy(); - protected static T GetOffsetDummy() where T : ServerObject => Unsafe.As(s_OffsetDummy); + protected T GetValue(CompositionProperty prop, ref T field) + { + if (_subscriptions.TryGetValue(prop, out var subs)) + subs.IsValid = true; + return field; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static int GetOffset(ServerObject obj, ref ServerObjectSubscriptionStore field) + protected void SetAnimatedValue(CompositionProperty prop, ref T field, + TimeSpan commitedAt, IAnimationInstance animation) where T : struct { - return Unsafe.ByteOffset(ref obj._activationCount, - ref Unsafe.As(ref field)) - .ToInt32(); + if (IsActive && _animations.TryGetValue(prop, out var oldAnimation)) + oldAnimation.Deactivate(); + _animations[prop] = animation; + + animation.Initialize(commitedAt, ExpressionVariant.Create(field), prop); + if(IsActive) + animation.Activate(); + + InvalidateSubscriptions(prop); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected ref ServerObjectSubscriptionStore GetStoreFromOffset(int offset) + protected void SetAnimatedValue(CompositionProperty property, out T field, T value) { -#if DEBUG - if (offset == 0) - throw new InvalidOperationException(); -#endif - return ref Unsafe.As(ref Unsafe.AddByteOffset(ref _activationCount, - new IntPtr(offset))); + if (_animations.TryGetAndRemoveValue(property, out var animation) && IsActive) + animation.Deactivate(); + field = value; + InvalidateSubscriptions(property); } + + protected T GetAnimatedValue(CompositionProperty property, ref T field) where T : struct + { + if (_subscriptions.TryGetValue(property, out var subscriptions)) + subscriptions.IsValid = true; + + if (_animations.TryGetValue(property, out var animation)) + field = animation.Evaluate(Compositor.ServerNow, ExpressionVariant.Create(field)) + .CastOrDefault(); - public virtual void NotifyAnimatedValueChanged(int offset) + return field; + } + + public virtual void NotifyAnimatedValueChanged(CompositionProperty prop) { - ref var store = ref GetStoreFromOffset(offset); - store.Invalidate(); + InvalidateSubscriptions(prop); ValuesInvalidated(); } @@ -110,21 +144,22 @@ namespace Avalonia.Rendering.Composition.Server } - public void SubscribeToInvalidation(int member, IAnimationInstance animation) + public void SubscribeToInvalidation(CompositionProperty member, IAnimationInstance animation) { - ref var store = ref GetStoreFromOffset(member); + if (!_subscriptions.TryGetValue(member, out var store)) + _subscriptions[member] = store = new ServerObjectSubscriptionStore(); if (store.Subscribers == null) store.Subscribers = new(); store.Subscribers.AddRef(animation); } - public void UnsubscribeFromInvalidation(int member, IAnimationInstance animation) + public void UnsubscribeFromInvalidation(CompositionProperty member, IAnimationInstance animation) { - ref var store = ref GetStoreFromOffset(member); - store.Subscribers?.ReleaseRef(animation); + if(_subscriptions.TryGetValue(member, out var store)) + store.Subscribers?.ReleaseRef(animation); } - public virtual int? GetFieldOffset(string fieldName) => null; + public virtual CompositionProperty? GetCompositionProperty(string fieldName) => null; protected virtual void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.DirtyProperties.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.DirtyProperties.cs index e434b97b2f..c5af74e2dd 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.DirtyProperties.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.DirtyProperties.cs @@ -50,27 +50,27 @@ partial class ServerCompositionVisual _clipSizeDirty = true; } - public override void NotifyAnimatedValueChanged(int offset) + public override void NotifyAnimatedValueChanged(CompositionProperty offset) { base.NotifyAnimatedValueChanged(offset); - if (offset == s_OffsetOf_clipToBounds - || offset == s_OffsetOf_opacity - || offset == s_OffsetOf_size) + if (offset == s_IdOfClipToBoundsProperty + || offset == s_IdOfOpacityProperty + || offset == s_IdOfSizeProperty) IsDirtyComposition = true; - if (offset == s_OffsetOf_size - || offset == s_OffsetOf_anchorPoint - || offset == s_OffsetOf_centerPoint - || offset == s_OffsetOf_adornedVisual - || offset == s_OffsetOf_transformMatrix - || offset == s_OffsetOf_scale - || offset == s_OffsetOf_rotationAngle - || offset == s_OffsetOf_orientation - || offset == s_OffsetOf_offset) + if (offset == s_IdOfSizeProperty + || offset == s_IdOfAnchorPointProperty + || offset == s_IdOfCenterPointProperty + || offset == s_IdOfAdornedVisualProperty + || offset == s_IdOfTransformMatrixProperty + || offset == s_IdOfScaleProperty + || offset == s_IdOfRotationAngleProperty + || offset == s_IdOfOrientationProperty + || offset == s_IdOfOffsetProperty) _combinedTransformDirty = true; - if (offset == s_OffsetOf_clipToBounds - || offset == s_OffsetOf_size) + if (offset == s_IdOfClipToBoundsProperty + || offset == s_IdOfSizeProperty) _clipSizeDirty = true; } } \ 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 index 3b36dfb87e..0ba63c70bc 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -121,7 +121,7 @@ namespace Avalonia.Rendering.Composition.Server var oldCombinedTransformedClipBounds = _combinedTransformedClipBounds; - if (_parent.Value?.IsDirtyComposition == true) + if (_parent?.IsDirtyComposition == true) { IsDirtyComposition = true; _isDirtyForUpdate = true; diff --git a/src/Avalonia.Base/Utilities/SmallDictionary.cs b/src/Avalonia.Base/Utilities/SmallDictionary.cs new file mode 100644 index 0000000000..b8f532c747 --- /dev/null +++ b/src/Avalonia.Base/Utilities/SmallDictionary.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Avalonia.Utilities; + +public struct InlineDictionary where TKey : class where TValue : class +{ + object? _data; + TValue? _value; + + void SetCore(TKey key, TValue value, bool overwrite) + { + if (_data == null) + { + _data = key; + _value = value; + } + else if (_data is KeyValuePair[] arr) + { + var free = -1; + for (var c = 0; c < arr.Length; c++) + { + if (arr[c].Key == key) + { + if (overwrite) + { + arr[c] = new(key, value); + return; + } + else + throw new ArgumentException("Key already exists in dictionary"); + } + + if (arr[c].Key == null) + free = c; + } + + if (free != -1) + { + arr[free] = new KeyValuePair(key, value); + return; + } + + // Upgrade to dictionary + var newDic = new Dictionary(); + foreach (var kvp in arr) + newDic.Add(kvp.Key!, kvp.Value!); + newDic.Add(key, value); + _data = newDic; + } + else if (_data is Dictionary dic) + { + if (overwrite) + dic[key] = value; + else + dic.Add(key, value); + } + else + { + // We have a single element, upgrade to array + arr = new KeyValuePair[6]; + arr[0] = new KeyValuePair((TKey)_data, _value); + arr[1] = new KeyValuePair(key, value); + _data = arr; + _value = null; + } + } + + public void Add(TKey key, TValue value) => SetCore(key, value, false); + public void Set(TKey key, TValue value) => SetCore(key, value, true); + + public TValue this[TKey key] + { + get + { + if (TryGetValue(key, out var rv)) + return rv; + throw new KeyNotFoundException(); + } + set => Set(key, value); + } + + public bool Remove(TKey key) + { + if (_data == key) + { + _data = null; + _value = null; + return true; + } + else if (_data is KeyValuePair[] arr) + { + for (var c = 0; c < arr.Length; c++) + { + if (arr[c].Key == key) + { + arr[c] = default; + return true; + } + } + + return false; + } + else if (_data is Dictionary dic) + return dic.Remove(key); + + return false; + } + + public bool TryGetValue(TKey key, [MaybeNullWhen(false)]out TValue value) + { + if (_data == key) + { + value = _value!; + return true; + } + else if (_data is KeyValuePair[] arr) + { + for (var c = 0; c < arr.Length; c++) + { + if (arr[c].Key == key) + { + value = arr[c].Value!; + return true; + } + } + + value = null; + return false; + } + else if (_data is Dictionary dic) + return dic.TryGetValue(key, out value); + + value = null; + return false; + } + + + public bool TryGetAndRemoveValue(TKey key, [MaybeNullWhen(false)]out TValue value) + { + if (_data == key) + { + value = _value!; + _value = null; + _data = null; + return true; + } + else if (_data is KeyValuePair[] arr) + { + for (var c = 0; c < arr.Length; c++) + { + if (arr[c].Key == key) + { + value = arr[c].Value!; + arr[c] = default; + return true; + } + } + + value = null; + return false; + } + else if (_data is Dictionary dic) + { + if (!dic.TryGetValue(key, out value)) + return false; + dic.Remove(key); + } + + value = null; + return false; + } + + public TValue GetAndRemove(TKey key) + { + if (TryGetAndRemoveValue(key, out var v)) + return v; + throw new KeyNotFoundException(); + } +} \ No newline at end of file diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.cs index 0bb4e30f75..18f1d1c1e5 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Generator.cs +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.cs @@ -42,8 +42,10 @@ namespace Avalonia.SourceGenerator.CompositionGenerator string ChangedFieldsTypeName(GClass c) => c.Name + "ChangedFields"; string ChangedFieldsFieldName(GClass c) => "_changedFieldsOf" + c.Name; string PropertyBackingFieldName(GProperty prop) => "_" + prop.Name.WithLowerFirst(); - string ServerPropertyOffsetFieldName(GProperty prop) => "s_OffsetOf" + PropertyBackingFieldName(prop); - string PropertyPendingAnimationFieldName(GProperty prop) => "_pendingAnimationFor" + prop.Name; + string CompositionPropertyField(GProperty prop) => "s_IdOf" + prop.Name + "Property"; + + ExpressionSyntax ClientProperty(GClass c, GProperty p) => + MemberAccess(ServerName(c.Name), CompositionPropertyField(p)); void GenerateClass(GClass cl) { @@ -140,30 +142,10 @@ namespace Avalonia.SourceGenerator.CompositionGenerator .AddParameterListParameters(Parameter(Identifier("c")).WithType(ParseTypeName("BatchStreamReader"))) .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 uninitializedObjectName = "dummy"; - var serverStaticCtorBody = Block( - ParseStatement($"var dummy = GetOffsetDummy<{serverName}>();") - ); - - var resetBody = Block(); var startAnimationBody = Block(); var serverGetPropertyBody = Block(); - var serverGetFieldOffsetBody = Block(); - var activatedBody = Block(ParseStatement("base.Activated();")); - var deactivatedBody = Block(ParseStatement("base.Deactivated();")); + var serverGetCompositionPropertyBody = Block(); var serializeMethodBody = SerializeChangesPrologue(cl); var deserializeMethodBody = DeserializeChangesPrologue(cl); @@ -172,16 +154,12 @@ namespace Avalonia.SourceGenerator.CompositionGenerator foreach (var prop in cl.Properties) { var fieldName = PropertyBackingFieldName(prop); - var animatedFieldName = PropertyPendingAnimationFieldName(prop); - var fieldOffsetName = ServerPropertyOffsetFieldName(prop); var propType = ParseTypeName(prop.Type); var filteredPropertyType = prop.Type.TrimEnd('?'); var isObject = _objects.Contains(filteredPropertyType); var isNullable = prop.Type.EndsWith("?"); bool isPassthrough = false; - if (prop.Animated) - client = client.AddMembers(DeclareField("IAnimationInstance?", animatedFieldName)); client = GenerateClientProperty(client, cl, prop, propType, isObject, isNullable); var animatedServer = prop.Animated; @@ -201,35 +179,50 @@ namespace Avalonia.SourceGenerator.CompositionGenerator if (animatedServer) server = server.AddMembers( - DeclareField("ServerAnimatedValueStore<" + serverPropertyType + ">", fieldName), + DeclareField(serverPropertyType, fieldName), PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) .AddModifiers(SyntaxKind.PublicKeyword) .WithExpressionBody(ArrowExpressionClause( - InvocationExpression(MemberAccess(fieldName, "GetAnimated"), - ArgumentList(SingletonSeparatedList(Argument(IdentifierName("Compositor"))))))) + InvocationExpression(IdentifierName("GetAnimatedValue"), + ArgumentList(SeparatedList(new[]{ + Argument(IdentifierName(CompositionPropertyField(prop))), + Argument(null, Token(SyntaxKind.RefKeyword), IdentifierName(fieldName)) + } + ))))) .WithSemicolonToken(Semicolon()) ); else { server = server - .AddMembers(DeclareField("ServerValueStore<" + serverPropertyType + ">", fieldName)) + .AddMembers(DeclareField(serverPropertyType, fieldName)) .AddMembers(PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name) .AddModifiers(SyntaxKind.PublicKeyword) .AddAccessorListAccessors( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, - Block(ReturnStatement(MemberAccess(IdentifierName(fieldName), "Value")))), + Block(ReturnStatement( + InvocationExpression(IdentifierName("GetValue"), + ArgumentList(SeparatedList(new[]{ + Argument(IdentifierName(CompositionPropertyField(prop))), + Argument(null, Token(SyntaxKind.RefKeyword), IdentifierName(fieldName)) + } + )))))), AccessorDeclaration(SyntaxKind.SetAccessorDeclaration, Block( ParseStatement("var changed = false;"), IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression, - MemberAccess(IdentifierName(fieldName), "Value"), + IdentifierName(fieldName), IdentifierName("value")), Block( ParseStatement("On" + prop.Name + "Changing();"), ParseStatement($"changed = true;")) ), - ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, - MemberAccess(IdentifierName(fieldName), "Value"), IdentifierName("value"))), + ExpressionStatement(InvocationExpression(IdentifierName("SetValue"), + ArgumentList(SeparatedList(new[]{ + Argument(IdentifierName(CompositionPropertyField(prop))), + Argument(null, Token(SyntaxKind.OutKeyword), IdentifierName(fieldName)), + Argument(IdentifierName("value")) + } + )))), ParseStatement($"if(changed) On" + prop.Name + "Changed();") )) )) @@ -238,36 +231,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator .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( - InvocationExpression(MemberAccess(fieldName, "SetValue"), - ArgumentList(SeparatedList(new[] - { - Argument(IdentifierName("this")), - Argument(MemberAccess(changesVar, prop.Name, "Value")), - }))))), - IfStatement(MemberAccess(changesVar, prop.Name, "IsAnimation"), - ExpressionStatement( - InvocationExpression(MemberAccess(fieldName, "SetAnimation"), - ArgumentList(SeparatedList(new[] - { - Argument(IdentifierName("this")), - Argument(ParseExpression("c.Batch.CommitedAt")), - Argument(MemberAccess(changesVar, prop.Name, "Animation")), - Argument(IdentifierName(fieldOffsetName)) - }))))) - ); - 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")))); @@ -277,24 +241,15 @@ namespace Avalonia.SourceGenerator.CompositionGenerator if (animatedServer) { startAnimationBody = ApplyStartAnimation(startAnimationBody, cl, prop); - activatedBody = activatedBody.AddStatements(ParseStatement($"{fieldName}.Activate(this);")); - deactivatedBody = deactivatedBody.AddStatements(ParseStatement($"{fieldName}.Deactivate(this);")); } serverGetPropertyBody = ApplyGetProperty(serverGetPropertyBody, prop); - serverGetFieldOffsetBody = ApplyGetProperty(serverGetFieldOffsetBody, prop, fieldOffsetName); + serverGetCompositionPropertyBody = ApplyGetProperty(serverGetCompositionPropertyBody, prop, CompositionPropertyField(prop)); - server = server.AddMembers(DeclareField("int", fieldOffsetName, SyntaxKind.StaticKeyword)); - serverStaticCtorBody = serverStaticCtorBody.AddStatements(ExpressionStatement( - AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, IdentifierName(fieldOffsetName), - InvocationExpression(IdentifierName("GetOffset"), - ArgumentList(SeparatedList(new[] - { - Argument(IdentifierName(uninitializedObjectName)), - Argument(RefExpression(MemberAccess( - MemberAccess(IdentifierName(uninitializedObjectName), fieldName), "Subscriptions"))) - })))))); + server = server.AddMembers(DeclareField("CompositionProperty", CompositionPropertyField(prop), + EqualsValueClause(ParseExpression("CompositionProperty.Register()")), + SyntaxKind.InternalKeyword, SyntaxKind.StaticKeyword)); if (prop.DefaultValue != null) { @@ -304,16 +259,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator IdentifierName(prop.Name), ParseExpression(prop.DefaultValue)))); } } - - server = server.AddMembers(ConstructorDeclaration(serverName) - .WithModifiers(TokenList(Token(SyntaxKind.StaticKeyword))) - .WithBody(serverStaticCtorBody)); - - server = server - .AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( - $"protected override void Activated(){{}}")!).WithBody(activatedBody)) - .AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( - $"protected override void Deactivated(){{}}")!).WithBody(deactivatedBody)); + if (cl.Properties.Count > 0) { server = server.AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( @@ -331,12 +277,15 @@ namespace Avalonia.SourceGenerator.CompositionGenerator .AddMembers( MethodDeclaration(ParseTypeName("void"), "InitializeDefaultsExtra") .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); - + if (cl.Properties.Count > 0) + { + serializeMethodBody = serializeMethodBody.AddStatements(SerializeChangesEpilogue(cl)); client = client.AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( $"private protected override void SerializeChangesCore(BatchStreamWriter writer){{}}")!) .WithBody(serializeMethodBody)); - + } + if (list != null) client = AppendListProxy(list, client); @@ -344,7 +293,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator client = WithStartAnimation(client, startAnimationBody); server = WithGetPropertyForAnimation(server, serverGetPropertyBody); - server = WithGetFieldOffset(server, serverGetFieldOffsetBody); + server = WithGetCompositionProperty(server, serverGetCompositionPropertyBody); if(cl.Implements.Count > 0) foreach (var impl in cl.Implements) @@ -430,8 +379,6 @@ namespace Avalonia.SourceGenerator.CompositionGenerator StatementSyntax GeneratePropertySetterAssignment(GClass cl, GProperty prop, bool isObject, bool isNullable) { - var pendingAnimationField = PropertyPendingAnimationFieldName(prop); - var code = @$" // Update the backing value {PropertyBackingFieldName(prop)} = value; @@ -444,7 +391,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator { code += @$" // Reset previous animation if any - {pendingAnimationField} = null; + PendingAnimations.Remove({ClientProperty(cl, prop)}); {ChangedFieldsFieldName(cl)} &= ~{ChangedFieldsTypeName(cl)}.{prop.Name}Animated; // Check for implicit animations if(ImplicitAnimations != null && ImplicitAnimations.TryGetValue(""{prop.Name}"", out var animation) == true) @@ -453,7 +400,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator if(animation is CompositionAnimation a) {{ {ChangedFieldsFieldName(cl)} |= {ChangedFieldsTypeName(cl)}.{prop.Name}Animated; - {pendingAnimationField} = a.CreateInstance(this.Server, value); + PendingAnimations[{ClientProperty(cl, prop)}] = a.CreateInstance(this.Server, value); }} // Animation is triggered by the current field, but does not necessary affects it StartAnimationGroup(animation, ""{prop.Name}"", value); @@ -471,7 +418,7 @@ if (propertyName == ""{prop.Name}"") {{ var current = {PropertyBackingFieldName(prop)}; var server = animation.CreateInstance(this.Server, finalValue); -{PropertyPendingAnimationFieldName(prop)} = server; +PendingAnimations[{ClientProperty(cl, prop)}] = server; {ChangedFieldsFieldName(cl)} |= {ChangedFieldsTypeName(cl)}.{prop.Name}Animated; RegisterForSerialization(); return; @@ -512,6 +459,9 @@ return; ParseStatement($"writer.Write({ChangedFieldsFieldName(cl)});") ); } + + private BlockSyntax SerializeChangesEpilogue(GClass cl) => + Block(ParseStatement(ChangedFieldsFieldName(cl) + " = default;")); BlockSyntax ApplySerializeField(BlockSyntax body, GClass cl, GProperty prop, bool isObject, bool isPassthrough) { @@ -523,7 +473,7 @@ return; { code = $@" if(({changedFields} & {changedFieldsType}.{prop.Name}Animated) == {changedFieldsType}.{prop.Name}Animated) - writer.WriteObject({PropertyPendingAnimationFieldName(prop)}); + writer.WriteObject(PendingAnimations.GetAndRemove({ClientProperty(cl, prop)})); else "; } @@ -556,7 +506,7 @@ var changed = reader.Read<{ChangedFieldsTypeName(cl)}>(); { code = $@" if((changed & {changedFieldsType}.{prop.Name}Animated) == {changedFieldsType}.{prop.Name}Animated) - {PropertyBackingFieldName(prop)}.SetAnimation(this, commitedAt, reader.ReadObject(), {ServerPropertyOffsetFieldName(prop)}); + SetAnimatedValue({CompositionPropertyField(prop)}, ref {PropertyBackingFieldName(prop)}, commitedAt, reader.ReadObject()); else "; } @@ -565,7 +515,7 @@ var changed = reader.Read<{ChangedFieldsTypeName(cl)}>(); if((changed & {changedFieldsType}.{prop.Name}) == {changedFieldsType}.{prop.Name}) "; if (prop.Animated) - code += $"{PropertyBackingFieldName(prop)}.SetValue(this, {readValueCode});"; + code += $"SetAnimatedValue({CompositionPropertyField(prop)}, out {PropertyBackingFieldName(prop)}, {readValueCode});"; else code += $"{prop.Name} = {readValueCode};"; return body.AddStatements(ParseStatement(code)); } @@ -583,14 +533,14 @@ var changed = reader.Read<{ChangedFieldsTypeName(cl)}>(); return cl.AddMembers(method); } - ClassDeclarationSyntax WithGetFieldOffset(ClassDeclarationSyntax cl, BlockSyntax body) + ClassDeclarationSyntax WithGetCompositionProperty(ClassDeclarationSyntax cl, BlockSyntax body) { if (body.Statements.Count == 0) return cl; body = body.AddStatements( - ParseStatement("return base.GetFieldOffset(name);")); + ParseStatement("return base.GetCompositionProperty(name);")); var method = ((MethodDeclarationSyntax)ParseMemberDeclaration( - $"public override int? GetFieldOffset(string name){{}}")) + $"public override CompositionProperty? GetCompositionProperty(string name){{}}")) .WithBody(body); return cl.AddMembers(method);