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);