Browse Source

Use dictionaries for storing composition animation infrastructure

pull/8105/head
Nikita Tsukanov 4 years ago
parent
commit
862b318906
  1. 124
      src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs
  2. 16
      src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs
  3. 4
      src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs
  4. 2
      src/Avalonia.Base/Rendering/Composition/Animations/IAnimationInstance.cs
  5. 4
      src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs
  6. 3
      src/Avalonia.Base/Rendering/Composition/CompositionObject.cs
  7. 15
      src/Avalonia.Base/Rendering/Composition/Server/CompositionProperty.cs
  8. 101
      src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs
  9. 30
      src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.DirtyProperties.cs
  10. 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs
  11. 181
      src/Avalonia.Base/Utilities/SmallDictionary.cs
  12. 154
      src/tools/DevGenerators/CompositionGenerator/Generator.cs

124
src/Avalonia.Base/Rendering/Composition/Animations/AnimatedValueStore.cs

@ -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
{
/// <summary>
/// This is the first element of both animated and non-animated value stores.
/// It's used to propagate property invalidation to subscribers
/// </summary>
internal struct ServerObjectSubscriptionStore
{
public bool IsValid;
public RefTrackingDictionary<IAnimationInstance>? Subscribers;
public void Invalidate()
{
if (IsValid)
return;
IsValid = false;
if (Subscribers != null)
foreach (var sub in Subscribers)
sub.Key.Invalidate();
}
}
/// <summary>
/// The value store for non-animated values that can still be referenced by animations.
/// Simply stores the value and notifies subscribers
/// </summary>
internal struct ServerValueStore<T>
{
public ServerObjectSubscriptionStore Subscriptions;
private T _value;
public T Value
{
set
{
_value = value;
Subscriptions.Invalidate();
}
get
{
Subscriptions.IsValid = true;
return _value;
}
}
}
/// <summary>
/// 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
/// </summary>
/// <typeparam name="T"></typeparam>
[StructLayout(LayoutKind.Sequential)]
internal struct ServerAnimatedValueStore<T> 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<T>();
_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();
}
}
}

16
src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs

@ -12,10 +12,10 @@ namespace Avalonia.Rendering.Composition.Animations;
/// </summary>
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);
}
}

4
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,

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

4
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<T>();
@ -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()

3
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.
/// </summary>
public ImplicitAnimationCollection? ImplicitAnimations { get; set; }
private protected InlineDictionary<CompositionProperty, IAnimationInstance> PendingAnimations;
internal CompositionObject(Compositor compositor, ServerObject server)
{
Compositor = compositor;

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

101
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<CompositionProperty, ServerObjectSubscriptionStore> _subscriptions;
private InlineDictionary<CompositionProperty, IAnimationInstance> _animations;
private class ServerObjectSubscriptionStore
{
public bool IsValid;
public RefTrackingDictionary<IAnimationInstance>? Subscribers;
public void Invalidate()
{
if (IsValid)
return;
IsValid = false;
if (Subscribers != null)
foreach (var sub in Subscribers)
sub.Key.Invalidate();
}
}
public ServerObject(ServerCompositor compositor)
{
Compositor = compositor;
@ -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<T>(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<T>() where T : ServerObject => Unsafe.As<T>(s_OffsetDummy);
protected T GetValue<T>(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<T>(CompositionProperty prop, ref T field,
TimeSpan commitedAt, IAnimationInstance animation) where T : struct
{
return Unsafe.ByteOffset(ref obj._activationCount,
ref Unsafe.As<ServerObjectSubscriptionStore, uint>(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<T>(CompositionProperty property, out T field, T value)
{
#if DEBUG
if (offset == 0)
throw new InvalidOperationException();
#endif
return ref Unsafe.As<uint, ServerObjectSubscriptionStore>(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<T>(CompositionProperty property, ref T field) where T : struct
{
if (_subscriptions.TryGetValue(property, out var subscriptions))
subscriptions.IsValid = true;
if (_animations.TryGetValue(property, out var animation))
field = animation.Evaluate(Compositor.ServerNow, ExpressionVariant.Create(field))
.CastOrDefault<T>();
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)
{

30
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;
}
}

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

181
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<TKey, TValue> 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<TKey?, TValue?>[] 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<TKey?, TValue?>(key, value);
return;
}
// Upgrade to dictionary
var newDic = new Dictionary<TKey, TValue?>();
foreach (var kvp in arr)
newDic.Add(kvp.Key!, kvp.Value!);
newDic.Add(key, value);
_data = newDic;
}
else if (_data is Dictionary<TKey, TValue?> dic)
{
if (overwrite)
dic[key] = value;
else
dic.Add(key, value);
}
else
{
// We have a single element, upgrade to array
arr = new KeyValuePair<TKey?, TValue?>[6];
arr[0] = new KeyValuePair<TKey?, TValue?>((TKey)_data, _value);
arr[1] = new KeyValuePair<TKey?, TValue?>(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<TKey?, TValue?>[] 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<TKey, TValue?> 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<TKey?, TValue?>[] 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<TKey, TValue?> 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<TKey?, TValue?>[] 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<TKey, TValue?> 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();
}
}

154
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<IAnimationInstance>(), {ServerPropertyOffsetFieldName(prop)});
SetAnimatedValue({CompositionPropertyField(prop)}, ref {PropertyBackingFieldName(prop)}, commitedAt, reader.ReadObject<IAnimationInstance>());
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);

Loading…
Cancel
Save